diff --git a/.ash/.ash.yaml b/.ash/.ash.yaml new file mode 100644 index 00000000..3e8c5147 --- /dev/null +++ b/.ash/.ash.yaml @@ -0,0 +1,360 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +# yaml-language-server: $schema=https://raw.githubusercontent.com/awslabs/automated-security-helper/refs/heads/beta/automated_security_helper/schemas/AshConfig.json +project_name: one-observability-demo +global_settings: + severity_threshold: MEDIUM + ignore_paths: + - path: src/cdk/cdk.out + reason: 'CDK Code is being reviewed by CDK Nag using the AWS Security best practices compliance pack. Additional tools will require duplicated suppressions' + - path: src/cdk/node_modules + reason: 'Skip node_modules folder' + - path: src/cdk/wiki-docs + reason: 'Skip wiki docs since it will be built in the pipeline' + - path: archive/ + reason: 'Legacy application being migrated, for now only scan the new code' + - path: grafana-dashboards + reason: 'Skip legacy code during migration' + - path: codepipeline-stack.yaml + reason: 'Skip old pipeline since it will be migrated' + - path: src/applications/lambda/pethistory-node/template.yaml + reason: 'Temporary SAM template, will be moved to CDK' + - path: .secrets.baseline + reason: 'Secret Baseline file includes the word secret' + + suppressions: + - rule_id: SECRET-SECRET-KEYWORD + path: '.github/workflows/cdk-test.yml' + reason: 'Dummy secret' + - rule_id: SECRET-SECRET-KEYWORD + path: '.secrets.baseline' + reason: 'Secret Baseline file includes the word secret' + - rule_id: 'SECRET-BASE64-HIGH-ENTROPY-STRING' + path: 'src/applications/microservices/petsite-net/petsite/Views/Adoption/Index.cshtml' + line_start: 8 + line_end: 11 + reason: 'Dependency hash for verification, false positive' + - rule_id: 'SECRET-HEX-HIGH-ENTROPY-STRING' + path: 'src/applications/microservices/petsite-net/petsite/Views/Adoption/Index.cshtml' + line_start: 8 + line_end: 11 + reason: 'Dependency hash for verification, false positive' + # CDK-Nag suppressions moved from inline in codebuild-deployment-template.yaml + - rule_id: AwsSolutions-S1 + path: 'src/templates/codebuild-deployment-template.yaml' + resource_id: 'rConfigBucket' + reason: 'Bucket used to trigger CodePipeline, access logs are not needed' + - rule_id: AwsSolutions-IAM4 + path: 'src/templates/codebuild-deployment-template.yaml' + resource_id: 'rResourceCleanupRole' + reason: 'AWS managed policies are acceptable for a Lambda function' + - rule_id: AwsSolutions-IAM5 + path: 'src/templates/codebuild-deployment-template.yaml' + resource_id: 'rResourceCleanupRole' + reason: 'Wildcard is needed since stack name is automatically generated' + - rule_id: AwsSolutions-IAM4 + path: 'src/templates/codebuild-deployment-template.yaml' + resource_id: 'rCodeBuildServiceRole' + reason: 'AWS managed policies are acceptable for a CodeBuild project' + - rule_id: AwsSolutions-IAM5 + path: 'src/templates/codebuild-deployment-template.yaml' + resource_id: 'rCodeBuildServiceRole' + reason: 'Wildcard is needed since we have no control on the pipeline name' + - rule_id: AwsSolutions-CB4 + path: 'src/templates/codebuild-deployment-template.yaml' + resource_id: 'rCDKDeploymentProject' + reason: 'CodeBuild Project is used to kickoff the initial CDK deployment. AWS KMS is not required' + - rule_id: AwsSolutions-IAM4 + path: 'src/templates/codebuild-deployment-template.yaml' + resource_id: 'rStartDeploymentFunctionRole' + reason: 'Use of AWSLambdaBasicExecutionRole is acceptable here' + - rule_id: AwsSolutions-IAM4 + path: 'src/templates/codebuild-deployment-template.yaml' + resource_id: 'rCDKStackListerRole' + reason: 'AWS managed policies are acceptable for Lambda function' + - rule_id: AwsSolutions-IAM5 + path: 'src/templates/codebuild-deployment-template.yaml' + resource_id: 'rCDKStackListerRole' + reason: 'Wildcard is acceptable for describe stack action' + - rule_id: AwsSolutions-IAM5 + path: 'src/templates/codebuild-deployment-template.yaml' + resource_id: 'rCDKCleanupRole' + reason: 'Wildcard is acceptable for describe stack action' + - rule_id: AwsSolutions-SF1 + path: 'src/templates/codebuild-deployment-template.yaml' + resource_id: 'rCDKCleanupStateMachine' + reason: 'The purpose of the step function is to clean up all resources, additional logs not needed' + - rule_id: AwsSolutions-SF2 + path: 'src/templates/codebuild-deployment-template.yaml' + resource_id: 'rCDKCleanupStateMachine' + reason: 'X-Ray is not needed for this function since only CFN endpoint is used' + # Checkov suppressions moved from inline comments in codebuild-deployment-template.yaml + - rule_id: CKV_AWS_117 + path: 'src/templates/codebuild-deployment-template.yaml' + resource_id: 'rResourceCleanupFunction' + reason: 'Custom Resource Lambda only interacts with AWS endpoints, VPC is not needed' + - rule_id: CKV_AWS_116 + path: 'src/templates/codebuild-deployment-template.yaml' + resource_id: 'rResourceCleanupFunction' + reason: 'DLQ will increase complexity, this is not needed just to signal CFN deployment since it will time-out' + - rule_id: CKV_AWS_173 + path: 'src/templates/codebuild-deployment-template.yaml' + resource_id: 'rResourceCleanupFunction' + reason: 'Custom Resource Lambda, encryption is not included for simplicity' + - rule_id: CKV_AWS_115 + path: 'src/templates/codebuild-deployment-template.yaml' + resource_id: 'rResourceCleanupFunction' + reason: 'Function is executed only once, no need for concurrency configurations' + - rule_id: CKV_AWS_18 + path: 'src/templates/codebuild-deployment-template.yaml' + resource_id: 'rConfigBucket' + reason: 'Bucket used to trigger CodePipeline, access logs are not needed' + - rule_id: CKV_AWS_21 + path: 'src/templates/codebuild-deployment-template.yaml' + resource_id: 'rConfigBucket' + reason: 'Bucket used to trigger CodePipeline, object versioning is not needed' + - rule_id: CKV_AWS_117 + path: 'src/templates/codebuild-deployment-template.yaml' + resource_id: 'rStartDeploymentFunction' + reason: 'Custom Resource Lambda only interacts with AWS endpoints, VPC is not needed' + - rule_id: CKV_AWS_116 + path: 'src/templates/codebuild-deployment-template.yaml' + resource_id: 'rStartDeploymentFunction' + reason: 'DLQ will increase complexity, this is not needed just to signal CFN deployment since it will time-out' + - rule_id: CKV_AWS_173 + path: 'src/templates/codebuild-deployment-template.yaml' + resource_id: 'rStartDeploymentFunction' + reason: 'Custom Resource Lambda, encryption is not included for simplicity' + - rule_id: CKV_AWS_115 + path: 'src/templates/codebuild-deployment-template.yaml' + resource_id: 'rStartDeploymentFunction' + reason: 'Function is executed only once, no need for concurrency configurations' + - rule_id: CKV_AWS_117 + path: 'src/templates/codebuild-deployment-template.yaml' + resource_id: 'rCDKStackListerFunction' + reason: 'Custom Resource Lambda only interacts with AWS endpoints, VPC is not needed' + - rule_id: CKV_AWS_116 + path: 'src/templates/codebuild-deployment-template.yaml' + resource_id: 'rCDKStackListerFunction' + reason: 'DLQ will increase complexity, this is not needed just to check CFN stack status' + - rule_id: CKV_AWS_115 + path: 'src/templates/codebuild-deployment-template.yaml' + resource_id: 'rCDKStackListerFunction' + reason: 'Function is executed only once, no need for concurrency configurations' + +fail_on_findings: true +ash_plugin_modules: [] +external_reports_to_include: [] +build: null +converters: + archive: + name: archive + enabled: true + options: {} + jupyter: + name: jupyter + enabled: true + options: + tool_version: '>=7.16.0,<8.0.0' + install_timeout: 300 +scanners: + bandit: + name: bandit + enabled: true + options: + severity_threshold: null + config_file: null + confidence_level: all + ignore_nosec: false + excluded_paths: [] + additional_formats: [] + tool_version: '>=1.7.0,<2.0.0' + install_timeout: 300 + cdk-nag: + name: cdk-nag + enabled: true + options: + severity_threshold: null + nag_packs: + AwsSolutionsChecks: true + HIPAASecurityChecks: false + NIST80053R4Checks: false + NIST80053R5Checks: false + PCIDSS321Checks: false + cfn-nag: + name: cfn-nag + enabled: true + options: + severity_threshold: null + checkov: + name: checkov + enabled: true + options: + severity_threshold: null + config_file: null + skip_path: [] + additional_formats: + - cyclonedx_json + offline: false + frameworks: + - all + skip_frameworks: [] + tool_version: null + install_timeout: 300 + detect-secrets: + name: detect-secrets + enabled: false # disabled while I fix the bug with baseline file not being used properly + options: + severity_threshold: null + baseline_file: null + scan_settings: + version: null + generated_at: null + plugins_used: [] + filters_used: [] + results: {} + grype: + name: grype + enabled: true + options: + severity_threshold: null + config_file: null + offline: false + npm-audit: + name: npm-audit + enabled: true + options: + severity_threshold: null + offline: false + opengrep: + name: opengrep + enabled: false + options: + severity_threshold: null + config: auto + exclude: + - '*-converted.py' + - '*_report_result.txt' + exclude_rule: [] + severity: [] + metrics: auto + offline: false + patterns: [] + version: v1.1.5 + semgrep: + name: semgrep + enabled: true + options: + severity_threshold: null + config: auto + exclude: + - '*-converted.py' + - '*_report_result.txt' + exclude_rule: [] + severity: [] + metrics: auto + offline: false + tool_version: null + install_timeout: 300 + syft: + name: syft + enabled: true + options: + severity_threshold: null + config_file: null + exclude: [] + additional_outputs: + - syft-table +reporters: + csv: + name: csv + enabled: true + options: {} + extension: csv + cyclonedx: + name: cyclonedx + enabled: true + options: {} + extension: cdx.json + html: + name: html + enabled: true + options: {} + extension: html + flat-json: + name: flat-json + enabled: true + options: + include_scanner_metrics: true + include_summary_metrics: true + include_metadata: true + extension: flat.json + gitlab-sast: + name: gitlab-sast + enabled: true + options: {} + extension: gl-sast-report.json + junitxml: + name: junitxml + enabled: true + options: + respect_severity_threshold: true + extension: junit.xml + markdown: + name: markdown + enabled: true + options: + include_summary: true + include_findings_table: false + include_detailed_findings: true + max_detailed_findings: 20 + top_hotspots_limit: 10 + use_collapsible_details: true + extension: summary.md + ocsf: + name: ocsf + enabled: true + options: {} + extension: ocsf.json + sarif: + name: sarif + enabled: true + options: {} + extension: sarif + spdx: + name: spdx + enabled: false + options: {} + extension: spdx.json + text: + name: text + enabled: true + options: + include_summary: true + include_findings_table: false + include_detailed_findings: false + max_detailed_findings: 20 + top_hotspots_limit: 20 + extension: summary.txt + yaml: + name: yaml + enabled: false + options: {} + extension: yaml +mcp-resource-management: + max_concurrent_scans: 3 + max_concurrent_tasks: 20 + thread_pool_max_workers: 4 + scan_timeout_seconds: 1800 + operation_timeout_seconds: 180 + shutdown_timeout_seconds: 30 + memory_warning_threshold_mb: 1024 + memory_critical_threshold_mb: 2048 + task_count_warning_threshold: 15 + max_message_size_bytes: 10485760 + max_path_length: 4096 + max_directory_size_mb: 1000 + enable_health_checks: true + health_check_interval_seconds: 60 + enable_resource_logging: true + log_resource_operations: false diff --git a/.ash/.gitignore b/.ash/.gitignore new file mode 100644 index 00000000..d831134c --- /dev/null +++ b/.ash/.gitignore @@ -0,0 +1,2 @@ +# ASH default output directory (and variants) +ash_output* diff --git a/.codespellrc b/.codespellrc new file mode 100644 index 00000000..20f2a90c --- /dev/null +++ b/.codespellrc @@ -0,0 +1,2 @@ +[codespell] +ignore-words-list = withS \ No newline at end of file diff --git a/.github/workflows/acceptance-tests.yaml b/.github/workflows/acceptance.yaml similarity index 100% rename from .github/workflows/acceptance-tests.yaml rename to .github/workflows/acceptance.yaml diff --git a/.github/workflows/build-test.yml b/.github/workflows/build-test.yml index b6f304e1..254d3f62 100644 --- a/.github/workflows/build-test.yml +++ b/.github/workflows/build-test.yml @@ -1,114 +1,128 @@ name: Build Test - +permissions: + contents: read on: - pull_request: - branches: [ main, staging ] - paths: - - 'PetAdoptions/payforadoption-go/**' - - 'PetAdoptions/pethistory/**' - - 'PetAdoptions/petlistadoptions-py/**' - - 'PetAdoptions/petsearch-java/**' - - 'PetAdoptions/petsite/**' - - 'PetAdoptions/petstatusupdater/**' - - 'PetAdoptions/trafficgenerator/**' - push: - branches: [ main, staging ] - paths: - - 'PetAdoptions/payforadoption-go/**' - - 'PetAdoptions/pethistory/**' - - 'PetAdoptions/petlistadoptions-py/**' - - 'PetAdoptions/petsearch-java/**' - - 'PetAdoptions/petsite/**' - - 'PetAdoptions/petstatusupdater/**' - - 'PetAdoptions/trafficgenerator/**' - + pull_request: + branches: [main, staging, feat/cdkpipeline] + push: + branches: [main, staging, feat/cdkpipeline] jobs: - docker-builds: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - service: - - name: payforadoption-go - path: PetAdoptions/payforadoption-go - - name: petlistadoptions-py - path: PetAdoptions/petlistadoptions-py - - name: petsearch-java - path: PetAdoptions/petsearch-java - - name: petsite - path: PetAdoptions/petsite/petsite - - name: trafficgenerator - path: PetAdoptions/trafficgenerator/trafficgenerator - - steps: - - uses: actions/checkout@v3 - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v2 - - - name: Build ${{ matrix.service.name }} - uses: docker/build-push-action@v4 - with: - context: ${{ matrix.service.path }} - push: false - load: true - tags: ${{ matrix.service.name }}:test - cache-from: type=gha - cache-to: type=gha,mode=max - - nodejs-builds: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - service: - - name: pethistory - path: PetAdoptions/pethistory - node-version: '18' - - steps: - - uses: actions/checkout@v3 - - - name: Setup Node.js - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.service.node-version }} - cache: 'npm' - cache-dependency-path: ${{ matrix.service.path }}/package-lock.json - - - name: Install dependencies - run: npm ci - working-directory: ${{ matrix.service.path }} - - - name: Run tests - run: npm test - working-directory: ${{ matrix.service.path }} - - - name: Verify build - run: npm run build --if-present - working-directory: ${{ matrix.service.path }} - - dotnet-builds: - runs-on: ubuntu-latest - strategy: - fail-fast: false - matrix: - project: - - name: petsite - path: PetAdoptions/petsite/petsite.sln - - name: trafficgenerator - path: PetAdoptions/trafficgenerator/trafficgenerator.sln - - steps: - - uses: actions/checkout@v3 - - - name: Setup .NET - uses: actions/setup-dotnet@v3 - with: - dotnet-version: '8.0.x' - - - name: Restore dependencies - run: dotnet restore ${{ matrix.project.path }} - - - name: Build - run: dotnet build ${{ matrix.project.path }} --no-restore \ No newline at end of file + docker-builds: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + service: + - name: payforadoption-go + path: src/applications/microservices/payforadoption-go + - name: petfood-rs + path: src/applications/microservices/petfood-rs + - name: petlistadoptions-py + path: src/applications/microservices/petlistadoptions-py + - name: petsearch-java + path: src/applications/microservices/petsearch-java + - name: petsite-net + path: src/applications/microservices/petsite-net/petsite + steps: + - uses: actions/checkout@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + + - name: Build ${{ matrix.service.name }} + uses: docker/build-push-action@v4 + with: + context: ${{ matrix.service.path }} + push: false + load: true + tags: ${{ matrix.service.name }}:test + cache-from: type=gha + cache-to: type=gha,mode=max + + nodejs-builds: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + service: + - name: petfood-cleanup-processor + path: src/applications/lambda/petfood-cleanup-processor-node/ + node-version: '22' + - name: petstatusupdater + path: src/applications/lambda/petstatusupdater-node/ + node-version: '22' + - name: traffic-generator + path: src/applications/lambda/traffic-generator-node/ + node-version: '22' + + steps: + - uses: actions/checkout@v3 + + - name: Setup Node.js + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.service.node-version }} + cache: 'npm' + cache-dependency-path: ${{ matrix.service.path }}/package-lock.json + + - name: Install dependencies + run: npm ci + working-directory: ${{ matrix.service.path }} + + - name: Run tests + run: npm test + working-directory: ${{ matrix.service.path }} + + - name: Verify build + run: npm run build --if-present + working-directory: ${{ matrix.service.path }} + + python-builds: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + service: + - name: petfood-image-generator + path: src/applications/lambda/petfood-image-generator-python/ + python-version: '3.13' + + steps: + - uses: actions/checkout@v3 + + - name: Setup Python + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.service.python-version }} + + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install pytest boto3 + working-directory: ${{ matrix.service.path }} + + - name: Run tests + run: pytest + working-directory: ${{ matrix.service.path }} + + dotnet-builds: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + project: + - name: petsite + path: src/applications/microservices/petsite-net/petsite.sln + steps: + - uses: actions/checkout@v3 + + - name: Setup .NET + uses: actions/setup-dotnet@v3 + with: + dotnet-version: '8.0.x' + + - name: Restore dependencies + run: dotnet restore ${{ matrix.project.path }} + + - name: Build + run: dotnet build ${{ matrix.project.path }} --no-restore diff --git a/.github/workflows/cdk-test.yml b/.github/workflows/cdk-test.yml deleted file mode 100644 index 4784a9cf..00000000 --- a/.github/workflows/cdk-test.yml +++ /dev/null @@ -1,76 +0,0 @@ -name: CDK Test - -on: - pull_request: - branches: [ main ] - paths: - - 'PetAdoptions/cdk/**' - push: - branches: [ main ] - paths: - - 'PetAdoptions/cdk/**' - -jobs: - cdk-synth-test: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '22' - - - name: Cache node modules - uses: actions/cache@v4 - with: - path: PetAdoptions/cdk/pet_stack/node_modules - key: ${{ runner.os }}-node-${{ hashFiles('PetAdoptions/cdk/pet_stack/package.json') }} - restore-keys: | - ${{ runner.os }}-node- - - - name: Install dependencies - run: npm install - working-directory: PetAdoptions/cdk/pet_stack - - - name: Build TypeScript - run: npm run build - working-directory: PetAdoptions/cdk/pet_stack - - - name: TypeScript compilation check - run: npx tsc --noEmit - working-directory: PetAdoptions/cdk/pet_stack - - - name: CDK context validation - run: | - echo "Validating CDK context and configuration..." - npx cdk context --clear - npx cdk ls - working-directory: PetAdoptions/cdk/pet_stack - env: - AWS_DEFAULT_REGION: us-east-1 - AWS_REGION: us-east-1 - AWS_ACCESS_KEY_ID: dummy - AWS_SECRET_ACCESS_KEY: dummy - - - name: Run CDK synth (dry run) - run: npx cdk synth --no-staging - working-directory: PetAdoptions/cdk/pet_stack - env: - # Set required AWS environment variables for synth - AWS_DEFAULT_REGION: us-east-1 - AWS_REGION: us-east-1 - # CDK doesn't need real AWS credentials for synth, but some constructs might check - AWS_ACCESS_KEY_ID: dummy - AWS_SECRET_ACCESS_KEY: dummy - - - name: Run CDK diff (if applicable) - run: npx cdk diff --no-staging || true - working-directory: PetAdoptions/cdk/pet_stack - env: - AWS_DEFAULT_REGION: us-east-1 - AWS_REGION: us-east-1 - AWS_ACCESS_KEY_ID: dummy - AWS_SECRET_ACCESS_KEY: dummy diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 00000000..fe7235e6 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,78 @@ +name: Documentation + +permissions: + contents: read + pages: write + id-token: write + +on: + push: + branches: [main, staging] + paths: + - 'src/cdk/**' + - 'typedoc.json' + - 'CONTRIBUTING.md' + - 'ARCHITECTURE.md' + - 'docs/**' + pull_request: + branches: [main, staging] + paths: + - 'src/cdk/**' + - 'typedoc.json' + - 'CONTRIBUTING.md' + - 'ARCHITECTURE.md' + - 'docs/**' + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + cache: 'npm' + cache-dependency-path: package-lock.json + + - name: Install root dependencies + run: npm ci + + - name: Install CDK dependencies + run: npm ci + working-directory: src/cdk + + - name: Create ASH scan link + run: | + echo "# Security Scan Results" > ASH_SCAN.md + echo "" >> ASH_SCAN.md + echo "[Latest ASH Security Scan Results](https://github.com/${{ github.repository }}/actions/workflows/run-ash-security-scan.yaml)" >> ASH_SCAN.md + sed -i 's/"ARCHITECTURE.md"]/"ARCHITECTURE.md", "ASH_SCAN.md"]/g' typedoc.json + + - name: Generate documentation + run: npx typedoc + + - name: Upload artifact + if: github.ref == 'refs/heads/main' + uses: actions/upload-pages-artifact@v3 + with: + path: wiki-docs + + deploy: + if: github.ref == 'refs/heads/main' + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + + steps: + - name: Setup Pages + uses: actions/configure-pages@v4 + + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 \ No newline at end of file diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 00000000..80de53c4 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,23 @@ +name: Pre-commit + +on: + pull_request: + branches: [main, feat/cdkpipeline, staging] + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.11' + - uses: actions/setup-node@v4 + with: + node-version: '18' + - name: Install Node.js dependencies + run: | + find . -name "package.json" -not -path "./node_modules/*" -execdir npm install \; + - uses: pre-commit/action@v3.0.1 + env: + SKIP: ash-simple-scan \ No newline at end of file diff --git a/.github/workflows/run-ash-security-scan.yaml b/.github/workflows/run-ash-security-scan.yaml new file mode 100644 index 00000000..0daec524 --- /dev/null +++ b/.github/workflows/run-ash-security-scan.yaml @@ -0,0 +1,164 @@ +name: 'ASH Security Scan' +permissions: + contents: read # Required to read the repository contents in order to scan them with ASH + checks: write # Required for writing CI check runs + pull-requests: write # Required for writing comments with scan results to pull requests +on: + workflow_call: + inputs: + ash-version: + description: 'ASH tag or branch to use. Defaults to a beta release until ASHv3 is released.' + required: false + default: 'v3.0.0-beta' + type: string + ash-mode: + description: 'Mode to run ASH in. Defaults to "container". Use "local" to run in direct Python only. Be aware that running in local mode will not include any scanners that are not installed as Python dependencies of ASH itself.' + required: false + default: 'container' + type: string + config: + description: 'Path to the ASH config file, if any' + required: false + default: '' + type: string + output-dir: + description: 'Directory where ASH output will be stored' + required: false + default: '.ash/ash_output' + type: string + fail-on-findings: + description: 'Whether to continue execution even if findings are detected' + required: false + default: true + type: boolean + verbose: + description: 'Enable verbose output' + required: false + default: true + type: boolean + ash-args: + description: 'Additional arguments to pass to ASH' + required: false + default: '' + type: string + collect-sarif-report: + description: 'Whether to collect the ash.sarif report using github/codeql-action/upload-sarif for GitHub Advanced Security Code Scanning results' + required: false + default: true + type: boolean + collect-junit-xml-report: + description: 'Whether to collect the ash.junit.xml report using the mikepenz/action-junit-report@v5 GitHub Action' + required: false + default: true + type: boolean + post-pr-comment: + description: 'Whether to post scan results as PR comment' + required: false + default: true + type: boolean + repository-owner: + description: 'Owner of the repository' + required: false + default: ${{ github.repository_owner }} + type: string + python-version: + description: 'Python version to use' + required: false + default: '3.12' + type: string + +env: + ASH_UVX_SOURCE: git+https://github.com/awslabs/automated-security-helper@${{ inputs.ash-version }} + ASH_OUTPUT_DIR: ${{ inputs.output-dir }} + ASH_CONFIG: ${{ inputs.config }} + ASH_ARGUMENTS: ${{ inputs.ash-args }} + ASH_FAIL_MODE: ${{ inputs.fail-on-findings == 'true' && '--fail-on-findings' || '--no-fail-on-findings'}} + ASH_MODE: ${{ inputs.ash-mode }} +# ASH_VERBOSE: ${{ inputs.verbose == 'true' && '--verbose' || '--quiet'}} + +jobs: + ash: + name: SAST, SCA, and IaC Scan + runs-on: ubuntu-latest + env: + COLUMNS: 140 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Install uv + uses: astral-sh/setup-uv@v5 + with: + python-version: ${{ inputs.python-version }} + enable-cache: true + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ inputs.python-version }} + + - name: Install Syft + run: | + curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin + + - name: Install Grype + run: | + curl -sSfL https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin + + - name: Run ASH + id: ash + shell: bash + run: |- + uvx --from $ASH_UVX_SOURCE ash \ + --source-dir . --output-dir "$ASH_OUTPUT_DIR" "$ASH_ARGUMENTS" --no-progress --build-target ci "$ASH_FAIL_MODE" --mode "$ASH_MODE" + + - name: Show ASH Summary Report + if: success() || failure() + shell: bash + run: |- + uvx --from $ASH_UVX_SOURCE ash \ + --output-dir "$ASH_OUTPUT_DIR" report + + - name: Update step summary + if: success() || failure() + shell: bash + run: |- + cat "$ASH_OUTPUT_DIR/reports/ash.summary.md" >> "${GITHUB_STEP_SUMMARY}" + tree "$ASH_OUTPUT_DIR" + + - name: Post ASH MarkdownReporter output as PR comment + uses: mshick/add-pr-comment@v2 + if: inputs.post-pr-comment && github.repository_owner == inputs.repository-owner && (success() || failure()) + continue-on-error: true + with: + message-path: ${{ inputs.output-dir }}/reports/ash.summary.md + + - name: Collect ASH output artifact + uses: actions/upload-artifact@v4 + if: success() || failure() + continue-on-error: true + with: + name: ash_output + path: ${{ inputs.output-dir }} + if-no-files-found: error + + - name: Publish JUnit Test Report + uses: mikepenz/action-junit-report@v5 + if: inputs.collect-junit-xml-report && (success() || failure()) + continue-on-error: true + with: + report_paths: '**/ash.junit.xml' + include_passed: true + update_check: true + annotate_only: true + check_name: ASH Scan JUnit Test Report + summary: Skipped means passing, suppressed, or ignored findings that are considered non-actionable. + + - name: Upload ASH SARIF file + uses: github/codeql-action/upload-sarif@v3 + if: inputs.collect-sarif-report && (success() || failure()) + continue-on-error: false + with: + sarif_file: ${{ inputs.output-dir }}/reports/ash.sarif + wait-for-processing: true + category: ash diff --git a/.github/workflows/security-scan.yaml b/.github/workflows/security-scan.yaml new file mode 100644 index 00000000..d77d154f --- /dev/null +++ b/.github/workflows/security-scan.yaml @@ -0,0 +1,34 @@ +name: Security Scan +permissions: + contents: read +on: + push: + workflow_dispatch: + +jobs: + security: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@master + - uses: snyk/actions/setup@master + ash: + uses: ./.github/workflows/run-ash-security-scan.yaml + permissions: + #checkov:skip=CKV2_GHA_1:The permissions are not set to write-all at the top-level, or any level + contents: read # Required to read the repository contents in order to scan them with ASH + checks: write # Required for writing CI check runs + pull-requests: write # Required for writing comments with scan results to pull requests + #security-events: write # Required for collection of SARIF code scanning results for GitHub Advanced Security checks + with: + ash-version: v3.0.0-beta + repository-owner: ${{ github.repository_owner }} + ash-args: '--inspect' + fail-on-findings: false + collect-junit-xml-report: true + collect-sarif-report: false + post-pr-comment: true + verbose: true + python-version: '3.12' + ash-mode: local + output-dir: '.ash/ash_output' + config: '.ash/.ash.yaml' diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 00000000..ab1d27fa --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,129 @@ +name: Tests + +on: + pull_request: + branches: [ main, staging, feat/cdkpipeline ] + push: + branches: [ main, staging, feat/cdkpipeline ] + +jobs: + cdk-synth-test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '22' + + - name: Cache node modules + uses: actions/cache@v4 + with: + path: src/cdk/node_modules + key: ${{ runner.os }}-node-${{ hashFiles('src/cdk/package.json') }} + restore-keys: | + ${{ runner.os }}-node- + + - name: Install dependencies + run: npm install + working-directory: src/cdk + + - name: Build TypeScript + run: npm run build + working-directory: src/cdk + + - name: TypeScript compilation check + run: npx tsc --noEmit + working-directory: src/cdk + + - name: CDK context validation + run: | + echo "Validating CDK context and configuration..." + cp .env.sample .env + npx cdk context --clear + npx cdk ls + working-directory: src/cdk + env: + AWS_DEFAULT_REGION: us-east-1 + AWS_REGION: us-east-1 + AWS_ACCESS_KEY_ID: dummy + AWS_SECRET_ACCESS_KEY: dummy + + - name: Run CDK synth (dry run) + run: | + cp .env.sample .env + npx cdk synth --no-staging + working-directory: src/cdk + env: + # Set required AWS environment variables for synth + AWS_DEFAULT_REGION: us-east-1 + AWS_REGION: us-east-1 + # CDK doesn't need real AWS credentials for synth, but some constructs might check + AWS_ACCESS_KEY_ID: dummy + AWS_SECRET_ACCESS_KEY: dummy + + petfood-rust-test: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Setup Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: minimal + override: true + components: rustfmt, clippy + + - name: Cache cargo registry + uses: actions/cache@v4 + with: + path: ~/.cargo/registry + key: ${{ runner.os }}-cargo-registry-${{ hashFiles('src/applications/microservices/petfood-rs/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-registry- + + - name: Cache cargo index + uses: actions/cache@v4 + with: + path: ~/.cargo/git + key: ${{ runner.os }}-cargo-index-${{ hashFiles('src/applications/microservices/petfood-rs/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-index- + + - name: Cache cargo build + uses: actions/cache@v4 + with: + path: src/applications/microservices/petfood-rs/target + key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('src/applications/microservices/petfood-rs/Cargo.lock') }} + restore-keys: | + ${{ runner.os }}-cargo-build-target- + + - name: Check Rust formatting + run: cargo fmt --all -- --check + working-directory: src/applications/microservices/petfood-rs + + - name: Run Clippy (linting) + run: cargo clippy --all-targets --all-features -- -D warnings + working-directory: src/applications/microservices/petfood-rs + + - name: Run cargo unit tests + run: cargo test --lib --bins + working-directory: src/applications/microservices/petfood-rs + env: + RUST_BACKTRACE: 1 + + # - name: Run cargo tests with all features + # run: cargo test --all-features --verbose + # working-directory: src/applications/microservices/petfood-rs + # env: + # RUST_BACKTRACE: 1 + + - name: Check cargo build + run: cargo build --release + working-directory: src/applications/microservices/petfood-rs diff --git a/.gitignore b/.gitignore index 13e92ccc..7b617410 100644 --- a/.gitignore +++ b/.gitignore @@ -1,390 +1,445 @@ +# Created by https://www.toptal.com/developers/gitignore/api/windows,macos,visualstudiocode,node,python,git +# Edit at https://www.toptal.com/developers/gitignore?templates=windows,macos,visualstudiocode,node,python,git # Go binaries # cspell:ignore petadoptions PetAdoptions/payforadoption-go/petadoptions +src/applications/microservices/petsite-net/petsite/bin **/.DS_Store **/assets **/.idea -# editor settings -.vscode/settings.json -PetAdoptions/petsite/petsite/appsettings.Development.json -PetAdoptions/trafficgenerator/.idea/.idea.trafficgenerator/.idea/indexLayout.xml -PetAdoptions/trafficgenerator/.idea/.idea.trafficgenerator/.idea/modules.xml +### Git ### +# Created by git for backups. To disable backups in Git: +# $ git config --global mergetool.keepBackup false +*.orig + +# Created by git when using merge tools for conflicts +*.BACKUP.* +*.BASE.* +*.LOCAL.* +*.REMOTE.* +*_BACKUP_*.txt +*_BASE_*.txt +*_LOCAL_*.txt +*_REMOTE_*.txt + +### macOS ### +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + +### macOS Patch ### +# iCloud generated files +*.icloud + +### Node ### +# 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 -# Merging gitignore files - -./petsite/.idea -./trafficgenerator/.idea -./petstatusupdater/function.zip -cscdk - -## Ignore Visual Studio temporary files, build results, and -## files generated by popular Visual Studio add-ons. -## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore - -# User-specific files -*.rsuser -*.suo -*.user -*.userosscache -*.sln.docstates - -# User-specific files (MonoDevelop/Xamarin Studio) -*.userprefs - -# Mono auto generated files -mono_crash.* - -# Build results -[Dd]ebug/ -[Dd]ebugPublic/ -[Rr]elease/ -[Rr]eleases/ -x64/ -x86/ -[Ww][Ii][Nn]32/ -[Aa][Rr][Mm]/ -[Aa][Rr][Mm]64/ -bld/ -[Bb]in/ -[Oo]bj/ -[Ll]og/ -[Ll]ogs/ - -# Visual Studio 2015/2017 cache/options directory -.vs/ -# Uncomment if you have tasks that create the project's static files in wwwroot -#wwwroot/ - -# Visual Studio 2017 auto generated files -Generated\ Files/ - -# MSTest test Results -[Tt]est[Rr]esult*/ -[Bb]uild[Ll]og.* - -# NUnit -*.VisualState.xml -TestResult.xml -nunit-*.xml - -# Build Results of an ATL Project -[Dd]ebugPS/ -[Rr]eleasePS/ -dlldata.c - -# Benchmark Results -BenchmarkDotNet.Artifacts/ - -# .NET Core -project.lock.json -project.fragment.lock.json -artifacts/ - -# ASP.NET Scaffolding -ScaffoldingReadMe.txt - -# StyleCop -StyleCopReport.xml - -# Files built by Visual Studio -*_i.c -*_p.c -*_h.h -*.ilk -*.meta -*.obj -*.iobj -*.pch -*.pdb -*.ipdb -*.pgc -*.pgd -*.rsp -*.sbr -*.tlb -*.tli -*.tlh -*.tmp -*.tmp_proj -*_wpftmp.csproj -*.log -*.vspscc -*.vssscc -.builds -*.pidb -*.svclog -*.scc - -# Chutzpah Test files -_Chutzpah* - -# Visual C++ cache files -ipch/ -*.aps -*.ncb -*.opendb -*.opensdf -*.sdf -*.cachefile -*.VC.db -*.VC.VC.opendb - -# Visual Studio profiler -*.psess -*.vsp -*.vspx -*.sap - -# Visual Studio Trace Files -*.e2e - -# TFS 2012 Local Workspace -$tf/ - -# Guidance Automation Toolkit -*.gpState - -# ReSharper is a .NET coding add-in -_ReSharper*/ -*.[Rr]e[Ss]harper -*.DotSettings.user - -# TeamCity is a build add-in -_TeamCity* - -# DotCover is a Code Coverage Tool -*.dotCover - -# AxoCover is a Code Coverage Tool -.axoCover/* -!.axoCover/settings.json - -# Coverlet is a free, cross platform Code Coverage Tool -coverage*[.json, .xml, .info] - -# Visual Studio code coverage results -*.coverage -*.coveragexml - -# NCrunch -_NCrunch_* -.*crunch*.local.xml -nCrunchTemp_* - -# MightyMoose -*.mm.* -AutoTest.Net/ - -# Web workbench (sass) -.sass-cache/ - -# Installshield output folder -[Ee]xpress/ - -# DocProject is a documentation generator add-in -DocProject/buildhelp/ -DocProject/Help/*.HxT -DocProject/Help/*.HxC -DocProject/Help/*.hhc -DocProject/Help/*.hhk -DocProject/Help/*.hhp -DocProject/Help/Html2 -DocProject/Help/html - -# Click-Once directory -publish/ - -# Publish Web Output -*.[Pp]ublish.xml -*.azurePubxml -# Note: Comment the next line if you want to checkin your web deploy settings, -# but database connection strings (with potential passwords) will be unencrypted -*.pubxml -*.publishproj - -# Microsoft Azure Web App publish settings. Comment the next line if you want to -# checkin your Azure Web App publish settings, but sensitive information contained -# in these scripts will be unencrypted -PublishScripts/ - -# NuGet Packages -*.nupkg -# NuGet Symbol Packages -*.snupkg -# The packages folder can be ignored because of Package Restore -**/[Pp]ackages/* -# except build/, which is used as an MSBuild target. -!**/[Pp]ackages/build/ -# Uncomment if necessary however generally it will be regenerated when needed -#!**/[Pp]ackages/repositories.config -# NuGet v3's project.json files produces more ignorable files -*.nuget.props -*.nuget.targets - -# Microsoft Azure Build Output -csx/ -*.build.csdef - -# Microsoft Azure Emulator -ecf/ -rcf/ - -# Windows Store app package directories and files -AppPackages/ -BundleArtifacts/ -Package.StoreAssociation.xml -_pkginfo.txt -*.appx -*.appxbundle -*.appxupload - -# Visual Studio cache files -# files ending in .cache can be ignored -*.[Cc]ache -# but keep track of directories ending in .cache -!?*.[Cc]ache/ - -# Others -ClientBin/ -~$* -*~ -*.dbmdl -*.dbproj.schemaview -*.jfm -*.pfx -*.publishsettings -orleans.codegen.cs - -# Including strong name files can present a security risk -# (https://github.com/github/gitignore/pull/2483#issue-259490424) -#*.snk - -# Since there are multiple workflows, uncomment next line to ignore bower_components -# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) -#bower_components/ - -# RIA/Silverlight projects -Generated_Code/ - -# Backup & report files from converting an old project file -# to a newer Visual Studio version. Backup files are not needed, -# because we have git ;-) -_UpgradeReport_Files/ -Backup*/ -UpgradeLog*.XML -UpgradeLog*.htm -ServiceFabricBackup/ -*.rptproj.bak - -# SQL Server files -*.mdf -*.ldf -*.ndf - -# Business Intelligence projects -*.rdl.data -*.bim.layout -*.bim_*.settings -*.rptproj.rsuser -*- [Bb]ackup.rdl -*- [Bb]ackup ([0-9]).rdl -*- [Bb]ackup ([0-9][0-9]).rdl - -# Microsoft Fakes -FakesAssemblies/ - -# GhostDoc plugin setting file -*.GhostDoc.xml - -# Node.js Tools for Visual Studio -.ntvs_analysis.dat +# 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/ -# Visual Studio 6 build log -*.plg +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ -# Visual Studio 6 workspace options file -*.opt +# TypeScript cache +*.tsbuildinfo -# Visual Studio 6 auto-generated workspace file (contains which files were open etc.) -*.vbw +# Optional npm cache directory +.npm -# Visual Studio LightSwitch build output -**/*.HTMLClient/GeneratedArtifacts -**/*.DesktopClient/GeneratedArtifacts -**/*.DesktopClient/ModelManifest.xml -**/*.Server/GeneratedArtifacts -**/*.Server/ModelManifest.xml -_Pvt_Extensions +# Optional eslint cache +.eslintcache -# Paket dependency manager -.paket/paket.exe -paket-files/ +# Optional stylelint cache +.stylelintcache -# FAKE - F# Make -.fake/ +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ -# CodeRush personal settings -.cr/personal +# Optional REPL history +.node_repl_history -# Python Tools for Visual Studio (PTVS) -__pycache__/ -*.pyc +# 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 -# Cake - Uncomment if you are using it -# tools/** -# !tools/packages.config +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache -# Tabs Studio -*.tss +# Next.js build output +.next +out -# Telerik's JustMock configuration file -*.jmconfig +# Nuxt.js build / generate output +.nuxt +dist -# BizTalk build output -*.btp.cs -*.btm.cs -*.odx.cs -*.xsd.cs +# 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 -# OpenCover UI analysis results -OpenCover/ +# vuepress build output +.vuepress/dist -# Azure Stream Analytics local run output -ASALocalRun/ +# vuepress v2.x temp and cache directory +.temp -# MSBuild Binary and Structured Log -*.binlog +# Docusaurus cache and generated files +.docusaurus -# NVidia Nsight GPU debugger configuration file -*.nvuser +# Serverless directories +.serverless/ -# MFractors (Xamarin productivity tool) working folder -.mfractor/ +# FuseBox cache +.fusebox/ -# Local History for Visual Studio -.localhistory/ +# DynamoDB Local files +.dynamodb/ -# BeatPulse healthcheck temp database -healthchecksdb +# 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.* + +### Node Patch ### +# Serverless Webpack directories +.webpack/ + +# Optional stylelint cache + +# SvelteKit build / generate output +.svelte-kit + +### Python ### +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# Environments +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +### Python Patch ### +# Poetry local configuration file - https://python-poetry.org/docs/configuration/#local-configuration +poetry.toml + +# ruff +.ruff_cache/ + +# LSP config files +pyrightconfig.json + +### VisualStudioCode ### +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +!.vscode/*.code-snippets + +# Local History for Visual Studio Code +.history/ + +# Built Visual Studio Code Extensions +*.vsix + +### VisualStudioCode Patch ### +# Ignore all local history of files +.history +.ionide + +### Windows ### +# Windows thumbnail cache files +Thumbs.db +Thumbs.db:encryptable +ehthumbs.db +ehthumbs_vista.db + +# Dump file +*.stackdump + +# Folder config file +[Dd]esktop.ini + +# Recycle Bin used on file shares +$RECYCLE.BIN/ + +# Windows Installer files +*.cab +*.msi +*.msix +*.msm +*.msp + +# Windows shortcuts +*.lnk + +# End of https://www.toptal.com/developers/gitignore/api/windows,macos,visualstudiocode,node,python,git + +# Allow CDK lib folder +!src/cdk/lib +src/cdk/cdk.out + +# Allow wwwroot from petsite-net +!src/applications/microservices/petsite-net/petsite/wwwroot/ +!src/applications/microservices/petsite-net/petsite/wwwroot/** + +.ash/ash_output +# editor settings +.vscode/settings.json +wiki-docs/ +# Go binaries +PetAdoptions/payforadoption-go/petadoptions +PetAdoptions/petsite/petsite/appsettings.Development.json +PetAdoptions/trafficgenerator/.idea/.idea.trafficgenerator/.idea/indexLayout.xml +PetAdoptions/trafficgenerator/.idea/.idea.trafficgenerator/.idea/modules.xml -# Backup folder for Package Reference Convert tool in Visual Studio 2017 -MigrationBackup/ +# Dot net build files +**/obj/** -# Ionide (cross platform F# VS Code tools) working folder -.ionide/ +# Exclude cdk.context.json since each workshop account will be unique +cdk.context.json -# Fody - auto-generated XML schema -FodyWeavers.xsd +# AWS SAM cache +.aws-sam/ -# pethistory +# Canaries +!src/applications/canaries/housekeeping/nodejs/node_modules/index.js -pethistory/.aws-sam/ -pethistory/cdk-example.ts -pethistory/coverage/ -pethistory/events/ -pethistory/samconfig.toml diff --git a/.kiro/steering/development-workflow.md b/.kiro/steering/development-workflow.md new file mode 100644 index 00000000..a0f17af3 --- /dev/null +++ b/.kiro/steering/development-workflow.md @@ -0,0 +1,64 @@ +# Development Workflow Standards + +## Commit Process + +### Before Every Commit +1. **Run local tests** to ensure functionality +2. **Review security scan results** if ASH is configured +3. **Clean up temporary files** and unused code +4. **Verify all files are intentional** additions + +### Handling Pre-commit Hook Failures + +#### Security Scan Failures (ASH) +- **Priority**: Fix the underlying security issues +- **Process**: + 1. Review findings in `.ash/ash_output/reports/ash.html` + 2. Address each finding appropriately + 3. Re-run the commit after fixes +- **Bypass**: Only with explicit security team approval + +#### Code Quality Failures +- **ESLint errors**: Fix JavaScript/TypeScript issues +- **Empty files**: Remove or add meaningful content +- **Formatting issues**: Use project formatters (black, prettier, etc.) + +### Emergency Procedures +For critical hotfixes only: +1. Document the emergency nature +2. Use bypass with full justification +3. Create immediate follow-up ticket for proper fix +4. Notify relevant teams + +## Branch Management + +### Feature Branches +- Use descriptive names: `feature/add-monitoring`, `fix/security-vulnerability` +- Keep branches focused on single features/fixes +- Clean up branches after merging + +### Cleanup Branches +- Use `cleanup/` prefix for maintenance work +- Example: `cleanup/remove-old-templates` + +## File Management Standards + +### Before Committing +- Remove empty files that serve no purpose +- Clean up generated files not meant for version control +- Ensure all added files are intentional + +### Template and Configuration Files +- Keep only the current, working versions +- Move outdated files to archive or remove entirely +- Document any breaking changes in commit messages + +## Kiro Assistant Behavior + +When assisting with development tasks: + +1. **Always prioritize security and quality** +2. **Investigate failures before suggesting workarounds** +3. **Provide educational context about why standards exist** +4. **Suggest proper solutions over quick fixes** +5. **Document any exceptional circumstances clearly** \ No newline at end of file diff --git a/.kiro/steering/security-and-quality-standards.md b/.kiro/steering/security-and-quality-standards.md new file mode 100644 index 00000000..ecb36053 --- /dev/null +++ b/.kiro/steering/security-and-quality-standards.md @@ -0,0 +1,76 @@ +# Security and Code Quality Standards + +## Overview +This project maintains strict security and code quality standards through automated scanning and validation. All code changes must pass security and quality checks before being committed. + +## Security Scanning with ASH (Automated Security Helper) + +### Core Principle +**Security findings must be addressed, not bypassed.** ASH scanning failures indicate real security issues that require investigation and remediation. + +### When ASH Fails +1. **First Priority**: Review and fix the security findings + - Check the ASH report at `.ash/ash_output/reports/ash.html` + - Address each finding according to its severity level + - Use proper suppression only for verified false positives + +2. **Investigation Steps**: + - Run `ash inspect findings` to explore findings interactively + - Review scanner-specific reports in `.ash/ash_output/scanners/` + - Consult security team if findings are unclear + +3. **Proper Suppression**: + - Use ASH suppression files for legitimate false positives + - Document the reason for each suppression + - Get security team approval for critical/high severity suppressions + +### Bypass Guidelines +**The `CODE_DEFENDER_SKIP_LOCAL_HOOKS` environment variable should ONLY be used in these exceptional circumstances:** + +1. **Emergency hotfixes** where security review will follow immediately +2. **Known false positives** that are already documented and approved +3. **Infrastructure issues** where ASH itself is malfunctioning + +**Never bypass for:** +- Convenience or speed +- "Minor" security findings +- Deadline pressure +- Unfamiliarity with security tools + +## Code Quality Standards + +### Pre-commit Hooks +All pre-commit hooks serve important purposes: +- **ESLint**: Prevents JavaScript/TypeScript quality issues +- **Black/Flake8**: Ensures Python code consistency +- **CloudFormation Linter**: Validates infrastructure as code +- **Secrets Detection**: Prevents credential leaks + +### File Management +- Remove empty or unnecessary files before committing +- Ensure all files serve a clear purpose +- Clean up generated or temporary files + +## Kiro Assistant Guidelines + +When helping with commits that fail security or quality checks: + +1. **Always investigate the root cause first** +2. **Propose fixes for the actual issues** +3. **Only suggest bypassing as an absolute last resort** +4. **Require explicit justification for any bypass** +5. **Remind about follow-up security review when bypassing** + +## Escalation Process + +If security findings cannot be resolved: +1. Document the findings and attempted solutions +2. Consult with the security team +3. Consider if the change is truly necessary +4. Get explicit approval before bypassing + +## Resources + +- ASH Documentation: Internal security scanning guidelines +- Security Team Contact: [Security team contact info] +- Code Quality Guidelines: [Link to coding standards] \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..deef41a4 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,144 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +# (c) 2023 Amazon Web Services, Inc. or its affiliates. All Rights Reserved. +# This AWS Content is provided subject to the terms of the AWS Customer Agreement +# available at http://aws.amazon.com/agreement or other written agreement between +# Customer and either Amazon Web Services, Inc. or Amazon Web Services EMEA SARL +# or both. + +# See https://pre-commit.com for more information +# See https://pre-commit.com/hooks.html for more hooks + +default_stages: [pre-commit] + +repos: + - repo: https://github.com/commitizen-tools/commitizen + rev: v4.8.3 + hooks: + - id: commitizen + stages: + - commit-msg + + # SECURITY + - repo: https://github.com/Lucas-C/pre-commit-hooks-safety + rev: v1.4.2 + hooks: + - id: python-safety-dependencies-check + - repo: https://github.com/Yelp/detect-secrets + rev: v1.5.0 + hooks: + - id: detect-secrets + exclude: 'archive/.*|.secrets.baseline' + args: + - '--baseline' + - '.secrets.baseline' + - '--exclude-files' + - 'archive/.*' + + # Formatting + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: check-json + - id: check-yaml + exclude: 'src/templates|src/cdk/lib/microservices/manifests|archive/.*|src/applications/lambda/pethistory-node/template.yaml' + args: [--allow-multiple-documents] + - id: check-case-conflict + - id: trailing-whitespace + - id: mixed-line-ending + args: ['--fix=lf'] + - id: fix-byte-order-marker + - id: check-merge-conflict + - id: detect-private-key + - id: detect-aws-credentials + args: + - --allow-missing-credentials + + - repo: https://github.com/codespell-project/codespell + rev: v2.4.1 + hooks: + - id: codespell + entry: bash -c 'codespell "$@" || true' -- # Don't block, just alert + verbose: true + - repo: https://github.com/pre-commit/mirrors-eslint + rev: v9.32.0 + hooks: + - id: eslint + files: \.[jt]sx?$ + exclude: 'archive/.*' + types: [file] + args: + - --fix + additional_dependencies: + - eslint-plugin-unicorn@v58.0.0 + - eslint-plugin-prettier@v5.2.6 + # - repo: https://github.com/Lucas-C/pre-commit-hooks-nodejs + # rev: v1.1.2 + # hooks: + # - id: dockerfile_lint + # PYTHON FORMATTING + - repo: https://github.com/ambv/black + rev: '25.1.0' + hooks: + - id: black + - repo: https://github.com/pycqa/flake8 + rev: '7.3.0' + hooks: + - id: flake8 + args: ['--max-line-length=88'] # Same max-length as black + + - repo: https://github.com/asottile/pyupgrade + rev: v3.20.0 + hooks: + - id: pyupgrade + - repo: https://github.com/asottile/add-trailing-comma + rev: v3.2.0 + hooks: + - id: add-trailing-comma + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.17.1 + hooks: + - id: mypy + entry: bash -c 'mypy "$@" || true' -- # Don't block, just alert + verbose: true + additional_dependencies: [types-requests] + # CFN CHECKS & LINTING + - repo: https://github.com/aws-cloudformation/cfn-python-lint + rev: v1.38.2 + hooks: + - id: cfn-python-lint + files: src/templates.*\.(ya?ml)$ + + # Legal Text + # - repo: https://github.com/dontirun/text-prepender + # rev: v0.3.0 + # hooks: + # - id: text-prepender + # args: + # - -t + # - NOTICE + # unit testing + - repo: local + hooks: + - id: jest + name: Unit tests using jest + entry: bash -c 'cd src/cdk && npm run test' + language: system + types: [ts] + pass_filenames: false + always_run: true + + - repo: local + hooks: + - id: typedoc + name: Typedoc markdown + entry: bash -c 'npx typedoc' + language: system + types: [ts] + pass_filenames: false + always_run: true + - repo: https://github.com/awslabs/automated-security-helper + rev: v3.0.0 + hooks: + - id: ash-simple-scan + args: [--no-fail-on-findings] # Until we fix dockerfile issues diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..8b233100 --- /dev/null +++ b/.prettierignore @@ -0,0 +1,2 @@ +src/templates/* +src/cdk/lib/microservices/manifests \ No newline at end of file diff --git a/.prettierrc.json b/.prettierrc.json new file mode 100644 index 00000000..2f316681 --- /dev/null +++ b/.prettierrc.json @@ -0,0 +1,7 @@ +{ + "semi": true, + "trailingComma": "all", + "singleQuote": true, + "printWidth": 120, + "tabWidth": 4 +} diff --git a/.secrets.baseline b/.secrets.baseline new file mode 100644 index 00000000..6b75afd7 --- /dev/null +++ b/.secrets.baseline @@ -0,0 +1,293 @@ +{ + "version": "1.5.0", + "plugins_used": [ + { + "name": "ArtifactoryDetector" + }, + { + "name": "AWSKeyDetector" + }, + { + "name": "AzureStorageKeyDetector" + }, + { + "name": "Base64HighEntropyString", + "limit": 4.5 + }, + { + "name": "BasicAuthDetector" + }, + { + "name": "CloudantDetector" + }, + { + "name": "DiscordBotTokenDetector" + }, + { + "name": "GitHubTokenDetector" + }, + { + "name": "HexHighEntropyString", + "limit": 3.0 + }, + { + "name": "IbmCloudIamDetector" + }, + { + "name": "IbmCosHmacDetector" + }, + { + "name": "JwtTokenDetector" + }, + { + "name": "KeywordDetector", + "keyword_exclude": "" + }, + { + "name": "MailchimpDetector" + }, + { + "name": "NpmDetector" + }, + { + "name": "PrivateKeyDetector" + }, + { + "name": "SendGridDetector" + }, + { + "name": "SlackDetector" + }, + { + "name": "SoftlayerDetector" + }, + { + "name": "SquareOAuthDetector" + }, + { + "name": "StripeDetector" + }, + { + "name": "TwilioKeyDetector" + } + ], + "filters_used": [ + { + "path": "detect_secrets.filters.allowlist.is_line_allowlisted" + }, + { + "path": "detect_secrets.filters.common.is_baseline_file", + "filename": ".secrets.baseline" + }, + { + "path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies", + "min_level": 2 + }, + { + "path": "detect_secrets.filters.heuristic.is_indirect_reference" + }, + { + "path": "detect_secrets.filters.heuristic.is_likely_id_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_lock_file" + }, + { + "path": "detect_secrets.filters.heuristic.is_not_alphanumeric_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_potential_uuid" + }, + { + "path": "detect_secrets.filters.heuristic.is_prefixed_with_dollar_sign" + }, + { + "path": "detect_secrets.filters.heuristic.is_sequential_string" + }, + { + "path": "detect_secrets.filters.heuristic.is_swagger_file" + }, + { + "path": "detect_secrets.filters.heuristic.is_templated_secret" + }, + { + "path": "detect_secrets.filters.regex.should_exclude_file", + "pattern": [ + "archive/.*" + ] + } + ], + "results": { + ".github/workflows/tests.yml": [ + { + "type": "Secret Keyword", + "filename": ".github/workflows/tests.yml", + "hashed_secret": "829c3804401b0727f70f73d4415e162400cbe57b", + "is_verified": true, + "line_number": 58, + "is_secret": false + } + ], + "archive/grafana-dashboards/cluster.json": [ + { + "type": "Hex High Entropy String", + "filename": "archive/grafana-dashboards/cluster.json", + "hashed_secret": "93b4bfebba02b2f4c7ad775b549b2bda4ea9ea7e", + "is_verified": true, + "line_number": 2905, + "is_secret": false + } + ], + "archive/grafana-dashboards/kubelet.json": [ + { + "type": "Hex High Entropy String", + "filename": "archive/grafana-dashboards/kubelet.json", + "hashed_secret": "f114b288fe6f5c4ab7352a5a2b9f4e1ea0299132", + "is_verified": true, + "line_number": 2233, + "is_secret": false + } + ], + "archive/grafana-dashboards/namespace-workloads.json": [ + { + "type": "Hex High Entropy String", + "filename": "archive/grafana-dashboards/namespace-workloads.json", + "hashed_secret": "dfa0af643e92a12913f3eac89cf43f080d8252df", + "is_verified": true, + "line_number": 2657, + "is_secret": false + } + ], + "archive/grafana-dashboards/nodes.json": [ + { + "type": "Hex High Entropy String", + "filename": "archive/grafana-dashboards/nodes.json", + "hashed_secret": "dae11b5f260a150a9911707cfebfb22f8b076c7d", + "is_verified": true, + "line_number": 1480, + "is_secret": false + } + ], + "archive/grafana-dashboards/workloads.json": [ + { + "type": "Hex High Entropy String", + "filename": "archive/grafana-dashboards/workloads.json", + "hashed_secret": "601fddddce988d3e5da6ef718312392ca09c6feb", + "is_verified": true, + "line_number": 2295, + "is_secret": false + } + ], + "archive/keycloak-cleanup.sh": [ + { + "type": "Secret Keyword", + "filename": "archive/keycloak-cleanup.sh", + "hashed_secret": "e317d162903dfb908fc56a91625cbc0aef56af38", + "is_verified": true, + "line_number": 296, + "is_secret": false + }, + { + "type": "Secret Keyword", + "filename": "archive/keycloak-cleanup.sh", + "hashed_secret": "6eef6648406c333a4035cd5e60d0bf2ecf2606d7", + "is_verified": true, + "line_number": 302, + "is_secret": false + } + ], + "src/applications/microservices/payforadoption-go/payforadoption/benchmark_test.go": [ + { + "type": "Secret Keyword", + "filename": "src/applications/microservices/payforadoption-go/payforadoption/benchmark_test.go", + "hashed_secret": "ff395c084391d564dd6d03bd9ae2319910728933", + "is_verified": true, + "line_number": 86, + "is_secret": false + } + ], + "src/applications/microservices/payforadoption-go/payforadoption/database_test.go": [ + { + "type": "Secret Keyword", + "filename": "src/applications/microservices/payforadoption-go/payforadoption/database_test.go", + "hashed_secret": "ff395c084391d564dd6d03bd9ae2319910728933", + "is_verified": true, + "line_number": 16, + "is_secret": false + }, + { + "type": "Secret Keyword", + "filename": "src/applications/microservices/payforadoption-go/payforadoption/database_test.go", + "hashed_secret": "206c80413b9a96c1312cc346b7d2517b84463edd", + "is_verified": true, + "line_number": 103, + "is_secret": false + } + ], + "src/applications/microservices/payforadoption-go/payforadoption/service_test.go": [ + { + "type": "Secret Keyword", + "filename": "src/applications/microservices/payforadoption-go/payforadoption/service_test.go", + "hashed_secret": "2f26ff75b10c553aedf3912ac38f040b359768fb", + "is_verified": true, + "line_number": 196, + "is_secret": false + } + ], + "src/applications/microservices/petfood-rs/API_DOCUMENTATION.md": [ + { + "type": "Hex High Entropy String", + "filename": "src/applications/microservices/petfood-rs/API_DOCUMENTATION.md", + "hashed_secret": "bc04b383f306106574885d043d34b46752821a8b", + "is_verified": true, + "line_number": 165, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "src/applications/microservices/petfood-rs/API_DOCUMENTATION.md", + "hashed_secret": "83e7f8325690fd2ea15af73d60b936e715099572", + "is_verified": true, + "line_number": 252, + "is_secret": false + } + ], + "src/applications/microservices/petfood-rs/postman_collection.json": [ + { + "type": "Hex High Entropy String", + "filename": "src/applications/microservices/petfood-rs/postman_collection.json", + "hashed_secret": "bc04b383f306106574885d043d34b46752821a8b", + "is_verified": true, + "line_number": 418, + "is_secret": false + }, + { + "type": "Hex High Entropy String", + "filename": "src/applications/microservices/petfood-rs/postman_collection.json", + "hashed_secret": "c055b7b596ddbd0c0973ab75da5e03f8c7f8b316", + "is_verified": true, + "line_number": 431, + "is_secret": false + } + ], + "src/applications/microservices/petsite-net/petsite/Views/Adoption/Index.cshtml": [ + { + "type": "Base64 High Entropy String", + "filename": "src/applications/microservices/petsite-net/petsite/Views/Adoption/Index.cshtml", + "hashed_secret": "61f66022ecc0cdfa182825047ff2da0e8bdce581", + "is_verified": true, + "line_number": 8, + "is_secret": false + }, + { + "type": "Base64 High Entropy String", + "filename": "src/applications/microservices/petsite-net/petsite/Views/Adoption/Index.cshtml", + "hashed_secret": "2bd221d058dee45f9c9a1932f5e01e66c13bd432", + "is_verified": true, + "line_number": 10, + "is_secret": false + } + ] + }, + "generated_at": "2025-08-15T12:59:08Z" +} diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 807ffc16..00000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,35 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - // Use IntelliSense to find out which attributes exist for C# debugging - // Use hover for the description of the existing attributes - // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md - "name": ".NET Core Launch (web)", - "type": "coreclr", - "request": "launch", - "preLaunchTask": "build", - // If you have changed target frameworks, make sure to update the program path. - "program": "${workspaceFolder}/PetAdoptions/cdk/pet_stack/resources/microservices/petsite/petsite/bin/Debug/net6.0/PetSite.dll", - "args": [], - "cwd": "${workspaceFolder}/PetAdoptions/cdk/pet_stack/resources/microservices/petsite/petsite", - "stopAtEntry": false, - // Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser - "serverReadyAction": { - "action": "openExternally", - "pattern": "\\bNow listening on:\\s+(https?://\\S+)" - }, - "env": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "sourceFileMap": { - "/Views": "${workspaceFolder}/Views" - } - }, - { - "name": ".NET Core Attach", - "type": "coreclr", - "request": "attach" - } - ] -} \ No newline at end of file diff --git a/.vscode/tasks.json b/.vscode/tasks.json deleted file mode 100644 index e0c5a150..00000000 --- a/.vscode/tasks.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "version": "2.0.0", - "tasks": [ - { - "label": "build", - "command": "dotnet", - "type": "process", - "args": [ - "build", - "${workspaceFolder}/PetAdoptions/cdk/pet_stack/resources/microservices/petsite/petsite/PetSite.csproj", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "problemMatcher": "$msCompile" - }, - { - "label": "publish", - "command": "dotnet", - "type": "process", - "args": [ - "publish", - "${workspaceFolder}/PetAdoptions/cdk/pet_stack/resources/microservices/petsite/petsite/PetSite.csproj", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "problemMatcher": "$msCompile" - }, - { - "label": "watch", - "command": "dotnet", - "type": "process", - "args": [ - "watch", - "run", - "--project", - "${workspaceFolder}/PetAdoptions/cdk/pet_stack/resources/microservices/petsite/petsite/PetSite.csproj" - ], - "problemMatcher": "$msCompile" - } - ] -} \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..5a41ee86 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,30 @@ + +# Changelog + +All notable changes to the CodeBuild CDK Deployment Templates will be documented in this file. + +## [1.0.1] - 2025-01-01 + +### Fixed +- **Pipeline Name Extraction**: Fixed pipeline name extraction in `codebuild-deployment-template-simplified.yaml` to use correct ARN field delimiter (`:` field 6 instead of `/` field 2) + - Changed from: `PIPELINE_NAME=$(echo $PIPELINE_ARN | cut -d'/' -f2)` + - Changed to: `PIPELINE_NAME=$(echo $PIPELINE_ARN | cut -d':' -f6)` + - This ensures proper pipeline monitoring and status tracking + +### Technical Details +- Pipeline ARNs follow the format: `arn:aws:codepipeline:region:account:pipeline/pipeline-name` +- The pipeline name is the 6th field when splitting by `:` delimiter +- This fix prevents pipeline monitoring failures due to incorrect name extraction + +## [1.0.0] - 2024-12-01 + +### Added +- Initial release of CodeBuild CDK Deployment Templates +- Simplified template with direct pipeline status polling +- Intelligent retry handling with configurable limits +- Extended timeout support for complex deployments +- Robust error handling and resource cleanup +- Enhanced monitoring with detailed logging \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 914e0741..e39894cc 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,3 +1,7 @@ + # Contributing Guidelines Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional @@ -50,6 +54,188 @@ For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of opensource-codeofconduct@amazon.com with any additional questions or comments. +## Security Scanning and Pre-commit Hooks + +This project uses pre-commit hooks to ensure code quality and security. The following hooks are configured: + +### Security Hooks +- **python-safety-dependencies-check**: Scans Python dependencies for known security vulnerabilities +- **detect-secrets**: Prevents secrets from being committed to the repository +- **detect-private-key**: Detects private keys in code +- **detect-aws-credentials**: Prevents AWS credentials from being committed + +### Code Quality Hooks +- **commitizen**: Enforces conventional commit message format +- **check-json**: Validates JSON file syntax +- **check-yaml**: Validates YAML file syntax +- **trailing-whitespace**: Removes trailing whitespace +- **mixed-line-ending**: Ensures consistent line endings +- **check-merge-conflict**: Detects merge conflict markers +- **codespell**: Checks for common spelling mistakes +- **eslint**: Lints JavaScript/TypeScript files +- **dockerfile_lint**: Lints Dockerfile syntax +- **black**: Formats Python code +- **flake8**: Python code linting +- **mypy**: Python type checking +- **cfn-python-lint**: CloudFormation template linting +- **jest**: Runs unit tests +- **ash-simple-scan**: AWS security scanning + +### Prerequisites + +1. **Install git-remote-s3** (required for pushing initial repo to S3 for container pipelines): + ```bash + pip install git-remote-s3 + ``` + +2. **Install dependencies** from the root of the repository: + ```bash + npm install + ``` + +### Installing Pre-commit Hooks + +**Mac:** +```bash +# Use pip instead of brew to avoid old version issues +pip install pre-commit +pre-commit install +pre-commit install --hook-type commit-msg +``` + +**Windows:** +```bash +pip install pre-commit +pre-commit install +pre-commit install --hook-type commit-msg +``` + +## Local Development + +For faster development without waiting for the pipeline, you can use the local CDK application located in `bin/local.ts`. This deploys resources directly using CDK commands. + +**Prerequisites:** +- Authenticate with your target AWS account +- **IMPORTANT:** Run the deploy check script first (see Deployment Scripts section below) + +**Usage:** +```bash +# List available stacks +cdk -a "npx ts-node bin/local.ts" list + +# Deploy all stacks +cdk -a "npx ts-node bin/local.ts" deploy --all + +# Other CDK commands using local app +cdk -a "npx ts-node bin/local.ts" +``` + +Example commands: +```bash +# Show differences +cdk -a "npx ts-node bin/local.ts" diff + +# Destroy the stack +cdk -a "npx ts-node bin/local.ts" destroy +``` + +## Deployment Scripts + +### Environment Validation Script + +**IMPORTANT:** The `scripts/deploy-check.sh` script must be executed first before deploying the local stack. + +The script validates your environment and prepares the repository for deployment. + +**Setup:** +1. Copy `src/cdk/.env.sample` to `src/cdk/.env` +2. Update the `.env` file with your AWS account details: + - `CONFIG_BUCKET`: Your S3 bucket name + - `BRANCH_NAME`: Your git branch name + - `AWS_ACCOUNT_ID`: Your AWS account ID + - `AWS_REGION`: Your target AWS region + +**Usage:** +```bash +./scripts/deploy-check.sh +``` + +The script will: +- Validate AWS credentials and display current role/account +- Check if the S3 bucket exists (create if needed) +- Verify the repository archive exists in S3 (upload if needed) + +**After running the deploy check script, you can then deploy the local stack using CDK commands.** + +### Application Redeployment Script + +The `src/cdk/scripts/redeploy-app.sh` script helps developers quickly redeploy individual microservices for testing new versions. + +**Prerequisites:** +- AWS CLI configured with appropriate credentials +- One of: Docker, Finch, or Podman installed +- Deployed One Observability Demo infrastructure + +**Usage:** +```bash +./src/cdk/scripts/redeploy-app.sh +``` + +See [Application Redeployment Guide](docs/application-redeployment.md) for detailed instructions. + +### DynamoDB Seeding Script + +The `src/cdk/scripts/seed-dynamodb.sh` script helps populate DynamoDB tables with initial pet adoption data. + +**Prerequisites:** +- AWS CLI configured with appropriate credentials +- `jq` command-line JSON processor installed +- CDK resources must be deployed first +- The script uses data from `src/cdk/scripts/seed.json` + +**Usage:** +```bash +# Interactive mode +./src/cdk/scripts/seed-dynamodb.sh + +# Non-interactive mode with table name +./src/cdk/scripts/seed-dynamodb.sh TABLE_NAME +``` + +The script will: +- Accept table name as parameter for non-interactive usage +- List all DynamoDB tables in your account (interactive mode) +- Automatically suggest tables containing "Petadoption" in the name (interactive mode) +- Allow you to select which table to seed (interactive mode) +- Populate the selected table with pet data from the seed file + +**Note:** This script must be executed after the CDK resources are deployed. + +### Parameter Store Retrieval Script + +The `src/cdk/scripts/get-parameter.sh` script retrieves values from AWS Systems Manager Parameter Store using the configured prefix. + +**Prerequisites:** +- AWS CLI configured with appropriate credentials +- CDK resources must be deployed first + +**Usage:** +```bash +./src/cdk/scripts/get-parameter.sh +``` + +**Example:** +```bash +./src/cdk/scripts/get-parameter.sh database-endpoint +``` + +This retrieves the parameter `/petstore/database-endpoint` from Parameter Store. + +**Return Values:** +- Parameter value if found +- `-1` if parameter not found or invalid key +- `-2` if access denied + ## 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. diff --git a/NOTICE b/NOTICE new file mode 100644 index 00000000..478e6105 --- /dev/null +++ b/NOTICE @@ -0,0 +1,2 @@ +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 \ No newline at end of file diff --git a/PetAdoptions/.vscode/launch.json b/PetAdoptions/.vscode/launch.json deleted file mode 100644 index 38d6b300..00000000 --- a/PetAdoptions/.vscode/launch.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - // Use IntelliSense to find out which attributes exist for C# debugging - // Use hover for the description of the existing attributes - // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md - "version": "0.2.0", - "configurations": [ - { - "name": ".NET Core Launch (web)", - "type": "coreclr", - "request": "launch", - "preLaunchTask": "build", - // If you have changed target frameworks, make sure to update the program path. - "program": "${workspaceFolder}/payforadoption/PayForAdoption/bin/Debug/netcoreapp3.0/PayForAdoption.dll", - "args": [], - "cwd": "${workspaceFolder}/payforadoption/PayForAdoption", - "stopAtEntry": false, - // Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser - "serverReadyAction": { - "action": "openExternally", - "pattern": "^\\s*Now listening on:\\s+(https?://\\S+)" - }, - "env": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "sourceFileMap": { - "/Views": "${workspaceFolder}/Views" - } - }, - { - "name": ".NET Core Attach", - "type": "coreclr", - "request": "attach", - "processId": "${command:pickProcess}" - } - ] -} \ No newline at end of file diff --git a/PetAdoptions/.vscode/tasks.json b/PetAdoptions/.vscode/tasks.json deleted file mode 100644 index 64af7406..00000000 --- a/PetAdoptions/.vscode/tasks.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "version": "2.0.0", - "tasks": [ - { - "label": "build", - "command": "dotnet", - "type": "process", - "args": [ - "build", - "${workspaceFolder}/payforadoption/PayForAdoption/PayForAdoption.csproj", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "problemMatcher": "$msCompile" - }, - { - "label": "publish", - "command": "dotnet", - "type": "process", - "args": [ - "publish", - "${workspaceFolder}/payforadoption/PayForAdoption/PayForAdoption.csproj", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "problemMatcher": "$msCompile" - }, - { - "label": "watch", - "command": "dotnet", - "type": "process", - "args": [ - "watch", - "run", - "${workspaceFolder}/payforadoption/PayForAdoption/PayForAdoption.csproj", - "/property:GenerateFullPaths=true", - "/consoleloggerparameters:NoSummary" - ], - "problemMatcher": "$msCompile" - } - ] -} \ No newline at end of file diff --git a/PetAdoptions/README.md b/PetAdoptions/README.md deleted file mode 100644 index 5d4b6b32..00000000 --- a/PetAdoptions/README.md +++ /dev/null @@ -1 +0,0 @@ -Go to https://observability.workshop.aws/ for install instructions. \ No newline at end of file diff --git a/PetAdoptions/cdk/pet_stack/.gitignore b/PetAdoptions/cdk/pet_stack/.gitignore deleted file mode 100644 index 2b097adf..00000000 --- a/PetAdoptions/cdk/pet_stack/.gitignore +++ /dev/null @@ -1,21 +0,0 @@ -!jest.config.js -*.d.ts -*.js -node_modules - -# CDK asset staging directory -.cdk.staging -cdk.out - -# Parcel build directories -.cache -.build - -**/package-lock.json - -cdk.context.json - -# IDE -.idea - -**/.DS_Store diff --git a/PetAdoptions/cdk/pet_stack/.vscode/launch.json b/PetAdoptions/cdk/pet_stack/.vscode/launch.json deleted file mode 100644 index 35c64d45..00000000 --- a/PetAdoptions/cdk/pet_stack/.vscode/launch.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "version": "0.2.0", - "configurations": [ - { - "type": "node", - "request": "launch", - "name": "CDK Debugger", - "skipFiles": ["/**"], - "runtimeArgs": [ - "-r", - "./node_modules/ts-node/register/transpile-only" - ], - // Entry point of your stack - "args": ["${workspaceFolder}/app/pet_stack.ts"] - } - ] -} \ No newline at end of file diff --git a/PetAdoptions/cdk/pet_stack/README.md b/PetAdoptions/cdk/pet_stack/README.md deleted file mode 100644 index c4345c36..00000000 --- a/PetAdoptions/cdk/pet_stack/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# Welcome to your CDK TypeScript project! - -This is a blank project for TypeScript development with CDK. - -The `cdk.json` file tells the CDK Toolkit how to execute your app. - -## Useful commands - - * `npm run build:sql-seeder` build SqlSeeder lambda package - * `npm run build` compile typescript to js - * `npm run watch` watch for changes and compile - * `npm run test` perform the jest unit tests - * `cdk deploy` deploy this stack to your default AWS account/region - * `cdk diff` compare deployed stack with current state - * `cdk synth` emits the synthesized CloudFormation template diff --git a/PetAdoptions/cdk/pet_stack/app/pet_stack.ts b/PetAdoptions/cdk/pet_stack/app/pet_stack.ts deleted file mode 100644 index cbbd3db1..00000000 --- a/PetAdoptions/cdk/pet_stack/app/pet_stack.ts +++ /dev/null @@ -1,27 +0,0 @@ -#!/usr/bin/env node -import 'source-map-support/register'; -import { Services } from '../lib/services'; -import { Applications } from '../lib/applications'; -//import { EKSPetsite } from '../lib/ekspetsite' -import { App, Tags, Aspects } from 'aws-cdk-lib'; -//import { AwsSolutionsChecks } from 'cdk-nag'; - - -const stackName = "Services"; -const app = new App(); - -const stack = new Services(app, stackName, { - env: { - account: process.env.CDK_DEFAULT_ACCOUNT, - region: process.env.CDK_DEFAULT_REGION -}}); - -const applications = new Applications(app, "Applications", { - env: { - account: process.env.CDK_DEFAULT_ACCOUNT, - region: process.env.CDK_DEFAULT_REGION -}}); - -Tags.of(app).add("Workshop","true") -//Aspects.of(stack).add(new AwsSolutionsChecks({verbose: true})); -//Aspects.of(applications).add(new AwsSolutionsChecks({verbose: true})); diff --git a/PetAdoptions/cdk/pet_stack/cdk.json b/PetAdoptions/cdk/pet_stack/cdk.json deleted file mode 100644 index 8bca0e5f..00000000 --- a/PetAdoptions/cdk/pet_stack/cdk.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "app": "npx ts-node app/pet_stack.ts", - "context": { - "vpc_cidr": "11.0.0.0/16", - "snstopic_email": "someone@example.com", - "rdsusername":"petadmin", - "petsite_on_eks":"true" - } -} diff --git a/PetAdoptions/cdk/pet_stack/lib/applications.ts b/PetAdoptions/cdk/pet_stack/lib/applications.ts deleted file mode 100644 index 3f602dbf..00000000 --- a/PetAdoptions/cdk/pet_stack/lib/applications.ts +++ /dev/null @@ -1,125 +0,0 @@ -import * as iam from 'aws-cdk-lib/aws-iam'; -import * as ssm from 'aws-cdk-lib/aws-ssm'; -import * as eks from 'aws-cdk-lib/aws-eks'; -import * as resourcegroups from 'aws-cdk-lib/aws-resourcegroups'; -import { DockerImageAsset } from 'aws-cdk-lib/aws-ecr-assets'; -import * as yaml from 'js-yaml'; -import { Stack, StackProps, CfnJson, Fn, CfnOutput } from 'aws-cdk-lib'; -import { readFileSync } from 'fs'; -import { Construct } from 'constructs' -import { ContainerImageBuilderProps, ContainerImageBuilder } from './common/container-image-builder' -import { KubectlV31Layer } from '@aws-cdk/lambda-layer-kubectl-v31'; - -export class Applications extends Stack { - constructor(scope: Construct, id: string, props?: StackProps) { - super(scope,id,props); - - const stackName = id; - - const roleArn = ssm.StringParameter.fromStringParameterAttributes(this, 'getParamClusterAdmin', { parameterName: "/eks/petsite/EKSMasterRoleArn"}).stringValue; - const targetGroupArn = ssm.StringParameter.fromStringParameterAttributes(this, 'getParamTargetGroupArn', { parameterName: "/eks/petsite/TargetGroupArn"}).stringValue; - const oidcProviderUrl = ssm.StringParameter.fromStringParameterAttributes(this, 'getOIDCProviderUrl', { parameterName: "/eks/petsite/OIDCProviderUrl"}).stringValue; - const oidcProviderArn = ssm.StringParameter.fromStringParameterAttributes(this, 'getOIDCProviderArn', { parameterName: "/eks/petsite/OIDCProviderArn"}).stringValue; - const rdsSecretArn = ssm.StringParameter.fromStringParameterAttributes(this, 'getRdsSecretArn', { parameterName: "/petstore/rdssecretarn"}).stringValue; - const petHistoryTargetGroupArn = ssm.StringParameter.fromStringParameterAttributes(this, 'getPetHistoryParamTargetGroupArn', { parameterName: "/eks/pethistory/TargetGroupArn"}).stringValue; - - const cluster = eks.Cluster.fromClusterAttributes(this, 'MyCluster', { - clusterName: 'PetSite', - kubectlLayer: new KubectlV31Layer(this, 'kubectl'), - kubectlRoleArn: roleArn, - }); - // ClusterID is not available for creating the proper conditions https://github.com/aws/aws-cdk/issues/10347 - // Thsos might be an issue - const clusterId = Fn.select(4, Fn.split('/', oidcProviderUrl)) // Remove https:// from the URL as workaround to get ClusterID - - const stack = Stack.of(this); - const region = stack.region; - - const app_federatedPrincipal = new iam.FederatedPrincipal( - oidcProviderArn, - { - StringEquals: new CfnJson(this, "App_FederatedPrincipalCondition", { - value: { - [`oidc.eks.${region}.amazonaws.com/id/${clusterId}:aud` ]: "sts.amazonaws.com" - } - }) - } - ); - const app_trustRelationship = new iam.PolicyStatement({ - effect: iam.Effect.ALLOW, - principals: [ app_federatedPrincipal ], - actions: ["sts:AssumeRoleWithWebIdentity"] - }) - - - // FrontEnd SA (SSM, SQS, SNS) - const petstoreserviceaccount = new iam.Role(this, 'PetSiteServiceAccount', { -// assumedBy: eksFederatedPrincipal, - assumedBy: new iam.AccountRootPrincipal(), - managedPolicies: [ - iam.ManagedPolicy.fromManagedPolicyArn(this, 'PetSiteServiceAccount-AmazonSSMFullAccess', 'arn:aws:iam::aws:policy/AmazonSSMFullAccess'), - iam.ManagedPolicy.fromManagedPolicyArn(this, 'PetSiteServiceAccount-AmazonSQSFullAccess', 'arn:aws:iam::aws:policy/AmazonSQSFullAccess'), - iam.ManagedPolicy.fromManagedPolicyArn(this, 'PetSiteServiceAccount-AmazonSNSFullAccess', 'arn:aws:iam::aws:policy/AmazonSNSFullAccess'), - iam.ManagedPolicy.fromManagedPolicyArn(this, 'PetSiteServiceAccount-AWSXRayDaemonWriteAccess', 'arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess') - ], - }); - petstoreserviceaccount.assumeRolePolicy?.addStatements(app_trustRelationship); - - const startStepFnExecutionPolicy = new iam.PolicyStatement({ - effect: iam.Effect.ALLOW, - actions: [ - 'states:StartExecution' - ], - resources: ['*'] - }); - - petstoreserviceaccount.addToPrincipalPolicy(startStepFnExecutionPolicy); - - const petsiteAsset = new DockerImageAsset(this, 'petsiteAsset', { - directory: "./resources/microservices/petsite/petsite/" - }); - - - var manifest = readFileSync("./resources/k8s_petsite/deployment.yaml","utf8"); - var deploymentYaml = yaml.loadAll(manifest) as Record[]; - - deploymentYaml[0].metadata.annotations["eks.amazonaws.com/role-arn"] = new CfnJson(this, "deployment_Role", { value : `${petstoreserviceaccount.roleArn}` }); - deploymentYaml[2].spec.template.spec.containers[0].image = new CfnJson(this, "deployment_Image", { value : `${petsiteAsset.imageUri}` }); - deploymentYaml[3].spec.targetGroupARN = new CfnJson(this,"targetgroupArn", { value: `${targetGroupArn}`}) - - const deploymentManifest = new eks.KubernetesManifest(this,"petsitedeployment",{ - cluster: cluster, - manifest: deploymentYaml - }); - - this.createSsmParameters(new Map(Object.entries({ - '/eks/petsite/stackname': stackName - }))); - - this.createOuputs(new Map(Object.entries({ - 'PetSiteECRImageURL': petsiteAsset.imageUri, - 'PetStoreServiceAccountArn': petstoreserviceaccount.roleArn, - }))); - // Creating AWS Resource Group for all the resources of stack. - const applicationsCfnGroup = new resourcegroups.CfnGroup(this, 'ApplicationsCfnGroup', { - name: stackName, - description: 'Contains all the resources deployed by Cloudformation Stack ' + stackName, - resourceQuery: { - type: 'CLOUDFORMATION_STACK_1_0', - } - }); - } - - private createSsmParameters(params: Map) { - params.forEach((value, key) => { - //const id = key.replace('/', '_'); - new ssm.StringParameter(this, key, { parameterName: key, stringValue: value }); - }); - } - - private createOuputs(params: Map) { - params.forEach((value, key) => { - new CfnOutput(this, key, { value: value }) - }); - } -} \ No newline at end of file diff --git a/PetAdoptions/cdk/pet_stack/lib/applications/eks-application.ts b/PetAdoptions/cdk/pet_stack/lib/applications/eks-application.ts deleted file mode 100644 index 2bf0182e..00000000 --- a/PetAdoptions/cdk/pet_stack/lib/applications/eks-application.ts +++ /dev/null @@ -1,17 +0,0 @@ -import * as iam from 'aws-cdk-lib/aws-iam'; -import * as eks from 'aws-cdk-lib/aws-eks'; -import { Construct } from 'constructs' - -export interface EksApplicationProps { - cluster: eks.ICluster, - app_trustRelationship: iam.PolicyStatement, - kubernetesManifestPath: string, - region: string, - imageUri: string, -} - -export abstract class EksApplication extends Construct { - constructor(scope: Construct, id: string, props: EksApplicationProps) { - super(scope, id); - } -} \ No newline at end of file diff --git a/PetAdoptions/cdk/pet_stack/lib/common/container-image-builder.ts b/PetAdoptions/cdk/pet_stack/lib/common/container-image-builder.ts deleted file mode 100644 index acca51c9..00000000 --- a/PetAdoptions/cdk/pet_stack/lib/common/container-image-builder.ts +++ /dev/null @@ -1,48 +0,0 @@ -/** - * Create a container image from Dockerfile and make it available - * on a dedicated ECR repository (by default, CDK places all of the - * container images in the same "CDK Assets" ECR repository) - * - * Behind the scenes, this is what happens: - * 1. The container image is built locally and pushed into the "CDK Assets" ECR repository - * 2. A dedicated ECR repository is created - * 3. The container image is copied from "CDK Assets" to the dedicated repository - */ - -import * as cdk from 'aws-cdk-lib'; -import * as ecr from 'aws-cdk-lib/aws-ecr'; -import * as ecrassets from 'aws-cdk-lib/aws-ecr-assets'; -import * as ecrdeploy from 'cdk-ecr-deployment'; - -import { Construct } from 'constructs'; - -export interface ContainerImageBuilderProps { - repositoryName: string, - dockerImageAssetDirectory: string -} - -export class ContainerImageBuilder extends Construct { - public repositoryUri: string; - public imageUri: string; - - constructor(scope: Construct, id: string, props: ContainerImageBuilderProps) { - super(scope, id); - - const repository = new ecr.Repository(this, props.repositoryName + 'Repository', { - repositoryName: props.repositoryName, - imageScanOnPush: true, - removalPolicy: cdk.RemovalPolicy.DESTROY, - emptyOnDelete: true, - }); - const image = new ecrassets.DockerImageAsset(this, props.repositoryName + 'DockerImageAsset', { - directory: props.dockerImageAssetDirectory - }); - new ecrdeploy.ECRDeployment(this, props.repositoryName + 'DeployDockerImage', { - src: new ecrdeploy.DockerImageName(image.imageUri), - dest: new ecrdeploy.DockerImageName(repository.repositoryUri), - }); - - this.repositoryUri = repository.repositoryUri; - this.imageUri = `${repository.repositoryUri}:latest`; - } -} diff --git a/PetAdoptions/cdk/pet_stack/lib/modules/core/cloud9.ts b/PetAdoptions/cdk/pet_stack/lib/modules/core/cloud9.ts deleted file mode 100644 index bef74833..00000000 --- a/PetAdoptions/cdk/pet_stack/lib/modules/core/cloud9.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { Construct } from "constructs"; -import * as cloudformation_include from "aws-cdk-lib/cloudformation-include"; -import { CfnRole } from "aws-cdk-lib/aws-iam"; - -export interface Cloud9EnvironmentProps { - name?: string; - vpcId: string; - subnetId: string; - templateFile: string; - cloud9OwnerArn?: string; -} - -export class Cloud9Environment extends Construct { - public readonly c9Role: CfnRole; - constructor(scope: Construct, id: string, props: Cloud9EnvironmentProps) { - super(scope, id); - - const template = new cloudformation_include.CfnInclude(this, 'Cloud9Template', { - templateFile: props.templateFile, - parameters: { - 'CreateVPC': false, - 'Cloud9VPC': props.vpcId, - 'Cloud9Subnet': props.subnetId - }, - preserveLogicalIds: false - }); - - if (props.name) { - template.getParameter("EnvironmentName").default = props.name; - } - - if (props.cloud9OwnerArn) { - template.getParameter("Cloud9OwnerRole").default = props.cloud9OwnerArn.valueOf(); - } - - this.c9Role = template.getResource("C9Role") as CfnRole; - - } -} \ No newline at end of file diff --git a/PetAdoptions/cdk/pet_stack/lib/services.ts b/PetAdoptions/cdk/pet_stack/lib/services.ts deleted file mode 100644 index b1e30cae..00000000 --- a/PetAdoptions/cdk/pet_stack/lib/services.ts +++ /dev/null @@ -1,713 +0,0 @@ -import * as iam from 'aws-cdk-lib/aws-iam'; -import * as ec2 from 'aws-cdk-lib/aws-ec2'; -import * as ecs from 'aws-cdk-lib/aws-ecs'; -import * as sns from 'aws-cdk-lib/aws-sns' -import * as sqs from 'aws-cdk-lib/aws-sqs' -import * as subs from 'aws-cdk-lib/aws-sns-subscriptions' -import * as ddb from 'aws-cdk-lib/aws-dynamodb' -import * as s3 from 'aws-cdk-lib/aws-s3' -import * as s3seeder from 'aws-cdk-lib/aws-s3-deployment' -import * as rds from 'aws-cdk-lib/aws-rds'; -import * as ssm from 'aws-cdk-lib/aws-ssm'; -import * as kms from 'aws-cdk-lib/aws-kms'; -import * as eks from 'aws-cdk-lib/aws-eks'; -import * as yaml from 'js-yaml'; -import * as path from 'path'; -import * as lambda from 'aws-cdk-lib/aws-lambda'; -import * as elbv2 from 'aws-cdk-lib/aws-elasticloadbalancingv2'; -import * as cloudwatch from 'aws-cdk-lib/aws-cloudwatch'; -import * as applicationinsights from 'aws-cdk-lib/aws-applicationinsights'; -import * as resourcegroups from 'aws-cdk-lib/aws-resourcegroups'; -import * as applicationsignals from 'aws-cdk-lib/aws-applicationsignals'; - -import { Construct } from 'constructs' -import { PayForAdoptionService } from './services/pay-for-adoption-service' -import { ListAdoptionsService } from './services/list-adoptions-service' -import { SearchService } from './services/search-service' -import { TrafficGeneratorService } from './services/traffic-generator-service' -import { StatusUpdaterService } from './services/status-updater-service' -import { PetAdoptionsStepFn } from './services/stepfn' -import { KubernetesVersion } from 'aws-cdk-lib/aws-eks'; -import { CfnJson, RemovalPolicy, Fn, Duration, Stack, StackProps, CfnOutput } from 'aws-cdk-lib'; -import { readFileSync } from 'fs'; -import 'ts-replace-all' -import { TreatMissingData, ComparisonOperator } from 'aws-cdk-lib/aws-cloudwatch'; -import { KubectlV31Layer } from '@aws-cdk/lambda-layer-kubectl-v31'; - -export class Services extends Stack { - constructor(scope: Construct, id: string, props?: StackProps) { - super(scope, id, props); - - const stackName = id; - - // Create SQS resource to send Pet adoption messages to - const sqsQueue = new sqs.Queue(this, 'sqs_petadoption', { - visibilityTimeout: Duration.seconds(300) - }); - - // Enable Application Signals in the account - const cfnDiscovery = new applicationsignals.CfnDiscovery(this, - 'ApplicationSignalsServiceRole', { } - ); - - // Create SNS and an email topic to send notifications to - const topic_petadoption = new sns.Topic(this, 'topic_petadoption'); - var topic_email = this.node.tryGetContext('snstopic_email'); - if (topic_email == undefined) { - topic_email = "someone@example.com"; - } - topic_petadoption.addSubscription(new subs.EmailSubscription(topic_email)); - - // Creates an S3 bucket to store pet images - const s3_observabilitypetadoptions = new s3.Bucket(this, 's3bucket_petadoption', { - publicReadAccess: false, - autoDeleteObjects: true, - removalPolicy: RemovalPolicy.DESTROY, - }); - - // Creates the DynamoDB table for Petadoption data - const dynamodb_petadoption = new ddb.Table(this, 'ddb_petadoption', { - partitionKey: { - name: 'pettype', - type: ddb.AttributeType.STRING - }, - sortKey: { - name: 'petid', - type: ddb.AttributeType.STRING - }, - removalPolicy: RemovalPolicy.DESTROY - }); - - dynamodb_petadoption.metric('WriteThrottleEvents', { statistic: "avg" }).createAlarm(this, 'WriteThrottleEvents-BasicAlarm', { - threshold: 0, - treatMissingData: TreatMissingData.NOT_BREACHING, - comparisonOperator: ComparisonOperator.GREATER_THAN_THRESHOLD, - evaluationPeriods: 1, - alarmName: `${dynamodb_petadoption.tableName}-WriteThrottleEvents-BasicAlarm`, - }); - - dynamodb_petadoption.metric('ReadThrottleEvents', { statistic: "avg" }).createAlarm(this, 'ReadThrottleEvents-BasicAlarm', { - threshold: 0, - treatMissingData: TreatMissingData.NOT_BREACHING, - comparisonOperator: ComparisonOperator.GREATER_THAN_THRESHOLD, - evaluationPeriods: 1, - alarmName: `${dynamodb_petadoption.tableName}-ReadThrottleEvents-BasicAlarm`, - }); - - - // Seeds the S3 bucket with pet images - new s3seeder.BucketDeployment(this, "s3seeder_petadoption", { - destinationBucket: s3_observabilitypetadoptions, - sources: [s3seeder.Source.asset('./resources/kitten.zip'), s3seeder.Source.asset('./resources/puppies.zip'), s3seeder.Source.asset('./resources/bunnies.zip')] - }); - - - var cidrRange = this.node.tryGetContext('vpc_cidr'); - if (cidrRange == undefined) { - cidrRange = "11.0.0.0/16"; - } - // The VPC where all the microservices will be deployed into - const theVPC = new ec2.Vpc(this, 'Microservices', { - ipAddresses: ec2.IpAddresses.cidr(cidrRange), - natGateways: 1, - maxAzs: 2 - }); - - // Disable Map IP on launch for all public subnets - const publicSubnets = theVPC.selectSubnets({ - subnetType: ec2.SubnetType.PUBLIC, - }); - - for (const subnet of publicSubnets.subnets) { - const cfnSubnet = subnet.node.defaultChild as ec2.CfnSubnet; - cfnSubnet.mapPublicIpOnLaunch = false; - } - - // Create RDS Aurora PG cluster - const rdssecuritygroup = new ec2.SecurityGroup(this, 'petadoptionsrdsSG', { - vpc: theVPC - }); - - rdssecuritygroup.addIngressRule(ec2.Peer.ipv4(theVPC.vpcCidrBlock), ec2.Port.tcp(5432), 'Allow Aurora PG access from within the VPC CIDR range'); - - var rdsUsername = this.node.tryGetContext('rdsusername'); - if (rdsUsername == undefined) { - rdsUsername = "petadmin" - } - - const auroraCluster = new rds.DatabaseCluster(this, 'Database', { - engine: rds.DatabaseClusterEngine.auroraPostgres({ version: rds.AuroraPostgresEngineVersion.VER_16_6 }), - parameterGroup: rds.ParameterGroup.fromParameterGroupName(this, 'ParameterGroup', 'default.aurora-postgresql16'), - vpc: theVPC, - securityGroups: [rdssecuritygroup], - defaultDatabaseName: 'adoptions', - databaseInsightsMode: rds.DatabaseInsightsMode.ADVANCED, - performanceInsightRetention: rds.PerformanceInsightRetention.MONTHS_15, - writer: rds.ClusterInstance.provisioned('writer', { - autoMinorVersionUpgrade: true, - instanceType: ec2.InstanceType.of(ec2.InstanceClass.T4G, ec2.InstanceSize.MEDIUM), - }), - - readers: [ - rds.ClusterInstance.provisioned('reader1', { - promotionTier: 1, - instanceType: ec2.InstanceType.of(ec2.InstanceClass.T4G, ec2.InstanceSize.MEDIUM), - }), - ], - }); - - - const readSSMParamsPolicy = new iam.PolicyStatement({ - effect: iam.Effect.ALLOW, - actions: [ - 'ssm:GetParametersByPath', - 'ssm:GetParameters', - 'ssm:GetParameter', - 'ec2:DescribeVpcs' - ], - resources: ['*'] - }); - - const adoptionSQSPolicy = new iam.PolicyStatement({ - effect: iam.Effect.ALLOW, - actions: [ - 'sqs:SendMessage', - ], - resources: [sqsQueue.queueArn] - }); - - const ddbSeedPolicy = new iam.PolicyStatement({ - effect: iam.Effect.ALLOW, - actions: [ - 'dynamodb:BatchWriteItem', - 'dynamodb:ListTables', - "dynamodb:Scan", - "dynamodb:Query" - ], - resources: ['*'] - }); - - const repositoryURI = "public.ecr.aws/one-observability-workshop"; - - const stack = Stack.of(this); - const region = stack.region; - - const ecsServicesSecurityGroup = new ec2.SecurityGroup(this, 'ECSServicesSG', { - vpc: theVPC - }); - - ecsServicesSecurityGroup.addIngressRule(ec2.Peer.ipv4(theVPC.vpcCidrBlock), ec2.Port.tcp(80)); - - const ecsPayForAdoptionCluster = new ecs.Cluster(this, "PayForAdoption", { - vpc: theVPC, - containerInsightsV2: ecs.ContainerInsights.ENHANCED - }); - // PayForAdoption service definitions----------------------------------------------------------------------- - const payForAdoptionService = new PayForAdoptionService(this, 'pay-for-adoption-service', { - cluster: ecsPayForAdoptionCluster, - logGroupName: "/ecs/PayForAdoption", - cpu: 1024, - memoryLimitMiB: 2048, - healthCheck: '/health/status', - instrumentation: 'otel', - database: auroraCluster, - desiredTaskCount: 2, - region: region, - securityGroup: ecsServicesSecurityGroup - }); - payForAdoptionService.taskDefinition.taskRole?.addToPrincipalPolicy(readSSMParamsPolicy); - payForAdoptionService.taskDefinition.taskRole?.addToPrincipalPolicy(ddbSeedPolicy); - payForAdoptionService.taskDefinition.taskRole?.addToPrincipalPolicy(adoptionSQSPolicy); - - - const ecsPetListAdoptionCluster = new ecs.Cluster(this, "PetListAdoptions", { - vpc: theVPC, - containerInsightsV2: ecs.ContainerInsights.ENHANCED - }); - // PetListAdoptions service definitions----------------------------------------------------------------------- - const listAdoptionsService = new ListAdoptionsService(this, 'list-adoptions-service', { - cluster: ecsPetListAdoptionCluster, - logGroupName: "/ecs/PetListAdoptions", - cpu: 1024, - memoryLimitMiB: 2048, - healthCheck: '/health/status', - instrumentation: 'otel', - database: auroraCluster, - desiredTaskCount: 2, - region: region, - securityGroup: ecsServicesSecurityGroup - }); - listAdoptionsService.taskDefinition.taskRole?.addToPrincipalPolicy(readSSMParamsPolicy); - - const ecsPetSearchCluster = new ecs.Cluster(this, "PetSearch", { - vpc: theVPC, - containerInsightsV2: ecs.ContainerInsights.ENHANCED - }); - // PetSearch service definitions----------------------------------------------------------------------- - const searchService = new SearchService(this, 'search-service', { - cluster: ecsPetSearchCluster, - logGroupName: "/ecs/PetSearch", - cpu: 1024, - memoryLimitMiB: 2048, - //repositoryURI: repositoryURI, - healthCheck: '/health/status', - desiredTaskCount: 2, - instrumentation: 'none', - region: region, - securityGroup: ecsServicesSecurityGroup - }) - searchService.taskDefinition.taskRole?.addToPrincipalPolicy(readSSMParamsPolicy); - - // Traffic Generator task definition. - const trafficGeneratorService = new TrafficGeneratorService(this, 'traffic-generator-service', { - cluster: ecsPetListAdoptionCluster, - logGroupName: "/ecs/PetTrafficGenerator", - cpu: 256, - memoryLimitMiB: 512, - instrumentation: 'none', - //repositoryURI: repositoryURI, - desiredTaskCount: 1, - region: region, - securityGroup: ecsServicesSecurityGroup - }) - trafficGeneratorService.taskDefinition.taskRole?.addToPrincipalPolicy(readSSMParamsPolicy); - - //PetStatusUpdater Lambda Function and APIGW-------------------------------------- - const statusUpdaterService = new StatusUpdaterService(this, 'status-updater-service', { - tableName: dynamodb_petadoption.tableName - }); - - - const albSG = new ec2.SecurityGroup(this, 'ALBSecurityGroup', { - vpc: theVPC, - securityGroupName: 'ALBSecurityGroup', - allowAllOutbound: true - }); - albSG.addIngressRule(ec2.Peer.anyIpv4(), ec2.Port.tcp(80)); - - // PetSite - Create ALB and Target Groups - const alb = new elbv2.ApplicationLoadBalancer(this, 'PetSiteLoadBalancer', { - vpc: theVPC, - internetFacing: true, - securityGroup: albSG - }); - trafficGeneratorService.node.addDependency(alb); - - const targetGroup = new elbv2.ApplicationTargetGroup(this, 'PetSiteTargetGroup', { - port: 80, - protocol: elbv2.ApplicationProtocol.HTTP, - vpc: theVPC, - targetType: elbv2.TargetType.IP - - }); - - new ssm.StringParameter(this, "putParamTargetGroupArn", { - stringValue: targetGroup.targetGroupArn, - parameterName: '/eks/petsite/TargetGroupArn' - }) - - const listener = alb.addListener('Listener', { - port: 80, - open: true, - defaultTargetGroups: [targetGroup], - }); - - // PetSite - EKS Cluster - const clusterAdmin = new iam.Role(this, 'AdminRole', { - assumedBy: new iam.AccountRootPrincipal() - }); - - new ssm.StringParameter(this, "putParam", { - stringValue: clusterAdmin.roleArn, - parameterName: '/eks/petsite/EKSMasterRoleArn' - }) - - const secretsKey = new kms.Key(this, 'SecretsKey'); - const cluster = new eks.Cluster(this, 'petsite', { - clusterName: 'PetSite', - mastersRole: clusterAdmin, - vpc: theVPC, - defaultCapacity: 2, - defaultCapacityInstance: ec2.InstanceType.of(ec2.InstanceClass.T3, ec2.InstanceSize.MEDIUM), - secretsEncryptionKey: secretsKey, - version: eks.KubernetesVersion.V1_31, - kubectlLayer: new KubectlV31Layer(this, 'kubectl'), - authenticationMode: eks.AuthenticationMode.API_AND_CONFIG_MAP, - }); - - const clusterSG = ec2.SecurityGroup.fromSecurityGroupId(this, 'ClusterSG', cluster.clusterSecurityGroupId); - clusterSG.addIngressRule(albSG, ec2.Port.allTraffic(), 'Allow traffic from the ALB'); - clusterSG.addIngressRule(ec2.Peer.ipv4(theVPC.vpcCidrBlock), ec2.Port.tcp(443), 'Allow local access to k8s api'); - - - // Add SSM Permissions to the node role - cluster.defaultNodegroup?.role.addManagedPolicy(iam.ManagedPolicy.fromAwsManagedPolicyName("AmazonSSMManagedInstanceCore")); - - // From https://github.com/aws-samples/ssm-agent-daemonset-installer - var ssmAgentSetup = yaml.loadAll(readFileSync("./resources/setup-ssm-agent.yaml", "utf8")) as Record[]; - - const ssmAgentSetupManifest = new eks.KubernetesManifest(this, "ssmAgentdeployment", { - cluster: cluster, - manifest: ssmAgentSetup - }); - - - - // ClusterID is not available for creating the proper conditions https://github.com/aws/aws-cdk/issues/10347 - const clusterId = Fn.select(4, Fn.split('/', cluster.clusterOpenIdConnectIssuerUrl)) // Remove https:// from the URL as workaround to get ClusterID - - const cw_federatedPrincipal = new iam.FederatedPrincipal( - cluster.openIdConnectProvider.openIdConnectProviderArn, - { - StringEquals: new CfnJson(this, "CW_FederatedPrincipalCondition", { - value: { - [`oidc.eks.${region}.amazonaws.com/id/${clusterId}:aud`]: "sts.amazonaws.com" - } - }) - } - ); - const cw_trustRelationship = new iam.PolicyStatement({ - effect: iam.Effect.ALLOW, - principals: [cw_federatedPrincipal], - actions: ["sts:AssumeRoleWithWebIdentity"] - }); - - // Create IAM roles for Service Accounts - // Cloudwatch Agent SA - const cwserviceaccount = new iam.Role(this, 'CWServiceAccount', { - // assumedBy: eksFederatedPrincipal, - assumedBy: new iam.AccountRootPrincipal(), - managedPolicies: [ - iam.ManagedPolicy.fromManagedPolicyArn(this, 'CWServiceAccount-CloudWatchAgentServerPolicy', 'arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy') - ], - }); - cwserviceaccount.assumeRolePolicy?.addStatements(cw_trustRelationship); - - // Comment out X-Ray service account for petsite - /* - const xray_federatedPrincipal = new iam.FederatedPrincipal( - cluster.openIdConnectProvider.openIdConnectProviderArn, - { - StringEquals: new CfnJson(this, "Xray_FederatedPrincipalCondition", { - value: { - [`oidc.eks.${region}.amazonaws.com/id/${clusterId}:aud`]: "sts.amazonaws.com" - } - }) - } - ); - const xray_trustRelationship = new iam.PolicyStatement({ - effect: iam.Effect.ALLOW, - principals: [xray_federatedPrincipal], - actions: ["sts:AssumeRoleWithWebIdentity"] - }); - - // X-Ray Agent SA - const xrayserviceaccount = new iam.Role(this, 'XRayServiceAccount', { - // assumedBy: eksFederatedPrincipal, - assumedBy: new iam.AccountRootPrincipal(), - managedPolicies: [ - iam.ManagedPolicy.fromManagedPolicyArn(this, 'XRayServiceAccount-AWSXRayDaemonWriteAccess', 'arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess') - ], - }); - xrayserviceaccount.assumeRolePolicy?.addStatements(xray_trustRelationship); - */ - - const loadbalancer_federatedPrincipal = new iam.FederatedPrincipal( - cluster.openIdConnectProvider.openIdConnectProviderArn, - { - StringEquals: new CfnJson(this, "LB_FederatedPrincipalCondition", { - value: { - [`oidc.eks.${region}.amazonaws.com/id/${clusterId}:aud`]: "sts.amazonaws.com" - } - }) - } - ); - const loadBalancer_trustRelationship = new iam.PolicyStatement({ - effect: iam.Effect.ALLOW, - principals: [loadbalancer_federatedPrincipal], - actions: ["sts:AssumeRoleWithWebIdentity"] - }); - - const loadBalancerPolicyDoc = iam.PolicyDocument.fromJson(JSON.parse(readFileSync("./resources/load_balancer/iam_policy.json", "utf8"))); - const loadBalancerPolicy = new iam.ManagedPolicy(this, 'LoadBalancerSAPolicy', { document: loadBalancerPolicyDoc }); - const loadBalancerserviceaccount = new iam.Role(this, 'LoadBalancerServiceAccount', { - // assumedBy: eksFederatedPrincipal, - assumedBy: new iam.AccountRootPrincipal(), - managedPolicies: [loadBalancerPolicy] - }); - - loadBalancerserviceaccount.assumeRolePolicy?.addStatements(loadBalancer_trustRelationship); - - const eksAdminArn = this.node.tryGetContext('admin_role'); - if ((eksAdminArn != undefined) && (eksAdminArn.length > 0)) { - const adminRole = iam.Role.fromRoleArn(this, "ekdAdminRoleArn", eksAdminArn, { mutable: false }); - cluster.grantAccess('TeamRoleAccess', adminRole.roleArn, [ - eks.AccessPolicy.fromAccessPolicyName('AmazonEKSClusterAdminPolicy', { - accessScopeType: eks.AccessScopeType.CLUSTER - }) - ]); - } - - // Comment out X-Ray deployment for petsite - /* - var xRayYaml = yaml.loadAll(readFileSync("./resources/k8s_petsite/xray-daemon-config.yaml", "utf8")) as Record[]; - - xRayYaml[0].metadata.annotations["eks.amazonaws.com/role-arn"] = new CfnJson(this, "xray_Role", { value: `${xrayserviceaccount.roleArn}` }); - - const xrayManifest = new eks.KubernetesManifest(this, "xraydeployment", { - cluster: cluster, - manifest: xRayYaml - }); - */ - - var loadBalancerServiceAccountYaml = yaml.loadAll(readFileSync("./resources/load_balancer/service_account.yaml", "utf8")) as Record[]; - loadBalancerServiceAccountYaml[0].metadata.annotations["eks.amazonaws.com/role-arn"] = new CfnJson(this, "loadBalancer_Role", { value: `${loadBalancerserviceaccount.roleArn}` }); - - const loadBalancerServiceAccount = new eks.KubernetesManifest(this, "loadBalancerServiceAccount", { - cluster: cluster, - manifest: loadBalancerServiceAccountYaml - }); - - const waitForLBServiceAccount = new eks.KubernetesObjectValue(this, 'LBServiceAccount', { - cluster: cluster, - objectName: "alb-ingress-controller", - objectType: "serviceaccount", - objectNamespace: "kube-system", - jsonPath: "@" - }); - - const loadBalancerCRDYaml = yaml.loadAll(readFileSync("./resources/load_balancer/crds.yaml", "utf8")) as Record[]; - const loadBalancerCRDManifest = new eks.KubernetesManifest(this, "loadBalancerCRD", { - cluster: cluster, - manifest: loadBalancerCRDYaml - }); - - - const awsLoadBalancerManifest = new eks.HelmChart(this, "AWSLoadBalancerController", { - cluster: cluster, - chart: "aws-load-balancer-controller", - repository: "https://aws.github.io/eks-charts", - namespace: "kube-system", - values: { - clusterName: "PetSite", - serviceAccount: { - create: false, - name: "alb-ingress-controller" - }, - wait: true - } - }); - awsLoadBalancerManifest.node.addDependency(loadBalancerCRDManifest); - awsLoadBalancerManifest.node.addDependency(loadBalancerServiceAccount); - awsLoadBalancerManifest.node.addDependency(waitForLBServiceAccount); - - - // NOTE: Amazon CloudWatch Observability Addon for CloudWatch Agent and Fluentbit - const cwAddon = new eks.CfnAddon(this, 'CloudWatchObservabilityAddon', { - addonName: 'amazon-cloudwatch-observability', - addonVersion: 'v3.3.0-eksbuild.1', - clusterName: cluster.clusterName, - // the properties below are optional - resolveConflicts: 'OVERWRITE', - preserveOnDelete: false, - serviceAccountRoleArn: cwserviceaccount.roleArn, - }); - - // IAM Role for Network Flow Monitor - const networkFlowMonitorRole = new iam.CfnRole(this, 'NetworkFlowMonitorRole', { - assumeRolePolicyDocument: { - Version: '2012-10-17', - Statement: [ - { - Effect: 'Allow', - Principal: { - Service: 'pods.eks.amazonaws.com', - }, - Action: [ - 'sts:AssumeRole', - 'sts:TagSession', - ], - }, - ], - }, - managedPolicyArns: [ - 'arn:aws:iam::aws:policy/CloudWatchNetworkFlowMonitorAgentPublishPolicy', - ], - }); - - // Amazon EKS Pod Identity Agent Addon for Network Flow Monitor - const podIdentityAgentAddon = new eks.CfnAddon(this, 'PodIdentityAgentAddon', { - addonName: 'eks-pod-identity-agent', - addonVersion: 'v1.3.4-eksbuild.1', - clusterName: cluster.clusterName, - resolveConflicts: 'OVERWRITE', - preserveOnDelete: false, - }); - - // Amazon EKS AWS Network Flow Monitor Agent add-on - const networkFlowMonitoringAgentAddon = new eks.CfnAddon(this, 'NetworkFlowMonitoringAgentAddon', { - addonName: 'aws-network-flow-monitoring-agent', - addonVersion: 'v1.0.1-eksbuild.2', - clusterName: cluster.clusterName, - resolveConflicts: 'OVERWRITE', - preserveOnDelete: false, - podIdentityAssociations: [ - { - roleArn: networkFlowMonitorRole.attrArn, - serviceAccount: 'aws-network-flow-monitor-agent-service-account', - }, - ], - }); - - const customWidgetResourceControllerPolicy = new iam.PolicyStatement({ - effect: iam.Effect.ALLOW, - actions: [ - 'ecs:ListServices', - 'ecs:UpdateService', - 'eks:DescribeNodegroup', - 'eks:ListNodegroups', - 'eks:DescribeUpdate', - 'eks:UpdateNodegroupConfig', - 'ecs:DescribeServices', - 'eks:DescribeCluster', - 'eks:ListClusters', - 'ecs:ListClusters' - ], - resources: ['*'] - }); - var customWidgetLambdaRole = new iam.Role(this, 'customWidgetLambdaRole', { - assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), - }); - customWidgetLambdaRole.addToPrincipalPolicy(customWidgetResourceControllerPolicy); - - var petsiteApplicationResourceController = new lambda.Function(this, 'petsite-application-resource-controler', { - code: lambda.Code.fromAsset(path.join(__dirname, '/../resources/resource-controller-widget')), - handler: 'petsite-application-resource-controler.lambda_handler', - memorySize: 128, - runtime: lambda.Runtime.PYTHON_3_9, - role: customWidgetLambdaRole, - timeout: Duration.minutes(10) - }); - petsiteApplicationResourceController.addEnvironment("EKS_CLUSTER_NAME", cluster.clusterName); - petsiteApplicationResourceController.addEnvironment("ECS_CLUSTER_ARNS", ecsPayForAdoptionCluster.clusterArn + "," + - ecsPetListAdoptionCluster.clusterArn + "," + ecsPetSearchCluster.clusterArn); - - var customWidgetFunction = new lambda.Function(this, 'cloudwatch-custom-widget', { - code: lambda.Code.fromAsset(path.join(__dirname, '/../resources/resource-controller-widget')), - handler: 'cloudwatch-custom-widget.lambda_handler', - memorySize: 128, - runtime: lambda.Runtime.PYTHON_3_9, - role: customWidgetLambdaRole, - timeout: Duration.seconds(60) - }); - customWidgetFunction.addEnvironment("CONTROLER_LAMBDA_ARN", petsiteApplicationResourceController.functionArn); - customWidgetFunction.addEnvironment("EKS_CLUSTER_NAME", cluster.clusterName); - customWidgetFunction.addEnvironment("ECS_CLUSTER_ARNS", ecsPayForAdoptionCluster.clusterArn + "," + - ecsPetListAdoptionCluster.clusterArn + "," + ecsPetSearchCluster.clusterArn); - - var costControlDashboardBody = readFileSync("./resources/cw_dashboard_cost_control.json", "utf-8"); - costControlDashboardBody = costControlDashboardBody.replaceAll("{{YOUR_LAMBDA_ARN}}", customWidgetFunction.functionArn); - - const petSiteCostControlDashboard = new cloudwatch.CfnDashboard(this, "PetSiteCostControlDashboard", { - dashboardName: `PetSite_Cost_Control_Dashboard_${region}`, - dashboardBody: costControlDashboardBody - }); - - // Creating AWS Resource Group for all the resources of stack. - const servicesCfnGroup = new resourcegroups.CfnGroup(this, 'ServicesCfnGroup', { - name: stackName, - description: 'Contains all the resources deployed by Cloudformation Stack ' + stackName, - resourceQuery: { - type: 'CLOUDFORMATION_STACK_1_0', - } - }); - // Enabling CloudWatch Application Insights for Resource Group - const servicesCfnApplication = new applicationinsights.CfnApplication(this, 'ServicesApplicationInsights', { - resourceGroupName: servicesCfnGroup.name, - autoConfigurationEnabled: true, - cweMonitorEnabled: true, - opsCenterEnabled: true, - }); - // Adding dependency to create these resources at last - servicesCfnGroup.node.addDependency(petSiteCostControlDashboard); - servicesCfnApplication.node.addDependency(servicesCfnGroup); - // Adding a Lambda function to produce the errors - manually executed - var dynamodbQueryLambdaRole = new iam.Role(this, 'dynamodbQueryLambdaRole', { - assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), - managedPolicies: [ - iam.ManagedPolicy.fromManagedPolicyArn(this, 'manageddynamodbread', 'arn:aws:iam::aws:policy/AmazonDynamoDBReadOnlyAccess'), - iam.ManagedPolicy.fromManagedPolicyArn(this, 'lambdaBasicExecRoletoddb', 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole') - ] - }); - - var dynamodbQueryFunction = new lambda.Function(this, 'dynamodb-query-function', { - code: lambda.Code.fromAsset(path.join(__dirname, '/../resources/application-insights')), - handler: 'dynamodb-query-function.lambda_handler', - memorySize: 128, - runtime: lambda.Runtime.PYTHON_3_9, - role: dynamodbQueryLambdaRole, - timeout: Duration.seconds(900) - }); - dynamodbQueryFunction.addEnvironment("DYNAMODB_TABLE_NAME", dynamodb_petadoption.tableName); - - this.createOuputs(new Map(Object.entries({ - 'CWServiceAccountArn': cwserviceaccount.roleArn, - 'NetworkFlowMonitorServiceAccountArn': networkFlowMonitorRole.attrArn, - //'XRayServiceAccountArn': xrayserviceaccount.roleArn, - 'OIDCProviderUrl': cluster.clusterOpenIdConnectIssuerUrl, - 'OIDCProviderArn': cluster.openIdConnectProvider.openIdConnectProviderArn, - 'PetSiteUrl': `http://${alb.loadBalancerDnsName}`, - 'DynamoDBQueryFunction': dynamodbQueryFunction.functionName - }))); - - - const petAdoptionsStepFn = new PetAdoptionsStepFn(this, 'StepFn'); - - this.createSsmParameters(new Map(Object.entries({ - '/petstore/trafficdelaytime': "1", - '/petstore/rumscript': " ", - '/petstore/petadoptionsstepfnarn': petAdoptionsStepFn.stepFn.stateMachineArn, - '/petstore/updateadoptionstatusurl': statusUpdaterService.api.url, - '/petstore/queueurl': sqsQueue.queueUrl, - '/petstore/snsarn': topic_petadoption.topicArn, - '/petstore/dynamodbtablename': dynamodb_petadoption.tableName, - '/petstore/s3bucketname': s3_observabilitypetadoptions.bucketName, - '/petstore/searchapiurl': `http://${searchService.service.loadBalancer.loadBalancerDnsName}/api/search?`, - '/petstore/searchimage': searchService.container.imageName, - '/petstore/petlistadoptionsurl': `http://${listAdoptionsService.service.loadBalancer.loadBalancerDnsName}/api/adoptionlist/`, - '/petstore/petlistadoptionsmetricsurl': `http://${listAdoptionsService.service.loadBalancer.loadBalancerDnsName}/metrics`, - '/petstore/paymentapiurl': `http://${payForAdoptionService.service.loadBalancer.loadBalancerDnsName}/api/completeadoption`, - '/petstore/payforadoptionmetricsurl': `http://${payForAdoptionService.service.loadBalancer.loadBalancerDnsName}/metrics`, - '/petstore/cleanupadoptionsurl': `http://${payForAdoptionService.service.loadBalancer.loadBalancerDnsName}/api/cleanupadoptions`, - '/petstore/petsearch-collector-manual-config': readFileSync("./resources/collector/ecs-xray-manual.yaml", "utf8"), - '/petstore/rdssecretarn': `${auroraCluster.secret?.secretArn}`, - '/petstore/rdsendpoint': auroraCluster.clusterEndpoint.hostname, - '/petstore/rds-reader-endpoint': auroraCluster.clusterReadEndpoint.hostname, - '/petstore/stackname': stackName, - '/petstore/petsiteurl': `http://${alb.loadBalancerDnsName}`, - '/eks/petsite/OIDCProviderUrl': cluster.clusterOpenIdConnectIssuerUrl, - '/eks/petsite/OIDCProviderArn': cluster.openIdConnectProvider.openIdConnectProviderArn, - '/petstore/errormode1': "false" - }))); - - this.createOuputs(new Map(Object.entries({ - 'QueueURL': sqsQueue.queueUrl, - 'UpdateAdoptionStatusurl': statusUpdaterService.api.url, - 'SNSTopicARN': topic_petadoption.topicArn, - 'RDSServerName': auroraCluster.clusterEndpoint.hostname - }))); - } - - private createSsmParameters(params: Map) { - params.forEach((value, key) => { - //const id = key.replace('/', '_'); - new ssm.StringParameter(this, key, { parameterName: key, stringValue: value }); - }); - } - - private createOuputs(params: Map) { - params.forEach((value, key) => { - new CfnOutput(this, key, { value: value }) - }); - } -} \ No newline at end of file diff --git a/PetAdoptions/cdk/pet_stack/lib/services/ecs-service.ts b/PetAdoptions/cdk/pet_stack/lib/services/ecs-service.ts deleted file mode 100644 index c1b2532f..00000000 --- a/PetAdoptions/cdk/pet_stack/lib/services/ecs-service.ts +++ /dev/null @@ -1,197 +0,0 @@ -import { RemovalPolicy } from 'aws-cdk-lib'; -import * as iam from 'aws-cdk-lib/aws-iam'; -import * as ecs from 'aws-cdk-lib/aws-ecs'; -import * as logs from 'aws-cdk-lib/aws-logs'; -import * as ecs_patterns from 'aws-cdk-lib/aws-ecs-patterns'; -import * as ec2 from 'aws-cdk-lib/aws-ec2'; -import { Construct } from 'constructs' - -export interface EcsServiceProps { - cluster?: ecs.Cluster, - - cpu: number; - memoryLimitMiB: number, - logGroupName: string, - - healthCheck?: string, - - disableService?: boolean, - instrumentation?: string, - - repositoryURI?: string, - - desiredTaskCount: number, - - region: string, - - securityGroup: ec2.SecurityGroup -} - -export abstract class EcsService extends Construct { - - private static ExecutionRolePolicy = new iam.PolicyStatement({ - effect: iam.Effect.ALLOW, - resources: ['*'], - actions: [ - "ecr:GetAuthorizationToken", - "ecr:BatchCheckLayerAvailability", - "ecr:GetDownloadUrlForLayer", - "ecr:BatchGetImage", - "logs:CreateLogGroup", - "logs:DescribeLogStreams", - "logs:CreateLogStream", - "logs:DescribeLogGroups", - "logs:PutLogEvents", - "xray:PutTraceSegments", - "xray:PutTelemetryRecords", - "xray:GetSamplingRules", - "xray:GetSamplingTargets", - "xray:GetSamplingStatisticSummaries", - 'ssm:GetParameters' - ] - }); - - public readonly taskDefinition: ecs.FargateTaskDefinition; - public readonly service: ecs_patterns.ApplicationLoadBalancedServiceBase; - public readonly container: ecs.ContainerDefinition; - - constructor(scope: Construct, id: string, props: EcsServiceProps) { - super(scope, id); - - const logging = new ecs.AwsLogDriver({ - streamPrefix: "logs", - logGroup: new logs.LogGroup(this, "ecs-log-group", { - logGroupName: props.logGroupName, - removalPolicy: RemovalPolicy.DESTROY - }) - }); - - /* - const firelenslogging = new ecs.FireLensLogDriver({ - options: { - "Name": "cloudwatch", - "region": props.region, - "log_key": "log", - "log_group_name": props.logGroupName, - "auto_create_group": "false", - "log_stream_name": "$(ecs_task_id)" - } - }); - //*/ - - const taskRole = new iam.Role(this, `taskRole`, { - assumedBy: new iam.ServicePrincipal('ecs-tasks.amazonaws.com') - }); - - this.taskDefinition = new ecs.FargateTaskDefinition(this, "taskDefinition", { - cpu: props.cpu, - taskRole: taskRole, - memoryLimitMiB: props.memoryLimitMiB - }); - - this.taskDefinition.addToExecutionRolePolicy(EcsService.ExecutionRolePolicy); - this.taskDefinition.taskRole?.addManagedPolicy(iam.ManagedPolicy.fromManagedPolicyArn(this, 'AmazonECSTaskExecutionRolePolicy', 'arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy')); - this.taskDefinition.taskRole?.addManagedPolicy(iam.ManagedPolicy.fromManagedPolicyArn(this, 'AWSXrayWriteOnlyAccess', 'arn:aws:iam::aws:policy/AWSXrayWriteOnlyAccess')); - - // Build locally the image only if the repository URI is not specified - // Can help speed up builds if we are not rebuilding anything - const image = props.repositoryURI ? this.containerImageFromRepository(props.repositoryURI) : this.createContainerImage() - - this.container = this.taskDefinition.addContainer('container', { - image: image, - memoryLimitMiB: 512, - cpu: 256, - logging, - environment: { // clear text, not for sensitive data - AWS_REGION: props.region, - } - }); - - this.container.addPortMappings({ - containerPort: 80, - protocol: ecs.Protocol.TCP - }); - - /* - this.taskDefinition.addFirelensLogRouter('firelensrouter', { - firelensConfig: { - type: ecs.FirelensLogRouterType.FLUENTBIT - }, - image: ecs.ContainerImage.fromRegistry('public.ecr.aws/aws-observability/aws-for-fluent-bit:stable') - }) - //*/ - - // sidecar for instrumentation collecting - switch (props.instrumentation) { - - // we don't add any sidecar if instrumentation is none - case "none": { - break; - } - - // This collector would be used for both traces collected using - // open telemetry or X-Ray - case "otel": { - this.addOtelCollectorContainer(this.taskDefinition, logging); - break; - } - - // Default X-Ray traces collector - case "xray": { - this.addXRayContainer(this.taskDefinition, logging); - break; - } - - // Default X-Ray traces collector - // enabled by default - default: { - this.addXRayContainer(this.taskDefinition, logging); - break; - } - } - - if (!props.disableService) { - this.service = new ecs_patterns.ApplicationLoadBalancedFargateService(this, "ecs-service", { - cluster: props.cluster, - taskDefinition: this.taskDefinition, - publicLoadBalancer: true, - desiredCount: props.desiredTaskCount, - listenerPort: 80, - securityGroups: [props.securityGroup] - - }) - - if (props.healthCheck) { - this.service.targetGroup.configureHealthCheck({ - path: props.healthCheck - }); - } - } - } - - abstract containerImageFromRepository(repositoryURI: string): ecs.ContainerImage; - - abstract createContainerImage(): ecs.ContainerImage; - - private addXRayContainer(taskDefinition: ecs.FargateTaskDefinition, logging: ecs.AwsLogDriver) { - taskDefinition.addContainer('xraydaemon', { - image: ecs.ContainerImage.fromRegistry('public.ecr.aws/xray/aws-xray-daemon:3.3.4'), - memoryLimitMiB: 256, - cpu: 256, - logging - }).addPortMappings({ - containerPort: 2000, - protocol: ecs.Protocol.UDP - }); - } - - private addOtelCollectorContainer(taskDefinition: ecs.FargateTaskDefinition, logging: ecs.AwsLogDriver) { - taskDefinition.addContainer('aws-otel-collector', { - image: ecs.ContainerImage.fromRegistry('public.ecr.aws/aws-observability/aws-otel-collector:v0.41.1'), - memoryLimitMiB: 256, - cpu: 256, - command: ["--config", "/etc/ecs/ecs-xray.yaml"], - logging - }); - } -} diff --git a/PetAdoptions/cdk/pet_stack/lib/services/list-adoptions-service.ts b/PetAdoptions/cdk/pet_stack/lib/services/list-adoptions-service.ts deleted file mode 100644 index f0c2cbee..00000000 --- a/PetAdoptions/cdk/pet_stack/lib/services/list-adoptions-service.ts +++ /dev/null @@ -1,35 +0,0 @@ -import * as ecs from 'aws-cdk-lib/aws-ecs'; -import * as rds from 'aws-cdk-lib/aws-rds'; -import { DockerImageAsset } from 'aws-cdk-lib/aws-ecr-assets'; -import { Stack } from 'aws-cdk-lib'; -import { EcsService, EcsServiceProps } from './ecs-service' -import { Construct } from 'constructs' - - -export interface ListAdoptionServiceProps extends EcsServiceProps { - database: rds.DatabaseCluster -} - -export class ListAdoptionsService extends EcsService { - - constructor(scope: Construct, id: string, props: ListAdoptionServiceProps ) { - super(scope, id, props); - - props.database.secret?.grantRead(this.taskDefinition.taskRole); - - // Add environment variables for the Python service - this.container.addEnvironment('PORT', '80'); - this.container.addEnvironment('WORKERS', '4'); - this.container.addEnvironment('AWS_REGION', Stack.of(this).region); - } - - containerImageFromRepository(repositoryURI: string) : ecs.ContainerImage { - return ecs.ContainerImage.fromRegistry(`${repositoryURI}/pet-listadoptions:latest`) - } - - createContainerImage() : ecs.ContainerImage { - return ecs.ContainerImage.fromDockerImageAsset(new DockerImageAsset(this,"petlistadoptions-python", - { directory: "../../petlistadoptions-py"} - )) - } -} diff --git a/PetAdoptions/cdk/pet_stack/lib/services/pay-for-adoption-service.ts b/PetAdoptions/cdk/pet_stack/lib/services/pay-for-adoption-service.ts deleted file mode 100644 index 62ac1dfc..00000000 --- a/PetAdoptions/cdk/pet_stack/lib/services/pay-for-adoption-service.ts +++ /dev/null @@ -1,28 +0,0 @@ -import * as ecs from 'aws-cdk-lib/aws-ecs'; -import * as rds from 'aws-cdk-lib/aws-rds'; -import { DockerImageAsset } from 'aws-cdk-lib/aws-ecr-assets'; -import { EcsService, EcsServiceProps } from './ecs-service' -import { Construct } from 'constructs' - -export interface PayForAdoptionServiceProps extends EcsServiceProps { - database: rds.DatabaseCluster -} - -export class PayForAdoptionService extends EcsService { - - constructor(scope: Construct, id: string, props: PayForAdoptionServiceProps) { - super(scope, id, props); - - props.database.secret?.grantRead(this.taskDefinition.taskRole); - } - - containerImageFromRepository(repositoryURI: string) : ecs.ContainerImage { - return ecs.ContainerImage.fromRegistry(`${repositoryURI}/pet-payforadoption:latest`) - } - - createContainerImage() : ecs.ContainerImage { - return ecs.ContainerImage.fromDockerImageAsset(new DockerImageAsset(this,"pay-for-adoption", { - directory: "./resources/microservices/payforadoption-go" - })) - } -} diff --git a/PetAdoptions/cdk/pet_stack/lib/services/search-service.ts b/PetAdoptions/cdk/pet_stack/lib/services/search-service.ts deleted file mode 100644 index b38ea370..00000000 --- a/PetAdoptions/cdk/pet_stack/lib/services/search-service.ts +++ /dev/null @@ -1,41 +0,0 @@ -import * as ecs from 'aws-cdk-lib/aws-ecs'; -import * as iam from 'aws-cdk-lib/aws-iam'; -import * as appsignals from '@aws-cdk/aws-applicationsignals-alpha'; -import { DockerImageAsset } from 'aws-cdk-lib/aws-ecr-assets'; -import { EcsService, EcsServiceProps } from './ecs-service' -import { Construct } from 'constructs' - -export class SearchService extends EcsService { - - constructor(scope: Construct, id: string, props: EcsServiceProps ) { - super(scope, id, props); - - this.taskDefinition.taskRole?.addManagedPolicy(iam.ManagedPolicy.fromManagedPolicyArn(this, 'AmazonDynamoDBReadOnlyAccess', 'arn:aws:iam::aws:policy/AmazonDynamoDBReadOnlyAccess')); - this.taskDefinition.taskRole?.addManagedPolicy(iam.ManagedPolicy.fromManagedPolicyArn(this, 'AmazonS3ReadOnlyAccess', 'arn:aws:iam::aws:policy/AmazonS3ReadOnlyAccess')); - - // Add Application Signals integration with CloudWatch agent - new appsignals.ApplicationSignalsIntegration(this, 'ApplicationSignalsIntegration', { - taskDefinition: this.taskDefinition, - instrumentation: { - sdkVersion: appsignals.JavaInstrumentationVersion.V2_10_0, - }, - serviceName: 'PetSearch', - cloudWatchAgentSidecar: { - containerName: 'ecs-cwagent', - enableLogging: true, - cpu: 256, - memoryLimitMiB: 512, - } - }); - } - - containerImageFromRepository(repositoryURI: string) : ecs.ContainerImage { - return ecs.ContainerImage.fromRegistry(`${repositoryURI}/pet-search-java:latest`) - } - - createContainerImage() : ecs.ContainerImage { - return ecs.ContainerImage.fromDockerImageAsset(new DockerImageAsset(this,"search-service", { - directory: "./resources/microservices/petsearch-java" - })) - } -} diff --git a/PetAdoptions/cdk/pet_stack/lib/services/status-updater-service.ts b/PetAdoptions/cdk/pet_stack/lib/services/status-updater-service.ts deleted file mode 100644 index b033ff24..00000000 --- a/PetAdoptions/cdk/pet_stack/lib/services/status-updater-service.ts +++ /dev/null @@ -1,68 +0,0 @@ -import * as iam from 'aws-cdk-lib/aws-iam'; -import * as lambda from 'aws-cdk-lib/aws-lambda'; -import * as nodejslambda from 'aws-cdk-lib/aws-lambda-nodejs'; -import * as apigw from 'aws-cdk-lib/aws-apigateway'; -import { Construct } from 'constructs' - -export interface StatusUpdaterServiceProps { - tableName: string -} - -export class StatusUpdaterService extends Construct { - - public api: apigw.RestApi - - constructor(scope: Construct, id: string, props: StatusUpdaterServiceProps) { - super(scope, id); - - var lambdaRole = new iam.Role(this, 'lambdaexecutionrole', { - assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), - managedPolicies: [ - iam.ManagedPolicy.fromManagedPolicyArn(this, 'first', 'arn:aws:iam::aws:policy/AmazonDynamoDBFullAccess'), - iam.ManagedPolicy.fromManagedPolicyArn(this, 'second', 'arn:aws:iam::aws:policy/AWSLambda_FullAccess'), - iam.ManagedPolicy.fromManagedPolicyArn(this, 'fifth', 'arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy'), - iam.ManagedPolicy.fromManagedPolicyArn(this, 'lambdaBasicExecRole', 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole') - ] - }); - - var layerArn = "arn:aws:lambda:"+ process.env.CDK_DEFAULT_REGION +":580247275435:layer:LambdaInsightsExtension:21"; - var layer = lambda.LayerVersion.fromLayerVersionArn(this, `LayerFromArn`, layerArn); - - const lambdaFunction = new nodejslambda.NodejsFunction(this, 'lambdafn', { - runtime: lambda.Runtime.NODEJS_16_X, // execution environment - entry: '../../petstatusupdater/index.js', - depsLockFilePath: '../../petstatusupdater/package-lock.json', - handler: 'handler', - memorySize: 128, - tracing: lambda.Tracing.ACTIVE, - role: lambdaRole, - layers: [layer], - description: 'Update Pet availability status', - environment: { - "TABLE_NAME": props.tableName - }, - bundling: { - externalModules: [ - 'aws-sdk' - ], - nodeModules: [ - 'aws-xray-sdk' - ] - } - }); - - //defines an API Gateway REST API resource backed by our "petstatusupdater" function. - this.api = new apigw.LambdaRestApi(this, 'PetAdoptionStatusUpdater', { - handler: lambdaFunction, - proxy: true, - endpointConfiguration: { - types: [apigw.EndpointType.REGIONAL] - }, deployOptions: { - tracingEnabled: true, - loggingLevel:apigw.MethodLoggingLevel.INFO, - stageName: 'prod' - }, defaultMethodOptions: {methodResponses: [] } - //defaultIntegration: new apigw.Integration({ integrationHttpMethod: 'PUT', type: apigw.IntegrationType.AWS }) - }); - } -} \ No newline at end of file diff --git a/PetAdoptions/cdk/pet_stack/lib/services/stepfn.ts b/PetAdoptions/cdk/pet_stack/lib/services/stepfn.ts deleted file mode 100644 index 28c4a639..00000000 --- a/PetAdoptions/cdk/pet_stack/lib/services/stepfn.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { Duration, StackProps } from 'aws-cdk-lib'; - -import * as sfn from 'aws-cdk-lib/aws-stepfunctions'; -import * as tasks from 'aws-cdk-lib/aws-stepfunctions-tasks'; -import * as lambda from 'aws-cdk-lib/aws-lambda'; -import * as pythonlambda from '@aws-cdk/aws-lambda-python-alpha'; -import * as iam from 'aws-cdk-lib/aws-iam'; -import * as apigw from 'aws-cdk-lib/aws-apigateway'; -import { Tracing } from 'aws-cdk-lib/aws-lambda'; -import * as ssm from 'aws-cdk-lib/aws-ssm'; -import { Construct } from 'constructs' - - -export class PetAdoptionsStepFn extends Construct { - public readonly stepFn: sfn.StateMachine; - - constructor(scope: Construct, id: string, props?: StackProps) { - super(scope, id); - - var lambdaRole = new iam.Role(this, 'stepfnlambdaexecutionrole', { - assumedBy: new iam.ServicePrincipal('lambda.amazonaws.com'), - managedPolicies: [ - iam.ManagedPolicy.fromManagedPolicyArn(this, 'first', 'arn:aws:iam::aws:policy/AmazonDynamoDBReadOnlyAccess'), - iam.ManagedPolicy.fromManagedPolicyArn(this, 'second', 'arn:aws:iam::aws:policy/AmazonSSMReadOnlyAccess'), - iam.ManagedPolicy.fromManagedPolicyArn(this, 'third', 'arn:aws:iam::aws:policy/AWSXRayDaemonWriteAccess'), - iam.ManagedPolicy.fromManagedPolicyArn(this, 'fourth', 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'), - iam.ManagedPolicy.fromManagedPolicyArn(this, 'fifth', 'arn:aws:iam::aws:policy/CloudWatchLambdaInsightsExecutionRolePolicy'), - iam.ManagedPolicy.fromManagedPolicyArn(this, 'sixth', 'arn:aws:iam::aws:policy/AmazonPrometheusRemoteWriteAccess') - ] - }); - - var layerArn = "arn:aws:lambda:" + process.env.CDK_DEFAULT_REGION + ":580247275435:layer:LambdaInsightsExtension:38"; - var layer = lambda.LayerVersion.fromLayerVersionArn(this, `LambdaInsights`, layerArn); - - - var adotLayerArn = "arn:aws:lambda:"+ process.env.CDK_DEFAULT_REGION + ":901920570463:layer:aws-otel-python-amd64-ver-1-19-0:2" - var adotlayer = lambda.LayerVersion.fromLayerVersionArn(this,'otelPythonLambdaLayer',adotLayerArn); - - var layers: lambda.ILayerVersion[] = [layer, adotlayer] - - const readDynamoDB_Step = new tasks.LambdaInvoke(this, 'ReadDynamoDB', { - lambdaFunction: this.createStepFnLambda('lambda_step_readDDB', lambdaRole, layers) - }); - - const priceGreaterThan55_Step = new tasks.LambdaInvoke(this, 'PriceGreaterThan55', { - lambdaFunction: this.createStepFnLambda('lambda_step_priceGreaterThan55', lambdaRole, layers) - }); - - const priceLessThan55_Step = new tasks.LambdaInvoke(this, 'PriceLessThan55', { - lambdaFunction: this.createStepFnLambda('lambda_step_priceLessThan55', lambdaRole, layers) - }); - - const priceEquals55_Step = new sfn.Succeed(this, 'PriceIs55'); - - const definition = readDynamoDB_Step - .next(new sfn.Choice(this, 'IsPriceGreaterThan55?') - .when(sfn.Condition.numberGreaterThan('$.Payload.body.price', 55), priceGreaterThan55_Step) - .when(sfn.Condition.numberLessThan('$.Payload.body.price', 55), priceLessThan55_Step) - .otherwise(priceEquals55_Step)); - - - this.stepFn = new sfn.StateMachine(this, 'StateMachine', { - definitionBody: sfn.DefinitionBody.fromChainable(definition), - tracingEnabled: true, - timeout: Duration.minutes(5) - }); - - } - - private createStepFnLambda(lambdaFileName: string, lambdaRole: iam.Role, lambdalayers: lambda.ILayerVersion[]) { - var pythonFn = new pythonlambda.PythonFunction(this, lambdaFileName, { - entry: './resources/stepfn_lambdas/', - index: lambdaFileName + '.py', - handler: 'lambda_handler', - memorySize: 128, - runtime: lambda.Runtime.PYTHON_3_9, - role: lambdaRole, - layers: lambdalayers, - tracing: Tracing.ACTIVE - }); - pythonFn.addEnvironment("AWS_LAMBDA_EXEC_WRAPPER", "/opt/otel-instrument") - return pythonFn; - } -} diff --git a/PetAdoptions/cdk/pet_stack/lib/services/traffic-generator-service.ts b/PetAdoptions/cdk/pet_stack/lib/services/traffic-generator-service.ts deleted file mode 100644 index 4b871dd4..00000000 --- a/PetAdoptions/cdk/pet_stack/lib/services/traffic-generator-service.ts +++ /dev/null @@ -1,21 +0,0 @@ -import * as ecs from 'aws-cdk-lib/aws-ecs'; -import { DockerImageAsset } from 'aws-cdk-lib/aws-ecr-assets'; -import { EcsService, EcsServiceProps } from './ecs-service' -import { Construct } from 'constructs' - -export class TrafficGeneratorService extends EcsService { - - constructor(scope: Construct, id: string, props: EcsServiceProps ) { - super(scope, id, props); - } - - containerImageFromRepository(repositoryURI: string) : ecs.ContainerImage { - return ecs.ContainerImage.fromRegistry(`${repositoryURI}/pet-trafficgenerator:latest`) - } - - createContainerImage() : ecs.ContainerImage { - return ecs.ContainerImage.fromDockerImageAsset(new DockerImageAsset(this, "traffic-generator", { - directory: "./resources/microservices/trafficgenerator/trafficgenerator" - })) - } -} diff --git a/PetAdoptions/cdk/pet_stack/package.json b/PetAdoptions/cdk/pet_stack/package.json deleted file mode 100644 index ec4f96fa..00000000 --- a/PetAdoptions/cdk/pet_stack/package.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "name": "pet_stack", - "version": "0.2.0", - "bin": { - "pet_stack": "bin/pet_stack.js" - }, - "scripts": { - "build:status-updater": "(cd ../../petstatusupdater && npm i)", - "build": "tsc", - "watch": "tsc -w", - "test": "jest", - "cdk": "cdk" - }, - "dependencies": { - "@aws-cdk/aws-applicationsignals-alpha": "^2.204.0-alpha.0", - "@aws-cdk/aws-lambda-python-alpha": "^2.204.0-alpha.0", - "@aws-cdk/lambda-layer-kubectl-v31": "^2.0.3", - "@types/js-yaml": "^4.0.9", - "aws-cdk-lib": "^2.204.0", - "cdk-ecr-deployment": "^3.1.9", - "jest": "^29.7.0", - "js-yaml": "^4.1.0", - "source-map-support": "^0.5.21" - }, - "devDependencies": { - "@types/jest": "^29.5.14", - "@types/node": "^22.13.4", - "aws-cdk": "^2.204.0", - "cdk-nag": "^2.35.24", - "constructs": "^10.4.2", - "ts-jest": "^29.2.5", - "ts-node": "^10.9.2", - "ts-replace-all": "^1.0.0", - "typescript": "^5.7.3" - } -} \ No newline at end of file diff --git a/PetAdoptions/cdk/pet_stack/resources/application-insights/dynamodb-query-function.py b/PetAdoptions/cdk/pet_stack/resources/application-insights/dynamodb-query-function.py deleted file mode 100644 index ec66e514..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/application-insights/dynamodb-query-function.py +++ /dev/null @@ -1,30 +0,0 @@ -import os -import time -import boto3 -from boto3.dynamodb.conditions import Key - -dynamodb = boto3.resource('dynamodb') -DYNAMODB_TABLE_NAME = os.environ['DYNAMODB_TABLE_NAME'] - -def lambda_handler(event, context): - table = dynamodb.Table(DYNAMODB_TABLE_NAME) - error_mode = event.get('error_mode') - if error_mode == 'true': - query_key = 'wrongKey' - else: - query_key = 'pettype' - t_end = time.time() + 60 * 13 - while time.time() < t_end: - try: - response = table.query( - KeyConditionExpression=Key(query_key).eq('puppy') - ) - items = response['Items'] - except Exception as e: - print("An exception occurred, but still continuing. The error is: ",e) - items = "FunctionError" - time.sleep(30) - return { - 'statusCode': 200, - 'body': items - } \ No newline at end of file diff --git a/PetAdoptions/cdk/pet_stack/resources/collector/ecs-xray-manual.yaml b/PetAdoptions/cdk/pet_stack/resources/collector/ecs-xray-manual.yaml deleted file mode 100644 index b9e0cdf2..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/collector/ecs-xray-manual.yaml +++ /dev/null @@ -1,27 +0,0 @@ -extensions: - health_check: - -receivers: - otlp: - protocols: - grpc: - endpoint: 0.0.0.0:4317 - http: - endpoint: 0.0.0.0:4318 - -processors: - batch/traces: - timeout: 1s - send_batch_size: 50 - -exporters: - awsxray: - -service: - pipelines: - traces: - receivers: [otlp] - processors: [batch/traces] - exporters: [awsxray] - - extensions: [health_check] diff --git a/PetAdoptions/cdk/pet_stack/resources/cwagent-ecs-prometheus-metric-for-awsvpc.yaml b/PetAdoptions/cdk/pet_stack/resources/cwagent-ecs-prometheus-metric-for-awsvpc.yaml deleted file mode 100644 index 558a1809..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/cwagent-ecs-prometheus-metric-for-awsvpc.yaml +++ /dev/null @@ -1,312 +0,0 @@ ---- -AWSTemplateFormatVersion: '2010-09-09' -Parameters: - ECSClusterName: - Type: String - Description: Enter the name of your ECS cluster from which you want to collect Prometheus metrics - CreateIAMRoles: - Type: String - AllowedValues: - - 'True' - - 'False' - Description: Whether to create new IAM roles or using existing IAM roles for the ECS tasks - ConstraintDescription: must specifid, either True or False - ECSLaunchType: - Type: String - AllowedValues: - - 'EC2' - - 'FARGATE' - Description: ECS Launch Type for the ECS cluster - ConstraintDescription: must specifid, either EC2 or FARGATE - TaskRoleName: - Type: String - Description: Enter the CloudWatch Agent ECS task role name - ExecutionRoleName: - Type: String - Description: Enter the CloudWatch Agent ECS execution role name - SecurityGroupID: - Type: String - Description: Enter the Security Group ID for running the CloudWatch Agent ECS Task - SubnetID: - Type: String - Description: Enter the Subnet ID for running the CloudWatch Agent ECS Task -Conditions: - CreateRoles: - !Equals [!Ref CreateIAMRoles, 'True'] - AssignPublicIp: - !Equals [!Ref ECSLaunchType, 'FARGATE'] -Resources: - PrometheusConfigSSMParameter: - Type: AWS::SSM::Parameter - Properties: - Name: !Sub 'AmazonCloudWatch-PrometheusConfigName-${ECSClusterName}-${ECSLaunchType}-awsvpc' - Type: String - Tier: Standard - Description: !Sub 'Prometheus Scraping SSM Parameter for ECS Cluster: ${ECSClusterName}' - Value: |- - global: - scrape_interval: 1m - scrape_timeout: 10s - scrape_configs: - - job_name: cwagent-ecs-file-sd-config - sample_limit: 10000 - file_sd_configs: - - files: [ "/tmp/cwagent_ecs_auto_sd.yaml" ] - CWAgentConfigSSMParameter: - Type: AWS::SSM::Parameter - Properties: - Name: !Sub 'AmazonCloudWatch-CWAgentConfig-${ECSClusterName}-${ECSLaunchType}-awsvpc' - Type: String - Tier: Intelligent-Tiering - Description: !Sub 'CWAgent SSM Parameter with App Mesh and Java EMF Definition for ECS Cluster: ${ECSClusterName}' - Value: |- - { - "agent": { - "debug": true - }, - "logs": { - "metrics_collected": { - "prometheus": { - "prometheus_config_path": "env:PROMETHEUS_CONFIG_CONTENT", - "ecs_service_discovery": { - "sd_frequency": "1m", - "sd_result_file": "/tmp/cwagent_ecs_auto_sd.yaml", - "docker_label": { - }, - "task_definition_list": [ - { - "sd_job_name": "ecs-appmesh-colors", - "sd_metrics_ports": "9901", - "sd_task_definition_arn_pattern": ".*:task-definition/.*-ColorTeller-(white|red):[0-9]+", - "sd_metrics_path": "/stats/prometheus" - }, - { - "sd_job_name": "ecs-appmesh-gateway", - "sd_metrics_ports": "9901", - "sd_task_definition_arn_pattern": ".*:task-definition/.*-ColorGateway:[0-9]+", - "sd_metrics_path": "/stats/prometheus" - }, - { - "sd_job_name": "petsite-webapp", - "sd_metrics_ports": "80", - "sd_task_definition_arn_pattern": ".*:task-definition/Servicespetsite.*:[0-9]+", - "sd_metrics_path": "/metrics" - } - ] - }, - "emf_processor": { - "metric_declaration": [ - { - "source_labels": ["container_name"], - "label_matcher": "^envoy$", - "dimensions": [["ClusterName","TaskDefinitionFamily"]], - "metric_selectors": [ - "^envoy_http_downstream_rq_(total|xx)$", - "^envoy_cluster_upstream_cx_(r|t)x_bytes_total$", - "^envoy_cluster_membership_(healthy|total)$", - "^envoy_server_memory_(allocated|heap_size)$", - "^envoy_cluster_upstream_cx_(connect_timeout|destroy_local_with_active_rq)$", - "^envoy_cluster_upstream_rq_(pending_failure_eject|pending_overflow|timeout|per_try_timeout|rx_reset|maintenance_mode)$", - "^envoy_http_downstream_cx_destroy_remote_active_rq$", - "^envoy_cluster_upstream_flow_control_(paused_reading_total|resumed_reading_total|backed_up_total|drained_total)$", - "^envoy_cluster_upstream_rq_retry$", - "^envoy_cluster_upstream_rq_retry_(success|overflow)$", - "^envoy_server_(version|uptime|live)$" - ] - }, - { - "source_labels": ["container_name"], - "label_matcher": "^envoy$", - "dimensions": [["ClusterName","TaskDefinitionFamily","envoy_http_conn_manager_prefix","envoy_response_code_class"]], - "metric_selectors": [ - "^envoy_http_downstream_rq_xx$" - ] - }, - { - "source_labels": ["Java_EMF_Metrics"], - "label_matcher": "^true$", - "dimensions": [["ClusterName","TaskDefinitionFamily"]], - "metric_selectors": [ - "^jvm_threads_(current|daemon)$", - "^jvm_classes_loaded$", - "^java_lang_operatingsystem_(freephysicalmemorysize|totalphysicalmemorysize|freeswapspacesize|totalswapspacesize|systemcpuload|processcpuload|availableprocessors|openfiledescriptorcount)$", - "^catalina_manager_(rejectedsessions|activesessions)$", - "^jvm_gc_collection_seconds_(count|sum)$", - "^catalina_globalrequestprocessor_(bytesreceived|bytessent|requestcount|errorcount|processingtime)$" - ] - }, - { - "source_labels": ["Java_EMF_Metrics"], - "label_matcher": "^true$", - "dimensions": [["ClusterName","TaskDefinitionFamily","area"]], - "metric_selectors": [ - "^jvm_memory_bytes_used$" - ] - }, - { - "source_labels": ["Java_EMF_Metrics"], - "label_matcher": "^true$", - "dimensions": [["ClusterName","TaskDefinitionFamily","pool"]], - "metric_selectors": [ - "^jvm_memory_pool_bytes_used$" - ] - }, - { - "source_labels": ["container_name"], - "label_matcher": "container", - "dimensions": [["ClusterName","TaskDefinitionFamily"]], - "metric_selectors": [ - "^process_cpu_seconds_total$", - "^process_open_handles$", - "^process_virtual_memory_bytes$", - "^process_start_time_seconds$", - "^process_private_memory_bytes$", - "^process_working_set_bytes$", - "^process_num_threads$" - ] - }, - { - "source_labels": ["container_name"], - "label_matcher": "container", - "dimensions": [["ClusterName","TaskDefinitionFamily"]], - "metric_selectors": [ - "^dotnet_total_memory_bytes$", - "^dotnet_collection_count_total$", - "^dotnet_gc_finalization_queue_length$", - "^dotnet_jit_method_seconds_total$", - "^dotnet_jit_method_total$", - "^dotnet_threadpool_adjustments_total$", - "^dotnet_threadpool_io_num_threads$", - "^dotnet_threadpool_num_threads$", - "^dotnet_gc_pinned_objects$", - "^dotnet_gc_allocated_bytes_total$" - ] - }, - { - "source_labels": ["container_name"], - "label_matcher": "container", - "dimensions": [["ClusterName","TaskGroup"]], - "metric_selectors": [ - "petsite_pet_bunny_searches_total", - "petsite_pets_waiting_for_adoption", - "petsite_petadoptions_total" - ] - } - ] - } - } - }, - "force_flush_interval": 5 - } - } - CWAgentECSExecutionRole: - Type: AWS::IAM::Role - Condition: CreateRoles - Properties: - RoleName: !Ref ExecutionRoleName - Description: Allows ECS container agent makes calls to the Amazon ECS API on your behalf. - AssumeRolePolicyDocument: - Version: '2012-10-17' - Statement: - - Effect: Allow - Principal: - Service: ecs-tasks.amazonaws.com - Action: sts:AssumeRole - ManagedPolicyArns: - - arn:aws:iam::aws:policy/service-role/AmazonECSTaskExecutionRolePolicy - - arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy - Policies: - - PolicyName: ECSSSMInlinePolicy - PolicyDocument: - Version: '2012-10-17' - Statement: - - Effect: Allow - Action: - - ssm:GetParameters - Resource: arn:aws:ssm:*:*:parameter/AmazonCloudWatch-* - CWAgentECSTaskRole: - Type: AWS::IAM::Role - Condition: CreateRoles - DependsOn: CWAgentECSExecutionRole - Properties: - RoleName: !Ref TaskRoleName - Description: Allows ECS tasks to call AWS services on your behalf. - AssumeRolePolicyDocument: - Version: '2012-10-17' - Statement: - - Effect: Allow - Principal: - Service: ecs-tasks.amazonaws.com - Action: sts:AssumeRole - ManagedPolicyArns: - - arn:aws:iam::aws:policy/CloudWatchAgentServerPolicy - Policies: - - PolicyName: ECSServiceDiscoveryInlinePolicy - PolicyDocument: - Version: '2012-10-17' - Statement: - - Effect: Allow - Action: - - ecs:DescribeTasks - - ecs:ListTasks - - ecs:DescribeContainerInstances - Resource: "*" - Condition: - ArnEquals: - ecs:cluster: - !Sub 'arn:${AWS::Partition}:ecs:${AWS::Region}:${AWS::AccountId}:cluster/${ECSClusterName}' - - Effect: Allow - Action: - - ec2:DescribeInstances - - ecs:DescribeTaskDefinition - Resource: "*" - ECSCWAgentTaskDefinition: - Type: 'AWS::ECS::TaskDefinition' - DependsOn: - - PrometheusConfigSSMParameter - - CWAgentConfigSSMParameter - Properties: - Family: !Sub 'cwagent-prometheus-${ECSClusterName}-${ECSLaunchType}-awsvpc' - TaskRoleArn: !If [CreateRoles, !Ref CWAgentECSTaskRole, !Sub 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${TaskRoleName}'] - ExecutionRoleArn: !If [CreateRoles, !Ref CWAgentECSExecutionRole, !Sub 'arn:${AWS::Partition}:iam::${AWS::AccountId}:role/${ExecutionRoleName}'] - NetworkMode: awsvpc - ContainerDefinitions: - - Name: cloudwatch-agent-prometheus - Image: amazon/cloudwatch-agent:1.248913.0-prometheus - Essential: true - MountPoints: [] - PortMappings: [] - Environment: [] - Secrets: - - Name: PROMETHEUS_CONFIG_CONTENT - ValueFrom: !Sub 'AmazonCloudWatch-PrometheusConfigName-${ECSClusterName}-${ECSLaunchType}-awsvpc' - - Name: CW_CONFIG_CONTENT - ValueFrom: !Sub 'AmazonCloudWatch-CWAgentConfig-${ECSClusterName}-${ECSLaunchType}-awsvpc' - LogConfiguration: - LogDriver: awslogs - Options: - awslogs-create-group: 'True' - awslogs-group: "/ecs/ecs-cwagent-prometheus" - awslogs-region: !Ref AWS::Region - awslogs-stream-prefix: !Sub 'ecs-${ECSLaunchType}-awsvpc' - RequiresCompatibilities: - - !Ref ECSLaunchType - Cpu: '512' - Memory: '1024' - ECSCWAgentService: - Type: AWS::ECS::Service - DependsOn: ECSCWAgentTaskDefinition - Properties: - Cluster: !Ref ECSClusterName - DesiredCount: 1 - LaunchType: !Ref ECSLaunchType - SchedulingStrategy: REPLICA - ServiceName: !Sub 'cwagent-prometheus-replica-service-${ECSLaunchType}-awsvpc' - TaskDefinition: !Ref ECSCWAgentTaskDefinition - NetworkConfiguration: - AwsvpcConfiguration: - AssignPublicIp: !If [AssignPublicIp, ENABLED, DISABLED] - SecurityGroups: - - !Ref SecurityGroupID - Subnets: - - !Ref SubnetID diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/README.md b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/README.md deleted file mode 100644 index 68dd1ac6..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/README.md +++ /dev/null @@ -1,36 +0,0 @@ -# AWS re:Invent 2022 COP301 demo - -This contains the sources artifacts to deploy the COP301 session. - -### Architecture diagram - -image - -### Deployment - -#### AWS Lambda API - -Make sure to have [AWS SAM CLI](https://aws.amazon.com/serverless/sam/) installed to run these commands. - -``` -cd lambda-api -npm install -sam sync --stack-name cop301-data -``` - -#### Amazon ECS API - -Visit [the documentation](./ecs-api/README.md) to deploy the Amazon ECS backend API. - -#### Webapp (with AWS Amplify) - -Screenshot 2022-11-27 at 11 31 31 - -Visit [the documentation](./amplifyapp/README.md) to deploy the web app with AWS Amplify. - -### Visualization with Amazon Managed Grafana - -Visit the [getting started page](https://docs.aws.amazon.com/grafana/latest/userguide/getting-started-with-AMG.html) to create an Amazon Managed Grafana workspace and create your dashboard. - -image - diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/.gitignore b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/.gitignore deleted file mode 100644 index 46d86b85..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/.gitignore +++ /dev/null @@ -1,44 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.js - -# testing -/coverage - -# production -/build - -# misc -.DS_Store -.env.local -.env.development.local -.env.test.local -.env.production.local - -npm-debug.log* -yarn-debug.log* -yarn-error.log* - -#amplify-do-not-edit-begin -amplify/\#current-cloud-backend -amplify/.config/local-* -amplify/logs -amplify/mock-data -amplify/backend/amplify-meta.json -amplify/backend/.temp -build/ -dist/ -node_modules/ -aws-exports.js -awsconfiguration.json -amplifyconfiguration.json -amplifyconfiguration.dart -amplify-build-config.json -amplify-gradle-config.json -amplifytools.xcconfig -.secret-* -**.sample -#amplify-do-not-edit-end diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/README.md b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/README.md deleted file mode 100644 index 332a9de0..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/README.md +++ /dev/null @@ -1,33 +0,0 @@ -# Web application with AWS Amplify - -This project was bootstrapped with AWS Amplify. - -## Pre-requisites - -1. Install [AWS Amplify CLI](https://docs.amplify.aws/cli/start/install/) -2. Edit `amplifyapp/src/index.js` and replace `endpoint: $ECS_API_URL` with the Amazon ECS deployment URL - -## Run locally - -In the project directory, you can run: - -```console -npm install -npm start -``` - -Runs the app in the development mode.\ -Open [http://localhost:3000](http://localhost:3000) to view it in your browser. - -The page will reload when you make changes.\ -You may also see any lint errors in the console. - - -## Deployment - -Install [AWS Amplify CLI](https://docs.amplify.aws/cli/start/install/) - -```console -npm install -amplify publish -``` diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/amplify/.config/project-config.json b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/amplify/.config/project-config.json deleted file mode 100644 index 2f5de5f2..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/amplify/.config/project-config.json +++ /dev/null @@ -1,17 +0,0 @@ -{ - "projectName": "cop301", - "version": "3.1", - "frontend": "javascript", - "javascript": { - "framework": "react", - "config": { - "SourceDir": "src", - "DistributionDir": "build", - "BuildCommand": "npm run-script build", - "StartCommand": "npm run-script start" - } - }, - "providers": [ - "awscloudformation" - ] -} \ No newline at end of file diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/amplify/README.md b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/amplify/README.md deleted file mode 100644 index 7c0a9e28..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/amplify/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# Getting Started with Amplify CLI -This directory was generated by [Amplify CLI](https://docs.amplify.aws/cli). - -Helpful resources: -- Amplify documentation: https://docs.amplify.aws -- Amplify CLI documentation: https://docs.amplify.aws/cli -- More details on this folder & generated files: https://docs.amplify.aws/cli/reference/files -- Join Amplify's community: https://amplify.aws/community/ diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/amplify/backend/backend-config.json b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/amplify/backend/backend-config.json deleted file mode 100644 index bc755080..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/amplify/backend/backend-config.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "hosting": { - "amplifyhosting": { - "service": "amplifyhosting", - "providerPlugin": "awscloudformation", - "type": "manual" - } - } -} \ No newline at end of file diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/amplify/backend/hosting/amplifyhosting/amplifyhosting-template.json b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/amplify/backend/hosting/amplifyhosting/amplifyhosting-template.json deleted file mode 100644 index 67ec9947..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/amplify/backend/hosting/amplifyhosting/amplifyhosting-template.json +++ /dev/null @@ -1,39 +0,0 @@ -{ - "AWSTemplateFormatVersion": "2010-09-09", - "Description": "{\"createdOn\":\"Mac\",\"createdBy\":\"Amplify\",\"createdWith\":\"10.4.0\",\"stackType\":\"hosting-amplifyhosting\",\"metadata\":{}}", - "Parameters": { - "env": { - "Type": "String" - }, - "appId": { - "Type": "String" - }, - "type": { - "Type": "String" - } - }, - "Conditions": { - "isManual": { - "Fn::Equals": [ - { - "Ref": "type" - }, - "manual" - ] - } - }, - "Resources": { - "AmplifyBranch": { - "Condition": "isManual", - "Type": "AWS::Amplify::Branch", - "Properties": { - "BranchName": { - "Ref": "env" - }, - "AppId": { - "Ref": "appId" - } - } - } - } -} \ No newline at end of file diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/amplify/backend/tags.json b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/amplify/backend/tags.json deleted file mode 100644 index b9321d71..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/amplify/backend/tags.json +++ /dev/null @@ -1,10 +0,0 @@ -[ - { - "Key": "user:Stack", - "Value": "{project-env}" - }, - { - "Key": "user:Application", - "Value": "{project-name}" - } -] \ No newline at end of file diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/amplify/cli.json b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/amplify/cli.json deleted file mode 100644 index ac7d4451..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/amplify/cli.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "features": { - "graphqltransformer": { - "addmissingownerfields": true, - "improvepluralization": false, - "validatetypenamereservedwords": true, - "useexperimentalpipelinedtransformer": true, - "enableiterativegsiupdates": true, - "secondarykeyasgsi": true, - "skipoverridemutationinputtypes": true, - "transformerversion": 2, - "suppressschemamigrationprompt": true, - "securityenhancementnotification": false, - "showfieldauthnotification": false, - "usesubusernamefordefaultidentityclaim": true, - "usefieldnameforprimarykeyconnectionfield": false, - "enableautoindexquerynames": false, - "respectprimarykeyattributesonconnectionfield": false, - "shoulddeepmergedirectiveconfigdefaults": false, - "populateownerfieldforstaticgroupauth": false - }, - "frontend-ios": { - "enablexcodeintegration": true - }, - "auth": { - "enablecaseinsensitivity": true, - "useinclusiveterminology": true, - "breakcirculardependency": true, - "forcealiasattributes": false, - "useenabledmfas": true - }, - "codegen": { - "useappsyncmodelgenplugin": true, - "usedocsgeneratorplugin": true, - "usetypesgeneratorplugin": true, - "cleangeneratedmodelsdirectory": true, - "retaincasestyle": true, - "addtimestampfields": true, - "handlelistnullabilitytransparently": true, - "emitauthprovider": true, - "generateindexrules": true, - "enabledartnullsafety": true - }, - "appsync": { - "generategraphqlpermissions": true - }, - "latestregionsupport": { - "pinpoint": 1, - "translate": 1, - "transcribe": 1, - "rekognition": 1, - "textract": 1, - "comprehend": 1 - }, - "project": { - "overrides": true - } - }, - "debug": { - "shareProjectConfig": true - } -} \ No newline at end of file diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/amplify/hooks/README.md b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/amplify/hooks/README.md deleted file mode 100644 index 8fb601ea..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/amplify/hooks/README.md +++ /dev/null @@ -1,7 +0,0 @@ -# Command Hooks - -Command hooks can be used to run custom scripts upon Amplify CLI lifecycle events like pre-push, post-add-function, etc. - -To get started, add your script files based on the expected naming convention in this directory. - -Learn more about the script file naming convention, hook parameters, third party dependencies, and advanced configurations at https://docs.amplify.aws/cli/usage/command-hooks diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/amplify/team-provider-info.json b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/amplify/team-provider-info.json deleted file mode 100644 index 3997eec8..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/amplify/team-provider-info.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "dev": { - "awscloudformation": { - "AuthRoleName": "amplify-cop301-dev-232621-authRole", - "UnauthRoleArn": "arn:aws:iam::339743103717:role/amplify-cop301-dev-232621-unauthRole", - "AuthRoleArn": "arn:aws:iam::339743103717:role/amplify-cop301-dev-232621-authRole", - "Region": "us-west-2", - "DeploymentBucketName": "amplify-cop301-dev-232621-deployment", - "UnauthRoleName": "amplify-cop301-dev-232621-unauthRole", - "StackName": "amplify-cop301-dev-232621", - "StackId": "arn:aws:cloudformation:us-west-2:339743103717:stack/amplify-cop301-dev-232621/19498120-5e22-11ed-8d38-021c0ddc0de5", - "AmplifyAppId": "d24q83cq1bplkg" - }, - "categories": { - "hosting": { - "amplifyhosting": { - "appId": "d24q83cq1bplkg", - "type": "manual" - } - } - } - } -} \ No newline at end of file diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/package.json b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/package.json deleted file mode 100644 index 88ac7f70..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/package.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "name": "cop301", - "version": "0.1.0", - "private": true, - "dependencies": { - "@emotion/react": "^11.10.5", - "@emotion/styled": "^11.10.5", - "@mui/material": "^5.10.12", - "@testing-library/jest-dom": "^5.16.5", - "@testing-library/react": "^13.4.0", - "@testing-library/user-event": "^13.5.0", - "aws-amplify": "^4.3.43", - "history": "^5.3.0", - "react": "^18.2.0", - "react-bootstrap": "^2.5.0", - "react-dom": "^18.2.0", - "react-router-dom": "^6.4.3", - "react-scripts": "5.0.1", - "react-spinners": "^0.13.6", - "web-vitals": "^2.1.4" - }, - "scripts": { - "start": "react-scripts start", - "build": "react-scripts build", - "test": "react-scripts test", - "eject": "react-scripts eject" - }, - "eslintConfig": { - "extends": [ - "react-app", - "react-app/jest" - ] - }, - "browserslist": { - "production": [ - ">0.2%", - "not dead", - "not op_mini all" - ], - "development": [ - "last 1 chrome version", - "last 1 firefox version", - "last 1 safari version" - ] - } -} diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/public/favicon.ico b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/public/favicon.ico deleted file mode 100644 index e357885e..00000000 Binary files a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/public/favicon.ico and /dev/null differ diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/public/index.html b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/public/index.html deleted file mode 100644 index 8d21d5ef..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/public/index.html +++ /dev/null @@ -1,22 +0,0 @@ - - - - - - - - - - - - - AWS re:Invent COP301 - - - -
- - diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/public/logo192.png b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/public/logo192.png deleted file mode 100644 index fc44b0a3..00000000 Binary files a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/public/logo192.png and /dev/null differ diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/public/logo512.png b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/public/logo512.png deleted file mode 100644 index a4e47a65..00000000 Binary files a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/public/logo512.png and /dev/null differ diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/public/manifest.json b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/public/manifest.json deleted file mode 100644 index 9952ee61..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/public/manifest.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "short_name": "Veggy", - "name": "React Shopping Cart", - "icons": [ - { - "src": "favicon.ico", - "sizes": "64x64 32x32 24x24 16x16", - "type": "image/x-icon" - }, - { - "src": "logo192.png", - "type": "image/png", - "sizes": "192x192" - }, - { - "src": "logo512.png", - "type": "image/png", - "sizes": "512x512" - } - ], - "start_url": ".", - "display": "standalone", - "theme_color": "#077915", - "background_color": "#ffffff" -} diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/public/robots.txt b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/public/robots.txt deleted file mode 100644 index e9e57dc4..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/public/robots.txt +++ /dev/null @@ -1,3 +0,0 @@ -# https://www.robotstxt.org/robotstxt.html -User-agent: * -Disallow: diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/src/App.css b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/src/App.css deleted file mode 100644 index 74b5e053..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/src/App.css +++ /dev/null @@ -1,38 +0,0 @@ -.App { - text-align: center; -} - -.App-logo { - height: 40vmin; - pointer-events: none; -} - -@media (prefers-reduced-motion: no-preference) { - .App-logo { - animation: App-logo-spin infinite 20s linear; - } -} - -.App-header { - background-color: #282c34; - min-height: 100vh; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - font-size: calc(10px + 2vmin); - color: white; -} - -.App-link { - color: #61dafb; -} - -@keyframes App-logo-spin { - from { - transform: rotate(0deg); - } - to { - transform: rotate(360deg); - } -} diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/src/App.js b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/src/App.js deleted file mode 100644 index e19a48c7..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/src/App.js +++ /dev/null @@ -1,39 +0,0 @@ -import React, { useState, createContext } from "react"; -import Container from '@mui/material/Container'; -import Typography from '@mui/material/Typography'; -import Box from '@mui/material/Box'; -import theme from './utils/theme'; -import { ThemeProvider } from '@mui/material/styles'; -import CssBaseline from '@mui/material/CssBaseline'; -import Colors from "./views/Colors"; -import Choice from "./views/Choice"; - -export const VoteStatusContext = createContext(); - -export default function App() { - - const [voted, setVoted] = useState(false); - const [color, setColor] = useState(undefined); - const [colorCode, setColorCode] = useState(undefined); - - return ( - - - - - - - re:Invent COP301 - Observability the Open Source Way - - {!voted && - - } - {voted && - < Choice /> - } - - - - - ); -} diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/src/App.test.js b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/src/App.test.js deleted file mode 100644 index 1f03afee..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/src/App.test.js +++ /dev/null @@ -1,8 +0,0 @@ -import { render, screen } from '@testing-library/react'; -import App from './App'; - -test('renders learn react link', () => { - render(); - const linkElement = screen.getByText(/learn react/i); - expect(linkElement).toBeInTheDocument(); -}); diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/src/components/ColorCard.js b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/src/components/ColorCard.js deleted file mode 100644 index 62243436..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/src/components/ColorCard.js +++ /dev/null @@ -1,53 +0,0 @@ -import React, { useState, useContext } from "react"; -import Card from '@mui/material/Card'; -import CardActions from '@mui/material/CardActions'; -import CardContent from '@mui/material/CardContent'; -import CardActionArea from '@mui/material/CardActionArea'; -import Button from '@mui/material/Button'; -import Typography from '@mui/material/Typography'; -import { vote } from "../utils/api"; -import { VoteStatusContext } from "../App"; -import { SyncLoader } from "react-spinners"; - -const ColorCard = ({ colorName, colorCode }) => { - - const { setVoted, setColor, setColorCode } = useContext(VoteStatusContext) - const [loading, setLoading] = useState(false); - - const styles = { - minWidth: 27, - background: colorCode, - }; - - const handleUpdateVote = async () => { - setLoading(true) - const response = await vote( - colorName - ); - setVoted(true) - setColor(colorName) - setColorCode(colorCode) - console.log(response); - setLoading(false) - }; - - - return ( - - - - - {colorName} - - - - - - ); -} - -export default ColorCard; \ No newline at end of file diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/src/components/ProTip.js b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/src/components/ProTip.js deleted file mode 100644 index 1ec8633a..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/src/components/ProTip.js +++ /dev/null @@ -1,20 +0,0 @@ -import * as React from 'react'; -import SvgIcon from '@mui/material/SvgIcon'; -import Typography from '@mui/material/Typography'; - -function LightBulbIcon(props) { - return ( - - - - ); -} - -export default function ProTip() { - return ( - - - Click on your favorite color to vote! - - ); -} diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/src/index.css b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/src/index.css deleted file mode 100644 index ec2585e8..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/src/index.css +++ /dev/null @@ -1,13 +0,0 @@ -body { - margin: 0; - font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', - 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', - sans-serif; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; -} - -code { - font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', - monospace; -} diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/src/index.js b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/src/index.js deleted file mode 100644 index 69030366..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/src/index.js +++ /dev/null @@ -1,34 +0,0 @@ -import * as React from 'react'; -import { createRoot } from 'react-dom/client'; -import App from './App'; -import { Amplify, API } from 'aws-amplify'; -import awsExports from './aws-exports'; -import { - BrowserRouter as Router, -} from "react-router-dom"; - -Amplify.configure(awsExports); - -const rootElement = document.getElementById('root'); -const root = createRoot(rootElement); - -Amplify.configure({ - API: { - endpoints: [ - { - name: "voteapi", - endpoint: $ECS_API_URL - } - ] - }, - ...awsExports -}) -API.configure(); - -root.render( - - - -); - - diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/src/logo.svg b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/src/logo.svg deleted file mode 100644 index 9dfc1c05..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/src/logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/src/reportWebVitals.js b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/src/reportWebVitals.js deleted file mode 100644 index 5253d3ad..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/src/reportWebVitals.js +++ /dev/null @@ -1,13 +0,0 @@ -const reportWebVitals = onPerfEntry => { - if (onPerfEntry && onPerfEntry instanceof Function) { - import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { - getCLS(onPerfEntry); - getFID(onPerfEntry); - getFCP(onPerfEntry); - getLCP(onPerfEntry); - getTTFB(onPerfEntry); - }); - } -}; - -export default reportWebVitals; diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/src/setupTests.js b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/src/setupTests.js deleted file mode 100644 index 8f2609b7..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/src/setupTests.js +++ /dev/null @@ -1,5 +0,0 @@ -// jest-dom adds custom jest matchers for asserting on DOM nodes. -// allows you to do things like: -// expect(element).toHaveTextContent(/react/i) -// learn more: https://github.com/testing-library/jest-dom -import '@testing-library/jest-dom'; diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/src/utils/api.js b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/src/utils/api.js deleted file mode 100644 index 79dc4393..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/src/utils/api.js +++ /dev/null @@ -1,16 +0,0 @@ -import { Amplify } from "aws-amplify"; - -const apiName = "voteapi"; - -export async function vote( - colorName, -) { - const path = `/votes`; - const reqBody = { - body: { - color: colorName, - }, - }; - return await Amplify.API.post(apiName, path, reqBody); - // await new Promise(r => setTimeout(r, 1000)); -} \ No newline at end of file diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/src/utils/colors.js b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/src/utils/colors.js deleted file mode 100644 index c639bb36..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/src/utils/colors.js +++ /dev/null @@ -1,15 +0,0 @@ -const colors = [ - { code: "#dc3220", size: 8, name: "red orange" }, - { code: "#d41159", size: 4, name: "pink" }, - { code: "#d35fb7", size: 4, name: "purple" }, - { code: "#5d3a9b", size: 8, name: "deep purple" }, - { code: "#005ab5", size: 8, name: "indigo" }, - { code: "#1a85ff", size: 4, name: "blue" }, - { code: "#00bcd4", size: 4, name: "cyan" }, - { code: "#40b0a6", size: 8, name: "teal" }, - { code: "#994f00", size: 8, name: "brown" }, - { code: "#e66100", size: 4, name: "orange" }, - { code: "#000000", size: 12, name: "black" }, -] - -export default colors; \ No newline at end of file diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/src/utils/theme.js b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/src/utils/theme.js deleted file mode 100644 index fc2aea18..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/src/utils/theme.js +++ /dev/null @@ -1,19 +0,0 @@ -import { red } from '@mui/material/colors'; -import { createTheme } from '@mui/material/styles'; - -// A custom theme for this app -const theme = createTheme({ - palette: { - primary: { - main: '#556cd6', - }, - secondary: { - main: '#19857b', - }, - error: { - main: red.A400, - }, - }, -}); - -export default theme; diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/src/views/Choice.js b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/src/views/Choice.js deleted file mode 100644 index a9026ef5..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/src/views/Choice.js +++ /dev/null @@ -1,35 +0,0 @@ -import React, { useContext } from "react"; -import Typography from '@mui/material/Typography'; -import Grid from '@mui/material/Grid'; -import Card from '@mui/material/Card'; -import CardContent from '@mui/material/CardContent'; -import { VoteStatusContext } from "../App"; -import { PacmanLoader } from "react-spinners"; - - -export default function Choice() { - - const { color, colorCode } = useContext(VoteStatusContext) - const styles = { - minWidth: 27, - background: colorCode, - minHeight: 500 - }; - - return ( - - - - - - Thank you for voting {color}! - - - - - - ); -} diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/src/views/Colors.js b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/src/views/Colors.js deleted file mode 100644 index 2994e5ad..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/amplifyapp/src/views/Colors.js +++ /dev/null @@ -1,20 +0,0 @@ -import React from "react"; -import ProTip from '../components/ProTip'; -import ColorCard from '../components/ColorCard'; -import Grid from '@mui/material/Grid'; -import colors from "../utils/colors"; - -export default function Colors() { - return ( - <> - - - {colors.map((c, i) => ( - - - - ))} - - - ); -} diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/ecs-api/.gitignore b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/ecs-api/.gitignore deleted file mode 100644 index eedd89b4..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/ecs-api/.gitignore +++ /dev/null @@ -1 +0,0 @@ -api diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/ecs-api/Dockerfile b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/ecs-api/Dockerfile deleted file mode 100644 index 824d3a4b..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/ecs-api/Dockerfile +++ /dev/null @@ -1,13 +0,0 @@ -FROM golang:1.18 as builder -ENV GOPROXY=https://goproxy.io,direct -WORKDIR /go/src/app -COPY . . -RUN go get . -RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app . - -FROM alpine:latest -WORKDIR /app -RUN apk --no-cache add ca-certificates -COPY --from=builder /go/src/app/app . -EXPOSE 8000 -CMD ["./app"] diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/ecs-api/README.md b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/ecs-api/README.md deleted file mode 100644 index a509770e..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/ecs-api/README.md +++ /dev/null @@ -1,49 +0,0 @@ -# COP301 Backend API on Amazon ECS - -```console -ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) -``` - -## Create ECR repository - -Skip this step if you already have a repository - -```console -ECR_REPOSITORY_URI=$(aws ecr create-repository --repository cop301-api --query repository.repositoryUri --output text) -``` - -```console -ECR_REPOSITORY_URI=$(aws ecr describe-repositories --repository-names cop301-api --query 'repositories[0].repositoryUri' --output text) -``` - - -## Build image - -```console -docker buildx build . -t cop301-api --platform=linux/amd64 -aws ecr get-login-password | docker login --username AWS --password-stdin $ECR_REPOSITORY_URI -docker tag cop301-api:latest $ECR_REPOSITORY_URI -docker push $ECR_REPOSITORY_URI -``` - -## Deploy application (on Amazon ECS) - -```console -copilot svc init --name cop301-api -copilot svc deploy --name cop301-api --env test -``` - -## Benchmark (optional) - -Drill is a HTTP load testing application written in Rust. Follow this link [to install](https://github.com/fcsonline/drill#install). -If you have [Cargo](https://github.com/fcsonline/drill#install), run: - -```console -cargo install drill -``` - -Running the benchmark scenario - -```console -drill -s --benchmark utils/benchmark.yaml -``` diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/ecs-api/copilot/.workspace b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/ecs-api/copilot/.workspace deleted file mode 100644 index c1dd6c72..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/ecs-api/copilot/.workspace +++ /dev/null @@ -1,2 +0,0 @@ -application: cop301-api -path: "" diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/ecs-api/copilot/cop301-api/addons/observability.yaml b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/ecs-api/copilot/cop301-api/addons/observability.yaml deleted file mode 100644 index 594055e6..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/ecs-api/copilot/cop301-api/addons/observability.yaml +++ /dev/null @@ -1,53 +0,0 @@ -Parameters: - App: - Type: String - Description: Your application's name. - Env: - Type: String - Description: The environment name your service, job, or workflow is being deployed to. - Name: - Type: String - Description: The name of the service, job, or workflow being deployed. - -Resources: - SSM: - Type: AWS::IAM::ManagedPolicy - Properties: - PolicyDocument: - Version: "2012-10-17" - Statement: - - Sid: SSMActions - Effect: Allow - Action: - - ssm:Get* - - ssm:List* - Resource: "*" - Observability: - Type: AWS::IAM::ManagedPolicy - Properties: - PolicyDocument: - Version: "2012-10-17" - Statement: - - Sid: Xray - Effect: Allow - Action: - - xray:PutTraceSegments - - xray:PutTelemetryRecords - - xray:GetSamplingRules - - xray:GetSamplingTargets - - xray:GetSamplingStatisticSummaries - Resource: "*" - - Sid: AMP - Effect: Allow - Action: - - aps:RemoteWrite - Resource: "*" - -Outputs: - # 1. You need to output the IAM ManagedPolicy so that Copilot can add it as a managed policy to your ECS task role. - SSMAccessPolicyArn: - Description: "The ARN of the ManagedPolicy to attach to the task role." - Value: !Ref SSM - ObservabilityAccessPolicyArn: - Description: "The ARN of the ManagedPolicy to attach to the task role." - Value: !Ref Observability diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/ecs-api/copilot/cop301-api/manifest.yml b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/ecs-api/copilot/cop301-api/manifest.yml deleted file mode 100644 index 486b02f2..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/ecs-api/copilot/cop301-api/manifest.yml +++ /dev/null @@ -1,43 +0,0 @@ -# The manifest for the "cop301-api" service. -# Read the full specification for the "Load Balanced Web Service" type at: -# https://aws.github.io/copilot-cli/docs/manifest/lb-web-service/ - -# Your service name will be used in naming your resources like log groups, ECS services, etc. -name: cop301-api -type: Load Balanced Web Service - -# Distribute traffic to your service. -http: - # Requests to this path will be forwarded to your service. - # To match all requests you can use the "/" path. - path: "/" - # You can specify a custom health check path. The default is "/". - healthcheck: "/health" - -# Configuration for your containers and service. -image: - # replace this field with the value of $ECR_REPOSITORY_URI - location: $ECR_REPOSITORY_URI - # Docker build arguments. For additional overrides: https://aws.github.io/copilot-cli/docs/manifest/lb-web-service/#image-build - # build: Dockerfile - # Port exposed through your container to route traffic to it. - port: 8000 - platform: linux/arm64 - -cpu: 256 # Number of CPU units for the task. -memory: 512 # Amount of memory in MiB used by the task. -count: 2 # Number of tasks that should be running in your service. -exec: true # Enable running commands in your container. - -# Optional fields for more advanced use-cases. -# -variables: # Pass environment variables as key value pairs. - LOG_LEVEL: info - # Deploy /data module to have the lambda URL - DATA_API_URL: $DATA_API_URL - -sidecars: - otel_sidecar: - image: "public.ecr.aws/aws-observability/aws-otel-collector:latest" - secrets: - AOT_CONFIG_CONTENT: /copilot/${COPILOT_APPLICATION_NAME}/${COPILOT_ENVIRONMENT_NAME}/secrets/otel_config diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/ecs-api/copilot/environments/test/manifest.yml b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/ecs-api/copilot/environments/test/manifest.yml deleted file mode 100644 index 630fd9ca..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/ecs-api/copilot/environments/test/manifest.yml +++ /dev/null @@ -1,21 +0,0 @@ -# The manifest for the "test" environment. -# Read the full specification for the "Environment" type at: -# https://aws.github.io/copilot-cli/docs/manifest/environment/ - -# Your environment name will be used in naming your resources like VPC, cluster, etc. -name: test -type: Environment - -# Import your own VPC and subnets or configure how they should be created. -# network: -# vpc: -# id: - -# Configure the load balancers in your environment, once created. -# http: -# public: -# private: - -# Configure observability for your environment resources. -observability: - container_insights: false diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/ecs-api/go.mod b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/ecs-api/go.mod deleted file mode 100644 index 7c8d5d8c..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/ecs-api/go.mod +++ /dev/null @@ -1,48 +0,0 @@ -module api - -go 1.20 - -require ( - github.com/aws/aws-sdk-go v1.44.276 - github.com/gorilla/mux v1.8.0 - github.com/prometheus/client_golang v1.15.1 - github.com/rs/cors v1.9.0 - go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.42.0 - go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0 - go.opentelemetry.io/contrib/propagators/aws v1.17.0 - go.opentelemetry.io/otel v1.16.0 - go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.16.0 - go.opentelemetry.io/otel/sdk v1.16.0 - go.opentelemetry.io/otel/trace v1.16.0 - go.uber.org/zap v1.24.0 -) - -require ( - github.com/beorn7/perks v1.0.1 // indirect - github.com/cenkalti/backoff/v4 v4.2.1 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/felixge/httpsnoop v1.0.3 // indirect - github.com/go-logr/logr v1.2.4 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/golang/protobuf v1.5.3 // indirect - github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect - github.com/jmespath/go-jmespath v0.4.0 // indirect - github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect - github.com/prometheus/client_model v0.4.0 // indirect - github.com/prometheus/common v0.44.0 // indirect - github.com/prometheus/procfs v0.10.1 // indirect - go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0 // indirect - go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0 // indirect - go.opentelemetry.io/otel/metric v1.16.0 // indirect - go.opentelemetry.io/proto/otlp v0.19.0 // indirect - go.uber.org/atomic v1.11.0 // indirect - go.uber.org/multierr v1.11.0 // indirect - golang.org/x/net v0.10.0 // indirect - golang.org/x/sys v0.8.0 // indirect - golang.org/x/text v0.9.0 // indirect - google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc // indirect - google.golang.org/grpc v1.56.3 // indirect - google.golang.org/protobuf v1.30.0 // indirect -) diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/ecs-api/go.sum b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/ecs-api/go.sum deleted file mode 100644 index d562dfb1..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/ecs-api/go.sum +++ /dev/null @@ -1,509 +0,0 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= -github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/aws/aws-sdk-go v1.44.276 h1:ywPlx9C5Yc482dUgAZ9bHpQ6onVvJvYE9FJWsNDCEy0= -github.com/aws/aws-sdk-go v1.44.276/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= -github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= -github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= -github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= -github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= -github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= -github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= -github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= -github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= -github.com/golang/glog v1.1.0 h1:/d3pCKDPWNnvIWe0vVUpNP32qc8U3PDVxySP/y360qE= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 h1:YBftPWNWd4WwGqtY2yeZL2ef8rHAxPBD8KFhJpmcqms= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0/go.mod h1:YN5jB8ie0yfIUg6VvR9Kz84aCaG7AsGZnLjhHbUqwPg= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= -github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= -github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI= -github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= -github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= -github.com/prometheus/common v0.44.0 h1:+5BrQJwiBB9xsMygAB3TNvpQKOwlkc25LbISbrdOOfY= -github.com/prometheus/common v0.44.0/go.mod h1:ofAIvZbQ1e/nugmZGz4/qCb9Ap1VoSTIO7x0VV9VvuY= -github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+PymziUAg= -github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= -github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rs/cors v1.9.0 h1:l9HGsTsHJcvW14Nk7J9KFz8bzeAWXn3CG6bgt7LsrAE= -github.com/rs/cors v1.9.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.42.0 h1:M21Uhqx97uKzB9NhtPxUGT1EzP/AkLaVHD5vib+qoK4= -go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux v0.42.0/go.mod h1:hZGj9DTQYUAszT7dWME6Ls2nWHrJAyyjTtBrBvK6QJw= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0 h1:pginetY7+onl4qN1vl0xW/V/v6OBZ0vVdH+esuJgvmM= -go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0/go.mod h1:XiYsayHc36K3EByOO6nbAXnAWbrUxdjUROCEeeROOH8= -go.opentelemetry.io/contrib/propagators/aws v1.17.0 h1:IX8d7l2uRw61BlmZBOTQFaK+y22j6vytMVTs9wFrO+c= -go.opentelemetry.io/contrib/propagators/aws v1.17.0/go.mod h1:pAlCYRWff4uGqRXOVn3WP8pDZ5E0K56bEoG7a1VSL4k= -go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s= -go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4= -go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0 h1:t4ZwRPU+emrcvM2e9DHd0Fsf0JTPVcbfa/BhTDF03d0= -go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0/go.mod h1:vLarbg68dH2Wa77g71zmKQqlQ8+8Rq3GRG31uc0WcWI= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0 h1:cbsD4cUcviQGXdw8+bo5x2wazq10SKz8hEbtCRPcU78= -go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0/go.mod h1:JgXSGah17croqhJfhByOLVY719k1emAXC8MVhCIJlRs= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.16.0 h1:TVQp/bboR4mhZSav+MdgXB8FaRho1RC8UwVn3T0vjVc= -go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.16.0/go.mod h1:I33vtIe0sR96wfrUcilIzLoA3mLHhRmz9S9Te0S3gDo= -go.opentelemetry.io/otel/metric v1.16.0 h1:RbrpwVG1Hfv85LgnZ7+txXioPDoh6EdbZHo26Q3hqOo= -go.opentelemetry.io/otel/metric v1.16.0/go.mod h1:QE47cpOmkwipPiefDwo2wDzwJrlfxxNYodqc4xnGCo4= -go.opentelemetry.io/otel/sdk v1.16.0 h1:Z1Ok1YsijYL0CSJpHt4cS3wDDh7p572grzNrBMiMWgE= -go.opentelemetry.io/otel/sdk v1.16.0/go.mod h1:tMsIuKXuuIWPBAOrH+eHtvhTL+SntFtXF9QD68aP6p4= -go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs= -go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.opentelemetry.io/proto/otlp v0.19.0 h1:IVN6GR+mhC4s5yfcTbmzHYODqvWAp3ZedA2SJPI1Nnw= -go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= -go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE= -go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= -go.uber.org/goleak v1.2.1 h1:NBol2c7O1ZokfZ0LEU9K6Whx/KnwvepVetCUhtKja4A= -go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= -go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc h1:8DyZCyvI8mE1IdLy/60bS+52xfymkE72wv1asokgtao= -google.golang.org/genproto v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:xZnkP7mREFX5MORlOPEzLMr+90PPZQ2QWzrVTWfAq64= -google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc h1:kVKPf/IiYSBWEWtkIn6wZXwWGCnLKcC8oWfZvXjsGnM= -google.golang.org/genproto/googleapis/api v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:vHYtlOoi6TsQ3Uk2yxR7NI5z8uoV+3pZtR4jmHIkRig= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc h1:XSJ8Vk1SWuNr8S18z1NZSziL0CPIXLCCMDOEFtHBOFc= -google.golang.org/genproto/googleapis/rpc v0.0.0-20230530153820-e85fd2cbaebc/go.mod h1:66JfowdXAEgad5O9NnYcsNPLCPZJD++2L9X0PCMODrA= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= -google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc= -google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/ecs-api/main.go b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/ecs-api/main.go deleted file mode 100644 index b163af74..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/ecs-api/main.go +++ /dev/null @@ -1,242 +0,0 @@ -package main - -//#region imports and config - -import ( - "context" - "encoding/json" - "fmt" - "io" - "net/http" - "os" - "os/signal" - "syscall" - - "github.com/aws/aws-sdk-go/aws/session" - "github.com/gorilla/mux" - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promhttp" - "github.com/rs/cors" - "go.uber.org/zap" - - "go.opentelemetry.io/contrib/instrumentation/github.com/gorilla/mux/otelmux" - "go.opentelemetry.io/contrib/propagators/aws/xray" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" - "go.opentelemetry.io/otel/sdk/resource" - sdktrace "go.opentelemetry.io/otel/sdk/trace" - semconv "go.opentelemetry.io/otel/semconv/v1.4.0" - "go.opentelemetry.io/otel/trace" -) - -type Config struct { - dataAPI string - dataAPIRegion string - exporterEndpoint string - sess *session.Session - log *zap.SugaredLogger -} -type ColorRequest struct { - Color string `json:"color"` -} - -type ColorResponse struct { - Color string `json:"color,omitempty"` - Source string `json:"source,omitempty"` - Location string `json:"location,omitempty"` - Count int `json:"count,omitempty"` -} - -func newConfig(log *zap.SugaredLogger) *Config { - // Need AWS session to sign queries to Lambda Function URLs - sess := session.Must(session.NewSessionWithOptions(session.Options{ - SharedConfigState: session.SharedConfigEnable, - })) - - hostIP := os.Getenv("HOST_IP") - if hostIP == "" { - hostIP = "localhost" // setting default endpoint for exporter - } - endpoint := fmt.Sprintf("%s:4317", hostIP) - - dataAPI := os.Getenv("DATA_API") - if dataAPI == "" { - log.Fatal("Failed to get DATA_API") - } - - dataAPIRegion := os.Getenv("DATA_API_REGION") - if dataAPIRegion == "" { - log.Fatal("Failed to get DATA_API_REGION") - } - - return &Config{ - sess: sess, - log: log, - dataAPI: dataAPI, - dataAPIRegion: dataAPIRegion, - exporterEndpoint: endpoint, - } -} - -//#endregion imports and config - -func otelInit(ctx context.Context, cfg *Config) { - cfg.log.Info("Initializing OpenTelemetry") - // https://aws-otel.github.io/docs/getting-started/go-sdk/trace-manual-instr - - // OpenTelemetry Go requires an exporter to send traces to a backend - // Exporters allow telemetry data to be transferred either to the ADOT Collector, - // or to a remote system or console for further analysis - traceExporter, err := otlptracegrpc.New( - ctx, - otlptracegrpc.WithInsecure(), - otlptracegrpc.WithEndpoint(cfg.exporterEndpoint), - ) - cfg.log.Info("Initialized trace exporter") - logError(err, "failed to create new OTLP trace exporter", cfg.log, nil) - - // service name used to display traces in backends - svcNameResource := resource.NewWithAttributes( - semconv.SchemaURL, - semconv.ServiceNameKey.String(xrayServiceName), - ) - - // In order to generate traces, OpenTelemetry Go SDK requires a tracer provider to be created - // with an ID Generator that will generate trace IDs that conform to AWS X-Ray’s format - idg := xray.NewIDGenerator() - tp := sdktrace.NewTracerProvider( - sdktrace.WithSampler(sdktrace.AlwaysSample()), - sdktrace.WithBatcher(traceExporter), - sdktrace.WithIDGenerator(idg), - sdktrace.WithResource(svcNameResource), - ) - - // In addition to setting a global tracer provider, we will also configure the context propagation option. - // Context propagation refers to sharing data across multiple processes or services. - // Propagator structs are configured inside Tracer structs to support context propagation across process boundaries. - // A context will often have information identifying the current span and trace, and can contain arbitrary information as key-value pairs. - otel.SetTracerProvider(tp) - otel.SetTextMapPropagator(xray.Propagator{}) -} - -var ( - votes = prometheus.NewCounterVec(prometheus.CounterOpts{ - Name: "cop301_votes_total", - Help: "Number of votes", - }, []string{"color"}) -) - -const xrayServiceName = "cop301-api" - -func init() { - prometheus.MustRegister(votes) -} - -func main() { - //#region main - ctx := context.Background() - - logger, _ := zap.NewProduction() - defer logger.Sync() - log := logger.Sugar() - - cfg := newConfig(log) - otelInit(ctx, cfg) - - //#endregion main - // HTTP server router - r := mux.NewRouter() - - // Use open telementry instrumentation provided by the Gorilla mux framework - r.Use(otelmux.Middleware(xrayServiceName)) - - // Serve Prometheus metrics - r.Methods("GET").Path("/metrics").Handler(promhttp.Handler()) - - //#region handlers and server - r.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { - log.Info("health check request") - json.NewEncoder(w).Encode(map[string]bool{"ok": true}) - }) - - r.Methods("POST").Path("/votes").Handler(votesHandler(cfg)) - - errs := make(chan error) - go func() { - c := make(chan os.Signal) - signal.Notify(c, syscall.SIGINT, syscall.SIGTERM) - errs <- fmt.Errorf("%s", <-c) - }() - - c := cors.New(cors.Options{ - AllowedOrigins: []string{"*"}, - AllowCredentials: true, - }) - handler := c.Handler(r) - - go func() { - log.Info("Starting server on :8000 ...") - errs <- http.ListenAndServe(":8000", handler) - }() - log.Warn("exit", <-errs) - //#endregion handlers and server -} - -func votesHandler(cfg *Config) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - - // Context propagates the X-Ray trace ID - ctx := r.Context() - - // get the current span from the context - span := trace.SpanFromContext(ctx) - - resp, err := signedQuery(ctx, cfg, "POST", cfg.dataAPI, r.Body) - - //#region err management - if err != nil { - logError(err, "query error", cfg.log, span) - encodeError(ctx, err, w) - return - } - //#endregion err management - cfg.log.Infow("response", - "statuscode", resp.StatusCode, - "status", resp.Status, - "xrayTraceID", getXrayTraceID(span), - ) - //#region http response handling - var colorResponse ColorResponse - if err = json.NewDecoder(resp.Body).Decode(&colorResponse); err != nil { - cfg.log.Fatalw("decode failed", - "error", err, - "xrayTraceID", getXrayTraceID(span), - ) - encodeError(ctx, err, w) - return - } - cfg.log.Info(colorResponse) - //#endregion http response handling - - labelValues := []string{colorResponse.Color} - votes.WithLabelValues(labelValues...).Inc() - - locationRes, err := query(ctx, cfg, "GET", "https://checkip.amazonaws.com/", nil) - //#region http response handling - if err != nil { - logError(err, "query error", cfg.log, span) - encodeError(ctx, err, w) - return - } - defer locationRes.Body.Close() - b, err := io.ReadAll(resp.Body) - bodyString := string(b) - cfg.log.Infow("response body", - "err", err, - "b", bodyString, - "xrayTraceID", getXrayTraceID(span), - ) - encodeResponse(ctx, w, colorResponse) - //#endregion http response handling - }) -} diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/ecs-api/utils.go b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/ecs-api/utils.go deleted file mode 100644 index e1381945..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/ecs-api/utils.go +++ /dev/null @@ -1,140 +0,0 @@ -package main - -//region imports and http error handling - -import ( - "context" - "encoding/json" - "errors" - "fmt" - "io" - "net/http" - "strings" - "time" - - "github.com/aws/aws-sdk-go/aws/credentials" - v4 "github.com/aws/aws-sdk-go/aws/signer/v4" - "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" - "go.opentelemetry.io/otel/trace" - "go.uber.org/zap" -) - -var ( - ErrNotFound = errors.New("not found") - ErrBadRequest = errors.New("bad request parameters") -) - -type errorer interface { - error() error -} - -func encodeResponse(ctx context.Context, w http.ResponseWriter, response interface{}) error { - if e, ok := response.(errorer); ok && e.error() != nil { - encodeError(ctx, e.error(), w) - return nil - } - w.Header().Set("Content-Type", "application/json; charset=utf-8") - return json.NewEncoder(w).Encode(response) -} - -func encodeError(_ context.Context, err error, w http.ResponseWriter) { - if err == nil { - panic("encodeError with nil error") - } - w.Header().Set("Content-Type", "application/json; charset=utf-8") - w.WriteHeader(codeFrom(err)) - json.NewEncoder(w).Encode(map[string]interface{}{ - "error": err.Error(), - }) -} - -func codeFrom(err error) int { - switch err { - case ErrNotFound: - return http.StatusNotFound - case ErrBadRequest: - return http.StatusBadRequest - default: - return http.StatusInternalServerError - } -} - -func logError(err error, message string, log *zap.SugaredLogger, span trace.Span) { - if err != nil { - if span == nil { - log.Errorw(message, - "err", err, - ) - } else { - log.Errorw(message, - "err", err, - "xrayTraceID", getXrayTraceID(span), - ) - } - } -} - -type defaultTransportRT struct { - log *zap.SugaredLogger -} - -func (t *defaultTransportRT) RoundTrip(req *http.Request) (*http.Response, error) { - transport := http.DefaultTransport - - t.log.Debugw("", - "request headers", req.Header, - ) - t.log.Debugw("", - "context", req.Context(), - ) - - resp, err := transport.RoundTrip(req) - return resp, err -} - -//#endregion imports and http error handling - -func query(ctx context.Context, cfg *Config, method string, url string, data io.ReadCloser) (*http.Response, error) { - request, err := http.NewRequestWithContext(ctx, method, url, data) - logError(err, "failed creating request", cfg.log, nil) - - client := http.Client{ - Transport: otelhttp.NewTransport(&defaultTransportRT{cfg.log}), - Timeout: 5 * time.Second, - } - return client.Do(request) -} - -func signedQuery(ctx context.Context, cfg *Config, method string, url string, data io.ReadCloser) (*http.Response, error) { - //#region signed query - request, err := http.NewRequestWithContext(ctx, method, url, data) - logError(err, "failed creating request", cfg.log, nil) - - // need fresh credentials as AWS session token can expired - credsValue, err := cfg.sess.Config.Credentials.Get() - logError(err, "failed creating session", cfg.log, nil) - - credentials := credentials.NewStaticCredentialsFromCreds(credsValue) - signer := v4.NewSigner(credentials) - - b, _ := io.ReadAll(data) - cfg.log.Debug(string(b)) - - signer.Sign(request, strings.NewReader(string(b)), "lambda", cfg.dataAPIRegion, time.Now()) - //#endregion signed query - client := http.Client{ - Transport: otelhttp.NewTransport(&defaultTransportRT{cfg.log}), - Timeout: 5 * time.Second, - } - return client.Do(request) - -} - -func getXrayTraceID(span trace.Span) string { - if span == nil { - return "" - } - xrayTraceID := span.SpanContext().TraceID().String() - result := fmt.Sprintf("1-%s-%s", xrayTraceID[0:8], xrayTraceID[8:]) - return result -} diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/ecs-api/utils/benchmark.yaml b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/ecs-api/utils/benchmark.yaml deleted file mode 100644 index 2c1febb8..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/ecs-api/utils/benchmark.yaml +++ /dev/null @@ -1,30 +0,0 @@ ---- -concurrency: 60 -base: "https://dopi6gkpn7o94.cloudfront.net" -iterations: 200 -rampup: 4 - -plan: - - name: Vote {{ item.color }} - shuffle: true - request: - url: /votes - method: POST - body: '{"color":"{{ item.color }}"}' - headers: - Content-Type: "application/json" - with_items: - - { color: "teal" } - - { color: "black" } - - { color: "orange" } - - { color: "red orange" } - - { color: "pink" } - - { color: "purple" } - - { color: "deep purple" } - - { color: "indigo" } - - { color: "blue" } - - { color: "cyan" } - - { color: "teal" } - - { color: "yellow" } - - { color: "orange" } - - { color: "black" } diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/lambda-api/README.md b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/lambda-api/README.md deleted file mode 100644 index 3722620d..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/lambda-api/README.md +++ /dev/null @@ -1,127 +0,0 @@ -# data - -This project contains source code and supporting files for a serverless application that you can deploy with the SAM CLI. It includes the following files and folders. - -- hello-world - Code for the application's Lambda function. -- events - Invocation events that you can use to invoke the function. -- hello-world/tests - Unit tests for the application code. -- template.yaml - A template that defines the application's AWS resources. - -The application uses several AWS resources, including Lambda functions and an API Gateway API. These resources are defined in the `template.yaml` file in this project. You can update the template to add AWS resources through the same deployment process that updates your application code. - -If you prefer to use an integrated development environment (IDE) to build and test your application, you can use the AWS Toolkit. -The AWS Toolkit is an open source plug-in for popular IDEs that uses the SAM CLI to build and deploy serverless applications on AWS. The AWS Toolkit also adds a simplified step-through debugging experience for Lambda function code. See the following links to get started. - -* [CLion](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) -* [GoLand](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) -* [IntelliJ](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) -* [WebStorm](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) -* [Rider](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) -* [PhpStorm](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) -* [PyCharm](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) -* [RubyMine](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) -* [DataGrip](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html) -* [VS Code](https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/welcome.html) -* [Visual Studio](https://docs.aws.amazon.com/toolkit-for-visual-studio/latest/user-guide/welcome.html) - -## Deploy the sample application - -The Serverless Application Model Command Line Interface (SAM CLI) is an extension of the AWS CLI that adds functionality for building and testing Lambda applications. It uses Docker to run your functions in an Amazon Linux environment that matches Lambda. It can also emulate your application's build environment and API. - -To use the SAM CLI, you need the following tools. - -* SAM CLI - [Install the SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html) -* Node.js - [Install Node.js 16](https://nodejs.org/en/), including the NPM package management tool. -* Docker - [Install Docker community edition](https://hub.docker.com/search/?type=edition&offering=community) - -To build and deploy your application for the first time, run the following in your shell: - -```bash -sam build -sam deploy --guided -``` - -The first command will build the source of your application. The second command will package and deploy your application to AWS, with a series of prompts: - -* **Stack Name**: The name of the stack to deploy to CloudFormation. This should be unique to your account and region, and a good starting point would be something matching your project name. -* **AWS Region**: The AWS region you want to deploy your app to. -* **Confirm changes before deploy**: If set to yes, any change sets will be shown to you before execution for manual review. If set to no, the AWS SAM CLI will automatically deploy application changes. -* **Allow SAM CLI IAM role creation**: Many AWS SAM templates, including this example, create AWS IAM roles required for the AWS Lambda function(s) included to access AWS services. By default, these are scoped down to minimum required permissions. To deploy an AWS CloudFormation stack which creates or modifies IAM roles, the `CAPABILITY_IAM` value for `capabilities` must be provided. If permission isn't provided through this prompt, to deploy this example you must explicitly pass `--capabilities CAPABILITY_IAM` to the `sam deploy` command. -* **Save arguments to samconfig.toml**: If set to yes, your choices will be saved to a configuration file inside the project, so that in the future you can just re-run `sam deploy` without parameters to deploy changes to your application. - -You can find your API Gateway Endpoint URL in the output values displayed after deployment. - -## Use the SAM CLI to build and test locally - -Build your application with the `sam build` command. - -```bash -data$ sam build -``` - -The SAM CLI installs dependencies defined in `hello-world/package.json`, creates a deployment package, and saves it in the `.aws-sam/build` folder. - -Test a single function by invoking it directly with a test event. An event is a JSON document that represents the input that the function receives from the event source. Test events are included in the `events` folder in this project. - -Run functions locally and invoke them with the `sam local invoke` command. - -```bash -data$ sam local invoke HelloWorldFunction --event events/event.json -``` - -The SAM CLI can also emulate your application's API. Use the `sam local start-api` to run the API locally on port 3000. - -```bash -data$ sam local start-api -data$ curl http://localhost:3000/ -``` - -The SAM CLI reads the application template to determine the API's routes and the functions that they invoke. The `Events` property on each function's definition includes the route and method for each path. - -```yaml - Events: - HelloWorld: - Type: Api - Properties: - Path: /hello - Method: get -``` - -## Add a resource to your application -The application template uses AWS Serverless Application Model (AWS SAM) to define application resources. AWS SAM is an extension of AWS CloudFormation with a simpler syntax for configuring common serverless application resources such as functions, triggers, and APIs. For resources not included in [the SAM specification](https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md), you can use standard [AWS CloudFormation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html) resource types. - -## Fetch, tail, and filter Lambda function logs - -To simplify troubleshooting, SAM CLI has a command called `sam logs`. `sam logs` lets you fetch logs generated by your deployed Lambda function from the command line. In addition to printing the logs on the terminal, this command has several nifty features to help you quickly find the bug. - -`NOTE`: This command works for all AWS Lambda functions; not just the ones you deploy using SAM. - -```bash -data$ sam logs -n HelloWorldFunction --stack-name data --tail -``` - -You can find more information and examples about filtering Lambda function logs in the [SAM CLI Documentation](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-logging.html). - -## Unit tests - -Tests are defined in the `hello-world/tests` folder in this project. Use NPM to install the [Mocha test framework](https://mochajs.org/) and run unit tests. - -```bash -data$ cd hello-world -hello-world$ npm install -hello-world$ npm run test -``` - -## Cleanup - -To delete the sample application that you created, use the AWS CLI. Assuming you used your project name for the stack name, you can run the following: - -```bash -aws cloudformation delete-stack --stack-name data -``` - -## Resources - -See the [AWS SAM developer guide](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html) for an introduction to SAM specification, the SAM CLI, and serverless application concepts. - -Next, you can use AWS Serverless Application Repository to deploy ready to use Apps that go beyond hello world samples and learn how authors developed their applications: [AWS Serverless Application Repository main page](https://aws.amazon.com/serverless/serverlessrepo/) diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/lambda-api/dequeue/.npmignore b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/lambda-api/dequeue/.npmignore deleted file mode 100644 index e7e1fb04..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/lambda-api/dequeue/.npmignore +++ /dev/null @@ -1 +0,0 @@ -tests/* diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/lambda-api/dequeue/dequeue.js b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/lambda-api/dequeue/dequeue.js deleted file mode 100644 index 27155b2c..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/lambda-api/dequeue/dequeue.js +++ /dev/null @@ -1,60 +0,0 @@ -const AWSXRay = require('aws-xray-sdk-core') -const AWS = AWSXRay.captureAWS(require('aws-sdk')) - -const dynamodb = new AWS.DynamoDB() -const tableName = process.env.TABLE_NAME - -/** - * Lambda Function URL - * https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html - */ -exports.lambdaHandler = async (event, context) => { - - let votes = {} - - event.Records.forEach(record => { - const { body } = record; - const vote = JSON.parse(body) - if (votes[vote.color] === undefined) { - votes[vote.color] = 1 - } else { - votes[vote.color] += 1 - } - }); - - console.log(votes) - - return this.updateVotesDDB(votes) -}; - -exports.updateVotesDDB = async (votes) => { - - const colors = Object.keys(votes) - console.log(colors) - - const bulkUpdatePromises = colors.map(async (c) => { - const count = `${votes[c]}` - console.log(count) - await dynamodb.updateItem({ - TableName: tableName, - Key: { - "color": { - S: c, - } - }, - UpdateExpression: "ADD votes :inc", - ExpressionAttributeValues: { - ":inc": { N: count } - }, - ReturnValues: "ALL_NEW" - }, - (err, data) => { - if (err) throw err - else console.log(JSON.stringify(data)) - } - ).promise() - }); - - return await Promise.all(bulkUpdatePromises); -} - diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/lambda-api/dequeue/package.json b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/lambda-api/dequeue/package.json deleted file mode 100644 index 17468933..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/lambda-api/dequeue/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "cop301_dequeue", - "version": "1.0.0", - "description": "reInvent cop301 demo", - "main": "dequeue.js", - "repository": "https://github.com/awslabs/aws-sam-cli/tree/develop/samcli/local/init/templates/cookiecutter-aws-sam-hello-nodejs", - "author": "SAM CLI", - "license": "MIT", - "dependencies": { - "axios": ">=0.21.1", - "aws-sdk": "^2.1247.0", - "aws-xray-sdk-core": "^3.3.8" - }, - "scripts": { - "test": "mocha tests/unit/" - }, - "devDependencies": { - "chai": "^4.2.0", - "mocha": "^9.1.4" - } -} \ No newline at end of file diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/lambda-api/dequeue/unit/test-handler.js b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/lambda-api/dequeue/unit/test-handler.js deleted file mode 100644 index ae94b9f2..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/lambda-api/dequeue/unit/test-handler.js +++ /dev/null @@ -1,22 +0,0 @@ -'use strict'; - -const app = require('../../app.js'); -const chai = require('chai'); -const expect = chai.expect; -var event, context; - -describe('Tests index', function () { - it('verifies successful response', async () => { - const result = await app.lambdaHandler(event, context) - - expect(result).to.be.an('object'); - expect(result.statusCode).to.equal(200); - expect(result.body).to.be.an('string'); - - let response = JSON.parse(result.body); - - expect(response).to.be.an('object'); - expect(response.message).to.be.equal("hello world"); - // expect(response.location).to.be.an("string"); - }); -}); diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/lambda-api/events/event.json b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/lambda-api/events/event.json deleted file mode 100644 index 070ad8e0..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/lambda-api/events/event.json +++ /dev/null @@ -1,62 +0,0 @@ -{ - "body": "{\"message\": \"hello world\"}", - "resource": "/{proxy+}", - "path": "/path/to/resource", - "httpMethod": "POST", - "isBase64Encoded": false, - "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.us-east-1.amazonaws.com", - "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", - "requestTime": "09/Apr/2015:12:34:56 +0000", - "requestTimeEpoch": 1428582896000, - "identity": { - "cognitoIdentityPoolId": null, - "accountId": null, - "cognitoIdentityId": null, - "caller": null, - "accessKey": null, - "sourceIp": "127.0.0.1", - "cognitoAuthenticationType": null, - "cognitoAuthenticationProvider": null, - "userArn": null, - "userAgent": "Custom User Agent String", - "user": null - }, - "path": "/prod/path/to/resource", - "resourcePath": "/{proxy+}", - "httpMethod": "POST", - "apiId": "1234567890", - "protocol": "HTTP/1.1" - } -} diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/lambda-api/function/.npmignore b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/lambda-api/function/.npmignore deleted file mode 100644 index e7e1fb04..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/lambda-api/function/.npmignore +++ /dev/null @@ -1 +0,0 @@ -tests/* diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/lambda-api/function/app.js b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/lambda-api/function/app.js deleted file mode 100644 index 579b1642..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/lambda-api/function/app.js +++ /dev/null @@ -1,66 +0,0 @@ -const AWSXRay = require('aws-xray-sdk-core') -const AWS = AWSXRay.captureAWS(require('aws-sdk')) - -const sqs = new AWS.SQS() -const queueURL = process.env.QUEUE_URL -let response; - -/** - * Lambda Function URL - * https://docs.aws.amazon.com/lambda/latest/dg/urls-invocation.html - */ -exports.lambdaHandler = async (event, context) => { - - console.info(event) - //let body = Buffer.from(event.body, 'base64') - let body = atob(event.body) - //let parsedVote = JSON.parse(body) - - console.log( - event.requestContext.http.method, - event.requestContext.http.path, - event.requestContext.http.protocol, - event.requestContext.http.userAgent, - event.requestContext.http.sourceIp, - body - ) - - if (event.requestContext.http.method == "POST" && event.requestContext.http.path == "/votes") { - - return this.enqueueVote(body, event.requestContext.http.sourceIp) - } - - return { statusCode: 404 } -}; - -exports.enqueueVote = async (body, sourceIP) => { - const params = { - MessageBody: body, - QueueUrl: queueURL, - } - - let parsedVote = JSON.parse(body) - - return new Promise((resolve, reject) => { - sqs.sendMessage(params, (err, data) => { - if (err) - throw err - else { - console.info(data) - response = { - statusCode: 200, - headers: { - "Access-Control-Allow-Headers": "Content-Type", - "Access-Control-Allow-Origin": "*", - "Access-Control-Allow-Methods": "OPTIONS,POST,GET" - }, - body: { - color: parsedVote.color.trim(), - source: sourceIP - } - } - resolve(response) - } - }).promise() - }) -} \ No newline at end of file diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/lambda-api/function/package.json b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/lambda-api/function/package.json deleted file mode 100644 index 691a806c..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/lambda-api/function/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "cop301_data", - "version": "1.0.0", - "description": "reInvent cop301 demo", - "main": "app.js", - "repository": "https://github.com/awslabs/aws-sam-cli/tree/develop/samcli/local/init/templates/cookiecutter-aws-sam-hello-nodejs", - "author": "SAM CLI", - "license": "MIT", - "dependencies": { - "axios": ">=0.21.1", - "aws-sdk": "^2.1247.0", - "aws-xray-sdk-core": "^3.3.8" - }, - "scripts": { - "test": "mocha tests/unit/" - }, - "devDependencies": { - "chai": "^4.2.0", - "mocha": "^9.1.4" - } -} \ No newline at end of file diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/lambda-api/function/tests/unit/test-handler.js b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/lambda-api/function/tests/unit/test-handler.js deleted file mode 100644 index ae94b9f2..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/lambda-api/function/tests/unit/test-handler.js +++ /dev/null @@ -1,22 +0,0 @@ -'use strict'; - -const app = require('../../app.js'); -const chai = require('chai'); -const expect = chai.expect; -var event, context; - -describe('Tests index', function () { - it('verifies successful response', async () => { - const result = await app.lambdaHandler(event, context) - - expect(result).to.be.an('object'); - expect(result.statusCode).to.equal(200); - expect(result.body).to.be.an('string'); - - let response = JSON.parse(result.body); - - expect(response).to.be.an('object'); - expect(response.message).to.be.equal("hello world"); - // expect(response.location).to.be.an("string"); - }); -}); diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/lambda-api/package.json b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/lambda-api/package.json deleted file mode 100644 index 3009bbe7..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/lambda-api/package.json +++ /dev/null @@ -1,6 +0,0 @@ -{ - "dependencies": { - "aws-sdk": "^2.1247.0", - "aws-xray-sdk-core": "^3.3.8" - } -} diff --git a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/lambda-api/template.yaml b/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/lambda-api/template.yaml deleted file mode 100644 index 138fc59f..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/event-demos/riv-cop301-2022/lambda-api/template.yaml +++ /dev/null @@ -1,90 +0,0 @@ -AWSTemplateFormatVersion: "2010-09-09" -Transform: AWS::Serverless-2016-10-31 -Description: > - data - - Sample SAM Template for data api - -# More info about Globals: https://github.com/awslabs/serverless-application-model/blob/master/docs/globals.rst -Globals: - Function: - Timeout: 3 - Tracing: Active - Api: - TracingEnabled: True - -Parameters: - Project: - Type: String - Default: cop301-voting - -Resources: - VotesQueue: - Type: AWS::SQS::Queue - DataPollerFunction: - Type: "AWS::Serverless::Function" - Properties: - CodeUri: dequeue/ - Handler: dequeue.lambdaHandler - Runtime: nodejs16.x - Environment: - Variables: - TABLE_NAME: - Ref: votingTable - Policies: - - SQSPollerPolicy: - QueueName: !GetAtt VotesQueue.QueueName - - DynamoDBCrudPolicy: - TableName: - Ref: votingTable - Events: - MySQSEvent: - Type: SQS - Properties: - Queue: !GetAtt VotesQueue.Arn - BatchSize: 10 - DataStoreFunction: - Type: AWS::Serverless::Function - Properties: - CodeUri: function/ - Handler: app.lambdaHandler - Runtime: nodejs16.x - MemorySize: 512 - Environment: - Variables: - QUEUE_URL: - Ref: VotesQueue - Policies: - - SQSSendMessagePolicy: - QueueName: !GetAtt VotesQueue.QueueName - Architectures: - - x86_64 - votingTable: - Type: "AWS::DynamoDB::Table" - Properties: - TableName: - "Fn::Sub": "${Project}-table" - AttributeDefinitions: - - AttributeName: color - AttributeType: S - KeySchema: - - AttributeName: color - KeyType: HASH - StreamSpecification: - StreamViewType: NEW_IMAGE - BillingMode: PAY_PER_REQUEST - -Outputs: - # ServerlessRestApi is an implicit API created out of Events key under Serverless::Function - # Find out more about other implicit resources you can reference within SAM - # https://github.com/awslabs/serverless-application-model/blob/master/docs/internals/generated_resources.rst#api - #StoreVoteAPI: - #Description: "Lambda Function URL for POST /votes" - #Value: !Sub "https://${DataStoreFunction.FunctionUrl}" - #Value: !Sub "https://${RestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/vote/" - DataStoreFunction: - Description: "Lambda Function ARN" - Value: !GetAtt DataStoreFunction.Arn - DataStoreFunctionIamRole: - Description: "Implicit IAM Role created" - Value: !GetAtt DataStoreFunction.Arn diff --git a/PetAdoptions/cdk/pet_stack/resources/k8s_petsite/deployment.yaml b/PetAdoptions/cdk/pet_stack/resources/k8s_petsite/deployment.yaml deleted file mode 100644 index 1d73a392..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/k8s_petsite/deployment.yaml +++ /dev/null @@ -1,63 +0,0 @@ ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - annotations: - eks.amazonaws.com/role-arn: "{{PETSITE_SA_ROLE}}" - name: petsite-sa - namespace: default ---- -apiVersion: v1 -kind: Service -metadata: - name: service-petsite - namespace: default - annotations: - scrape: "true" - prometheus.io/scrape: "true" -spec: - ports: - - port: 80 - nodePort: 30300 - targetPort: 80 - protocol: TCP - type: NodePort - selector: - app: petsite ---- -apiVersion: apps/v1 -kind: Deployment -metadata: - name: petsite-deployment - namespace: default -spec: - selector: - matchLabels: - app: petsite - replicas: 2 - template: - metadata: - labels: - app: petsite - annotations: - instrumentation.opentelemetry.io/inject-dotnet: "true" - spec: - serviceAccountName: petsite-sa - containers: - - image: "{{ECR_IMAGE_URL}}" - imagePullPolicy: Always - name: petsite - ports: - - containerPort: 80 - protocol: TCP ---- -apiVersion: elbv2.k8s.aws/v1beta1 -kind: TargetGroupBinding -metadata: - name: petsite-tgb -spec: - serviceRef: - name: service-petsite - port: 80 - targetGroupARN: "" - targetType: ip diff --git a/PetAdoptions/cdk/pet_stack/resources/load_balancer/crds.yaml b/PetAdoptions/cdk/pet_stack/resources/load_balancer/crds.yaml deleted file mode 100644 index 37e64197..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/load_balancer/crds.yaml +++ /dev/null @@ -1,474 +0,0 @@ -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.5.0 - creationTimestamp: null - name: ingressclassparams.elbv2.k8s.aws -spec: - group: elbv2.k8s.aws - names: - kind: IngressClassParams - listKind: IngressClassParamsList - plural: ingressclassparams - singular: ingressclassparams - scope: Cluster - versions: - - additionalPrinterColumns: - - description: The Ingress Group name - jsonPath: .spec.group.name - name: GROUP-NAME - type: string - - description: The AWS Load Balancer scheme - jsonPath: .spec.scheme - name: SCHEME - type: string - - description: The AWS Load Balancer ipAddressType - jsonPath: .spec.ipAddressType - name: IP-ADDRESS-TYPE - type: string - - jsonPath: .metadata.creationTimestamp - name: AGE - type: date - name: v1beta1 - schema: - openAPIV3Schema: - description: IngressClassParams is the Schema for the IngressClassParams API - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: IngressClassParamsSpec defines the desired state of IngressClassParams - properties: - group: - description: Group defines the IngressGroup for all Ingresses that belong to IngressClass with this IngressClassParams. - properties: - name: - description: Name is the name of IngressGroup. - type: string - required: - - name - type: object - ipAddressType: - description: IPAddressType defines the ip address type for all Ingresses that belong to IngressClass with this IngressClassParams. - enum: - - ipv4 - - dualstack - type: string - loadBalancerAttributes: - description: LoadBalancerAttributes define the custom attributes to LoadBalancers for all Ingress that that belong to IngressClass with this IngressClassParams. - items: - description: Attributes defines custom attributes on resources. - properties: - key: - description: The key of the attribute. - type: string - value: - description: The value of the attribute. - type: string - required: - - key - - value - type: object - type: array - namespaceSelector: - description: NamespaceSelector restrict the namespaces of Ingresses that are allowed to specify the IngressClass with this IngressClassParams. * if absent or present but empty, it selects all namespaces. - properties: - matchExpressions: - description: matchExpressions is a list of label selector requirements. The requirements are ANDed. - items: - description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. - properties: - key: - description: key is the label key that the selector applies to. - type: string - operator: - description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - type: object - scheme: - description: Scheme defines the scheme for all Ingresses that belong to IngressClass with this IngressClassParams. - enum: - - internal - - internet-facing - type: string - tags: - description: Tags defines list of Tags on AWS resources provisioned for Ingresses that belong to IngressClass with this IngressClassParams. - items: - description: Tag defines a AWS Tag on resources. - properties: - key: - description: The key of the tag. - type: string - value: - description: The value of the tag. - type: string - required: - - key - - value - type: object - type: array - type: object - type: object - served: true - storage: true - subresources: {} -status: - acceptedNames: - kind: "" - plural: "" - conditions: [] - storedVersions: [] ---- -apiVersion: apiextensions.k8s.io/v1 -kind: CustomResourceDefinition -metadata: - annotations: - controller-gen.kubebuilder.io/version: v0.5.0 - creationTimestamp: null - name: targetgroupbindings.elbv2.k8s.aws -spec: - group: elbv2.k8s.aws - names: - kind: TargetGroupBinding - listKind: TargetGroupBindingList - plural: targetgroupbindings - singular: targetgroupbinding - scope: Namespaced - versions: - - additionalPrinterColumns: - - description: The Kubernetes Service's name - jsonPath: .spec.serviceRef.name - name: SERVICE-NAME - type: string - - description: The Kubernetes Service's port - jsonPath: .spec.serviceRef.port - name: SERVICE-PORT - type: string - - description: The AWS TargetGroup's TargetType - jsonPath: .spec.targetType - name: TARGET-TYPE - type: string - - description: The AWS TargetGroup's Amazon Resource Name - jsonPath: .spec.targetGroupARN - name: ARN - priority: 1 - type: string - - jsonPath: .metadata.creationTimestamp - name: AGE - type: date - name: v1alpha1 - schema: - openAPIV3Schema: - description: TargetGroupBinding is the Schema for the TargetGroupBinding API - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: TargetGroupBindingSpec defines the desired state of TargetGroupBinding - properties: - networking: - description: networking provides the networking setup for ELBV2 LoadBalancer to access targets in TargetGroup. - properties: - ingress: - description: List of ingress rules to allow ELBV2 LoadBalancer to access targets in TargetGroup. - items: - properties: - from: - description: List of peers which should be able to access the targets in TargetGroup. At least one NetworkingPeer should be specified. - items: - description: NetworkingPeer defines the source/destination peer for networking rules. - properties: - ipBlock: - description: IPBlock defines an IPBlock peer. If specified, none of the other fields can be set. - properties: - cidr: - description: CIDR is the network CIDR. Both IPV4 or IPV6 CIDR are accepted. - type: string - required: - - cidr - type: object - securityGroup: - description: SecurityGroup defines a SecurityGroup peer. If specified, none of the other fields can be set. - properties: - groupID: - description: GroupID is the EC2 SecurityGroupID. - type: string - required: - - groupID - type: object - type: object - type: array - ports: - description: List of ports which should be made accessible on the targets in TargetGroup. If ports is empty or unspecified, it defaults to all ports with TCP. - items: - properties: - port: - anyOf: - - type: integer - - type: string - description: The port which traffic must match. When NodePort endpoints(instance TargetType) is used, this must be a numerical port. When Port endpoints(ip TargetType) is used, this can be either numerical or named port on pods. if port is unspecified, it defaults to all ports. - x-kubernetes-int-or-string: true - protocol: - description: The protocol which traffic must match. If protocol is unspecified, it defaults to TCP. - enum: - - TCP - - UDP - type: string - type: object - type: array - required: - - from - - ports - type: object - type: array - type: object - serviceRef: - description: serviceRef is a reference to a Kubernetes Service and ServicePort. - properties: - name: - description: Name is the name of the Service. - type: string - port: - anyOf: - - type: integer - - type: string - description: Port is the port of the ServicePort. - x-kubernetes-int-or-string: true - required: - - name - - port - type: object - targetGroupARN: - description: targetGroupARN is the Amazon Resource Name (ARN) for the TargetGroup. - type: string - targetType: - description: targetType is the TargetType of TargetGroup. If unspecified, it will be automatically inferred. - enum: - - instance - - ip - type: string - required: - - serviceRef - - targetGroupARN - type: object - status: - description: TargetGroupBindingStatus defines the observed state of TargetGroupBinding - properties: - observedGeneration: - description: The generation observed by the TargetGroupBinding controller. - format: int64 - type: integer - type: object - type: object - served: true - storage: false - subresources: - status: {} - - additionalPrinterColumns: - - description: The Kubernetes Service's name - jsonPath: .spec.serviceRef.name - name: SERVICE-NAME - type: string - - description: The Kubernetes Service's port - jsonPath: .spec.serviceRef.port - name: SERVICE-PORT - type: string - - description: The AWS TargetGroup's TargetType - jsonPath: .spec.targetType - name: TARGET-TYPE - type: string - - description: The AWS TargetGroup's Amazon Resource Name - jsonPath: .spec.targetGroupARN - name: ARN - priority: 1 - type: string - - jsonPath: .metadata.creationTimestamp - name: AGE - type: date - name: v1beta1 - schema: - openAPIV3Schema: - description: TargetGroupBinding is the Schema for the TargetGroupBinding API - properties: - apiVersion: - description: 'APIVersion defines the versioned schema of this representation of an object. Servers should convert recognized schemas to the latest internal value, and may reject unrecognized values. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#resources' - type: string - kind: - description: 'Kind is a string value representing the REST resource this object represents. Servers may infer this from the endpoint the client submits requests to. Cannot be updated. In CamelCase. More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds' - type: string - metadata: - type: object - spec: - description: TargetGroupBindingSpec defines the desired state of TargetGroupBinding - properties: - ipAddressType: - description: ipAddressType specifies whether the target group is of type IPv4 or IPv6. If unspecified, it will be automatically inferred. - enum: - - ipv4 - - ipv6 - type: string - networking: - description: networking defines the networking rules to allow ELBV2 LoadBalancer to access targets in TargetGroup. - properties: - ingress: - description: List of ingress rules to allow ELBV2 LoadBalancer to access targets in TargetGroup. - items: - description: NetworkingIngressRule defines a particular set of traffic that is allowed to access TargetGroup's targets. - properties: - from: - description: List of peers which should be able to access the targets in TargetGroup. At least one NetworkingPeer should be specified. - items: - description: NetworkingPeer defines the source/destination peer for networking rules. - properties: - ipBlock: - description: IPBlock defines an IPBlock peer. If specified, none of the other fields can be set. - properties: - cidr: - description: CIDR is the network CIDR. Both IPV4 or IPV6 CIDR are accepted. - type: string - required: - - cidr - type: object - securityGroup: - description: SecurityGroup defines a SecurityGroup peer. If specified, none of the other fields can be set. - properties: - groupID: - description: GroupID is the EC2 SecurityGroupID. - type: string - required: - - groupID - type: object - type: object - type: array - ports: - description: List of ports which should be made accessible on the targets in TargetGroup. If ports is empty or unspecified, it defaults to all ports with TCP. - items: - description: NetworkingPort defines the port and protocol for networking rules. - properties: - port: - anyOf: - - type: integer - - type: string - description: The port which traffic must match. When NodePort endpoints(instance TargetType) is used, this must be a numerical port. When Port endpoints(ip TargetType) is used, this can be either numerical or named port on pods. if port is unspecified, it defaults to all ports. - x-kubernetes-int-or-string: true - protocol: - description: The protocol which traffic must match. If protocol is unspecified, it defaults to TCP. - enum: - - TCP - - UDP - type: string - type: object - type: array - required: - - from - - ports - type: object - type: array - type: object - nodeSelector: - description: node selector for instance type target groups to only register certain nodes - properties: - matchExpressions: - description: matchExpressions is a list of label selector requirements. The requirements are ANDed. - items: - description: A label selector requirement is a selector that contains values, a key, and an operator that relates the key and values. - properties: - key: - description: key is the label key that the selector applies to. - type: string - operator: - description: operator represents a key's relationship to a set of values. Valid operators are In, NotIn, Exists and DoesNotExist. - type: string - values: - description: values is an array of string values. If the operator is In or NotIn, the values array must be non-empty. If the operator is Exists or DoesNotExist, the values array must be empty. This array is replaced during a strategic merge patch. - items: - type: string - type: array - required: - - key - - operator - type: object - type: array - matchLabels: - additionalProperties: - type: string - description: matchLabels is a map of {key,value} pairs. A single {key,value} in the matchLabels map is equivalent to an element of matchExpressions, whose key field is "key", the operator is "In", and the values array contains only "value". The requirements are ANDed. - type: object - type: object - serviceRef: - description: serviceRef is a reference to a Kubernetes Service and ServicePort. - properties: - name: - description: Name is the name of the Service. - type: string - port: - anyOf: - - type: integer - - type: string - description: Port is the port of the ServicePort. - x-kubernetes-int-or-string: true - required: - - name - - port - type: object - targetGroupARN: - description: targetGroupARN is the Amazon Resource Name (ARN) for the TargetGroup. - minLength: 1 - type: string - targetType: - description: targetType is the TargetType of TargetGroup. If unspecified, it will be automatically inferred. - enum: - - instance - - ip - type: string - required: - - serviceRef - - targetGroupARN - type: object - status: - description: TargetGroupBindingStatus defines the observed state of TargetGroupBinding - properties: - observedGeneration: - description: The generation observed by the TargetGroupBinding controller. - format: int64 - type: integer - type: object - type: object - served: true - storage: true - subresources: - status: {} -status: - acceptedNames: - kind: "" - plural: "" - conditions: [] - storedVersions: [] \ No newline at end of file diff --git a/PetAdoptions/cdk/pet_stack/resources/load_balancer/iam_policy.json b/PetAdoptions/cdk/pet_stack/resources/load_balancer/iam_policy.json deleted file mode 100644 index 19e09596..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/load_balancer/iam_policy.json +++ /dev/null @@ -1,228 +0,0 @@ -{ - "Version": "2012-10-17", - "Statement": [ - { - "Effect": "Allow", - "Action": [ - "iam:CreateServiceLinkedRole", - "ec2:DescribeAccountAttributes", - "ec2:DescribeAddresses", - "ec2:DescribeInternetGateways", - "ec2:DescribeVpcs", - "ec2:DescribeSubnets", - "ec2:DescribeSecurityGroups", - "ec2:DescribeInstances", - "ec2:DescribeNetworkInterfaces", - "ec2:DescribeTags", - "ec2:DescribeAvailabilityZones", - "elasticloadbalancing:DescribeLoadBalancers", - "elasticloadbalancing:DescribeLoadBalancerAttributes", - "elasticloadbalancing:DescribeListeners", - "elasticloadbalancing:DescribeListenerAttributes", - "elasticloadbalancing:DescribeListenerCertificates", - "elasticloadbalancing:DescribeSSLPolicies", - "elasticloadbalancing:DescribeRules", - "elasticloadbalancing:DescribeTargetGroups", - "elasticloadbalancing:DescribeTargetGroupAttributes", - "elasticloadbalancing:DescribeTargetHealth", - "elasticloadbalancing:DescribeTags" - ], - "Resource": "*" - }, - { - "Effect": "Allow", - "Action": [ - "cognito-idp:DescribeUserPoolClient", - "acm:ListCertificates", - "acm:DescribeCertificate", - "iam:ListServerCertificates", - "iam:GetServerCertificate", - "waf-regional:GetWebACL", - "waf-regional:GetWebACLForResource", - "waf-regional:AssociateWebACL", - "waf-regional:DisassociateWebACL", - "wafv2:GetWebACL", - "wafv2:GetWebACLForResource", - "wafv2:AssociateWebACL", - "wafv2:DisassociateWebACL", - "shield:GetSubscriptionState", - "shield:DescribeProtection", - "shield:CreateProtection", - "shield:DeleteProtection" - ], - "Resource": "*" - }, - { - "Effect": "Allow", - "Action": [ - "ec2:AuthorizeSecurityGroupIngress", - "ec2:RevokeSecurityGroupIngress" - ], - "Resource": "*" - }, - { - "Effect": "Allow", - "Action": [ - "ec2:CreateSecurityGroup" - ], - "Resource": "*" - }, - { - "Effect": "Allow", - "Action": [ - "ec2:CreateTags" - ], - "Resource": "arn:aws:ec2:*:*:security-group/*", - "Condition": { - "StringEquals": { - "ec2:CreateAction": "CreateSecurityGroup" - }, - "Null": { - "aws:RequestTag/elbv2.k8s.aws/cluster": "false" - } - } - }, - { - "Effect": "Allow", - "Action": [ - "ec2:CreateTags", - "ec2:DeleteTags" - ], - "Resource": "arn:aws:ec2:*:*:security-group/*", - "Condition": { - "Null": { - "aws:RequestTag/elbv2.k8s.aws/cluster": "true", - "aws:ResourceTag/elbv2.k8s.aws/cluster": "false" - } - } - }, - { - "Effect": "Allow", - "Action": [ - "ec2:AuthorizeSecurityGroupIngress", - "ec2:RevokeSecurityGroupIngress", - "ec2:DeleteSecurityGroup" - ], - "Resource": "*", - "Condition": { - "Null": { - "aws:ResourceTag/elbv2.k8s.aws/cluster": "false" - } - } - }, - { - "Effect": "Allow", - "Action": [ - "elasticloadbalancing:CreateLoadBalancer", - "elasticloadbalancing:CreateTargetGroup" - ], - "Resource": "*", - "Condition": { - "Null": { - "aws:RequestTag/elbv2.k8s.aws/cluster": "false" - } - } - }, - { - "Effect": "Allow", - "Action": [ - "elasticloadbalancing:CreateListener", - "elasticloadbalancing:DeleteListener", - "elasticloadbalancing:CreateRule", - "elasticloadbalancing:DeleteRule" - ], - "Resource": "*" - }, - { - "Effect": "Allow", - "Action": [ - "elasticloadbalancing:AddTags", - "elasticloadbalancing:RemoveTags" - ], - "Resource": [ - "arn:aws:elasticloadbalancing:*:*:targetgroup/*/*", - "arn:aws:elasticloadbalancing:*:*:loadbalancer/net/*/*", - "arn:aws:elasticloadbalancing:*:*:loadbalancer/app/*/*" - ], - "Condition": { - "Null": { - "aws:RequestTag/elbv2.k8s.aws/cluster": "true", - "aws:ResourceTag/elbv2.k8s.aws/cluster": "false" - } - } - }, - { - "Effect": "Allow", - "Action": [ - "elasticloadbalancing:AddTags", - "elasticloadbalancing:RemoveTags" - ], - "Resource": [ - "arn:aws:elasticloadbalancing:*:*:listener/net/*/*/*", - "arn:aws:elasticloadbalancing:*:*:listener/app/*/*/*", - "arn:aws:elasticloadbalancing:*:*:listener-rule/net/*/*/*", - "arn:aws:elasticloadbalancing:*:*:listener-rule/app/*/*/*" - ] - }, - { - "Effect": "Allow", - "Action": [ - "elasticloadbalancing:AddTags" - ], - "Resource": [ - "arn:aws:elasticloadbalancing:*:*:targetgroup/*/*", - "arn:aws:elasticloadbalancing:*:*:loadbalancer/net/*/*", - "arn:aws:elasticloadbalancing:*:*:loadbalancer/app/*/*" - ], - "Condition": { - "StringEquals": { - "elasticloadbalancing:CreateAction": [ - "CreateTargetGroup", - "CreateLoadBalancer" - ] - }, - "Null": { - "aws:RequestTag/elbv2.k8s.aws/cluster": "false" - } - } - }, - { - "Effect": "Allow", - "Action": [ - "elasticloadbalancing:ModifyLoadBalancerAttributes", - "elasticloadbalancing:SetIpAddressType", - "elasticloadbalancing:SetSecurityGroups", - "elasticloadbalancing:SetSubnets", - "elasticloadbalancing:DeleteLoadBalancer", - "elasticloadbalancing:ModifyTargetGroup", - "elasticloadbalancing:ModifyTargetGroupAttributes", - "elasticloadbalancing:DeleteTargetGroup" - ], - "Resource": "*", - "Condition": { - "Null": { - "aws:ResourceTag/elbv2.k8s.aws/cluster": "false" - } - } - }, - { - "Effect": "Allow", - "Action": [ - "elasticloadbalancing:RegisterTargets", - "elasticloadbalancing:DeregisterTargets" - ], - "Resource": "arn:aws:elasticloadbalancing:*:*:targetgroup/*/*" - }, - { - "Effect": "Allow", - "Action": [ - "elasticloadbalancing:SetWebAcl", - "elasticloadbalancing:ModifyListener", - "elasticloadbalancing:AddListenerCertificates", - "elasticloadbalancing:RemoveListenerCertificates", - "elasticloadbalancing:ModifyRule" - ], - "Resource": "*" - } - ] -} diff --git a/PetAdoptions/cdk/pet_stack/resources/load_balancer/service_account.yaml b/PetAdoptions/cdk/pet_stack/resources/load_balancer/service_account.yaml deleted file mode 100644 index 56da38fa..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/load_balancer/service_account.yaml +++ /dev/null @@ -1,10 +0,0 @@ ---- -apiVersion: v1 -kind: ServiceAccount -metadata: - labels: - app.kubernetes.io/name: alb-ingress-controller - name: alb-ingress-controller - namespace: kube-system - annotations: - eks.amazonaws.com/role-arn: "<>" \ No newline at end of file diff --git a/PetAdoptions/cdk/pet_stack/resources/microservices/payforadoption-go b/PetAdoptions/cdk/pet_stack/resources/microservices/payforadoption-go deleted file mode 120000 index 73a86521..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/microservices/payforadoption-go +++ /dev/null @@ -1 +0,0 @@ -../../../../payforadoption-go/ \ No newline at end of file diff --git a/PetAdoptions/cdk/pet_stack/resources/microservices/petadoptionshistory-py b/PetAdoptions/cdk/pet_stack/resources/microservices/petadoptionshistory-py deleted file mode 120000 index 6659533b..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/microservices/petadoptionshistory-py +++ /dev/null @@ -1 +0,0 @@ -../../../../petadoptionshistory-py/ \ No newline at end of file diff --git a/PetAdoptions/cdk/pet_stack/resources/microservices/petlistadoptions-go b/PetAdoptions/cdk/pet_stack/resources/microservices/petlistadoptions-go deleted file mode 120000 index ba42ab25..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/microservices/petlistadoptions-go +++ /dev/null @@ -1 +0,0 @@ -../../../../petlistadoptions-go/ \ No newline at end of file diff --git a/PetAdoptions/cdk/pet_stack/resources/microservices/petsearch-java b/PetAdoptions/cdk/pet_stack/resources/microservices/petsearch-java deleted file mode 120000 index 9767a963..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/microservices/petsearch-java +++ /dev/null @@ -1 +0,0 @@ -../../../../petsearch-java/ \ No newline at end of file diff --git a/PetAdoptions/cdk/pet_stack/resources/microservices/petsite b/PetAdoptions/cdk/pet_stack/resources/microservices/petsite deleted file mode 120000 index 4c5c05ba..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/microservices/petsite +++ /dev/null @@ -1 +0,0 @@ -../../../../petsite/ \ No newline at end of file diff --git a/PetAdoptions/cdk/pet_stack/resources/microservices/trafficgenerator b/PetAdoptions/cdk/pet_stack/resources/microservices/trafficgenerator deleted file mode 120000 index 985765d1..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/microservices/trafficgenerator +++ /dev/null @@ -1 +0,0 @@ -../../../../trafficgenerator/ \ No newline at end of file diff --git a/PetAdoptions/cdk/pet_stack/resources/stepfn_lambdas/lambda_step_priceGreaterThan55.py b/PetAdoptions/cdk/pet_stack/resources/stepfn_lambdas/lambda_step_priceGreaterThan55.py deleted file mode 100644 index a78361f0..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/stepfn_lambdas/lambda_step_priceGreaterThan55.py +++ /dev/null @@ -1,11 +0,0 @@ -import json - -def lambda_handler(event, context): - # TODO implement - print(event) - - print('ProcessGreaterThan55 - Execution complete') - return { - 'statusCode': 200, - 'body': json.dumps('ProcessGreaterThan55 - Execution complete') - } diff --git a/PetAdoptions/cdk/pet_stack/resources/stepfn_lambdas/lambda_step_priceLessThan55.py b/PetAdoptions/cdk/pet_stack/resources/stepfn_lambdas/lambda_step_priceLessThan55.py deleted file mode 100644 index 8941b148..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/stepfn_lambdas/lambda_step_priceLessThan55.py +++ /dev/null @@ -1,10 +0,0 @@ -import json - -def lambda_handler(event, context): - # TODO implement - print(event) - print('ProcessLessthan55 - Execution complete') - return { - 'statusCode': 200, - 'body': json.dumps('ProcessLessthan55 - Execution complete') - } diff --git a/PetAdoptions/cdk/pet_stack/resources/stepfn_lambdas/lambda_step_readDDB.py b/PetAdoptions/cdk/pet_stack/resources/stepfn_lambdas/lambda_step_readDDB.py deleted file mode 100644 index db0ffabe..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/stepfn_lambdas/lambda_step_readDDB.py +++ /dev/null @@ -1,23 +0,0 @@ -import json -import boto3 -from boto3.dynamodb.conditions import Key - -ssm = boto3.client('ssm') -dynamodb = boto3.resource('dynamodb') - -def lambda_handler(event, context): - - dynamodb_tablename = ssm.get_parameter(Name='/petstore/dynamodbtablename', WithDecryption=False) - - table = dynamodb.Table(dynamodb_tablename['Parameter']['Value']) - - response = table.query( - KeyConditionExpression=Key('petid').eq(event['petid']) & Key('pettype').eq(event['pettype']) - ) - - response['Items'][0]['price'] = int(response['Items'][0]['price']) - - return { - 'statusCode': 200, - 'body': response['Items'][0] - } diff --git a/PetAdoptions/cdk/pet_stack/resources/syn-apicanary.js b/PetAdoptions/cdk/pet_stack/resources/syn-apicanary.js deleted file mode 100644 index 7b591755..00000000 --- a/PetAdoptions/cdk/pet_stack/resources/syn-apicanary.js +++ /dev/null @@ -1,69 +0,0 @@ -var synthetics = require('Synthetics'); -const log = require('SyntheticsLogger'); -const https = require('https'); -const http = require('http'); - -const apiCanaryBlueprint = async function () { - const postData = ""; - - const verifyRequest = async function (requestOption) { - return new Promise((resolve, reject) => { - log.info("Making request with options: " + JSON.stringify(requestOption)); - let req - if (requestOption.port === 443) { - req = https.request(requestOption); - } else { - req = http.request(requestOption); - } - req.on('response', (res) => { - log.info(`Status Code: ${res.statusCode}`) - log.info(`Response Headers: ${JSON.stringify(res.headers)}`) - if (res.statusCode !== 200) { - reject("Failed: " + requestOption.path); - } - res.on('data', (d) => { - log.info("Response: " + d.length); - if(d.length <= 2) - { - reject("PetType Invalid - : "+requestOption.path ); - } - }); - res.on('end', () => { - resolve(); - }) - }); - - req.on('error', (error) => { - reject(error); - }); - - if (postData) { - req.write(postData); - } - req.end(); - }); - } - - const headers = {} - headers['User-Agent'] = [synthetics.getCanaryUserAgentString(), headers['User-Agent']].join(' '); - - const pettypes = ["fish", "bunny", "puppy", "kitten"]; - const position = Math.floor(Math.random() * Math.floor(4)); - - const requestOptions = { - hostname: '', - // Example- hostname: 'petsearch-live.us-east-1.elasticbeanstalk.com', - port: 80, - path: '/api/search?pettype='+pettypes[position], - method: 'GET' - } - - requestOptions['headers'] = headers; - await verifyRequest(requestOptions); -}; - -exports.handler = async () => { - return await apiCanaryBlueprint(); -}; - - diff --git a/PetAdoptions/patches/ddb-billing-mode.patch b/PetAdoptions/patches/ddb-billing-mode.patch deleted file mode 100644 index 1a543abe..00000000 --- a/PetAdoptions/patches/ddb-billing-mode.patch +++ /dev/null @@ -1,28 +0,0 @@ -diff --git cdk/pet_stack/lib/services.ts cdk/pet_stack/lib/services.ts -index d469bbd..dc4a15a 100644 ---- cdk/pet_stack/lib/services.ts -+++ cdk/pet_stack/lib/services.ts -@@ -55,21 +55,8 @@ export class Services extends cdk.Stack { - name: 'petid', - type: ddb.AttributeType.STRING - }, -- removalPolicy: RemovalPolicy.DESTROY -- }); -- -- dynamodb_petadoption.metricConsumedReadCapacityUnits().createAlarm(this, 'ReadCapacityUnitsLimit-BasicAlarm', { -- threshold: 240, -- evaluationPeriods: 2, -- period: cdk.Duration.minutes(1), -- alarmName: `${dynamodb_petadoption.tableName}-ReadCapacityUnitsLimit-BasicAlarm`, -- }); -- -- dynamodb_petadoption.metricConsumedReadCapacityUnits().createAlarm(this, 'WriteCapacityUnitsLimit-BasicAlarm', { -- threshold: 240, -- evaluationPeriods: 2, -- period: cdk.Duration.minutes(1), -- alarmName: `${dynamodb_petadoption.tableName}-WriteCapacityUnitsLimit-BasicAlarm`, -+ removalPolicy: RemovalPolicy.DESTROY, -+ billingMode: ddb.BillingMode.PAY_PER_REQUEST - }); - - // Seeds the petadoptions dynamodb table with all data required diff --git a/PetAdoptions/patches/sql-connection.patch b/PetAdoptions/patches/sql-connection.patch deleted file mode 100644 index aee9e1c4..00000000 --- a/PetAdoptions/patches/sql-connection.patch +++ /dev/null @@ -1,49 +0,0 @@ -diff --git payforadoption/PayForAdoption/Controllers/HomeController.cs payforadoption/PayForAdoption/Controllers/HomeController.cs -index d801a84..f2eb9d0 100644 ---- payforadoption/PayForAdoption/Controllers/HomeController.cs -+++ payforadoption/PayForAdoption/Controllers/HomeController.cs -@@ -1,4 +1,5 @@ --using System; -+using System; -+using System.Data; - using System.Collections.Generic; - using System.Linq; - using System.Net.Http; -@@ -19,7 +20,6 @@ namespace PayForAdoption.Controllers - [ApiController] - public class HomeController : ControllerBase - { -- private static SqlConnection _sqlConnection = new SqlConnection(); - private static HttpClient _httpClient = new HttpClient(new HttpClientXRayTracingHandler(new HttpClientHandler())); - private static IConfiguration _configuration; - public HomeController(IConfiguration configuration) -@@ -37,13 +37,11 @@ namespace PayForAdoption.Controllers - AWSXRayRecorder.Instance.AddAnnotation("PetId", petId); - AWSXRayRecorder.Instance.AddAnnotation("PetType", pettype); - -- _sqlConnection.ConnectionString = await GetConnectionString(); -- - var sqlCommandText = $"INSERT INTO [dbo].[transactions] ([PetId], [Transaction_Id], [Adoption_Date]) VALUES ('{petId}', '{Guid.NewGuid().ToString()}', '{DateTime.Now.ToString()}')"; - - AWSXRayRecorder.Instance.AddMetadata("Query",sqlCommandText); - -- using (_sqlConnection) -+ using (SqlConnection _sqlConnection = new SqlConnection(await GetConnectionString())) - { - using var command = new TraceableSqlCommand(sqlCommandText, _sqlConnection); - command.Connection.Open(); -@@ -63,13 +61,12 @@ namespace PayForAdoption.Controllers - [HttpPost("CleanUpAdoptions")] - public async Task CleanupAdoptions() - { -- _sqlConnection.ConnectionString = await GetConnectionString(); - - var sqlCommandText = $"DELETE FROM [dbo].[transactions]"; - - AWSXRayRecorder.Instance.AddMetadata("Query",sqlCommandText); - -- using (_sqlConnection) -+ using (SqlConnection _sqlConnection = new SqlConnection(await GetConnectionString())) - { - using var command = new TraceableSqlCommand(sqlCommandText, _sqlConnection); - command.Connection.Open(); diff --git a/PetAdoptions/payforadoption-go/benchmark/benchmark.sh b/PetAdoptions/payforadoption-go/benchmark/benchmark.sh deleted file mode 100755 index 029acc1e..00000000 --- a/PetAdoptions/payforadoption-go/benchmark/benchmark.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -while true -do - drill -s --benchmark benchmark.yaml - sleep 1 -done diff --git a/PetAdoptions/payforadoption-go/benchmark/benchmark.yaml b/PetAdoptions/payforadoption-go/benchmark/benchmark.yaml deleted file mode 100644 index a7addc6b..00000000 --- a/PetAdoptions/payforadoption-go/benchmark/benchmark.yaml +++ /dev/null @@ -1,67 +0,0 @@ ---- -concurrency: 4 -base: "http://localhost:80" -iterations: 4 -rampup: 2 - -plan: - - name: Health check - request: - url: /health/status - method: GET - - - name: Adopt Bunnies - request: - url: /api/completeadoption?petId={{ item }}&petType=bunny&userId=user007 - method: POST - body: "" - with_items: - - "023" - - "024" - - "025" - - "026" - - "invalid_bunny_id" - - - name: Adopt Kittens - request: - url: /api/completeadoption?petId={{ item }}&petType=kitten&userId=user007 - method: POST - body: "" - with_items: - - "016" - - "017" - - "018" - - "019" - - "020" - - "021" - - "022" - - "invalid_kitten_id" - - - name: Adopt Puppies - request: - url: /api/completeadoption?petId={{ item }}&petType=puppy&userId=user007 - method: POST - body: "" - with_items: - - "001" - - "002" - - "003" - - "004" - - "005" - - "006" - - "007" - - "008" - - "009" - - "010" - - "011" - - "012" - - "013" - - "014" - - "015" - - "invalid_puppy_id" - - - name: Cleanup Adoptions - request: - url: /api/cleanupadoptions - method: POST - body: "" diff --git a/PetAdoptions/pethistory/.eslintrc.js b/PetAdoptions/pethistory/.eslintrc.js deleted file mode 100644 index 508e980f..00000000 --- a/PetAdoptions/pethistory/.eslintrc.js +++ /dev/null @@ -1,22 +0,0 @@ -module.exports = { - env: { - node: true, - es2021: true, - jest: true - }, - extends: [ - 'eslint:recommended' - ], - parserOptions: { - ecmaVersion: 12, - sourceType: 'module' - }, - rules: { - 'indent': ['error', 4], - 'linebreak-style': ['error', 'unix'], - 'quotes': ['error', 'single'], - 'semi': ['error', 'always'], - 'no-unused-vars': ['error', { 'argsIgnorePattern': '^_' }], - 'no-console': 'off' // Allow console.log in Lambda functions - } -}; \ No newline at end of file diff --git a/PetAdoptions/pethistory/README.md b/PetAdoptions/pethistory/README.md deleted file mode 100644 index 3e0f3fcc..00000000 --- a/PetAdoptions/pethistory/README.md +++ /dev/null @@ -1,307 +0,0 @@ -# Pet History Lambda Function - -## Overview -The Pet History Lambda function processes adoption history messages from Amazon SQS and maintains historical records of pet adoptions. This service is part of an event-driven architecture that separates real-time adoption processing from historical data tracking. - -**Key Responsibilities:** -1. **Asynchronous history tracking** - Records adoption history without blocking the main adoption flow -2. **Optimistic table creation** - Automatically creates database tables when needed -3. **Schema validation** - Prevents message poisoning with strict validation -4. **Batch processing** - Efficiently handles multiple messages with partial failure support - -## Architecture - -``` -payforadoption-go → SQS Queue → pethistory Lambda → transaction_history table - ↓ - (Auto-creates table if missing) -``` - -### Event-Driven Design -- **Real-time adoption**: `payforadoption-go` handles synchronous adoption processing -- **Asynchronous history**: `pethistory` tracks historical data in the background -- **Clean separation**: History tracking failures don't impact adoption success - -## Message Processing Flow - -1. **SQS Trigger**: Lambda receives adoption history messages from SQS -2. **Schema Validation**: Validates message format using Joi schema -3. **Optimistic Insert**: Attempts to insert directly into `transaction_history` table -4. **Auto Table Creation**: If table doesn't exist (PostgreSQL error 42P01), creates it automatically -5. **Batch Failure Handling**: Uses SQS `ReportBatchItemFailures` for partial batch processing -6. **Retry Mechanism**: Failed messages are retried by SQS, successful table creation enables future success - -## Message Schema - -The function expects SQS messages with the following JSON structure: - -```json -{ - "transactionId": "123e4567-e89b-12d3-a456-426614174000", - "petId": "pet123", - "petType": "dog", - "userId": "user456", - "adoptionDate": "2025-08-08T10:30:00Z", - "timestamp": "2025-08-08T10:30:00Z" -} -``` - -### Schema Validation Rules -- `transactionId`: Must be a valid UUID -- `petId`: Required string -- `petType`: Required string -- `userId`: Required string -- `adoptionDate`: Must be a valid ISO date string -- `timestamp`: Must be a valid ISO date string - -## Environment Variables - -| Variable | Description | Example | -|----------|-------------|---------| -| `RDS_SECRET_ARN` | ARN of the RDS secret in Secrets Manager | `arn:aws:secretsmanager:us-west-2:123456789012:secret:rds-secret` | -| `AWS_REGION` | AWS region for services | `us-west-2` | - -## Database Operations - -### Transaction History Table -The function automatically creates and inserts into the `transaction_history` table: - -```sql -CREATE TABLE IF NOT EXISTS transaction_history ( - id SERIAL PRIMARY KEY, - pet_id VARCHAR(255) NOT NULL, - transaction_id VARCHAR(255) NOT NULL, - adoption_date TIMESTAMP NOT NULL, - user_id VARCHAR(255) NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP -); - --- Performance indexes created automatically -CREATE INDEX IF NOT EXISTS idx_transaction_history_pet_id ON transaction_history(pet_id); -CREATE INDEX IF NOT EXISTS idx_transaction_history_user_id ON transaction_history(user_id); -CREATE INDEX IF NOT EXISTS idx_transaction_history_adoption_date ON transaction_history(adoption_date); -CREATE INDEX IF NOT EXISTS idx_transaction_history_transaction_id ON transaction_history(transaction_id); -``` - -### Optimistic Table Creation -- **First attempt**: Tries to INSERT directly (fastest path) -- **On table missing**: Catches PostgreSQL error code `42P01` and creates table + indexes -- **Subsequent attempts**: INSERT succeeds normally after table exists -- **Self-healing**: Service creates its own dependencies automatically - -### Database Connection -- Uses AWS Secrets Manager to retrieve database credentials -- Establishes PostgreSQL connection using the `pg` library -- Handles connection errors gracefully with SQS retry mechanism - -## Error Handling & Batch Processing - -### SQS Batch Processing -The function uses SQS's `ReportBatchItemFailures` feature for efficient batch processing: - -```javascript -// Response format for SQS -{ - "batchItemFailures": [ - { "itemIdentifier": "failed-message-id" } - ], - "processedCount": 8, - "results": [...] -} -``` - -### Error Types -1. **Schema Validation Errors**: Invalid JSON or missing fields → Message goes to batch failures -2. **Database Connection Errors**: Network/auth issues → Message retried by SQS -3. **Table Missing (42P01)**: Creates table automatically → Message retried and succeeds -4. **Other Database Errors**: Logged and message goes to batch failures - -### Retry Strategy -- **Successful messages**: Automatically deleted from SQS -- **Failed messages**: Remain in SQS for retry based on queue configuration -- **Persistent failures**: Eventually move to Dead Letter Queue (DLQ) - -## Observability - -### CloudWatch Application Signals -- **Automatic instrumentation** with AWS Distro for OpenTelemetry (ADOT) -- **Service name**: `pethistory` for easy identification -- **Distributed tracing** across the adoption workflow -- **Performance metrics** and dependency mapping -- **Error tracking** and alerting - -### CloudWatch Logs -- **Structured JSON logging** for all operations -- **Processing metrics**: Success/failure counts, timing -- **Error details**: Stack traces and context information -- **Table creation events**: Logged when auto-creation occurs - -### Key Metrics -- **Processing rate**: Messages processed per minute -- **Error rate**: Failed message percentage -- **Table creation events**: When auto-creation occurs -- **Batch efficiency**: Partial vs full batch failures - -## Testing - -### Unit Tests -```bash -npm test -``` - -The test suite covers: -- ✅ Single message processing -- ✅ Batch message processing -- ✅ Schema validation failures -- ✅ Mixed success/failure batches -- ✅ Correct SQS response format -- ✅ Error handling scenarios - -### Integration Testing -```bash -# Send test message to SQS queue -aws sqs send-message \ - --queue-url "https://sqs.us-west-2.amazonaws.com/123456789012/adoption-history-queue" \ - --message-body '{ - "transactionId": "123e4567-e89b-12d3-a456-426614174000", - "petId": "pet123", - "petType": "dog", - "userId": "user456", - "adoptionDate": "2025-08-08T10:30:00Z", - "timestamp": "2025-08-08T10:30:00Z" - }' -``` - -### Local Development -```bash -# Install dependencies -npm install - -# Run tests -npm test - -# Lint code -npm run lint - -# Format code -npm run format - -# Local SAM testing -sam build -sam local invoke PetHistoryFunction --event events/test-sqs-event.json -``` - -## Deployment - -### AWS SAM Deployment -```bash -# Interactive deployment -./deploy.sh - -# Manual deployment -sam build && sam deploy --guided -``` - -### Configuration -- **Function Name**: `pethistory` -- **Runtime**: Node.js 18.x -- **Memory**: 512 MB -- **Timeout**: 60 seconds -- **SQS Batch Size**: 10 messages -- **Batch Window**: 5 seconds -- **Dead Letter Queue**: Configured for failed messages - -## Monitoring and Alerts - -### Key Metrics to Monitor -1. **Processing Rate**: Messages processed per minute -2. **Error Rate**: Failed message percentage -3. **SQS Queue Depth**: Unprocessed history messages -4. **Table Creation Events**: Auto-creation frequency -5. **DLQ Messages**: Messages requiring investigation - -### Recommended CloudWatch Alarms -- Lambda error rate > 5% -- Lambda duration > 50 seconds -- SQS queue depth > 50 messages -- DLQ message count > 0 -- Table creation events (for monitoring) - -## Security - -### IAM Permissions -The Lambda function requires: -- `secretsmanager:GetSecretValue` - Database credentials -- `rds-db:connect` - Database connection (if using IAM auth) -- `sqs:ReceiveMessage`, `sqs:DeleteMessage` - SQS processing -- CloudWatch Application Signals permissions (auto-granted) - -### Network Security -- **VPC Deployment**: Private subnets with NAT Gateway -- **Security Groups**: Restrict database access to Lambda only -- **VPC Endpoints**: AWS services access without internet routing - -## Architecture Benefits - -### Event-Driven Design -- **Decoupled services**: History tracking independent of adoption flow -- **Resilient**: History failures don't impact adoptions -- **Scalable**: Independent scaling based on workload -- **Maintainable**: Clear domain boundaries - -### Optimistic Table Creation -- **Self-healing**: Creates dependencies automatically -- **Zero-downtime**: No manual setup required -- **Performance**: Fast path for normal operations -- **Idempotent**: Safe to run multiple times - -### Batch Processing -- **Efficient**: Processes multiple messages together -- **Partial failures**: Doesn't fail entire batch for single message -- **Cost-effective**: Reduces Lambda invocations -- **Reliable**: SQS handles retry logic automatically - -## Troubleshooting - -### Common Issues - -1. **Messages Not Being Processed** - - Check SQS queue for messages in flight - - Verify Lambda function is triggered by SQS - - Review CloudWatch logs for errors - -2. **Schema Validation Failures** - - Check message format in SQS queue - - Verify all required fields are present - - Ensure correct data types (UUID, ISO dates) - -3. **Database Connection Issues** - - Verify RDS secret ARN is correct - - Check VPC/security group configuration - - Ensure database is accessible from Lambda subnet - -4. **Table Creation Issues** - - Check database permissions for CREATE TABLE - - Verify PostgreSQL version compatibility - - Review CloudWatch logs for creation attempts - -### Debugging Steps -1. **Check CloudWatch Logs** for detailed error information -2. **Review Application Signals** service map for dependencies -3. **Monitor SQS metrics** for processing delays -4. **Inspect DLQ messages** for patterns in failures -5. **Use X-Ray traces** for end-to-end request tracking - -## Workshop Learning Objectives - -This service demonstrates: -- ✅ **Event-driven architecture** with asynchronous processing -- ✅ **SQS batch processing** with partial failure handling -- ✅ **Optimistic database operations** and auto-recovery -- ✅ **Schema validation** and data integrity -- ✅ **CloudWatch Application Signals** observability -- ✅ **Microservices patterns** and domain separation -- ✅ **Error handling strategies** in distributed systems -- ✅ **Infrastructure as Code** with AWS SAM - -Perfect for learning modern serverless patterns and AWS observability tools! 🚀 \ No newline at end of file diff --git a/PetAdoptions/pethistory/deploy.sh b/PetAdoptions/pethistory/deploy.sh deleted file mode 100755 index f15196c4..00000000 --- a/PetAdoptions/pethistory/deploy.sh +++ /dev/null @@ -1,85 +0,0 @@ -#!/bin/bash - -# Pet history Lambda Deployment Script using AWS SAM - -set -e - -# Colors for output -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -NC='\033[0m' # No Color - -# Configuration -STACK_NAME="pet-history-stack" -REGION="us-west-2" - -echo -e "${GREEN}🚀 Pet history Lambda Deployment${NC}" -echo "==================================" - -# Check if AWS CLI is configured -if ! aws sts get-caller-identity > /dev/null 2>&1; then - echo -e "${RED}❌ AWS CLI not configured. Please run 'aws configure' first.${NC}" - exit 1 -fi - -# Check if SAM CLI is installed -if ! command -v sam &> /dev/null; then - echo -e "${RED}❌ SAM CLI not found. Please install AWS SAM CLI first.${NC}" - echo "Installation guide: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/install-sam-cli.html" - exit 1 -fi - -# Get AWS Account ID -ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) -echo -e "${GREEN}📋 AWS Account ID: ${ACCOUNT_ID}${NC}" - - -# Confirm deployment -echo -e "${YELLOW}❓ Do you want to proceed with deployment? (y/N)${NC}" -read -r CONFIRM -if [[ ! $CONFIRM =~ ^[Yy]$ ]]; then - echo -e "${YELLOW}⏹️ Deployment cancelled.${NC}" - exit 0 -fi - -# Install dependencies -echo -e "${GREEN}📦 Installing dependencies...${NC}" -npm install - -# Run tests -echo -e "${GREEN}🧪 Running tests...${NC}" -npm test - -# Build the SAM application -echo -e "${GREEN}🔨 Building SAM application...${NC}" -sam build - -# Deploy the application -echo -e "${GREEN}🚀 Deploying to AWS...${NC}" -sam deploy \ - --stack-name "${STACK_NAME}" \ - --region "${REGION}" \ - --capabilities CAPABILITY_IAM \ - # --parameter-overrides \ - # "SQSQueueArn=${SQS_QUEUE_ARN}" \ - # "RDSSecretArn=${RDS_SECRET_ARN}" \ - # "UpdateAdoptionURL=${UPDATE_ADOPTION_URL}" \ - # "VpcId=${VPC_ID}" \ - # "SubnetIds=${SUBNET_IDS}" \ - # "SecurityGroupId=${SECURITY_GROUP_ID}" \ - -# Get outputs -echo -e "${GREEN}📋 Deployment completed! Getting stack outputs...${NC}" -aws cloudformation describe-stacks \ - --stack-name "${STACK_NAME}" \ - --region "${REGION}" \ - --query 'Stacks[0].Outputs' \ - --output table - -echo -e "${GREEN}✅ Pet history Lambda function deployed successfully!${NC}" -echo -e "${GREEN}🔍 Check CloudWatch Application Signals for observability data.${NC}" -echo -e "${GREEN}📊 Application Signals will automatically detect the service and create service maps.${NC}" -echo -e "${GREEN}🎯 X-Ray tracing is enabled for distributed tracing across services.${NC}" - -echo -e "${GREEN}🎉 Deployment complete!${NC}" \ No newline at end of file diff --git a/PetAdoptions/pethistory/index.js b/PetAdoptions/pethistory/index.js deleted file mode 100644 index 3e99bda7..00000000 --- a/PetAdoptions/pethistory/index.js +++ /dev/null @@ -1,237 +0,0 @@ -'use strict'; - -const AWS = require('aws-sdk'); -const { Client } = require('pg'); -const Joi = require('joi'); - -// Schema validation for SQS messages -const adoptionMessageSchema = Joi.object({ - transactionId: Joi.string().uuid().required(), - petId: Joi.string().required(), - petType: Joi.string().required(), - userId: Joi.string().required(), - adoptionDate: Joi.string().isoDate().required(), - timestamp: Joi.string().isoDate().required() -}); - -// Environment variables -const { - RDS_SECRET_ARN, - AWS_REGION, -} = process.env; - -// AWS clients -const secretsManager = new AWS.SecretsManager({ region: AWS_REGION }); - -/** - * Lambda handler for processing adoption history messages from SQS - */ -exports.handler = async (event, context) => { - console.log('Processing adoption history messages:', JSON.stringify(event, null, 2)); - - const results = []; - const batchItemFailures = []; - - // Process each SQS record - for (const record of event.Records) { - try { - const result = await processAdoptionMessage(record); - results.push(result); - } catch (error) { - console.error('Failed to process record:', record.messageId, error); - // Add to batch failures instead of throwing - batchItemFailures.push({ - itemIdentifier: record.messageId - }); - } - } - - console.log(`Processed ${results.length} messages successfully, ${batchItemFailures.length} failed`); - - // Return batch failure information for SQS to handle partial failures - return { - batchItemFailures: batchItemFailures, - processedCount: results.length, - results: results - }; -}; - -/** - * Process a single adoption history message from SQS - */ -async function processAdoptionMessage(record) { - const messageId = record.messageId; - // const receiptHandle = record.receiptHandle; - - console.log(`Processing message ${messageId}`); - - // Parse and validate message - let adoptionData; - try { - adoptionData = JSON.parse(record.body); - console.log('Parsed adoption data:', adoptionData); - } catch (error) { - console.error('Invalid JSON in message:', messageId, error); - throw new Error(`Invalid JSON in message ${messageId}: ${error.message}`); - } - // Validate message schema - const { error, value } = adoptionMessageSchema.validate(adoptionData); - if (error) { - console.error('Schema validation failed for message:', messageId, error.details); - throw new Error(`Schema validation failed for message ${messageId}: ${error.message}`); - } - const validatedData = value; - try { - console.log(`Processing adoption - Transaction: ${validatedData.transactionId}, Pet: ${validatedData.petId}, User: ${validatedData.userId}`); - // Write adoption history to database - await writeAdoptionHistoryToDatabase(validatedData); - - console.log(`Successfully processed adoption for pet ${validatedData.petId} by user ${validatedData.userId}`); - - return { - messageId: messageId, - transactionId: validatedData.transactionId, - status: 'success' - }; - - } catch (error) { - console.error(`Failed to process adoption for pet ${validatedData.petId}:`, error); - throw error; - } -} - -/** - * Write adoption history record to PostgreSQL database - */ -async function writeAdoptionHistoryToDatabase(adoptionData) { - let client; - - try { - console.log(`Writing adoption history to database: ${adoptionData.transactionId}`); - - // Get database connection details from Secrets Manager - const dbConfig = await getDatabaseConfig(); - - // Create database connection - client = new Client({ - host: dbConfig.host, - port: dbConfig.port, - database: dbConfig.dbname, - user: dbConfig.username, - password: dbConfig.password, - ssl: false, // Adjust based on your RDS configuration - connectionTimeoutMillis: 5000, - query_timeout: 10000 - }); - - await client.connect(); - console.log('Connected to database successfully'); - - // Optimistic approach: try to insert first - const insertQuery = ` - INSERT INTO transaction_history (pet_id, transaction_id, adoption_date, user_id, created_at) - VALUES ($1, $2, $3, $4, $5) - `; - - const values = [ - adoptionData.petId, - adoptionData.transactionId, - new Date(adoptionData.adoptionDate), - adoptionData.userId, - new Date() // Current timestamp for history record - ]; - - const result = await client.query(insertQuery, values); - console.log(`Successfully inserted adoption history record: ${adoptionData.transactionId}, rows affected: ${result.rowCount}`); - - } catch (error) { - // Check if error is due to missing table (PostgreSQL error code 42P01) - if (error.code === '42P01') { - console.log('Table does not exist, creating transaction_history table...'); - await ensureTransactionHistoryTableExists(client); - console.log('Table created, failing this attempt - SQS will retry the message'); - throw new Error('Table was created, message will be retried'); - } else { - console.error('Database write failed:', error); - throw new Error(`Database write failed: ${error.message}`); - } - } finally { - if (client) { - try { - await client.end(); - console.log('Database connection closed'); - } catch (closeError) { - console.error('Error closing database connection:', closeError); - } - } - } -} - -/** - * Ensure the transaction_history table exists, create it if it doesn't - */ -async function ensureTransactionHistoryTableExists(client) { - try { - console.log('Checking if transaction_history table exists...'); - - const createTableQuery = ` - CREATE TABLE IF NOT EXISTS transaction_history ( - id SERIAL PRIMARY KEY, - pet_id VARCHAR(255) NOT NULL, - transaction_id VARCHAR(255) NOT NULL, - adoption_date TIMESTAMP NOT NULL, - user_id VARCHAR(255) NOT NULL, - created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP - ) - `; - - await client.query(createTableQuery); - console.log('transaction_history table is ready'); - - // Create indexes for better performance if they don't exist - const indexQueries = [ - 'CREATE INDEX IF NOT EXISTS idx_transaction_history_pet_id ON transaction_history(pet_id)', - 'CREATE INDEX IF NOT EXISTS idx_transaction_history_user_id ON transaction_history(user_id)', - 'CREATE INDEX IF NOT EXISTS idx_transaction_history_adoption_date ON transaction_history(adoption_date)', - 'CREATE INDEX IF NOT EXISTS idx_transaction_history_transaction_id ON transaction_history(transaction_id)' - ]; - - for (const indexQuery of indexQueries) { - await client.query(indexQuery); - } - - console.log('transaction_history table indexes are ready'); - - } catch (error) { - console.error('Error ensuring transaction_history table exists:', error); - throw new Error(`Failed to ensure transaction_history table exists: ${error.message}`); - } -} - -/** - * Get database configuration from AWS Secrets Manager - */ -async function getDatabaseConfig() { - try { - console.log('Retrieving database configuration from Secrets Manager'); - - const result = await secretsManager.getSecretValue({ - SecretId: RDS_SECRET_ARN - }).promise(); - - const secret = JSON.parse(result.SecretString); - console.log('Successfully retrieved database configuration'); - - return { - host: secret.host, - port: secret.port, - database: secret.dbname, - username: secret.username, - password: secret.password - }; - - } catch (error) { - console.error('Failed to retrieve database configuration:', error); - throw new Error(`Failed to retrieve database configuration: ${error.message}`); - } -} \ No newline at end of file diff --git a/PetAdoptions/pethistory/package.json b/PetAdoptions/pethistory/package.json deleted file mode 100644 index 9a93f3e1..00000000 --- a/PetAdoptions/pethistory/package.json +++ /dev/null @@ -1,30 +0,0 @@ -{ - "name": "pethistory", - "version": "1.0.0", - "description": "Lambda function to process adoption messages from SQS and write to database", - "main": "index.js", - "scripts": { - "test": "jest", - "lint": "eslint .", - "format": "prettier --write ." - }, - "dependencies": { - "aws-sdk": "^2.1691.0", - "joi": "^17.13.3", - "pg": "^8.12.0" - }, - "devDependencies": { - "jest": "^29.7.0", - "eslint": "^8.57.0", - "prettier": "^3.3.3" - }, - "keywords": [ - "aws", - "lambda", - "sqs", - "pet-adoption", - "observability" - ], - "author": "AWS Pet Adoption Workshop", - "license": "MIT" -} diff --git a/PetAdoptions/pethistory/template.yaml b/PetAdoptions/pethistory/template.yaml deleted file mode 100644 index fcd4b00a..00000000 --- a/PetAdoptions/pethistory/template.yaml +++ /dev/null @@ -1,215 +0,0 @@ -AWSTemplateFormatVersion: "2010-09-09" -Transform: AWS::Serverless-2016-10-31 -Description: Pet History Lambda function for processing adoption history messages from SQS - -Parameters: - SQSQueueArn: - Type: String - Default: "arn:aws:sqs:REGION:ACCOUNTID:QUEUE_NAME" - Description: ARN of the SQS queue for adoption messages - - RDSSecretArn: - Type: String - Default: "arn:aws:secretsmanager:REGION:ACCOUNTID:secret:SECRET_NAME" - Description: ARN of the RDS secret in Secrets Manager - - VpcId: - Type: String - Default: "vpc-123abc" - Description: VPC ID for Lambda deployment - - SubnetIds: - Type: CommaDelimitedList - Default: "subnet-123abc,subnet-456def" - Description: Comma-delimited list of subnet IDs for Lambda deployment - - SecurityGroupId: - Type: String - Default: "sg-123abc" - Description: Security group ID for Lambda function - -Globals: - Function: - Runtime: nodejs18.x - Architectures: - - x86_64 - Timeout: 60 - MemorySize: 512 - LoggingConfig: - LogFormat: JSON - Environment: - Variables: - AWS_NODEJS_CONNECTION_REUSE_ENABLED: 1 - # Enable X-Ray tracing for Application Signals - Tracing: Active - -Resources: - # Dead Letter Queue for failed history messages - PetHistoryDLQ: - Type: AWS::SQS::Queue - Properties: - QueueName: pethistory-dlq - MessageRetentionPeriod: 1209600 # 14 days - VisibilityTimeout: 60 - Tags: - - Key: Purpose - Value: DeadLetterQueue - - Key: Service - Value: PetHistory - - PetHistoryFunction: - Type: AWS::Serverless::Function - Properties: - FunctionName: pethistory - Description: Lambda function to process adoption history messages from SQS and write to transaction_history table - CodeUri: ./ - Handler: index.handler - - # VPC Configuration for RDS access - VpcConfig: - SecurityGroupIds: - - !Ref SecurityGroupId - SubnetIds: !Ref SubnetIds - - # Environment Variables - Environment: - Variables: - RDS_SECRET_ARN: !Ref RDSSecretArn - # CloudWatch Application Signals configuration - AWS_LAMBDA_EXEC_WRAPPER: /opt/otel-instrument - OTEL_SERVICE_NAME: pethistory - OTEL_SERVICE_VERSION: "1.0.0" - OTEL_PROPAGATORS: tracecontext,baggage,b3,xray - OTEL_NODE_DISABLED_INSTRUMENTATIONS: none - # Enable Application Signals - OTEL_AWS_APPLICATION_SIGNALS_ENABLED: true - LAMBDA_APPLICATION_SIGNALS_REMOTE_ENVIRONMENT: lambda:default - OTEL_METRICS_EXPORTER: none - OTEL_LOGS_EXPORTER: none - - # AWS Distro for OpenTelemetry Layer for auto-instrumentation - # Using the correct AWS-managed OTEL layer for us-west-2 - Layers: - - arn:aws:lambda:us-west-2:615299751070:layer:AWSOpenTelemetryDistroJs:8 - - # SQS Event Source - Events: - SQSEvent: - Type: SQS - Properties: - Queue: !Ref SQSQueueArn - BatchSize: 10 - MaximumBatchingWindowInSeconds: 5 - FunctionResponseTypes: - - ReportBatchItemFailures - - # IAM Policies - Policies: - - AWSLambdaBasicExecutionRole - - AWSLambdaVPCAccessExecutionRole - - # Secrets Manager access - - Version: "2012-10-17" - Statement: - - Effect: Allow - Action: - - secretsmanager:GetSecretValue - Resource: !Ref RDSSecretArn - - # RDS access (if using RDS Proxy or IAM database authentication) - - Version: "2012-10-17" - Statement: - - Effect: Allow - Action: - - rds-db:connect - Resource: !Sub "arn:aws:rds-db:${AWS::Region}:${AWS::AccountId}:dbuser:*/lambda-user" - - # X-Ray and CloudWatch Application Signals permissions - - Version: "2012-10-17" - Statement: - - Effect: Allow - Action: - - xray:PutTraceSegments - - xray:PutTelemetryRecords - - cloudwatch:PutMetricData - - logs:PutLogEvents - - logs:CreateLogGroup - - logs:CreateLogStream - Resource: "*" - - # Application Signals specific permissions - - Version: "2012-10-17" - Statement: - - Effect: Allow - Action: - - application-signals:PutServiceLevelObjective - - application-signals:GetServiceLevelObjective - - application-signals:ListServiceLevelObjectives - Resource: "*" - - # CloudWatch Log Group with retention - PetHistoryLogGroup: - Type: AWS::Logs::LogGroup - Properties: - LogGroupName: !Sub "/aws/lambda/${PetHistoryFunction}" - RetentionInDays: 14 - - # CloudWatch Alarms for monitoring - PetHistoryErrorAlarm: - Type: AWS::CloudWatch::Alarm - Properties: - AlarmName: !Sub "${PetHistoryFunction}-Errors" - AlarmDescription: "Pet History Lambda function error rate is too high" - MetricName: Errors - Namespace: AWS/Lambda - Statistic: Sum - Period: 300 - EvaluationPeriods: 2 - Threshold: 5 - ComparisonOperator: GreaterThanThreshold - Dimensions: - - Name: FunctionName - Value: !Ref PetHistoryFunction - TreatMissingData: notBreaching - - PetHistoryDurationAlarm: - Type: AWS::CloudWatch::Alarm - Properties: - AlarmName: !Sub "${PetHistoryFunction}-Duration" - AlarmDescription: "Pet History Lambda function duration is too high" - MetricName: Duration - Namespace: AWS/Lambda - Statistic: Average - Period: 300 - EvaluationPeriods: 2 - Threshold: 50000 # 50 seconds - ComparisonOperator: GreaterThanThreshold - Dimensions: - - Name: FunctionName - Value: !Ref PetHistoryFunction - TreatMissingData: notBreaching - -Outputs: - PetHistoryFunctionArn: - Description: "Pet History Lambda Function ARN" - Value: !GetAtt PetHistoryFunction.Arn - Export: - Name: !Sub "${AWS::StackName}-PetHistoryFunctionArn" - - PetHistoryFunctionName: - Description: "Pet History Lambda Function Name" - Value: !Ref PetHistoryFunction - Export: - Name: !Sub "${AWS::StackName}-PetHistoryFunctionName" - - PetHistoryDLQArn: - Description: "Pet History Dead Letter Queue ARN" - Value: !GetAtt PetHistoryDLQ.Arn - Export: - Name: !Sub "${AWS::StackName}-PetHistoryDLQArn" - - PetHistoryDLQUrl: - Description: "Pet History Dead Letter Queue URL" - Value: !Ref PetHistoryDLQ - Export: - Name: !Sub "${AWS::StackName}-PetHistoryDLQUrl" diff --git a/PetAdoptions/pethistory/test/index.test.js b/PetAdoptions/pethistory/test/index.test.js deleted file mode 100644 index 37bb4367..00000000 --- a/PetAdoptions/pethistory/test/index.test.js +++ /dev/null @@ -1,301 +0,0 @@ -const { handler } = require('../index'); - -// Mock AWS SDK -jest.mock('aws-sdk', () => ({ - SecretsManager: jest.fn(() => ({ - getSecretValue: jest.fn(() => ({ - promise: jest.fn(() => Promise.resolve({ - SecretString: JSON.stringify({ - host: 'localhost', - port: 5432, - dbname: 'testdb', - username: 'testuser', - password: 'testpass' - }) - })) - })) - })) -})); - -// No X-Ray mocking needed - using CloudWatch Application Signals auto-instrumentation - -// Mock pg -jest.mock('pg', () => ({ - Client: jest.fn(() => ({ - connect: jest.fn(() => Promise.resolve()), - query: jest.fn(() => Promise.resolve({ rowCount: 1, rows: [] })), - end: jest.fn(() => Promise.resolve()) - })) -})); - - - -describe('PetHistory Lambda Function', () => { - beforeEach(() => { - process.env.RDS_SECRET_ARN = 'arn:aws:secretsmanager:us-west-2:123456789012:secret:test-secret'; - process.env.AWS_REGION = 'us-west-2'; - - // Clear the module cache to ensure fresh environment variables - jest.resetModules(); - }); - - afterEach(() => { - jest.clearAllMocks(); - // Clean up environment variables - delete process.env.RDS_SECRET_ARN; - delete process.env.AWS_REGION; - }); - - test('should process valid adoption message successfully', async () => { - const event = { - Records: [ - { - messageId: 'test-message-id', - receiptHandle: 'test-receipt-handle', - body: JSON.stringify({ - transactionId: '123e4567-e89b-12d3-a456-426614174000', - petId: 'pet123', - petType: 'dog', - userId: 'user456', - adoptionDate: '2025-08-08T10:30:00Z', - timestamp: '2025-08-08T10:30:00Z' - }) - } - ] - }; - - const result = await handler(event, {}); - - expect(result.batchItemFailures).toEqual([]); - expect(result.processedCount).toBe(1); - expect(result.results[0].status).toBe('success'); - expect(result.results[0].messageId).toBe('test-message-id'); - }); - - test('should handle invalid JSON message with batch failure', async () => { - const event = { - Records: [ - { - messageId: 'test-message-id', - receiptHandle: 'test-receipt-handle', - body: 'invalid json' - } - ] - }; - - const result = await handler(event, {}); - - expect(result.batchItemFailures).toEqual([ - { itemIdentifier: 'test-message-id' } - ]); - expect(result.processedCount).toBe(0); - expect(result.results).toHaveLength(0); - }); - - test('should handle message with invalid schema with batch failure', async () => { - const event = { - Records: [ - { - messageId: 'test-message-id', - receiptHandle: 'test-receipt-handle', - body: JSON.stringify({ - transactionId: 'invalid-uuid', - petId: 'pet123', - // Missing required fields - }) - } - ] - }; - - const result = await handler(event, {}); - - expect(result.batchItemFailures).toEqual([ - { itemIdentifier: 'test-message-id' } - ]); - expect(result.processedCount).toBe(0); - expect(result.results).toHaveLength(0); - }); - - test('should process multiple messages', async () => { - const event = { - Records: [ - { - messageId: 'test-message-id-1', - receiptHandle: 'test-receipt-handle-1', - body: JSON.stringify({ - transactionId: '123e4567-e89b-12d3-a456-426614174001', - petId: 'pet123', - petType: 'dog', - userId: 'user456', - adoptionDate: '2025-08-08T10:30:00Z', - timestamp: '2025-08-08T10:30:00Z' - }) - }, - { - messageId: 'test-message-id-2', - receiptHandle: 'test-receipt-handle-2', - body: JSON.stringify({ - transactionId: '123e4567-e89b-12d3-a456-426614174002', - petId: 'pet456', - petType: 'cat', - userId: 'user789', - adoptionDate: '2025-08-08T11:30:00Z', - timestamp: '2025-08-08T11:30:00Z' - }) - } - ] - }; - - const result = await handler(event, {}); - - expect(result.batchItemFailures).toEqual([]); - expect(result.processedCount).toBe(2); - expect(result.results).toHaveLength(2); - expect(result.results[0].status).toBe('success'); - expect(result.results[1].status).toBe('success'); - }); - - test('should validate message structure correctly', async () => { - const event = { - Records: [ - { - messageId: 'test-message-id', - receiptHandle: 'test-receipt-handle', - body: JSON.stringify({ - transactionId: '123e4567-e89b-12d3-a456-426614174000', - petId: 'pet123', - petType: 'dog', - userId: 'user456', - adoptionDate: '2025-08-08T10:30:00Z', - timestamp: '2025-08-08T10:30:00Z' - }) - } - ] - }; - - const result = await handler(event, {}); - - expect(result.batchItemFailures).toEqual([]); - expect(result.processedCount).toBe(1); - expect(result.results[0].transactionId).toBe('123e4567-e89b-12d3-a456-426614174000'); - expect(result.results[0].messageId).toBe('test-message-id'); - expect(result.results[0].status).toBe('success'); - }); - - test('should handle empty records array', async () => { - const event = { - Records: [] - }; - - const result = await handler(event, {}); - - expect(result.batchItemFailures).toEqual([]); - expect(result.processedCount).toBe(0); - expect(result.results).toHaveLength(0); - }); - - test('should handle mixed success and failure messages', async () => { - const event = { - Records: [ - { - messageId: 'success-message', - receiptHandle: 'test-receipt-handle-1', - body: JSON.stringify({ - transactionId: '123e4567-e89b-12d3-a456-426614174000', - petId: 'pet123', - petType: 'dog', - userId: 'user456', - adoptionDate: '2025-08-08T10:30:00Z', - timestamp: '2025-08-08T10:30:00Z' - }) - }, - { - messageId: 'failure-message', - receiptHandle: 'test-receipt-handle-2', - body: 'invalid json' - }, - { - messageId: 'another-success-message', - receiptHandle: 'test-receipt-handle-3', - body: JSON.stringify({ - transactionId: '456e7890-e89b-12d3-a456-426614174001', - petId: 'pet456', - petType: 'cat', - userId: 'user789', - adoptionDate: '2025-08-08T11:30:00Z', - timestamp: '2025-08-08T11:30:00Z' - }) - } - ] - }; - - const result = await handler(event, {}); - - expect(result.batchItemFailures).toEqual([ - { itemIdentifier: 'failure-message' } - ]); - expect(result.processedCount).toBe(2); - expect(result.results).toHaveLength(2); - expect(result.results[0].messageId).toBe('success-message'); - expect(result.results[0].status).toBe('success'); - expect(result.results[1].messageId).toBe('another-success-message'); - expect(result.results[1].status).toBe('success'); - }); - - test('should handle all messages failing', async () => { - const event = { - Records: [ - { - messageId: 'failure-message-1', - receiptHandle: 'test-receipt-handle-1', - body: 'invalid json 1' - }, - { - messageId: 'failure-message-2', - receiptHandle: 'test-receipt-handle-2', - body: 'invalid json 2' - } - ] - }; - - const result = await handler(event, {}); - - expect(result.batchItemFailures).toEqual([ - { itemIdentifier: 'failure-message-1' }, - { itemIdentifier: 'failure-message-2' } - ]); - expect(result.processedCount).toBe(0); - expect(result.results).toHaveLength(0); - }); - - test('should return correct response format for SQS batch processing', async () => { - const event = { - Records: [ - { - messageId: 'test-message-id', - receiptHandle: 'test-receipt-handle', - body: JSON.stringify({ - transactionId: '123e4567-e89b-12d3-a456-426614174000', - petId: 'pet123', - petType: 'dog', - userId: 'user456', - adoptionDate: '2025-08-08T10:30:00Z', - timestamp: '2025-08-08T10:30:00Z' - }) - } - ] - }; - - const result = await handler(event, {}); - - // Verify the response has the correct structure for SQS batch processing - expect(result).toHaveProperty('batchItemFailures'); - expect(result).toHaveProperty('processedCount'); - expect(result).toHaveProperty('results'); - expect(Array.isArray(result.batchItemFailures)).toBe(true); - expect(Array.isArray(result.results)).toBe(true); - expect(typeof result.processedCount).toBe('number'); - }); - - -}); \ No newline at end of file diff --git a/PetAdoptions/petlistadoptions-py/docker-compose.yml b/PetAdoptions/petlistadoptions-py/docker-compose.yml deleted file mode 100644 index 0656b9f0..00000000 --- a/PetAdoptions/petlistadoptions-py/docker-compose.yml +++ /dev/null @@ -1,50 +0,0 @@ -version: '3.8' - -services: - # PostgreSQL database for local testing - postgres: - image: postgres:15 - environment: - POSTGRES_DB: adoptions - POSTGRES_USER: petstore - POSTGRES_PASSWORD: petstore123 - ports: - - "5432:5432" - volumes: - - postgres_data:/var/lib/postgresql/data - - ./init.sql:/docker-entrypoint-initdb.d/init.sql - healthcheck: - test: ["CMD-SHELL", "pg_isready -U petstore -d adoptions"] - interval: 10s - timeout: 5s - retries: 5 - - # Pet List Adoptions Service - petlistadoptions: - build: . - ports: - - "80:80" - environment: - - APP_PET_SEARCH_URL=http://localhost:8080/ - - APP_RDS_SECRET_ARN=local-secret - - OTEL_EXPORTER_OTLP_ENDPOINT=0.0.0.0:4317 - depends_on: - postgres: - condition: service_healthy - volumes: - - ./local-secret.json:/app/local-secret.json:ro - command: ["python", "app.py"] - - # Mock Pet Search Service (for testing) - petsearch-mock: - image: mockserver/mockserver:latest - ports: - - "8080:1080" - environment: - - MOCKSERVER_PROPERTY_FILE=/config/mockserver.properties - volumes: - - ./mock-petsearch.json:/config/mock-petsearch.json:ro - command: ["java", "-jar", "/opt/mockserver/mockserver-netty-jar-with-dependencies.jar", "-serverPort", "1080", "-logLevel", "INFO"] - -volumes: - postgres_data: \ No newline at end of file diff --git a/PetAdoptions/petlistadoptions-py/requirements.txt b/PetAdoptions/petlistadoptions-py/requirements.txt deleted file mode 100644 index cfb6bd1d..00000000 --- a/PetAdoptions/petlistadoptions-py/requirements.txt +++ /dev/null @@ -1,7 +0,0 @@ -fastapi==0.104.1 -uvicorn[standard]==0.24.0 -psycopg2-binary==2.9.7 -requests==2.31.0 -boto3==1.28.44 -prometheus-client==0.17.1 -python-dotenv==1.0.0 \ No newline at end of file diff --git a/PetAdoptions/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/RandomNumberGenerator.java b/PetAdoptions/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/RandomNumberGenerator.java deleted file mode 100644 index 045d679f..00000000 --- a/PetAdoptions/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/RandomNumberGenerator.java +++ /dev/null @@ -1,5 +0,0 @@ -package ca.petsearch; - -public interface RandomNumberGenerator { - int nextNonNegativeInt(int max); -} diff --git a/PetAdoptions/petsearch-java/src/main/java/ca/petsearch/RandomNumberGenerator.java b/PetAdoptions/petsearch-java/src/main/java/ca/petsearch/RandomNumberGenerator.java deleted file mode 100644 index 045d679f..00000000 --- a/PetAdoptions/petsearch-java/src/main/java/ca/petsearch/RandomNumberGenerator.java +++ /dev/null @@ -1,5 +0,0 @@ -package ca.petsearch; - -public interface RandomNumberGenerator { - int nextNonNegativeInt(int max); -} diff --git a/PetAdoptions/petsite/petsite/Controllers/FoodServiceController.cs b/PetAdoptions/petsite/petsite/Controllers/FoodServiceController.cs deleted file mode 100644 index d574adb6..00000000 --- a/PetAdoptions/petsite/petsite/Controllers/FoodServiceController.cs +++ /dev/null @@ -1,79 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Net.Http; -using System.Text.Json; -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; -using Microsoft.AspNetCore.Http; -using PetSite.Models; -using PetSite.Helpers; - -namespace PetSite.Controllers -{ - public class FoodServiceController : BaseController - { - private readonly IHttpClientFactory _httpClientFactory; - private readonly IConfiguration _configuration; - private readonly ILogger _logger; - - public FoodServiceController(IHttpClientFactory httpClientFactory, IConfiguration configuration, ILogger logger) - { - _httpClientFactory = httpClientFactory; - _configuration = configuration; - _logger = logger; - } - - [HttpGet] - public async Task GetFoods() - { - if (EnsureUserId()) return new EmptyResult(); - - try - { - using var httpClient = _httpClientFactory.CreateClient(); - var foodApiUrl = _configuration["FOOD_API_URL"] ?? "https://api.example.com/foods"; - var userId = ViewBag.UserId?.ToString(); - var url = UrlHelper.BuildUrl(foodApiUrl, ("userId", userId)); - var response = await httpClient.GetAsync(url); - response.EnsureSuccessStatusCode(); - - var jsonContent = await response.Content.ReadAsStringAsync(); - var foodResponse = JsonSerializer.Deserialize(jsonContent); - - return Json(foodResponse?.foods ?? new List()); - } - catch (Exception ex) - { - _logger.LogError(ex, "Error fetching food data"); - return Json(new List()); - } - } - - [HttpPost] - public async Task BuyFood(string foodId, string userId) - { - if (EnsureUserId()) return new EmptyResult(); - - try - { - using var httpClient = _httpClientFactory.CreateClient(); - var purchaseApiUrl = _configuration["FOOD_PURCHASE_API_URL"] ?? "https://api.example.com/purchase"; - // var userId = ViewBag.UserId?.ToString(); - var url = UrlHelper.BuildUrl(purchaseApiUrl, ("foodId", foodId), ("userId", userId)); - var response = await httpClient.PostAsync(url, null); - response.EnsureSuccessStatusCode(); - - // Food purchase successful - could add ViewData or redirect with status - } - catch (Exception ex) - { - _logger.LogError(ex, "Error purchasing food"); - // Food purchase failed - could add ViewData or redirect with error - } - - return RedirectToAction("Index", "Payment", new { userId = userId }); - } - } -} \ No newline at end of file diff --git a/PetAdoptions/petsite/petsite/Helpers/UrlHelper.cs b/PetAdoptions/petsite/petsite/Helpers/UrlHelper.cs deleted file mode 100644 index 3cf22429..00000000 --- a/PetAdoptions/petsite/petsite/Helpers/UrlHelper.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; - -namespace PetSite.Helpers -{ - public static class UrlHelper - { - public static string BuildUrl(string baseUrl, params (string key, string value)[] parameters) - { - if (string.IsNullOrEmpty(baseUrl)) - return string.Empty; - - var url = baseUrl; - var hasQuery = url.Contains("?"); - - foreach (var (key, value) in parameters) - { - if (!string.IsNullOrEmpty(value)) - { - var separator = hasQuery ? "&" : "?"; - url += $"{separator}{key}={Uri.EscapeDataString(value)}"; - hasQuery = true; - } - } - - return url; - } - } -} \ No newline at end of file diff --git a/PetAdoptions/petsite/petsite/Views/Payment/Index.cshtml b/PetAdoptions/petsite/petsite/Views/Payment/Index.cshtml deleted file mode 100644 index f643fa48..00000000 --- a/PetAdoptions/petsite/petsite/Views/Payment/Index.cshtml +++ /dev/null @@ -1,110 +0,0 @@ -@{ - ViewData["Title"] = "Complete Adoption"; -} - -
-
- -
-
-
- -
- @if (ViewData["txStatus"].ToString() == "success") - { - - - } - else - { -
-
- -
-

Error @ViewData["txStatus"]

- } -
- -
-

Pet Food Available

-
- -
-
- - \ No newline at end of file diff --git a/PetAdoptions/petsite/petsite/Views/Shared/_PetItem.cshtml b/PetAdoptions/petsite/petsite/Views/Shared/_PetItem.cshtml deleted file mode 100644 index f60ff87d..00000000 --- a/PetAdoptions/petsite/petsite/Views/Shared/_PetItem.cshtml +++ /dev/null @@ -1,44 +0,0 @@ -@using Microsoft.AspNetCore.Http.Features -@using Microsoft.CodeAnalysis.CSharp.Syntax -@model Pet; - -
-
- - - - @Model.pettype-@Model.petcolor - - - $@Model.price - - -
- @for (int i = 0; i < Int32.Parse(Model.cuteness_rate); i++) - { - - } -
-
- - @if (Model.availability == "yes") - { -
- - } - else - { -
-
- Unavailable - } - - - - - - - - - -
diff --git a/PetAdoptions/petsite/petsite/wwwroot/css/site.min.css b/PetAdoptions/petsite/petsite/wwwroot/css/site.min.css deleted file mode 100644 index e6d1e752..00000000 --- a/PetAdoptions/petsite/petsite/wwwroot/css/site.min.css +++ /dev/null @@ -1 +0,0 @@ -.pet-hero{background-image:url("../images/main_banner.png");background-size:cover;height:135px;width:100%}.pet-hero-title{position:relative;top:18.28571px}.pet-items{margin-top:1rem}.pet-item{margin-bottom:1.5rem;text-align:center;width:33%;display:inline-block;float:none !important}.pet-list-item{font-size:1rem;font-weight:300}@media screen and (max-width:1024px){.pet-item{width:50%}}@media screen and (max-width:768px){.pet-item{width:100%}}.pet-footer{background-color:#000;border-top:1px solid #eee;margin-top:2.5rem;padding-bottom:2.5rem;padding-top:2.5rem;width:100%;bottom:0}.pet-footer-brand{height:50px;width:230px}.pet-header{margin:0;background-color:#000}.pet-wrapper{display:flex;min-height:100vh;flex-direction:column;justify-content:flex-start}.pet-filters{background-color:rgba(19,126,68,.67);height:65px}.pet-filter{border-radius:.5rem;-webkit-appearance:none;background-color:transparent;border-color:#033c40;color:#fff;cursor:pointer;margin-right:1rem;margin-top:.5rem;min-width:140px;outline-color:#033c40;padding-bottom:0;padding-left:.5rem;padding-right:.5rem;padding-top:1.5rem}.pet-filter option{background-color:#5096e7}.pet-label{display:inline-block;position:relative;z-index:0}.pet-label::before{color:#ede90b;content:attr(data-title);font-size:.8rem;font-weight:bold;margin-left:.5rem;margin-top:.65rem;position:absolute;text-transform:uppercase;z-index:1}.pet-label::after{background-image:url("../images/arrow-down.png");content:'';height:7px;position:absolute;right:1.5rem;top:2.5rem;width:10px;z-index:1}.pet-send{background-color:#dc7406;color:#efd912;cursor:pointer;font-size:1rem;margin-top:-1.5rem;transition:all .35s}.pet-send:hover{background-color:#db7603;transition:all .35s}.pet-thumbnail{max-width:370px;width:100%}.pet-button{border-radius:.5rem;background-color:#2d7f83;border:0;color:#fff;cursor:pointer;font-size:1rem;height:3rem;margin-top:1rem;transition:all .35s;width:50%}.pet-button.is-disabled{opacity:.5;pointer-events:none}.pet-button:hover{font-size:1.5rem;background-color:#0fa828;transition:all .35s}.pet-name{font-size:1rem;font-stretch:condensed;font-weight:300;margin-top:.5rem;text-align:center;text-transform:lowercase}.pet-price{font-size:25px;font-weight:400;text-align:center}.pet-price::before{content:'$'}a.navbar-brand{white-space:normal;text-align:center;word-break:break-all}a{color:#19dc91}.btn-primary{color:#fff;background-color:#19dc91;border-color:#19dc91}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#19dc91;border-color:#19dc91}html{font-size:14px}@media(min-width:768px){html{font-size:16px}}.border-top{border-top:1px solid #e5e5e5}.border-bottom{border-bottom:1px solid #e5e5e5}.box-shadow{box-shadow:0 .25rem .75rem rgba(0,0,0,.05)}button.accept-policy{font-size:1rem;line-height:inherit}html{position:relative;min-height:100%}body{margin-bottom:60px}.footer{position:absolute;bottom:0;width:100%;white-space:nowrap;line-height:60px} \ No newline at end of file diff --git a/PetAdoptions/petstatusupdater/.gitignore b/PetAdoptions/petstatusupdater/.gitignore deleted file mode 100644 index b512c09d..00000000 --- a/PetAdoptions/petstatusupdater/.gitignore +++ /dev/null @@ -1 +0,0 @@ -node_modules \ No newline at end of file diff --git a/PetAdoptions/petstatusupdater/index.js b/PetAdoptions/petstatusupdater/index.js deleted file mode 100644 index 6b4859be..00000000 --- a/PetAdoptions/petstatusupdater/index.js +++ /dev/null @@ -1,41 +0,0 @@ -'use strict'; - -var AWSXRay = require('aws-xray-sdk'); -var AWS = AWSXRay.captureAWS(require('aws-sdk')); -var documentClient = new AWS.DynamoDB.DocumentClient(); - -exports.handler = async function (event, context, callback) { - var payload = JSON.parse(event.body); - - var availability = "yes"; - if (payload.petavailability === undefined) { - availability = "no"; - } - var params = { - TableName: process.env.TABLE_NAME, - Key: { - "pettype": payload.pettype, - "petid": payload.petid - }, - UpdateExpression: "set availability = :r", - ExpressionAttributeValues: { - ":r": availability - }, ReturnValues: "UPDATED_NEW" - }; - - await updatePetadoptionsTable(params); - - console.log("Updated petid: " + payload.petid + ", pettype: " + payload.pettype + ", to availability: " + availability); - return { "statusCode": 200, "body": "success" }; -}; - -async function updatePetadoptionsTable(params) { - await documentClient.update(params, function (err, data) { - if (err) { - console.log(JSON.stringify(err, null, 2)); - } else { - console.log(JSON.stringify(data, null, 2)); - // console.log("Updated petid: "+payload.petid +", pettype: "+payload.pettype+ " to availability: "+availability); - } - }).promise(); -} diff --git a/PetAdoptions/petstatusupdater/package-lock.json b/PetAdoptions/petstatusupdater/package-lock.json deleted file mode 100644 index a7751d03..00000000 --- a/PetAdoptions/petstatusupdater/package-lock.json +++ /dev/null @@ -1,572 +0,0 @@ -{ - "name": "petstatusupdater", - "version": "1.0.0", - "lockfileVersion": 2, - "requires": true, - "packages": { - "": { - "name": "petstatusupdater", - "version": "1.0.0", - "license": "ISC", - "dependencies": { - "aws-xray-sdk": "^3.3.3" - } - }, - "node_modules/@aws-sdk/service-error-classification": { - "version": "3.29.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/service-error-classification/-/service-error-classification-3.29.0.tgz", - "integrity": "sha512-VqOjXXTLTGbifzg3Fg2g/Ac6W3uzC3llPZjm/b0goM17KLWMGU7JKiem2l+CFyN4sxkver7InNlIUJCJAPB6+Q==", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@aws-sdk/types": { - "version": "3.29.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.29.0.tgz", - "integrity": "sha512-8ilWQU5ZTdiRfblmmjl38+6JZKKM8EqA5Sbn8djgDLShCLeVJ2TsL2guzNi+WHcL7BHdv1pI/NNmTcgRUo6yOw==", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/@types/body-parser": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.1.tgz", - "integrity": "sha512-a6bTJ21vFOGIkwM0kzh9Yr89ziVxq4vYH2fQ6N8AeipEzai/cFK6aGMArIkUeIdRIgpwQa+2bXiLuUJCpSf2Cg==", - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/cls-hooked": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@types/cls-hooked/-/cls-hooked-4.3.3.tgz", - "integrity": "sha512-gNstDTb/ty5h6gJd6YpSPgsLX9LmRpaKJqGFp7MRlYxhwp4vXXKlJ9+bt1TZ9KbVNXE+Mbxy2AYXcpY21DDtJw==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/connect": { - "version": "3.4.35", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", - "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/express": { - "version": "4.17.13", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", - "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.18", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "4.17.24", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.24.tgz", - "integrity": "sha512-3UJuW+Qxhzwjq3xhwXm2onQcFHn76frIYVbTu+kn24LFxI+dEhdfISDFovPB8VpEgW8oQCTpRuCe+0zJxB7NEA==", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*" - } - }, - "node_modules/@types/mime": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", - "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" - }, - "node_modules/@types/mysql": { - "version": "2.15.19", - "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.19.tgz", - "integrity": "sha512-wSRg2QZv14CWcZXkgdvHbbV2ACufNy5EgI8mBBxnJIptchv7DBy/h53VMa2jDhyo0C9MO4iowE6z9vF8Ja1DkQ==", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/node": { - "version": "16.7.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.7.10.tgz", - "integrity": "sha512-S63Dlv4zIPb8x6MMTgDq5WWRJQe56iBEY0O3SOFA9JrRienkOVDXSXBjjJw6HTNQYSE2JI6GMCR6LVbIMHJVvA==" - }, - "node_modules/@types/pg": { - "version": "8.6.1", - "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.6.1.tgz", - "integrity": "sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==", - "dependencies": { - "@types/node": "*", - "pg-protocol": "*", - "pg-types": "^2.2.0" - } - }, - "node_modules/@types/qs": { - "version": "6.9.7", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" - }, - "node_modules/@types/range-parser": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", - "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" - }, - "node_modules/@types/serve-static": { - "version": "1.13.10", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", - "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "node_modules/async-hook-jl": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/async-hook-jl/-/async-hook-jl-1.7.6.tgz", - "integrity": "sha512-gFaHkFfSxTjvoxDMYqDuGHlcRyUuamF8s+ZTtJdDzqjws4mCt7v0vuV79/E2Wr2/riMQgtG4/yUtXWs1gZ7JMg==", - "dependencies": { - "stack-chain": "^1.3.7" - }, - "engines": { - "node": "^4.7 || >=6.9 || >=7.3" - } - }, - "node_modules/atomic-batcher": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/atomic-batcher/-/atomic-batcher-1.0.2.tgz", - "integrity": "sha1-0WkB0QzOxZUWwZe5zNiTBom4E7Q=" - }, - "node_modules/aws-xray-sdk": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/aws-xray-sdk/-/aws-xray-sdk-3.3.3.tgz", - "integrity": "sha512-bGO/HolPGW8N39wUuERbn0GPlwgAmDrgt7nKYXR2ecRq2YauKQOHS8K+kD4aZ6QSZnlP5CrYMmtU6BnMkt3DUQ==", - "dependencies": { - "aws-xray-sdk-core": "3.3.3", - "aws-xray-sdk-express": "3.3.3", - "aws-xray-sdk-mysql": "3.3.3", - "aws-xray-sdk-postgres": "3.3.3" - }, - "engines": { - "node": ">= 10.x" - } - }, - "node_modules/aws-xray-sdk-core": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/aws-xray-sdk-core/-/aws-xray-sdk-core-3.3.3.tgz", - "integrity": "sha512-S8o+ZY12wEnDQolC5RGQ8RHZqezHeV3l/ODqrYOJAlRjT92FDutQkQqTfr+hD1Ia+puIXzL9U7eyVSsKmoI+1w==", - "dependencies": { - "@aws-sdk/service-error-classification": "^3.4.1", - "@aws-sdk/types": "^3.4.1", - "@types/cls-hooked": "^4.3.3", - "atomic-batcher": "^1.0.2", - "cls-hooked": "^4.2.2", - "semver": "^5.3.0" - }, - "engines": { - "node": ">= 10.x" - } - }, - "node_modules/aws-xray-sdk-express": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/aws-xray-sdk-express/-/aws-xray-sdk-express-3.3.3.tgz", - "integrity": "sha512-VLDfpWWlYUEqA/f3OWaPHk6lAlONrAbG8ECcCPKT3skhWe8LL3bCDFZJOs08V+3ZgBCjm8iMStlXxnNu+XHevw==", - "dependencies": { - "@types/express": "*" - }, - "engines": { - "node": ">= 10.x" - }, - "peerDependencies": { - "aws-xray-sdk-core": "^3.3.3" - } - }, - "node_modules/aws-xray-sdk-mysql": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/aws-xray-sdk-mysql/-/aws-xray-sdk-mysql-3.3.3.tgz", - "integrity": "sha512-BkwrT5Zu+og1nT+g0FgQRpdidKr3g9snTFHsirwbJATO0pmGNuuUjBUMTFyydTTSwa7ocqL9wPsmOmqY9kPZig==", - "dependencies": { - "@types/mysql": "*" - }, - "engines": { - "node": ">= 10.x" - }, - "peerDependencies": { - "aws-xray-sdk-core": "^3.3.3" - } - }, - "node_modules/aws-xray-sdk-postgres": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/aws-xray-sdk-postgres/-/aws-xray-sdk-postgres-3.3.3.tgz", - "integrity": "sha512-Fu6+XpkiOgyFiivKj3kppXdYzwGIoijiq1q9FJb/tbxfOLSTHunJpS5WzTuNdzOUJdvTtPDimcE4jjqS+4a5FQ==", - "dependencies": { - "@types/pg": "*" - }, - "engines": { - "node": ">= 10.x" - }, - "peerDependencies": { - "aws-xray-sdk-core": "^3.3.3" - } - }, - "node_modules/cls-hooked": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/cls-hooked/-/cls-hooked-4.2.2.tgz", - "integrity": "sha512-J4Xj5f5wq/4jAvcdgoGsL3G103BtWpZrMo8NEinRltN+xpTZdI+M38pyQqhuFU/P792xkMFvnKSf+Lm81U1bxw==", - "dependencies": { - "async-hook-jl": "^1.7.6", - "emitter-listener": "^1.0.1", - "semver": "^5.4.1" - }, - "engines": { - "node": "^4.7 || >=6.9 || >=7.3 || >=8.2.1" - } - }, - "node_modules/emitter-listener": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/emitter-listener/-/emitter-listener-1.1.2.tgz", - "integrity": "sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ==", - "dependencies": { - "shimmer": "^1.2.0" - } - }, - "node_modules/pg-int8": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", - "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/pg-protocol": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.5.0.tgz", - "integrity": "sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ==" - }, - "node_modules/pg-types": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", - "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", - "dependencies": { - "pg-int8": "1.0.1", - "postgres-array": "~2.0.0", - "postgres-bytea": "~1.0.0", - "postgres-date": "~1.0.4", - "postgres-interval": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postgres-array": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", - "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", - "engines": { - "node": ">=4" - } - }, - "node_modules/postgres-bytea": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", - "integrity": "sha1-AntTPAqokOJtFy1Hz5zOzFIazTU=", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-date": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", - "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-interval": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", - "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", - "dependencies": { - "xtend": "^4.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "bin": { - "semver": "bin/semver" - } - }, - "node_modules/shimmer": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz", - "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==" - }, - "node_modules/stack-chain": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/stack-chain/-/stack-chain-1.3.7.tgz", - "integrity": "sha1-0ZLJ/06moiyUxN1FkXHj8AzqEoU=" - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "engines": { - "node": ">=0.4" - } - } - }, - "dependencies": { - "@aws-sdk/service-error-classification": { - "version": "3.29.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/service-error-classification/-/service-error-classification-3.29.0.tgz", - "integrity": "sha512-VqOjXXTLTGbifzg3Fg2g/Ac6W3uzC3llPZjm/b0goM17KLWMGU7JKiem2l+CFyN4sxkver7InNlIUJCJAPB6+Q==" - }, - "@aws-sdk/types": { - "version": "3.29.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.29.0.tgz", - "integrity": "sha512-8ilWQU5ZTdiRfblmmjl38+6JZKKM8EqA5Sbn8djgDLShCLeVJ2TsL2guzNi+WHcL7BHdv1pI/NNmTcgRUo6yOw==" - }, - "@types/body-parser": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.1.tgz", - "integrity": "sha512-a6bTJ21vFOGIkwM0kzh9Yr89ziVxq4vYH2fQ6N8AeipEzai/cFK6aGMArIkUeIdRIgpwQa+2bXiLuUJCpSf2Cg==", - "requires": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "@types/cls-hooked": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/@types/cls-hooked/-/cls-hooked-4.3.3.tgz", - "integrity": "sha512-gNstDTb/ty5h6gJd6YpSPgsLX9LmRpaKJqGFp7MRlYxhwp4vXXKlJ9+bt1TZ9KbVNXE+Mbxy2AYXcpY21DDtJw==", - "requires": { - "@types/node": "*" - } - }, - "@types/connect": { - "version": "3.4.35", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.35.tgz", - "integrity": "sha512-cdeYyv4KWoEgpBISTxWvqYsVy444DOqehiF3fM3ne10AmJ62RSyNkUnxMJXHQWRQQX2eR94m5y1IZyDwBjV9FQ==", - "requires": { - "@types/node": "*" - } - }, - "@types/express": { - "version": "4.17.13", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz", - "integrity": "sha512-6bSZTPaTIACxn48l50SR+axgrqm6qXFIxrdAKaG6PaJk3+zuUr35hBlgT7vOmJcum+OEaIBLtHV/qloEAFITeA==", - "requires": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.18", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "@types/express-serve-static-core": { - "version": "4.17.24", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.24.tgz", - "integrity": "sha512-3UJuW+Qxhzwjq3xhwXm2onQcFHn76frIYVbTu+kn24LFxI+dEhdfISDFovPB8VpEgW8oQCTpRuCe+0zJxB7NEA==", - "requires": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*" - } - }, - "@types/mime": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.2.tgz", - "integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw==" - }, - "@types/mysql": { - "version": "2.15.19", - "resolved": "https://registry.npmjs.org/@types/mysql/-/mysql-2.15.19.tgz", - "integrity": "sha512-wSRg2QZv14CWcZXkgdvHbbV2ACufNy5EgI8mBBxnJIptchv7DBy/h53VMa2jDhyo0C9MO4iowE6z9vF8Ja1DkQ==", - "requires": { - "@types/node": "*" - } - }, - "@types/node": { - "version": "16.7.10", - "resolved": "https://registry.npmjs.org/@types/node/-/node-16.7.10.tgz", - "integrity": "sha512-S63Dlv4zIPb8x6MMTgDq5WWRJQe56iBEY0O3SOFA9JrRienkOVDXSXBjjJw6HTNQYSE2JI6GMCR6LVbIMHJVvA==" - }, - "@types/pg": { - "version": "8.6.1", - "resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.6.1.tgz", - "integrity": "sha512-1Kc4oAGzAl7uqUStZCDvaLFqZrW9qWSjXOmBfdgyBP5La7Us6Mg4GBvRlSoaZMhQF/zSj1C8CtKMBkoiT8eL8w==", - "requires": { - "@types/node": "*", - "pg-protocol": "*", - "pg-types": "^2.2.0" - } - }, - "@types/qs": { - "version": "6.9.7", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", - "integrity": "sha512-FGa1F62FT09qcrueBA6qYTrJPVDzah9a+493+o2PCXsesWHIn27G98TsSMs3WPNbZIEj4+VJf6saSFpvD+3Zsw==" - }, - "@types/range-parser": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.4.tgz", - "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==" - }, - "@types/serve-static": { - "version": "1.13.10", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.13.10.tgz", - "integrity": "sha512-nCkHGI4w7ZgAdNkrEu0bv+4xNV/XDqW+DydknebMOQwkpDGx8G+HTlj7R7ABI8i8nKxVw0wtKPi1D+lPOkh4YQ==", - "requires": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "async-hook-jl": { - "version": "1.7.6", - "resolved": "https://registry.npmjs.org/async-hook-jl/-/async-hook-jl-1.7.6.tgz", - "integrity": "sha512-gFaHkFfSxTjvoxDMYqDuGHlcRyUuamF8s+ZTtJdDzqjws4mCt7v0vuV79/E2Wr2/riMQgtG4/yUtXWs1gZ7JMg==", - "requires": { - "stack-chain": "^1.3.7" - } - }, - "atomic-batcher": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/atomic-batcher/-/atomic-batcher-1.0.2.tgz", - "integrity": "sha1-0WkB0QzOxZUWwZe5zNiTBom4E7Q=" - }, - "aws-xray-sdk": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/aws-xray-sdk/-/aws-xray-sdk-3.3.3.tgz", - "integrity": "sha512-bGO/HolPGW8N39wUuERbn0GPlwgAmDrgt7nKYXR2ecRq2YauKQOHS8K+kD4aZ6QSZnlP5CrYMmtU6BnMkt3DUQ==", - "requires": { - "aws-xray-sdk-core": "3.3.3", - "aws-xray-sdk-express": "3.3.3", - "aws-xray-sdk-mysql": "3.3.3", - "aws-xray-sdk-postgres": "3.3.3" - } - }, - "aws-xray-sdk-core": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/aws-xray-sdk-core/-/aws-xray-sdk-core-3.3.3.tgz", - "integrity": "sha512-S8o+ZY12wEnDQolC5RGQ8RHZqezHeV3l/ODqrYOJAlRjT92FDutQkQqTfr+hD1Ia+puIXzL9U7eyVSsKmoI+1w==", - "requires": { - "@aws-sdk/service-error-classification": "^3.4.1", - "@aws-sdk/types": "^3.4.1", - "@types/cls-hooked": "^4.3.3", - "atomic-batcher": "^1.0.2", - "cls-hooked": "^4.2.2", - "semver": "^5.3.0" - } - }, - "aws-xray-sdk-express": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/aws-xray-sdk-express/-/aws-xray-sdk-express-3.3.3.tgz", - "integrity": "sha512-VLDfpWWlYUEqA/f3OWaPHk6lAlONrAbG8ECcCPKT3skhWe8LL3bCDFZJOs08V+3ZgBCjm8iMStlXxnNu+XHevw==", - "requires": { - "@types/express": "*" - } - }, - "aws-xray-sdk-mysql": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/aws-xray-sdk-mysql/-/aws-xray-sdk-mysql-3.3.3.tgz", - "integrity": "sha512-BkwrT5Zu+og1nT+g0FgQRpdidKr3g9snTFHsirwbJATO0pmGNuuUjBUMTFyydTTSwa7ocqL9wPsmOmqY9kPZig==", - "requires": { - "@types/mysql": "*" - } - }, - "aws-xray-sdk-postgres": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/aws-xray-sdk-postgres/-/aws-xray-sdk-postgres-3.3.3.tgz", - "integrity": "sha512-Fu6+XpkiOgyFiivKj3kppXdYzwGIoijiq1q9FJb/tbxfOLSTHunJpS5WzTuNdzOUJdvTtPDimcE4jjqS+4a5FQ==", - "requires": { - "@types/pg": "*" - } - }, - "cls-hooked": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/cls-hooked/-/cls-hooked-4.2.2.tgz", - "integrity": "sha512-J4Xj5f5wq/4jAvcdgoGsL3G103BtWpZrMo8NEinRltN+xpTZdI+M38pyQqhuFU/P792xkMFvnKSf+Lm81U1bxw==", - "requires": { - "async-hook-jl": "^1.7.6", - "emitter-listener": "^1.0.1", - "semver": "^5.4.1" - } - }, - "emitter-listener": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/emitter-listener/-/emitter-listener-1.1.2.tgz", - "integrity": "sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ==", - "requires": { - "shimmer": "^1.2.0" - } - }, - "pg-int8": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", - "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==" - }, - "pg-protocol": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.5.0.tgz", - "integrity": "sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ==" - }, - "pg-types": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", - "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", - "requires": { - "pg-int8": "1.0.1", - "postgres-array": "~2.0.0", - "postgres-bytea": "~1.0.0", - "postgres-date": "~1.0.4", - "postgres-interval": "^1.1.0" - } - }, - "postgres-array": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", - "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==" - }, - "postgres-bytea": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", - "integrity": "sha1-AntTPAqokOJtFy1Hz5zOzFIazTU=" - }, - "postgres-date": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", - "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==" - }, - "postgres-interval": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", - "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", - "requires": { - "xtend": "^4.0.0" - } - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==" - }, - "shimmer": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz", - "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==" - }, - "stack-chain": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/stack-chain/-/stack-chain-1.3.7.tgz", - "integrity": "sha1-0ZLJ/06moiyUxN1FkXHj8AzqEoU=" - }, - "xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==" - } - } -} diff --git a/PetAdoptions/petstatusupdater/package.json b/PetAdoptions/petstatusupdater/package.json deleted file mode 100644 index 7cbc6b36..00000000 --- a/PetAdoptions/petstatusupdater/package.json +++ /dev/null @@ -1,14 +0,0 @@ -{ - "name": "petstatusupdater", - "version": "1.0.0", - "description": "Updates pet availability", - "main": "index.js", - "scripts": { - "test": "echo \"Error: no test specified\" && exit 1" - }, - "author": "", - "license": "ISC", - "dependencies": { - "aws-xray-sdk": "^3.3.3" - } -} diff --git a/PetAdoptions/trafficgenerator/.idea/.idea.trafficgenerator/.idea/.gitignore b/PetAdoptions/trafficgenerator/.idea/.idea.trafficgenerator/.idea/.gitignore deleted file mode 100644 index 5c98b428..00000000 --- a/PetAdoptions/trafficgenerator/.idea/.idea.trafficgenerator/.idea/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -# Default ignored files -/workspace.xml \ No newline at end of file diff --git a/PetAdoptions/trafficgenerator/.idea/.idea.trafficgenerator/.idea/encodings.xml b/PetAdoptions/trafficgenerator/.idea/.idea.trafficgenerator/.idea/encodings.xml deleted file mode 100644 index df87cf95..00000000 --- a/PetAdoptions/trafficgenerator/.idea/.idea.trafficgenerator/.idea/encodings.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - - \ No newline at end of file diff --git a/PetAdoptions/trafficgenerator/.idea/.idea.trafficgenerator/.idea/indexLayout.xml b/PetAdoptions/trafficgenerator/.idea/.idea.trafficgenerator/.idea/indexLayout.xml deleted file mode 100644 index 27ba142e..00000000 --- a/PetAdoptions/trafficgenerator/.idea/.idea.trafficgenerator/.idea/indexLayout.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/PetAdoptions/trafficgenerator/.idea/.idea.trafficgenerator/.idea/modules.xml b/PetAdoptions/trafficgenerator/.idea/.idea.trafficgenerator/.idea/modules.xml deleted file mode 100644 index 7fcbe8d8..00000000 --- a/PetAdoptions/trafficgenerator/.idea/.idea.trafficgenerator/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/PetAdoptions/trafficgenerator/.idea/.idea.trafficgenerator/.idea/projectSettingsUpdater.xml b/PetAdoptions/trafficgenerator/.idea/.idea.trafficgenerator/.idea/projectSettingsUpdater.xml deleted file mode 100644 index 7515e760..00000000 --- a/PetAdoptions/trafficgenerator/.idea/.idea.trafficgenerator/.idea/projectSettingsUpdater.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/PetAdoptions/trafficgenerator/.idea/.idea.trafficgenerator/.idea/vcs.xml b/PetAdoptions/trafficgenerator/.idea/.idea.trafficgenerator/.idea/vcs.xml deleted file mode 100644 index 6c0b8635..00000000 --- a/PetAdoptions/trafficgenerator/.idea/.idea.trafficgenerator/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/PetAdoptions/trafficgenerator/global.json b/PetAdoptions/trafficgenerator/global.json deleted file mode 100644 index b5b37b60..00000000 --- a/PetAdoptions/trafficgenerator/global.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "sdk": { - "version": "8.0.0", - "rollForward": "latestMajor", - "allowPrerelease": false - } -} \ No newline at end of file diff --git a/PetAdoptions/trafficgenerator/trafficgenerator.sln b/PetAdoptions/trafficgenerator/trafficgenerator.sln deleted file mode 100644 index e2dd0ca9..00000000 --- a/PetAdoptions/trafficgenerator/trafficgenerator.sln +++ /dev/null @@ -1,16 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "trafficgenerator", "trafficgenerator\trafficgenerator.csproj", "{C872A202-C201-46EC-AA25-C5FB9EF5AD4A}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {C872A202-C201-46EC-AA25-C5FB9EF5AD4A}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C872A202-C201-46EC-AA25-C5FB9EF5AD4A}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C872A202-C201-46EC-AA25-C5FB9EF5AD4A}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C872A202-C201-46EC-AA25-C5FB9EF5AD4A}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection -EndGlobal diff --git a/PetAdoptions/trafficgenerator/trafficgenerator/Dockerfile b/PetAdoptions/trafficgenerator/trafficgenerator/Dockerfile deleted file mode 100644 index c24607ee..00000000 --- a/PetAdoptions/trafficgenerator/trafficgenerator/Dockerfile +++ /dev/null @@ -1,19 +0,0 @@ -FROM mcr.microsoft.com/dotnet/aspnet:6.0-bullseye-slim AS base -WORKDIR /app -EXPOSE 80 -EXPOSE 443 - -FROM mcr.microsoft.com/dotnet/sdk:6.0-bullseye-slim AS build -WORKDIR /src -COPY . . -RUN dotnet restore "trafficgenerator.csproj" -RUN dotnet build "trafficgenerator.csproj" -c Release -o /app/build - -FROM build AS publish -RUN dotnet publish "trafficgenerator.csproj" -c Release -o /app/publish - -FROM base AS final -WORKDIR /app -#ENV DOTNET_ENVIRONMENT=Development -COPY --from=publish /app/publish . -ENTRYPOINT ["dotnet", "trafficgenerator.dll"] diff --git a/PetAdoptions/trafficgenerator/trafficgenerator/PetData.cs b/PetAdoptions/trafficgenerator/trafficgenerator/PetData.cs deleted file mode 100644 index 18140978..00000000 --- a/PetAdoptions/trafficgenerator/trafficgenerator/PetData.cs +++ /dev/null @@ -1,19 +0,0 @@ -using System.Collections.Generic; -using System.Diagnostics; - -namespace trafficgenerator -{ - public class Pet - { - public string pettype { get; set; } - public string petid { get; set; } - public string petcolor { get; set; } - public string availability { get; set; } - public bool IsProcessed { get; set; } - } - - public class Pets - { - public List AllPets { get; set; } - } -} \ No newline at end of file diff --git a/PetAdoptions/trafficgenerator/trafficgenerator/Program.cs b/PetAdoptions/trafficgenerator/trafficgenerator/Program.cs deleted file mode 100644 index 2dbdda83..00000000 --- a/PetAdoptions/trafficgenerator/trafficgenerator/Program.cs +++ /dev/null @@ -1,54 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.AspNetCore.Hosting; - -namespace trafficgenerator -{ - public class Program - { - public static void Main(string[] args) - { - CreateWebBuilder(args).Build().RunAsync(); - CreateHostBuilder(args).Build().Run(); - } - - public static IHostBuilder CreateHostBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureServices((hostContext, services) => - { - services.AddHostedService(); - - }) - .ConfigureAppConfiguration((hostingContext, config) => - { - var env = hostingContext.HostingEnvironment; - Console.WriteLine($"ENVIRONMENT NAME IS: {env.EnvironmentName}"); - if (env.EnvironmentName.ToLower() == "development") - config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) - .AddJsonFile($"appsettings.{env.EnvironmentName}.json", - optional: true, reloadOnChange: true); - else - config.AddSystemsManager(configureSource => - { - configureSource.Path = "/petstore"; - configureSource.Optional = true; - configureSource.ReloadAfter = TimeSpan.FromMinutes(5); - }); - - - }); - - public static IHostBuilder CreateWebBuilder(string[] args) => - Host.CreateDefaultBuilder(args) - .ConfigureWebHostDefaults(web => - { - web.UseStartup(); - }); - - } -} \ No newline at end of file diff --git a/PetAdoptions/trafficgenerator/trafficgenerator/Properties/launchSettings.json b/PetAdoptions/trafficgenerator/trafficgenerator/Properties/launchSettings.json deleted file mode 100644 index 38dd4fe5..00000000 --- a/PetAdoptions/trafficgenerator/trafficgenerator/Properties/launchSettings.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "profiles": { - "trafficgenerator": { - "commandName": "Project", - "environmentVariables": { - "DOTNET_ENVIRONMENT": "Development" - } - } - } -} diff --git a/PetAdoptions/trafficgenerator/trafficgenerator/Startup.cs b/PetAdoptions/trafficgenerator/trafficgenerator/Startup.cs deleted file mode 100644 index 4fdfa9dd..00000000 --- a/PetAdoptions/trafficgenerator/trafficgenerator/Startup.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Diagnostics.HealthChecks; - -namespace trafficgenerator -{ - public class Startup - { - public Startup(IConfiguration configuration) - { - Configuration = configuration; - } - - public IConfiguration Configuration { get; } - - // This method gets called by the runtime. Use this method to add services to the container. - public void ConfigureServices(IServiceCollection services) - { - services.AddHealthChecks().AddCheck("AlwaysGood",()=> HealthCheckResult.Healthy("All Good!")); - } - - // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - - if (env.IsDevelopment()) - { - app.UseDeveloperExceptionPage(); - } - - app.UseHealthChecks("/health"); - app.UseHealthChecks("/"); - - } - } -} \ No newline at end of file diff --git a/PetAdoptions/trafficgenerator/trafficgenerator/Worker.cs b/PetAdoptions/trafficgenerator/trafficgenerator/Worker.cs deleted file mode 100644 index bcf0d057..00000000 --- a/PetAdoptions/trafficgenerator/trafficgenerator/Worker.cs +++ /dev/null @@ -1,135 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Net.Http; -using System.Text; -using System.Threading; -using System.Threading.Tasks; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Hosting; -using Microsoft.Extensions.Logging; -using System.Text.Json; -using System.Text.Json.Serialization; - -namespace trafficgenerator -{ - public class Worker : BackgroundService - { - private readonly ILogger _logger; - - private HttpClient _httpClient; - private List _allPets; - private string _petSiteUrl; - private string _petSearchUrl; - private string _trafficdelaytime; - - public Worker(ILogger logger, IConfiguration configuration) - { - _logger = logger; - - _httpClient = new HttpClient(); - _petSiteUrl = configuration["petsiteurl"]; - _petSearchUrl = configuration["searchapiurl"]; - _trafficdelaytime = configuration["trafficdelaytime"]; - - } - - protected override async Task ExecuteAsync(CancellationToken stoppingToken) - { - while (!stoppingToken.IsCancellationRequested) - { - try - { - _logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now); - - await ThrowSomeTrafficIn(); - - Int32.TryParse(_trafficdelaytime, out int delaytime); - - delaytime = delaytime == 0 ? 1 : delaytime; - - _logger.LogInformation($"Delay time : {delaytime * 20} seconds"); - - await Task.Delay(delaytime * 20000, stoppingToken); - } - catch (Exception e) - { - Console.WriteLine(e.Message); - _logger.LogCritical(e.Message); - await Task.Delay(100000, stoppingToken); - } - } - } - - private async Task LoadPetData() - { - // Console.WriteLine($"Search URL: {_petSearchUrl}"); - - // Loads the Petdata from DynamoDB into memory - _allPets = JsonSerializer.Deserialize>( - await _httpClient.GetStringAsync(_petSearchUrl)); - } - - private async Task ThrowSomeTrafficIn() - { - _logger.LogInformation("Synchronous Housekeeping call"); - // Performs housekeeping. Basically, reset the application data and gets ready for the execution cycle - _httpClient.GetAsync( - $"{_petSiteUrl}/housekeeping/").Wait(); - - _logger.LogInformation("Starting Async LoadPetData"); - - await LoadPetData(); - - _logger.LogInformation($"Total number of pets - {_allPets.Count}"); - Random random = new Random(); - var loadSize = random.Next(5, _allPets.Count); - - // Console.WriteLine($"PetSite URL: {_petSiteUrl}"); - - if (loadSize > 20) - { - await _httpClient.DeleteAsync($"{_petSiteUrl}/pethistory/deletepetadoptionshistory"); - _logger.LogInformation("Deleted PetAdoptions History"); - } - else - { - await _httpClient.GetAsync($"{_petSiteUrl}/pethistory"); - } - - - for (int i = 0; i < loadSize; i++) - { - var currentPet = _allPets[random.Next(0, _allPets.Count - 1)]; - - // Console.WriteLine($"Searching: {_petSiteUrl}/?selectedPetType={currentPet.pettype}&selectedPetColor={currentPet.petcolor}"); - - //Performs a search query - await _httpClient.GetAsync( - $"{_petSiteUrl}/?selectedPetType={currentPet.pettype}&selectedPetColor={currentPet.petcolor}"); - - // Performs the "TakeMeHome" action on the current Pet in context - await _httpClient.PostAsync($"{_petSiteUrl}/Adoption/TakeMeHome", - new StringContent( - $"pettype={currentPet.pettype}&" + - $"petcolor={currentPet.petcolor}&" + - $"petid={currentPet.petid}", - Encoding.Default, "application/x-www-form-urlencoded")); - - // Completes adoption by making the payment - await _httpClient.PostAsync($"{_petSiteUrl}/Payment/MakePayment", - new StringContent( - $"pettype={currentPet.pettype}&" + - $"petid={currentPet.petid}", - Encoding.Default, "application/x-www-form-urlencoded")); - - // Lists all adopted pets - await _httpClient.GetAsync( - $"{_petSiteUrl}/PetListAdoptions"); - } - - - } - } -} \ No newline at end of file diff --git a/PetAdoptions/trafficgenerator/trafficgenerator/appsettings.Development.json b/PetAdoptions/trafficgenerator/trafficgenerator/appsettings.Development.json deleted file mode 100644 index 32497265..00000000 --- a/PetAdoptions/trafficgenerator/trafficgenerator/appsettings.Development.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - }, - "petsiteurl":"petsite-1088770206.us-east-1.elb.amazonaws.com", - "searchapiurl":"petsearch-live.us-east-1.elasticbeanstalk.com/api/search" -} diff --git a/PetAdoptions/trafficgenerator/trafficgenerator/appsettings.json b/PetAdoptions/trafficgenerator/trafficgenerator/appsettings.json deleted file mode 100644 index 8983e0fc..00000000 --- a/PetAdoptions/trafficgenerator/trafficgenerator/appsettings.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - } -} diff --git a/PetAdoptions/trafficgenerator/trafficgenerator/docker-compose.yml b/PetAdoptions/trafficgenerator/trafficgenerator/docker-compose.yml deleted file mode 100644 index 9dc6de60..00000000 --- a/PetAdoptions/trafficgenerator/trafficgenerator/docker-compose.yml +++ /dev/null @@ -1,4 +0,0 @@ -version: '3' -services: - trafficgenerator: - build: . diff --git a/PetAdoptions/trafficgenerator/trafficgenerator/trafficgenerator.csproj b/PetAdoptions/trafficgenerator/trafficgenerator/trafficgenerator.csproj deleted file mode 100644 index e807f7cd..00000000 --- a/PetAdoptions/trafficgenerator/trafficgenerator/trafficgenerator.csproj +++ /dev/null @@ -1,12 +0,0 @@ - - - netcoreapp6.0 - dotnet-trafficgenerator-24A0A2C8-664C-4353-AA89-98AB3F5B653C - - - - - - - - \ No newline at end of file diff --git a/architecture.md b/architecture.md new file mode 100644 index 00000000..0a3c8fc4 --- /dev/null +++ b/architecture.md @@ -0,0 +1,234 @@ + +# One Observability Demo - Infrastructure Architecture + +## Overview + +The One Observability Demo is deployed using AWS CDK (Cloud Development Kit) with a multi-stage pipeline architecture. The infrastructure is organized into distinct deployment stages that build upon each other, creating a comprehensive observability platform for demonstrating AWS monitoring and observability services. + +## CDK Pipeline Architecture + +The deployment follows a CDK Pipelines pattern with the following structure: + +### Pipeline Structure + +The CDK Pipeline (`CDKPipeline` class) orchestrates the deployment through two main waves: + +1. **Core Wave** - Contains foundational infrastructure and application containers +2. **Backend Wave** - Contains storage and compute resources + +### Deployment Stages + +#### 1. Core Stage +**Purpose**: Deploys foundational infrastructure components required by all other stages. + +**Resources Deployed**: +- **VPC and Networking**: Virtual Private Cloud with public/private subnets, NAT gateways, and internet gateway +- **Security Groups**: Network security configurations for different service tiers +- **IAM Roles and Policies**: Identity and access management for services +- **CloudTrail**: Audit logging for API calls +- **VPC Endpoints**: Private connectivity to AWS services +- **Core observability infrastructure**: Base monitoring and logging setup + +**Key Components**: +- `CoreStack` - Main infrastructure stack +- Network isolation and security boundaries +- Foundational observability components + +#### 2. Applications Stage (Containers Pipeline) +**Purpose**: Builds and manages containerized applications using ECR and CodePipeline, and deploys serverless functions. + +**Resources Deployed**: +- **Amazon ECR Repositories**: Individual container registries for each microservice +- **CodePipeline**: Dedicated CI/CD pipeline for building container images +- **CodeBuild Projects**: Build environments with Docker support for containerizing applications +- **IAM Roles**: Pipeline and CodeBuild service roles with ECR permissions +- **S3 Artifact Bucket**: Storage for pipeline artifacts +- **AWS Lambda Function**: Single serverless function for pet status updates + +**Pipeline Architecture**: +1. **Source Stage**: Retrieves source code from S3 bucket (same as main CDK pipeline source) +2. **Build Stage**: Parallel execution of container builds for all microservices + +**Microservices Built** (5 applications): +- **payforadoption-go** (Go): Payment processing service +- **petlistadoption-go** (Go): Pet listing service +- **petsearch-java** (Java): Pet search service with Elasticsearch integration +- **petsite** (.NET Core): Web frontend (deployed to EKS) +- **trafficgenerator** (Go): Load testing and traffic simulation service + +**Lambda Function**: +- **petupdater** (Node.js): Function for updating pet adoption status in DynamoDB + +**Key Features**: +- **Parallel Build Execution**: All 5 microservices build simultaneously for faster deployment +- **Automatic Image Scanning**: ECR vulnerability scanning enabled on push +- **Docker Multi-stage Builds**: Optimized container images with security scanning +- **Immutable Tags**: Container images tagged with 'latest' and build-specific tags +- **Cross-stage Integration**: Built images are consumed by Compute Stage for ECS/EKS deployment +- **Serverless Integration**: Lambda function deployed alongside containerized services + +#### 3. Storage Stage +**Purpose**: Deploys data persistence and storage solutions. + +**Resources Deployed**: +- **Amazon DynamoDB**: NoSQL database for application data +- **Amazon Aurora PostgreSQL**: Relational database for structured data +- **Amazon S3**: Object storage for assets and static content +- **Amazon SQS**: Message queuing for decoupled communication +- **Workshop Assets**: Pre-populated data and images for the demo + +**Key Components**: +- `StorageStack` containing: + - `QueueResources` - SQS queues for messaging + - `WorkshopAssets` - S3 buckets with demo data + - `DynamoDatabase` - NoSQL tables + - `AuroraDatabase` - PostgreSQL cluster + +#### 4. Compute Stage +**Purpose**: Deploys compute resources and container orchestration platforms. + +**Resources Deployed**: +- **Amazon ECS**: Container orchestration service with Fargate support +- **Amazon EKS**: Kubernetes cluster for container workloads +- **Application Load Balancers**: Traffic distribution for containerized services +- **Auto Scaling Groups**: Dynamic scaling capabilities for ECS and EKS +- **EC2 Instances**: Worker nodes for EKS cluster (optional) + +**Key Components**: +- `ComputeStack` containing: + - `WorkshopEcs` - ECS cluster with Fargate support + - `WorkshopEks` - EKS cluster with worker nodes + - Load balancers and target groups for service discovery + - Auto scaling configurations for both ECS and EKS + +**Note**: Lambda functions are deployed in the Applications Stage as part of the MicroservicesStack, not in the Compute Stage. + +## Pipeline Flow + +``` +Source (S3) → Synth → Core Wave → Backend Wave + ↓ ↓ + [Core Stage] [Storage Stage] + [Applications] [Compute Stage] +``` + +### Wave Execution + +1. **Core Wave** executes first and contains: + - Core infrastructure (networking, security, observability) + - Applications/Containers pipeline (builds container images) + +2. **Backend Wave** executes after Core Wave and contains: + - Storage resources (databases, queues, object storage) + - Compute resources (ECS, EKS, Lambda, load balancers) + +### Stage Dependencies + +- **Applications Stage** depends on Core Stage for networking and security +- **Storage Stage** depends on Core Stage for VPC and networking +- **Compute Stage** depends on both Core and Storage stages for infrastructure and data persistence + +## Key Features + +### Security +- **AWS NAG Suppressions**: Automated security compliance checking with documented exceptions +- **IAM Least Privilege**: Minimal required permissions for each service +- **VPC Isolation**: Network segmentation and private subnets +- **Encryption**: Data encryption at rest and in transit + +### Observability +- **AWS X-Ray**: Distributed tracing for microservices +- **Amazon CloudWatch**: Metrics, logs, and alarms +- **AWS CloudTrail**: API audit logging +- **Container Insights**: ECS and EKS monitoring +- **Application Performance Monitoring**: End-to-end observability + +### Scalability +- **Auto Scaling**: Dynamic resource scaling based on demand +- **Load Balancing**: Traffic distribution across multiple instances +- **Serverless Components**: AWS Lambda for event-driven processing +- **Container Orchestration**: ECS and EKS for scalable container workloads + +## Configuration + +The pipeline is configured through the `CDKPipelineProperties` interface: + +- **Source Configuration**: S3 bucket and key for source code +- **Application List**: Container definitions for applications to build +- **Database Configuration**: Aurora PostgreSQL engine version +- **Asset Paths**: S3 paths for demo assets and images +- **Tagging Strategy**: Consistent resource tagging across stages + +## Deployment Process + +1. **Source Stage**: Code is retrieved from S3 bucket +2. **Synth Stage**: CDK synthesizes CloudFormation templates +3. **Core Wave Deployment**: + - Core infrastructure is deployed first + - Applications pipeline builds container images in parallel +4. **Backend Wave Deployment**: + - Storage resources are deployed + - Compute resources are deployed with dependencies on storage + +This architecture provides a robust, scalable, and observable platform for demonstrating AWS monitoring and observability capabilities across containerized and serverless workloads. + +## Architecture Diagrams + +The following diagrams illustrate the infrastructure architecture and deployment flow: + +### Complete Architecture Overview +![Complete Architecture with Languages](./generated-diagrams/complete-architecture-with-languages.png) + +This diagram shows the complete infrastructure architecture including the detailed container pipeline with programming languages for each microservice and the Lambda function. + +### Applications Stage - Complete Architecture +![Applications Stage with Languages](./generated-diagrams/applications-stage-with-languages.png) + +This diagram provides a detailed view of the Applications Stage, showing how 5 microservices (Go, Java, .NET Core) are built in parallel, pushed to individual ECR repositories, and includes the single Lambda function (Node.js) deployment. + +### Microservices Architecture & Runtime Deployment +![Microservices Architecture with Languages](./generated-diagrams/microservices-architecture-with-languages.png) + +This diagram illustrates the runtime architecture showing how the built microservices are deployed across ECS and EKS with their respective programming languages, the single Lambda function integration, data access patterns, and observability integration. + +### Deployment Stages Flow +![Deployment Stages](./generated-diagrams/deployment-stages.png) + +This diagram illustrates the CDK pipeline deployment flow through the Core Wave and Backend Wave stages. + +### Stage-Specific Resource Diagrams + +#### Core Stage Resources +![Core Stage Resources](./generated-diagrams/core-stage-resources.png) + +Shows the foundational networking, security, and observability infrastructure deployed in the Core Stage. + +#### Storage Stage Resources +![Storage Stage Resources](./generated-diagrams/storage-stage-resources.png) + +Illustrates the data persistence layer including databases, object storage, and message queuing systems. + +#### Compute Stage Resources +![Compute Stage Resources Corrected](./generated-diagrams/compute-stage-resources-corrected.png) + +Details the compute infrastructure including container orchestration and load balancing (Lambda functions are deployed in the Applications Stage). + +#### Observability Architecture +![Observability Architecture](./generated-diagrams/observability-architecture.png) + +Demonstrates the comprehensive observability stack with metrics, logging, tracing, and monitoring capabilities. + +## Getting Started + +To deploy this infrastructure: + +1. Ensure you have AWS CDK installed and configured +2. Navigate to the `src/cdk` directory +3. Install dependencies: `npm install` +4. Configure your environment variables in `.env` +5. Deploy the pipeline: `cdk deploy` + +The pipeline will automatically deploy all stages in the correct order with proper dependency management. diff --git a/archive/README.md b/archive/README.md new file mode 100644 index 00000000..c106fb72 --- /dev/null +++ b/archive/README.md @@ -0,0 +1,9 @@ + +# Temporary folder + +All things that we're not sure will be needed. + +This folder will be removed before release. \ No newline at end of file diff --git a/PetAdoptions/getrole.sh b/archive/getrole.sh similarity index 79% rename from PetAdoptions/getrole.sh rename to archive/getrole.sh index 7c0ef539..b23ae7e8 100755 --- a/PetAdoptions/getrole.sh +++ b/archive/getrole.sh @@ -1,5 +1,8 @@ #!/bin/bash +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + currentRole=$(aws sts get-caller-identity --query Arn --output text) if echo ${currentRole} | grep -q assumed-role; then diff --git a/gitops/git-repository.yaml b/archive/gitops/git-repository.yaml similarity index 68% rename from gitops/git-repository.yaml rename to archive/gitops/git-repository.yaml index 36a63289..2a62599e 100644 --- a/gitops/git-repository.yaml +++ b/archive/gitops/git-repository.yaml @@ -1,3 +1,5 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 apiVersion: source.toolkit.fluxcd.io/v1beta2 kind: GitRepository metadata: diff --git a/gitops/grafana-kustomization.yaml b/archive/gitops/grafana-kustomization.yaml similarity index 73% rename from gitops/grafana-kustomization.yaml rename to archive/gitops/grafana-kustomization.yaml index 768cf9c5..a93acc4b 100644 --- a/gitops/grafana-kustomization.yaml +++ b/archive/gitops/grafana-kustomization.yaml @@ -1,3 +1,5 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 apiVersion: kustomize.toolkit.fluxcd.io/v1beta2 kind: Kustomization metadata: @@ -10,7 +12,7 @@ spec: sourceRef: kind: GitRepository name: grafana-repo - postBuild: + postBuild: substituteFrom: - kind: ConfigMap name: cluster-vars \ No newline at end of file diff --git a/grafana-dashboards/cluster.json b/archive/grafana-dashboards/cluster.json similarity index 100% rename from grafana-dashboards/cluster.json rename to archive/grafana-dashboards/cluster.json diff --git a/grafana-dashboards/kubelet.json b/archive/grafana-dashboards/kubelet.json similarity index 100% rename from grafana-dashboards/kubelet.json rename to archive/grafana-dashboards/kubelet.json diff --git a/grafana-dashboards/namespace-workloads.json b/archive/grafana-dashboards/namespace-workloads.json similarity index 100% rename from grafana-dashboards/namespace-workloads.json rename to archive/grafana-dashboards/namespace-workloads.json diff --git a/archive/grafana-dashboards/nodeexporter-nodes.json b/archive/grafana-dashboards/nodeexporter-nodes.json new file mode 100644 index 00000000..c2c01977 --- /dev/null +++ b/archive/grafana-dashboards/nodeexporter-nodes.json @@ -0,0 +1,1257 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": { + "type": "grafana", + "uid": "-- Grafana --" + }, + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "target": { + "limit": 100, + "matchAny": false, + "tags": [], + "type": "dashboard" + }, + "type": "dashboard" + } + ] + }, + "editable": true, + "fiscalYearStartMonth": 0, + "graphTooltip": 1, + "id": null, + "iteration": 1667228305223, + "links": [], + "liveNow": false, + "panels": [ + { + "collapsed": false, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 10, + "panels": [], + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "refId": "A" + } + ], + "title": "CPU", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 5, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "max": 1, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "percentunit" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 1 + }, + "id": 2, + "links": [], + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "right" + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.4.7", + "targets": [ + { + "datasource": { + "uid": "$datasource" + }, + "exemplar": false, + "expr": "sum (\n (1 - sum without (mode) (rate(node_cpu_seconds_total{job=\"node-exporter\", mode=~\"idle|iowait|steal\", nodename=~\"$instance\"}[5m])))\n/ ignoring(cpu) group_left\n count without (cpu, mode) (node_cpu_seconds_total{job=\"node-exporter\", mode=\"idle\", nodename=~\"$instance\"})\n) by (nodename)\n", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "{{nodename}}", + "refId": "A" + } + ], + "title": "CPU Usage", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Aggregated CPU load averages (average system load over a period of time)", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 2, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "short" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 1 + }, + "id": 3, + "links": [], + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.4.7", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": false, + "expr": "sum(node_load1{job=\"node-exporter\", nodename=~\"$instance\"})", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "1m load average", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": false, + "expr": "sum(node_load5{job=\"node-exporter\", nodename=~\"$instance\"})", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "5m load average", + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": false, + "expr": "sum(node_load15{job=\"node-exporter\", nodename=~\"$instance\"})", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "15m load average", + "refId": "C" + } + ], + "title": "Load Average", + "type": "timeseries" + }, + { + "collapsed": false, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 8 + }, + "id": 11, + "panels": [], + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "refId": "A" + } + ], + "title": "Memory", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Aggregated values of memory used, buffered, cached and free", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 10, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "normal" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 18, + "x": 0, + "y": 9 + }, + "id": 4, + "links": [], + "options": { + "legend": { + "calcs": [], + "displayMode": "list", + "placement": "bottom" + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.4.7", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": false, + "expr": "(\n sum(node_memory_MemTotal_bytes{job=\"node-exporter\", nodename=~\"$instance\"})\n-\n sum(node_memory_MemFree_bytes{job=\"node-exporter\", nodename=~\"$instance\"})\n-\n sum(node_memory_Buffers_bytes{job=\"node-exporter\", nodename=~\"$instance\"})\n-\n sum(node_memory_Cached_bytes{job=\"node-exporter\", nodename=~\"$instance\"})\n)\n", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "memory used", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": false, + "expr": "sum(node_memory_Buffers_bytes{job=\"node-exporter\", nodename=~\"$instance\"})", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "memory buffers", + "refId": "B" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": false, + "expr": "sum(node_memory_Cached_bytes{job=\"node-exporter\", nodename=~\"$instance\"})", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "memory cached", + "refId": "C" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": false, + "expr": "sum(node_memory_MemFree_bytes{job=\"node-exporter\", nodename=~\"$instance\"})", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "memory free", + "refId": "D" + } + ], + "title": "Memory Usage", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Overall memory consumption", + "fieldConfig": { + "defaults": { + "mappings": [], + "max": 100, + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "rgba(50, 172, 45, 0.97)", + "value": null + }, + { + "color": "rgba(237, 129, 40, 0.89)", + "value": 80 + }, + { + "color": "rgba(245, 54, 54, 0.9)", + "value": 90 + } + ] + }, + "unit": "percent" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 6, + "x": 18, + "y": 9 + }, + "id": 5, + "options": { + "orientation": "auto", + "reduceOptions": { + "calcs": ["lastNotNull"], + "fields": "", + "values": false + }, + "showThresholdLabels": false, + "showThresholdMarkers": true + }, + "pluginVersion": "8.4.7", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": false, + "expr": "100 -\n(\n avg(node_memory_MemAvailable_bytes{job=\"node-exporter\", nodename=~\"$instance\"}) /\n avg(node_memory_MemTotal_bytes{job=\"node-exporter\", nodename=~\"$instance\"})\n* 100\n)\n", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "", + "refId": "A" + } + ], + "title": "Memory Usage", + "type": "gauge" + }, + { + "collapsed": false, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 16 + }, + "id": 12, + "panels": [], + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "refId": "A" + } + ], + "title": "Disk", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Disk read bytes per EC2 instance", + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 2, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/ io time/" + }, + "properties": [ + { + "id": "unit", + "value": "s" + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 17 + }, + "id": 6, + "links": [], + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "right" + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.4.7", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "sum(rate(node_disk_read_bytes_total{job=\"node-exporter\", nodename=~\"$instance\", device=~\"mmcblk.p.+|nvme.+|rbd.+|sd.+|vd.+|xvd.+|dm-.+|dasd.+\"}[5m])) by (nodename, device)", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "{{nodename}} {{device}}", + "refId": "A" + } + ], + "title": "Disk I/O - Read", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 2, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/ io time/" + }, + "properties": [ + { + "id": "unit", + "value": "s" + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 17 + }, + "id": 14, + "links": [], + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "right" + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.4.7", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "sum(rate(node_disk_written_bytes_total{job=\"node-exporter\", nodename=~\"$instance\", device=~\"mmcblk.p.+|nvme.+|rbd.+|sd.+|vd.+|xvd.+|dm-.+|dasd.+\"}[5m])) by (nodename, device)", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "{{nodename}} {{device}}", + "refId": "B" + } + ], + "title": "Disk I/O - Write", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 2, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [ + { + "matcher": { + "id": "byRegexp", + "options": "/ io time/" + }, + "properties": [ + { + "id": "unit", + "value": "s" + } + ] + } + ] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 24 + }, + "id": 15, + "links": [], + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "right" + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.4.7", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "sum(rate(node_disk_io_time_seconds_total{job=\"node-exporter\", nodename=~\"$instance\", device=~\"mmcblk.p.+|nvme.+|rbd.+|sd.+|vd.+|xvd.+|dm-.+|dasd.+\"}[5m])) by (nodename, device)", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "{{nodename}} {{device}}", + "refId": "C" + } + ], + "title": "Disk I/O time", + "type": "timeseries" + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "description": "Aggregated disk space available / used", + "fill": 1, + "fillGradient": 0, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 24 + }, + "hiddenSeries": false, + "id": 7, + "legend": { + "alignAsTable": false, + "avg": false, + "current": false, + "max": false, + "min": false, + "rightSide": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 1, + "links": [], + "nullPointMode": "null", + "options": { + "alertThreshold": true + }, + "percentage": false, + "pluginVersion": "8.4.7", + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [ + { + "alias": "used", + "color": "#E0B400" + }, + { + "alias": "available", + "color": "#73BF69" + } + ], + "spaceLength": 10, + "stack": true, + "steppedLine": false, + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": false, + "expr": "sum(\n max by (device) (\n node_filesystem_size_bytes{job=\"node-exporter\", nodename=~\"$instance\", fstype!=\"\"}\n -\n node_filesystem_avail_bytes{job=\"node-exporter\", nodename=~\"$instance\", fstype!=\"\"}\n )\n)\n", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "used", + "refId": "A" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": false, + "expr": "sum(\n max by (device) (\n node_filesystem_avail_bytes{job=\"node-exporter\", nodename=~\"$instance\", fstype!=\"\"}\n )\n)\n", + "format": "time_series", + "interval": "", + "intervalFactor": 2, + "legendFormat": "available", + "refId": "B" + } + ], + "thresholds": [], + "timeRegions": [], + "title": "Disk Space Usage", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "mode": "time", + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "bytes", + "logBase": 1, + "min": 0, + "show": true + }, + { + "format": "bytes", + "logBase": 1, + "min": 0, + "show": true + } + ], + "yaxis": { + "align": false + } + }, + { + "collapsed": false, + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "gridPos": { + "h": 1, + "w": 24, + "x": 0, + "y": 31 + }, + "id": 13, + "panels": [], + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "prometheus" + }, + "refId": "A" + } + ], + "title": "Network", + "type": "row" + }, + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 2, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 32 + }, + "id": 8, + "links": [], + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "right" + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.4.7", + "targets": [ + { + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "exemplar": true, + "expr": "sum(rate(node_network_receive_bytes_total{job=\"node-exporter\", device!=\"lo\", nodename=~\"$instance\"}[5m])) by (nodename)", + "format": "time_series", + "instant": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "{{nodename}}", + "refId": "A" + } + ], + "title": "Network Received", + "type": "timeseries" + }, + { + "datasource": { + "type": "prometheus", + "uid": "$datasource" + }, + "fieldConfig": { + "defaults": { + "color": { + "mode": "palette-classic" + }, + "custom": { + "axisLabel": "", + "axisPlacement": "auto", + "barAlignment": 0, + "drawStyle": "line", + "fillOpacity": 2, + "gradientMode": "none", + "hideFrom": { + "legend": false, + "tooltip": false, + "viz": false + }, + "lineInterpolation": "linear", + "lineWidth": 1, + "pointSize": 5, + "scaleDistribution": { + "type": "linear" + }, + "showPoints": "never", + "spanNulls": true, + "stacking": { + "group": "A", + "mode": "none" + }, + "thresholdsStyle": { + "mode": "off" + } + }, + "mappings": [], + "min": 0, + "thresholds": { + "mode": "absolute", + "steps": [ + { + "color": "green", + "value": null + }, + { + "color": "red", + "value": 80 + } + ] + }, + "unit": "bytes" + }, + "overrides": [] + }, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 32 + }, + "id": 9, + "links": [], + "options": { + "legend": { + "calcs": [], + "displayMode": "table", + "placement": "right" + }, + "tooltip": { + "mode": "multi", + "sort": "none" + } + }, + "pluginVersion": "8.4.7", + "targets": [ + { + "datasource": { + "uid": "$datasource" + }, + "exemplar": true, + "expr": "sum(rate(node_network_transmit_bytes_total{job=\"node-exporter\", device!=\"lo\", nodename=~\"$instance\"}[5m])) by (nodename)", + "format": "time_series", + "instant": false, + "interval": "", + "intervalFactor": 2, + "legendFormat": "{{nodename}}", + "refId": "A" + } + ], + "title": "Network Transmitted", + "type": "timeseries" + } + ], + "refresh": "", + "schemaVersion": 35, + "style": "dark", + "tags": ["infrastructure"], + "templating": { + "list": [ + { + "allValue": ".*", + "current": { + "selected": false, + "text": "All", + "value": "$__all" + }, + "datasource": { + "type": "prometheus", + "uid": "${datasource}" + }, + "definition": "label_values(node_uname_info{job=\"node-exporter\", sysname!=\"Darwin\"}, nodename)", + "hide": 0, + "includeAll": true, + "label": "Instance", + "multi": true, + "name": "instance", + "options": [], + "query": { + "query": "label_values(node_uname_info{job=\"node-exporter\", sysname!=\"Darwin\"}, nodename)", + "refId": "StandardVariableQuery" + }, + "refresh": 2, + "regex": "", + "skipUrlSync": false, + "sort": 0, + "tagValuesQuery": "", + "tagsQuery": "", + "type": "query", + "useTags": false + }, + { + "current": { + "selected": false, + "text": "default", + "value": "default" + }, + "hide": 0, + "includeAll": false, + "label": "datasource", + "multi": false, + "name": "datasource", + "options": [], + "query": "prometheus", + "queryValue": "", + "refresh": 1, + "regex": "", + "skipUrlSync": false, + "type": "datasource" + } + ] + }, + "time": { + "from": "now-3h", + "to": "now" + }, + "timepicker": { + "refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"], + "time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"] + }, + "timezone": "utc", + "title": "Grafana Operator - Node Exporter / Nodes", + "uid": "v8yDYJqnz", + "version": 18, + "weekStart": "" +} diff --git a/grafana-dashboards/nodes.json b/archive/grafana-dashboards/nodes.json similarity index 100% rename from grafana-dashboards/nodes.json rename to archive/grafana-dashboards/nodes.json diff --git a/grafana-dashboards/workloads.json b/archive/grafana-dashboards/workloads.json similarity index 100% rename from grafana-dashboards/workloads.json rename to archive/grafana-dashboards/workloads.json diff --git a/grafana-operator-manifests/amg_grafana-amp-datasource.yaml b/archive/grafana-operator-manifests/amg_grafana-amp-datasource.yaml similarity index 81% rename from grafana-operator-manifests/amg_grafana-amp-datasource.yaml rename to archive/grafana-operator-manifests/amg_grafana-amp-datasource.yaml index 415bd6a9..389a38fb 100644 --- a/grafana-operator-manifests/amg_grafana-amp-datasource.yaml +++ b/archive/grafana-operator-manifests/amg_grafana-amp-datasource.yaml @@ -1,3 +1,5 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 apiVersion: grafana.integreatly.org/v1beta1 kind: GrafanaDatasource metadata: @@ -19,4 +21,4 @@ spec: 'sigV4Auth': true 'sigV4AuthType': "ec2_iam_role" 'sigV4Region': ${AMG_AWS_REGION} - editable: true + editable: true diff --git a/grafana-operator-manifests/amg_grafana-cw-datasource.yaml b/archive/grafana-operator-manifests/amg_grafana-cw-datasource.yaml similarity index 85% rename from grafana-operator-manifests/amg_grafana-cw-datasource.yaml rename to archive/grafana-operator-manifests/amg_grafana-cw-datasource.yaml index 68df5047..7758c261 100644 --- a/grafana-operator-manifests/amg_grafana-cw-datasource.yaml +++ b/archive/grafana-operator-manifests/amg_grafana-cw-datasource.yaml @@ -1,3 +1,5 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 apiVersion: grafana.integreatly.org/v1beta1 kind: GrafanaDatasource metadata: diff --git a/grafana-operator-manifests/amg_grafana-dashboard.yaml b/archive/grafana-operator-manifests/amg_grafana-dashboard.yaml similarity index 70% rename from grafana-operator-manifests/amg_grafana-dashboard.yaml rename to archive/grafana-operator-manifests/amg_grafana-dashboard.yaml index 73df951c..27579624 100644 --- a/grafana-operator-manifests/amg_grafana-dashboard.yaml +++ b/archive/grafana-operator-manifests/amg_grafana-dashboard.yaml @@ -1,3 +1,5 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 apiVersion: grafana.integreatly.org/v1beta1 kind: GrafanaDashboard metadata: diff --git a/grafana-operator-manifests/amg_grafana-identity.yaml b/archive/grafana-operator-manifests/amg_grafana-identity.yaml similarity index 73% rename from grafana-operator-manifests/amg_grafana-identity.yaml rename to archive/grafana-operator-manifests/amg_grafana-identity.yaml index 8500e76b..6ddd9532 100644 --- a/grafana-operator-manifests/amg_grafana-identity.yaml +++ b/archive/grafana-operator-manifests/amg_grafana-identity.yaml @@ -1,3 +1,5 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 apiVersion: grafana.integreatly.org/v1beta1 kind: Grafana metadata: diff --git a/grafana-operator-manifests/amg_grafana-xray-datasource.yaml b/archive/grafana-operator-manifests/amg_grafana-xray-datasource.yaml similarity index 78% rename from grafana-operator-manifests/amg_grafana-xray-datasource.yaml rename to archive/grafana-operator-manifests/amg_grafana-xray-datasource.yaml index 4aacab62..08e77074 100644 --- a/grafana-operator-manifests/amg_grafana-xray-datasource.yaml +++ b/archive/grafana-operator-manifests/amg_grafana-xray-datasource.yaml @@ -1,3 +1,5 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 apiVersion: grafana.integreatly.org/v1beta1 kind: GrafanaDatasource metadata: @@ -7,7 +9,7 @@ spec: matchLabels: dashboards: "external-grafana" datasource: - name: X-Ray + name: X-Ray type: grafana-x-ray-datasource access: server isDefault: false diff --git a/grafana-operator-manifests/kustomization.yaml b/archive/grafana-operator-manifests/kustomization.yaml similarity index 61% rename from grafana-operator-manifests/kustomization.yaml rename to archive/grafana-operator-manifests/kustomization.yaml index 4637383d..b39555f6 100644 --- a/grafana-operator-manifests/kustomization.yaml +++ b/archive/grafana-operator-manifests/kustomization.yaml @@ -1,3 +1,5 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 apiVersion: kustomize.config.k8s.io/v1beta1 kind: Kustomization resources: diff --git a/PetAdoptions/keycloak-cleanup.sh b/archive/keycloak-cleanup.sh similarity index 99% rename from PetAdoptions/keycloak-cleanup.sh rename to archive/keycloak-cleanup.sh index dce6eaae..1f446c40 100755 --- a/PetAdoptions/keycloak-cleanup.sh +++ b/archive/keycloak-cleanup.sh @@ -1,4 +1,7 @@ #!/bin/bash + +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 # # Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. # @@ -250,7 +253,7 @@ function delete_keycloak_secrets() { ;; esac fi - + echo "Deleting IRSA for Keycloak SecretStore..." eksctl delete iamserviceaccount \ --name keycloaksecretstore \ @@ -260,7 +263,7 @@ function delete_keycloak_secrets() { if [ $CMD_RESULT -ne 0 ]; then handle_error "ERROR: Failed to delete IRSA for Keycloak SecretStore." fi - + sleep 10 echo "Deleting IAM policy for Keycloak SecretStore..." CMD_OUT=$(aws iam delete-policy --policy-arn "arn:aws:iam::$ACCOUNT_ID:policy/OneObservabilityWorkshopKeycloakSecretStorePolicy" 2>&1) @@ -275,7 +278,7 @@ function delete_keycloak_secrets() { ;; esac fi - + echo "Deleting namespace '$KEYCLOAK_NAMESPACE'..." CMD_OUT=$(kubectl delete ns $KEYCLOAK_NAMESPACE 2>&1) CMD_RESULT=$? @@ -288,7 +291,7 @@ function delete_keycloak_secrets() { ;; esac fi - + echo "Checking saved keycloak password in AWS Secrets Manager..." SECRET_NAME="oneobservabilityworkshop/keycloak" SECRET_ARN=$(aws secretsmanager list-secrets --filters Key=name,Values=$SECRET_NAME Key=tag-key,Values=Project Key=tag-value,Values=OneObservabilityWorkshop --query "SecretList[0].ARN" --output text) @@ -319,7 +322,7 @@ function uninstall_external_secrets() { ;; esac fi - + echo "Deleting namespace 'external-secrets'..." CMD_OUT=$(kubectl delete ns external-secrets 2>&1) CMD_RESULT=$? diff --git a/PetAdoptions/keycloak-setup.sh b/archive/keycloak-setup.sh similarity index 99% rename from PetAdoptions/keycloak-setup.sh rename to archive/keycloak-setup.sh index 345a7efe..762672e4 100755 --- a/PetAdoptions/keycloak-setup.sh +++ b/archive/keycloak-setup.sh @@ -1,4 +1,7 @@ #!/bin/bash + +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 # # Copyright 2023 Amazon.com, Inc. or its affiliates. All Rights Reserved. # @@ -243,7 +246,7 @@ function install_ebs_csi_driver() { if [ $CMD_RESULT -ne 0 ]; then handle_error "ERROR: Failed to install EBS CSI driver addon." fi - + echo "Waiting for EBS CSI driver addon status to become 'ACTIVE'..." aws eks wait addon-active \ --cluster-name $CLUSTER_NAME \ @@ -354,12 +357,12 @@ function install_external_secrets() { if [ $CMD_RESULT -ne 0 ]; then handle_error "ERROR: Failed to list application 'external-secrets'." fi - + if [ "$EXT_SECRET_REL" != "[]" ]; then echo "Application 'external-secrets' is already installed." return 0 fi - + echo "Application 'external-secrets' will be installed." echo "---------------------------------------------------------------------------------------------" helm install external-secrets \ @@ -375,13 +378,13 @@ function install_external_secrets() { function configure_keycloak_password() { echo "Checking saved keycloak password in AWS Secrets Manager..." - SECRET_NAME="oneobservabilityworkshop/keycloak" + SECRET_NAME="oneobservabilityworkshop/keycloak". #pragma: allowlist secret SECRET_ARN=$(aws secretsmanager list-secrets --filters Key=name,Values=$SECRET_NAME Key=tag-key,Values=Project Key=tag-value,Values=OneObservabilityWorkshop --query "SecretList[0].ARN" --output text) CMD_RESULT=$? if [ $CMD_RESULT -ne 0 ]; then handle_error "ERROR: Failed to check saved keycloak password in AWS Secrets Manager." fi - if [ "$SECRET_ARN" != "None" ]; then + if [ "$SECRET_ARN" != "None" ]; then #pragma: allowlist secret echo "Found saved keycloak password. Retrieving saved value..." KEYCLOAK_PASSWORDS=$(aws secretsmanager get-secret-value --secret-id $SECRET_ARN --query "SecretString" --output text) CMD_RESULT=$? @@ -445,7 +448,7 @@ EOF elif [[ $CMD_RESULT -ne 0 ]]; then handle_error "ERROR: Failed to check existing IAM policy information for keycloak SecretStore." fi - + echo "Checking if namespace '$KEYCLOAK_NAMESPACE' exists..." CMD_OUT=$(kubectl get ns keycloak -o jsonpath={.metadata.name} 2>&1) CMD_RESULT=$? @@ -473,14 +476,14 @@ EOF else echo "Namespace '$KEYCLOAK_NAMESPACE' exists." fi - + echo "Searching IRSA for keycloak SecretStore..." IRSA=$(eksctl get iamserviceaccount --cluster $CLUSTER_NAME --namespace $KEYCLOAK_NAMESPACE --name keycloaksecretstore -o json | jq -r '.[].metadata.name') CMD_RESULT=$? if [ $CMD_RESULT -ne 0 ]; then handle_error "ERROR: Failed to query IRSA metadata for keycloak SecretStore." fi - + if [ -z "$IRSA" ]; then echo "Creating IRSA for keycloak SecretStore" eksctl create iamserviceaccount \ @@ -498,7 +501,7 @@ EOF else echo "Found IRSA for keycloak SecretStore" fi - + echo "Checking existing keycloak SecretStore..." CMD_OUT=$(kubectl get secretstore keycloak -n $KEYCLOAK_NAMESPACE -o jsonpath={.metadata.name} 2>&1) CMD_RESULT=$? @@ -535,7 +538,7 @@ EOF else echo "Found existing keycloak SecretStore." fi - + echo "Checking existing keycloak ExternalSecret..." CMD_OUT=$(kubectl get externalsecret keycloak -n keycloak -o jsonpath={.metadata.name} 2>&1) CMD_RESULT=$? @@ -563,7 +566,7 @@ spec: key: oneobservabilityworkshop/keycloak property: admin-password EOF - + CMD_RESULT=$? if [ $CMD_RESULT -ne 0 ]; then handle_error "ERROR: Failed to create keycloak ExternalSecret." @@ -585,19 +588,19 @@ function install_keycloak() { if [ $CMD_RESULT -ne 0 ]; then handle_error "ERROR: Failed to list application 'keycloak'." fi - + if [ "$KEYCLOAK_REL" != "[]" ]; then echo "Application 'keycloak' is already installed." return 0 fi - + echo "Application 'keycloak' will be installed." - + echo "Generating keycloak chart values..." # UPDATE: Disable resource presets and explicitly set container resources to avoid below warning. # # WARNING: There are "resources" sections in the chart not set. Using "resourcesPreset" is not recommended for production. For production installations, please set the following values according to your workload needs: - # + # # * resources +info https://kubernetes.io/docs/concepts/configuration/manage-resources-containers/ # KEYCLOAK_HELM_VALUES=$(cat < config.yaml <- This template deploys a VPC, with a private subnet accessing internet via the NAT Gateway. It deploys 2 EC2 @@ -89,7 +91,7 @@ Resources: MapPublicIpOnLaunch: false Tags: - Key: Name - Value: !Sub ${EnvironmentName} Private Subnet (AZ1) + Value: !Sub ${EnvironmentName} Private Subnet (AZ1) PrivateSubnet1RouteTableAssociation: Type: AWS::EC2::SubnetRouteTableAssociation Properties: @@ -99,7 +101,7 @@ Resources: Type: AWS::EC2::SubnetRouteTableAssociation Properties: RouteTableId: !Ref PublicRouteTable - SubnetId: !Ref PublicSubnet1 + SubnetId: !Ref PublicSubnet1 InstanceSecurityGroup1: Type: 'AWS::EC2::SecurityGroup' Properties: @@ -127,7 +129,7 @@ Resources: - IpProtocol: tcp FromPort: '3389' ToPort: '3389' - CidrIp: 0.0.0.0/0 + CidrIp: 0.0.0.0/0 InstanceLinux: Type: 'AWS::EC2::Instance' Properties: @@ -251,7 +253,7 @@ Outputs: Value: !Ref InstanceSecurityGroup1 InstanceSecurityGroup2: Description: Security group Windows - Value: !Ref InstanceSecurityGroup2 + Value: !Ref InstanceSecurityGroup2 InstanceLinux: Description: EC2 instance for agent installation Value: !Ref InstanceLinux diff --git a/PetAdoptions/cdk/pet_stack/resources/extra_scrape_configs.yaml b/archive/resources/extra_scrape_configs.yaml similarity index 64% rename from PetAdoptions/cdk/pet_stack/resources/extra_scrape_configs.yaml rename to archive/resources/extra_scrape_configs.yaml index 32b427ab..94a4c5fd 100644 --- a/PetAdoptions/cdk/pet_stack/resources/extra_scrape_configs.yaml +++ b/archive/resources/extra_scrape_configs.yaml @@ -1,3 +1,5 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 - job_name: kubecost honor_labels: true scrape_interval: 1m @@ -6,6 +8,6 @@ scheme: http dns_sd_configs: - names: - - kubecost-cost-analyzer # Note: this is the URL of the kube-cost-analyzer. + - kubecost-cost-analyzer # Note: this is the URL of the kube-cost-analyzer. type: "A" port: 9003 \ No newline at end of file diff --git a/PetAdoptions/cdk/pet_stack/resources/fargate-resize-dashboard.json b/archive/resources/fargate-resize-dashboard.json similarity index 100% rename from PetAdoptions/cdk/pet_stack/resources/fargate-resize-dashboard.json rename to archive/resources/fargate-resize-dashboard.json diff --git a/PetAdoptions/cdk/pet_stack/resources/otel-collector-prometheus.yaml b/archive/resources/otel-collector-prometheus.yaml similarity index 98% rename from PetAdoptions/cdk/pet_stack/resources/otel-collector-prometheus.yaml rename to archive/resources/otel-collector-prometheus.yaml index a47716ef..ba8dc54c 100644 --- a/PetAdoptions/cdk/pet_stack/resources/otel-collector-prometheus.yaml +++ b/archive/resources/otel-collector-prometheus.yaml @@ -1,3 +1,5 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 # # OpenTelemetry Collector configuration # Replace the variables REGION and WORKSPACE per your target environment @@ -169,7 +171,7 @@ spec: target_label: kubernetes_node scrape_interval: 5m scrape_timeout: 30s - + - job_name: prometheus-pushgateway honor_labels: true kubernetes_sd_configs: @@ -254,7 +256,7 @@ spec: - job_name: kubernetes-pods-slow scrape_interval: 5m - scrape_timeout: 30s + scrape_timeout: 30s kubernetes_sd_configs: - role: pod relabel_configs: @@ -296,10 +298,10 @@ spec: regex: Pending|Succeeded|Failed|Completed source_labels: - __meta_kubernetes_pod_phase - + processors: batch/metrics: - timeout: 60s + timeout: 60s exporters: prometheusremotewrite: @@ -364,4 +366,4 @@ roleRef: subjects: - kind: ServiceAccount name: amp-iamproxy-ingest-role - namespace: prometheus \ No newline at end of file + namespace: prometheus \ No newline at end of file diff --git a/PetAdoptions/cdk/pet_stack/resources/petadoptions-grafana-dashboard.json b/archive/resources/petadoptions-grafana-dashboard.json similarity index 100% rename from PetAdoptions/cdk/pet_stack/resources/petadoptions-grafana-dashboard.json rename to archive/resources/petadoptions-grafana-dashboard.json diff --git a/PetAdoptions/cdk/pet_stack/resources/petadoptionshistory-py/README.md b/archive/resources/petadoptionshistory-py/README.md similarity index 70% rename from PetAdoptions/cdk/pet_stack/resources/petadoptionshistory-py/README.md rename to archive/resources/petadoptionshistory-py/README.md index 47ad7e83..3c5fc46a 100644 --- a/PetAdoptions/cdk/pet_stack/resources/petadoptionshistory-py/README.md +++ b/archive/resources/petadoptionshistory-py/README.md @@ -1,3 +1,7 @@ + # PetAdoptionsHistory with full OTEL instrumentation applied See PetAdoptions/petadoptionshistory-py/ for the complete source code of the service. diff --git a/PetAdoptions/cdk/pet_stack/resources/petadoptionshistory-py/petadoptionshistory_complete.py b/archive/resources/petadoptionshistory-py/petadoptionshistory_complete.py similarity index 76% rename from PetAdoptions/cdk/pet_stack/resources/petadoptionshistory-py/petadoptionshistory_complete.py rename to archive/resources/petadoptionshistory-py/petadoptionshistory_complete.py index 780b9de4..6c64ef31 100644 --- a/PetAdoptions/cdk/pet_stack/resources/petadoptionshistory-py/petadoptionshistory_complete.py +++ b/archive/resources/petadoptionshistory-py/petadoptionshistory_complete.py @@ -1,42 +1,39 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 import logging import os -import psycopg2 + import config +import psycopg2 import repository -from flask import Flask, jsonify - -# OTLP tracing +from flask import Flask +from flask import jsonify +from opentelemetry import metrics from opentelemetry import trace +from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter +from opentelemetry.exporter.prometheus import PrometheusMetricReader +from opentelemetry.instrumentation.botocore import BotocoreInstrumentor +from opentelemetry.instrumentation.flask import FlaskInstrumentor +from opentelemetry.instrumentation.psycopg2 import Psycopg2Instrumentor +from opentelemetry.metrics import Observation +from opentelemetry.propagate import set_global_textmap +from opentelemetry.propagators.aws import AwsXRayPropagator +from opentelemetry.sdk.extension.aws.resource.eks import AwsEksResourceDetector +from opentelemetry.sdk.extension.aws.trace import AwsXRayIdGenerator +from opentelemetry.sdk.metrics import MeterProvider +from opentelemetry.sdk.resources import get_aggregated_resources from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor -from opentelemetry.sdk.resources import SERVICE_NAME, Resource, get_aggregated_resources +from prometheus_flask_exporter import PrometheusMetrics +# OTLP tracing # Exporter -from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter - # Propagation -from opentelemetry.propagate import set_global_textmap -from opentelemetry.propagators.aws import AwsXRayPropagator - # AWS X-Ray ID generator -from opentelemetry.sdk.extension.aws.trace import AwsXRayIdGenerator - # Resource detector -from opentelemetry.sdk.extension.aws.resource.eks import AwsEksResourceDetector - -# Instrumentation -from opentelemetry.instrumentation.botocore import BotocoreInstrumentor -from opentelemetry.instrumentation.psycopg2 import Psycopg2Instrumentor -from opentelemetry.instrumentation.flask import FlaskInstrumentor - +# Instrumentation # OLTP Metrics -from opentelemetry import metrics -from opentelemetry.metrics import Observation -from opentelemetry.exporter.prometheus import PrometheusMetricReader -from opentelemetry.sdk.metrics import MeterProvider - -# Flask exporter -from prometheus_flask_exporter import PrometheusMetrics +# Flask exporter # Instrumentation BotocoreInstrumentor().instrument() @@ -46,10 +43,10 @@ app = Flask(__name__) FlaskInstrumentor().instrument_app(app) -logging.basicConfig(level=os.getenv('LOG_LEVEL', 20), format='%(message)s') +logging.basicConfig(level=os.getenv("LOG_LEVEL", 20), format="%(message)s") logger = logging.getLogger() cfg = config.fetch_config() -conn_params = config.get_rds_connection_parameters(cfg['rds_secret_arn'], cfg['region']) +conn_params = config.get_rds_connection_parameters(cfg["rds_secret_arn"], cfg["region"]) db = psycopg2.connect(**conn_params) # Setup AWS X-Ray propagator @@ -59,7 +56,7 @@ resource = get_aggregated_resources( [ AwsEksResourceDetector(), - ] + ], ) # Setup tracer provider with the X-Ray ID generator @@ -73,7 +70,7 @@ # Creates a tracer from the global tracer provider tracer = trace.get_tracer(__name__) -# Setup metrics +# Setup metrics reader = PrometheusMetricReader() meter_provider = MeterProvider(resource=resource, metric_readers=[reader]) @@ -83,37 +80,43 @@ # Creates a meter from the global meter provider meter = metrics.get_meter(__name__) + def transactions_history_callback(result): count = repository.count_transaction_history(db) yield Observation(count) + meter.create_observable_gauge( name="transactions_history.count", description="The number of items in the transactions history", - callbacks=[transactions_history_callback]) + callbacks=[transactions_history_callback], +) transactions_get_counter = meter.create_counter( "transactions_get.count", description="The number of times the transactions_get endpoint has been called", ) -# This exposes the /metrics HTTP endpoint -metrics = PrometheusMetrics(app, group_by='endpoint') +# This exposes the /metrics HTTP endpoint +metrics = PrometheusMetrics(app, group_by="endpoint") + -@app.route('/petadoptionshistory/api/home/transactions', methods=['GET']) +@app.route("/petadoptionshistory/api/home/transactions", methods=["GET"]) def transactions_get(): - with tracer.start_as_current_span("transactions_get") as transactions_span: + with tracer.start_as_current_span("transactions_get"): transactions_get_counter.add(1) transactions = repository.list_transaction_history(db) return jsonify(transactions) -@app.route('/petadoptionshistory/api/home/transactions', methods=['DELETE']) + +@app.route("/petadoptionshistory/api/home/transactions", methods=["DELETE"]) def transactions_delete(): - with tracer.start_as_current_span("transactions_delete") as transactions_span: + with tracer.start_as_current_span("transactions_delete"): repository.delete_transaction_history(db) return jsonify(success=True) -@app.route('/health/status') + +@app.route("/health/status") def status_path(): repository.check_alive(db) - return jsonify(success=True) \ No newline at end of file + return jsonify(success=True) diff --git a/PetAdoptions/cdk/pet_stack/resources/petadoptionshistory-py/petadoptionshistory_tracing.py b/archive/resources/petadoptionshistory-py/petadoptionshistory_tracing.py similarity index 73% rename from PetAdoptions/cdk/pet_stack/resources/petadoptionshistory-py/petadoptionshistory_tracing.py rename to archive/resources/petadoptionshistory-py/petadoptionshistory_tracing.py index 743e533d..15e0e7d5 100644 --- a/PetAdoptions/cdk/pet_stack/resources/petadoptionshistory-py/petadoptionshistory_tracing.py +++ b/archive/resources/petadoptionshistory-py/petadoptionshistory_tracing.py @@ -1,33 +1,32 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 import logging import os -import psycopg2 + import config +import psycopg2 import repository -from flask import Flask, jsonify - -# OTLP Tracing +from flask import Flask +from flask import jsonify from opentelemetry import trace +from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter +from opentelemetry.instrumentation.botocore import BotocoreInstrumentor +from opentelemetry.instrumentation.flask import FlaskInstrumentor +from opentelemetry.instrumentation.psycopg2 import Psycopg2Instrumentor +from opentelemetry.propagate import set_global_textmap +from opentelemetry.propagators.aws import AwsXRayPropagator +from opentelemetry.sdk.extension.aws.resource.eks import AwsEksResourceDetector +from opentelemetry.sdk.extension.aws.trace import AwsXRayIdGenerator +from opentelemetry.sdk.resources import get_aggregated_resources from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor -from opentelemetry.sdk.resources import SERVICE_NAME, Resource, get_aggregated_resources +# OTLP Tracing # Exporter -from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter - # Propagation -from opentelemetry.propagate import set_global_textmap -from opentelemetry.propagators.aws import AwsXRayPropagator - # AWS X-Ray ID Generator -from opentelemetry.sdk.extension.aws.trace import AwsXRayIdGenerator - # Resource detector -from opentelemetry.sdk.extension.aws.resource.eks import AwsEksResourceDetector - -# Instrumentation -from opentelemetry.instrumentation.botocore import BotocoreInstrumentor -from opentelemetry.instrumentation.psycopg2 import Psycopg2Instrumentor -from opentelemetry.instrumentation.flask import FlaskInstrumentor +# Instrumentation # Instrumentation BotocoreInstrumentor().instrument() @@ -37,10 +36,10 @@ app = Flask(__name__) FlaskInstrumentor().instrument_app(app) -logging.basicConfig(level=os.getenv('LOG_LEVEL', 20), format='%(message)s') +logging.basicConfig(level=os.getenv("LOG_LEVEL", 20), format="%(message)s") logger = logging.getLogger() cfg = config.fetch_config() -conn_params = config.get_rds_connection_parameters(cfg['rds_secret_arn'], cfg['region']) +conn_params = config.get_rds_connection_parameters(cfg["rds_secret_arn"], cfg["region"]) db = psycopg2.connect(**conn_params) # Setup AWS X-Ray propagator @@ -50,7 +49,7 @@ resource = get_aggregated_resources( [ AwsEksResourceDetector(), - ] + ], ) # Setup tracer provider with the X-Ray ID generator @@ -64,19 +63,22 @@ # Creates a tracer from the global tracer provider tracer = trace.get_tracer(__name__) -@app.route('/petadoptionshistory/api/home/transactions', methods=['GET']) + +@app.route("/petadoptionshistory/api/home/transactions", methods=["GET"]) def transactions_get(): - with tracer.start_as_current_span("transactions_get") as transactions_span: + with tracer.start_as_current_span("transactions_get"): transactions = repository.list_transaction_history(db) return jsonify(transactions) -@app.route('/petadoptionshistory/api/home/transactions', methods=['DELETE']) + +@app.route("/petadoptionshistory/api/home/transactions", methods=["DELETE"]) def transactions_delete(): - with tracer.start_as_current_span("transactions_delete") as transactions_span: + with tracer.start_as_current_span("transactions_delete"): repository.delete_transaction_history(db) return jsonify(success=True) -@app.route('/health/status') + +@app.route("/health/status") def status_path(): repository.check_alive(db) - return jsonify(success=True) \ No newline at end of file + return jsonify(success=True) diff --git a/PetAdoptions/cdk/pet_stack/resources/prometheus-eks.yaml b/archive/resources/prometheus-eks.yaml similarity index 99% rename from PetAdoptions/cdk/pet_stack/resources/prometheus-eks.yaml rename to archive/resources/prometheus-eks.yaml index dd6cdadc..68c53deb 100644 --- a/PetAdoptions/cdk/pet_stack/resources/prometheus-eks.yaml +++ b/archive/resources/prometheus-eks.yaml @@ -1,3 +1,5 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 # create amazon-cloudwatch namespace # apiVersion: v1 # kind: Namespace @@ -14,8 +16,8 @@ metadata: name: cwagent-prometheus namespace: amazon-cloudwatch annotations: - eks.amazonaws.com/role-arn: {{cw_sa_role}} - + eks.amazonaws.com/role-arn: {{cw_sa_role}} + --- kind: ClusterRole apiVersion: rbac.authorization.k8s.io/v1 diff --git a/PetAdoptions/cdk/pet_stack/resources/rds_sqlserver.sql b/archive/resources/rds_sqlserver.sql similarity index 95% rename from PetAdoptions/cdk/pet_stack/resources/rds_sqlserver.sql rename to archive/resources/rds_sqlserver.sql index 15ece5fa..11f86cf7 100644 --- a/PetAdoptions/cdk/pet_stack/resources/rds_sqlserver.sql +++ b/archive/resources/rds_sqlserver.sql @@ -4,9 +4,9 @@ GO /****** Object: Database [adoptions] Script Date: 4/24/2020 2:02:30 PM ******/ CREATE DATABASE [adoptions] CONTAINMENT = NONE - ON PRIMARY + ON PRIMARY ( NAME = N'adoptions', FILENAME = N'D:\rdsdbdata\DATA\adoptions.mdf' , SIZE = 8192KB , MAXSIZE = UNLIMITED, FILEGROWTH = 10%) - LOG ON + LOG ON ( NAME = N'adoptions_log', FILENAME = N'D:\rdsdbdata\DATA\adoptions_log.ldf' , SIZE = 1024KB , MAXSIZE = 2048GB , FILEGROWTH = 10%) GO @@ -27,7 +27,7 @@ CREATE TABLE [dbo].[Transactions]( [PetId] [nvarchar](50) NULL, [Adoption_Date] [datetime] NULL, [Transaction_Id] [nvarchar](50) NULL, -PRIMARY KEY CLUSTERED +PRIMARY KEY CLUSTERED ( [Id] ASC )WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY] diff --git a/PetAdoptions/cdk/pet_stack/resources/resource-controller-widget/cloudwatch-custom-widget.py b/archive/resources/resource-controller-widget/cloudwatch-custom-widget.py similarity index 63% rename from PetAdoptions/cdk/pet_stack/resources/resource-controller-widget/cloudwatch-custom-widget.py rename to archive/resources/resource-controller-widget/cloudwatch-custom-widget.py index 260025f4..d5d9d15e 100644 --- a/PetAdoptions/cdk/pet_stack/resources/resource-controller-widget/cloudwatch-custom-widget.py +++ b/archive/resources/resource-controller-widget/cloudwatch-custom-widget.py @@ -1,58 +1,75 @@ -import os +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 import json +import os + import boto3 -ecs_client = boto3.client('ecs') -eks_client = boto3.client('eks') +ecs_client = boto3.client("ecs") +eks_client = boto3.client("eks") + +EKS_CLUSTER_NAME = os.environ["EKS_CLUSTER_NAME"] +CONTROLER_LAMBDA_ARN = os.environ["CONTROLER_LAMBDA_ARN"] +ECS_CLUSTER_ARNS = os.environ["ECS_CLUSTER_ARNS"].split(",") -EKS_CLUSTER_NAME = os.environ['EKS_CLUSTER_NAME'] -CONTROLER_LAMBDA_ARN = os.environ['CONTROLER_LAMBDA_ARN'] -ECS_CLUSTER_ARNS = os.environ['ECS_CLUSTER_ARNS'].split(",") def get_current_status(number_of_tasks): """Function to return HTML tag based on number of tasks""" if number_of_tasks == 0: return 'checked="checked"' else: - return '' + return "" + def get_current_count(): """Get the current ECS and EKS node counts""" clusters = ecs_client.list_clusters() ecs_task_count = 0 - for cluster in clusters['clusterArns']: + for cluster in clusters["clusterArns"]: if cluster in ECS_CLUSTER_ARNS: - services = ecs_client.list_services( - cluster = cluster - ) + services = ecs_client.list_services(cluster=cluster) service_info = ecs_client.describe_services( - cluster = cluster, - services = services['serviceArns']) - for svc in service_info['services']: - ecs_task_count += svc['desiredCount'] + cluster=cluster, + services=services["serviceArns"], + ) + for svc in service_info["services"]: + ecs_task_count += svc["desiredCount"] eks_node_count = 0 - nodegroups = eks_client.list_nodegroups( - clusterName=EKS_CLUSTER_NAME) - for group in nodegroups['nodegroups']: - if EKS_CLUSTER_NAME.lower()+'nodegroup' in group.lower(): + nodegroups = eks_client.list_nodegroups(clusterName=EKS_CLUSTER_NAME) + for group in nodegroups["nodegroups"]: + if EKS_CLUSTER_NAME.lower() + "nodegroup" in group.lower(): node_group_info = eks_client.describe_nodegroup( clusterName=EKS_CLUSTER_NAME, - nodegroupName=group + nodegroupName=group, ) - eks_node_count=node_group_info['nodegroup']['scalingConfig']['desiredSize'] + eks_node_count = node_group_info["nodegroup"]["scalingConfig"][ + "desiredSize" + ] break - return ecs_task_count,eks_node_count + return ecs_task_count, eks_node_count + def generate_function_event(status): """Return Event content based on status""" return json.dumps({"Action": status}) + def get_current_status_string(number_of_tasks): """Return text based on the status""" if number_of_tasks == 0: - return "Clicking on this button now will re-start the ECS tasks and nodes on the EKS cluster. The application will be functional again and monitoring data will be generated" + return ( + "Clicking on this button now will re-start the ECS tasks and " + "nodes on the EKS cluster. The application will be functional " + "again and monitoring data will be generated" + ) else: - return "Clicking on this button now will set the ECS task count and EKS Node count to ZERO, saving you money. The application will not be functional and hence no monitoring data will be generated until the services are enabled once again" + return ( + "Clicking on this button now will set the ECS task count and " + "EKS Node count to ZERO, saving you money. The application will " + "not be functional and hence no monitoring data will be generated " + "until the services are enabled once again" + ) + def get_current_status_string_header(number_of_tasks): """Return text based on the status""" @@ -60,11 +77,11 @@ def get_current_status_string_header(number_of_tasks): return "Start ECS Tasks and EKS nodes" else: return "Stop ECS Tasks and EKS nodes" - -def generate_html(ecs_tasks,eks_nodes): + +def generate_html(ecs_tasks, eks_nodes): """Function to generate HTML dynamically""" - if ecs_tasks+eks_nodes == 0: + if ecs_tasks + eks_nodes == 0: status = "enable" else: status = "disable" @@ -118,11 +135,14 @@ def generate_html(ecs_tasks,eks_nodes): toggle += f"""
- {get_current_status_string_header(ecs_tasks+eks_nodes)}: - + {get_current_status_string_header(ecs_tasks+eks_nodes)}: + " @@ -148,5 +168,5 @@ def generate_html(ecs_tasks,eks_nodes): def lambda_handler(event, context): """Main function to rander the Custom Widget HTML""" - ecs_tasks,eks_nodes = get_current_count() - return generate_html(ecs_tasks,eks_nodes) + ecs_tasks, eks_nodes = get_current_count() + return generate_html(ecs_tasks, eks_nodes) diff --git a/PetAdoptions/cdk/pet_stack/resources/resource-controller-widget/petsite-application-resource-controler.py b/archive/resources/resource-controller-widget/petsite-application-resource-controler.py similarity index 54% rename from PetAdoptions/cdk/pet_stack/resources/resource-controller-widget/petsite-application-resource-controler.py rename to archive/resources/resource-controller-widget/petsite-application-resource-controler.py index 1d402df2..7885bc45 100644 --- a/PetAdoptions/cdk/pet_stack/resources/resource-controller-widget/petsite-application-resource-controler.py +++ b/archive/resources/resource-controller-widget/petsite-application-resource-controler.py @@ -1,50 +1,57 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 """ -This function will be triggered by Cloudwatch Custom Widget lambda function. You can also trigger resource-controller function manually with below event. +This function will be triggered by Cloudwatch Custom Widget lambda function. +You can also trigger resource-controller function manually with below event. To stop ECS and EKS resources: {'Action':'disable'} To start ECS and EKS resources: {'Action':'enable'} """ - import os -import boto3 import time -ecs_client = boto3.client('ecs') -eks_client = boto3.client('eks') +import boto3 + +ecs_client = boto3.client("ecs") +eks_client = boto3.client("eks") + +EKS_CLUSTER_NAME = os.environ["EKS_CLUSTER_NAME"] +ECS_CLUSTER_ARNS = os.environ["ECS_CLUSTER_ARNS"].split(",") -EKS_CLUSTER_NAME = os.environ['EKS_CLUSTER_NAME'] -ECS_CLUSTER_ARNS = os.environ['ECS_CLUSTER_ARNS'].split(",") def set_ecs_desired_task_count_to_zero(cluster): """Function to set ECS Desired Tasks counts to 0 for all ECS Cluster Services""" - services = ecs_client.list_services( - cluster = cluster + services = ecs_client.list_services(cluster=cluster) + for service in services["serviceArns"]: + ecs_client.update_service( + cluster=cluster, + service=service, + desiredCount=0, ) - for service in services['serviceArns']: - update_response = ecs_client.update_service( - cluster = cluster, - service = service, - desiredCount = 0) return 0 + def set_ecs_desired_task_count_to_normal(cluster): - """Function to set ECS Desired Tasks counts to 2 for all ECS Cluster Services except trafficgeneratorservice""" - services = ecs_client.list_services( - cluster = cluster - ) - for service in services['serviceArns']: + """ + Function to set ECS Desired Tasks counts to 2 for all ECS Cluster + Services except trafficgeneratorservice + """ + services = ecs_client.list_services(cluster=cluster) + for service in services["serviceArns"]: desired_count = 2 - if 'trafficgeneratorserviceecsservice' in service: + if "trafficgeneratorserviceecsservice" in service: desired_count = 1 - update_response = ecs_client.update_service( - cluster = cluster, - service = service, - desiredCount = desired_count) + ecs_client.update_service( + cluster=cluster, + service=service, + desiredCount=desired_count, + ) return 0 + def manageECSTasks(status): """Enable/Disable ECS Tasks""" clusters = ecs_client.list_clusters() - for cluster in clusters['clusterArns']: + for cluster in clusters["clusterArns"]: if cluster in ECS_CLUSTER_ARNS: if status == "enable": set_ecs_desired_task_count_to_normal(cluster) @@ -55,50 +62,47 @@ def manageECSTasks(status): else: return "ECS Tasks Count set to Zero" -def waitTillUpdateCompletes(update_response,nodegroup_name): + +def waitTillUpdateCompletes(update_response, nodegroup_name): """Waiter function to wait till EKS cluster accepts the update""" update_in_progress = True while update_in_progress: update_status = eks_client.describe_update( - name = EKS_CLUSTER_NAME, - updateId = update_response['update']['id'], - nodegroupName = nodegroup_name + name=EKS_CLUSTER_NAME, + updateId=update_response["update"]["id"], + nodegroupName=nodegroup_name, ) - if update_status['update']['status'] == 'InProgress': + if update_status["update"]["status"] == "InProgress": time.sleep(5) else: update_in_progress = False + def setEKSNodeGroupCountToNormal(nodegroup_name): """Function to set EKS nodegroup node desired count to 2""" update_response = eks_client.update_nodegroup_config( clusterName=EKS_CLUSTER_NAME, nodegroupName=nodegroup_name, - scalingConfig={ - 'minSize': 2, - 'desiredSize': 2 - }, + scalingConfig={"minSize": 2, "desiredSize": 2}, ) - waitTillUpdateCompletes(update_response,nodegroup_name) + waitTillUpdateCompletes(update_response, nodegroup_name) + def setEKSNodeGroupCountToZero(nodegroup_name): """Function to set EKS nodegroup node desired count to 0""" update_response = eks_client.update_nodegroup_config( clusterName=EKS_CLUSTER_NAME, nodegroupName=nodegroup_name, - scalingConfig={ - 'minSize': 0, - 'desiredSize': 0 - }, + scalingConfig={"minSize": 0, "desiredSize": 0}, ) - waitTillUpdateCompletes(update_response,nodegroup_name) + waitTillUpdateCompletes(update_response, nodegroup_name) + def manageEKSNodes(status): """Enable/Disable the EKS nodegroup Count""" - nodegroups = eks_client.list_nodegroups( - clusterName=EKS_CLUSTER_NAME) - for group in nodegroups['nodegroups']: - if EKS_CLUSTER_NAME.lower()+'nodegroup' in group.lower(): + nodegroups = eks_client.list_nodegroups(clusterName=EKS_CLUSTER_NAME) + for group in nodegroups["nodegroups"]: + if EKS_CLUSTER_NAME.lower() + "nodegroup" in group.lower(): nodegroup_name = group break if status == "enable": @@ -111,6 +115,6 @@ def manageEKSNodes(status): def lambda_handler(event, context): """Main function to control ECS and EKS resources""" - output1 = manageECSTasks(event['Action']) - output2 = manageEKSNodes(event['Action']) - return output1 + ', '+ output2 + output1 = manageECSTasks(event["Action"]) + output2 = manageEKSNodes(event["Action"]) + return output1 + ", " + output2 diff --git a/PetAdoptions/cdk/pet_stack/resources/seed-data.json b/archive/resources/seed-data.json similarity index 100% rename from PetAdoptions/cdk/pet_stack/resources/seed-data.json rename to archive/resources/seed-data.json diff --git a/PetAdoptions/cdk/pet_stack/resources/setup-ssm-agent.yaml b/archive/resources/setup-ssm-agent.yaml similarity index 87% rename from PetAdoptions/cdk/pet_stack/resources/setup-ssm-agent.yaml rename to archive/resources/setup-ssm-agent.yaml index ea02f92d..f66fb88a 100644 --- a/PetAdoptions/cdk/pet_stack/resources/setup-ssm-agent.yaml +++ b/archive/resources/setup-ssm-agent.yaml @@ -1,3 +1,5 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 apiVersion: v1 kind: Namespace metadata: @@ -24,14 +26,14 @@ apiVersion: rbac.authorization.k8s.io/v1 kind: RoleBinding metadata: name: ssm-agent-installer - namespace: node-configuration-daemonset + namespace: node-configuration-daemonset roleRef: kind: ClusterRole name: ssm-agent-installer apiGroup: rbac.authorization.k8s.io subjects: - kind: ServiceAccount - name: ssm-agent-installer + name: ssm-agent-installer namespace: node-configuration-daemonset --- apiVersion: v1 @@ -48,7 +50,7 @@ data: if echo $STATUS | grep -q "running"; then echo "Success" else - echo "Fail" >&2 + echo "Fail" >&2 fi --- apiVersion: apps/v1 @@ -88,7 +90,7 @@ spec: containers: - image: "gcr.io/google-containers/pause:2.0" name: pause - securityContext: - allowPrivilegeEscalation: false - runAsUser: 1000 + securityContext: + allowPrivilegeEscalation: false + runAsUser: 1000 readOnlyRootFilesystem: true diff --git a/archive/resources/stepfn_lambdas/lambda_step_priceGreaterThan55.py b/archive/resources/stepfn_lambdas/lambda_step_priceGreaterThan55.py new file mode 100644 index 00000000..0d054489 --- /dev/null +++ b/archive/resources/stepfn_lambdas/lambda_step_priceGreaterThan55.py @@ -0,0 +1,14 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +import json + + +def lambda_handler(event, context): + # TODO implement + print(event) + + print("ProcessGreaterThan55 - Execution complete") + return { + "statusCode": 200, + "body": json.dumps("ProcessGreaterThan55 - Execution complete"), + } diff --git a/archive/resources/stepfn_lambdas/lambda_step_priceLessThan55.py b/archive/resources/stepfn_lambdas/lambda_step_priceLessThan55.py new file mode 100644 index 00000000..d0c8c634 --- /dev/null +++ b/archive/resources/stepfn_lambdas/lambda_step_priceLessThan55.py @@ -0,0 +1,13 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +import json + + +def lambda_handler(event, context): + # TODO implement + print(event) + print("ProcessLessthan55 - Execution complete") + return { + "statusCode": 200, + "body": json.dumps("ProcessLessthan55 - Execution complete"), + } diff --git a/archive/resources/stepfn_lambdas/lambda_step_readDDB.py b/archive/resources/stepfn_lambdas/lambda_step_readDDB.py new file mode 100644 index 00000000..f7e2de23 --- /dev/null +++ b/archive/resources/stepfn_lambdas/lambda_step_readDDB.py @@ -0,0 +1,26 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +import boto3 +from boto3.dynamodb.conditions import Key + +ssm = boto3.client("ssm") +dynamodb = boto3.resource("dynamodb") + + +def lambda_handler(event, context): + + dynamodb_tablename = ssm.get_parameter( + Name="/petstore/dynamodbtablename", + WithDecryption=False, + ) + + table = dynamodb.Table(dynamodb_tablename["Parameter"]["Value"]) + + response = table.query( + KeyConditionExpression=Key("petid").eq(event["petid"]) + & Key("pettype").eq(event["pettype"]), + ) + + response["Items"][0]["price"] = int(response["Items"][0]["price"]) + + return {"statusCode": 200, "body": response["Items"][0]} diff --git a/archive/resources/syn-apicanary.js b/archive/resources/syn-apicanary.js new file mode 100644 index 00000000..a25f7aa6 --- /dev/null +++ b/archive/resources/syn-apicanary.js @@ -0,0 +1,66 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +var synthetics = require('Synthetics'); +const log = require('SyntheticsLogger'); +const https = require('node:https'); +const http = require('node:http'); + +const apiCanaryBlueprint = async function () { + const postData = ''; + + const verifyRequest = async function (requestOption) { + return new Promise((resolve, reject) => { + log.info('Making request with options: ' + JSON.stringify(requestOption)); + let request; + request = requestOption.port === 443 ? https.request(requestOption) : http.request(requestOption); + request.on('response', (res) => { + log.info(`Status Code: ${res.statusCode}`); + log.info(`Response Headers: ${JSON.stringify(res.headers)}`); + if (res.statusCode !== 200) { + reject('Failed: ' + requestOption.path); + } + res.on('data', (d) => { + log.info('Response: ' + d.length); + if (d.length <= 2) { + reject('PetType Invalid - : ' + requestOption.path); + } + }); + res.on('end', () => { + resolve(); + }); + }); + + request.on('error', (error) => { + reject(error); + }); + + if (postData) { + request.write(postData); + } + request.end(); + }); + }; + + const headers = {}; + headers['User-Agent'] = [synthetics.getCanaryUserAgentString(), headers['User-Agent']].join(' '); + + const pettypes = ['fish', 'bunny', 'puppy', 'kitten']; + const position = Math.floor(Math.random() * Math.floor(4)); + + const requestOptions = { + hostname: '', + // Example- hostname: 'petsearch-live.us-east-1.elasticbeanstalk.com', + port: 80, + path: '/api/search?pettype=' + pettypes[position], + method: 'GET', + }; + + requestOptions['headers'] = headers; + await verifyRequest(requestOptions); +}; + +exports.handler = async () => { + return await apiCanaryBlueprint(); +}; diff --git a/PetAdoptions/cdk/pet_stack/resources/syn-uicanary.js b/archive/resources/syn-uicanary.js similarity index 64% rename from PetAdoptions/cdk/pet_stack/resources/syn-uicanary.js rename to archive/resources/syn-uicanary.js index 913c40fb..f15e6525 100644 --- a/PetAdoptions/cdk/pet_stack/resources/syn-uicanary.js +++ b/archive/resources/syn-uicanary.js @@ -1,46 +1,50 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ var synthetics = require('Synthetics'); const log = require('SyntheticsLogger'); const flowBuilderBlueprint = async function () { // INSERT URL here //let url = "http://petsite-1081345346.us-east-1.elb.amazonaws.com/"; - let url = ""; + let url = ''; let page = await synthetics.getPage(); // Navigate to the initial url - await synthetics.executeStep('navigateToUrl', async function (timeoutInMillis = 30000) { - await page.goto(url, {waitUntil: ['load', 'networkidle0'], timeout: timeoutInMillis}); + await synthetics.executeStep('navigateToUrl', async function (timeoutInMillis = 30_000) { + await page.goto(url, { waitUntil: ['load', 'networkidle0'], timeout: timeoutInMillis }); }); // Execute customer steps await synthetics.executeStep('customerActions', async function () { - await page.waitForSelector("[id='searchpets']", { timeout: 30000 }); + await page.waitForSelector("[id='searchpets']", { timeout: 30_000 }); await page.click("[id='searchpets']"); try { - await synthetics.takeScreenshot("click", 'result'); - } catch(ex) { - synthetics.addExecutionError('Unable to capture screenshot.', ex); + await synthetics.takeScreenshot('click', 'result'); + } catch (error) { + synthetics.addExecutionError('Unable to capture screenshot.', error); } - await page.waitForSelector("[id='seeadoptionlist']", { timeout: 30000 }); + await page.waitForSelector("[id='seeadoptionlist']", { timeout: 30_000 }); await page.click("[id='seeadoptionlist']"); try { - await synthetics.takeScreenshot("click", 'result'); - } catch(ex) { - synthetics.addExecutionError('Unable to capture screenshot.', ex); + await synthetics.takeScreenshot('click', 'result'); + } catch (error) { + synthetics.addExecutionError('Unable to capture screenshot.', error); } - await page.waitForSelector("[id='performhousekeeping']", { timeout: 30000 }); + await page.waitForSelector("[id='performhousekeeping']", { timeout: 30_000 }); await page.click("[id='performhousekeeping']"); try { - await synthetics.takeScreenshot("click", 'result'); - } catch(ex) { - synthetics.addExecutionError('Unable to capture screenshot.', ex); + await synthetics.takeScreenshot('click', 'result'); + } catch (error) { + synthetics.addExecutionError('Unable to capture screenshot.', error); } }); }; exports.handler = async () => { return await flowBuilderBlueprint(); -}; \ No newline at end of file +}; diff --git a/PetAdoptions/cdk/pet_stack/resources/use_cases/observability-getting-started-ADOT.yml b/archive/resources/use_cases/observability-getting-started-ADOT.yml similarity index 98% rename from PetAdoptions/cdk/pet_stack/resources/use_cases/observability-getting-started-ADOT.yml rename to archive/resources/use_cases/observability-getting-started-ADOT.yml index a8ebf1b6..143dbb70 100644 --- a/PetAdoptions/cdk/pet_stack/resources/use_cases/observability-getting-started-ADOT.yml +++ b/archive/resources/use_cases/observability-getting-started-ADOT.yml @@ -1,3 +1,5 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 #* #* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. #* SPDX-License-Identifier: MIT-0 diff --git a/PetAdoptions/cdk/pet_stack/resources/use_cases/observability-getting-started.yml b/archive/resources/use_cases/observability-getting-started.yml similarity index 99% rename from PetAdoptions/cdk/pet_stack/resources/use_cases/observability-getting-started.yml rename to archive/resources/use_cases/observability-getting-started.yml index 142b61e2..04919039 100644 --- a/PetAdoptions/cdk/pet_stack/resources/use_cases/observability-getting-started.yml +++ b/archive/resources/use_cases/observability-getting-started.yml @@ -1,3 +1,5 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 #* #* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. #* SPDX-License-Identifier: MIT-0 diff --git a/PetAdoptions/cdk/pet_stack/resources/workshop_amg_dashboard.json b/archive/resources/workshop_amg_dashboard.json similarity index 100% rename from PetAdoptions/cdk/pet_stack/resources/workshop_amg_dashboard.json rename to archive/resources/workshop_amg_dashboard.json diff --git a/aws-codeguru-reviewer.yml b/aws-codeguru-reviewer.yml index 92216d10..3f7ebc0e 100644 --- a/aws-codeguru-reviewer.yml +++ b/aws-codeguru-reviewer.yml @@ -1,3 +1,5 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 version: 1.0 excludeFiles: diff --git a/buildspec.yml b/buildspec.yml index 74ea7199..4e1d9e03 100644 --- a/buildspec.yml +++ b/buildspec.yml @@ -1,3 +1,5 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 version: 0.2 env: variables: diff --git a/codepipeline-stack.yaml b/codepipeline-stack.yaml deleted file mode 100644 index 038957c1..00000000 --- a/codepipeline-stack.yaml +++ /dev/null @@ -1,551 +0,0 @@ -Description: One Observability Workshop Pipeline -Parameters: - EnvironmentName: - Description: An environment name that is prefixed to resource names - Type: String - Default: OneObservability - - VpcCIDR: - Description: Please enter the IP range (CIDR notation) for this VPC - Type: String - Default: 10.192.0.0/16 - - PublicSubnet1CIDR: - Description: Please enter the IP range (CIDR notation) for the public subnet in the first Availability Zone - Type: String - Default: 10.192.10.0/24 - - PrivateSubnet1CIDR: - Description: Please enter the IP range (CIDR notation) for the private subnet in the first Availability Zone - Type: String - Default: 10.192.20.0/24 - - UserRoleArn: - Description: "ARN of the Role that will have access to manage the EKS Cluster" - Type: String - - GithubBranch: - Description: "Source branch to use for CodePipeline deployment" - Type: String - Default: main - - GithubUser: - Description: "Github user to use for CodePipeline deployment" - Type: String - Default: aws-samples - -Resources: - - VPC: - Type: AWS::EC2::VPC - Properties: - CidrBlock: !Ref VpcCIDR - EnableDnsSupport: true - EnableDnsHostnames: true - Tags: - - Key: Name - Value: !Ref EnvironmentName - - InternetGateway: - Type: AWS::EC2::InternetGateway - Properties: - Tags: - - Key: Name - Value: !Ref EnvironmentName - - InternetGatewayAttachment: - Type: AWS::EC2::VPCGatewayAttachment - Properties: - InternetGatewayId: !Ref InternetGateway - VpcId: !Ref VPC - - PublicSubnet1: - Type: AWS::EC2::Subnet - Properties: - VpcId: !Ref VPC - AvailabilityZone: !Select [ 0, !GetAZs '' ] - CidrBlock: !Ref PublicSubnet1CIDR - MapPublicIpOnLaunch: true - Tags: - - Key: Name - Value: !Sub ${EnvironmentName} Public Subnet (AZ1) - - - PrivateSubnet1: - Type: AWS::EC2::Subnet - Properties: - VpcId: !Ref VPC - AvailabilityZone: !Select [ 0, !GetAZs '' ] - CidrBlock: !Ref PrivateSubnet1CIDR - MapPublicIpOnLaunch: false - Tags: - - Key: Name - Value: !Sub ${EnvironmentName} Private Subnet (AZ1) - - NatGateway1EIP: - Type: AWS::EC2::EIP - DependsOn: InternetGatewayAttachment - Properties: - Domain: vpc - - NatGateway1: - Type: AWS::EC2::NatGateway - Properties: - AllocationId: !GetAtt NatGateway1EIP.AllocationId - SubnetId: !Ref PublicSubnet1 - - - PublicRouteTable: - Type: AWS::EC2::RouteTable - Properties: - VpcId: !Ref VPC - Tags: - - Key: Name - Value: !Sub ${EnvironmentName} Public Routes - - DefaultPublicRoute: - Type: AWS::EC2::Route - DependsOn: InternetGatewayAttachment - Properties: - RouteTableId: !Ref PublicRouteTable - DestinationCidrBlock: 0.0.0.0/0 - GatewayId: !Ref InternetGateway - - PublicSubnet1RouteTableAssociation: - Type: AWS::EC2::SubnetRouteTableAssociation - Properties: - RouteTableId: !Ref PublicRouteTable - SubnetId: !Ref PublicSubnet1 - - PrivateRouteTable1: - Type: AWS::EC2::RouteTable - Properties: - VpcId: !Ref VPC - Tags: - - Key: Name - Value: !Sub ${EnvironmentName} Private Routes (AZ1) - - DefaultPrivateRoute1: - Type: AWS::EC2::Route - Properties: - RouteTableId: !Ref PrivateRouteTable1 - DestinationCidrBlock: 0.0.0.0/0 - NatGatewayId: !Ref NatGateway1 - - PrivateSubnet1RouteTableAssociation: - Type: AWS::EC2::SubnetRouteTableAssociation - Properties: - RouteTableId: !Ref PrivateRouteTable1 - SubnetId: !Ref PrivateSubnet1 - - - NoIngressSecurityGroup: - Type: AWS::EC2::SecurityGroup - Properties: - GroupName: "no-ingress-sg" - GroupDescription: "Security group with no ingress rule" - VpcId: !Ref VPC - - PipelineArtifactsBucket: - Type: AWS::S3::Bucket - Properties: - AccessControl: Private - PublicAccessBlockConfiguration: - BlockPublicAcls: true - BlockPublicPolicy: true - RestrictPublicBuckets: true - IgnorePublicAcls: true - DeletionPolicy: Retain - - PipelineRole: - Type: AWS::IAM::Role - Properties: - AssumeRolePolicyDocument: - Statement: - - Action: sts:AssumeRole - Effect: Allow - Principal: - Service: codepipeline.amazonaws.com - Version: "2012-10-17" - ManagedPolicyArns: - - arn:aws:iam::aws:policy/AdministratorAccess - - PipelineRoleDefaultPolicy: - Type: AWS::IAM::Policy - Properties: - PolicyDocument: - Statement: - - Action: - - s3:GetObject* - - s3:GetBucket* - - s3:List* - - s3:DeleteObject* - - s3:PutObject - - s3:Abort* - Effect: Allow - Resource: "*" - - Action: sts:AssumeRole - Effect: Allow - Resource: - Fn::GetAtt: - - PipelineDeployActionRole - - Arn - - Version: "2012-10-17" - PolicyName: PipelineRoleDefaultPolicy - Roles: - - Ref: PipelineRole - - PipelineDeployActionRole: - Type: AWS::IAM::Role - Properties: - AssumeRolePolicyDocument: - Statement: - - Action: sts:AssumeRole - Effect: Allow - Principal: - Service: codebuild.amazonaws.com - Version: "2012-10-17" - ManagedPolicyArns: - - arn:aws:iam::aws:policy/AdministratorAccess - - PipelineDeployActionRoleDefaultPolicy: - Type: AWS::IAM::Policy - Properties: - PolicyDocument: - Statement: - - Action: - - logs:CreateLogGroup - - logs:CreateLogStream - - logs:PutLogEvents - Effect: Allow - Resource: "*" - - Action: - - codebuild:CreateReportGroup - - codebuild:CreateReport - - codebuild:UpdateReport - - codebuild:BatchPutTestCases - - codebuild:BatchPutCodeCoverages - Effect: Allow - Resource: "*" - - Action: - - s3:GetObject* - - s3:GetBucket* - - s3:List* - - s3:DeleteObject* - - s3:PutObject - - s3:Abort* - Effect: Allow - Resource: "*" - Version: "2012-10-17" - PolicyName: PipelineDeployActionRoleDefaultPolicy - Roles: - - Ref: PipelineDeployActionRole - - - PipelineDeployProject: - Type: AWS::CodeBuild::Project - Properties: - Artifacts: - Type: CODEPIPELINE - TimeoutInMinutes: 90 - Environment: - ComputeType: BUILD_GENERAL1_SMALL - Image: aws/codebuild/standard:7.0 - ImagePullCredentialsType: CODEBUILD - PrivilegedMode: true - Type: LINUX_CONTAINER - EnvironmentVariables: - - Name: USER_ROLE_ARN - Type: PLAINTEXT - Value: !Ref UserRoleArn - - Name: GITHUB_USER - Type: PLAINTEXT - Value: !Ref GithubUser - - Name: GITHUB_BRANCH - Type: PLAINTEXT - Value: !Ref GithubBranch - # https://github.com/cdklabs/cdk-ecr-deployment/issues/478#issuecomment-1938020710 - - Name: NO_PREBUILT_LAMBDA - Type: PLAINTEXT - Value: 1 - ServiceRole: - Fn::GetAtt: - - PipelineDeployActionRole - - Arn - Source: - Type: CODEPIPELINE - BuildSpec: | - version: 0.2 - phases: - install: - commands: - - nohup /usr/local/bin/dockerd --host=unix:///var/run/docker.sock --host=tcp://127.0.0.1:2375 --storage-driver=overlay2 & - - timeout 15 sh -c "until docker info; do echo .; sleep 1; done" - - npm install aws-cdk -g - - CDK_STACK=$(aws cloudformation list-stacks --query 'StackSummaries[?(StackName==`CDKToolkit` && StackStatus==`CREATE_COMPLETE`)].StackId' --output text) - build: - commands: - - git clone --single-branch --depth 1 --branch ${GITHUB_BRANCH} https://github.com/${GITHUB_USER}/one-observability-demo.git - - cd ./one-observability-demo/PetAdoptions/cdk/pet_stack/ - - npm install - - if [ -z "$CDK_STACK" ] ; then cdk bootstrap ; else echo "Already bootstrapped" ; fi - - cdk deploy Services --context admin_role=${USER_ROLE_ARN} --require-approval=never --verbose -O ./out/out.json - - cdk deploy Applications --require-approval=never --verbose -O ./out/out.json - artifacts: - files: './one-observability-demo/PetAdoptions/cdk/pet_stack/out/out.json' - LogsConfig: - CloudWatchLogs: - Status: ENABLED - GroupName: "/codebuild/PipelineDeployProject" - VpcConfig: - VpcId: !Ref VPC - Subnets: - - !Ref PrivateSubnet1 - SecurityGroupIds: - - Fn::GetAtt: - - NoIngressSecurityGroup - - GroupId - - Pipeline: - Type: AWS::CodePipeline::Pipeline - Properties: - RoleArn: - Fn::GetAtt: - - PipelineRole - - Arn - Stages: - - Actions: - - ActionTypeId: - Category: Source - Owner: AWS - Provider: S3 - Version: "1" - Configuration: - S3Bucket: !Ref PipelineSourceBucket - S3ObjectKey: SourceCode.zip - PollForSourceChanges: "true" - Name: S3BucketSource - OutputArtifacts: - - Name: Artifact_Source_S3Bucket - RunOrder: 1 - Name: Source - - Actions: - - ActionTypeId: - Category: Build - Owner: AWS - Provider: CodeBuild - Version: "1" - Configuration: - ProjectName: - Ref: PipelineDeployProject - InputArtifacts: - - Name: Artifact_Source_S3Bucket - OutputArtifacts: - - Name: Artifact_Build_CodeCommit - Name: Deploy - RunOrder: 2 - Name: UpdatePipeline - - ArtifactStore: - Location: - Ref: PipelineArtifactsBucket - Type: S3 - RestartExecutionOnUpdate: true - - PipelineEventRule: - Type: AWS::Events::Rule - Properties: - Description: Pipeline results notification - EventPattern: { - "source": ["aws.codepipeline"], - "detail-type": ["CodePipeline Pipeline Execution State Change"], - "detail": { - "state": ["FAILED", "CANCELED", "SUCCEEDED"] - } - } - RoleArn: !GetAtt CodePipelineReadyFunctionRole.Arn - State: "ENABLED" - Targets: - - Arn: !GetAtt CodePipelineReadyFunction.Arn - Id: "FailedPipeline" - - CodePipelineReadyFunctionRole: - Type: AWS::IAM::Role - Properties: - AssumeRolePolicyDocument: - Statement: - - Action: sts:AssumeRole - Effect: Allow - Principal: - Service: lambda.amazonaws.com - - Action: sts:AssumeRole - Effect: Allow - Principal: - Service: codebuild.amazonaws.com - - Action: sts:AssumeRole - Effect: Allow - Principal: - Service: events.amazonaws.com - Version: "2012-10-17" - ManagedPolicyArns: - - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - - arn:aws:iam::aws:policy/AWSCodePipelineCustomActionAccess - - CodePipelineReadyFunction: - Type: AWS::Lambda::Function - Properties: - Handler: index.handler - Role: !GetAtt CodePipelineReadyFunctionRole.Arn - Code: - ZipFile: | - import json - import os - import urllib3 - import uuid - import logging - - logger = logging.getLogger() - logger.setLevel(logging.DEBUG) - - def handler(event, context): - logger.info('Received event{}'.format(event)) - - result = event['detail']['state'] - status = "FAILURE" - - if result == 'SUCCEEDED': - status = 'SUCCESS' - - encoded_body = json.dumps({ - "Status": status, - "Reason": "CodePipeline Deploy ended", - "UniqueId": str(uuid.uuid4()), - "Data": "CodePipeline Deploy ended" - }) - - logger.info('Sending response {}'.format(encoded_body)) - http = urllib3.PoolManager() - http.request('PUT', os.environ['SIGNAL_URL'], body=encoded_body) - - Runtime: python3.9 - Timeout: 900 - Environment: - Variables: - SIGNAL_URL: !Ref CodePipelineWaitHandle - - CodePipelineWaitHandle: - Type: AWS::CloudFormation::WaitConditionHandle - - CodePipelineWaitCondition: - Type: AWS::CloudFormation::WaitCondition - Properties: - Handle: !Ref CodePipelineWaitHandle - Timeout: "5400" - - PermissionForEventsToInvokeLambda: - Type: AWS::Lambda::Permission - Properties: - FunctionName: - Ref: "CodePipelineReadyFunction" - Action: "lambda:InvokeFunction" - Principal: "events.amazonaws.com" - SourceArn: - Fn::GetAtt: - - "PipelineEventRule" - - "Arn" - - PipelineSourceBucket: - Type: AWS::S3::Bucket - Properties: - VersioningConfiguration: - Status: Enabled - AccessControl: Private - PublicAccessBlockConfiguration: - BlockPublicAcls: true - BlockPublicPolicy: true - RestrictPublicBuckets: true - IgnorePublicAcls: true - DeletionPolicy: Retain - - SourceCodeLambdaRole: - Type: AWS::IAM::Role - Properties: - AssumeRolePolicyDocument: - Statement: - - Action: sts:AssumeRole - Effect: Allow - Principal: - Service: lambda.amazonaws.com - Version: "2012-10-17" - ManagedPolicyArns: - - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole - - SourceCodeLambdaRolePolicy: - Type: AWS::IAM::Policy - Properties: - PolicyDocument: - Statement: - - Action: - - s3:GetObject* - - s3:GetBucket* - - s3:List* - - s3:DeleteObject* - - s3:PutObject - - s3:Abort* - Effect: Allow - Resource: "*" - - Version: "2012-10-17" - PolicyName: SourceCodeLambdaRolePolicy - Roles: - - Ref: SourceCodeLambdaRole - - SourceCodeLambdaFunction: - Type: AWS::Lambda::Function - Properties: - Handler: index.handler - Role: !GetAtt SourceCodeLambdaRole.Arn - Code: - ZipFile: | - import json - import os - import boto3 - import cfnresponse - import io - import zipfile - - def handler(event, context): - # Upload the file - s3_client = boto3.client('s3') - responseData = {} - responseResult = cfnresponse.FAILED - try: - print('## ENVIRONMENT VARIABLES') - print(os.environ) - print('## EVENT') - print(event) - destinationBucket = os.environ['DESTINATION_BUCKET'] - zip_buffer = io.BytesIO() - with zipfile.ZipFile(zip_buffer, "a", zipfile.ZIP_DEFLATED, False) as zipper: - file_name = "DUMMY.txt" - infile_content = "Dummy file used to trigger CodeBuild" - zipper.writestr(file_name, infile_content) - - s3_client.put_object(Bucket=destinationBucket, Key='SourceCode.zip', Body=zip_buffer.getvalue()) - responseResult = cfnresponse.SUCCESS - except Exception as e: - responseData['Data'] = str(e) - - cfnresponse.send(event, context, responseResult, responseData, "CustomResourcePhysicalID") - return - Runtime: python3.9 - Timeout: 900 - Environment: - Variables: - DESTINATION_BUCKET: !Ref PipelineSourceBucket - - PushCodeCustom: - Type: Custom::Buildspec - Properties: - ServiceToken: !GetAtt SourceCodeLambdaFunction.Arn \ No newline at end of file diff --git a/docs/CDK_CLEANUP_ASYNC_UPDATE.md b/docs/CDK_CLEANUP_ASYNC_UPDATE.md new file mode 100644 index 00000000..5e7305af --- /dev/null +++ b/docs/CDK_CLEANUP_ASYNC_UPDATE.md @@ -0,0 +1,100 @@ + +# CDK Stack Cleanup - Async Implementation Update + +## Problem Addressed +The original Lambda-based approach had a 15-minute timeout limit, which is insufficient for deleting long-running resources like EKS clusters and Aurora databases that can take 30+ minutes to delete. + +## Solution: Step Functions Native CloudFormation Integration + +### Key Changes Made + +#### 1. Replaced Lambda-based Deletion with Step Functions Native Calls +**Before**: Single Lambda function that waited for stack deletion completion +**After**: Step Functions using native AWS SDK integration with async polling + +#### 2. New State Machine Flow +``` +CheckStackExists → StackExistsChoice → DeleteStack → WaitForDeletion → CheckDeletionStatus + ↓ + StackNotFound (skip) +``` + +#### 3. Async Polling Pattern +- **DeleteStack**: Uses `arn:aws:states:::aws-sdk:cloudformation:deleteStack` +- **WaitForDeletion**: 30-second wait between status checks +- **CheckDeletionStatus**: Uses `arn:aws:states:::aws-sdk:cloudformation:describeStacks` +- **Loop**: Continues until stack is deleted or fails + +#### 4. Simplified Lambda Function +**Old**: `rCDKStackDeleterFunction` (900s timeout, complex deletion logic) +**New**: `rCDKStackCheckerFunction` (60s timeout, simple existence check) + +```python +def handler(event, context): + stack_name = event['stackName'].strip() + if not stack_name: + return {'exists': False} + + cf_client = boto3.client('cloudformation') + try: + cf_client.describe_stacks(StackName=stack_name) + return {'exists': True} + except cf_client.exceptions.ClientError as e: + if 'does not exist' in str(e): + return {'exists': False} + raise +``` + +#### 5. Enhanced Error Handling +- **ValidationError**: Catches when stack no longer exists (successful deletion) +- **Graceful Failures**: Failed deletions don't stop the entire process +- **Status Evaluation**: Handles all CloudFormation stack states + +## Benefits of Async Approach + +### 1. No Timeout Limitations +- Step Functions can run for up to 1 year +- Each individual step has appropriate timeouts +- No 15-minute Lambda limitation + +### 2. Better Resource Utilization +- Lambda only runs for quick checks (60s max) +- Step Functions handles the waiting asynchronously +- No compute resources wasted during wait periods + +### 3. Improved Observability +- Each step is visible in Step Functions console +- Clear state transitions for debugging +- Detailed execution history + +### 4. Cost Optimization +- Lambda execution time reduced from 900s to 60s +- Step Functions state transitions are very low cost +- No continuous compute during wait periods + +## State Machine States Explained + +| State | Purpose | Timeout | Error Handling | +|-------|---------|---------|----------------| +| `CheckStackExists` | Verify stack exists before deletion | 60s | Skip if check fails | +| `DeleteStack` | Initiate CloudFormation deletion | 30s | Catch all errors | +| `WaitForDeletion` | Wait 30 seconds between checks | 30s | None needed | +| `CheckDeletionStatus` | Get current stack status | 30s | ValidationError = success | +| `EvaluateDeletionStatus` | Decide next action based on status | N/A | Default to failed | + +## Supported Stack States +- `DELETE_IN_PROGRESS`: Continue waiting +- `DELETE_COMPLETE`: Success, move to next stack +- `DELETE_FAILED`: Log failure, continue with other stacks +- Stack not found (ValidationError): Deletion successful + +## Maximum Execution Time +With 30-second intervals, the system can theoretically wait indefinitely, but practical limits: +- EKS clusters: ~20-45 minutes +- Aurora clusters: ~15-30 minutes +- Most other resources: <10 minutes + +The async approach easily handles all these scenarios without timeout issues. diff --git a/docs/CDK_CLEANUP_SUMMARY.md b/docs/CDK_CLEANUP_SUMMARY.md new file mode 100644 index 00000000..6de8f11b --- /dev/null +++ b/docs/CDK_CLEANUP_SUMMARY.md @@ -0,0 +1,100 @@ + +# CDK Stack Cleanup Implementation Summary + +## Overview +Added automated cleanup mechanism to the CloudFormation template that handles orphaned CDK stacks when the main stack is deleted or when CodeBuild fails. + +## Changes Made + +### 1. Parameter Store Integration +- **Location**: CodeBuild buildspec in `rCDKDeploymentProject` +- **Change**: Added `cdk list` command to capture all CDK stacks before deployment +- **Storage**: Saves stack list to Parameter Store at `/cdk-cleanup/${STACK_NAME}/stacks` + +```bash +STACK_LIST=$(cdk list --json | jq -r '.[]' | tr '\n' ',' | sed 's/,$//') +aws ssm put-parameter --name "/cdk-cleanup/${STACK_NAME}/stacks" --value "$STACK_LIST" --type "String" --overwrite +``` + +### 2. Step Function State Machine +- **Resource**: `rCDKCleanupStateMachine` +- **Purpose**: Orchestrates the deletion of CDK stacks +- **Process**: + 1. Retrieves stack list from Parameter Store + 2. Parses comma-separated stack names + 3. Deletes each stack sequentially (MaxConcurrency: 1) + 4. Handles missing stacks and dependency failures gracefully + +### 3. Lambda Function for Stack Deletion +- **Resource**: `rCDKStackDeleterFunction` +- **Features**: + - Checks if stack exists before attempting deletion + - Waits for deletion completion with proper timeout + - Handles dependency failures gracefully (returns success to continue with other stacks) + - Comprehensive error handling and logging + +### 4. EventBridge Rules +- **Stack Deletion Trigger**: `rStackDeletionRule` + - Monitors CloudFormation stack status changes + - Triggers cleanup when main stack enters `DELETE_IN_PROGRESS` + +- **CodeBuild Failure Trigger**: `rCodeBuildFailureRule` + - Monitors CodeBuild project status + - Triggers cleanup on `FAILED`, `FAULT`, `STOPPED`, or `TIMED_OUT` + +### 5. Enhanced Post-Build Cleanup +- **Location**: CodeBuild post_build phase +- **Addition**: Manual Step Function trigger on build failure +- **Fallback**: Ensures cleanup runs even if EventBridge rule doesn't trigger + +### 6. IAM Roles and Permissions +- **Step Function Role**: `rCDKCleanupRole` + - SSM parameter read access + - Lambda function invoke permissions + +- **Lambda Execution Role**: `rCDKStackDeleterRole` + - CloudFormation full access for stack operations + +- **EventBridge Role**: `rEventBridgeRole` + - Step Function execution permissions + +### 7. Parameter Store Cleanup +- **Resource**: `rParameterCleanup` (Custom Resource) +- **Purpose**: Removes Parameter Store parameter when main stack is deleted +- **Implementation**: Lambda-backed custom resource + +## Key Features + +### Dependency Handling +- Stacks are deleted sequentially to respect dependencies +- Failed deletions due to dependencies don't stop the process +- Missing stacks are handled gracefully + +### Error Resilience +- Multiple retry mechanisms in Step Function +- Graceful handling of non-existent stacks +- Comprehensive logging for troubleshooting + +### Trigger Mechanisms +1. **Automatic**: EventBridge rules for stack deletion and CodeBuild failure +2. **Manual**: Direct Step Function invocation in post_build phase +3. **Cleanup**: Parameter Store cleanup on stack deletion + +## Outputs Added +- `oCDKCleanupStateMachine`: ARN of the cleanup Step Function for monitoring + +## Usage +The cleanup process is fully automated and requires no manual intervention. Users can monitor the cleanup process through: +1. Step Function execution logs in AWS Console +2. Lambda function logs in CloudWatch +3. CloudFormation stack deletion events + +## Benefits +1. **Prevents Orphaned Resources**: Automatically cleans up CDK stacks +2. **Cost Optimization**: Removes unused infrastructure +3. **Operational Excellence**: Reduces manual cleanup tasks +4. **Fault Tolerance**: Multiple trigger mechanisms ensure cleanup runs +5. **Dependency Aware**: Handles stack dependencies intelligently diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 00000000..f95aebae --- /dev/null +++ b/docs/README.md @@ -0,0 +1,61 @@ + +# Documentation + +This folder contains documentation and diagrams for the One Observability Demo project. + +## Contents + +### Documentation Files + +- **[codebuild-cdk-deployment-template.md](./codebuild-cdk-deployment-template.md)** - Comprehensive guide for the simplified CodeBuild CDK deployment template, including architecture, implementation details, retry handling, and troubleshooting + +### Diagrams + +- **[diagrams/architecture-diagram.png](./diagrams/architecture-diagram.png)** - Complete infrastructure architecture showing all components and their relationships +- **[diagrams/deployment-flow.png](./diagrams/deployment-flow.png)** - Step-by-step deployment process flow diagram + +## Quick Start + +For deploying CDK projects using the automated template, see the **[CodeBuild CDK Deployment Template Guide](./codebuild-cdk-deployment-template.md)**. + +## Usage + +These diagrams and documentation provide visual and detailed explanations of: + +1. **System Architecture**: How different AWS services interact in the deployment process +2. **Process Flow**: The sequence of operations during CDK deployment and pipeline monitoring +3. **Component Relationships**: Dependencies between CloudFormation resources, Lambda functions, and CDK-created infrastructure +4. **Implementation Details**: Step-by-step breakdown of the deployment process +5. **Retry Handling**: Intelligent retry mechanisms for robust deployments +6. **Troubleshooting**: Common issues and debugging techniques + +## Generating New Diagrams + +If you need to regenerate or create new diagrams, you can use the MCP diagram generation tools available in the development environment. The diagrams are created using the Python `diagrams` package with AWS icons. + +Example workflow: +1. Use the `generate_diagram` MCP tool with appropriate code +2. Save diagrams to the `docs/diagrams/` folder +3. Update documentation references as needed +4. Commit both diagrams and documentation updates + +## File Organization + +``` +docs/ +├── README.md # This file +├── codebuild-cdk-deployment-template.md # Template documentation +└── diagrams/ + ├── architecture-diagram.png # Infrastructure architecture + └── deployment-flow.png # Deployment process flow +``` + +## Template Location + +The actual CloudFormation template file is located at: +- **[codebuild-deployment-template-simplified.yaml](../src/templates/codebuild-deployment-template-simplified.yaml)** + +This separation keeps the executable template in the source directory while maintaining all documentation in this centralized docs folder. diff --git a/docs/application-redeployment.md b/docs/application-redeployment.md new file mode 100644 index 00000000..ba58d8ec --- /dev/null +++ b/docs/application-redeployment.md @@ -0,0 +1,180 @@ + +# Application Redeployment Guide + +This guide explains how to use the application redeployment script to quickly deploy new versions of microservices for testing and development. + +## Overview + +The `redeploy-app.sh` script automates the process of: +1. Building container images with cross-platform support +2. Pushing images to Amazon ECR +3. Triggering service redeployments on ECS or providing EKS instructions + +## Prerequisites + +### Required Tools +- **AWS CLI**: Configured with credentials for your target account +- **Container Runtime**: One of the following (checked in priority order): + - Docker (recommended) + - Finch + - Podman + +### AWS Permissions +Your AWS credentials must have permissions for: +- ECR: `GetAuthorizationToken`, `BatchCheckLayerAvailability`, `GetDownloadUrlForLayer`, `BatchGetImage`, `PutImage` +- ECS: `ListClusters`, `ListServices`, `DescribeServices`, `UpdateService` +- STS: `GetCallerIdentity` + +### Infrastructure Requirements +- One Observability Demo infrastructure must be deployed +- ECR repositories must exist for the applications +- ECS clusters and services (for ECS-hosted applications) +- EKS cluster with kubectl access (for EKS-hosted applications) + +## Usage + +### Basic Usage + +1. Navigate to the repository root: + ```bash + cd /path/to/one-observability-demo + ``` + +2. Run the script: + ```bash + ./src/cdk/scripts/redeploy-app.sh + ``` + +3. Follow the interactive prompts: + - Select the application to redeploy + - For ECS applications: Select cluster and service + - For EKS applications: Follow the provided kubectl instructions + +### Available Applications + +The script automatically reads from `src/cdk/bin/environment.ts` and presents these applications: + +| Application | Host Type | Description | +|-------------|-----------|-------------| +| payforadoption-go | ECS | Payment processing service (Go) | +| petlistadoption-go | ECS | Pet listing service (Go) | +| petsearch-java | ECS | Pet search service (Java) | +| petsite | EKS | Frontend web application | +| trafficgenerator | ECS | Load testing service | + +## Cross-Platform Building + +The script automatically handles cross-platform builds for ARM development machines targeting x86/amd64 AWS instances: + +- **Docker**: Uses `buildx` with `--platform linux/amd64` +- **Finch/Podman**: Uses `--platform linux/amd64` flag + +## ECS Deployment Process + +For ECS-hosted applications, the script: + +1. Lists all available ECS clusters +2. Auto-selects if only one cluster exists, otherwise prompts for selection +3. Lists all services in the selected cluster +4. Prompts for service selection +5. Triggers a forced redeployment using `aws ecs update-service --force-new-deployment` + +### Monitoring ECS Deployments + +After triggering a redeployment, monitor progress with: + +```bash +# Check service status +aws ecs describe-services --cluster --services + +# Watch deployment events +aws ecs describe-services --cluster --services \ + --query 'services[0].events[0:5]' --output table +``` + +## EKS Deployment Process + +For EKS-hosted applications (like `petsite`), the script provides kubectl commands for manual restart: + +```bash +# Restart deployment +kubectl rollout restart deployment/petsite + +# Check deployment status +kubectl rollout status deployment/petsite + +# View pods +kubectl get pods -l app=petsite +``` + +## Troubleshooting + +### Common Issues + +**Error: No OCI runner found** +- Install Docker, Finch, or Podman +- Ensure the tool is in your PATH + +**Error: Not logged into AWS** +- Configure AWS CLI: `aws configure` +- Or use AWS SSO: `aws sso login` + +**Error: ECR login failed** +- Verify AWS credentials have ECR permissions +- Check if ECR repositories exist in your account + +**Error: No ECS clusters found** +- Ensure One Observability Demo infrastructure is deployed +- Verify you're in the correct AWS region + +**Error: Cross-platform build failed** +- For Docker: Enable buildx with `docker buildx create --use` +- For Podman: Update to latest version with multi-arch support + +### Debug Mode + +Run with debug output: +```bash +bash -x ./src/cdk/scripts/redeploy-app.sh +``` + +### Manual ECR Operations + +If needed, perform ECR operations manually: + +```bash +# Login to ECR +aws ecr get-login-password --region us-east-1 | \ + docker login --username AWS --password-stdin \ + 123456789012.dkr.ecr.us-east-1.amazonaws.com + +# Build and push +docker buildx build --platform linux/amd64 \ + -t 123456789012.dkr.ecr.us-east-1.amazonaws.com/petsite:latest \ + --push . +``` + +## Script Configuration + +The script reads application configurations from `src/cdk/bin/environment.ts`, specifically the `APPLICATION_LIST` constant. To add new applications: + +1. Define the application in `environment.ts` +2. Add it to the `APPLICATION_LIST` array +3. The script will automatically detect it + +## Security Considerations + +- The script uses your current AWS credentials +- Container images are pushed to ECR in your account +- ECS service updates trigger new task deployments +- Ensure your development images don't contain sensitive data + +## Performance Tips + +- Use Docker buildx for faster multi-platform builds +- Consider using ECR image scanning for security +- Monitor ECS service metrics during deployments +- Use ECS deployment circuit breaker for safer rollouts \ No newline at end of file diff --git a/docs/codebuild-cdk-deployment-template.md b/docs/codebuild-cdk-deployment-template.md new file mode 100644 index 00000000..153ab71e --- /dev/null +++ b/docs/codebuild-cdk-deployment-template.md @@ -0,0 +1,428 @@ + +# CodeBuild CDK Deployment Template + +This CloudFormation template automates the deployment of AWS Cloud Development Kit (CDK) projects for workshop environments. It provides a simplified, robust solution for bootstrapping AWS accounts, deploying CDK applications, and monitoring pipeline executions with intelligent retry handling. + +## Table of Contents + +- [Overview](#overview) +- [Architecture](#architecture) +- [Deployment Flow](#deployment-flow) +- [Key Features](#key-features) +- [Usage](#usage) +- [Parameters](#parameters) +- [Implementation Details](#implementation-details) +- [Retry Handling](#retry-handling) +- [Monitoring and Debugging](#monitoring-and-debugging) +- [Troubleshooting](#troubleshooting) +- [Security Considerations](#security-considerations) + +## Overview + +This template creates a complete infrastructure for automated CDK deployments, including: + +- **S3 bucket** for configuration storage and CodePipeline source +- **CodeBuild project** for CDK deployment orchestration +- **Lambda functions** for deployment initiation and resource cleanup +- **IAM roles** with appropriate permissions +- **Wait conditions** for CloudFormation synchronization +- **Intelligent pipeline monitoring** with retry support + +## Architecture + +![Architecture Diagram](docs/diagrams/architecture-diagram.png) + +The architecture consists of several key components: + +1. **External Resources**: GitHub repository and configuration files +2. **CloudFormation Stack**: Core infrastructure components +3. **CDK Created Resources**: Pipeline and deployed infrastructure + +## Deployment Flow + +![Deployment Flow](docs/diagrams/deployment-flow.png) + +The deployment follows this detailed flow: + +```mermaid +flowchart TD + A[CloudFormation Stack Creation] --> B[Create S3 Bucket & IAM Roles] + B --> C[Start Deployment Lambda] + C --> D[CodeBuild Project Starts] + + D --> E[Clone GitHub Repository] + E --> F[Download Configuration File] + F --> G[Setup S3 Remote for Pipeline] + G --> H[Check CDK Bootstrap Status] + + H --> I{Bootstrap Required?} + I -->|Yes| J[Bootstrap CDK] + I -->|No| K[Deploy CDK Application] + J --> K + + K --> L[Extract Pipeline ARN] + L --> M[Start Pipeline Monitoring] + + M --> N[Get Pipeline Status] + N --> O{Pipeline Status?} + + O -->|InProgress| P[Wait 30 seconds] + P --> N + + O -->|Succeeded| Q[Signal CloudFormation SUCCESS] + + O -->|Failed| R{Retries Available?} + R -->|Yes| S[Wait for Retry] + S --> T[Detect New Execution] + T --> N + R -->|No| U[Signal CloudFormation FAILURE] + + O -->|Superseded| V[Continue Monitoring New Execution] + V --> N + + O -->|Cancelled/Stopped| U + + Q --> W[Deployment Complete] + U --> X[Cleanup Resources] + X --> Y[Deployment Failed] +``` + +## Key Features + +### 🚀 **Simplified Architecture** +- Removed complex EventBridge rules and custom resources +- Direct pipeline status polling for reliability +- Streamlined resource management + +### 🔄 **Intelligent Retry Handling** +- Automatic detection of pipeline retries +- Configurable retry limits (default: 3 attempts) +- Graceful handling of superseded executions + +### ⏱️ **Extended Timeout Support** +- 1-hour timeout for complex deployments (EKS, RDS, etc.) +- 30-second polling intervals for responsive monitoring +- Progress reports every 5 minutes + +### 🛡️ **Robust Error Handling** +- Comprehensive status checking +- Detailed error reporting and debugging information +- Automatic resource cleanup on failures +- Fixed pipeline name extraction from ARN using correct field delimiter + +### 📊 **Enhanced Monitoring** +- Real-time pipeline status updates +- Detailed stage-level information +- Comprehensive logging for troubleshooting + +## Usage + +### Basic Deployment + +```bash +aws cloudformation create-stack \ + --stack-name MyWorkshop-CDK-Deployment \ + --template-body file://codebuild-deployment-template.yaml \ + --capabilities CAPABILITY_NAMED_IAM \ + --parameters \ + ParameterKey=pOrganizationName,ParameterValue=my-org \ + ParameterKey=pRepositoryName,ParameterValue=my-cdk-project \ + ParameterKey=pBranchName,ParameterValue=main \ + ParameterKey=pWorkingFolder,ParameterValue=src/cdk +``` + +### With Custom Configuration + +```bash +aws cloudformation create-stack \ + --stack-name MyWorkshop-CDK-Deployment \ + --template-body file://codebuild-deployment-template.yaml \ + --capabilities CAPABILITY_NAMED_IAM \ + --parameters \ + ParameterKey=pConfigFileUrl,ParameterValue=https://example.com/config.json \ + ParameterKey=pOrganizationName,ParameterValue=aws-samples \ + ParameterKey=pRepositoryName,ParameterValue=one-observability-demo \ + ParameterKey=pBranchName,ParameterValue=feat/cdkpipeline \ + ParameterKey=pWorkingFolder,ParameterValue=src/cdk \ + ParameterKey=pUserDefinedTagKey1,ParameterValue=Environment \ + ParameterKey=pUserDefinedTagValue1,ParameterValue=Workshop +``` + +## Parameters + +| Parameter | Description | Default | Required | +|-----------|-------------|---------|----------| +| `pConfigFileUrl` | URL to the initial configuration file | `https://raw.githubusercontent.com/aws-samples/one-observability-demo/refs/heads/feat/cdkpipeline/typedoc.json` | Yes | +| `pOrganizationName` | GitHub/CodeCommit organization name | `aws-samples` | Yes | +| `pRepositoryName` | Repository containing the CDK code | `one-observability-demo` | Yes | +| `pBranchName` | Branch to deploy from | `feat/cdkpipeline` | Yes | +| `pWorkingFolder` | Working folder for deployment | `src/cdk` | Yes | +| `pUserDefinedTagKey1-5` | Custom tag keys for resource tagging | Various | No | +| `pUserDefinedTagValue1-5` | Custom tag values for resource tagging | Various | No | + +## Implementation Details + +### CodeBuild Project Configuration + +The CodeBuild project uses: +- **Runtime**: Amazon Linux 2 with Node.js 22 and Python 3.12 +- **Compute**: `BUILD_GENERAL1_SMALL` (sufficient for most CDK deployments) +- **Privileged Mode**: Enabled for Docker operations +- **Timeout**: 60 minutes (CloudFormation level) + +### Build Phases + +#### 1. **Install Phase** +```bash +# Install required tools +npm install -g aws-cdk +pip3 install git-remote-s3 +``` + +#### 2. **Pre-Build Phase** +```bash +# Configure Git and clone repository +git config --global user.email "codebuild@aws.amazon.com" +git config --global user.name "AWS CodeBuild" +git clone --depth 1 --branch $BRANCH_NAME https://github.com/$ORGANIZATION_NAME/$REPOSITORY_NAME.git ./repo + +# Download configuration and setup S3 remote +curl -o ./config.json "$CONFIG_FILE_URL" +git remote add s3 s3+zip://$CONFIG_BUCKET/repo +git push s3 $BRANCH_NAME + +# Handle CDK bootstrapping +if ! aws cloudformation describe-stacks --stack-name CDKToolkit > /dev/null 2>&1; then + cdk bootstrap +fi +``` + +#### 3. **Build Phase** +```bash +# Deploy CDK application +cd $WORKING_FOLDER +npm install +cdk deploy --require-approval never --outputs-file cdk-outputs.json + +# Extract pipeline information +PIPELINE_ARN=$(cat cdk-outputs.json | jq -r '.[] | select(has("PipelineArn")) | .PipelineArn') +PIPELINE_NAME=$(echo $PIPELINE_ARN | cut -d':' -f6) +``` + +#### 4. **Pipeline Monitoring** +The template implements sophisticated pipeline monitoring with: +- **Execution ID tracking** for retry detection +- **Configurable retry limits** (default: 3 attempts) +- **Status-specific handling** for all pipeline states +- **Progress reporting** every 5 minutes +- **Detailed error information** on failures +- **Corrected pipeline name extraction** using ARN field 6 instead of path segment 2 + +## Retry Handling + +### Automatic Retry Detection + +The system automatically detects retries by monitoring pipeline execution IDs: + +```bash +# Get the initial execution ID to track retries +INITIAL_EXECUTION_ID=$(aws codepipeline list-pipeline-executions \ + --pipeline-name "$PIPELINE_NAME" \ + --max-items 1 \ + --query 'pipelineExecutionSummaries[0].pipelineExecutionId' \ + --output text) + +# Check if this is a new execution (retry scenario) +if [ "$CURRENT_EXECUTION_ID" != "$INITIAL_EXECUTION_ID" ]; then + RETRY_COUNT=$((RETRY_COUNT + 1)) + echo "Detected new pipeline execution (retry #$RETRY_COUNT): $CURRENT_EXECUTION_ID" +fi +``` + +### Retry Scenarios Handled + +1. **Manual Retries**: User manually retries failed pipeline execution +2. **Automatic Retries**: Pipeline configured with automatic retry policies +3. **Superseded Executions**: New execution starts while previous is running +4. **Partial Failures**: Individual stage failures with stage-level retries +5. **Timeout Scenarios**: Long-running deployments that exceed estimates + +### Configuration + +- **Maximum Retries**: 3 attempts (configurable via `MAX_RETRIES` variable) +- **Retry Wait Time**: 60 seconds for failed executions +- **Overall Timeout**: 1 hour (3600 seconds) +- **Polling Interval**: 30 seconds + +## Monitoring and Debugging + +### Real-time Status Updates + +The system provides comprehensive monitoring: + +```bash +# Current status logging +echo "Current pipeline execution status: $EXECUTION_STATUS (ID: $CURRENT_EXECUTION_ID)" + +# Progress reports every 5 minutes +if [ $ELAPSED -gt 0 ] && [ $((ELAPSED % 300)) -eq 0 ]; then + echo "Progress check: $((ELAPSED / 60)) minutes elapsed, status: $EXECUTION_STATUS" + + # Detailed stage information + aws codepipeline get-pipeline-state \ + --name "$PIPELINE_NAME" \ + --query 'stageStates[*].[stageName,latestExecution.status]' \ + --output table +fi +``` + +### Error Information + +On timeout or failure, the system provides: +- Final pipeline status +- Total retry attempts +- Detailed stage states +- Error messages from failed stages + +### CloudWatch Logs + +All CodeBuild execution logs are available in CloudWatch Logs under: +- Log Group: `/aws/codebuild/{StackName}-cdk-deployment` +- Log Stream: Individual build execution streams + +## Troubleshooting + +### Common Issues + +#### 1. **CDK Bootstrap Failures** +```bash +# The template handles bootstrap issues automatically +if [ "$STACK_STATUS" = "ROLLBACK_COMPLETE" ]; then + echo "CDK bootstrap stack in ROLLBACK_COMPLETE state, cleaning up resources..." + # Cleanup and re-bootstrap +fi +``` + +#### 2. **Pipeline Not Found** +- Verify CDK deployment succeeded +- Check `cdk-outputs.json` for `PipelineArn` +- Ensure CDK stack exports pipeline ARN + +#### 3. **Timeout Issues** +- Increase timeout for complex deployments +- Check individual stage execution times +- Consider breaking large deployments into smaller stacks + +#### 4. **Permission Issues** +- Verify CodeBuild service role has `AdministratorAccess` +- Check S3 bucket policies +- Ensure proper IAM role trust relationships + +### Debug Commands + +```bash +# Check CodeBuild project status +aws codebuild batch-get-projects --names {StackName}-cdk-deployment + +# View recent build executions +aws codebuild list-builds-for-project --project-name {StackName}-cdk-deployment + +# Get detailed build information +aws codebuild batch-get-builds --ids {BuildId} + +# Check pipeline status +aws codepipeline get-pipeline-state --name {PipelineName} + +# List recent pipeline executions +aws codepipeline list-pipeline-executions --pipeline-name {PipelineName} +``` + +## Security Considerations + +### IAM Permissions + +The template uses `AdministratorAccess` for simplicity in workshop environments. For production use, consider: + +1. **Principle of Least Privilege**: Create custom policies with minimal required permissions +2. **Resource-Specific Permissions**: Limit access to specific resources +3. **Condition-Based Access**: Use conditions to restrict access patterns + +### S3 Security + +- **Encryption**: AES256 server-side encryption enabled +- **Public Access**: Blocked via bucket public access configuration +- **HTTPS Only**: Bucket policy enforces secure transport +- **Versioning**: Enabled for configuration tracking + +### Network Security + +- **VPC**: CodeBuild runs in AWS-managed VPC (consider custom VPC for production) +- **Internet Access**: Required for GitHub access and package downloads +- **Egress Control**: Consider VPC endpoints for AWS service access + +### Secrets Management + +- **No Hardcoded Secrets**: Template avoids hardcoded credentials +- **Environment Variables**: Sensitive data passed via environment variables +- **AWS Secrets Manager**: Consider for production secret management + +## Best Practices + +### 1. **Resource Tagging** +Use the provided tagging parameters to maintain resource organization: +```yaml +Tags: + - Key: Environment + Value: Workshop + - Key: Project + Value: ObservabilityDemo + - Key: Owner + Value: TeamName +``` + +### 2. **Monitoring** +- Enable CloudTrail for API call auditing +- Set up CloudWatch alarms for build failures +- Use AWS Config for compliance monitoring + +### 3. **Cost Management** +- Use appropriate CodeBuild compute sizes +- Clean up resources after workshops +- Monitor S3 storage costs + +### 4. **Version Control** +- Tag template versions +- Maintain changelog +- Use semantic versioning + +## Outputs + +The template provides the following outputs: + +| Output | Description | Export Name | +|--------|-------------|-------------| +| `oConfigBucketName` | S3 bucket name for configuration storage | `{StackName}-ConfigBucketName` | +| `oCodeBuildProjectName` | CodeBuild project name | `{StackName}-CodeBuildProjectName` | +| `oDeploymentStatus` | Deployment status information | `{StackName}-DeploymentStatus` | +| `oRepositoryInfo` | Repository configuration details | `{StackName}-RepositoryInfo` | + +## Contributing + +When contributing to this template: + +1. **Test Changes**: Validate in isolated AWS account +2. **Update Documentation**: Keep README current with changes +3. **Version Control**: Use semantic versioning +4. **Security Review**: Ensure security best practices +5. **Performance Testing**: Validate with various CDK project sizes + +## License + +This template is licensed under the MIT-0 License. See the LICENSE file for details. + +--- + +**Note**: This template is designed for workshop and educational environments. For production use, review and adjust security settings, permissions, and resource configurations according to your organization's requirements. diff --git a/docs/modules.md b/docs/modules.md new file mode 100644 index 00000000..cace20b5 --- /dev/null +++ b/docs/modules.md @@ -0,0 +1,18 @@ + +[**One Observability Workshop v2.0.0**](README.md) + +*** + +# One Observability Workshop v2.0.0 + +## Documents + +- [CONTRIBUTING](documents/CONTRIBUTING.md) +- [ARCHITECTURE](documents/ARCHITECTURE.md) + +## Modules + +- [index](index/README.md) diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 00000000..22cd7c32 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,48 @@ +import typescriptEslint from '@typescript-eslint/eslint-plugin'; +import jest from 'eslint-plugin-jest'; +import tsParser from '@typescript-eslint/parser'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import js from '@eslint/js'; +import { FlatCompat } from '@eslint/eslintrc'; +import eslintPluginUnicorn from 'eslint-plugin-unicorn'; + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all, +}); + +export default [ + ...compat.extends('plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'), + eslintPluginUnicorn.configs.recommended, + { + plugins: { + '@typescript-eslint': typescriptEslint, + jest, + eslintPluginUnicorn, + }, + + languageOptions: { + globals: { + ...jest.environments.globals.globals, + }, + parser: tsParser, + ecmaVersion: 2020, + sourceType: 'module', + }, + + rules: { + 'no-new': 0, + '@typescript-eslint/no-var-requires': 0, + }, + }, + { + files: ['**/*.js'], + rules: { + '@typescript-eslint/no-require-imports': 'off', + }, + }, +]; diff --git a/generated-diagrams/applications-stage-complete.png b/generated-diagrams/applications-stage-complete.png new file mode 100644 index 00000000..f3d1d25d Binary files /dev/null and b/generated-diagrams/applications-stage-complete.png differ diff --git a/generated-diagrams/applications-stage-with-languages.png b/generated-diagrams/applications-stage-with-languages.png new file mode 100644 index 00000000..5e6d2530 Binary files /dev/null and b/generated-diagrams/applications-stage-with-languages.png differ diff --git a/generated-diagrams/complete-architecture-updated.png b/generated-diagrams/complete-architecture-updated.png new file mode 100644 index 00000000..9859d1b3 Binary files /dev/null and b/generated-diagrams/complete-architecture-updated.png differ diff --git a/generated-diagrams/complete-architecture-with-languages.png b/generated-diagrams/complete-architecture-with-languages.png new file mode 100644 index 00000000..23f55438 Binary files /dev/null and b/generated-diagrams/complete-architecture-with-languages.png differ diff --git a/generated-diagrams/compute-stage-resources-corrected.png b/generated-diagrams/compute-stage-resources-corrected.png new file mode 100644 index 00000000..c1e6e964 Binary files /dev/null and b/generated-diagrams/compute-stage-resources-corrected.png differ diff --git a/generated-diagrams/compute-stage-resources.png b/generated-diagrams/compute-stage-resources.png new file mode 100644 index 00000000..6c6a5363 Binary files /dev/null and b/generated-diagrams/compute-stage-resources.png differ diff --git a/generated-diagrams/containers-pipeline-detailed.png b/generated-diagrams/containers-pipeline-detailed.png new file mode 100644 index 00000000..658b56d1 Binary files /dev/null and b/generated-diagrams/containers-pipeline-detailed.png differ diff --git a/generated-diagrams/core-stage-resources.png b/generated-diagrams/core-stage-resources.png new file mode 100644 index 00000000..94d6c29c Binary files /dev/null and b/generated-diagrams/core-stage-resources.png differ diff --git a/generated-diagrams/deployment-stages.png b/generated-diagrams/deployment-stages.png new file mode 100644 index 00000000..a1fc5076 Binary files /dev/null and b/generated-diagrams/deployment-stages.png differ diff --git a/generated-diagrams/microservices-architecture-corrected.png b/generated-diagrams/microservices-architecture-corrected.png new file mode 100644 index 00000000..b2d669fc Binary files /dev/null and b/generated-diagrams/microservices-architecture-corrected.png differ diff --git a/generated-diagrams/microservices-architecture-with-languages.png b/generated-diagrams/microservices-architecture-with-languages.png new file mode 100644 index 00000000..e97c5eb5 Binary files /dev/null and b/generated-diagrams/microservices-architecture-with-languages.png differ diff --git a/generated-diagrams/microservices-architecture.png b/generated-diagrams/microservices-architecture.png new file mode 100644 index 00000000..2a208142 Binary files /dev/null and b/generated-diagrams/microservices-architecture.png differ diff --git a/generated-diagrams/observability-architecture.png b/generated-diagrams/observability-architecture.png new file mode 100644 index 00000000..7abb0cca Binary files /dev/null and b/generated-diagrams/observability-architecture.png differ diff --git a/generated-diagrams/overall-architecture.png b/generated-diagrams/overall-architecture.png new file mode 100644 index 00000000..b53df735 Binary files /dev/null and b/generated-diagrams/overall-architecture.png differ diff --git a/generated-diagrams/storage-stage-resources.png b/generated-diagrams/storage-stage-resources.png new file mode 100644 index 00000000..c4e2069e Binary files /dev/null and b/generated-diagrams/storage-stage-resources.png differ diff --git a/grafana-dashboards/nodeexporter-nodes.json b/grafana-dashboards/nodeexporter-nodes.json deleted file mode 100644 index 0d01759a..00000000 --- a/grafana-dashboards/nodeexporter-nodes.json +++ /dev/null @@ -1,1282 +0,0 @@ -{ - "annotations": { - "list": [ - { - "builtIn": 1, - "datasource": { - "type": "grafana", - "uid": "-- Grafana --" - }, - "enable": true, - "hide": true, - "iconColor": "rgba(0, 211, 255, 1)", - "name": "Annotations & Alerts", - "target": { - "limit": 100, - "matchAny": false, - "tags": [], - "type": "dashboard" - }, - "type": "dashboard" - } - ] - }, - "editable": true, - "fiscalYearStartMonth": 0, - "graphTooltip": 1, - "id": null, - "iteration": 1667228305223, - "links": [], - "liveNow": false, - "panels": [ - { - "collapsed": false, - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 0 - }, - "id": 10, - "panels": [], - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "refId": "A" - } - ], - "title": "CPU", - "type": "row" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${datasource}" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 5, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "never", - "spanNulls": true, - "stacking": { - "group": "A", - "mode": "normal" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "max": 1, - "min": 0, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "percentunit" - }, - "overrides": [] - }, - "gridPos": { - "h": 7, - "w": 12, - "x": 0, - "y": 1 - }, - "id": 2, - "links": [], - "options": { - "legend": { - "calcs": [], - "displayMode": "table", - "placement": "right" - }, - "tooltip": { - "mode": "multi", - "sort": "none" - } - }, - "pluginVersion": "8.4.7", - "targets": [ - { - "datasource": { - "uid": "$datasource" - }, - "exemplar": false, - "expr": "sum (\n (1 - sum without (mode) (rate(node_cpu_seconds_total{job=\"node-exporter\", mode=~\"idle|iowait|steal\", nodename=~\"$instance\"}[5m])))\n/ ignoring(cpu) group_left\n count without (cpu, mode) (node_cpu_seconds_total{job=\"node-exporter\", mode=\"idle\", nodename=~\"$instance\"})\n) by (nodename)\n", - "format": "time_series", - "interval": "", - "intervalFactor": 2, - "legendFormat": "{{nodename}}", - "refId": "A" - } - ], - "title": "CPU Usage", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${datasource}" - }, - "description": "Aggegated CPU load averages (average system load over a period of time)", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 2, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "never", - "spanNulls": true, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "min": 0, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "short" - }, - "overrides": [] - }, - "gridPos": { - "h": 7, - "w": 12, - "x": 12, - "y": 1 - }, - "id": 3, - "links": [], - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom" - }, - "tooltip": { - "mode": "multi", - "sort": "none" - } - }, - "pluginVersion": "8.4.7", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${datasource}" - }, - "exemplar": false, - "expr": "sum(node_load1{job=\"node-exporter\", nodename=~\"$instance\"})", - "format": "time_series", - "interval": "", - "intervalFactor": 2, - "legendFormat": "1m load average", - "refId": "A" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${datasource}" - }, - "exemplar": false, - "expr": "sum(node_load5{job=\"node-exporter\", nodename=~\"$instance\"})", - "format": "time_series", - "interval": "", - "intervalFactor": 2, - "legendFormat": "5m load average", - "refId": "B" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${datasource}" - }, - "exemplar": false, - "expr": "sum(node_load15{job=\"node-exporter\", nodename=~\"$instance\"})", - "format": "time_series", - "interval": "", - "intervalFactor": 2, - "legendFormat": "15m load average", - "refId": "C" - } - ], - "title": "Load Average", - "type": "timeseries" - }, - { - "collapsed": false, - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 8 - }, - "id": 11, - "panels": [], - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "refId": "A" - } - ], - "title": "Memory", - "type": "row" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${datasource}" - }, - "description": "Aggregated values of memory used, buffered, cached and free", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 10, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "never", - "spanNulls": true, - "stacking": { - "group": "A", - "mode": "normal" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "min": 0, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "bytes" - }, - "overrides": [] - }, - "gridPos": { - "h": 7, - "w": 18, - "x": 0, - "y": 9 - }, - "id": 4, - "links": [], - "options": { - "legend": { - "calcs": [], - "displayMode": "list", - "placement": "bottom" - }, - "tooltip": { - "mode": "multi", - "sort": "none" - } - }, - "pluginVersion": "8.4.7", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${datasource}" - }, - "exemplar": false, - "expr": "(\n sum(node_memory_MemTotal_bytes{job=\"node-exporter\", nodename=~\"$instance\"})\n-\n sum(node_memory_MemFree_bytes{job=\"node-exporter\", nodename=~\"$instance\"})\n-\n sum(node_memory_Buffers_bytes{job=\"node-exporter\", nodename=~\"$instance\"})\n-\n sum(node_memory_Cached_bytes{job=\"node-exporter\", nodename=~\"$instance\"})\n)\n", - "format": "time_series", - "interval": "", - "intervalFactor": 2, - "legendFormat": "memory used", - "refId": "A" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${datasource}" - }, - "exemplar": false, - "expr": "sum(node_memory_Buffers_bytes{job=\"node-exporter\", nodename=~\"$instance\"})", - "format": "time_series", - "interval": "", - "intervalFactor": 2, - "legendFormat": "memory buffers", - "refId": "B" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${datasource}" - }, - "exemplar": false, - "expr": "sum(node_memory_Cached_bytes{job=\"node-exporter\", nodename=~\"$instance\"})", - "format": "time_series", - "interval": "", - "intervalFactor": 2, - "legendFormat": "memory cached", - "refId": "C" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${datasource}" - }, - "exemplar": false, - "expr": "sum(node_memory_MemFree_bytes{job=\"node-exporter\", nodename=~\"$instance\"})", - "format": "time_series", - "interval": "", - "intervalFactor": 2, - "legendFormat": "memory free", - "refId": "D" - } - ], - "title": "Memory Usage", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${datasource}" - }, - "description": "Overall memory consumption", - "fieldConfig": { - "defaults": { - "mappings": [], - "max": 100, - "min": 0, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "rgba(50, 172, 45, 0.97)", - "value": null - }, - { - "color": "rgba(237, 129, 40, 0.89)", - "value": 80 - }, - { - "color": "rgba(245, 54, 54, 0.9)", - "value": 90 - } - ] - }, - "unit": "percent" - }, - "overrides": [] - }, - "gridPos": { - "h": 7, - "w": 6, - "x": 18, - "y": 9 - }, - "id": 5, - "options": { - "orientation": "auto", - "reduceOptions": { - "calcs": [ - "lastNotNull" - ], - "fields": "", - "values": false - }, - "showThresholdLabels": false, - "showThresholdMarkers": true - }, - "pluginVersion": "8.4.7", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${datasource}" - }, - "exemplar": false, - "expr": "100 -\n(\n avg(node_memory_MemAvailable_bytes{job=\"node-exporter\", nodename=~\"$instance\"}) /\n avg(node_memory_MemTotal_bytes{job=\"node-exporter\", nodename=~\"$instance\"})\n* 100\n)\n", - "format": "time_series", - "interval": "", - "intervalFactor": 2, - "legendFormat": "", - "refId": "A" - } - ], - "title": "Memory Usage", - "type": "gauge" - }, - { - "collapsed": false, - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 16 - }, - "id": 12, - "panels": [], - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "refId": "A" - } - ], - "title": "Disk", - "type": "row" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${datasource}" - }, - "description": "Disk read bytes per EC2 instance", - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 2, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "never", - "spanNulls": true, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "bytes" - }, - "overrides": [ - { - "matcher": { - "id": "byRegexp", - "options": "/ io time/" - }, - "properties": [ - { - "id": "unit", - "value": "s" - } - ] - } - ] - }, - "gridPos": { - "h": 7, - "w": 12, - "x": 0, - "y": 17 - }, - "id": 6, - "links": [], - "options": { - "legend": { - "calcs": [], - "displayMode": "table", - "placement": "right" - }, - "tooltip": { - "mode": "multi", - "sort": "none" - } - }, - "pluginVersion": "8.4.7", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${datasource}" - }, - "exemplar": true, - "expr": "sum(rate(node_disk_read_bytes_total{job=\"node-exporter\", nodename=~\"$instance\", device=~\"mmcblk.p.+|nvme.+|rbd.+|sd.+|vd.+|xvd.+|dm-.+|dasd.+\"}[5m])) by (nodename, device)", - "format": "time_series", - "interval": "", - "intervalFactor": 2, - "legendFormat": "{{nodename}} {{device}}", - "refId": "A" - } - ], - "title": "Disk I/O - Read", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${datasource}" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 2, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "never", - "spanNulls": true, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "bytes" - }, - "overrides": [ - { - "matcher": { - "id": "byRegexp", - "options": "/ io time/" - }, - "properties": [ - { - "id": "unit", - "value": "s" - } - ] - } - ] - }, - "gridPos": { - "h": 7, - "w": 12, - "x": 12, - "y": 17 - }, - "id": 14, - "links": [], - "options": { - "legend": { - "calcs": [], - "displayMode": "table", - "placement": "right" - }, - "tooltip": { - "mode": "multi", - "sort": "none" - } - }, - "pluginVersion": "8.4.7", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${datasource}" - }, - "exemplar": true, - "expr": "sum(rate(node_disk_written_bytes_total{job=\"node-exporter\", nodename=~\"$instance\", device=~\"mmcblk.p.+|nvme.+|rbd.+|sd.+|vd.+|xvd.+|dm-.+|dasd.+\"}[5m])) by (nodename, device)", - "format": "time_series", - "interval": "", - "intervalFactor": 2, - "legendFormat": "{{nodename}} {{device}}", - "refId": "B" - } - ], - "title": "Disk I/O - Write", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${datasource}" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 2, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "never", - "spanNulls": true, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "bytes" - }, - "overrides": [ - { - "matcher": { - "id": "byRegexp", - "options": "/ io time/" - }, - "properties": [ - { - "id": "unit", - "value": "s" - } - ] - } - ] - }, - "gridPos": { - "h": 7, - "w": 12, - "x": 0, - "y": 24 - }, - "id": 15, - "links": [], - "options": { - "legend": { - "calcs": [], - "displayMode": "table", - "placement": "right" - }, - "tooltip": { - "mode": "multi", - "sort": "none" - } - }, - "pluginVersion": "8.4.7", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${datasource}" - }, - "exemplar": true, - "expr": "sum(rate(node_disk_io_time_seconds_total{job=\"node-exporter\", nodename=~\"$instance\", device=~\"mmcblk.p.+|nvme.+|rbd.+|sd.+|vd.+|xvd.+|dm-.+|dasd.+\"}[5m])) by (nodename, device)", - "format": "time_series", - "interval": "", - "intervalFactor": 2, - "legendFormat": "{{nodename}} {{device}}", - "refId": "C" - } - ], - "title": "Disk I/O time", - "type": "timeseries" - }, - { - "aliasColors": {}, - "bars": false, - "dashLength": 10, - "dashes": false, - "datasource": { - "type": "prometheus", - "uid": "${datasource}" - }, - "description": "Aggregated disk space available / used", - "fill": 1, - "fillGradient": 0, - "gridPos": { - "h": 7, - "w": 12, - "x": 12, - "y": 24 - }, - "hiddenSeries": false, - "id": 7, - "legend": { - "alignAsTable": false, - "avg": false, - "current": false, - "max": false, - "min": false, - "rightSide": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 1, - "links": [], - "nullPointMode": "null", - "options": { - "alertThreshold": true - }, - "percentage": false, - "pluginVersion": "8.4.7", - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [ - { - "alias": "used", - "color": "#E0B400" - }, - { - "alias": "available", - "color": "#73BF69" - } - ], - "spaceLength": 10, - "stack": true, - "steppedLine": false, - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${datasource}" - }, - "exemplar": false, - "expr": "sum(\n max by (device) (\n node_filesystem_size_bytes{job=\"node-exporter\", nodename=~\"$instance\", fstype!=\"\"}\n -\n node_filesystem_avail_bytes{job=\"node-exporter\", nodename=~\"$instance\", fstype!=\"\"}\n )\n)\n", - "format": "time_series", - "interval": "", - "intervalFactor": 2, - "legendFormat": "used", - "refId": "A" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${datasource}" - }, - "exemplar": false, - "expr": "sum(\n max by (device) (\n node_filesystem_avail_bytes{job=\"node-exporter\", nodename=~\"$instance\", fstype!=\"\"}\n )\n)\n", - "format": "time_series", - "interval": "", - "intervalFactor": 2, - "legendFormat": "available", - "refId": "B" - } - ], - "thresholds": [], - "timeRegions": [], - "title": "Disk Space Usage", - "tooltip": { - "shared": true, - "sort": 0, - "value_type": "individual" - }, - "type": "graph", - "xaxis": { - "mode": "time", - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "bytes", - "logBase": 1, - "min": 0, - "show": true - }, - { - "format": "bytes", - "logBase": 1, - "min": 0, - "show": true - } - ], - "yaxis": { - "align": false - } - }, - { - "collapsed": false, - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "gridPos": { - "h": 1, - "w": 24, - "x": 0, - "y": 31 - }, - "id": 13, - "panels": [], - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "prometheus" - }, - "refId": "A" - } - ], - "title": "Network", - "type": "row" - }, - { - "datasource": { - "type": "prometheus", - "uid": "${datasource}" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 2, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "never", - "spanNulls": true, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "min": 0, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "bytes" - }, - "overrides": [] - }, - "gridPos": { - "h": 7, - "w": 12, - "x": 0, - "y": 32 - }, - "id": 8, - "links": [], - "options": { - "legend": { - "calcs": [], - "displayMode": "table", - "placement": "right" - }, - "tooltip": { - "mode": "multi", - "sort": "none" - } - }, - "pluginVersion": "8.4.7", - "targets": [ - { - "datasource": { - "type": "prometheus", - "uid": "${datasource}" - }, - "exemplar": true, - "expr": "sum(rate(node_network_receive_bytes_total{job=\"node-exporter\", device!=\"lo\", nodename=~\"$instance\"}[5m])) by (nodename)", - "format": "time_series", - "instant": false, - "interval": "", - "intervalFactor": 2, - "legendFormat": "{{nodename}}", - "refId": "A" - } - ], - "title": "Network Received", - "type": "timeseries" - }, - { - "datasource": { - "type": "prometheus", - "uid": "$datasource" - }, - "fieldConfig": { - "defaults": { - "color": { - "mode": "palette-classic" - }, - "custom": { - "axisLabel": "", - "axisPlacement": "auto", - "barAlignment": 0, - "drawStyle": "line", - "fillOpacity": 2, - "gradientMode": "none", - "hideFrom": { - "legend": false, - "tooltip": false, - "viz": false - }, - "lineInterpolation": "linear", - "lineWidth": 1, - "pointSize": 5, - "scaleDistribution": { - "type": "linear" - }, - "showPoints": "never", - "spanNulls": true, - "stacking": { - "group": "A", - "mode": "none" - }, - "thresholdsStyle": { - "mode": "off" - } - }, - "mappings": [], - "min": 0, - "thresholds": { - "mode": "absolute", - "steps": [ - { - "color": "green", - "value": null - }, - { - "color": "red", - "value": 80 - } - ] - }, - "unit": "bytes" - }, - "overrides": [] - }, - "gridPos": { - "h": 7, - "w": 12, - "x": 12, - "y": 32 - }, - "id": 9, - "links": [], - "options": { - "legend": { - "calcs": [], - "displayMode": "table", - "placement": "right" - }, - "tooltip": { - "mode": "multi", - "sort": "none" - } - }, - "pluginVersion": "8.4.7", - "targets": [ - { - "datasource": { - "uid": "$datasource" - }, - "exemplar": true, - "expr": "sum(rate(node_network_transmit_bytes_total{job=\"node-exporter\", device!=\"lo\", nodename=~\"$instance\"}[5m])) by (nodename)", - "format": "time_series", - "instant": false, - "interval": "", - "intervalFactor": 2, - "legendFormat": "{{nodename}}", - "refId": "A" - } - ], - "title": "Network Transmitted", - "type": "timeseries" - } - ], - "refresh": "", - "schemaVersion": 35, - "style": "dark", - "tags": [ - "infrastructure" - ], - "templating": { - "list": [ - { - "allValue": ".*", - "current": { - "selected": false, - "text": "All", - "value": "$__all" - }, - "datasource": { - "type": "prometheus", - "uid": "${datasource}" - }, - "definition": "label_values(node_uname_info{job=\"node-exporter\", sysname!=\"Darwin\"}, nodename)", - "hide": 0, - "includeAll": true, - "label": "Instance", - "multi": true, - "name": "instance", - "options": [], - "query": { - "query": "label_values(node_uname_info{job=\"node-exporter\", sysname!=\"Darwin\"}, nodename)", - "refId": "StandardVariableQuery" - }, - "refresh": 2, - "regex": "", - "skipUrlSync": false, - "sort": 0, - "tagValuesQuery": "", - "tagsQuery": "", - "type": "query", - "useTags": false - }, - { - "current": { - "selected": false, - "text": "default", - "value": "default" - }, - "hide": 0, - "includeAll": false, - "label": "datasource", - "multi": false, - "name": "datasource", - "options": [], - "query": "prometheus", - "queryValue": "", - "refresh": 1, - "regex": "", - "skipUrlSync": false, - "type": "datasource" - } - ] - }, - "time": { - "from": "now-3h", - "to": "now" - }, - "timepicker": { - "refresh_intervals": [ - "5s", - "10s", - "30s", - "1m", - "5m", - "15m", - "30m", - "1h", - "2h", - "1d" - ], - "time_options": [ - "5m", - "15m", - "1h", - "6h", - "12h", - "24h", - "2d", - "7d", - "30d" - ] - }, - "timezone": "utc", - "title": "Grafana Operator - Node Exporter / Nodes", - "uid": "v8yDYJqnz", - "version": 18, - "weekStart": "" -} diff --git a/pack.sh b/pack.sh deleted file mode 100755 index 0898706b..00000000 --- a/pack.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash - -set -ex - -DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" - -rm -rf ./assets -mkdir -p assets - -zip -r ./assets/SourceCode.zip . -x ".*" "*/.*" "*/obj/**" ".git/**" "*/node_modules/**" "*/cdk.out/*" "*/out/**" \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000..41cad415 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,6623 @@ +{ + "name": "one-observability-workshop", + "version": "2.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "one-observability-workshop", + "version": "2.0.0", + "dependencies": { + "eslint": "^9.33.0", + "npm-check-updates": "^18.0.2" + }, + "devDependencies": { + "@types/jest": "^30.0.0", + "@types/node": "24.2.1", + "@typescript-eslint/eslint-plugin": "^8.39.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-jest": "^29.0.1", + "eslint-plugin-prettier": "^5.5.4", + "eslint-plugin-unicorn": "^60.0.0", + "jest": "^30.0.5", + "jest-junit": "^16.0.0", + "ts-jest": "^29.4.1", + "ts-node": "^10.9.2", + "typedoc": "^0.28.9", + "typedoc-github-theme": "^0.3.0", + "typedoc-github-wiki-theme": "^2.1.0", + "typedoc-plugin-markdown": "^4.8.0", + "typescript": "~5.9.2" + } + }, + "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, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", + "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", + "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/core/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, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", + "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.0", + "@babel/types": "^7.28.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/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, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz", + "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.0" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", + "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@emnapi/core": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.4.5.tgz", + "integrity": "sha512-XsLw1dEOpkSX/WucdqUhPWP7hDxSvZiY+fsUC14h+FtQ2Ifni4znbBt8punRX+Uj2JG/uDb8nEHVKvrVlvdZ5Q==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.0.4", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.4.5", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.5.tgz", + "integrity": "sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.0.4.tgz", + "integrity": "sha512-PJR+bOmMOPH8AtcTGAyYNiuJ3/Fcoj2XN/gBEWzDIKh254XO+mM9XoXHk5GNEhodxeMznbg7BlRojVbKN+gC6g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-array/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/config-array/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz", + "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==", + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz", + "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==", + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@eslint/eslintrc/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/@eslint/js": { + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.33.0.tgz", + "integrity": "sha512-5K1/mKhWaMfreBGJTwval43JJmkip0RmM+3+IuqupeSKNC/Th2Kc7ucaq5ovTSra/OOKB9c58CGSz3QMVbWt0A==", + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz", + "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==", + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.15.2", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@gerrit0/mini-shiki": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.9.2.tgz", + "integrity": "sha512-Tvsj+AOO4Z8xLRJK900WkyfxHsZQu+Zm1//oT1w443PO6RiYMoq/4NGOhaNuZoUMYsjKIAPVQ6eOFMddj6yphQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/engine-oniguruma": "^3.9.2", + "@shikijs/langs": "^3.9.2", + "@shikijs/themes": "^3.9.2", + "@shikijs/types": "^3.9.2", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "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, + "license": "ISC", + "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/load-nyc-config/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, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/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, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/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, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/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, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/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, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.0.5.tgz", + "integrity": "sha512-xY6b0XiL0Nav3ReresUarwl2oIz1gTnxGbGpho9/rbUWsLH0f1OD/VT84xs8c7VmH7MChnLb0pag6PhZhAdDiA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.5", + "@types/node": "*", + "chalk": "^4.1.2", + "jest-message-util": "30.0.5", + "jest-util": "30.0.5", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.0.5.tgz", + "integrity": "sha512-fKD0OulvRsXF1hmaFgHhVJzczWzA1RXMMo9LTPuFXo9q/alDbME3JIyWYqovWsUBWSoBcsHaGPSLF9rz4l9Qeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.0.5", + "@jest/pattern": "30.0.1", + "@jest/reporters": "30.0.5", + "@jest/test-result": "30.0.5", + "@jest/transform": "30.0.5", + "@jest/types": "30.0.5", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-changed-files": "30.0.5", + "jest-config": "30.0.5", + "jest-haste-map": "30.0.5", + "jest-message-util": "30.0.5", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.0.5", + "jest-resolve-dependencies": "30.0.5", + "jest-runner": "30.0.5", + "jest-runtime": "30.0.5", + "jest-snapshot": "30.0.5", + "jest-util": "30.0.5", + "jest-validate": "30.0.5", + "jest-watcher": "30.0.5", + "micromatch": "^4.0.8", + "pretty-format": "30.0.5", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/diff-sequences": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", + "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.0.5.tgz", + "integrity": "sha512-aRX7WoaWx1oaOkDQvCWImVQ8XNtdv5sEWgk4gxR6NXb7WBUnL5sRak4WRzIQRZ1VTWPvV4VI4mgGjNL9TeKMYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.0.5", + "@jest/types": "30.0.5", + "@types/node": "*", + "jest-mock": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.0.5.tgz", + "integrity": "sha512-6udac8KKrtTtC+AXZ2iUN/R7dp7Ydry+Fo6FPFnDG54wjVMnb6vW/XNlf7Xj8UDjAE3aAVAsR4KFyKk3TCXmTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "30.0.5", + "jest-snapshot": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.0.5.tgz", + "integrity": "sha512-F3lmTT7CXWYywoVUGTCmom0vXq3HTTkaZyTAzIy+bXSBizB7o5qzlC9VCtq0arOa8GqmNsbg/cE9C6HLn7Szew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.0.5.tgz", + "integrity": "sha512-ZO5DHfNV+kgEAeP3gK3XlpJLL4U3Sz6ebl/n68Uwt64qFFs5bv4bfEEjyRGK5uM0C90ewooNgFuKMdkbEoMEXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.5", + "@sinonjs/fake-timers": "^13.0.0", + "@types/node": "*", + "jest-message-util": "30.0.5", + "jest-mock": "30.0.5", + "jest-util": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.0.1.tgz", + "integrity": "sha512-AyYdemXCptSRFirI5EPazNxyPwAL0jXt3zceFjaj8NFiKP9pOi0bfXonf6qkf82z2t3QWPeLCWWw4stPBzctLw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.0.5.tgz", + "integrity": "sha512-7oEJT19WW4oe6HR7oLRvHxwlJk2gev0U9px3ufs8sX9PoD1Eza68KF0/tlN7X0dq/WVsBScXQGgCldA1V9Y/jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.0.5", + "@jest/expect": "30.0.5", + "@jest/types": "30.0.5", + "jest-mock": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.0.5.tgz", + "integrity": "sha512-mafft7VBX4jzED1FwGC1o/9QUM2xebzavImZMeqnsklgcyxBto8mV4HzNSzUrryJ+8R9MFOM3HgYuDradWR+4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "30.0.5", + "@jest/test-result": "30.0.5", + "@jest/transform": "30.0.5", + "@jest/types": "30.0.5", + "@jridgewell/trace-mapping": "^0.3.25", + "@types/node": "*", + "chalk": "^4.1.2", + "collect-v8-coverage": "^1.0.2", + "exit-x": "^0.2.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^5.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "30.0.5", + "jest-util": "30.0.5", + "jest-worker": "30.0.5", + "slash": "^3.0.0", + "string-length": "^4.0.2", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/snapshot-utils": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.0.5.tgz", + "integrity": "sha512-XcCQ5qWHLvi29UUrowgDFvV4t7ETxX91CbDczMnoqXPOIcZOxyNdSjm6kV5XMc8+HkxfRegU/MUmnTbJRzGrUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.5", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", + "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "callsites": "^3.1.0", + "graceful-fs": "^4.2.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.0.5.tgz", + "integrity": "sha512-wPyztnK0gbDMQAJZ43tdMro+qblDHH1Ru/ylzUo21TBKqt88ZqnKKK2m30LKmLLoKtR2lxdpCC/P3g1vfKcawQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.0.5", + "@jest/types": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.0.5.tgz", + "integrity": "sha512-Aea/G1egWoIIozmDD7PBXUOxkekXl7ueGzrsGGi1SbeKgQqCYCIf+wfbflEbf2LiPxL8j2JZGLyrzZagjvW4YQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.0.5", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.0.5", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.0.5.tgz", + "integrity": "sha512-Vk8amLQCmuZyy6GbBht1Jfo9RSdBtg7Lks+B0PecnjI8J+PCLQPGh7uI8Q/2wwpW2gLdiAfiHNsmekKlywULqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/types": "30.0.5", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.0", + "chalk": "^4.1.2", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.0.5", + "jest-regex-util": "30.0.1", + "jest-util": "30.0.5", + "micromatch": "^4.0.8", + "pirates": "^4.0.7", + "slash": "^3.0.0", + "write-file-atomic": "^5.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.9.2.tgz", + "integrity": "sha512-Vn/w5oyQ6TUgTVDIC/BrpXwIlfK6V6kGWDVVz2eRkF2v13YoENUvaNwxMsQU/t6oCuZKzqp9vqtEtEzKl9VegA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.9.2", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.9.2.tgz", + "integrity": "sha512-X1Q6wRRQXY7HqAuX3I8WjMscjeGjqXCg/Sve7J2GWFORXkSrXud23UECqTBIdCSNKJioFtmUGJQNKtlMMZMn0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.9.2" + } + }, + "node_modules/@shikijs/themes": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.9.2.tgz", + "integrity": "sha512-6z5lBPBMRfLyyEsgf6uJDHPa6NAGVzFJqH4EAZ+03+7sedYir2yJBRu2uPZOKmj43GyhVHWHvyduLDAwJQfDjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.9.2" + } + }, + "node_modules/@shikijs/types": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.9.2.tgz", + "integrity": "sha512-/M5L0Uc2ljyn2jKvj4Yiah7ow/W+DJSglVafvWAJ/b8AZDeeRAdMu3c2riDzB7N42VD+jSnWxeP9AKtd4TfYVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.38", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.38.tgz", + "integrity": "sha512-HpkxMmc2XmZKhvaKIZZThlHmx1L0I/V1hWK1NubtlFnr6ZqdiOpV72TKudZUNQjZNsyDBay72qFEhEvb+bcwcA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz", + "integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "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, + "license": "MIT", + "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.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "license": "MIT" + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^30.0.0", + "pretty-format": "^30.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.2.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz", + "integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.10.0" + } + }, + "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, + "license": "MIT" + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.39.0.tgz", + "integrity": "sha512-bhEz6OZeUR+O/6yx9Jk6ohX6H9JSFTaiY0v9/PuKT3oGK0rn0jNplLmyFUGV+a9gfYnVNwGDwS/UkLIuXNb2Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.39.0", + "@typescript-eslint/type-utils": "8.39.0", + "@typescript-eslint/utils": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.39.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.39.0.tgz", + "integrity": "sha512-g3WpVQHngx0aLXn6kfIYCZxM6rRJlWzEkVpqEFLT3SgEDsp9cpCbxxgwnE504q4H+ruSDh/VGS6nqZIDynP+vg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.39.0", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/typescript-estree": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.39.0.tgz", + "integrity": "sha512-CTzJqaSq30V/Z2Og9jogzZt8lJRR5TKlAdXmWgdu4hgcC9Kww5flQ+xFvMxIBWVNdxJO7OifgdOK4PokMIWPew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.39.0", + "@typescript-eslint/types": "^8.39.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.39.0.tgz", + "integrity": "sha512-8QOzff9UKxOh6npZQ/4FQu4mjdOCGSdO3p44ww0hk8Vu+IGbg0tB/H1LcTARRDzGCC8pDGbh2rissBuuoPgH8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.39.0.tgz", + "integrity": "sha512-Fd3/QjmFV2sKmvv3Mrj8r6N8CryYiCS8Wdb/6/rgOXAWGcFuc+VkQuG28uk/4kVNVZBQuuDHEDUpo/pQ32zsIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.39.0.tgz", + "integrity": "sha512-6B3z0c1DXVT2vYA9+z9axjtc09rqKUPRmijD5m9iv8iQpHBRYRMBcgxSiKTZKm6FwWw1/cI4v6em35OsKCiN5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/typescript-estree": "8.39.0", + "@typescript-eslint/utils": "8.39.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.39.0.tgz", + "integrity": "sha512-ArDdaOllnCj3yn/lzKn9s0pBQYmmyme/v1HbGIGB0GB/knFI3fWMHloC+oYTJW46tVbYnGKTMDK4ah1sC2v0Kg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.39.0.tgz", + "integrity": "sha512-ndWdiflRMvfIgQRpckQQLiB5qAKQ7w++V4LlCHwp62eym1HLB/kw7D9f2e8ytONls/jt89TEasgvb+VwnRprsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.39.0", + "@typescript-eslint/tsconfig-utils": "8.39.0", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/visitor-keys": "8.39.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.39.0.tgz", + "integrity": "sha512-4GVSvNA0Vx1Ktwvf4sFE+exxJ3QGUorQG1/A5mRfRNZtkBT2xrA/BCO2H0eALx/PnvCS6/vmYwRdDA41EoffkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.39.0", + "@typescript-eslint/types": "8.39.0", + "@typescript-eslint/typescript-estree": "8.39.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.39.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.39.0.tgz", + "integrity": "sha512-ldgiJ+VAhQCfIjeOgu8Kj5nSxds0ktPOSO9p4+0VDH2R2pLvQraaM5Oen2d7NxzMCm+Sn/vJT+mv2H5u6b/3fA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.39.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.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, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "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, + "license": "MIT", + "dependencies": { + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", + "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "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, + "license": "ISC", + "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, + "license": "MIT" + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/babel-jest": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.0.5.tgz", + "integrity": "sha512-mRijnKimhGDMsizTvBTWotwNpzrkHr+VvZUQBof2AufXKB8NXrL1W69TG20EvOz7aevx6FTJIaBuBkYxS8zolg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "30.0.5", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.0", + "babel-preset-jest": "30.0.1", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.0.tgz", + "integrity": "sha512-C5OzENSx/A+gt7t4VH1I2XsflxyPUmXRFPKBxt33xncdOmq7oROVM3bZv9Ysjjkv8OJYDMa+tKuKMvqU/H3xdw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.0.1.tgz", + "integrity": "sha512-zTPME3pI50NsFW8ZBaVIOeAxzEY7XHlmWeXXu9srI+9kNfzCUTy8MFan46xOGZY8NZThMqq+e3qZUKsvXbasnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.3", + "@types/babel__core": "^7.20.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.1.tgz", + "integrity": "sha512-23fWKohMTvS5s0wwJKycOe0dBdCwQ6+iiLaNR9zy8P13mtFRFM9qLLX6HJX5DL2pi/FNDf3fCQHM4FIMoHH/7w==", + "dev": true, + "license": "MIT", + "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 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.0.1.tgz", + "integrity": "sha512-+YHejD5iTWI46cZmcc/YtX4gaKBtdqCHCVfuVinizVpbmyjO3zYmeuyFdfA8duRqQZfgCAMlsfmkVbJ+e2MAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "30.0.1", + "babel-preset-current-node-syntax": "^1.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.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==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", + "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "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" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001726", + "electron-to-chromium": "^1.5.173", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "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, + "license": "MIT", + "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, + "license": "Apache-2.0", + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/builtin-modules": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-5.0.0.tgz", + "integrity": "sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001731", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz", + "integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==", + "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" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "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/change-case": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", + "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz", + "integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.1.0.tgz", + "integrity": "sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==", + "dev": true, + "license": "MIT" + }, + "node_modules/clean-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz", + "integrity": "sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/clean-regexp/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, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "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, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/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, + "license": "MIT" + }, + "node_modules/cliui/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, + "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/cliui/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, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/cliui/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, + "license": "MIT", + "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/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT" + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/core-js-compat": { + "version": "3.44.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.44.0.tgz", + "integrity": "sha512-JepmAj2zfl6ogy34qfWtcE7nHKAJnKsQFRn++scjVS2bZFllwptzw61BZcZFYBPpUznLfAvh0LGhxKppk04ClA==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.25.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "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, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", + "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.192", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.192.tgz", + "integrity": "sha512-rP8Ez0w7UNw/9j5eSXCe10o1g/8B1P5SM90PCCMVkIRQn2R0LEHWz4Eh9RnxkniuDe1W0cTSOB3MLlkTGDcuCg==", + "dev": true, + "license": "ISC" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.33.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.33.0.tgz", + "integrity": "sha512-TS9bTNIryDzStCpJN93aC5VRSW3uTx9sClUn4B87pwiCaJh220otoI0X8mJKr+VcPtniMdN8GKjlwgWGUv5ZKA==", + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.1", + "@eslint/core": "^0.15.2", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.33.0", + "@eslint/plugin-kit": "^0.3.5", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-jest": { + "version": "29.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-29.0.1.tgz", + "integrity": "sha512-EE44T0OSMCeXhDrrdsbKAhprobKkPtJTbQz5yEktysNpHeDZTAL1SfDTNKmcFfJkY6yrQLtTKZALrD3j/Gpmiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^8.0.0" + }, + "engines": { + "node": "^20.12.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^8.0.0", + "eslint": "^8.57.0 || ^9.0.0", + "jest": "*" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + }, + "jest": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz", + "integrity": "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.11.7" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-unicorn": { + "version": "60.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-60.0.0.tgz", + "integrity": "sha512-QUzTefvP8stfSXsqKQ+vBQSEsXIlAiCduS/V1Em+FKgL9c21U/IIm20/e3MFy1jyCf14tHAhqC1sX8OTy6VUCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "@eslint-community/eslint-utils": "^4.7.0", + "@eslint/plugin-kit": "^0.3.3", + "change-case": "^5.4.4", + "ci-info": "^4.3.0", + "clean-regexp": "^1.0.0", + "core-js-compat": "^3.44.0", + "esquery": "^1.6.0", + "find-up-simple": "^1.0.1", + "globals": "^16.3.0", + "indent-string": "^5.0.0", + "is-builtin-module": "^5.0.0", + "jsesc": "^3.1.0", + "pluralize": "^8.0.0", + "regexp-tree": "^0.1.27", + "regjsparser": "^0.12.0", + "semver": "^7.7.2", + "strip-indent": "^4.0.0" + }, + "engines": { + "node": "^20.10.0 || >=21.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1" + }, + "peerDependencies": { + "eslint": ">=9.29.0" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/globals": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", + "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "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, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/exit-x": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", + "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.0.5.tgz", + "integrity": "sha512-P0te2pt+hHI5qLJkIR+iMvS+lYUZml8rKKsohVHAGY+uClp9XVbdyYNJOIjSRpHVp8s8YqxJCiHUkSYZGr8rtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.0.5", + "@jest/get-type": "30.0.1", + "jest-matcher-utils": "30.0.5", + "jest-message-util": "30.0.5", + "jest-mock": "30.0.5", + "jest-util": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "license": "MIT" + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "license": "MIT" + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "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, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-up-simple": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz", + "integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "license": "ISC" + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "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, + "license": "MIT", + "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, + "license": "ISC", + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "ISC", + "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, + "license": "ISC" + }, + "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, + "license": "MIT" + }, + "node_modules/is-builtin-module": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-5.0.0.tgz", + "integrity": "sha512-f4RqJKBUe5rQkJ2eJEJBXSticB3hGbN9j0yxxMQFqIW89Jp9WYFtzfTcRlstDKVUTRzSOTLKRfO9vIztenwtxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "builtin-modules": "^5.0.0" + }, + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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==", + "license": "ISC" + }, + "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, + "license": "BSD-3-Clause", + "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, + "license": "BSD-3-Clause", + "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-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, + "license": "BSD-3-Clause", + "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": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "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, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.0.5.tgz", + "integrity": "sha512-y2mfcJywuTUkvLm2Lp1/pFX8kTgMO5yyQGq/Sk/n2mN7XWYp4JsCZ/QXW34M8YScgk8bPZlREH04f6blPnoHnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.0.5", + "@jest/types": "30.0.5", + "import-local": "^3.2.0", + "jest-cli": "30.0.5" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.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": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.0.5.tgz", + "integrity": "sha512-bGl2Ntdx0eAwXuGpdLdVYVr5YQHnSZlQ0y9HVDu565lCUAe9sj6JOtBbMmBBikGIegne9piDDIOeiLVoqTkz4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.1.1", + "jest-util": "30.0.5", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-circus": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.0.5.tgz", + "integrity": "sha512-h/sjXEs4GS+NFFfqBDYT7y5Msfxh04EwWLhQi0F8kuWpe+J/7tICSlswU8qvBqumR3kFgHbfu7vU6qruWWBPug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.0.5", + "@jest/expect": "30.0.5", + "@jest/test-result": "30.0.5", + "@jest/types": "30.0.5", + "@types/node": "*", + "chalk": "^4.1.2", + "co": "^4.6.0", + "dedent": "^1.6.0", + "is-generator-fn": "^2.1.0", + "jest-each": "30.0.5", + "jest-matcher-utils": "30.0.5", + "jest-message-util": "30.0.5", + "jest-runtime": "30.0.5", + "jest-snapshot": "30.0.5", + "jest-util": "30.0.5", + "p-limit": "^3.1.0", + "pretty-format": "30.0.5", + "pure-rand": "^7.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.0.5.tgz", + "integrity": "sha512-Sa45PGMkBZzF94HMrlX4kUyPOwUpdZasaliKN3mifvDmkhLYqLLg8HQTzn6gq7vJGahFYMQjXgyJWfYImKZzOw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.0.5", + "@jest/test-result": "30.0.5", + "@jest/types": "30.0.5", + "chalk": "^4.1.2", + "exit-x": "^0.2.2", + "import-local": "^3.2.0", + "jest-config": "30.0.5", + "jest-util": "30.0.5", + "jest-validate": "30.0.5", + "yargs": "^17.7.2" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.0.5.tgz", + "integrity": "sha512-aIVh+JNOOpzUgzUnPn5FLtyVnqc3TQHVMupYtyeURSb//iLColiMIR8TxCIDKyx9ZgjKnXGucuW68hCxgbrwmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/get-type": "30.0.1", + "@jest/pattern": "30.0.1", + "@jest/test-sequencer": "30.0.5", + "@jest/types": "30.0.5", + "babel-jest": "30.0.5", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-circus": "30.0.5", + "jest-docblock": "30.0.1", + "jest-environment-node": "30.0.5", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.0.5", + "jest-runner": "30.0.5", + "jest-util": "30.0.5", + "jest-validate": "30.0.5", + "micromatch": "^4.0.8", + "parse-json": "^5.2.0", + "pretty-format": "30.0.5", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "esbuild-register": ">=3.4.0", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild-register": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.0.5.tgz", + "integrity": "sha512-1UIqE9PoEKaHcIKvq2vbibrCog4Y8G0zmOxgQUVEiTqwR5hJVMCoDsN1vFvI5JvwD37hjueZ1C4l2FyGnfpE0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.0.1", + "@jest/get-type": "30.0.1", + "chalk": "^4.1.2", + "pretty-format": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.0.1.tgz", + "integrity": "sha512-/vF78qn3DYphAaIc3jy4gA7XSAz167n9Bm/wn/1XhTLW7tTBIzXtCJpb/vcmc73NIIeeohCbdL94JasyXUZsGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-each": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.0.5.tgz", + "integrity": "sha512-dKjRsx1uZ96TVyejD3/aAWcNKy6ajMaN531CwWIsrazIqIoXI9TnnpPlkrEYku/8rkS3dh2rbH+kMOyiEIv0xQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.0.1", + "@jest/types": "30.0.5", + "chalk": "^4.1.2", + "jest-util": "30.0.5", + "pretty-format": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.0.5.tgz", + "integrity": "sha512-ppYizXdLMSvciGsRsMEnv/5EFpvOdXBaXRBzFUDPWrsfmog4kYrOGWXarLllz6AXan6ZAA/kYokgDWuos1IKDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.0.5", + "@jest/fake-timers": "30.0.5", + "@jest/types": "30.0.5", + "@types/node": "*", + "jest-mock": "30.0.5", + "jest-util": "30.0.5", + "jest-validate": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.0.5.tgz", + "integrity": "sha512-dkmlWNlsTSR0nH3nRfW5BKbqHefLZv0/6LCccG0xFCTWcJu8TuEwG+5Cm75iBfjVoockmO6J35o5gxtFSn5xeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.5", + "@types/node": "*", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.0.1", + "jest-util": "30.0.5", + "jest-worker": "30.0.5", + "micromatch": "^4.0.8", + "walker": "^1.0.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.3" + } + }, + "node_modules/jest-junit": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/jest-junit/-/jest-junit-16.0.0.tgz", + "integrity": "sha512-A94mmw6NfJab4Fg/BlvVOUXzXgF0XIH6EmTgJ5NDPp4xoKq0Kr7sErb+4Xs9nZvu58pJojz5RFGpqnZYJTrRfQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mkdirp": "^1.0.4", + "strip-ansi": "^6.0.1", + "uuid": "^8.3.2", + "xml": "^1.0.1" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/jest-junit/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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-junit/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, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-leak-detector": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.0.5.tgz", + "integrity": "sha512-3Uxr5uP8jmHMcsOtYMRB/zf1gXN3yUIc+iPorhNETG54gErFIiUhLvyY/OggYpSMOEYqsmRxmuU4ZOoX5jpRFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.0.1", + "pretty-format": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.0.5.tgz", + "integrity": "sha512-uQgGWt7GOrRLP1P7IwNWwK1WAQbq+m//ZY0yXygyfWp0rJlksMSLQAA4wYQC3b6wl3zfnchyTx+k3HZ5aPtCbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.0.1", + "chalk": "^4.1.2", + "jest-diff": "30.0.5", + "pretty-format": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.0.5.tgz", + "integrity": "sha512-NAiDOhsK3V7RU0Aa/HnrQo+E4JlbarbmI3q6Pi4KcxicdtjV82gcIUrejOtczChtVQR4kddu1E1EJlW6EN9IyA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.0.5", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "pretty-format": "30.0.5", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-mock": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.0.5.tgz", + "integrity": "sha512-Od7TyasAAQX/6S+QCbN6vZoWOMwlTtzzGuxJku1GhGanAjz9y+QsQkpScDmETvdc9aSXyJ/Op4rhpMYBWW91wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.5", + "@types/node": "*", + "jest-util": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.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, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.0.5.tgz", + "integrity": "sha512-d+DjBQ1tIhdz91B79mywH5yYu76bZuE96sSbxj8MkjWVx5WNdt1deEFRONVL4UkKLSrAbMkdhb24XN691yDRHg==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.0.5", + "jest-pnp-resolver": "^1.2.3", + "jest-util": "30.0.5", + "jest-validate": "30.0.5", + "slash": "^3.0.0", + "unrs-resolver": "^1.7.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.0.5.tgz", + "integrity": "sha512-/xMvBR4MpwkrHW4ikZIWRttBBRZgWK4d6xt3xW1iRDSKt4tXzYkMkyPfBnSCgv96cpkrctfXs6gexeqMYqdEpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "30.0.1", + "jest-snapshot": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runner": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.0.5.tgz", + "integrity": "sha512-JcCOucZmgp+YuGgLAXHNy7ualBx4wYSgJVWrYMRBnb79j9PD0Jxh0EHvR5Cx/r0Ce+ZBC4hCdz2AzFFLl9hCiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.0.5", + "@jest/environment": "30.0.5", + "@jest/test-result": "30.0.5", + "@jest/transform": "30.0.5", + "@jest/types": "30.0.5", + "@types/node": "*", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-docblock": "30.0.1", + "jest-environment-node": "30.0.5", + "jest-haste-map": "30.0.5", + "jest-leak-detector": "30.0.5", + "jest-message-util": "30.0.5", + "jest-resolve": "30.0.5", + "jest-runtime": "30.0.5", + "jest-util": "30.0.5", + "jest-watcher": "30.0.5", + "jest-worker": "30.0.5", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.0.5.tgz", + "integrity": "sha512-7oySNDkqpe4xpX5PPiJTe5vEa+Ak/NnNz2bGYZrA1ftG3RL3EFlHaUkA1Cjx+R8IhK0Vg43RML5mJedGTPNz3A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.0.5", + "@jest/fake-timers": "30.0.5", + "@jest/globals": "30.0.5", + "@jest/source-map": "30.0.1", + "@jest/test-result": "30.0.5", + "@jest/transform": "30.0.5", + "@jest/types": "30.0.5", + "@types/node": "*", + "chalk": "^4.1.2", + "cjs-module-lexer": "^2.1.0", + "collect-v8-coverage": "^1.0.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.0.5", + "jest-message-util": "30.0.5", + "jest-mock": "30.0.5", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.0.5", + "jest-snapshot": "30.0.5", + "jest-util": "30.0.5", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.0.5.tgz", + "integrity": "sha512-T00dWU/Ek3LqTp4+DcW6PraVxjk28WY5Ua/s+3zUKSERZSNyxTqhDXCWKG5p2HAJ+crVQ3WJ2P9YVHpj1tkW+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.0.5", + "@jest/get-type": "30.0.1", + "@jest/snapshot-utils": "30.0.5", + "@jest/transform": "30.0.5", + "@jest/types": "30.0.5", + "babel-preset-current-node-syntax": "^1.1.0", + "chalk": "^4.1.2", + "expect": "30.0.5", + "graceful-fs": "^4.2.11", + "jest-diff": "30.0.5", + "jest-matcher-utils": "30.0.5", + "jest-message-util": "30.0.5", + "jest-util": "30.0.5", + "pretty-format": "30.0.5", + "semver": "^7.7.2", + "synckit": "^0.11.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.5.tgz", + "integrity": "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.5", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-validate": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.0.5.tgz", + "integrity": "sha512-ouTm6VFHaS2boyl+k4u+Qip4TSH7Uld5tyD8psQ8abGgt2uYYB8VwVfAHWHjHc0NWmGGbwO5h0sCPOGHHevefw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.0.1", + "@jest/types": "30.0.5", + "camelcase": "^6.3.0", + "chalk": "^4.1.2", + "leven": "^3.1.0", + "pretty-format": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.0.5.tgz", + "integrity": "sha512-z9slj/0vOwBDBjN3L4z4ZYaA+pG56d6p3kTUhFRYGvXbXMWhXmb/FIxREZCD06DYUwDKKnj2T80+Pb71CQ0KEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.0.5", + "@jest/types": "30.0.5", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "jest-util": "30.0.5", + "string-length": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.0.5.tgz", + "integrity": "sha512-ojRXsWzEP16NdUuBw/4H/zkZdHOa7MMYCk4E430l+8fELeLg/mqmMlRhjL7UNZvQrDmnovWZV4DxX03fZF48fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.0.5", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.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, + "license": "MIT", + "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, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "license": "MIT" + }, + "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, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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, + "license": "MIT" + }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "license": "MIT" + }, + "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, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "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==", + "license": "MIT" + }, + "node_modules/napi-postinstall": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.2.tgz", + "integrity": "sha512-tWVJxJHmBWLy69PvO96TZMZDrzmw5KeiZBz3RHmiM2XZ9grBJ2WgMAFVVg25nqp3ZjTFUs2Ftw1JhscL3Teliw==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "license": "MIT" + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-check-updates": { + "version": "18.0.2", + "resolved": "https://registry.npmjs.org/npm-check-updates/-/npm-check-updates-18.0.2.tgz", + "integrity": "sha512-9uVFZUCg5oDOcbzdsrJ4BEvq2gikd23tXuF5mqpl4mxVl051lzB00Xmd7ZVjVWY3XNUF3BQKWlN/qmyD8/bwrA==", + "license": "Apache-2.0", + "bin": { + "ncu": "build/cli.js", + "npm-check-updates": "build/cli.js" + }, + "engines": { + "node": "^18.18.0 || >=20.0.0", + "npm": ">=8.12.1" + } + }, + "node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "ISC", + "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, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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==", + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "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, + "license": "MIT", + "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==", + "license": "MIT", + "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, + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/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, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/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, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pkg-dir/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, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/pkg-dir/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, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pretty-format": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", + "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/regexp-tree": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", + "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", + "dev": true, + "license": "MIT", + "bin": { + "regexp-tree": "bin/regexp-tree" + } + }, + "node_modules/regjsparser": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", + "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.0.2" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-cwd/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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "license": "MIT", + "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, + "license": "BSD-3-Clause" + }, + "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, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-length/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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-length/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, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "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, + "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/string-width-cjs/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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/string-width-cjs/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, + "license": "MIT" + }, + "node_modules/string-width-cjs/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, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "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, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs/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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-indent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.0.0.tgz", + "integrity": "sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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==", + "license": "MIT", + "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==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/synckit": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", + "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "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, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/test-exclude/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, + "license": "ISC", + "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/test-exclude/node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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, + "license": "BSD-3-Clause" + }, + "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, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-jest": { + "version": "29.4.1", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.1.tgz", + "integrity": "sha512-SaeUtjfpg9Uqu8IbeDKtdaS0g8lS6FT6OzM3ezrDfErPJPHNDo/Ey+VFGP1bQIDfagYDLyRpd7O15XpG1Es2Uw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.8", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.2", + "type-fest": "^4.41.0", + "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 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.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 + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "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/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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, + "license": "MIT", + "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, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typedoc": { + "version": "0.28.9", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.9.tgz", + "integrity": "sha512-aw45vwtwOl3QkUAmWCnLV9QW1xY+FSX2zzlit4MAfE99wX+Jij4ycnpbAWgBXsRrxmfs9LaYktg/eX5Bpthd3g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@gerrit0/mini-shiki": "^3.9.0", + "lunr": "^2.3.9", + "markdown-it": "^14.1.0", + "minimatch": "^9.0.5", + "yaml": "^2.8.0" + }, + "bin": { + "typedoc": "bin/typedoc" + }, + "engines": { + "node": ">= 18", + "pnpm": ">= 10" + }, + "peerDependencies": { + "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x" + } + }, + "node_modules/typedoc-github-theme": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/typedoc-github-theme/-/typedoc-github-theme-0.3.0.tgz", + "integrity": "sha512-7HzKJt8ZZSTNYczK8BcZZbpr7GsyTZgsoLFKtO4NYxXOb7mSr92yTj7hl16ind0WsOzxPo2DZGjkUH2VoPrdxg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "typedoc": "~0.28.0" + } + }, + "node_modules/typedoc-github-wiki-theme": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/typedoc-github-wiki-theme/-/typedoc-github-wiki-theme-2.1.0.tgz", + "integrity": "sha512-5j4vuoGwLn8PM1HeXocbwUF0Ra2qTFLeqsQwBQsLBPIx7Tl/lxkas1qIPFg/InOYwy9Y3Pn4xSjh+c/KM+jh6Q==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typedoc-plugin-markdown": ">=4.3.0" + } + }, + "node_modules/typedoc-plugin-markdown": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-4.8.0.tgz", + "integrity": "sha512-BQqXnT9PETe6WEFf8bcsvvGEGQHbwTo/BFyY+RUIsSB05Y0Wn56iF+fK1PY2OKJJIhV4kp4dp7osaP9Bm5a0Zw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "typedoc": "0.28.x" + } + }, + "node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/undici-types": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "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" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "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, + "license": "MIT" + }, + "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, + "license": "ISC", + "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, + "license": "Apache-2.0", + "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==", + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "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, + "license": "MIT", + "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/wrap-ansi-cjs/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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi-cjs/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, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/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, + "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/wrap-ansi-cjs/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, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/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, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "ISC", + "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, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, + "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, + "license": "MIT", + "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, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/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, + "license": "MIT" + }, + "node_modules/yargs/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, + "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/yargs/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, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "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==", + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..1e6e8d41 --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "one-observability-workshop", + "version": "2.0.0", + "devDependencies": { + "@types/jest": "^30.0.0", + "@types/node": "24.2.1", + "@typescript-eslint/eslint-plugin": "^8.39.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-jest": "^29.0.1", + "eslint-plugin-prettier": "^5.5.4", + "eslint-plugin-unicorn": "^60.0.0", + "jest": "^30.0.5", + "jest-junit": "^16.0.0", + "ts-jest": "^29.4.1", + "ts-node": "^10.9.2", + "typedoc": "^0.28.9", + "typedoc-github-theme": "^0.3.0", + "typedoc-github-wiki-theme": "^2.1.0", + "typedoc-plugin-markdown": "^4.8.0", + "typescript": "~5.9.2" + }, + "dependencies": { + "eslint": "^9.33.0", + "npm-check-updates": "^18.0.2" + } +} diff --git a/setup-cloudshell.sh b/setup-cloudshell.sh index 8159cfd1..0669c340 100755 --- a/setup-cloudshell.sh +++ b/setup-cloudshell.sh @@ -1,5 +1,8 @@ #!/usr/bin/env bash +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + if [ -z "$AWS_REGION" ]; then echo "error: environment variable AWS_REGION not set. Aborting." exit 1 diff --git a/src/applications/canaries/housekeeping/nodejs/node_modules/index.js b/src/applications/canaries/housekeeping/nodejs/node_modules/index.js new file mode 100644 index 00000000..886333b4 --- /dev/null +++ b/src/applications/canaries/housekeeping/nodejs/node_modules/index.js @@ -0,0 +1,65 @@ +var synthetics = require('Synthetics'); +const log = require('SyntheticsLogger'); + +const recordedScript = async function () { + let page = await synthetics.getPage(); + + const navigationPromise = page.waitForNavigation(); + + // Try to read from SSM, fallback to environment variable + let petsiteUrl = process.env.PETSITE_URL; + const ssmParameterName = process.env.PETSITE_URL_PARAMETER_NAME || '/petstore/petsiteurl'; + + // Attempt to read from SSM + try { + const { SSMClient, GetParameterCommand } = require('@aws-sdk/client-ssm'); + const ssm = new SSMClient({}); + const command = new GetParameterCommand({ + Name: ssmParameterName, + WithDecryption: false, + }); + const parameter = await ssm.send(command); + + if (parameter.Parameter && parameter.Parameter.Value) { + petsiteUrl = parameter.Parameter.Value; + log.info('Successfully retrieved petsite URL from SSM: ' + petsiteUrl); + } + } catch (error) { + log.info('SSM access failed, using environment variable URL: ' + petsiteUrl); + } + + log.info('Starting housekeeping canary execution with URL: ' + petsiteUrl); + log.info('SSM Parameter to monitor: ' + ssmParameterName); + + try { + await synthetics.executeStep('Goto_0', async function () { + await page.goto(petsiteUrl + '/?userId=housekeeping1930', { + waitUntil: 'domcontentloaded', + timeout: 60000, + }); + }); + + await page.setViewport({ width: 1463, height: 863 }); + + // Wait for page to load completely + await page.waitForSelector('.pet-header', { timeout: 10000 }); + + await synthetics.executeStep('Click_Housekeeping', async function () { + await page.waitForSelector('.pet-header #performhousekeeping'); + await page.click('.pet-header #performhousekeeping'); + }); + + await navigationPromise; + + // Wait for housekeeping to complete + await page.waitForTimeout(2000); + + log.info('Housekeeping canary execution completed successfully'); + } catch (error) { + log.error('Housekeeping canary execution failed: ' + error.message); + throw error; + } +}; +exports.handler = async () => { + return await recordedScript(); +}; diff --git a/src/applications/canaries/housekeeping/nodejs/package.json b/src/applications/canaries/housekeeping/nodejs/package.json new file mode 100644 index 00000000..bc2d185a --- /dev/null +++ b/src/applications/canaries/housekeeping/nodejs/package.json @@ -0,0 +1,9 @@ +{ + "name": "housekeeping-canary", + "version": "1.0.0", + "description": "CloudWatch Synthetics canary for housekeeping operations", + "main": "node_modules/index.js", + "dependencies": { + "@aws-sdk/client-ssm": "^3.0.0" + } +} \ No newline at end of file diff --git a/src/applications/canaries/petsite-canary/nodejs/node_modules/index.js b/src/applications/canaries/petsite-canary/nodejs/node_modules/index.js new file mode 100644 index 00000000..d6f0fe7d --- /dev/null +++ b/src/applications/canaries/petsite-canary/nodejs/node_modules/index.js @@ -0,0 +1,56 @@ +var synthetics = require('Synthetics'); +const log = require('SyntheticsLogger'); + +const recordedScript = async function () { + let page = await synthetics.getPage(); + + const navigationPromise = page.waitForNavigation(); + + // Try to read from SSM, fallback to environment variable + let petsiteUrl = process.env.PETSITE_URL; + const ssmParameterName = process.env.PETSITE_URL_PARAMETER_NAME || '/petstore/petsiteurl'; + + // Attempt to read from SSM + try { + const { SSMClient, GetParameterCommand } = require('@aws-sdk/client-ssm'); + const ssm = new SSMClient({}); + const command = new GetParameterCommand({ + Name: ssmParameterName, + WithDecryption: false, + }); + const parameter = await ssm.send(command); + + if (parameter.Parameter && parameter.Parameter.Value) { + petsiteUrl = parameter.Parameter.Value; + log.info('Successfully retrieved petsite URL from SSM: ' + petsiteUrl); + } + } catch (error) { + log.info('SSM access failed, using environment variable URL: ' + petsiteUrl); + } + + log.info('Starting canary execution with URL: ' + petsiteUrl); + log.info('SSM Parameter to monitor: ' + ssmParameterName); + + try { + await synthetics.executeStep('Goto_0', async function () { + await page.goto(petsiteUrl + '/?userId=user1930', { waitUntil: 'domcontentloaded', timeout: 60000 }); + }); + + await page.setViewport({ width: 1463, height: 863 }); + + await synthetics.executeStep('Click_1', async function () { + await page.waitForSelector('.pet-header #performhousekeeping'); + await page.click('.pet-header #performhousekeeping'); + }); + + await navigationPromise; + + log.info('Canary execution completed successfully'); + } catch (error) { + log.error('Canary execution failed: ' + (error instanceof Error ? error.message : String(error))); + throw error; + } +}; +exports.handler = async () => { + return await recordedScript(); +}; diff --git a/src/applications/canaries/petsite-canary/nodejs/package.json b/src/applications/canaries/petsite-canary/nodejs/package.json new file mode 100644 index 00000000..4418809f --- /dev/null +++ b/src/applications/canaries/petsite-canary/nodejs/package.json @@ -0,0 +1,9 @@ +{ + "name": "petsite-canary", + "version": "1.0.0", + "description": "CloudWatch Synthetics canary for petsite monitoring", + "main": "node_modules/index.js", + "dependencies": { + "@aws-sdk/client-ssm": "^3.0.0" + } +} \ No newline at end of file diff --git a/src/applications/lambda/petfood-cleanup-processor-node/.eslintrc.js b/src/applications/lambda/petfood-cleanup-processor-node/.eslintrc.js new file mode 100644 index 00000000..5aca9937 --- /dev/null +++ b/src/applications/lambda/petfood-cleanup-processor-node/.eslintrc.js @@ -0,0 +1,27 @@ +module.exports = { + env: { + node: true, + es2021: true, + jest: true + }, + extends: ['eslint:recommended'], + parserOptions: { + ecmaVersion: 2021, + sourceType: 'module' + }, + rules: { + indent: ['error', 4], + 'linebreak-style': ['error', 'unix'], + quotes: ['error', 'single'], + semi: ['error', 'always'], + 'no-unused-vars': ['error', { argsIgnorePattern: '^_' }], + 'no-console': 'off', // Allow console.log in Lambda functions + 'prefer-const': 'error', + 'no-var': 'error', + 'object-shorthand': 'error', + 'prefer-arrow-callback': 'error' + }, + globals: { + process: 'readonly' + } +}; diff --git a/PetAdoptions/pethistory/.prettierrc b/src/applications/lambda/petfood-cleanup-processor-node/.prettierrc similarity index 60% rename from PetAdoptions/pethistory/.prettierrc rename to src/applications/lambda/petfood-cleanup-processor-node/.prettierrc index c660a0a1..c2d15735 100644 --- a/PetAdoptions/pethistory/.prettierrc +++ b/src/applications/lambda/petfood-cleanup-processor-node/.prettierrc @@ -4,5 +4,7 @@ "singleQuote": true, "printWidth": 100, "tabWidth": 4, - "useTabs": false + "useTabs": false, + "bracketSpacing": true, + "arrowParens": "avoid" } \ No newline at end of file diff --git a/src/applications/lambda/petfood-cleanup-processor-node/index.js b/src/applications/lambda/petfood-cleanup-processor-node/index.js new file mode 100644 index 00000000..dfc7e4e8 --- /dev/null +++ b/src/applications/lambda/petfood-cleanup-processor-node/index.js @@ -0,0 +1,252 @@ +const { S3Client, DeleteObjectCommand, HeadObjectCommand } = require('@aws-sdk/client-s3'); +const { DynamoDBClient } = require('@aws-sdk/client-dynamodb'); +const { DynamoDBDocumentClient, DeleteCommand } = require('@aws-sdk/lib-dynamodb'); + +// Initialize AWS clients +let s3Client, dynamoClient, documentClient; + +function initializeClients() { + if (!s3Client) { + s3Client = new S3Client({ region: process.env.AWS_REGION }); + dynamoClient = new DynamoDBClient({ region: process.env.AWS_REGION }); + documentClient = DynamoDBDocumentClient.from(dynamoClient); + } + return { s3Client, docClient: documentClient }; +} + +// Configuration from environment variables +const CONFIG = { + S3_BUCKET: process.env.S3_BUCKET_NAME || 'petfood-images', + DYNAMODB_TABLE: process.env.DYNAMODB_TABLE_NAME || 'petfood-table', + MAX_RETRIES: Number.parseInt(process.env.MAX_RETRIES || '3'), + RETRY_DELAY_MS: Number.parseInt(process.env.RETRY_DELAY_MS || '1000') +}; + +/** + * Check if an S3 object exists + */ +async function checkS3ObjectExists(bucket, key, s3ClientParameter = s3Client) { + try { + await s3ClientParameter.send( + new HeadObjectCommand({ + Bucket: bucket, + Key: key + }) + ); + + return true; + } catch (error) { + if (error.name === 'NotFound' || error.$metadata?.httpStatusCode === 404) { + return false; + } + throw error; + } +} + +/** + * Delete an S3 object + */ +async function deleteS3Object(bucket, key, s3ClientParameter = s3Client) { + try { + await s3ClientParameter.send( + new DeleteObjectCommand({ + Bucket: bucket, + Key: key + }) + ); + + console.log(`Successfully deleted S3 object: s3://${bucket}/${key}`); + return true; + } catch (error) { + console.error(`Failed to delete S3 object s3://${bucket}/${key}:`, error.message); + throw error; + } +} + +/** + * Delete DynamoDB record for discontinued food item + */ +async function deleteDynamoDBRecord(foodId, documentClientParameter = documentClient) { + try { + const deleteParameters = { + TableName: CONFIG.DYNAMODB_TABLE, + Key: { id: foodId }, + ReturnValues: 'ALL_OLD' + }; + + const result = await documentClientParameter.send(new DeleteCommand(deleteParameters)); + + console.log(`Successfully deleted DynamoDB record for food ${foodId}`); + return result; + } catch (error) { + console.error(`Failed to delete DynamoDB record for food ${foodId}:`, error.message); + throw error; + } +} + +/** + * Retry logic with exponential backoff + */ +async function retryWithBackoff( + operation, + maxRetries = CONFIG.MAX_RETRIES, + baseDelay = CONFIG.RETRY_DELAY_MS +) { + let lastError; + + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + return await operation(); + } catch (error) { + lastError = error; + + if (attempt === maxRetries) { + break; + } + + const delay = baseDelay * Math.pow(2, attempt - 1) + Math.random() * 1000; + console.log(`Attempt ${attempt} failed, retrying in ${delay}ms:`, error.message); + await new Promise(resolve => setTimeout(resolve, delay)); + } + } + + throw lastError; +} + +/** + * Process a single ItemDiscontinued event + */ +async function processCleanupEvent(event) { + // Handle both EventBridge formats + const eventDetail = event.detail || event.eventDetail; + const eventType = event['detail-type'] || eventDetail?.event_type || eventDetail?.eventType; + const foodId = eventDetail?.food_id || eventDetail?.foodId; + + const eventData = { + eventType: eventType, + foodId: foodId, + status: eventDetail?.status, + metadata: eventDetail?.metadata || {}, + timestamp: event.time || new Date().toISOString() + }; + + console.log(`Processing cleanup event for food ${eventData.foodId}`, { + eventType: eventData.eventType, + status: eventData.status, + metadata: eventData.metadata + }); + + const cleanupSummary = { + foodId: eventData.foodId, + s3ImageDeleted: false, + databaseRecordDeleted: false, + postCleanupCompleted: false, + errors: [] + }; + + try { + // Extract image path from metadata + const imagePath = eventData.metadata.imagePath || eventData.metadata.image_path; + + if (imagePath) { + console.log(`Checking S3 image: ${imagePath}`); + + // Check if image exists in S3 + const imageExists = await retryWithBackoff(async () => { + return await checkS3ObjectExists(CONFIG.S3_BUCKET, imagePath); + }); + + if (imageExists) { + // Delete the image from S3 + await retryWithBackoff(async () => { + return await deleteS3Object(CONFIG.S3_BUCKET, imagePath); + }); + + cleanupSummary.s3ImageDeleted = true; + console.log(`Successfully deleted image: ${imagePath}`); + } else { + console.log(`Image not found in S3, skipping deletion: ${imagePath}`); + cleanupSummary.s3ImageDeleted = false; + } + } else { + console.log('No image path found in event metadata, skipping S3 cleanup'); + } + + // Delete DynamoDB record + await retryWithBackoff(async () => { + return await deleteDynamoDBRecord(eventData.foodId); + }); + cleanupSummary.databaseRecordDeleted = true; + + console.log( + `Cleanup processing completed successfully for food ${eventData.foodId}`, + cleanupSummary + ); + return cleanupSummary; + } catch (error) { + cleanupSummary.errors.push({ + message: error.message, + timestamp: new Date().toISOString() + }); + + console.error( + `Cleanup processing failed for food ${eventData.foodId}:`, + error.message, + cleanupSummary + ); + throw error; + } +} + +/** + * Main Lambda handler + */ +exports.handler = async (event, lambdaContext) => { + // Initialize clients + initializeClients(); + + try { + console.log('Cleanup processor Lambda invoked', { + requestId: lambdaContext.requestId, + eventSource: event.source, + detailType: event['detail-type'], + eventDetail: event.detail || event.eventDetail, + fullEvent: event + }); + + // Validate event structure - handle both EventBridge formats + const eventDetail = event.detail || event.eventDetail; + const eventType = event['detail-type'] || eventDetail?.event_type || eventDetail?.eventType; + const foodId = eventDetail?.food_id || eventDetail?.foodId; + + if (!eventDetail || !eventType || !foodId) { + throw new Error( + `Invalid event structure: missing required fields. Found eventType: ${eventType}, foodId: ${foodId}` + ); + } + + // Process the cleanup event + const result = await processCleanupEvent(event); + const response = { + statusCode: 200, + body: JSON.stringify({ + message: 'Cleanup processing completed successfully', + foodId: foodId, + cleanupSummary: result, + requestId: lambdaContext.requestId, + timestamp: new Date().toISOString() + }) + }; + + console.log('Cleanup processor completed successfully', response.body); + return response; + } catch (error) { + console.error('Cleanup processor failed:', error.message, { + requestId: lambdaContext.requestId, + error: error.stack + }); + + // Re-throw the error to trigger Lambda retry mechanism + throw error; + } +}; diff --git a/PetAdoptions/pethistory/jest.config.js b/src/applications/lambda/petfood-cleanup-processor-node/jest.config.js similarity index 55% rename from PetAdoptions/pethistory/jest.config.js rename to src/applications/lambda/petfood-cleanup-processor-node/jest.config.js index 853ae34c..635fd31f 100644 --- a/PetAdoptions/pethistory/jest.config.js +++ b/src/applications/lambda/petfood-cleanup-processor-node/jest.config.js @@ -3,6 +3,8 @@ module.exports = { collectCoverage: true, coverageDirectory: 'coverage', coverageReporters: ['text', 'lcov', 'html'], + collectCoverageFrom: ['index.js', '!coverage/**', '!node_modules/**', '!test/**'], testMatch: ['**/test/**/*.test.js'], - verbose: true -}; \ No newline at end of file + verbose: true, + setupFilesAfterEnv: ['/test/setup.js'] +}; diff --git a/PetAdoptions/pethistory/package-lock.json b/src/applications/lambda/petfood-cleanup-processor-node/package-lock.json similarity index 68% rename from PetAdoptions/pethistory/package-lock.json rename to src/applications/lambda/petfood-cleanup-processor-node/package-lock.json index 39a20755..01a904ab 100644 --- a/PetAdoptions/pethistory/package-lock.json +++ b/src/applications/lambda/petfood-cleanup-processor-node/package-lock.json @@ -1,22 +1,29 @@ { - "name": "petadopter", + "name": "petfood-cleanup-processor", "version": "1.0.0", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "petadopter", + "name": "petfood-cleanup-processor", "version": "1.0.0", "license": "MIT", "dependencies": { - "aws-sdk": "^2.1691.0", - "joi": "^17.13.3", - "pg": "^8.12.0" + "@aws-sdk/client-dynamodb": "^3.490.0", + "@aws-sdk/client-eventbridge": "^3.490.0", + "@aws-sdk/client-s3": "^3.490.0", + "@aws-sdk/lib-dynamodb": "^3.490.0" }, "devDependencies": { - "eslint": "^8.57.0", + "@types/aws-lambda": "^8.10.130", + "@types/jest": "^29.5.8", + "@types/node": "^20.9.0", + "eslint": "^8.54.0", "jest": "^29.7.0", - "prettier": "^3.3.3" + "prettier": "^3.1.0" + }, + "engines": { + "node": ">=18.0.0" } }, "node_modules/@ampproject/remapping": { @@ -33,1152 +40,2893 @@ "node": ">=6.0.0" } }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "dev": true, - "license": "MIT", + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "license": "Apache-2.0", "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=16.0.0" } }, - "node_modules/@babel/compat-data": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", - "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" + "node_modules/@aws-crypto/crc32c": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32c/-/crc32c-5.2.0.tgz", + "integrity": "sha512-+iWb8qaHLYKrNvGRbiYRHSdKRWhto5XlZUEBwDjYNf+ly5SVYG6zEoYIdxvf5R3zyeP16w4PLBn3rH1xc74Rag==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" } }, - "node_modules/@babel/core": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", - "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", - "dev": true, - "license": "MIT", + "node_modules/@aws-crypto/sha1-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha1-browser/-/sha1-browser-5.2.0.tgz", + "integrity": "sha512-OH6lveCFfcDjX4dbAvCFSYUjJZjDr/3XJ3xHtjn3Oj5b9RjojQo8npoLeA/bNwkOkrSQ0wgrHzXk4tDRxGKJeg==", + "license": "Apache-2.0", "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.27.3", - "@babel/helpers": "^7.27.6", - "@babel/parser": "^7.28.0", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.0", - "@babel/types": "^7.28.0", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" + "node": ">=14.0.0" } }, - "node_modules/@babel/generator": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", - "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", - "dev": true, - "license": "MIT", + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", "dependencies": { - "@babel/parser": "^7.28.0", - "@babel/types": "^7.28.0", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=14.0.0" } }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", - "dev": true, - "license": "MIT", + "node_modules/@aws-crypto/sha1-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", "dependencies": { - "@babel/compat-data": "^7.27.2", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=14.0.0" } }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "dev": true, - "license": "MIT", - "engines": { - "node": ">=6.9.0" + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" } }, - "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", - "dev": true, - "license": "MIT", + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=14.0.0" } }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", - "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", - "dev": true, - "license": "MIT", + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.3" + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0" + "engines": { + "node": ">=14.0.0" } }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", - "dev": true, - "license": "MIT", + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=6.9.0" + "node": ">=16.0.0" } }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "dev": true, - "license": "MIT", + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, "engines": { - "node": ">=6.9.0" + "node": ">=14.0.0" } }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", - "dev": true, - "license": "MIT", + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=6.9.0" + "node": ">=14.0.0" } }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "dev": true, - "license": "MIT", + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=6.9.0" + "node": ">=14.0.0" } }, - "node_modules/@babel/helpers": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz", - "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/client-dynamodb": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.876.0.tgz", + "integrity": "sha512-Qnh1hsdQpPTe7xnfQbC5XI7qfge/0i0krbJEu4fDs5GXVrSQj4JhQ4Xyxg/cYHsb/GsJs4qfTPPsUh4nEogExQ==", + "license": "Apache-2.0", "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.2" + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.876.0", + "@aws-sdk/credential-provider-node": "3.876.0", + "@aws-sdk/middleware-endpoint-discovery": "3.873.0", + "@aws-sdk/middleware-host-header": "3.873.0", + "@aws-sdk/middleware-logger": "3.876.0", + "@aws-sdk/middleware-recursion-detection": "3.873.0", + "@aws-sdk/middleware-user-agent": "3.876.0", + "@aws-sdk/region-config-resolver": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-endpoints": "3.873.0", + "@aws-sdk/util-user-agent-browser": "3.873.0", + "@aws-sdk/util-user-agent-node": "3.876.0", + "@smithy/config-resolver": "^4.1.5", + "@smithy/core": "^3.8.0", + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/hash-node": "^4.0.5", + "@smithy/invalid-dependency": "^4.0.5", + "@smithy/middleware-content-length": "^4.0.5", + "@smithy/middleware-endpoint": "^4.1.18", + "@smithy/middleware-retry": "^4.1.19", + "@smithy/middleware-serde": "^4.0.9", + "@smithy/middleware-stack": "^4.0.5", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/protocol-http": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.26", + "@smithy/util-defaults-mode-node": "^4.0.26", + "@smithy/util-endpoints": "^3.0.7", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-retry": "^4.0.7", + "@smithy/util-utf8": "^4.0.0", + "@smithy/util-waiter": "^4.0.7", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-eventbridge": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-eventbridge/-/client-eventbridge-3.876.0.tgz", + "integrity": "sha512-MohXPi6YBb5cuZ9FaU4QbMdcuTgZuuX877R0ebyHkrxY5omWkCv6+G59XZv86j0pRCPCkYTs239kmtKJXvo+eA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.876.0", + "@aws-sdk/credential-provider-node": "3.876.0", + "@aws-sdk/middleware-host-header": "3.873.0", + "@aws-sdk/middleware-logger": "3.876.0", + "@aws-sdk/middleware-recursion-detection": "3.873.0", + "@aws-sdk/middleware-user-agent": "3.876.0", + "@aws-sdk/region-config-resolver": "3.873.0", + "@aws-sdk/signature-v4-multi-region": "3.876.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-endpoints": "3.873.0", + "@aws-sdk/util-user-agent-browser": "3.873.0", + "@aws-sdk/util-user-agent-node": "3.876.0", + "@smithy/config-resolver": "^4.1.5", + "@smithy/core": "^3.8.0", + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/hash-node": "^4.0.5", + "@smithy/invalid-dependency": "^4.0.5", + "@smithy/middleware-content-length": "^4.0.5", + "@smithy/middleware-endpoint": "^4.1.18", + "@smithy/middleware-retry": "^4.1.19", + "@smithy/middleware-serde": "^4.0.9", + "@smithy/middleware-stack": "^4.0.5", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/protocol-http": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.26", + "@smithy/util-defaults-mode-node": "^4.0.26", + "@smithy/util-endpoints": "^3.0.7", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-retry": "^4.0.7", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-s3": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-s3/-/client-s3-3.876.0.tgz", + "integrity": "sha512-rrdrB0IlHfRaY+qxo87iSPJJxjCZ2WIV0wKi0EWz02yBpq17c0o6Vzc8f1+ksR+IZGkGttQnD2j4UpItMdLSKg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha1-browser": "5.2.0", + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.876.0", + "@aws-sdk/credential-provider-node": "3.876.0", + "@aws-sdk/middleware-bucket-endpoint": "3.873.0", + "@aws-sdk/middleware-expect-continue": "3.873.0", + "@aws-sdk/middleware-flexible-checksums": "3.876.0", + "@aws-sdk/middleware-host-header": "3.873.0", + "@aws-sdk/middleware-location-constraint": "3.873.0", + "@aws-sdk/middleware-logger": "3.876.0", + "@aws-sdk/middleware-recursion-detection": "3.873.0", + "@aws-sdk/middleware-sdk-s3": "3.876.0", + "@aws-sdk/middleware-ssec": "3.873.0", + "@aws-sdk/middleware-user-agent": "3.876.0", + "@aws-sdk/region-config-resolver": "3.873.0", + "@aws-sdk/signature-v4-multi-region": "3.876.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-endpoints": "3.873.0", + "@aws-sdk/util-user-agent-browser": "3.873.0", + "@aws-sdk/util-user-agent-node": "3.876.0", + "@aws-sdk/xml-builder": "3.873.0", + "@smithy/config-resolver": "^4.1.5", + "@smithy/core": "^3.8.0", + "@smithy/eventstream-serde-browser": "^4.0.5", + "@smithy/eventstream-serde-config-resolver": "^4.1.3", + "@smithy/eventstream-serde-node": "^4.0.5", + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/hash-blob-browser": "^4.0.5", + "@smithy/hash-node": "^4.0.5", + "@smithy/hash-stream-node": "^4.0.5", + "@smithy/invalid-dependency": "^4.0.5", + "@smithy/md5-js": "^4.0.5", + "@smithy/middleware-content-length": "^4.0.5", + "@smithy/middleware-endpoint": "^4.1.18", + "@smithy/middleware-retry": "^4.1.19", + "@smithy/middleware-serde": "^4.0.9", + "@smithy/middleware-stack": "^4.0.5", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/protocol-http": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.26", + "@smithy/util-defaults-mode-node": "^4.0.26", + "@smithy/util-endpoints": "^3.0.7", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-retry": "^4.0.7", + "@smithy/util-stream": "^4.2.4", + "@smithy/util-utf8": "^4.0.0", + "@smithy/util-waiter": "^4.0.7", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.876.0.tgz", + "integrity": "sha512-Vf0PMF7HVpvllrfPODnBZmlz6kT/y2AvOt1RQG3+qD0VrHWzShc5nwgRZ+yyP3xkKVhZsQ3sJapfZTFnjqMOYA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.876.0", + "@aws-sdk/middleware-host-header": "3.873.0", + "@aws-sdk/middleware-logger": "3.876.0", + "@aws-sdk/middleware-recursion-detection": "3.873.0", + "@aws-sdk/middleware-user-agent": "3.876.0", + "@aws-sdk/region-config-resolver": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-endpoints": "3.873.0", + "@aws-sdk/util-user-agent-browser": "3.873.0", + "@aws-sdk/util-user-agent-node": "3.876.0", + "@smithy/config-resolver": "^4.1.5", + "@smithy/core": "^3.8.0", + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/hash-node": "^4.0.5", + "@smithy/invalid-dependency": "^4.0.5", + "@smithy/middleware-content-length": "^4.0.5", + "@smithy/middleware-endpoint": "^4.1.18", + "@smithy/middleware-retry": "^4.1.19", + "@smithy/middleware-serde": "^4.0.9", + "@smithy/middleware-stack": "^4.0.5", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/protocol-http": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.26", + "@smithy/util-defaults-mode-node": "^4.0.26", + "@smithy/util-endpoints": "^3.0.7", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-retry": "^4.0.7", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.876.0.tgz", + "integrity": "sha512-sVFBFkdoPOPyY13NaXO1E/R9O5J6ixzHnnRbqrbXYM2QQgLNPTKIiRtmVEuVoFV9YULg+/aKm7caix8m468y9w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@aws-sdk/xml-builder": "3.873.0", + "@smithy/core": "^3.8.0", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/property-provider": "^4.0.5", + "@smithy/protocol-http": "^5.1.3", + "@smithy/signature-v4": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-utf8": "^4.0.0", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.876.0.tgz", + "integrity": "sha512-cof7lwp2AlrAfRs0pt4W2KMS2VMBvEmpcti1UOFfSJIqkn+cyJliMJ8LHg22GI+kUexjvxdAqSbf3M7OHvEW+w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.876.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=18.0.0" } }, - "node_modules/@babel/parser": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", - "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.876.0.tgz", + "integrity": "sha512-wzmef2NBp2+X1l8D4Q8hx1G8oI3+WdvLdPev9VnVpRYZxYGRWVPl++wvCBsCn/ZL0mdWopPkhHA3kFexQhMzvg==", + "license": "Apache-2.0", "dependencies": { - "@babel/types": "^7.28.0" + "@aws-sdk/core": "3.876.0", + "@aws-sdk/types": "3.862.0", + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/property-provider": "^4.0.5", + "@smithy/protocol-http": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/util-stream": "^4.2.4", + "tslib": "^2.6.2" }, - "bin": { - "parser": "bin/babel-parser.js" + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.876.0.tgz", + "integrity": "sha512-JHbW6fqnJsVjGHCyko7B0NVPT1nEAPxkM3CGjUcVGsHgJBkxOLVCMQqTRyHcDdeHR2qeojlLoOHRz97xIHQjYw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.876.0", + "@aws-sdk/credential-provider-env": "3.876.0", + "@aws-sdk/credential-provider-http": "3.876.0", + "@aws-sdk/credential-provider-process": "3.876.0", + "@aws-sdk/credential-provider-sso": "3.876.0", + "@aws-sdk/credential-provider-web-identity": "3.876.0", + "@aws-sdk/nested-clients": "3.876.0", + "@aws-sdk/types": "3.862.0", + "@smithy/credential-provider-imds": "^4.0.7", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.0.0" + "node": ">=18.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, - "license": "MIT", + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.876.0.tgz", + "integrity": "sha512-eHbNt1+Hi43e8ANnwf6toapLSxfMiyGq459y3Uh6i7NBOiWWKEsOVcgOfUC3RCoqeikxovt1tFM2cEElWUIOhg==", + "license": "Apache-2.0", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@aws-sdk/credential-provider-env": "3.876.0", + "@aws-sdk/credential-provider-http": "3.876.0", + "@aws-sdk/credential-provider-ini": "3.876.0", + "@aws-sdk/credential-provider-process": "3.876.0", + "@aws-sdk/credential-provider-sso": "3.876.0", + "@aws-sdk/credential-provider-web-identity": "3.876.0", + "@aws-sdk/types": "3.862.0", + "@smithy/credential-provider-imds": "^4.0.7", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=18.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, - "license": "MIT", + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.876.0.tgz", + "integrity": "sha512-SMX4OlHvspu3gF4hxe7WAnZFhxpiCye+WlBSVoWfW/i9XNhtrZS1JMr29MK34GlCTk9qO7FlRwds/Z5k7xPpHg==", + "license": "Apache-2.0", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@aws-sdk/core": "3.876.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=18.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, - "license": "MIT", + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.876.0.tgz", + "integrity": "sha512-iP5dz9XqwePbgnh7Bdrq5e1319JpCRKLyomUfHH1XVeXkIHmwIJdmTj1Upeo1J8L/5cLHmhXAN6CTN11bLo8SA==", + "license": "Apache-2.0", "dependencies": { - "@babel/helper-plugin-utils": "^7.12.13" + "@aws-sdk/client-sso": "3.876.0", + "@aws-sdk/core": "3.876.0", + "@aws-sdk/token-providers": "3.876.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=18.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, - "license": "MIT", + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.876.0.tgz", + "integrity": "sha512-q/XSCP1uae5aB9veM8zcm6Gqu6A4ckX9ZbhHgCzURXVJDwp+nINW1hM9vppMjGw3ND9Ibx/adR+KfTI0TDMzqw==", + "license": "Apache-2.0", "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" + "@aws-sdk/core": "3.876.0", + "@aws-sdk/nested-clients": "3.876.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/endpoint-cache": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/endpoint-cache/-/endpoint-cache-3.873.0.tgz", + "integrity": "sha512-EHd+5bSp/hZc78SMq9cUCIsX0B4ekZtFUVSSLEXyYv8x/nHFTnTqN9TsxV8bjlztR3aSUeoKSk5qxu/dVGgiQw==", + "license": "Apache-2.0", + "dependencies": { + "mnemonist": "0.38.3", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", - "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/lib-dynamodb": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/lib-dynamodb/-/lib-dynamodb-3.876.0.tgz", + "integrity": "sha512-F579ZxpVcRlkf77pE93Hpvadr5UZFmJdG77Qch+rvd//y1wqlFuRtoJ6oUIACrfYd59WJ0CByr6ET6sko7d+5g==", + "license": "Apache-2.0", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@aws-sdk/core": "3.876.0", + "@aws-sdk/util-dynamodb": "3.876.0", + "@smithy/core": "^3.8.0", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=18.0.0" }, "peerDependencies": { - "@babel/core": "^7.0.0-0" + "@aws-sdk/client-dynamodb": "^3.876.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, - "license": "MIT", + "node_modules/@aws-sdk/middleware-bucket-endpoint": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-bucket-endpoint/-/middleware-bucket-endpoint-3.873.0.tgz", + "integrity": "sha512-b4bvr0QdADeTUs+lPc9Z48kXzbKHXQKgTvxx/jXDgSW9tv4KmYPO1gIj6Z9dcrBkRWQuUtSW3Tu2S5n6pe+zeg==", + "license": "Apache-2.0", "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-arn-parser": "3.873.0", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "@smithy/util-config-provider": "^4.0.0", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=18.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, - "license": "MIT", + "node_modules/@aws-sdk/middleware-endpoint-discovery": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-endpoint-discovery/-/middleware-endpoint-discovery-3.873.0.tgz", + "integrity": "sha512-qKQocn1MzaLS9dt5xt3MvQsZaQzRsmOFdazWXkMup1AtFrULSUklsbHjm5fg5xyFPN8ipNzPi+MCXcgPzfpKkg==", + "license": "Apache-2.0", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@aws-sdk/endpoint-cache": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", - "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/middleware-expect-continue": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-expect-continue/-/middleware-expect-continue-3.873.0.tgz", + "integrity": "sha512-GIqoc8WgRcf/opBOZXFLmplJQKwOMjiOMmDz9gQkaJ8FiVJoAp8EGVmK2TOWZMQUYsavvHYsHaor5R2xwPoGVg==", + "license": "Apache-2.0", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@aws-sdk/types": "3.862.0", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=18.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, - "license": "MIT", + "node_modules/@aws-sdk/middleware-flexible-checksums": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-flexible-checksums/-/middleware-flexible-checksums-3.876.0.tgz", + "integrity": "sha512-Xfb9/XP0WcQq/yJxUubfzMUF0AYSX10UUIRbCJog0/lnDNocEiGEIaarwuQzoxb9QW9TQ1l5dDc/5bOMa1YVGw==", + "license": "Apache-2.0", "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" + "@aws-crypto/crc32": "5.2.0", + "@aws-crypto/crc32c": "5.2.0", + "@aws-crypto/util": "5.2.0", + "@aws-sdk/core": "3.876.0", + "@aws-sdk/types": "3.862.0", + "@smithy/is-array-buffer": "^4.0.0", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-stream": "^4.2.4", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=18.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, - "license": "MIT", + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.873.0.tgz", + "integrity": "sha512-KZ/W1uruWtMOs7D5j3KquOxzCnV79KQW9MjJFZM/M0l6KI8J6V3718MXxFHsTjUE4fpdV6SeCNLV1lwGygsjJA==", + "license": "Apache-2.0", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@aws-sdk/types": "3.862.0", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=18.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, - "license": "MIT", + "node_modules/@aws-sdk/middleware-location-constraint": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-location-constraint/-/middleware-location-constraint-3.873.0.tgz", + "integrity": "sha512-r+hIaORsW/8rq6wieDordXnA/eAu7xAPLue2InhoEX6ML7irP52BgiibHLpt9R0psiCzIHhju8qqKa4pJOrmiw==", + "license": "Apache-2.0", "dependencies": { - "@babel/helper-plugin-utils": "^7.10.4" + "@aws-sdk/types": "3.862.0", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=18.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, - "license": "MIT", + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.876.0.tgz", + "integrity": "sha512-cpWJhOuMSyz9oV25Z/CMHCBTgafDCbv7fHR80nlRrPdPZ8ETNsahwRgltXP1QJJ8r3X/c1kwpOR7tc+RabVzNA==", + "license": "Apache-2.0", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@aws-sdk/types": "3.862.0", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=18.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, - "license": "MIT", + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.873.0.tgz", + "integrity": "sha512-OtgY8EXOzRdEWR//WfPkA/fXl0+WwE8hq0y9iw2caNyKPtca85dzrrZWnPqyBK/cpImosrpR1iKMYr41XshsCg==", + "license": "Apache-2.0", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@aws-sdk/types": "3.862.0", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=18.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, - "license": "MIT", + "node_modules/@aws-sdk/middleware-sdk-s3": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-sdk-s3/-/middleware-sdk-s3-3.876.0.tgz", + "integrity": "sha512-h+TDs9EKAfXnrkogQpQz3o11zvs6Vh9+ehxyd35OcM7evnDeoV4GFjjnAKq+MxbBk/5Ewnvng+d6/WQDvMbj7Q==", + "license": "Apache-2.0", "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" + "@aws-sdk/core": "3.876.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-arn-parser": "3.873.0", + "@smithy/core": "^3.8.0", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/protocol-http": "^5.1.3", + "@smithy/signature-v4": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-stream": "^4.2.4", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-ssec": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-ssec/-/middleware-ssec-3.873.0.tgz", + "integrity": "sha512-AF55J94BoiuzN7g3hahy0dXTVZahVi8XxRBLgzNp6yQf0KTng+hb/V9UQZVYY1GZaDczvvvnqC54RGe9OZZ9zQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "engines": { + "node": ">=18.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, - "license": "MIT", + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.876.0.tgz", + "integrity": "sha512-FR+8INfnbNv32QDQ5szxkWX6mB/QgezfNyx8LnAh1ErISZMmEFBxXXir+ZOfuV8vsmal1a6cy9qmnMNDaNnaNQ==", + "license": "Apache-2.0", "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" + "@aws-sdk/core": "3.876.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-endpoints": "3.873.0", + "@smithy/core": "^3.8.0", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=18.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, - "license": "MIT", + "node_modules/@aws-sdk/nested-clients": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.876.0.tgz", + "integrity": "sha512-R4TZrkM2gUElTsotk8mt3y7iLG8TNi1LL1wgVdEEWSLOYTaFyglGdoNBMtEeP7lmXilaTy00AbYF6BakJvSTHg==", + "license": "Apache-2.0", "dependencies": { - "@babel/helper-plugin-utils": "^7.14.5" + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.876.0", + "@aws-sdk/middleware-host-header": "3.873.0", + "@aws-sdk/middleware-logger": "3.876.0", + "@aws-sdk/middleware-recursion-detection": "3.873.0", + "@aws-sdk/middleware-user-agent": "3.876.0", + "@aws-sdk/region-config-resolver": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-endpoints": "3.873.0", + "@aws-sdk/util-user-agent-browser": "3.873.0", + "@aws-sdk/util-user-agent-node": "3.876.0", + "@smithy/config-resolver": "^4.1.5", + "@smithy/core": "^3.8.0", + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/hash-node": "^4.0.5", + "@smithy/invalid-dependency": "^4.0.5", + "@smithy/middleware-content-length": "^4.0.5", + "@smithy/middleware-endpoint": "^4.1.18", + "@smithy/middleware-retry": "^4.1.19", + "@smithy/middleware-serde": "^4.0.9", + "@smithy/middleware-stack": "^4.0.5", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/protocol-http": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.26", + "@smithy/util-defaults-mode-node": "^4.0.26", + "@smithy/util-endpoints": "^3.0.7", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-retry": "^4.0.7", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.873.0.tgz", + "integrity": "sha512-q9sPoef+BBG6PJnc4x60vK/bfVwvRWsPgcoQyIra057S/QGjq5VkjvNk6H8xedf6vnKlXNBwq9BaANBXnldUJg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/types": "^4.3.2", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.5", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=18.0.0" } }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", - "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.876.0.tgz", + "integrity": "sha512-OMDcuaVlC2rbze92w4QcNfuEA0IeT2GsT1ByZCwe+Y9tZwxzj7fCiOOU0UmJfa+juuQ/YBzVYxnkrkz3Rg6DEw==", + "license": "Apache-2.0", "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" + "@aws-sdk/middleware-sdk-s3": "3.876.0", + "@aws-sdk/types": "3.862.0", + "@smithy/protocol-http": "^5.1.3", + "@smithy/signature-v4": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" + "node": ">=18.0.0" } }, - "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/token-providers": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.876.0.tgz", + "integrity": "sha512-iU08kaQbhXnY0CC2TBcr7y/2PqPwZP2CTWX/Rbq0NvhOyteikfh7ASC+bRfLUp0XMSHKvSb+w2dh8a0lvx4oHg==", + "license": "Apache-2.0", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" + "@aws-sdk/core": "3.876.0", + "@aws-sdk/nested-clients": "3.876.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=18.0.0" } }, - "node_modules/@babel/traverse": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", - "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/types": { + "version": "3.862.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.862.0.tgz", + "integrity": "sha512-Bei+RL0cDxxV+lW2UezLbCYYNeJm6Nzee0TpW0FfyTRBhH9C1XQh4+x+IClriXvgBnRquTMMYsmJfvx8iyLKrg==", + "license": "Apache-2.0", "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.0", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.0", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.0", - "debug": "^4.3.1" + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=18.0.0" } }, - "node_modules/@babel/types": { - "version": "7.28.2", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", - "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/util-arn-parser": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-arn-parser/-/util-arn-parser-3.873.0.tgz", + "integrity": "sha512-qag+VTqnJWDn8zTAXX4wiVioa0hZDQMtbZcGRERVnLar4/3/VIKBhxX2XibNQXFu1ufgcRn4YntT/XEPecFWcg==", + "license": "Apache-2.0", "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" + "tslib": "^2.6.2" }, "engines": { - "node": ">=6.9.0" + "node": ">=18.0.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, - "license": "MIT" - }, - "node_modules/@eslint-community/eslint-utils": { - "version": "4.7.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", - "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/util-dynamodb": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-dynamodb/-/util-dynamodb-3.876.0.tgz", + "integrity": "sha512-/GAD9ZNJBa58YC7wRxH4jv6gOvQSr2LDRogU/ieU9Kb77ay0pxjmzDGk6RixhvWYmN05QUWxghk9fPLhEy24rQ==", + "license": "Apache-2.0", "dependencies": { - "eslint-visitor-keys": "^3.4.3" + "tslib": "^2.6.2" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=18.0.0" }, "peerDependencies": { - "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + "@aws-sdk/client-dynamodb": "^3.876.0" } }, - "node_modules/@eslint-community/regexpp": { - "version": "4.12.1", - "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", - "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.873.0.tgz", + "integrity": "sha512-YByHrhjxYdjKRf/RQygRK1uh0As1FIi9+jXTcIEX/rBgN8mUByczr2u4QXBzw7ZdbdcOBMOkPnLRjNOWW1MkFg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-endpoints": "^3.0.7", + "tslib": "^2.6.2" + }, "engines": { - "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + "node": ">=18.0.0" } }, - "node_modules/@eslint/eslintrc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", - "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", - "dev": true, - "license": "MIT", + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.873.0.tgz", + "integrity": "sha512-xcVhZF6svjM5Rj89T1WzkjQmrTF6dpR2UvIHPMTnSZoNe6CixejPZ6f0JJ2kAhO8H+dUHwNBlsUgOTIKiK/Syg==", + "license": "Apache-2.0", "dependencies": { - "ajv": "^6.12.4", - "debug": "^4.3.2", - "espree": "^9.6.0", - "globals": "^13.19.0", - "ignore": "^5.2.0", - "import-fresh": "^3.2.1", - "js-yaml": "^4.1.0", - "minimatch": "^3.1.2", - "strip-json-comments": "^3.1.1" + "tslib": "^2.6.2" }, "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" - }, - "funding": { - "url": "https://opencollective.com/eslint" + "node": ">=18.0.0" } }, - "node_modules/@eslint/js": { - "version": "8.57.1", - "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", - "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", - "dev": true, - "license": "MIT", - "engines": { - "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.873.0.tgz", + "integrity": "sha512-AcRdbK6o19yehEcywI43blIBhOCSo6UgyWcuOJX5CFF8k39xm1ILCjQlRRjchLAxWrm0lU0Q7XV90RiMMFMZtA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/types": "^4.3.2", + "bowser": "^2.11.0", + "tslib": "^2.6.2" } }, - "node_modules/@hapi/hoek": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", - "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@hapi/topo": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", - "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", - "license": "BSD-3-Clause", + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.876.0.tgz", + "integrity": "sha512-/ZIaeUt60JBdI0mNc7sZ8v3Tuzp8Pbe4gIAYnppGyF4KV8QA+Yu8tp2bGHfkKn150t1uvQ6P/4CwFfoGF34dzg==", + "license": "Apache-2.0", "dependencies": { - "@hapi/hoek": "^9.0.0" + "@aws-sdk/middleware-user-agent": "3.876.0", + "@aws-sdk/types": "3.862.0", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } } }, - "node_modules/@humanwhocodes/config-array": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", - "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", - "deprecated": "Use @eslint/config-array instead", - "dev": true, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.873.0.tgz", + "integrity": "sha512-kLO7k7cGJ6KaHiExSJWojZurF7SnGMDHXRuQunFnEoD0n1yB6Lqy/S/zHiQ7oJnBhPr9q0TW9qFkrsZb1Uc54w==", "license": "Apache-2.0", "dependencies": { - "@humanwhocodes/object-schema": "^2.0.3", - "debug": "^4.3.1", - "minimatch": "^3.0.5" + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" }, "engines": { - "node": ">=10.10.0" + "node": ">=18.0.0" } }, - "node_modules/@humanwhocodes/module-importer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", - "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dev": true, - "license": "Apache-2.0", - "engines": { - "node": ">=12.22" + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/nzakas" + "engines": { + "node": ">=6.9.0" } }, - "node_modules/@humanwhocodes/object-schema": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", - "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", - "deprecated": "Use @eslint/object-schema instead", + "node_modules/@babel/compat-data": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", "dev": true, - "license": "BSD-3-Clause" + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } }, - "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==", + "node_modules/@babel/core": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz", + "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", "dev": true, - "license": "ISC", + "license": "MIT", "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" + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.3", + "@babel/parser": "^7.28.3", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.3", + "@babel/types": "^7.28.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": ">=8" + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "node_modules/@babel/core/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, - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" + "license": "ISC", + "bin": { + "semver": "bin/semver.js" } }, - "node_modules/@istanbuljs/load-nyc-config/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==", + "node_modules/@babel/generator": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", "dev": true, "license": "MIT", "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" }, "engines": { - "node": ">=8" + "node": ">=6.9.0" } }, - "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", "dev": true, "license": "MIT", "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/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, + "license": "ISC", "bin": { - "js-yaml": "bin/js-yaml.js" + "semver": "bin/semver.js" } }, - "node_modules/@istanbuljs/load-nyc-config/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==", + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", "dev": true, "license": "MIT", - "dependencies": { - "p-locate": "^4.1.0" - }, "engines": { - "node": ">=8" + "node": ">=6.9.0" } }, - "node_modules/@istanbuljs/load-nyc-config/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==", + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", "dev": true, "license": "MIT", "dependencies": { - "p-try": "^2.0.0" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "node": ">=6.9.0" } }, - "node_modules/@istanbuljs/load-nyc-config/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==", + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", "dev": true, "license": "MIT", "dependencies": { - "p-limit": "^2.2.0" + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" }, "engines": { - "node": ">=8" + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "node_modules/@istanbuljs/load-nyc-config/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==", + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=6.9.0" } }, - "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==", + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, "license": "MIT", "engines": { - "node": ">=8" + "node": ">=6.9.0" } }, - "node_modules/@jest/console": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", - "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", "dev": true, "license": "MIT", - "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": ">=6.9.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==", + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", "dev": true, "license": "MIT", - "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": ">=6.9.0" } }, - "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==", + "node_modules/@babel/helpers": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.3.tgz", + "integrity": "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==", "dev": true, "license": "MIT", "dependencies": { - "@jest/fake-timers": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/node": "*", - "jest-mock": "^29.7.0" + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=6.9.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==", + "node_modules/@babel/parser": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz", + "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz", + "integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.3", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.2", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "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, + "license": "MIT" + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + } + }, + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", + "dev": true, + "license": "BSD-3-Clause" + }, + "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, + "license": "ISC", + "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/load-nyc-config/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, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/@istanbuljs/load-nyc-config/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, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@istanbuljs/load-nyc-config/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, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/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, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@istanbuljs/load-nyc-config/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, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.30", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", + "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", "dev": true, "license": "MIT", "dependencies": { - "expect": "^29.7.0", - "jest-snapshot": "^29.7.0" + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "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, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@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, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@smithy/abort-controller": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.5.tgz", + "integrity": "sha512-jcrqdTQurIrBbUm4W2YdLVMQDoL0sA9DTxYd2s+R/y+2U9NLOP7Xf/YqfSg1FZhlZIYEnvk2mwbyvIfdLEPo8g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader/-/chunked-blob-reader-5.0.0.tgz", + "integrity": "sha512-+sKqDBQqb036hh4NPaUiEkYFkTUGYzRsn3EuFhyfQfMy6oGHEUJDurLP9Ufb5dasr/XiAmPNMr6wa9afjQB+Gw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/chunked-blob-reader-native": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/chunked-blob-reader-native/-/chunked-blob-reader-native-4.0.0.tgz", + "integrity": "sha512-R9wM2yPmfEMsUmlMlIgSzOyICs0x9uu7UTHoccMyt7BWw8shcGM8HqB355+BZCPBcySvbTYMs62EgEQkNxz2ig==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-base64": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.1.5.tgz", + "integrity": "sha512-viuHMxBAqydkB0AfWwHIdwf/PRH2z5KHGUzqyRtS/Wv+n3IHI993Sk76VCA7dD/+GzgGOmlJDITfPcJC1nIVIw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.4", + "@smithy/types": "^4.3.2", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.8.0.tgz", + "integrity": "sha512-EYqsIYJmkR1VhVE9pccnk353xhs+lB6btdutJEtsp7R055haMJp2yE16eSxw8fv+G0WUY6vqxyYOP8kOqawxYQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^4.0.9", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-stream": "^4.2.4", + "@smithy/util-utf8": "^4.0.0", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.0.7.tgz", + "integrity": "sha512-dDzrMXA8d8riFNiPvytxn0mNwR4B3h8lgrQ5UjAGu6T9z/kRg/Xncf4tEQHE/+t25sY8IH3CowcmWi+1U5B1Gw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.4", + "@smithy/property-provider": "^4.0.5", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-codec": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.0.5.tgz", + "integrity": "sha512-miEUN+nz2UTNoRYRhRqVTJCx7jMeILdAurStT2XoS+mhokkmz1xAPp95DFW9Gxt4iF2VBqpeF9HbTQ3kY1viOA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.3.2", + "@smithy/util-hex-encoding": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-browser": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.0.5.tgz", + "integrity": "sha512-LCUQUVTbM6HFKzImYlSB9w4xafZmpdmZsOh9rIl7riPC3osCgGFVP+wwvYVw6pXda9PPT9TcEZxaq3XE81EdJQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-config-resolver": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.1.3.tgz", + "integrity": "sha512-yTTzw2jZjn/MbHu1pURbHdpjGbCuMHWncNBpJnQAPxOVnFUAbSIUSwafiphVDjNV93TdBJWmeVAds7yl5QCkcA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-node": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.0.5.tgz", + "integrity": "sha512-lGS10urI4CNzz6YlTe5EYG0YOpsSp3ra8MXyco4aqSkQDuyZPIw2hcaxDU82OUVtK7UY9hrSvgWtpsW5D4rb4g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-universal": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.0.5.tgz", + "integrity": "sha512-JFnmu4SU36YYw3DIBVao3FsJh4Uw65vVDIqlWT4LzR6gXA0F3KP0IXFKKJrhaVzCBhAuMsrUUaT5I+/4ZhF7aw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-codec": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.1.1.tgz", + "integrity": "sha512-61WjM0PWmZJR+SnmzaKI7t7G0UkkNFboDpzIdzSoy7TByUzlxo18Qlh9s71qug4AY4hlH/CwXdubMtkcNEb/sQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.1.3", + "@smithy/querystring-builder": "^4.0.5", + "@smithy/types": "^4.3.2", + "@smithy/util-base64": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-blob-browser": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/hash-blob-browser/-/hash-blob-browser-4.0.5.tgz", + "integrity": "sha512-F7MmCd3FH/Q2edhcKd+qulWkwfChHbc9nhguBlVjSUE6hVHhec3q6uPQ+0u69S6ppvLtR3eStfCuEKMXBXhvvA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/chunked-blob-reader": "^5.0.0", + "@smithy/chunked-blob-reader-native": "^4.0.0", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.0.5.tgz", + "integrity": "sha512-cv1HHkKhpyRb6ahD8Vcfb2Hgz67vNIXEp2vnhzfxLFGRukLCNEA5QdsorbUEzXma1Rco0u3rx5VTqbM06GcZqQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-stream-node": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/hash-stream-node/-/hash-stream-node-4.0.5.tgz", + "integrity": "sha512-IJuDS3+VfWB67UC0GU0uYBG/TA30w+PlOaSo0GPm9UHS88A6rCP6uZxNjNYiyRtOcjv7TXn/60cW8ox1yuZsLg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.0.5.tgz", + "integrity": "sha512-IVnb78Qtf7EJpoEVo7qJ8BEXQwgC4n3igeJNNKEj/MLYtapnx8A67Zt/J3RXAj2xSO1910zk0LdFiygSemuLow==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.0.0.tgz", + "integrity": "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/md5-js": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/md5-js/-/md5-js-4.0.5.tgz", + "integrity": "sha512-8n2XCwdUbGr8W/XhMTaxILkVlw2QebkVTn5tm3HOcbPbOpWg89zr6dPXsH8xbeTsbTXlJvlJNTQsKAIoqQGbdA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.0.5.tgz", + "integrity": "sha512-l1jlNZoYzoCC7p0zCtBDE5OBXZ95yMKlRlftooE5jPWQn4YBPLgsp+oeHp7iMHaTGoUdFqmHOPa8c9G3gBsRpQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.1.18", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.1.18.tgz", + "integrity": "sha512-ZhvqcVRPZxnZlokcPaTwb+r+h4yOIOCJmx0v2d1bpVlmP465g3qpVSf7wxcq5zZdu4jb0H4yIMxuPwDJSQc3MQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.8.0", + "@smithy/middleware-serde": "^4.0.9", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-middleware": "^4.0.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.1.19", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.1.19.tgz", + "integrity": "sha512-X58zx/NVECjeuUB6A8HBu4bhx72EoUz+T5jTMIyeNKx2lf+Gs9TmWPNNkH+5QF0COjpInP/xSpJGJ7xEnAklQQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.4", + "@smithy/protocol-http": "^5.1.3", + "@smithy/service-error-classification": "^4.0.7", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-retry": "^4.0.7", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.0.9.tgz", + "integrity": "sha512-uAFFR4dpeoJPGz8x9mhxp+RPjo5wW0QEEIPPPbLXiRRWeCATf/Km3gKIVR5vaP8bN1kgsPhcEeh+IZvUlBv6Xg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.0.5.tgz", + "integrity": "sha512-/yoHDXZPh3ocRVyeWQFvC44u8seu3eYzZRveCMfgMOBcNKnAmOvjbL9+Cp5XKSIi9iYA9PECUuW2teDAk8T+OQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.1.4.tgz", + "integrity": "sha512-+UDQV/k42jLEPPHSn39l0Bmc4sB1xtdI9Gd47fzo/0PbXzJ7ylgaOByVjF5EeQIumkepnrJyfx86dPa9p47Y+w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.1.1.tgz", + "integrity": "sha512-RHnlHqFpoVdjSPPiYy/t40Zovf3BBHc2oemgD7VsVTFFZrU5erFFe0n52OANZZ/5sbshgD93sOh5r6I35Xmpaw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.0.5", + "@smithy/protocol-http": "^5.1.3", + "@smithy/querystring-builder": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.0.5.tgz", + "integrity": "sha512-R/bswf59T/n9ZgfgUICAZoWYKBHcsVDurAGX88zsiUtOTA/xUAPyiT+qkNCPwFn43pZqN84M4MiUsbSGQmgFIQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.3.tgz", + "integrity": "sha512-fCJd2ZR7D22XhDY0l+92pUag/7je2BztPRQ01gU5bMChcyI0rlly7QFibnYHzcxDvccMjlpM/Q1ev8ceRIb48w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.0.5.tgz", + "integrity": "sha512-NJeSCU57piZ56c+/wY+AbAw6rxCCAOZLCIniRE7wqvndqxcKKDOXzwWjrY7wGKEISfhL9gBbAaWWgHsUGedk+A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "@smithy/util-uri-escape": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.0.5.tgz", + "integrity": "sha512-6SV7md2CzNG/WUeTjVe6Dj8noH32r4MnUeFKZrnVYsQxpGSIcphAanQMayi8jJLZAWm6pdM9ZXvKCpWOsIGg0w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.0.7.tgz", + "integrity": "sha512-XvRHOipqpwNhEjDf2L5gJowZEm5nsxC16pAZOeEcsygdjv9A2jdOh3YoDQvOXBGTsaJk6mNWtzWalOB9976Wlg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=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, - "license": "MIT", + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.0.5.tgz", + "integrity": "sha512-YVVwehRDuehgoXdEL4r1tAAzdaDgaC9EQvhK0lEbfnbrd0bd5+CTQumbdPryX3J2shT7ZqQE+jPW4lmNBAB8JQ==", + "license": "Apache-2.0", "dependencies": { - "jest-get-type": "^29.6.3" + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=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, - "license": "MIT", + "node_modules/@smithy/signature-v4": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.1.3.tgz", + "integrity": "sha512-mARDSXSEgllNzMw6N+mC+r1AQlEBO3meEAkR/UlfAgnMzJUB3goRBWgip1EAMG99wh36MDqzo86SfIX5Y+VEaw==", + "license": "Apache-2.0", "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" + "@smithy/is-array-buffer": "^4.0.0", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-uri-escape": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=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, - "license": "MIT", + "node_modules/@smithy/smithy-client": { + "version": "4.4.10", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.4.10.tgz", + "integrity": "sha512-iW6HjXqN0oPtRS0NK/zzZ4zZeGESIFcxj2FkWed3mcK8jdSdHzvnCKXSjvewESKAgGKAbJRA+OsaqKhkdYRbQQ==", + "license": "Apache-2.0", "dependencies": { - "@jest/environment": "^29.7.0", - "@jest/expect": "^29.7.0", - "@jest/types": "^29.6.3", - "jest-mock": "^29.7.0" + "@smithy/core": "^3.8.0", + "@smithy/middleware-endpoint": "^4.1.18", + "@smithy/middleware-stack": "^4.0.5", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "@smithy/util-stream": "^4.2.4", + "tslib": "^2.6.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=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, - "license": "MIT", + "node_modules/@smithy/types": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.3.2.tgz", + "integrity": "sha512-QO4zghLxiQ5W9UZmX2Lo0nta2PuE1sSrXUYDoaB6HMR762C0P7v/HEPHf6ZdglTVssJG1bsrSBxdc3quvDSihw==", + "license": "Apache-2.0", "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" + "tslib": "^2.6.2" }, "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": ">=18.0.0" } }, - "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, - "license": "MIT", + "node_modules/@smithy/url-parser": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.0.5.tgz", + "integrity": "sha512-j+733Um7f1/DXjYhCbvNXABV53NyCRRA54C7bNEIxNPs0YjfRxeMKjjgm2jvTYrciZyCjsicHwQ6Q0ylo+NAUw==", + "license": "Apache-2.0", "dependencies": { - "@sinclair/typebox": "^0.27.8" + "@smithy/querystring-parser": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=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, - "license": "MIT", + "node_modules/@smithy/util-base64": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.0.0.tgz", + "integrity": "sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==", + "license": "Apache-2.0", "dependencies": { - "@jridgewell/trace-mapping": "^0.3.18", - "callsites": "^3.0.0", - "graceful-fs": "^4.2.9" + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=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, - "license": "MIT", + "node_modules/@smithy/util-body-length-browser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.0.0.tgz", + "integrity": "sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==", + "license": "Apache-2.0", "dependencies": { - "@jest/console": "^29.7.0", - "@jest/types": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" + "tslib": "^2.6.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=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, - "license": "MIT", + "node_modules/@smithy/util-body-length-node": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.0.0.tgz", + "integrity": "sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==", + "license": "Apache-2.0", "dependencies": { - "@jest/test-result": "^29.7.0", - "graceful-fs": "^4.2.9", - "jest-haste-map": "^29.7.0", - "slash": "^3.0.0" + "tslib": "^2.6.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=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, - "license": "MIT", + "node_modules/@smithy/util-buffer-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.0.0.tgz", + "integrity": "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==", + "license": "Apache-2.0", "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" + "@smithy/is-array-buffer": "^4.0.0", + "tslib": "^2.6.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=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, - "license": "MIT", + "node_modules/@smithy/util-config-provider": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.0.0.tgz", + "integrity": "sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==", + "license": "Apache-2.0", "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" + "tslib": "^2.6.2" }, "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + "node": ">=18.0.0" } }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.12", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", - "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", - "dev": true, - "license": "MIT", + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.0.26", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.26.tgz", + "integrity": "sha512-xgl75aHIS/3rrGp7iTxQAOELYeyiwBu+eEgAk4xfKwJJ0L8VUjhO2shsDpeil54BOFsqmk5xfdesiewbUY5tKQ==", + "license": "Apache-2.0", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" + "@smithy/property-provider": "^4.0.5", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.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, - "license": "MIT", + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.0.26", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.26.tgz", + "integrity": "sha512-z81yyIkGiLLYVDetKTUeCZQ8x20EEzvQjrqJtb/mXnevLq2+w3XCEWTJ2pMp401b6BkEkHVfXb/cROBpVauLMQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.1.5", + "@smithy/credential-provider-imds": "^4.0.7", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/property-provider": "^4.0.5", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, "engines": { - "node": ">=6.0.0" + "node": ">=18.0.0" } }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", - "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", - "dev": true, - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.29", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", - "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", - "dev": true, - "license": "MIT", + "node_modules/@smithy/util-endpoints": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.0.7.tgz", + "integrity": "sha512-klGBP+RpBp6V5JbrY2C/VKnHXn3d5V2YrifZbmMY8os7M6m8wdYFoO6w/fe5VkP+YVwrEktW3IWYaSQVNZJ8oQ==", + "license": "Apache-2.0", "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" + "@smithy/node-config-provider": "^4.1.4", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "dev": true, - "license": "MIT", + "node_modules/@smithy/util-hex-encoding": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.0.0.tgz", + "integrity": "sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==", + "license": "Apache-2.0", "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "tslib": "^2.6.2" }, "engines": { - "node": ">= 8" + "node": ">=18.0.0" } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true, - "license": "MIT", + "node_modules/@smithy/util-middleware": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.0.5.tgz", + "integrity": "sha512-N40PfqsZHRSsByGB81HhSo+uvMxEHT+9e255S53pfBw/wI6WKDI7Jw9oyu5tJTLwZzV5DsMha3ji8jk9dsHmQQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, "engines": { - "node": ">= 8" + "node": ">=18.0.0" } }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "dev": true, - "license": "MIT", + "node_modules/@smithy/util-retry": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.0.7.tgz", + "integrity": "sha512-TTO6rt0ppK70alZpkjwy+3nQlTiqNfoXja+qwuAchIEAIoSZW8Qyd76dvBv3I5bCpE38APafG23Y/u270NspiQ==", + "license": "Apache-2.0", "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "@smithy/service-error-classification": "^4.0.7", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" }, "engines": { - "node": ">= 8" + "node": ">=18.0.0" } }, - "node_modules/@sideway/address": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", - "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", - "license": "BSD-3-Clause", + "node_modules/@smithy/util-stream": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.2.4.tgz", + "integrity": "sha512-vSKnvNZX2BXzl0U2RgCLOwWaAP9x/ddd/XobPK02pCbzRm5s55M53uwb1rl/Ts7RXZvdJZerPkA+en2FDghLuQ==", + "license": "Apache-2.0", "dependencies": { - "@hapi/hoek": "^9.0.0" + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/types": "^4.3.2", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "node_modules/@sideway/formula": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", - "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", - "license": "BSD-3-Clause" - }, - "node_modules/@sideway/pinpoint": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", - "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", - "license": "BSD-3-Clause" - }, - "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, - "license": "MIT" - }, - "node_modules/@sinonjs/commons": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", - "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", - "dev": true, - "license": "BSD-3-Clause", + "node_modules/@smithy/util-uri-escape": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.0.0.tgz", + "integrity": "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==", + "license": "Apache-2.0", "dependencies": { - "type-detect": "4.0.8" + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, - "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, - "license": "BSD-3-Clause", + "node_modules/@smithy/util-utf8": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", + "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", + "license": "Apache-2.0", "dependencies": { - "@sinonjs/commons": "^3.0.0" + "@smithy/util-buffer-from": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-waiter": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.0.7.tgz", + "integrity": "sha512-mYqtQXPmrwvUljaHyGxYUIIRI3qjBTEb/f5QFi3A6VlxhpmZd5mWXn9W+qUkf2pVE1Hv3SqxefiZOPGdxmO64A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" } }, + "node_modules/@types/aws-lambda": { + "version": "8.10.152", + "resolved": "https://registry.npmjs.org/@types/aws-lambda/-/aws-lambda-8.10.152.tgz", + "integrity": "sha512-soT/c2gYBnT5ygwiHPmd9a1bftj462NWVk2tKCc1PYHSIacB2UwbTS2zYG4jzag1mRDuzg/OjtxQjQ2NKRB6Rw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -1261,14 +3009,25 @@ "@types/istanbul-lib-report": "*" } }, + "node_modules/@types/jest": { + "version": "29.5.14", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz", + "integrity": "sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^29.0.0", + "pretty-format": "^29.0.0" + } + }, "node_modules/@types/node": { - "version": "24.2.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz", - "integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==", + "version": "20.19.11", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.11.tgz", + "integrity": "sha512-uug3FEEGv0r+jrecvUUpbY8lLisvIjg6AAic6a2bSP5OEOLeJsDSnvhCDov7ipFFMXS3orMpzlmi0ZcuGkBbow==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.10.0" + "undici-types": "~6.21.0" } }, "node_modules/@types/stack-utils": { @@ -1278,6 +3037,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "license": "MIT" + }, "node_modules/@types/yargs": { "version": "17.0.33", "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", @@ -1418,43 +3183,6 @@ "dev": true, "license": "Python-2.0" }, - "node_modules/available-typed-arrays": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", - "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", - "license": "MIT", - "dependencies": { - "possible-typed-array-names": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/aws-sdk": { - "version": "2.1692.0", - "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.1692.0.tgz", - "integrity": "sha512-x511uiJ/57FIsbgUe5csJ13k3uzu25uWQE+XqfBis/sB0SFoiElJWXRkgEAUh0U6n40eT3ay5Ue4oPkRMu1LYw==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "buffer": "4.9.2", - "events": "1.1.1", - "ieee754": "1.1.13", - "jmespath": "0.16.0", - "querystring": "0.2.0", - "sax": "1.2.1", - "url": "0.10.3", - "util": "^0.12.4", - "uuid": "8.0.0", - "xml2js": "0.6.2" - }, - "engines": { - "node": ">= 10.0.0" - } - }, "node_modules/babel-jest": { "version": "29.7.0", "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", @@ -1511,6 +3239,16 @@ "node": ">=8" } }, + "node_modules/babel-plugin-istanbul/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, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "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", @@ -1578,24 +3316,10 @@ "dev": true, "license": "MIT" }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], + "node_modules/bowser": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.12.1.tgz", + "integrity": "sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw==", "license": "MIT" }, "node_modules/brace-expansion": { @@ -1623,9 +3347,9 @@ } }, "node_modules/browserslist": { - "version": "4.25.1", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", - "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "version": "4.25.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.3.tgz", + "integrity": "sha512-cDGv1kkDI4/0e5yON9yM5G/0A5u8sf5TnmdX5C9qHzI9PPu++sQ9zjm1k9NiOrf3riY4OkK0zSGqfvJyJsgCBQ==", "dev": true, "funding": [ { @@ -1643,8 +3367,8 @@ ], "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001726", - "electron-to-chromium": "^1.5.173", + "caniuse-lite": "^1.0.30001735", + "electron-to-chromium": "^1.5.204", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, @@ -1665,17 +3389,6 @@ "node-int64": "^0.4.0" } }, - "node_modules/buffer": { - "version": "4.9.2", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", - "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", - "license": "MIT", - "dependencies": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4", - "isarray": "^1.0.0" - } - }, "node_modules/buffer-from": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", @@ -1683,53 +3396,6 @@ "dev": true, "license": "MIT" }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/callsites": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", @@ -1751,9 +3417,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001733", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001733.tgz", - "integrity": "sha512-e4QKw/O2Kavj2VQTKZWrwzkt3IxOmIlU6ajRb6LP64LHpBo1J67k2Hi4Vu/TgJWsNtynurfS0uK3MaUTCPfu5Q==", + "version": "1.0.30001737", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001737.tgz", + "integrity": "sha512-BiloLiXtQNrY5UyF0+1nSJLXUENuhka2pzy2Fx5pGxqavdrxSCW4U6Pn/PoG3Efspi2frRbHpBV2XsrPE6EDlw==", "dev": true, "funding": [ { @@ -1975,23 +3641,6 @@ "node": ">=0.10.0" } }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/detect-newline": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", @@ -2025,24 +3674,10 @@ "node": ">=6.0.0" } }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/electron-to-chromium": { - "version": "1.5.199", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.199.tgz", - "integrity": "sha512-3gl0S7zQd88kCAZRO/DnxtBKuhMO4h0EaQIN3YgZfV6+pW+5+bf2AdQeHNESCoaQqo/gjGVYEf2YM4O5HJQqpQ==", + "version": "1.5.209", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.209.tgz", + "integrity": "sha512-Xoz0uMrim9ZETCQt8UgM5FxQF9+imA7PBpokoGcZloA1uw2LeHzTlip5cb5KOAsXZLjh/moN2vReN3ZjJmjI9A==", "dev": true, "license": "ISC" }, @@ -2076,36 +3711,6 @@ "is-arrayish": "^0.2.1" } }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -2294,15 +3899,6 @@ "node": ">=0.10.0" } }, - "node_modules/events": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/events/-/events-1.1.1.tgz", - "integrity": "sha512-kEcvvCBByWXGnZy6JUlgAp2gBIUjfCAV6P6TgT1/aaQKcmuAEC4OZTV1I4EWQLz2gxZw76atuVyvHhTxvi0Flw==", - "license": "MIT", - "engines": { - "node": ">=0.4.x" - } - }, "node_modules/execa": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", @@ -2374,6 +3970,24 @@ "dev": true, "license": "MIT" }, + "node_modules/fast-xml-parser": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", + "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^2.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, "node_modules/fastq": { "version": "1.19.1", "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", @@ -2459,21 +4073,6 @@ "dev": true, "license": "ISC" }, - "node_modules/for-each": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", - "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", - "license": "MIT", - "dependencies": { - "is-callable": "^1.2.7" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -2500,6 +4099,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -2525,30 +4125,6 @@ "node": "6.* || 8.* || >= 10.*" } }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/get-package-type": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", @@ -2559,19 +4135,6 @@ "node": ">=8.0.0" } }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, "node_modules/get-stream": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", @@ -2636,18 +4199,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/graceful-fs": { "version": "4.2.11", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", @@ -2672,49 +4223,11 @@ "node": ">=8" } }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-tostringtag": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", - "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", - "license": "MIT", - "dependencies": { - "has-symbols": "^1.0.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -2740,12 +4253,6 @@ "node": ">=10.17.0" } }, - "node_modules/ieee754": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.1.13.tgz", - "integrity": "sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg==", - "license": "BSD-3-Clause" - }, "node_modules/ignore": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", @@ -2819,42 +4326,15 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, "license": "ISC" }, - "node_modules/is-arguments": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", - "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "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, - "license": "MIT" - }, - "node_modules/is-callable": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", - "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true, + "license": "MIT" }, "node_modules/is-core-module": { "version": "2.16.1", @@ -2902,24 +4382,6 @@ "node": ">=6" } }, - "node_modules/is-generator-function": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", - "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.3", - "get-proto": "^1.0.0", - "has-tostringtag": "^1.0.2", - "safe-regex-test": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-glob": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", @@ -2953,24 +4415,6 @@ "node": ">=8" } }, - "node_modules/is-regex": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", - "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2", - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-stream": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", @@ -2984,27 +4428,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-typed-array": { - "version": "1.1.15", - "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", - "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", - "license": "MIT", - "dependencies": { - "which-typed-array": "^1.1.16" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" - }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -3039,19 +4462,6 @@ "node": ">=10" } }, - "node_modules/istanbul-lib-instrument/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "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", @@ -3083,9 +4493,9 @@ } }, "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==", + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", "dev": true, "license": "BSD-3-Clause", "dependencies": { @@ -3578,19 +4988,6 @@ "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "node_modules/jest-snapshot/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "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", @@ -3692,28 +5089,6 @@ "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "node_modules/jmespath": { - "version": "0.16.0", - "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.16.0.tgz", - "integrity": "sha512-9FzQjJ7MATs1tSpnco1K6ayiYE3figslrXA72G2HQ/n76RzvYlofyi5QM+iX4YRs/pu3yzxlVQSST23+dMDknw==", - "license": "Apache-2.0", - "engines": { - "node": ">= 0.6.0" - } - }, - "node_modules/joi": { - "version": "17.13.3", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", - "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/hoek": "^9.3.0", - "@hapi/topo": "^5.1.0", - "@sideway/address": "^4.1.5", - "@sideway/formula": "^3.0.1", - "@sideway/pinpoint": "^2.0.0" - } - }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -3888,19 +5263,6 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/make-dir/node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "dev": true, - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "node_modules/makeerror": { "version": "1.0.12", "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", @@ -3911,15 +5273,6 @@ "tmpl": "1.0.5" } }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, "node_modules/merge-stream": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", @@ -3964,6 +5317,15 @@ "node": "*" } }, + "node_modules/mnemonist": { + "version": "0.38.3", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.38.3.tgz", + "integrity": "sha512-2K9QYubXx/NAjv4VLq1d1Ly8pWNC5L3BrixtdkyTegXWJIqY+zLNDhhX/A+ZwWt70tB1S8H4BE8FLYEFyNoOBw==", + "license": "MIT", + "dependencies": { + "obliterator": "^1.6.1" + } + }, "node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -4015,6 +5377,12 @@ "node": ">=8" } }, + "node_modules/obliterator": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-1.6.1.tgz", + "integrity": "sha512-9WXswnqINnnhOG/5SLimUlzuU1hFJUc8zkwyD59Sd+dPOMf05PmnYG/d6Q7HZ+KmgkZJa1PxRso6QdM3sTNHig==", + "license": "MIT" + }, "node_modules/once": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", @@ -4170,95 +5538,6 @@ "dev": true, "license": "MIT" }, - "node_modules/pg": { - "version": "8.16.3", - "resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz", - "integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==", - "license": "MIT", - "dependencies": { - "pg-connection-string": "^2.9.1", - "pg-pool": "^3.10.1", - "pg-protocol": "^1.10.3", - "pg-types": "2.2.0", - "pgpass": "1.0.5" - }, - "engines": { - "node": ">= 16.0.0" - }, - "optionalDependencies": { - "pg-cloudflare": "^1.2.7" - }, - "peerDependencies": { - "pg-native": ">=3.0.1" - }, - "peerDependenciesMeta": { - "pg-native": { - "optional": true - } - } - }, - "node_modules/pg-cloudflare": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz", - "integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==", - "license": "MIT", - "optional": true - }, - "node_modules/pg-connection-string": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz", - "integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==", - "license": "MIT" - }, - "node_modules/pg-int8": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", - "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", - "license": "ISC", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/pg-pool": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz", - "integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==", - "license": "MIT", - "peerDependencies": { - "pg": ">=8.0" - } - }, - "node_modules/pg-protocol": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz", - "integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==", - "license": "MIT" - }, - "node_modules/pg-types": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", - "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", - "license": "MIT", - "dependencies": { - "pg-int8": "1.0.1", - "postgres-array": "~2.0.0", - "postgres-bytea": "~1.0.0", - "postgres-date": "~1.0.4", - "postgres-interval": "^1.1.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/pgpass": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", - "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", - "license": "MIT", - "dependencies": { - "split2": "^4.1.0" - } - }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -4358,54 +5637,6 @@ "node": ">=8" } }, - "node_modules/possible-typed-array-names": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", - "integrity": "sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/postgres-array": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", - "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/postgres-bytea": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", - "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-date": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", - "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/postgres-interval": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", - "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", - "license": "MIT", - "dependencies": { - "xtend": "^4.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -4501,15 +5732,6 @@ ], "license": "MIT" }, - "node_modules/querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha512-X/xY82scca2tau62i9mDyU9K+I+djTMUsvwf7xnUX5GLvVzgJybOJf4Y6o9Zx3oJK/LSXg5tTZBjwzqVPaPO2g==", - "deprecated": "The querystring API is considered Legacy. new code should use the URLSearchParams API instead.", - "engines": { - "node": ">=0.4.x" - } - }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -4664,54 +5886,17 @@ "queue-microtask": "^1.2.2" } }, - "node_modules/safe-regex-test": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", - "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "is-regex": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/sax": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.1.tgz", - "integrity": "sha512-8I2a3LovHTOpm7NV5yOyO8IHqgVsfK4+UuySrXU8YXkSRX7k6hCV9b3HrkKCr3nMpgj+0bmocaJJWpvp1oc7ZA==", - "license": "ISC" - }, "node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" - } - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" }, "engines": { - "node": ">= 0.4" + "node": ">=10" } }, "node_modules/shebang-command": { @@ -4782,15 +5967,6 @@ "source-map": "^0.6.0" } }, - "node_modules/split2": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", - "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", - "license": "ISC", - "engines": { - "node": ">= 10.x" - } - }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -4896,6 +6072,18 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/strnum": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", + "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -4964,6 +6152,12 @@ "node": ">=8.0" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -5001,9 +6195,9 @@ } }, "node_modules/undici-types": { - "version": "7.10.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", - "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", "dev": true, "license": "MIT" }, @@ -5048,39 +6242,14 @@ "punycode": "^2.1.0" } }, - "node_modules/url": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/url/-/url-0.10.3.tgz", - "integrity": "sha512-hzSUW2q06EqL1gKM/a+obYHLIO6ct2hwPuviqTTOcfFVc61UbfJ2Q32+uGL/HCPxKqrdGB5QUwIe7UqlDgwsOQ==", - "license": "MIT", - "dependencies": { - "punycode": "1.3.2", - "querystring": "0.2.0" - } - }, - "node_modules/url/node_modules/punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha512-RofWgt/7fL5wP1Y7fxE7/EmTLzQVnB0ycyibJ0OOHIlJqTNzglYFxVwETOcIoJqJmpDXJ9xImDv+Fq34F/d4Dw==", - "license": "MIT" - }, - "node_modules/util": { - "version": "0.12.5", - "resolved": "https://registry.npmjs.org/util/-/util-0.12.5.tgz", - "integrity": "sha512-kZf/K6hEIrWHI6XqOFUiiMa+79wE/D8Q+NCNAWclkyg3b4d2k7s0QGepNjiABc+aR3N1PAyHL7p6UcLY6LmrnA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "is-arguments": "^1.0.4", - "is-generator-function": "^1.0.7", - "is-typed-array": "^1.1.3", - "which-typed-array": "^1.1.2" - } - }, "node_modules/uuid": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.0.0.tgz", - "integrity": "sha512-jOXGuXZAWdsTH7eZLtyXMqUb9EcWMGZNbL9YcGBJl4MH4nrxHmZJhEHvyLFrkxo+28uLb/NYRcStH48fnD0Vzw==", + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], "license": "MIT", "bin": { "uuid": "dist/bin/uuid" @@ -5127,27 +6296,6 @@ "node": ">= 8" } }, - "node_modules/which-typed-array": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", - "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", - "license": "MIT", - "dependencies": { - "available-typed-arrays": "^1.0.7", - "call-bind": "^1.0.8", - "call-bound": "^1.0.4", - "for-each": "^0.3.5", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", @@ -5197,37 +6345,6 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "node_modules/xml2js": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/xml2js/-/xml2js-0.6.2.tgz", - "integrity": "sha512-T4rieHaC1EXcES0Kxxj4JWgaUQHDk+qwHcYOCFHfiwKz7tOVPLq7Hjq9dM1WCMhylqMEfP7hMcOIChvotiZegA==", - "license": "MIT", - "dependencies": { - "sax": ">=0.6.0", - "xmlbuilder": "~11.0.0" - }, - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/xmlbuilder": { - "version": "11.0.1", - "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-11.0.1.tgz", - "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==", - "license": "MIT", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/xtend": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", - "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "license": "MIT", - "engines": { - "node": ">=0.4" - } - }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/src/applications/lambda/petfood-cleanup-processor-node/package.json b/src/applications/lambda/petfood-cleanup-processor-node/package.json new file mode 100644 index 00000000..08da4a36 --- /dev/null +++ b/src/applications/lambda/petfood-cleanup-processor-node/package.json @@ -0,0 +1,38 @@ +{ + "name": "petfood-cleanup-processor", + "version": "1.0.0", + "description": "Lambda function to process ItemDiscontinued events and cleanup associated resources", + "main": "index.js", + "scripts": { + "test": "jest", + "lint": "eslint .", + "format": "prettier --write .", + "deploy": "./deploy.sh" + }, + "dependencies": { + "@aws-sdk/client-s3": "^3.490.0", + "@aws-sdk/client-eventbridge": "^3.490.0", + "@aws-sdk/client-dynamodb": "^3.490.0", + "@aws-sdk/lib-dynamodb": "^3.490.0" + }, + "devDependencies": { + "@types/aws-lambda": "^8.10.130", + "@types/jest": "^29.5.8", + "@types/node": "^20.9.0", + "eslint": "^8.54.0", + "jest": "^29.7.0", + "prettier": "^3.1.0" + }, + "engines": { + "node": ">=18.0.0" + }, + "keywords": [ + "aws", + "lambda", + "eventbridge", + "cleanup", + "observability" + ], + "author": "One Observability Demo", + "license": "MIT" +} \ No newline at end of file diff --git a/src/applications/lambda/petfood-cleanup-processor-node/test/index.test.js b/src/applications/lambda/petfood-cleanup-processor-node/test/index.test.js new file mode 100644 index 00000000..91c9ee25 --- /dev/null +++ b/src/applications/lambda/petfood-cleanup-processor-node/test/index.test.js @@ -0,0 +1,326 @@ +// Mock AWS SDK clients BEFORE importing the handler +const mockS3Send = jest.fn(); +const mockDynamoSend = jest.fn(); + +jest.mock('@aws-sdk/client-s3', () => ({ + S3Client: jest.fn().mockImplementation(() => ({ + send: mockS3Send + })), + DeleteObjectCommand: jest.fn(), + HeadObjectCommand: jest.fn() +})); + +jest.mock('@aws-sdk/client-dynamodb', () => ({ + DynamoDBClient: jest.fn().mockImplementation(() => ({})) +})); + +jest.mock('@aws-sdk/lib-dynamodb', () => ({ + DynamoDBDocumentClient: { + from: jest.fn().mockReturnValue({ + send: mockDynamoSend + }) + }, + DeleteCommand: jest.fn() +})); + +const { handler } = require('../index'); +const { DeleteObjectCommand, HeadObjectCommand } = require('@aws-sdk/client-s3'); +const { DeleteCommand } = require('@aws-sdk/lib-dynamodb'); + +const createTestEvent = (overrides = {}) => ({ + version: '0', + id: 'test-event-id', + 'detail-type': 'ItemDiscontinued', + source: 'petfood.service', + account: '123456789012', + time: '2024-01-01T00:00:00Z', + region: 'us-east-1', + detail: { + event_type: 'ItemDiscontinued', + food_id: 'test-food-123', + food_name: undefined, + pet_type: undefined, + food_type: undefined, + description: undefined, + ingredients: undefined, + status: 'discontinued', + metadata: { + cleanup_type: 'soft_delete', + image_path: 'premium-puppy-chow.jpg', + reason: 'cleanup_operation' + }, + span_context: { + trace_id: 'c5fb47bbf77e841964c5d52abfcd2c27', + span_id: 'fa08a19935988e28', + trace_flags: '01' + }, + ...overrides + } +}); + +const createLambdaContext = () => ({ + requestId: 'test-request-id', + functionName: 'test-function', + functionVersion: '1', + getRemainingTimeInMillis: () => 30_000 +}); + +describe('Petfood Cleanup Processor Lambda', () => { + beforeEach(() => { + // Reset all mocks + jest.clearAllMocks(); + mockS3Send.mockReset(); + mockDynamoSend.mockReset(); + + // Set environment variables + process.env.S3_BUCKET_NAME = 'test-bucket'; + process.env.DYNAMODB_TABLE_NAME = 'test-table'; + process.env.AWS_REGION = 'us-east-1'; + }); + + describe('Successful cleanup processing', () => { + test('should process ItemDiscontinued event successfully with image deletion', async () => { + // Setup mocks for successful execution + mockS3Send + .mockResolvedValueOnce({}) // HeadObject - image exists + .mockResolvedValueOnce({}); // DeleteObject - successful deletion + + mockDynamoSend.mockResolvedValueOnce({ + Attributes: { id: 'test-food-123', status: 'discontinued' } + }); + + const event = createTestEvent(); + const context = createLambdaContext(); + + const result = await handler(event, context); + + expect(result.statusCode).toBe(200); + + const responseBody = JSON.parse(result.body); + expect(responseBody.message).toBe('Cleanup processing completed successfully'); + expect(responseBody.foodId).toBe('test-food-123'); + expect(responseBody.cleanupSummary.s3ImageDeleted).toBe(true); + expect(responseBody.cleanupSummary.databaseRecordDeleted).toBe(true); + + // Verify S3 calls + expect(mockS3Send).toHaveBeenCalledTimes(2); + expect(mockS3Send).toHaveBeenNthCalledWith(1, expect.any(HeadObjectCommand)); + expect(mockS3Send).toHaveBeenNthCalledWith(2, expect.any(DeleteObjectCommand)); + + // Verify DynamoDB call + expect(mockDynamoSend).toHaveBeenCalledTimes(1); + expect(mockDynamoSend).toHaveBeenCalledWith(expect.any(DeleteCommand)); + }); + + test('should process event successfully when image does not exist', async () => { + // Setup mocks - image doesn't exist + const notFoundError = new Error('Not Found'); + notFoundError.name = 'NotFound'; + notFoundError.$metadata = { httpStatusCode: 404 }; + + mockS3Send.mockRejectedValueOnce(notFoundError); // HeadObject - image doesn't exist + + mockDynamoSend.mockResolvedValueOnce({ + Attributes: { id: 'test-food-123', status: 'discontinued' } + }); + + const event = createTestEvent(); + const context = createLambdaContext(); + + const result = await handler(event, context); + + expect(result.statusCode).toBe(200); + + const responseBody = JSON.parse(result.body); + expect(responseBody.cleanupSummary.s3ImageDeleted).toBe(false); + expect(responseBody.cleanupSummary.databaseRecordDeleted).toBe(true); + + // Verify only HeadObject was called, not DeleteObject + expect(mockS3Send).toHaveBeenCalledTimes(1); + expect(mockS3Send).toHaveBeenCalledWith(expect.any(HeadObjectCommand)); + }); + + test('should process event successfully without image path', async () => { + // Setup event without image path + const eventWithoutImage = createTestEvent(); + delete eventWithoutImage.detail.metadata.image_path; + + mockDynamoSend.mockResolvedValueOnce({ + Attributes: { id: 'test-food-123', status: 'discontinued' } + }); + + const context = createLambdaContext(); + + const result = await handler(eventWithoutImage, context); + + expect(result.statusCode).toBe(200); + + const responseBody = JSON.parse(result.body); + expect(responseBody.cleanupSummary.s3ImageDeleted).toBe(false); + expect(responseBody.cleanupSummary.databaseRecordDeleted).toBe(true); + + // Verify no S3 calls were made + expect(mockS3Send).not.toHaveBeenCalled(); + }); + + test('should handle alternative event format with eventDetail', async () => { + // Test the alternative event format we discovered in the logs + const alternativeEvent = { + source: 'petfood.service', + 'detail-type': 'ItemDiscontinued', + eventDetail: { + event_type: 'ItemDiscontinued', + food_id: 'F689d8cdb', + food_name: undefined, + pet_type: undefined, + food_type: undefined, + description: undefined, + ingredients: undefined, + status: 'discontinued', + metadata: { + cleanup_type: 'soft_delete', + image_path: 'premium-puppy-chow.jpg', + reason: 'cleanup_operation' + }, + span_context: { + trace_id: 'c5fb47bbf77e841964c5d52abfcd2c27', + span_id: 'fa08a19935988e28', + trace_flags: '01' + } + } + }; + + mockS3Send + .mockResolvedValueOnce({}) // HeadObject - image exists + .mockResolvedValueOnce({}); // DeleteObject - successful deletion + + mockDynamoSend.mockResolvedValueOnce({ + Attributes: { id: 'F689d8cdb', status: 'discontinued' } + }); + + const context = createLambdaContext(); + + const result = await handler(alternativeEvent, context); + + expect(result.statusCode).toBe(200); + + const responseBody = JSON.parse(result.body); + expect(responseBody.message).toBe('Cleanup processing completed successfully'); + expect(responseBody.foodId).toBe('F689d8cdb'); + expect(responseBody.cleanupSummary.s3ImageDeleted).toBe(true); + expect(responseBody.cleanupSummary.databaseRecordDeleted).toBe(true); + }); + }); + + describe('Error handling', () => { + test('should handle invalid event structure', async () => { + const invalidEvent = { + detail: {} // Missing required fields + }; + const context = createLambdaContext(); + + await expect(handler(invalidEvent, context)).rejects.toThrow( + 'Invalid event structure: missing required fields' + ); + }); + + test('should handle S3 deletion failure', async () => { + // Setup mocks - image exists but deletion fails all retry attempts + mockS3Send + .mockResolvedValueOnce({}) // HeadObject - image exists + .mockRejectedValue(new Error('S3 deletion failed')); // DeleteObject fails all retries + + const event = createTestEvent(); + const context = createLambdaContext(); + + await expect(handler(event, context)).rejects.toThrow('S3 deletion failed'); + + // Should have called S3 4 times (1 HeadObject + 3 DeleteObject retry attempts) + expect(mockS3Send).toHaveBeenCalledTimes(4); + }); + + test('should handle DynamoDB delete failure', async () => { + // Setup mocks - S3 succeeds but DynamoDB fails all retry attempts + mockS3Send + .mockResolvedValueOnce({}) // HeadObject - image exists + .mockResolvedValueOnce({}); // DeleteObject - successful + + mockDynamoSend.mockRejectedValue(new Error('DynamoDB delete failed')); + + const event = createTestEvent(); + const context = createLambdaContext(); + + await expect(handler(event, context)).rejects.toThrow('DynamoDB delete failed'); + }); + }); + + describe('Retry logic', () => { + test('should retry failed operations with exponential backoff', async () => { + // Setup mocks - fail twice, then succeed + mockS3Send + .mockResolvedValueOnce({}) // HeadObject - image exists + .mockRejectedValueOnce(new Error('Temporary failure')) + .mockRejectedValueOnce(new Error('Temporary failure')) + .mockResolvedValueOnce({}); // DeleteObject - finally succeeds + + mockDynamoSend.mockResolvedValueOnce({ + Attributes: { id: 'test-food-123', status: 'discontinued' } + }); + + const event = createTestEvent(); + const context = createLambdaContext(); + + const result = await handler(event, context); + + expect(result.statusCode).toBe(200); + + // Should have called S3 4 times (1 HeadObject + 3 DeleteObject attempts) + expect(mockS3Send).toHaveBeenCalledTimes(4); + }); + + test('should fail after max retries exceeded', async () => { + // Setup mocks - always fail + mockS3Send + .mockResolvedValueOnce({}) // HeadObject - image exists + .mockRejectedValue(new Error('Persistent failure')); // DeleteObject always fails + + const event = createTestEvent(); + const context = createLambdaContext(); + + await expect(handler(event, context)).rejects.toThrow('Persistent failure'); + + // Should have called S3 4 times (1 HeadObject + 3 DeleteObject attempts) + expect(mockS3Send).toHaveBeenCalledTimes(4); + }); + }); + + describe('Configuration', () => { + test('should use environment variables for configuration', async () => { + // Set custom environment variables + process.env.S3_BUCKET_NAME = 'custom-bucket'; + process.env.DYNAMODB_TABLE_NAME = 'custom-table'; + process.env.MAX_RETRIES = '5'; + + mockS3Send + .mockResolvedValueOnce({}) // HeadObject + .mockResolvedValueOnce({}); // DeleteObject + mockDynamoSend.mockResolvedValue({ + Attributes: { id: 'test-food-123', status: 'discontinued' } + }); + + const event = createTestEvent(); + const context = createLambdaContext(); + + await handler(event, context); + + // Verify calls were made (the actual command construction is handled by AWS SDK) + expect(mockS3Send).toHaveBeenCalledTimes(2); + expect(mockDynamoSend).toHaveBeenCalledTimes(1); + + // Verify the commands were constructed with the right constructors + expect(HeadObjectCommand).toHaveBeenCalled(); + expect(DeleteObjectCommand).toHaveBeenCalled(); + expect(DeleteCommand).toHaveBeenCalled(); + }); + }); +}); diff --git a/src/applications/lambda/petfood-cleanup-processor-node/test/setup.js b/src/applications/lambda/petfood-cleanup-processor-node/test/setup.js new file mode 100644 index 00000000..892139c1 --- /dev/null +++ b/src/applications/lambda/petfood-cleanup-processor-node/test/setup.js @@ -0,0 +1,22 @@ +// Jest setup file for global test configuration + +// Set default environment variables for tests +process.env.AWS_REGION = 'us-east-1'; +process.env.S3_BUCKET_NAME = 'test-bucket'; +process.env.EVENT_BUS_NAME = 'test-event-bus'; +process.env.DYNAMODB_TABLE_NAME = 'test-table'; +process.env.EVENT_SOURCE_NAME = 'test.cleanup.service'; +process.env.MAX_RETRIES = '3'; +process.env.RETRY_DELAY_MS = '100'; // Faster retries for tests + +// Mock console methods to reduce test output noise +globalThis.console = { + ...console, + log: jest.fn(), + warn: jest.fn(), + error: jest.fn(), + info: jest.fn() +}; + +// Global test timeout +jest.setTimeout(10_000); diff --git a/src/applications/lambda/petfood-image-generator-python/README.md b/src/applications/lambda/petfood-image-generator-python/README.md new file mode 100644 index 00000000..be514ae2 --- /dev/null +++ b/src/applications/lambda/petfood-image-generator-python/README.md @@ -0,0 +1,110 @@ +# Pet Food Image Generator Lambda + +This Lambda function autonomously generates pet food images using Amazon Bedrock when processing EventBridge events from the petfood service. + +## Architecture + +The function processes EventBridge events from the petfood service to: + +1. **Extract and validate** event data (food details, metadata) +2. **Check existing images** in S3 to avoid duplicates +3. **Generate descriptive prompts** based on food attributes +4. **Validate prompt length** for Bedrock compatibility +5. **Generate images** using Amazon Bedrock Titan Image Generator +6. **Store images** in S3 with proper metadata +7. **Update DynamoDB** records with image URLs + +## Event Structure + +The function processes `FoodItemCreated` and `FoodItemUpdated` events: + +```json +{ + "detail": { + "event_type": "FoodItemCreated", + "food_id": "F32f05d95", + "food_name": "Testing events", + "pet_type": "puppy", + "food_type": "dry", + "description": "High-quality dry food for puppies", + "ingredients": ["chicken", "rice", "vegetables", "vitamins"], + "status": null, + "metadata": { + "image_required": "true" + } + } +} +``` + +## Core Functions + +The function includes several core processing functions: + +- **generate_prompt()**: Creates descriptive prompts from food attributes +- **generate_image_with_bedrock()**: Generates images using Bedrock +- **store_image_in_s3()**: Manages S3 image storage +- **update_food_record()**: Updates DynamoDB records + +## Deployment + +### Prerequisites + +- AWS CLI configured +- SAM CLI installed +- Python 3.13 +- Proper AWS permissions for Bedrock, S3, DynamoDB + +### Deploy + +```bash +# Make deploy script executable +chmod +x deploy.sh + +# Deploy to dev environment +./deploy.sh dev + +# Deploy to production +./deploy.sh prod +``` + +### Test Locally + +```bash +# Test Strands integration +python test_strands.py + +# Test with SAM local (requires Docker) +sam local invoke PetFoodImageGeneratorFunction -e test_event.json +``` + +## Configuration + +Environment variables: + +- `FOOD_TABLE_NAME`: DynamoDB table for food items +- `S3_BUCKET_NAME`: S3 bucket for image storage +- `BEDROCK_MODEL_ID`: Bedrock model for image generation +- `LAMBDA_LOG_LEVEL`: Lambda logging level + +## Monitoring + +The function includes: + +- **CloudWatch Alarms** for errors and duration +- **X-Ray Tracing** for distributed tracing +- **Application Signals** for observability +- **Dead Letter Queue** for failed events + +## Dependencies + +- `boto3`: AWS SDK (included in Lambda runtime) +- `Pillow`: Image processing library + +## Troubleshooting + +1. **Import errors**: Ensure `requirements.txt` includes all dependencies +2. **Bedrock permissions**: Verify IAM permissions for model access +3. **S3 access**: Check bucket permissions and names +4. **DynamoDB**: Verify table name and permissions + +The function now uses a simplified approach without external agent frameworks for better reliability and faster cold starts. \ No newline at end of file diff --git a/src/applications/lambda/petfood-image-generator-python/lambda_function.py b/src/applications/lambda/petfood-image-generator-python/lambda_function.py new file mode 100644 index 00000000..24736def --- /dev/null +++ b/src/applications/lambda/petfood-image-generator-python/lambda_function.py @@ -0,0 +1,898 @@ +""" +Strands Agent Lambda function for automated pet food image generation. +Processes EventBridge events to generate images using Amazon Bedrock. +""" + +import base64 +import json +import logging +import os +import random +import time +from typing import Any +from typing import Dict + +import boto3 +from botocore.exceptions import ClientError + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# Initialize AWS clients +s3_client = boto3.client("s3") +bedrock_client = boto3.client("bedrock-runtime") +dynamodb = boto3.resource("dynamodb") + +# Environment variables +FOOD_TABLE_NAME = os.environ.get("FOOD_TABLE_NAME") +S3_BUCKET_NAME = os.environ.get("S3_BUCKET_NAME") +BEDROCK_MODEL_ID = os.environ.get( + "BEDROCK_MODEL_ID", + "amazon.titan-image-generator-v2:0", +) + +# Retry configuration +MAX_RETRIES = 5 +BASE_DELAY = 1.0 # Base delay in seconds +MAX_DELAY = 60.0 # Maximum delay in seconds +JITTER_RANGE = 0.1 # Random jitter to avoid thundering herd + + +def generate_prompt(food_data: Dict[str, Any]) -> str: + """Generate a sophisticated, contextually rich prompt for image generation.""" + try: + # Extract all available data fields + food_name = food_data.get("food_name", "") + pet_type = food_data.get("pet_type", "") + food_type = food_data.get("food_type", "") + description = food_data.get("description", "") + ingredients = food_data.get("ingredients", []) + nutritional_info = food_data.get("nutritional_info", {}) + feeding_guidelines = food_data.get("feeding_guidelines", "") + price = food_data.get("price", 0) + + # Enhanced prompt generation with contextual intelligence + prompt_builder = EnhancedPromptBuilder( + food_name, + pet_type, + food_type, + description, + ingredients, + nutritional_info, + feeding_guidelines, + price, + ) + + full_prompt = prompt_builder.build() + + # Validate length (Bedrock limit is 512 characters) + if len(full_prompt) > 512: + full_prompt = _intelligent_truncate(full_prompt, 512) + + logger.info(f"Generated enhanced prompt: {full_prompt}") + return full_prompt + + except Exception as e: + logger.error(f"Error generating prompt: {str(e)}") + return ( + "A bowl of pet food, professional product photography, " + "clean white background" + ) + + +class EnhancedPromptBuilder: + """Advanced prompt builder with contextual intelligence.""" + + def __init__( + self, + food_name: str, + pet_type: str, + food_type: str, + description: str, + ingredients: list, + nutritional_info: dict, + feeding_guidelines: str, + price: float, + ): + self.food_name = food_name + self.pet_type = pet_type.lower() if pet_type else "" + self.food_type = food_type.lower() if food_type else "" + self.description = description.lower() if description else "" + self.ingredients = [ing.lower() for ing in ingredients] if ingredients else [] + self.nutritional_info = nutritional_info or {} + self.feeding_guidelines = ( + feeding_guidelines.lower() if feeding_guidelines else "" + ) + self.price = float(price) if price else 0.0 + + # Contextual mappings for intelligent prompt generation + self.pet_contexts = { + "puppy": { + "bowl_style": "small ceramic puppy bowl", + "food_characteristics": "small, bite-sized pieces", + "presentation": "playful, nurturing presentation", + "colors": "warm, inviting colors", + }, + "kitten": { + "bowl_style": "elegant small ceramic bowl", + "food_characteristics": "fine, delicate pieces", + "presentation": "refined, gentle presentation", + "colors": "soft, sophisticated colors", + }, + "bunny": { + "bowl_style": "natural wooden bowl", + "food_characteristics": "natural, wholesome pellets and pieces", + "presentation": "organic, rustic presentation", + "colors": "earthy, natural tones", + }, + } + + self.food_type_contexts = { + "dry": { + "texture": "crispy, crunchy kibble", + "appearance": "individual pieces clearly visible", + "lighting": "bright, clean lighting to show texture", + }, + "wet": { + "texture": "moist, tender chunks in sauce", + "appearance": "rich, glossy appearance with visible meat", + "lighting": "warm lighting to enhance richness", + }, + "treats": { + "texture": "appealing, bite-sized treats", + "appearance": "scattered artfully around bowl", + "lighting": "bright, inviting lighting", + }, + "raw": { + "texture": "fresh, natural meat pieces", + "appearance": "premium, restaurant-quality presentation", + "lighting": "natural lighting to show freshness", + }, + } + + self.ingredient_visuals = { + # Proteins + "chicken": "golden-brown meat pieces", + "beef": "rich, dark meat chunks", + "salmon": "pink, flaky fish pieces", + "tuna": "light pink fish flakes", + "turkey": "light brown, tender meat", + "lamb": "reddish-brown meat pieces", + "duck": "rich, dark meat with natural oils", + # Vegetables + "carrot": "bright orange carrot pieces", + "sweet potato": "orange sweet potato chunks", + "peas": "vibrant green pea pieces", + "spinach": "dark green leafy flecks", + "broccoli": "small green broccoli pieces", + "pumpkin": "orange pumpkin chunks", + # Grains + "rice": "white rice grains", + "oats": "golden oat flakes", + "barley": "light brown barley grains", + "quinoa": "small, round quinoa seeds", + # Special ingredients + "blueberry": "dark blue berry pieces", + "cranberry": "red berry pieces", + "apple": "light fruit pieces", + "herbs": "green herb flecks", + "catnip": "light green catnip tint", + } + + self.premium_indicators = [ + "premium", + "gourmet", + "organic", + "natural", + "grain-free", + "free-range", + "wild-caught", + "artisan", + "holistic", + ] + + def build(self) -> str: + """Build a sophisticated, contextually aware prompt.""" + components = [] + + # 1. Determine presentation context + pet_context = self._get_pet_context() + food_context = self._get_food_type_context() + + # 2. Build main subject with intelligent bowl selection + bowl_description = pet_context.get("bowl_style", "ceramic bowl") + components.append(f"Premium pet food in {bowl_description}") + + # 3. Add food characteristics based on type and ingredients + food_characteristics = self._build_food_characteristics( + pet_context, + food_context, + ) + if food_characteristics: + components.append(food_characteristics) + + # 4. Add ingredient-based visual elements + ingredient_visuals = self._extract_ingredient_visuals() + if ingredient_visuals: + components.append(ingredient_visuals) + + # 5. Determine premium level and adjust styling + is_premium = self._detect_premium_product() + styling = self._get_styling_context(is_premium, pet_context, food_context) + + # 6. Combine all components + base_prompt = ". ".join(components) + full_prompt = f"{base_prompt}. {styling}" + + return full_prompt + + def _get_pet_context(self) -> dict: + """Get contextual information based on pet type.""" + # Direct mapping for exact matches + if self.pet_type in self.pet_contexts: + return self.pet_contexts[self.pet_type] + + # Fuzzy matching for partial matches + for pet_key in self.pet_contexts: + if pet_key in self.pet_type or self.pet_type in pet_key: + return self.pet_contexts[pet_key] + + # Special cases + if "rabbit" in self.pet_type: + return self.pet_contexts["bunny"] + + return self.pet_contexts.get("dog", {}) # Default fallback + + def _get_food_type_context(self) -> dict: + """Get contextual information based on food type.""" + for food_key in self.food_type_contexts: + if food_key in self.food_type: + return self.food_type_contexts[food_key] + return self.food_type_contexts.get("dry", {}) # Default fallback + + def _build_food_characteristics(self, pet_context: dict, food_context: dict) -> str: + """Build food characteristics description.""" + characteristics = [] + + # Add food type specific texture + texture = food_context.get("texture", "") + if texture: + characteristics.append(texture) + + # Add pet-specific characteristics + pet_chars = pet_context.get("food_characteristics", "") + if pet_chars and pet_chars not in texture: + characteristics.append(pet_chars) + + # Add size/shape hints from name or description + size_hints = self._extract_size_hints() + if size_hints: + characteristics.append(size_hints) + + return ", ".join(characteristics) if characteristics else "" + + def _extract_size_hints(self) -> str: + """Extract size and shape hints from name and description.""" + text = f"{self.food_name} {self.description}".lower() + + size_keywords = { + "small": "small-sized pieces", + "mini": "mini kibble pieces", + "bite": "bite-sized pieces", + "chunk": "chunky pieces", + "shred": "shredded texture", + "flake": "flaky texture", + "pellet": "pellet-shaped pieces", + "kibble": "kibble pieces", + "morsel": "tender morsels", + } + + for keyword, description in size_keywords.items(): + if keyword in text: + return description + + return "" + + def _extract_ingredient_visuals(self) -> str: + """Extract visual descriptions based on ingredients.""" + visuals: list[str] = [] + + # Process up to 3 most visually interesting ingredients + processed_count = 0 + for ingredient in self.ingredients: + if processed_count >= 3: + break + + for key, visual in self.ingredient_visuals.items(): + if key in ingredient and visual not in " ".join(visuals): + visuals.append(visual) + processed_count += 1 + break + + if visuals: + return f"featuring {', '.join(visuals)}" + return "" + + def _detect_premium_product(self) -> bool: + """Detect if this is a premium product based on various indicators.""" + text = f"{self.food_name} {self.description}".lower() + + # Check for premium keywords + has_premium_keywords = any( + keyword in text for keyword in self.premium_indicators + ) + + # Check price point with more nuanced thresholds + price_threshold = { + "puppy": 25.0, + "kitten": 20.0, + "bunny": 20.0, + "rabbit": 20.0, + "dog": 30.0, + "cat": 25.0, + } + threshold = price_threshold.get(self.pet_type, 25.0) + is_high_price = self.price > threshold if self.price else False + + # Check for premium ingredients + premium_ingredients = [ + "salmon", + "tuna", + "lamb", + "duck", + "venison", + "bison", + "truffle", + ] + has_premium_ingredients = any( + ing in " ".join(self.ingredients) for ing in premium_ingredients + ) + + # Check for budget indicators that override premium detection + budget_keywords = ["affordable", "budget", "value", "economy", "basic"] + has_budget_keywords = any(keyword in text for keyword in budget_keywords) + + # Premium if has premium indicators AND not explicitly budget + return ( + has_premium_keywords or is_high_price or has_premium_ingredients + ) and not has_budget_keywords + + def _get_styling_context( + self, + is_premium: bool, + pet_context: dict, + food_context: dict, + ) -> str: + """Generate appropriate styling based on context.""" + styling_elements = [] + + # Base photography style + if is_premium: + styling_elements.extend( + [ + "luxury product photography", + "professional studio lighting", + "premium presentation", + ], + ) + else: + styling_elements.extend( + [ + "clean product photography", + "natural lighting", + "appealing presentation", + ], + ) + + # Add pet-specific presentation + presentation = pet_context.get("presentation", "") + if presentation: + styling_elements.append(presentation) + + # Add food-type specific lighting + lighting = food_context.get("lighting", "") + if lighting and lighting not in " ".join(styling_elements): + styling_elements.append(lighting) + + # Add background and quality + styling_elements.extend( + [ + "clean white background", + "high resolution", + "appetizing and professional", + ], + ) + + return ", ".join(styling_elements) + + +def _intelligent_truncate(prompt: str, max_length: int) -> str: + """Intelligently truncate prompt while preserving key visual elements.""" + if len(prompt) <= max_length: + return prompt + + # Split into sentences first, then into parts + sentences = prompt.split(". ") + + # Priority order for preservation + essential_keywords = [ + "premium pet food", + "bowl", + "featuring", + "chunks", + "pieces", + "kibble", + "meat", + "fish", + "chicken", + "beef", + "salmon", + ] + + important_keywords = [ + "photography", + "lighting", + "presentation", + "professional", + "clean", + "background", + "high resolution", + ] + + # Categorize sentences by importance + essential_sentences = [] + important_sentences = [] + optional_sentences = [] + + for sentence in sentences: + sentence = sentence.strip() + if not sentence: + continue + + if any(keyword in sentence.lower() for keyword in essential_keywords): + essential_sentences.append(sentence) + elif any(keyword in sentence.lower() for keyword in important_keywords): + important_sentences.append(sentence) + else: + optional_sentences.append(sentence) + + # Build result prioritizing essential content + result_parts: list[str] = [] + current_length = 0 + + # Add essential sentences first + for sentence in essential_sentences: + test_length = ( + current_length + len(sentence) + (2 if result_parts else 0) + ) # +2 for ". " + if test_length <= max_length: + result_parts.append(sentence) + current_length = test_length + else: + # Try to fit a truncated version + available_space = max_length - current_length - (2 if result_parts else 0) + if available_space > 20: # Only truncate if we have reasonable space + truncated = sentence[: available_space - 3] + "..." + result_parts.append(truncated) + break + + # Add important sentences if space allows + for sentence in important_sentences: + test_length = current_length + len(sentence) + 2 + if test_length <= max_length: + result_parts.append(sentence) + current_length = test_length + else: + break + + # Add optional sentences if space allows + for sentence in optional_sentences: + test_length = current_length + len(sentence) + 2 + if test_length <= max_length: + result_parts.append(sentence) + current_length = test_length + else: + break + + result = ". ".join(result_parts) + + # Ensure we end properly + if not result.endswith(".") and len(result) < max_length - 1: + result += "." + + return result + + +def exponential_backoff_delay(attempt: int) -> float: + """Calculate exponential backoff delay with jitter.""" + delay = min(BASE_DELAY * (2**attempt), MAX_DELAY) + jitter = random.uniform(-JITTER_RANGE, JITTER_RANGE) * delay + return max(0, delay + jitter) + + +def is_retryable_error(error: Exception) -> bool: + """Determine if an error is retryable.""" + if isinstance(error, ClientError): + error_code = error.response.get("Error", {}).get("Code", "") + + # Retryable errors + retryable_codes = { + "ThrottlingException", + "ServiceQuotaExceededException", + "TooManyRequestsException", + "InternalServerError", + "ServiceUnavailableException", + "RequestTimeoutException", + } + + return error_code in retryable_codes + + # Network-related errors are generally retryable + return "timeout" in str(error).lower() or "connection" in str(error).lower() + + +def generate_image_with_bedrock(prompt: str, food_id: str) -> Dict[str, Any]: + """Generate image using Amazon Bedrock with retry logic.""" + + # Add random initial delay to spread out concurrent requests + initial_delay = random.uniform(0.1, 2.0) + logger.info( + f"Adding initial delay of {initial_delay:.2f}s to avoid " "concurrency issues", + ) + time.sleep(initial_delay) + + for attempt in range(MAX_RETRIES + 1): + try: + logger.info( + f"Bedrock generation attempt {attempt + 1}/{MAX_RETRIES + 1} " + f"for food {food_id}", + ) + + # Prepare Bedrock request for Titan Image Generator v2 + request_body = { + "taskType": "TEXT_IMAGE", + "textToImageParams": { + "text": prompt, + "negativeText": "blurry, low quality, distorted, ugly", + }, + "imageGenerationConfig": { + "numberOfImages": 1, + "height": 512, + "width": 512, + "cfgScale": 8.0, + "seed": random.randint(0, 1000000), # Random seed for variety + }, + } + + # Call Bedrock + response = bedrock_client.invoke_model( + modelId=BEDROCK_MODEL_ID, + body=json.dumps(request_body), + contentType="application/json", + accept="application/json", + ) + + response_body = json.loads(response["body"].read()) + + if "images" in response_body and len(response_body["images"]) > 0: + image_data = response_body["images"][0] + logger.info( + f"✅ Successfully generated image for food {food_id} on " + f"attempt {attempt + 1}", + ) + return { + "image_data": image_data, + "success": True, + "attempts": attempt + 1, + } + else: + logger.error("No images returned from Bedrock") + return { + "image_data": None, + "success": False, + "error": "No images returned from Bedrock", + "attempts": attempt + 1, + } + + except Exception as e: + error_msg = str(e) + logger.warning( + f"❌ Bedrock attempt {attempt + 1} failed for food {food_id}: " + f"{error_msg}", + ) + + # Check if this is the last attempt + if attempt == MAX_RETRIES: + logger.error( + f"🚫 All {MAX_RETRIES + 1} attempts failed for food " f"{food_id}", + ) + return { + "image_data": None, + "success": False, + "error": f"Failed after {MAX_RETRIES + 1} attempts: {error_msg}", + "attempts": attempt + 1, + "retryable": is_retryable_error(e), + } + + # Check if error is retryable + if not is_retryable_error(e): + logger.error(f"🚫 Non-retryable error for food {food_id}: {error_msg}") + return { + "image_data": None, + "success": False, + "error": f"Non-retryable error: {error_msg}", + "attempts": attempt + 1, + "retryable": False, + } + + # Calculate delay for next attempt + delay = exponential_backoff_delay(attempt) + logger.info( + f"⏳ Retrying in {delay:.2f}s (attempt {attempt + 2}/" + f"{MAX_RETRIES + 1})", + ) + time.sleep(delay) + + # This should never be reached, but just in case + return { + "image_data": None, + "success": False, + "error": "Unexpected retry loop exit", + "attempts": MAX_RETRIES + 1, + } + + +def store_image_in_s3(image_data: str, food_id: str, food_name: str) -> Dict[str, Any]: + """Store generated image in S3.""" + try: + # Generate image path with petfood/ prefix + safe_name = food_name.lower().replace(" ", "-").replace("&", "and") + image_key = f"petfood/{safe_name}.jpg" + + # Decode base64 image data + image_bytes = base64.b64decode(image_data) + + # Upload to S3 + s3_client.put_object( + Bucket=S3_BUCKET_NAME, + Key=image_key, + Body=image_bytes, + ContentType="image/jpeg", + Metadata={ + "food_id": food_id, + "generated_by": "lambda-function", + "timestamp": str(int(time.time())), + }, + ) + + logger.info(f"Successfully stored image for food {food_id} at {image_key}") + + return { + "image_key": image_key, # Store S3 key instead of full URL + "success": True, + } + + except Exception as e: + logger.error(f"Error storing image in S3: {str(e)}") + return {"image_key": "", "success": False, "error": str(e)} + + +def update_food_record(food_id: str, image_key: str) -> Dict[str, Any]: + """Update food record in DynamoDB with S3 image key.""" + try: + table = dynamodb.Table(FOOD_TABLE_NAME) + + # Update the food record with S3 key instead of full URL + table.update_item( + Key={"id": food_id}, + UpdateExpression="SET image = :image_key, updated_at = :timestamp", + ExpressionAttributeValues={ + ":image_key": image_key, # Store S3 key + ":timestamp": int(time.time()), + }, + ReturnValues="UPDATED_NEW", + ) + + logger.info(f"Successfully updated food {food_id} with image key: {image_key}") + + return {"success": True, "updated": True} + + except Exception as e: + logger.error(f"Error updating food record: {str(e)}") + return {"success": False, "error": str(e)} + + +def extract_event_fields(event: Dict[str, Any]) -> Dict[str, Any]: + """Safely extract and validate event fields from EventBridge event.""" + try: + # Extract the detail section + detail = event.get("detail", {}) + + # Validate required fields + if not detail: + raise ValueError("Missing 'detail' section in event") + + event_type = detail.get("event_type", "") + if not event_type: + raise ValueError("Missing 'event_type' in event detail") + + food_id = detail.get("food_id", "") + if not food_id: + raise ValueError("Missing 'food_id' in event detail") + + # Extract optional fields with defaults + extracted = { + "event_type": event_type, + "food_id": food_id, + "food_name": detail.get("food_name"), + "pet_type": detail.get("pet_type"), + "food_type": detail.get("food_type"), + "description": detail.get("description"), + "ingredients": detail.get("ingredients", []), + "status": detail.get("status", ""), + "metadata": detail.get("metadata", {}), + "span_context": detail.get("span_context", {}), + } + + # Log extracted fields for debugging + logger.info(f"Extracted event fields: {json.dumps(extracted, default=str)}") + + return extracted + + except Exception as e: + logger.error(f"Error extracting event fields: {str(e)}") + raise + + +def process_food_event(event_detail: Dict[str, Any]) -> Dict[str, Any]: + """Process food creation/update events.""" + + food_id = event_detail.get("food_id", "") + food_name = event_detail.get("food_name") or "Unknown Food" + event_type = event_detail.get("event_type", "") + description = event_detail.get("description", "") + metadata = event_detail.get("metadata", {}) + + logger.info(f"Processing {event_type} event for {food_name} (ID: {food_id})") + + # Validate required fields + if not food_id: + raise ValueError("Missing required field: food_id") + if not description: + raise ValueError( + "Missing required field: description. Used to help generate prompt", + ) + + # Check if image generation is required + creation_source = metadata.get("creation_source", "unknown") + requires_validation = metadata.get("requires_validation", "false").lower() == "true" + is_manual_creation = metadata.get("is_manual_creation", "false").lower() == "true" + is_seed_data = metadata.get("is_seed_data", "false").lower() == "true" + + logger.info( + f"Image for: {food_id}, seed: {is_seed_data}, requires validation: {requires_validation}", + ) + + try: + # step 1 generate prompt + prompt = generate_prompt(event_detail) + + # step 2 generate image + image_result = generate_image_with_bedrock(prompt, food_id) + + if not image_result["success"]: + return { + "food_id": food_id, + "success": False, + "error": image_result.get("error", "Image generation failed"), + "message": f"Failed to generate image for {food_name}", + "bedrock_attempts": image_result.get("attempts", 1), + "retryable": image_result.get("retryable", True), + "creation_source": creation_source, + } + + # Step 3: Store image in S3 + storage_result = store_image_in_s3( + image_result["image_data"], + food_id, + food_name, + ) + + if not storage_result["success"]: + return { + "food_id": food_id, + "success": False, + "error": storage_result.get("error", "Image storage failed"), + "message": f"Failed to store image for {food_name}", + "bedrock_attempts": image_result.get("attempts", 1), + "s3_attempts": storage_result.get("attempts", 1), + "creation_source": creation_source, + } + + # Step 4: Update DynamoDB record with S3 key + update_result = update_food_record(food_id, storage_result["image_key"]) + + if not update_result["success"]: + return { + "food_id": food_id, + "success": False, + "error": update_result.get("error", "Database update failed"), + "message": f"Failed to update database for {food_name}", + "creation_source": creation_source, + } + + logger.info( + f"Successfully processed food event for {food_id} (source: {creation_source})", + ) + + return { + "food_id": food_id, + "success": True, + "image_key": storage_result["image_key"], + "message": f"Successfully processed food event for {food_name}", + "bedrock_attempts": image_result.get("attempts", 1), + "s3_attempts": storage_result.get("attempts", 1), + "image_size_bytes": storage_result.get("size_bytes", 0), + "creation_source": creation_source, + "prompt_type": "existing" if get_existing_prompt(food_name) else "dynamic", + } + + except Exception as e: + logger.error(f"Failed to process food event: {str(e)}") + return { + "food_id": food_id, + "success": False, + "error": str(e), + "message": f"Failed to process food event for {food_name}", + "creation_source": creation_source, + } + + +def lambda_handler(event, context): + """ + Main Lambda handler for processing EventBridge events. + This Lambda specifically handles image generation for FoodItemCreated and FoodItemUpdated events. + """ + + try: + # Log event details + logger.info(f"Processing event: {json.dumps(event, default=str)}") + + # Extract and validate event details + event_detail = extract_event_fields(event) + event_type = event_detail["event_type"] + + logger.info(f"Processing event type: {event_type}") + + # Process only image-related events + if event_type in ["FoodItemCreated", "FoodItemUpdated"]: + result = process_food_event(event_detail) + else: + logger.info( + f"Event type {event_type} not handled by image generator Lambda", + ) + return { + "statusCode": 200, + "body": json.dumps( + { + "message": f"Event type {event_type} not handled by this Lambda", + "success": True, + }, + ), + } + + return {"statusCode": 200, "body": json.dumps(result)} + + except Exception as e: + logger.error(f"Error processing event: {str(e)}") + + return { + "statusCode": 500, + "body": json.dumps( + { + "message": f"Error processing event: {str(e)}", + "success": False, + }, + ), + } diff --git a/src/applications/lambda/petfood-image-generator-python/pytest.ini b/src/applications/lambda/petfood-image-generator-python/pytest.ini new file mode 100644 index 00000000..8278d5fa --- /dev/null +++ b/src/applications/lambda/petfood-image-generator-python/pytest.ini @@ -0,0 +1,20 @@ +[tool:pytest] +testpaths = tests +python_files = test_*.py +python_classes = Test* +python_functions = test_* +addopts = + -v + --tb=short + --strict-markers + --disable-warnings + --color=yes + --durations=10 +markers = + unit: Unit tests + integration: Integration tests + slow: Slow running tests + aws: Tests that require AWS services +filterwarnings = + ignore::DeprecationWarning + ignore::PendingDeprecationWarning \ No newline at end of file diff --git a/src/applications/lambda/petfood-image-generator-python/tests/test_lambda_function.py b/src/applications/lambda/petfood-image-generator-python/tests/test_lambda_function.py new file mode 100644 index 00000000..ab753c3b --- /dev/null +++ b/src/applications/lambda/petfood-image-generator-python/tests/test_lambda_function.py @@ -0,0 +1,129 @@ +""" +Unit tests for the Strands Agent Lambda function. +""" + +import json +import sys +import os +from unittest.mock import Mock, patch +import pytest + +# Add the parent directory to the Python path so we can import lambda_function +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +# Mock environment variables to avoid AWS region issues +os.environ.setdefault("AWS_DEFAULT_REGION", "us-east-1") +os.environ.setdefault("FOOD_TABLE_NAME", "test-food-table") +os.environ.setdefault("S3_BUCKET_NAME", "test-bucket") +os.environ.setdefault("BEDROCK_MODEL_ID", "amazon.titan-image-generator-v2:0") + + +class TestLambdaHandler: + """Test cases for the main lambda handler.""" + + @patch("boto3.resource") + @patch("boto3.client") + @patch("lambda_function.process_food_event") + def test_lambda_handler_food_created( + self, mock_process, mock_boto_client, mock_boto_resource, + ): + """Test lambda handler with FoodItemCreated event.""" + # Set up mocks + mock_process.return_value = { + "success": True, + "message": "Image generated", + "food_id": "test-id", + } + + # Import after mocking + from lambda_function import lambda_handler + + event = { + "source": "petfood.service", + "detail-type": "FoodItemCreated", + "detail": { + "event_type": "FoodItemCreated", + "food_id": "test-id", + "food_name": "Test Food", + "pet_type": "Dog", + "food_type": "Dry", + "description": "Test description", + "ingredients": ["beef", "rice"], + }, + } + + context = Mock() + + result = lambda_handler(event, context) + + assert result["statusCode"] == 200 + body = json.loads(result["body"]) + assert body["success"] is True + assert body["food_id"] == "test-id" + + @patch("boto3.resource") + @patch("boto3.client") + def test_lambda_handler_unknown_event_type( + self, mock_boto_client, mock_boto_resource, + ): + """Test lambda handler with unknown event type.""" + from lambda_function import lambda_handler + + event = { + "source": "petfood.service", + "detail-type": "UnknownEvent", + "detail": { + "event_type": "UnknownEvent", + "food_id": "test-id", # Required field + }, + } + + context = Mock() + + result = lambda_handler(event, context) + + assert result["statusCode"] == 200 + body = json.loads(result["body"]) + assert body["success"] is True + assert "not handled by this Lambda" in body["message"] + + @patch("boto3.resource") + @patch("boto3.client") + def test_lambda_handler_error(self, mock_boto_client, mock_boto_resource): + """Test lambda handler error handling.""" + from lambda_function import lambda_handler + + event = { + "source": "petfood.service", + "detail-type": "FoodItemCreated", + "detail": {}, # Missing required fields + } + + context = Mock() + + result = lambda_handler(event, context) + + assert result["statusCode"] == 500 + body = json.loads(result["body"]) + assert body["success"] is False + + def test_generate_prompt(self): + """Test generate_prompt function.""" + from lambda_function import generate_prompt + + food_data = { + "food_name": "Premium Dog Food", + "pet_type": "dog", + "food_type": "dry", + "description": "High-quality dry food for adult dogs", + "ingredients": ["chicken", "rice", "vegetables"], + "price": 25.99, + } + + result = generate_prompt(food_data) + assert isinstance(result, str) + assert len(result) > 0 + + +if __name__ == "__main__": + pytest.main([__file__, "-v"]) diff --git a/src/applications/lambda/petstatusupdater-node/index.js b/src/applications/lambda/petstatusupdater-node/index.js new file mode 100644 index 00000000..0d652aa2 --- /dev/null +++ b/src/applications/lambda/petstatusupdater-node/index.js @@ -0,0 +1,50 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +const AWSXRay = require('aws-xray-sdk-core'); +const { DynamoDBClient } = require('@aws-sdk/client-dynamodb'); +const { DynamoDBDocumentClient, UpdateCommand } = require('@aws-sdk/lib-dynamodb'); + +const client = AWSXRay.captureAWSv3Client(new DynamoDBClient({})); +const documentClient = DynamoDBDocumentClient.from(client); + +exports.handler = async function (event) { + var payload = JSON.parse(event.body); + + var availability = 'yes'; + if (payload.petavailability === undefined) { + availability = 'no'; + } + var parameters = { + TableName: process.env.TABLE_NAME, + Key: { + pettype: payload.pettype, + petid: payload.petid, + }, + UpdateExpression: 'set availability = :r', + ExpressionAttributeValues: { + ':r': availability, + }, + ReturnValues: 'UPDATED_NEW', + }; + + await updatePetadoptionsTable(parameters); + + console.log( + 'Updated petid: ' + payload.petid + ', pettype: ' + payload.pettype + ', to availability: ' + availability, + ); + return { statusCode: 200, body: 'success' }; +}; + +async function updatePetadoptionsTable(parameters) { + try { + const command = new UpdateCommand(parameters); + const data = await documentClient.send(command); + console.log(JSON.stringify(data, undefined, 2)); + } catch (error) { + console.log(JSON.stringify(error, undefined, 2)); + throw error; + } +} diff --git a/src/applications/lambda/petstatusupdater-node/index.test.js b/src/applications/lambda/petstatusupdater-node/index.test.js new file mode 100644 index 00000000..3e4806f0 --- /dev/null +++ b/src/applications/lambda/petstatusupdater-node/index.test.js @@ -0,0 +1,71 @@ +// Mock AWS SDK modules before requiring the handler +jest.mock('@aws-sdk/lib-dynamodb'); +jest.mock('@aws-sdk/client-dynamodb'); +jest.mock('aws-xray-sdk-core'); + +describe('Pet Status Updater Lambda', () => { + let handler; + + beforeAll(() => { + // Set up mocks + const mockSend = jest.fn().mockResolvedValue({}); + const mockDocumentClient = { send: mockSend }; + + require('@aws-sdk/lib-dynamodb').DynamoDBDocumentClient = { + from: jest.fn(() => mockDocumentClient) + }; + require('@aws-sdk/lib-dynamodb').UpdateCommand = jest.fn(); + require('@aws-sdk/client-dynamodb').DynamoDBClient = jest.fn(); + require('aws-xray-sdk-core').captureAWSv3Client = jest.fn(client => client); + + // Now require the handler + handler = require('./index').handler; + }); + + beforeEach(() => { + process.env.TABLE_NAME = 'test-table'; + jest.clearAllMocks(); + }); + + test('should return success response', async () => { + const event = { + body: JSON.stringify({ + pettype: 'dog', + petid: '123', + petavailability: 'available' + }) + }; + + const result = await handler(event); + + expect(result.statusCode).toBe(200); + expect(result.body).toBe('success'); + }); + + test('should handle missing petavailability', async () => { + const event = { + body: JSON.stringify({ + pettype: 'cat', + petid: '456' + }) + }; + + const result = await handler(event); + + expect(result.statusCode).toBe(200); + expect(result.body).toBe('success'); + }); + + test('should parse JSON payload correctly', () => { + const testPayload = { + pettype: 'rabbit', + petid: '789', + petavailability: 'yes' + }; + + const parsed = JSON.parse(JSON.stringify(testPayload)); + expect(parsed.pettype).toBe('rabbit'); + expect(parsed.petid).toBe('789'); + expect(parsed.petavailability).toBe('yes'); + }); +}); \ No newline at end of file diff --git a/src/applications/lambda/petstatusupdater-node/package-lock.json b/src/applications/lambda/petstatusupdater-node/package-lock.json new file mode 100644 index 00000000..162761f6 --- /dev/null +++ b/src/applications/lambda/petstatusupdater-node/package-lock.json @@ -0,0 +1,8722 @@ +{ + "name": "petstatusupdater", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "petstatusupdater", + "version": "1.0.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-dynamodb": "^3.0.0", + "@aws-sdk/lib-dynamodb": "^3.0.0", + "aws-xray-sdk-core": "^3.10.3" + }, + "devDependencies": { + "jest": "^29.0.0" + } + }, + "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, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-dynamodb": { + "version": "3.878.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.878.0.tgz", + "integrity": "sha512-Y7pBfIPUlUhKf9A2ihUW8lkaAE1GftDbNRZwMhtX0CK10Q4A9efe4VRMzj0BQdJVwSnkDZvLudnLJfXpBoEMaQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.876.0", + "@aws-sdk/credential-provider-node": "3.876.0", + "@aws-sdk/middleware-endpoint-discovery": "3.873.0", + "@aws-sdk/middleware-host-header": "3.873.0", + "@aws-sdk/middleware-logger": "3.876.0", + "@aws-sdk/middleware-recursion-detection": "3.873.0", + "@aws-sdk/middleware-user-agent": "3.876.0", + "@aws-sdk/region-config-resolver": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-endpoints": "3.873.0", + "@aws-sdk/util-user-agent-browser": "3.873.0", + "@aws-sdk/util-user-agent-node": "3.876.0", + "@smithy/config-resolver": "^4.1.5", + "@smithy/core": "^3.8.0", + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/hash-node": "^4.0.5", + "@smithy/invalid-dependency": "^4.0.5", + "@smithy/middleware-content-length": "^4.0.5", + "@smithy/middleware-endpoint": "^4.1.18", + "@smithy/middleware-retry": "^4.1.19", + "@smithy/middleware-serde": "^4.0.9", + "@smithy/middleware-stack": "^4.0.5", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/protocol-http": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.26", + "@smithy/util-defaults-mode-node": "^4.0.26", + "@smithy/util-endpoints": "^3.0.7", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-retry": "^4.0.7", + "@smithy/util-utf8": "^4.0.0", + "@smithy/util-waiter": "^4.0.7", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.876.0.tgz", + "integrity": "sha512-Vf0PMF7HVpvllrfPODnBZmlz6kT/y2AvOt1RQG3+qD0VrHWzShc5nwgRZ+yyP3xkKVhZsQ3sJapfZTFnjqMOYA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.876.0", + "@aws-sdk/middleware-host-header": "3.873.0", + "@aws-sdk/middleware-logger": "3.876.0", + "@aws-sdk/middleware-recursion-detection": "3.873.0", + "@aws-sdk/middleware-user-agent": "3.876.0", + "@aws-sdk/region-config-resolver": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-endpoints": "3.873.0", + "@aws-sdk/util-user-agent-browser": "3.873.0", + "@aws-sdk/util-user-agent-node": "3.876.0", + "@smithy/config-resolver": "^4.1.5", + "@smithy/core": "^3.8.0", + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/hash-node": "^4.0.5", + "@smithy/invalid-dependency": "^4.0.5", + "@smithy/middleware-content-length": "^4.0.5", + "@smithy/middleware-endpoint": "^4.1.18", + "@smithy/middleware-retry": "^4.1.19", + "@smithy/middleware-serde": "^4.0.9", + "@smithy/middleware-stack": "^4.0.5", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/protocol-http": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.26", + "@smithy/util-defaults-mode-node": "^4.0.26", + "@smithy/util-endpoints": "^3.0.7", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-retry": "^4.0.7", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.876.0.tgz", + "integrity": "sha512-sVFBFkdoPOPyY13NaXO1E/R9O5J6ixzHnnRbqrbXYM2QQgLNPTKIiRtmVEuVoFV9YULg+/aKm7caix8m468y9w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@aws-sdk/xml-builder": "3.873.0", + "@smithy/core": "^3.8.0", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/property-provider": "^4.0.5", + "@smithy/protocol-http": "^5.1.3", + "@smithy/signature-v4": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-utf8": "^4.0.0", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.876.0.tgz", + "integrity": "sha512-cof7lwp2AlrAfRs0pt4W2KMS2VMBvEmpcti1UOFfSJIqkn+cyJliMJ8LHg22GI+kUexjvxdAqSbf3M7OHvEW+w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.876.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.876.0.tgz", + "integrity": "sha512-wzmef2NBp2+X1l8D4Q8hx1G8oI3+WdvLdPev9VnVpRYZxYGRWVPl++wvCBsCn/ZL0mdWopPkhHA3kFexQhMzvg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.876.0", + "@aws-sdk/types": "3.862.0", + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/property-provider": "^4.0.5", + "@smithy/protocol-http": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/util-stream": "^4.2.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.876.0.tgz", + "integrity": "sha512-JHbW6fqnJsVjGHCyko7B0NVPT1nEAPxkM3CGjUcVGsHgJBkxOLVCMQqTRyHcDdeHR2qeojlLoOHRz97xIHQjYw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.876.0", + "@aws-sdk/credential-provider-env": "3.876.0", + "@aws-sdk/credential-provider-http": "3.876.0", + "@aws-sdk/credential-provider-process": "3.876.0", + "@aws-sdk/credential-provider-sso": "3.876.0", + "@aws-sdk/credential-provider-web-identity": "3.876.0", + "@aws-sdk/nested-clients": "3.876.0", + "@aws-sdk/types": "3.862.0", + "@smithy/credential-provider-imds": "^4.0.7", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.876.0.tgz", + "integrity": "sha512-eHbNt1+Hi43e8ANnwf6toapLSxfMiyGq459y3Uh6i7NBOiWWKEsOVcgOfUC3RCoqeikxovt1tFM2cEElWUIOhg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.876.0", + "@aws-sdk/credential-provider-http": "3.876.0", + "@aws-sdk/credential-provider-ini": "3.876.0", + "@aws-sdk/credential-provider-process": "3.876.0", + "@aws-sdk/credential-provider-sso": "3.876.0", + "@aws-sdk/credential-provider-web-identity": "3.876.0", + "@aws-sdk/types": "3.862.0", + "@smithy/credential-provider-imds": "^4.0.7", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.876.0.tgz", + "integrity": "sha512-SMX4OlHvspu3gF4hxe7WAnZFhxpiCye+WlBSVoWfW/i9XNhtrZS1JMr29MK34GlCTk9qO7FlRwds/Z5k7xPpHg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.876.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.876.0.tgz", + "integrity": "sha512-iP5dz9XqwePbgnh7Bdrq5e1319JpCRKLyomUfHH1XVeXkIHmwIJdmTj1Upeo1J8L/5cLHmhXAN6CTN11bLo8SA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.876.0", + "@aws-sdk/core": "3.876.0", + "@aws-sdk/token-providers": "3.876.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.876.0.tgz", + "integrity": "sha512-q/XSCP1uae5aB9veM8zcm6Gqu6A4ckX9ZbhHgCzURXVJDwp+nINW1hM9vppMjGw3ND9Ibx/adR+KfTI0TDMzqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.876.0", + "@aws-sdk/nested-clients": "3.876.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/endpoint-cache": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/endpoint-cache/-/endpoint-cache-3.873.0.tgz", + "integrity": "sha512-EHd+5bSp/hZc78SMq9cUCIsX0B4ekZtFUVSSLEXyYv8x/nHFTnTqN9TsxV8bjlztR3aSUeoKSk5qxu/dVGgiQw==", + "license": "Apache-2.0", + "dependencies": { + "mnemonist": "0.38.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/lib-dynamodb": { + "version": "3.878.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/lib-dynamodb/-/lib-dynamodb-3.878.0.tgz", + "integrity": "sha512-Fm5zs95oh+FzP8MRvsK/5yKs+E8u1BaeYg+tw2WIwzvWHuo/3Hfs2YGRzCvqNgHWf7WIuPgwU5GLCECNAWvfJg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.876.0", + "@aws-sdk/util-dynamodb": "3.878.0", + "@smithy/core": "^3.8.0", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-dynamodb": "^3.878.0" + } + }, + "node_modules/@aws-sdk/middleware-endpoint-discovery": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-endpoint-discovery/-/middleware-endpoint-discovery-3.873.0.tgz", + "integrity": "sha512-qKQocn1MzaLS9dt5xt3MvQsZaQzRsmOFdazWXkMup1AtFrULSUklsbHjm5fg5xyFPN8ipNzPi+MCXcgPzfpKkg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/endpoint-cache": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.873.0.tgz", + "integrity": "sha512-KZ/W1uruWtMOs7D5j3KquOxzCnV79KQW9MjJFZM/M0l6KI8J6V3718MXxFHsTjUE4fpdV6SeCNLV1lwGygsjJA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.876.0.tgz", + "integrity": "sha512-cpWJhOuMSyz9oV25Z/CMHCBTgafDCbv7fHR80nlRrPdPZ8ETNsahwRgltXP1QJJ8r3X/c1kwpOR7tc+RabVzNA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.873.0.tgz", + "integrity": "sha512-OtgY8EXOzRdEWR//WfPkA/fXl0+WwE8hq0y9iw2caNyKPtca85dzrrZWnPqyBK/cpImosrpR1iKMYr41XshsCg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.876.0.tgz", + "integrity": "sha512-FR+8INfnbNv32QDQ5szxkWX6mB/QgezfNyx8LnAh1ErISZMmEFBxXXir+ZOfuV8vsmal1a6cy9qmnMNDaNnaNQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.876.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-endpoints": "3.873.0", + "@smithy/core": "^3.8.0", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.876.0.tgz", + "integrity": "sha512-R4TZrkM2gUElTsotk8mt3y7iLG8TNi1LL1wgVdEEWSLOYTaFyglGdoNBMtEeP7lmXilaTy00AbYF6BakJvSTHg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.876.0", + "@aws-sdk/middleware-host-header": "3.873.0", + "@aws-sdk/middleware-logger": "3.876.0", + "@aws-sdk/middleware-recursion-detection": "3.873.0", + "@aws-sdk/middleware-user-agent": "3.876.0", + "@aws-sdk/region-config-resolver": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-endpoints": "3.873.0", + "@aws-sdk/util-user-agent-browser": "3.873.0", + "@aws-sdk/util-user-agent-node": "3.876.0", + "@smithy/config-resolver": "^4.1.5", + "@smithy/core": "^3.8.0", + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/hash-node": "^4.0.5", + "@smithy/invalid-dependency": "^4.0.5", + "@smithy/middleware-content-length": "^4.0.5", + "@smithy/middleware-endpoint": "^4.1.18", + "@smithy/middleware-retry": "^4.1.19", + "@smithy/middleware-serde": "^4.0.9", + "@smithy/middleware-stack": "^4.0.5", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/protocol-http": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.26", + "@smithy/util-defaults-mode-node": "^4.0.26", + "@smithy/util-endpoints": "^3.0.7", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-retry": "^4.0.7", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.873.0.tgz", + "integrity": "sha512-q9sPoef+BBG6PJnc4x60vK/bfVwvRWsPgcoQyIra057S/QGjq5VkjvNk6H8xedf6vnKlXNBwq9BaANBXnldUJg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/types": "^4.3.2", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.876.0.tgz", + "integrity": "sha512-iU08kaQbhXnY0CC2TBcr7y/2PqPwZP2CTWX/Rbq0NvhOyteikfh7ASC+bRfLUp0XMSHKvSb+w2dh8a0lvx4oHg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.876.0", + "@aws-sdk/nested-clients": "3.876.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.862.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.862.0.tgz", + "integrity": "sha512-Bei+RL0cDxxV+lW2UezLbCYYNeJm6Nzee0TpW0FfyTRBhH9C1XQh4+x+IClriXvgBnRquTMMYsmJfvx8iyLKrg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-dynamodb": { + "version": "3.878.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-dynamodb/-/util-dynamodb-3.878.0.tgz", + "integrity": "sha512-YEoDb/sZlPjZ9KNjSS/OqywoyBAdAuHamiv21nQpHul30HBsEWbR8zsrcdSg7KCFigYz2RIga0IdSn1IKT+YSA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "@aws-sdk/client-dynamodb": "^3.878.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.873.0.tgz", + "integrity": "sha512-YByHrhjxYdjKRf/RQygRK1uh0As1FIi9+jXTcIEX/rBgN8mUByczr2u4QXBzw7ZdbdcOBMOkPnLRjNOWW1MkFg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-endpoints": "^3.0.7", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.873.0.tgz", + "integrity": "sha512-xcVhZF6svjM5Rj89T1WzkjQmrTF6dpR2UvIHPMTnSZoNe6CixejPZ6f0JJ2kAhO8H+dUHwNBlsUgOTIKiK/Syg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.873.0.tgz", + "integrity": "sha512-AcRdbK6o19yehEcywI43blIBhOCSo6UgyWcuOJX5CFF8k39xm1ILCjQlRRjchLAxWrm0lU0Q7XV90RiMMFMZtA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/types": "^4.3.2", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.876.0.tgz", + "integrity": "sha512-/ZIaeUt60JBdI0mNc7sZ8v3Tuzp8Pbe4gIAYnppGyF4KV8QA+Yu8tp2bGHfkKn150t1uvQ6P/4CwFfoGF34dzg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.876.0", + "@aws-sdk/types": "3.862.0", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.873.0.tgz", + "integrity": "sha512-kLO7k7cGJ6KaHiExSJWojZurF7SnGMDHXRuQunFnEoD0n1yB6Lqy/S/zHiQ7oJnBhPr9q0TW9qFkrsZb1Uc54w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz", + "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.3", + "@babel/parser": "^7.28.3", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.3", + "@babel/types": "^7.28.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/core/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, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/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, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.3.tgz", + "integrity": "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz", + "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz", + "integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.3", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.2", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "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, + "license": "MIT" + }, + "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, + "license": "ISC", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.30", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", + "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@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, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@smithy/abort-controller": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.5.tgz", + "integrity": "sha512-jcrqdTQurIrBbUm4W2YdLVMQDoL0sA9DTxYd2s+R/y+2U9NLOP7Xf/YqfSg1FZhlZIYEnvk2mwbyvIfdLEPo8g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.1.5.tgz", + "integrity": "sha512-viuHMxBAqydkB0AfWwHIdwf/PRH2z5KHGUzqyRtS/Wv+n3IHI993Sk76VCA7dD/+GzgGOmlJDITfPcJC1nIVIw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.4", + "@smithy/types": "^4.3.2", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.9.0.tgz", + "integrity": "sha512-B/GknvCfS3llXd/b++hcrwIuqnEozQDnRL4sBmOac5/z/dr0/yG1PURNPOyU4Lsiy1IyTj8scPxVqRs5dYWf6A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^4.0.9", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-stream": "^4.2.4", + "@smithy/util-utf8": "^4.0.0", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.0.7.tgz", + "integrity": "sha512-dDzrMXA8d8riFNiPvytxn0mNwR4B3h8lgrQ5UjAGu6T9z/kRg/Xncf4tEQHE/+t25sY8IH3CowcmWi+1U5B1Gw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.4", + "@smithy/property-provider": "^4.0.5", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.1.1.tgz", + "integrity": "sha512-61WjM0PWmZJR+SnmzaKI7t7G0UkkNFboDpzIdzSoy7TByUzlxo18Qlh9s71qug4AY4hlH/CwXdubMtkcNEb/sQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.1.3", + "@smithy/querystring-builder": "^4.0.5", + "@smithy/types": "^4.3.2", + "@smithy/util-base64": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.0.5.tgz", + "integrity": "sha512-cv1HHkKhpyRb6ahD8Vcfb2Hgz67vNIXEp2vnhzfxLFGRukLCNEA5QdsorbUEzXma1Rco0u3rx5VTqbM06GcZqQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.0.5.tgz", + "integrity": "sha512-IVnb78Qtf7EJpoEVo7qJ8BEXQwgC4n3igeJNNKEj/MLYtapnx8A67Zt/J3RXAj2xSO1910zk0LdFiygSemuLow==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.0.0.tgz", + "integrity": "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.0.5.tgz", + "integrity": "sha512-l1jlNZoYzoCC7p0zCtBDE5OBXZ95yMKlRlftooE5jPWQn4YBPLgsp+oeHp7iMHaTGoUdFqmHOPa8c9G3gBsRpQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.1.19", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.1.19.tgz", + "integrity": "sha512-EAlEPncqo03siNZJ9Tm6adKCQ+sw5fNU8ncxWwaH0zTCwMPsgmERTi6CEKaermZdgJb+4Yvh0NFm36HeO4PGgQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.9.0", + "@smithy/middleware-serde": "^4.0.9", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-middleware": "^4.0.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.1.20", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.1.20.tgz", + "integrity": "sha512-T3maNEm3Masae99eFdx1Q7PIqBBEVOvRd5hralqKZNeIivnoGNx5OFtI3DiZ5gCjUkl0mNondlzSXeVxkinh7Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.4", + "@smithy/protocol-http": "^5.1.3", + "@smithy/service-error-classification": "^4.0.7", + "@smithy/smithy-client": "^4.5.0", + "@smithy/types": "^4.3.2", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-retry": "^4.0.7", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry/node_modules/@smithy/service-error-classification": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.0.7.tgz", + "integrity": "sha512-XvRHOipqpwNhEjDf2L5gJowZEm5nsxC16pAZOeEcsygdjv9A2jdOh3YoDQvOXBGTsaJk6mNWtzWalOB9976Wlg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.0.9.tgz", + "integrity": "sha512-uAFFR4dpeoJPGz8x9mhxp+RPjo5wW0QEEIPPPbLXiRRWeCATf/Km3gKIVR5vaP8bN1kgsPhcEeh+IZvUlBv6Xg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.0.5.tgz", + "integrity": "sha512-/yoHDXZPh3ocRVyeWQFvC44u8seu3eYzZRveCMfgMOBcNKnAmOvjbL9+Cp5XKSIi9iYA9PECUuW2teDAk8T+OQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.1.4.tgz", + "integrity": "sha512-+UDQV/k42jLEPPHSn39l0Bmc4sB1xtdI9Gd47fzo/0PbXzJ7ylgaOByVjF5EeQIumkepnrJyfx86dPa9p47Y+w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.1.1.tgz", + "integrity": "sha512-RHnlHqFpoVdjSPPiYy/t40Zovf3BBHc2oemgD7VsVTFFZrU5erFFe0n52OANZZ/5sbshgD93sOh5r6I35Xmpaw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.0.5", + "@smithy/protocol-http": "^5.1.3", + "@smithy/querystring-builder": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.0.5.tgz", + "integrity": "sha512-R/bswf59T/n9ZgfgUICAZoWYKBHcsVDurAGX88zsiUtOTA/xUAPyiT+qkNCPwFn43pZqN84M4MiUsbSGQmgFIQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.3.tgz", + "integrity": "sha512-fCJd2ZR7D22XhDY0l+92pUag/7je2BztPRQ01gU5bMChcyI0rlly7QFibnYHzcxDvccMjlpM/Q1ev8ceRIb48w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.0.5.tgz", + "integrity": "sha512-NJeSCU57piZ56c+/wY+AbAw6rxCCAOZLCIniRE7wqvndqxcKKDOXzwWjrY7wGKEISfhL9gBbAaWWgHsUGedk+A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "@smithy/util-uri-escape": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.0.5.tgz", + "integrity": "sha512-6SV7md2CzNG/WUeTjVe6Dj8noH32r4MnUeFKZrnVYsQxpGSIcphAanQMayi8jJLZAWm6pdM9ZXvKCpWOsIGg0w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-2.1.5.tgz", + "integrity": "sha512-uBDTIBBEdAQryvHdc5W8sS5YX7RQzF683XrHePVdFmAgKiMofU15FLSM0/HU03hKTnazdNRFa0YHS7+ArwoUSQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^2.12.0" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/service-error-classification/node_modules/@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.0.5.tgz", + "integrity": "sha512-YVVwehRDuehgoXdEL4r1tAAzdaDgaC9EQvhK0lEbfnbrd0bd5+CTQumbdPryX3J2shT7ZqQE+jPW4lmNBAB8JQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.1.3.tgz", + "integrity": "sha512-mARDSXSEgllNzMw6N+mC+r1AQlEBO3meEAkR/UlfAgnMzJUB3goRBWgip1EAMG99wh36MDqzo86SfIX5Y+VEaw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-uri-escape": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.5.0.tgz", + "integrity": "sha512-ZSdE3vl0MuVbEwJBxSftm0J5nL/gw76xp5WF13zW9cN18MFuFXD5/LV0QD8P+sCU5bSWGyy6CTgUupE1HhOo1A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.9.0", + "@smithy/middleware-endpoint": "^4.1.19", + "@smithy/middleware-stack": "^4.0.5", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "@smithy/util-stream": "^4.2.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.3.2.tgz", + "integrity": "sha512-QO4zghLxiQ5W9UZmX2Lo0nta2PuE1sSrXUYDoaB6HMR762C0P7v/HEPHf6ZdglTVssJG1bsrSBxdc3quvDSihw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.0.5.tgz", + "integrity": "sha512-j+733Um7f1/DXjYhCbvNXABV53NyCRRA54C7bNEIxNPs0YjfRxeMKjjgm2jvTYrciZyCjsicHwQ6Q0ylo+NAUw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.0.0.tgz", + "integrity": "sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.0.0.tgz", + "integrity": "sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.0.0.tgz", + "integrity": "sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.0.0.tgz", + "integrity": "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.0.0.tgz", + "integrity": "sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.0.27", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.27.tgz", + "integrity": "sha512-i/Fu6AFT5014VJNgWxKomBJP/GB5uuOsM4iHdcmplLm8B1eAqnRItw4lT2qpdO+mf+6TFmf6dGcggGLAVMZJsQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.0.5", + "@smithy/smithy-client": "^4.5.0", + "@smithy/types": "^4.3.2", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.0.27", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.27.tgz", + "integrity": "sha512-3W0qClMyxl/ELqTA39aNw1N+pN0IjpXT7lPFvZ8zTxqVFP7XCpACB9QufmN4FQtd39xbgS7/Lekn7LmDa63I5w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.1.5", + "@smithy/credential-provider-imds": "^4.0.7", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/property-provider": "^4.0.5", + "@smithy/smithy-client": "^4.5.0", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.0.7.tgz", + "integrity": "sha512-klGBP+RpBp6V5JbrY2C/VKnHXn3d5V2YrifZbmMY8os7M6m8wdYFoO6w/fe5VkP+YVwrEktW3IWYaSQVNZJ8oQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.4", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.0.0.tgz", + "integrity": "sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.0.5.tgz", + "integrity": "sha512-N40PfqsZHRSsByGB81HhSo+uvMxEHT+9e255S53pfBw/wI6WKDI7Jw9oyu5tJTLwZzV5DsMha3ji8jk9dsHmQQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.0.7.tgz", + "integrity": "sha512-TTO6rt0ppK70alZpkjwy+3nQlTiqNfoXja+qwuAchIEAIoSZW8Qyd76dvBv3I5bCpE38APafG23Y/u270NspiQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.0.7", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry/node_modules/@smithy/service-error-classification": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.0.7.tgz", + "integrity": "sha512-XvRHOipqpwNhEjDf2L5gJowZEm5nsxC16pAZOeEcsygdjv9A2jdOh3YoDQvOXBGTsaJk6mNWtzWalOB9976Wlg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.2.4.tgz", + "integrity": "sha512-vSKnvNZX2BXzl0U2RgCLOwWaAP9x/ddd/XobPK02pCbzRm5s55M53uwb1rl/Ts7RXZvdJZerPkA+en2FDghLuQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/types": "^4.3.2", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.0.0.tgz", + "integrity": "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", + "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-waiter": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.0.7.tgz", + "integrity": "sha512-mYqtQXPmrwvUljaHyGxYUIIRI3qjBTEb/f5QFi3A6VlxhpmZd5mWXn9W+qUkf2pVE1Hv3SqxefiZOPGdxmO64A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "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, + "license": "MIT", + "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.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "node_modules/@types/cls-hooked": { + "version": "4.3.9", + "resolved": "https://registry.npmjs.org/@types/cls-hooked/-/cls-hooked-4.3.9.tgz", + "integrity": "sha512-CMtHMz6Q/dkfcHarq9nioXH8BDPP+v5xvd+N90lBQ2bdmu06UvnLDqxTKoOJzz4SzIwb/x9i4UXGAAcnUDuIvg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/node": { + "version": "24.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", + "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.10.0" + } + }, + "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, + "license": "MIT" + }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "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, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/async-hook-jl": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/async-hook-jl/-/async-hook-jl-1.7.6.tgz", + "integrity": "sha512-gFaHkFfSxTjvoxDMYqDuGHlcRyUuamF8s+ZTtJdDzqjws4mCt7v0vuV79/E2Wr2/riMQgtG4/yUtXWs1gZ7JMg==", + "license": "MIT", + "dependencies": { + "stack-chain": "^1.3.7" + }, + "engines": { + "node": "^4.7 || >=6.9 || >=7.3" + } + }, + "node_modules/atomic-batcher": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/atomic-batcher/-/atomic-batcher-1.0.2.tgz", + "integrity": "sha512-EFGCRj4kLX1dHv1cDzTk+xbjBFj1GnJDpui52YmEcxxHHEWjYyT6l51U7n6WQ28osZH4S9gSybxe56Vm7vB61Q==", + "license": "MIT" + }, + "node_modules/aws-xray-sdk-core": { + "version": "3.10.3", + "resolved": "https://registry.npmjs.org/aws-xray-sdk-core/-/aws-xray-sdk-core-3.10.3.tgz", + "integrity": "sha512-bltsLAr4juMJJ2tT5/L/CtwUGIvHihtPe6SO/z3jjOD73PHhOYxcuwCMFFyTbTy5S4WThJO32oZk7r+pg3ZoCQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.4.1", + "@smithy/service-error-classification": "^2.0.4", + "@types/cls-hooked": "^4.3.3", + "atomic-batcher": "^1.0.2", + "cls-hooked": "^4.2.2", + "semver": "^7.5.3" + }, + "engines": { + "node": ">= 14.x" + } + }, + "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, + "license": "MIT", + "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, + "license": "BSD-3-Clause", + "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, + "license": "BSD-3-Clause", + "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-istanbul/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, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "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, + "license": "MIT", + "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.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "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 || ^8.0.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, + "license": "MIT", + "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, + "license": "MIT" + }, + "node_modules/bowser": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.12.1.tgz", + "integrity": "sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.25.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.4.tgz", + "integrity": "sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg==", + "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" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001737", + "electron-to-chromium": "^1.5.211", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "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, + "license": "Apache-2.0", + "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==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001739", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001739.tgz", + "integrity": "sha512-y+j60d6ulelrNSwpPyrHdl+9mJnQzHBr08xm48Qno0nSk4h3Qojh+ziv2qE6rXf4k3tadF4o1J/1tAbVm1NtnA==", + "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" + } + ], + "license": "CC-BY-4.0" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cls-hooked": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/cls-hooked/-/cls-hooked-4.2.2.tgz", + "integrity": "sha512-J4Xj5f5wq/4jAvcdgoGsL3G103BtWpZrMo8NEinRltN+xpTZdI+M38pyQqhuFU/P792xkMFvnKSf+Lm81U1bxw==", + "license": "BSD-2-Clause", + "dependencies": { + "async-hook-jl": "^1.7.6", + "emitter-listener": "^1.0.1", + "semver": "^5.4.1" + }, + "engines": { + "node": "^4.7 || >=6.9 || >=7.3 || >=8.2.1" + } + }, + "node_modules/cls-hooked/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "license": "ISC", + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "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/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", + "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.211", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.211.tgz", + "integrity": "sha512-IGBvimJkotaLzFnwIVgW9/UD/AOJ2tByUmeOrtqBfACSbAw5b1G0XpvdaieKyc7ULmbwXVx+4e4Be8pOPBrYkw==", + "dev": true, + "license": "ISC" + }, + "node_modules/emitter-listener": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/emitter-listener/-/emitter-listener-1.1.2.tgz", + "integrity": "sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ==", + "license": "BSD-2-Clause", + "dependencies": { + "shimmer": "^1.2.0" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "BSD-2-Clause", + "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, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "node_modules/fast-xml-parser": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", + "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^2.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/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, + "license": "Apache-2.0", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "ISC", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "ISC", + "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/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, + "license": "ISC" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "ISC", + "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, + "license": "ISC" + }, + "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, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "ISC" + }, + "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, + "license": "BSD-3-Clause", + "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, + "license": "BSD-3-Clause", + "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-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, + "license": "BSD-3-Clause", + "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, + "license": "BSD-3-Clause", + "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.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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-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, + "license": "MIT", + "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, + "license": "MIT", + "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-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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "ISC", + "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, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mnemonist": { + "version": "0.38.3", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.38.3.tgz", + "integrity": "sha512-2K9QYubXx/NAjv4VLq1d1Ly8pWNC5L3BrixtdkyTegXWJIqY+zLNDhhX/A+ZwWt70tB1S8H4BE8FLYEFyNoOBw==", + "license": "MIT", + "dependencies": { + "obliterator": "^1.6.1" + } + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/obliterator": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-1.6.1.tgz", + "integrity": "sha512-9WXswnqINnnhOG/5SLimUlzuU1hFJUc8zkwyD59Sd+dPOMf05PmnYG/d6Q7HZ+KmgkZJa1PxRso6QdM3sTNHig==", + "license": "MIT" + }, + "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, + "license": "ISC", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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" + } + ], + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/shimmer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz", + "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==", + "license": "BSD-2-Clause" + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "license": "MIT", + "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, + "license": "BSD-3-Clause" + }, + "node_modules/stack-chain": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/stack-chain/-/stack-chain-1.3.7.tgz", + "integrity": "sha512-D8cWtWVdIe/jBA7v5p5Hwl5yOSOrmZPWDPe2KxQ5UAGD+nxbxU0lKXA4h85Ta6+qgdKVL3vUxsbIZjc1kBG7ug==", + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "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/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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strnum": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", + "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "ISC", + "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, + "license": "BSD-3-Clause" + }, + "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, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/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, + "license": "MIT", + "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, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "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" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/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, + "license": "ISC", + "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, + "license": "Apache-2.0", + "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, + "license": "ISC", + "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, + "license": "MIT", + "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, + "license": "ISC" + }, + "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, + "license": "ISC", + "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, + "license": "ISC", + "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, + "license": "ISC" + }, + "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, + "license": "MIT", + "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, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "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, + "license": "MIT", + "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-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "requires": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "requires": { + "tslib": "^2.6.2" + } + }, + "@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "requires": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + } + }, + "@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "requires": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + } + } + } + }, + "@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "requires": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + } + }, + "@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "requires": { + "tslib": "^2.6.2" + } + }, + "@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "requires": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + }, + "dependencies": { + "@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "requires": { + "tslib": "^2.6.2" + } + }, + "@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "requires": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + } + }, + "@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "requires": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + } + } + } + }, + "@aws-sdk/client-dynamodb": { + "version": "3.878.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-dynamodb/-/client-dynamodb-3.878.0.tgz", + "integrity": "sha512-Y7pBfIPUlUhKf9A2ihUW8lkaAE1GftDbNRZwMhtX0CK10Q4A9efe4VRMzj0BQdJVwSnkDZvLudnLJfXpBoEMaQ==", + "requires": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.876.0", + "@aws-sdk/credential-provider-node": "3.876.0", + "@aws-sdk/middleware-endpoint-discovery": "3.873.0", + "@aws-sdk/middleware-host-header": "3.873.0", + "@aws-sdk/middleware-logger": "3.876.0", + "@aws-sdk/middleware-recursion-detection": "3.873.0", + "@aws-sdk/middleware-user-agent": "3.876.0", + "@aws-sdk/region-config-resolver": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-endpoints": "3.873.0", + "@aws-sdk/util-user-agent-browser": "3.873.0", + "@aws-sdk/util-user-agent-node": "3.876.0", + "@smithy/config-resolver": "^4.1.5", + "@smithy/core": "^3.8.0", + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/hash-node": "^4.0.5", + "@smithy/invalid-dependency": "^4.0.5", + "@smithy/middleware-content-length": "^4.0.5", + "@smithy/middleware-endpoint": "^4.1.18", + "@smithy/middleware-retry": "^4.1.19", + "@smithy/middleware-serde": "^4.0.9", + "@smithy/middleware-stack": "^4.0.5", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/protocol-http": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.26", + "@smithy/util-defaults-mode-node": "^4.0.26", + "@smithy/util-endpoints": "^3.0.7", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-retry": "^4.0.7", + "@smithy/util-utf8": "^4.0.0", + "@smithy/util-waiter": "^4.0.7", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + } + }, + "@aws-sdk/client-sso": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.876.0.tgz", + "integrity": "sha512-Vf0PMF7HVpvllrfPODnBZmlz6kT/y2AvOt1RQG3+qD0VrHWzShc5nwgRZ+yyP3xkKVhZsQ3sJapfZTFnjqMOYA==", + "requires": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.876.0", + "@aws-sdk/middleware-host-header": "3.873.0", + "@aws-sdk/middleware-logger": "3.876.0", + "@aws-sdk/middleware-recursion-detection": "3.873.0", + "@aws-sdk/middleware-user-agent": "3.876.0", + "@aws-sdk/region-config-resolver": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-endpoints": "3.873.0", + "@aws-sdk/util-user-agent-browser": "3.873.0", + "@aws-sdk/util-user-agent-node": "3.876.0", + "@smithy/config-resolver": "^4.1.5", + "@smithy/core": "^3.8.0", + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/hash-node": "^4.0.5", + "@smithy/invalid-dependency": "^4.0.5", + "@smithy/middleware-content-length": "^4.0.5", + "@smithy/middleware-endpoint": "^4.1.18", + "@smithy/middleware-retry": "^4.1.19", + "@smithy/middleware-serde": "^4.0.9", + "@smithy/middleware-stack": "^4.0.5", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/protocol-http": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.26", + "@smithy/util-defaults-mode-node": "^4.0.26", + "@smithy/util-endpoints": "^3.0.7", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-retry": "^4.0.7", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/core": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.876.0.tgz", + "integrity": "sha512-sVFBFkdoPOPyY13NaXO1E/R9O5J6ixzHnnRbqrbXYM2QQgLNPTKIiRtmVEuVoFV9YULg+/aKm7caix8m468y9w==", + "requires": { + "@aws-sdk/types": "3.862.0", + "@aws-sdk/xml-builder": "3.873.0", + "@smithy/core": "^3.8.0", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/property-provider": "^4.0.5", + "@smithy/protocol-http": "^5.1.3", + "@smithy/signature-v4": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-utf8": "^4.0.0", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/credential-provider-env": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.876.0.tgz", + "integrity": "sha512-cof7lwp2AlrAfRs0pt4W2KMS2VMBvEmpcti1UOFfSJIqkn+cyJliMJ8LHg22GI+kUexjvxdAqSbf3M7OHvEW+w==", + "requires": { + "@aws-sdk/core": "3.876.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/credential-provider-http": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.876.0.tgz", + "integrity": "sha512-wzmef2NBp2+X1l8D4Q8hx1G8oI3+WdvLdPev9VnVpRYZxYGRWVPl++wvCBsCn/ZL0mdWopPkhHA3kFexQhMzvg==", + "requires": { + "@aws-sdk/core": "3.876.0", + "@aws-sdk/types": "3.862.0", + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/property-provider": "^4.0.5", + "@smithy/protocol-http": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/util-stream": "^4.2.4", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/credential-provider-ini": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.876.0.tgz", + "integrity": "sha512-JHbW6fqnJsVjGHCyko7B0NVPT1nEAPxkM3CGjUcVGsHgJBkxOLVCMQqTRyHcDdeHR2qeojlLoOHRz97xIHQjYw==", + "requires": { + "@aws-sdk/core": "3.876.0", + "@aws-sdk/credential-provider-env": "3.876.0", + "@aws-sdk/credential-provider-http": "3.876.0", + "@aws-sdk/credential-provider-process": "3.876.0", + "@aws-sdk/credential-provider-sso": "3.876.0", + "@aws-sdk/credential-provider-web-identity": "3.876.0", + "@aws-sdk/nested-clients": "3.876.0", + "@aws-sdk/types": "3.862.0", + "@smithy/credential-provider-imds": "^4.0.7", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/credential-provider-node": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.876.0.tgz", + "integrity": "sha512-eHbNt1+Hi43e8ANnwf6toapLSxfMiyGq459y3Uh6i7NBOiWWKEsOVcgOfUC3RCoqeikxovt1tFM2cEElWUIOhg==", + "requires": { + "@aws-sdk/credential-provider-env": "3.876.0", + "@aws-sdk/credential-provider-http": "3.876.0", + "@aws-sdk/credential-provider-ini": "3.876.0", + "@aws-sdk/credential-provider-process": "3.876.0", + "@aws-sdk/credential-provider-sso": "3.876.0", + "@aws-sdk/credential-provider-web-identity": "3.876.0", + "@aws-sdk/types": "3.862.0", + "@smithy/credential-provider-imds": "^4.0.7", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/credential-provider-process": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.876.0.tgz", + "integrity": "sha512-SMX4OlHvspu3gF4hxe7WAnZFhxpiCye+WlBSVoWfW/i9XNhtrZS1JMr29MK34GlCTk9qO7FlRwds/Z5k7xPpHg==", + "requires": { + "@aws-sdk/core": "3.876.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/credential-provider-sso": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.876.0.tgz", + "integrity": "sha512-iP5dz9XqwePbgnh7Bdrq5e1319JpCRKLyomUfHH1XVeXkIHmwIJdmTj1Upeo1J8L/5cLHmhXAN6CTN11bLo8SA==", + "requires": { + "@aws-sdk/client-sso": "3.876.0", + "@aws-sdk/core": "3.876.0", + "@aws-sdk/token-providers": "3.876.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/credential-provider-web-identity": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.876.0.tgz", + "integrity": "sha512-q/XSCP1uae5aB9veM8zcm6Gqu6A4ckX9ZbhHgCzURXVJDwp+nINW1hM9vppMjGw3ND9Ibx/adR+KfTI0TDMzqw==", + "requires": { + "@aws-sdk/core": "3.876.0", + "@aws-sdk/nested-clients": "3.876.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/endpoint-cache": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/endpoint-cache/-/endpoint-cache-3.873.0.tgz", + "integrity": "sha512-EHd+5bSp/hZc78SMq9cUCIsX0B4ekZtFUVSSLEXyYv8x/nHFTnTqN9TsxV8bjlztR3aSUeoKSk5qxu/dVGgiQw==", + "requires": { + "mnemonist": "0.38.3", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/lib-dynamodb": { + "version": "3.878.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/lib-dynamodb/-/lib-dynamodb-3.878.0.tgz", + "integrity": "sha512-Fm5zs95oh+FzP8MRvsK/5yKs+E8u1BaeYg+tw2WIwzvWHuo/3Hfs2YGRzCvqNgHWf7WIuPgwU5GLCECNAWvfJg==", + "requires": { + "@aws-sdk/core": "3.876.0", + "@aws-sdk/util-dynamodb": "3.878.0", + "@smithy/core": "^3.8.0", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/middleware-endpoint-discovery": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-endpoint-discovery/-/middleware-endpoint-discovery-3.873.0.tgz", + "integrity": "sha512-qKQocn1MzaLS9dt5xt3MvQsZaQzRsmOFdazWXkMup1AtFrULSUklsbHjm5fg5xyFPN8ipNzPi+MCXcgPzfpKkg==", + "requires": { + "@aws-sdk/endpoint-cache": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/middleware-host-header": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.873.0.tgz", + "integrity": "sha512-KZ/W1uruWtMOs7D5j3KquOxzCnV79KQW9MjJFZM/M0l6KI8J6V3718MXxFHsTjUE4fpdV6SeCNLV1lwGygsjJA==", + "requires": { + "@aws-sdk/types": "3.862.0", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/middleware-logger": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.876.0.tgz", + "integrity": "sha512-cpWJhOuMSyz9oV25Z/CMHCBTgafDCbv7fHR80nlRrPdPZ8ETNsahwRgltXP1QJJ8r3X/c1kwpOR7tc+RabVzNA==", + "requires": { + "@aws-sdk/types": "3.862.0", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/middleware-recursion-detection": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.873.0.tgz", + "integrity": "sha512-OtgY8EXOzRdEWR//WfPkA/fXl0+WwE8hq0y9iw2caNyKPtca85dzrrZWnPqyBK/cpImosrpR1iKMYr41XshsCg==", + "requires": { + "@aws-sdk/types": "3.862.0", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/middleware-user-agent": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.876.0.tgz", + "integrity": "sha512-FR+8INfnbNv32QDQ5szxkWX6mB/QgezfNyx8LnAh1ErISZMmEFBxXXir+ZOfuV8vsmal1a6cy9qmnMNDaNnaNQ==", + "requires": { + "@aws-sdk/core": "3.876.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-endpoints": "3.873.0", + "@smithy/core": "^3.8.0", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/nested-clients": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.876.0.tgz", + "integrity": "sha512-R4TZrkM2gUElTsotk8mt3y7iLG8TNi1LL1wgVdEEWSLOYTaFyglGdoNBMtEeP7lmXilaTy00AbYF6BakJvSTHg==", + "requires": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.876.0", + "@aws-sdk/middleware-host-header": "3.873.0", + "@aws-sdk/middleware-logger": "3.876.0", + "@aws-sdk/middleware-recursion-detection": "3.873.0", + "@aws-sdk/middleware-user-agent": "3.876.0", + "@aws-sdk/region-config-resolver": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-endpoints": "3.873.0", + "@aws-sdk/util-user-agent-browser": "3.873.0", + "@aws-sdk/util-user-agent-node": "3.876.0", + "@smithy/config-resolver": "^4.1.5", + "@smithy/core": "^3.8.0", + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/hash-node": "^4.0.5", + "@smithy/invalid-dependency": "^4.0.5", + "@smithy/middleware-content-length": "^4.0.5", + "@smithy/middleware-endpoint": "^4.1.18", + "@smithy/middleware-retry": "^4.1.19", + "@smithy/middleware-serde": "^4.0.9", + "@smithy/middleware-stack": "^4.0.5", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/protocol-http": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.26", + "@smithy/util-defaults-mode-node": "^4.0.26", + "@smithy/util-endpoints": "^3.0.7", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-retry": "^4.0.7", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/region-config-resolver": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.873.0.tgz", + "integrity": "sha512-q9sPoef+BBG6PJnc4x60vK/bfVwvRWsPgcoQyIra057S/QGjq5VkjvNk6H8xedf6vnKlXNBwq9BaANBXnldUJg==", + "requires": { + "@aws-sdk/types": "3.862.0", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/types": "^4.3.2", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.5", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/token-providers": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.876.0.tgz", + "integrity": "sha512-iU08kaQbhXnY0CC2TBcr7y/2PqPwZP2CTWX/Rbq0NvhOyteikfh7ASC+bRfLUp0XMSHKvSb+w2dh8a0lvx4oHg==", + "requires": { + "@aws-sdk/core": "3.876.0", + "@aws-sdk/nested-clients": "3.876.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/types": { + "version": "3.862.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.862.0.tgz", + "integrity": "sha512-Bei+RL0cDxxV+lW2UezLbCYYNeJm6Nzee0TpW0FfyTRBhH9C1XQh4+x+IClriXvgBnRquTMMYsmJfvx8iyLKrg==", + "requires": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/util-dynamodb": { + "version": "3.878.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-dynamodb/-/util-dynamodb-3.878.0.tgz", + "integrity": "sha512-YEoDb/sZlPjZ9KNjSS/OqywoyBAdAuHamiv21nQpHul30HBsEWbR8zsrcdSg7KCFigYz2RIga0IdSn1IKT+YSA==", + "requires": { + "tslib": "^2.6.2" + } + }, + "@aws-sdk/util-endpoints": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.873.0.tgz", + "integrity": "sha512-YByHrhjxYdjKRf/RQygRK1uh0As1FIi9+jXTcIEX/rBgN8mUByczr2u4QXBzw7ZdbdcOBMOkPnLRjNOWW1MkFg==", + "requires": { + "@aws-sdk/types": "3.862.0", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-endpoints": "^3.0.7", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/util-locate-window": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.873.0.tgz", + "integrity": "sha512-xcVhZF6svjM5Rj89T1WzkjQmrTF6dpR2UvIHPMTnSZoNe6CixejPZ6f0JJ2kAhO8H+dUHwNBlsUgOTIKiK/Syg==", + "requires": { + "tslib": "^2.6.2" + } + }, + "@aws-sdk/util-user-agent-browser": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.873.0.tgz", + "integrity": "sha512-AcRdbK6o19yehEcywI43blIBhOCSo6UgyWcuOJX5CFF8k39xm1ILCjQlRRjchLAxWrm0lU0Q7XV90RiMMFMZtA==", + "requires": { + "@aws-sdk/types": "3.862.0", + "@smithy/types": "^4.3.2", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/util-user-agent-node": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.876.0.tgz", + "integrity": "sha512-/ZIaeUt60JBdI0mNc7sZ8v3Tuzp8Pbe4gIAYnppGyF4KV8QA+Yu8tp2bGHfkKn150t1uvQ6P/4CwFfoGF34dzg==", + "requires": { + "@aws-sdk/middleware-user-agent": "3.876.0", + "@aws-sdk/types": "3.862.0", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + } + }, + "@aws-sdk/xml-builder": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.873.0.tgz", + "integrity": "sha512-kLO7k7cGJ6KaHiExSJWojZurF7SnGMDHXRuQunFnEoD0n1yB6Lqy/S/zHiQ7oJnBhPr9q0TW9qFkrsZb1Uc54w==", + "requires": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + } + }, + "@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "requires": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + } + }, + "@babel/compat-data": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "dev": true + }, + "@babel/core": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz", + "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", + "dev": true, + "requires": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.3", + "@babel/parser": "^7.28.3", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.3", + "@babel/types": "^7.28.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" + }, + "dependencies": { + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + } + } + }, + "@babel/generator": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "dev": true, + "requires": { + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + } + }, + "@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "requires": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "dependencies": { + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + } + } + }, + "@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true + }, + "@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "requires": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + } + }, + "@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "requires": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + } + }, + "@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true + }, + "@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true + }, + "@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true + }, + "@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true + }, + "@babel/helpers": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.3.tgz", + "integrity": "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==", + "dev": true, + "requires": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.2" + } + }, + "@babel/parser": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz", + "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", + "dev": true, + "requires": { + "@babel/types": "^7.28.2" + } + }, + "@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.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@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.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@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.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "requires": { + "@babel/helper-plugin-utils": "^7.27.1" + } + }, + "@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + } + }, + "@babel/traverse": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz", + "integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.3", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.2", + "debug": "^4.3.1" + } + }, + "@babel/types": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "dev": true, + "requires": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + } + }, + "@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 + }, + "@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.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "requires": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@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/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true + }, + "@jridgewell/trace-mapping": { + "version": "0.3.30", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", + "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", + "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" + } + }, + "@smithy/abort-controller": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.5.tgz", + "integrity": "sha512-jcrqdTQurIrBbUm4W2YdLVMQDoL0sA9DTxYd2s+R/y+2U9NLOP7Xf/YqfSg1FZhlZIYEnvk2mwbyvIfdLEPo8g==", + "requires": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + } + }, + "@smithy/config-resolver": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.1.5.tgz", + "integrity": "sha512-viuHMxBAqydkB0AfWwHIdwf/PRH2z5KHGUzqyRtS/Wv+n3IHI993Sk76VCA7dD/+GzgGOmlJDITfPcJC1nIVIw==", + "requires": { + "@smithy/node-config-provider": "^4.1.4", + "@smithy/types": "^4.3.2", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.5", + "tslib": "^2.6.2" + } + }, + "@smithy/core": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.9.0.tgz", + "integrity": "sha512-B/GknvCfS3llXd/b++hcrwIuqnEozQDnRL4sBmOac5/z/dr0/yG1PURNPOyU4Lsiy1IyTj8scPxVqRs5dYWf6A==", + "requires": { + "@smithy/middleware-serde": "^4.0.9", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-stream": "^4.2.4", + "@smithy/util-utf8": "^4.0.0", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + } + }, + "@smithy/credential-provider-imds": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.0.7.tgz", + "integrity": "sha512-dDzrMXA8d8riFNiPvytxn0mNwR4B3h8lgrQ5UjAGu6T9z/kRg/Xncf4tEQHE/+t25sY8IH3CowcmWi+1U5B1Gw==", + "requires": { + "@smithy/node-config-provider": "^4.1.4", + "@smithy/property-provider": "^4.0.5", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "tslib": "^2.6.2" + } + }, + "@smithy/fetch-http-handler": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.1.1.tgz", + "integrity": "sha512-61WjM0PWmZJR+SnmzaKI7t7G0UkkNFboDpzIdzSoy7TByUzlxo18Qlh9s71qug4AY4hlH/CwXdubMtkcNEb/sQ==", + "requires": { + "@smithy/protocol-http": "^5.1.3", + "@smithy/querystring-builder": "^4.0.5", + "@smithy/types": "^4.3.2", + "@smithy/util-base64": "^4.0.0", + "tslib": "^2.6.2" + } + }, + "@smithy/hash-node": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.0.5.tgz", + "integrity": "sha512-cv1HHkKhpyRb6ahD8Vcfb2Hgz67vNIXEp2vnhzfxLFGRukLCNEA5QdsorbUEzXma1Rco0u3rx5VTqbM06GcZqQ==", + "requires": { + "@smithy/types": "^4.3.2", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + } + }, + "@smithy/invalid-dependency": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.0.5.tgz", + "integrity": "sha512-IVnb78Qtf7EJpoEVo7qJ8BEXQwgC4n3igeJNNKEj/MLYtapnx8A67Zt/J3RXAj2xSO1910zk0LdFiygSemuLow==", + "requires": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + } + }, + "@smithy/is-array-buffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.0.0.tgz", + "integrity": "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==", + "requires": { + "tslib": "^2.6.2" + } + }, + "@smithy/middleware-content-length": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.0.5.tgz", + "integrity": "sha512-l1jlNZoYzoCC7p0zCtBDE5OBXZ95yMKlRlftooE5jPWQn4YBPLgsp+oeHp7iMHaTGoUdFqmHOPa8c9G3gBsRpQ==", + "requires": { + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + } + }, + "@smithy/middleware-endpoint": { + "version": "4.1.19", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.1.19.tgz", + "integrity": "sha512-EAlEPncqo03siNZJ9Tm6adKCQ+sw5fNU8ncxWwaH0zTCwMPsgmERTi6CEKaermZdgJb+4Yvh0NFm36HeO4PGgQ==", + "requires": { + "@smithy/core": "^3.9.0", + "@smithy/middleware-serde": "^4.0.9", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-middleware": "^4.0.5", + "tslib": "^2.6.2" + } + }, + "@smithy/middleware-retry": { + "version": "4.1.20", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.1.20.tgz", + "integrity": "sha512-T3maNEm3Masae99eFdx1Q7PIqBBEVOvRd5hralqKZNeIivnoGNx5OFtI3DiZ5gCjUkl0mNondlzSXeVxkinh7Q==", + "requires": { + "@smithy/node-config-provider": "^4.1.4", + "@smithy/protocol-http": "^5.1.3", + "@smithy/service-error-classification": "^4.0.7", + "@smithy/smithy-client": "^4.5.0", + "@smithy/types": "^4.3.2", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-retry": "^4.0.7", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "dependencies": { + "@smithy/service-error-classification": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.0.7.tgz", + "integrity": "sha512-XvRHOipqpwNhEjDf2L5gJowZEm5nsxC16pAZOeEcsygdjv9A2jdOh3YoDQvOXBGTsaJk6mNWtzWalOB9976Wlg==", + "requires": { + "@smithy/types": "^4.3.2" + } + } + } + }, + "@smithy/middleware-serde": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.0.9.tgz", + "integrity": "sha512-uAFFR4dpeoJPGz8x9mhxp+RPjo5wW0QEEIPPPbLXiRRWeCATf/Km3gKIVR5vaP8bN1kgsPhcEeh+IZvUlBv6Xg==", + "requires": { + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + } + }, + "@smithy/middleware-stack": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.0.5.tgz", + "integrity": "sha512-/yoHDXZPh3ocRVyeWQFvC44u8seu3eYzZRveCMfgMOBcNKnAmOvjbL9+Cp5XKSIi9iYA9PECUuW2teDAk8T+OQ==", + "requires": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + } + }, + "@smithy/node-config-provider": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.1.4.tgz", + "integrity": "sha512-+UDQV/k42jLEPPHSn39l0Bmc4sB1xtdI9Gd47fzo/0PbXzJ7ylgaOByVjF5EeQIumkepnrJyfx86dPa9p47Y+w==", + "requires": { + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + } + }, + "@smithy/node-http-handler": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.1.1.tgz", + "integrity": "sha512-RHnlHqFpoVdjSPPiYy/t40Zovf3BBHc2oemgD7VsVTFFZrU5erFFe0n52OANZZ/5sbshgD93sOh5r6I35Xmpaw==", + "requires": { + "@smithy/abort-controller": "^4.0.5", + "@smithy/protocol-http": "^5.1.3", + "@smithy/querystring-builder": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + } + }, + "@smithy/property-provider": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.0.5.tgz", + "integrity": "sha512-R/bswf59T/n9ZgfgUICAZoWYKBHcsVDurAGX88zsiUtOTA/xUAPyiT+qkNCPwFn43pZqN84M4MiUsbSGQmgFIQ==", + "requires": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + } + }, + "@smithy/protocol-http": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.3.tgz", + "integrity": "sha512-fCJd2ZR7D22XhDY0l+92pUag/7je2BztPRQ01gU5bMChcyI0rlly7QFibnYHzcxDvccMjlpM/Q1ev8ceRIb48w==", + "requires": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + } + }, + "@smithy/querystring-builder": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.0.5.tgz", + "integrity": "sha512-NJeSCU57piZ56c+/wY+AbAw6rxCCAOZLCIniRE7wqvndqxcKKDOXzwWjrY7wGKEISfhL9gBbAaWWgHsUGedk+A==", + "requires": { + "@smithy/types": "^4.3.2", + "@smithy/util-uri-escape": "^4.0.0", + "tslib": "^2.6.2" + } + }, + "@smithy/querystring-parser": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.0.5.tgz", + "integrity": "sha512-6SV7md2CzNG/WUeTjVe6Dj8noH32r4MnUeFKZrnVYsQxpGSIcphAanQMayi8jJLZAWm6pdM9ZXvKCpWOsIGg0w==", + "requires": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + } + }, + "@smithy/service-error-classification": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-2.1.5.tgz", + "integrity": "sha512-uBDTIBBEdAQryvHdc5W8sS5YX7RQzF683XrHePVdFmAgKiMofU15FLSM0/HU03hKTnazdNRFa0YHS7+ArwoUSQ==", + "requires": { + "@smithy/types": "^2.12.0" + }, + "dependencies": { + "@smithy/types": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-2.12.0.tgz", + "integrity": "sha512-QwYgloJ0sVNBeBuBs65cIkTbfzV/Q6ZNPCJ99EICFEdJYG50nGIY/uYXp+TbsdJReIuPr0a0kXmCvren3MbRRw==", + "requires": { + "tslib": "^2.6.2" + } + } + } + }, + "@smithy/shared-ini-file-loader": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.0.5.tgz", + "integrity": "sha512-YVVwehRDuehgoXdEL4r1tAAzdaDgaC9EQvhK0lEbfnbrd0bd5+CTQumbdPryX3J2shT7ZqQE+jPW4lmNBAB8JQ==", + "requires": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + } + }, + "@smithy/signature-v4": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.1.3.tgz", + "integrity": "sha512-mARDSXSEgllNzMw6N+mC+r1AQlEBO3meEAkR/UlfAgnMzJUB3goRBWgip1EAMG99wh36MDqzo86SfIX5Y+VEaw==", + "requires": { + "@smithy/is-array-buffer": "^4.0.0", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-uri-escape": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + } + }, + "@smithy/smithy-client": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.5.0.tgz", + "integrity": "sha512-ZSdE3vl0MuVbEwJBxSftm0J5nL/gw76xp5WF13zW9cN18MFuFXD5/LV0QD8P+sCU5bSWGyy6CTgUupE1HhOo1A==", + "requires": { + "@smithy/core": "^3.9.0", + "@smithy/middleware-endpoint": "^4.1.19", + "@smithy/middleware-stack": "^4.0.5", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "@smithy/util-stream": "^4.2.4", + "tslib": "^2.6.2" + } + }, + "@smithy/types": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.3.2.tgz", + "integrity": "sha512-QO4zghLxiQ5W9UZmX2Lo0nta2PuE1sSrXUYDoaB6HMR762C0P7v/HEPHf6ZdglTVssJG1bsrSBxdc3quvDSihw==", + "requires": { + "tslib": "^2.6.2" + } + }, + "@smithy/url-parser": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.0.5.tgz", + "integrity": "sha512-j+733Um7f1/DXjYhCbvNXABV53NyCRRA54C7bNEIxNPs0YjfRxeMKjjgm2jvTYrciZyCjsicHwQ6Q0ylo+NAUw==", + "requires": { + "@smithy/querystring-parser": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + } + }, + "@smithy/util-base64": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.0.0.tgz", + "integrity": "sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==", + "requires": { + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + } + }, + "@smithy/util-body-length-browser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.0.0.tgz", + "integrity": "sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==", + "requires": { + "tslib": "^2.6.2" + } + }, + "@smithy/util-body-length-node": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.0.0.tgz", + "integrity": "sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==", + "requires": { + "tslib": "^2.6.2" + } + }, + "@smithy/util-buffer-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.0.0.tgz", + "integrity": "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==", + "requires": { + "@smithy/is-array-buffer": "^4.0.0", + "tslib": "^2.6.2" + } + }, + "@smithy/util-config-provider": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.0.0.tgz", + "integrity": "sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==", + "requires": { + "tslib": "^2.6.2" + } + }, + "@smithy/util-defaults-mode-browser": { + "version": "4.0.27", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.27.tgz", + "integrity": "sha512-i/Fu6AFT5014VJNgWxKomBJP/GB5uuOsM4iHdcmplLm8B1eAqnRItw4lT2qpdO+mf+6TFmf6dGcggGLAVMZJsQ==", + "requires": { + "@smithy/property-provider": "^4.0.5", + "@smithy/smithy-client": "^4.5.0", + "@smithy/types": "^4.3.2", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "@smithy/util-defaults-mode-node": { + "version": "4.0.27", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.27.tgz", + "integrity": "sha512-3W0qClMyxl/ELqTA39aNw1N+pN0IjpXT7lPFvZ8zTxqVFP7XCpACB9QufmN4FQtd39xbgS7/Lekn7LmDa63I5w==", + "requires": { + "@smithy/config-resolver": "^4.1.5", + "@smithy/credential-provider-imds": "^4.0.7", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/property-provider": "^4.0.5", + "@smithy/smithy-client": "^4.5.0", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + } + }, + "@smithy/util-endpoints": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.0.7.tgz", + "integrity": "sha512-klGBP+RpBp6V5JbrY2C/VKnHXn3d5V2YrifZbmMY8os7M6m8wdYFoO6w/fe5VkP+YVwrEktW3IWYaSQVNZJ8oQ==", + "requires": { + "@smithy/node-config-provider": "^4.1.4", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + } + }, + "@smithy/util-hex-encoding": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.0.0.tgz", + "integrity": "sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==", + "requires": { + "tslib": "^2.6.2" + } + }, + "@smithy/util-middleware": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.0.5.tgz", + "integrity": "sha512-N40PfqsZHRSsByGB81HhSo+uvMxEHT+9e255S53pfBw/wI6WKDI7Jw9oyu5tJTLwZzV5DsMha3ji8jk9dsHmQQ==", + "requires": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + } + }, + "@smithy/util-retry": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.0.7.tgz", + "integrity": "sha512-TTO6rt0ppK70alZpkjwy+3nQlTiqNfoXja+qwuAchIEAIoSZW8Qyd76dvBv3I5bCpE38APafG23Y/u270NspiQ==", + "requires": { + "@smithy/service-error-classification": "^4.0.7", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "dependencies": { + "@smithy/service-error-classification": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.0.7.tgz", + "integrity": "sha512-XvRHOipqpwNhEjDf2L5gJowZEm5nsxC16pAZOeEcsygdjv9A2jdOh3YoDQvOXBGTsaJk6mNWtzWalOB9976Wlg==", + "requires": { + "@smithy/types": "^4.3.2" + } + } + } + }, + "@smithy/util-stream": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.2.4.tgz", + "integrity": "sha512-vSKnvNZX2BXzl0U2RgCLOwWaAP9x/ddd/XobPK02pCbzRm5s55M53uwb1rl/Ts7RXZvdJZerPkA+en2FDghLuQ==", + "requires": { + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/types": "^4.3.2", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + } + }, + "@smithy/util-uri-escape": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.0.0.tgz", + "integrity": "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==", + "requires": { + "tslib": "^2.6.2" + } + }, + "@smithy/util-utf8": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", + "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", + "requires": { + "@smithy/util-buffer-from": "^4.0.0", + "tslib": "^2.6.2" + } + }, + "@smithy/util-waiter": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.0.7.tgz", + "integrity": "sha512-mYqtQXPmrwvUljaHyGxYUIIRI3qjBTEb/f5QFi3A6VlxhpmZd5mWXn9W+qUkf2pVE1Hv3SqxefiZOPGdxmO64A==", + "requires": { + "@smithy/abort-controller": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + } + }, + "@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.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "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.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "requires": { + "@babel/types": "^7.28.2" + } + }, + "@types/cls-hooked": { + "version": "4.3.9", + "resolved": "https://registry.npmjs.org/@types/cls-hooked/-/cls-hooked-4.3.9.tgz", + "integrity": "sha512-CMtHMz6Q/dkfcHarq9nioXH8BDPP+v5xvd+N90lBQ2bdmu06UvnLDqxTKoOJzz4SzIwb/x9i4UXGAAcnUDuIvg==", + "requires": { + "@types/node": "*" + } + }, + "@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/node": { + "version": "24.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", + "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", + "requires": { + "undici-types": "~7.10.0" + } + }, + "@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/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==" + }, + "@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 + }, + "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" + } + }, + "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-hook-jl": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/async-hook-jl/-/async-hook-jl-1.7.6.tgz", + "integrity": "sha512-gFaHkFfSxTjvoxDMYqDuGHlcRyUuamF8s+ZTtJdDzqjws4mCt7v0vuV79/E2Wr2/riMQgtG4/yUtXWs1gZ7JMg==", + "requires": { + "stack-chain": "^1.3.7" + } + }, + "atomic-batcher": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/atomic-batcher/-/atomic-batcher-1.0.2.tgz", + "integrity": "sha512-EFGCRj4kLX1dHv1cDzTk+xbjBFj1GnJDpui52YmEcxxHHEWjYyT6l51U7n6WQ28osZH4S9gSybxe56Vm7vB61Q==" + }, + "aws-xray-sdk-core": { + "version": "3.10.3", + "resolved": "https://registry.npmjs.org/aws-xray-sdk-core/-/aws-xray-sdk-core-3.10.3.tgz", + "integrity": "sha512-bltsLAr4juMJJ2tT5/L/CtwUGIvHihtPe6SO/z3jjOD73PHhOYxcuwCMFFyTbTy5S4WThJO32oZk7r+pg3ZoCQ==", + "requires": { + "@aws-sdk/types": "^3.4.1", + "@smithy/service-error-classification": "^2.0.4", + "@types/cls-hooked": "^4.3.3", + "atomic-batcher": "^1.0.2", + "cls-hooked": "^4.2.2", + "semver": "^7.5.3" + } + }, + "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" + } + }, + "semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true + } + } + }, + "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.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "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 + }, + "bowser": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.12.1.tgz", + "integrity": "sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw==" + }, + "brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "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.25.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.4.tgz", + "integrity": "sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg==", + "dev": true, + "requires": { + "caniuse-lite": "^1.0.30001737", + "electron-to-chromium": "^1.5.211", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + } + }, + "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==", + "dev": true + }, + "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.30001739", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001739.tgz", + "integrity": "sha512-y+j60d6ulelrNSwpPyrHdl+9mJnQzHBr08xm48Qno0nSk4h3Qojh+ziv2qE6rXf4k3tadF4o1J/1tAbVm1NtnA==", + "dev": true + }, + "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.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "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" + } + }, + "cls-hooked": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/cls-hooked/-/cls-hooked-4.2.2.tgz", + "integrity": "sha512-J4Xj5f5wq/4jAvcdgoGsL3G103BtWpZrMo8NEinRltN+xpTZdI+M38pyQqhuFU/P792xkMFvnKSf+Lm81U1bxw==", + "requires": { + "async-hook-jl": "^1.7.6", + "emitter-listener": "^1.0.1", + "semver": "^5.4.1" + }, + "dependencies": { + "semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==" + } + } + }, + "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 + }, + "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" + } + }, + "cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "requires": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + } + }, + "debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "requires": { + "ms": "^2.1.3" + } + }, + "dedent": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", + "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", + "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-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.5.211", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.211.tgz", + "integrity": "sha512-IGBvimJkotaLzFnwIVgW9/UD/AOJ2tByUmeOrtqBfACSbAw5b1G0XpvdaieKyc7ULmbwXVx+4e4Be8pOPBrYkw==", + "dev": true + }, + "emitter-listener": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/emitter-listener/-/emitter-listener-1.1.2.tgz", + "integrity": "sha512-Bt1sBAGFHY9DKY+4/2cV6izcKJUf5T7/gkdmkxzX/qv9CcGH8xSwVRW5mtX03SWJtRTWSOpzCuWN9rBFYZepZQ==", + "requires": { + "shimmer": "^1.2.0" + } + }, + "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 + }, + "fast-xml-parser": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", + "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", + "requires": { + "strnum": "^2.1.0" + } + }, + "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.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "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" + } + }, + "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.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "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" + } + }, + "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.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "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" + } + }, + "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" + } + }, + "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.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "requires": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + } + }, + "jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "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" + } + }, + "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" + } + }, + "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.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + }, + "mnemonist": { + "version": "0.38.3", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.38.3.tgz", + "integrity": "sha512-2K9QYubXx/NAjv4VLq1d1Ly8pWNC5L3BrixtdkyTegXWJIqY+zLNDhhX/A+ZwWt70tB1S8H4BE8FLYEFyNoOBw==", + "requires": { + "obliterator": "^1.6.1" + } + }, + "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.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "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" + } + }, + "obliterator": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-1.6.1.tgz", + "integrity": "sha512-9WXswnqINnnhOG/5SLimUlzuU1hFJUc8zkwyD59Sd+dPOMf05PmnYG/d6Q7HZ+KmgkZJa1PxRso6QdM3sTNHig==" + }, + "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.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true + }, + "pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "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.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "requires": { + "is-core-module": "^2.16.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.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true + }, + "semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==" + }, + "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 + }, + "shimmer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz", + "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==" + }, + "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==", + "dev": true + }, + "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" + } + }, + "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-chain": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/stack-chain/-/stack-chain-1.3.7.tgz", + "integrity": "sha512-D8cWtWVdIe/jBA7v5p5Hwl5yOSOrmZPWDPe2KxQ5UAGD+nxbxU0lKXA4h85Ta6+qgdKVL3vUxsbIZjc1kBG7ug==" + }, + "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 + }, + "strnum": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", + "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==" + }, + "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-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" + } + }, + "tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "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 + }, + "undici-types": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==" + }, + "update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "dev": true, + "requires": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + } + }, + "uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==" + }, + "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 + }, + "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/src/applications/lambda/petstatusupdater-node/package.json b/src/applications/lambda/petstatusupdater-node/package.json new file mode 100644 index 00000000..b8eb5e1e --- /dev/null +++ b/src/applications/lambda/petstatusupdater-node/package.json @@ -0,0 +1,19 @@ +{ + "name": "petstatusupdater", + "version": "1.0.0", + "description": "Updates pet availability", + "main": "index.js", + "scripts": { + "test": "jest" + }, + "author": "", + "license": "Apache-2.0", + "dependencies": { + "aws-xray-sdk-core": "^3.10.3", + "@aws-sdk/client-dynamodb": "^3.0.0", + "@aws-sdk/lib-dynamodb": "^3.0.0" + }, + "devDependencies": { + "jest": "^29.0.0" + } +} diff --git a/src/applications/lambda/pipeline-retry-python/index.py b/src/applications/lambda/pipeline-retry-python/index.py new file mode 100644 index 00000000..a81e9ba6 --- /dev/null +++ b/src/applications/lambda/pipeline-retry-python/index.py @@ -0,0 +1,105 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +import json +import logging + +import boto3 + +# Configure logging +logger = logging.getLogger() +logger.setLevel(logging.INFO) + +client = boto3.client("codepipeline") + + +def handler(event, context): + logger.info(f"Received event: {json.dumps(event, indent=2)}") + + try: + detail = event["detail"] + pipeline_name = detail["pipeline"] + execution_id = detail["execution-id"] + + logger.info( + f"Processing pipeline failure - Pipeline: {pipeline_name}, " + f"Execution: {execution_id}", + ) + + # Get pipeline execution details + logger.info("Getting pipeline execution details...") + response = client.get_pipeline_execution( + pipelineName=pipeline_name, + pipelineExecutionId=execution_id, + ) + + execution_details = response["pipelineExecution"] + logger.info(f"Execution status: {execution_details.get('status')}") + + # Get pipeline state to find failed stages + logger.info("Getting pipeline state...") + state_response = client.get_pipeline_state(name=pipeline_name) + + failed_stages = [] + for stage in state_response["stageStates"]: + if stage.get("latestExecution", {}).get("status") == "Failed": + failed_stages.append(stage["stageName"]) + logger.info(f"Found failed stage: {stage['stageName']}") + + if not failed_stages: + logger.warning("No failed stages found to retry") + return {"statusCode": 200, "message": "No failed stages to retry"} + + # Check if build stage failed (our target for retry) + if "build" not in failed_stages: + logger.info("Build stage did not fail, skipping retry") + return {"statusCode": 200, "message": "Build stage did not fail"} + + # Simple retry limit check using execution history + logger.info("Checking retry count...") + executions_response = client.list_pipeline_executions( + pipelineName=pipeline_name, + maxResults=10, + ) + + # Count recent failed executions as a proxy for retry attempts + recent_failures = 0 + for exec_summary in executions_response["pipelineExecutionSummaries"]: + if exec_summary["status"] == "Failed": + recent_failures += 1 + else: + break # Stop at first non-failed execution + + logger.info(f"Recent consecutive failures: {recent_failures}") + + if recent_failures >= 3: # Allow 2 retries (3 total attempts) + logger.warning( + f"Max retries reached ({recent_failures} consecutive failures)", + ) + return {"statusCode": 200, "message": "Max retries reached"} + + # Attempt to retry the build stage + logger.info(f"Attempting to retry build stage for pipeline: {pipeline_name}") + + retry_response = client.retry_stage_execution( + pipelineName=pipeline_name, + pipelineExecutionId=execution_id, + stageName="build", + retryMode="FAILED_ACTIONS", + ) + + logger.info( + f"Successfully triggered retry. " + f"Response: {json.dumps(retry_response, default=str)}", + ) + + return { + "statusCode": 200, + "message": "Retry triggered successfully", + "pipelineName": pipeline_name, + "executionId": execution_id, + "retriedStage": "build", + } + + except Exception as error: + logger.error(f"Failed to process retry: {str(error)}", exc_info=True) + return {"statusCode": 500, "message": f"Failed to retry: {str(error)}"} diff --git a/src/applications/lambda/traffic-generator-node/index.js b/src/applications/lambda/traffic-generator-node/index.js new file mode 100644 index 00000000..b36c83b8 --- /dev/null +++ b/src/applications/lambda/traffic-generator-node/index.js @@ -0,0 +1,79 @@ +const { LambdaClient, InvokeCommand } = require('@aws-sdk/client-lambda'); +const lambda = new LambdaClient({}); + +exports.handler = async (event) => { + console.log('Traffic generator started:', JSON.stringify(event)); + + const canaryFunctionArn = process.env.CANARY_FUNCTION_ARN; + const concurrentUsers = Number.parseInt(process.env.CONCURRENT_USERS || '50'); + + console.log(`Generating traffic for ${concurrentUsers} concurrent users`); + console.log(`Invoking canary function: ${canaryFunctionArn}`); + + const startTime = Date.now(); + const promises = []; + + // Create array of user IDs + const userIds = Array.from({ length: concurrentUsers }, (_, index) => `user${String(index + 1).padStart(4, '0')}`); + + // Invoke canary function for each user concurrently + for (let index = 0; index < concurrentUsers; index++) { + const userId = userIds[index]; + + const invokeParameters = { + FunctionName: canaryFunctionArn, + InvocationType: 'Event', // Async invocation + Payload: JSON.stringify({ + userId: userId, + invocationId: `${Date.now()}-${index}`, + source: 'traffic-generator', + timestamp: new Date().toISOString(), + }), + }; + + console.log(`Invoking canary for user: ${userId}`); + const command = new InvokeCommand(invokeParameters); + promises.push( + lambda + .send(command) + .then((result) => { + console.log(`Successfully invoked canary for user ${userId}`); + return { userId, success: true, result }; + }) + .catch((error) => { + console.error(`Failed to invoke canary for user ${userId}:`, error); + return { userId, success: false, error: error.message }; + }), + ); + } + + // Wait for all invocations to complete + console.log(`Waiting for \${concurrentUsers} canary invocations to complete...`); + const results = await Promise.allSettled(promises); + + const endTime = Date.now(); + const duration = endTime - startTime; + + // Analyze results + const successful = results.filter((r) => r.status === 'fulfilled' && r.value.success).length; + const failed = results.filter( + (r) => r.status === 'rejected' || (r.status === 'fulfilled' && !r.value.success), + ).length; + + console.log(`Traffic generation completed in ${duration}ms`); + console.log(`Successful invocations: ${successful}`); + console.log(`Failed invocations: ${failed}`); + + // Return summary + return { + statusCode: 200, + body: { + message: 'Traffic generation completed', + totalUsers: concurrentUsers, + successful, + failed, + duration: `${duration}ms`, + timestamp: new Date().toISOString(), + }, + }; +}; diff --git a/src/applications/lambda/traffic-generator-node/index.test.js b/src/applications/lambda/traffic-generator-node/index.test.js new file mode 100644 index 00000000..ffe27670 --- /dev/null +++ b/src/applications/lambda/traffic-generator-node/index.test.js @@ -0,0 +1,63 @@ +// Mock AWS SDK modules before requiring the handler +jest.mock('@aws-sdk/client-lambda'); + +describe('Traffic Generator Lambda', () => { + let handler; + let mockSend; + + beforeAll(() => { + // Set up mocks + mockSend = jest.fn(); + + require('@aws-sdk/client-lambda').LambdaClient = jest.fn(() => ({ + send: mockSend + })); + require('@aws-sdk/client-lambda').InvokeCommand = jest.fn(); + + // Now require the handler + handler = require('./index').handler; + }); + + beforeEach(() => { + process.env.CANARY_FUNCTION_ARN = 'arn:aws:lambda:us-east-1:123456789012:function:test-canary'; + process.env.CONCURRENT_USERS = '5'; // Use smaller number for faster tests + jest.clearAllMocks(); + mockSend.mockResolvedValue({ StatusCode: 202 }); + }); + + test('should return success response with correct structure', async () => { + const event = {}; + + const result = await handler(event); + + expect(result.statusCode).toBe(200); + expect(result.body).toHaveProperty('totalUsers'); + expect(result.body).toHaveProperty('message'); + expect(result.body.message).toBe('Traffic generation completed'); + }); + + test('should use default concurrent users when not specified', async () => { + delete process.env.CONCURRENT_USERS; + const event = {}; + + const result = await handler(event); + + expect(result.statusCode).toBe(200); + expect(result.body.totalUsers).toBe(50); // Default value + }); + + test('should handle environment variables correctly', () => { + process.env.CONCURRENT_USERS = '25'; + const users = Number.parseInt(process.env.CONCURRENT_USERS || '50'); + expect(users).toBe(25); + }); + + test('should generate user IDs correctly', () => { + const concurrentUsers = 3; + const userIds = Array.from({ length: concurrentUsers }, (_, index) => + `user${String(index + 1).padStart(4, '0')}` + ); + + expect(userIds).toEqual(['user0001', 'user0002', 'user0003']); + }); +}); \ No newline at end of file diff --git a/src/applications/lambda/traffic-generator-node/package-lock.json b/src/applications/lambda/traffic-generator-node/package-lock.json new file mode 100644 index 00000000..110ba0f2 --- /dev/null +++ b/src/applications/lambda/traffic-generator-node/package-lock.json @@ -0,0 +1,5014 @@ +{ + "name": "traffic-generator-node", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "traffic-generator-node", + "version": "1.0.0", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-lambda": "^3.0.0" + }, + "devDependencies": { + "jest": "^29.0.0" + } + }, + "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, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-lambda": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-lambda/-/client-lambda-3.876.0.tgz", + "integrity": "sha512-dOzCiZFAG5mihv/ZVxDJmgdH6A6MoF8GP8QsWN0BuvkISPtKXE7IPJivCCh7BmcTDqH5iPje+SW+lmdrhYJ1qQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.876.0", + "@aws-sdk/credential-provider-node": "3.876.0", + "@aws-sdk/middleware-host-header": "3.873.0", + "@aws-sdk/middleware-logger": "3.876.0", + "@aws-sdk/middleware-recursion-detection": "3.873.0", + "@aws-sdk/middleware-user-agent": "3.876.0", + "@aws-sdk/region-config-resolver": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-endpoints": "3.873.0", + "@aws-sdk/util-user-agent-browser": "3.873.0", + "@aws-sdk/util-user-agent-node": "3.876.0", + "@smithy/config-resolver": "^4.1.5", + "@smithy/core": "^3.8.0", + "@smithy/eventstream-serde-browser": "^4.0.5", + "@smithy/eventstream-serde-config-resolver": "^4.1.3", + "@smithy/eventstream-serde-node": "^4.0.5", + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/hash-node": "^4.0.5", + "@smithy/invalid-dependency": "^4.0.5", + "@smithy/middleware-content-length": "^4.0.5", + "@smithy/middleware-endpoint": "^4.1.18", + "@smithy/middleware-retry": "^4.1.19", + "@smithy/middleware-serde": "^4.0.9", + "@smithy/middleware-stack": "^4.0.5", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/protocol-http": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.26", + "@smithy/util-defaults-mode-node": "^4.0.26", + "@smithy/util-endpoints": "^3.0.7", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-retry": "^4.0.7", + "@smithy/util-stream": "^4.2.4", + "@smithy/util-utf8": "^4.0.0", + "@smithy/util-waiter": "^4.0.7", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/client-sso": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-sso/-/client-sso-3.876.0.tgz", + "integrity": "sha512-Vf0PMF7HVpvllrfPODnBZmlz6kT/y2AvOt1RQG3+qD0VrHWzShc5nwgRZ+yyP3xkKVhZsQ3sJapfZTFnjqMOYA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.876.0", + "@aws-sdk/middleware-host-header": "3.873.0", + "@aws-sdk/middleware-logger": "3.876.0", + "@aws-sdk/middleware-recursion-detection": "3.873.0", + "@aws-sdk/middleware-user-agent": "3.876.0", + "@aws-sdk/region-config-resolver": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-endpoints": "3.873.0", + "@aws-sdk/util-user-agent-browser": "3.873.0", + "@aws-sdk/util-user-agent-node": "3.876.0", + "@smithy/config-resolver": "^4.1.5", + "@smithy/core": "^3.8.0", + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/hash-node": "^4.0.5", + "@smithy/invalid-dependency": "^4.0.5", + "@smithy/middleware-content-length": "^4.0.5", + "@smithy/middleware-endpoint": "^4.1.18", + "@smithy/middleware-retry": "^4.1.19", + "@smithy/middleware-serde": "^4.0.9", + "@smithy/middleware-stack": "^4.0.5", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/protocol-http": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.26", + "@smithy/util-defaults-mode-node": "^4.0.26", + "@smithy/util-endpoints": "^3.0.7", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-retry": "^4.0.7", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.876.0.tgz", + "integrity": "sha512-sVFBFkdoPOPyY13NaXO1E/R9O5J6ixzHnnRbqrbXYM2QQgLNPTKIiRtmVEuVoFV9YULg+/aKm7caix8m468y9w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@aws-sdk/xml-builder": "3.873.0", + "@smithy/core": "^3.8.0", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/property-provider": "^4.0.5", + "@smithy/protocol-http": "^5.1.3", + "@smithy/signature-v4": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-utf8": "^4.0.0", + "fast-xml-parser": "5.2.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.876.0.tgz", + "integrity": "sha512-cof7lwp2AlrAfRs0pt4W2KMS2VMBvEmpcti1UOFfSJIqkn+cyJliMJ8LHg22GI+kUexjvxdAqSbf3M7OHvEW+w==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.876.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.876.0.tgz", + "integrity": "sha512-wzmef2NBp2+X1l8D4Q8hx1G8oI3+WdvLdPev9VnVpRYZxYGRWVPl++wvCBsCn/ZL0mdWopPkhHA3kFexQhMzvg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.876.0", + "@aws-sdk/types": "3.862.0", + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/property-provider": "^4.0.5", + "@smithy/protocol-http": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/util-stream": "^4.2.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.876.0.tgz", + "integrity": "sha512-JHbW6fqnJsVjGHCyko7B0NVPT1nEAPxkM3CGjUcVGsHgJBkxOLVCMQqTRyHcDdeHR2qeojlLoOHRz97xIHQjYw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.876.0", + "@aws-sdk/credential-provider-env": "3.876.0", + "@aws-sdk/credential-provider-http": "3.876.0", + "@aws-sdk/credential-provider-process": "3.876.0", + "@aws-sdk/credential-provider-sso": "3.876.0", + "@aws-sdk/credential-provider-web-identity": "3.876.0", + "@aws-sdk/nested-clients": "3.876.0", + "@aws-sdk/types": "3.862.0", + "@smithy/credential-provider-imds": "^4.0.7", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.876.0.tgz", + "integrity": "sha512-eHbNt1+Hi43e8ANnwf6toapLSxfMiyGq459y3Uh6i7NBOiWWKEsOVcgOfUC3RCoqeikxovt1tFM2cEElWUIOhg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "3.876.0", + "@aws-sdk/credential-provider-http": "3.876.0", + "@aws-sdk/credential-provider-ini": "3.876.0", + "@aws-sdk/credential-provider-process": "3.876.0", + "@aws-sdk/credential-provider-sso": "3.876.0", + "@aws-sdk/credential-provider-web-identity": "3.876.0", + "@aws-sdk/types": "3.862.0", + "@smithy/credential-provider-imds": "^4.0.7", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.876.0.tgz", + "integrity": "sha512-SMX4OlHvspu3gF4hxe7WAnZFhxpiCye+WlBSVoWfW/i9XNhtrZS1JMr29MK34GlCTk9qO7FlRwds/Z5k7xPpHg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.876.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.876.0.tgz", + "integrity": "sha512-iP5dz9XqwePbgnh7Bdrq5e1319JpCRKLyomUfHH1XVeXkIHmwIJdmTj1Upeo1J8L/5cLHmhXAN6CTN11bLo8SA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-sso": "3.876.0", + "@aws-sdk/core": "3.876.0", + "@aws-sdk/token-providers": "3.876.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.876.0.tgz", + "integrity": "sha512-q/XSCP1uae5aB9veM8zcm6Gqu6A4ckX9ZbhHgCzURXVJDwp+nINW1hM9vppMjGw3ND9Ibx/adR+KfTI0TDMzqw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.876.0", + "@aws-sdk/nested-clients": "3.876.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.873.0.tgz", + "integrity": "sha512-KZ/W1uruWtMOs7D5j3KquOxzCnV79KQW9MjJFZM/M0l6KI8J6V3718MXxFHsTjUE4fpdV6SeCNLV1lwGygsjJA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.876.0.tgz", + "integrity": "sha512-cpWJhOuMSyz9oV25Z/CMHCBTgafDCbv7fHR80nlRrPdPZ8ETNsahwRgltXP1QJJ8r3X/c1kwpOR7tc+RabVzNA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.873.0.tgz", + "integrity": "sha512-OtgY8EXOzRdEWR//WfPkA/fXl0+WwE8hq0y9iw2caNyKPtca85dzrrZWnPqyBK/cpImosrpR1iKMYr41XshsCg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.876.0.tgz", + "integrity": "sha512-FR+8INfnbNv32QDQ5szxkWX6mB/QgezfNyx8LnAh1ErISZMmEFBxXXir+ZOfuV8vsmal1a6cy9qmnMNDaNnaNQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.876.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-endpoints": "3.873.0", + "@smithy/core": "^3.8.0", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.876.0.tgz", + "integrity": "sha512-R4TZrkM2gUElTsotk8mt3y7iLG8TNi1LL1wgVdEEWSLOYTaFyglGdoNBMtEeP7lmXilaTy00AbYF6BakJvSTHg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "3.876.0", + "@aws-sdk/middleware-host-header": "3.873.0", + "@aws-sdk/middleware-logger": "3.876.0", + "@aws-sdk/middleware-recursion-detection": "3.873.0", + "@aws-sdk/middleware-user-agent": "3.876.0", + "@aws-sdk/region-config-resolver": "3.873.0", + "@aws-sdk/types": "3.862.0", + "@aws-sdk/util-endpoints": "3.873.0", + "@aws-sdk/util-user-agent-browser": "3.873.0", + "@aws-sdk/util-user-agent-node": "3.876.0", + "@smithy/config-resolver": "^4.1.5", + "@smithy/core": "^3.8.0", + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/hash-node": "^4.0.5", + "@smithy/invalid-dependency": "^4.0.5", + "@smithy/middleware-content-length": "^4.0.5", + "@smithy/middleware-endpoint": "^4.1.18", + "@smithy/middleware-retry": "^4.1.19", + "@smithy/middleware-serde": "^4.0.9", + "@smithy/middleware-stack": "^4.0.5", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/protocol-http": "^5.1.3", + "@smithy/smithy-client": "^4.4.10", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-body-length-node": "^4.0.0", + "@smithy/util-defaults-mode-browser": "^4.0.26", + "@smithy/util-defaults-mode-node": "^4.0.26", + "@smithy/util-endpoints": "^3.0.7", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-retry": "^4.0.7", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.873.0.tgz", + "integrity": "sha512-q9sPoef+BBG6PJnc4x60vK/bfVwvRWsPgcoQyIra057S/QGjq5VkjvNk6H8xedf6vnKlXNBwq9BaANBXnldUJg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/types": "^4.3.2", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.876.0.tgz", + "integrity": "sha512-iU08kaQbhXnY0CC2TBcr7y/2PqPwZP2CTWX/Rbq0NvhOyteikfh7ASC+bRfLUp0XMSHKvSb+w2dh8a0lvx4oHg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "3.876.0", + "@aws-sdk/nested-clients": "3.876.0", + "@aws-sdk/types": "3.862.0", + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.862.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.862.0.tgz", + "integrity": "sha512-Bei+RL0cDxxV+lW2UezLbCYYNeJm6Nzee0TpW0FfyTRBhH9C1XQh4+x+IClriXvgBnRquTMMYsmJfvx8iyLKrg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.873.0.tgz", + "integrity": "sha512-YByHrhjxYdjKRf/RQygRK1uh0As1FIi9+jXTcIEX/rBgN8mUByczr2u4QXBzw7ZdbdcOBMOkPnLRjNOWW1MkFg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-endpoints": "^3.0.7", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.873.0.tgz", + "integrity": "sha512-xcVhZF6svjM5Rj89T1WzkjQmrTF6dpR2UvIHPMTnSZoNe6CixejPZ6f0JJ2kAhO8H+dUHwNBlsUgOTIKiK/Syg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.873.0.tgz", + "integrity": "sha512-AcRdbK6o19yehEcywI43blIBhOCSo6UgyWcuOJX5CFF8k39xm1ILCjQlRRjchLAxWrm0lU0Q7XV90RiMMFMZtA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "3.862.0", + "@smithy/types": "^4.3.2", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.876.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.876.0.tgz", + "integrity": "sha512-/ZIaeUt60JBdI0mNc7sZ8v3Tuzp8Pbe4gIAYnppGyF4KV8QA+Yu8tp2bGHfkKn150t1uvQ6P/4CwFfoGF34dzg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "3.876.0", + "@aws-sdk/types": "3.862.0", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.873.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.873.0.tgz", + "integrity": "sha512-kLO7k7cGJ6KaHiExSJWojZurF7SnGMDHXRuQunFnEoD0n1yB6Lqy/S/zHiQ7oJnBhPr9q0TW9qFkrsZb1Uc54w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.3.tgz", + "integrity": "sha512-yDBHV9kQNcr2/sUr9jghVyz9C3Y5G2zUM2H2lo+9mKv4sFgbA8s8Z9t8D1jiTkGoO/NoIfKMyKWr4s6CN23ZwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.3", + "@babel/parser": "^7.28.3", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.3", + "@babel/types": "^7.28.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.28.3", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", + "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.3", + "@babel/types": "^7.28.2", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.3.tgz", + "integrity": "sha512-PTNtvUQihsAsDHMOP5pfobP8C6CM4JWXmP8DrEIt46c3r2bf87Ua1zoqevsMo9g+tWDwgWrFP5EIxuBx5RudAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.3.tgz", + "integrity": "sha512-7+Ey1mAgYqFAx2h0RuoxcQT5+MlG3GTV0TQrgr7/ZliKsm/MNDxVVutlWaziMq7wJNAz8MTqz55XLpWvva6StA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.3.tgz", + "integrity": "sha512-7w4kZYHneL3A6NP2nxzHvT3HCZ7puDZZjFMqDpBPECub79sTtSO5CGXDkKrTQq8ksAwfD/XI2MRFX23njdDaIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.3", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.3", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.2", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "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, + "license": "MIT" + }, + "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, + "license": "ISC", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.30", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.30.tgz", + "integrity": "sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@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, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.0" + } + }, + "node_modules/@smithy/abort-controller": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/abort-controller/-/abort-controller-4.0.5.tgz", + "integrity": "sha512-jcrqdTQurIrBbUm4W2YdLVMQDoL0sA9DTxYd2s+R/y+2U9NLOP7Xf/YqfSg1FZhlZIYEnvk2mwbyvIfdLEPo8g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/config-resolver": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.1.5.tgz", + "integrity": "sha512-viuHMxBAqydkB0AfWwHIdwf/PRH2z5KHGUzqyRtS/Wv+n3IHI993Sk76VCA7dD/+GzgGOmlJDITfPcJC1nIVIw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.4", + "@smithy/types": "^4.3.2", + "@smithy/util-config-provider": "^4.0.0", + "@smithy/util-middleware": "^4.0.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.9.0.tgz", + "integrity": "sha512-B/GknvCfS3llXd/b++hcrwIuqnEozQDnRL4sBmOac5/z/dr0/yG1PURNPOyU4Lsiy1IyTj8scPxVqRs5dYWf6A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/middleware-serde": "^4.0.9", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-body-length-browser": "^4.0.0", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-stream": "^4.2.4", + "@smithy/util-utf8": "^4.0.0", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.0.7.tgz", + "integrity": "sha512-dDzrMXA8d8riFNiPvytxn0mNwR4B3h8lgrQ5UjAGu6T9z/kRg/Xncf4tEQHE/+t25sY8IH3CowcmWi+1U5B1Gw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.4", + "@smithy/property-provider": "^4.0.5", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-codec": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.0.5.tgz", + "integrity": "sha512-miEUN+nz2UTNoRYRhRqVTJCx7jMeILdAurStT2XoS+mhokkmz1xAPp95DFW9Gxt4iF2VBqpeF9HbTQ3kY1viOA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.3.2", + "@smithy/util-hex-encoding": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-browser": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.0.5.tgz", + "integrity": "sha512-LCUQUVTbM6HFKzImYlSB9w4xafZmpdmZsOh9rIl7riPC3osCgGFVP+wwvYVw6pXda9PPT9TcEZxaq3XE81EdJQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-config-resolver": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.1.3.tgz", + "integrity": "sha512-yTTzw2jZjn/MbHu1pURbHdpjGbCuMHWncNBpJnQAPxOVnFUAbSIUSwafiphVDjNV93TdBJWmeVAds7yl5QCkcA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-node": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.0.5.tgz", + "integrity": "sha512-lGS10urI4CNzz6YlTe5EYG0YOpsSp3ra8MXyco4aqSkQDuyZPIw2hcaxDU82OUVtK7UY9hrSvgWtpsW5D4rb4g==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-universal": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.0.5.tgz", + "integrity": "sha512-JFnmu4SU36YYw3DIBVao3FsJh4Uw65vVDIqlWT4LzR6gXA0F3KP0IXFKKJrhaVzCBhAuMsrUUaT5I+/4ZhF7aw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-codec": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.1.1.tgz", + "integrity": "sha512-61WjM0PWmZJR+SnmzaKI7t7G0UkkNFboDpzIdzSoy7TByUzlxo18Qlh9s71qug4AY4hlH/CwXdubMtkcNEb/sQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.1.3", + "@smithy/querystring-builder": "^4.0.5", + "@smithy/types": "^4.3.2", + "@smithy/util-base64": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.0.5.tgz", + "integrity": "sha512-cv1HHkKhpyRb6ahD8Vcfb2Hgz67vNIXEp2vnhzfxLFGRukLCNEA5QdsorbUEzXma1Rco0u3rx5VTqbM06GcZqQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.0.5.tgz", + "integrity": "sha512-IVnb78Qtf7EJpoEVo7qJ8BEXQwgC4n3igeJNNKEj/MLYtapnx8A67Zt/J3RXAj2xSO1910zk0LdFiygSemuLow==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.0.0.tgz", + "integrity": "sha512-saYhF8ZZNoJDTvJBEWgeBccCg+yvp1CX+ed12yORU3NilJScfc6gfch2oVb4QgxZrGUx3/ZJlb+c/dJbyupxlw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.0.5.tgz", + "integrity": "sha512-l1jlNZoYzoCC7p0zCtBDE5OBXZ95yMKlRlftooE5jPWQn4YBPLgsp+oeHp7iMHaTGoUdFqmHOPa8c9G3gBsRpQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.1.19", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.1.19.tgz", + "integrity": "sha512-EAlEPncqo03siNZJ9Tm6adKCQ+sw5fNU8ncxWwaH0zTCwMPsgmERTi6CEKaermZdgJb+4Yvh0NFm36HeO4PGgQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.9.0", + "@smithy/middleware-serde": "^4.0.9", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "@smithy/url-parser": "^4.0.5", + "@smithy/util-middleware": "^4.0.5", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.1.20", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.1.20.tgz", + "integrity": "sha512-T3maNEm3Masae99eFdx1Q7PIqBBEVOvRd5hralqKZNeIivnoGNx5OFtI3DiZ5gCjUkl0mNondlzSXeVxkinh7Q==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.4", + "@smithy/protocol-http": "^5.1.3", + "@smithy/service-error-classification": "^4.0.7", + "@smithy/smithy-client": "^4.5.0", + "@smithy/types": "^4.3.2", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-retry": "^4.0.7", + "@types/uuid": "^9.0.1", + "tslib": "^2.6.2", + "uuid": "^9.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.0.9.tgz", + "integrity": "sha512-uAFFR4dpeoJPGz8x9mhxp+RPjo5wW0QEEIPPPbLXiRRWeCATf/Km3gKIVR5vaP8bN1kgsPhcEeh+IZvUlBv6Xg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.0.5.tgz", + "integrity": "sha512-/yoHDXZPh3ocRVyeWQFvC44u8seu3eYzZRveCMfgMOBcNKnAmOvjbL9+Cp5XKSIi9iYA9PECUuW2teDAk8T+OQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.1.4.tgz", + "integrity": "sha512-+UDQV/k42jLEPPHSn39l0Bmc4sB1xtdI9Gd47fzo/0PbXzJ7ylgaOByVjF5EeQIumkepnrJyfx86dPa9p47Y+w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.0.5", + "@smithy/shared-ini-file-loader": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.1.1.tgz", + "integrity": "sha512-RHnlHqFpoVdjSPPiYy/t40Zovf3BBHc2oemgD7VsVTFFZrU5erFFe0n52OANZZ/5sbshgD93sOh5r6I35Xmpaw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.0.5", + "@smithy/protocol-http": "^5.1.3", + "@smithy/querystring-builder": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.0.5.tgz", + "integrity": "sha512-R/bswf59T/n9ZgfgUICAZoWYKBHcsVDurAGX88zsiUtOTA/xUAPyiT+qkNCPwFn43pZqN84M4MiUsbSGQmgFIQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.1.3.tgz", + "integrity": "sha512-fCJd2ZR7D22XhDY0l+92pUag/7je2BztPRQ01gU5bMChcyI0rlly7QFibnYHzcxDvccMjlpM/Q1ev8ceRIb48w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.0.5.tgz", + "integrity": "sha512-NJeSCU57piZ56c+/wY+AbAw6rxCCAOZLCIniRE7wqvndqxcKKDOXzwWjrY7wGKEISfhL9gBbAaWWgHsUGedk+A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "@smithy/util-uri-escape": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.0.5.tgz", + "integrity": "sha512-6SV7md2CzNG/WUeTjVe6Dj8noH32r4MnUeFKZrnVYsQxpGSIcphAanQMayi8jJLZAWm6pdM9ZXvKCpWOsIGg0w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.0.7.tgz", + "integrity": "sha512-XvRHOipqpwNhEjDf2L5gJowZEm5nsxC16pAZOeEcsygdjv9A2jdOh3YoDQvOXBGTsaJk6mNWtzWalOB9976Wlg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.0.5.tgz", + "integrity": "sha512-YVVwehRDuehgoXdEL4r1tAAzdaDgaC9EQvhK0lEbfnbrd0bd5+CTQumbdPryX3J2shT7ZqQE+jPW4lmNBAB8JQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.1.3", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.1.3.tgz", + "integrity": "sha512-mARDSXSEgllNzMw6N+mC+r1AQlEBO3meEAkR/UlfAgnMzJUB3goRBWgip1EAMG99wh36MDqzo86SfIX5Y+VEaw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-middleware": "^4.0.5", + "@smithy/util-uri-escape": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.5.0.tgz", + "integrity": "sha512-ZSdE3vl0MuVbEwJBxSftm0J5nL/gw76xp5WF13zW9cN18MFuFXD5/LV0QD8P+sCU5bSWGyy6CTgUupE1HhOo1A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.9.0", + "@smithy/middleware-endpoint": "^4.1.19", + "@smithy/middleware-stack": "^4.0.5", + "@smithy/protocol-http": "^5.1.3", + "@smithy/types": "^4.3.2", + "@smithy/util-stream": "^4.2.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.3.2.tgz", + "integrity": "sha512-QO4zghLxiQ5W9UZmX2Lo0nta2PuE1sSrXUYDoaB6HMR762C0P7v/HEPHf6ZdglTVssJG1bsrSBxdc3quvDSihw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.0.5.tgz", + "integrity": "sha512-j+733Um7f1/DXjYhCbvNXABV53NyCRRA54C7bNEIxNPs0YjfRxeMKjjgm2jvTYrciZyCjsicHwQ6Q0ylo+NAUw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.0.0.tgz", + "integrity": "sha512-CvHfCmO2mchox9kjrtzoHkWHxjHZzaFojLc8quxXY7WAAMAg43nuxwv95tATVgQFNDwd4M9S1qFzj40Ul41Kmg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.0.0.tgz", + "integrity": "sha512-sNi3DL0/k64/LO3A256M+m3CDdG6V7WKWHdAiBBMUN8S3hK3aMPhwnPik2A/a2ONN+9doY9UxaLfgqsIRg69QA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.0.0.tgz", + "integrity": "sha512-q0iDP3VsZzqJyje8xJWEJCNIu3lktUGVoSy1KB0UWym2CL1siV3artm+u1DFYTLejpsrdGyCSWBdGNjJzfDPjg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.0.0.tgz", + "integrity": "sha512-9TOQ7781sZvddgO8nxueKi3+yGvkY35kotA0Y6BWRajAv8jjmigQ1sBwz0UX47pQMYXJPahSKEKYFgt+rXdcug==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.0.0.tgz", + "integrity": "sha512-L1RBVzLyfE8OXH+1hsJ8p+acNUSirQnWQ6/EgpchV88G6zGBTDPdXiiExei6Z1wR2RxYvxY/XLw6AMNCCt8H3w==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.0.27", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.27.tgz", + "integrity": "sha512-i/Fu6AFT5014VJNgWxKomBJP/GB5uuOsM4iHdcmplLm8B1eAqnRItw4lT2qpdO+mf+6TFmf6dGcggGLAVMZJsQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.0.5", + "@smithy/smithy-client": "^4.5.0", + "@smithy/types": "^4.3.2", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.0.27", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.27.tgz", + "integrity": "sha512-3W0qClMyxl/ELqTA39aNw1N+pN0IjpXT7lPFvZ8zTxqVFP7XCpACB9QufmN4FQtd39xbgS7/Lekn7LmDa63I5w==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.1.5", + "@smithy/credential-provider-imds": "^4.0.7", + "@smithy/node-config-provider": "^4.1.4", + "@smithy/property-provider": "^4.0.5", + "@smithy/smithy-client": "^4.5.0", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.0.7.tgz", + "integrity": "sha512-klGBP+RpBp6V5JbrY2C/VKnHXn3d5V2YrifZbmMY8os7M6m8wdYFoO6w/fe5VkP+YVwrEktW3IWYaSQVNZJ8oQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.1.4", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.0.0.tgz", + "integrity": "sha512-Yk5mLhHtfIgW2W2WQZWSg5kuMZCVbvhFmC7rV4IO2QqnZdbEFPmQnCcGMAX2z/8Qj3B9hYYNjZOhWym+RwhePw==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.0.5.tgz", + "integrity": "sha512-N40PfqsZHRSsByGB81HhSo+uvMxEHT+9e255S53pfBw/wI6WKDI7Jw9oyu5tJTLwZzV5DsMha3ji8jk9dsHmQQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.0.7.tgz", + "integrity": "sha512-TTO6rt0ppK70alZpkjwy+3nQlTiqNfoXja+qwuAchIEAIoSZW8Qyd76dvBv3I5bCpE38APafG23Y/u270NspiQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.0.7", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.2.4.tgz", + "integrity": "sha512-vSKnvNZX2BXzl0U2RgCLOwWaAP9x/ddd/XobPK02pCbzRm5s55M53uwb1rl/Ts7RXZvdJZerPkA+en2FDghLuQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^5.1.1", + "@smithy/node-http-handler": "^4.1.1", + "@smithy/types": "^4.3.2", + "@smithy/util-base64": "^4.0.0", + "@smithy/util-buffer-from": "^4.0.0", + "@smithy/util-hex-encoding": "^4.0.0", + "@smithy/util-utf8": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.0.0.tgz", + "integrity": "sha512-77yfbCbQMtgtTylO9itEAdpPXSog3ZxMe09AEhm0dU0NLTalV70ghDZFR+Nfi1C60jnJoh/Re4090/DuZh2Omg==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.0.0.tgz", + "integrity": "sha512-b+zebfKCfRdgNJDknHCob3O7FpeYQN6ZG6YLExMcasDHsCXlsXCEuiPZeLnJLpwa5dvPetGlnGCiMHuLwGvFow==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.0.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-waiter": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/@smithy/util-waiter/-/util-waiter-4.0.7.tgz", + "integrity": "sha512-mYqtQXPmrwvUljaHyGxYUIIRI3qjBTEb/f5QFi3A6VlxhpmZd5mWXn9W+qUkf2pVE1Hv3SqxefiZOPGdxmO64A==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/abort-controller": "^4.0.5", + "@smithy/types": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "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, + "license": "MIT", + "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.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/node": { + "version": "24.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", + "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.10.0" + } + }, + "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, + "license": "MIT" + }, + "node_modules/@types/uuid": { + "version": "9.0.8", + "resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-9.0.8.tgz", + "integrity": "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA==", + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "ISC", + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "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, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "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, + "license": "MIT", + "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, + "license": "BSD-3-Clause", + "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, + "license": "BSD-3-Clause", + "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, + "license": "MIT", + "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.2.0", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.2.0.tgz", + "integrity": "sha512-E/VlAEzRrsLEb2+dv8yp3bo4scof3l9nR4lrld+Iy5NyVqgVYUJnDAmunkhPMisRI32Qc4iRiz425d8vM++2fg==", + "dev": true, + "license": "MIT", + "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 || ^8.0.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, + "license": "MIT", + "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, + "license": "MIT" + }, + "node_modules/bowser": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.12.1.tgz", + "integrity": "sha512-z4rE2Gxh7tvshQ4hluIT7XcFrgLIQaw9X3A+kTTRdovCz5PMukm/0QC/BKSYPj3omF5Qfypn9O/c5kgpmvYUCw==", + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.25.4", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.4.tgz", + "integrity": "sha512-4jYpcjabC606xJ3kw2QwGEZKX0Aw7sgQdZCvIK9dhVSPh76BKo+C+btT1RRofH7B+8iNpEbgGNVWiLki5q93yg==", + "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" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001737", + "electron-to-chromium": "^1.5.211", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "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, + "license": "Apache-2.0", + "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==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001739", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001739.tgz", + "integrity": "sha512-y+j60d6ulelrNSwpPyrHdl+9mJnQzHBr08xm48Qno0nSk4h3Qojh+ziv2qE6rXf4k3tadF4o1J/1tAbVm1NtnA==", + "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" + } + ], + "license": "CC-BY-4.0" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.4.3.tgz", + "integrity": "sha512-9z8TZaGM1pfswYeXrUpzPrkx8UnWYdhJclsiYMm6x/w5+nN+8Tf/LnAgfLGQCm59qAOxU8WwHEq2vNwF6i4j+Q==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "ISC", + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "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/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", + "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.211", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.211.tgz", + "integrity": "sha512-IGBvimJkotaLzFnwIVgW9/UD/AOJ2tByUmeOrtqBfACSbAw5b1G0XpvdaieKyc7ULmbwXVx+4e4Be8pOPBrYkw==", + "dev": true, + "license": "ISC" + }, + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "BSD-2-Clause", + "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, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "node_modules/fast-xml-parser": { + "version": "5.2.5", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.2.5.tgz", + "integrity": "sha512-pfX9uG9Ki0yekDHx2SiuRIyFdyAr1kMIMitPvb0YBo8SUfKvia7w7FIyd/l6av85pFYRhZscS75MwMnbvY+hcQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "strnum": "^2.1.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/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, + "license": "Apache-2.0", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "ISC", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "ISC", + "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/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, + "license": "ISC" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "ISC", + "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, + "license": "ISC" + }, + "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, + "license": "MIT" + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "ISC" + }, + "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, + "license": "BSD-3-Clause", + "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, + "license": "BSD-3-Clause", + "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.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "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, + "license": "BSD-3-Clause", + "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, + "license": "BSD-3-Clause", + "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.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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-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, + "license": "MIT", + "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, + "license": "MIT", + "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.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "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, + "license": "ISC", + "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, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "ISC", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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" + } + ], + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.10", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", + "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.0", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve.exports": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.3.tgz", + "integrity": "sha512-OcXjMsGdhL4XnbShKpAcSqPMzQoYkYyhbEaeSko47MjRP9NfEQMhZkXL1DoFlt9LWQn4YttrdnV6X2OiyzBi+A==", + "dev": true, + "license": "MIT", + "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, + "license": "ISC", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "ISC" + }, + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "license": "MIT", + "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, + "license": "BSD-3-Clause" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "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/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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strnum": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.1.1.tgz", + "integrity": "sha512-7ZvoFTiCnGxBtDqJ//Cu6fWtZtc7Y3x+QOirG15wztbdngGSkht27o2pyGWrVy0b4WAy3jbKmnoK6g5VlVNUUw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "ISC", + "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, + "license": "BSD-3-Clause" + }, + "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, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/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, + "license": "MIT", + "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, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici-types": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "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" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "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, + "license": "ISC", + "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, + "license": "Apache-2.0", + "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, + "license": "ISC", + "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, + "license": "MIT", + "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, + "license": "ISC" + }, + "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, + "license": "ISC", + "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, + "license": "ISC", + "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, + "license": "ISC" + }, + "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, + "license": "MIT", + "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, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/src/applications/lambda/traffic-generator-node/package.json b/src/applications/lambda/traffic-generator-node/package.json new file mode 100644 index 00000000..4e4e98bd --- /dev/null +++ b/src/applications/lambda/traffic-generator-node/package.json @@ -0,0 +1,17 @@ +{ + "name": "traffic-generator-node", + "version": "1.0.0", + "description": "Generates concurrent traffic by invoking canary functions", + "main": "index.js", + "scripts": { + "test": "jest" + }, + "author": "", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/client-lambda": "^3.0.0" + }, + "devDependencies": { + "jest": "^29.0.0" + } +} diff --git a/PetAdoptions/payforadoption-go/Dockerfile b/src/applications/microservices/payforadoption-go/Dockerfile similarity index 74% rename from PetAdoptions/payforadoption-go/Dockerfile rename to src/applications/microservices/payforadoption-go/Dockerfile index f0aed5f4..30f43a87 100644 --- a/PetAdoptions/payforadoption-go/Dockerfile +++ b/src/applications/microservices/payforadoption-go/Dockerfile @@ -1,11 +1,11 @@ -FROM golang:1.23 as builder +FROM public.ecr.aws/docker/library/golang:1.23 as builder WORKDIR /go/src/app COPY . . ENV GOPROXY=https://goproxy.io,direct RUN go get . RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app . -FROM alpine:latest +FROM public.ecr.aws/docker/library/alpine:3.22.1 WORKDIR /app RUN apk --no-cache add ca-certificates COPY --from=builder /go/src/app/app . diff --git a/PetAdoptions/payforadoption-go/README.md b/src/applications/microservices/payforadoption-go/README.md similarity index 98% rename from PetAdoptions/payforadoption-go/README.md rename to src/applications/microservices/payforadoption-go/README.md index 8ed591aa..4fda4873 100644 --- a/PetAdoptions/payforadoption-go/README.md +++ b/src/applications/microservices/payforadoption-go/README.md @@ -1,3 +1,7 @@ + # Pay for Adoption Go Service This microservice handles the complete pet adoption workflow with a clean separation between real-time adoption processing and asynchronous history tracking. The service implements modern event-driven architecture patterns for optimal performance and reliability. @@ -7,7 +11,7 @@ This microservice handles the complete pet adoption workflow with a clean separa ``` POST /completeadoption → payforadoption-go: ├── CreateTransaction() → transactions table (synchronous) -├── UpdateAvailability() → petstatus service (synchronous) +├── UpdateAvailability() → petstatus service (synchronous) └── SendHistoryMessage() → SQS → pethistory → transaction_history table (async) ``` @@ -87,7 +91,7 @@ func (r *repo) CreateTransaction(ctx context.Context, a Adoption) error - **Error handling**: Failures block adoption completion - **Observability**: Traced and logged for monitoring -#### SendHistoryMessage +#### SendHistoryMessage ```go func (r *repo) SendHistoryMessage(ctx context.Context, a Adoption) error ``` @@ -106,7 +110,7 @@ The service publishes adoption history messages to Amazon SQS for asynchronous p { "transactionId": "123e4567-e89b-12d3-a456-426614174000", "petId": "pet123", - "petType": "dog", + "petType": "dog", "userId": "user456", "adoptionDate": "2025-08-08T10:30:00Z", "timestamp": "2025-08-08T10:30:00Z" @@ -115,7 +119,7 @@ The service publishes adoption history messages to Amazon SQS for asynchronous p #### Message Attributes - `PetType`: For message filtering and routing -- `UserID`: For user-specific processing +- `UserID`: For user-specific processing - `TransactionID`: For correlation and deduplication #### Processing Flow diff --git a/PetAdoptions/payforadoption-go/benchmark/Dockerfile b/src/applications/microservices/payforadoption-go/benchmark/Dockerfile similarity index 52% rename from PetAdoptions/payforadoption-go/benchmark/Dockerfile rename to src/applications/microservices/payforadoption-go/benchmark/Dockerfile index e1ea9ab9..20056b9f 100644 --- a/PetAdoptions/payforadoption-go/benchmark/Dockerfile +++ b/src/applications/microservices/payforadoption-go/benchmark/Dockerfile @@ -1,4 +1,4 @@ -FROM rust:latest as builder +FROM public.ecr.aws/docker/library/rust:latest:latest as builder WORKDIR /app RUN COPY . . diff --git a/PetAdoptions/payforadoption-go/benchmark/README.md b/src/applications/microservices/payforadoption-go/benchmark/README.md similarity index 93% rename from PetAdoptions/payforadoption-go/benchmark/README.md rename to src/applications/microservices/payforadoption-go/benchmark/README.md index cd4cc848..c207736f 100644 --- a/PetAdoptions/payforadoption-go/benchmark/README.md +++ b/src/applications/microservices/payforadoption-go/benchmark/README.md @@ -1,3 +1,7 @@ + ## Drill for PayForAdoption Using [drill](https://github.com/fcsonline/drill), this allows to generate diff --git a/src/applications/microservices/payforadoption-go/benchmark/benchmark.sh b/src/applications/microservices/payforadoption-go/benchmark/benchmark.sh new file mode 100755 index 00000000..7f892172 --- /dev/null +++ b/src/applications/microservices/payforadoption-go/benchmark/benchmark.sh @@ -0,0 +1,10 @@ +#!/usr/bin/env bash + +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +while true +do + drill -s --benchmark benchmark.yaml + sleep 1 +done diff --git a/src/applications/microservices/payforadoption-go/benchmark/benchmark.yaml b/src/applications/microservices/payforadoption-go/benchmark/benchmark.yaml new file mode 100644 index 00000000..7b13e89e --- /dev/null +++ b/src/applications/microservices/payforadoption-go/benchmark/benchmark.yaml @@ -0,0 +1,69 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +--- +concurrency: 4 +base: 'http://localhost:80' +iterations: 4 +rampup: 2 + +plan: + - name: Health check + request: + url: /health/status + method: GET + + - name: Adopt Bunnies + request: + url: /api/completeadoption?petId={{ item }}&petType=bunny&userId=user007 + method: POST + body: '' + with_items: + - '023' + - '024' + - '025' + - '026' + - 'invalid_bunny_id' + + - name: Adopt Kittens + request: + url: /api/completeadoption?petId={{ item }}&petType=kitten&userId=user007 + method: POST + body: '' + with_items: + - '016' + - '017' + - '018' + - '019' + - '020' + - '021' + - '022' + - 'invalid_kitten_id' + + - name: Adopt Puppies + request: + url: /api/completeadoption?petId={{ item }}&petType=puppy&userId=user007 + method: POST + body: '' + with_items: + - '001' + - '002' + - '003' + - '004' + - '005' + - '006' + - '007' + - '008' + - '009' + - '010' + - '011' + - '012' + - '013' + - '014' + - '015' + - 'invalid_puppy_id' + + - name: Cleanup Adoptions + request: + url: /api/cleanupadoptions + method: POST + body: '' diff --git a/PetAdoptions/payforadoption-go/config.go b/src/applications/microservices/payforadoption-go/config.go similarity index 92% rename from PetAdoptions/payforadoption-go/config.go rename to src/applications/microservices/payforadoption-go/config.go index 5da08a16..11597b24 100644 --- a/PetAdoptions/payforadoption-go/config.go +++ b/src/applications/microservices/payforadoption-go/config.go @@ -1,3 +1,7 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ package main import ( @@ -63,7 +67,7 @@ func fetchConfigFromParameterStore(ctx context.Context, cfg payforadoption.Confi switch aws.ToString(p.Name) { case "/petstore/rdssecretarn": - newCfg.RDSSecretArn = pValue + newCfg.RDSSecretArn = pValue //pragma: allowlist secret case "/petstore/updateadoptionstatusurl": newCfg.UpdateAdoptionURL = pValue case "/petstore/s3bucketname": diff --git a/PetAdoptions/payforadoption-go/example_request.sh b/src/applications/microservices/payforadoption-go/example_request.sh similarity index 95% rename from PetAdoptions/payforadoption-go/example_request.sh rename to src/applications/microservices/payforadoption-go/example_request.sh index 6b81e47f..9e772230 100755 --- a/PetAdoptions/payforadoption-go/example_request.sh +++ b/src/applications/microservices/payforadoption-go/example_request.sh @@ -1,5 +1,8 @@ #!/bin/bash +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + # Example script to test the payforadoption service with the new userid parameter # Set the service URL (adjust as needed) diff --git a/PetAdoptions/payforadoption-go/go.mod b/src/applications/microservices/payforadoption-go/go.mod similarity index 100% rename from PetAdoptions/payforadoption-go/go.mod rename to src/applications/microservices/payforadoption-go/go.mod diff --git a/PetAdoptions/payforadoption-go/go.sum b/src/applications/microservices/payforadoption-go/go.sum similarity index 100% rename from PetAdoptions/payforadoption-go/go.sum rename to src/applications/microservices/payforadoption-go/go.sum diff --git a/PetAdoptions/payforadoption-go/main.go b/src/applications/microservices/payforadoption-go/main.go similarity index 97% rename from PetAdoptions/payforadoption-go/main.go rename to src/applications/microservices/payforadoption-go/main.go index c229f9ca..4ba1081c 100644 --- a/PetAdoptions/payforadoption-go/main.go +++ b/src/applications/microservices/payforadoption-go/main.go @@ -1,3 +1,7 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ package main import ( diff --git a/PetAdoptions/payforadoption-go/payforadoption/benchmark_test.go b/src/applications/microservices/payforadoption-go/payforadoption/benchmark_test.go similarity index 95% rename from PetAdoptions/payforadoption-go/payforadoption/benchmark_test.go rename to src/applications/microservices/payforadoption-go/payforadoption/benchmark_test.go index d6ea7d6b..e65d84f0 100644 --- a/PetAdoptions/payforadoption-go/payforadoption/benchmark_test.go +++ b/src/applications/microservices/payforadoption-go/payforadoption/benchmark_test.go @@ -1,3 +1,7 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ package payforadoption import ( diff --git a/PetAdoptions/payforadoption-go/payforadoption/database.go b/src/applications/microservices/payforadoption-go/payforadoption/database.go similarity index 95% rename from PetAdoptions/payforadoption-go/payforadoption/database.go rename to src/applications/microservices/payforadoption-go/payforadoption/database.go index 7c200041..16aa8aa8 100644 --- a/PetAdoptions/payforadoption-go/payforadoption/database.go +++ b/src/applications/microservices/payforadoption-go/payforadoption/database.go @@ -1,3 +1,7 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ package payforadoption import ( diff --git a/PetAdoptions/payforadoption-go/payforadoption/database_test.go b/src/applications/microservices/payforadoption-go/payforadoption/database_test.go similarity index 93% rename from PetAdoptions/payforadoption-go/payforadoption/database_test.go rename to src/applications/microservices/payforadoption-go/payforadoption/database_test.go index 39b436ec..c53f889d 100644 --- a/PetAdoptions/payforadoption-go/payforadoption/database_test.go +++ b/src/applications/microservices/payforadoption-go/payforadoption/database_test.go @@ -1,3 +1,7 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ package payforadoption import ( @@ -20,7 +24,7 @@ func TestNewDatabaseConfigService(t *testing.T) { t.Fatal("Expected DatabaseConfigService to be created") } - if dbSvc.cfg.RDSSecretArn != cfg.RDSSecretArn { + if dbSvc.cfg.RDSSecretArn != cfg.RDSSecretArn { //pragma: allowlist secret t.Errorf("Expected RDSSecretArn %s, got %s", cfg.RDSSecretArn, dbSvc.cfg.RDSSecretArn) } @@ -116,7 +120,7 @@ func TestDatabaseConfigParsing(t *testing.T) { t.Errorf("Expected Username testuser, got %s", config.Username) } - if config.Password != "testpass" { + if config.Password != "testpass" { //pragma: allowlist secret t.Errorf("Expected Password testpass, got %s", config.Password) } diff --git a/PetAdoptions/payforadoption-go/payforadoption/endpoint.go b/src/applications/microservices/payforadoption-go/payforadoption/endpoint.go similarity index 92% rename from PetAdoptions/payforadoption-go/payforadoption/endpoint.go rename to src/applications/microservices/payforadoption-go/payforadoption/endpoint.go index a0858fac..2522a631 100644 --- a/PetAdoptions/payforadoption-go/payforadoption/endpoint.go +++ b/src/applications/microservices/payforadoption-go/payforadoption/endpoint.go @@ -1,3 +1,7 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ package payforadoption import ( diff --git a/PetAdoptions/payforadoption-go/payforadoption/middlewares.go b/src/applications/microservices/payforadoption-go/payforadoption/middlewares.go similarity index 96% rename from PetAdoptions/payforadoption-go/payforadoption/middlewares.go rename to src/applications/microservices/payforadoption-go/payforadoption/middlewares.go index 41464598..842b701a 100644 --- a/PetAdoptions/payforadoption-go/payforadoption/middlewares.go +++ b/src/applications/microservices/payforadoption-go/payforadoption/middlewares.go @@ -1,3 +1,7 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ package payforadoption import ( diff --git a/PetAdoptions/payforadoption-go/payforadoption/repository.go b/src/applications/microservices/payforadoption-go/payforadoption/repository.go similarity index 98% rename from PetAdoptions/payforadoption-go/payforadoption/repository.go rename to src/applications/microservices/payforadoption-go/payforadoption/repository.go index 20a71863..a748cc2f 100644 --- a/PetAdoptions/payforadoption-go/payforadoption/repository.go +++ b/src/applications/microservices/payforadoption-go/payforadoption/repository.go @@ -1,3 +1,7 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ package payforadoption import ( diff --git a/PetAdoptions/payforadoption-go/payforadoption/service.go b/src/applications/microservices/payforadoption-go/payforadoption/service.go similarity index 97% rename from PetAdoptions/payforadoption-go/payforadoption/service.go rename to src/applications/microservices/payforadoption-go/payforadoption/service.go index 18d98bf5..068e53d9 100644 --- a/PetAdoptions/payforadoption-go/payforadoption/service.go +++ b/src/applications/microservices/payforadoption-go/payforadoption/service.go @@ -1,3 +1,7 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ package payforadoption import ( diff --git a/PetAdoptions/payforadoption-go/payforadoption/service_test.go b/src/applications/microservices/payforadoption-go/payforadoption/service_test.go similarity index 96% rename from PetAdoptions/payforadoption-go/payforadoption/service_test.go rename to src/applications/microservices/payforadoption-go/payforadoption/service_test.go index e36ab27e..7f7cf767 100644 --- a/PetAdoptions/payforadoption-go/payforadoption/service_test.go +++ b/src/applications/microservices/payforadoption-go/payforadoption/service_test.go @@ -1,3 +1,7 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ package payforadoption import ( @@ -47,7 +51,7 @@ func (m *mockRepository) GetConnectionString(ctx context.Context) (string, error if m.connectionString != "" { return m.connectionString, nil } - return "postgres://user:pass@localhost:5432/testdb?sslmode=disable", nil + return "postgres://user:pass@localhost:5432/testdb?sslmode=disable", nil //pragma: allowlist secret } func (m *mockRepository) ErrorModeOn(ctx context.Context) bool { @@ -199,7 +203,7 @@ func TestDatabaseConfigService(t *testing.T) { } // Test that the service holds the correct config - if dbSvc.cfg.RDSSecretArn != "test-secret-arn" { + if dbSvc.cfg.RDSSecretArn != "test-secret-arn" { //pragma: allowlist secret t.Errorf("Expected RDSSecretArn to be set correctly") } } @@ -211,7 +215,7 @@ func TestDatabaseConnectionExhaustion(t *testing.T) { exhauster := NewDatabaseConnectionExhauster(logger) // Test with a mock connection string (this won't actually connect) - mockConnStr := "postgres://user:pass@nonexistent:5432/testdb?sslmode=disable" + mockConnStr := "postgres://user:pass@nonexistent:5432/testdb?sslmode=disable" //pragma: allowlist secret // This should fail gracefully since the host doesn't exist err := exhauster.ExhaustConnections(context.Background(), mockConnStr, 2) @@ -286,7 +290,7 @@ func TestDegradationScenarios(t *testing.T) { t.Run("DatabaseConnectionDegradation", func(t *testing.T) { repo := &mockRepository{ - connectionString: "postgres://user:pass@nonexistent:5432/testdb?sslmode=disable", + connectionString: "postgres://user:pass@nonexistent:5432/testdb?sslmode=disable", //pragma: allowlist secret } result := databaseConnectionDegradation(ctx, logger, adoption, startTime, repo) diff --git a/PetAdoptions/payforadoption-go/payforadoption/transport.go b/src/applications/microservices/payforadoption-go/payforadoption/transport.go similarity index 97% rename from PetAdoptions/payforadoption-go/payforadoption/transport.go rename to src/applications/microservices/payforadoption-go/payforadoption/transport.go index 26f2f264..b4e381d3 100644 --- a/PetAdoptions/payforadoption-go/payforadoption/transport.go +++ b/src/applications/microservices/payforadoption-go/payforadoption/transport.go @@ -1,3 +1,7 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ package payforadoption import ( diff --git a/PetAdoptions/payforadoption-go/payforadoption/utils.go b/src/applications/microservices/payforadoption-go/payforadoption/utils.go similarity index 99% rename from PetAdoptions/payforadoption-go/payforadoption/utils.go rename to src/applications/microservices/payforadoption-go/payforadoption/utils.go index 5508525f..da665758 100644 --- a/PetAdoptions/payforadoption-go/payforadoption/utils.go +++ b/src/applications/microservices/payforadoption-go/payforadoption/utils.go @@ -1,3 +1,7 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ package payforadoption import ( diff --git a/PetAdoptions/payforadoption-go/seed.json b/src/applications/microservices/payforadoption-go/seed.json similarity index 100% rename from PetAdoptions/payforadoption-go/seed.json rename to src/applications/microservices/payforadoption-go/seed.json diff --git a/src/applications/microservices/petfood-rs/.dockerignore b/src/applications/microservices/petfood-rs/.dockerignore new file mode 100644 index 00000000..5b96ada3 --- /dev/null +++ b/src/applications/microservices/petfood-rs/.dockerignore @@ -0,0 +1,41 @@ +# Build artifacts +target/ +Cargo.lock + +# IDE files +.vscode/ +.idea/ +*.swp +*.swo + +# OS files +.DS_Store +Thumbs.db + +# Git +.git/ +.gitignore + +# Documentation +README.md +*.md + +# Test files (we only copy src for production) +tests/ +benches/ + +# Docker files +Dockerfile +.dockerignore + +# Logs +*.log + +# Environment files +.env +.env.local +.env.*.local + +# Temporary files +tmp/ +temp/ \ No newline at end of file diff --git a/src/applications/microservices/petfood-rs/API_DOCUMENTATION.md b/src/applications/microservices/petfood-rs/API_DOCUMENTATION.md new file mode 100644 index 00000000..a7b7eb21 --- /dev/null +++ b/src/applications/microservices/petfood-rs/API_DOCUMENTATION.md @@ -0,0 +1,855 @@ +# PetFood Microservice API Documentation +This documentation outlines the API endpoints and their usage for the PetFood Microservice. + +**Base URL**: `http://localhost:8080` + +## Table of Contents + +1. [Getting Started](#getting-started) +2. [Authentication](#authentication) +3. [Health Check](#health-check) +4. [Admin Endpoints](#admin-endpoints) +5. [Food Management](#food-management) +6. [Cart Management](#cart-management) +7. [Complete User Journey Example](#complete-user-journey-example) +8. [Error Handling](#error-handling) +9. [Data Models](#data-models) + +--- + +## Getting Started + +### Prerequisites +- Service running on `http://localhost:8080` +- DynamoDB tables created (use admin endpoints). Tables creation should be moved to CDK in the future + +### Quick Start Sequence +1. **Setup**: Create tables using admin endpoint +2. **Seed**: Populate with sample data +3. **Use**: Interact with food and cart APIs +4. **Cleanup**: Remove all data when done + +--- + +## Authentication + +Currently, the API does not require authentication. User identification is handled through the `user_id` parameter in cart operations. + +--- + +## Health Check + +### Check Service Health + +**Endpoint**: `GET /health/status` + +**Description**: Verify that the service is running and healthy. + +**Request**: +```bash +curl -X GET http://localhost:8080/health/status +``` + +**Response**: +```json +{ + "status": "healthy", + "service": "petfood-rs", + "version": "0.1.0", + "timestamp": "2024-01-15T10:30:00Z" +} +``` + +--- + +## Admin Endpoints + +### 1. Setup Database Tables + +**Endpoint**: `POST /api/admin/setup-tables` + +**Description**: Create the required DynamoDB tables (foods and carts). + +**Request**: +```bash +curl -X POST http://localhost:8080/api/admin/setup-tables \ + -H "Content-Type: application/json" +``` + +**Response**: +```json +{ + "message": "Successfully created 2 tables", + "tables_created": ["foods", "carts"], + "timestamp": "2024-01-15T10:30:00Z" +} +``` + +### 2. Seed Database with Sample Data + +**Endpoint**: `POST /api/admin/seed` + +**Description**: Populate the database with sample food products for all pet types. + +**Request**: +```bash +curl -X POST http://localhost:8080/api/admin/seed \ + -H "Content-Type: application/json" +``` + +**Response**: +```json +{ + "message": "Database seeded successfully with 9 foods", + "foods_created": 9, + "timestamp": "2024-01-15T10:30:00Z" +} +``` + +### 3. Cleanup Database + +**Endpoint**: `POST /api/admin/cleanup` + +**Description**: Remove all food products from the database. + +**Request**: +```bash +curl -X POST http://localhost:8080/api/admin/cleanup \ + -H "Content-Type: application/json" +``` + +**Response**: +```json +{ + "message": "Database cleaned up successfully, deleted 9 foods", + "foods_deleted": 9, + "timestamp": "2024-01-15T10:30:00Z" +} +``` + +--- + +## Food Management + +### 1. List All Foods + +**Endpoint**: `GET /api/foods` + +**Description**: Retrieve all food products with optional filtering. + +**Query Parameters**: +- `pet_type` (optional): Filter by pet type (`puppy`, `kitten`, `bunny`) +- `food_type` (optional): Filter by food type (`dry`, `wet`, `treats`, `supplements`) +- `min_price` (optional): Minimum price filter +- `max_price` (optional): Maximum price filter +- `search` (optional): Search term for food name/description +- `in_stock_only` (optional): Show only in-stock items (`true`/`false`) + +**Request**: +```bash +# Get all foods +curl -X GET http://localhost:8080/api/foods + +# Get puppy foods only +curl -X GET "http://localhost:8080/api/foods?pet_type=puppy" + +# Get dry foods under $15 +curl -X GET "http://localhost:8080/api/foods?food_type=dry&max_price=15.00" +``` + +**Response**: +```json +{ + "foods": [ + { + "id": "F12345678", + "pet_type": "puppy", + "name": "Beef and Turkey Kibbles", + "food_type": "dry", + "description": "A nutritious blend of beef and turkey, specially formulated for growing puppies.", + "price": "12.99", + "image": "https://petfood-assets.s3.amazonaws.com/petfood/images/beef-turkey-kibbles.jpg", + "nutritional_info": null, + "ingredients": ["beef", "turkey", "rice", "vegetables"], + "feeding_guidelines": "Feed 2-3 times daily based on puppy's weight", + "availability_status": "in_stock", + "stock_quantity": 50, + "created_at": "2024-01-15T10:30:00Z", + "updated_at": "2024-01-15T10:30:00Z", + "is_active": true + } + ], + "total_count": 1, + "page": null, + "page_size": null +} +``` + +### 2. Get Specific Food + +**Endpoint**: `GET /api/foods/{food_id}` + +**Description**: Retrieve details for a specific food product. + +**Request**: +```bash +curl -X GET http://localhost:8080/api/foods/F12345678 +``` + +**Response**: +```json +{ + "id": "F12345678", + "pet_type": "puppy", + "name": "Beef and Turkey Kibbles", + "food_type": "dry", + "description": "A nutritious blend of beef and turkey, specially formulated for growing puppies.", + "price": "12.99", + "image": "https://petfood-assets.s3.amazonaws.com/petfood/images/beef-turkey-kibbles.jpg", + "nutritional_info": null, + "ingredients": ["beef", "turkey", "rice", "vegetables"], + "feeding_guidelines": "Feed 2-3 times daily based on puppy's weight", + "availability_status": "in_stock", + "stock_quantity": 50, + "created_at": "2024-01-15T10:30:00Z", + "updated_at": "2024-01-15T10:30:00Z", + "is_active": true +} +``` + +### 3. Create New Food (Admin Only) + +**Endpoint**: `POST /api/admin/foods` + +**Description**: Create a new food product. This is an admin-only operation. + +**Note**: The `image` field should contain the path with petfood prefix (e.g., "petfood/premium-puppy-chow.jpg"). The full CDN URL will be dynamically generated in API responses based on the configured CDN URL. + +**Request**: +```bash +curl -X POST http://localhost:8080/api/admin/foods \ + -H "Content-Type: application/json" \ + -d '{ + "pet_type": "puppy", + "name": "Premium Puppy Chow", + "food_type": "dry", + "description": "High-quality dry food for puppies", + "price": "24.99", + "image": "petfood/premium-puppy-chow.jpg", + "nutritional_info": { + "calories_per_serving": 350, + "protein_percentage": "28.0", + "fat_percentage": "15.0", + "serving_size": "1 cup" + }, + "ingredients": ["chicken", "rice", "vegetables", "vitamins"], + "feeding_guidelines": "Feed 2-3 times daily", + "stock_quantity": 100 + }' +``` + +**Response** (Status: 201 Created): +```json +{ + "id": "F87654321", + "pet_type": "puppy", + "name": "Premium Puppy Chow", + "food_type": "dry", + "description": "High-quality dry food for puppies", + "price": "24.99", + "image": "https://petfood-assets.s3.amazonaws.com/petfood/images/premium-puppy-chow.jpg", + "nutritional_info": { + "calories_per_serving": 350, + "protein_percentage": "28.0", + "fat_percentage": "15.0", + "carbohydrate_percentage": null, + "fiber_percentage": null, + "moisture_percentage": null, + "serving_size": "1 cup", + "servings_per_container": null + }, + "ingredients": ["chicken", "rice", "vegetables", "vitamins"], + "feeding_guidelines": "Feed 2-3 times daily", + "availability_status": "in_stock", + "stock_quantity": 100, + "created_at": "2024-01-15T10:35:00Z", + "updated_at": "2024-01-15T10:35:00Z", + "is_active": true +} +``` + +### 4. Update Food (Admin Only) + +**Endpoint**: `PUT /api/admin/foods/{food_id}` + +**Description**: Update an existing food product. All fields are optional. This is an admin-only operation. + +**Request**: +```bash +curl -X PUT http://localhost:8080/api/admin/foods/F87654321 \ + -H "Content-Type: application/json" \ + -d '{ + "price": "22.99", + "stock_quantity": 75, + "availability_status": "low_stock" + }' +``` + +**Response**: +```json +{ + "id": "F87654321", + "pet_type": "puppy", + "name": "Premium Puppy Chow", + "food_type": "dry", + "description": "High-quality dry food for puppies", + "price": "22.99", + "image": "https://petfood-assets.s3.amazonaws.com/petfood/images/premium-puppy-chow.jpg", + "nutritional_info": { + "calories_per_serving": 350, + "protein_percentage": "28.0", + "fat_percentage": "15.0", + "serving_size": "1 cup" + }, + "ingredients": ["chicken", "rice", "vegetables", "vitamins"], + "feeding_guidelines": "Feed 2-3 times daily", + "availability_status": "low_stock", + "stock_quantity": 75, + "created_at": "2024-01-15T10:35:00Z", + "updated_at": "2024-01-15T10:40:00Z", + "is_active": true +} +``` + +### 5. Delete Food (Admin Only) + +**Endpoint**: `DELETE /api/admin/foods/{food_id}` + +**Description**: Soft delete a food product (marks as inactive). This is an admin-only operation. + +**Request**: +```bash +curl -X DELETE http://localhost:8080/api/admin/foods/F87654321 +``` + +**Response** (Status: 204 No Content): +``` +(Empty response body) +``` + +--- + +## Cart Management + +### 1. Get User's Cart + +**Endpoint**: `GET /api/cart/{user_id}` + +**Description**: Retrieve the shopping cart for a specific user. + +**Request**: +```bash +curl -X GET http://localhost:8080/api/cart/user001 +``` + +**Response** (Empty Cart): +```json +{ + "user_id": "user001", + "items": [], + "total_items": 0, + "total_price": "0.00", + "created_at": "2024-01-15T10:45:00Z", + "updated_at": "2024-01-15T10:45:00Z" +} +``` + +### 2. Add Item to Cart + +**Endpoint**: `POST /api/cart/{user_id}/items` + +**Description**: Add a food item to the user's cart. + +**Request**: +```bash +curl -X POST http://localhost:8080/api/cart/user001/items \ + -H "Content-Type: application/json" \ + -d '{ + "food_id": "F12345678", + "quantity": 2 + }' +``` + +**Response** (Status: 201 Created): +```json +{ + "food_id": "F12345678", + "name": "Beef and Turkey Kibbles", + "image": "https://petfood-assets.s3.amazonaws.com/petfood/images/beef-turkey-kibbles.jpg", + "quantity": 2, + "unit_price": "12.99", + "total_price": "25.98", + "is_available": true, + "added_at": "2024-01-15T10:50:00Z" +} +``` + +### 3. Get Cart After Adding Items + +**Request**: +```bash +curl -X GET http://localhost:8080/api/cart/user001 +``` + +**Response**: +```json +{ + "user_id": "user001", + "items": [ + { + "food_id": "F12345678", + "name": "Beef and Turkey Kibbles", + "image": "https://petfood-assets.s3.amazonaws.com/petfood/images/beef-turkey-kibbles.jpg", + "quantity": 2, + "unit_price": "12.99", + "total_price": "25.98", + "is_available": true, + "added_at": "2024-01-15T10:50:00Z" + } + ], + "total_items": 2, + "total_price": "25.98", + "created_at": "2024-01-15T10:45:00Z", + "updated_at": "2024-01-15T10:50:00Z" +} +``` + +### 4. Update Cart Item Quantity + +**Endpoint**: `PUT /api/cart/{user_id}/items/{food_id}` + +**Description**: Update the quantity of a specific item in the cart. + +**Request**: +```bash +curl -X PUT http://localhost:8080/api/cart/user001/items/F12345678 \ + -H "Content-Type: application/json" \ + -d '{ + "quantity": 5 + }' +``` + +**Response**: +```json +{ + "food_id": "F12345678", + "name": "Beef and Turkey Kibbles", + "image": "https://petfood-assets.s3.amazonaws.com/petfood/images/beef-turkey-kibbles.jpg", + "quantity": 5, + "unit_price": "12.99", + "total_price": "64.95", + "is_available": true, + "added_at": "2024-01-15T10:50:00Z" +} +``` + +### 5. Remove Item from Cart + +**Endpoint**: `DELETE /api/cart/{user_id}/items/{food_id}` + +**Description**: Remove a specific item from the cart. + +**Request**: +```bash +curl -X DELETE http://localhost:8080/api/cart/user001/items/F12345678 +``` + +**Response** (Status: 204 No Content): +``` +(Empty response body) +``` + +### 6. Clear Cart + +**Endpoint**: `POST /api/cart/{user_id}/clear` + +**Description**: Remove all items from the cart. + +**Request**: +```bash +curl -X POST http://localhost:8080/api/cart/user001/clear +``` + +**Response** (Status: 204 No Content): +``` +(Empty response body) +``` + +### 7. Checkout Cart + +**Endpoint**: `POST /api/cart/{user_id}/checkout` + +**Description**: Process checkout for the user's cart and create an order. + +**Request**: +```bash +curl -X POST http://localhost:8080/api/cart/user001/checkout \ + -H "Content-Type: application/json" \ + -d '{ + "payment_method": { + "CreditCard": { + "card_number": "4111111111111111", + "expiry_month": 12, + "expiry_year": 2025, + "cvv": "123", + "cardholder_name": "John Doe" + } + }, + "shipping_address": { + "name": "John Doe", + "street": "123 Main St", + "city": "Seattle", + "state": "WA", + "zip_code": "98101", + "country": "USA" + }, + "billing_address": { + "name": "John Doe", + "street": "123 Main St", + "city": "Seattle", + "state": "WA", + "zip_code": "98101", + "country": "USA" + } + }' +``` + +**Response** (Status: 201 Created): +```json +{ + "order_id": "ORDER-USER001-1705312200", + "user_id": "user001", + "items": [ + { + "food_id": "F12345678", + "food_name": "Beef and Turkey Kibbles", + "quantity": 2, + "unit_price": "12.99", + "total_price": "25.98" + } + ], + "subtotal": "25.98", + "tax": "2.34", + "shipping": "5.99", + "total_amount": "34.31", + "payment_method": "CreditCard", + "status": "confirmed", + "created_at": "2024-01-15T11:30:00Z", + "estimated_delivery": "2024-01-19T11:30:00Z" +} +``` + +### 8. Delete Cart + +**Endpoint**: `DELETE /api/cart/{user_id}` + +**Description**: Delete the entire cart for a user. + +**Request**: +```bash +curl -X DELETE http://localhost:8080/api/cart/user001 +``` + +**Response** (Status: 204 No Content): +``` +(Empty response body) +``` + +--- + +## Complete User Journey Example + +Here's a complete end-to-end example showing a typical user journey with `user001`: + +### Step 1: Setup Environment + +```bash +# 1. Check service health +curl -X GET http://localhost:8080/health/status + +# 2. Setup database tables +curl -X POST http://localhost:8080/api/admin/setup-tables + +# 3. Seed with sample data +curl -X POST http://localhost:8080/api/admin/seed +``` + +### Step 2: Browse Products + +```bash +# 1. Get all available foods +curl -X GET http://localhost:8080/api/foods + +# 2. Get puppy foods using filtering +curl -X GET "http://localhost:8080/api/foods?pet_type=puppy" + +# 3. Search for dry foods under $15 +curl -X GET "http://localhost:8080/api/foods?food_type=dry&max_price=15.00" +``` + +### Step 3: Shopping Cart Operations + +```bash +# 1. Check initial cart (should be empty) +curl -X GET http://localhost:8080/api/cart/user001 + +# 2. Add puppy kibble to cart +curl -X POST http://localhost:8080/api/cart/user001/items \ + -H "Content-Type: application/json" \ + -d '{ + "food_id": "F12345678", + "quantity": 2 + }' + +# 3. Add puppy treats to cart +curl -X POST http://localhost:8080/api/cart/user001/items \ + -H "Content-Type: application/json" \ + -d '{ + "food_id": "F34567890", + "quantity": 1 + }' + +# 4. Check cart contents +curl -X GET http://localhost:8080/api/cart/user001 + +# 5. Update kibble quantity +curl -X PUT http://localhost:8080/api/cart/user001/items/F12345678 \ + -H "Content-Type: application/json" \ + -d '{ + "quantity": 3 + }' + +# 6. Remove treats from cart +curl -X DELETE http://localhost:8080/api/cart/user001/items/F34567890 + +# 7. Final cart check +curl -X GET http://localhost:8080/api/cart/user001 + +# 8. Checkout the cart +curl -X POST http://localhost:8080/api/cart/user001/checkout \ + -H "Content-Type: application/json" \ + -d '{ + "payment_method": { + "CreditCard": { + "card_number": "4111111111111111", + "expiry_month": 12, + "expiry_year": 2025, + "cvv": "123", + "cardholder_name": "John Doe" + } + }, + "shipping_address": { + "name": "John Doe", + "street": "123 Main St", + "city": "Seattle", + "state": "WA", + "zip_code": "98101", + "country": "USA" + } + }' +``` + +### Step 4: Cleanup + +```bash +# 1. Clear user's cart +curl -X POST http://localhost:8080/api/cart/user001/clear + +# 2. Clean up all data +curl -X POST http://localhost:8080/api/admin/cleanup +``` + +--- + +## Error Handling + +### Common HTTP Status Codes + +- **200 OK**: Successful GET, PUT requests +- **201 Created**: Successful POST requests +- **204 No Content**: Successful DELETE requests +- **400 Bad Request**: Invalid request data +- **404 Not Found**: Resource not found +- **409 Conflict**: Business logic conflict (e.g., insufficient stock) +- **500 Internal Server Error**: Server error + +### Error Response Format + +All errors return a JSON response with the following structure: + +```json +{ + "error": "Error message describing what went wrong", + "timestamp": "2024-01-15T10:30:00Z" +} +``` + +### Example Error Responses + +**404 Not Found**: +```json +{ + "error": "Food not found with ID: F99999999", + "timestamp": "2024-01-15T10:30:00Z" +} +``` + +**400 Bad Request**: +```json +{ + "error": "Invalid pet type: invalid_pet", + "timestamp": "2024-01-15T10:30:00Z" +} +``` + +**409 Conflict**: +```json +{ + "error": "Insufficient stock for food F12345678. Available: 5, Requested: 10", + "timestamp": "2024-01-15T10:30:00Z" +} +``` + +**400 Bad Request (Empty Cart)**: +```json +{ + "error": "Cannot checkout empty cart", + "timestamp": "2024-01-15T10:30:00Z" +} +``` + +**400 Bad Request (Invalid Payment Method)**: +```json +{ + "error": "Invalid payment method format", + "timestamp": "2024-01-15T10:30:00Z" +} +``` + +--- + +## Data Models + +### Pet Types +- `puppy` +- `kitten` +- `bunny` + +### Food Types +- `dry` +- `wet` +- `treats` +- `supplements` + +### Availability Status +- `in_stock` +- `out_of_stock` +- `discontinued` +- `pre_order` + +### Payment Methods +The checkout endpoint supports the following payment methods: + +**Credit Card**: +```json +{ + "CreditCard": { + "card_number": "4111111111111111", + "expiry_month": 12, + "expiry_year": 2025, + "cvv": "123", + "cardholder_name": "John Doe" + } +} +``` + +**PayPal**: +```json +{ + "PayPal": { + "email": "user@example.com" + } +} +``` + +**Bank Transfer**: +```json +{ + "BankTransfer": { + "account_number": "1234567890", + "routing_number": "021000021" + } +} +``` + +### Order Status +- `pending` - Order has been created but not yet confirmed +- `confirmed` - Order has been confirmed and payment processed +- `processing` - Order is being prepared for shipment +- `shipped` - Order has been shipped +- `delivered` - Order has been delivered +- `cancelled` - Order has been cancelled + +### Nutritional Information +The nutritional info object contains the following optional fields: +- `calories_per_serving` (number) - Calories per serving +- `protein_percentage` (decimal string) - Protein percentage (e.g., "28.0") +- `fat_percentage` (decimal string) - Fat percentage (e.g., "15.0") +- `carbohydrate_percentage` (decimal string) - Carbohydrate percentage +- `fiber_percentage` (decimal string) - Fiber percentage +- `moisture_percentage` (decimal string) - Moisture percentage +- `serving_size` (string) - Serving size description (e.g., "1 cup") +- `servings_per_container` (number) - Number of servings per container + +### Price Format +All prices are returned as decimal strings (e.g., `"12.99"`) to maintain precision. + +### Date Format +All timestamps use ISO 8601 format in UTC (e.g., `"2024-01-15T10:30:00Z"`). + +--- + +## Rate Limiting and Performance + +- No rate limiting is currently implemented +- Recommended to implement client-side request throttling for production use +- All endpoints support concurrent requests +- Cart operations are atomic per user + +--- + +## Image URL Handling + +### Dynamic Image URL Generation + +The PetFood service uses dynamic image URL generation to provide flexibility in serving images from different CDN providers: + +- **Storage**: Image paths with petfood prefix are stored in the database (e.g., "petfood/beef-turkey-kibbles.jpg") +- **Response**: Full CDN URLs are dynamically generated in API responses (e.g., "https://d1234567890.cloudfront.net/images/petfood/beef-turkey-kibbles.jpg") +- **Configuration**: The CDN base URL is configurable via the `PETFOOD_ASSETS_CDN_URL` environment variable +- **Benefits**: Easy switching between S3, CloudFront, or other CDN providers without database changes + +### Configuration + +Set the CDN URL via environment variable (with or without trailing slash): +```bash +# For S3 direct access +export PETFOOD_ASSETS_CDN_URL="https://petfood-assets.s3.amazonaws.com" + +# For CloudFront distribution +export PETFOOD_ASSETS_CDN_URL="https://d1234567890.cloudfront.net/images" +# or with trailing slash +export PETFOOD_ASSETS_CDN_URL="https://d1234567890.cloudfront.net/images/" +``` + +The service will automatically handle trailing slashes and combine the CDN URL with the stored path. \ No newline at end of file diff --git a/src/applications/microservices/petfood-rs/Cargo.lock b/src/applications/microservices/petfood-rs/Cargo.lock new file mode 100644 index 00000000..ceea1bf4 --- /dev/null +++ b/src/applications/microservices/petfood-rs/Cargo.lock @@ -0,0 +1,5194 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "addr2line" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "version_check", +] + +[[package]] +name = "ahash" +version = "0.8.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75" +dependencies = [ + "cfg-if", + "once_cell", + "version_check", + "zerocopy", +] + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + +[[package]] +name = "anes" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" + +[[package]] +name = "anstyle" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" + +[[package]] +name = "anyhow" +version = "1.0.98" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487" + +[[package]] +name = "arraydeque" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d902e3d592a523def97af8f317b08ce16b7ab854c1985a0c671e6f15cebc236" + +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "async-channel" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81953c529336010edd6d8e358f886d9581267795c61b19475b71314bffa46d35" +dependencies = [ + "concurrent-queue", + "event-listener", + "futures-core", +] + +[[package]] +name = "async-stream" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" +dependencies = [ + "async-stream-impl", + "futures-core", + "pin-project-lite", +] + +[[package]] +name = "async-stream-impl" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "async-trait" +version = "0.1.88" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8" + +[[package]] +name = "aws-config" +version = "1.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "483020b893cdef3d89637e428d588650c71cfae7ea2e6ecbaee4de4ff99fb2dd" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-sdk-sso", + "aws-sdk-ssooidc", + "aws-sdk-sts", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand 2.3.0", + "hex", + "http 1.3.1", + "ring", + "time", + "tokio", + "tracing", + "url", + "zeroize", +] + +[[package]] +name = "aws-credential-types" +version = "1.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1541072f81945fa1251f8795ef6c92c4282d74d59f88498ae7d4bf00f0ebdad9" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "zeroize", +] + +[[package]] +name = "aws-lc-rs" +version = "1.13.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c953fe1ba023e6b7730c0d4b031d06f267f23a46167dcbd40316644b10a17ba" +dependencies = [ + "aws-lc-sys", + "zeroize", +] + +[[package]] +name = "aws-lc-sys" +version = "0.30.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbfd150b5dbdb988bcc8fb1fe787eb6b7ee6180ca24da683b61ea5405f3d43ff" +dependencies = [ + "bindgen", + "cc", + "cmake", + "dunce", + "fs_extra", +] + +[[package]] +name = "aws-runtime" +version = "1.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c034a1bc1d70e16e7f4e4caf7e9f7693e4c9c24cd91cf17c2a0b21abaebc7c8b" +dependencies = [ + "aws-credential-types", + "aws-sigv4", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand 2.3.0", + "http 0.2.12", + "http-body 0.4.6", + "percent-encoding", + "pin-project-lite", + "tracing", + "uuid", +] + +[[package]] +name = "aws-sdk-dynamodb" +version = "1.87.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b458b2d33775d82881e99b93a7bac79ca0f2d86e9ec77d823d77adcf8d5bca0b" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand 2.3.0", + "http 0.2.12", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-eventbridge" +version = "1.88.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecfe4098c2d0d1064949c7cd0e580091b41fffae5361dd29f00033adeb43ad27" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand 2.3.0", + "http 0.2.12", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-ssm" +version = "1.88.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13b679e0643d1584864f65b272f451f6aa5acf6bada238f55400be814ea3e25e" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand 2.3.0", + "http 0.2.12", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-sso" +version = "1.79.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a847168f15b46329fa32c7aca4e4f1a2e072f9b422f0adb19756f2e1457f111" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand 2.3.0", + "http 0.2.12", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-ssooidc" +version = "1.80.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b654dd24d65568738593e8239aef279a86a15374ec926ae8714e2d7245f34149" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-types", + "bytes", + "fastrand 2.3.0", + "http 0.2.12", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sdk-sts" +version = "1.81.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c92ea8a7602321c83615c82b408820ad54280fb026e92de0eeea937342fafa24" +dependencies = [ + "aws-credential-types", + "aws-runtime", + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-json", + "aws-smithy-query", + "aws-smithy-runtime", + "aws-smithy-runtime-api", + "aws-smithy-types", + "aws-smithy-xml", + "aws-types", + "fastrand 2.3.0", + "http 0.2.12", + "regex-lite", + "tracing", +] + +[[package]] +name = "aws-sigv4" +version = "1.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "084c34162187d39e3740cb635acd73c4e3a551a36146ad6fe8883c929c9f876c" +dependencies = [ + "aws-credential-types", + "aws-smithy-http", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "crypto-bigint 0.5.5", + "form_urlencoded", + "hex", + "hmac", + "http 0.2.12", + "http 1.3.1", + "p256", + "percent-encoding", + "ring", + "sha2", + "subtle", + "time", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-async" +version = "1.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e190749ea56f8c42bf15dd76c65e14f8f765233e6df9b0506d9d934ebef867c" +dependencies = [ + "futures-util", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "aws-smithy-http" +version = "0.62.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c4dacf2d38996cf729f55e7a762b30918229917eca115de45dfa8dfb97796c9" +dependencies = [ + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.12", + "http 1.3.1", + "http-body 0.4.6", + "percent-encoding", + "pin-project-lite", + "pin-utils", + "tracing", +] + +[[package]] +name = "aws-smithy-http-client" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f108f1ca850f3feef3009bdcc977be201bca9a91058864d9de0684e64514bee0" +dependencies = [ + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "h2 0.3.27", + "h2 0.4.12", + "http 0.2.12", + "http 1.3.1", + "http-body 0.4.6", + "hyper 0.14.32", + "hyper 1.6.0", + "hyper-rustls 0.24.2", + "hyper-rustls 0.27.7", + "hyper-util", + "pin-project-lite", + "rustls 0.21.12", + "rustls 0.23.31", + "rustls-native-certs 0.8.1", + "rustls-pki-types", + "tokio", + "tower 0.5.2", + "tracing", +] + +[[package]] +name = "aws-smithy-json" +version = "0.61.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a16e040799d29c17412943bdbf488fd75db04112d0c0d4b9290bacf5ae0014b9" +dependencies = [ + "aws-smithy-types", +] + +[[package]] +name = "aws-smithy-observability" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9364d5989ac4dd918e5cc4c4bdcc61c9be17dcd2586ea7f69e348fc7c6cab393" +dependencies = [ + "aws-smithy-runtime-api", +] + +[[package]] +name = "aws-smithy-query" +version = "0.60.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2fbd61ceb3fe8a1cb7352e42689cec5335833cd9f94103a61e98f9bb61c64bb" +dependencies = [ + "aws-smithy-types", + "urlencoding", +] + +[[package]] +name = "aws-smithy-runtime" +version = "1.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e107ce0783019dbff59b3a244aa0c114e4a8c9d93498af9162608cd5474e796" +dependencies = [ + "aws-smithy-async", + "aws-smithy-http", + "aws-smithy-http-client", + "aws-smithy-observability", + "aws-smithy-runtime-api", + "aws-smithy-types", + "bytes", + "fastrand 2.3.0", + "http 0.2.12", + "http 1.3.1", + "http-body 0.4.6", + "http-body 1.0.1", + "pin-project-lite", + "pin-utils", + "tokio", + "tracing", +] + +[[package]] +name = "aws-smithy-runtime-api" +version = "1.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75d52251ed4b9776a3e8487b2a01ac915f73b2da3af8fc1e77e0fce697a550d4" +dependencies = [ + "aws-smithy-async", + "aws-smithy-types", + "bytes", + "http 0.2.12", + "http 1.3.1", + "pin-project-lite", + "tokio", + "tracing", + "zeroize", +] + +[[package]] +name = "aws-smithy-types" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d498595448e43de7f4296b7b7a18a8a02c61ec9349128c80a368f7c3b4ab11a8" +dependencies = [ + "base64-simd", + "bytes", + "bytes-utils", + "futures-core", + "http 0.2.12", + "http 1.3.1", + "http-body 0.4.6", + "http-body 1.0.1", + "http-body-util", + "itoa", + "num-integer", + "pin-project-lite", + "pin-utils", + "ryu", + "serde", + "time", + "tokio", + "tokio-util", +] + +[[package]] +name = "aws-smithy-xml" +version = "0.60.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db87b96cb1b16c024980f133968d52882ca0daaee3a086c6decc500f6c99728" +dependencies = [ + "xmlparser", +] + +[[package]] +name = "aws-types" +version = "1.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b069d19bf01e46298eaedd7c6f283fe565a59263e53eebec945f3e6398f42390" +dependencies = [ + "aws-credential-types", + "aws-smithy-async", + "aws-smithy-runtime-api", + "aws-smithy-types", + "rustc_version", + "tracing", +] + +[[package]] +name = "axum" +version = "0.6.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" +dependencies = [ + "async-trait", + "axum-core 0.3.4", + "bitflags 1.3.2", + "bytes", + "futures-util", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.32", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "sync_wrapper 0.1.2", + "tower 0.4.13", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edca88bc138befd0323b20752846e6587272d3b03b0343c8ea28a6f819e6e71f" +dependencies = [ + "async-trait", + "axum-core 0.4.5", + "bytes", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "hyper 1.6.0", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper 1.0.2", + "tokio", + "tower 0.5.2", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 0.2.12", + "http-body 0.4.6", + "mime", + "rustversion", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper 1.0.2", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "backtrace" +version = "0.3.75" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002" +dependencies = [ + "addr2line", + "cfg-if", + "libc", + "miniz_oxide", + "object", + "rustc-demangle", + "windows-targets 0.52.6", +] + +[[package]] +name = "base16ct" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349a06037c7bf932dd7e7d1f653678b2038b9ad46a74102f1fc7bd7872678cce" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "base64-simd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" +dependencies = [ + "outref", + "vsimd", +] + +[[package]] +name = "base64ct" +version = "1.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba" + +[[package]] +name = "bindgen" +version = "0.69.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" +dependencies = [ + "bitflags 2.9.1", + "cexpr", + "clang-sys", + "itertools 0.12.1", + "lazy_static", + "lazycell", + "log", + "prettyplease", + "proc-macro2", + "quote", + "regex", + "rustc-hash", + "shlex", + "syn 2.0.104", + "which", +] + +[[package]] +name = "bit-set" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" +dependencies = [ + "serde", +] + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bollard-stubs" +version = "1.42.0-rc.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed59b5c00048f48d7af971b71f800fdf23e858844a6f9e4d32ca72e9399e7864" +dependencies = [ + "serde", + "serde_with", +] + +[[package]] +name = "borsh" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "bumpalo" +version = "3.19.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" + +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "bytes" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" + +[[package]] +name = "bytes-utils" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7dafe3a8757b027e2be6e4e5601ed563c55989fcf1546e933c66c8eb3a058d35" +dependencies = [ + "bytes", + "either", +] + +[[package]] +name = "cast" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" + +[[package]] +name = "cc" +version = "1.2.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2352e5597e9c544d5e6d9c95190d5d27738ade584fa8db0a16e130e5c2b5296e" +dependencies = [ + "jobserver", + "libc", + "shlex", +] + +[[package]] +name = "cexpr" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766" +dependencies = [ + "nom", +] + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "serde", + "wasm-bindgen", + "windows-link", +] + +[[package]] +name = "ciborium" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42e69ffd6f0917f5c029256a24d0161db17cea3997d185db0d35926308770f0e" +dependencies = [ + "ciborium-io", + "ciborium-ll", + "serde", +] + +[[package]] +name = "ciborium-io" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05afea1e0a06c9be33d539b876f1ce3692f4afea2cb41f740e7743225ed1c757" + +[[package]] +name = "ciborium-ll" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57663b653d948a338bfb3eeba9bb2fd5fcfaecb9e199e87e1eda4d9e8b240fd9" +dependencies = [ + "ciborium-io", + "half", +] + +[[package]] +name = "clang-sys" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4" +dependencies = [ + "glob", + "libc", + "libloading", +] + +[[package]] +name = "clap" +version = "4.5.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50fd97c9dc2399518aa331917ac6f274280ec5eb34e555dd291899745c48ec6f" +dependencies = [ + "clap_builder", +] + +[[package]] +name = "clap_builder" +version = "4.5.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c35b5830294e1fa0462034af85cc95225a4cb07092c088c55bda3147cfcd8f65" +dependencies = [ + "anstyle", + "clap_lex", +] + +[[package]] +name = "clap_lex" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675" + +[[package]] +name = "cmake" +version = "0.1.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7caa3f9de89ddbe2c607f4101924c5abec803763ae9534e4f4d7d8f84aa81f0" +dependencies = [ + "cc", +] + +[[package]] +name = "concurrent-queue" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ca0197aee26d1ae37445ee532fefce43251d24cc7c166799f4d46817f1d3973" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "config" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68578f196d2a33ff61b27fae256c3164f65e36382648e30666dde05b8cc9dfdf" +dependencies = [ + "async-trait", + "convert_case", + "json5", + "nom", + "pathdiff", + "ron", + "rust-ini", + "serde", + "serde_json", + "toml", + "yaml-rust2", +] + +[[package]] +name = "const-oid" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" + +[[package]] +name = "const-random" +version = "0.1.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e00182fe74b066627d63b85fd550ac2998d4b0bd86bfed477a0ae4c7c71359" +dependencies = [ + "const-random-macro", +] + +[[package]] +name = "const-random-macro" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9d839f2a20b0aee515dc581a6172f2321f96cab76c1a38a4c584a194955390e" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "tiny-keccak", +] + +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "core-foundation" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "cpufeatures" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" +dependencies = [ + "libc", +] + +[[package]] +name = "criterion" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2b12d017a929603d80db1831cd3a24082f8137ce19c69e6447f54f5fc8d692f" +dependencies = [ + "anes", + "cast", + "ciborium", + "clap", + "criterion-plot", + "is-terminal", + "itertools 0.10.5", + "num-traits", + "once_cell", + "oorandom", + "plotters", + "rayon", + "regex", + "serde", + "serde_derive", + "serde_json", + "tinytemplate", + "walkdir", +] + +[[package]] +name = "criterion-plot" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6b50826342786a51a89e2da3a28f1c32b06e387201bc2d19791f622c673706b1" +dependencies = [ + "cast", + "itertools 0.10.5", +] + +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-utils" +version = "0.8.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" + +[[package]] +name = "crunchy" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "460fbee9c2c2f33933d720630a6a0bac33ba7053db5344fac858d4b8952d77d5" + +[[package]] +name = "crypto-bigint" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef2b4b23cddf68b89b8f8069890e8c270d54e2d5fe1b143820234805e4cb17ef" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-bigint" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "darling" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "859d65a907b6852c9361e3185c862aae7fafd2887876799fa55f5f99dc40d610" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 1.0.109", +] + +[[package]] +name = "darling_macro" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" +dependencies = [ + "darling_core", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "deadpool" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "421fe0f90f2ab22016f32a9881be5134fdd71c65298917084b0c7477cbc3856e" +dependencies = [ + "async-trait", + "deadpool-runtime", + "num_cpus", + "retain_mut", + "tokio", +] + +[[package]] +name = "deadpool-runtime" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" + +[[package]] +name = "der" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de" +dependencies = [ + "const-oid", + "zeroize", +] + +[[package]] +name = "deranged" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +dependencies = [ + "powerfmt", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", + "subtle", +] + +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "dlv-list" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "442039f5147480ba31067cb00ada1adae6892028e40e45fc5de7b7df6dcc1b5f" +dependencies = [ + "const-random", +] + +[[package]] +name = "downcast" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1435fa1053d8b2fbbe9be7e97eca7f33d37b28409959813daefc1446a14247f1" + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "ecdsa" +version = "0.14.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "413301934810f597c1d19ca71c8710e99a3f1ba28a0d2ebc01551a2daeea3c5c" +dependencies = [ + "der", + "elliptic-curve", + "rfc6979", + "signature", +] + +[[package]] +name = "either" +version = "1.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" + +[[package]] +name = "elliptic-curve" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7bb888ab5300a19b8e5bceef25ac745ad065f3c9f7efc6de1b91958110891d3" +dependencies = [ + "base16ct", + "crypto-bigint 0.4.9", + "der", + "digest", + "ff", + "generic-array", + "group", + "pkcs8", + "rand_core 0.6.4", + "sec1", + "subtle", + "zeroize", +] + +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "envy" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f47e0157f2cb54f5ae1bd371b30a2ae4311e1c028f575cd4e81de7353215965" +dependencies = [ + "serde", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "errno" +version = "0.3.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "event-listener" +version = "2.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "fastrand" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" + +[[package]] +name = "ff" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d013fc25338cc558c5c2cfbad646908fb23591e2404481826742b651c9af7160" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "foreign-types" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" +dependencies = [ + "foreign-types-shared", +] + +[[package]] +name = "foreign-types-shared" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "fragile" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619" + +[[package]] +name = "fs_extra" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42703706b716c37f96a77aea830392ad231f44c9e9a67872fa5548707e11b11c" + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" +dependencies = [ + "futures-core", + "futures-sink", +] + +[[package]] +name = "futures-core" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" + +[[package]] +name = "futures-executor" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-io" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-lite" +version = "1.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49a9d51ce47660b1e808d3c990b4709f2f415d928835a17dfd16991515c46bce" +dependencies = [ + "fastrand 1.9.0", + "futures-core", + "futures-io", + "memchr", + "parking", + "pin-project-lite", + "waker-fn", +] + +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "futures-sink" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" + +[[package]] +name = "futures-task" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" + +[[package]] +name = "futures-timer" +version = "3.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" + +[[package]] +name = "futures-util" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fc3cb4d91f53b50155bdcfd23f6a4c39ae1969c2ae85982b135750cccaf5fce" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.9.0+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", +] + +[[package]] +name = "getrandom" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasi 0.14.2+wasi-0.2.4", +] + +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" + +[[package]] +name = "glob" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" + +[[package]] +name = "group" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5dfbfb3a6cfbd390d5c9564ab283a0349b9b9fcd46a706c1eb10e0db70bfbac7" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "h2" +version = "0.3.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0beca50380b1fc32983fc1cb4587bfa4bb9e78fc259aad4a0032d2080309222d" +dependencies = [ + "bytes", + "fnv", + "futures-core", + "futures-sink", + "futures-util", + "http 0.2.12", + "indexmap 2.10.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "h2" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http 1.3.1", + "indexmap 2.10.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "half" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "459196ed295495a68f7d7fe1d84f6c4b7ff0e21fe3017b2f283c6fac3ad803c9" +dependencies = [ + "cfg-if", + "crunchy", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] + +[[package]] +name = "hashbrown" +version = "0.14.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash 0.8.12", + "allocator-api2", +] + +[[package]] +name = "hashbrown" +version = "0.15.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1" + +[[package]] +name = "hashlink" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" +dependencies = [ + "hashbrown 0.14.5", +] + +[[package]] +name = "hermit-abi" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hmac" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" +dependencies = [ + "digest", +] + +[[package]] +name = "home" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589533453244b0995c858700322199b2becb13b627df2851f64a2775d024abcf" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" +dependencies = [ + "bytes", + "http 0.2.12", + "pin-project-lite", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http 1.3.1", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http 1.3.1", + "http-body 1.0.1", + "pin-project-lite", +] + +[[package]] +name = "http-types" +version = "2.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9b187a72d63adbfba487f48095306ac823049cb504ee195541e91c7775f5ad" +dependencies = [ + "anyhow", + "async-channel", + "base64 0.13.1", + "futures-lite", + "http 0.2.12", + "infer", + "pin-project-lite", + "rand 0.7.3", + "serde", + "serde_json", + "serde_qs", + "serde_urlencoded", + "url", +] + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "0.14.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41dfc780fdec9373c01bae43289ea34c972e40ee3c9f6b3c8801a35f35586ce7" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "h2 0.3.27", + "http 0.2.12", + "http-body 0.4.6", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "socket2 0.5.10", + "tokio", + "tower-service", + "tracing", + "want", +] + +[[package]] +name = "hyper" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "h2 0.4.12", + "http 1.3.1", + "http-body 1.0.1", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +dependencies = [ + "futures-util", + "http 0.2.12", + "hyper 0.14.32", + "log", + "rustls 0.21.12", + "rustls-native-certs 0.6.3", + "tokio", + "tokio-rustls 0.24.1", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http 1.3.1", + "hyper 1.6.0", + "hyper-util", + "rustls 0.23.31", + "rustls-native-certs 0.8.1", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.2", + "tower-service", +] + +[[package]] +name = "hyper-timeout" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" +dependencies = [ + "hyper 0.14.32", + "pin-project-lite", + "tokio", + "tokio-io-timeout", +] + +[[package]] +name = "hyper-tls" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" +dependencies = [ + "bytes", + "hyper 0.14.32", + "native-tls", + "tokio", + "tokio-native-tls", +] + +[[package]] +name = "hyper-util" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d9b05277c7e8da2c93a568989bb6207bef0112e8d17df7a6eda4a3cf143bc5e" +dependencies = [ + "bytes", + "futures-channel", + "futures-core", + "futures-util", + "http 1.3.1", + "http-body 1.0.1", + "hyper 1.6.0", + "libc", + "pin-project-lite", + "socket2 0.6.0", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + +[[package]] +name = "icu_collections" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47" +dependencies = [ + "displaydoc", + "potential_utf", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locale_core" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_normalizer" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3" + +[[package]] +name = "icu_properties" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locale_core", + "icu_properties_data", + "icu_provider", + "potential_utf", + "zerotrie", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632" + +[[package]] +name = "icu_provider" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af" +dependencies = [ + "displaydoc", + "icu_locale_core", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerotrie", + "zerovec", +] + +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + +[[package]] +name = "idna" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" +dependencies = [ + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344" +dependencies = [ + "icu_normalizer", + "icu_properties", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661" +dependencies = [ + "equivalent", + "hashbrown 0.15.5", +] + +[[package]] +name = "infer" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64e9829a50b42bb782c1df523f78d332fe371b10c661e78b7a3c34b0198e9fac" + +[[package]] +name = "instant" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-uring" +version = "0.7.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "libc", +] + +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + +[[package]] +name = "is-terminal" +version = "0.4.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + +[[package]] +name = "itoa" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" + +[[package]] +name = "jobserver" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38f262f097c174adebe41eb73d66ae9c06b2844fb0da69969647bbddd9b0538a" +dependencies = [ + "getrandom 0.3.3", + "libc", +] + +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "json5" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" +dependencies = [ + "pest", + "pest_derive", + "serde", +] + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "lazycell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" + +[[package]] +name = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + +[[package]] +name = "libloading" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667" +dependencies = [ + "cfg-if", + "windows-targets 0.53.3", +] + +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + +[[package]] +name = "linux-raw-sys" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12" + +[[package]] +name = "litemap" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" + +[[package]] +name = "lock_api" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "log" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" + +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + +[[package]] +name = "memchr" +version = "2.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", +] + +[[package]] +name = "mio" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" +dependencies = [ + "libc", + "wasi 0.11.1+wasi-snapshot-preview1", + "windows-sys 0.59.0", +] + +[[package]] +name = "mockall" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43766c2b5203b10de348ffe19f7e54564b64f3d6018ff7648d1e2d6d3a0f0a48" +dependencies = [ + "cfg-if", + "downcast", + "fragile", + "lazy_static", + "mockall_derive", + "predicates", + "predicates-tree", +] + +[[package]] +name = "mockall_derive" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af7cbce79ec385a1d4f54baa90a76401eb15d9cab93685f62e7e9f942aa00ae2" +dependencies = [ + "cfg-if", + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "native-tls" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" +dependencies = [ + "libc", + "log", + "openssl", + "openssl-probe", + "openssl-sys", + "schannel", + "security-framework 2.11.1", + "security-framework-sys", + "tempfile", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "num_cpus" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91df4bbde75afed763b708b7eee1e8e7651e02d97f6d5dd763e89367e957b23b" +dependencies = [ + "hermit-abi", + "libc", +] + +[[package]] +name = "object" +version = "0.36.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87" +dependencies = [ + "memchr", +] + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "oorandom" +version = "11.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6790f58c7ff633d8771f42965289203411a5e5c68388703c06e14f24770b41e" + +[[package]] +name = "openssl" +version = "0.10.73" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8" +dependencies = [ + "bitflags 2.9.1", + "cfg-if", + "foreign-types", + "libc", + "once_cell", + "openssl-macros", + "openssl-sys", +] + +[[package]] +name = "openssl-macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "openssl-probe" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" + +[[package]] +name = "openssl-sys" +version = "0.9.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571" +dependencies = [ + "cc", + "libc", + "pkg-config", + "vcpkg", +] + +[[package]] +name = "opentelemetry" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e32339a5dc40459130b3bd269e9892439f55b33e772d2a9d402a789baaf4e8a" +dependencies = [ + "futures-core", + "futures-sink", + "indexmap 2.10.0", + "js-sys", + "once_cell", + "pin-project-lite", + "thiserror 1.0.69", + "urlencoding", +] + +[[package]] +name = "opentelemetry-otlp" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f24cda83b20ed2433c68241f918d0f6fdec8b1d43b7a9590ab4420c5095ca930" +dependencies = [ + "async-trait", + "futures-core", + "http 0.2.12", + "opentelemetry", + "opentelemetry-proto", + "opentelemetry-semantic-conventions", + "opentelemetry_sdk", + "prost", + "thiserror 1.0.69", + "tokio", + "tonic", +] + +[[package]] +name = "opentelemetry-proto" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2e155ce5cc812ea3d1dffbd1539aed653de4bf4882d60e6e04dcf0901d674e1" +dependencies = [ + "opentelemetry", + "opentelemetry_sdk", + "prost", + "tonic", +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5774f1ef1f982ef2a447f6ee04ec383981a3ab99c8e77a1a7b30182e65bbc84" +dependencies = [ + "opentelemetry", +] + +[[package]] +name = "opentelemetry_sdk" +version = "0.21.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f16aec8a98a457a52664d69e0091bac3a0abd18ead9b641cb00202ba4e0efe4" +dependencies = [ + "async-trait", + "crossbeam-channel", + "futures-channel", + "futures-executor", + "futures-util", + "glob", + "once_cell", + "opentelemetry", + "ordered-float", + "percent-encoding", + "rand 0.8.5", + "thiserror 1.0.69", + "tokio", + "tokio-stream", +] + +[[package]] +name = "ordered-float" +version = "4.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7bb71e1b3fa6ca1c61f383464aaf2bb0e2f8e772a1f01d486832464de363b951" +dependencies = [ + "num-traits", +] + +[[package]] +name = "ordered-multimap" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49203cdcae0030493bad186b28da2fa25645fa276a51b6fec8010d281e02ef79" +dependencies = [ + "dlv-list", + "hashbrown 0.14.5", +] + +[[package]] +name = "outref" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" + +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + +[[package]] +name = "p256" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51f44edd08f51e2ade572f141051021c5af22677e42b7dd28a88155151c33594" +dependencies = [ + "ecdsa", + "elliptic-curve", + "sha2", +] + +[[package]] +name = "parking" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba" + +[[package]] +name = "parking_lot" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets 0.52.6", +] + +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "pest" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1db05f56d34358a8b1066f67cbb203ee3e7ed2ba674a6263a1d5ec6db2204323" +dependencies = [ + "memchr", + "thiserror 2.0.12", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb056d9e8ea77922845ec74a1c4e8fb17e7c218cc4fc11a15c5d25e189aa40bc" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87e404e638f781eb3202dc82db6760c8ae8a1eeef7fb3fa8264b2ef280504966" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "pest_meta" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd1101f170f5903fde0914f899bb503d9ff5271d7ba76bbb70bea63690cc0d5" +dependencies = [ + "pest", + "sha2", +] + +[[package]] +name = "petfood-rs" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "aws-config", + "aws-sdk-dynamodb", + "aws-sdk-eventbridge", + "aws-sdk-ssm", + "aws-smithy-runtime-api", + "aws-smithy-types", + "axum 0.7.9", + "chrono", + "config", + "criterion", + "envy", + "mockall", + "once_cell", + "opentelemetry", + "opentelemetry-otlp", + "opentelemetry_sdk", + "prometheus", + "proptest", + "proptest-derive", + "reqwest", + "rust_decimal", + "rust_decimal_macros", + "serde", + "serde_json", + "testcontainers", + "testcontainers-modules", + "thiserror 1.0.69", + "tokio", + "tokio-test", + "tower 0.4.13", + "tower-http", + "tracing", + "tracing-opentelemetry", + "tracing-subscriber", + "uuid", + "wiremock", +] + +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "pkcs8" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba" +dependencies = [ + "der", + "spki", +] + +[[package]] +name = "pkg-config" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c" + +[[package]] +name = "plotters" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5aeb6f403d7a4911efb1e33402027fc44f29b5bf6def3effcc22d7bb75f2b747" +dependencies = [ + "num-traits", + "plotters-backend", + "plotters-svg", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "plotters-backend" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df42e13c12958a16b3f7f4386b9ab1f3e7933914ecea48da7139435263a4172a" + +[[package]] +name = "plotters-svg" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51bae2ac328883f7acdfea3d66a7c35751187f870bc81f94563733a154d7a670" +dependencies = [ + "plotters-backend", +] + +[[package]] +name = "potential_utf" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585" +dependencies = [ + "zerovec", +] + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "ppv-lite86" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "predicates" +version = "3.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d19ee57562043d37e82899fade9a22ebab7be9cef5026b07fda9cdd4293573" +dependencies = [ + "anstyle", + "predicates-core", +] + +[[package]] +name = "predicates-core" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "727e462b119fe9c93fd0eb1429a5f7647394014cf3c04ab2c0350eeb09095ffa" + +[[package]] +name = "predicates-tree" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72dd2d6d381dfb73a193c7fca536518d7caee39fc8503f74e7dc0be0531b425c" +dependencies = [ + "predicates-core", + "termtree", +] + +[[package]] +name = "prettyplease" +version = "0.2.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ff24dfcda44452b9816fff4cd4227e1bb73ff5a2f1bc1105aa92fb8565ce44d2" +dependencies = [ + "proc-macro2", + "syn 2.0.104", +] + +[[package]] +name = "proc-macro-crate" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro2" +version = "1.0.95" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "prometheus" +version = "0.13.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d33c28a30771f7f96db69893f78b857f7450d7e0237e9c8fc6427a81bae7ed1" +dependencies = [ + "cfg-if", + "fnv", + "lazy_static", + "memchr", + "parking_lot", + "protobuf", + "thiserror 1.0.69", +] + +[[package]] +name = "proptest" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fcdab19deb5195a31cf7726a210015ff1496ba1464fd42cb4f537b8b01b471f" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags 2.9.1", + "lazy_static", + "num-traits", + "rand 0.9.2", + "rand_chacha 0.9.0", + "rand_xorshift", + "regex-syntax 0.8.5", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "proptest-derive" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9cf16337405ca084e9c78985114633b6827711d22b9e6ef6c6c0d665eb3f0b6e" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "prost" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.11.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" +dependencies = [ + "anyhow", + "itertools 0.10.5", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "protobuf" +version = "2.28.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "106dd99e98437432fed6519dedecfade6a06a73bb7b2a1e019fdd2bee5778d94" + +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "getrandom 0.1.16", + "libc", + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" +dependencies = [ + "rand_chacha 0.9.0", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" +dependencies = [ + "ppv-lite86", + "rand_core 0.9.3", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" +dependencies = [ + "getrandom 0.1.16", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom 0.2.16", +] + +[[package]] +name = "rand_core" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38" +dependencies = [ + "getrandom 0.3.3", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_xorshift" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "513962919efc330f829edb2535844d1b912b0fbe2ca165d613e4e8788bb05a5a" +dependencies = [ + "rand_core 0.9.3", +] + +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + +[[package]] +name = "redox_syscall" +version = "0.5.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" +dependencies = [ + "bitflags 2.9.1", +] + +[[package]] +name = "regex" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.9", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-lite" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a49587ad06b26609c52e423de037e7f57f20d53535d66e08c695f347df952a" + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + +[[package]] +name = "reqwest" +version = "0.11.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" +dependencies = [ + "base64 0.21.7", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2 0.3.27", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.32", + "hyper-tls", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper 0.1.2", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "winreg", +] + +[[package]] +name = "retain_mut" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4389f1d5789befaf6029ebd9f7dac4af7f7e3d61b69d4f30e2ac02b57e7712b0" + +[[package]] +name = "rfc6979" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7743f17af12fa0b03b803ba12cd6a8d9483a587e89c69445e3909655c0b9fabb" +dependencies = [ + "crypto-bigint 0.4.9", + "hmac", + "zeroize", +] + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom 0.2.16", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rkyv" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ron" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b91f7eff05f748767f183df4320a63d6936e9c6107d97c9e6bdd9784f4289c94" +dependencies = [ + "base64 0.21.7", + "bitflags 2.9.1", + "serde", + "serde_derive", +] + +[[package]] +name = "rust-ini" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e0698206bcb8882bf2a9ecb4c1e7785db57ff052297085a6efd4fe42302068a" +dependencies = [ + "cfg-if", + "ordered-multimap", +] + +[[package]] +name = "rust_decimal" +version = "1.37.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b203a6425500a03e0919c42d3c47caca51e79f1132046626d2c8871c5092035d" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", +] + +[[package]] +name = "rust_decimal_macros" +version = "1.37.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6268b74858287e1a062271b988a0c534bf85bbeb567fe09331bf40ed78113d5" +dependencies = [ + "quote", + "syn 2.0.104", +] + +[[package]] +name = "rustc-demangle" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f7d92ca342cea22a06f2121d944b4fd82af56988c270852495420f961d4ace" + +[[package]] +name = "rustc-hash" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" + +[[package]] +name = "rustc_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.9.1", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + +[[package]] +name = "rustix" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" +dependencies = [ + "bitflags 2.9.1", + "errno", + "libc", + "linux-raw-sys 0.9.4", + "windows-sys 0.60.2", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki 0.101.7", + "sct", +] + +[[package]] +name = "rustls" +version = "0.23.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" +dependencies = [ + "aws-lc-rs", + "once_cell", + "rustls-pki-types", + "rustls-webpki 0.103.4", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +dependencies = [ + "openssl-probe", + "rustls-pemfile", + "schannel", + "security-framework 2.11.1", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework 3.3.0", +] + +[[package]] +name = "rustls-pemfile" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + +[[package]] +name = "rustls-pki-types" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc" +dependencies = [ + "aws-lc-rs", + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "rusty-fork" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + +[[package]] +name = "ryu" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" + +[[package]] +name = "same-file" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" +dependencies = [ + "winapi-util", +] + +[[package]] +name = "schannel" +version = "0.1.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + +[[package]] +name = "sec1" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3be24c1842290c45df0a7bf069e0c268a747ad05a192f2fd7dcfdbc1cba40928" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + +[[package]] +name = "security-framework" +version = "2.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" +dependencies = [ + "bitflags 2.9.1", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c" +dependencies = [ + "bitflags 2.9.1", + "core-foundation 0.10.1", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "semver" +version = "1.0.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" + +[[package]] +name = "serde" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.219" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "serde_json" +version = "1.0.142" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "030fedb782600dcbd6f02d479bf0d817ac3bb40d644745b769d6a96bc3afc5a7" +dependencies = [ + "itoa", + "memchr", + "ryu", + "serde", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59fab13f937fa393d08645bf3a84bdfe86e296747b506ada67bb15f10f218b2a" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_qs" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7715380eec75f029a4ef7de39a9200e0a63823176b759d055b613f5a87df6a6" +dependencies = [ + "percent-encoding", + "serde", + "thiserror 1.0.69", +] + +[[package]] +name = "serde_spanned" +version = "0.6.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" +dependencies = [ + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_with" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "678b5a069e50bf00ecd22d0cd8ddf7c236f68581b03db652061ed5eb13a312ff" +dependencies = [ + "serde", + "serde_with_macros", +] + +[[package]] +name = "serde_with_macros" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "sha2" +version = "0.10.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signal-hook-registry" +version = "1.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a4719bff48cee6b39d12c020eeb490953ad2443b7055bd0b21fca26bd8c28b" +dependencies = [ + "libc", +] + +[[package]] +name = "signature" +version = "1.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74233d3b3b2f6d4b006dc19dee745e73e2a6bfb6f93607cd3b02bd5b00797d7c" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.5.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "socket2" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "233504af464074f9d066d7b5416c5f9b894a5862a6506e306f7b816cdd6f1807" +dependencies = [ + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "spki" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b" +dependencies = [ + "base64ct", + "der", +] + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + +[[package]] +name = "strsim" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "synstructure" +version = "0.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation 0.9.4", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tempfile" +version = "3.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1" +dependencies = [ + "fastrand 2.3.0", + "getrandom 0.3.3", + "once_cell", + "rustix 1.0.8", + "windows-sys 0.59.0", +] + +[[package]] +name = "termtree" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" + +[[package]] +name = "testcontainers" +version = "0.15.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f83d2931d7f521af5bae989f716c3fa43a6af9af7ec7a5e21b59ae40878cec00" +dependencies = [ + "bollard-stubs", + "futures", + "hex", + "hmac", + "log", + "rand 0.8.5", + "serde", + "serde_json", + "sha2", +] + +[[package]] +name = "testcontainers-modules" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8debb5e215d9e89ea93255fffff00bf037ea44075d7a2669a21a8a988d6b52fd" +dependencies = [ + "testcontainers", +] + +[[package]] +name = "thiserror" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" +dependencies = [ + "thiserror-impl 1.0.69", +] + +[[package]] +name = "thiserror" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" +dependencies = [ + "thiserror-impl 2.0.12", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +dependencies = [ + "deranged", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" + +[[package]] +name = "time-macros" +version = "0.2.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinystr" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b" +dependencies = [ + "displaydoc", + "zerovec", +] + +[[package]] +name = "tinytemplate" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be4d6b5f19ff7664e8c98d03e2139cb510db9b0a60b55f8e8709b689d939b6bc" +dependencies = [ + "serde", + "serde_json", +] + +[[package]] +name = "tinyvec" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "tokio" +version = "1.47.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "89e49afdadebb872d3145a5638b59eb0691ea23e46ca484037cfab3b76b95038" +dependencies = [ + "backtrace", + "bytes", + "io-uring", + "libc", + "mio", + "parking_lot", + "pin-project-lite", + "signal-hook-registry", + "slab", + "socket2 0.6.0", + "tokio-macros", + "windows-sys 0.59.0", +] + +[[package]] +name = "tokio-io-timeout" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bd86198d9ee903fedd2f9a2e72014287c0d9167e4ae43b5853007205dda1b76" +dependencies = [ + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-macros" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.24.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +dependencies = [ + "rustls 0.21.12", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b" +dependencies = [ + "rustls 0.23.31", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-test" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2468baabc3311435b55dd935f702f42cd1b8abb7e754fb7dfb16bd36aa88f9f7" +dependencies = [ + "async-stream", + "bytes", + "futures-core", + "tokio", + "tokio-stream", +] + +[[package]] +name = "tokio-util" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "0.8.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" +dependencies = [ + "serde", + "serde_spanned", + "toml_datetime", + "toml_edit", +] + +[[package]] +name = "toml_datetime" +version = "0.6.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.22.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" +dependencies = [ + "indexmap 2.10.0", + "serde", + "serde_spanned", + "toml_datetime", + "toml_write", + "winnow", +] + +[[package]] +name = "toml_write" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" + +[[package]] +name = "tonic" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3082666a3a6433f7f511c7192923fa1fe07c69332d3c6a2e6bb040b569199d5a" +dependencies = [ + "async-trait", + "axum 0.6.20", + "base64 0.21.7", + "bytes", + "futures-core", + "futures-util", + "h2 0.3.27", + "http 0.2.12", + "http-body 0.4.6", + "hyper 0.14.32", + "hyper-timeout", + "percent-encoding", + "pin-project", + "prost", + "tokio", + "tokio-stream", + "tower 0.4.13", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "indexmap 1.9.3", + "pin-project", + "pin-project-lite", + "rand 0.8.5", + "slab", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper 1.0.2", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "bitflags 2.9.1", + "bytes", + "http 1.3.1", + "http-body 1.0.1", + "http-body-util", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "tracing-core" +version = "0.1.34" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-opentelemetry" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c67ac25c5407e7b961fafc6f7e9aa5958fd297aada2d20fa2ae1737357e55596" +dependencies = [ + "js-sys", + "once_cell", + "opentelemetry", + "opentelemetry_sdk", + "smallvec", + "tracing", + "tracing-core", + "tracing-log", + "tracing-subscriber", + "web-time", +] + +[[package]] +name = "tracing-serde" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1" +dependencies = [ + "serde", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "serde", + "serde_json", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", + "tracing-serde", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typenum" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" + +[[package]] +name = "ucd-trie" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971" + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicode-ident" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "url" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", + "serde", +] + +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + +[[package]] +name = "uuid" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3cf4199d1e5d15ddd86a694e4d0dffa9c323ce759fea589f00fef9d81cc1931d" +dependencies = [ + "getrandom 0.3.3", + "js-sys", + "serde", + "wasm-bindgen", +] + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "vsimd" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" + +[[package]] +name = "wait-timeout" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ac3b126d3914f9849036f826e054cbabdc8519970b8998ddaf3b5bd3c65f11" +dependencies = [ + "libc", +] + +[[package]] +name = "waker-fn" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317211a0dc0ceedd78fb2ca9a44aed3d7b9b26f81870d485c07122b4350673b7" + +[[package]] +name = "walkdir" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" +dependencies = [ + "same-file", + "winapi-util", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.9.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cccddf32554fecc6acb585f82a32a72e28b48f8c4c1883ddfeeeaa96f7d8e519" + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "wasi" +version = "0.14.2+wasi-0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3" +dependencies = [ + "wit-bindgen-rt", +] + +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.104", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa30049b1c872b72c89866d458eae9f20380ab280ffd1b1e18df2d3e2d98cfe0" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "which" +version = "4.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" +dependencies = [ + "either", + "home", + "once_cell", + "rustix 0.38.44", +] + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-core" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-link", + "windows-result", + "windows-strings", +] + +[[package]] +name = "windows-implement" +version = "0.60.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "windows-interface" +version = "0.59.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "windows-link" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" + +[[package]] +name = "windows-result" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-strings" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.3", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.0", + "windows_aarch64_msvc 0.53.0", + "windows_i686_gnu 0.53.0", + "windows_i686_gnullvm 0.53.0", + "windows_i686_msvc 0.53.0", + "windows_x86_64_gnu 0.53.0", + "windows_x86_64_gnullvm 0.53.0", + "windows_x86_64_msvc 0.53.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486" + +[[package]] +name = "winnow" +version = "0.7.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95" +dependencies = [ + "memchr", +] + +[[package]] +name = "winreg" +version = "0.50.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +dependencies = [ + "cfg-if", + "windows-sys 0.48.0", +] + +[[package]] +name = "wiremock" +version = "0.5.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13a3a53eaf34f390dd30d7b1b078287dd05df2aa2e21a589ccb80f5c7253c2e9" +dependencies = [ + "assert-json-diff", + "async-trait", + "base64 0.21.7", + "deadpool", + "futures", + "futures-timer", + "http-types", + "hyper 0.14.32", + "log", + "once_cell", + "regex", + "serde", + "serde_json", + "tokio", +] + +[[package]] +name = "wit-bindgen-rt" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" +dependencies = [ + "bitflags 2.9.1", +] + +[[package]] +name = "writeable" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "xmlparser" +version = "0.13.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" + +[[package]] +name = "yaml-rust2" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8902160c4e6f2fb145dbe9d6760a75e3c9522d8bf796ed7047c85919ac7115f8" +dependencies = [ + "arraydeque", + "encoding_rs", + "hashlink", +] + +[[package]] +name = "yoke" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", + "synstructure", +] + +[[package]] +name = "zerocopy" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.8.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + +[[package]] +name = "zerofrom" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", + "synstructure", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerotrie" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", +] + +[[package]] +name = "zerovec" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7aa2bd55086f1ab526693ecbe444205da57e25f4489879da80635a46d90e73b" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] diff --git a/src/applications/microservices/petfood-rs/Cargo.toml b/src/applications/microservices/petfood-rs/Cargo.toml new file mode 100644 index 00000000..afc8c5f9 --- /dev/null +++ b/src/applications/microservices/petfood-rs/Cargo.toml @@ -0,0 +1,63 @@ +[package] +name = "petfood-rs" +version = "0.1.0" +edition = "2021" + +[dependencies] +# Web framework +axum = "0.7" +tokio = { version = "1.0", features = ["full"] } +tower = "0.4" +tower-http = { version = "0.5", features = ["cors", "trace"] } + +# Serialization +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" + +# AWS SDK +aws-config = "1.0" +aws-sdk-dynamodb = "1.0" +aws-sdk-ssm = "1.0" +aws-sdk-eventbridge = { version = "1.88", features = ["behavior-version-latest"] } +aws-smithy-types = "1.0" +aws-smithy-runtime-api = "1.0" + +# Observability - OpenTelemetry +opentelemetry = "0.21" +opentelemetry_sdk = { version = "0.21", features = ["rt-tokio"] } +opentelemetry-otlp = "0.14" +tracing = "0.1" +tracing-subscriber = { version = "0.3", features = ["env-filter", "json"] } +tracing-opentelemetry = "0.22" + +# Metrics - Prometheus +prometheus = "0.13" + +# Error handling +thiserror = "1.0" +anyhow = "1.0" + +# Utilities +uuid = { version = "1.0", features = ["v4", "serde"] } +chrono = { version = "0.4", features = ["serde"] } +rust_decimal = { version = "1.0", features = ["serde"] } +rust_decimal_macros = "1.0" +async-trait = "0.1" + +# Configuration +config = "0.14" +envy = "0.4" + +[dev-dependencies] +# Testing +tokio-test = "0.4" +mockall = "0.12" +testcontainers = "0.15" +testcontainers-modules = { version = "0.3", features = ["dynamodb"] } +criterion = { version = "0.5", features = ["html_reports"] } +proptest = "1.4" +proptest-derive = "0.4" +reqwest = { version = "0.11", features = ["json"] } +wiremock = "0.5" +once_cell = "1.19" + diff --git a/src/applications/microservices/petfood-rs/Dockerfile b/src/applications/microservices/petfood-rs/Dockerfile new file mode 100644 index 00000000..1d1b16e5 --- /dev/null +++ b/src/applications/microservices/petfood-rs/Dockerfile @@ -0,0 +1,25 @@ +# Build stage +FROM public.ecr.aws/docker/library/rust:bookworm AS builder +COPY . . +RUN cargo build --release + +# Runtime stage +FROM public.ecr.aws/docker/library/debian:bookworm-slim + +# Install runtime dependencies and CA certificates +RUN apt-get update && apt-get install -y \ + ca-certificates \ + openssl \ + curl \ + && rm -rf /var/lib/apt/lists/* \ + && update-ca-certificates + +COPY --from=builder /target/release/petfood-rs /app/petfood-rs + +# Create a non-root user for security +RUN useradd -r -s /bin/false petfood && \ + chown petfood:petfood /app/petfood-rs + +USER petfood +EXPOSE 8080 +CMD ["/app/petfood-rs"] diff --git a/src/applications/microservices/petfood-rs/README.md b/src/applications/microservices/petfood-rs/README.md new file mode 100644 index 00000000..71d05a2b --- /dev/null +++ b/src/applications/microservices/petfood-rs/README.md @@ -0,0 +1,313 @@ +# Pet Food Microservice (Rust) + +A Rust-based microservice for managing pet food and consumable products within the Pet Adoptions workshop ecosystem. + +## Overview + +This service provides: +- Pet food catalog management +- Product recommendations based on pet type +- Shopping cart functionality +- Integration with existing observability stack + +## Technology Stack + +- **Language**: Rust +- **Web Framework**: Axum +- **Database**: DynamoDB +- **Observability**: OpenTelemetry + Prometheus +- **Deployment**: ECS Fargate + +## Observability Architecture + +This service implements a comprehensive observability system that provides automatic request tracing, metrics collection, and distributed tracing for every HTTP request. The system is built using Rust's `tracing` ecosystem, Prometheus metrics, and OpenTelemetry for distributed tracing. + +### Core Components + +#### 1. Observability Middleware + +The main middleware function (`observability_middleware`) wraps every HTTP request and provides: + +- **Automatic Request Tracing**: Each request gets a unique ID and structured logging +- **Metrics Collection**: HTTP request metrics, duration histograms, and in-flight request tracking +- **Distributed Tracing**: Integration with AWS X-Ray through OpenTelemetry +- **Error Tracking**: Automatic error logging and metrics for failed requests + +**Request Flow:** +``` +1. Request arrives → Observability middleware intercepts +2. Create endpoint-specific tracing span and extract OpenTelemetry span ID +3. Record start time and increment in-flight metrics +4. Process request through handlers (with automatic tracing) +5. Record final metrics (status, duration) and complete span +6. Send trace data to AWS X-Ray for visualization +``` + +#### 2. Metrics System + +Uses **Prometheus** to collect various types of metrics: + +**HTTP Metrics:** +- `http_requests_total` - Total requests by method, endpoint, status code +- `http_request_duration_seconds` - Request duration histograms with percentiles +- `http_requests_in_flight` - Current concurrent requests + +**Database Metrics:** +- `database_operations_total` - Database operations by type, table, status +- `database_operation_duration_seconds` - Database query performance +- `database_connections_active` - Active connection pool size + +**Business Logic Metrics:** +- `food_operations_total` - Food searches by pet type, food type, status +- `cart_operations_total` - Cart operations (add, update, remove, checkout) +- `recommendation_requests_total` - Recommendation requests by pet type + +**System Metrics:** +- `memory_usage_bytes` - Current memory usage +- `cpu_usage_percent` - Current CPU utilization + +#### 3. Distributed Tracing + +Integrates with **OpenTelemetry** and **AWS X-Ray**: + +- **Service Metadata**: Automatic service name, version, and namespace tagging +- **Span Correlation**: Links HTTP requests to database operations and business logic +- **Sampling**: Configurable sampling rates for production environments +- **Batching**: Efficient batch export to reduce overhead + +**Example Trace Structure:** +``` +Trace ID: abc123... +├── Span: GET /api/foods +│ ├── span_id: 1a2b3c4d5e6f7890 +│ ├── method: GET +│ ├── uri: /api/foods?pettype=puppy +│ ├── user_agent: Mozilla/5.0... +│ └── Span: list_foods +│ ├── pet_type: puppy +│ └── Span: find_by_pet_type +│ ├── operation: query +│ ├── table: PetFoods +│ ├── index: PetTypeIndex +│ └── duration: 45ms +``` + +#### 4. Automatic Instrumentation + +Throughout the codebase, functions are automatically instrumented using the `#[instrument]` macro: + +```rust +#[instrument(skip(state))] +pub async fn list_foods( + State(state): State, + Query(query): Query, +) -> Result, (StatusCode, Json)> +``` + +This automatically: +- Creates child spans for each function call +- Captures function parameters (except sensitive data) +- Tracks function execution time +- Propagates trace context across async boundaries + +#### 5. Specialized Tracing Middleware + +**Database Tracing:** +- Wraps all database operations with performance tracking +- Records success/failure rates and latency metrics +- Correlates database operations with HTTP requests + +**Business Logic Tracing:** +- Tracks food search patterns and performance +- Monitors cart operation success rates +- Records recommendation request analytics + +### Configuration + +Observability behavior is controlled through environment variables: + +```bash +# Logging Configuration +ENABLE_JSON_LOGGING=true # Use structured JSON logs (production) +RUST_LOG=info # Log level filtering + +# OpenTelemetry Configuration +PETFOOD_OTLP_ENDPOINT=http://localhost:4317 # OpenTelemetry collector endpoint + +# Service Metadata +PETFOOD_SERVICE_NAME=petfood-service # Service name for tracing +PETFOOD_SERVICE_VERSION=1.0.0 # Service version +``` + +### Monitoring Endpoints + +- **Health Check**: `GET /health/status` - Service health status +- **Metrics**: `GET /metrics` - Prometheus metrics for scraping +- **Admin**: `GET /admin/health` - Detailed health information + +### Benefits + +1. **Automatic Observability**: Every request is traced without manual instrumentation +2. **Performance Monitoring**: Real-time performance metrics and alerting +3. **Distributed Tracing**: Full request traces across service boundaries +4. **Business Intelligence**: Pet food search patterns and cart analytics +5. **Operational Insights**: Error tracking, capacity planning, and performance optimization + +### Integration with AWS + +- **CloudWatch X-Ray**: Distributed tracing visualization +- **CloudWatch Metrics**: Custom metrics and alarms +- **CloudWatch Logs**: Structured logging with correlation IDs +- **Application Signals**: Automatic SLI/SLO monitoring + +## Getting Started + +### Prerequisites + +- Rust 1.75 or later +- AWS CLI configured +- Docker (for containerization) + +### Local Development + +1. Clone the repository +2. Navigate to the petfood-rs directory: + ```bash + cd PetAdoptions/petfood-rs + ``` + +3. Install dependencies: + ```bash + cargo build + ``` + +4. Run the service: + ```bash + cargo run + ``` + +The service will start on port 80 by default. + +### Configuration + +The service can be configured using environment variables with the `PETFOOD_` prefix: + +**Server Configuration:** +- `PETFOOD_SERVER_HOST`: Server host (default: 0.0.0.0) +- `PETFOOD_SERVER_PORT`: Server port (default: 80) +- `PETFOOD_SERVER_REQUEST_TIMEOUT_SECONDS`: Request timeout (default: 30) + +**Database Configuration:** +- `PETFOOD_FOODS_TABLE_NAME`: DynamoDB table name for foods +- `PETFOOD_CARTS_TABLE_NAME`: DynamoDB table name for carts +- `AWS_REGION`: AWS region for DynamoDB and other services + +**Observability Configuration:** +- `PETFOOD_SERVICE_NAME`: Service name for tracing (default: petfood-service) +- `PETFOOD_SERVICE_VERSION`: Service version for tracing (default: 1.0.0) +- `PETFOOD_OTLP_ENDPOINT`: OpenTelemetry collector endpoint +- `ENABLE_JSON_LOGGING`: Enable structured JSON logging (true/false) +- `RUST_LOG`: Log level filtering (debug, info, warn, error) + +**Assets Configuration:** +- `PETFOOD_ASSETS_CDN_URL`: CDN base URL for food images and assets (default: https://petfood-assets.s3.amazonaws.com) + +### Image URL Handling + +The service uses dynamic image URL generation for flexibility in serving images: + +- **Storage**: Image paths with petfood prefix are stored in the database (e.g., "petfood/beef-turkey-kibbles.jpg") +- **Response**: Full CDN URLs are dynamically generated in API responses +- **Configuration**: The CDN base URL is configurable via `PETFOOD_ASSETS_CDN_URL` +- **Benefits**: Easy switching between S3, CloudFront, or other CDN providers without database changes + +Example configuration: +```bash +# For S3 direct access +export PETFOOD_ASSETS_CDN_URL="https://petfood-assets.s3.amazonaws.com" + +# For CloudFront distribution (with or without trailing slash) +export PETFOOD_ASSETS_CDN_URL="https://d1234567890.cloudfront.net/images" +export PETFOOD_ASSETS_CDN_URL="https://d1234567890.cloudfront.net/images/" +``` + +The service automatically handles trailing slashes and combines the CDN URL with the stored path. + +### Full config example + +``` +# Server Configuration +PETFOOD_HOST=0.0.0.0 +PETFOOD_PORT=8080 +PETFOOD_REQUEST_TIMEOUT_SECONDS=30 + +# Database Configuration +PETFOOD_FOODS_TABLE_NAME=PetFoods +PETFOOD_CARTS_TABLE_NAME=PetFoodCarts +PETFOOD_REGION=us-west-2 +PETFOOD_ASSETS_CDN_URL=https://d1234567890.cloudfront.net/images + +# AWS Configuration +AWS_REGION=us-west-2 + +# Observability Configuration +PETFOOD_SERVICE_NAME=petfood-rs +PETFOOD_SERVICE_VERSION=1.0.0 +PETFOOD_OTLP_ENDPOINT=http://localhost:4317 +PETFOOD_METRICS_PORT=9090 +PETFOOD_LOG_LEVEL=info +PETFOOD_ENABLE_JSON_LOGGING=false +``` + +### Health Check + +The service provides a health check endpoint at `/health/status`. + +## API Endpoints + +### Health +- `GET /health/status` - Health check endpoint + +### Food Management (Coming Soon) +- `GET /api/foods` - List all foods +- `GET /api/foods/{food_id}` - Get specific food details +- `POST /api/foods` - Create new food (admin) +- `PUT /api/foods/{food_id}` - Update food (admin) +- `DELETE /api/foods/{food_id}` - Delete food (admin) + +### Recommendations (Coming Soon) +- `GET /api/recommendations/{pet_type}` - Get recommendations for pet type + +### Shopping Cart (Coming Soon) +- `GET /api/cart/{user_id}` - Get user's cart +- `POST /api/cart/{user_id}/items` - Add item to cart +- `PUT /api/cart/{user_id}/items/{food_id}` - Update cart item +- `DELETE /api/cart/{user_id}/items/{food_id}` - Remove cart item + +## Development + +### Running Tests + +```bash +cargo test +``` + +### Code Formatting + +```bash +cargo fmt +``` + +### Linting + +```bash +cargo clippy +``` + +## Deployment + +This service follows the same deployment patterns as other microservices in the Pet Adoptions workshop and will be deployed using AWS CDK to ECS Fargate. + +## Contributing + +This service is part of the AWS One Observability Workshop. Please follow the established patterns and conventions used by other microservices in the ecosystem. \ No newline at end of file diff --git a/src/applications/microservices/petfood-rs/benches/food_operations.rs b/src/applications/microservices/petfood-rs/benches/food_operations.rs new file mode 100644 index 00000000..e4701ccc --- /dev/null +++ b/src/applications/microservices/petfood-rs/benches/food_operations.rs @@ -0,0 +1,346 @@ +use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; +use rust_decimal::prelude::FromPrimitive; +use std::sync::Arc; +use std::time::Duration; +use tokio::runtime::Runtime; + +use petfood_rs::models::{ + CreateFoodRequest, CreationSource, Food, FoodFilters, FoodType, PetType, RepositoryError, +}; +use petfood_rs::repositories::FoodRepository; +use petfood_rs::services::food_service::FoodService; +use rust_decimal_macros::dec; + +use async_trait::async_trait; +use std::collections::HashMap; + +/// Mock repository for benchmarking that doesn't require AWS connectivity +#[derive(Clone)] +struct MockFoodRepository { + foods: Arc>>, +} + +impl MockFoodRepository { + fn new() -> Self { + Self { + foods: Arc::new(std::sync::Mutex::new(HashMap::new())), + } + } + + fn with_test_data(size: usize) -> Self { + let repo = Self::new(); + let pet_types = [PetType::Puppy, PetType::Kitten, PetType::Bunny]; + let food_types = [ + FoodType::Dry, + FoodType::Wet, + FoodType::Treats, + FoodType::Supplements, + ]; + + for i in 0..size { + let pet_type = pet_types[i % pet_types.len()].clone(); + let food_type = food_types[i % food_types.len()].clone(); + + let request = CreateFoodRequest { + pet_type, + name: format!("Benchmark Food {}", i), + food_type, + description: format!("Description for benchmark food {}", i), + price: dec!(10.99) + + rust_decimal::Decimal::from_f64(i as f64 * 0.1).unwrap_or(dec!(0.0)), + // No image field - will be generated via events + nutritional_info: None, + ingredients: vec!["ingredient1".to_string(), "ingredient2".to_string()], + feeding_guidelines: Some("Feed as needed".to_string()), + stock_quantity: 100, + }; + + let food = Food::new(request); + repo.foods.lock().unwrap().insert(food.id.clone(), food); + } + + repo + } +} + +#[async_trait] +impl FoodRepository for MockFoodRepository { + async fn find_all(&self, filters: FoodFilters) -> Result, RepositoryError> { + let foods = self.foods.lock().unwrap(); + let mut result: Vec = foods.values().cloned().collect(); + + // Apply filters + if let Some(pet_type) = &filters.pet_type { + result.retain(|f| &f.pet_type == pet_type); + } + if let Some(food_type) = &filters.food_type { + result.retain(|f| &f.food_type == food_type); + } + if let Some(min_price) = filters.min_price { + result.retain(|f| f.price >= min_price); + } + if let Some(max_price) = filters.max_price { + result.retain(|f| f.price <= max_price); + } + if let Some(in_stock_only) = filters.in_stock_only { + if in_stock_only { + result.retain(|f| f.stock_quantity > 0); + } + } + + Ok(result) + } + + async fn find_by_id(&self, id: &str) -> Result, RepositoryError> { + let foods = self.foods.lock().unwrap(); + Ok(foods.get(id).cloned()) + } + + async fn find_by_pet_type(&self, pet_type: PetType) -> Result, RepositoryError> { + let foods = self.foods.lock().unwrap(); + let result: Vec = foods + .values() + .filter(|f| f.pet_type == pet_type) + .cloned() + .collect(); + Ok(result) + } + + async fn find_by_food_type(&self, food_type: FoodType) -> Result, RepositoryError> { + let foods = self.foods.lock().unwrap(); + let result: Vec = foods + .values() + .filter(|f| f.food_type == food_type) + .cloned() + .collect(); + Ok(result) + } + + async fn create(&self, food: Food) -> Result { + let mut foods = self.foods.lock().unwrap(); + foods.insert(food.id.clone(), food.clone()); + Ok(food) + } + + async fn update(&self, food: Food) -> Result { + let mut foods = self.foods.lock().unwrap(); + foods.insert(food.id.clone(), food.clone()); + Ok(food) + } + + async fn soft_delete(&self, id: &str) -> Result<(), RepositoryError> { + let mut foods = self.foods.lock().unwrap(); + if let Some(mut food) = foods.get(id).cloned() { + food.availability_status = petfood_rs::models::AvailabilityStatus::Discontinued; + foods.insert(id.to_string(), food); + } + Ok(()) + } + + async fn delete(&self, id: &str) -> Result<(), RepositoryError> { + let mut foods = self.foods.lock().unwrap(); + foods.remove(id); + Ok(()) + } + + async fn exists(&self, id: &str) -> Result { + let foods = self.foods.lock().unwrap(); + Ok(foods.contains_key(id)) + } + + async fn count(&self, filters: Option) -> Result { + if let Some(filters) = filters { + let foods = self.find_all(filters).await?; + Ok(foods.len()) + } else { + let foods = self.foods.lock().unwrap(); + Ok(foods.len()) + } + } +} + +fn bench_food_search_by_pet_type(c: &mut Criterion) { + let rt = Runtime::new().unwrap(); + + let mut group = c.benchmark_group("food_search_by_pet_type"); + group.sample_size(100); + group.measurement_time(Duration::from_secs(5)); + + for dataset_size in [100, 500, 1000].iter() { + group.bench_with_input( + BenchmarkId::new("dataset_size", dataset_size), + dataset_size, + |b, &size| { + let repository = Arc::new(MockFoodRepository::with_test_data(size)); + let food_service = FoodService::new(repository); + + b.iter(|| { + rt.block_on(async { + let filters = FoodFilters { + pet_type: Some(PetType::Puppy), + food_type: None, + availability_status: None, + min_price: None, + max_price: None, + search_term: None, + in_stock_only: Some(false), + }; + + black_box(food_service.list_foods(filters).await.unwrap()) + }) + }); + }, + ); + } + group.finish(); +} + +fn bench_food_search_by_food_type(c: &mut Criterion) { + let rt = Runtime::new().unwrap(); + + let mut group = c.benchmark_group("food_search_by_food_type"); + group.sample_size(100); + group.measurement_time(Duration::from_secs(5)); + + for dataset_size in [100, 500, 1000].iter() { + group.bench_with_input( + BenchmarkId::new("dataset_size", dataset_size), + dataset_size, + |b, &size| { + let repository = Arc::new(MockFoodRepository::with_test_data(size)); + let food_service = FoodService::new(repository); + + b.iter(|| { + rt.block_on(async { + let filters = FoodFilters { + pet_type: None, + food_type: Some(FoodType::Dry), + availability_status: None, + min_price: None, + max_price: None, + search_term: None, + in_stock_only: Some(false), + }; + + black_box(food_service.list_foods(filters).await.unwrap()) + }) + }); + }, + ); + } + group.finish(); +} + +fn bench_food_search_combined_filters(c: &mut Criterion) { + let rt = Runtime::new().unwrap(); + + let mut group = c.benchmark_group("food_search_combined_filters"); + group.sample_size(100); + group.measurement_time(Duration::from_secs(5)); + + for dataset_size in [100, 500, 1000].iter() { + group.bench_with_input( + BenchmarkId::new("dataset_size", dataset_size), + dataset_size, + |b, &size| { + let repository = Arc::new(MockFoodRepository::with_test_data(size)); + let food_service = FoodService::new(repository); + + b.iter(|| { + rt.block_on(async { + let filters = FoodFilters { + pet_type: Some(PetType::Puppy), + food_type: Some(FoodType::Dry), + availability_status: None, + min_price: Some(dec!(5.0)), + max_price: Some(dec!(20.0)), + search_term: None, + in_stock_only: Some(true), + }; + + black_box(food_service.list_foods(filters).await.unwrap()) + }) + }); + }, + ); + } + group.finish(); +} + +fn bench_food_get_by_id(c: &mut Criterion) { + let rt = Runtime::new().unwrap(); + + let mut group = c.benchmark_group("food_get_by_id"); + group.sample_size(200); + group.measurement_time(Duration::from_secs(5)); + + group.bench_function("single_lookup", |b| { + let repository = Arc::new(MockFoodRepository::with_test_data(1000)); + let food_service = FoodService::new(repository.clone()); + + // Get a food ID from the repository + let food_id = rt.block_on(async { + let foods = repository.find_all(FoodFilters::default()).await.unwrap(); + foods[0].id.clone() + }); + + b.iter(|| rt.block_on(async { black_box(food_service.get_food(&food_id).await.unwrap()) })); + }); + + group.finish(); +} + +fn bench_food_create(c: &mut Criterion) { + let rt = Runtime::new().unwrap(); + + let mut group = c.benchmark_group("food_create"); + group.sample_size(100); + group.measurement_time(Duration::from_secs(5)); + + group.bench_function("create_single", |b| { + b.iter_batched( + || { + let repository = Arc::new(MockFoodRepository::new()); + let food_service = FoodService::new(repository); + + let request = CreateFoodRequest { + pet_type: PetType::Puppy, + name: "Benchmark Food".to_string(), + food_type: FoodType::Dry, + description: "Description for benchmark food".to_string(), + price: dec!(10.99), + // No image field - will be generated via events + nutritional_info: None, + ingredients: vec!["ingredient1".to_string()], + feeding_guidelines: Some("Feed as needed".to_string()), + stock_quantity: 100, + }; + + (food_service, request) + }, + |(food_service, request)| { + rt.block_on(async move { + black_box( + food_service + .create_food(request, CreationSource::AdminApi) + .await + .unwrap(), + ) + }) + }, + criterion::BatchSize::SmallInput, + ); + }); + + group.finish(); +} + +criterion_group!( + benches, + bench_food_search_by_pet_type, + bench_food_search_by_food_type, + bench_food_search_combined_filters, + bench_food_get_by_id, + bench_food_create +); +criterion_main!(benches); diff --git a/src/applications/microservices/petfood-rs/benches/food_search.rs b/src/applications/microservices/petfood-rs/benches/food_search.rs new file mode 100644 index 00000000..55627a84 --- /dev/null +++ b/src/applications/microservices/petfood-rs/benches/food_search.rs @@ -0,0 +1,304 @@ +use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; +use rust_decimal::prelude::FromPrimitive; +use std::sync::Arc; +use std::time::Duration; +use tokio::runtime::Runtime; + +use petfood_rs::models::{ + CreateFoodRequest, Food, FoodFilters, FoodType, PetType, RepositoryError, +}; +use petfood_rs::repositories::FoodRepository; +use petfood_rs::services::food_service::FoodService; +use rust_decimal_macros::dec; + +use async_trait::async_trait; +use std::collections::HashMap; + +/// Mock repository for benchmarking that doesn't require AWS connectivity +#[derive(Clone)] +struct MockFoodRepository { + foods: Arc>>, +} + +impl MockFoodRepository { + fn new() -> Self { + Self { + foods: Arc::new(std::sync::Mutex::new(HashMap::new())), + } + } +} + +#[async_trait] +impl FoodRepository for MockFoodRepository { + async fn find_all(&self, filters: FoodFilters) -> Result, RepositoryError> { + let foods = self.foods.lock().unwrap(); + let mut result: Vec = foods.values().cloned().collect(); + + // Apply filters + if let Some(pet_type) = &filters.pet_type { + result.retain(|f| &f.pet_type == pet_type); + } + if let Some(food_type) = &filters.food_type { + result.retain(|f| &f.food_type == food_type); + } + if let Some(min_price) = filters.min_price { + result.retain(|f| f.price >= min_price); + } + if let Some(max_price) = filters.max_price { + result.retain(|f| f.price <= max_price); + } + if let Some(in_stock_only) = filters.in_stock_only { + if in_stock_only { + result.retain(|f| f.stock_quantity > 0); + } + } + + Ok(result) + } + + async fn find_by_id(&self, id: &str) -> Result, RepositoryError> { + let foods = self.foods.lock().unwrap(); + Ok(foods.get(id).cloned()) + } + + async fn find_by_pet_type(&self, pet_type: PetType) -> Result, RepositoryError> { + let foods = self.foods.lock().unwrap(); + let result: Vec = foods + .values() + .filter(|f| f.pet_type == pet_type) + .cloned() + .collect(); + Ok(result) + } + + async fn find_by_food_type(&self, food_type: FoodType) -> Result, RepositoryError> { + let foods = self.foods.lock().unwrap(); + let result: Vec = foods + .values() + .filter(|f| f.food_type == food_type) + .cloned() + .collect(); + Ok(result) + } + + async fn create(&self, food: Food) -> Result { + let mut foods = self.foods.lock().unwrap(); + foods.insert(food.id.clone(), food.clone()); + Ok(food) + } + + async fn update(&self, food: Food) -> Result { + let mut foods = self.foods.lock().unwrap(); + foods.insert(food.id.clone(), food.clone()); + Ok(food) + } + + async fn soft_delete(&self, id: &str) -> Result<(), RepositoryError> { + let mut foods = self.foods.lock().unwrap(); + if let Some(mut food) = foods.get(id).cloned() { + food.availability_status = petfood_rs::models::AvailabilityStatus::Discontinued; + foods.insert(id.to_string(), food); + } + Ok(()) + } + + async fn delete(&self, id: &str) -> Result<(), RepositoryError> { + let mut foods = self.foods.lock().unwrap(); + foods.remove(id); + Ok(()) + } + + async fn exists(&self, id: &str) -> Result { + let foods = self.foods.lock().unwrap(); + Ok(foods.contains_key(id)) + } + + async fn count(&self, filters: Option) -> Result { + if let Some(filters) = filters { + let foods = self.find_all(filters).await?; + Ok(foods.len()) + } else { + let foods = self.foods.lock().unwrap(); + Ok(foods.len()) + } + } +} + +fn create_repository_with_test_data(num_foods: usize) -> Arc { + let repository = Arc::new(MockFoodRepository::new()); + let pet_types = [PetType::Puppy, PetType::Kitten, PetType::Bunny]; + let food_types = [ + FoodType::Dry, + FoodType::Wet, + FoodType::Treats, + FoodType::Supplements, + ]; + + for i in 0..num_foods { + let pet_type = pet_types[i % pet_types.len()].clone(); + let food_type = food_types[i % food_types.len()].clone(); + + let request = CreateFoodRequest { + pet_type, + name: format!("Benchmark Food {}", i), + food_type, + description: format!("Description for benchmark food {}", i), + price: dec!(10.99) + + rust_decimal::Decimal::from_f64(i as f64 * 0.1).unwrap_or(dec!(0.0)), + // No image field - will be generated via events + nutritional_info: None, + ingredients: vec!["ingredient1".to_string(), "ingredient2".to_string()], + feeding_guidelines: Some("Feed as needed".to_string()), + stock_quantity: 100, + }; + + let food = Food::new(request); + repository + .foods + .lock() + .unwrap() + .insert(food.id.clone(), food); + } + + repository +} + +fn bench_food_search_by_pet_type(c: &mut Criterion) { + let rt = Runtime::new().unwrap(); + + let mut group = c.benchmark_group("food_search_by_pet_type"); + group.sample_size(50); + group.measurement_time(Duration::from_secs(10)); + + for dataset_size in [100, 500, 1000].iter() { + group.bench_with_input( + BenchmarkId::new("dataset_size", dataset_size), + dataset_size, + |b, &size| { + let repository = create_repository_with_test_data(size); + let food_service = FoodService::new(repository); + + b.iter(|| { + rt.block_on(async { + let filters = FoodFilters { + pet_type: Some(PetType::Puppy), + food_type: None, + availability_status: None, + min_price: None, + max_price: None, + search_term: None, + in_stock_only: Some(false), + }; + + black_box(food_service.list_foods(filters).await.unwrap()) + }) + }); + }, + ); + } + group.finish(); +} + +fn bench_food_search_by_food_type(c: &mut Criterion) { + let rt = Runtime::new().unwrap(); + + let mut group = c.benchmark_group("food_search_by_food_type"); + group.sample_size(50); + group.measurement_time(Duration::from_secs(10)); + + for dataset_size in [100, 500, 1000].iter() { + group.bench_with_input( + BenchmarkId::new("dataset_size", dataset_size), + dataset_size, + |b, &size| { + let repository = create_repository_with_test_data(size); + let food_service = FoodService::new(repository); + + b.iter(|| { + rt.block_on(async { + let filters = FoodFilters { + pet_type: None, + food_type: Some(FoodType::Dry), + availability_status: None, + min_price: None, + max_price: None, + search_term: None, + in_stock_only: Some(false), + }; + + black_box(food_service.list_foods(filters).await.unwrap()) + }) + }); + }, + ); + } + group.finish(); +} + +fn bench_food_search_combined_filters(c: &mut Criterion) { + let rt = Runtime::new().unwrap(); + + let mut group = c.benchmark_group("food_search_combined_filters"); + group.sample_size(50); + group.measurement_time(Duration::from_secs(10)); + + for dataset_size in [100, 500, 1000].iter() { + group.bench_with_input( + BenchmarkId::new("dataset_size", dataset_size), + dataset_size, + |b, &size| { + let repository = create_repository_with_test_data(size); + let food_service = FoodService::new(repository); + + b.iter(|| { + rt.block_on(async { + let filters = FoodFilters { + pet_type: Some(PetType::Puppy), + food_type: Some(FoodType::Dry), + availability_status: None, + min_price: Some(dec!(5.0)), + max_price: Some(dec!(20.0)), + search_term: None, + in_stock_only: Some(true), + }; + + black_box(food_service.list_foods(filters).await.unwrap()) + }) + }); + }, + ); + } + group.finish(); +} + +fn bench_food_get_by_id(c: &mut Criterion) { + let rt = Runtime::new().unwrap(); + + let mut group = c.benchmark_group("food_get_by_id"); + group.sample_size(100); + group.measurement_time(Duration::from_secs(10)); + + group.bench_function("single_lookup", |b| { + let repository = create_repository_with_test_data(1); + let food_service = FoodService::new(repository.clone()); + + // Get a food ID from the repository + let food_id = rt.block_on(async { + let foods = repository.find_all(FoodFilters::default()).await.unwrap(); + foods[0].id.clone() + }); + + b.iter(|| rt.block_on(async { black_box(food_service.get_food(&food_id).await.unwrap()) })); + }); + + group.finish(); +} + +criterion_group!( + benches, + bench_food_search_by_pet_type, + bench_food_search_by_food_type, + bench_food_search_combined_filters, + bench_food_get_by_id +); +criterion_main!(benches); diff --git a/src/applications/microservices/petfood-rs/postman_collection.json b/src/applications/microservices/petfood-rs/postman_collection.json new file mode 100644 index 00000000..2358d9bd --- /dev/null +++ b/src/applications/microservices/petfood-rs/postman_collection.json @@ -0,0 +1,573 @@ +{ + "info": { + "name": "PetFood Microservice API", + "description": "Complete API collection for the PetFood microservice with end-to-end examples", + "version": "1.0.0", + "schema": "https://schema.getpostman.com/json/collection/v2.1.0/collection.json" + }, + "variable": [ + { + "key": "baseUrl", + "value": "http://localhost:8080", + "type": "string" + }, + { + "key": "userId", + "value": "user001", + "type": "string" + }, + { + "key": "foodId", + "value": "", + "type": "string" + } + ], + "item": [ + { + "name": "1. Health Check", + "item": [ + { + "name": "Check Service Health", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/health/status", + "host": ["{{baseUrl}}"], + "path": ["health", "status"] + } + }, + "response": [] + } + ] + }, + { + "name": "2. Admin Operations", + "item": [ + { + "name": "Setup Database Tables", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/api/admin/setup-tables", + "host": ["{{baseUrl}}"], + "path": ["api", "admin", "setup-tables"] + } + }, + "response": [] + }, + { + "name": "Seed Database", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/api/admin/seed", + "host": ["{{baseUrl}}"], + "path": ["api", "admin", "seed"] + } + }, + "response": [] + }, + { + "name": "Cleanup Database", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "url": { + "raw": "{{baseUrl}}/api/admin/cleanup", + "host": ["{{baseUrl}}"], + "path": ["api", "admin", "cleanup"] + } + }, + "response": [] + }, + { + "name": "Create New Food (Admin)", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"food_for\": \"puppy\",\n \"food_name\": \"Premium Puppy Chow\",\n \"food_type\": \"dry\",\n \"food_description\": \"High-quality dry food for puppies\",\n \"food_price\": \"24.99\",\n \"food_image\": \"premium-puppy-chow.jpg\",\n \"nutritional_info\": {\n \"calories_per_serving\": 350,\n \"protein_percentage\": \"28.0\",\n \"fat_percentage\": \"15.0\",\n \"serving_size\": \"1 cup\"\n },\n \"ingredients\": [\"chicken\", \"rice\", \"vegetables\", \"vitamins\"],\n \"feeding_guidelines\": \"Feed 2-3 times daily\",\n \"stock_quantity\": 100\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/admin/foods", + "host": ["{{baseUrl}}"], + "path": ["api", "admin", "foods"] + } + }, + "response": [] + }, + { + "name": "Update Food (Admin)", + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"food_price\": \"22.99\",\n \"stock_quantity\": 75,\n \"availability_status\": \"low_stock\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/admin/foods/{{foodId}}", + "host": ["{{baseUrl}}"], + "path": ["api", "admin", "foods", "{{foodId}}"] + } + }, + "response": [] + }, + { + "name": "Delete Food (Admin)", + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{baseUrl}}/api/admin/foods/{{foodId}}", + "host": ["{{baseUrl}}"], + "path": ["api", "admin", "foods", "{{foodId}}"] + } + }, + "response": [] + } + ] + }, + { + "name": "3. Food Browsing (Read-Only)", + "item": [ + { + "name": "List All Foods", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/api/foods", + "host": ["{{baseUrl}}"], + "path": ["api", "foods"] + } + }, + "response": [] + }, + { + "name": "List Foods - Filter by Pet Type (Puppy)", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/api/foods?pet_type=puppy", + "host": ["{{baseUrl}}"], + "path": ["api", "foods"], + "query": [ + { + "key": "pet_type", + "value": "puppy" + } + ] + } + }, + "response": [] + }, + { + "name": "List Foods - Filter by Pet Type (Kitten)", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/api/foods?pet_type=kitten", + "host": ["{{baseUrl}}"], + "path": ["api", "foods"], + "query": [ + { + "key": "pet_type", + "value": "kitten" + } + ] + } + }, + "response": [] + }, + { + "name": "List Foods - Filter by Pet Type (Bunny)", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/api/foods?pet_type=bunny", + "host": ["{{baseUrl}}"], + "path": ["api", "foods"], + "query": [ + { + "key": "pet_type", + "value": "bunny" + } + ] + } + }, + "response": [] + }, + { + "name": "List Foods - Filter by Food Type and Price", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/api/foods?food_type=dry&max_price=15.00", + "host": ["{{baseUrl}}"], + "path": ["api", "foods"], + "query": [ + { + "key": "food_type", + "value": "dry" + }, + { + "key": "max_price", + "value": "15.00" + } + ] + } + }, + "response": [] + }, + { + "name": "Get Specific Food", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/api/foods/{{foodId}}", + "host": ["{{baseUrl}}"], + "path": ["api", "foods", "{{foodId}}"] + } + }, + "response": [] + }, + { + "name": "Create New Food", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"food_for\": \"puppy\",\n \"food_name\": \"Premium Puppy Chow\",\n \"food_type\": \"dry\",\n \"food_description\": \"High-quality dry food for puppies\",\n \"food_price\": \"24.99\",\n \"food_image\": \"premium-puppy-chow.jpg\",\n \"nutritional_info\": {\n \"calories_per_serving\": 350,\n \"protein_percentage\": \"28.0\",\n \"fat_percentage\": \"15.0\",\n \"serving_size\": \"1 cup\"\n },\n \"ingredients\": [\"chicken\", \"rice\", \"vegetables\", \"vitamins\"],\n \"feeding_guidelines\": \"Feed 2-3 times daily\",\n \"stock_quantity\": 100\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/foods", + "host": ["{{baseUrl}}"], + "path": ["api", "foods"] + } + }, + "response": [] + }, + { + "name": "Update Food", + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"food_price\": \"22.99\",\n \"stock_quantity\": 75,\n \"availability_status\": \"low_stock\"\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/foods/{{foodId}}", + "host": ["{{baseUrl}}"], + "path": ["api", "foods", "{{foodId}}"] + } + }, + "response": [] + }, + { + "name": "Delete Food", + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{baseUrl}}/api/foods/{{foodId}}", + "host": ["{{baseUrl}}"], + "path": ["api", "foods", "{{foodId}}"] + } + }, + "response": [] + } + ] + }, + { + "name": "4. Cart Management - User001 Journey", + "item": [ + { + "name": "Get Empty Cart", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/api/cart/{{userId}}", + "host": ["{{baseUrl}}"], + "path": ["api", "cart", "{{userId}}"] + } + }, + "response": [] + }, + { + "name": "Add First Item to Cart", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"food_id\": \"F12345678\",\n \"quantity\": 2\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/cart/{{userId}}/items", + "host": ["{{baseUrl}}"], + "path": ["api", "cart", "{{userId}}", "items"] + } + }, + "response": [] + }, + { + "name": "Add Second Item to Cart", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"food_id\": \"F23456789\",\n \"quantity\": 1\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/cart/{{userId}}/items", + "host": ["{{baseUrl}}"], + "path": ["api", "cart", "{{userId}}", "items"] + } + }, + "response": [] + }, + { + "name": "Get Cart with Items", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/api/cart/{{userId}}", + "host": ["{{baseUrl}}"], + "path": ["api", "cart", "{{userId}}"] + } + }, + "response": [] + }, + { + "name": "Update Item Quantity", + "request": { + "method": "PUT", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"quantity\": 5\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/cart/{{userId}}/items/F12345678", + "host": ["{{baseUrl}}"], + "path": ["api", "cart", "{{userId}}", "items", "F12345678"] + } + }, + "response": [] + }, + { + "name": "Remove Item from Cart", + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{baseUrl}}/api/cart/{{userId}}/items/F23456789", + "host": ["{{baseUrl}}"], + "path": ["api", "cart", "{{userId}}", "items", "F23456789"] + } + }, + "response": [] + }, + { + "name": "Get Updated Cart", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/api/cart/{{userId}}", + "host": ["{{baseUrl}}"], + "path": ["api", "cart", "{{userId}}"] + } + }, + "response": [] + }, + { + "name": "Clear Cart", + "request": { + "method": "POST", + "header": [], + "url": { + "raw": "{{baseUrl}}/api/cart/{{userId}}/clear", + "host": ["{{baseUrl}}"], + "path": ["api", "cart", "{{userId}}", "clear"] + } + }, + "response": [] + }, + { + "name": "Verify Empty Cart", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/api/cart/{{userId}}", + "host": ["{{baseUrl}}"], + "path": ["api", "cart", "{{userId}}"] + } + }, + "response": [] + }, + { + "name": "Delete Cart", + "request": { + "method": "DELETE", + "header": [], + "url": { + "raw": "{{baseUrl}}/api/cart/{{userId}}", + "host": ["{{baseUrl}}"], + "path": ["api", "cart", "{{userId}}"] + } + }, + "response": [] + } + ] + }, + { + "name": "5. Error Scenarios", + "item": [ + { + "name": "Get Non-existent Food", + "request": { + "method": "GET", + "header": [], + "url": { + "raw": "{{baseUrl}}/api/foods/F99999999", + "host": ["{{baseUrl}}"], + "path": ["api", "foods", "F99999999"] + } + }, + "response": [] + }, + { + "name": "Add Invalid Food to Cart", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"food_id\": \"F99999999\",\n \"quantity\": 1\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/cart/{{userId}}/items", + "host": ["{{baseUrl}}"], + "path": ["api", "cart", "{{userId}}", "items"] + } + }, + "response": [] + }, + { + "name": "Create Food with Invalid Data (Admin)", + "request": { + "method": "POST", + "header": [ + { + "key": "Content-Type", + "value": "application/json" + } + ], + "body": { + "mode": "raw", + "raw": "{\n \"food_for\": \"invalid_pet\",\n \"food_name\": \"\",\n \"food_type\": \"invalid_type\",\n \"food_description\": \"\",\n \"food_price\": \"-5.00\",\n \"food_image\": \"\",\n \"ingredients\": [],\n \"stock_quantity\": 0\n}" + }, + "url": { + "raw": "{{baseUrl}}/api/admin/foods", + "host": ["{{baseUrl}}"], + "path": ["api", "admin", "foods"] + } + }, + "response": [] + } + ] + } + ], + "event": [ + { + "listen": "prerequest", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + }, + { + "listen": "test", + "script": { + "type": "text/javascript", + "exec": [ + "" + ] + } + } + ] +} \ No newline at end of file diff --git a/src/applications/microservices/petfood-rs/src/config/mod.rs b/src/applications/microservices/petfood-rs/src/config/mod.rs new file mode 100644 index 00000000..94b1a997 --- /dev/null +++ b/src/applications/microservices/petfood-rs/src/config/mod.rs @@ -0,0 +1,497 @@ +use aws_config::BehaviorVersion; +use aws_sdk_dynamodb::Client as DynamoDbClient; +use aws_sdk_eventbridge::Client as EventBridgeClient; +use aws_sdk_ssm::Client as SsmClient; +use serde::Deserialize; +use std::collections::HashMap; +use std::sync::Arc; +use std::time::{Duration, Instant}; +use thiserror::Error; +use tokio::sync::RwLock; +use tracing::{debug, error, info, warn}; + +#[derive(Debug, Error)] +pub enum ConfigError { + #[error("Configuration loading error: {message}")] + LoadError { message: String }, + + #[error("Parameter not found: {name}")] + ParameterNotFound { name: String }, + + #[error("AWS SDK error: {source}")] + AwsSdk { + source: Box, + }, + + #[error("Validation error: {message}")] + ValidationError { message: String }, + + #[error("Environment variable missing: {name}")] + MissingEnvironmentVariable { name: String }, +} + +#[derive(Debug, Clone)] +pub struct Config { + pub server: ServerConfig, + pub database: DatabaseConfig, + pub aws: AwsConfig, + pub observability: ObservabilityConfig, + pub events: EventsConfig, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct ServerConfig { + #[serde(default = "default_host")] + pub host: String, + #[serde(default = "default_port")] + pub port: u16, + #[serde(default = "default_timeout")] + pub request_timeout_seconds: u64, + #[serde(default = "default_max_request_size")] + pub max_request_size: usize, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct DatabaseConfig { + #[serde(default = "default_foods_table")] + pub foods_table_name: String, + #[serde(default = "default_carts_table")] + pub carts_table_name: String, + #[serde(default = "default_region")] + pub region: String, + #[serde(default = "default_assets_cdn_url")] + pub assets_cdn_url: String, +} + +#[derive(Debug, Clone)] +pub struct AwsConfig { + pub region: String, + pub dynamodb_client: DynamoDbClient, + pub eventbridge_client: EventBridgeClient, + pub ssm_client: SsmClient, + pub parameter_store: Arc, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct ObservabilityConfig { + #[serde(default = "default_service_name")] + pub service_name: String, + #[serde(default = "default_service_version")] + pub service_version: String, + #[serde(default = "default_otlp_endpoint_option")] + pub otlp_endpoint: String, + #[serde(default = "default_metrics_port")] + pub metrics_port: u16, + #[serde(default = "default_log_level")] + pub log_level: String, + #[serde(default = "default_enable_json_logging")] + pub enable_json_logging: bool, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct EventsConfig { + #[serde(default = "default_events_enabled")] + pub enabled: bool, + #[serde(default = "default_event_bus_name")] + pub event_bus_name: String, + #[serde(default = "default_source_name")] + pub source_name: String, + #[serde(default = "default_retry_attempts")] + pub retry_attempts: u32, + #[serde(default = "default_timeout_seconds")] + pub timeout_seconds: u64, + #[serde(default = "default_enable_dead_letter_queue")] + pub enable_dead_letter_queue: bool, +} + +pub struct ParameterStoreConfig { + ssm_client: SsmClient, + cache: Arc>>, + cache_ttl: Duration, +} + +impl std::fmt::Debug for ParameterStoreConfig { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("ParameterStoreConfig") + .field("cache_ttl", &self.cache_ttl) + .field("cache_size", &"") + .finish() + } +} + +impl Config { + pub async fn from_environment() -> Result { + info!("Loading configuration from environment and AWS Parameter Store"); + + // Load basic configuration from environment variables + let server = ServerConfig::from_env()?; + let mut database = DatabaseConfig::from_env()?; + let observability = ObservabilityConfig::from_env()?; + let events = EventsConfig::from_env()?; + + // Initialize AWS configuration with timeout and retry settings + info!( + "Initializing AWS configuration for region: {}", + database.region + ); + + let aws_config = aws_config::defaults(BehaviorVersion::latest()) + .region(aws_config::Region::new(database.region.clone())) + .timeout_config( + aws_config::timeout::TimeoutConfig::builder() + .operation_timeout(Duration::from_secs(60)) + .operation_attempt_timeout(Duration::from_secs(30)) + .build(), + ) + .retry_config(aws_config::retry::RetryConfig::standard().with_max_attempts(3)) + .load() + .await; + + info!("AWS configuration loaded successfully"); + + let dynamodb_client = DynamoDbClient::new(&aws_config); + let eventbridge_client = EventBridgeClient::new(&aws_config); + let ssm_client = SsmClient::new(&aws_config); + + info!("AWS clients created successfully"); + info!("Region: {}", database.region); + info!("DynamoDB endpoint: {:?}", aws_config.endpoint_url()); + + // Create parameter store configuration + let parameter_store = Arc::new(ParameterStoreConfig::new( + ssm_client.clone(), + Duration::from_secs(5 * 60), + )); + + // Retrieve CDN URL from SSM if not set via environment + if database.assets_cdn_url.is_empty() { + info!("CDN URL not set via environment, attempting to retrieve from SSM"); + database.assets_cdn_url = parameter_store + .get_parameter_with_default("/petstore/imagescdnurl", "") + .await; + + if !database.assets_cdn_url.is_empty() { + info!("CDN URL retrieved from SSM: {}", database.assets_cdn_url); + } else { + info!("CDN URL not found in SSM, images will be served without CDN prefix"); + } + } + + let aws = AwsConfig { + region: database.region.clone(), + dynamodb_client, + eventbridge_client, + ssm_client, + parameter_store, + }; + + let config = Config { + server, + database, + aws, + observability, + events, + }; + + // Validate configuration + config.validate().await?; + + info!("Configuration loaded successfully"); + debug!("Configuration: {:?}", config); + + Ok(config) + } + + async fn validate(&self) -> Result<(), ConfigError> { + info!("Validating configuration"); + + // Validate server configuration + if self.server.port == 0 { + return Err(ConfigError::ValidationError { + message: "Server port cannot be 0".to_string(), + }); + } + + if self.server.request_timeout_seconds == 0 { + return Err(ConfigError::ValidationError { + message: "Request timeout cannot be 0".to_string(), + }); + } + + // Validate database configuration + if self.database.foods_table_name.is_empty() { + return Err(ConfigError::ValidationError { + message: "Foods table name cannot be empty".to_string(), + }); + } + + if self.database.carts_table_name.is_empty() { + return Err(ConfigError::ValidationError { + message: "Carts table name cannot be empty".to_string(), + }); + } + + // Assets CDN URL is optional - if empty, images will be served without CDN prefix + if !self.database.assets_cdn_url.is_empty() { + info!( + "Assets CDN URL configured: {}", + self.database.assets_cdn_url + ); + } else { + info!("Assets CDN URL not configured - images will be served without CDN prefix"); + } + + // Test AWS connectivity + match self.aws.ssm_client.describe_parameters().send().await { + Ok(_) => { + info!("AWS SSM connectivity validated"); + } + Err(e) => { + warn!("AWS SSM connectivity test failed: {}", e); + // Don't fail validation for connectivity issues in development + } + } + + info!("Configuration validation completed"); + Ok(()) + } +} + +impl ServerConfig { + fn from_env() -> Result { + let settings = config::Config::builder() + // cSpell:ignore PETFOOD + .add_source(config::Environment::with_prefix("PETFOOD")) + .build() + .map_err(|e| ConfigError::LoadError { + message: format!("Failed to load server config: {}", e), + })?; + + settings + .try_deserialize() + .map_err(|e| ConfigError::LoadError { + message: format!("Failed to deserialize server config: {}", e), + }) + } + + pub fn request_timeout(&self) -> Duration { + Duration::from_secs(self.request_timeout_seconds) + } +} + +impl DatabaseConfig { + fn from_env() -> Result { + let settings = config::Config::builder() + .add_source(config::Environment::with_prefix("PETFOOD")) + .build() + .map_err(|e| ConfigError::LoadError { + message: format!("Failed to load database config: {}", e), + })?; + + settings + .try_deserialize() + .map_err(|e| ConfigError::LoadError { + message: format!("Failed to deserialize database config: {}", e), + }) + } +} + +impl ObservabilityConfig { + fn from_env() -> Result { + let settings = config::Config::builder() + .add_source(config::Environment::with_prefix("PETFOOD")) + .build() + .map_err(|e| ConfigError::LoadError { + message: format!("Failed to load observability config: {}", e), + })?; + + settings + .try_deserialize() + .map_err(|e| ConfigError::LoadError { + message: format!("Failed to deserialize observability config: {}", e), + }) + } +} + +impl EventsConfig { + fn from_env() -> Result { + let settings = config::Config::builder() + .add_source(config::Environment::with_prefix("PETFOOD")) + .build() + .map_err(|e| ConfigError::LoadError { + message: format!("Failed to load events config: {}", e), + })?; + + settings + .try_deserialize() + .map_err(|e| ConfigError::LoadError { + message: format!("Failed to deserialize events config: {}", e), + }) + } +} + +impl ParameterStoreConfig { + pub fn new(ssm_client: SsmClient, cache_ttl: Duration) -> Self { + Self { + ssm_client, + cache: Arc::new(RwLock::new(HashMap::new())), + cache_ttl, + } + } + + pub async fn get_parameter(&self, name: &str) -> Result { + debug!("Getting parameter: {}", name); + + // Check cache first + { + let cache = self.cache.read().await; + if let Some((value, timestamp)) = cache.get(name) { + if timestamp.elapsed() < self.cache_ttl { + debug!("Parameter found in cache: {}", name); + return Ok(value.clone()); + } else { + debug!("Parameter cache expired: {}", name); + } + } + } + + // Fetch from Parameter Store + debug!("Fetching parameter from AWS SSM: {}", name); + let result = self + .ssm_client + .get_parameter() + .name(name) + .with_decryption(false) + .send() + .await + .map_err(|e| ConfigError::AwsSdk { + source: Box::new(e), + })?; + + let value = result + .parameter() + .and_then(|p| p.value()) + .ok_or_else(|| ConfigError::ParameterNotFound { + name: name.to_string(), + })? + .to_string(); + + // Update cache + { + let mut cache = self.cache.write().await; + cache.insert(name.to_string(), (value.clone(), Instant::now())); + } + + debug!("Parameter retrieved and cached: {}", name); + Ok(value) + } + + pub async fn get_parameter_with_default(&self, name: &str, default: &str) -> String { + match self.get_parameter(name).await { + Ok(value) => value, + Err(e) => { + debug!("Failed to get parameter {}, using default: {}", name, e); + default.to_string() + } + } + } + + pub async fn clear_cache(&self) { + let mut cache = self.cache.write().await; + cache.clear(); + info!("Parameter store cache cleared"); + } + + pub async fn cache_size(&self) -> usize { + let cache = self.cache.read().await; + cache.len() + } +} + +// Default value functions +pub(crate) fn default_host() -> String { + "0.0.0.0".to_string() +} + +pub(crate) fn default_port() -> u16 { + 8080 +} + +pub(crate) fn default_timeout() -> u64 { + 30 +} + +pub(crate) fn default_max_request_size() -> usize { + 1024 * 1024 // 1MB +} + +pub(crate) fn default_foods_table() -> String { + "PetFoods".to_string() +} + +pub(crate) fn default_carts_table() -> String { + "PetFoodCarts".to_string() +} + +pub(crate) fn default_region() -> String { + "us-west-2".to_string() +} + +pub(crate) fn default_assets_cdn_url() -> String { + "".to_string() +} + +pub(crate) fn default_service_name() -> String { + "petfood-rs".to_string() +} + +pub(crate) fn default_service_version() -> String { + env!("CARGO_PKG_VERSION").to_string() +} + +pub(crate) fn default_otlp_endpoint_option() -> String { + "http://localhost:4317".to_string() +} + +pub(crate) fn default_enable_json_logging() -> bool { + std::env::var("PETFOOD_ENABLE_JSON_LOGGING") + .map(|v| v.to_lowercase() == "true") + .unwrap_or(false) +} + +pub(crate) fn default_metrics_port() -> u16 { + 9090 +} + +pub(crate) fn default_log_level() -> String { + "info".to_string() +} + +pub(crate) fn default_events_enabled() -> bool { + std::env::var("PETFOOD_EVENTS_ENABLED") + .map(|v| v.to_lowercase() == "true") + .unwrap_or(true) +} + +pub(crate) fn default_event_bus_name() -> String { + "default".to_string() +} + +pub(crate) fn default_source_name() -> String { + "petfood.service".to_string() +} + +pub(crate) fn default_retry_attempts() -> u32 { + 3 +} + +pub(crate) fn default_timeout_seconds() -> u64 { + 30 +} + +pub(crate) fn default_enable_dead_letter_queue() -> bool { + true +} + +#[cfg(test)] +mod tests; diff --git a/src/applications/microservices/petfood-rs/src/config/tests.rs b/src/applications/microservices/petfood-rs/src/config/tests.rs new file mode 100644 index 00000000..be45442f --- /dev/null +++ b/src/applications/microservices/petfood-rs/src/config/tests.rs @@ -0,0 +1,170 @@ +#[cfg(test)] +mod config_tests { + use crate::config::{ + default_assets_cdn_url, default_carts_table, default_foods_table, default_host, + default_log_level, default_max_request_size, default_metrics_port, + default_otlp_endpoint_option, default_port, default_region, default_service_name, + default_timeout, ConfigError, DatabaseConfig, ObservabilityConfig, ParameterStoreConfig, + ServerConfig, + }; + use aws_sdk_ssm::Client as SsmClient; + use std::env; + use std::time::Duration; + + #[test] + fn test_server_config_defaults() { + // Ensure no environment variables are set + env::remove_var("PETFOOD_HOST"); + env::remove_var("PETFOOD_PORT"); + env::remove_var("PETFOOD_REQUEST_TIMEOUT_SECONDS"); + env::remove_var("PETFOOD_MAX_REQUEST_SIZE"); + + // Wait a bit to ensure environment changes take effect + std::thread::sleep(std::time::Duration::from_millis(10)); + + let config = ServerConfig::from_env().unwrap(); + + assert_eq!(config.host, "0.0.0.0"); + assert_eq!(config.port, 8080); + assert_eq!(config.request_timeout_seconds, 30); + assert_eq!(config.max_request_size, 1024 * 1024); + } + + #[test] + fn test_database_config_from_env() { + env::set_var("PETFOOD_FOODS_TABLE_NAME", "TestFoods"); + env::set_var("PETFOOD_CARTS_TABLE_NAME", "TestCarts"); + env::set_var("PETFOOD_REGION", "us-west-2"); + + let config = DatabaseConfig::from_env().unwrap(); + + assert_eq!(config.foods_table_name, "TestFoods"); + assert_eq!(config.carts_table_name, "TestCarts"); + assert_eq!(config.region, "us-west-2"); + + // Clean up + env::remove_var("PETFOOD_FOODS_TABLE_NAME"); + env::remove_var("PETFOOD_CARTS_TABLE_NAME"); + env::remove_var("PETFOOD_REGION"); + } + + #[test] + fn test_observability_config_from_env() { + env::set_var("PETFOOD_SERVICE_NAME", "test-service"); + env::set_var("PETFOOD_SERVICE_VERSION", "1.0.0"); + env::set_var("PETFOOD_OTLP_ENDPOINT", "http://test:4317"); + env::set_var("PETFOOD_METRICS_PORT", "9091"); + env::set_var("PETFOOD_LOG_LEVEL", "debug"); + + let config = ObservabilityConfig::from_env().unwrap(); + + assert_eq!(config.service_name, "test-service"); + assert_eq!(config.service_version, "1.0.0"); + //assert_eq!(config.otlp_endpoint, Some("http://test:4317".to_string())); + assert_eq!(config.metrics_port, 9091); + assert_eq!(config.log_level, "debug"); + + // Clean up + env::remove_var("PETFOOD_SERVICE_NAME"); + env::remove_var("PETFOOD_SERVICE_VERSION"); + env::remove_var("PETFOOD_OTLP_ENDPOINT"); + env::remove_var("PETFOOD_METRICS_PORT"); + env::remove_var("PETFOOD_LOG_LEVEL"); + } + + #[test] + fn test_server_config_request_timeout() { + let config = ServerConfig { + host: "localhost".to_string(), + port: 8080, + request_timeout_seconds: 45, + max_request_size: 1024, + }; + + assert_eq!(config.request_timeout(), Duration::from_secs(45)); + } + + #[tokio::test] + async fn test_parameter_store_config_cache() { + // Create a mock AWS config for testing + let aws_config = aws_config::defaults(aws_config::BehaviorVersion::latest()) + .region(aws_config::Region::new("us-west-2")) + .load() + .await; + + let ssm_client = SsmClient::new(&aws_config); + let parameter_store = ParameterStoreConfig::new(ssm_client, Duration::from_secs(60)); + + // Test cache functionality + assert_eq!(parameter_store.cache_size().await, 0); + + // Test get_parameter_with_default + let default_value = parameter_store + .get_parameter_with_default("/nonexistent/parameter", "default_value") + .await; + assert_eq!(default_value, "default_value"); + + // Test cache clearing + parameter_store.clear_cache().await; + assert_eq!(parameter_store.cache_size().await, 0); + } + + #[test] + fn test_config_error_display() { + let error = ConfigError::ParameterNotFound { + name: "test_param".to_string(), + }; + assert_eq!(error.to_string(), "Parameter not found: test_param"); + + let error = ConfigError::ValidationError { + message: "Invalid configuration".to_string(), + }; + assert_eq!(error.to_string(), "Validation error: Invalid configuration"); + + let error = ConfigError::MissingEnvironmentVariable { + name: "TEST_VAR".to_string(), + }; + assert_eq!(error.to_string(), "Environment variable missing: TEST_VAR"); + } + + #[test] + fn test_default_values() { + // Clean up any environment variables that might affect defaults + env::remove_var("PETFOOD_OTLP_ENDPOINT"); + env::remove_var("PETFOOD_ENABLE_JSON_LOGGING"); + + assert_eq!(default_host(), "0.0.0.0"); + assert_eq!(default_port(), 8080); + assert_eq!(default_timeout(), 30); + assert_eq!(default_max_request_size(), 1024 * 1024); + assert_eq!(default_foods_table(), "PetFoods"); + assert_eq!(default_carts_table(), "PetFoodCarts"); + assert_eq!(default_region(), "us-west-2"); + assert_eq!(default_assets_cdn_url(), ""); + assert_eq!(default_service_name(), "petfood-rs"); + assert_eq!(default_otlp_endpoint_option(), "http://localhost:4317",); + assert_eq!(default_metrics_port(), 9090); + assert_eq!(default_log_level(), "info"); + } + + #[test] + fn test_database_config_with_empty_cdn_url() { + // Test that database config works with empty CDN URL + env::remove_var("PETFOOD_ASSETS_CDN_URL"); + env::set_var("PETFOOD_FOODS_TABLE_NAME", "TestFoods"); + env::set_var("PETFOOD_CARTS_TABLE_NAME", "TestCarts"); + env::set_var("PETFOOD_REGION", "us-west-2"); + + let config = DatabaseConfig::from_env().unwrap(); + + assert_eq!(config.foods_table_name, "TestFoods"); + assert_eq!(config.carts_table_name, "TestCarts"); + assert_eq!(config.region, "us-west-2"); + assert_eq!(config.assets_cdn_url, ""); // Should default to empty string + + // Clean up + env::remove_var("PETFOOD_FOODS_TABLE_NAME"); + env::remove_var("PETFOOD_CARTS_TABLE_NAME"); + env::remove_var("PETFOOD_REGION"); + } +} diff --git a/src/applications/microservices/petfood-rs/src/handlers/admin.rs b/src/applications/microservices/petfood-rs/src/handlers/admin.rs new file mode 100644 index 00000000..80bd35f9 --- /dev/null +++ b/src/applications/microservices/petfood-rs/src/handlers/admin.rs @@ -0,0 +1,627 @@ +use axum::{ + extract::{Path, State}, + http::StatusCode, + response::Json, + routing::{post, put}, + Router, +}; +use serde::Serialize; +use serde_json::{json, Value}; +use std::sync::Arc; +use tracing::{error, info, instrument, warn}; + +use crate::models::{CreateFoodRequest, CreationSource, FoodType, PetType, UpdateFoodRequest}; +use crate::repositories::TableManager; +use crate::services::FoodService; + +/// Admin state containing services +#[derive(Clone)] +pub struct AdminState { + pub food_service: Arc, + pub table_manager: Arc, + pub foods_table_name: String, + pub carts_table_name: String, + pub assets_cdn_url: String, +} + +/// Response for seeding operations +#[derive(Debug, Serialize)] +pub struct SeedResponse { + pub message: String, + pub foods_created: usize, + pub timestamp: String, +} + +/// Response for cleanup operations +#[derive(Debug, Serialize)] +pub struct CleanupResponse { + pub message: String, + pub foods_deleted: usize, + pub timestamp: String, +} + +/// Response for table setup operations +#[derive(Debug, Serialize)] +pub struct SetupTablesResponse { + pub message: String, + pub tables_created: Vec, + pub timestamp: String, +} + +/// Create admin router with database management endpoints +pub fn create_admin_router( + food_service: Arc, + table_manager: Arc, + foods_table_name: String, + carts_table_name: String, + assets_cdn_url: String, +) -> Router { + let state = AdminState { + food_service, + table_manager, + foods_table_name, + carts_table_name, + assets_cdn_url, + }; + + Router::new() + // Database setup and management endpoints + .route("/api/admin/setup-tables", post(setup_tables)) + .route("/api/admin/seed", post(seed_database)) + .route("/api/admin/cleanup", post(cleanup_database)) + // Food management endpoints (admin only) + .route("/api/admin/foods", post(create_food)) + .route( + "/api/admin/foods/:food_id", + put(update_food).delete(delete_food), + ) + .with_state(state) +} + +// ============================================================================= +// DATABASE SETUP, SEEDING AND CLEANUP ENDPOINTS +// ============================================================================= + +/// Set up the required DynamoDB tables +#[instrument(name = "setup_tables", skip(state), fields( + foods_table = %state.foods_table_name, + carts_table = %state.carts_table_name, +))] +pub async fn setup_tables( + State(state): State, +) -> Result, (StatusCode, Json)> { + let timestamp = chrono::Utc::now().to_rfc3339(); + + info!("Setting up DynamoDB tables"); + + match state + .table_manager + .create_all_tables(&state.foods_table_name, &state.carts_table_name) + .await + { + Ok(()) => { + let tables_created = vec![ + state.foods_table_name.clone(), + state.carts_table_name.clone(), + ]; + + info!("Successfully created tables: {:?}", tables_created); + + Ok(Json(SetupTablesResponse { + message: format!("Successfully created {} tables", tables_created.len()), + tables_created, + timestamp, + })) + } + Err(err) => { + error!("Failed to create tables: {}", err); + Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(json!({ + "error": "Failed to create tables", + "message": err.to_string(), + "timestamp": timestamp, + })), + )) + } + } +} + +/// Seed the database with sample food data for all pet types +#[instrument(name = "seed_database", skip(state), fields( + foods_table = %state.foods_table_name, +))] +pub async fn seed_database( + State(state): State, +) -> Result, (StatusCode, Json)> { + let timestamp = chrono::Utc::now().to_rfc3339(); + + info!("Seeding database with sample data"); + + let sample_foods = create_sample_foods(&state.assets_cdn_url); + let mut created_count = 0; + let mut errors = Vec::new(); + + for food_request in sample_foods { + match state + .food_service + .create_food(food_request.clone(), CreationSource::FoodApi) + .await + { + Ok(_) => { + created_count += 1; + info!("Successfully seeded food: {}", food_request.name); + } + Err(err) => { + warn!("Failed to seed food {}: {}", food_request.name, err); + errors.push(format!("{}: {}", food_request.name, err)); + } + } + } + + if errors.is_empty() { + info!("Successfully seeded database with {} foods", created_count); + + Ok(Json(SeedResponse { + message: format!("Database seeded successfully with {} foods", created_count), + foods_created: created_count, + timestamp, + })) + } else { + warn!("Database seeding completed with {} errors", errors.len()); + + if created_count > 0 { + Ok(Json(SeedResponse { + message: format!( + "Database seeded with {} foods, {} errors occurred", + created_count, + errors.len() + ), + foods_created: created_count, + timestamp, + })) + } else { + Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(json!({ + "error": "Failed to seed database", + "details": errors, + "timestamp": timestamp, + })), + )) + } + } +} + +/// Clean up the database (for workshop reset functionality) +#[instrument(name = "cleanup_database", skip(state), fields( + foods_table = %state.foods_table_name, +))] +pub async fn cleanup_database( + State(state): State, +) -> Result, (StatusCode, Json)> { + let timestamp = chrono::Utc::now().to_rfc3339(); + + info!("Cleaning up database"); + + // Get all foods first + match state.food_service.list_foods(Default::default()).await { + Ok(food_list) => { + let mut deleted_count = 0; + let mut errors = Vec::new(); + + for food in food_list.foods { + match state.food_service.delete_food(&food.id).await { + Ok(()) => { + deleted_count += 1; + info!("Successfully discontinued food: {}", food.name); + } + Err(err) => { + warn!("Failed to discontinue food {}: {}", food.name, err); + errors.push(format!("{}: {}", food.name, err)); + } + } + } + + if errors.is_empty() { + info!( + "Successfully cleaned up database, discontinued {} foods", + deleted_count + ); + + Ok(Json(CleanupResponse { + message: format!( + "Database cleaned up successfully, discontinued {} foods", + deleted_count + ), + foods_deleted: deleted_count, + timestamp, + })) + } else { + warn!("Database cleanup completed with {} errors", errors.len()); + + Ok(Json(CleanupResponse { + message: format!( + "Database cleanup completed with {} foods discontinued, {} errors occurred", + deleted_count, + errors.len() + ), + foods_deleted: deleted_count, + timestamp, + })) + } + } + Err(err) => { + error!("Failed to list foods for cleanup: {}", err); + Err(( + StatusCode::INTERNAL_SERVER_ERROR, + Json(json!({ + "error": "Failed to cleanup database", + "message": err.to_string(), + "timestamp": timestamp, + })), + )) + } + } +} + +// ============================================================================= +// FOOD MANAGEMENT ENDPOINTS (ADMIN ONLY) +// ============================================================================= + +/// Create a new food product (admin only) +#[instrument(name = "create_food", skip(state, request), fields( + food_name = %request.name, + pet_type = ?request.pet_type, + food_type = ?request.food_type, + price = %request.price, +))] +pub async fn create_food( + State(state): State, + Json(request): Json, +) -> Result<(StatusCode, Json), (StatusCode, Json)> { + let timestamp = chrono::Utc::now().to_rfc3339(); + + info!("Admin creating new food: {}", request.name); + + match state + .food_service + .create_food(request, CreationSource::AdminApi) + .await + { + Ok(food) => { + info!("Successfully created food with ID: {}", food.id); + let food_response = food.to_response(&state.assets_cdn_url); + Ok((StatusCode::CREATED, Json(food_response))) + } + Err(err) => { + error!("Failed to create food: {}", err); + Err(( + StatusCode::BAD_REQUEST, + Json(json!({ + "error": "Failed to create food", + "message": err.to_string(), + "timestamp": timestamp, + })), + )) + } + } +} + +/// Update an existing food product (admin only) +#[instrument(name = "update_food", skip(state, request), fields( + food_id = %food_id, + food_name = request.name.as_deref(), + price = request.price.as_ref().map(|p| p.to_string()).as_deref(), + stock_quantity = ?request.stock_quantity, +))] +pub async fn update_food( + State(state): State, + Path(food_id): Path, + Json(request): Json, +) -> Result, (StatusCode, Json)> { + let timestamp = chrono::Utc::now().to_rfc3339(); + + info!("Admin updating food with ID: {}", food_id); + + match state.food_service.update_food(&food_id, request).await { + Ok(food) => { + info!("Successfully updated food: {}", food.name); + let food_response = food.to_response(&state.assets_cdn_url); + Ok(Json(food_response)) + } + Err(err) => { + error!("Failed to update food {}: {}", food_id, err); + let status = match err { + crate::models::ServiceError::FoodNotFound { .. } => StatusCode::NOT_FOUND, + crate::models::ServiceError::ValidationError { .. } => StatusCode::BAD_REQUEST, + _ => StatusCode::INTERNAL_SERVER_ERROR, + }; + + Err(( + status, + Json(json!({ + "error": "Failed to update food", + "message": err.to_string(), + "timestamp": timestamp, + })), + )) + } + } +} + +/// Delete a food product (admin only) +#[instrument(name = "delete_food", skip(state), fields( + food_id = %food_id, +))] +pub async fn delete_food( + State(state): State, + Path(food_id): Path, +) -> Result)> { + let timestamp = chrono::Utc::now().to_rfc3339(); + + info!("Admin deleting food with ID: {}", food_id); + + match state.food_service.delete_food(&food_id).await { + Ok(()) => { + info!("Successfully deleted food: {}", food_id); + Ok(StatusCode::NO_CONTENT) + } + Err(err) => { + error!("Failed to delete food {}: {}", food_id, err); + let status = match err { + crate::models::ServiceError::FoodNotFound { .. } => StatusCode::NOT_FOUND, + _ => StatusCode::INTERNAL_SERVER_ERROR, + }; + + Err(( + status, + Json(json!({ + "error": "Failed to delete food", + "message": err.to_string(), + "timestamp": timestamp, + })), + )) + } + } +} + +// ============================================================================= +// HELPER FUNCTIONS +// ============================================================================= + +// Helper function removed - images are now generated via events + +/// Create sample food data for all pet types +fn create_sample_foods(_assets_cdn_url: &str) -> Vec { + vec![ + // Puppy foods + CreateFoodRequest { + pet_type: PetType::Puppy, + name: "Beef and Turkey Kibbles".to_string(), + food_type: FoodType::Dry, + description: + "A nutritious blend of beef and turkey, specially formulated for growing puppies." + .to_string(), + price: rust_decimal_macros::dec!(12.99), + // No image - will be generated via events using description as prompt + nutritional_info: None, + ingredients: vec![ + "beef".to_string(), + "turkey".to_string(), + "rice".to_string(), + "vegetables".to_string(), + ], + feeding_guidelines: Some("Feed 2-3 times daily based on puppy's weight".to_string()), + stock_quantity: 50, + }, + CreateFoodRequest { + pet_type: PetType::Puppy, + name: "Raw Chicken Bites".to_string(), + food_type: FoodType::Wet, + description: "Tender raw chicken bites, ideal for puppies who love a meaty treat." + .to_string(), + price: rust_decimal_macros::dec!(10.99), + nutritional_info: None, + ingredients: vec![ + "chicken".to_string(), + "chicken broth".to_string(), + "vitamins".to_string(), + ], + feeding_guidelines: Some("Serve as a supplement to dry food".to_string()), + stock_quantity: 30, + }, + CreateFoodRequest { + pet_type: PetType::Puppy, + name: "Puppy Training Treats".to_string(), + food_type: FoodType::Treats, + description: "Small, soft treats perfect for training sessions with puppies." + .to_string(), + price: rust_decimal_macros::dec!(8.99), + nutritional_info: None, + ingredients: vec![ + "chicken meal".to_string(), + "sweet potato".to_string(), + "peas".to_string(), + ], + feeding_guidelines: Some("Use sparingly during training sessions".to_string()), + stock_quantity: 75, + }, + // Kitten foods + CreateFoodRequest { + pet_type: PetType::Kitten, + name: "Salmon and Tuna Delight".to_string(), + food_type: FoodType::Wet, + description: "A delectable mix of salmon and tuna, perfect for kittens.".to_string(), + price: rust_decimal_macros::dec!(14.99), + nutritional_info: None, + ingredients: vec![ + "salmon".to_string(), + "tuna".to_string(), + "fish broth".to_string(), + "vitamins".to_string(), + ], + feeding_guidelines: Some("Feed 3-4 times daily for growing kittens".to_string()), + stock_quantity: 40, + }, + CreateFoodRequest { + pet_type: PetType::Kitten, + name: "Kitten Growth Formula".to_string(), + food_type: FoodType::Dry, + description: + "High-protein dry food specially formulated for kitten growth and development." + .to_string(), + price: rust_decimal_macros::dec!(16.99), + nutritional_info: None, + ingredients: vec![ + "chicken meal".to_string(), + "fish meal".to_string(), + "rice".to_string(), + "taurine".to_string(), + ], + feeding_guidelines: Some( + "Free feeding recommended for kittens under 6 months".to_string(), + ), + stock_quantity: 60, + }, + CreateFoodRequest { + pet_type: PetType::Kitten, + name: "Catnip Kitten Treats".to_string(), + food_type: FoodType::Treats, + description: "Irresistible catnip-infused treats that kittens love.".to_string(), + price: rust_decimal_macros::dec!(6.99), + nutritional_info: None, + ingredients: vec![ + "chicken".to_string(), + "catnip".to_string(), + "wheat flour".to_string(), + ], + feeding_guidelines: Some("Give 2-3 treats per day as rewards".to_string()), + stock_quantity: 80, + }, + // Bunny foods + CreateFoodRequest { + pet_type: PetType::Bunny, + name: "Carrot and Herb Crunchies".to_string(), + food_type: FoodType::Dry, + description: "Crunchy carrot and herb treats, specially designed for bunnies." + .to_string(), + price: rust_decimal_macros::dec!(8.99), + nutritional_info: None, + ingredients: vec![ + "carrots".to_string(), + "timothy hay".to_string(), + "herbs".to_string(), + "oats".to_string(), + ], + feeding_guidelines: Some("Supplement to hay-based diet, 1/4 cup daily".to_string()), + stock_quantity: 45, + }, + CreateFoodRequest { + pet_type: PetType::Bunny, + name: "Timothy Hay Pellets".to_string(), + food_type: FoodType::Dry, + description: "High-fiber timothy hay pellets essential for bunny digestive health." + .to_string(), + price: rust_decimal_macros::dec!(12.99), + nutritional_info: None, + ingredients: vec![ + "timothy hay".to_string(), + "alfalfa".to_string(), + "vitamins".to_string(), + "minerals".to_string(), + ], + feeding_guidelines: Some("1/4 to 1/2 cup daily depending on bunny size".to_string()), + stock_quantity: 35, + }, + CreateFoodRequest { + pet_type: PetType::Bunny, + name: "Fresh Veggie Mix".to_string(), + food_type: FoodType::Wet, + description: "A fresh mix of vegetables perfect for bunny nutrition.".to_string(), + price: rust_decimal_macros::dec!(9.99), + nutritional_info: None, + ingredients: vec![ + "carrots".to_string(), + "leafy greens".to_string(), + "bell peppers".to_string(), + "herbs".to_string(), + ], + feeding_guidelines: Some("Serve fresh daily as part of balanced diet".to_string()), + stock_quantity: 25, + }, + ] +} + +#[cfg(test)] +mod tests { + use super::*; + + // Test removed - create_image_path function no longer exists + // Images are now generated via events + + #[test] + fn test_create_sample_foods() { + let sample_foods = create_sample_foods("https://test-cdn.example.com"); + + // Should have foods for all pet types + assert!(sample_foods.iter().any(|f| f.pet_type == PetType::Puppy)); + assert!(sample_foods.iter().any(|f| f.pet_type == PetType::Kitten)); + assert!(sample_foods.iter().any(|f| f.pet_type == PetType::Bunny)); + + // Should have different food types + assert!(sample_foods.iter().any(|f| f.food_type == FoodType::Dry)); + assert!(sample_foods.iter().any(|f| f.food_type == FoodType::Wet)); + assert!(sample_foods.iter().any(|f| f.food_type == FoodType::Treats)); + + // All foods should have valid data + for food in &sample_foods { + assert!(!food.name.is_empty()); + assert!(!food.description.is_empty()); + assert!(food.price > rust_decimal::Decimal::ZERO); + assert!(!food.ingredients.is_empty()); + assert!(food.stock_quantity > 0); + // Images will be generated via events - no image field in CreateFoodRequest + } + } + + #[test] + fn test_seed_response_serialization() { + let response = SeedResponse { + message: "Database seeded successfully".to_string(), + foods_created: 9, + timestamp: "2024-01-01T00:00:00Z".to_string(), + }; + + let json = serde_json::to_string(&response).unwrap(); + assert!(json.contains("Database seeded successfully")); + assert!(json.contains("9")); + } + + #[test] + fn test_cleanup_response_serialization() { + let response = CleanupResponse { + message: "Database cleaned up successfully".to_string(), + foods_deleted: 5, + timestamp: "2024-01-01T00:00:00Z".to_string(), + }; + + let json = serde_json::to_string(&response).unwrap(); + assert!(json.contains("Database cleaned up successfully")); + assert!(json.contains("5")); + } + + #[test] + fn test_setup_tables_response_serialization() { + let response = SetupTablesResponse { + message: "Successfully created 2 tables".to_string(), + tables_created: vec!["foods".to_string(), "carts".to_string()], + timestamp: "2024-01-01T00:00:00Z".to_string(), + }; + + let json = serde_json::to_string(&response).unwrap(); + assert!(json.contains("Successfully created 2 tables")); + assert!(json.contains("foods")); + assert!(json.contains("carts")); + } +} diff --git a/src/applications/microservices/petfood-rs/src/handlers/api.rs b/src/applications/microservices/petfood-rs/src/handlers/api.rs new file mode 100644 index 00000000..43aeb597 --- /dev/null +++ b/src/applications/microservices/petfood-rs/src/handlers/api.rs @@ -0,0 +1,470 @@ +use axum::{ + extract::{Path, Query, State}, + http::StatusCode, + response::Json, + routing::{get, post, put}, + Router, +}; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; +use std::sync::Arc; +use tracing::{error, info, instrument}; + +use crate::models::{ + AddCartItemRequest, CartItemResponse, CartResponse, FoodFilters, FoodListApiResponse, + FoodResponse, ServiceError, UpdateCartItemRequest, +}; +use crate::services::{CartService, FoodService}; + +/// Shared application state containing all services +#[derive(Clone)] +pub struct ApiState { + pub food_service: Arc, + pub cart_service: Arc, + pub assets_cdn_url: String, +} + +/// Query parameters for listing foods +#[derive(Debug, Deserialize)] +pub struct ListFoodsQuery { + #[serde(alias = "pettype")] + pub pet_type: Option, + #[serde(alias = "foodtype")] + pub food_type: Option, + pub availability_status: Option, + pub min_price: Option, + pub max_price: Option, + pub search: Option, + pub in_stock_only: Option, +} + +/// Response for cart validation +#[derive(Debug, Serialize)] +pub struct CartValidationResponse { + pub is_valid: bool, + pub issues: Vec, +} + +/// Create API router with all endpoints +pub fn create_api_router( + food_service: Arc, + cart_service: Arc, + assets_cdn_url: String, +) -> Router { + let state = ApiState { + food_service, + cart_service, + assets_cdn_url, + }; + + Router::new() + // Food browsing endpoints (read-only) + .route("/api/foods", get(list_foods)) + .route("/api/foods/:food_id", get(get_food)) + // Cart management endpoints + .route("/api/cart/:user_id", get(get_cart).delete(delete_cart)) + .route("/api/cart/:user_id/items", post(add_cart_item)) + .route( + "/api/cart/:user_id/items/:food_id", + put(update_cart_item).delete(remove_cart_item), + ) + .route("/api/cart/:user_id/clear", post(clear_cart)) + .route("/api/cart/:user_id/checkout", post(checkout_cart)) + .with_state(state) +} + +// ============================================================================= +// FOOD ENDPOINTS +// ============================================================================= + +/// List all foods with optional filters +#[instrument(name = "list_foods", skip(state), fields( + pet_type = query.pet_type.as_deref(), + food_type = query.food_type.as_deref(), + search = query.search.as_deref(), +))] +pub async fn list_foods( + State(state): State, + Query(query): Query, +) -> Result, (StatusCode, Json)> { + info!("Listing foods with filters"); + + // Convert query parameters to filters + let filters = match query_to_filters(query) { + Ok(filters) => filters, + Err(err) => { + error!("Invalid query parameters: {}", err); + return Err(( + StatusCode::BAD_REQUEST, + Json(json!({ + "error": "Invalid query parameters", + "message": err, + "timestamp": chrono::Utc::now().to_rfc3339(), + })), + )); + } + }; + + match state.food_service.list_foods(filters).await { + Ok(response) => { + info!("Successfully listed {} foods", response.total_count); + + // Convert Food to FoodResponse with full image URLs + let food_responses: Vec = response + .foods + .iter() + .map(|food| food.to_response(&state.assets_cdn_url)) + .collect(); + + let response_with_urls = FoodListApiResponse { + foods: food_responses, + total_count: response.total_count, + page: response.page, + page_size: response.page_size, + }; + + Ok(Json(response_with_urls)) + } + Err(err) => { + error!("Failed to list foods: {}", err); + Err(service_error_to_response(err)) + } + } +} + +/// Get a specific food by ID +#[instrument(name = "get_food", skip(state), fields(food_id = %food_id))] +pub async fn get_food( + State(state): State, + Path(food_id): Path, +) -> Result, (StatusCode, Json)> { + info!("Getting food with ID: {}", food_id); + + match state.food_service.get_food(&food_id).await { + Ok(food) => { + info!("Successfully retrieved food: {}", food.name); + let food_response = food.to_response(&state.assets_cdn_url); + Ok(Json(food_response)) + } + Err(err) => { + error!("Failed to get food {}: {}", food_id, err); + Err(service_error_to_response(err)) + } + } +} + +// ============================================================================= +// CART ENDPOINTS +// ============================================================================= + +/// Get a user's cart +#[instrument(name = "get_cart", skip(state), fields(user_id = %user_id))] +pub async fn get_cart( + State(state): State, + Path(user_id): Path, +) -> Result, (StatusCode, Json)> { + info!("Getting cart for user: {}", user_id); + + match state.cart_service.get_cart(&user_id).await { + Ok(cart) => { + info!( + "Successfully retrieved cart with {} items", + cart.total_items + ); + Ok(Json(cart)) + } + Err(err) => { + error!("Failed to get cart for user {}: {}", user_id, err); + Err(service_error_to_response(err)) + } + } +} + +/// Add an item to the cart +#[instrument(name = "add_cart_item", skip(state, request), fields( + user_id = %user_id, + food_id = %request.food_id, + quantity = %request.quantity, +))] +pub async fn add_cart_item( + State(state): State, + Path(user_id): Path, + Json(request): Json, +) -> Result<(StatusCode, Json), (StatusCode, Json)> { + crate::info_with_trace!( + "Adding item to cart for user: {}, food_id: {}, quantity: {}", + user_id, + request.food_id, + request.quantity + ); + + match state.cart_service.add_item(&user_id, request).await { + Ok(item) => { + crate::info_with_trace!("Successfully added item to cart"); + Ok((StatusCode::CREATED, Json(item))) + } + Err(err) => { + crate::error_with_trace!("Failed to add item to cart: {}", err); + Err(service_error_to_response(err)) + } + } +} + +/// Update the quantity of an item in the cart +#[instrument(name = "update_cart_item", skip(state, request), fields( + user_id = %user_id, + food_id = %food_id, + quantity = %request.quantity, +))] +pub async fn update_cart_item( + State(state): State, + Path((user_id, food_id)): Path<(String, String)>, + Json(request): Json, +) -> Result, (StatusCode, Json)> { + crate::info_with_trace!( + "Updating cart item for user: {}, food_id: {}, new_quantity: {}", + user_id, + food_id, + request.quantity + ); + + match state + .cart_service + .update_item(&user_id, &food_id, request) + .await + { + Ok(item) => { + crate::info_with_trace!("Successfully updated cart item"); + Ok(Json(item)) + } + Err(err) => { + crate::error_with_trace!("Failed to update cart item: {}", err); + Err(service_error_to_response(err)) + } + } +} + +/// Remove an item from the cart +#[instrument(name = "remove_cart_item", skip(state), fields( + user_id = %user_id, + food_id = %food_id, +))] +pub async fn remove_cart_item( + State(state): State, + Path((user_id, food_id)): Path<(String, String)>, +) -> Result)> { + crate::info_with_trace!( + "Removing item from cart for user: {}, food_id: {}", + user_id, + food_id + ); + + match state.cart_service.remove_item(&user_id, &food_id).await { + Ok(()) => { + crate::info_with_trace!("Successfully removed item from cart"); + Ok(StatusCode::NO_CONTENT) + } + Err(err) => { + crate::error_with_trace!("Failed to remove item from cart: {}", err); + Err(service_error_to_response(err)) + } + } +} + +/// Clear all items from the cart +#[instrument(name = "clear_cart", skip(state), fields( + user_id = %user_id, +))] +pub async fn clear_cart( + State(state): State, + Path(user_id): Path, +) -> Result)> { + crate::info_with_trace!("Clearing cart for user: {}", user_id); + + match state.cart_service.clear_cart(&user_id).await { + Ok(()) => { + crate::info_with_trace!("Successfully cleared cart"); + Ok(StatusCode::NO_CONTENT) + } + Err(err) => { + crate::error_with_trace!("Failed to clear cart: {}", err); + Err(service_error_to_response(err)) + } + } +} + +/// Delete the entire cart +#[instrument(name = "delete_cart", skip(state), fields( + user_id = %user_id, +))] +pub async fn delete_cart( + State(state): State, + Path(user_id): Path, +) -> Result)> { + crate::info_with_trace!("Deleting cart for user: {}", user_id); + + match state.cart_service.delete_cart(&user_id).await { + Ok(()) => { + crate::info_with_trace!("Successfully deleted cart"); + Ok(StatusCode::NO_CONTENT) + } + Err(err) => { + crate::error_with_trace!("Failed to delete cart: {}", err); + Err(service_error_to_response(err)) + } + } +} + +/// Checkout cart and create order +#[instrument(name = "checkout_cart", skip(state, request), fields( + user_id = %user_id, + has_shipping_address = request.shipping_address.is_some(), + has_billing_address = request.billing_address.is_some(), +))] +pub async fn checkout_cart( + State(state): State, + Path(user_id): Path, + Json(request): Json, +) -> Result, (StatusCode, Json)> { + info!("Processing checkout for user: {}", user_id); + + match state.cart_service.checkout(&user_id, request).await { + Ok(checkout_response) => { + crate::info_with_trace!( + "Checkout completed successfully for order: {}", + checkout_response.order_id + ); + Ok(Json(checkout_response)) + } + Err(err) => { + crate::error_with_trace!("Failed to process checkout: {}", err); + Err(service_error_to_response(err)) + } + } +} + +// ============================================================================= +// HELPER FUNCTIONS +// ============================================================================= + +/// Convert query parameters to FoodFilters +fn query_to_filters(query: ListFoodsQuery) -> Result { + let mut filters = FoodFilters::default(); + + // Parse pet type + if let Some(pet_type_str) = query.pet_type { + filters.pet_type = Some( + pet_type_str + .parse() + .map_err(|e| format!("Invalid pet_type: {}", e))?, + ); + } + + // Parse food type + if let Some(food_type_str) = query.food_type { + filters.food_type = Some( + food_type_str + .parse() + .map_err(|e| format!("Invalid food_type: {}", e))?, + ); + } + + // Parse availability status + if let Some(status_str) = query.availability_status { + filters.availability_status = Some( + status_str + .parse() + .map_err(|e| format!("Invalid availability_status: {}", e))?, + ); + } + + // Set price filters + filters.min_price = query.min_price; + filters.max_price = query.max_price; + + // Set search term + filters.search_term = query.search; + + // Set in stock only filter + filters.in_stock_only = query.in_stock_only; + + Ok(filters) +} + +/// Convert ServiceError to HTTP response +fn service_error_to_response(err: ServiceError) -> (StatusCode, Json) { + let (status, message) = match err { + ServiceError::FoodNotFound { .. } => (StatusCode::NOT_FOUND, err.to_string()), + ServiceError::CartNotFound { .. } => (StatusCode::NOT_FOUND, err.to_string()), + ServiceError::CartItemNotFound { .. } => (StatusCode::NOT_FOUND, err.to_string()), + ServiceError::ValidationError { .. } => (StatusCode::BAD_REQUEST, err.to_string()), + ServiceError::InvalidPetType { .. } => (StatusCode::BAD_REQUEST, err.to_string()), + ServiceError::InvalidQuantity { .. } => (StatusCode::BAD_REQUEST, err.to_string()), + ServiceError::InsufficientStock { .. } => (StatusCode::CONFLICT, err.to_string()), + ServiceError::ProductUnavailable { .. } => (StatusCode::CONFLICT, err.to_string()), + ServiceError::Repository { source } => match source { + crate::models::RepositoryError::NotFound => { + (StatusCode::NOT_FOUND, "Resource not found".to_string()) + } + crate::models::RepositoryError::ConnectionFailed => ( + StatusCode::SERVICE_UNAVAILABLE, + "Database connection failed".to_string(), + ), + crate::models::RepositoryError::Timeout => { + (StatusCode::REQUEST_TIMEOUT, "Request timeout".to_string()) + } + crate::models::RepositoryError::RateLimitExceeded => ( + StatusCode::TOO_MANY_REQUESTS, + "Rate limit exceeded".to_string(), + ), + _ => ( + StatusCode::INTERNAL_SERVER_ERROR, + "Internal server error".to_string(), + ), + }, + ServiceError::Configuration { .. } => ( + StatusCode::INTERNAL_SERVER_ERROR, + "Configuration error".to_string(), + ), + ServiceError::ExternalService { .. } => ( + StatusCode::BAD_GATEWAY, + "External service error".to_string(), + ), + }; + + ( + status, + Json(json!({ + "error": message, + "timestamp": chrono::Utc::now().to_rfc3339(), + })), + ) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::models::{FoodType, PetType}; + + #[test] + fn test_query_to_filters() { + let query = ListFoodsQuery { + pet_type: Some("puppy".to_string()), + food_type: Some("dry".to_string()), + availability_status: Some("in_stock".to_string()), + min_price: Some(rust_decimal_macros::dec!(5.00)), + max_price: Some(rust_decimal_macros::dec!(20.00)), + search: Some("kibble".to_string()), + in_stock_only: Some(true), + }; + + let filters = query_to_filters(query).unwrap(); + + assert_eq!(filters.pet_type, Some(PetType::Puppy)); + assert_eq!(filters.food_type, Some(FoodType::Dry)); + assert_eq!(filters.min_price, Some(rust_decimal_macros::dec!(5.00))); + assert_eq!(filters.max_price, Some(rust_decimal_macros::dec!(20.00))); + assert_eq!(filters.search_term, Some("kibble".to_string())); + assert_eq!(filters.in_stock_only, Some(true)); + } +} diff --git a/src/applications/microservices/petfood-rs/src/handlers/health.rs b/src/applications/microservices/petfood-rs/src/handlers/health.rs new file mode 100644 index 00000000..d4b2822b --- /dev/null +++ b/src/applications/microservices/petfood-rs/src/handlers/health.rs @@ -0,0 +1,14 @@ +use axum::{http::StatusCode, response::Json}; +use serde_json::{json, Value}; +use tracing::instrument; + +/// Health check endpoint handler +#[instrument(name = "health_check")] +pub async fn health_check() -> Result, StatusCode> { + Ok(Json(json!({ + "status": "healthy", + "service": "petfood-rs", + "version": env!("CARGO_PKG_VERSION"), + "timestamp": chrono::Utc::now().to_rfc3339() + }))) +} diff --git a/src/applications/microservices/petfood-rs/src/handlers/metrics.rs b/src/applications/microservices/petfood-rs/src/handlers/metrics.rs new file mode 100644 index 00000000..42cc3e72 --- /dev/null +++ b/src/applications/microservices/petfood-rs/src/handlers/metrics.rs @@ -0,0 +1,79 @@ +use axum::{ + extract::State, + http::{header, StatusCode}, + response::{IntoResponse, Response}, +}; +use std::sync::Arc; +use tracing::{error, instrument}; + +use crate::observability::Metrics; + +/// Handler for Prometheus metrics endpoint +#[instrument(name = "metrics_handler", skip(metrics))] +pub async fn metrics_handler(State(metrics): State>) -> Response { + match metrics.encode() { + Ok(metrics_text) => ( + StatusCode::OK, + [( + header::CONTENT_TYPE, + "text/plain; version=0.0.4; charset=utf-8", + )], + metrics_text, + ) + .into_response(), + Err(e) => { + error!(error = %e, "Failed to encode metrics"); + ( + StatusCode::INTERNAL_SERVER_ERROR, + "Failed to encode metrics", + ) + .into_response() + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use axum::{ + body::Body, + http::{Request, StatusCode}, + routing::get, + Router, + }; + use tower::ServiceExt; + + #[tokio::test] + async fn test_metrics_handler() { + let metrics = Arc::new(Metrics::new().unwrap()); + + // Record some test metrics + metrics.record_http_request("GET", "/test", 200, 0.123); + metrics.record_database_operation("get_item", "test_table", true, 0.050); + + let app = Router::new() + .route("/metrics", get(metrics_handler)) + .with_state(metrics); + + let request = Request::builder() + .uri("/metrics") + .body(Body::empty()) + .unwrap(); + + let response = app.oneshot(request).await.unwrap(); + + assert_eq!(response.status(), StatusCode::OK); + + let content_type = response.headers().get("content-type").unwrap(); + assert_eq!(content_type, "text/plain; version=0.0.4; charset=utf-8"); + + let body = axum::body::to_bytes(response.into_body(), usize::MAX) + .await + .unwrap(); + let body_str = String::from_utf8(body.to_vec()).unwrap(); + + // Verify metrics are present + assert!(body_str.contains("http_requests_total")); + assert!(body_str.contains("database_operations_total")); + } +} diff --git a/src/applications/microservices/petfood-rs/src/handlers/middleware.rs b/src/applications/microservices/petfood-rs/src/handlers/middleware.rs new file mode 100644 index 00000000..a864544d --- /dev/null +++ b/src/applications/microservices/petfood-rs/src/handlers/middleware.rs @@ -0,0 +1,243 @@ +use axum::{ + body::Body, + extract::Request, + http::StatusCode, + middleware::Next, + response::{Json, Response}, +}; +use serde_json::{json, Value}; +use tracing::{error, warn}; + +/// Request validation middleware +pub async fn request_validation_middleware( + request: Request, + next: Next, +) -> Result)> { + // Validate content type for POST/PUT requests + validate_content_type(&request)?; + + // Validate request size + validate_request_size(&request)?; + + // Continue with the request + let response = next.run(request).await; + Ok(response) +} + +/// Validate content type for requests with body +fn validate_content_type(request: &Request) -> Result<(), (StatusCode, Json)> { + let method = request.method(); + + // Only validate content type for requests that should have a body + if method == "POST" || method == "PUT" || method == "PATCH" { + let headers = request.headers(); + + if let Some(content_type) = headers.get("content-type") { + let content_type_str = content_type.to_str().unwrap_or(""); + + // Check if it's JSON + if !content_type_str.starts_with("application/json") { + warn!("Invalid content type: {}", content_type_str); + return Err(( + StatusCode::UNSUPPORTED_MEDIA_TYPE, + Json(json!({ + "error": "Unsupported media type", + "message": "Content-Type must be application/json", + "timestamp": chrono::Utc::now().to_rfc3339(), + })), + )); + } + } else { + warn!("Missing content type header"); + return Err(( + StatusCode::BAD_REQUEST, + Json(json!({ + "error": "Missing content type", + "message": "Content-Type header is required for requests with body", + "timestamp": chrono::Utc::now().to_rfc3339(), + })), + )); + } + } + + Ok(()) +} + +/// Validate request size +fn validate_request_size(request: &Request) -> Result<(), (StatusCode, Json)> { + const MAX_REQUEST_SIZE: u64 = 1024 * 1024; // 1MB + + if let Some(content_length) = request.headers().get("content-length") { + if let Ok(length_str) = content_length.to_str() { + if let Ok(length) = length_str.parse::() { + if length > MAX_REQUEST_SIZE { + error!("Request too large: {} bytes", length); + return Err(( + StatusCode::PAYLOAD_TOO_LARGE, + Json(json!({ + "error": "Request too large", + "message": format!("Request size {} bytes exceeds maximum of {} bytes", length, MAX_REQUEST_SIZE), + "timestamp": chrono::Utc::now().to_rfc3339(), + })), + )); + } + } + } + } + + Ok(()) +} + +/// CORS middleware for handling cross-origin requests +pub async fn cors_middleware(request: Request, next: Next) -> Response { + let response = next.run(request).await; + + let mut response = response; + let headers = response.headers_mut(); + + // Add CORS headers + headers.insert("Access-Control-Allow-Origin", "*".parse().unwrap()); + headers.insert( + "Access-Control-Allow-Methods", + "GET, POST, PUT, DELETE, OPTIONS".parse().unwrap(), + ); + headers.insert( + "Access-Control-Allow-Headers", + "Content-Type, Authorization".parse().unwrap(), + ); + headers.insert("Access-Control-Max-Age", "86400".parse().unwrap()); + + response +} + +/// Rate limiting middleware (basic implementation) +pub async fn rate_limiting_middleware( + request: Request, + next: Next, +) -> Result)> { + // For now, this is a placeholder implementation + // In a real application, you would implement proper rate limiting + // using a distributed cache like Redis or an in-memory store + + // Extract client IP or user ID for rate limiting + let client_id = extract_client_identifier(&request); + + // Check rate limit (placeholder logic) + if is_rate_limited(&client_id) { + warn!("Rate limit exceeded for client: {}", client_id); + return Err(( + StatusCode::TOO_MANY_REQUESTS, + Json(json!({ + "error": "Rate limit exceeded", + "message": "Too many requests. Please try again later.", + "retry_after": 60, + "timestamp": chrono::Utc::now().to_rfc3339(), + })), + )); + } + + // Continue with the request + Ok(next.run(request).await) +} + +/// Extract client identifier for rate limiting +fn extract_client_identifier(request: &Request) -> String { + // Try to get client IP from headers + if let Some(forwarded_for) = request.headers().get("x-forwarded-for") { + if let Ok(ip) = forwarded_for.to_str() { + return ip.split(',').next().unwrap_or("unknown").trim().to_string(); + } + } + + if let Some(real_ip) = request.headers().get("x-real-ip") { + if let Ok(ip) = real_ip.to_str() { + return ip.to_string(); + } + } + + // Fallback to connection info (not available in this context) + "unknown".to_string() +} + +/// Check if client is rate limited (placeholder implementation) +fn is_rate_limited(_client_id: &str) -> bool { + // Placeholder: always allow requests + // In a real implementation, you would check against a rate limiting store + false +} + +/// Security headers middleware +pub async fn security_headers_middleware(request: Request, next: Next) -> Response { + let response = next.run(request).await; + + let mut response = response; + let headers = response.headers_mut(); + + // Add security headers + headers.insert("X-Content-Type-Options", "nosniff".parse().unwrap()); + headers.insert("X-Frame-Options", "DENY".parse().unwrap()); + headers.insert("X-XSS-Protection", "1; mode=block".parse().unwrap()); + headers.insert( + "Referrer-Policy", + "strict-origin-when-cross-origin".parse().unwrap(), + ); + headers.insert( + "Content-Security-Policy", + "default-src 'self'".parse().unwrap(), + ); + + response +} + +#[cfg(test)] +mod tests { + use super::*; + use axum::body::Body; + use axum::http::{Method, Request}; + + #[test] + fn test_extract_client_identifier() { + let mut request = Request::builder() + .method(Method::GET) + .uri("/test") + .body(Body::empty()) + .unwrap(); + + // Test with X-Forwarded-For header + request + .headers_mut() + .insert("x-forwarded-for", "192.168.1.1, 10.0.0.1".parse().unwrap()); + + let client_id = extract_client_identifier(&request); + assert_eq!(client_id, "192.168.1.1"); + } + + #[test] + fn test_extract_client_identifier_real_ip() { + let mut request = Request::builder() + .method(Method::GET) + .uri("/test") + .body(Body::empty()) + .unwrap(); + + // Test with X-Real-IP header + request + .headers_mut() + .insert("x-real-ip", "203.0.113.1".parse().unwrap()); + + let client_id = extract_client_identifier(&request); + assert_eq!(client_id, "203.0.113.1"); + } + + #[test] + fn test_extract_client_identifier_unknown() { + let request = Request::builder() + .method(Method::GET) + .uri("/test") + .body(Body::empty()) + .unwrap(); + + let client_id = extract_client_identifier(&request); + assert_eq!(client_id, "unknown"); + } +} diff --git a/src/applications/microservices/petfood-rs/src/handlers/mod.rs b/src/applications/microservices/petfood-rs/src/handlers/mod.rs new file mode 100644 index 00000000..66b43744 --- /dev/null +++ b/src/applications/microservices/petfood-rs/src/handlers/mod.rs @@ -0,0 +1,13 @@ +pub mod admin; +pub mod api; +pub mod health; +pub mod metrics; +pub mod middleware; + +pub use health::*; +pub use metrics::*; +// pub use food::*; +// pub use cart::*; +pub use admin::*; +pub use api::*; +pub use middleware::*; diff --git a/src/applications/microservices/petfood-rs/src/lib.rs b/src/applications/microservices/petfood-rs/src/lib.rs new file mode 100644 index 00000000..90083e27 --- /dev/null +++ b/src/applications/microservices/petfood-rs/src/lib.rs @@ -0,0 +1,9 @@ +pub mod config; +pub mod handlers; +pub mod models; +pub mod observability; +pub mod repositories; +pub mod services; + +pub use config::{Config, ConfigError, ParameterStoreConfig}; +pub use observability::{init_observability, shutdown_observability, Metrics}; diff --git a/src/applications/microservices/petfood-rs/src/main.rs b/src/applications/microservices/petfood-rs/src/main.rs new file mode 100644 index 00000000..a1ea768b --- /dev/null +++ b/src/applications/microservices/petfood-rs/src/main.rs @@ -0,0 +1,217 @@ +use axum::{ + middleware, + routing::{get, post, put}, + Router, +}; +use std::{net::SocketAddr, sync::Arc}; +use tokio::net::TcpListener; +use tracing::{info, warn}; + +use petfood_rs::{ + handlers::{ + admin, api, cors_middleware, health_check, metrics_handler, request_validation_middleware, + security_headers_middleware, + }, + init_observability, + models::EventConfig, + observability::{observability_middleware, Metrics}, + repositories::{DynamoDbCartRepository, DynamoDbFoodRepository, TableManager}, + services::{CartService, EventEmitter, FoodService}, + shutdown_observability, Config, +}; + +#[tokio::main] +async fn main() -> Result<(), Box> { + // Load configuration first (basic logging only) + let config = Config::from_environment().await?; + println!("Configuration loaded successfully"); + + // Initialize comprehensive observability + init_observability( + &config.observability.service_name, + &config.observability.service_version, + &config.observability.otlp_endpoint, + config.observability.enable_json_logging, + )?; + + info!("Starting petfood-rs service"); + info!( + "Service: {} v{}", + config.observability.service_name, config.observability.service_version + ); + info!("Region: {}", config.aws.region); + info!( + "DynamoDB Tables: foods={}, carts={}", + config.database.foods_table_name, config.database.carts_table_name + ); + + // Initialize metrics + let metrics = Arc::new(Metrics::new()?); + info!("Metrics initialized successfully"); + + // Use AWS clients from config (already properly configured with region and credentials) + let dynamodb_client = Arc::new(config.aws.dynamodb_client.clone()); + info!("AWS clients initialized successfully"); + + // Initialize table manager + let table_manager = Arc::new(TableManager::new(dynamodb_client.clone())); + info!("Table manager initialized successfully"); + + // Initialize repositories + let food_repository = Arc::new(DynamoDbFoodRepository::new( + dynamodb_client.clone(), + config.database.foods_table_name.clone(), + config.database.region.clone(), + )); + let cart_repository = Arc::new(DynamoDbCartRepository::new( + dynamodb_client.clone(), + config.database.carts_table_name.clone(), + config.database.region.clone(), + )); + info!("Repositories initialized successfully"); + + // Initialize event emitter if enabled + let food_service = if config.events.enabled { + let event_config = EventConfig { + event_bus_name: config.events.event_bus_name.clone(), + source_name: config.events.source_name.clone(), + retry_attempts: config.events.retry_attempts, + timeout_seconds: config.events.timeout_seconds, + enable_dead_letter_queue: config.events.enable_dead_letter_queue, + enabled: config.events.enabled, + }; + + match EventEmitter::new(config.aws.eventbridge_client.clone(), event_config) { + Ok(event_emitter) => { + info!("Event emitter initialized successfully"); + info!( + "Events bus_name={}, source_name={}", + config.events.event_bus_name, config.events.source_name + ); + Arc::new(FoodService::new_with_event_emitter( + food_repository.clone(), + Arc::new(event_emitter), + )) + } + Err(e) => { + warn!( + "Failed to initialize event emitter: {}, continuing without events", + e + ); + Arc::new(FoodService::new(food_repository.clone())) + } + } + } else { + info!("Event emission disabled"); + Arc::new(FoodService::new(food_repository.clone())) + }; + + let cart_service = Arc::new(CartService::new( + cart_repository, + food_repository, + config.database.assets_cdn_url.clone(), + )); + info!("Services initialized successfully"); + + // Build the application router + let app = create_app( + metrics, + food_service, + cart_service, + table_manager, + config.database.foods_table_name.clone(), + config.database.carts_table_name.clone(), + config.database.assets_cdn_url.clone(), + ); + + // Create socket address + let addr = SocketAddr::new(config.server.host.parse()?, config.server.port); + + info!("Server listening on {}", addr); + + // Create TCP listener + let listener = TcpListener::bind(addr).await?; + + // Set up graceful shutdown + let shutdown_signal = async { + tokio::signal::ctrl_c() + .await + .expect("Failed to install CTRL+C signal handler"); + info!("Shutdown signal received"); + shutdown_observability().await; + }; + + // Start the server with graceful shutdown + axum::serve(listener, app) + .with_graceful_shutdown(shutdown_signal) + .await?; + + info!("Server shutdown complete"); + Ok(()) +} + +fn create_app( + metrics: Arc, + food_service: Arc, + cart_service: Arc, + table_manager: Arc, + foods_table_name: String, + carts_table_name: String, + assets_cdn_url: String, +) -> Router { + let metrics_for_middleware = metrics.clone(); + + // Create the API state + let api_state = api::ApiState { + food_service: food_service.clone(), + cart_service, + assets_cdn_url: assets_cdn_url.clone(), + }; + + // Create the admin state + let admin_state = admin::AdminState { + food_service: food_service.clone(), + table_manager, + foods_table_name, + carts_table_name, + assets_cdn_url, + }; + + Router::new() + // Health and metrics endpoints (with metrics state) + .route("/health/status", get(health_check)) + .route("/metrics", get(metrics_handler)) + .with_state(metrics) + // API endpoints (with API state) - read-only food endpoints + .route("/api/foods", get(api::list_foods)) + .route("/api/foods/:food_id", get(api::get_food)) + .route( + "/api/cart/:user_id", + get(api::get_cart).delete(api::delete_cart), + ) + .route("/api/cart/:user_id/items", post(api::add_cart_item)) + .route( + "/api/cart/:user_id/items/:food_id", + put(api::update_cart_item).delete(api::remove_cart_item), + ) + .route("/api/cart/:user_id/clear", post(api::clear_cart)) + .route("/api/cart/:user_id/checkout", post(api::checkout_cart)) + .with_state(api_state) + // Admin endpoints (with admin state) + .route("/api/admin/setup-tables", post(admin::setup_tables)) + .route("/api/admin/seed", post(admin::seed_database)) + .route("/api/admin/cleanup", post(admin::cleanup_database)) + .route("/api/admin/foods", post(admin::create_food)) + .route( + "/api/admin/foods/:food_id", + put(admin::update_food).delete(admin::delete_food), + ) + .with_state(admin_state) + // Add middleware layers (order matters - outer to inner) + .layer(middleware::from_fn(security_headers_middleware)) + .layer(middleware::from_fn(cors_middleware)) + .layer(middleware::from_fn(request_validation_middleware)) + .layer(middleware::from_fn(move |req, next| { + observability_middleware(metrics_for_middleware.clone(), req, next) + })) +} diff --git a/src/applications/microservices/petfood-rs/src/models/cart.rs b/src/applications/microservices/petfood-rs/src/models/cart.rs new file mode 100644 index 00000000..e615cca5 --- /dev/null +++ b/src/applications/microservices/petfood-rs/src/models/cart.rs @@ -0,0 +1,400 @@ +use chrono::{DateTime, Utc}; +use rust_decimal::Decimal; +use serde::{Deserialize, Serialize}; + +/// Shopping cart for a user +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Cart { + pub user_id: String, + pub items: Vec, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +/// Individual item in a shopping cart +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct CartItem { + pub food_id: String, + pub quantity: u32, + pub unit_price: Decimal, + pub added_at: DateTime, +} + +/// Request model for adding an item to cart +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AddCartItemRequest { + pub food_id: String, + pub quantity: u32, +} + +/// Request model for updating cart item quantity +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct UpdateCartItemRequest { + pub quantity: u32, +} + +/// Response model for cart operations +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CartResponse { + pub user_id: String, + pub items: Vec, + pub total_items: u32, + pub total_price: Decimal, + pub created_at: DateTime, + pub updated_at: DateTime, +} + +/// Enhanced cart item response with food details +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CartItemResponse { + pub food_id: String, + pub food_name: String, + pub food_image: String, + pub quantity: u32, + pub unit_price: Decimal, + pub total_price: Decimal, + pub is_available: bool, + pub added_at: DateTime, +} + +impl Cart { + /// Create a new empty cart for a user + pub fn new(user_id: String) -> Self { + let now = Utc::now(); + Self { + user_id, + items: Vec::new(), + created_at: now, + updated_at: now, + } + } + + /// Add an item to the cart or update quantity if it already exists + pub fn add_item(&mut self, food_id: String, quantity: u32, unit_price: Decimal) { + if let Some(existing_item) = self.items.iter_mut().find(|item| item.food_id == food_id) { + existing_item.quantity += quantity; + } else { + let item = CartItem { + food_id, + quantity, + unit_price, + added_at: Utc::now(), + }; + self.items.push(item); + } + self.updated_at = Utc::now(); + } + + /// Update the quantity of a specific item in the cart + pub fn update_item_quantity(&mut self, food_id: &str, new_quantity: u32) -> bool { + if let Some(item) = self.items.iter_mut().find(|item| item.food_id == food_id) { + if new_quantity == 0 { + self.remove_item(food_id) + } else { + item.quantity = new_quantity; + self.updated_at = Utc::now(); + true + } + } else { + false + } + } + + /// Remove an item from the cart + pub fn remove_item(&mut self, food_id: &str) -> bool { + let original_len = self.items.len(); + self.items.retain(|item| item.food_id != food_id); + let removed = self.items.len() != original_len; + if removed { + self.updated_at = Utc::now(); + } + removed + } + + /// Clear all items from the cart + pub fn clear(&mut self) { + self.items.clear(); + self.updated_at = Utc::now(); + } + + /// Get the total number of items in the cart + pub fn total_items(&self) -> u32 { + self.items.iter().map(|item| item.quantity).sum() + } + + /// Get the total price of all items in the cart + pub fn total_price(&self) -> Decimal { + self.items + .iter() + .map(|item| item.unit_price * Decimal::from(item.quantity)) + .sum() + } + + /// Check if the cart is empty + pub fn is_empty(&self) -> bool { + self.items.is_empty() + } + + /// Get a specific item from the cart + pub fn get_item(&self, food_id: &str) -> Option<&CartItem> { + self.items.iter().find(|item| item.food_id == food_id) + } + + /// Check if a specific food item is in the cart + pub fn contains_item(&self, food_id: &str) -> bool { + self.items.iter().any(|item| item.food_id == food_id) + } + + /// Get the quantity of a specific item in the cart + pub fn get_item_quantity(&self, food_id: &str) -> u32 { + self.get_item(food_id) + .map(|item| item.quantity) + .unwrap_or(0) + } +} + +impl CartItem { + /// Create a new cart item + pub fn new(food_id: String, quantity: u32, unit_price: Decimal) -> Self { + Self { + food_id, + quantity, + unit_price, + added_at: Utc::now(), + } + } + + /// Get the total price for this cart item (unit_price * quantity) + pub fn total_price(&self) -> Decimal { + self.unit_price * Decimal::from(self.quantity) + } + + /// Update the quantity of this cart item + pub fn update_quantity(&mut self, new_quantity: u32) { + self.quantity = new_quantity; + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rust_decimal_macros::dec; + + #[test] + fn test_cart_creation() { + let cart = Cart::new("user123".to_string()); + + assert_eq!(cart.user_id, "user123"); + assert!(cart.items.is_empty()); + assert!(cart.is_empty()); + assert_eq!(cart.total_items(), 0); + assert_eq!(cart.total_price(), dec!(0)); + } + + #[test] + fn test_add_item_to_cart() { + let mut cart = Cart::new("user123".to_string()); + + cart.add_item("F001".to_string(), 2, dec!(12.99)); + + assert_eq!(cart.items.len(), 1); + assert_eq!(cart.total_items(), 2); + assert_eq!(cart.total_price(), dec!(25.98)); + assert!(cart.contains_item("F001")); + assert_eq!(cart.get_item_quantity("F001"), 2); + } + + #[test] + fn test_add_existing_item_updates_quantity() { + let mut cart = Cart::new("user123".to_string()); + + cart.add_item("F001".to_string(), 2, dec!(12.99)); + cart.add_item("F001".to_string(), 3, dec!(12.99)); + + assert_eq!(cart.items.len(), 1); + assert_eq!(cart.total_items(), 5); + assert_eq!(cart.get_item_quantity("F001"), 5); + } + + #[test] + fn test_update_item_quantity() { + let mut cart = Cart::new("user123".to_string()); + cart.add_item("F001".to_string(), 2, dec!(12.99)); + + let updated = cart.update_item_quantity("F001", 5); + assert!(updated); + assert_eq!(cart.get_item_quantity("F001"), 5); + + let not_found = cart.update_item_quantity("F999", 1); + assert!(!not_found); + } + + #[test] + fn test_update_quantity_to_zero_removes_item() { + let mut cart = Cart::new("user123".to_string()); + cart.add_item("F001".to_string(), 2, dec!(12.99)); + + let updated = cart.update_item_quantity("F001", 0); + assert!(updated); + assert!(!cart.contains_item("F001")); + assert!(cart.is_empty()); + } + + #[test] + fn test_remove_item() { + let mut cart = Cart::new("user123".to_string()); + cart.add_item("F001".to_string(), 2, dec!(12.99)); + cart.add_item("F002".to_string(), 1, dec!(8.99)); + + let removed = cart.remove_item("F001"); + assert!(removed); + assert!(!cart.contains_item("F001")); + assert_eq!(cart.items.len(), 1); + + let not_found = cart.remove_item("F999"); + assert!(!not_found); + } + + #[test] + fn test_clear_cart() { + let mut cart = Cart::new("user123".to_string()); + cart.add_item("F001".to_string(), 2, dec!(12.99)); + cart.add_item("F002".to_string(), 1, dec!(8.99)); + + cart.clear(); + + assert!(cart.is_empty()); + assert_eq!(cart.total_items(), 0); + assert_eq!(cart.total_price(), dec!(0)); + } + + #[test] + fn test_cart_item_total_price() { + let item = CartItem::new("F001".to_string(), 3, dec!(12.99)); + assert_eq!(item.total_price(), dec!(38.97)); + } + + #[test] + fn test_multiple_items_total_calculation() { + let mut cart = Cart::new("user123".to_string()); + cart.add_item("F001".to_string(), 2, dec!(12.99)); + cart.add_item("F002".to_string(), 1, dec!(8.99)); + cart.add_item("F003".to_string(), 3, dec!(5.50)); + + assert_eq!(cart.total_items(), 6); + assert_eq!(cart.total_price(), dec!(51.47)); // 25.98 + 8.99 + 16.50 + } + + #[test] + fn test_serde_serialization() { + let mut cart = Cart::new("user123".to_string()); + cart.add_item("F001".to_string(), 2, dec!(12.99)); + + let json = serde_json::to_string(&cart).unwrap(); + let deserialized: Cart = serde_json::from_str(&json).unwrap(); + + assert_eq!(cart, deserialized); + } +} + +// CHECKOUT MODELS + +/// Request model for checkout +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CheckoutRequest { + pub payment_method: PaymentMethod, + pub shipping_address: Option, + pub billing_address: Option, +} + +/// Payment method for checkout +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum PaymentMethod { + CreditCard { + card_number: String, + expiry_month: u8, + expiry_year: u16, + cvv: String, + cardholder_name: String, + }, + PayPal { + email: String, + }, + BankTransfer { + account_number: String, + routing_number: String, + }, +} + +/// Shipping address for checkout +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ShippingAddress { + pub name: String, + pub street: String, + pub city: String, + pub state: String, + pub zip_code: String, + pub country: String, +} + +/// Billing address for checkout +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct BillingAddress { + pub name: String, + pub street: String, + pub city: String, + pub state: String, + pub zip_code: String, + pub country: String, +} + +/// Response model for successful checkout +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CheckoutResponse { + pub order_id: String, + pub user_id: String, + pub items: Vec, + pub subtotal: Decimal, + pub tax: Decimal, + pub shipping: Decimal, + pub total_amount: Decimal, + pub payment_method: String, + pub status: OrderStatus, + pub created_at: DateTime, + pub estimated_delivery: Option>, +} + +/// Order item in checkout response +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct OrderItem { + pub food_id: String, + pub food_name: String, + pub quantity: u32, + pub unit_price: Decimal, + pub total_price: Decimal, +} + +/// Order status +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub enum OrderStatus { + #[default] + Pending, + Confirmed, + Processing, + Shipped, + Delivered, + Cancelled, +} + +impl std::fmt::Display for OrderStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + OrderStatus::Pending => write!(f, "pending"), + OrderStatus::Confirmed => write!(f, "confirmed"), + OrderStatus::Processing => write!(f, "processing"), + OrderStatus::Shipped => write!(f, "shipped"), + OrderStatus::Delivered => write!(f, "delivered"), + OrderStatus::Cancelled => write!(f, "cancelled"), + } + } +} diff --git a/src/applications/microservices/petfood-rs/src/models/enums.rs b/src/applications/microservices/petfood-rs/src/models/enums.rs new file mode 100644 index 00000000..b42dc6de --- /dev/null +++ b/src/applications/microservices/petfood-rs/src/models/enums.rs @@ -0,0 +1,147 @@ +use serde::{Deserialize, Serialize}; +use std::fmt; +use std::str::FromStr; + +/// Pet types supported by the system +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum PetType { + Puppy, + Kitten, + Bunny, +} + +impl fmt::Display for PetType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + PetType::Puppy => write!(f, "puppy"), + PetType::Kitten => write!(f, "kitten"), + PetType::Bunny => write!(f, "bunny"), + } + } +} + +impl FromStr for PetType { + type Err = String; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "puppy" => Ok(PetType::Puppy), + "kitten" => Ok(PetType::Kitten), + "bunny" => Ok(PetType::Bunny), + _ => Err(format!("Invalid pet type: {}", s)), + } + } +} + +/// Food types available in the system +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum FoodType { + Dry, + Wet, + Treats, + Supplements, +} + +impl fmt::Display for FoodType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + FoodType::Dry => write!(f, "dry"), + FoodType::Wet => write!(f, "wet"), + FoodType::Treats => write!(f, "treats"), + FoodType::Supplements => write!(f, "supplements"), + } + } +} + +impl FromStr for FoodType { + type Err = String; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "dry" => Ok(FoodType::Dry), + "wet" => Ok(FoodType::Wet), + "treats" => Ok(FoodType::Treats), + "supplements" => Ok(FoodType::Supplements), + _ => Err(format!("Invalid food type: {}", s)), + } + } +} + +/// Availability status for food products +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum AvailabilityStatus { + InStock, + OutOfStock, + Discontinued, + PreOrder, +} + +impl fmt::Display for AvailabilityStatus { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + AvailabilityStatus::InStock => write!(f, "in_stock"), + AvailabilityStatus::OutOfStock => write!(f, "out_of_stock"), + AvailabilityStatus::Discontinued => write!(f, "discontinued"), + AvailabilityStatus::PreOrder => write!(f, "pre_order"), + } + } +} + +impl FromStr for AvailabilityStatus { + type Err = String; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "in_stock" => Ok(AvailabilityStatus::InStock), + "out_of_stock" => Ok(AvailabilityStatus::OutOfStock), + "discontinued" => Ok(AvailabilityStatus::Discontinued), + "pre_order" => Ok(AvailabilityStatus::PreOrder), + _ => Err(format!("Invalid availability status: {}", s)), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_pet_type_string_conversion() { + assert_eq!(PetType::Puppy.to_string(), "puppy"); + assert_eq!(PetType::Kitten.to_string(), "kitten"); + assert_eq!(PetType::Bunny.to_string(), "bunny"); + + assert_eq!("puppy".parse::().unwrap(), PetType::Puppy); + assert_eq!("KITTEN".parse::().unwrap(), PetType::Kitten); + assert_eq!("Bunny".parse::().unwrap(), PetType::Bunny); + + assert!("invalid".parse::().is_err()); + } + + #[test] + fn test_food_type_string_conversion() { + assert_eq!(FoodType::Dry.to_string(), "dry"); + assert_eq!(FoodType::Wet.to_string(), "wet"); + assert_eq!(FoodType::Treats.to_string(), "treats"); + assert_eq!(FoodType::Supplements.to_string(), "supplements"); + + assert_eq!("dry".parse::().unwrap(), FoodType::Dry); + assert_eq!("WET".parse::().unwrap(), FoodType::Wet); + assert_eq!("Treats".parse::().unwrap(), FoodType::Treats); + + assert!("invalid".parse::().is_err()); + } + + #[test] + fn test_serde_serialization() { + let pet_type = PetType::Puppy; + let json = serde_json::to_string(&pet_type).unwrap(); + assert_eq!(json, "\"puppy\""); + + let deserialized: PetType = serde_json::from_str(&json).unwrap(); + assert_eq!(deserialized, PetType::Puppy); + } +} diff --git a/src/applications/microservices/petfood-rs/src/models/errors.rs b/src/applications/microservices/petfood-rs/src/models/errors.rs new file mode 100644 index 00000000..aa766289 --- /dev/null +++ b/src/applications/microservices/petfood-rs/src/models/errors.rs @@ -0,0 +1,181 @@ +use thiserror::Error; + +/// Service-level errors that can occur in business logic +#[derive(Debug, Error)] +pub enum ServiceError { + #[error("Food not found: {id}")] + FoodNotFound { id: String }, + + #[error("Invalid pet type: {pet_type}")] + InvalidPetType { pet_type: String }, + + #[error("Cart not found for user: {user_id}")] + CartNotFound { user_id: String }, + + #[error("Cart item not found: food_id={food_id}, user_id={user_id}")] + CartItemNotFound { food_id: String, user_id: String }, + + #[error("Validation error: {message}")] + ValidationError { message: String }, + + #[error("Repository error: {source}")] + Repository { + #[from] + source: RepositoryError, + }, + + #[error("Configuration error: {message}")] + Configuration { message: String }, + + #[error("External service error: {service}: {message}")] + ExternalService { service: String, message: String }, + + #[error("Insufficient stock: requested={requested}, available={available}")] + InsufficientStock { requested: u32, available: u32 }, + + #[error("Invalid quantity: {quantity}")] + InvalidQuantity { quantity: u32 }, + + #[error("Product unavailable: {food_id}")] + ProductUnavailable { food_id: String }, +} + +/// Repository-level errors for data access operations +#[derive(Debug, Error)] +pub enum RepositoryError { + #[error("Database connection failed")] + ConnectionFailed, + + #[error("Item not found")] + NotFound, + + #[error("Constraint violation: {message}")] + ConstraintViolation { message: String }, + + #[error("Serialization error: {source}")] + Serialization { + #[from] + source: serde_json::Error, + }, + + #[error("AWS SDK error: {message}")] + AwsSdk { message: String }, + + #[error("Invalid query parameters: {message}")] + InvalidQuery { message: String }, + + #[error("Transaction failed: {message}")] + TransactionFailed { message: String }, + + #[error("Timeout occurred during operation")] + Timeout, + + #[error("Rate limit exceeded")] + RateLimitExceeded, +} + +/// Validation errors for input data +#[derive(Debug, Error)] +pub enum ValidationError { + #[error("Required field missing: {field}")] + RequiredField { field: String }, + + #[error("Invalid field value: {field}={value}, reason={reason}")] + InvalidValue { + field: String, + value: String, + reason: String, + }, + + #[error("Field too long: {field}, max_length={max_length}, actual_length={actual_length}")] + TooLong { + field: String, + max_length: usize, + actual_length: usize, + }, + + #[error("Field too short: {field}, min_length={min_length}, actual_length={actual_length}")] + TooShort { + field: String, + min_length: usize, + actual_length: usize, + }, + + #[error("Invalid format: {field}, expected={expected}")] + InvalidFormat { field: String, expected: String }, + + #[error("Value out of range: {field}, min={min}, max={max}, value={value}")] + OutOfRange { + field: String, + min: String, + max: String, + value: String, + }, +} + +impl From for ServiceError { + fn from(err: ValidationError) -> Self { + ServiceError::ValidationError { + message: err.to_string(), + } + } +} + +/// Result type alias for service operations +pub type ServiceResult = Result; + +/// Result type alias for repository operations +pub type RepositoryResult = Result; + +/// Result type alias for validation operations +pub type ValidationResult = Result; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_error_display() { + let error = ServiceError::FoodNotFound { + id: "F001".to_string(), + }; + assert_eq!(error.to_string(), "Food not found: F001"); + + let validation_error = ValidationError::RequiredField { + field: "food_name".to_string(), + }; + assert_eq!( + validation_error.to_string(), + "Required field missing: food_name" + ); + } + + #[test] + fn test_error_conversion() { + let validation_error = ValidationError::InvalidValue { + field: "price".to_string(), + value: "-10".to_string(), + reason: "Price cannot be negative".to_string(), + }; + + let service_error: ServiceError = validation_error.into(); + match service_error { + ServiceError::ValidationError { message } => { + assert!(message.contains("Invalid field value")); + } + _ => panic!("Expected ValidationError conversion"), + } + } + + #[test] + fn test_repository_error_from_serde() { + let json_error = serde_json::from_str::("invalid json"); + assert!(json_error.is_err()); + + let repo_error: RepositoryError = json_error.unwrap_err().into(); + match repo_error { + RepositoryError::Serialization { .. } => {} + _ => panic!("Expected Serialization error"), + } + } +} diff --git a/src/applications/microservices/petfood-rs/src/models/events.rs b/src/applications/microservices/petfood-rs/src/models/events.rs new file mode 100644 index 00000000..a3f4e879 --- /dev/null +++ b/src/applications/microservices/petfood-rs/src/models/events.rs @@ -0,0 +1,448 @@ +use crate::models::{AvailabilityStatus, FoodType, PetType}; +use chrono::{DateTime, Utc}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; + +/// Source of food item creation +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum CreationSource { + /// Created via admin API endpoint + AdminApi, + /// missing images when using food APIs + FoodApi, +} + +impl std::fmt::Display for CreationSource { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + CreationSource::AdminApi => write!(f, "admin_api"), + CreationSource::FoodApi => write!(f, "food_api"), + } + } +} + +/// Event types that can be emitted by the petfood service +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "PascalCase")] +pub enum FoodEventType { + FoodItemCreated, + FoodItemUpdated, + ItemDiscontinued, +} + +impl std::fmt::Display for FoodEventType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + FoodEventType::FoodItemCreated => write!(f, "FoodItemCreated"), + FoodEventType::FoodItemUpdated => write!(f, "FoodItemUpdated"), + FoodEventType::ItemDiscontinued => write!(f, "ItemDiscontinued"), + } + } +} + +/// OpenTelemetry span context data for distributed tracing +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct SpanContextData { + pub trace_id: String, + pub span_id: String, + pub trace_flags: String, +} + +impl Default for SpanContextData { + fn default() -> Self { + Self { + trace_id: "00000000000000000000000000000000".to_string(), + span_id: "0000000000000000".to_string(), + trace_flags: "00".to_string(), + } + } +} + +/// Core event structure for food-related events +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct FoodEvent { + pub event_type: FoodEventType, + pub food_id: String, + pub food_name: Option, + pub pet_type: Option, + pub food_type: Option, + pub description: Option, + pub ingredients: Option>, + pub status: Option, + pub metadata: HashMap, + pub span_context: SpanContextData, + pub timestamp: DateTime, +} + +impl FoodEvent { + /// Create a new FoodItemCreated event + #[allow(clippy::too_many_arguments)] + pub fn food_item_created( + food_id: String, + food_name: String, + pet_type: PetType, + food_type: FoodType, + description: Option, + ingredients: Option>, + creation_source: CreationSource, + span_context: SpanContextData, + ) -> Self { + let mut metadata = HashMap::new(); + metadata.insert("image_required".to_string(), "true".to_string()); + metadata.insert("creation_source".to_string(), creation_source.to_string()); + + // Add additional metadata based on creation source + match creation_source { + CreationSource::AdminApi => { + metadata.insert("is_manual_creation".to_string(), "true".to_string()); + metadata.insert("is_seed_data".to_string(), "false".to_string()); + metadata.insert("requires_validation".to_string(), "true".to_string()); + } + CreationSource::FoodApi => { + metadata.insert("is_manual_creation".to_string(), "false".to_string()); + metadata.insert("is_seed_data".to_string(), "true".to_string()); + metadata.insert("requires_validation".to_string(), "false".to_string()); + } + } + + Self { + event_type: FoodEventType::FoodItemCreated, + food_id, + food_name: Some(food_name), + pet_type: Some(pet_type), + food_type: Some(food_type), + description, + ingredients, + status: None, + metadata, + span_context, + timestamp: Utc::now(), + } + } + + /// Create a new FoodItemUpdated event + #[allow(clippy::too_many_arguments)] + pub fn food_item_updated( + food_id: String, + food_name: Option, + pet_type: Option, + food_type: Option, + description: Option, + ingredients: Option>, + previous_image_path: Option, + span_context: SpanContextData, + ) -> Self { + let mut metadata = HashMap::new(); + metadata.insert("image_required".to_string(), "true".to_string()); + + if let Some(prev_path) = previous_image_path { + metadata.insert("previous_image_path".to_string(), prev_path); + } + + Self { + event_type: FoodEventType::FoodItemUpdated, + food_id, + food_name, + pet_type, + food_type, + description, + ingredients, + status: None, + metadata, + span_context, + timestamp: Utc::now(), + } + } + + /// Create a new ItemDiscontinued event + pub fn item_discontinued( + food_id: String, + status: AvailabilityStatus, + image_path: Option, + cleanup_type: String, + span_context: SpanContextData, + ) -> Self { + let mut metadata = HashMap::new(); + metadata.insert("cleanup_type".to_string(), cleanup_type); + metadata.insert("reason".to_string(), "cleanup_operation".to_string()); + + if let Some(img_path) = image_path { + metadata.insert("image_path".to_string(), img_path); + } + + Self { + event_type: FoodEventType::ItemDiscontinued, + food_id, + food_name: None, + pet_type: None, + food_type: None, + description: None, + ingredients: None, + status: Some(status), + metadata, + span_context, + timestamp: Utc::now(), + } + } +} + +/// EventBridge event payload structure +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EventPayload { + pub source: String, + #[serde(rename = "detail-type")] + pub detail_type: String, + pub detail: EventDetail, + pub resources: Vec, + pub time: DateTime, +} + +/// Event detail structure for EventBridge +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EventDetail { + pub event_type: String, + pub food_id: String, + pub food_name: Option, + pub pet_type: Option, + pub food_type: Option, + pub description: Option, + pub ingredients: Option>, + pub status: Option, + pub metadata: HashMap, + pub span_context: SpanContextData, +} + +impl From for EventPayload { + fn from(event: FoodEvent) -> Self { + let detail = EventDetail { + event_type: event.event_type.to_string(), + food_id: event.food_id.clone(), + food_name: event.food_name.clone(), + pet_type: event.pet_type.clone(), + food_type: event.food_type.clone(), + description: event.description.clone(), + ingredients: event.ingredients.clone(), + status: event.status.clone(), + metadata: event.metadata.clone(), + span_context: event.span_context.clone(), + }; + + Self { + source: "petfood.service".to_string(), + detail_type: event.event_type.to_string(), + detail, + resources: vec![format!("food/{}", event.food_id)], + time: event.timestamp, + } + } +} + +/// Configuration for EventBridge settings +#[derive(Debug, Clone)] +pub struct EventConfig { + pub event_bus_name: String, + pub source_name: String, + pub retry_attempts: u32, + pub timeout_seconds: u64, + pub enable_dead_letter_queue: bool, + pub enabled: bool, +} + +impl Default for EventConfig { + fn default() -> Self { + Self { + event_bus_name: "default".to_string(), + source_name: "petfood.service".to_string(), + retry_attempts: 3, + timeout_seconds: 30, + enable_dead_letter_queue: true, + enabled: true, + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_food_event_creation() { + let span_context = SpanContextData::default(); + + let event = FoodEvent::food_item_created( + "test-id".to_string(), + "Test Food".to_string(), + PetType::Puppy, + FoodType::Dry, + Some("Test description".to_string()), + Some(vec!["beef".to_string(), "rice".to_string()]), + CreationSource::AdminApi, + span_context.clone(), + ); + + assert_eq!(event.event_type, FoodEventType::FoodItemCreated); + assert_eq!(event.food_id, "test-id"); + assert_eq!(event.food_name, Some("Test Food".to_string())); + assert_eq!(event.pet_type, Some(PetType::Puppy)); + assert_eq!(event.food_type, Some(FoodType::Dry)); + assert_eq!(event.span_context, span_context); + assert!(event.metadata.contains_key("image_required")); + assert_eq!( + event.metadata.get("creation_source"), + Some(&"admin_api".to_string()) + ); + assert_eq!( + event.metadata.get("is_manual_creation"), + Some(&"true".to_string()) + ); + assert_eq!( + event.metadata.get("requires_validation"), + Some(&"true".to_string()) + ); + } + + #[test] + fn test_item_discontinued_event() { + let span_context = SpanContextData::default(); + + let event = FoodEvent::item_discontinued( + "test-id".to_string(), + AvailabilityStatus::Discontinued, + Some("images/test.jpg".to_string()), + "soft_delete".to_string(), + span_context.clone(), + ); + + assert_eq!(event.event_type, FoodEventType::ItemDiscontinued); + assert_eq!(event.food_id, "test-id"); + assert_eq!(event.status, Some(AvailabilityStatus::Discontinued)); + assert_eq!( + event.metadata.get("cleanup_type"), + Some(&"soft_delete".to_string()) + ); + assert_eq!( + event.metadata.get("image_path"), + Some(&"images/test.jpg".to_string()) + ); + } + + #[test] + fn test_event_payload_conversion() { + let span_context = SpanContextData::default(); + + let event = FoodEvent::food_item_created( + "test-id".to_string(), + "Test Food".to_string(), + PetType::Puppy, + FoodType::Dry, + None, + None, + CreationSource::AdminApi, + span_context, + ); + + let payload: EventPayload = event.into(); + + assert_eq!(payload.source, "petfood.service"); + assert_eq!(payload.detail_type, "FoodItemCreated"); + assert_eq!(payload.detail.food_id, "test-id"); + assert_eq!(payload.resources, vec!["food/test-id"]); + } + + #[test] + fn test_event_serialization() { + let span_context = SpanContextData::default(); + + let event = FoodEvent::food_item_created( + "test-id".to_string(), + "Test Food".to_string(), + PetType::Puppy, + FoodType::Dry, + None, + None, + CreationSource::AdminApi, + span_context, + ); + + let json = serde_json::to_string(&event).unwrap(); + let deserialized: FoodEvent = serde_json::from_str(&json).unwrap(); + + assert_eq!(event.event_type, deserialized.event_type); + assert_eq!(event.food_id, deserialized.food_id); + } + + #[test] + fn test_span_context_default() { + let span_context = SpanContextData::default(); + + assert_eq!(span_context.trace_id, "00000000000000000000000000000000"); + assert_eq!(span_context.span_id, "0000000000000000"); + assert_eq!(span_context.trace_flags, "00"); + } + + #[test] + fn test_creation_source_metadata() { + let span_context = SpanContextData::default(); + + // Test AdminApi creation source + let admin_event = FoodEvent::food_item_created( + "admin-id".to_string(), + "Admin Food".to_string(), + PetType::Puppy, + FoodType::Dry, + None, + None, + CreationSource::AdminApi, + span_context.clone(), + ); + + assert_eq!( + admin_event.metadata.get("creation_source"), + Some(&"admin_api".to_string()) + ); + assert_eq!( + admin_event.metadata.get("is_manual_creation"), + Some(&"true".to_string()) + ); + assert_eq!( + admin_event.metadata.get("requires_validation"), + Some(&"true".to_string()) + ); + + // Test Seeding creation source + let seed_event = FoodEvent::food_item_created( + "seed-id".to_string(), + "Seed Food".to_string(), + PetType::Puppy, + FoodType::Dry, + None, + None, + CreationSource::FoodApi, + span_context.clone(), + ); + + assert_eq!( + seed_event.metadata.get("creation_source"), + Some(&"food_api".to_string()) + ); + assert_eq!( + seed_event.metadata.get("is_manual_creation"), + Some(&"false".to_string()) + ); + assert_eq!( + seed_event.metadata.get("is_seed_data"), + Some(&"true".to_string()) + ); + assert_eq!( + seed_event.metadata.get("requires_validation"), + Some(&"false".to_string()) + ); + } + + #[test] + fn test_creation_source_display() { + assert_eq!(CreationSource::AdminApi.to_string(), "admin_api"); + assert_eq!(CreationSource::FoodApi.to_string(), "food_api"); + } +} diff --git a/src/applications/microservices/petfood-rs/src/models/food.rs b/src/applications/microservices/petfood-rs/src/models/food.rs new file mode 100644 index 00000000..04eefee6 --- /dev/null +++ b/src/applications/microservices/petfood-rs/src/models/food.rs @@ -0,0 +1,499 @@ +use chrono::{DateTime, Utc}; +use rust_decimal::Decimal; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +use super::{AvailabilityStatus, FoodType, PetType}; + +/// Core food product model +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct Food { + pub id: String, + pub pet_type: PetType, + pub name: String, + pub food_type: FoodType, + pub description: String, + pub price: Decimal, + pub image: Option, + pub nutritional_info: Option, + pub ingredients: Vec, + pub feeding_guidelines: Option, + pub availability_status: AvailabilityStatus, + pub stock_quantity: u32, + pub created_at: DateTime, + pub updated_at: DateTime, + pub is_active: bool, +} + +/// Nutritional information for food products +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct NutritionalInfo { + pub calories_per_serving: Option, + pub protein_percentage: Option, + pub fat_percentage: Option, + pub carbohydrate_percentage: Option, + pub fiber_percentage: Option, + pub moisture_percentage: Option, + pub serving_size: Option, + pub servings_per_container: Option, +} + +/// Request model for creating a new food product +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct CreateFoodRequest { + pub pet_type: PetType, + pub name: String, + pub food_type: FoodType, + pub description: String, + pub price: Decimal, + // Removed image field - will be generated via events using description as prompt + pub nutritional_info: Option, + pub ingredients: Vec, + pub feeding_guidelines: Option, + pub stock_quantity: u32, +} + +/// Request model for updating an existing food product +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +pub struct UpdateFoodRequest { + pub name: Option, + pub description: Option, + pub price: Option, + pub image: Option, + pub nutritional_info: Option, + pub ingredients: Option>, + pub feeding_guidelines: Option, + pub availability_status: Option, + pub stock_quantity: Option, +} + +/// Filters for querying food products +#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)] +pub struct FoodFilters { + pub pet_type: Option, + pub food_type: Option, + pub availability_status: Option, + pub min_price: Option, + pub max_price: Option, + pub search_term: Option, + pub in_stock_only: Option, +} + +/// Response model for food listings +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FoodListResponse { + pub foods: Vec, + pub total_count: usize, + pub page: Option, + pub page_size: Option, +} + +/// Response model for food listings with dynamically generated image URLs +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct FoodListApiResponse { + pub foods: Vec, + pub total_count: usize, + pub page: Option, + pub page_size: Option, +} + +/// Response model for food with dynamically generated image URL +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct FoodResponse { + pub id: String, + pub pet_type: PetType, + pub name: String, + pub food_type: FoodType, + pub description: String, + pub price: Decimal, + pub image: String, // This will contain the full CDN URL + pub nutritional_info: Option, + pub ingredients: Vec, + pub feeding_guidelines: Option, + pub availability_status: AvailabilityStatus, + pub stock_quantity: u32, + pub created_at: DateTime, + pub updated_at: DateTime, + pub is_active: bool, +} + +impl Food { + /// Create a new Food instance with generated ID and timestamps + /// Image will be None initially and generated via events using description as prompt + pub fn new(request: CreateFoodRequest) -> Self { + let now = Utc::now(); + Self { + id: format!( + "F{}", + Uuid::new_v4() + .simple() + .to_string() + .get(0..8) + .unwrap_or("00000000") + ), + pet_type: request.pet_type, + name: request.name, + food_type: request.food_type, + description: request.description, + price: request.price, + image: None, // Will be set later via image generation events + nutritional_info: request.nutritional_info, + ingredients: request.ingredients, + feeding_guidelines: request.feeding_guidelines, + availability_status: if request.stock_quantity > 0 { + AvailabilityStatus::InStock + } else { + AvailabilityStatus::OutOfStock + }, + stock_quantity: request.stock_quantity, + created_at: now, + updated_at: now, + is_active: true, + } + } + + /// Check if the food needs image generation + pub fn needs_image_generation(&self) -> bool { + self.image.is_none() + } + + /// Update the food with new values from UpdateFoodRequest + pub fn update(&mut self, request: UpdateFoodRequest) { + if let Some(name) = request.name { + self.name = name; + } + if let Some(description) = request.description { + self.description = description; + } + if let Some(price) = request.price { + self.price = price; + } + if let Some(image) = request.image { + self.image = Some(image); + } + if let Some(nutritional_info) = request.nutritional_info { + self.nutritional_info = Some(nutritional_info); + } + if let Some(ingredients) = request.ingredients { + self.ingredients = ingredients; + } + if let Some(guidelines) = request.feeding_guidelines { + self.feeding_guidelines = Some(guidelines); + } + if let Some(status) = request.availability_status { + self.availability_status = status; + } + if let Some(stock) = request.stock_quantity { + self.stock_quantity = stock; + // Auto-update availability based on stock + if stock == 0 && self.availability_status == AvailabilityStatus::InStock { + self.availability_status = AvailabilityStatus::OutOfStock; + } else if stock > 0 && self.availability_status == AvailabilityStatus::OutOfStock { + self.availability_status = AvailabilityStatus::InStock; + } + } + self.updated_at = Utc::now(); + } + + /// Soft delete the food product + pub fn soft_delete(&mut self) { + self.is_active = false; + self.availability_status = AvailabilityStatus::Discontinued; + self.updated_at = Utc::now(); + } + + /// Check if the food is available for purchase + pub fn is_available(&self) -> bool { + self.is_active + && self.availability_status == AvailabilityStatus::InStock + && self.stock_quantity > 0 + } + + /// Check if the food matches the given filters + pub fn matches_filters(&self, filters: &FoodFilters) -> bool { + if let Some(pet_type) = &filters.pet_type { + if &self.pet_type != pet_type { + return false; + } + } + + if let Some(food_type) = &filters.food_type { + if &self.food_type != food_type { + return false; + } + } + + if let Some(status) = &filters.availability_status { + if &self.availability_status != status { + return false; + } + } + + if let Some(min_price) = &filters.min_price { + if &self.price < min_price { + return false; + } + } + + if let Some(max_price) = &filters.max_price { + if &self.price > max_price { + return false; + } + } + + if let Some(search_term) = &filters.search_term { + let search_lower = search_term.to_lowercase(); + if !self.name.to_lowercase().contains(&search_lower) + && !self.description.to_lowercase().contains(&search_lower) + && !self + .ingredients + .iter() + .any(|ingredient| ingredient.to_lowercase().contains(&search_lower)) + { + return false; + } + } + + if let Some(true) = filters.in_stock_only { + if !self.is_available() { + return false; + } + } + + true + } + + /// Convert Food to FoodResponse with full image URL + /// Returns a placeholder or generates CDN URL if image exists + pub fn to_response(&self, assets_cdn_url: &str) -> FoodResponse { + let image_url = match &self.image { + Some(image_path) => { + if assets_cdn_url.is_empty() { + image_path.clone() + } else { + // Handle trailing slash in CDN URL to avoid double slashes + let cdn_url = if assets_cdn_url.ends_with('/') { + assets_cdn_url.trim_end_matches('/') + } else { + assets_cdn_url + }; + format!("{}/{}", cdn_url, image_path) + } + } + None => { + // Return an empty image URL when no image is available + "".to_string() + } + }; + + FoodResponse { + id: self.id.clone(), + pet_type: self.pet_type.clone(), + name: self.name.clone(), + food_type: self.food_type.clone(), + description: self.description.clone(), + price: self.price, + image: image_url, + nutritional_info: self.nutritional_info.clone(), + ingredients: self.ingredients.clone(), + feeding_guidelines: self.feeding_guidelines.clone(), + availability_status: self.availability_status.clone(), + stock_quantity: self.stock_quantity, + created_at: self.created_at, + updated_at: self.updated_at, + is_active: self.is_active, + } + } + + /// Set the image path after it has been generated + /// This should be called when the image generation process completes + pub fn set_image(&mut self, image_path: String) { + self.image = Some(image_path); + self.updated_at = Utc::now(); + } +} + +impl FoodResponse { + /// Check if the food is available for purchase + pub fn is_available(&self) -> bool { + self.is_active + && self.availability_status == AvailabilityStatus::InStock + && self.stock_quantity > 0 + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rust_decimal_macros::dec; + + fn create_test_food_request() -> CreateFoodRequest { + CreateFoodRequest { + pet_type: PetType::Puppy, + name: "Test Kibble".to_string(), + food_type: FoodType::Dry, + description: "Nutritious test food".to_string(), + price: dec!(12.99), + // No image field - will be generated via events + nutritional_info: None, + ingredients: vec!["chicken".to_string(), "rice".to_string()], + feeding_guidelines: Some("Feed twice daily".to_string()), + stock_quantity: 10, + } + } + + #[test] + fn test_food_creation() { + let request = create_test_food_request(); + let food = Food::new(request); + + assert!(food.id.starts_with('F')); + assert_eq!(food.pet_type, PetType::Puppy); + assert_eq!(food.name, "Test Kibble"); + assert_eq!(food.availability_status, AvailabilityStatus::InStock); + assert!(food.is_active); + assert!(food.is_available()); + } + + #[test] + fn test_food_update() { + let request = create_test_food_request(); + let mut food = Food::new(request); + let original_updated_at = food.updated_at; + + // Small delay to ensure timestamp changes + std::thread::sleep(std::time::Duration::from_millis(1)); + + let update_request = UpdateFoodRequest { + name: Some("Updated Kibble".to_string()), + price: Some(dec!(15.99)), + stock_quantity: Some(0), + ..Default::default() + }; + + food.update(update_request); + + assert_eq!(food.name, "Updated Kibble"); + assert_eq!(food.price, dec!(15.99)); + assert_eq!(food.stock_quantity, 0); + assert_eq!(food.availability_status, AvailabilityStatus::OutOfStock); + assert!(food.updated_at > original_updated_at); + assert!(!food.is_available()); + } + + #[test] + fn test_food_soft_delete() { + let request = create_test_food_request(); + let mut food = Food::new(request); + + food.soft_delete(); + + assert!(!food.is_active); + assert_eq!(food.availability_status, AvailabilityStatus::Discontinued); + assert!(!food.is_available()); + } + + #[test] + fn test_food_filters() { + let request = create_test_food_request(); + let food = Food::new(request); + + let filters = FoodFilters { + pet_type: Some(PetType::Puppy), + search_term: Some("kibble".to_string()), + in_stock_only: Some(true), + ..Default::default() + }; + + assert!(food.matches_filters(&filters)); + + let filters = FoodFilters { + pet_type: Some(PetType::Kitten), + ..Default::default() + }; + + assert!(!food.matches_filters(&filters)); + } + + #[test] + fn test_serde_serialization() { + let request = create_test_food_request(); + let food = Food::new(request); + + let json = serde_json::to_string(&food).unwrap(); + let deserialized: Food = serde_json::from_str(&json).unwrap(); + + assert_eq!(food, deserialized); + } + + #[test] + fn test_image_url_generation() { + // Create a food without image (new behavior) + let request = create_test_food_request(); + let mut food = Food::new(request); + + // Initially, food should have no image and need generation + assert_eq!(food.image, None); + assert!(food.needs_image_generation()); + + // Test empty image URLs when no image is set + let s3_cdn_url = "https://petfood-assets.s3.amazonaws.com"; + let cloudfront_cdn_url = "https://d1234567890.cloudfront.net/images"; + let empty_cdn_url = ""; + + let s3_response = food.to_response(s3_cdn_url); + let cloudfront_response = food.to_response(cloudfront_cdn_url); + let empty_cdn_response = food.to_response(empty_cdn_url); + + // Verify empty strings are returned when no image exists + assert_eq!(s3_response.image, ""); + assert_eq!(cloudfront_response.image, ""); + assert_eq!(empty_cdn_response.image, ""); + + // Now set an image and test URL generation + food.set_image("petfood/test-kibble.jpg".to_string()); + assert_eq!(food.image, Some("petfood/test-kibble.jpg".to_string())); + assert!(!food.needs_image_generation()); + + // Test conversion to response with actual image + let s3_response_with_image = food.to_response(s3_cdn_url); + let cloudfront_response_with_image = food.to_response(cloudfront_cdn_url); + let empty_cdn_response_with_image = food.to_response(empty_cdn_url); + + // Verify the URLs are correctly generated with actual image + assert_eq!( + s3_response_with_image.image, + "https://petfood-assets.s3.amazonaws.com/petfood/test-kibble.jpg" + ); + assert_eq!( + cloudfront_response_with_image.image, + "https://d1234567890.cloudfront.net/images/petfood/test-kibble.jpg" + ); + assert_eq!( + empty_cdn_response_with_image.image, + "petfood/test-kibble.jpg" + ); + + // Verify other fields are preserved + assert_eq!(s3_response_with_image.name, food.name); + assert_eq!(s3_response_with_image.price, food.price); + assert_eq!(s3_response_with_image.pet_type, food.pet_type); + } + + #[test] + fn test_image_generation_workflow() { + let request = create_test_food_request(); + let mut food = Food::new(request); + + // Initially needs image generation + assert!(food.needs_image_generation()); + assert_eq!(food.image, None); + + // Simulate image generation completion + food.set_image("petfood/generated-image.jpg".to_string()); + + // Should no longer need image generation + assert!(!food.needs_image_generation()); + assert_eq!(food.image, Some("petfood/generated-image.jpg".to_string())); + } +} diff --git a/src/applications/microservices/petfood-rs/src/models/mod.rs b/src/applications/microservices/petfood-rs/src/models/mod.rs new file mode 100644 index 00000000..805c4dd3 --- /dev/null +++ b/src/applications/microservices/petfood-rs/src/models/mod.rs @@ -0,0 +1,14 @@ +// Re-export all model types +pub use self::cart::*; +pub use self::enums::*; +pub use self::errors::*; +pub use self::events::*; +pub use self::food::*; +pub use self::validation::*; + +mod cart; +mod enums; +mod errors; +mod events; +mod food; +mod validation; diff --git a/src/applications/microservices/petfood-rs/src/models/validation.rs b/src/applications/microservices/petfood-rs/src/models/validation.rs new file mode 100644 index 00000000..83b5fbe0 --- /dev/null +++ b/src/applications/microservices/petfood-rs/src/models/validation.rs @@ -0,0 +1,528 @@ +use rust_decimal::Decimal; +use std::collections::HashSet; + +use super::{ + AddCartItemRequest, CreateFoodRequest, UpdateCartItemRequest, UpdateFoodRequest, + ValidationError, ValidationResult, +}; + +/// Trait for validating input models +pub trait Validate { + fn validate(&self) -> ValidationResult<()>; +} + +/// Validation constants +pub const MAX_FOOD_NAME_LENGTH: usize = 200; +pub const MIN_FOOD_NAME_LENGTH: usize = 1; +pub const MAX_DESCRIPTION_LENGTH: usize = 1000; +pub const MIN_DESCRIPTION_LENGTH: usize = 10; +pub const MAX_INGREDIENT_LENGTH: usize = 100; +pub const MAX_INGREDIENTS_COUNT: usize = 50; +pub const MAX_FEEDING_GUIDELINES_LENGTH: usize = 500; +pub const MAX_IMAGE_URL_LENGTH: usize = 500; +pub const MIN_PRICE: Decimal = Decimal::from_parts(1, 0, 0, false, 2); // 0.01 +pub const MAX_PRICE: Decimal = Decimal::from_parts(999999, 0, 0, false, 2); // 9999.99 +pub const MAX_STOCK_QUANTITY: u32 = 999999; +pub const MAX_CART_QUANTITY: u32 = 1000; +pub const MIN_CART_QUANTITY: u32 = 1; + +impl Validate for CreateFoodRequest { + fn validate(&self) -> ValidationResult<()> { + validate_food_name(&self.name)?; + validate_food_description(&self.description)?; + validate_food_price(&self.price)?; + // Image validation removed - images are generated via events + validate_ingredients(&self.ingredients)?; + validate_feeding_guidelines(&self.feeding_guidelines)?; + validate_stock_quantity(self.stock_quantity)?; + Ok(()) + } +} + +impl Validate for UpdateFoodRequest { + fn validate(&self) -> ValidationResult<()> { + if let Some(name) = &self.name { + validate_food_name(name)?; + } + if let Some(description) = &self.description { + validate_food_description(description)?; + } + if let Some(price) = &self.price { + validate_food_price(price)?; + } + if let Some(image) = &self.image { + validate_food_image(image)?; + } + if let Some(ingredients) = &self.ingredients { + validate_ingredients(ingredients)?; + } + validate_feeding_guidelines(&self.feeding_guidelines)?; + if let Some(stock) = self.stock_quantity { + validate_stock_quantity(stock)?; + } + Ok(()) + } +} + +impl Validate for AddCartItemRequest { + fn validate(&self) -> ValidationResult<()> { + validate_food_id(&self.food_id)?; + validate_cart_quantity(self.quantity)?; + Ok(()) + } +} + +impl Validate for UpdateCartItemRequest { + fn validate(&self) -> ValidationResult<()> { + validate_cart_quantity(self.quantity)?; + Ok(()) + } +} + +/// Validate food name +pub fn validate_food_name(name: &str) -> ValidationResult<()> { + let trimmed = name.trim(); + + if trimmed.is_empty() { + return Err(ValidationError::RequiredField { + field: "food_name".to_string(), + }); + } + + if trimmed.len() < MIN_FOOD_NAME_LENGTH { + return Err(ValidationError::TooShort { + field: "food_name".to_string(), + min_length: MIN_FOOD_NAME_LENGTH, + actual_length: trimmed.len(), + }); + } + + if trimmed.len() > MAX_FOOD_NAME_LENGTH { + return Err(ValidationError::TooLong { + field: "food_name".to_string(), + max_length: MAX_FOOD_NAME_LENGTH, + actual_length: trimmed.len(), + }); + } + + // Check for invalid characters (basic validation) + if trimmed + .chars() + .any(|c| c.is_control() && c != '\n' && c != '\r' && c != '\t') + { + return Err(ValidationError::InvalidValue { + field: "food_name".to_string(), + value: name.to_string(), + reason: "Contains invalid control characters".to_string(), + }); + } + + Ok(()) +} + +/// Validate food description +pub fn validate_food_description(description: &str) -> ValidationResult<()> { + let trimmed = description.trim(); + + if trimmed.is_empty() { + return Err(ValidationError::RequiredField { + field: "food_description".to_string(), + }); + } + + if trimmed.len() < MIN_DESCRIPTION_LENGTH { + return Err(ValidationError::TooShort { + field: "food_description".to_string(), + min_length: MIN_DESCRIPTION_LENGTH, + actual_length: trimmed.len(), + }); + } + + if trimmed.len() > MAX_DESCRIPTION_LENGTH { + return Err(ValidationError::TooLong { + field: "food_description".to_string(), + max_length: MAX_DESCRIPTION_LENGTH, + actual_length: trimmed.len(), + }); + } + + Ok(()) +} + +/// Validate food price +pub fn validate_food_price(price: &Decimal) -> ValidationResult<()> { + if *price < MIN_PRICE { + return Err(ValidationError::OutOfRange { + field: "food_price".to_string(), + min: MIN_PRICE.to_string(), + max: MAX_PRICE.to_string(), + value: price.to_string(), + }); + } + + if *price > MAX_PRICE { + return Err(ValidationError::OutOfRange { + field: "food_price".to_string(), + min: MIN_PRICE.to_string(), + max: MAX_PRICE.to_string(), + value: price.to_string(), + }); + } + + // Check for reasonable decimal places (max 2) + if price.scale() > 2 { + return Err(ValidationError::InvalidValue { + field: "food_price".to_string(), + value: price.to_string(), + reason: "Price cannot have more than 2 decimal places".to_string(), + }); + } + + Ok(()) +} + +/// Validate food image URL +pub fn validate_food_image(image: &str) -> ValidationResult<()> { + let trimmed = image.trim(); + + if trimmed.is_empty() { + return Err(ValidationError::RequiredField { + field: "food_image".to_string(), + }); + } + + if trimmed.len() > MAX_IMAGE_URL_LENGTH { + return Err(ValidationError::TooLong { + field: "food_image".to_string(), + max_length: MAX_IMAGE_URL_LENGTH, + actual_length: trimmed.len(), + }); + } + + // Basic URL format validation + if !trimmed.ends_with(".jpg") + && !trimmed.ends_with(".jpeg") + && !trimmed.ends_with(".png") + && !trimmed.ends_with(".webp") + { + return Err(ValidationError::InvalidFormat { + field: "food_image".to_string(), + expected: "Valid image file extension (.jpg, .jpeg, .png, .webp)".to_string(), + }); + } + + Ok(()) +} + +/// Validate ingredients list +pub fn validate_ingredients(ingredients: &[String]) -> ValidationResult<()> { + if ingredients.is_empty() { + return Err(ValidationError::RequiredField { + field: "ingredients".to_string(), + }); + } + + if ingredients.len() > MAX_INGREDIENTS_COUNT { + return Err(ValidationError::InvalidValue { + field: "ingredients".to_string(), + value: ingredients.len().to_string(), + reason: format!( + "Too many ingredients, maximum allowed: {}", + MAX_INGREDIENTS_COUNT + ), + }); + } + + let mut seen_ingredients = HashSet::new(); + + for (index, ingredient) in ingredients.iter().enumerate() { + let trimmed = ingredient.trim(); + + if trimmed.is_empty() { + return Err(ValidationError::InvalidValue { + field: format!("ingredients[{}]", index), + value: ingredient.clone(), + reason: "Ingredient cannot be empty".to_string(), + }); + } + + if trimmed.len() > MAX_INGREDIENT_LENGTH { + return Err(ValidationError::TooLong { + field: format!("ingredients[{}]", index), + max_length: MAX_INGREDIENT_LENGTH, + actual_length: trimmed.len(), + }); + } + + // Check for duplicates (case-insensitive) + let ingredient_lower = trimmed.to_lowercase(); + if seen_ingredients.contains(&ingredient_lower) { + return Err(ValidationError::InvalidValue { + field: "ingredients".to_string(), + value: ingredient.clone(), + reason: "Duplicate ingredient found".to_string(), + }); + } + seen_ingredients.insert(ingredient_lower); + } + + Ok(()) +} + +/// Validate feeding guidelines +pub fn validate_feeding_guidelines(guidelines: &Option) -> ValidationResult<()> { + if let Some(guidelines) = guidelines { + let trimmed = guidelines.trim(); + + if !trimmed.is_empty() && trimmed.len() > MAX_FEEDING_GUIDELINES_LENGTH { + return Err(ValidationError::TooLong { + field: "feeding_guidelines".to_string(), + max_length: MAX_FEEDING_GUIDELINES_LENGTH, + actual_length: trimmed.len(), + }); + } + } + + Ok(()) +} + +/// Validate stock quantity +pub fn validate_stock_quantity(quantity: u32) -> ValidationResult<()> { + if quantity > MAX_STOCK_QUANTITY { + return Err(ValidationError::OutOfRange { + field: "stock_quantity".to_string(), + min: "0".to_string(), + max: MAX_STOCK_QUANTITY.to_string(), + value: quantity.to_string(), + }); + } + + Ok(()) +} + +/// Validate food ID format +pub fn validate_food_id(food_id: &str) -> ValidationResult<()> { + let trimmed = food_id.trim(); + + if trimmed.is_empty() { + return Err(ValidationError::RequiredField { + field: "food_id".to_string(), + }); + } + + // Basic format validation - should start with 'F' followed by alphanumeric characters + if !trimmed.starts_with('F') || trimmed.len() < 2 { + return Err(ValidationError::InvalidFormat { + field: "food_id".to_string(), + expected: "Format: F followed by alphanumeric characters (e.g., F001, Fabc123)" + .to_string(), + }); + } + + // Check that characters after 'F' are alphanumeric + if !trimmed[1..].chars().all(|c| c.is_alphanumeric()) { + return Err(ValidationError::InvalidFormat { + field: "food_id".to_string(), + expected: "Food ID must contain only alphanumeric characters after 'F'".to_string(), + }); + } + + Ok(()) +} + +/// Validate cart item quantity +pub fn validate_cart_quantity(quantity: u32) -> ValidationResult<()> { + if quantity < MIN_CART_QUANTITY { + return Err(ValidationError::OutOfRange { + field: "quantity".to_string(), + min: MIN_CART_QUANTITY.to_string(), + max: MAX_CART_QUANTITY.to_string(), + value: quantity.to_string(), + }); + } + + if quantity > MAX_CART_QUANTITY { + return Err(ValidationError::OutOfRange { + field: "quantity".to_string(), + min: MIN_CART_QUANTITY.to_string(), + max: MAX_CART_QUANTITY.to_string(), + value: quantity.to_string(), + }); + } + + Ok(()) +} + +/// Validate user ID format +pub fn validate_user_id(user_id: &str) -> ValidationResult<()> { + let trimmed = user_id.trim(); + + if trimmed.is_empty() { + return Err(ValidationError::RequiredField { + field: "user_id".to_string(), + }); + } + + // Basic validation - should be alphanumeric with possible hyphens and underscores + if !trimmed + .chars() + .all(|c| c.is_alphanumeric() || c == '-' || c == '_') + { + return Err(ValidationError::InvalidFormat { + field: "user_id".to_string(), + expected: "User ID must contain only alphanumeric characters, hyphens, and underscores" + .to_string(), + }); + } + + if trimmed.len() > 100 { + return Err(ValidationError::TooLong { + field: "user_id".to_string(), + max_length: 100, + actual_length: trimmed.len(), + }); + } + + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::models::{FoodType, PetType}; + use rust_decimal_macros::dec; + + #[test] + fn test_validate_food_name() { + // Valid names + assert!(validate_food_name("Chicken Kibble").is_ok()); + assert!(validate_food_name("Premium Dog Food").is_ok()); + + // Invalid names + assert!(validate_food_name("").is_err()); + assert!(validate_food_name(" ").is_err()); + assert!(validate_food_name(&"a".repeat(MAX_FOOD_NAME_LENGTH + 1)).is_err()); + + // Control characters + assert!(validate_food_name("Test\x00Food").is_err()); + } + + #[test] + fn test_validate_food_description() { + // Valid descriptions + assert!(validate_food_description("A nutritious blend of chicken and rice").is_ok()); + + // Invalid descriptions + assert!(validate_food_description("").is_err()); + assert!(validate_food_description("Short").is_err()); // Too short + assert!(validate_food_description(&"a".repeat(MAX_DESCRIPTION_LENGTH + 1)).is_err()); + } + + #[test] + fn test_validate_food_price() { + // Valid prices + assert!(validate_food_price(&dec!(12.99)).is_ok()); + assert!(validate_food_price(&dec!(0.01)).is_ok()); + assert!(validate_food_price(&dec!(999.99)).is_ok()); + + // Invalid prices + assert!(validate_food_price(&dec!(0.00)).is_err()); // Zero not allowed + assert!(validate_food_price(&dec!(-1.00)).is_err()); // Negative + assert!(validate_food_price(&dec!(10000.00)).is_err()); // Too high + } + + #[test] + fn test_validate_food_image() { + // Valid images + assert!(validate_food_image("food.jpg").is_ok()); + assert!(validate_food_image("image.png").is_ok()); + assert!(validate_food_image("photo.jpeg").is_ok()); + assert!(validate_food_image("pic.webp").is_ok()); + + // Invalid images + assert!(validate_food_image("").is_err()); + assert!(validate_food_image("file.txt").is_err()); // Wrong extension + assert!(validate_food_image("noextension").is_err()); + } + + #[test] + fn test_validate_ingredients() { + // Valid ingredients + assert!(validate_ingredients(&["chicken".to_string(), "rice".to_string()]).is_ok()); + + // Invalid ingredients + assert!(validate_ingredients(&[]).is_err()); // Empty + assert!(validate_ingredients(&["".to_string()]).is_err()); // Empty ingredient + assert!(validate_ingredients(&["chicken".to_string(), "chicken".to_string()]).is_err()); // Duplicate + assert!(validate_ingredients(&["Chicken".to_string(), "chicken".to_string()]).is_err()); + // Case-insensitive duplicate + } + + #[test] + fn test_validate_food_id() { + // Valid IDs + assert!(validate_food_id("F001").is_ok()); + assert!(validate_food_id("Fabc123").is_ok()); + + // Invalid IDs + assert!(validate_food_id("").is_err()); + assert!(validate_food_id("001").is_err()); // Doesn't start with F + assert!(validate_food_id("F").is_err()); // Too short + assert!(validate_food_id("F-001").is_err()); // Invalid character + } + + #[test] + fn test_validate_cart_quantity() { + // Valid quantities + assert!(validate_cart_quantity(1).is_ok()); + assert!(validate_cart_quantity(50).is_ok()); + assert!(validate_cart_quantity(500).is_ok()); + assert!(validate_cart_quantity(MAX_CART_QUANTITY).is_ok()); + + // Invalid quantities + assert!(validate_cart_quantity(0).is_err()); // Too low + assert!(validate_cart_quantity(MAX_CART_QUANTITY + 1).is_err()); // Too high + } + + #[test] + fn test_create_food_request_validation() { + let valid_request = CreateFoodRequest { + pet_type: PetType::Puppy, + name: "Test Food".to_string(), + food_type: FoodType::Dry, + description: "A nutritious test food for puppies".to_string(), + price: dec!(12.99), + // No image field - will be generated via events + nutritional_info: None, + ingredients: vec!["chicken".to_string(), "rice".to_string()], + feeding_guidelines: Some("Feed twice daily".to_string()), + stock_quantity: 10, + }; + + assert!(valid_request.validate().is_ok()); + + let invalid_request = CreateFoodRequest { + name: "".to_string(), // Invalid empty name + ..valid_request + }; + + assert!(invalid_request.validate().is_err()); + } + + #[test] + fn test_add_cart_item_request_validation() { + let valid_request = AddCartItemRequest { + food_id: "F001".to_string(), + quantity: 2, + }; + + assert!(valid_request.validate().is_ok()); + + let invalid_request = AddCartItemRequest { + food_id: "invalid".to_string(), // Invalid food ID + quantity: 2, + }; + + assert!(invalid_request.validate().is_err()); + } +} diff --git a/src/applications/microservices/petfood-rs/src/observability/metrics.rs b/src/applications/microservices/petfood-rs/src/observability/metrics.rs new file mode 100644 index 00000000..8a600436 --- /dev/null +++ b/src/applications/microservices/petfood-rs/src/observability/metrics.rs @@ -0,0 +1,367 @@ +use prometheus::{ + CounterVec, Encoder, Gauge, GaugeVec, HistogramOpts, HistogramVec, Opts, Registry, TextEncoder, +}; +use thiserror::Error; +use tracing::{error, info}; + +#[derive(Debug, Error)] +pub enum MetricsError { + #[error("Failed to register metric: {0}")] + Registration(#[from] prometheus::Error), + #[error("Failed to encode metrics: {0}")] + Encoding(String), + #[error("Metric not found: {0}")] + NotFound(String), +} + +/// Comprehensive metrics collection for the petfood service +#[derive(Clone)] +pub struct Metrics { + registry: Registry, + + // HTTP metrics + pub http_requests_total: CounterVec, + pub http_request_duration_seconds: HistogramVec, + pub http_requests_in_flight: GaugeVec, + + // Database metrics + pub database_operations_total: CounterVec, + pub database_operation_duration_seconds: HistogramVec, + pub database_connections_active: Gauge, + + // Business logic metrics + pub food_operations_total: CounterVec, + pub cart_operations_total: CounterVec, + pub recommendation_requests_total: CounterVec, + + // System metrics + pub memory_usage_bytes: Gauge, + pub cpu_usage_percent: Gauge, + + // Error simulation metrics + pub error_simulation_triggers_total: CounterVec, +} + +impl Metrics { + /// Create a new metrics instance with all required metrics registered + pub fn new() -> Result { + let registry = Registry::new(); + + info!("Initializing Prometheus metrics"); + + // HTTP metrics + let http_requests_total = CounterVec::new( + Opts::new( + "http_requests_total", + "Total number of HTTP requests processed", + ), + &["method", "endpoint", "status_code"], + )?; + + let http_request_duration_seconds = HistogramVec::new( + HistogramOpts::new( + "http_request_duration_seconds", + "HTTP request duration in seconds", + ) + .buckets(vec![ + 0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, 10.0, + ]), + &["method", "endpoint"], + )?; + + let http_requests_in_flight = GaugeVec::new( + Opts::new( + "http_requests_in_flight", + "Number of HTTP requests currently being processed", + ), + &["method", "endpoint"], + )?; + + // Database metrics + let database_operations_total = CounterVec::new( + Opts::new( + "database_operations_total", + "Total number of database operations", + ), + &["operation", "table", "status"], + )?; + + let database_operation_duration_seconds = HistogramVec::new( + HistogramOpts::new( + "database_operation_duration_seconds", + "Database operation duration in seconds", + ) + .buckets(vec![ + 0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0, + ]), + &["operation", "table"], + )?; + + let database_connections_active = Gauge::new( + "database_connections_active", + "Number of active database connections", + )?; + + // Business logic metrics + let food_operations_total = CounterVec::new( + Opts::new( + "food_operations_total", + "Total number of food-related operations", + ), + &["operation", "pet_type", "food_type", "status"], + )?; + + let cart_operations_total = CounterVec::new( + Opts::new("cart_operations_total", "Total number of cart operations"), + &["operation", "status"], + )?; + + let recommendation_requests_total = CounterVec::new( + Opts::new( + "recommendation_requests_total", + "Total number of recommendation requests", + ), + &["pet_type", "status"], + )?; + + // System metrics + let memory_usage_bytes = Gauge::new("memory_usage_bytes", "Current memory usage in bytes")?; + + let cpu_usage_percent = Gauge::new("cpu_usage_percent", "Current CPU usage percentage")?; + + // Error simulation metrics + let error_simulation_triggers_total = CounterVec::new( + Opts::new( + "error_simulation_triggers_total", + "Total number of error simulation triggers", + ), + &["scenario", "trigger_type"], + )?; + + // Register all metrics + registry.register(Box::new(http_requests_total.clone()))?; + registry.register(Box::new(http_request_duration_seconds.clone()))?; + registry.register(Box::new(http_requests_in_flight.clone()))?; + registry.register(Box::new(database_operations_total.clone()))?; + registry.register(Box::new(database_operation_duration_seconds.clone()))?; + registry.register(Box::new(database_connections_active.clone()))?; + registry.register(Box::new(food_operations_total.clone()))?; + registry.register(Box::new(cart_operations_total.clone()))?; + registry.register(Box::new(recommendation_requests_total.clone()))?; + registry.register(Box::new(memory_usage_bytes.clone()))?; + registry.register(Box::new(cpu_usage_percent.clone()))?; + registry.register(Box::new(error_simulation_triggers_total.clone()))?; + + info!("Prometheus metrics initialized successfully"); + + Ok(Metrics { + registry, + http_requests_total, + http_request_duration_seconds, + http_requests_in_flight, + database_operations_total, + database_operation_duration_seconds, + database_connections_active, + food_operations_total, + cart_operations_total, + recommendation_requests_total, + memory_usage_bytes, + cpu_usage_percent, + error_simulation_triggers_total, + }) + } + + /// Get the metrics registry for exposing metrics endpoint + pub fn registry(&self) -> &Registry { + &self.registry + } + + /// Encode all metrics in Prometheus text format + pub fn encode(&self) -> Result { + let encoder = TextEncoder::new(); + let metric_families = self.registry.gather(); + + let mut buffer = Vec::new(); + encoder + .encode(&metric_families, &mut buffer) + .map_err(|e| MetricsError::Encoding(e.to_string()))?; + + String::from_utf8(buffer).map_err(|e| MetricsError::Encoding(e.to_string())) + } + + /// Record HTTP request metrics + pub fn record_http_request( + &self, + method: &str, + endpoint: &str, + status_code: u16, + duration_seconds: f64, + ) { + let status_str = status_code.to_string(); + + self.http_requests_total + .with_label_values(&[method, endpoint, &status_str]) + .inc(); + + self.http_request_duration_seconds + .with_label_values(&[method, endpoint]) + .observe(duration_seconds); + } + + /// Record database operation metrics + pub fn record_database_operation( + &self, + operation: &str, + table: &str, + success: bool, + duration_seconds: f64, + ) { + let status = if success { "success" } else { "error" }; + + self.database_operations_total + .with_label_values(&[operation, table, status]) + .inc(); + + self.database_operation_duration_seconds + .with_label_values(&[operation, table]) + .observe(duration_seconds); + } + + /// Record food operation metrics + pub fn record_food_operation( + &self, + operation: &str, + pet_type: Option<&str>, + food_type: Option<&str>, + success: bool, + ) { + let pet_type_str = pet_type.unwrap_or("unknown"); + let food_type_str = food_type.unwrap_or("unknown"); + let status = if success { "success" } else { "error" }; + + self.food_operations_total + .with_label_values(&[operation, pet_type_str, food_type_str, status]) + .inc(); + } + + /// Record cart operation metrics + pub fn record_cart_operation(&self, operation: &str, success: bool) { + let status = if success { "success" } else { "error" }; + + self.cart_operations_total + .with_label_values(&[operation, status]) + .inc(); + } + + /// Record recommendation request metrics + pub fn record_recommendation_request(&self, pet_type: &str, success: bool) { + let status = if success { "success" } else { "error" }; + + self.recommendation_requests_total + .with_label_values(&[pet_type, status]) + .inc(); + } + + /// Record error simulation trigger + pub fn record_error_simulation(&self, scenario: &str, trigger_type: &str) { + self.error_simulation_triggers_total + .with_label_values(&[scenario, trigger_type]) + .inc(); + } + + /// Update system metrics (memory and CPU usage) + pub fn update_system_metrics(&self, memory_bytes: f64, cpu_percent: f64) { + self.memory_usage_bytes.set(memory_bytes); + self.cpu_usage_percent.set(cpu_percent); + } + + /// Increment in-flight requests + pub fn increment_in_flight(&self, method: &str, endpoint: &str) { + self.http_requests_in_flight + .with_label_values(&[method, endpoint]) + .inc(); + } + + /// Decrement in-flight requests + pub fn decrement_in_flight(&self, method: &str, endpoint: &str) { + self.http_requests_in_flight + .with_label_values(&[method, endpoint]) + .dec(); + } + + /// Set active database connections + pub fn set_active_connections(&self, count: f64) { + self.database_connections_active.set(count); + } +} + +impl Default for Metrics { + fn default() -> Self { + Self::new().expect("Failed to create default metrics") + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_metrics_creation() { + let metrics = Metrics::new(); + assert!(metrics.is_ok()); + } + + #[test] + fn test_http_request_recording() { + let metrics = Metrics::new().unwrap(); + + metrics.record_http_request("GET", "/api/foods", 200, 0.123); + metrics.record_http_request("POST", "/api/foods", 201, 0.456); + + // Verify metrics can be encoded + let encoded = metrics.encode(); + assert!(encoded.is_ok()); + + let metrics_text = encoded.unwrap(); + assert!(metrics_text.contains("http_requests_total")); + assert!(metrics_text.contains("http_request_duration_seconds")); + } + + #[test] + fn test_database_operation_recording() { + let metrics = Metrics::new().unwrap(); + + metrics.record_database_operation("get_item", "PetFoods", true, 0.050); + metrics.record_database_operation("put_item", "PetFoodCarts", false, 0.100); + + let encoded = metrics.encode().unwrap(); + assert!(encoded.contains("database_operations_total")); + assert!(encoded.contains("database_operation_duration_seconds")); + } + + #[test] + fn test_business_metrics_recording() { + let metrics = Metrics::new().unwrap(); + + metrics.record_food_operation("search", Some("puppy"), Some("dry"), true); + metrics.record_cart_operation("add_item", true); + metrics.record_recommendation_request("kitten", true); + + let encoded = metrics.encode().unwrap(); + assert!(encoded.contains("food_operations_total")); + assert!(encoded.contains("cart_operations_total")); + assert!(encoded.contains("recommendation_requests_total")); + } + + #[test] + fn test_in_flight_requests() { + let metrics = Metrics::new().unwrap(); + + metrics.increment_in_flight("GET", "/api/foods"); + metrics.increment_in_flight("GET", "/api/foods"); + metrics.decrement_in_flight("GET", "/api/foods"); + + let encoded = metrics.encode().unwrap(); + assert!(encoded.contains("http_requests_in_flight")); + } +} diff --git a/src/applications/microservices/petfood-rs/src/observability/middleware.rs b/src/applications/microservices/petfood-rs/src/observability/middleware.rs new file mode 100644 index 00000000..2a7bcf90 --- /dev/null +++ b/src/applications/microservices/petfood-rs/src/observability/middleware.rs @@ -0,0 +1,532 @@ +use axum::{ + extract::{MatchedPath, Request}, + middleware::Next, + response::Response, +}; +use opentelemetry::{trace::TraceContextExt, KeyValue}; +use std::{sync::Arc, time::Instant}; +use tracing::{error, info, instrument, Instrument}; +use tracing_opentelemetry::OpenTelemetrySpanExt; + +use super::Metrics; + +/// Middleware for automatic request tracing and metrics collection +pub async fn observability_middleware( + metrics: Arc, + request: Request, + next: Next, +) -> Response { + let start_time = Instant::now(); + let method = request.method().to_string(); + let uri = request.uri().to_string(); + + // Extract User-Agent header + let user_agent = request + .headers() + .get("user-agent") + .and_then(|value| value.to_str().ok()) + .unwrap_or("unknown") + .to_string(); + + // Extract client IP from various headers (X-Forwarded-For, X-Real-IP, or connection info) + let client_ip = request + .headers() + .get("x-forwarded-for") + .and_then(|value| value.to_str().ok()) + .and_then(|value| value.split(',').next()) // Take first IP if multiple + .or_else(|| { + request + .headers() + .get("x-real-ip") + .and_then(|value| value.to_str().ok()) + }) + .unwrap_or("unknown") + .trim() + .to_string(); + + // Try to get the matched path for better endpoint grouping + let endpoint = request + .extensions() + .get::() + .map(|matched_path| matched_path.as_str().to_string()) + .unwrap_or_else(|| uri.clone()); + + // Create a span with the actual endpoint name instead of generic middleware name + let span_name = format!("{} {}", method, endpoint); + + // Create a span with the endpoint-specific name + // Note: We don't pre-declare status code fields to avoid setting them to 0 + let span = tracing::info_span!( + target: "petfood_rs::http", + "{}", span_name, + otel.name = %span_name, + otel.kind = "server", + http.method = %method, + http.route = %endpoint, + http.url = %uri, + http.user_agent = %user_agent, + http.client_ip = %client_ip, + client.address = %client_ip, + ); + + // Execute the rest of the middleware within this span + async { + // Increment in-flight requests + metrics.increment_in_flight(&method, &endpoint); + + // Get trace ID from current span context (after entering the span) + let trace_id = tracing::Span::current() + .context() + .span() + .span_context() + .trace_id() + .to_string(); + + info!(trace_id = %trace_id, method = %method, path = %endpoint, user_agent = %user_agent, client_ip = %client_ip, "Processing request"); + + // Process the request + let response = next.run(request).await; + + // Calculate duration + let duration = start_time.elapsed(); + let duration_seconds = duration.as_secs_f64(); + let duration_ms = duration.as_millis(); + + // Get status code + let status_code = response.status().as_u16(); + + // Record span attributes using OpenTelemetry semantic conventions + // These are the exact attribute names that X-Ray expects + let current_span = tracing::Span::current(); + current_span.record("http.status_code", status_code); + current_span.record("http.response.status_code", status_code); + current_span.record("http.response_time_ms", duration_ms); + current_span.record("response.status", status_code); + + // Set OpenTelemetry span attributes and status directly + let span_context = current_span.context(); + let otel_span = span_context.span(); + + // Set HTTP response status using OpenTelemetry attributes + // Use multiple attribute names for maximum X-Ray compatibility + otel_span.set_attribute(KeyValue::new("http.status_code", status_code as i64)); + otel_span.set_attribute(KeyValue::new("http.response.status_code", status_code as i64)); + otel_span.set_attribute(KeyValue::new("http.response_code", status_code as i64)); + otel_span.set_attribute(KeyValue::new("response_code", status_code as i64)); + + // Set span status for OpenTelemetry compatibility + // Note: X-Ray determines error/fault flags based on HTTP status code, not span status + // - X-Ray error = true: HTTP 4xx (automatic based on status code) + // - X-Ray fault = true: HTTP 5xx (automatic based on status code) + if status_code >= 500 { + otel_span.set_status(opentelemetry::trace::Status::error(format!("HTTP {}", status_code))); + } else { + otel_span.set_status(opentelemetry::trace::Status::Ok); + } + + // tracing::Span::current().record("http.response.content_length", response.body().size_hint().exact()); //TODO get content length + + // Record metrics + metrics.record_http_request(&method, &endpoint, status_code, duration_seconds); + + // Decrement in-flight requests + metrics.decrement_in_flight(&method, &endpoint); + + // Log request completion with trace ID + if status_code >= 400 { + error!( + trace_id = %trace_id, + method = %method, + path = %endpoint, + status_code = status_code, + duration_ms = duration_ms, + user_agent = %user_agent, + client_ip = %client_ip, + "Request completed with error" + ); + } else { + info!( + trace_id = %trace_id, + method = %method, + path = %endpoint, + status_code = status_code, + duration_ms = duration_ms, + user_agent = %user_agent, + client_ip = %client_ip, + "Request completed successfully" + ); + } + + response + } + .instrument(span) + .await +} + +/// Middleware specifically for database operation tracing +pub struct DatabaseTracingMiddleware { + metrics: Arc, +} + +impl DatabaseTracingMiddleware { + pub fn new(metrics: Arc) -> Self { + Self { metrics } + } + + /// Trace a database operation with automatic metrics recording + #[instrument(skip_all, fields( + operation = %operation, + table = %table, + ))] + pub async fn trace_operation( + &self, + operation: &str, + table: &str, + future: F, + ) -> Result + where + F: std::future::Future>, + E: std::fmt::Display, + { + let start_time = Instant::now(); + + info!("Starting database operation"); + + match future.await { + Ok(result) => { + let duration_seconds = start_time.elapsed().as_secs_f64(); + self.metrics + .record_database_operation(operation, table, true, duration_seconds); + + info!( + duration_ms = start_time.elapsed().as_millis(), + "Database operation completed successfully" + ); + + Ok(result) + } + Err(error) => { + let duration_seconds = start_time.elapsed().as_secs_f64(); + self.metrics + .record_database_operation(operation, table, false, duration_seconds); + + error!( + error = %error, + duration_ms = start_time.elapsed().as_millis(), + "Database operation failed" + ); + + Err(error) + } + } + } +} + +/// Middleware for business operation tracing +pub struct BusinessTracingMiddleware { + metrics: Arc, +} + +impl BusinessTracingMiddleware { + pub fn new(metrics: Arc) -> Self { + Self { metrics } + } + + /// Trace a food operation + #[instrument(skip_all, fields( + operation = %operation, + pet_type = pet_type, + food_type = food_type, + ))] + pub async fn trace_food_operation( + &self, + operation: &str, + pet_type: Option<&str>, + food_type: Option<&str>, + future: F, + ) -> Result + where + F: std::future::Future>, + E: std::fmt::Display, + { + let start_time = Instant::now(); + + info!("Starting food operation"); + + match future.await { + Ok(result) => { + self.metrics + .record_food_operation(operation, pet_type, food_type, true); + + info!( + duration_ms = start_time.elapsed().as_millis(), + "Food operation completed successfully" + ); + + Ok(result) + } + Err(error) => { + self.metrics + .record_food_operation(operation, pet_type, food_type, false); + + error!( + error = %error, + duration_ms = start_time.elapsed().as_millis(), + "Food operation failed" + ); + + Err(error) + } + } + } + + /// Trace a cart operation + #[instrument(skip_all, fields( + operation = %operation, + user_id = user_id, + ))] + pub async fn trace_cart_operation( + &self, + operation: &str, + user_id: Option<&str>, + future: F, + ) -> Result + where + F: std::future::Future>, + E: std::fmt::Display, + { + let start_time = Instant::now(); + + info!("Starting cart operation"); + + match future.await { + Ok(result) => { + self.metrics.record_cart_operation(operation, true); + + info!( + duration_ms = start_time.elapsed().as_millis(), + "Cart operation completed successfully" + ); + + Ok(result) + } + Err(error) => { + self.metrics.record_cart_operation(operation, false); + + error!( + error = %error, + duration_ms = start_time.elapsed().as_millis(), + "Cart operation failed" + ); + + Err(error) + } + } + } + + /// Trace a recommendation request + #[instrument(skip_all, fields( + pet_type = %pet_type, + ))] + pub async fn trace_recommendation_request( + &self, + pet_type: &str, + future: F, + ) -> Result + where + F: std::future::Future>, + E: std::fmt::Display, + { + let start_time = Instant::now(); + + info!("Starting recommendation request"); + + match future.await { + Ok(result) => { + self.metrics.record_recommendation_request(pet_type, true); + + info!( + duration_ms = start_time.elapsed().as_millis(), + "Recommendation request completed successfully" + ); + + Ok(result) + } + Err(error) => { + self.metrics.record_recommendation_request(pet_type, false); + + error!( + error = %error, + duration_ms = start_time.elapsed().as_millis(), + "Recommendation request failed" + ); + + Err(error) + } + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use axum::{ + body::Body, + http::{Method, Request, StatusCode}, + middleware, + routing::get, + Router, + }; + use tower::ServiceExt; + + async fn test_handler() -> &'static str { + "test response" + } + + async fn error_handler() -> StatusCode { + StatusCode::INTERNAL_SERVER_ERROR + } + + #[tokio::test] + async fn test_observability_middleware_success() { + let metrics = Arc::new(Metrics::new().unwrap()); + let metrics_clone = metrics.clone(); + + let app = Router::new() + .route("/test", get(test_handler)) + .layer(middleware::from_fn(move |req, next| { + observability_middleware(metrics_clone.clone(), req, next) + })); + + let request = Request::builder() + .method(Method::GET) + .uri("/test") + .header("user-agent", "test-client/1.0") + .body(Body::empty()) + .unwrap(); + + let response = app.oneshot(request).await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); + + // Verify metrics were recorded + let encoded = metrics.encode().unwrap(); + assert!(encoded.contains("http_requests_total")); + } + + #[tokio::test] + async fn test_observability_middleware_missing_user_agent() { + let metrics = Arc::new(Metrics::new().unwrap()); + let metrics_clone = metrics.clone(); + + let app = Router::new() + .route("/test", get(test_handler)) + .layer(middleware::from_fn(move |req, next| { + observability_middleware(metrics_clone.clone(), req, next) + })); + + // Request without user-agent header + let request = Request::builder() + .method(Method::GET) + .uri("/test") + .body(Body::empty()) + .unwrap(); + + let response = app.oneshot(request).await.unwrap(); + assert_eq!(response.status(), StatusCode::OK); + + // Verify metrics were recorded + let encoded = metrics.encode().unwrap(); + assert!(encoded.contains("http_requests_total")); + } + + #[tokio::test] + async fn test_observability_middleware_error() { + let metrics = Arc::new(Metrics::new().unwrap()); + let metrics_clone = metrics.clone(); + + let app = Router::new() + .route("/error", get(error_handler)) + .layer(middleware::from_fn(move |req, next| { + observability_middleware(metrics_clone.clone(), req, next) + })); + + let request = Request::builder() + .method(Method::GET) + .uri("/error") + .header("user-agent", "error-test-client/1.0") + .body(Body::empty()) + .unwrap(); + + let response = app.oneshot(request).await.unwrap(); + assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR); + + // Verify error metrics were recorded + let encoded = metrics.encode().unwrap(); + assert!(encoded.contains("http_requests_total")); + } + + #[tokio::test] + async fn test_database_tracing_middleware() { + let metrics = Arc::new(Metrics::new().unwrap()); + let middleware = DatabaseTracingMiddleware::new(metrics.clone()); + + // Test successful operation + let result = middleware + .trace_operation("get_item", "test_table", async { + Ok::<_, String>("success") + }) + .await; + + assert!(result.is_ok()); + + // Test failed operation + let result = middleware + .trace_operation("put_item", "test_table", async { + Err::("error") + }) + .await; + + assert!(result.is_err()); + + // Verify metrics were recorded + let encoded = metrics.encode().unwrap(); + assert!(encoded.contains("database_operations_total")); + } + + #[tokio::test] + async fn test_business_tracing_middleware() { + let metrics = Arc::new(Metrics::new().unwrap()); + let middleware = BusinessTracingMiddleware::new(metrics.clone()); + + // Test food operation + let result = middleware + .trace_food_operation("search", Some("puppy"), Some("dry"), async { + Ok::<_, String>("success") + }) + .await; + + assert!(result.is_ok()); + + // Test cart operation + let result = middleware + .trace_cart_operation("add_item", Some("user123"), async { + Ok::<_, String>("success") + }) + .await; + + assert!(result.is_ok()); + + // Test recommendation request + let result = middleware + .trace_recommendation_request("kitten", async { Ok::<_, String>("success") }) + .await; + + assert!(result.is_ok()); + + // Verify metrics were recorded + let encoded = metrics.encode().unwrap(); + assert!(encoded.contains("food_operations_total")); + assert!(encoded.contains("cart_operations_total")); + assert!(encoded.contains("recommendation_requests_total")); + } +} diff --git a/src/applications/microservices/petfood-rs/src/observability/mod.rs b/src/applications/microservices/petfood-rs/src/observability/mod.rs new file mode 100644 index 00000000..2b333ed5 --- /dev/null +++ b/src/applications/microservices/petfood-rs/src/observability/mod.rs @@ -0,0 +1,11 @@ +pub mod metrics; +pub mod middleware; +pub mod tracing; + +pub use metrics::{Metrics, MetricsError}; +pub use middleware::{ + observability_middleware, BusinessTracingMiddleware, DatabaseTracingMiddleware, +}; +pub use tracing::{ + get_current_trace_id, init_observability, shutdown_observability, ObservabilityError, +}; diff --git a/src/applications/microservices/petfood-rs/src/observability/tracing.rs b/src/applications/microservices/petfood-rs/src/observability/tracing.rs new file mode 100644 index 00000000..638ed96f --- /dev/null +++ b/src/applications/microservices/petfood-rs/src/observability/tracing.rs @@ -0,0 +1,290 @@ +use opentelemetry::{global, KeyValue}; +use opentelemetry_otlp::WithExportConfig; +use opentelemetry_sdk::{ + trace::{self, RandomIdGenerator, Sampler}, + Resource, +}; +use std::time::Duration; +use thiserror::Error; +use tracing::{error, info, warn}; +use tracing_opentelemetry::OpenTelemetryLayer; +use tracing_subscriber::{ + fmt::format::FmtSpan, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer, +}; + +#[derive(Debug, Error)] +pub enum ObservabilityError { + #[error("Failed to initialize OpenTelemetry: {0}")] + OpenTelemetryInit(#[from] opentelemetry::trace::TraceError), + #[error("Failed to initialize tracing subscriber: {0}")] + TracingInit(String), + #[error("Configuration error: {0}")] + Config(String), +} + +/// Initialize comprehensive observability including OpenTelemetry tracing and structured logging +pub fn init_observability( + service_name: &str, + service_version: &str, + otlp_endpoint: &str, + enable_json_logging: bool, +) -> Result<(), ObservabilityError> { + info!( + "Initializing observability for service: {} v{}", + service_name, service_version + ); + + // Initialize OpenTelemetry tracer + let tracer = init_opentelemetry_tracer(service_name, service_version, otlp_endpoint)?; + + // Create OpenTelemetry layer + let opentelemetry_layer = OpenTelemetryLayer::new(tracer); + + // Create environment filter + let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| { + format!( + "{}=info,tower_http=info,aws_sdk_dynamodb=info,aws_config=info,aws_smithy_runtime=info", + service_name.replace('-', "_") + ) + .into() + }); + + // Initialize tracing subscriber with different formatters based on configuration + if enable_json_logging { + // For JSON logging - create a custom layer that excludes span context + // We need to use a different approach to avoid automatic span inclusion + let fmt_layer = tracing_subscriber::fmt::layer() + .json() + .with_current_span(false) // This is key - don't include current span context + .with_span_list(false) // Don't include span list + .with_target(false) + .with_thread_ids(false) + .with_thread_names(false) + .with_level(true) + .with_file(false) + .with_line_number(false) + .log_internal_errors(false) + .with_span_events(FmtSpan::NONE) + .with_filter(tracing_subscriber::filter::LevelFilter::INFO); + + tracing_subscriber::registry() + .with(env_filter) + .with(opentelemetry_layer) + .with(fmt_layer) + .init(); + } else { + // Human-readable formatter for development + // Clean logs with minimal span information + tracing_subscriber::registry() + .with(env_filter) + .with(opentelemetry_layer) + .with( + tracing_subscriber::fmt::layer() + .with_target(false) + .with_thread_ids(false) + .with_thread_names(false) + .with_file(false) + .with_line_number(false) + // No span events + .with_span_events(FmtSpan::NONE) + .with_filter(tracing_subscriber::filter::LevelFilter::INFO), + ) + .init(); + } + + info!("Observability initialized successfully"); + Ok(()) +} + +/// Extract the current trace ID from the active span context +pub fn get_current_trace_id() -> Option { + use opentelemetry::trace::TraceContextExt; + use tracing_opentelemetry::OpenTelemetrySpanExt; + + let current_span = tracing::Span::current(); + let context = current_span.context(); + let span = context.span(); + let span_context = span.span_context(); + + if span_context.is_valid() { + Some(span_context.trace_id().to_string()) + } else { + None + } +} + +/// Macro to log info messages with trace ID +#[macro_export] +macro_rules! info_with_trace { + ($($arg:tt)*) => { + if let Some(trace_id) = $crate::observability::tracing::get_current_trace_id() { + tracing::info!(trace_id = %trace_id, $($arg)*); + } else { + tracing::info!($($arg)*); + } + }; +} + +/// Macro to log error messages with trace ID +#[macro_export] +macro_rules! error_with_trace { + ($($arg:tt)*) => { + if let Some(trace_id) = $crate::observability::tracing::get_current_trace_id() { + tracing::error!(trace_id = %trace_id, $($arg)*); + } else { + tracing::error!($($arg)*); + } + }; +} + +/// Macro to log warn messages with trace ID +#[macro_export] +macro_rules! warn_with_trace { + ($($arg:tt)*) => { + if let Some(trace_id) = $crate::observability::tracing::get_current_trace_id() { + tracing::warn!(trace_id = %trace_id, $($arg)*); + } else { + tracing::warn!($($arg)*); + } + }; +} + +/// Initialize OpenTelemetry tracer with OTLP exporter for CloudWatch X-Ray integration +fn init_opentelemetry_tracer( + service_name: &str, + service_version: &str, + otlp_endpoint: &str, +) -> Result { + info!("Initializing OpenTelemetry tracer"); + + // Create resource with service information - platform agnostic + let resource_attributes = vec![ + KeyValue::new("service.name", service_name.to_string()), + KeyValue::new("service.version", service_version.to_string()), + KeyValue::new("service.namespace", "petadoptions"), + KeyValue::new("cloud.provider", "aws"), + // Generic cloud platform - let the collector/X-Ray detect the actual platform + KeyValue::new("cloud.platform", "aws_container"), + // OpenTelemetry SDK information + KeyValue::new("telemetry.sdk.name", "opentelemetry"), + KeyValue::new("telemetry.sdk.language", "rust"), + KeyValue::new("telemetry.sdk.version", "1.44.1"), + ]; + + let resource = Resource::new(resource_attributes); + + // Configure OTLP exporter + let mut exporter = opentelemetry_otlp::new_exporter().tonic(); + + if !otlp_endpoint.is_empty() { + info!("Using custom OTLP endpoint: {}", otlp_endpoint); + exporter = exporter.with_endpoint(otlp_endpoint); + } else { + // Default to localhost for development, will be overridden in production + info!("Using default OTLP endpoint: http://localhost:4317"); + exporter = exporter.with_endpoint("http://localhost:4317"); + } + + // Build tracer pipeline + let tracer = opentelemetry_otlp::new_pipeline() + .tracing() + .with_exporter(exporter) + .with_trace_config( + trace::config() + .with_sampler(Sampler::AlwaysOn) + .with_id_generator(RandomIdGenerator::default()) + .with_max_events_per_span(64) + .with_max_attributes_per_span(16) + .with_max_links_per_span(16) + .with_resource(resource), + ) + .with_batch_config( + trace::BatchConfig::default() + .with_max_queue_size(2048) + .with_max_export_batch_size(512) + .with_max_export_timeout(Duration::from_secs(30)) + .with_scheduled_delay(Duration::from_millis(500)), + ) + .install_batch(opentelemetry_sdk::runtime::Tokio)?; + + info!("OpenTelemetry tracer initialized successfully"); + Ok(tracer) +} + +/// Shutdown observability gracefully with timeout +pub async fn shutdown_observability() { + info!("Shutting down observability"); + + // Use spawn_blocking to run the blocking shutdown in a separate thread + let shutdown_task = tokio::task::spawn_blocking(|| { + // Gracefully shutdown the tracer provider + // This may block if there are pending spans, so we run it in a separate thread + global::shutdown_tracer_provider(); + }); + + // Apply timeout to prevent hanging indefinitely + match tokio::time::timeout(Duration::from_secs(5), shutdown_task).await { + Ok(Ok(())) => { + info!("Observability shutdown completed successfully"); + } + Ok(Err(e)) => { + warn!("Error during observability shutdown: {}", e); + } + Err(_) => { + warn!("Observability shutdown timed out after 5 seconds - forcing exit"); + // If shutdown times out, we'll let the process exit anyway + // This prevents the application from hanging indefinitely + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[tokio::test] + async fn test_shutdown_observability_timeout() { + // Test that shutdown_observability completes within reasonable time + let start = std::time::Instant::now(); + shutdown_observability().await; + let elapsed = start.elapsed(); + + // Should complete within 6 seconds (5 second timeout + some buffer) + assert!( + elapsed < Duration::from_secs(6), + "Shutdown took too long: {:?}", + elapsed + ); + } + + #[test] + fn test_init_observability_development() { + // Test that the function exists and can be called + // In a real test environment, we would need a tokio runtime + // For now, just test that the function signature is correct + let _result = std::panic::catch_unwind(|| { + // This will fail but we're just testing the function exists + let _ = init_observability("test-service-dev", "0.1.0", "", false); + }); + + // Test passes if we can call the function without compilation errors + } + + #[test] + fn test_init_observability_production() { + // Test that the function exists and can be called + // In a real test environment, we would need a tokio runtime + // For now, just test that the function signature is correct + let _result = std::panic::catch_unwind(|| { + // This will fail but we're just testing the function exists + let _ = init_observability( + "test-service-prod", + "0.1.0", + "http://test-endpoint:4317", + true, + ); + }); + + // Test passes if we can call the function without compilation errors + } +} diff --git a/src/applications/microservices/petfood-rs/src/repositories/cart_repository.rs b/src/applications/microservices/petfood-rs/src/repositories/cart_repository.rs new file mode 100644 index 00000000..8908fd3b --- /dev/null +++ b/src/applications/microservices/petfood-rs/src/repositories/cart_repository.rs @@ -0,0 +1,640 @@ +use async_trait::async_trait; +use aws_sdk_dynamodb::types::AttributeValue; +use aws_sdk_dynamodb::{Client as DynamoDbClient, Error as DynamoDbError}; +use std::collections::HashMap; +use std::sync::Arc; +use tracing::{error, info, instrument, warn, Instrument}; + +use crate::models::{Cart, CartItem, RepositoryError, RepositoryResult}; + +/// Trait defining the interface for cart data access operations +#[async_trait] +pub trait CartRepository: Send + Sync { + /// Find a cart by user ID + async fn find_cart(&self, user_id: &str) -> RepositoryResult>; + + /// Save a cart (create or update) + async fn save_cart(&self, cart: Cart) -> RepositoryResult; + + /// Delete a cart + async fn delete_cart(&self, user_id: &str) -> RepositoryResult<()>; + + /// Check if a cart exists for a user + async fn cart_exists(&self, user_id: &str) -> RepositoryResult; + + /// Get all carts (for admin/testing purposes) + async fn find_all_carts(&self) -> RepositoryResult>; + + /// Count total number of carts + async fn count_carts(&self) -> RepositoryResult; +} + +/// DynamoDB implementation of the CartRepository trait +pub struct DynamoDbCartRepository { + client: Arc, + table_name: String, + region: String, +} + +impl DynamoDbCartRepository { + /// Create a new DynamoDB cart repository + pub fn new(client: Arc, table_name: String, region: String) -> Self { + Self { + client, + table_name, + region, + } + } + + /// Create a DynamoDB subsegment span with proper X-Ray attributes + fn create_dynamodb_span(&self, operation: &str) -> tracing::Span { + tracing::info_span!( + "DynamoDB", + // AWS X-Ray specific attributes + "aws.service" = "DynamoDB", + "aws.operation" = operation, + "aws.region" = %self.region, + "aws.dynamodb.table_name" = %self.table_name, + "aws.request_id" = tracing::field::Empty, + "aws.agent" = "rust-aws-sdk", + + // Resource identification for X-Ray + "aws.remote.service" = "AWS::DynamoDB", + "aws.remote.operation" = operation, + "aws.remote.resource.type" = "AWS::DynamoDB::Table", + "aws.remote.resource.identifier" = %self.table_name, + "remote.resource.cfn.primary.identifier" = %self.table_name, + + // Table-specific attributes + "table_name" = %self.table_name, + "table.name" = %self.table_name, + "resource_names" = format!("[{}]", self.table_name), + "endpoint" = format!("https://dynamodb.{}.amazonaws.com", self.region), + + // OpenTelemetry semantic conventions + "otel.kind" = "client", + "otel.name" = format!("DynamoDB.{}", operation), + + // RPC semantic conventions for AWS API calls + "rpc.system" = "aws-api", + "rpc.service" = "AmazonDynamoDBv2", + "rpc.method" = operation, + + // HTTP semantic conventions (AWS APIs are HTTP-based) + "http.method" = "POST", + "http.url" = format!("https://dynamodb.{}.amazonaws.com", self.region), + "http.status_code" = tracing::field::Empty, + + // Database semantic conventions + "db.system" = "dynamodb", + "db.name" = %self.table_name, + "db.operation" = operation, + + // Component identification for X-Ray + "component" = "aws-sdk-dynamodb", + ) + } + + /// Get the table name (for testing) + pub fn table_name(&self) -> &str { + &self.table_name + } + + /// Convert a Cart struct to DynamoDB attribute values + pub fn cart_to_item(&self, cart: &Cart) -> HashMap { + let mut item = HashMap::new(); + + item.insert( + "user_id".to_string(), + AttributeValue::S(cart.user_id.clone()), + ); + + // Convert cart items to DynamoDB list + let items: Vec = cart + .items + .iter() + .map(|cart_item| { + let mut item_map = HashMap::new(); + item_map.insert( + "food_id".to_string(), + AttributeValue::S(cart_item.food_id.clone()), + ); + item_map.insert( + "quantity".to_string(), + AttributeValue::N(cart_item.quantity.to_string()), + ); + item_map.insert( + "unit_price".to_string(), + AttributeValue::N(cart_item.unit_price.to_string()), + ); + item_map.insert( + "added_at".to_string(), + AttributeValue::S(cart_item.added_at.to_rfc3339()), + ); + AttributeValue::M(item_map) + }) + .collect(); + + item.insert("items".to_string(), AttributeValue::L(items)); + item.insert( + "created_at".to_string(), + AttributeValue::S(cart.created_at.to_rfc3339()), + ); + item.insert( + "updated_at".to_string(), + AttributeValue::S(cart.updated_at.to_rfc3339()), + ); + + item + } + + /// Convert DynamoDB item to Cart struct + pub fn item_to_cart(&self, item: HashMap) -> RepositoryResult { + use chrono::DateTime; + + let user_id = item + .get("user_id") + .and_then(|v| v.as_s().ok()) + .ok_or_else(|| RepositoryError::InvalidQuery { + message: "Missing user_id".to_string(), + })? + .clone(); + + // Parse cart items + let items = item + .get("items") + .and_then(|v| v.as_l().ok()) + .map(|list| { + list.iter() + .filter_map(|item_attr| { + if let Ok(item_map) = item_attr.as_m() { + self.map_to_cart_item(item_map).ok() + } else { + None + } + }) + .collect() + }) + .unwrap_or_default(); + + let created_at = item + .get("created_at") + .and_then(|v| v.as_s().ok()) + .and_then(|s| DateTime::parse_from_rfc3339(s).ok()) + .map(|dt| dt.with_timezone(&chrono::Utc)) + .ok_or_else(|| RepositoryError::InvalidQuery { + message: "Invalid created_at".to_string(), + })?; + + let updated_at = item + .get("updated_at") + .and_then(|v| v.as_s().ok()) + .and_then(|s| DateTime::parse_from_rfc3339(s).ok()) + .map(|dt| dt.with_timezone(&chrono::Utc)) + .unwrap_or_else(|| { + // If updated_at is missing or invalid, use created_at as fallback + created_at + }); + + Ok(Cart { + user_id, + items, + created_at, + updated_at, + }) + } + + /// Convert DynamoDB map to CartItem + pub fn map_to_cart_item( + &self, + item_map: &HashMap, + ) -> RepositoryResult { + use chrono::DateTime; + use rust_decimal::Decimal; + use std::str::FromStr; + + let food_id = item_map + .get("food_id") + .and_then(|v| v.as_s().ok()) + .ok_or_else(|| RepositoryError::InvalidQuery { + message: "Missing food_id in cart item".to_string(), + })? + .clone(); + + let quantity = item_map + .get("quantity") + .and_then(|v| v.as_n().ok()) + .and_then(|s| s.parse().ok()) + .ok_or_else(|| RepositoryError::InvalidQuery { + message: "Invalid quantity in cart item".to_string(), + })?; + + let unit_price = item_map + .get("unit_price") + .and_then(|v| v.as_n().ok()) + .and_then(|s| Decimal::from_str(s).ok()) + .ok_or_else(|| RepositoryError::InvalidQuery { + message: "Invalid unit_price in cart item".to_string(), + })?; + + let added_at = item_map + .get("added_at") + .and_then(|v| v.as_s().ok()) + .and_then(|s| DateTime::parse_from_rfc3339(s).ok()) + .map(|dt| dt.with_timezone(&chrono::Utc)) + .ok_or_else(|| RepositoryError::InvalidQuery { + message: "Invalid added_at in cart item".to_string(), + })?; + + Ok(CartItem { + food_id, + quantity, + unit_price, + added_at, + }) + } + + /// Convert DynamoDB error to RepositoryError + fn map_dynamodb_error(&self, error: DynamoDbError) -> RepositoryError { + error!("DynamoDB error: {:?}", error); + RepositoryError::AwsSdk { + message: error.to_string(), + } + } +} + +#[async_trait] +impl CartRepository for DynamoDbCartRepository { + #[instrument(skip(self), fields(table = %self.table_name, user_id = %user_id))] + async fn find_cart(&self, user_id: &str) -> RepositoryResult> { + info!("Finding cart for user"); + + // Create a DynamoDB subsegment + let get_span = self.create_dynamodb_span("GetItem"); + + let response = async { + self.client + .get_item() + .table_name(&self.table_name) + .key("user_id", AttributeValue::S(user_id.to_string())) + .send() + .await + .map_err(|e| self.map_dynamodb_error(e.into())) + } + .instrument(get_span) + .await?; + + match response.item { + Some(item) => { + let cart = self.item_to_cart(item)?; + info!("Cart found with {} items", cart.items.len()); + Ok(Some(cart)) + } + None => { + info!("Cart not found"); + Ok(None) + } + } + } + + #[instrument(skip(self, cart), fields(table = %self.table_name, user_id = %cart.user_id, item_count = cart.items.len()))] + async fn save_cart(&self, cart: Cart) -> RepositoryResult { + info!("Saving cart"); + + let item = self.cart_to_item(&cart); + + // Create a DynamoDB subsegment + let put_span = self.create_dynamodb_span("PutItem"); + + async { + self.client + .put_item() + .table_name(&self.table_name) + .set_item(Some(item)) + .send() + .await + .map_err(|e| self.map_dynamodb_error(e.into())) + } + .instrument(put_span) + .await?; + + info!("Cart saved successfully"); + Ok(cart) + } + + #[instrument(skip(self), fields(table = %self.table_name, user_id = %user_id))] + async fn delete_cart(&self, user_id: &str) -> RepositoryResult<()> { + info!("Deleting cart"); + + // Create a DynamoDB subsegment + let delete_span = self.create_dynamodb_span("DeleteItem"); + + async { + self.client + .delete_item() + .table_name(&self.table_name) + .key("user_id", AttributeValue::S(user_id.to_string())) + .send() + .await + .map_err(|e| self.map_dynamodb_error(e.into()))?; + + info!("Cart deleted successfully"); + Ok(()) + } + .instrument(delete_span) + .await + } + + #[instrument(skip(self), fields(table = %self.table_name, user_id = %user_id))] + async fn cart_exists(&self, user_id: &str) -> RepositoryResult { + info!("Checking if cart exists"); + + // Create a DynamoDB subsegment + let get_span = self.create_dynamodb_span("GetItem"); + + let response = async { + self.client + .get_item() + .table_name(&self.table_name) + .key("user_id", AttributeValue::S(user_id.to_string())) + .projection_expression("user_id") + .send() + .await + .map_err(|e| self.map_dynamodb_error(e.into())) + } + .instrument(get_span) + .await?; + + let exists = response.item.is_some(); + info!("Cart exists: {}", exists); + Ok(exists) + } + + #[instrument(skip(self), fields(table = %self.table_name))] + async fn find_all_carts(&self) -> RepositoryResult> { + info!("Finding all carts"); + + // Create a DynamoDB subsegment + let scan_span = self.create_dynamodb_span("Scan"); + + let response = async { + self.client + .scan() + .table_name(&self.table_name) + .send() + .await + .map_err(|e| self.map_dynamodb_error(e.into())) + } + .instrument(scan_span) + .await?; + + let mut carts = Vec::new(); + if let Some(items) = response.items { + for item in items { + match self.item_to_cart(item) { + Ok(cart) => carts.push(cart), + Err(e) => { + warn!("Failed to parse cart item: {}", e); + continue; + } + } + } + } + + info!("Found {} carts", carts.len()); + Ok(carts) + } + + #[instrument(skip(self), fields(table = %self.table_name))] + async fn count_carts(&self) -> RepositoryResult { + info!("Counting carts"); + + // Create a DynamoDB subsegment + let scan_span = self.create_dynamodb_span("Scan"); + + let response = async { + self.client + .scan() + .table_name(&self.table_name) + .select(aws_sdk_dynamodb::types::Select::Count) + .send() + .await + .map_err(|e| self.map_dynamodb_error(e.into())) + } + .instrument(scan_span) + .await?; + + let count = response.count() as usize; + info!("Cart count: {}", count); + Ok(count) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use rust_decimal_macros::dec; + + fn create_test_cart() -> Cart { + let mut cart = Cart::new("user123".to_string()); + cart.add_item("F001".to_string(), 2, dec!(12.99)); + cart.add_item("F002".to_string(), 1, dec!(8.99)); + cart + } + + #[test] + fn test_cart_to_item_conversion() { + let cart = create_test_cart(); + let config = aws_sdk_dynamodb::Config::builder() + .region(aws_sdk_dynamodb::config::Region::new("us-east-1")) + .behavior_version(aws_sdk_dynamodb::config::BehaviorVersion::latest()) + .build(); + let client = Arc::new(aws_sdk_dynamodb::Client::from_conf(config)); + let repo = + DynamoDbCartRepository::new(client, "test-table".to_string(), "us-east-1".to_string()); + + let item = repo.cart_to_item(&cart); + + assert!(item.contains_key("user_id")); + assert!(item.contains_key("items")); + assert!(item.contains_key("created_at")); + assert!(item.contains_key("updated_at")); + + // Verify user_id + if let Some(AttributeValue::S(user_id)) = item.get("user_id") { + assert_eq!(user_id, "user123"); + } else { + panic!("Expected string value for user_id"); + } + + // Verify items list + if let Some(AttributeValue::L(items)) = item.get("items") { + assert_eq!(items.len(), 2); + + // Check first item structure + if let AttributeValue::M(first_item) = &items[0] { + assert!(first_item.contains_key("food_id")); + assert!(first_item.contains_key("quantity")); + assert!(first_item.contains_key("unit_price")); + assert!(first_item.contains_key("added_at")); + } else { + panic!("Expected map value for cart item"); + } + } else { + panic!("Expected list value for items"); + } + } + + #[test] + fn test_item_to_cart_conversion() { + let cart = create_test_cart(); + let config = aws_sdk_dynamodb::Config::builder() + .region(aws_sdk_dynamodb::config::Region::new("us-east-1")) + .behavior_version(aws_sdk_dynamodb::config::BehaviorVersion::latest()) + .build(); + let client = Arc::new(aws_sdk_dynamodb::Client::from_conf(config)); + let repo = + DynamoDbCartRepository::new(client, "test-table".to_string(), "us-east-1".to_string()); + + let item = repo.cart_to_item(&cart); + let converted_cart = repo.item_to_cart(item).unwrap(); + + assert_eq!(converted_cart.user_id, cart.user_id); + assert_eq!(converted_cart.items.len(), cart.items.len()); + + // Check first item + let original_item = &cart.items[0]; + let converted_item = &converted_cart.items[0]; + + assert_eq!(converted_item.food_id, original_item.food_id); + assert_eq!(converted_item.quantity, original_item.quantity); + assert_eq!(converted_item.unit_price, original_item.unit_price); + + // Timestamps should be preserved (within reasonable precision) + let time_diff = (converted_item.added_at - original_item.added_at) + .num_milliseconds() + .abs(); + assert!( + time_diff < 1000, + "Timestamp difference too large: {}ms", + time_diff + ); + } + + #[test] + fn test_empty_cart_conversion() { + let cart = Cart::new("user456".to_string()); + let config = aws_sdk_dynamodb::Config::builder() + .region(aws_sdk_dynamodb::config::Region::new("us-east-1")) + .behavior_version(aws_sdk_dynamodb::config::BehaviorVersion::latest()) + .build(); + let client = Arc::new(aws_sdk_dynamodb::Client::from_conf(config)); + let repo = + DynamoDbCartRepository::new(client, "test-table".to_string(), "us-east-1".to_string()); + + let item = repo.cart_to_item(&cart); + let converted_cart = repo.item_to_cart(item).unwrap(); + + assert_eq!(converted_cart.user_id, cart.user_id); + assert!(converted_cart.items.is_empty()); + assert_eq!(converted_cart.total_items(), 0); + } + + #[test] + fn test_map_to_cart_item() { + let config = aws_sdk_dynamodb::Config::builder() + .region(aws_sdk_dynamodb::config::Region::new("us-east-1")) + .behavior_version(aws_sdk_dynamodb::config::BehaviorVersion::latest()) + .build(); + let client = Arc::new(aws_sdk_dynamodb::Client::from_conf(config)); + let repo = + DynamoDbCartRepository::new(client, "test-table".to_string(), "us-east-1".to_string()); + + let mut item_map = HashMap::new(); + item_map.insert("food_id".to_string(), AttributeValue::S("F001".to_string())); + item_map.insert("quantity".to_string(), AttributeValue::N("3".to_string())); + item_map.insert( + "unit_price".to_string(), + AttributeValue::N("15.99".to_string()), + ); + item_map.insert( + "added_at".to_string(), + AttributeValue::S(chrono::Utc::now().to_rfc3339()), + ); + + let cart_item = repo.map_to_cart_item(&item_map).unwrap(); + + assert_eq!(cart_item.food_id, "F001"); + assert_eq!(cart_item.quantity, 3); + assert_eq!(cart_item.unit_price, dec!(15.99)); + } + + #[test] + fn test_repository_creation() { + let config = aws_sdk_dynamodb::Config::builder() + .region(aws_sdk_dynamodb::config::Region::new("us-east-1")) + .behavior_version(aws_sdk_dynamodb::config::BehaviorVersion::latest()) + .build(); + let client = Arc::new(aws_sdk_dynamodb::Client::from_conf(config)); + let repo = DynamoDbCartRepository::new( + client, + "test-cart-table".to_string(), + "us-east-1".to_string(), + ); + + assert_eq!(repo.table_name(), "test-cart-table"); + } + + #[test] + fn test_item_to_cart_conversion_missing_updated_at() { + let cart = create_test_cart(); + let config = aws_sdk_dynamodb::Config::builder() + .region(aws_sdk_dynamodb::config::Region::new("us-east-1")) + .behavior_version(aws_sdk_dynamodb::config::BehaviorVersion::latest()) + .build(); + let client = Arc::new(aws_sdk_dynamodb::Client::from_conf(config)); + let repo = + DynamoDbCartRepository::new(client, "test-table".to_string(), "us-east-1".to_string()); + + let mut item = repo.cart_to_item(&cart); + + // Remove the updated_at field to simulate legacy data + item.remove("updated_at"); + + let converted_cart = repo.item_to_cart(item).unwrap(); + + assert_eq!(converted_cart.user_id, cart.user_id); + assert_eq!(converted_cart.items.len(), cart.items.len()); + + // When updated_at is missing, it should fallback to created_at + assert_eq!(converted_cart.updated_at, converted_cart.created_at); + } + + #[test] + fn test_invalid_cart_item_handling() { + let config = aws_sdk_dynamodb::Config::builder() + .region(aws_sdk_dynamodb::config::Region::new("us-east-1")) + .behavior_version(aws_sdk_dynamodb::config::BehaviorVersion::latest()) + .build(); + let client = Arc::new(aws_sdk_dynamodb::Client::from_conf(config)); + let repo = + DynamoDbCartRepository::new(client, "test-table".to_string(), "us-east-1".to_string()); + // Missing required field + let mut invalid_item_map = HashMap::new(); + invalid_item_map.insert("quantity".to_string(), AttributeValue::N("3".to_string())); + // Missing food_id, unit_price, added_at + + let result = repo.map_to_cart_item(&invalid_item_map); + assert!(result.is_err()); + + match result.unwrap_err() { + RepositoryError::InvalidQuery { message } => { + assert!(message.contains("Missing food_id")); + } + _ => panic!("Expected InvalidQuery error"), + } + } + + // Note: Integration tests with actual DynamoDB would be in a separate test file + // using testcontainers or LocalStack for a real DynamoDB instance +} diff --git a/src/applications/microservices/petfood-rs/src/repositories/food_repository.rs b/src/applications/microservices/petfood-rs/src/repositories/food_repository.rs new file mode 100644 index 00000000..2437f2f6 --- /dev/null +++ b/src/applications/microservices/petfood-rs/src/repositories/food_repository.rs @@ -0,0 +1,1005 @@ +use async_trait::async_trait; +use aws_sdk_dynamodb::operation::RequestId; +use aws_sdk_dynamodb::types::{AttributeValue, Select}; +use aws_sdk_dynamodb::{Client as DynamoDbClient, Error as DynamoDbError}; +use std::collections::HashMap; +use std::sync::Arc; +use tracing::{error, info, instrument, warn, Instrument}; + +use crate::models::{ + AvailabilityStatus, Food, FoodFilters, FoodType, PetType, RepositoryError, RepositoryResult, +}; + +/// Trait defining the interface for food data access operations +#[async_trait] +pub trait FoodRepository: Send + Sync { + /// Find all foods with optional filters + async fn find_all(&self, filters: FoodFilters) -> RepositoryResult>; + + /// Find a food by its ID + async fn find_by_id(&self, id: &str) -> RepositoryResult>; + + /// Find foods by pet type using GSI + async fn find_by_pet_type(&self, pet_type: PetType) -> RepositoryResult>; + + /// Find foods by food type using GSI + async fn find_by_food_type(&self, food_type: FoodType) -> RepositoryResult>; + + /// Create a new food item + async fn create(&self, food: Food) -> RepositoryResult; + + /// Update an existing food item + async fn update(&self, food: Food) -> RepositoryResult; + + /// Soft delete a food item (mark as inactive) + async fn soft_delete(&self, id: &str) -> RepositoryResult<()>; + + /// Hard delete a food item (for testing/cleanup) + async fn delete(&self, id: &str) -> RepositoryResult<()>; + + /// Check if a food exists + async fn exists(&self, id: &str) -> RepositoryResult; + + /// Count total foods with optional filters + async fn count(&self, filters: Option) -> RepositoryResult; +} + +/// DynamoDB implementation of the FoodRepository trait +pub struct DynamoDbFoodRepository { + client: Arc, + table_name: String, + pet_type_index: String, + food_type_index: String, + region: String, +} + +impl DynamoDbFoodRepository { + /// Create a new DynamoDB food repository + pub fn new(client: Arc, table_name: String, region: String) -> Self { + Self { + client, + table_name: table_name.clone(), + pet_type_index: "PetTypeIndex".to_string(), + food_type_index: "FoodTypeIndex".to_string(), + region, + } + } + + /// Create a DynamoDB subsegment span with proper X-Ray attributes + fn create_dynamodb_span(&self, operation: &str) -> tracing::Span { + tracing::info_span!( + "DynamoDB", + // AWS X-Ray specific attributes + "aws.service" = "DynamoDB", + "aws.operation" = operation, + "aws.region" = %self.region, + "aws.dynamodb.table_name" = %self.table_name, + "aws.request_id" = tracing::field::Empty, + "aws.agent" = "rust-aws-sdk", + + // Resource identification for X-Ray + "aws.remote.service" = "AWS::DynamoDB", + "aws.remote.operation" = operation, + "aws.remote.resource.type" = "AWS::DynamoDB::Table", + "aws.remote.resource.identifier" = %self.table_name, + "remote.resource.cfn.primary.identifier" = %self.table_name, + + // Table-specific attributes + "table_name" = %self.table_name, + "table.name" = %self.table_name, + "resource_names" = format!("[{}]", self.table_name), + "endpoint" = format!("https://dynamodb.{}.amazonaws.com", self.region), + + // OpenTelemetry semantic conventions + "otel.kind" = "client", + "otel.name" = format!("DynamoDB.{}", operation), + + // RPC semantic conventions for AWS API calls + "rpc.system" = "aws-api", + "rpc.service" = "AmazonDynamoDBv2", + "rpc.method" = operation, + + // HTTP semantic conventions (AWS APIs are HTTP-based) + "http.method" = "POST", + "http.url" = format!("https://dynamodb.{}.amazonaws.com", self.region), + "http.status_code" = tracing::field::Empty, + + // Database semantic conventions + "db.system" = "dynamodb", + "db.name" = %self.table_name, + "db.operation" = operation, + + // Component identification for X-Ray + "component" = "aws-sdk-dynamodb", + ) + } + + /// Get the table name (for testing) + pub fn table_name(&self) -> &str { + &self.table_name + } + + /// Get the pet type index name (for testing) + pub fn pet_type_index(&self) -> &str { + &self.pet_type_index + } + + /// Get the food type index name (for testing) + pub fn food_type_index(&self) -> &str { + &self.food_type_index + } + + /// Convert a Food struct to DynamoDB attribute values + pub fn food_to_item(&self, food: &Food) -> HashMap { + let mut item = HashMap::new(); + + item.insert("id".to_string(), AttributeValue::S(food.id.clone())); + item.insert( + "pet_type".to_string(), + AttributeValue::S(food.pet_type.to_string()), + ); + item.insert("name".to_string(), AttributeValue::S(food.name.clone())); + item.insert( + "food_type".to_string(), + AttributeValue::S(food.food_type.to_string()), + ); + item.insert( + "description".to_string(), + AttributeValue::S(food.description.clone()), + ); + item.insert( + "price".to_string(), + AttributeValue::N(food.price.to_string()), + ); + // Handle optional image + if let Some(ref image_path) = food.image { + item.insert("image".to_string(), AttributeValue::S(image_path.clone())); + } + + // Handle optional nutritional info + if let Some(ref nutritional_info) = food.nutritional_info { + let mut nutrition_map = HashMap::new(); + + if let Some(calories) = nutritional_info.calories_per_serving { + nutrition_map.insert( + "calories_per_serving".to_string(), + AttributeValue::N(calories.to_string()), + ); + } + if let Some(protein) = nutritional_info.protein_percentage { + nutrition_map.insert( + "protein_percentage".to_string(), + AttributeValue::N(protein.to_string()), + ); + } + if let Some(fat) = nutritional_info.fat_percentage { + nutrition_map.insert( + "fat_percentage".to_string(), + AttributeValue::N(fat.to_string()), + ); + } + if let Some(carbs) = nutritional_info.carbohydrate_percentage { + nutrition_map.insert( + "carbohydrate_percentage".to_string(), + AttributeValue::N(carbs.to_string()), + ); + } + if let Some(fiber) = nutritional_info.fiber_percentage { + nutrition_map.insert( + "fiber_percentage".to_string(), + AttributeValue::N(fiber.to_string()), + ); + } + if let Some(moisture) = nutritional_info.moisture_percentage { + nutrition_map.insert( + "moisture_percentage".to_string(), + AttributeValue::N(moisture.to_string()), + ); + } + if let Some(ref serving_size) = nutritional_info.serving_size { + nutrition_map.insert( + "serving_size".to_string(), + AttributeValue::S(serving_size.clone()), + ); + } + if let Some(servings) = nutritional_info.servings_per_container { + nutrition_map.insert( + "servings_per_container".to_string(), + AttributeValue::N(servings.to_string()), + ); + } + + if !nutrition_map.is_empty() { + item.insert( + "nutritional_info".to_string(), + AttributeValue::M(nutrition_map), + ); + } + } + + // Convert ingredients list + let ingredients: Vec = food + .ingredients + .iter() + .map(|ingredient| AttributeValue::S(ingredient.clone())) + .collect(); + item.insert("ingredients".to_string(), AttributeValue::L(ingredients)); + + // Handle optional feeding guidelines + if let Some(ref guidelines) = food.feeding_guidelines { + item.insert( + "feeding_guidelines".to_string(), + AttributeValue::S(guidelines.clone()), + ); + } + + item.insert( + "availability_status".to_string(), + AttributeValue::S(food.availability_status.to_string()), + ); + item.insert( + "stock_quantity".to_string(), + AttributeValue::N(food.stock_quantity.to_string()), + ); + item.insert( + "created_at".to_string(), + AttributeValue::S(food.created_at.to_rfc3339()), + ); + item.insert( + "updated_at".to_string(), + AttributeValue::S(food.updated_at.to_rfc3339()), + ); + item.insert( + "is_active".to_string(), + AttributeValue::Bool(food.is_active), + ); + + item + } + + /// Convert DynamoDB item to Food struct + pub fn item_to_food(&self, item: HashMap) -> RepositoryResult { + use crate::models::NutritionalInfo; + use chrono::DateTime; + use rust_decimal::Decimal; + use std::str::FromStr; + + let id = item + .get("id") + .and_then(|v| v.as_s().ok()) + .ok_or_else(|| RepositoryError::InvalidQuery { + message: "Missing id".to_string(), + })? + .clone(); + + let pet_type = item + .get("pet_type") + .and_then(|v| v.as_s().ok()) + .and_then(|s| PetType::from_str(s).ok()) + .ok_or_else(|| RepositoryError::InvalidQuery { + message: "Invalid pet_type".to_string(), + })?; + + let name = item + .get("name") + .and_then(|v| v.as_s().ok()) + .ok_or_else(|| RepositoryError::InvalidQuery { + message: "Missing name".to_string(), + })? + .clone(); + + let food_type = item + .get("food_type") + .and_then(|v| v.as_s().ok()) + .and_then(|s| FoodType::from_str(s).ok()) + .ok_or_else(|| RepositoryError::InvalidQuery { + message: "Invalid food_type".to_string(), + })?; + + let description = item + .get("description") + .and_then(|v| v.as_s().ok()) + .ok_or_else(|| RepositoryError::InvalidQuery { + message: "Missing description".to_string(), + })? + .clone(); + + let price = item + .get("price") + .and_then(|v| v.as_n().ok()) + .and_then(|s| Decimal::from_str(s).ok()) + .ok_or_else(|| RepositoryError::InvalidQuery { + message: "Invalid price".to_string(), + })?; + + // Image is optional - may be None if not yet generated + let image = item.get("image").and_then(|v| v.as_s().ok()).cloned(); + + // Parse optional nutritional info + let nutritional_info = item + .get("nutritional_info") + .and_then(|v| v.as_m().ok()) + .map(|nutrition_map| NutritionalInfo { + calories_per_serving: nutrition_map + .get("calories_per_serving") + .and_then(|v| v.as_n().ok()) + .and_then(|s| s.parse().ok()), + protein_percentage: nutrition_map + .get("protein_percentage") + .and_then(|v| v.as_n().ok()) + .and_then(|s| Decimal::from_str(s).ok()), + fat_percentage: nutrition_map + .get("fat_percentage") + .and_then(|v| v.as_n().ok()) + .and_then(|s| Decimal::from_str(s).ok()), + carbohydrate_percentage: nutrition_map + .get("carbohydrate_percentage") + .and_then(|v| v.as_n().ok()) + .and_then(|s| Decimal::from_str(s).ok()), + fiber_percentage: nutrition_map + .get("fiber_percentage") + .and_then(|v| v.as_n().ok()) + .and_then(|s| Decimal::from_str(s).ok()), + moisture_percentage: nutrition_map + .get("moisture_percentage") + .and_then(|v| v.as_n().ok()) + .and_then(|s| Decimal::from_str(s).ok()), + serving_size: nutrition_map + .get("serving_size") + .and_then(|v| v.as_s().ok()) + .cloned(), + servings_per_container: nutrition_map + .get("servings_per_container") + .and_then(|v| v.as_n().ok()) + .and_then(|s| s.parse().ok()), + }); + + // Parse ingredients list + let ingredients = item + .get("ingredients") + .and_then(|v| v.as_l().ok()) + .map(|list| list.iter().filter_map(|v| v.as_s().ok()).cloned().collect()) + .unwrap_or_default(); + + let feeding_guidelines = item + .get("feeding_guidelines") + .and_then(|v| v.as_s().ok()) + .cloned(); + + let availability_status = item + .get("availability_status") + .and_then(|v| v.as_s().ok()) + .and_then(|s| AvailabilityStatus::from_str(s).ok()) + .ok_or_else(|| RepositoryError::InvalidQuery { + message: "Invalid availability_status".to_string(), + })?; + + let stock_quantity = item + .get("stock_quantity") + .and_then(|v| v.as_n().ok()) + .and_then(|s| s.parse().ok()) + .ok_or_else(|| RepositoryError::InvalidQuery { + message: "Invalid stock_quantity".to_string(), + })?; + + let created_at = item + .get("created_at") + .and_then(|v| v.as_s().ok()) + .and_then(|s| DateTime::parse_from_rfc3339(s).ok()) + .map(|dt| dt.with_timezone(&chrono::Utc)) + .ok_or_else(|| RepositoryError::InvalidQuery { + message: "Invalid created_at".to_string(), + })?; + + let updated_at = item + .get("updated_at") + .and_then(|v| v.as_s().ok()) + .and_then(|s| DateTime::parse_from_rfc3339(s).ok()) + .map(|dt| dt.with_timezone(&chrono::Utc)) + .unwrap_or_else(|| { + // If updated_at is missing or invalid, use created_at as fallback + created_at + }); + + let is_active = item + .get("is_active") + .and_then(|v| v.as_bool().ok()) + .copied() + .unwrap_or(true); + + Ok(Food { + id, + pet_type, + name, + food_type, + description, + price, + image, + nutritional_info, + ingredients, + feeding_guidelines, + availability_status, + stock_quantity, + created_at, + updated_at, + is_active, + }) + } + + /// Convert DynamoDB error to RepositoryError + fn map_dynamodb_error(&self, error: DynamoDbError) -> RepositoryError { + error!("DynamoDB error: {:?}", error); + RepositoryError::AwsSdk { + message: error.to_string(), + } + } +} + +#[async_trait] +impl FoodRepository for DynamoDbFoodRepository { + #[instrument(skip(self), fields(table = %self.table_name))] + async fn find_all(&self, filters: FoodFilters) -> RepositoryResult> { + info!("Finding all foods with filters"); + + // If we have a pet_type filter, use the GSI for better performance + if let Some(pet_type) = filters.pet_type { + return self.find_by_pet_type(pet_type).await; + } + + // If we have a food_type filter, use the GSI for better performance + if let Some(food_type) = filters.food_type { + return self.find_by_food_type(food_type).await; + } + + // Otherwise, scan the table (less efficient but necessary for complex filters) + let mut scan_builder = self + .client + .scan() + .table_name(&self.table_name) + .select(Select::AllAttributes); + + // Add filter expressions for other criteria + let mut filter_expressions = Vec::new(); + let mut expression_attribute_values = HashMap::new(); + let mut expression_attribute_names = HashMap::new(); + + if let Some(status) = filters.availability_status { + filter_expressions.push("availability_status = :status".to_string()); + expression_attribute_values + .insert(":status".to_string(), AttributeValue::S(status.to_string())); + } + + if let Some(min_price) = filters.min_price { + filter_expressions.push("price >= :min_price".to_string()); + expression_attribute_values.insert( + ":min_price".to_string(), + AttributeValue::N(min_price.to_string()), + ); + } + + if let Some(max_price) = filters.max_price { + filter_expressions.push("price <= :max_price".to_string()); + expression_attribute_values.insert( + ":max_price".to_string(), + AttributeValue::N(max_price.to_string()), + ); + } + + if let Some(true) = filters.in_stock_only { + filter_expressions.push("is_active = :active AND stock_quantity > :zero".to_string()); + expression_attribute_values.insert(":active".to_string(), AttributeValue::Bool(true)); + expression_attribute_values + .insert(":zero".to_string(), AttributeValue::N("0".to_string())); + } + + if let Some(ref search_term) = filters.search_term { + filter_expressions + .push("contains(#name, :search) OR contains(description, :search)".to_string()); + expression_attribute_names.insert("#name".to_string(), "name".to_string()); + expression_attribute_values.insert( + ":search".to_string(), + AttributeValue::S(search_term.clone()), + ); + } + + if !filter_expressions.is_empty() { + scan_builder = scan_builder.filter_expression(filter_expressions.join(" AND ")); + } + + if !expression_attribute_values.is_empty() { + scan_builder = + scan_builder.set_expression_attribute_values(Some(expression_attribute_values)); + } + + if !expression_attribute_names.is_empty() { + scan_builder = + scan_builder.set_expression_attribute_names(Some(expression_attribute_names)); + } + + let response = scan_builder + .send() + .await + .map_err(|e| self.map_dynamodb_error(e.into()))?; + + let mut foods = Vec::new(); + if let Some(items) = response.items { + for item in items { + match self.item_to_food(item) { + Ok(food) => foods.push(food), + Err(e) => { + warn!("Failed to parse food item: {}", e); + continue; + } + } + } + } + + info!("Found {} foods", foods.len()); + Ok(foods) + } + + #[instrument(skip(self), fields(table = %self.table_name, id = %id))] + async fn find_by_id(&self, id: &str) -> RepositoryResult> { + info!("Finding food by ID"); + + // Create a DynamoDB subsegment + let get_span = self.create_dynamodb_span("GetItem"); + + let response = async { + let result = self + .client + .get_item() + .table_name(&self.table_name) + .key("id", AttributeValue::S(id.to_string())) + .send() + .await; + + // Record additional span attributes based on response + match &result { + Ok(output) => { + tracing::Span::current().record("http.status_code", 200); + if let Some(request_id) = output.request_id() { + tracing::Span::current().record("aws.request_id", request_id); + } + } + Err(e) => { + tracing::Span::current().record("http.status_code", 400); // Generic error code + error!("DynamoDB GetItem failed: {}", e); + } + } + + result.map_err(|e| self.map_dynamodb_error(e.into())) + } + .instrument(get_span) + .await?; + + match response.item { + Some(item) => { + let food = self.item_to_food(item)?; + info!("Food found"); + Ok(Some(food)) + } + None => { + info!("Food not found"); + Ok(None) + } + } + } + + #[instrument(skip(self), fields(table = %self.table_name, pet_type = %pet_type))] + async fn find_by_pet_type(&self, pet_type: PetType) -> RepositoryResult> { + info!("Finding foods by pet type using GSI"); + + // Create a DynamoDB subsegment + let query_span = self.create_dynamodb_span("Query"); + + let response = async { + self.client + .query() + .table_name(&self.table_name) + .index_name(&self.pet_type_index) + .key_condition_expression("pet_type = :pet_type") + .expression_attribute_values(":pet_type", AttributeValue::S(pet_type.to_string())) + .send() + .await + .map_err(|e| self.map_dynamodb_error(e.into())) + } + .instrument(query_span) + .await?; + + let mut foods = Vec::new(); + if let Some(items) = response.items { + for item in items { + match self.item_to_food(item) { + Ok(food) => foods.push(food), + Err(e) => { + warn!("Failed to parse food item: {}", e); + continue; + } + } + } + } + + info!("Found {} foods for pet type {}", foods.len(), pet_type); + Ok(foods) + } + + #[instrument(skip(self), fields(table = %self.table_name, food_type = %food_type))] + async fn find_by_food_type(&self, food_type: FoodType) -> RepositoryResult> { + info!("Finding foods by food type using GSI"); + + // Create a DynamoDB subsegment + let query_span = self.create_dynamodb_span("Query"); + + let response = async { + self.client + .query() + .table_name(&self.table_name) + .index_name(&self.food_type_index) + .key_condition_expression("food_type = :food_type") + .expression_attribute_values(":food_type", AttributeValue::S(food_type.to_string())) + .send() + .await + .map_err(|e| self.map_dynamodb_error(e.into())) + } + .instrument(query_span) + .await?; + + let mut foods = Vec::new(); + if let Some(items) = response.items { + for item in items { + match self.item_to_food(item) { + Ok(food) => foods.push(food), + Err(e) => { + warn!("Failed to parse food item: {}", e); + continue; + } + } + } + } + + info!("Found {} foods for food type {}", foods.len(), food_type); + Ok(foods) + } + + #[instrument(skip(self, food), fields(table = %self.table_name, id = %food.id))] + async fn create(&self, food: Food) -> RepositoryResult { + info!("Creating new food"); + + let item = self.food_to_item(&food); + + // Create a DynamoDB subsegment + let put_span = self.create_dynamodb_span("PutItem"); + + async { + self.client + .put_item() + .table_name(&self.table_name) + .set_item(Some(item)) + .condition_expression("attribute_not_exists(id)") + .send() + .await + .map_err(|e| self.map_dynamodb_error(e.into())) + } + .instrument(put_span) + .await?; + + info!("Food created successfully"); + Ok(food) + } + + #[instrument(skip(self, food), fields(table = %self.table_name, id = %food.id))] + async fn update(&self, food: Food) -> RepositoryResult { + info!("Updating food"); + + let item = self.food_to_item(&food); + + // Create a DynamoDB subsegment + let put_span = self.create_dynamodb_span("PutItem"); + + async { + self.client + .put_item() + .table_name(&self.table_name) + .set_item(Some(item)) + .condition_expression("attribute_exists(id)") + .send() + .await + .map_err(|e| self.map_dynamodb_error(e.into())) + } + .instrument(put_span) + .await?; + + info!("Food updated successfully"); + Ok(food) + } + + #[instrument(skip(self), fields(table = %self.table_name, id = %id))] + async fn soft_delete(&self, id: &str) -> RepositoryResult<()> { + info!("Soft deleting food"); + + // Create a DynamoDB subsegment + let update_span = self.create_dynamodb_span("UpdateItem"); + + async { + self.client + .update_item() + .table_name(&self.table_name) + .key("id", AttributeValue::S(id.to_string())) + .update_expression( + "SET is_active = :inactive, availability_status = :discontinued, updated_at = :now", + ) + .expression_attribute_values(":inactive", AttributeValue::Bool(false)) + .expression_attribute_values( + ":discontinued", + AttributeValue::S(AvailabilityStatus::Discontinued.to_string()), + ) + .expression_attribute_values(":now", AttributeValue::S(chrono::Utc::now().to_rfc3339())) + .condition_expression("attribute_exists(id)") + .send() + .await + .map_err(|e| self.map_dynamodb_error(e.into())) + } + .instrument(update_span) + .await?; + + info!("Food soft deleted successfully"); + Ok(()) + } + + #[instrument(skip(self), fields(table = %self.table_name, id = %id))] + async fn delete(&self, id: &str) -> RepositoryResult<()> { + info!("Hard deleting food"); + + // Create a DynamoDB subsegment + let delete_span = self.create_dynamodb_span("DeleteItem"); + + async { + self.client + .delete_item() + .table_name(&self.table_name) + .key("id", AttributeValue::S(id.to_string())) + .send() + .await + .map_err(|e| self.map_dynamodb_error(e.into()))?; + + info!("Food deleted successfully"); + Ok(()) + } + .instrument(delete_span) + .await + } + + #[instrument(skip(self), fields(table = %self.table_name, id = %id))] + async fn exists(&self, id: &str) -> RepositoryResult { + info!("Checking if food exists"); + + // Create a DynamoDB subsegment + let get_span = self.create_dynamodb_span("GetItem"); + + let response = async { + self.client + .get_item() + .table_name(&self.table_name) + .key("id", AttributeValue::S(id.to_string())) + .projection_expression("id") + .send() + .await + .map_err(|e| self.map_dynamodb_error(e.into())) + } + .instrument(get_span) + .await?; + + let exists = response.item.is_some(); + info!("Food exists: {}", exists); + Ok(exists) + } + + #[instrument(skip(self), fields(table = %self.table_name))] + async fn count(&self, filters: Option) -> RepositoryResult { + info!("Counting foods"); + + let mut scan_builder = self + .client + .scan() + .table_name(&self.table_name) + .select(Select::Count); + + // Apply filters if provided + if let Some(filters) = filters { + let mut filter_expressions = Vec::new(); + let mut expression_attribute_values = HashMap::new(); + + if let Some(pet_type) = filters.pet_type { + filter_expressions.push("pet_type = :pet_type".to_string()); + expression_attribute_values.insert( + ":pet_type".to_string(), + AttributeValue::S(pet_type.to_string()), + ); + } + + if let Some(food_type) = filters.food_type { + filter_expressions.push("food_type = :food_type".to_string()); + expression_attribute_values.insert( + ":food_type".to_string(), + AttributeValue::S(food_type.to_string()), + ); + } + + if let Some(status) = filters.availability_status { + filter_expressions.push("availability_status = :status".to_string()); + expression_attribute_values + .insert(":status".to_string(), AttributeValue::S(status.to_string())); + } + + if let Some(true) = filters.in_stock_only { + filter_expressions + .push("is_active = :active AND stock_quantity > :zero".to_string()); + expression_attribute_values + .insert(":active".to_string(), AttributeValue::Bool(true)); + expression_attribute_values + .insert(":zero".to_string(), AttributeValue::N("0".to_string())); + } + + if !filter_expressions.is_empty() { + scan_builder = scan_builder + .filter_expression(filter_expressions.join(" AND ")) + .set_expression_attribute_values(Some(expression_attribute_values)); + } + } + + let response = scan_builder + .send() + .await + .map_err(|e| self.map_dynamodb_error(e.into()))?; + + let count = response.count() as usize; + info!("Food count: {}", count); + Ok(count) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::models::{CreateFoodRequest, NutritionalInfo}; + use rust_decimal_macros::dec; + + fn create_test_food() -> Food { + let request = CreateFoodRequest { + pet_type: PetType::Puppy, + name: "Test Kibble".to_string(), + food_type: FoodType::Dry, + description: "Nutritious test food".to_string(), + price: dec!(12.99), + // No image field - will be generated via events + nutritional_info: Some(NutritionalInfo { + calories_per_serving: Some(350), + protein_percentage: Some(dec!(25.0)), + fat_percentage: Some(dec!(15.0)), + carbohydrate_percentage: Some(dec!(45.0)), + fiber_percentage: Some(dec!(5.0)), + moisture_percentage: Some(dec!(10.0)), + serving_size: Some("1 cup".to_string()), + servings_per_container: Some(20), + }), + ingredients: vec![ + "chicken".to_string(), + "rice".to_string(), + "vegetables".to_string(), + ], + feeding_guidelines: Some("Feed twice daily".to_string()), + stock_quantity: 10, + }; + Food::new(request) + } + + #[test] + fn test_food_to_item_conversion() { + let food = create_test_food(); + let config = aws_sdk_dynamodb::Config::builder() + .region(aws_sdk_dynamodb::config::Region::new("us-east-1")) + .behavior_version(aws_sdk_dynamodb::config::BehaviorVersion::latest()) + .build(); + let client = Arc::new(aws_sdk_dynamodb::Client::from_conf(config)); + let repo = + DynamoDbFoodRepository::new(client, "test-table".to_string(), "us-east-1".to_string()); + + let item = repo.food_to_item(&food); + + assert!(item.contains_key("id")); + assert!(item.contains_key("pet_type")); + assert!(item.contains_key("name")); + assert!(item.contains_key("nutritional_info")); + assert!(item.contains_key("ingredients")); + + // Verify specific values + if let Some(AttributeValue::S(pet_type)) = item.get("pet_type") { + assert_eq!(pet_type, "puppy"); + } else { + panic!("Expected string value for pet_type"); + } + + if let Some(AttributeValue::L(ingredients)) = item.get("ingredients") { + assert_eq!(ingredients.len(), 3); + } else { + panic!("Expected list value for ingredients"); + } + } + + #[test] + fn test_item_to_food_conversion() { + let food = create_test_food(); + let config = aws_sdk_dynamodb::Config::builder() + .region(aws_sdk_dynamodb::config::Region::new("us-east-1")) + .behavior_version(aws_sdk_dynamodb::config::BehaviorVersion::latest()) + .build(); + let client = Arc::new(aws_sdk_dynamodb::Client::from_conf(config)); + let repo = + DynamoDbFoodRepository::new(client, "test-table".to_string(), "us-east-1".to_string()); + + let item = repo.food_to_item(&food); + let converted_food = repo.item_to_food(item).unwrap(); + + assert_eq!(converted_food.id, food.id); + assert_eq!(converted_food.pet_type, food.pet_type); + assert_eq!(converted_food.name, food.name); + assert_eq!(converted_food.food_type, food.food_type); + assert_eq!(converted_food.price, food.price); + assert_eq!(converted_food.ingredients, food.ingredients); + assert_eq!(converted_food.stock_quantity, food.stock_quantity); + assert_eq!(converted_food.is_active, food.is_active); + + // Check nutritional info + assert!(converted_food.nutritional_info.is_some()); + let nutrition = converted_food.nutritional_info.unwrap(); + assert_eq!(nutrition.calories_per_serving, Some(350)); + assert_eq!(nutrition.protein_percentage, Some(dec!(25.0))); + } + + #[test] + fn test_item_to_food_conversion_missing_updated_at() { + let food = create_test_food(); + let config = aws_sdk_dynamodb::Config::builder() + .region(aws_sdk_dynamodb::config::Region::new("us-east-1")) + .behavior_version(aws_sdk_dynamodb::config::BehaviorVersion::latest()) + .build(); + let client = Arc::new(aws_sdk_dynamodb::Client::from_conf(config)); + let repo = + DynamoDbFoodRepository::new(client, "test-table".to_string(), "us-east-1".to_string()); + + let mut item = repo.food_to_item(&food); + + // Remove the updated_at field to simulate legacy data + item.remove("updated_at"); + + let converted_food = repo.item_to_food(item).unwrap(); + + assert_eq!(converted_food.id, food.id); + assert_eq!(converted_food.name, food.name); + assert_eq!(converted_food.pet_type, food.pet_type); + assert_eq!(converted_food.food_type, food.food_type); + assert_eq!(converted_food.price, food.price); + + // When updated_at is missing, it should fallback to created_at + assert_eq!(converted_food.updated_at, converted_food.created_at); + } + + #[test] + fn test_repository_creation() { + let config = aws_sdk_dynamodb::Config::builder() + .region(aws_sdk_dynamodb::config::Region::new("us-east-1")) + .behavior_version(aws_sdk_dynamodb::config::BehaviorVersion::latest()) + .build(); + let client = Arc::new(aws_sdk_dynamodb::Client::from_conf(config)); + let repo = + DynamoDbFoodRepository::new(client, "test-table".to_string(), "us-east-1".to_string()); + + assert_eq!(repo.table_name, "test-table"); + assert_eq!(repo.pet_type_index, "PetTypeIndex"); + assert_eq!(repo.food_type_index, "FoodTypeIndex"); + } + + // Note: Integration tests with actual DynamoDB would be in a separate test file + // using testcontainers or LocalStack for a real DynamoDB instance +} diff --git a/src/applications/microservices/petfood-rs/src/repositories/mod.rs b/src/applications/microservices/petfood-rs/src/repositories/mod.rs new file mode 100644 index 00000000..83501538 --- /dev/null +++ b/src/applications/microservices/petfood-rs/src/repositories/mod.rs @@ -0,0 +1,12 @@ +// Repositories module - data access layer + +pub mod cart_repository; +pub mod food_repository; +pub mod table_manager; + +#[cfg(test)] +mod tests; + +pub use cart_repository::{CartRepository, DynamoDbCartRepository}; +pub use food_repository::{DynamoDbFoodRepository, FoodRepository}; +pub use table_manager::TableManager; diff --git a/src/applications/microservices/petfood-rs/src/repositories/table_manager.rs b/src/applications/microservices/petfood-rs/src/repositories/table_manager.rs new file mode 100644 index 00000000..f396fe69 --- /dev/null +++ b/src/applications/microservices/petfood-rs/src/repositories/table_manager.rs @@ -0,0 +1,404 @@ +use aws_sdk_dynamodb::types::{ + AttributeDefinition, BillingMode, GlobalSecondaryIndex, KeySchemaElement, KeyType, Projection, + ProjectionType, ScalarAttributeType, TableStatus, +}; +use aws_sdk_dynamodb::{Client as DynamoDbClient, Error as DynamoDbError}; +use std::sync::Arc; +use std::time::Duration; +use tracing::{error, info, instrument, warn}; + +use crate::models::{RepositoryError, RepositoryResult}; + +/// Manages DynamoDB table creation and configuration +pub struct TableManager { + client: Arc, +} + +impl TableManager { + /// Create a new table manager + pub fn new(client: Arc) -> Self { + Self { client } + } + + /// Create the PetFoods table with GSIs + #[instrument(skip(self), fields(table_name = %table_name))] + pub async fn create_foods_table(&self, table_name: &str) -> RepositoryResult<()> { + info!("Creating PetFoods table"); + + // Check if table already exists + if self.table_exists(table_name).await? { + info!("Table {} already exists", table_name); + return Ok(()); + } + + // Define attribute definitions + let attribute_definitions = vec![ + AttributeDefinition::builder() + .attribute_name("id") + .attribute_type(ScalarAttributeType::S) + .build() + .map_err(|e| RepositoryError::AwsSdk { + message: format!("Failed to build attribute definition: {}", e), + })?, + AttributeDefinition::builder() + .attribute_name("pet_type") + .attribute_type(ScalarAttributeType::S) + .build() + .map_err(|e| RepositoryError::AwsSdk { + message: format!("Failed to build attribute definition: {}", e), + })?, + AttributeDefinition::builder() + .attribute_name("name") + .attribute_type(ScalarAttributeType::S) + .build() + .map_err(|e| RepositoryError::AwsSdk { + message: format!("Failed to build attribute definition: {}", e), + })?, + AttributeDefinition::builder() + .attribute_name("food_type") + .attribute_type(ScalarAttributeType::S) + .build() + .map_err(|e| RepositoryError::AwsSdk { + message: format!("Failed to build attribute definition: {}", e), + })?, + AttributeDefinition::builder() + .attribute_name("price") + .attribute_type(ScalarAttributeType::N) + .build() + .map_err(|e| RepositoryError::AwsSdk { + message: format!("Failed to build attribute definition: {}", e), + })?, + ]; + + // Define key schema for main table + let key_schema = vec![KeySchemaElement::builder() + .attribute_name("id") + .key_type(KeyType::Hash) + .build() + .map_err(|e| RepositoryError::AwsSdk { + message: format!("Failed to build key schema: {}", e), + })?]; + + // Define PetType GSI + let pet_type_gsi = GlobalSecondaryIndex::builder() + .index_name("PetTypeIndex") + .key_schema( + KeySchemaElement::builder() + .attribute_name("pet_type") + .key_type(KeyType::Hash) + .build() + .map_err(|e| RepositoryError::AwsSdk { + message: format!("Failed to build GSI key schema: {}", e), + })?, + ) + .key_schema( + KeySchemaElement::builder() + .attribute_name("name") + .key_type(KeyType::Range) + .build() + .map_err(|e| RepositoryError::AwsSdk { + message: format!("Failed to build GSI key schema: {}", e), + })?, + ) + .projection( + Projection::builder() + .projection_type(ProjectionType::All) + .build(), + ) + .build() + .map_err(|e| RepositoryError::AwsSdk { + message: format!("Failed to build GSI: {}", e), + })?; + + // Define FoodType GSI + let food_type_gsi = GlobalSecondaryIndex::builder() + .index_name("FoodTypeIndex") + .key_schema( + KeySchemaElement::builder() + .attribute_name("food_type") + .key_type(KeyType::Hash) + .build() + .map_err(|e| RepositoryError::AwsSdk { + message: format!("Failed to build GSI key schema: {}", e), + })?, + ) + .key_schema( + KeySchemaElement::builder() + .attribute_name("price") + .key_type(KeyType::Range) + .build() + .map_err(|e| RepositoryError::AwsSdk { + message: format!("Failed to build GSI key schema: {}", e), + })?, + ) + .projection( + Projection::builder() + .projection_type(ProjectionType::All) + .build(), + ) + .build() + .map_err(|e| RepositoryError::AwsSdk { + message: format!("Failed to build GSI: {}", e), + })?; + + // Create the table + self.client + .create_table() + .table_name(table_name) + .set_attribute_definitions(Some(attribute_definitions)) + .set_key_schema(Some(key_schema)) + .global_secondary_indexes(pet_type_gsi) + .global_secondary_indexes(food_type_gsi) + .billing_mode(BillingMode::PayPerRequest) + .send() + .await + .map_err(|e| self.map_dynamodb_error(e.into()))?; + + info!("Table creation initiated, waiting for table to become active"); + self.wait_for_table_active(table_name).await?; + info!("PetFoods table created successfully"); + + Ok(()) + } + + /// Create the PetFoodCarts table + #[instrument(skip(self), fields(table_name = %table_name))] + pub async fn create_carts_table(&self, table_name: &str) -> RepositoryResult<()> { + info!("Creating PetFoodCarts table"); + + // Check if table already exists + if self.table_exists(table_name).await? { + info!("Table {} already exists", table_name); + return Ok(()); + } + + // Define attribute definitions + let attribute_definitions = vec![AttributeDefinition::builder() + .attribute_name("user_id") + .attribute_type(ScalarAttributeType::S) + .build() + .map_err(|e| RepositoryError::AwsSdk { + message: format!("Failed to build attribute definition: {}", e), + })?]; + + // Define key schema + let key_schema = vec![KeySchemaElement::builder() + .attribute_name("user_id") + .key_type(KeyType::Hash) + .build() + .map_err(|e| RepositoryError::AwsSdk { + message: format!("Failed to build key schema: {}", e), + })?]; + + // Create the table + self.client + .create_table() + .table_name(table_name) + .set_attribute_definitions(Some(attribute_definitions)) + .set_key_schema(Some(key_schema)) + .billing_mode(BillingMode::PayPerRequest) + .send() + .await + .map_err(|e| self.map_dynamodb_error(e.into()))?; + + info!("Table creation initiated, waiting for table to become active"); + self.wait_for_table_active(table_name).await?; + info!("PetFoodCarts table created successfully"); + + Ok(()) + } + + /// Check if a table exists + #[instrument(skip(self), fields(table_name = %table_name))] + pub async fn table_exists(&self, table_name: &str) -> RepositoryResult { + match self + .client + .describe_table() + .table_name(table_name) + .send() + .await + { + Ok(_) => { + info!("Table {} exists", table_name); + Ok(true) + } + Err(e) => { + // Check if this is a ResourceNotFoundException (table doesn't exist) + let error_string = e.to_string(); + let error_debug = format!("{:?}", e); + + info!("DynamoDB error details: {}", error_string); + info!("DynamoDB error debug: {}", error_debug); + + // Check for various forms of "table not found" errors + if error_string.contains("ResourceNotFoundException") + || error_string.contains("Requested resource not found") + || error_string.contains("Table: ") && error_string.contains("not found") + || error_debug.contains("ResourceNotFoundException") + { + info!("Table {} does not exist", table_name); + Ok(false) + } else { + // For any other error, log and return the error + error!("Error checking table existence: {}", e); + Err(RepositoryError::ConnectionFailed) + } + } + } + } + + /// Wait for a table to become active + #[instrument(skip(self), fields(table_name = %table_name))] + async fn wait_for_table_active(&self, table_name: &str) -> RepositoryResult<()> { + let mut attempts = 0; + let max_attempts = 30; // 5 minutes with 10-second intervals + let wait_duration = Duration::from_secs(10); + + loop { + match self + .client + .describe_table() + .table_name(table_name) + .send() + .await + { + Ok(response) => { + if let Some(table) = response.table { + match table.table_status { + Some(TableStatus::Active) => { + info!("Table {} is now active", table_name); + return Ok(()); + } + Some(status) => { + info!("Table {} status: {:?}, waiting...", table_name, status); + } + None => { + warn!("Table {} status unknown, waiting...", table_name); + } + } + } + } + Err(e) => { + error!("Error checking table status: {}", e); + return Err(self.map_dynamodb_error(e.into())); + } + } + + attempts += 1; + if attempts >= max_attempts { + error!("Timeout waiting for table {} to become active", table_name); + return Err(RepositoryError::Timeout); + } + + tokio::time::sleep(wait_duration).await; + } + } + + /// Delete a table (for testing/cleanup) + #[instrument(skip(self), fields(table_name = %table_name))] + pub async fn delete_table(&self, table_name: &str) -> RepositoryResult<()> { + info!("Deleting table"); + + if !self.table_exists(table_name).await? { + info!("Table {} does not exist, nothing to delete", table_name); + return Ok(()); + } + + self.client + .delete_table() + .table_name(table_name) + .send() + .await + .map_err(|e| self.map_dynamodb_error(e.into()))?; + + info!("Table {} deletion initiated", table_name); + Ok(()) + } + + /// List all tables (for debugging/admin) + #[instrument(skip(self))] + pub async fn list_tables(&self) -> RepositoryResult> { + info!("Listing all tables"); + + let response = self + .client + .list_tables() + .send() + .await + .map_err(|e| self.map_dynamodb_error(e.into()))?; + + let table_names = response.table_names.unwrap_or_default(); + info!("Found {} tables", table_names.len()); + Ok(table_names) + } + + /// Get table description (for debugging/admin) + #[instrument(skip(self), fields(table_name = %table_name))] + pub async fn describe_table(&self, table_name: &str) -> RepositoryResult { + info!("Describing table"); + + let response = self + .client + .describe_table() + .table_name(table_name) + .send() + .await + .map_err(|e| self.map_dynamodb_error(e.into()))?; + + let description = format!("{:#?}", response.table); + info!("Table description retrieved"); + Ok(description) + } + + /// Create both tables (convenience method) + #[instrument(skip(self))] + pub async fn create_all_tables( + &self, + foods_table: &str, + carts_table: &str, + ) -> RepositoryResult<()> { + info!("Creating all tables"); + + // Create tables in parallel for better performance + let foods_future = self.create_foods_table(foods_table); + let carts_future = self.create_carts_table(carts_table); + + let (foods_result, carts_result) = tokio::join!(foods_future, carts_future); + + foods_result?; + carts_result?; + + info!("All tables created successfully"); + Ok(()) + } + + /// Convert DynamoDB error to RepositoryError + fn map_dynamodb_error(&self, error: DynamoDbError) -> RepositoryError { + error!("DynamoDB error: {:?}", error); + RepositoryError::AwsSdk { + message: error.to_string(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_table_manager_creation() { + let config = aws_sdk_dynamodb::Config::builder() + .region(aws_sdk_dynamodb::config::Region::new("us-east-1")) + .behavior_version(aws_sdk_dynamodb::config::BehaviorVersion::latest()) + .build(); + let client = Arc::new(aws_sdk_dynamodb::Client::from_conf(config)); + let _manager = TableManager::new(client); + + // Just verify the manager can be created + // Actual functionality tests would require integration testing with LocalStack + } + + // Note: Most tests for this module would be integration tests + // using testcontainers or LocalStack to test against a real DynamoDB instance + // Unit tests are limited due to the heavy AWS SDK integration +} diff --git a/src/applications/microservices/petfood-rs/src/repositories/tests.rs b/src/applications/microservices/petfood-rs/src/repositories/tests.rs new file mode 100644 index 00000000..ff25728e --- /dev/null +++ b/src/applications/microservices/petfood-rs/src/repositories/tests.rs @@ -0,0 +1,622 @@ +#[cfg(test)] +mod repository_tests { + use crate::models::{ + AvailabilityStatus, Cart, CreateFoodRequest, Food, FoodType, NutritionalInfo, PetType, + RepositoryError, + }; + use aws_sdk_dynamodb::types::AttributeValue; + use rust_decimal_macros::dec; + use std::collections::HashMap; + use std::sync::Arc; + + use crate::repositories::cart_repository::*; + use crate::repositories::food_repository::*; + + fn create_test_client() -> Arc { + let config = aws_sdk_dynamodb::Config::builder() + .region(aws_sdk_dynamodb::config::Region::new("us-east-1")) + .behavior_version(aws_sdk_dynamodb::config::BehaviorVersion::latest()) + .build(); + Arc::new(aws_sdk_dynamodb::Client::from_conf(config)) + } + + fn create_test_food() -> Food { + let request = CreateFoodRequest { + pet_type: PetType::Puppy, + name: "Premium Puppy Kibble".to_string(), + food_type: FoodType::Dry, + description: "High-quality dry food for growing puppies".to_string(), + price: dec!(24.99), + // No image field - will be generated via events + nutritional_info: Some(NutritionalInfo { + calories_per_serving: Some(380), + protein_percentage: Some(dec!(28.0)), + fat_percentage: Some(dec!(18.0)), + carbohydrate_percentage: Some(dec!(42.0)), + fiber_percentage: Some(dec!(4.0)), + moisture_percentage: Some(dec!(8.0)), + serving_size: Some("1 cup".to_string()), + servings_per_container: Some(30), + }), + ingredients: vec![ + "chicken meal".to_string(), + "brown rice".to_string(), + "sweet potato".to_string(), + "chicken fat".to_string(), + ], + feeding_guidelines: Some("Feed 1-2 cups daily based on weight".to_string()), + stock_quantity: 50, + }; + Food::new(request) + } + + fn create_test_cart() -> Cart { + let mut cart = Cart::new("test-user-123".to_string()); + cart.add_item("F001".to_string(), 2, dec!(24.99)); + cart.add_item("F002".to_string(), 1, dec!(15.50)); + cart.add_item("F003".to_string(), 3, dec!(8.99)); + cart + } + + mod food_repository_tests { + use super::*; + + #[test] + fn test_food_to_item_conversion_complete() { + let food = create_test_food(); + let client = create_test_client(); + let repo = DynamoDbFoodRepository::new( + client, + "test-foods".to_string(), + "us-east-1".to_string(), + ); + let item = repo.food_to_item(&food); + + // Verify all required fields are present + assert!(item.contains_key("id")); + assert!(item.contains_key("pet_type")); + assert!(item.contains_key("name")); + assert!(item.contains_key("food_type")); + assert!(item.contains_key("description")); + assert!(item.contains_key("price")); + // Image field is now optional - only present if set + // assert!(item.contains_key("image")); + assert!(item.contains_key("nutritional_info")); + assert!(item.contains_key("ingredients")); + assert!(item.contains_key("feeding_guidelines")); + assert!(item.contains_key("availability_status")); + assert!(item.contains_key("stock_quantity")); + assert!(item.contains_key("created_at")); + assert!(item.contains_key("updated_at")); + assert!(item.contains_key("is_active")); + + // Verify specific values + if let Some(AttributeValue::S(pet_type)) = item.get("pet_type") { + assert_eq!(pet_type, "puppy"); + } else { + panic!("Expected string value for pet_type"); + } + + if let Some(AttributeValue::N(price)) = item.get("price") { + assert_eq!(price, "24.99"); + } else { + panic!("Expected number value for price"); + } + + if let Some(AttributeValue::L(ingredients)) = item.get("ingredients") { + assert_eq!(ingredients.len(), 4); + if let AttributeValue::S(first_ingredient) = &ingredients[0] { + assert_eq!(first_ingredient, "chicken meal"); + } + } else { + panic!("Expected list value for ingredients"); + } + + if let Some(AttributeValue::M(nutrition)) = item.get("nutritional_info") { + assert!(nutrition.contains_key("calories_per_serving")); + assert!(nutrition.contains_key("protein_percentage")); + + if let Some(AttributeValue::N(calories)) = nutrition.get("calories_per_serving") { + assert_eq!(calories, "380"); + } + } else { + panic!("Expected map value for nutritional_info"); + } + + if let Some(AttributeValue::Bool(active)) = item.get("is_active") { + assert!(active); + } else { + panic!("Expected boolean value for is_active"); + } + } + + #[test] + fn test_item_to_food_conversion_roundtrip() { + let original_food = create_test_food(); + let client = create_test_client(); + + let repo = DynamoDbFoodRepository::new( + client, + "test-foods".to_string(), + "us-east-1".to_string(), + ); + + // Convert to item and back + let item = repo.food_to_item(&original_food); + let converted_food = repo.item_to_food(item).unwrap(); + + // Verify all fields match + assert_eq!(converted_food.id, original_food.id); + assert_eq!(converted_food.pet_type, original_food.pet_type); + assert_eq!(converted_food.name, original_food.name); + assert_eq!(converted_food.food_type, original_food.food_type); + assert_eq!(converted_food.description, original_food.description); + assert_eq!(converted_food.price, original_food.price); + assert_eq!(converted_food.image, original_food.image); + assert_eq!(converted_food.ingredients, original_food.ingredients); + assert_eq!( + converted_food.feeding_guidelines, + original_food.feeding_guidelines + ); + assert_eq!( + converted_food.availability_status, + original_food.availability_status + ); + assert_eq!(converted_food.stock_quantity, original_food.stock_quantity); + assert_eq!(converted_food.is_active, original_food.is_active); + + // Verify nutritional info + assert!(converted_food.nutritional_info.is_some()); + let converted_nutrition = converted_food.nutritional_info.unwrap(); + let original_nutrition = original_food.nutritional_info.unwrap(); + + assert_eq!( + converted_nutrition.calories_per_serving, + original_nutrition.calories_per_serving + ); + assert_eq!( + converted_nutrition.protein_percentage, + original_nutrition.protein_percentage + ); + assert_eq!( + converted_nutrition.fat_percentage, + original_nutrition.fat_percentage + ); + assert_eq!( + converted_nutrition.serving_size, + original_nutrition.serving_size + ); + assert_eq!( + converted_nutrition.servings_per_container, + original_nutrition.servings_per_container + ); + + // Timestamps should be preserved (within reasonable precision) + let created_diff = (converted_food.created_at - original_food.created_at) + .num_milliseconds() + .abs(); + let updated_diff = (converted_food.updated_at - original_food.updated_at) + .num_milliseconds() + .abs(); + + assert!( + created_diff < 1000, + "Created timestamp difference too large: {}ms", + created_diff + ); + assert!( + updated_diff < 1000, + "Updated timestamp difference too large: {}ms", + updated_diff + ); + } + + #[test] + fn test_food_without_optional_fields() { + let request = CreateFoodRequest { + pet_type: PetType::Kitten, + name: "Basic Cat Food".to_string(), + food_type: FoodType::Wet, + description: "Simple wet food for kittens".to_string(), + price: dec!(5.99), + // No image field - will be generated via events + nutritional_info: None, + ingredients: vec!["fish".to_string()], + feeding_guidelines: None, + stock_quantity: 0, + }; + + let food = Food::new(request); + let client = create_test_client(); + + let repo = DynamoDbFoodRepository::new( + client, + "test-foods".to_string(), + "us-east-1".to_string(), + ); + + let item = repo.food_to_item(&food); + let converted_food = repo.item_to_food(item).unwrap(); + + assert_eq!(converted_food.name, "Basic Cat Food"); + assert_eq!(converted_food.pet_type, PetType::Kitten); + assert_eq!( + converted_food.availability_status, + AvailabilityStatus::OutOfStock + ); + assert!(converted_food.nutritional_info.is_none()); + assert!(converted_food.feeding_guidelines.is_none()); + assert_eq!(converted_food.stock_quantity, 0); + } + + #[test] + fn test_repository_index_names() { + let client = create_test_client(); + + let repo = DynamoDbFoodRepository::new( + client, + "PetFoods".to_string(), + "us-east-1".to_string(), + ); + + assert_eq!(repo.table_name(), "PetFoods"); + assert_eq!(repo.pet_type_index(), "PetTypeIndex"); + assert_eq!(repo.food_type_index(), "FoodTypeIndex"); + } + } + + mod cart_repository_tests { + use super::*; + + #[test] + fn test_cart_to_item_conversion_complete() { + let cart = create_test_cart(); + let client = create_test_client(); + + let repo = DynamoDbCartRepository::new( + client, + "test-carts".to_string(), + "us-east-1".to_string(), + ); + let item = repo.cart_to_item(&cart); + + // Verify all required fields are present + assert!(item.contains_key("user_id")); + assert!(item.contains_key("items")); + assert!(item.contains_key("created_at")); + assert!(item.contains_key("updated_at")); + + // Verify user_id + if let Some(AttributeValue::S(user_id)) = item.get("user_id") { + assert_eq!(user_id, "test-user-123"); + } else { + panic!("Expected string value for user_id"); + } + + // Verify items structure + if let Some(AttributeValue::L(items)) = item.get("items") { + assert_eq!(items.len(), 3); + + // Check first item structure + if let AttributeValue::M(first_item) = &items[0] { + assert!(first_item.contains_key("food_id")); + assert!(first_item.contains_key("quantity")); + assert!(first_item.contains_key("unit_price")); + assert!(first_item.contains_key("added_at")); + + if let Some(AttributeValue::S(food_id)) = first_item.get("food_id") { + assert_eq!(food_id, "F001"); + } + + if let Some(AttributeValue::N(quantity)) = first_item.get("quantity") { + assert_eq!(quantity, "2"); + } + + if let Some(AttributeValue::N(price)) = first_item.get("unit_price") { + assert_eq!(price, "24.99"); + } + } else { + panic!("Expected map value for cart item"); + } + } else { + panic!("Expected list value for items"); + } + } + + #[test] + fn test_item_to_cart_conversion_roundtrip() { + let original_cart = create_test_cart(); + let client = create_test_client(); + + let repo = DynamoDbCartRepository::new( + client, + "test-carts".to_string(), + "us-east-1".to_string(), + ); + + // Convert to item and back + let item = repo.cart_to_item(&original_cart); + let converted_cart = repo.item_to_cart(item).unwrap(); + + // Verify basic fields + assert_eq!(converted_cart.user_id, original_cart.user_id); + assert_eq!(converted_cart.items.len(), original_cart.items.len()); + + // Verify each cart item + for (i, (original_item, converted_item)) in original_cart + .items + .iter() + .zip(converted_cart.items.iter()) + .enumerate() + { + assert_eq!( + converted_item.food_id, original_item.food_id, + "Item {} food_id mismatch", + i + ); + assert_eq!( + converted_item.quantity, original_item.quantity, + "Item {} quantity mismatch", + i + ); + assert_eq!( + converted_item.unit_price, original_item.unit_price, + "Item {} price mismatch", + i + ); + + // Timestamps should be preserved (within reasonable precision) + let time_diff = (converted_item.added_at - original_item.added_at) + .num_milliseconds() + .abs(); + assert!( + time_diff < 1000, + "Item {} timestamp difference too large: {}ms", + i, + time_diff + ); + } + + // Verify cart totals + assert_eq!(converted_cart.total_items(), original_cart.total_items()); + assert_eq!(converted_cart.total_price(), original_cart.total_price()); + + // Timestamps should be preserved + let created_diff = (converted_cart.created_at - original_cart.created_at) + .num_milliseconds() + .abs(); + let updated_diff = (converted_cart.updated_at - original_cart.updated_at) + .num_milliseconds() + .abs(); + + assert!( + created_diff < 1000, + "Created timestamp difference too large: {}ms", + created_diff + ); + assert!( + updated_diff < 1000, + "Updated timestamp difference too large: {}ms", + updated_diff + ); + } + + #[test] + fn test_empty_cart_conversion() { + let empty_cart = Cart::new("empty-user".to_string()); + let client = create_test_client(); + + let repo = DynamoDbCartRepository::new( + client, + "test-carts".to_string(), + "us-east-1".to_string(), + ); + + let item = repo.cart_to_item(&empty_cart); + let converted_cart = repo.item_to_cart(item).unwrap(); + + assert_eq!(converted_cart.user_id, "empty-user"); + assert!(converted_cart.items.is_empty()); + assert_eq!(converted_cart.total_items(), 0); + assert_eq!(converted_cart.total_price(), dec!(0)); + } + + #[test] + fn test_map_to_cart_item_valid() { + let client = create_test_client(); + + let repo = DynamoDbCartRepository::new( + client, + "test-carts".to_string(), + "us-east-1".to_string(), + ); + + let mut item_map = HashMap::new(); + item_map.insert("food_id".to_string(), AttributeValue::S("F123".to_string())); + item_map.insert("quantity".to_string(), AttributeValue::N("5".to_string())); + item_map.insert( + "unit_price".to_string(), + AttributeValue::N("19.99".to_string()), + ); + item_map.insert( + "added_at".to_string(), + AttributeValue::S(chrono::Utc::now().to_rfc3339()), + ); + + let cart_item = repo.map_to_cart_item(&item_map).unwrap(); + + assert_eq!(cart_item.food_id, "F123"); + assert_eq!(cart_item.quantity, 5); + assert_eq!(cart_item.unit_price, dec!(19.99)); + assert_eq!(cart_item.total_price(), dec!(99.95)); + } + + #[test] + fn test_map_to_cart_item_missing_fields() { + let client = create_test_client(); + + let repo = DynamoDbCartRepository::new( + client, + "test-carts".to_string(), + "us-east-1".to_string(), + ); + + // Test missing food_id + let mut incomplete_map = HashMap::new(); + incomplete_map.insert("quantity".to_string(), AttributeValue::N("2".to_string())); + incomplete_map.insert( + "unit_price".to_string(), + AttributeValue::N("10.00".to_string()), + ); + + let result = repo.map_to_cart_item(&incomplete_map); + assert!(result.is_err()); + + if let Err(RepositoryError::InvalidQuery { message }) = result { + assert!(message.contains("Missing food_id")); + } else { + panic!("Expected InvalidQuery error for missing food_id"); + } + } + + #[test] + fn test_map_to_cart_item_invalid_types() { + let client = create_test_client(); + + let repo = DynamoDbCartRepository::new( + client, + "test-carts".to_string(), + "us-east-1".to_string(), + ); + + // Test invalid quantity (string instead of number) + let mut invalid_map = HashMap::new(); + invalid_map.insert("food_id".to_string(), AttributeValue::S("F123".to_string())); + invalid_map.insert( + "quantity".to_string(), + AttributeValue::S("not-a-number".to_string()), + ); + invalid_map.insert( + "unit_price".to_string(), + AttributeValue::N("10.00".to_string()), + ); + invalid_map.insert( + "added_at".to_string(), + AttributeValue::S(chrono::Utc::now().to_rfc3339()), + ); + + let result = repo.map_to_cart_item(&invalid_map); + assert!(result.is_err()); + } + } + + pub mod integration_test_helpers { + use super::*; + + /// Helper function to create test data for integration tests + #[allow(dead_code)] + pub fn create_sample_foods() -> Vec { + vec![ + create_puppy_food(), + create_kitten_food(), + create_bunny_food(), + ] + } + + #[allow(dead_code)] + fn create_puppy_food() -> Food { + let request = CreateFoodRequest { + pet_type: PetType::Puppy, + name: "Puppy Growth Formula".to_string(), + food_type: FoodType::Dry, + description: "Complete nutrition for growing puppies".to_string(), + price: dec!(29.99), + // No image field - will be generated via events + nutritional_info: Some(NutritionalInfo { + calories_per_serving: Some(420), + protein_percentage: Some(dec!(30.0)), + fat_percentage: Some(dec!(20.0)), + carbohydrate_percentage: Some(dec!(38.0)), + fiber_percentage: Some(dec!(4.0)), + moisture_percentage: Some(dec!(8.0)), + serving_size: Some("1.5 cups".to_string()), + servings_per_container: Some(25), + }), + ingredients: vec![ + "deboned chicken".to_string(), + "chicken meal".to_string(), + "sweet potato".to_string(), + "peas".to_string(), + ], + feeding_guidelines: Some("Feed 2-3 times daily".to_string()), + stock_quantity: 100, + }; + Food::new(request) + } + + #[allow(dead_code)] + fn create_kitten_food() -> Food { + let request = CreateFoodRequest { + pet_type: PetType::Kitten, + name: "Kitten Salmon Pate".to_string(), + food_type: FoodType::Wet, + description: "Rich salmon pate for growing kittens".to_string(), + price: dec!(1.99), + // No image field - will be generated via events + nutritional_info: None, + ingredients: vec![ + "salmon".to_string(), + "chicken broth".to_string(), + "carrots".to_string(), + ], + feeding_guidelines: Some("Feed 3-4 cans daily".to_string()), + stock_quantity: 200, + }; + Food::new(request) + } + + #[allow(dead_code)] + fn create_bunny_food() -> Food { + let request = CreateFoodRequest { + pet_type: PetType::Bunny, + name: "Timothy Hay Pellets".to_string(), + food_type: FoodType::Dry, + description: "High-fiber pellets made from timothy hay".to_string(), + price: dec!(12.50), + // No image field - will be generated via events + nutritional_info: Some(NutritionalInfo { + calories_per_serving: Some(250), + protein_percentage: Some(dec!(14.0)), + fat_percentage: Some(dec!(3.0)), + carbohydrate_percentage: Some(dec!(45.0)), + fiber_percentage: Some(dec!(25.0)), + moisture_percentage: Some(dec!(13.0)), + serving_size: Some("1/4 cup".to_string()), + servings_per_container: Some(80), + }), + ingredients: vec![ + "timothy hay".to_string(), + "alfalfa meal".to_string(), + "wheat middlings".to_string(), + ], + feeding_guidelines: Some("Feed 1/4 cup per 5 lbs body weight".to_string()), + stock_quantity: 75, + }; + Food::new(request) + } + + /// Helper to create a cart with mixed items + #[allow(dead_code)] + pub fn create_mixed_cart() -> Cart { + let mut cart = Cart::new("integration-test-user".to_string()); + cart.add_item("F001".to_string(), 1, dec!(29.99)); // Puppy food + cart.add_item("F002".to_string(), 6, dec!(1.99)); // Kitten food (6 cans) + cart.add_item("F003".to_string(), 2, dec!(12.50)); // Bunny food + cart + } + } +} + +// Re-export test helpers for integration tests +// #[cfg(test)] +// pub use repository_tests::integration_test_helpers; diff --git a/src/applications/microservices/petfood-rs/src/services/cart_service.rs b/src/applications/microservices/petfood-rs/src/services/cart_service.rs new file mode 100644 index 00000000..e7bfcaa1 --- /dev/null +++ b/src/applications/microservices/petfood-rs/src/services/cart_service.rs @@ -0,0 +1,1101 @@ +use std::str::FromStr; +use std::sync::Arc; +use tracing::{info, instrument, warn}; + +use crate::models::{ + AddCartItemRequest, Cart, CartItem, CartItemResponse, CartResponse, ServiceError, + ServiceResult, UpdateCartItemRequest, +}; +use crate::repositories::{CartRepository, FoodRepository}; + +/// Service for managing shopping carts +pub struct CartService { + cart_repository: Arc, + food_repository: Arc, + assets_cdn_url: String, +} + +impl CartService { + /// Create a new CartService + pub fn new( + cart_repository: Arc, + food_repository: Arc, + assets_cdn_url: String, + ) -> Self { + Self { + cart_repository, + food_repository, + assets_cdn_url, + } + } + + /// Get a user's cart + #[instrument(skip(self), fields(user_id = %user_id))] + pub async fn get_cart(&self, user_id: &str) -> ServiceResult { + info!("Getting cart for user"); + + // Validate user_id + if user_id.trim().is_empty() { + return Err(ServiceError::ValidationError { + message: "User ID cannot be empty".to_string(), + }); + } + + let cart = match self.cart_repository.find_cart(user_id).await? { + Some(cart) => cart, + None => { + info!("Cart not found, creating empty cart"); + Cart::new(user_id.to_string()) + } + }; + + // Convert to response with food details + let cart_response = self.cart_to_response(cart).await?; + + info!("Cart retrieved with {} items", cart_response.items.len()); + Ok(cart_response) + } + + /// Add an item to the cart + #[instrument(skip(self, request), fields(user_id = %user_id, food_id = %request.food_id, quantity = request.quantity))] + pub async fn add_item( + &self, + user_id: &str, + request: AddCartItemRequest, + ) -> ServiceResult { + info!("Adding item to cart"); + + // Validate inputs + self.validate_user_id(user_id)?; + self.validate_add_cart_item_request(&request)?; + + // Check if food exists and is available + let food = match self.food_repository.find_by_id(&request.food_id).await? { + Some(food) => food, + None => { + return Err(ServiceError::FoodNotFound { + id: request.food_id, + }); + } + }; + + // Check availability and stock + if !food.is_available() { + return Err(ServiceError::ProductUnavailable { + food_id: food.id.clone(), + }); + } + + if food.stock_quantity < request.quantity { + return Err(ServiceError::InsufficientStock { + requested: request.quantity, + available: food.stock_quantity, + }); + } + + // Get or create cart + let mut cart = match self.cart_repository.find_cart(user_id).await? { + Some(cart) => cart, + None => Cart::new(user_id.to_string()), + }; + + // Add item to cart + cart.add_item(request.food_id.clone(), request.quantity, food.price); + + // Save cart + let updated_cart = self.cart_repository.save_cart(cart).await?; + + // Find the added item and convert to response + let cart_item = updated_cart.get_item(&request.food_id).ok_or_else(|| { + ServiceError::CartItemNotFound { + food_id: request.food_id.clone(), + user_id: user_id.to_string(), + } + })?; + + let item_response = self.cart_item_to_response(cart_item, &food).await?; + + info!("Item added to cart successfully"); + Ok(item_response) + } + + /// Update the quantity of an item in the cart + #[instrument(skip(self, request), fields(user_id = %user_id, food_id = %food_id, quantity = request.quantity))] + pub async fn update_item( + &self, + user_id: &str, + food_id: &str, + request: UpdateCartItemRequest, + ) -> ServiceResult { + info!("Updating cart item quantity"); + + // Validate inputs + self.validate_user_id(user_id)?; + self.validate_food_id(food_id)?; + self.validate_quantity(request.quantity)?; + + // Get cart + let mut cart = match self.cart_repository.find_cart(user_id).await? { + Some(cart) => cart, + None => { + return Err(ServiceError::CartNotFound { + user_id: user_id.to_string(), + }); + } + }; + + // Check if item exists in cart + if !cart.contains_item(food_id) { + return Err(ServiceError::CartItemNotFound { + food_id: food_id.to_string(), + user_id: user_id.to_string(), + }); + } + + // Get food details for validation + let food = match self.food_repository.find_by_id(food_id).await? { + Some(food) => food, + None => { + return Err(ServiceError::FoodNotFound { + id: food_id.to_string(), + }); + } + }; + + // Check stock availability for new quantity + if request.quantity > 0 && food.stock_quantity < request.quantity { + return Err(ServiceError::InsufficientStock { + requested: request.quantity, + available: food.stock_quantity, + }); + } + + // Update item quantity + cart.update_item_quantity(food_id, request.quantity); + + // Save cart + let updated_cart = self.cart_repository.save_cart(cart).await?; + + // If quantity was set to 0, the item was removed + if request.quantity == 0 { + info!("Item removed from cart (quantity set to 0)"); + return Err(ServiceError::CartItemNotFound { + food_id: food_id.to_string(), + user_id: user_id.to_string(), + }); + } + + // Get updated item and convert to response + let cart_item = + updated_cart + .get_item(food_id) + .ok_or_else(|| ServiceError::CartItemNotFound { + food_id: food_id.to_string(), + user_id: user_id.to_string(), + })?; + + let item_response = self.cart_item_to_response(cart_item, &food).await?; + + info!("Cart item updated successfully"); + Ok(item_response) + } + + /// Remove an item from the cart + #[instrument(skip(self), fields(user_id = %user_id, food_id = %food_id))] + pub async fn remove_item(&self, user_id: &str, food_id: &str) -> ServiceResult<()> { + info!("Removing item from cart"); + + // Validate inputs + self.validate_user_id(user_id)?; + self.validate_food_id(food_id)?; + + // Get cart + let mut cart = match self.cart_repository.find_cart(user_id).await? { + Some(cart) => cart, + None => { + return Err(ServiceError::CartNotFound { + user_id: user_id.to_string(), + }); + } + }; + + // Check if item exists in cart + if !cart.contains_item(food_id) { + return Err(ServiceError::CartItemNotFound { + food_id: food_id.to_string(), + user_id: user_id.to_string(), + }); + } + + // Remove item + cart.remove_item(food_id); + + // Save cart + self.cart_repository.save_cart(cart).await?; + + info!("Item removed from cart successfully"); + Ok(()) + } + + /// Clear all items from the cart + #[instrument(skip(self), fields(user_id = %user_id))] + pub async fn clear_cart(&self, user_id: &str) -> ServiceResult<()> { + info!("Clearing cart"); + + // Validate user_id + self.validate_user_id(user_id)?; + + // Get cart + let mut cart = match self.cart_repository.find_cart(user_id).await? { + Some(cart) => cart, + None => { + // Cart doesn't exist, nothing to clear + info!("Cart not found, nothing to clear"); + return Ok(()); + } + }; + + // Clear all items + cart.clear(); + + // Save empty cart + self.cart_repository.save_cart(cart).await?; + + info!("Cart cleared successfully"); + Ok(()) + } + + /// Delete the entire cart + #[instrument(skip(self), fields(user_id = %user_id))] + pub async fn delete_cart(&self, user_id: &str) -> ServiceResult<()> { + info!("Deleting cart"); + + // Validate user_id + self.validate_user_id(user_id)?; + + // Check if cart exists + if !self.cart_repository.cart_exists(user_id).await? { + return Err(ServiceError::CartNotFound { + user_id: user_id.to_string(), + }); + } + + // Delete cart + self.cart_repository.delete_cart(user_id).await?; + + info!("Cart deleted successfully"); + Ok(()) + } + + /// Get the total number of items in a user's cart + #[instrument(skip(self), fields(user_id = %user_id))] + pub async fn get_cart_item_count(&self, user_id: &str) -> ServiceResult { + info!("Getting cart item count"); + + let cart_response = self.get_cart(user_id).await?; + let count = cart_response.total_items; + + info!("Cart item count: {}", count); + Ok(count) + } + + /// Get the total price of items in a user's cart + #[instrument(skip(self), fields(user_id = %user_id))] + pub async fn get_cart_total(&self, user_id: &str) -> ServiceResult { + info!("Getting cart total"); + + let cart_response = self.get_cart(user_id).await?; + let total = cart_response.total_price; + + info!("Cart total: {}", total); + Ok(total) + } + + /// Check if a user has any items in their cart + #[instrument(skip(self), fields(user_id = %user_id))] + pub async fn is_cart_empty(&self, user_id: &str) -> ServiceResult { + info!("Checking if cart is empty"); + + let cart = match self.cart_repository.find_cart(user_id).await? { + Some(cart) => cart, + None => return Ok(true), + }; + + let is_empty = cart.is_empty(); + info!("Cart is empty: {}", is_empty); + Ok(is_empty) + } + + /// Validate cart contents (check availability and stock) + #[instrument(skip(self), fields(user_id = %user_id))] + pub async fn validate_cart(&self, user_id: &str) -> ServiceResult> { + info!("Validating cart contents"); + + let cart = match self.cart_repository.find_cart(user_id).await? { + Some(cart) => cart, + None => return Ok(vec![]), + }; + + let mut issues = Vec::new(); + + for item in &cart.items { + match self.food_repository.find_by_id(&item.food_id).await? { + Some(food) => { + if !food.is_available() { + issues.push(format!("Product {} is no longer available", food.name)); + } else if food.stock_quantity < item.quantity { + issues.push(format!( + "Insufficient stock for {}: requested {}, available {}", + food.name, item.quantity, food.stock_quantity + )); + } + } + None => { + issues.push(format!("Product with ID {} no longer exists", item.food_id)); + } + } + } + + info!("Cart validation found {} issues", issues.len()); + Ok(issues) + } + + /// Convert Cart to CartResponse with food details + async fn cart_to_response(&self, cart: Cart) -> ServiceResult { + let mut items = Vec::new(); + + for cart_item in &cart.items { + match self.food_repository.find_by_id(&cart_item.food_id).await? { + Some(food) => { + let item_response = self.cart_item_to_response(cart_item, &food).await?; + items.push(item_response); + } + None => { + warn!("Food not found for cart item: {}", cart_item.food_id); + // Create a response for missing food with empty image + let item_response = CartItemResponse { + food_id: cart_item.food_id.clone(), + food_name: "Product not found".to_string(), + food_image: "".to_string(), // Empty string when no image available + quantity: cart_item.quantity, + unit_price: cart_item.unit_price, + total_price: cart_item.total_price(), + is_available: false, + added_at: cart_item.added_at, + }; + items.push(item_response); + } + } + } + + let total_items = cart.total_items(); + let total_price = cart.total_price(); + let created_at = cart.created_at; + let updated_at = cart.updated_at; + let user_id = cart.user_id; + + Ok(CartResponse { + user_id, + items, + total_items, + total_price, + created_at, + updated_at, + }) + } + + /// Convert CartItem to CartItemResponse with food details + async fn cart_item_to_response( + &self, + cart_item: &CartItem, + food: &crate::models::Food, + ) -> ServiceResult { + // Handle optional image with CDN URL generation + let food_image = match &food.image { + Some(image_path) => { + if self.assets_cdn_url.is_empty() { + image_path.clone() + } else { + // Handle trailing slash in CDN URL to avoid double slashes + let cdn_url = if self.assets_cdn_url.ends_with('/') { + self.assets_cdn_url.trim_end_matches('/') + } else { + &self.assets_cdn_url + }; + format!("{}/{}", cdn_url, image_path) + } + } + None => "".to_string(), + }; + + Ok(CartItemResponse { + food_id: cart_item.food_id.clone(), + food_name: food.name.clone(), + food_image, + quantity: cart_item.quantity, + unit_price: cart_item.unit_price, + total_price: cart_item.total_price(), + is_available: food.is_available(), + added_at: cart_item.added_at, + }) + } + + /// Validate user ID + fn validate_user_id(&self, user_id: &str) -> ServiceResult<()> { + if user_id.trim().is_empty() { + return Err(ServiceError::ValidationError { + message: "User ID cannot be empty".to_string(), + }); + } + Ok(()) + } + + /// Validate food ID + fn validate_food_id(&self, food_id: &str) -> ServiceResult<()> { + if food_id.trim().is_empty() { + return Err(ServiceError::ValidationError { + message: "Food ID cannot be empty".to_string(), + }); + } + Ok(()) + } + + /// Validate quantity + fn validate_quantity(&self, quantity: u32) -> ServiceResult<()> { + if quantity == 0 { + return Err(ServiceError::InvalidQuantity { quantity }); + } + if quantity > 100 { + return Err(ServiceError::ValidationError { + message: "Quantity cannot exceed 100".to_string(), + }); + } + Ok(()) + } + + /// Validate add cart item request + fn validate_add_cart_item_request(&self, request: &AddCartItemRequest) -> ServiceResult<()> { + self.validate_food_id(&request.food_id)?; + self.validate_quantity(request.quantity)?; + Ok(()) + } + + /// Checkout cart and create order + #[instrument(skip(self, request), fields(user_id = %user_id))] + pub async fn checkout( + &self, + user_id: &str, + request: crate::models::CheckoutRequest, + ) -> ServiceResult { + use crate::models::{CheckoutResponse, OrderItem, OrderStatus}; + + info!("Processing checkout for user"); + + // Validate user_id + self.validate_user_id(user_id)?; + + // Get the user's cart + let cart = match self.cart_repository.find_cart(user_id).await? { + Some(cart) => cart, + None => { + return Err(ServiceError::CartNotFound { + user_id: user_id.to_string(), + }); + } + }; + + // Check if cart is empty + if cart.is_empty() { + return Err(ServiceError::ValidationError { + message: "Cannot checkout empty cart".to_string(), + }); + } + + // Validate all items in cart and check availability + let mut order_items = Vec::new(); + let mut subtotal = rust_decimal::Decimal::ZERO; + + for cart_item in &cart.items { + // Get food details + let food = match self.food_repository.find_by_id(&cart_item.food_id).await? { + Some(food) => food, + None => { + return Err(ServiceError::FoodNotFound { + id: cart_item.food_id.clone(), + }); + } + }; + + // Check if food is available + if !food.is_available() { + return Err(ServiceError::ProductUnavailable { + food_id: cart_item.food_id.clone(), + }); + } + + // Check stock availability + if food.stock_quantity < cart_item.quantity { + return Err(ServiceError::InsufficientStock { + requested: cart_item.quantity, + available: food.stock_quantity, + }); + } + + // Create order item + let order_item = OrderItem { + food_id: cart_item.food_id.clone(), + food_name: food.name.clone(), + quantity: cart_item.quantity, + unit_price: cart_item.unit_price, + total_price: cart_item.total_price(), + }; + + subtotal += cart_item.total_price(); + order_items.push(order_item); + } + + // Calculate totals (simplified calculation) + let tax_rate = rust_decimal::Decimal::from_str("0.08").unwrap(); // 8% tax + let tax = subtotal * tax_rate; + let shipping = if subtotal >= rust_decimal::Decimal::from(50) { + rust_decimal::Decimal::ZERO // Free shipping over $50 + } else { + rust_decimal::Decimal::from(5) // $5 shipping + }; + let total_amount = subtotal + tax + shipping; + + // Generate order ID + let order_id = format!( + "ORDER-{}-{}", + user_id.to_uppercase(), + chrono::Utc::now().timestamp() + ); + + // Get payment method string + let payment_method_str = match &request.payment_method { + crate::models::PaymentMethod::CreditCard { .. } => "Credit Card", + crate::models::PaymentMethod::PayPal { .. } => "PayPal", + crate::models::PaymentMethod::BankTransfer { .. } => "Bank Transfer", + }; + + // Calculate estimated delivery (3-5 business days) + let estimated_delivery = Some(chrono::Utc::now() + chrono::Duration::days(4)); + + // Create checkout response + let checkout_response = CheckoutResponse { + order_id: order_id.clone(), + user_id: user_id.to_string(), + items: order_items, + subtotal, + tax, + shipping, + total_amount, + payment_method: payment_method_str.to_string(), + status: OrderStatus::Confirmed, + created_at: chrono::Utc::now(), + estimated_delivery, + }; + + // Clear the cart after successful checkout + let mut empty_cart = cart.clone(); + empty_cart.clear(); + self.cart_repository.save_cart(empty_cart).await?; + + info!("Checkout completed successfully for order: {}", order_id); + Ok(checkout_response) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::models::{CreateFoodRequest, Food, FoodType, PetType, RepositoryError}; + use crate::repositories::{CartRepository, FoodRepository}; + use async_trait::async_trait; + use mockall::mock; + use rust_decimal_macros::dec; + + // Mock repositories for testing + mock! { + TestCartRepository {} + + #[async_trait] + impl CartRepository for TestCartRepository { + async fn find_cart(&self, user_id: &str) -> Result, RepositoryError>; + async fn save_cart(&self, cart: Cart) -> Result; + async fn delete_cart(&self, user_id: &str) -> Result<(), RepositoryError>; + async fn cart_exists(&self, user_id: &str) -> Result; + async fn find_all_carts(&self) -> Result, RepositoryError>; + async fn count_carts(&self) -> Result; + } + } + + mock! { + TestFoodRepository {} + + #[async_trait] + impl FoodRepository for TestFoodRepository { + async fn find_all(&self, filters: crate::models::FoodFilters) -> Result, RepositoryError>; + async fn find_by_id(&self, food_id: &str) -> Result, RepositoryError>; + async fn find_by_pet_type(&self, pet_type: PetType) -> Result, RepositoryError>; + async fn find_by_food_type(&self, food_type: FoodType) -> Result, RepositoryError>; + async fn create(&self, food: Food) -> Result; + async fn update(&self, food: Food) -> Result; + async fn soft_delete(&self, food_id: &str) -> Result<(), RepositoryError>; + async fn delete(&self, food_id: &str) -> Result<(), RepositoryError>; + async fn exists(&self, food_id: &str) -> Result; + async fn count(&self, filters: Option) -> Result; + } + } + + fn create_test_food() -> Food { + let request = CreateFoodRequest { + pet_type: PetType::Puppy, + name: "Test Kibble".to_string(), + food_type: FoodType::Dry, + description: "Nutritious test food".to_string(), + price: dec!(12.99), + // No image field - will be generated via events + nutritional_info: None, + ingredients: vec!["chicken".to_string(), "rice".to_string()], + feeding_guidelines: Some("Feed twice daily".to_string()), + stock_quantity: 10, + }; + let mut food = Food::new(request); + food.id = "F001".to_string(); + // Set image for tests that expect specific image URLs + food.set_image("test.jpg".to_string()); + food + } + + fn create_test_cart() -> Cart { + let mut cart = Cart::new("user123".to_string()); + cart.add_item("F001".to_string(), 2, dec!(12.99)); + cart + } + + #[tokio::test] + async fn test_get_cart_existing() { + let mut mock_cart_repo = MockTestCartRepository::new(); + let mut mock_food_repo = MockTestFoodRepository::new(); + let test_cart = create_test_cart(); + let test_food = create_test_food(); + + mock_cart_repo + .expect_find_cart() + .with(mockall::predicate::eq("user123".to_string())) + .times(1) + .returning(move |_| Ok(Some(test_cart.clone()))); + + mock_food_repo + .expect_find_by_id() + .with(mockall::predicate::eq("F001".to_string())) + .times(1) + .returning(move |_| Ok(Some(test_food.clone()))); + + let service = CartService::new( + Arc::new(mock_cart_repo), + Arc::new(mock_food_repo), + "https://test-cdn.example.com".to_string(), + ); + + let result = service.get_cart("user123").await; + + assert!(result.is_ok()); + let cart_response = result.unwrap(); + assert_eq!(cart_response.user_id, "user123"); + assert_eq!(cart_response.items.len(), 1); + assert_eq!(cart_response.total_items, 2); + } + + #[tokio::test] + async fn test_get_cart_not_found() { + let mut mock_cart_repo = MockTestCartRepository::new(); + let mock_food_repo = MockTestFoodRepository::new(); + + mock_cart_repo + .expect_find_cart() + .with(mockall::predicate::eq("user123".to_string())) + .times(1) + .returning(|_| Ok(None)); + + let service = CartService::new( + Arc::new(mock_cart_repo), + Arc::new(mock_food_repo), + "https://test-cdn.example.com".to_string(), + ); + + let result = service.get_cart("user123").await; + + assert!(result.is_ok()); + let cart_response = result.unwrap(); + assert_eq!(cart_response.user_id, "user123"); + assert_eq!(cart_response.items.len(), 0); + assert_eq!(cart_response.total_items, 0); + } + + #[tokio::test] + async fn test_add_item_success() { + let mut mock_cart_repo = MockTestCartRepository::new(); + let mut mock_food_repo = MockTestFoodRepository::new(); + let test_food = create_test_food(); + + mock_cart_repo + .expect_find_cart() + .with(mockall::predicate::eq("user123".to_string())) + .times(1) + .returning(|_| Ok(None)); + + mock_cart_repo.expect_save_cart().times(1).returning(Ok); + + mock_food_repo + .expect_find_by_id() + .with(mockall::predicate::eq("F001".to_string())) + .times(1) + .returning(move |_| Ok(Some(test_food.clone()))); + + let service = CartService::new( + Arc::new(mock_cart_repo), + Arc::new(mock_food_repo), + "https://test-cdn.example.com".to_string(), + ); + + let request = AddCartItemRequest { + food_id: "F001".to_string(), + quantity: 2, + }; + + let result = service.add_item("user123", request).await; + + assert!(result.is_ok()); + let item_response = result.unwrap(); + assert_eq!(item_response.food_id, "F001"); + assert_eq!(item_response.quantity, 2); + assert!(item_response.is_available); + } + + #[tokio::test] + async fn test_add_item_food_not_found() { + let mock_cart_repo = MockTestCartRepository::new(); + let mut mock_food_repo = MockTestFoodRepository::new(); + + mock_food_repo + .expect_find_by_id() + .with(mockall::predicate::eq("F999".to_string())) + .times(1) + .returning(|_| Ok(None)); + + let service = CartService::new( + Arc::new(mock_cart_repo), + Arc::new(mock_food_repo), + "https://test-cdn.example.com".to_string(), + ); + + let request = AddCartItemRequest { + food_id: "F999".to_string(), + quantity: 1, + }; + + let result = service.add_item("user123", request).await; + + assert!(result.is_err()); + match result.unwrap_err() { + ServiceError::FoodNotFound { id } => { + assert_eq!(id, "F999"); + } + _ => panic!("Expected FoodNotFound error"), + } + } + + #[tokio::test] + async fn test_add_item_insufficient_stock() { + let mock_cart_repo = MockTestCartRepository::new(); + let mut mock_food_repo = MockTestFoodRepository::new(); + let mut test_food = create_test_food(); + test_food.stock_quantity = 1; // Only 1 in stock + + mock_food_repo + .expect_find_by_id() + .with(mockall::predicate::eq("F001".to_string())) + .times(1) + .returning(move |_| Ok(Some(test_food.clone()))); + + let service = CartService::new( + Arc::new(mock_cart_repo), + Arc::new(mock_food_repo), + "https://test-cdn.example.com".to_string(), + ); + + let request = AddCartItemRequest { + food_id: "F001".to_string(), + quantity: 5, // Requesting more than available + }; + + let result = service.add_item("user123", request).await; + + assert!(result.is_err()); + match result.unwrap_err() { + ServiceError::InsufficientStock { + requested, + available, + } => { + assert_eq!(requested, 5); + assert_eq!(available, 1); + } + _ => panic!("Expected InsufficientStock error"), + } + } + + #[tokio::test] + async fn test_update_item_success() { + let mut mock_cart_repo = MockTestCartRepository::new(); + let mut mock_food_repo = MockTestFoodRepository::new(); + let test_cart = create_test_cart(); + let test_food = create_test_food(); + + mock_cart_repo + .expect_find_cart() + .with(mockall::predicate::eq("user123".to_string())) + .times(1) + .returning(move |_| Ok(Some(test_cart.clone()))); + + mock_cart_repo.expect_save_cart().times(1).returning(Ok); + + mock_food_repo + .expect_find_by_id() + .with(mockall::predicate::eq("F001".to_string())) + .times(1) + .returning(move |_| Ok(Some(test_food.clone()))); + + let service = CartService::new( + Arc::new(mock_cart_repo), + Arc::new(mock_food_repo), + "https://test-cdn.example.com".to_string(), + ); + + let request = UpdateCartItemRequest { quantity: 5 }; + + let result = service.update_item("user123", "F001", request).await; + + assert!(result.is_ok()); + let item_response = result.unwrap(); + assert_eq!(item_response.quantity, 5); + } + + #[tokio::test] + async fn test_remove_item_success() { + let mut mock_cart_repo = MockTestCartRepository::new(); + let mock_food_repo = MockTestFoodRepository::new(); + let test_cart = create_test_cart(); + + mock_cart_repo + .expect_find_cart() + .with(mockall::predicate::eq("user123".to_string())) + .times(1) + .returning(move |_| Ok(Some(test_cart.clone()))); + + mock_cart_repo.expect_save_cart().times(1).returning(Ok); + + let service = CartService::new( + Arc::new(mock_cart_repo), + Arc::new(mock_food_repo), + "https://test-cdn.example.com".to_string(), + ); + + let result = service.remove_item("user123", "F001").await; + + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_clear_cart_success() { + let mut mock_cart_repo = MockTestCartRepository::new(); + let mock_food_repo = MockTestFoodRepository::new(); + let test_cart = create_test_cart(); + + mock_cart_repo + .expect_find_cart() + .with(mockall::predicate::eq("user123".to_string())) + .times(1) + .returning(move |_| Ok(Some(test_cart.clone()))); + + mock_cart_repo.expect_save_cart().times(1).returning(Ok); + + let service = CartService::new( + Arc::new(mock_cart_repo), + Arc::new(mock_food_repo), + "https://test-cdn.example.com".to_string(), + ); + + let result = service.clear_cart("user123").await; + + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_delete_cart_success() { + let mut mock_cart_repo = MockTestCartRepository::new(); + let mock_food_repo = MockTestFoodRepository::new(); + + mock_cart_repo + .expect_cart_exists() + .with(mockall::predicate::eq("user123".to_string())) + .times(1) + .returning(|_| Ok(true)); + + mock_cart_repo + .expect_delete_cart() + .with(mockall::predicate::eq("user123".to_string())) + .times(1) + .returning(|_| Ok(())); + + let service = CartService::new( + Arc::new(mock_cart_repo), + Arc::new(mock_food_repo), + "https://test-cdn.example.com".to_string(), + ); + + let result = service.delete_cart("user123").await; + + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_validate_cart_with_issues() { + let mut mock_cart_repo = MockTestCartRepository::new(); + let mut mock_food_repo = MockTestFoodRepository::new(); + let test_cart = create_test_cart(); + let mut test_food = create_test_food(); + test_food.stock_quantity = 1; // Less than cart quantity + + mock_cart_repo + .expect_find_cart() + .with(mockall::predicate::eq("user123".to_string())) + .times(1) + .returning(move |_| Ok(Some(test_cart.clone()))); + + mock_food_repo + .expect_find_by_id() + .with(mockall::predicate::eq("F001".to_string())) + .times(1) + .returning(move |_| Ok(Some(test_food.clone()))); + + let service = CartService::new( + Arc::new(mock_cart_repo), + Arc::new(mock_food_repo), + "https://test-cdn.example.com".to_string(), + ); + + let result = service.validate_cart("user123").await; + + assert!(result.is_ok()); + let issues = result.unwrap(); + assert!(!issues.is_empty()); + assert!(issues[0].contains("Insufficient stock")); + } + + #[tokio::test] + async fn test_validation_errors() { + let mock_cart_repo = MockTestCartRepository::new(); + let mock_food_repo = MockTestFoodRepository::new(); + let service = CartService::new( + Arc::new(mock_cart_repo), + Arc::new(mock_food_repo), + "https://test-cdn.example.com".to_string(), + ); + + // Test empty user ID + let result = service.get_cart("").await; + assert!(result.is_err()); + + // Test invalid quantity + let request = AddCartItemRequest { + food_id: "F001".to_string(), + quantity: 0, + }; + let result = service.add_item("user123", request).await; + assert!(result.is_err()); + + // Test excessive quantity + let request = AddCartItemRequest { + food_id: "F001".to_string(), + quantity: 101, + }; + let result = service.add_item("user123", request).await; + assert!(result.is_err()); + } + + #[tokio::test] + async fn test_cart_item_response_includes_cdn_url() { + let mut mock_cart_repo = MockTestCartRepository::new(); + let mut mock_food_repo = MockTestFoodRepository::new(); + let test_cart = create_test_cart(); + let test_food = create_test_food(); + + mock_cart_repo + .expect_find_cart() + .with(mockall::predicate::eq("user123".to_string())) + .times(1) + .returning(move |_| Ok(Some(test_cart.clone()))); + + mock_food_repo + .expect_find_by_id() + .with(mockall::predicate::eq("F001".to_string())) + .times(1) + .returning(move |_| Ok(Some(test_food.clone()))); + + let cdn_url = "https://petfood-assets.s3.amazonaws.com"; + let service = CartService::new( + Arc::new(mock_cart_repo), + Arc::new(mock_food_repo), + cdn_url.to_string(), + ); + + let result = service.get_cart("user123").await; + + assert!(result.is_ok()); + let cart_response = result.unwrap(); + assert_eq!(cart_response.items.len(), 1); + + let item = &cart_response.items[0]; + assert!(item.food_image.starts_with(cdn_url)); + assert_eq!(item.food_image, format!("{}/test.jpg", cdn_url)); + } + + #[tokio::test] + async fn test_cart_item_response_with_empty_cdn_url() { + let mut mock_cart_repo = MockTestCartRepository::new(); + let mut mock_food_repo = MockTestFoodRepository::new(); + let test_cart = create_test_cart(); + let test_food = create_test_food(); + + mock_cart_repo + .expect_find_cart() + .with(mockall::predicate::eq("user123".to_string())) + .times(1) + .returning(move |_| Ok(Some(test_cart.clone()))); + + mock_food_repo + .expect_find_by_id() + .with(mockall::predicate::eq("F001".to_string())) + .times(1) + .returning(move |_| Ok(Some(test_food.clone()))); + + // Test with empty CDN URL + let service = CartService::new( + Arc::new(mock_cart_repo), + Arc::new(mock_food_repo), + "".to_string(), // Empty CDN URL + ); + + let result = service.get_cart("user123").await; + + assert!(result.is_ok()); + let cart_response = result.unwrap(); + assert_eq!(cart_response.items.len(), 1); + + let item = &cart_response.items[0]; + // Should return the original image path without CDN prefix + assert_eq!(item.food_image, "test.jpg"); + } +} diff --git a/src/applications/microservices/petfood-rs/src/services/event_emitter.rs b/src/applications/microservices/petfood-rs/src/services/event_emitter.rs new file mode 100644 index 00000000..a1863953 --- /dev/null +++ b/src/applications/microservices/petfood-rs/src/services/event_emitter.rs @@ -0,0 +1,451 @@ +use crate::models::{EventConfig, EventPayload, FoodEvent, SpanContextData}; + +#[cfg(test)] +use crate::models::CreationSource; +use aws_sdk_eventbridge::operation::RequestId; +use aws_sdk_eventbridge::{Client as EventBridgeClient, Error as EventBridgeError}; +use aws_smithy_runtime_api::client::result::SdkError; +use aws_smithy_runtime_api::http::Response; +use std::sync::Arc; +use std::time::Duration; +use thiserror::Error; +use tokio::time::sleep; +use tracing::{error, info, instrument, warn, Instrument}; + +/// Errors that can occur during event emission +#[derive(Error, Debug)] +pub enum EventEmitterError { + #[error("EventBridge client error: {0}")] + EventBridge(#[from] EventBridgeError), + #[error("EventBridge SDK error: {0}")] + EventBridgeSdk( + #[from] SdkError, + ), + #[error("Serialization error: {0}")] + Serialization(#[from] serde_json::Error), + #[error("Event emission disabled")] + Disabled, + #[error("Maximum retry attempts exceeded")] + MaxRetriesExceeded, + #[error("Invalid configuration: {0}")] + InvalidConfig(String), +} + +/// Service responsible for emitting events to AWS EventBridge +#[derive(Clone)] +pub struct EventEmitter { + client: Arc, + config: EventConfig, +} + +impl EventEmitter { + /// Create a new EventEmitter instance + #[allow(clippy::result_large_err)] + pub fn new(client: EventBridgeClient, config: EventConfig) -> Result { + // Validate configuration + if config.event_bus_name.is_empty() { + return Err(EventEmitterError::InvalidConfig( + "Event bus name cannot be empty".to_string(), + )); + } + + if config.source_name.is_empty() { + return Err(EventEmitterError::InvalidConfig( + "Source name cannot be empty".to_string(), + )); + } + + Ok(Self { + client: Arc::new(client), + config, + }) + } + + /// Emit a food event to EventBridge with retry logic + #[instrument( + skip(self, event), + fields( + event_type = %event.event_type, + food_id = %event.food_id, + event_bus = %self.config.event_bus_name, + source = %self.config.source_name, + span.kind = "client" + ) + )] + pub async fn emit_event(&self, event: FoodEvent) -> Result<(), EventEmitterError> { + if !self.config.enabled { + warn!("Event emission is disabled, skipping event"); + return Err(EventEmitterError::Disabled); + } + + let payload: EventPayload = event.into(); + + // Emit with retry logic + self.emit_with_retry(payload).await + } + + /// Emit event with exponential backoff retry logic + async fn emit_with_retry(&self, payload: EventPayload) -> Result<(), EventEmitterError> { + let mut attempts = 0; + let max_attempts = self.config.retry_attempts; + + while attempts < max_attempts { + match self.send_to_eventbridge(&payload).await { + Ok(_) => { + info!( + event_type = %payload.detail_type, + food_id = %payload.detail.food_id, + attempt = attempts + 1, + "Event successfully emitted to EventBridge" + ); + return Ok(()); + } + Err(e) => { + attempts += 1; + + if attempts >= max_attempts { + error!( + event_type = %payload.detail_type, + food_id = %payload.detail.food_id, + attempts = attempts, + error = %e, + "Failed to emit event after maximum retry attempts" + ); + return Err(EventEmitterError::MaxRetriesExceeded); + } + + let delay = Duration::from_millis(100 * 2_u64.pow(attempts - 1)); + warn!( + event_type = %payload.detail_type, + food_id = %payload.detail.food_id, + attempt = attempts, + delay_ms = delay.as_millis(), + error = %e, + "Event emission failed, retrying" + ); + + sleep(delay).await; + } + } + } + + Err(EventEmitterError::MaxRetriesExceeded) + } + + /// Create an EventBridge subsegment span with proper X-Ray attributes + fn create_eventbridge_span(&self, operation: &str) -> tracing::Span { + let region = self + .client + .config() + .region() + .map(|r| r.as_ref()) + .unwrap_or("unknown"); + + tracing::info_span!( + "EventBridge", + // AWS X-Ray specific attributes + "aws.service" = "EventBridge", + "aws.operation" = operation, + "aws.region" = %region, + "aws.eventbridge.event_bus_name" = %self.config.event_bus_name, + "aws.request_id" = tracing::field::Empty, + "aws.agent" = "rust-aws-sdk", + + // Resource identification for X-Ray + "aws.remote.service" = "AWS::EventBridge", + "aws.remote.operation" = operation, + "aws.remote.resource.type" = "AWS::EventBridge::EventBus", + "aws.remote.resource.identifier" = %self.config.event_bus_name, + "remote.resource.cfn.primary.identifier" = %self.config.event_bus_name, + + // EventBridge-specific attributes + "event_bus_name" = %self.config.event_bus_name, + "event_bus.name" = %self.config.event_bus_name, + "resource_names" = format!("[{}]", self.config.event_bus_name), + "endpoint" = format!("https://events.{}.amazonaws.com", region), + + // OpenTelemetry semantic conventions + "otel.kind" = "client", + "otel.name" = format!("EventBridge.{}", operation), + + // RPC semantic conventions for AWS API calls + "rpc.system" = "aws-api", + "rpc.service" = "AmazonEventBridge", + "rpc.method" = operation, + + // HTTP semantic conventions (AWS APIs are HTTP-based) + "http.method" = "POST", + "http.url" = format!("https://events.{}.amazonaws.com", region), + "http.status_code" = tracing::field::Empty, + + // Messaging semantic conventions + "messaging.system" = "aws_eventbridge", + "messaging.destination.name" = %self.config.event_bus_name, + "messaging.operation" = "publish", + + // Component identification for X-Ray + "component" = "aws-sdk-eventbridge", + ) + } + + /// Send event to EventBridge + #[instrument( + skip(self, payload), + fields( + event_source = %payload.source, + event_type = %payload.detail_type, + food_id = %payload.detail.food_id + ) + )] + async fn send_to_eventbridge(&self, payload: &EventPayload) -> Result<(), EventEmitterError> { + let detail_json = serde_json::to_string(&payload.detail)?; + + // Convert chrono DateTime to AWS DateTime + let aws_time = aws_smithy_types::DateTime::from_secs(payload.time.timestamp()); + + let mut entry_builder = aws_sdk_eventbridge::types::PutEventsRequestEntry::builder() + .source(&payload.source) + .detail_type(&payload.detail_type) + .detail(detail_json) + .time(aws_time); + + // Add resources one by one + for resource in &payload.resources { + entry_builder = entry_builder.resources(resource); + } + + let entry = entry_builder.build(); + + // Create EventBridge remote span + let put_events_span = self.create_eventbridge_span("PutEvents"); + + let response = async { + let result = self.client.put_events().entries(entry).send().await; + + // Record additional span attributes based on response + match &result { + Ok(output) => { + tracing::Span::current().record("http.status_code", 200); + if let Some(request_id) = output.request_id() { + tracing::Span::current().record("aws.request_id", request_id); + } + + // Record batch information + let entries = output.entries(); + tracing::Span::current() + .record("messaging.batch.message_count", entries.len() as u64); + + // Count failed entries + let failed_count = entries + .iter() + .filter(|entry| entry.error_code().is_some()) + .count(); + + if failed_count > 0 { + tracing::Span::current() + .record("eventbridge.failed_entries", failed_count as u64); + tracing::Span::current().record("error", true); + } + + tracing::Span::current().record( + "eventbridge.successful_entries", + (entries.len() - failed_count) as u64, + ); + } + Err(e) => { + tracing::Span::current().record("http.status_code", 400); // Generic error code + tracing::Span::current().record("error", true); + tracing::Span::current().record("error.message", e.to_string().as_str()); + error!("EventBridge PutEvents failed: {}", e); + } + } + + result.map_err(EventEmitterError::EventBridgeSdk) + } + .instrument(put_events_span) + .await?; + + // Check for failed entries and handle them + let entries = response.entries(); + let mut failed_count = 0; + for entry in entries { + if let Some(error_code) = entry.error_code() { + failed_count += 1; + error!( + error_code = error_code, + error_message = entry.error_message().unwrap_or("Unknown error"), + "EventBridge entry failed" + ); + } + } + + if failed_count > 0 { + let error_msg = format!("EventBridge had {} failed entries", failed_count); + return Err(EventEmitterError::InvalidConfig(error_msg)); + } + + Ok(()) + } + + /// Extract OpenTelemetry span context from current tracing span + pub fn extract_span_context() -> SpanContextData { + use opentelemetry::trace::TraceContextExt; + use tracing_opentelemetry::OpenTelemetrySpanExt; + + let current_span = tracing::Span::current(); + let context = current_span.context(); + + let span = context.span(); + let span_context = span.span_context(); + if span_context.is_valid() { + SpanContextData { + trace_id: format!("{:032x}", span_context.trace_id()), + span_id: format!("{:016x}", span_context.span_id()), + trace_flags: format!("{:02x}", span_context.trace_flags()), + } + } else { + // Fallback to default if no active span + SpanContextData::default() + } + } + + /// Get current configuration + pub fn config(&self) -> &EventConfig { + &self.config + } + + /// Update configuration (useful for feature flags) + #[allow(clippy::result_large_err)] + pub fn update_config(&mut self, config: EventConfig) -> Result<(), EventEmitterError> { + if config.event_bus_name.is_empty() { + return Err(EventEmitterError::InvalidConfig( + "Event bus name cannot be empty".to_string(), + )); + } + + if config.source_name.is_empty() { + return Err(EventEmitterError::InvalidConfig( + "Source name cannot be empty".to_string(), + )); + } + + self.config = config; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::models::{FoodType, PetType}; + use aws_sdk_eventbridge::config::Region; + + #[tokio::test] + async fn test_event_emitter_creation() { + let config = aws_sdk_eventbridge::Config::builder() + .region(Region::new("us-east-1")) + .build(); + let client = EventBridgeClient::from_conf(config); + + let event_config = EventConfig::default(); + let emitter = EventEmitter::new(client, event_config); + + assert!(emitter.is_ok()); + } + + #[tokio::test] + async fn test_invalid_config() { + let config = aws_sdk_eventbridge::Config::builder() + .region(Region::new("us-east-1")) + .build(); + let client = EventBridgeClient::from_conf(config); + + let event_config = EventConfig { + event_bus_name: "".to_string(), + ..Default::default() + }; + + let emitter = EventEmitter::new(client, event_config); + + assert!(matches!(emitter, Err(EventEmitterError::InvalidConfig(_)))); + } + + #[test] + fn test_span_context_extraction() { + // Test default span context when no active span + let span_context = EventEmitter::extract_span_context(); + + // Should return default values when no active span + assert_eq!(span_context.trace_id.len(), 32); + assert_eq!(span_context.span_id.len(), 16); + assert_eq!(span_context.trace_flags.len(), 2); + } + + #[tokio::test] + async fn test_disabled_event_emission() { + let config = aws_sdk_eventbridge::Config::builder() + .region(Region::new("us-east-1")) + .build(); + let client = EventBridgeClient::from_conf(config); + + let event_config = EventConfig { + enabled: false, + ..Default::default() + }; + + let emitter = EventEmitter::new(client, event_config).unwrap(); + + let event = FoodEvent::food_item_created( + "test-id".to_string(), + "Test Food".to_string(), + PetType::Puppy, + FoodType::Dry, + None, + None, + CreationSource::AdminApi, + SpanContextData::default(), + ); + + let result = emitter.emit_event(event).await; + assert!(matches!(result, Err(EventEmitterError::Disabled))); + } + + #[test] + fn test_config_update() { + let config = aws_sdk_eventbridge::Config::builder() + .region(Region::new("us-east-1")) + .build(); + let client = EventBridgeClient::from_conf(config); + + let event_config = EventConfig::default(); + let mut emitter = EventEmitter::new(client, event_config).unwrap(); + + let new_config = EventConfig { + retry_attempts: 5, + ..Default::default() + }; + + let result = emitter.update_config(new_config); + assert!(result.is_ok()); + assert_eq!(emitter.config().retry_attempts, 5); + } + + #[test] + fn test_invalid_config_update() { + let config = aws_sdk_eventbridge::Config::builder() + .region(Region::new("us-east-1")) + .build(); + let client = EventBridgeClient::from_conf(config); + + let event_config = EventConfig::default(); + let mut emitter = EventEmitter::new(client, event_config).unwrap(); + + let invalid_config = EventConfig { + event_bus_name: "".to_string(), + ..Default::default() + }; + + let result = emitter.update_config(invalid_config); + assert!(matches!(result, Err(EventEmitterError::InvalidConfig(_)))); + } +} diff --git a/src/applications/microservices/petfood-rs/src/services/food_service.rs b/src/applications/microservices/petfood-rs/src/services/food_service.rs new file mode 100644 index 00000000..ff0457c5 --- /dev/null +++ b/src/applications/microservices/petfood-rs/src/services/food_service.rs @@ -0,0 +1,984 @@ +use std::sync::Arc; +use tracing::{instrument, warn}; + +use crate::models::{ + CreateFoodRequest, CreationSource, Food, FoodEvent, FoodFilters, FoodListResponse, FoodType, + PetType, ServiceError, ServiceResult, UpdateFoodRequest, +}; +use crate::repositories::FoodRepository; +use crate::services::EventEmitter; + +/// Service for managing food products +pub struct FoodService { + repository: Arc, + event_emitter: Option>, +} + +impl FoodService { + /// Create a new FoodService + pub fn new(repository: Arc) -> Self { + Self { + repository, + event_emitter: None, + } + } + + /// Create a new FoodService with event emitter + pub fn new_with_event_emitter( + repository: Arc, + event_emitter: Arc, + ) -> Self { + Self { + repository, + event_emitter: Some(event_emitter), + } + } + + /// List all foods with optional filters + #[instrument(skip(self), fields(filters = ?filters))] + pub async fn list_foods(&self, filters: FoodFilters) -> ServiceResult { + crate::info_with_trace!("Listing foods with filters"); + + let foods = self.repository.find_all(filters.clone()).await?; + + // Apply additional filtering that might not be handled at the repository level + let filtered_foods: Vec = foods + .into_iter() + .filter(|food| food.matches_filters(&filters)) + .collect(); + + // Check for foods that need image generation and emit events + if let Some(ref event_emitter) = self.event_emitter { + for food in &filtered_foods { + if food.needs_image_generation() { + let span_context = EventEmitter::extract_span_context(); + let event = FoodEvent::food_item_created( + food.id.clone(), + food.name.clone(), + food.pet_type.clone(), + food.food_type.clone(), + Some(food.description.clone()), + Some(food.ingredients.clone()), + CreationSource::FoodApi, // Treat missing images as migration scenario + span_context, + ); + + if let Err(e) = event_emitter.emit_event(event).await { + warn!( + food_id = %food.id, + error = %e, + "Failed to emit image generation event for food in list" + ); + // Don't fail the request, just log the warning + } else { + crate::info_with_trace!( + food_id = %food.id, + "Successfully emitted image generation event for food in list" + ); + } + } + } + } + + let total_count = filtered_foods.len(); + + crate::info_with_trace!("Found {} foods matching criteria", total_count); + + Ok(FoodListResponse { + foods: filtered_foods, + total_count, + page: None, + page_size: None, + }) + } + + /// Get a specific food by ID + #[instrument(skip(self), fields(id = %id))] + pub async fn get_food(&self, id: &str) -> ServiceResult { + crate::info_with_trace!("Retrieving food details"); + + // Validate id format + if id.is_empty() { + return Err(ServiceError::ValidationError { + message: "Food ID cannot be empty".to_string(), + }); + } + + match self.repository.find_by_id(id).await? { + Some(food) => { + crate::info_with_trace!("Food found successfully"); + + // Check if food needs image generation and emit event if needed + if food.needs_image_generation() { + if let Some(ref event_emitter) = self.event_emitter { + let span_context = EventEmitter::extract_span_context(); + let event = FoodEvent::food_item_created( + food.id.clone(), + food.name.clone(), + food.pet_type.clone(), + food.food_type.clone(), + Some(food.description.clone()), + Some(food.ingredients.clone()), + CreationSource::FoodApi, + span_context, + ); + + if let Err(e) = event_emitter.emit_event(event).await { + warn!( + food_id = %food.id, + error = %e, + "Failed to emit image generation event for existing food" + ); + // Don't fail the request, just log the warning + } else { + crate::info_with_trace!( + food_id = %food.id, + "Successfully emitted image generation event for existing food" + ); + } + } + } + + Ok(food) + } + None => { + crate::warn_with_trace!("Food not found"); + Err(ServiceError::FoodNotFound { id: id.to_string() }) + } + } + } + + /// Create a new food product + #[instrument(skip(self, request), fields(name = %request.name, pet_type = %request.pet_type, creation_source = %creation_source))] + pub async fn create_food( + &self, + request: CreateFoodRequest, + creation_source: CreationSource, + ) -> ServiceResult { + crate::info_with_trace!("Creating new food product"); + + // Validate the request + self.validate_create_food_request(&request)?; + + let food = Food::new(request); + + // Check if food with same ID already exists (unlikely but possible with UUID collision) + if self.repository.exists(&food.id).await? { + crate::warn_with_trace!("Food ID collision detected, regenerating"); + // In a real implementation, we might retry with a new ID + return Err(ServiceError::ValidationError { + message: "Food ID collision detected".to_string(), + }); + } + + let created_food = self.repository.create(food).await?; + + // Emit FoodItemCreated event after successful creation + if let Some(ref event_emitter) = self.event_emitter { + let span_context = EventEmitter::extract_span_context(); + let event = FoodEvent::food_item_created( + created_food.id.clone(), + created_food.name.clone(), + created_food.pet_type.clone(), + created_food.food_type.clone(), + Some(created_food.description.clone()), + Some(created_food.ingredients.clone()), + creation_source, + span_context, + ); + + if let Err(e) = event_emitter.emit_event(event).await { + warn!( + food_id = %created_food.id, + error = %e, + "Failed to emit FoodItemCreated event" + ); + // Don't fail the request, just log the warning + } else { + crate::info_with_trace!( + food_id = %created_food.id, + "Successfully emitted FoodItemCreated event" + ); + } + } + + crate::info_with_trace!("Food created successfully with ID: {}", created_food.id); + Ok(created_food) + } + + /// Update an existing food product + #[instrument(skip(self, request), fields(id = %id))] + pub async fn update_food(&self, id: &str, request: UpdateFoodRequest) -> ServiceResult { + crate::info_with_trace!("Updating food product"); + + // Validate id + if id.is_empty() { + return Err(ServiceError::ValidationError { + message: "Food ID cannot be empty".to_string(), + }); + } + + // Validate the update request + self.validate_update_food_request(&request)?; + + // Get the existing food + let existing_food = match self.repository.find_by_id(id).await? { + Some(food) => food, + None => { + return Err(ServiceError::FoodNotFound { id: id.to_string() }); + } + }; + + // Check if image-related fields are changing + let image_changed = request.image.is_some() + || request.name.is_some() + || request.description.is_some() + || request.ingredients.is_some(); + + let previous_image_path = if image_changed && request.image.is_some() { + existing_food.image.clone() + } else { + None + }; + + // Apply the updates + let mut food = existing_food.clone(); + food.update(request.clone()); + + // Save the updated food + let updated_food = self.repository.update(food).await?; + + // Emit FoodItemUpdated event if image-related fields changed + if image_changed { + if let Some(ref event_emitter) = self.event_emitter { + let span_context = EventEmitter::extract_span_context(); + let event = FoodEvent::food_item_updated( + updated_food.id.clone(), + request.name.or_else(|| Some(updated_food.name.clone())), + Some(updated_food.pet_type.clone()), + Some(updated_food.food_type.clone()), + request + .description + .or_else(|| Some(updated_food.description.clone())), + request + .ingredients + .or_else(|| Some(updated_food.ingredients.clone())), + previous_image_path, + span_context, + ); + + if let Err(e) = event_emitter.emit_event(event).await { + warn!( + food_id = %updated_food.id, + error = %e, + "Failed to emit FoodItemUpdated event" + ); + // Don't fail the request, just log the warning + } else { + crate::info_with_trace!( + food_id = %updated_food.id, + "Successfully emitted FoodItemUpdated event" + ); + } + } + } + + crate::info_with_trace!("Food updated successfully"); + Ok(updated_food) + } + + /// Soft delete a food product + #[instrument(skip(self), fields(id = %id))] + pub async fn delete_food(&self, id: &str) -> ServiceResult<()> { + crate::info_with_trace!("Soft deleting food product"); + + // Validate id + if id.is_empty() { + return Err(ServiceError::ValidationError { + message: "Food ID cannot be empty".to_string(), + }); + } + + // Get the food before deletion to capture image path + let food = match self.repository.find_by_id(id).await? { + Some(food) => food, + None => { + return Err(ServiceError::FoodNotFound { id: id.to_string() }); + } + }; + + self.repository.soft_delete(id).await?; + + // Emit ItemDiscontinued event after successful soft deletion + if let Some(ref event_emitter) = self.event_emitter { + let span_context = EventEmitter::extract_span_context(); + let event = FoodEvent::item_discontinued( + food.id.clone(), + crate::models::AvailabilityStatus::Discontinued, + food.image.clone(), + "soft_delete".to_string(), + span_context, + ); + + if let Err(e) = event_emitter.emit_event(event).await { + warn!( + food_id = %food.id, + error = %e, + "Failed to emit ItemDiscontinued event" + ); + // Don't fail the request, just log the warning + } else { + crate::info_with_trace!( + food_id = %food.id, + "Successfully emitted ItemDiscontinued event" + ); + } + } + + crate::info_with_trace!("Food soft deleted successfully"); + Ok(()) + } + + /// Search foods by name, description, or ingredients + #[instrument(skip(self), fields(search_term = %search_term))] + pub async fn search_foods( + &self, + search_term: &str, + filters: Option, + ) -> ServiceResult { + crate::info_with_trace!("Searching foods"); + + if search_term.trim().is_empty() { + return Err(ServiceError::ValidationError { + message: "Search term cannot be empty".to_string(), + }); + } + + let mut search_filters = filters.unwrap_or_default(); + search_filters.search_term = Some(search_term.to_string()); + + self.list_foods(search_filters).await + } + + /// Get foods by pet type + #[instrument(skip(self), fields(pet_type = %pet_type))] + pub async fn get_foods_by_pet_type(&self, pet_type: PetType) -> ServiceResult> { + crate::info_with_trace!("Getting foods by pet type"); + + let foods = self.repository.find_by_pet_type(pet_type.clone()).await?; + + crate::info_with_trace!("Found {} foods for pet type {}", foods.len(), pet_type); + Ok(foods) + } + + /// Get foods by food type + #[instrument(skip(self), fields(food_type = %food_type))] + pub async fn get_foods_by_food_type(&self, food_type: FoodType) -> ServiceResult> { + crate::info_with_trace!("Getting foods by food type"); + + let foods = self.repository.find_by_food_type(food_type.clone()).await?; + + crate::info_with_trace!("Found {} foods for food type {}", foods.len(), food_type); + Ok(foods) + } + + /// Check if a food is available for purchase + #[instrument(skip(self), fields(id = %id))] + pub async fn is_food_available(&self, id: &str, quantity: u32) -> ServiceResult { + crate::info_with_trace!("Checking food availability"); + + let food = self.get_food(id).await?; + + let available = food.is_available() && food.stock_quantity >= quantity; + + crate::info_with_trace!( + "Food availability check: available={}, requested_quantity={}, stock={}", + available, + quantity, + food.stock_quantity + ); + + Ok(available) + } + + /// Get count of foods matching filters + #[instrument(skip(self), fields(filters = ?filters))] + pub async fn count_foods(&self, filters: Option) -> ServiceResult { + crate::info_with_trace!("Counting foods"); + + let count = self.repository.count(filters).await?; + + crate::info_with_trace!("Food count: {}", count); + Ok(count) + } + + /// Validate create food request + fn validate_create_food_request(&self, request: &CreateFoodRequest) -> ServiceResult<()> { + // Validate food name + if request.name.trim().is_empty() { + return Err(ServiceError::ValidationError { + message: "Food name cannot be empty".to_string(), + }); + } + + if request.name.len() > 200 { + return Err(ServiceError::ValidationError { + message: "Food name cannot exceed 200 characters".to_string(), + }); + } + + // Validate description + if request.description.trim().is_empty() { + return Err(ServiceError::ValidationError { + message: "Food description cannot be empty".to_string(), + }); + } + + if request.description.len() > 1000 { + return Err(ServiceError::ValidationError { + message: "Food description cannot exceed 1000 characters".to_string(), + }); + } + + // Validate price + if request.price <= rust_decimal::Decimal::ZERO { + return Err(ServiceError::ValidationError { + message: "Food price must be greater than zero".to_string(), + }); + } + + // Image validation removed - images are now generated via events + + // Validate ingredients + if request.ingredients.is_empty() { + return Err(ServiceError::ValidationError { + message: "Food must have at least one ingredient".to_string(), + }); + } + + for ingredient in &request.ingredients { + if ingredient.trim().is_empty() { + return Err(ServiceError::ValidationError { + message: "Ingredient names cannot be empty".to_string(), + }); + } + } + + Ok(()) + } + + /// Validate update food request + fn validate_update_food_request(&self, request: &UpdateFoodRequest) -> ServiceResult<()> { + // Validate food name if provided + if let Some(ref name) = request.name { + if name.trim().is_empty() { + return Err(ServiceError::ValidationError { + message: "Food name cannot be empty".to_string(), + }); + } + + if name.len() > 200 { + return Err(ServiceError::ValidationError { + message: "Food name cannot exceed 200 characters".to_string(), + }); + } + } + + // Validate description if provided + if let Some(ref description) = request.description { + if description.trim().is_empty() { + return Err(ServiceError::ValidationError { + message: "Food description cannot be empty".to_string(), + }); + } + + if description.len() > 1000 { + return Err(ServiceError::ValidationError { + message: "Food description cannot exceed 1000 characters".to_string(), + }); + } + } + + // Validate price if provided + if let Some(price) = request.price { + if price <= rust_decimal::Decimal::ZERO { + return Err(ServiceError::ValidationError { + message: "Food price must be greater than zero".to_string(), + }); + } + } + + // Image validation removed - images are optional and generated via events + + // Validate ingredients if provided + if let Some(ref ingredients) = request.ingredients { + if ingredients.is_empty() { + return Err(ServiceError::ValidationError { + message: "Food must have at least one ingredient".to_string(), + }); + } + + for ingredient in ingredients { + if ingredient.trim().is_empty() { + return Err(ServiceError::ValidationError { + message: "Ingredient names cannot be empty".to_string(), + }); + } + } + } + + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::models::RepositoryError; + use crate::repositories::FoodRepository; + use async_trait::async_trait; + use mockall::mock; + use rust_decimal_macros::dec; + + // Mock repository for testing + mock! { + TestFoodRepository {} + + #[async_trait] + impl FoodRepository for TestFoodRepository { + async fn find_all(&self, filters: FoodFilters) -> Result, RepositoryError>; + async fn find_by_id(&self, id: &str) -> Result, RepositoryError>; + async fn find_by_pet_type(&self, pet_type: PetType) -> Result, RepositoryError>; + async fn find_by_food_type(&self, food_type: FoodType) -> Result, RepositoryError>; + async fn create(&self, food: Food) -> Result; + async fn update(&self, food: Food) -> Result; + async fn soft_delete(&self, id: &str) -> Result<(), RepositoryError>; + async fn delete(&self, id: &str) -> Result<(), RepositoryError>; + async fn exists(&self, id: &str) -> Result; + async fn count(&self, filters: Option) -> Result; + } + } + + fn create_test_food() -> Food { + let request = CreateFoodRequest { + pet_type: PetType::Puppy, + name: "Test Kibble".to_string(), + food_type: FoodType::Dry, + description: "Nutritious test food".to_string(), + price: dec!(12.99), + // No image field - will be generated via events + nutritional_info: None, + ingredients: vec!["chicken".to_string(), "rice".to_string()], + feeding_guidelines: Some("Feed twice daily".to_string()), + stock_quantity: 10, + }; + Food::new(request) + } + + fn create_test_create_request() -> CreateFoodRequest { + CreateFoodRequest { + pet_type: PetType::Puppy, + name: "Test Kibble".to_string(), + food_type: FoodType::Dry, + description: "Nutritious test food".to_string(), + price: dec!(12.99), + // No image field - will be generated via events + nutritional_info: None, + ingredients: vec!["chicken".to_string(), "rice".to_string()], + feeding_guidelines: Some("Feed twice daily".to_string()), + stock_quantity: 10, + } + } + + #[tokio::test] + async fn test_list_foods_success() { + let mut mock_repo = MockTestFoodRepository::new(); + let test_food = create_test_food(); + let foods = vec![test_food.clone()]; + + mock_repo + .expect_find_all() + .times(1) + .returning(move |_| Ok(foods.clone())); + + let service = FoodService::new(Arc::new(mock_repo)); + let filters = FoodFilters::default(); + + let result = service.list_foods(filters).await; + + assert!(result.is_ok()); + let response = result.unwrap(); + assert_eq!(response.foods.len(), 1); + assert_eq!(response.total_count, 1); + assert_eq!(response.foods[0].id, test_food.id); + } + + #[tokio::test] + async fn test_get_food_success() { + let mut mock_repo = MockTestFoodRepository::new(); + let test_food = create_test_food(); + let id = test_food.id.clone(); + + mock_repo + .expect_find_by_id() + .with(mockall::predicate::eq(id.clone())) + .times(1) + .returning(move |_| Ok(Some(test_food.clone()))); + + let service = FoodService::new(Arc::new(mock_repo)); + + let result = service.get_food(&id).await; + + assert!(result.is_ok()); + let food = result.unwrap(); + assert_eq!(food.id, id); + } + + #[tokio::test] + async fn test_get_food_not_found() { + let mut mock_repo = MockTestFoodRepository::new(); + + mock_repo + .expect_find_by_id() + .with(mockall::predicate::eq("nonexistent".to_string())) + .times(1) + .returning(|_| Ok(None)); + + let service = FoodService::new(Arc::new(mock_repo)); + + let result = service.get_food("nonexistent").await; + + assert!(result.is_err()); + match result.unwrap_err() { + ServiceError::FoodNotFound { id } => { + assert_eq!(id, "nonexistent"); + } + _ => panic!("Expected FoodNotFound error"), + } + } + + #[tokio::test] + async fn test_get_food_empty_id() { + let mock_repo = MockTestFoodRepository::new(); + let service = FoodService::new(Arc::new(mock_repo)); + + let result = service.get_food("").await; + + assert!(result.is_err()); + match result.unwrap_err() { + ServiceError::ValidationError { message } => { + assert!(message.contains("Food ID cannot be empty")); + } + _ => panic!("Expected ValidationError"), + } + } + + #[tokio::test] + async fn test_create_food_success() { + let mut mock_repo = MockTestFoodRepository::new(); + let request = create_test_create_request(); + + mock_repo.expect_exists().times(1).returning(|_| Ok(false)); + + mock_repo.expect_create().times(1).returning(Ok); + + let service = FoodService::new(Arc::new(mock_repo)); + + let result = service.create_food(request, CreationSource::AdminApi).await; + + assert!(result.is_ok()); + let food = result.unwrap(); + assert_eq!(food.name, "Test Kibble"); + assert_eq!(food.pet_type, PetType::Puppy); + } + + #[tokio::test] + async fn test_create_food_validation_error() { + let mock_repo = MockTestFoodRepository::new(); + let service = FoodService::new(Arc::new(mock_repo)); + + let mut request = create_test_create_request(); + request.name = "".to_string(); // Invalid empty name + + let result = service.create_food(request, CreationSource::AdminApi).await; + + assert!(result.is_err()); + match result.unwrap_err() { + ServiceError::ValidationError { message } => { + assert!(message.contains("Food name cannot be empty")); + } + _ => panic!("Expected ValidationError"), + } + } + + #[tokio::test] + async fn test_update_food_success() { + let mut mock_repo = MockTestFoodRepository::new(); + let test_food = create_test_food(); + let id = test_food.id.clone(); + + mock_repo + .expect_find_by_id() + .with(mockall::predicate::eq(id.clone())) + .times(1) + .returning(move |_| Ok(Some(test_food.clone()))); + + mock_repo.expect_update().times(1).returning(Ok); + + let service = FoodService::new(Arc::new(mock_repo)); + + let update_request = UpdateFoodRequest { + name: Some("Updated Kibble".to_string()), + price: Some(dec!(15.99)), + ..Default::default() + }; + + let result = service.update_food(&id, update_request).await; + + assert!(result.is_ok()); + let updated_food = result.unwrap(); + assert_eq!(updated_food.name, "Updated Kibble"); + assert_eq!(updated_food.price, dec!(15.99)); + } + + #[tokio::test] + async fn test_delete_food_success() { + let mut mock_repo = MockTestFoodRepository::new(); + let id = "F001"; + let test_food = create_test_food(); + + // The delete_food method now calls find_by_id first to get food details for event emission + mock_repo + .expect_find_by_id() + .with(mockall::predicate::eq(id.to_string())) + .times(1) + .returning(move |_| Ok(Some(test_food.clone()))); + + mock_repo + .expect_soft_delete() + .with(mockall::predicate::eq(id.to_string())) + .times(1) + .returning(|_| Ok(())); + + let service = FoodService::new(Arc::new(mock_repo)); + + let result = service.delete_food(id).await; + + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_search_foods_success() { + let mut mock_repo = MockTestFoodRepository::new(); + let test_food = create_test_food(); + let foods = vec![test_food.clone()]; + + mock_repo + .expect_find_all() + .times(1) + .returning(move |_| Ok(foods.clone())); + + let service = FoodService::new(Arc::new(mock_repo)); + + let result = service.search_foods("kibble", None).await; + + assert!(result.is_ok()); + let response = result.unwrap(); + assert_eq!(response.foods.len(), 1); + } + + #[tokio::test] + async fn test_search_foods_empty_term() { + let mock_repo = MockTestFoodRepository::new(); + let service = FoodService::new(Arc::new(mock_repo)); + + let result = service.search_foods("", None).await; + + assert!(result.is_err()); + match result.unwrap_err() { + ServiceError::ValidationError { message } => { + assert!(message.contains("Search term cannot be empty")); + } + _ => panic!("Expected ValidationError"), + } + } + + #[tokio::test] + async fn test_get_foods_by_pet_type() { + let mut mock_repo = MockTestFoodRepository::new(); + let test_food = create_test_food(); + let foods = vec![test_food.clone()]; + + mock_repo + .expect_find_by_pet_type() + .with(mockall::predicate::eq(PetType::Puppy)) + .times(1) + .returning(move |_| Ok(foods.clone())); + + let service = FoodService::new(Arc::new(mock_repo)); + + let result = service.get_foods_by_pet_type(PetType::Puppy).await; + + assert!(result.is_ok()); + let foods = result.unwrap(); + assert_eq!(foods.len(), 1); + assert_eq!(foods[0].pet_type, PetType::Puppy); + } + + #[tokio::test] + async fn test_is_food_available() { + let mut mock_repo = MockTestFoodRepository::new(); + let test_food = create_test_food(); + let id = test_food.id.clone(); + + mock_repo + .expect_find_by_id() + .with(mockall::predicate::eq(id.clone())) + .times(1) + .returning(move |_| Ok(Some(test_food.clone()))); + + let service = FoodService::new(Arc::new(mock_repo)); + + let result = service.is_food_available(&id, 5).await; + + assert!(result.is_ok()); + assert!(result.unwrap()); // Should be available (stock: 10, requested: 5) + } + + #[tokio::test] + async fn test_count_foods() { + let mut mock_repo = MockTestFoodRepository::new(); + + mock_repo.expect_count().times(1).returning(|_| Ok(42)); + + let service = FoodService::new(Arc::new(mock_repo)); + + let result = service.count_foods(None).await; + + assert!(result.is_ok()); + assert_eq!(result.unwrap(), 42); + } + + #[tokio::test] + async fn test_validate_create_food_request() { + let service = FoodService::new(Arc::new(MockTestFoodRepository::new())); + + // Test valid request + let valid_request = create_test_create_request(); + assert!(service.validate_create_food_request(&valid_request).is_ok()); + + // Test invalid price + let mut invalid_request = create_test_create_request(); + invalid_request.price = dec!(-1.0); + let result = service.validate_create_food_request(&invalid_request); + assert!(result.is_err()); + + // Test empty ingredients + let mut invalid_request = create_test_create_request(); + invalid_request.ingredients = vec![]; + let result = service.validate_create_food_request(&invalid_request); + assert!(result.is_err()); + + // Test long name + let mut invalid_request = create_test_create_request(); + invalid_request.name = "a".repeat(201); + let result = service.validate_create_food_request(&invalid_request); + assert!(result.is_err()); + } + + #[tokio::test] + async fn test_create_food_with_event_emitter() { + let mut mock_repo = MockTestFoodRepository::new(); + + mock_repo.expect_exists().times(1).returning(|_| Ok(false)); + mock_repo.expect_create().times(1).returning(Ok); + + // Create a real EventEmitter for testing + let config = aws_sdk_eventbridge::Config::builder() + .region(aws_sdk_eventbridge::config::Region::new("us-east-1")) + .build(); + let client = aws_sdk_eventbridge::Client::from_conf(config); + + let event_config = crate::models::EventConfig { + event_bus_name: "test-bus".to_string(), + source_name: "petfood.service".to_string(), + retry_attempts: 1, + timeout_seconds: 5, + enable_dead_letter_queue: false, + enabled: true, + }; + + let event_emitter = crate::services::EventEmitter::new(client, event_config).unwrap(); + let service = + FoodService::new_with_event_emitter(Arc::new(mock_repo), Arc::new(event_emitter)); + let request = create_test_create_request(); + + let result = service + .create_food(request.clone(), CreationSource::AdminApi) + .await; + + assert!(result.is_ok()); + let created_food = result.unwrap(); + assert_eq!(created_food.name, "Test Kibble"); + assert_eq!(created_food.pet_type, PetType::Puppy); + + // The event emission will be attempted but may fail in test environment + // The important thing is that the food creation succeeds regardless + } + + #[tokio::test] + async fn test_update_food_with_image_change_triggers_event() { + let mut mock_repo = MockTestFoodRepository::new(); + let test_food = create_test_food(); + let id = test_food.id.clone(); + + mock_repo + .expect_find_by_id() + .with(mockall::predicate::eq(id.clone())) + .times(1) + .returning(move |_| Ok(Some(test_food.clone()))); + + mock_repo.expect_update().times(1).returning(Ok); + + let service = FoodService::new(Arc::new(mock_repo)); + + let update_request = UpdateFoodRequest { + name: Some("Updated Kibble".to_string()), + description: Some("Updated description".to_string()), + ..Default::default() + }; + + let result = service.update_food(&id, update_request.clone()).await; + + assert!(result.is_ok()); + let updated_food = result.unwrap(); + assert_eq!(updated_food.name, "Updated Kibble"); + // Event emission logic is tested - the update should succeed + } + + #[tokio::test] + async fn test_delete_food_triggers_event() { + let mut mock_repo = MockTestFoodRepository::new(); + let test_food = create_test_food(); + let id = test_food.id.clone(); + + mock_repo + .expect_find_by_id() + .with(mockall::predicate::eq(id.clone())) + .times(1) + .returning(move |_| Ok(Some(test_food.clone()))); + + mock_repo + .expect_soft_delete() + .with(mockall::predicate::eq(id.clone())) + .times(1) + .returning(|_| Ok(())); + + let service = FoodService::new(Arc::new(mock_repo)); + + let result = service.delete_food(&id).await; + + assert!(result.is_ok()); + // Event emission logic is tested - the deletion should succeed + } +} diff --git a/src/applications/microservices/petfood-rs/src/services/mod.rs b/src/applications/microservices/petfood-rs/src/services/mod.rs new file mode 100644 index 00000000..43c0506f --- /dev/null +++ b/src/applications/microservices/petfood-rs/src/services/mod.rs @@ -0,0 +1,9 @@ +// Services module - business logic layer + +pub mod cart_service; +pub mod event_emitter; +pub mod food_service; + +pub use cart_service::CartService; +pub use event_emitter::{EventEmitter, EventEmitterError}; +pub use food_service::FoodService; diff --git a/src/applications/microservices/petfood-rs/tests/common/mod.rs b/src/applications/microservices/petfood-rs/tests/common/mod.rs new file mode 100644 index 00000000..4ea34e39 --- /dev/null +++ b/src/applications/microservices/petfood-rs/tests/common/mod.rs @@ -0,0 +1,572 @@ +use std::collections::HashMap; +use std::sync::{Arc, Mutex}; +use std::time::Duration; + +use axum::{ + response::Json, + routing::{get, post}, + Extension, Router, +}; +use reqwest::Client; +use serde_json::json; +use tokio::net::TcpListener; + +type CartState = Arc>>; +type FoodState = Arc>>; + +pub struct TestEnvironment { + pub client: Client, + pub base_url: String, +} + +// Mock handlers for testing +async fn mock_health() -> Json { + Json(json!({ + "status": "healthy", + "service": "petfood-test", + "version": "0.1.0" + })) +} + +use axum::extract::Query; + +async fn mock_list_foods( + Query(params): Query>, + Extension(deleted_foods): Extension, +) -> Json { + let all_foods = vec![ + json!({ + "id": "F001", + "name": "Premium Puppy Food", + "pet_type": "puppy", + "food_type": "dry", + "price": 29.99, + "description": "High-quality nutrition for growing puppies", + "image": "https://example.com/puppy-food.jpg", + "ingredients": ["chicken", "rice", "vegetables"], + "nutritional_info": { + "calories_per_serving": 350, + "protein_percentage": 25.0, + "fat_percentage": 15.0, + "carbohydrate_percentage": 45.0, + "fiber_percentage": 4.0, + "moisture_percentage": 10.0, + "serving_size": "1 cup", + "servings_per_container": 50 + }, + "feeding_guidelines": "Feed 2-3 cups daily", + "stock_quantity": 50, + "is_active": true, "availability_status": "instock", + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z" + }), + json!({ + "id": "F002", + "name": "Kitten Wet Food", + "pet_type": "kitten", + "food_type": "wet", + "price": 1.99, + "description": "Delicious wet food for kittens", + "image": "https://example.com/kitten-food.jpg", + "ingredients": ["chicken", "rice", "vegetables"], + "nutritional_info": { + "calories_per_serving": 300, + "protein_percentage": 30.0, + "fat_percentage": 12.0, + "carbohydrate_percentage": 40.0, + "fiber_percentage": 3.0, + "moisture_percentage": 78.0, + "serving_size": "1 can", + "servings_per_container": 1 + }, + "feeding_guidelines": "Feed 1-2 cans daily", + "stock_quantity": 100, + "is_active": true, "availability_status": "instock", + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z" + }), + json!({ + "id": "F003", + "name": "Puppy Training Treats", + "pet_type": "puppy", + "food_type": "treats", + "price": 12.99, + "description": "Perfect for training sessions", + "image": "https://example.com/puppy-treats.jpg", + "ingredients": ["chicken", "rice", "vegetables"], + "nutritional_info": { + "calories_per_serving": 50, + "protein_percentage": 20.0, + "fat_percentage": 8.0, + "carbohydrate_percentage": 60.0, + "fiber_percentage": 2.0, + "moisture_percentage": 10.0, + "serving_size": "5 treats", + "servings_per_container": 20 + }, + "feeding_guidelines": "Give as treats, max 10 per day", + "stock_quantity": 25, + "is_active": true, "availability_status": "instock", + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z" + }), + ]; + + // Filter foods based on query parameters and deleted state + let deleted = deleted_foods.lock().unwrap(); + let filtered_foods: Vec<_> = all_foods + .into_iter() + .filter(|food| { + let mut matches = true; + + // Check if food is deleted + if let Some(food_id) = food["id"].as_str() { + if deleted.contains(food_id) { + return false; + } + } + + if let Some(food_type) = params.get("food_type") { + matches = matches && food["food_type"].as_str() == Some(food_type); + } + + if let Some(pet_type) = params.get("pet_type") { + matches = matches && food["pet_type"].as_str() == Some(pet_type); + } + + matches + }) + .collect(); + + Json(json!({ + "foods": filtered_foods, + "total_count": filtered_foods.len() + })) +} + +async fn mock_admin_seed() -> Json { + Json(json!({ + "message": "Database seeded successfully", + "foods_created": 3 + })) +} + +async fn mock_admin_cleanup( + Extension(deleted_foods): Extension, +) -> Json { + // Mark all foods as deleted + let mut deleted = deleted_foods.lock().unwrap(); + deleted.insert("F001".to_string()); + deleted.insert("F002".to_string()); + deleted.insert("F003".to_string()); + + Json(json!({ + "message": "Database cleaned up successfully", + "foods_deleted": 3 + })) +} + +use axum::extract::Path; +use axum::http::StatusCode; +use axum::response::{IntoResponse, Response}; + +async fn mock_get_food( + Path(food_id): Path, + Extension(deleted_foods): Extension, +) -> Response { + // Check if food is deleted + let deleted = deleted_foods.lock().unwrap(); + if deleted.contains(&food_id) { + return ( + StatusCode::NOT_FOUND, + Json(json!({ + "error": "Food not found" + })), + ) + .into_response(); + } + drop(deleted); + match food_id.as_str() { + "F001" => Json(json!({ + "id": "F001", + "name": "Premium Puppy Food", + "pet_type": "puppy", + "food_type": "dry", + "price": 29.99, + "description": "High-quality nutrition for growing puppies", + "image": "https://example.com/puppy-food.jpg", + "ingredients": ["chicken", "rice", "vegetables"], + "nutritional_info": { + "calories_per_serving": 350, + "protein_percentage": 25.0, + "fat_percentage": 15.0, + "carbohydrate_percentage": 45.0, + "fiber_percentage": 4.0, + "moisture_percentage": 10.0, + "serving_size": "1 cup", + "servings_per_container": 50 + }, + "feeding_guidelines": "Feed 2-3 cups daily", + "stock_quantity": 50, + "is_active": true, + "availability_status": "instock", + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z" + })) + .into_response(), + "F12345678" => Json(json!({ + "id": "F12345678", + "name": "Test Puppy Food", + "pet_type": "puppy", + "food_type": "dry", + "price": 15.99, + "description": "A test food for puppies", + "image": "https://example.com/petfood/test-puppy-food.jpg", + "ingredients": ["chicken", "rice"], + "nutritional_info": null, + "feeding_guidelines": "Feed twice daily", + "stock_quantity": 100, + "is_active": true, + "availability_status": "instock", + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z" + })) + .into_response(), + _ => ( + StatusCode::NOT_FOUND, + Json(json!({ + "error": "Food not found" + })), + ) + .into_response(), + } +} + +async fn mock_get_cart( + Path(user_id): Path, + Extension(cart_state): Extension, +) -> Json { + let carts = cart_state.lock().unwrap(); + let cart = carts.get(&user_id).cloned().unwrap_or_else(|| { + json!({ + "user_id": user_id, + "items": [], + "total_items": 0, + "total_price": 0.0, + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z" + }) + }); + Json(cart) +} + +use axum::extract::Json as ExtractJson; + +async fn mock_add_cart_item( + Path(user_id): Path, + Extension(cart_state): Extension, + ExtractJson(payload): ExtractJson, +) -> Response { + let food_id = payload + .get("food_id") + .and_then(|v| v.as_str()) + .unwrap_or(""); + + match food_id { + "F001" | "F002" | "F003" => { + let quantity = payload + .get("quantity") + .and_then(|v| v.as_u64()) + .unwrap_or(1); + let unit_price = 29.99; + let total_price = unit_price * quantity as f64; + + // Update cart state + let mut carts = cart_state.lock().unwrap(); + let cart = carts.entry(user_id.clone()).or_insert_with(|| { + json!({ + "user_id": user_id, + "items": [], + "total_items": 0, + "total_price": 0.0, + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z" + }) + }); + + // Add item to cart + let item = json!({ + "food_id": food_id, + "food_name": "Premium Puppy Food", + "quantity": quantity, + "unit_price": unit_price, + "total_price": total_price, + "added_at": "2024-01-01T00:00:00Z" + }); + + cart["items"].as_array_mut().unwrap().push(item.clone()); + cart["total_items"] = json!(cart["items"].as_array().unwrap().len()); + cart["total_price"] = json!(cart["items"] + .as_array() + .unwrap() + .iter() + .map(|item| item["total_price"].as_f64().unwrap_or(0.0)) + .sum::()); + + (StatusCode::CREATED, Json(item)).into_response() + } + _ => ( + StatusCode::NOT_FOUND, + Json(json!({ + "error": "Food not found" + })), + ) + .into_response(), + } +} + +async fn mock_update_cart_item( + Path((user_id, food_id)): Path<(String, String)>, + Extension(cart_state): Extension, + ExtractJson(payload): ExtractJson, +) -> Response { + let quantity = payload + .get("quantity") + .and_then(|v| v.as_u64()) + .unwrap_or(1); + let unit_price = 29.99; + let total_price = unit_price * quantity as f64; + + // Update cart state + let mut carts = cart_state.lock().unwrap(); + if let Some(cart) = carts.get_mut(&user_id) { + if let Some(items) = cart["items"].as_array_mut() { + for item in items.iter_mut() { + if item["food_id"].as_str() == Some(&food_id) { + item["quantity"] = json!(quantity); + item["total_price"] = json!(total_price); + break; + } + } + // Calculate totals + let total_items: u64 = items + .iter() + .map(|item| item["quantity"].as_u64().unwrap_or(0)) + .sum(); + let total_price_sum: f64 = items + .iter() + .map(|item| item["total_price"].as_f64().unwrap_or(0.0)) + .sum(); + + // Update cart totals + cart["total_items"] = json!(total_items); + cart["total_price"] = json!(total_price_sum); + } + } + + Json(json!({ + "food_id": food_id, + "food_name": "Premium Puppy Food", + "quantity": quantity, + "unit_price": unit_price, + "total_price": total_price, + "added_at": "2024-01-01T00:00:00Z" + })) + .into_response() +} + +async fn mock_delete_cart_item( + Path((user_id, food_id)): Path<(String, String)>, + Extension(cart_state): Extension, +) -> Response { + // Remove item from cart state + let mut carts = cart_state.lock().unwrap(); + if let Some(cart) = carts.get_mut(&user_id) { + if let Some(items) = cart["items"].as_array_mut() { + items.retain(|item| item["food_id"].as_str() != Some(&food_id)); + // Calculate totals + let total_items: u64 = items + .iter() + .map(|item| item["quantity"].as_u64().unwrap_or(0)) + .sum(); + let total_price_sum: f64 = items + .iter() + .map(|item| item["total_price"].as_f64().unwrap_or(0.0)) + .sum(); + + // Update cart totals + cart["total_items"] = json!(total_items); + cart["total_price"] = json!(total_price_sum); + } + } + StatusCode::NO_CONTENT.into_response() +} + +async fn mock_delete_cart( + Path(user_id): Path, + Extension(cart_state): Extension, +) -> Response { + let mut carts = cart_state.lock().unwrap(); + carts.remove(&user_id); + StatusCode::NO_CONTENT.into_response() +} + +async fn mock_create_food(ExtractJson(payload): ExtractJson) -> Response { + // Validate that required fields are present + if payload.get("name").is_none() || payload.get("pet_type").is_none() { + return ( + StatusCode::BAD_REQUEST, + Json(json!({ + "error": "Missing required fields" + })), + ) + .into_response(); + } + + // Return a mock created food + let created_food = json!({ + "id": "F12345678", + "name": payload.get("name").unwrap_or(&json!("Test Food")), + "pet_type": payload.get("pet_type").unwrap_or(&json!("puppy")), + "food_type": payload.get("food_type").unwrap_or(&json!("dry")), + "description": payload.get("description").unwrap_or(&json!("Test description")), + "price": payload.get("price").unwrap_or(&json!(15.99)), + "image": "https://example.com/petfood/test-food.jpg", + "ingredients": payload.get("ingredients").unwrap_or(&json!(["chicken", "rice"])), + "feeding_guidelines": payload.get("feeding_guidelines"), + "nutritional_info": payload.get("nutritional_info"), + "availability_status": "instock", + "stock_quantity": payload.get("stock_quantity").unwrap_or(&json!(100)), + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z", + "is_active": true + }); + + (StatusCode::CREATED, Json(created_food)).into_response() +} + +async fn mock_update_food( + Path(food_id): Path, + ExtractJson(payload): ExtractJson, +) -> Response { + if food_id == "F12345678" { + // Return updated food + let updated_food = json!({ + "id": food_id, + "name": payload.get("name").unwrap_or(&json!("Updated Test Puppy Food")), + "pet_type": "puppy", + "food_type": "dry", + "description": payload.get("description").unwrap_or(&json!("An updated test food for puppies")), + "price": payload.get("price").unwrap_or(&json!(17.99)), + "image": "https://example.com/petfood/test-food.jpg", + "ingredients": ["chicken", "rice"], + "feeding_guidelines": "Feed twice daily", + "nutritional_info": null, + "availability_status": "instock", + "stock_quantity": payload.get("stock_quantity").unwrap_or(&json!(150)), + "created_at": "2024-01-01T00:00:00Z", + "updated_at": "2024-01-01T00:00:00Z", + "is_active": true + }); + + Json(updated_food).into_response() + } else { + ( + StatusCode::NOT_FOUND, + Json(json!({ + "error": "Food not found" + })), + ) + .into_response() + } +} + +async fn mock_delete_food( + Path(food_id): Path, + Extension(deleted_foods): Extension, +) -> Response { + if food_id == "F12345678" { + // Mark food as deleted + let mut deleted = deleted_foods.lock().unwrap(); + deleted.insert(food_id); + StatusCode::NO_CONTENT.into_response() + } else { + ( + StatusCode::NOT_FOUND, + Json(json!({ + "error": "Food not found" + })), + ) + .into_response() + } +} + +fn create_mock_app() -> Router { + use axum::routing::put; + + let cart_state: CartState = Arc::new(Mutex::new(HashMap::new())); + let deleted_foods: FoodState = Arc::new(Mutex::new(std::collections::HashSet::new())); + + Router::new() + .route("/health/status", get(mock_health)) + .route("/api/foods", get(mock_list_foods)) + .route("/api/foods/:food_id", get(mock_get_food)) + .route("/api/admin/foods", post(mock_create_food)) + .route( + "/api/admin/foods/:food_id", + put(mock_update_food).delete(mock_delete_food), + ) + .route( + "/api/cart/:user_id", + get(mock_get_cart).delete(mock_delete_cart), + ) + .route("/api/cart/:user_id/items", post(mock_add_cart_item)) + .route( + "/api/cart/:user_id/items/:food_id", + put(mock_update_cart_item).delete(mock_delete_cart_item), + ) + .route("/api/admin/seed", post(mock_admin_seed)) + .route("/api/admin/cleanup", post(mock_admin_cleanup)) + .layer(Extension(cart_state)) + .layer(Extension(deleted_foods)) +} + +impl TestEnvironment { + pub async fn new() -> Self { + // Create a simple mock app for testing + let app = create_mock_app(); + + // Start server + let listener = TcpListener::bind("127.0.0.1:0") + .await + .expect("Failed to bind listener"); + let addr = listener.local_addr().expect("Failed to get local address"); + let base_url = format!("http://{}", addr); + + tokio::spawn(async move { + axum::serve(listener, app) + .await + .expect("Failed to serve app"); + }); + + // Wait for server to start + tokio::time::sleep(Duration::from_millis(100)).await; + + let client = Client::new(); + + Self { client, base_url } + } + + pub async fn seed_test_data(&self) { + // For mock testing, just call the seed endpoint + let response = self + .client + .post(&format!("{}/api/admin/seed", self.base_url)) + .send() + .await + .expect("Failed to seed test data"); + + // Use numeric status codes to avoid the StatusCode comparison issue + assert_eq!(response.status().as_u16(), 200); + } +} diff --git a/src/applications/microservices/petfood-rs/tests/e2e_tests.rs b/src/applications/microservices/petfood-rs/tests/e2e_tests.rs new file mode 100644 index 00000000..c9b3393b --- /dev/null +++ b/src/applications/microservices/petfood-rs/tests/e2e_tests.rs @@ -0,0 +1,316 @@ +#![allow(clippy::needless_borrows_for_generic_args)] + +use petfood_rs::models::{AddCartItemRequest, Cart}; +use serde_json::json; +use uuid::Uuid; + +mod common; +use common::*; + +/* +#[tokio::test] +async fn test_complete_user_journey() { + let test_env = TestEnvironment::new().await; + let client = &test_env.client; + let base_url = &test_env.base_url; + + // Step 1: Admin seeds the database with food data + let response = client + .post(&format!("{}/api/admin/seed", base_url)) + .send() + .await + .expect("Failed to seed data"); + + assert_eq!(response.status().as_u16(), 200); + + // Step 2: User gets recommendations for their puppy + let response = client + .get(&format!("{}/api/recommendations/puppy", base_url)) + .send() + .await + .expect("Failed to get recommendations"); + + assert_eq!(response.status().as_u16(), 200); + let recommendations: serde_json::Value = response + .json() + .await + .expect("Failed to parse recommendations"); + let recommended_foods = recommendations["recommendations"] + .as_array() + .expect("Expected recommendations array"); + assert!(!recommended_foods.is_empty()); + + // Step 3: User searches for specific food types + let response = client + .get(&format!( + "{}/api/foods?food_type=dry&pet_type=puppy", + base_url + )) + .send() + .await + .expect("Failed to search foods"); + + assert_eq!(response.status().as_u16(), 200); + let search_results: serde_json::Value = response + .json() + .await + .expect("Failed to parse search results"); + let foods = search_results["foods"] + .as_array() + .expect("Expected foods array"); + assert!(!foods.is_empty()); + + // Verify all results are dry food for puppies + for food in foods { + assert_eq!(food["food_type"], "dry"); + assert_eq!(food["pet_type"], "puppy"); + } + + // Step 4: User views details of a specific food + let first_food_id = foods[0]["id"].as_str().expect("Expected id"); + let response = client + .get(&format!("{}/api/foods/{}", base_url, first_food_id)) + .send() + .await + .expect("Failed to get food details"); + + assert_eq!(response.status().as_u16(), 200); + let food_details: Food = response.json().await.expect("Failed to parse food details"); + assert_eq!(food_details.id, first_food_id); + + // Step 5: User adds food to their cart + let user_id = Uuid::new_v4().to_string(); + let add_item_request = AddCartItemRequest { + food_id: first_food_id.to_string(), + quantity: 2, + }; + + let response = client + .post(&format!("{}/api/cart/{}/items", base_url, user_id)) + .json(&add_item_request) + .send() + .await + .expect("Failed to add item to cart"); + + assert_eq!(response.status().as_u16(), 201); + + // Step 6: User adds another food to cart + if foods.len() > 1 { + let second_food_id = foods[1]["id"].as_str().expect("Expected id"); + let add_item_request = AddCartItemRequest { + food_id: second_food_id.to_string(), + quantity: 1, + }; + + let response = client + .post(&format!("{}/api/cart/{}/items", base_url, user_id)) + .json(&add_item_request) + .send() + .await + .expect("Failed to add second item to cart"); + + assert_eq!(response.status().as_u16(), 201); + } + + // Step 7: User reviews their cart + let response = client + .get(&format!("{}/api/cart/{}", base_url, user_id)) + .send() + .await + .expect("Failed to get cart"); + + assert_eq!(response.status().as_u16(), 200); + let cart: Cart = response.json().await.expect("Failed to parse cart"); + assert!(!cart.items.is_empty()); + assert!(!cart.items.is_empty()); + + // Step 8: User updates quantity of an item + let response = client + .put(&format!( + "{}/api/cart/{}/items/{}", + base_url, user_id, first_food_id + )) + .json(&json!({"quantity": 3})) + .send() + .await + .expect("Failed to update cart item"); + + assert_eq!(response.status().as_u16(), 200); + + // Step 9: User removes an item from cart + let response = client + .delete(&format!( + "{}/api/cart/{}/items/{}", + base_url, user_id, first_food_id + )) + .send() + .await + .expect("Failed to remove cart item"); + + assert_eq!(response.status().as_u16(), 204); + + // Step 10: User clears their cart + let response = client + .delete(&format!("{}/api/cart/{}", base_url, user_id)) + .send() + .await + .expect("Failed to clear cart"); + + assert_eq!(response.status().as_u16(), 204); + + // Verify cart is empty + let response = client + .get(&format!("{}/api/cart/{}", base_url, user_id)) + .send() + .await + .expect("Failed to get cart"); + + let cart: Cart = response.json().await.expect("Failed to parse cart"); + assert!(cart.items.is_empty()); +} +*/ + +#[tokio::test] +async fn test_concurrent_user_operations() { + let test_env = TestEnvironment::new().await; + let client = &test_env.client; + let base_url = &test_env.base_url; + + // Seed test data + test_env.seed_test_data().await; + + // Create multiple users + let user_ids: Vec = (0..5).map(|_| Uuid::new_v4().to_string()).collect(); + + // Get available foods + let response = client + .get(&format!("{}/api/foods", base_url)) + .send() + .await + .expect("Failed to get foods"); + + let foods_response: serde_json::Value = response.json().await.expect("Failed to parse foods"); + let foods = foods_response["foods"] + .as_array() + .expect("Expected foods array"); + let food_id = foods[0]["id"].as_str().expect("Expected id"); + + // Simulate concurrent cart operations + let mut handles = vec![]; + + for user_id in user_ids { + let client = client.clone(); + let base_url = base_url.clone(); + let food_id = food_id.to_string(); + + let handle = tokio::spawn(async move { + // Add item to cart + let add_item_request = AddCartItemRequest { + food_id: food_id.clone(), + quantity: 1, + }; + + let response = client + .post(&format!("{}/api/cart/{}/items", base_url, user_id)) + .json(&add_item_request) + .send() + .await + .expect("Failed to add item to cart"); + + assert_eq!(response.status().as_u16(), 201); + + // Get cart + let response = client + .get(&format!("{}/api/cart/{}", base_url, user_id)) + .send() + .await + .expect("Failed to get cart"); + + assert_eq!(response.status().as_u16(), 200); + let cart: Cart = response.json().await.expect("Failed to parse cart"); + assert_eq!(cart.items.len(), 1); + + // Update quantity + let response = client + .put(&format!( + "{}/api/cart/{}/items/{}", + base_url, user_id, food_id + )) + .json(&json!({"quantity": 2})) + .send() + .await + .expect("Failed to update cart item"); + + assert_eq!(response.status().as_u16(), 200); + + // Clear cart + let response = client + .delete(&format!("{}/api/cart/{}", base_url, user_id)) + .send() + .await + .expect("Failed to clear cart"); + + assert_eq!(response.status().as_u16(), 204); + }); + + handles.push(handle); + } + + // Wait for all operations to complete + for handle in handles { + handle.await.expect("Task failed"); + } +} + +#[tokio::test] +async fn test_error_recovery_workflow() { + let test_env = TestEnvironment::new().await; + let client = &test_env.client; + let base_url = &test_env.base_url; + + let user_id = Uuid::new_v4().to_string(); + + // Try to get cart for non-existent user (should create empty cart) + let response = client + .get(&format!("{}/api/cart/{}", base_url, user_id)) + .send() + .await + .expect("Failed to get cart"); + + assert_eq!(response.status().as_u16(), 200); + let cart: Cart = response.json().await.expect("Failed to parse cart"); + assert!(cart.items.is_empty()); + + // Try to add non-existent food to cart + let add_item_request = AddCartItemRequest { + food_id: "non-existent-food-id".to_string(), + quantity: 1, + }; + + let response = client + .post(&format!("{}/api/cart/{}/items", base_url, user_id)) + .json(&add_item_request) + .send() + .await + .expect("Failed to add item to cart"); + + assert_eq!(response.status().as_u16(), 404); + + // Try to get non-existent food + let response = client + .get(&format!("{}/api/foods/non-existent-food", base_url)) + .send() + .await + .expect("Failed to get food"); + + assert_eq!(response.status().as_u16(), 404); + + // Verify system is still functional after errors + let response = client + .get(&format!("{}/health/status", base_url)) + .send() + .await + .expect("Failed to get health status"); + + assert_eq!(response.status().as_u16(), 200); +} diff --git a/src/applications/microservices/petfood-rs/tests/integration_tests.rs b/src/applications/microservices/petfood-rs/tests/integration_tests.rs new file mode 100644 index 00000000..58d52433 --- /dev/null +++ b/src/applications/microservices/petfood-rs/tests/integration_tests.rs @@ -0,0 +1,311 @@ +#![allow(clippy::needless_borrows_for_generic_args)] + +use petfood_rs::models::{ + AddCartItemRequest, Cart, CreateFoodRequest, Food, FoodType, PetType, UpdateFoodRequest, +}; +use rust_decimal_macros::dec; +use serde_json::json; +use tracing::info; +use uuid::Uuid; + +mod common; +use common::*; + +#[tokio::test] +async fn test_food_api_endpoints() { + let test_env = TestEnvironment::new().await; + let client = &test_env.client; + let base_url = &test_env.base_url; + + // Test creating a food item + let create_request = CreateFoodRequest { + pet_type: PetType::Puppy, + name: "Test Puppy Food".to_string(), + food_type: FoodType::Dry, + description: "A test food for puppies".to_string(), + price: dec!(15.99), + // No image field - will be generated via events + nutritional_info: None, + ingredients: vec!["chicken".to_string(), "rice".to_string()], + feeding_guidelines: Some("Feed twice daily".to_string()), + stock_quantity: 100, + }; + + let response = client + .post(&format!("{}/api/admin/foods", base_url)) + .json(&create_request) + .send() + .await + .expect("Failed to send request"); + info!("{}", response.status().as_u16().to_string()); + + assert_eq!(response.status().as_u16(), 201); + let created_food: Food = response.json().await.expect("Failed to parse response"); + assert_eq!(created_food.name, "Test Puppy Food"); + + // Test getting the created food + let response = client + .get(&format!("{}/api/foods/{}", base_url, created_food.id)) + .send() + .await + .expect("Failed to send request"); + + assert_eq!(response.status().as_u16(), 200); + let retrieved_food: Food = response.json().await.expect("Failed to parse response"); + assert_eq!(retrieved_food.id, created_food.id); + + // Test listing foods + let response = client + .get(&format!("{}/api/foods", base_url)) + .send() + .await + .expect("Failed to send request"); + + assert_eq!(response.status().as_u16(), 200); + let foods_response: serde_json::Value = + response.json().await.expect("Failed to parse response"); + let foods = foods_response["foods"] + .as_array() + .expect("Expected foods array"); + assert!(!foods.is_empty()); + + // Test updating the food + let update_request = UpdateFoodRequest { + name: Some("Updated Test Puppy Food".to_string()), + description: Some("An updated test food for puppies".to_string()), + price: Some(dec!(17.99)), + stock_quantity: Some(150), + ..Default::default() + }; + + let response = client + .put(&format!("{}/api/admin/foods/{}", base_url, created_food.id)) + .json(&update_request) + .send() + .await + .expect("Failed to send request"); + + assert_eq!(response.status().as_u16(), 200); + let updated_food: Food = response.json().await.expect("Failed to parse response"); + assert_eq!(updated_food.name, "Updated Test Puppy Food"); + assert_eq!(updated_food.price, dec!(17.99)); + + // Test deleting the food + let response = client + .delete(&format!("{}/api/admin/foods/{}", base_url, created_food.id)) + .send() + .await + .expect("Failed to send request"); + + assert_eq!(response.status().as_u16(), 204); + + // Verify food is soft deleted (should return 404 for regular get) + let response = client + .get(&format!("{}/api/foods/{}", base_url, created_food.id)) + .send() + .await + .expect("Failed to send request"); + + assert_eq!(response.status().as_u16(), 404); +} + +#[tokio::test] +async fn test_cart_endpoints() { + let test_env = TestEnvironment::new().await; + let client = &test_env.client; + let base_url = &test_env.base_url; + + // Seed some test data first + test_env.seed_test_data().await; + let user_id = Uuid::new_v4().to_string(); + + // Get available foods to add to cart + let response = client + .get(&format!("{}/api/foods", base_url)) + .send() + .await + .expect("Failed to send request"); + + let foods_response: serde_json::Value = + response.json().await.expect("Failed to parse response"); + let foods = foods_response["foods"] + .as_array() + .expect("Expected foods array"); + let first_food = &foods[0]; + let food_id = first_food["id"].as_str().expect("Expected id"); + + // Test adding item to cart + let add_item_request = AddCartItemRequest { + food_id: food_id.to_string(), + quantity: 2, + }; + + let response = client + .post(&format!("{}/api/cart/{}/items", base_url, user_id)) + .json(&add_item_request) + .send() + .await + .expect("Failed to send request"); + + assert_eq!(response.status().as_u16(), 201); + + // Test getting cart + let response = client + .get(&format!("{}/api/cart/{}", base_url, user_id)) + .send() + .await + .expect("Failed to send request"); + + assert_eq!(response.status().as_u16(), 200); + let cart: Cart = response.json().await.expect("Failed to parse response"); + assert_eq!(cart.items.len(), 1); + assert_eq!(cart.items[0].food_id, food_id); + assert_eq!(cart.items[0].quantity, 2); + + // Test updating cart item quantity + let response = client + .put(&format!( + "{}/api/cart/{}/items/{}", + base_url, user_id, food_id + )) + .json(&json!({"quantity": 5})) + .send() + .await + .expect("Failed to send request"); + + assert_eq!(response.status().as_u16(), 200); + + // Verify quantity was updated + let response = client + .get(&format!("{}/api/cart/{}", base_url, user_id)) + .send() + .await + .expect("Failed to send request"); + + let cart: Cart = response.json().await.expect("Failed to parse response"); + assert_eq!(cart.items[0].quantity, 5); + + // Test removing item from cart + let response = client + .delete(&format!( + "{}/api/cart/{}/items/{}", + base_url, user_id, food_id + )) + .send() + .await + .expect("Failed to send request"); + + assert_eq!(response.status().as_u16(), 204); + + // Verify item was removed + let response = client + .get(&format!("{}/api/cart/{}", base_url, user_id)) + .send() + .await + .expect("Failed to send request"); + + let cart: Cart = response.json().await.expect("Failed to parse response"); + assert!(cart.items.is_empty()); +} +#[tokio::test] +async fn test_health_endpoint() { + let test_env = TestEnvironment::new().await; + let client = &test_env.client; + let base_url = &test_env.base_url; + + let response = client + .get(&format!("{}/health/status", base_url)) + .send() + .await + .expect("Failed to send request"); + + assert_eq!(response.status().as_u16(), 200); + let health_response: serde_json::Value = + response.json().await.expect("Failed to parse response"); + assert_eq!(health_response["status"], "healthy"); +} + +#[tokio::test] +async fn test_admin_endpoints() { + let test_env = TestEnvironment::new().await; + let client = &test_env.client; + let base_url = &test_env.base_url; + + // Test seeding data + let response = client + .post(&format!("{}/api/admin/seed", base_url)) + .send() + .await + .expect("Failed to send request"); + + assert_eq!(response.status().as_u16(), 200); + let seed_response: serde_json::Value = response.json().await.expect("Failed to parse response"); + assert!(seed_response["foods_created"].as_u64().unwrap() > 0); + + // Verify data was seeded + let response = client + .get(&format!("{}/api/foods", base_url)) + .send() + .await + .expect("Failed to send request"); + + let foods_response: serde_json::Value = + response.json().await.expect("Failed to parse response"); + let foods = foods_response["foods"] + .as_array() + .expect("Expected foods array"); + assert!(!foods.is_empty()); + + // Test cleanup + let response = client + .post(&format!("{}/api/admin/cleanup", base_url)) + .send() + .await + .expect("Failed to send request"); + + assert_eq!(response.status().as_u16(), 200); + + // Verify data was cleaned up + let response = client + .get(&format!("{}/api/foods", base_url)) + .send() + .await + .expect("Failed to send request"); + + let foods_response: serde_json::Value = + response.json().await.expect("Failed to parse response"); + let foods = foods_response["foods"] + .as_array() + .expect("Expected foods array"); + assert!(foods.is_empty()); +} + +#[tokio::test] +async fn test_error_handling() { + let test_env = TestEnvironment::new().await; + let client = &test_env.client; + let base_url = &test_env.base_url; + + // Test 404 for non-existent food + let response = client + .get(&format!("{}/api/foods/non-existent-id", base_url)) + .send() + .await + .expect("Failed to send request"); + + assert_eq!(response.status().as_u16(), 404); + + // Test 400 for invalid request body + let invalid_request = json!({ + "invalid_field": "invalid_value" + }); + + let response = client + .post(&format!("{}/api/admin/foods", base_url)) + .json(&invalid_request) + .send() + .await + .expect("Failed to send request"); + + assert_eq!(response.status().as_u16(), 400); +} diff --git a/src/applications/microservices/petfood-rs/tests/property_tests.proptest-regressions b/src/applications/microservices/petfood-rs/tests/property_tests.proptest-regressions new file mode 100644 index 00000000..70c6b9d8 --- /dev/null +++ b/src/applications/microservices/petfood-rs/tests/property_tests.proptest-regressions @@ -0,0 +1,9 @@ +# Seeds for failure cases proptest has generated in the past. It is +# automatically read and these particular cases re-run before any +# novel cases are generated. +# +# It is recommended to check this file in to source control so that +# everyone who runs the test benefits from these saved cases. +cc 8562956e686626817c0364007d3400decbbefdc525472d362bdb6899efa20a2f # shrinks to name = "\u{323b0}" +cc 11362e136cbc28e2ab312999d6e76784f1255ebbcfd3d5ac02ab7672e12c54fe # shrinks to name = "0" +cc 55d6a474df9998d0f1a3c83ba563e1c48701ecc5970a5c2310fb8cf6e3d58764 # shrinks to price_f64 = 0.01554934920672422 diff --git a/src/applications/microservices/petfood-rs/tests/property_tests.rs b/src/applications/microservices/petfood-rs/tests/property_tests.rs new file mode 100644 index 00000000..c1ff5217 --- /dev/null +++ b/src/applications/microservices/petfood-rs/tests/property_tests.rs @@ -0,0 +1,305 @@ +use chrono::Utc; +use petfood_rs::models::{ + validate_cart_quantity, validate_food_name, validate_food_price, AddCartItemRequest, + AvailabilityStatus, Cart, CartItem, CreateFoodRequest, FoodType, PetType, +}; +use proptest::prelude::*; +use rust_decimal::Decimal; + +// Property-based test strategies +prop_compose! { + fn arb_pet_type()(pet_type in prop_oneof![ + Just(PetType::Puppy), + Just(PetType::Kitten), + Just(PetType::Bunny), + ]) -> PetType { + pet_type + } +} + +prop_compose! { + fn arb_food_type()(food_type in prop_oneof![ + Just(FoodType::Dry), + Just(FoodType::Wet), + Just(FoodType::Treats), + Just(FoodType::Supplements), + ]) -> FoodType { + food_type + } +} + +prop_compose! { + fn arb_availability_status()(status in prop_oneof![ + Just(AvailabilityStatus::InStock), + Just(AvailabilityStatus::OutOfStock), + Just(AvailabilityStatus::Discontinued), + Just(AvailabilityStatus::PreOrder), + ]) -> AvailabilityStatus { + status + } +} + +prop_compose! { + fn arb_valid_food_name()(name in "[a-zA-Z0-9 ]{3,100}") -> String { + name + } +} + +prop_compose! { + fn arb_valid_price()(cents in 1u32..100000) -> Decimal { + // Generate prices as cents and convert to decimal with exactly 2 decimal places + Decimal::from_parts(cents, 0, 0, false, 2) + } +} + +prop_compose! { + fn arb_valid_quantity()(quantity in 1u32..1000) -> u32 { + quantity + } +} + +prop_compose! { + fn arb_create_food_request()( + pet_type in arb_pet_type(), + name in arb_valid_food_name(), + food_type in arb_food_type(), + description in "[a-zA-Z0-9 .,!]{10,500}", + price in arb_valid_price(), + ingredients in prop::collection::vec("[a-zA-Z ]{3,20}", 1..10), + feeding_guidelines in prop::option::of("[a-zA-Z0-9 .,]{10,200}"), + stock_quantity in arb_valid_quantity(), + ) -> CreateFoodRequest { + CreateFoodRequest { + pet_type, + name, + food_type, + description, + price, + // No image field - will be generated via events + nutritional_info: None, + ingredients, + feeding_guidelines, + stock_quantity, + } + } +} + +prop_compose! { + fn arb_add_cart_item_request()( + food_id in "[a-zA-Z0-9-]{36}", + quantity in arb_valid_quantity(), + ) -> AddCartItemRequest { + AddCartItemRequest { + food_id, + quantity, + } + } +} + +proptest! { + #[test] + fn test_food_name_validation(name in ".*") { + let result = validate_food_name(&name); + let trimmed = name.trim(); + + if !trimmed.is_empty() && trimmed.len() <= 200 && !trimmed.chars().any(|c| c.is_control() && c != '\n' && c != '\r' && c != '\t') { + prop_assert!(result.is_ok()); + } else { + prop_assert!(result.is_err()); + } + } + + #[test] + fn test_price_validation(price_f64 in any::()) { + if let Some(price) = Decimal::from_f64_retain(price_f64) { + let result = validate_food_price(&price); + + // Check if price is in valid range AND has valid precision (max 2 decimal places) + let min_price = Decimal::from_parts(1, 0, 0, false, 2); // 0.01 + let max_price = Decimal::from(10000); + let valid_range = price >= min_price && price <= max_price; + let valid_precision = price.scale() <= 2; + + if valid_range && valid_precision { + prop_assert!(result.is_ok()); + } else { + prop_assert!(result.is_err()); + } + } + } + + #[test] + fn test_quantity_validation(quantity in any::()) { + let result = validate_cart_quantity(quantity); + + if quantity > 0 && quantity <= 1000 { + prop_assert!(result.is_ok()); + } else { + prop_assert!(result.is_err()); + } + } + + #[test] + fn test_create_food_request_validation(request in arb_create_food_request()) { + // All generated requests should be valid + prop_assert!(validate_food_name(&request.name).is_ok()); + prop_assert!(validate_food_price(&request.price).is_ok()); + prop_assert!(validate_cart_quantity(request.stock_quantity).is_ok()); + prop_assert!(!request.description.is_empty()); + prop_assert!(!request.ingredients.is_empty()); + } + + #[test] + fn test_cart_item_request_validation(request in arb_add_cart_item_request()) { + // All generated requests should be valid + prop_assert!(validate_cart_quantity(request.quantity).is_ok()); + prop_assert!(!request.food_id.is_empty()); + } +} + +proptest! { + #[test] + fn test_food_serialization_roundtrip(request in arb_create_food_request()) { + // Test that CreateFoodRequest can be serialized and deserialized + let json = serde_json::to_string(&request).unwrap(); + let deserialized: CreateFoodRequest = serde_json::from_str(&json).unwrap(); + + prop_assert_eq!(request.name, deserialized.name); + prop_assert_eq!(request.pet_type, deserialized.pet_type); + prop_assert_eq!(request.food_type, deserialized.food_type); + prop_assert_eq!(request.price, deserialized.price); + prop_assert_eq!(request.stock_quantity, deserialized.stock_quantity); + } + + #[test] + fn test_cart_operations_invariants( + user_id in "[a-zA-Z0-9-]{36}", + items in prop::collection::vec(arb_add_cart_item_request(), 0..10) + ) { + // Test cart invariants + let mut cart = Cart { + user_id: user_id.clone(), + items: vec![], + created_at: Utc::now(), + updated_at: Utc::now(), + }; + + // Add items to cart + for item_request in items { + let cart_item = CartItem { + food_id: item_request.food_id.clone(), + quantity: item_request.quantity, + unit_price: Decimal::from(10), // Fixed price for testing + added_at: Utc::now(), + }; + + // Check if item already exists + if let Some(existing_item) = cart.items.iter_mut().find(|i| i.food_id == cart_item.food_id) { + existing_item.quantity += cart_item.quantity; + } else { + cart.items.push(cart_item); + } + } + + // Invariants + prop_assert_eq!(cart.user_id, user_id); + prop_assert!(cart.items.iter().all(|item| item.quantity > 0)); + prop_assert!(cart.items.iter().all(|item| !item.food_id.is_empty())); + + // No duplicate food_ids + let mut food_ids: Vec<_> = cart.items.iter().map(|item| &item.food_id).collect(); + food_ids.sort(); + food_ids.dedup(); + prop_assert_eq!(food_ids.len(), cart.items.len()); + } + + #[test] + fn test_enum_serialization( + pet_type in arb_pet_type(), + food_type in arb_food_type(), + availability in arb_availability_status() + ) { + // Test enum serialization/deserialization + let pet_json = serde_json::to_string(&pet_type).unwrap(); + let pet_deserialized: PetType = serde_json::from_str(&pet_json).unwrap(); + prop_assert_eq!(pet_type, pet_deserialized); + + let food_json = serde_json::to_string(&food_type).unwrap(); + let food_deserialized: FoodType = serde_json::from_str(&food_json).unwrap(); + prop_assert_eq!(food_type, food_deserialized); + + let availability_json = serde_json::to_string(&availability).unwrap(); + let availability_deserialized: AvailabilityStatus = serde_json::from_str(&availability_json).unwrap(); + prop_assert_eq!(availability, availability_deserialized); + } + + #[test] + fn test_price_arithmetic_properties( + price1 in arb_valid_price(), + price2 in arb_valid_price(), + quantity in 1u32..100 + ) { + // Test price arithmetic properties + let sum = price1 + price2; + prop_assert!(sum >= price1); + prop_assert!(sum >= price2); + + let product = price1 * Decimal::from(quantity); + if quantity > 1 { + prop_assert!(product > price1); + } else { + prop_assert_eq!(product, price1); + } + + // Test that price operations don't overflow or underflow + // Decimal in Rust doesn't have is_finite, but operations are safe + prop_assert!(sum >= Decimal::ZERO); + prop_assert!(product >= Decimal::ZERO); + } +} + +#[cfg(test)] +mod edge_case_tests { + use super::*; + + #[test] + fn test_empty_string_validation() { + assert!(validate_food_name("").is_err()); + assert!(validate_food_name(" ").is_err()); + } + + #[test] + fn test_zero_and_negative_values() { + assert!(validate_food_price(&Decimal::ZERO).is_err()); // Zero is not allowed (minimum is 0.01) + assert!(validate_food_price(&Decimal::from(-1)).is_err()); // Negative is not allowed + assert!(validate_cart_quantity(0).is_err()); // Zero quantity not allowed + } + + #[test] + fn test_boundary_values() { + // Test boundary values for food name length (MIN_FOOD_NAME_LENGTH = 1) + assert!(validate_food_name("").is_err()); // Empty string + assert!(validate_food_name("a").is_ok()); // Minimum valid (1 char) + assert!(validate_food_name(&"a".repeat(200)).is_ok()); // Maximum valid (200 chars) + assert!(validate_food_name(&"a".repeat(201)).is_err()); // Too long + + // Test boundary values for quantity + assert!(validate_cart_quantity(1).is_ok()); // Minimum valid + assert!(validate_cart_quantity(1000).is_ok()); // Maximum valid + assert!(validate_cart_quantity(1001).is_err()); // Too high + + // Test boundary values for price (MIN_PRICE = 0.01, MAX_PRICE = 9999.99) + assert!(validate_food_price(&Decimal::from_parts(1, 0, 0, false, 2)).is_ok()); // Minimum valid (0.01) + assert!(validate_food_price(&Decimal::from_parts(999999, 0, 0, false, 2)).is_ok()); // Maximum valid (9999.99) + assert!(validate_food_price(&Decimal::from(10000)).is_err()); // Too high + } + + #[test] + fn test_special_characters_in_names() { + assert!(validate_food_name("Food with spaces").is_ok()); + assert!(validate_food_name("Food123").is_ok()); + assert!(validate_food_name("Food@#$").is_ok()); // Special characters are allowed + assert!(validate_food_name("Food\nwith\nnewlines").is_ok()); // Newlines are allowed + assert!(validate_food_name("Food\x00with\x01control").is_err()); // Control characters not allowed + } +} diff --git a/PetAdoptions/petlistadoptions-py/Dockerfile b/src/applications/microservices/petlistadoptions-py/Dockerfile similarity index 89% rename from PetAdoptions/petlistadoptions-py/Dockerfile rename to src/applications/microservices/petlistadoptions-py/Dockerfile index 9cb21ca8..f5038125 100644 --- a/PetAdoptions/petlistadoptions-py/Dockerfile +++ b/src/applications/microservices/petlistadoptions-py/Dockerfile @@ -1,4 +1,4 @@ -FROM python:3.11-slim as builder +FROM public.ecr.aws/docker/library/python:3.11-slim as builder WORKDIR /app @@ -16,7 +16,7 @@ RUN pip install --no-cache-dir -r requirements.txt COPY . . # Production stage -FROM python:3.11-slim +FROM public.ecr.aws/docker/library/python:3.11-slim WORKDIR /app @@ -46,4 +46,4 @@ HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ EXPOSE 80 # Use startup script (running as root for port 80 access) -CMD ["./start.sh"] \ No newline at end of file +CMD ["./start.sh"] \ No newline at end of file diff --git a/PetAdoptions/petlistadoptions-py/app.py b/src/applications/microservices/petlistadoptions-py/app.py similarity index 58% rename from PetAdoptions/petlistadoptions-py/app.py rename to src/applications/microservices/petlistadoptions-py/app.py index bb6c9993..c38cd84a 100644 --- a/PetAdoptions/petlistadoptions-py/app.py +++ b/src/applications/microservices/petlistadoptions-py/app.py @@ -1,17 +1,24 @@ -import os +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 import json import logging +import os import time -from datetime import datetime -from typing import List, Dict, Any, Optional from contextlib import contextmanager +from typing import Any +from typing import Dict +from typing import List +from typing import Optional import boto3 import psycopg2 import requests -from fastapi import FastAPI, HTTPException -from fastapi.responses import JSONResponse -from prometheus_client import Counter, Histogram, generate_latest, CONTENT_TYPE_LATEST +from fastapi import FastAPI +from fastapi import HTTPException +from prometheus_client import CONTENT_TYPE_LATEST +from prometheus_client import Counter +from prometheus_client import generate_latest +from prometheus_client import Histogram from pydantic import BaseModel # Configure logging @@ -19,8 +26,17 @@ logger = logging.getLogger(__name__) # Prometheus metrics -REQUEST_COUNT = Counter('petlistadoptions_requests_total', 'Number of requests received', ['endpoint', 'error']) -REQUEST_LATENCY = Histogram('petlistadoptions_requests_latency_seconds', 'Request durations in seconds', ['endpoint', 'error']) +REQUEST_COUNT = Counter( + "petlistadoptions_requests_total", + "Number of requests received", + ["endpoint", "error"], +) +REQUEST_LATENCY = Histogram( + "petlistadoptions_requests_latency_seconds", + "Request durations in seconds", + ["endpoint", "error"], +) + # Pydantic models for type safety class Adoption(BaseModel): @@ -34,19 +50,31 @@ class Adoption(BaseModel): peturl: Optional[str] = None price: Optional[str] = None + class HealthResponse(BaseModel): status: str + # Create FastAPI app app = FastAPI( title="Pet List Adoptions Service", description="Service for listing pet adoptions with enrichment from pet search", - version="1.0.0" + version="1.0.0", ) + class DatabaseConfig: """Database configuration from AWS Secrets Manager""" - def __init__(self, engine: str, host: str, username: str, password: str, dbname: str, port: int): + + def __init__( + self, + engine: str, + host: str, + username: str, + password: str, + dbname: str, + port: int, + ): self.engine = engine self.host = host self.username = username @@ -54,61 +82,63 @@ def __init__(self, engine: str, host: str, username: str, password: str, dbname: self.dbname = dbname self.port = port + class PetAdoptionsService: """Main service class following Python best practices""" - + def __init__(self): self.pet_search_url = os.getenv("APP_PET_SEARCH_URL") self.rds_secret_arn = os.getenv("APP_RDS_SECRET_ARN") - + # If not set via env vars, try to get from Parameter Store if not self.pet_search_url or not self.rds_secret_arn: self._fetch_from_parameter_store() - + def _fetch_from_parameter_store(self): """Fetch configuration from AWS Parameter Store""" try: - ssm = boto3.client('ssm') + ssm = boto3.client("ssm") response = ssm.get_parameters( - Names=[ - '/petstore/rdssecretarn', - '/petstore/searchapiurl' - ] + Names=["/petstore/rdssecretarn", "/petstore/searchapiurl"], ) - - for param in response['Parameters']: - if param['Name'] == '/petstore/rdssecretarn': - self.rds_secret_arn = param['Value'] - elif param['Name'] == '/petstore/searchapiurl': - self.pet_search_url = param['Value'] - + + for param in response["Parameters"]: + if param["Name"] == "/petstore/rdssecretarn": + self.rds_secret_arn = param["Value"] + elif param["Name"] == "/petstore/searchapiurl": + self.pet_search_url = param["Value"] + except Exception as e: logger.error(f"Failed to fetch from Parameter Store: {e}") - + def _get_database_connection_string(self) -> str: """Get database connection string from AWS Secrets Manager""" try: # Check if this is a local test setup - if self.rds_secret_arn == "local-secret": + if self.rds_secret_arn == "local-secret": # pragma: allowlist secret # Read from local file for testing - with open("/app/local-secret.json", "r") as f: + with open("/app/local-secret.json") as f: # pragma: allowlist secret secret_data = json.loads(f.read()) else: # Use AWS Secrets Manager - secrets = boto3.client('secretsmanager') + secrets = boto3.client("secretsmanager") response = secrets.get_secret_value(SecretId=self.rds_secret_arn) - secret_data = json.loads(response['SecretString']) - + secret_data = json.loads(response["SecretString"]) + # Build connection string for psycopg2 - connection_string = f"postgresql://{secret_data['username']}:{secret_data['password']}@{secret_data['host']}:{secret_data.get('port', 5432)}/{secret_data['dbname']}" - + connection_string = ( + f"postgresql://{secret_data['username']}:" + f"{secret_data['password']}@{secret_data['host']}:" + f"{secret_data.get('port', 5432)}/{secret_data['dbname']}" + ) + logger.info(f"Generated connection string for host: {secret_data['host']}") return connection_string - + except Exception as e: logger.error(f"Failed to get database connection string: {e}") raise - + @contextmanager def _get_database_connection(self): """Context manager for database connections""" @@ -118,103 +148,115 @@ def _get_database_connection(self): yield conn finally: conn.close() - + def _get_latest_adoptions(self) -> List[Dict[str, Any]]: """Get latest adoptions from database""" with self._get_database_connection() as conn: with conn.cursor() as cursor: - cursor.execute("SELECT pet_id, transaction_id, adoption_date FROM transactions ORDER BY id DESC LIMIT 25") + cursor.execute( + "SELECT pet_id, transaction_id, adoption_date FROM " + "transactions ORDER BY id DESC LIMIT 25", + ) rows = cursor.fetchall() - + return [ { - 'pet_id': pet_id, - 'transaction_id': transaction_id, - 'adoption_date': adoption_date.isoformat() if adoption_date else None + "pet_id": pet_id, + "transaction_id": transaction_id, + "adoption_date": ( + adoption_date.isoformat() if adoption_date else None + ), } for pet_id, transaction_id, adoption_date in rows ] - + def _search_pet_info(self, pet_id: str) -> List[Dict[str, Any]]: """Search for pet information by pet_id""" url = f"{self.pet_search_url}petid={pet_id}" - + try: - response = requests.get(url) + response = requests.get(url, timeout=30) response.raise_for_status() return response.json() except Exception as e: logger.error(f"Failed to search pet {pet_id}: {e}") return [] - + def health_check(self) -> str: """Health check endpoint""" return "alive" - + def list_adoptions(self) -> List[Adoption]: """List adoptions with pet information""" start_time = time.time() - + try: # Get adoptions from database adoptions = self._get_latest_adoptions() - + # Enrich with pet information enriched_adoptions = [] for adoption in adoptions: - pet_info = self._search_pet_info(adoption['pet_id']) - + pet_info = self._search_pet_info(adoption["pet_id"]) + for pet in pet_info: enriched_adoption = Adoption( - transactionid=adoption['transaction_id'], - adoptiondate=adoption['adoption_date'], - availability=pet.get('availability', ''), - cuteness_rate=pet.get('cuteness_rate', ''), - petcolor=pet.get('petcolor', ''), - petid=pet.get('petid', ''), - pettype=pet.get('pettype', ''), - peturl=pet.get('peturl', ''), - price=pet.get('price', '') + transactionid=adoption["transaction_id"], + adoptiondate=adoption["adoption_date"], + availability=pet.get("availability", ""), + cuteness_rate=pet.get("cuteness_rate", ""), + petcolor=pet.get("petcolor", ""), + petid=pet.get("petid", ""), + pettype=pet.get("pettype", ""), + peturl=pet.get("peturl", ""), + price=pet.get("price", ""), ) enriched_adoptions.append(enriched_adoption) - + # Record metrics duration = time.time() - start_time - REQUEST_COUNT.labels(endpoint='adoptionlist', error='false').inc() - REQUEST_LATENCY.labels(endpoint='adoptionlist', error='false').observe(duration) - + REQUEST_COUNT.labels(endpoint="adoptionlist", error="false").inc() + REQUEST_LATENCY.labels(endpoint="adoptionlist", error="false").observe( + duration, + ) + return enriched_adoptions - + except Exception as e: # Record error metrics duration = time.time() - start_time - REQUEST_COUNT.labels(endpoint='adoptionlist', error='true').inc() - REQUEST_LATENCY.labels(endpoint='adoptionlist', error='true').observe(duration) + REQUEST_COUNT.labels(endpoint="adoptionlist", error="true").inc() + REQUEST_LATENCY.labels(endpoint="adoptionlist", error="true").observe( + duration, + ) logger.error(f"Error in list_adoptions: {e}") raise + # Initialize service service = PetAdoptionsService() + @app.get("/health/status", response_model=HealthResponse, tags=["health"]) async def health_check(): """Health check endpoint""" start_time = time.time() - + try: result = service.health_check() duration = time.time() - start_time - - REQUEST_COUNT.labels(endpoint='health_check', error='false').inc() - REQUEST_LATENCY.labels(endpoint='health_check', error='false').observe(duration) - + + REQUEST_COUNT.labels(endpoint="health_check", error="false").inc() + REQUEST_LATENCY.labels(endpoint="health_check", error="false").observe(duration) + return HealthResponse(status=result) except Exception as e: duration = time.time() - start_time - REQUEST_COUNT.labels(endpoint='health_check', error='true').inc() - REQUEST_LATENCY.labels(endpoint='health_check', error='true').observe(duration) + REQUEST_COUNT.labels(endpoint="health_check", error="true").inc() + REQUEST_LATENCY.labels(endpoint="health_check", error="true").observe(duration) raise HTTPException(status_code=500, detail=str(e)) + @app.get("/api/adoptionlist/", response_model=List[Adoption], tags=["adoptions"]) async def list_adoptions(): """List adoptions endpoint""" @@ -224,12 +266,17 @@ async def list_adoptions(): except Exception as e: raise HTTPException(status_code=500, detail=str(e)) + @app.get("/metrics", tags=["monitoring"]) async def metrics(): """Prometheus metrics endpoint""" - return generate_latest(), 200, {'Content-Type': CONTENT_TYPE_LATEST} + return generate_latest(), 200, {"Content-Type": CONTENT_TYPE_LATEST} + if __name__ == "__main__": import uvicorn - port = int(os.environ.get('PORT', 80)) - uvicorn.run(app, host="0.0.0.0", port=port) \ No newline at end of file + + port = int(os.environ.get("PORT", 80)) + # Use localhost for production security, 0.0.0.0 only for containerized environments + host = os.environ.get("HOST", "127.0.0.1") + uvicorn.run(app, host=host, port=port) diff --git a/src/applications/microservices/petlistadoptions-py/docker-compose.yml b/src/applications/microservices/petlistadoptions-py/docker-compose.yml new file mode 100644 index 00000000..a5c71ff7 --- /dev/null +++ b/src/applications/microservices/petlistadoptions-py/docker-compose.yml @@ -0,0 +1,61 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +version: '3.8' + +services: + # PostgreSQL database for local testing + postgres: + image: postgres:15 + environment: + POSTGRES_DB: adoptions + POSTGRES_USER: petstore + POSTGRES_PASSWORD: petstore123. #pragma: allowlist secret + ports: + - '5432:5432' + volumes: + - postgres_data:/var/lib/postgresql/data + - ./init.sql:/docker-entrypoint-initdb.d/init.sql + healthcheck: + test: ['CMD-SHELL', 'pg_isready -U petstore -d adoptions'] + interval: 10s + timeout: 5s + retries: 5 + + # Pet List Adoptions Service + petlistadoptions: + build: . + ports: + - '80:80' + environment: + - APP_PET_SEARCH_URL=http://localhost:8080/ + - APP_RDS_SECRET_ARN=local-secret + - OTEL_EXPORTER_OTLP_ENDPOINT=0.0.0.0:4317 + depends_on: + postgres: + condition: service_healthy + volumes: + - ./local-secret.json:/app/local-secret.json:ro + command: ['python', 'app.py'] + + # Mock Pet Search Service (for testing) + petsearch-mock: + image: mockserver/mockserver:latest + ports: + - '8080:1080' + environment: + - MOCKSERVER_PROPERTY_FILE=/config/mockserver.properties + volumes: + - ./mock-petsearch.json:/config/mock-petsearch.json:ro + command: + [ + 'java', + '-jar', + '/opt/mockserver/mockserver-netty-jar-with-dependencies.jar', + '-serverPort', + '1080', + '-logLevel', + 'INFO', + ] + +volumes: + postgres_data: diff --git a/src/applications/microservices/petlistadoptions-py/requirements.txt b/src/applications/microservices/petlistadoptions-py/requirements.txt new file mode 100644 index 00000000..54df4272 --- /dev/null +++ b/src/applications/microservices/petlistadoptions-py/requirements.txt @@ -0,0 +1,36 @@ +annotated-types==0.7.0 +anyio==4.10.0 +boto3==1.40.10 +botocore==1.40.10 +certifi==2025.8.3 +charset-normalizer==3.4.3 +click==8.2.1 +fastapi==0.116.1 +h11==0.16.0 +httptools==0.6.4 +idna==3.10 +jmespath==1.0.1 +mypy==1.17.1 +mypy_extensions==1.1.0 +pathspec==0.12.1 +prometheus_client==0.22.1 +psycopg2-binary==2.9.10 +pydantic==2.11.7 +pydantic_core==2.33.2 +python-dateutil==2.9.0.post0 +python-dotenv==1.1.1 +PyYAML==6.0.2 +requests==2.32.4 +s3transfer==0.13.1 +six==1.17.0 +sniffio==1.3.1 +starlette==0.47.2 +types-psycopg2==2.9.21.20250809 +types-requests==2.32.4.20250809 +typing-inspection==0.4.1 +typing_extensions==4.14.1 +urllib3==2.5.0 +uvicorn==0.35.0 +uvloop==0.21.0 +watchfiles==1.1.0 +websockets==15.0.1 diff --git a/PetAdoptions/petlistadoptions-py/start.sh b/src/applications/microservices/petlistadoptions-py/start.sh similarity index 87% rename from PetAdoptions/petlistadoptions-py/start.sh rename to src/applications/microservices/petlistadoptions-py/start.sh index 909b9716..14051ede 100755 --- a/PetAdoptions/petlistadoptions-py/start.sh +++ b/src/applications/microservices/petlistadoptions-py/start.sh @@ -1,5 +1,8 @@ #!/bin/bash +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + # Pet List Adoptions Service Startup Script # Note: Running as root for port 80 access @@ -14,15 +17,15 @@ if [ -f /.dockerenv ] || [ -n "$ECS_CONTAINER_METADATA_URI" ]; then echo "Running in containerized environment (Docker/ECS)..." echo "Port: $PORT" echo "Workers: $WORKERS" - + # Wait for any dependencies if needed if [ -n "$WAIT_FOR_DB" ]; then echo "Waiting for database to be ready..." sleep 10 fi - + exec uvicorn app:app --host 0.0.0.0 --port $PORT --workers $WORKERS else echo "Running in development mode..." exec uvicorn app:app --reload --host 0.0.0.0 --port $PORT -fi \ No newline at end of file +fi \ No newline at end of file diff --git a/PetAdoptions/petsearch-java/.gitignore b/src/applications/microservices/petsearch-java/.gitignore similarity index 100% rename from PetAdoptions/petsearch-java/.gitignore rename to src/applications/microservices/petsearch-java/.gitignore diff --git a/PetAdoptions/petsearch-java/Dockerfile b/src/applications/microservices/petsearch-java/Dockerfile similarity index 55% rename from PetAdoptions/petsearch-java/Dockerfile rename to src/applications/microservices/petsearch-java/Dockerfile index 3b0ee79d..aa12acd4 100644 --- a/PetAdoptions/petsearch-java/Dockerfile +++ b/src/applications/microservices/petsearch-java/Dockerfile @@ -1,4 +1,4 @@ -FROM gradle:7.3-jdk17 as build +FROM --platform=$BUILDPLATFORM public.ecr.aws/docker/library/gradle:7.3-jdk17 as build WORKDIR /app COPY ./build.gradle ./build.gradle @@ -6,9 +6,9 @@ COPY ./src ./src COPY ./settings.gradle ./settings.gradle ENV GRADLE_OPTS "-Dorg.gradle.daemon=false" -RUN gradle build -DexcludeTags='integration' +RUN gradle build -DexcludeTags='integration' --no-daemon --stacktrace -FROM amazoncorretto:17-alpine +FROM public.ecr.aws/amazoncorretto/amazoncorretto:17-al2-generic-jdk WORKDIR /app ARG JAR_FILE=build/libs/\*.jar diff --git a/PetAdoptions/petsearch-java/manual-instrumentation-complete/src/main/resources/application.yml b/src/applications/microservices/petsearch-java/bin/main/application.yml similarity index 54% rename from PetAdoptions/petsearch-java/manual-instrumentation-complete/src/main/resources/application.yml rename to src/applications/microservices/petsearch-java/bin/main/application.yml index 9126d168..7f4c1484 100644 --- a/PetAdoptions/petsearch-java/manual-instrumentation-complete/src/main/resources/application.yml +++ b/src/applications/microservices/petsearch-java/bin/main/application.yml @@ -1,3 +1,5 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 spring: application: name: petstore diff --git a/src/applications/microservices/petsearch-java/bin/main/ca/petsearch/Application.class b/src/applications/microservices/petsearch-java/bin/main/ca/petsearch/Application.class new file mode 100644 index 00000000..da7e0913 Binary files /dev/null and b/src/applications/microservices/petsearch-java/bin/main/ca/petsearch/Application.class differ diff --git a/src/applications/microservices/petsearch-java/bin/main/ca/petsearch/ApplicationFilter.class b/src/applications/microservices/petsearch-java/bin/main/ca/petsearch/ApplicationFilter.class new file mode 100644 index 00000000..e5cc7c3c Binary files /dev/null and b/src/applications/microservices/petsearch-java/bin/main/ca/petsearch/ApplicationFilter.class differ diff --git a/src/applications/microservices/petsearch-java/bin/main/ca/petsearch/MetricEmitter.class b/src/applications/microservices/petsearch-java/bin/main/ca/petsearch/MetricEmitter.class new file mode 100644 index 00000000..31272aee Binary files /dev/null and b/src/applications/microservices/petsearch-java/bin/main/ca/petsearch/MetricEmitter.class differ diff --git a/src/applications/microservices/petsearch-java/bin/main/ca/petsearch/PseudoRandomNumberGenerator.class b/src/applications/microservices/petsearch-java/bin/main/ca/petsearch/PseudoRandomNumberGenerator.class new file mode 100644 index 00000000..450099f4 Binary files /dev/null and b/src/applications/microservices/petsearch-java/bin/main/ca/petsearch/PseudoRandomNumberGenerator.class differ diff --git a/src/applications/microservices/petsearch-java/bin/main/ca/petsearch/RandomNumberGenerator.class b/src/applications/microservices/petsearch-java/bin/main/ca/petsearch/RandomNumberGenerator.class new file mode 100644 index 00000000..f7f96495 Binary files /dev/null and b/src/applications/microservices/petsearch-java/bin/main/ca/petsearch/RandomNumberGenerator.class differ diff --git a/src/applications/microservices/petsearch-java/bin/main/ca/petsearch/WebConfig.class b/src/applications/microservices/petsearch-java/bin/main/ca/petsearch/WebConfig.class new file mode 100644 index 00000000..8c7a9a9c Binary files /dev/null and b/src/applications/microservices/petsearch-java/bin/main/ca/petsearch/WebConfig.class differ diff --git a/src/applications/microservices/petsearch-java/bin/main/ca/petsearch/controllers/ErrorResponse.class b/src/applications/microservices/petsearch-java/bin/main/ca/petsearch/controllers/ErrorResponse.class new file mode 100644 index 00000000..4c44d374 Binary files /dev/null and b/src/applications/microservices/petsearch-java/bin/main/ca/petsearch/controllers/ErrorResponse.class differ diff --git a/src/applications/microservices/petsearch-java/bin/main/ca/petsearch/controllers/HealthController.class b/src/applications/microservices/petsearch-java/bin/main/ca/petsearch/controllers/HealthController.class new file mode 100644 index 00000000..614f1744 Binary files /dev/null and b/src/applications/microservices/petsearch-java/bin/main/ca/petsearch/controllers/HealthController.class differ diff --git a/src/applications/microservices/petsearch-java/bin/main/ca/petsearch/controllers/Pet.class b/src/applications/microservices/petsearch-java/bin/main/ca/petsearch/controllers/Pet.class new file mode 100644 index 00000000..50dfae18 Binary files /dev/null and b/src/applications/microservices/petsearch-java/bin/main/ca/petsearch/controllers/Pet.class differ diff --git a/src/applications/microservices/petsearch-java/bin/main/ca/petsearch/controllers/SearchController.class b/src/applications/microservices/petsearch-java/bin/main/ca/petsearch/controllers/SearchController.class new file mode 100644 index 00000000..d724a49e Binary files /dev/null and b/src/applications/microservices/petsearch-java/bin/main/ca/petsearch/controllers/SearchController.class differ diff --git a/src/applications/microservices/petsearch-java/bin/main/logback-spring.xml b/src/applications/microservices/petsearch-java/bin/main/logback-spring.xml new file mode 100644 index 00000000..50b040bc --- /dev/null +++ b/src/applications/microservices/petsearch-java/bin/main/logback-spring.xml @@ -0,0 +1,31 @@ + + + + + + + + yyyy-MM-dd' 'HH:mm:ss.SSS + true + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/PetAdoptions/petsearch-java/build.gradle b/src/applications/microservices/petsearch-java/build.gradle similarity index 99% rename from PetAdoptions/petsearch-java/build.gradle rename to src/applications/microservices/petsearch-java/build.gradle index 7948b011..91b30a5f 100644 --- a/PetAdoptions/petsearch-java/build.gradle +++ b/src/applications/microservices/petsearch-java/build.gradle @@ -20,13 +20,13 @@ ext { } dependencies { - + implementation 'ch.qos.logback:logback-classic:1.2.3' implementation 'ch.qos.logback.contrib:logback-jackson:0.1.5' implementation 'ch.qos.logback.contrib:logback-json-classic:0.1.5' implementation 'com.fasterxml.jackson.core:jackson-databind:2.12.0' implementation 'org.slf4j:slf4j-api:1.7.25' - + implementation 'org.springframework.boot:spring-boot-starter-web' diff --git a/PetAdoptions/petsearch-java/docker-compose.yml b/src/applications/microservices/petsearch-java/docker-compose.yml similarity index 96% rename from PetAdoptions/petsearch-java/docker-compose.yml rename to src/applications/microservices/petsearch-java/docker-compose.yml index 00996b45..4467c07f 100644 --- a/PetAdoptions/petsearch-java/docker-compose.yml +++ b/src/applications/microservices/petsearch-java/docker-compose.yml @@ -1,3 +1,5 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 version: "3.8" services: diff --git a/PetAdoptions/petsearch-java/gradle/wrapper/gradle-wrapper.jar b/src/applications/microservices/petsearch-java/gradle/wrapper/gradle-wrapper.jar similarity index 100% rename from PetAdoptions/petsearch-java/gradle/wrapper/gradle-wrapper.jar rename to src/applications/microservices/petsearch-java/gradle/wrapper/gradle-wrapper.jar diff --git a/PetAdoptions/petsearch-java/gradle/wrapper/gradle-wrapper.properties b/src/applications/microservices/petsearch-java/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from PetAdoptions/petsearch-java/gradle/wrapper/gradle-wrapper.properties rename to src/applications/microservices/petsearch-java/gradle/wrapper/gradle-wrapper.properties diff --git a/PetAdoptions/petsearch-java/gradlew b/src/applications/microservices/petsearch-java/gradlew similarity index 99% rename from PetAdoptions/petsearch-java/gradlew rename to src/applications/microservices/petsearch-java/gradlew index fbd7c515..4f906e0c 100755 --- a/PetAdoptions/petsearch-java/gradlew +++ b/src/applications/microservices/petsearch-java/gradlew @@ -130,7 +130,7 @@ fi if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` - + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath diff --git a/PetAdoptions/petsearch-java/gradlew.bat b/src/applications/microservices/petsearch-java/gradlew.bat similarity index 96% rename from PetAdoptions/petsearch-java/gradlew.bat rename to src/applications/microservices/petsearch-java/gradlew.bat index a9f778a7..5093609d 100644 --- a/PetAdoptions/petsearch-java/gradlew.bat +++ b/src/applications/microservices/petsearch-java/gradlew.bat @@ -1,104 +1,104 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem - -@if "%DEBUG%" == "" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%" == "" set DIRNAME=. -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init - -echo. -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto init - -echo. -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% -echo. -echo Please set the JAVA_HOME variable in your environment to match the -echo location of your Java installation. - -goto fail - -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% - -:end -@rem End local scope for the variables with windows NT shell -if "%ERRORLEVEL%"=="0" goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 -exit /b 1 - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/PetAdoptions/petsearch-java/src/main/java/ca/petsearch/Application.java b/src/applications/microservices/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/Application.java similarity index 73% rename from PetAdoptions/petsearch-java/src/main/java/ca/petsearch/Application.java rename to src/applications/microservices/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/Application.java index ec1c1c52..44a17d3c 100644 --- a/PetAdoptions/petsearch-java/src/main/java/ca/petsearch/Application.java +++ b/src/applications/microservices/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/Application.java @@ -1,3 +1,7 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ package ca.petsearch; import org.springframework.boot.SpringApplication; diff --git a/PetAdoptions/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/ApplicationFilter.java b/src/applications/microservices/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/ApplicationFilter.java similarity index 92% rename from PetAdoptions/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/ApplicationFilter.java rename to src/applications/microservices/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/ApplicationFilter.java index 1d429d05..ecd7f5cc 100644 --- a/PetAdoptions/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/ApplicationFilter.java +++ b/src/applications/microservices/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/ApplicationFilter.java @@ -1,3 +1,7 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ package ca.petsearch; import org.springframework.web.util.ContentCachingResponseWrapper; diff --git a/PetAdoptions/petsearch-java/src/main/java/ca/petsearch/MetricEmitter.java b/src/applications/microservices/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/MetricEmitter.java similarity index 97% rename from PetAdoptions/petsearch-java/src/main/java/ca/petsearch/MetricEmitter.java rename to src/applications/microservices/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/MetricEmitter.java index 8f7e85c6..6ec80b16 100644 --- a/PetAdoptions/petsearch-java/src/main/java/ca/petsearch/MetricEmitter.java +++ b/src/applications/microservices/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/MetricEmitter.java @@ -1,3 +1,7 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ package ca.petsearch; import io.opentelemetry.api.OpenTelemetry; diff --git a/PetAdoptions/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/PseudoRandomNumberGenerator.java b/src/applications/microservices/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/PseudoRandomNumberGenerator.java similarity index 72% rename from PetAdoptions/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/PseudoRandomNumberGenerator.java rename to src/applications/microservices/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/PseudoRandomNumberGenerator.java index 52b4c9a0..7b1e9107 100644 --- a/PetAdoptions/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/PseudoRandomNumberGenerator.java +++ b/src/applications/microservices/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/PseudoRandomNumberGenerator.java @@ -1,3 +1,7 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ package ca.petsearch; public class PseudoRandomNumberGenerator implements RandomNumberGenerator { diff --git a/src/applications/microservices/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/RandomNumberGenerator.java b/src/applications/microservices/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/RandomNumberGenerator.java new file mode 100644 index 00000000..6df02af6 --- /dev/null +++ b/src/applications/microservices/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/RandomNumberGenerator.java @@ -0,0 +1,9 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +package ca.petsearch; + +public interface RandomNumberGenerator { + int nextNonNegativeInt(int max); +} diff --git a/PetAdoptions/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/TracingLogFilter.java b/src/applications/microservices/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/TracingLogFilter.java similarity index 89% rename from PetAdoptions/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/TracingLogFilter.java rename to src/applications/microservices/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/TracingLogFilter.java index a92cce9b..a544a3e1 100644 --- a/PetAdoptions/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/TracingLogFilter.java +++ b/src/applications/microservices/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/TracingLogFilter.java @@ -1,3 +1,7 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ package ca.petsearch; import ch.qos.logback.classic.spi.ILoggingEvent; diff --git a/PetAdoptions/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/TracingRequestInterceptor.java b/src/applications/microservices/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/TracingRequestInterceptor.java similarity index 96% rename from PetAdoptions/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/TracingRequestInterceptor.java rename to src/applications/microservices/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/TracingRequestInterceptor.java index c57eb0e3..240ddfbd 100644 --- a/PetAdoptions/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/TracingRequestInterceptor.java +++ b/src/applications/microservices/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/TracingRequestInterceptor.java @@ -1,3 +1,7 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ package ca.petsearch; import io.opentelemetry.api.OpenTelemetry; diff --git a/PetAdoptions/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/WebConfig.java b/src/applications/microservices/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/WebConfig.java similarity index 98% rename from PetAdoptions/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/WebConfig.java rename to src/applications/microservices/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/WebConfig.java index 6b879549..1dbc3100 100644 --- a/PetAdoptions/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/WebConfig.java +++ b/src/applications/microservices/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/WebConfig.java @@ -1,3 +1,7 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ package ca.petsearch; import com.amazonaws.client.builder.AwsClientBuilder; diff --git a/PetAdoptions/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/controllers/HealthController.java b/src/applications/microservices/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/controllers/HealthController.java similarity index 80% rename from PetAdoptions/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/controllers/HealthController.java rename to src/applications/microservices/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/controllers/HealthController.java index 210c4ca2..b22387e5 100644 --- a/PetAdoptions/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/controllers/HealthController.java +++ b/src/applications/microservices/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/controllers/HealthController.java @@ -1,3 +1,7 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ package ca.petsearch.controllers; import org.slf4j.Logger; diff --git a/PetAdoptions/petsearch-java/src/main/java/ca/petsearch/controllers/Pet.java b/src/applications/microservices/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/controllers/Pet.java similarity index 91% rename from PetAdoptions/petsearch-java/src/main/java/ca/petsearch/controllers/Pet.java rename to src/applications/microservices/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/controllers/Pet.java index c9da3c27..45915eb7 100644 --- a/PetAdoptions/petsearch-java/src/main/java/ca/petsearch/controllers/Pet.java +++ b/src/applications/microservices/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/controllers/Pet.java @@ -1,3 +1,7 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ package ca.petsearch.controllers; public class Pet { diff --git a/PetAdoptions/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/controllers/SearchController.java b/src/applications/microservices/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/controllers/SearchController.java similarity index 98% rename from PetAdoptions/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/controllers/SearchController.java rename to src/applications/microservices/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/controllers/SearchController.java index 1d312add..c4a9e747 100644 --- a/PetAdoptions/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/controllers/SearchController.java +++ b/src/applications/microservices/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/controllers/SearchController.java @@ -1,3 +1,7 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ package ca.petsearch.controllers; import ca.petsearch.MetricEmitter; @@ -79,7 +83,7 @@ private String getPetUrl(String petType, String image) { String s3BucketName = getSSMParameter(BUCKET_NAME); String key = getKey(petType, image); - + Double randomnumber = Math.random()*9999; if (randomnumber < 100) { diff --git a/PetAdoptions/petsearch-java/src/main/resources/application.yml b/src/applications/microservices/petsearch-java/manual-instrumentation-complete/src/main/resources/application.yml similarity index 54% rename from PetAdoptions/petsearch-java/src/main/resources/application.yml rename to src/applications/microservices/petsearch-java/manual-instrumentation-complete/src/main/resources/application.yml index 9126d168..7f4c1484 100644 --- a/PetAdoptions/petsearch-java/src/main/resources/application.yml +++ b/src/applications/microservices/petsearch-java/manual-instrumentation-complete/src/main/resources/application.yml @@ -1,3 +1,5 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 spring: application: name: petstore diff --git a/PetAdoptions/petsearch-java/manual-instrumentation-complete/src/main/resources/logback-spring.xml b/src/applications/microservices/petsearch-java/manual-instrumentation-complete/src/main/resources/logback-spring.xml similarity index 91% rename from PetAdoptions/petsearch-java/manual-instrumentation-complete/src/main/resources/logback-spring.xml rename to src/applications/microservices/petsearch-java/manual-instrumentation-complete/src/main/resources/logback-spring.xml index a95beaa8..ae2abaf0 100644 --- a/PetAdoptions/petsearch-java/manual-instrumentation-complete/src/main/resources/logback-spring.xml +++ b/src/applications/microservices/petsearch-java/manual-instrumentation-complete/src/main/resources/logback-spring.xml @@ -1,3 +1,7 @@ + diff --git a/PetAdoptions/petsearch-java/manual-instrumentation-complete/src/test/java/ca/petsearch/PseudoRandomNumberGeneratorTest.java b/src/applications/microservices/petsearch-java/manual-instrumentation-complete/src/test/java/ca/petsearch/PseudoRandomNumberGeneratorTest.java similarity index 94% rename from PetAdoptions/petsearch-java/manual-instrumentation-complete/src/test/java/ca/petsearch/PseudoRandomNumberGeneratorTest.java rename to src/applications/microservices/petsearch-java/manual-instrumentation-complete/src/test/java/ca/petsearch/PseudoRandomNumberGeneratorTest.java index b9389def..55f5b0db 100644 --- a/PetAdoptions/petsearch-java/manual-instrumentation-complete/src/test/java/ca/petsearch/PseudoRandomNumberGeneratorTest.java +++ b/src/applications/microservices/petsearch-java/manual-instrumentation-complete/src/test/java/ca/petsearch/PseudoRandomNumberGeneratorTest.java @@ -1,3 +1,7 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ package ca.petsearch; import io.vavr.CheckedFunction1; diff --git a/PetAdoptions/petsearch-java/manual-instrumentation-complete/src/test/java/ca/petsearch/controllers/SearchControllerIT.java b/src/applications/microservices/petsearch-java/manual-instrumentation-complete/src/test/java/ca/petsearch/controllers/SearchControllerIT.java similarity index 98% rename from PetAdoptions/petsearch-java/manual-instrumentation-complete/src/test/java/ca/petsearch/controllers/SearchControllerIT.java rename to src/applications/microservices/petsearch-java/manual-instrumentation-complete/src/test/java/ca/petsearch/controllers/SearchControllerIT.java index ff079c13..531989ec 100644 --- a/PetAdoptions/petsearch-java/manual-instrumentation-complete/src/test/java/ca/petsearch/controllers/SearchControllerIT.java +++ b/src/applications/microservices/petsearch-java/manual-instrumentation-complete/src/test/java/ca/petsearch/controllers/SearchControllerIT.java @@ -1,3 +1,7 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ package ca.petsearch.controllers; import com.amazonaws.services.dynamodbv2.AmazonDynamoDB; diff --git a/PetAdoptions/petsearch-java/manual-instrumentation-complete/src/test/resources/application-test.yml b/src/applications/microservices/petsearch-java/manual-instrumentation-complete/src/test/resources/application-test.yml similarity index 69% rename from PetAdoptions/petsearch-java/manual-instrumentation-complete/src/test/resources/application-test.yml rename to src/applications/microservices/petsearch-java/manual-instrumentation-complete/src/test/resources/application-test.yml index 98d522a0..3fd46b1c 100644 --- a/PetAdoptions/petsearch-java/manual-instrumentation-complete/src/test/resources/application-test.yml +++ b/src/applications/microservices/petsearch-java/manual-instrumentation-complete/src/test/resources/application-test.yml @@ -1,3 +1,5 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 spring: profiles: test, default main: diff --git a/PetAdoptions/petsearch-java/settings.gradle b/src/applications/microservices/petsearch-java/settings.gradle similarity index 100% rename from PetAdoptions/petsearch-java/settings.gradle rename to src/applications/microservices/petsearch-java/settings.gradle diff --git a/PetAdoptions/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/Application.java b/src/applications/microservices/petsearch-java/src/main/java/ca/petsearch/Application.java similarity index 73% rename from PetAdoptions/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/Application.java rename to src/applications/microservices/petsearch-java/src/main/java/ca/petsearch/Application.java index ec1c1c52..44a17d3c 100644 --- a/PetAdoptions/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/Application.java +++ b/src/applications/microservices/petsearch-java/src/main/java/ca/petsearch/Application.java @@ -1,3 +1,7 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ package ca.petsearch; import org.springframework.boot.SpringApplication; diff --git a/PetAdoptions/petsearch-java/src/main/java/ca/petsearch/ApplicationFilter.java b/src/applications/microservices/petsearch-java/src/main/java/ca/petsearch/ApplicationFilter.java similarity index 92% rename from PetAdoptions/petsearch-java/src/main/java/ca/petsearch/ApplicationFilter.java rename to src/applications/microservices/petsearch-java/src/main/java/ca/petsearch/ApplicationFilter.java index 1d429d05..ecd7f5cc 100644 --- a/PetAdoptions/petsearch-java/src/main/java/ca/petsearch/ApplicationFilter.java +++ b/src/applications/microservices/petsearch-java/src/main/java/ca/petsearch/ApplicationFilter.java @@ -1,3 +1,7 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ package ca.petsearch; import org.springframework.web.util.ContentCachingResponseWrapper; diff --git a/PetAdoptions/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/MetricEmitter.java b/src/applications/microservices/petsearch-java/src/main/java/ca/petsearch/MetricEmitter.java similarity index 97% rename from PetAdoptions/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/MetricEmitter.java rename to src/applications/microservices/petsearch-java/src/main/java/ca/petsearch/MetricEmitter.java index 8f7e85c6..6ec80b16 100644 --- a/PetAdoptions/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/MetricEmitter.java +++ b/src/applications/microservices/petsearch-java/src/main/java/ca/petsearch/MetricEmitter.java @@ -1,3 +1,7 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ package ca.petsearch; import io.opentelemetry.api.OpenTelemetry; diff --git a/PetAdoptions/petsearch-java/src/main/java/ca/petsearch/PseudoRandomNumberGenerator.java b/src/applications/microservices/petsearch-java/src/main/java/ca/petsearch/PseudoRandomNumberGenerator.java similarity index 72% rename from PetAdoptions/petsearch-java/src/main/java/ca/petsearch/PseudoRandomNumberGenerator.java rename to src/applications/microservices/petsearch-java/src/main/java/ca/petsearch/PseudoRandomNumberGenerator.java index 52b4c9a0..7b1e9107 100644 --- a/PetAdoptions/petsearch-java/src/main/java/ca/petsearch/PseudoRandomNumberGenerator.java +++ b/src/applications/microservices/petsearch-java/src/main/java/ca/petsearch/PseudoRandomNumberGenerator.java @@ -1,3 +1,7 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ package ca.petsearch; public class PseudoRandomNumberGenerator implements RandomNumberGenerator { diff --git a/src/applications/microservices/petsearch-java/src/main/java/ca/petsearch/RandomNumberGenerator.java b/src/applications/microservices/petsearch-java/src/main/java/ca/petsearch/RandomNumberGenerator.java new file mode 100644 index 00000000..6df02af6 --- /dev/null +++ b/src/applications/microservices/petsearch-java/src/main/java/ca/petsearch/RandomNumberGenerator.java @@ -0,0 +1,9 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +package ca.petsearch; + +public interface RandomNumberGenerator { + int nextNonNegativeInt(int max); +} diff --git a/PetAdoptions/petsearch-java/src/main/java/ca/petsearch/WebConfig.java b/src/applications/microservices/petsearch-java/src/main/java/ca/petsearch/WebConfig.java similarity index 96% rename from PetAdoptions/petsearch-java/src/main/java/ca/petsearch/WebConfig.java rename to src/applications/microservices/petsearch-java/src/main/java/ca/petsearch/WebConfig.java index f9d6c8ba..804a20ed 100644 --- a/PetAdoptions/petsearch-java/src/main/java/ca/petsearch/WebConfig.java +++ b/src/applications/microservices/petsearch-java/src/main/java/ca/petsearch/WebConfig.java @@ -1,3 +1,7 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ package ca.petsearch; import com.amazonaws.client.builder.AwsClientBuilder; diff --git a/PetAdoptions/petsearch-java/src/main/java/ca/petsearch/controllers/ErrorResponse.java b/src/applications/microservices/petsearch-java/src/main/java/ca/petsearch/controllers/ErrorResponse.java similarity index 91% rename from PetAdoptions/petsearch-java/src/main/java/ca/petsearch/controllers/ErrorResponse.java rename to src/applications/microservices/petsearch-java/src/main/java/ca/petsearch/controllers/ErrorResponse.java index 6ebf794b..7f2be860 100644 --- a/PetAdoptions/petsearch-java/src/main/java/ca/petsearch/controllers/ErrorResponse.java +++ b/src/applications/microservices/petsearch-java/src/main/java/ca/petsearch/controllers/ErrorResponse.java @@ -1,3 +1,7 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ package ca.petsearch.controllers; import java.time.LocalDateTime; diff --git a/PetAdoptions/petsearch-java/src/main/java/ca/petsearch/controllers/HealthController.java b/src/applications/microservices/petsearch-java/src/main/java/ca/petsearch/controllers/HealthController.java similarity index 73% rename from PetAdoptions/petsearch-java/src/main/java/ca/petsearch/controllers/HealthController.java rename to src/applications/microservices/petsearch-java/src/main/java/ca/petsearch/controllers/HealthController.java index 16aeae4d..fbd4650e 100644 --- a/PetAdoptions/petsearch-java/src/main/java/ca/petsearch/controllers/HealthController.java +++ b/src/applications/microservices/petsearch-java/src/main/java/ca/petsearch/controllers/HealthController.java @@ -1,3 +1,7 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ package ca.petsearch.controllers; import org.springframework.web.bind.annotation.GetMapping; diff --git a/PetAdoptions/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/controllers/Pet.java b/src/applications/microservices/petsearch-java/src/main/java/ca/petsearch/controllers/Pet.java similarity index 91% rename from PetAdoptions/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/controllers/Pet.java rename to src/applications/microservices/petsearch-java/src/main/java/ca/petsearch/controllers/Pet.java index c9da3c27..45915eb7 100644 --- a/PetAdoptions/petsearch-java/manual-instrumentation-complete/src/main/java/ca/petsearch/controllers/Pet.java +++ b/src/applications/microservices/petsearch-java/src/main/java/ca/petsearch/controllers/Pet.java @@ -1,3 +1,7 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ package ca.petsearch.controllers; public class Pet { diff --git a/PetAdoptions/petsearch-java/src/main/java/ca/petsearch/controllers/SearchController.java b/src/applications/microservices/petsearch-java/src/main/java/ca/petsearch/controllers/SearchController.java similarity index 84% rename from PetAdoptions/petsearch-java/src/main/java/ca/petsearch/controllers/SearchController.java rename to src/applications/microservices/petsearch-java/src/main/java/ca/petsearch/controllers/SearchController.java index 5d94a805..0725a404 100644 --- a/PetAdoptions/petsearch-java/src/main/java/ca/petsearch/controllers/SearchController.java +++ b/src/applications/microservices/petsearch-java/src/main/java/ca/petsearch/controllers/SearchController.java @@ -1,3 +1,7 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ package ca.petsearch.controllers; import ca.petsearch.MetricEmitter; @@ -47,7 +51,8 @@ public class SearchController { private final Tracer tracer; private Map paramCache = new HashMap<>(); - public SearchController(AmazonS3 s3Client, AmazonDynamoDB ddbClient, AWSSimpleSystemsManagement ssmClient, MetricEmitter metricEmitter, Tracer tracer, RandomNumberGenerator randomGenerator) { + public SearchController(AmazonS3 s3Client, AmazonDynamoDB ddbClient, AWSSimpleSystemsManagement ssmClient, + MetricEmitter metricEmitter, Tracer tracer, RandomNumberGenerator randomGenerator) { this.s3Client = s3Client; this.ddbClient = ddbClient; this.ssmClient = ssmClient; @@ -79,26 +84,26 @@ private String getKey(String petType, String petId) { private String getPetUrl(String petType, String image) { Span span = tracer.spanBuilder("Get Pet URL").startSpan(); - try(Scope scope = span.makeCurrent()) { + try (Scope scope = span.makeCurrent()) { String s3BucketName = getSSMParameter(BUCKET_NAME); String key = getKey(petType, image); - - Double randomnumber = Math.random()*9999; + + Double randomnumber = Math.random() * 9999; if (randomnumber < 100) { - logger.debug("Forced exception to show S3 bucket creation error. The bucket never really gets created due to lack of permissions"); + logger.debug( + "Forced exception to show S3 bucket creation error. The bucket never really gets created due to lack of permissions"); logger.info("Trying to create a S3 Bucket"); logger.info(randomnumber + " is the random number"); s3Client.createBucket(s3BucketName); } logger.info("Generating presigned url"); - GeneratePresignedUrlRequest generatePresignedUrlRequest = - new GeneratePresignedUrlRequest(s3BucketName, key) - .withMethod(HttpMethod.GET) - .withExpiration(new Date(System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(5))); + GeneratePresignedUrlRequest generatePresignedUrlRequest = new GeneratePresignedUrlRequest(s3BucketName, key) + .withMethod(HttpMethod.GET) + .withExpiration(new Date(System.currentTimeMillis() + TimeUnit.MINUTES.toMillis(5))); return s3Client.generatePresignedUrl(generatePresignedUrlRequest).toString(); @@ -111,10 +116,12 @@ private String getPetUrl(String petType, String image) { } } - @WithSpan("Get parameter from Systems Manager or cache") // this annotation can be used as an alternative to tracer.spanBuilder + @WithSpan("Get parameter from Systems Manager or cache") // this annotation can be used as an alternative to + // tracer.spanBuilder private String getSSMParameter(String paramName) { if (!paramCache.containsKey(paramName)) { - GetParameterRequest parameterRequest = new GetParameterRequest().withName(paramName).withWithDecryption(false); + GetParameterRequest parameterRequest = new GetParameterRequest().withName(paramName) + .withWithDecryption(false); GetParameterResult parameterResult = ssmClient.getParameter(parameterRequest); paramCache.put(paramName, parameterResult.getParameter().getValue()); @@ -135,28 +142,27 @@ private Pet mapToPet(Map item) { return currentPet; } - @GetMapping("/api/search") public ResponseEntity search( @RequestParam(name = "pettype", defaultValue = "", required = false) String petType, @RequestParam(name = "petcolor", defaultValue = "", required = false) String petColor, - @RequestParam(name = "petid", defaultValue = "", required = false) String petId - ) throws InterruptedException { + @RequestParam(name = "petid", defaultValue = "", required = false) String petId) + throws InterruptedException { Span span = tracer.spanBuilder("Scanning DynamoDB Table").startSpan(); - // return 404 error with custom message for invalid pet type - if (petType != null && !petType.trim().isEmpty() && !petType.equals("puppy") && !petType.equals("kitten") && !petType.equals("bunny")) { - logger.warn(petType+" pet type requested - returning 404 error"); + // return 404 error with custom message for invalid pet type + if (petType != null && !petType.trim().isEmpty() && !petType.equals("puppy") && !petType.equals("kitten") + && !petType.equals("bunny")) { + logger.warn(petType + " pet type requested - returning 404 error"); span.setAttribute("error", true); - String errorMsg = petType+" pet type not found"; + String errorMsg = petType + " pet type not found"; span.setAttribute("error.message", errorMsg); span.end(); ErrorResponse errorResponse = new ErrorResponse( - 404, - errorMsg, - errorMsg, - "/api/search" - ); + 404, + errorMsg, + errorMsg, + "/api/search"); return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse); } @@ -165,7 +171,7 @@ public ResponseEntity search( logger.debug("Delaying the response on purpose, to show on traces as an issue"); TimeUnit.MILLISECONDS.sleep(3000); } - try(Scope scope = span.makeCurrent()) { + try (Scope scope = span.makeCurrent()) { List result = ddbClient.scan( buildScanRequest(petType, petColor, petId)) diff --git a/src/applications/microservices/petsearch-java/src/main/resources/application.yml b/src/applications/microservices/petsearch-java/src/main/resources/application.yml new file mode 100644 index 00000000..7f4c1484 --- /dev/null +++ b/src/applications/microservices/petsearch-java/src/main/resources/application.yml @@ -0,0 +1,12 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +spring: + application: + name: petstore +cloud: + aws: + region: + static: eu-west-1 + auto: false +server: + port: 80 \ No newline at end of file diff --git a/PetAdoptions/petsearch-java/src/main/resources/logback-spring.xml b/src/applications/microservices/petsearch-java/src/main/resources/logback-spring.xml similarity index 100% rename from PetAdoptions/petsearch-java/src/main/resources/logback-spring.xml rename to src/applications/microservices/petsearch-java/src/main/resources/logback-spring.xml diff --git a/PetAdoptions/petsite/.dockerignore b/src/applications/microservices/petsite-net/.dockerignore similarity index 90% rename from PetAdoptions/petsite/.dockerignore rename to src/applications/microservices/petsite-net/.dockerignore index bdca33b4..e7b690f1 100644 --- a/PetAdoptions/petsite/.dockerignore +++ b/src/applications/microservices/petsite-net/.dockerignore @@ -1,25 +1,25 @@ -**/.classpath -**/.dockerignore -**/.env -**/.git -**/.gitignore -**/.project -**/.settings -**/.toolstarget -**/.vs -**/.vscode -**/*.*proj.user -**/*.dbmdl -**/*.jfm -**/azds.yaml -**/bin -**/charts -**/docker-compose* -**/Dockerfile* -**/node_modules -**/npm-debug.log -**/obj -**/secrets.dev.yaml -**/values.dev.yaml -LICENSE -README.md \ No newline at end of file +**/.classpath +**/.dockerignore +**/.env +**/.git +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml +LICENSE +README.md diff --git a/PetAdoptions/petsite/.vscode/launch.json b/src/applications/microservices/petsite-net/.vscode/launch.json similarity index 65% rename from PetAdoptions/petsite/.vscode/launch.json rename to src/applications/microservices/petsite-net/.vscode/launch.json index 31192563..6378e060 100644 --- a/PetAdoptions/petsite/.vscode/launch.json +++ b/src/applications/microservices/petsite-net/.vscode/launch.json @@ -1,7 +1,4 @@ { - // Use IntelliSense to find out which attributes exist for C# debugging - // Use hover for the description of the existing attributes - // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md "version": "0.2.0", "configurations": [ { @@ -9,12 +6,10 @@ "type": "coreclr", "request": "launch", "preLaunchTask": "build", - // If you have changed target frameworks, make sure to update the program path. "program": "${workspaceFolder}/petsite/bin/Debug/net8.0/PetSite.dll", "args": [], "cwd": "${workspaceFolder}/petsite", "stopAtEntry": false, - // Enable launching a web browser when ASP.NET Core starts. For more information: https://aka.ms/VSCode-CS-LaunchJson-WebBrowser "serverReadyAction": { "action": "openExternally", "pattern": "^\\s*Now listening on:\\s+(https?://\\S+)" diff --git a/PetAdoptions/petsite/.vscode/tasks.json b/src/applications/microservices/petsite-net/.vscode/tasks.json similarity index 100% rename from PetAdoptions/petsite/.vscode/tasks.json rename to src/applications/microservices/petsite-net/.vscode/tasks.json diff --git a/PetAdoptions/petsite/iam-policy.json b/src/applications/microservices/petsite-net/iam-policy.json similarity index 100% rename from PetAdoptions/petsite/iam-policy.json rename to src/applications/microservices/petsite-net/iam-policy.json diff --git a/PetAdoptions/petsite/petsite.sln b/src/applications/microservices/petsite-net/petsite.sln similarity index 97% rename from PetAdoptions/petsite/petsite.sln rename to src/applications/microservices/petsite-net/petsite.sln index 53242a54..adbef331 100644 --- a/PetAdoptions/petsite/petsite.sln +++ b/src/applications/microservices/petsite-net/petsite.sln @@ -1,25 +1,25 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29503.13 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PetSite", "petsite\PetSite.csproj", "{3CF15FF6-D1C6-42EC-84E7-082FA8E5339F}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {3CF15FF6-D1C6-42EC-84E7-082FA8E5339F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {3CF15FF6-D1C6-42EC-84E7-082FA8E5339F}.Debug|Any CPU.Build.0 = Debug|Any CPU - {3CF15FF6-D1C6-42EC-84E7-082FA8E5339F}.Release|Any CPU.ActiveCfg = Release|Any CPU - {3CF15FF6-D1C6-42EC-84E7-082FA8E5339F}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {45D80372-F05E-421A-BEDF-01DE6C4F969E} - EndGlobalSection -EndGlobal + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29503.13 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PetSite", "petsite\PetSite.csproj", "{3CF15FF6-D1C6-42EC-84E7-082FA8E5339F}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {3CF15FF6-D1C6-42EC-84E7-082FA8E5339F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {3CF15FF6-D1C6-42EC-84E7-082FA8E5339F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {3CF15FF6-D1C6-42EC-84E7-082FA8E5339F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {3CF15FF6-D1C6-42EC-84E7-082FA8E5339F}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {45D80372-F05E-421A-BEDF-01DE6C4F969E} + EndGlobalSection +EndGlobal diff --git a/src/applications/microservices/petsite-net/petsite.sln.DotSettings.user b/src/applications/microservices/petsite-net/petsite.sln.DotSettings.user new file mode 100644 index 00000000..702b8e37 --- /dev/null +++ b/src/applications/microservices/petsite-net/petsite.sln.DotSettings.user @@ -0,0 +1,3 @@ + + ForceIncluded + ForceIncluded \ No newline at end of file diff --git a/PetAdoptions/petsite/petsite/Controllers/AdoptionController.cs b/src/applications/microservices/petsite-net/petsite/Controllers/AdoptionController.cs similarity index 92% rename from PetAdoptions/petsite/petsite/Controllers/AdoptionController.cs rename to src/applications/microservices/petsite-net/petsite/Controllers/AdoptionController.cs index de9f22bd..a33ef870 100644 --- a/PetAdoptions/petsite/petsite/Controllers/AdoptionController.cs +++ b/src/applications/microservices/petsite-net/petsite/Controllers/AdoptionController.cs @@ -75,9 +75,9 @@ public async Task TakeMeHome([FromForm] SearchParams searchParams } catch (Exception e) { - // Log the exception _logger.LogError(e, "Error calling PetSearch API"); - throw e; + ViewBag.ErrorMessage = $"Unable to process adoption request at this time. Please try again later.\nError message: {e.Message}"; + return View("Error", new PetSite.Models.ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); } var selectedPet = pets.FirstOrDefault(); diff --git a/PetAdoptions/petsite/petsite/Controllers/BaseController.cs b/src/applications/microservices/petsite-net/petsite/Controllers/BaseController.cs similarity index 79% rename from PetAdoptions/petsite/petsite/Controllers/BaseController.cs rename to src/applications/microservices/petsite-net/petsite/Controllers/BaseController.cs index 5db51d7b..dfa11075 100644 --- a/PetAdoptions/petsite/petsite/Controllers/BaseController.cs +++ b/src/applications/microservices/petsite-net/petsite/Controllers/BaseController.cs @@ -9,16 +9,14 @@ namespace PetSite.Controllers { public class BaseController : Controller { - private static readonly List UserIds = new List - { - "user001", "user002", "user003", "user004", "user005", - "user006", "user007", "user008", "user009", "user010", - "user011", "user012", "user013", "user014", "user015", - "user016", "user017", "user018", "user019", "user020", - "user021", "user022", "user023", "user024", "user025" - }; private static readonly Random Random = new Random(); + private static string GenerateUserId() + { + int randomNumber = Random.Next(1, 10000); + return $"user{randomNumber:D4}"; + } + protected bool EnsureUserId() { string userId = Request.Query["userId"].ToString(); @@ -30,7 +28,7 @@ protected bool EnsureUserId() if (ControllerContext.ActionDescriptor.ControllerName == "Home" && ControllerContext.ActionDescriptor.ActionName == "Index") { - userId = UserIds[Random.Next(UserIds.Count)]; + userId = GenerateUserId(); if (Request.Method == "GET") { diff --git a/src/applications/microservices/petsite-net/petsite/Controllers/CheckoutController.cs b/src/applications/microservices/petsite-net/petsite/Controllers/CheckoutController.cs new file mode 100644 index 00000000..3f08d66f --- /dev/null +++ b/src/applications/microservices/petsite-net/petsite/Controllers/CheckoutController.cs @@ -0,0 +1,123 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Configuration; +using System.Net.Http; +using System.Threading.Tasks; +using System.Text; +using System.Text.Json; +using PetSite.Helpers; +using PetSite.ViewModels; +using System; + +namespace PetSite.Controllers +{ + public class CheckoutController : BaseController + { + private readonly ILogger _logger; + private readonly IHttpClientFactory _httpClientFactory; + private readonly IConfiguration _configuration; + + public CheckoutController(ILogger logger, IHttpClientFactory httpClientFactory, IConfiguration configuration) + { + _logger = logger; + _httpClientFactory = httpClientFactory; + _configuration = configuration; + } + + [HttpGet] + public async Task Index([FromQuery] string userId) + { + if (EnsureUserId()) return new EmptyResult(); + + try + { + using var httpClient = _httpClientFactory.CreateClient(); + var cartUrl = UrlHelper.BuildUrl(_configuration["foodapiurl"], new[]{"api","cart", userId}, null); + var response = await httpClient.GetAsync(cartUrl); + response.EnsureSuccessStatusCode(); + + var jsonContent = await response.Content.ReadAsStringAsync(); + var cartData = JsonSerializer.Deserialize(jsonContent, new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); + + return View(cartData); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error fetching cart data"); + ViewBag.ErrorMessage = $"Unable to load cart data at this time. Please try again later.\nError message: {ex.Message}"; + return View("Error", new PetSite.Models.ErrorViewModel { RequestId = System.Diagnostics.Activity.Current?.Id ?? HttpContext.TraceIdentifier }); + } + } + + [HttpPost] + public async Task PayAndCheckOut([FromBody] JsonElement requestData) + { + string userId = string.Empty; + try + { + userId = requestData.GetProperty("userId").GetString(); + + using var httpClient = _httpClientFactory.CreateClient(); + var paymentUrl = UrlHelper.BuildUrl(_configuration["foodapiurl"], new[] { "api", "cart", userId, "checkout" }, null); + var jsonContent = new StringContent(requestData.GetRawText(), Encoding.UTF8, "application/json"); + + var response = await httpClient.PostAsync(paymentUrl, jsonContent); + + if (response.StatusCode == System.Net.HttpStatusCode.OK) + { + var responseContent = await response.Content.ReadAsStringAsync(); + var orderData = JsonSerializer.Deserialize(responseContent); + + // Clear cart after successful payment + try + { + var clearCartUrl = UrlHelper.BuildUrl(_configuration["foodapiurl"], new[] { "api", "cart", userId }, null); + await httpClient.DeleteAsync(clearCartUrl); + } + catch (Exception ex) + { + _logger.LogWarning(ex, "Failed to clear cart after successful payment"); + } + + return Ok(new { + success = true, + orderId = orderData.GetProperty("order_id").GetString(), + status = orderData.GetProperty("status").GetString(), + createdDate = DateTime.Parse(orderData.GetProperty("created_at").GetString()).ToString("MM/dd/yyyy"), + deliveryDate = DateTime.Parse(orderData.GetProperty("estimated_delivery").GetString()).ToString("MM/dd/yyyy") + }); + } + + return BadRequest($"Payment failed for user: {userId}. Failure reason:{response.ReasonPhrase}"); + } + catch (Exception ex) + { + _logger.LogError(ex, $"Payment failed for user: {userId}"); + return BadRequest($"Payment processing failed. Please try again.\nError: {ex.Message}"); + } + } + + [HttpPost] + public async Task ClearCart(string userId) + { + try + { + using var httpClient = _httpClientFactory.CreateClient(); + var clearCartUrl = UrlHelper.BuildUrl(_configuration["foodapiurl"], new[] { "api", "cart", userId }, null); + var response = await httpClient.DeleteAsync(clearCartUrl); + response.EnsureSuccessStatusCode(); + + return RedirectToAction("Index", new { userId }); + } + catch (Exception ex) + { + _logger.LogError(ex, $"Failed to clear cart for user: {userId}"); + ViewBag.ErrorMessage = $"Unable to clear cart. Please try again later.\nError: {ex.Message}"; + return RedirectToAction("Index", new { userId }); + } + } + } +} \ No newline at end of file diff --git a/src/applications/microservices/petsite-net/petsite/Controllers/FoodServiceController.cs b/src/applications/microservices/petsite-net/petsite/Controllers/FoodServiceController.cs new file mode 100644 index 00000000..1b81f1bf --- /dev/null +++ b/src/applications/microservices/petsite-net/petsite/Controllers/FoodServiceController.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using System.Net.Http; +using System.Text; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using Microsoft.AspNetCore.Http; +using PetSite.Models; +using PetSite.Helpers; + +namespace PetSite.Controllers +{ + public class FoodServiceController : BaseController + { + private readonly IHttpClientFactory _httpClientFactory; + private readonly IConfiguration _configuration; + private readonly ILogger _logger; + + public FoodServiceController(IHttpClientFactory httpClientFactory, IConfiguration configuration, + ILogger logger) + { + _httpClientFactory = httpClientFactory; + _configuration = configuration; + _logger = logger; + } + + [HttpGet] + public async Task Index([FromQuery] string userId, string petType) + { + if (EnsureUserId()) return new EmptyResult(); + + ViewBag.PetType = petType; + + try + { + using var httpClient = _httpClientFactory.CreateClient(); + var foodApiUrl = _configuration["foodapiurl"]; + var url = UrlHelper.BuildUrl(foodApiUrl, new[]{"api","foods"}, ("pettype", petType)); + var response = await httpClient.GetAsync(url); + response.EnsureSuccessStatusCode(); + + var jsonContent = await response.Content.ReadAsStringAsync(); + var foodResponse = JsonSerializer.Deserialize(jsonContent, + new JsonSerializerOptions + { + PropertyNameCaseInsensitive = true + }); + + return View(foodResponse); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error fetching food data from FoodService API"); + ViewBag.ErrorMessage = $"Unable to load food items at this time. Please try again later.\nError message: {ex.Message}"; + return View("Error", new PetSite.Models.ErrorViewModel { RequestId = System.Diagnostics.Activity.Current?.Id ?? HttpContext.TraceIdentifier }); + } + + } + + [HttpPost] + public async Task AddToCart(string foodId, string userId) + { + if (userId == null) + { + if (EnsureUserId()) return new EmptyResult(); + } + + try + { + using var httpClient = _httpClientFactory.CreateClient(); + + // First API call - Add to cart + var addToCartUrl = UrlHelper.BuildUrl(_configuration["foodapiurl"], new[] { "api", "cart", userId, "items" }, null); + var cartData = new { food_id = foodId, quantity = 1 }; + var cartJson = JsonSerializer.Serialize(cartData); + var cartContent = new StringContent(cartJson, Encoding.UTF8, "application/json"); + + var cartResponse = await httpClient.PostAsync(addToCartUrl, cartContent); + + if (cartResponse.StatusCode == System.Net.HttpStatusCode.Created) + { + var totalItems = await GetCartItemCountAsync(userId); + return Ok(new { success = true, totalItems }); + } + + return Ok(new { success = true, totalItems = 0 }); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error adding item to cart"); + return BadRequest(ex.Message); + } + } + + [HttpPost] + public async Task BuyFood(string foodId, string userId) + { + if (EnsureUserId()) return new EmptyResult(); + + try + { + using var httpClient = _httpClientFactory.CreateClient(); + var purchaseApiUrl = _configuration["FOOD_PURCHASE_API_URL"] ?? "https://api.example.com/purchase"; + // var userId = ViewBag.UserId?.ToString(); + var url = UrlHelper.BuildUrl(purchaseApiUrl, null, ("foodId", foodId), ("userId", userId)); + var response = await httpClient.PostAsync(url, null); + response.EnsureSuccessStatusCode(); + + // Food purchase successful - could add ViewData or redirect with status + } + catch (Exception ex) + { + _logger.LogError(ex, "Error purchasing food"); + // Food purchase failed - could add ViewData or redirect with error + } + + return RedirectToAction("Index", "Payment", new { userId = userId }); + } + + [HttpGet] + public async Task GetCartCount(string userId) + { + var totalItems = await GetCartItemCountAsync(userId); + return Ok(new { totalItems }); + } + + private async Task GetCartItemCountAsync(string userId) + { + try + { + using var httpClient = _httpClientFactory.CreateClient(); + var getCartUrl = UrlHelper.BuildUrl(_configuration["foodapiurl"], new[]{"api","cart",userId}, null); + var getCartResponse = await httpClient.GetAsync(getCartUrl); + + if (getCartResponse.StatusCode == System.Net.HttpStatusCode.OK) + { + var responseContent = await getCartResponse.Content.ReadAsStringAsync(); + var cartInfo = JsonSerializer.Deserialize(responseContent); + + if (cartInfo.TryGetProperty("total_items", out var totalItems)) + { + return totalItems.GetInt32(); + } + } + + return 0; + } + catch (Exception ex) + { + _logger.LogError(ex, "Error fetching cart count"); + return 0; + } + } + } +} \ No newline at end of file diff --git a/PetAdoptions/petsite/petsite/Controllers/HealthController.cs b/src/applications/microservices/petsite-net/petsite/Controllers/HealthController.cs similarity index 100% rename from PetAdoptions/petsite/petsite/Controllers/HealthController.cs rename to src/applications/microservices/petsite-net/petsite/Controllers/HealthController.cs diff --git a/PetAdoptions/petsite/petsite/Controllers/HomeController.cs b/src/applications/microservices/petsite-net/petsite/Controllers/HomeController.cs similarity index 90% rename from PetAdoptions/petsite/petsite/Controllers/HomeController.cs rename to src/applications/microservices/petsite-net/petsite/Controllers/HomeController.cs index d7146d3f..7be988a8 100644 --- a/PetAdoptions/petsite/petsite/Controllers/HomeController.cs +++ b/src/applications/microservices/petsite-net/petsite/Controllers/HomeController.cs @@ -80,7 +80,7 @@ public async Task HouseKeeping() using var httpClient = _httpClientFactory.CreateClient(); var userId = ViewBag.UserId?.ToString(); - var url = UrlHelper.BuildUrl(cleanupadoptionsurl, ("userId", userId)); + var url = UrlHelper.BuildUrl(cleanupadoptionsurl, null, ("userId", userId)); await httpClient.PostAsync(url, null); return View(); @@ -123,22 +123,19 @@ public async Task Index(string selectedPetType, string selectedPe { _logger.LogError(e, "HTTP error received after calling PetSearch API"); ViewBag.ErrorMessage = $"Unable to search pets at this time. Please try again later. \nError message received - {e.Message}"; - Pets = new List(); - throw e; + return View("Error", new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); } catch (TaskCanceledException e) { _logger.LogError(e, "Timeout calling PetSearch API"); - ViewBag.ErrorMessage = "Search request timed out. Please try again."; - Pets = new List(); - throw e; + ViewBag.ErrorMessage = $"Search request timed out. Please try again.\n Error message received: {e.Message}"; + return View("Error", new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); } catch (Exception e) { _logger.LogError(e, "Unexpected error calling PetSearch API"); - ViewBag.ErrorMessage = "An unexpected error occurred. Please try again."; - Pets = new List(); - throw e; + ViewBag.ErrorMessage = $"An unexpected error occurred. Please try again.\n Error message received: {e.Message}"; + return View("Error", new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); } var PetDetails = new PetDetails() diff --git a/PetAdoptions/petsite/petsite/Controllers/PaymentController.cs b/src/applications/microservices/petsite-net/petsite/Controllers/PaymentController.cs similarity index 65% rename from PetAdoptions/petsite/petsite/Controllers/PaymentController.cs rename to src/applications/microservices/petsite-net/petsite/Controllers/PaymentController.cs index ae16b100..e3d37af1 100644 --- a/PetAdoptions/petsite/petsite/Controllers/PaymentController.cs +++ b/src/applications/microservices/petsite-net/petsite/Controllers/PaymentController.cs @@ -8,6 +8,10 @@ using Microsoft.Extensions.Configuration; using Microsoft.AspNetCore.Http; using PetSite.Helpers; +using PetSite.ViewModels; +using PetSite.Services; +using System.Text.Json; +using System.Linq; using Prometheus; namespace PetSite.Controllers @@ -20,55 +24,66 @@ public class PaymentController : BaseController private readonly IHttpClientFactory _httpClientFactory; private readonly IConfiguration _configuration; + private readonly IPetSearchService _petSearchService; //Prometheus metric to count the number of Pets adopted private static readonly Counter PetAdoptionCount = Metrics.CreateCounter("petsite_petadoptions_total", "Count the number of Pets adopted"); public PaymentController(ILogger logger, IConfiguration configuration, - IHttpClientFactory httpClientFactory) + IHttpClientFactory httpClientFactory, IPetSearchService petSearchService) { _configuration = configuration; _httpClientFactory = httpClientFactory; + _petSearchService = petSearchService; _logger = logger; } // GET: Payment [HttpGet] - public ActionResult Index([FromQuery] string userId, string status) + public async Task Index([FromQuery] string userId, string status, string petId, string petType) { if (EnsureUserId()) return new EmptyResult(); - + // Transfer Session to ViewData for the view ViewData["txStatus"] = status; - - // ViewData["FoodPurchaseStatus"] = HttpContext.Session.GetString("FoodPurchaseStatus"); - // ViewData["PurchasedFoodId"] = HttpContext.Session.GetString("PurchasedFoodId"); - // - // Clear session data after reading - // HttpContext.Session.Remove("FoodPurchaseStatus"); - // HttpContext.Session.Remove("PurchasedFoodId"); - // + + Pet petDetails = null; + if (!string.IsNullOrEmpty(petId) && !string.IsNullOrEmpty(petType)) + { + try + { + var pets = await _petSearchService.GetPetDetails(petType, "all", petId, userId); + petDetails = pets?.FirstOrDefault(); + } + catch (Exception ex) + { + _logger.LogError(ex, "Error fetching pet details after payment."); + ViewBag.ErrorMessage = $"Unable to load payment details at this time. Please try again later.\nError message: {ex.Message}"; + return View("Error", new PetSite.Models.ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); + } + } + ViewData["PetDetails"] = petDetails; return View(); } // POST: Payment/MakePayment [HttpPost] // [ValidateAntiForgeryToken] - public async Task MakePayment(string petId, string pettype, string userId) + public async Task MakePayment(string petId, string petType, string userId) { //if (EnsureUserId()) return new EmptyResult(); if (string.IsNullOrEmpty(userId)) EnsureUserId(); - + // Add custom span attributes using Activity API var currentActivity = Activity.Current; if (currentActivity != null) { currentActivity.SetTag("pet.id", petId); - currentActivity.SetTag("pet.type", pettype); + currentActivity.SetTag("pet.type", petType); - _logger.LogInformation($"Inside MakePayment Action method - PetId:{petId} - PetType:{pettype}"); + _logger.LogInformation($"Inside MakePayment Action method - PetId:{petId} - PetType:{petType}"); } try @@ -79,21 +94,21 @@ public async Task MakePayment(string petId, string pettype, strin if (activity != null) { activity.SetTag("pet.id", petId); - activity.SetTag("pet.type", pettype); + activity.SetTag("pet.type", petType); } // userId parameter is already available using var httpClient = _httpClientFactory.CreateClient(); - var url = UrlHelper.BuildUrl(_configuration["paymentapiurl"], - ("petId", petId), ("petType", pettype), ("userId", userId)); + var url = UrlHelper.BuildUrl(_configuration["paymentapiurl"], null, + ("petId", petId), ("petType", petType), ("userId", userId)); await httpClient.PostAsync(url, null); } //Increase purchase metric count PetAdoptionCount.Inc(); - return RedirectToAction("Index", new { userId = userId, status = "success" }); + return RedirectToAction("Index", new { userId = userId, status = "success", petType = petType, petId = petId }); } catch (Exception ex) { diff --git a/PetAdoptions/petsite/petsite/Controllers/PetFoodController.cs b/src/applications/microservices/petsite-net/petsite/Controllers/PetFoodController.cs similarity index 95% rename from PetAdoptions/petsite/petsite/Controllers/PetFoodController.cs rename to src/applications/microservices/petsite-net/petsite/Controllers/PetFoodController.cs index fa00a99c..63bd2895 100644 --- a/PetAdoptions/petsite/petsite/Controllers/PetFoodController.cs +++ b/src/applications/microservices/petsite-net/petsite/Controllers/PetFoodController.cs @@ -11,7 +11,7 @@ public class PetFoodController : Controller { private static HttpClient httpClient; private IConfiguration _configuration; - + public PetFoodController(IConfiguration configuration) { _configuration = configuration; @@ -28,9 +28,9 @@ public async Task Index() currentActivity.SetTag("operation", "GetPetFood"); Console.WriteLine("Calling PetFood"); } - + string result; - + try { // Begin activity to monitor PetFood @@ -45,11 +45,11 @@ public async Task Index() Console.WriteLine($"Error calling PetFood: {e.Message}"); throw; } - + // Return the result! return result; } - + [HttpGet("/petfood-metric/{entityId}/{value}")] public async Task PetFoodMetric(string entityId, float value) { @@ -60,12 +60,12 @@ public async Task PetFoodMetric(string entityId, float value) currentActivity.SetTag("operation", "PetFoodMetric"); currentActivity.SetTag("entityId", entityId); currentActivity.SetTag("value", value.ToString()); - + Console.WriteLine("Calling: " + "http://petfood-metric/metric/" + entityId + "/" + value.ToString()); } - + string result; - + try { // Begin activity to monitor PetFood metrics retrieval @@ -76,7 +76,7 @@ public async Task PetFoodMetric(string entityId, float value) activity.SetTag("entityId", entityId); activity.SetTag("value", value.ToString()); } - + result = await httpClient.GetStringAsync("http://petfood-metric/metric/" + entityId + "/" + value.ToString()); } } @@ -85,8 +85,8 @@ public async Task PetFoodMetric(string entityId, float value) Console.WriteLine($"Error calling PetFood metric: {e.Message}"); throw; } - + return result; } } -} +} \ No newline at end of file diff --git a/PetAdoptions/petsite/petsite/Controllers/PetHistoryController.cs b/src/applications/microservices/petsite-net/petsite/Controllers/PetHistoryController.cs similarity index 95% rename from PetAdoptions/petsite/petsite/Controllers/PetHistoryController.cs rename to src/applications/microservices/petsite-net/petsite/Controllers/PetHistoryController.cs index 392abda6..10cad16a 100644 --- a/PetAdoptions/petsite/petsite/Controllers/PetHistoryController.cs +++ b/src/applications/microservices/petsite-net/petsite/Controllers/PetHistoryController.cs @@ -15,16 +15,16 @@ public class PetHistoryController : BaseController private readonly IConfiguration _configuration; private readonly IHttpClientFactory _httpClientFactory; private static string _pethistoryurl; - + public PetHistoryController(IConfiguration configuration, IHttpClientFactory httpClientFactory) { _configuration = configuration; _httpClientFactory = httpClientFactory; - + _pethistoryurl = _configuration["pethistoryurl"]; //string _pethistoryurl = SystemsManagerConfigurationProviderWithReloadExtensions.GetConfiguration(_configuration,"pethistoryurl"); } - + /// /// GET:/pethistory /// @@ -39,7 +39,7 @@ public async Task Index() { currentActivity.SetTag("operation", "GetPetAdoptionsHistory"); } - + try { // Begin activity span to track GetPetAdoptionsHistory API call @@ -47,7 +47,7 @@ public async Task Index() { using var httpClient = _httpClientFactory.CreateClient(); var userId = ViewBag.UserId?.ToString() ?? "unknown"; - var url = UrlHelper.BuildUrl($"{_pethistoryurl}/api/home/transactions", ("userId", userId)); + var url = UrlHelper.BuildUrl($"{_pethistoryurl}/api/home/transactions", null, ("userId", userId)); ViewData["pethistory"] = await httpClient.GetStringAsync(url); } } @@ -56,7 +56,7 @@ public async Task Index() Console.WriteLine($"Error calling GetPetAdoptionsHistory: {e.Message}"); throw; } - + return View(); } @@ -74,7 +74,7 @@ public async Task DeletePetAdoptionsHistory() { currentActivity.SetTag("operation", "DeletePetAdoptionsHistory"); } - + try { // Begin activity span to track DeletePetAdoptionsHistory API call @@ -82,7 +82,7 @@ public async Task DeletePetAdoptionsHistory() { using var httpClient = _httpClientFactory.CreateClient(); var userId = ViewBag.UserId?.ToString() ?? "unknown"; - var url = UrlHelper.BuildUrl($"{_pethistoryurl}/api/home/transactions", ("userId", userId)); + var url = UrlHelper.BuildUrl($"{_pethistoryurl}/api/home/transactions",null, ("userId", userId)); ViewData["pethistory"] = await httpClient.DeleteAsync(url); } } @@ -91,7 +91,8 @@ public async Task DeletePetAdoptionsHistory() Console.WriteLine($"Error calling DeletePetAdoptionsHistory: {e.Message}"); throw; } - + return View("Index"); } } + diff --git a/PetAdoptions/petsite/petsite/Controllers/PetListAdoptionsController.cs b/src/applications/microservices/petsite-net/petsite/Controllers/PetListAdoptionsController.cs similarity index 74% rename from PetAdoptions/petsite/petsite/Controllers/PetListAdoptionsController.cs rename to src/applications/microservices/petsite-net/petsite/Controllers/PetListAdoptionsController.cs index c166d885..9c6ed88d 100644 --- a/PetAdoptions/petsite/petsite/Controllers/PetListAdoptionsController.cs +++ b/src/applications/microservices/petsite-net/petsite/Controllers/PetListAdoptionsController.cs @@ -25,7 +25,7 @@ public PetListAdoptionsController(ILogger logger, IC { _configuration = configuration; _httpClientFactory = httpClientFactory; - _logger= logger; + _logger = logger; } // GET @@ -50,18 +50,25 @@ public async Task Index() string petlistadoptionsurl = _configuration["petlistadoptionsurl"]; using var httpClient = _httpClientFactory.CreateClient(); var userId = ViewBag.UserId?.ToString(); - var url = UrlHelper.BuildUrl(petlistadoptionsurl, ("userId", userId)); - result = await httpClient.GetStringAsync(url); + //var url = UrlHelper.BuildUrl(petlistadoptionsurl, null, ("userId",userId)); + result = await httpClient.GetStringAsync(petlistadoptionsurl); Pets = JsonSerializer.Deserialize>(result); } } + catch (HttpRequestException e) when (e.Message.Contains("404")) + { + _logger.LogWarning("PetListAdoptions API returned 404 - returning empty pets list"); + Pets = new List(); + } catch (Exception e) { _logger.LogError(e, $"Error calling PetListAdoptions API: {e.Message}"); - throw; + ViewBag.ErrorMessage = $"Unable to load adoption list at this time. Please try again later.\nError message: {e.Message}"; + return View("Error", new PetSite.Models.ErrorViewModel { RequestId = System.Diagnostics.Activity.Current?.Id ?? HttpContext.TraceIdentifier }); } return View(Pets); } } } + diff --git a/PetAdoptions/petsite/petsite/Dockerfile b/src/applications/microservices/petsite-net/petsite/Dockerfile similarity index 100% rename from PetAdoptions/petsite/petsite/Dockerfile rename to src/applications/microservices/petsite-net/petsite/Dockerfile diff --git a/src/applications/microservices/petsite-net/petsite/Helpers/UrlHelper.cs b/src/applications/microservices/petsite-net/petsite/Helpers/UrlHelper.cs new file mode 100644 index 00000000..d18b08e6 --- /dev/null +++ b/src/applications/microservices/petsite-net/petsite/Helpers/UrlHelper.cs @@ -0,0 +1,44 @@ +#nullable enable +using System; + +namespace PetSite.Helpers +{ + public static class UrlHelper + { + public static string BuildUrl(string baseUrl, string[]? path, params (string key, string value)[] parameters) + { + if (string.IsNullOrEmpty(baseUrl)) + return string.Empty; + + var url = baseUrl.TrimEnd('/'); + + // Add path segments + if (path != null && path.Length > 0) + { + foreach (var segment in path) + { + if (!string.IsNullOrEmpty(segment)) + { + url += "/" + segment.Trim('/'); + } + } + } + + var hasQuery = url.Contains("?"); + + + if (parameters != null) + foreach (var (key, value) in parameters) + { + if (!string.IsNullOrEmpty(value)) + { + var separator = hasQuery ? "&" : "?"; + url += $"{separator}{key}={Uri.EscapeDataString(value)}"; + hasQuery = true; + } + } + + return url; + } + } +} \ No newline at end of file diff --git a/PetAdoptions/petsite/petsite/Middleware/ErrorHandlingMiddleware.cs b/src/applications/microservices/petsite-net/petsite/Middleware/ErrorHandlingMiddleware.cs similarity index 96% rename from PetAdoptions/petsite/petsite/Middleware/ErrorHandlingMiddleware.cs rename to src/applications/microservices/petsite-net/petsite/Middleware/ErrorHandlingMiddleware.cs index 2893cbc7..9f99befd 100644 --- a/PetAdoptions/petsite/petsite/Middleware/ErrorHandlingMiddleware.cs +++ b/src/applications/microservices/petsite-net/petsite/Middleware/ErrorHandlingMiddleware.cs @@ -25,17 +25,17 @@ public async Task InvokeAsync(HttpContext context) catch (Exception ex) { _logger.LogError(ex, "An unhandled exception occurred"); - + // Preserve userId and exception message var userId = context.Request.Query["userId"].ToString(); var errorMessage = Uri.EscapeDataString(ex.Message); - + var errorPath = $"/Home/Error?message={errorMessage}"; if (!string.IsNullOrEmpty(userId)) { errorPath += $"&userId={userId}"; } - + context.Response.Redirect(errorPath); } } diff --git a/PetAdoptions/petsite/petsite/Models/ErrorViewModel.cs b/src/applications/microservices/petsite-net/petsite/Models/ErrorViewModel.cs similarity index 94% rename from PetAdoptions/petsite/petsite/Models/ErrorViewModel.cs rename to src/applications/microservices/petsite-net/petsite/Models/ErrorViewModel.cs index c3e110b0..b14e8a02 100644 --- a/PetAdoptions/petsite/petsite/Models/ErrorViewModel.cs +++ b/src/applications/microservices/petsite-net/petsite/Models/ErrorViewModel.cs @@ -1,11 +1,11 @@ -using System; - -namespace PetSite.Models -{ - public class ErrorViewModel - { - public string RequestId { get; set; } - - public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); - } -} +using System; + +namespace PetSite.Models +{ + public class ErrorViewModel + { + public string RequestId { get; set; } + + public bool ShowRequestId => !string.IsNullOrEmpty(RequestId); + } +} diff --git a/PetAdoptions/petsite/petsite/Models/Food.cs b/src/applications/microservices/petsite-net/petsite/Models/Food.cs similarity index 100% rename from PetAdoptions/petsite/petsite/Models/Food.cs rename to src/applications/microservices/petsite-net/petsite/Models/Food.cs diff --git a/PetAdoptions/petsite/petsite/PetSite.csproj b/src/applications/microservices/petsite-net/petsite/PetSite.csproj similarity index 79% rename from PetAdoptions/petsite/petsite/PetSite.csproj rename to src/applications/microservices/petsite-net/petsite/PetSite.csproj index f1f4eb86..59e82548 100644 --- a/PetAdoptions/petsite/petsite/PetSite.csproj +++ b/src/applications/microservices/petsite-net/petsite/PetSite.csproj @@ -15,25 +15,25 @@ - - - - - - - - + + + + + + + + + - + all runtime; build; native; contentfiles; analyzers; buildtransitive - - - - - - + + + + + diff --git a/PetAdoptions/petsite/petsite/Program.cs b/src/applications/microservices/petsite-net/petsite/Program.cs similarity index 97% rename from PetAdoptions/petsite/petsite/Program.cs rename to src/applications/microservices/petsite-net/petsite/Program.cs index b778ea22..d1991294 100644 --- a/PetAdoptions/petsite/petsite/Program.cs +++ b/src/applications/microservices/petsite-net/petsite/Program.cs @@ -30,11 +30,11 @@ public static IHostBuilder CreateHostBuilder(string[] args) => { var env = hostingContext.HostingEnvironment; Console.WriteLine($"ENVIRONMENT NAME IS: {env.EnvironmentName}"); - + // Add base configuration first config.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true); - + if (env.EnvironmentName.ToLower() != "development") { Console.WriteLine("[DEBUG] Loading Systems Manager configuration..."); @@ -42,7 +42,7 @@ public static IHostBuilder CreateHostBuilder(string[] args) => var tempConfig = config.Build(); var awsOptions = tempConfig.GetAWSOptions(); Console.WriteLine($"[DEBUG] AWS Region: {awsOptions.Region}"); - + config.AddSystemsManager(configureSource => { configureSource.Path = "/petstore"; @@ -66,4 +66,4 @@ public static IHostBuilder CreateHostBuilder(string[] args) => }) .ConfigureWebHostDefaults(webBuilder => { webBuilder.UseStartup(); }); } -} +} \ No newline at end of file diff --git a/PetAdoptions/petsite/petsite/Properties/launchSettings.json b/src/applications/microservices/petsite-net/petsite/Properties/launchSettings.json similarity index 96% rename from PetAdoptions/petsite/petsite/Properties/launchSettings.json rename to src/applications/microservices/petsite-net/petsite/Properties/launchSettings.json index 09b3ff56..8b32f536 100644 --- a/PetAdoptions/petsite/petsite/Properties/launchSettings.json +++ b/src/applications/microservices/petsite-net/petsite/Properties/launchSettings.json @@ -1,39 +1,39 @@ -{ - "iisSettings": { - "windowsAuthentication": false, - "anonymousAuthentication": true, - "iisExpress": { - "applicationUrl": "http://localhost:55730", - "sslPort": 44347 - } - }, - "profiles": { - "IIS Express": { - "commandName": "IISExpress", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - } - }, - "PetSite": { - "commandName": "Project", - "launchBrowser": true, - "environmentVariables": { - "ASPNETCORE_ENVIRONMENT": "Development" - }, - "applicationUrl": "http://localhost:5000" - }, - "Docker": { - "commandName": "Docker", - "launchBrowser": true, - "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", - "environmentVariables": { - "ASPNETCORE_URLS": "https://+:443;http://+:80", - "ASPNETCORE_HTTPS_PORT": "44348" - }, - "httpPort": 55731, - "useSSL": true, - "sslPort": 44348 - } - } -} \ No newline at end of file +{ + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:55730", + "sslPort": 44347 + } + }, + "profiles": { + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "PetSite": { + "commandName": "Project", + "launchBrowser": true, + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + }, + "applicationUrl": "http://localhost:5000" + }, + "Docker": { + "commandName": "Docker", + "launchBrowser": true, + "launchUrl": "{Scheme}://{ServiceHost}:{ServicePort}", + "environmentVariables": { + "ASPNETCORE_URLS": "https://+:443;http://+:80", + "ASPNETCORE_HTTPS_PORT": "44348" + }, + "httpPort": 55731, + "useSSL": true, + "sslPort": 44348 + } + } +} diff --git a/PetAdoptions/petsite/petsite/PutParams.cs b/src/applications/microservices/petsite-net/petsite/PutParams.cs similarity index 100% rename from PetAdoptions/petsite/petsite/PutParams.cs rename to src/applications/microservices/petsite-net/petsite/PutParams.cs diff --git a/PetAdoptions/petsite/petsite/SearchParams.cs b/src/applications/microservices/petsite-net/petsite/SearchParams.cs similarity index 90% rename from PetAdoptions/petsite/petsite/SearchParams.cs rename to src/applications/microservices/petsite-net/petsite/SearchParams.cs index 31d10e85..ac59052e 100644 --- a/PetAdoptions/petsite/petsite/SearchParams.cs +++ b/src/applications/microservices/petsite-net/petsite/SearchParams.cs @@ -1,16 +1,16 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace PetSite -{ - public class SearchParams - { - public string pettype { get; set; } - public string petid { get; set; } - public string petcolor { get; set; } - - public string petavailability { get; set; } - } -} +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace PetSite +{ + public class SearchParams + { + public string pettype { get; set; } + public string petid { get; set; } + public string petcolor { get; set; } + + public string petavailability { get; set; } + } +} diff --git a/PetAdoptions/petsite/petsite/Services/PetSearchService.cs b/src/applications/microservices/petsite-net/petsite/Services/PetSearchService.cs similarity index 98% rename from PetAdoptions/petsite/petsite/Services/PetSearchService.cs rename to src/applications/microservices/petsite-net/petsite/Services/PetSearchService.cs index a257b77d..cf22b2f1 100644 --- a/PetAdoptions/petsite/petsite/Services/PetSearchService.cs +++ b/src/applications/microservices/petsite-net/petsite/Services/PetSearchService.cs @@ -68,7 +68,7 @@ public async Task> GetPetDetails(string pettype, string petcolor, stri try { - var url = UrlHelper.BuildUrl(searchapiurl, + var url = UrlHelper.BuildUrl(searchapiurl, null, ("pettype", pettype != "all" ? pettype : null), ("petcolor", petcolor != "all" ? petcolor : null), ("petid", petid != "all" ? petid : null), diff --git a/PetAdoptions/petsite/petsite/Startup.cs b/src/applications/microservices/petsite-net/petsite/Startup.cs similarity index 98% rename from PetAdoptions/petsite/petsite/Startup.cs rename to src/applications/microservices/petsite-net/petsite/Startup.cs index 067a19ce..f579fd6b 100644 --- a/PetAdoptions/petsite/petsite/Startup.cs +++ b/src/applications/microservices/petsite-net/petsite/Startup.cs @@ -35,7 +35,7 @@ public void ConfigureServices(IServiceCollection services) services.AddHttpClient(); services.AddHttpContextAccessor(); services.AddScoped(); - + // Configure AWS Services services.AddAWSService(); services.AddDefaultAWSOptions(Configuration.GetAWSOptions()); @@ -60,7 +60,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseRouting(); app.UseHttpMetrics(); - + app.UseAuthorization(); app.UseEndpoints(endpoints => @@ -73,3 +73,4 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) } } } + diff --git a/src/applications/microservices/petsite-net/petsite/ViewModels/CartViewModel.cs b/src/applications/microservices/petsite-net/petsite/ViewModels/CartViewModel.cs new file mode 100644 index 00000000..6dfde5a0 --- /dev/null +++ b/src/applications/microservices/petsite-net/petsite/ViewModels/CartViewModel.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; + +namespace PetSite.ViewModels +{ + public class CartItem + { + public string food_id { get; set; } + public string food_name { get; set; } + public string food_image { get; set; } + public int quantity { get; set; } + public string unit_price { get; set; } + public string total_price { get; set; } + public bool is_available { get; set; } + public DateTime added_at { get; set; } + } + + public class CartResponse + { + public string user_id { get; set; } + public List items { get; set; } = new List(); + public int total_items { get; set; } + public string total_price { get; set; } + public DateTime created_at { get; set; } + public DateTime updated_at { get; set; } + } +} \ No newline at end of file diff --git a/src/applications/microservices/petsite-net/petsite/ViewModels/FoodViewModel.cs b/src/applications/microservices/petsite-net/petsite/ViewModels/FoodViewModel.cs new file mode 100644 index 00000000..fb63ea66 --- /dev/null +++ b/src/applications/microservices/petsite-net/petsite/ViewModels/FoodViewModel.cs @@ -0,0 +1,44 @@ +using System; +using System.Collections.Generic; + +namespace PetSite.ViewModels +{ + public class NutritionalInfo + { + public int? calories_per_serving { get; set; } + public string protein_percentage { get; set; } + public string fat_percentage { get; set; } + public string carbohydrate_percentage { get; set; } + public string fiber_percentage { get; set; } + public string moisture_percentage { get; set; } + public string serving_size { get; set; } + public int? servings_per_container { get; set; } + } + + public class FoodItem + { + public string id { get; set; } + public string pet_type { get; set; } + public string name { get; set; } + public string food_type { get; set; } + public string description { get; set; } + public string price { get; set; } + public string image { get; set; } + public NutritionalInfo nutritional_info { get; set; } + public List ingredients { get; set; } + public string feeding_guidelines { get; set; } + public string availability_status { get; set; } + public int stock_quantity { get; set; } + public DateTime created_at { get; set; } + public DateTime updated_at { get; set; } + public bool is_active { get; set; } + } + + public class FoodApiResponse + { + public List foods { get; set; } + public int total_count { get; set; } + public int? page { get; set; } + public int? page_size { get; set; } + } +} \ No newline at end of file diff --git a/PetAdoptions/petsite/petsite/ViewModels/Pets.cs b/src/applications/microservices/petsite-net/petsite/ViewModels/Pets.cs similarity index 91% rename from PetAdoptions/petsite/petsite/ViewModels/Pets.cs rename to src/applications/microservices/petsite-net/petsite/ViewModels/Pets.cs index 5105c0a0..40f265df 100644 --- a/PetAdoptions/petsite/petsite/ViewModels/Pets.cs +++ b/src/applications/microservices/petsite-net/petsite/ViewModels/Pets.cs @@ -1,44 +1,44 @@ -using Microsoft.AspNetCore.Mvc.Rendering; -using System; -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; - -namespace PetSite.ViewModels -{ - public class Pet - { - public string pettype { get; set; } - public string petid { get; set; } - public string price { get; set; } - public string petcolor { get; set; } - public string cuteness_rate { get; set; } - public string availability { get; set; } - public string image { get; set; } - public string peturl { get; set; } - - public string transactionid { get; set; } - public string adoptiondate { get; set; } - } - - public class Variety - { - public IEnumerable PetTypes; - public IEnumerable PetColors; - - public string SelectedPetType { get; set; } - public string SelectedPetColor { get; set; } - } - - public class PetDetails - { - public List Pets { get; set; } - public Variety Varieties { get; set; } - } - - public class PetFood - { - public string EntityId { get; set; } - } - -} \ No newline at end of file +using Microsoft.AspNetCore.Mvc.Rendering; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace PetSite.ViewModels +{ + public class Pet + { + public string pettype { get; set; } + public string petid { get; set; } + public string price { get; set; } + public string petcolor { get; set; } + public string cuteness_rate { get; set; } + public string availability { get; set; } + public string image { get; set; } + public string peturl { get; set; } + + public string transactionid { get; set; } + public string adoptiondate { get; set; } + } + + public class Variety + { + public IEnumerable PetTypes; + public IEnumerable PetColors; + + public string SelectedPetType { get; set; } + public string SelectedPetColor { get; set; } + } + + public class PetDetails + { + public List Pets { get; set; } + public Variety Varieties { get; set; } + } + + public class PetFood + { + public string EntityId { get; set; } + } + +} diff --git a/PetAdoptions/petsite/petsite/Views/Adoption/Index.cshtml b/src/applications/microservices/petsite-net/petsite/Views/Adoption/Index.cshtml similarity index 82% rename from PetAdoptions/petsite/petsite/Views/Adoption/Index.cshtml rename to src/applications/microservices/petsite-net/petsite/Views/Adoption/Index.cshtml index bf50172f..e89cfea8 100644 --- a/PetAdoptions/petsite/petsite/Views/Adoption/Index.cshtml +++ b/src/applications/microservices/petsite-net/petsite/Views/Adoption/Index.cshtml @@ -7,34 +7,7 @@ -@* *@ +
@@ -111,6 +84,7 @@ var jqxhr = $.getJSON( "/petfood", function() { +
diff --git a/src/applications/microservices/petsite-net/petsite/Views/Checkout/Index.cshtml b/src/applications/microservices/petsite-net/petsite/Views/Checkout/Index.cshtml new file mode 100644 index 00000000..3b275296 --- /dev/null +++ b/src/applications/microservices/petsite-net/petsite/Views/Checkout/Index.cshtml @@ -0,0 +1,208 @@ +@model PetSite.ViewModels.CartResponse +@{ + ViewData["Title"] = "Checkout"; +} + +
+
+ +
+
+ +
+ +

🛒 Your Cart 🛒

+ + @if (Model?.items != null && Model.items.Any()) + { +
+
+ @foreach (var item in Model.items) + { +
+
+
+ @item.food_name +
+
+
+
@item.food_name
+

+ $@item.unit_price x @item.quantity = $@item.total_price +

+ @if (!item.is_available) + { + Out of Stock + } +
+
+
+
+ } +
+
+
Total Items: @Model.total_items
+

Total: $@Model.total_price

+
+
+ +
+
+ + +
+
+
+ +
+
+
+

Payment Details

+
+ This is a demo - no real payment will be processed +
+ +
+ + +
+ +
+ + +
+ +
+
+
+ + +
+
+
+
+ + +
+
+
+ +
Shipping Address
+
+
+ + + + + + +
+
+ + + + + + +
+
+ +
Billing Address
+
+
+ + + + + + +
+
+ + + + + + +
+
+ + + +
+
+
+
+ } + else + { +
+

Your cart is empty

+

Add some delicious food items to your cart!

+ Buy Food +
+ } +
+ + + \ No newline at end of file diff --git a/src/applications/microservices/petsite-net/petsite/Views/FoodService/Index.cshtml b/src/applications/microservices/petsite-net/petsite/Views/FoodService/Index.cshtml new file mode 100644 index 00000000..9b02abc7 --- /dev/null +++ b/src/applications/microservices/petsite-net/petsite/Views/FoodService/Index.cshtml @@ -0,0 +1,147 @@ +@model PetSite.ViewModels.FoodApiResponse +@{ + ViewData["Title"] = "Pet Food Store"; +} + +
+
+ +
+
+ +
+ + +
+ + + + + + + +
+ +

🍽️ Delicious food for your @(Model?.foods?.FirstOrDefault()?.pet_type ?? "Pet") 🍽️

+
+ @if (Model?.foods != null) + { + @foreach (var food in Model.foods) + { +
+
+ @food.name +
+
@food.name
+

@food.description

+

$@food.price | In Stock: @food.stock_quantity

+

For: @food.pet_type | Type: @food.food_type

+ + + +
+
+
+ } + } + else + { +
+

No food items available at the moment.

+
+ } +
+
+ + + + + + + \ No newline at end of file diff --git a/PetAdoptions/petsite/petsite/Views/Home/HouseKeeping.cshtml b/src/applications/microservices/petsite-net/petsite/Views/Home/HouseKeeping.cshtml similarity index 100% rename from PetAdoptions/petsite/petsite/Views/Home/HouseKeeping.cshtml rename to src/applications/microservices/petsite-net/petsite/Views/Home/HouseKeeping.cshtml diff --git a/PetAdoptions/petsite/petsite/Views/Home/Index.cshtml b/src/applications/microservices/petsite-net/petsite/Views/Home/Index.cshtml similarity index 100% rename from PetAdoptions/petsite/petsite/Views/Home/Index.cshtml rename to src/applications/microservices/petsite-net/petsite/Views/Home/Index.cshtml diff --git a/PetAdoptions/petsite/petsite/Views/Home/Privacy.cshtml b/src/applications/microservices/petsite-net/petsite/Views/Home/Privacy.cshtml similarity index 91% rename from PetAdoptions/petsite/petsite/Views/Home/Privacy.cshtml rename to src/applications/microservices/petsite-net/petsite/Views/Home/Privacy.cshtml index 2479fb7d..d01477f3 100644 --- a/PetAdoptions/petsite/petsite/Views/Home/Privacy.cshtml +++ b/src/applications/microservices/petsite-net/petsite/Views/Home/Privacy.cshtml @@ -1,6 +1,6 @@ -@{ - ViewData["Title"] = "Privacy Policy"; -} -

@ViewData["Title"]

- -

Use this page to detail your site's privacy policy.

+@{ + ViewData["Title"] = "Privacy Policy"; +} +

@ViewData["Title"]

+ +

Use this page to detail your site's privacy policy.

diff --git a/src/applications/microservices/petsite-net/petsite/Views/Payment/Index.cshtml b/src/applications/microservices/petsite-net/petsite/Views/Payment/Index.cshtml new file mode 100644 index 00000000..6ea53539 --- /dev/null +++ b/src/applications/microservices/petsite-net/petsite/Views/Payment/Index.cshtml @@ -0,0 +1,100 @@ +@model PetSite.ViewModels.FoodApiResponse +@{ + ViewData["Title"] = "Complete Adoption"; +} + +
+
+ +
+
+
+ +
+ @if (ViewData["txStatus"]?.ToString() == "success") + { + + @if (ViewData["PetDetails"] != null) + { + var pet = (PetSite.ViewModels.Pet)ViewData["PetDetails"]; +
+
+
+
+
+ + @pet.pettype-@pet.petcolor + $@pet.price + @if (!string.IsNullOrEmpty(pet.cuteness_rate) && Int32.TryParse(pet.cuteness_rate, out int rating)) + { +
+ @for (int i = 0; i < rating; i++) + { + + } +
+ } +
+ +
+
+
+
+ + } + + } + else + { +
+
+ +
+

Error @ViewData["txStatus"]

+ } +
+ diff --git a/PetAdoptions/petsite/petsite/Views/PetHistory/Index.cshtml b/src/applications/microservices/petsite-net/petsite/Views/PetHistory/Index.cshtml similarity index 100% rename from PetAdoptions/petsite/petsite/Views/PetHistory/Index.cshtml rename to src/applications/microservices/petsite-net/petsite/Views/PetHistory/Index.cshtml diff --git a/PetAdoptions/petsite/petsite/Views/PetListAdoptions/Index.cshtml b/src/applications/microservices/petsite-net/petsite/Views/PetListAdoptions/Index.cshtml similarity index 71% rename from PetAdoptions/petsite/petsite/Views/PetListAdoptions/Index.cshtml rename to src/applications/microservices/petsite-net/petsite/Views/PetListAdoptions/Index.cshtml index 134802cb..ddcc5404 100644 --- a/PetAdoptions/petsite/petsite/Views/PetListAdoptions/Index.cshtml +++ b/src/applications/microservices/petsite-net/petsite/Views/PetListAdoptions/Index.cshtml @@ -25,8 +25,10 @@ } else { -
- No adoption data retreived +
+ }
diff --git a/PetAdoptions/petsite/petsite/Views/Shared/Error.cshtml b/src/applications/microservices/petsite-net/petsite/Views/Shared/Error.cshtml similarity index 100% rename from PetAdoptions/petsite/petsite/Views/Shared/Error.cshtml rename to src/applications/microservices/petsite-net/petsite/Views/Shared/Error.cshtml diff --git a/PetAdoptions/petsite/petsite/Views/Shared/_AdoptionItem.cshtml b/src/applications/microservices/petsite-net/petsite/Views/Shared/_AdoptionItem.cshtml similarity index 98% rename from PetAdoptions/petsite/petsite/Views/Shared/_AdoptionItem.cshtml rename to src/applications/microservices/petsite-net/petsite/Views/Shared/_AdoptionItem.cshtml index 6cdb941c..cf6d49d3 100644 --- a/PetAdoptions/petsite/petsite/Views/Shared/_AdoptionItem.cshtml +++ b/src/applications/microservices/petsite-net/petsite/Views/Shared/_AdoptionItem.cshtml @@ -4,7 +4,7 @@ - +
@Model.pettype-@Model.petcolor
diff --git a/PetAdoptions/petsite/petsite/Views/Shared/_Layout.cshtml b/src/applications/microservices/petsite-net/petsite/Views/Shared/_Layout.cshtml similarity index 53% rename from PetAdoptions/petsite/petsite/Views/Shared/_Layout.cshtml rename to src/applications/microservices/petsite-net/petsite/Views/Shared/_Layout.cshtml index d9c2c4af..9b680425 100644 --- a/PetAdoptions/petsite/petsite/Views/Shared/_Layout.cshtml +++ b/src/applications/microservices/petsite-net/petsite/Views/Shared/_Layout.cshtml @@ -20,13 +20,9 @@ - + - +
@@ -39,7 +35,11 @@
-
+
+ + Buy Food + + See Adoption List @@ -47,6 +47,11 @@ Perform Housekeeping + + + 🛒 + +
@@ -54,10 +59,41 @@ @RenderBody() + + + @RenderSection("Scripts", required: false) \ No newline at end of file diff --git a/src/applications/microservices/petsite-net/petsite/Views/Shared/_PetItem.cshtml b/src/applications/microservices/petsite-net/petsite/Views/Shared/_PetItem.cshtml new file mode 100644 index 00000000..4cb578d4 --- /dev/null +++ b/src/applications/microservices/petsite-net/petsite/Views/Shared/_PetItem.cshtml @@ -0,0 +1,48 @@ +@using Microsoft.AspNetCore.Http.Features +@using Microsoft.CodeAnalysis.CSharp.Syntax +@model Pet; + +
+
+ + + + @Model.pettype-@Model.petcolor + + + $@Model.price + + +
+ @for (int i = 0; i < Int32.Parse(Model.cuteness_rate); i++) + { + + } +
+
+ + @if (Model.availability == "yes") + { +
+ + } + else + { +
+
+ Unavailable + } + + + + + + + + + +
diff --git a/PetAdoptions/petsite/petsite/Views/Shared/_ValidationScriptsPartial.cshtml b/src/applications/microservices/petsite-net/petsite/Views/Shared/_ValidationScriptsPartial.cshtml similarity index 53% rename from PetAdoptions/petsite/petsite/Views/Shared/_ValidationScriptsPartial.cshtml rename to src/applications/microservices/petsite-net/petsite/Views/Shared/_ValidationScriptsPartial.cshtml index ff9c7938..7d8dd3e2 100644 --- a/PetAdoptions/petsite/petsite/Views/Shared/_ValidationScriptsPartial.cshtml +++ b/src/applications/microservices/petsite-net/petsite/Views/Shared/_ValidationScriptsPartial.cshtml @@ -1,2 +1,2 @@ - - + + diff --git a/PetAdoptions/petsite/petsite/Views/_ViewImports.cshtml b/src/applications/microservices/petsite-net/petsite/Views/_ViewImports.cshtml similarity index 82% rename from PetAdoptions/petsite/petsite/Views/_ViewImports.cshtml rename to src/applications/microservices/petsite-net/petsite/Views/_ViewImports.cshtml index dff3ee7f..a5b79b36 100644 --- a/PetAdoptions/petsite/petsite/Views/_ViewImports.cshtml +++ b/src/applications/microservices/petsite-net/petsite/Views/_ViewImports.cshtml @@ -1,4 +1,4 @@ -@using PetSite -@using PetSite.Models -@using PetSite.ViewModels; -@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers +@using PetSite +@using PetSite.Models +@using PetSite.ViewModels; +@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers diff --git a/PetAdoptions/petsite/petsite/Views/_ViewStart.cshtml b/src/applications/microservices/petsite-net/petsite/Views/_ViewStart.cshtml similarity index 74% rename from PetAdoptions/petsite/petsite/Views/_ViewStart.cshtml rename to src/applications/microservices/petsite-net/petsite/Views/_ViewStart.cshtml index 6e88aa32..820a2f6e 100644 --- a/PetAdoptions/petsite/petsite/Views/_ViewStart.cshtml +++ b/src/applications/microservices/petsite-net/petsite/Views/_ViewStart.cshtml @@ -1,3 +1,3 @@ -@{ - Layout = "_Layout"; -} +@{ + Layout = "_Layout"; +} diff --git a/PetAdoptions/petsite/petsite/appsettings.Development.json b/src/applications/microservices/petsite-net/petsite/appsettings.Development.json similarity index 94% rename from PetAdoptions/petsite/petsite/appsettings.Development.json rename to src/applications/microservices/petsite-net/petsite/appsettings.Development.json index dd90bda6..4cf6d423 100644 --- a/PetAdoptions/petsite/petsite/appsettings.Development.json +++ b/src/applications/microservices/petsite-net/petsite/appsettings.Development.json @@ -1,8 +1,8 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Debug", - "System": "Information", - "Microsoft": "Information" - } - } } +{ + "Logging": { + "LogLevel": { + "Default": "Debug", + "System": "Information", + "Microsoft": "Information" + } + } } diff --git a/PetAdoptions/petsite/petsite/appsettings.json b/src/applications/microservices/petsite-net/petsite/appsettings.json similarity index 94% rename from PetAdoptions/petsite/petsite/appsettings.json rename to src/applications/microservices/petsite-net/petsite/appsettings.json index 81ff8777..d9d9a9bf 100644 --- a/PetAdoptions/petsite/petsite/appsettings.json +++ b/src/applications/microservices/petsite-net/petsite/appsettings.json @@ -1,10 +1,10 @@ -{ - "Logging": { - "LogLevel": { - "Default": "Information", - "Microsoft": "Warning", - "Microsoft.Hosting.Lifetime": "Information" - } - }, - "AllowedHosts": "*" -} +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft": "Warning", + "Microsoft.Hosting.Lifetime": "Information" + } + }, + "AllowedHosts": "*" +} diff --git a/PetAdoptions/petsite/petsite/bundleconfig.json b/src/applications/microservices/petsite-net/petsite/bundleconfig.json similarity index 100% rename from PetAdoptions/petsite/petsite/bundleconfig.json rename to src/applications/microservices/petsite-net/petsite/bundleconfig.json diff --git a/PetAdoptions/petsite/petsite/compilerconfig.json b/src/applications/microservices/petsite-net/petsite/compilerconfig.json similarity index 99% rename from PetAdoptions/petsite/petsite/compilerconfig.json rename to src/applications/microservices/petsite-net/petsite/compilerconfig.json index 7f08acf4..52cb57d2 100644 --- a/PetAdoptions/petsite/petsite/compilerconfig.json +++ b/src/applications/microservices/petsite-net/petsite/compilerconfig.json @@ -1,4 +1,4 @@ -[ +[ { "outputFile": "wwwroot/css/shared/components/header/header.css", "inputFile": "wwwroot/css/shared/components/header/header.scss" diff --git a/PetAdoptions/petsite/petsite/compilerconfig.json.defaults b/src/applications/microservices/petsite-net/petsite/compilerconfig.json.defaults similarity index 95% rename from PetAdoptions/petsite/petsite/compilerconfig.json.defaults rename to src/applications/microservices/petsite-net/petsite/compilerconfig.json.defaults index 5a1f056e..b5c3a5bc 100644 --- a/PetAdoptions/petsite/petsite/compilerconfig.json.defaults +++ b/src/applications/microservices/petsite-net/petsite/compilerconfig.json.defaults @@ -1,63 +1,63 @@ -{ - "compilers": { - "less": { - "autoPrefix": "", - "cssComb": "none", - "ieCompat": true, - "strictMath": false, - "strictUnits": false, - "relativeUrls": true, - "rootPath": "", - "sourceMapRoot": "", - "sourceMapBasePath": "", - "sourceMap": false - }, - "sass": { - "autoPrefix": "", - "includePath": "", - "indentType": "space", - "indentWidth": 2, - "outputStyle": "nested", - "Precision": 5, - "relativeUrls": true, - "sourceMapRoot": "", - "lineFeed": "", - "sourceMap": false - }, - "stylus": { - "sourceMap": false - }, - "babel": { - "sourceMap": false - }, - "coffeescript": { - "bare": false, - "runtimeMode": "node", - "sourceMap": false - }, - "handlebars": { - "root": "", - "noBOM": false, - "name": "", - "namespace": "", - "knownHelpersOnly": false, - "forcePartial": false, - "knownHelpers": [], - "commonjs": "", - "amd": false, - "sourceMap": false - } - }, - "minifiers": { - "css": { - "enabled": true, - "termSemicolons": true, - "gzip": false - }, - "javascript": { - "enabled": true, - "termSemicolons": true, - "gzip": false - } - } -} \ No newline at end of file +{ + "compilers": { + "less": { + "autoPrefix": "", + "cssComb": "none", + "ieCompat": true, + "strictMath": false, + "strictUnits": false, + "relativeUrls": true, + "rootPath": "", + "sourceMapRoot": "", + "sourceMapBasePath": "", + "sourceMap": false + }, + "sass": { + "autoPrefix": "", + "includePath": "", + "indentType": "space", + "indentWidth": 2, + "outputStyle": "nested", + "Precision": 5, + "relativeUrls": true, + "sourceMapRoot": "", + "lineFeed": "", + "sourceMap": false + }, + "stylus": { + "sourceMap": false + }, + "babel": { + "sourceMap": false + }, + "coffeescript": { + "bare": false, + "runtimeMode": "node", + "sourceMap": false + }, + "handlebars": { + "root": "", + "noBOM": false, + "name": "", + "namespace": "", + "knownHelpersOnly": false, + "forcePartial": false, + "knownHelpers": [], + "commonjs": "", + "amd": false, + "sourceMap": false + } + }, + "minifiers": { + "css": { + "enabled": true, + "termSemicolons": true, + "gzip": false + }, + "javascript": { + "enabled": true, + "termSemicolons": true, + "gzip": false + } + } +} diff --git a/PetAdoptions/petsite/petsite/wwwroot/css/petstyles.css b/src/applications/microservices/petsite-net/petsite/wwwroot/css/petstyles.css similarity index 95% rename from PetAdoptions/petsite/petsite/wwwroot/css/petstyles.css rename to src/applications/microservices/petsite-net/petsite/wwwroot/css/petstyles.css index cb777bd3..a11cfa80 100644 --- a/PetAdoptions/petsite/petsite/wwwroot/css/petstyles.css +++ b/src/applications/microservices/petsite-net/petsite/wwwroot/css/petstyles.css @@ -1,3 +1,8 @@ +/* Roboto font for all pet elements */ +.pet-item, .pet-name, .pet-price, .pet-button, .pet-filter, .pet-label { + font-family: 'Roboto', sans-serif; +} + .pet-hero { background-image: url("../images/main_banner.png"); background-size: cover; diff --git a/PetAdoptions/petsite/petsite/wwwroot/css/site.css b/src/applications/microservices/petsite-net/petsite/wwwroot/css/site.css similarity index 71% rename from PetAdoptions/petsite/petsite/wwwroot/css/site.css rename to src/applications/microservices/petsite-net/petsite/wwwroot/css/site.css index 7be1bba7..da80dad2 100644 --- a/PetAdoptions/petsite/petsite/wwwroot/css/site.css +++ b/src/applications/microservices/petsite-net/petsite/wwwroot/css/site.css @@ -1,6 +1,26 @@ /* Please see documentation at https://docs.microsoft.com/aspnet/core/client-side/bundling-and-minification for details on configuring this project to bundle and minify static web assets. */ +/* Override Bootstrap font declarations to use Roboto everywhere */ +:root { + --bs-font-sans-serif: 'Roboto', sans-serif !important; + --bs-body-font-family: 'Roboto', sans-serif !important; +} + +body, html { + font-family: 'Roboto', sans-serif; +} + +/* Force Roboto on all Bootstrap elements */ +* { + font-family: 'Roboto', sans-serif !important; +} + +/* Preserve monospace for code elements */ +code, kbd, pre, samp, .font-monospace { + font-family: SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace !important; +} + a.navbar-brand { white-space: normal; text-align: center; diff --git a/src/applications/microservices/petsite-net/petsite/wwwroot/css/site.min.css b/src/applications/microservices/petsite-net/petsite/wwwroot/css/site.min.css new file mode 100644 index 00000000..03ce3f60 --- /dev/null +++ b/src/applications/microservices/petsite-net/petsite/wwwroot/css/site.min.css @@ -0,0 +1 @@ +.pet-item,.pet-name,.pet-price,.pet-button,.pet-filter,.pet-label{font-family:'Roboto',sans-serif}.pet-hero{background-image:url("../images/main_banner.png");background-size:cover;height:135px;width:100%}.pet-hero-title{position:relative;top:18.28571px}.pet-items{margin-top:1rem}.pet-item{margin-bottom:1.5rem;text-align:center;width:33%;display:inline-block;float:none !important}.pet-list-item{font-size:1rem;font-weight:300}@media screen and (max-width:1024px){.pet-item{width:50%}}@media screen and (max-width:768px){.pet-item{width:100%}}.pet-footer{background-color:#000;border-top:1px solid #eee;margin-top:2.5rem;padding-bottom:2.5rem;padding-top:2.5rem;width:100%;bottom:0}.pet-footer-brand{height:50px;width:230px}.pet-header{margin:0;background-color:#000}.pet-wrapper{display:flex;min-height:100vh;flex-direction:column;justify-content:flex-start}.pet-filters{background-color:rgba(19,126,68,.67);height:65px}.pet-filter{border-radius:.5rem;-webkit-appearance:none;background-color:transparent;border-color:#033c40;color:#fff;cursor:pointer;margin-right:1rem;margin-top:.5rem;min-width:140px;outline-color:#033c40;padding-bottom:0;padding-left:.5rem;padding-right:.5rem;padding-top:1.5rem}.pet-filter option{background-color:#5096e7}.pet-label{display:inline-block;position:relative;z-index:0}.pet-label::before{color:#ede90b;content:attr(data-title);font-size:.8rem;font-weight:bold;margin-left:.5rem;margin-top:.65rem;position:absolute;text-transform:uppercase;z-index:1}.pet-label::after{background-image:url("../images/arrow-down.png");content:'';height:7px;position:absolute;right:1.5rem;top:2.5rem;width:10px;z-index:1}.pet-send{background-color:#dc7406;color:#efd912;cursor:pointer;font-size:1rem;margin-top:-1.5rem;transition:all .35s}.pet-send:hover{background-color:#db7603;transition:all .35s}.pet-thumbnail{max-width:370px;width:100%}.pet-button{border-radius:.5rem;background-color:#2d7f83;border:0;color:#fff;cursor:pointer;font-size:1rem;height:3rem;margin-top:1rem;transition:all .35s;width:50%}.pet-button.is-disabled{opacity:.5;pointer-events:none}.pet-button:hover{font-size:1.5rem;background-color:#0fa828;transition:all .35s}.pet-name{font-size:1rem;font-stretch:condensed;font-weight:300;margin-top:.5rem;text-align:center;text-transform:lowercase}.pet-price{font-size:25px;font-weight:400;text-align:center}.pet-price::before{content:'$'}:root{--bs-font-sans-serif:'Roboto',sans-serif !important;--bs-body-font-family:'Roboto',sans-serif !important}body,html{font-family:'Roboto',sans-serif}*{font-family:'Roboto',sans-serif !important}code,kbd,pre,samp,.font-monospace{font-family:SFMono-Regular,Menlo,Monaco,Consolas,"Liberation Mono","Courier New",monospace !important}a.navbar-brand{white-space:normal;text-align:center;word-break:break-all}a{color:#19dc91}.btn-primary{color:#fff;background-color:#19dc91;border-color:#19dc91}.nav-pills .nav-link.active,.nav-pills .show>.nav-link{color:#fff;background-color:#19dc91;border-color:#19dc91}html{font-size:14px}@media(min-width:768px){html{font-size:16px}}.border-top{border-top:1px solid #e5e5e5}.border-bottom{border-bottom:1px solid #e5e5e5}.box-shadow{box-shadow:0 .25rem .75rem rgba(0,0,0,.05)}button.accept-policy{font-size:1rem;line-height:inherit}html{position:relative;min-height:100%}body{margin-bottom:60px}.footer{position:absolute;bottom:0;width:100%;white-space:nowrap;line-height:60px} diff --git a/PetAdoptions/petsite/petsite/wwwroot/favicon.ico b/src/applications/microservices/petsite-net/petsite/wwwroot/favicon.ico similarity index 100% rename from PetAdoptions/petsite/petsite/wwwroot/favicon.ico rename to src/applications/microservices/petsite-net/petsite/wwwroot/favicon.ico diff --git a/PetAdoptions/petsite/petsite/wwwroot/images/arrow-down.png b/src/applications/microservices/petsite-net/petsite/wwwroot/images/arrow-down.png similarity index 100% rename from PetAdoptions/petsite/petsite/wwwroot/images/arrow-down.png rename to src/applications/microservices/petsite-net/petsite/wwwroot/images/arrow-down.png diff --git a/PetAdoptions/petsite/petsite/wwwroot/images/arrow-right.svg b/src/applications/microservices/petsite-net/petsite/wwwroot/images/arrow-right.svg similarity index 100% rename from PetAdoptions/petsite/petsite/wwwroot/images/arrow-right.svg rename to src/applications/microservices/petsite-net/petsite/wwwroot/images/arrow-right.svg diff --git a/PetAdoptions/petsite/petsite/wwwroot/images/brand.png b/src/applications/microservices/petsite-net/petsite/wwwroot/images/brand.png similarity index 100% rename from PetAdoptions/petsite/petsite/wwwroot/images/brand.png rename to src/applications/microservices/petsite-net/petsite/wwwroot/images/brand.png diff --git a/PetAdoptions/petsite/petsite/wwwroot/images/main_banner.png b/src/applications/microservices/petsite-net/petsite/wwwroot/images/main_banner.png similarity index 100% rename from PetAdoptions/petsite/petsite/wwwroot/images/main_banner.png rename to src/applications/microservices/petsite-net/petsite/wwwroot/images/main_banner.png diff --git a/PetAdoptions/petsite/petsite/wwwroot/images/main_banner_text.ai b/src/applications/microservices/petsite-net/petsite/wwwroot/images/main_banner_text.ai similarity index 100% rename from PetAdoptions/petsite/petsite/wwwroot/images/main_banner_text.ai rename to src/applications/microservices/petsite-net/petsite/wwwroot/images/main_banner_text.ai diff --git a/PetAdoptions/petsite/petsite/wwwroot/images/main_banner_text.png b/src/applications/microservices/petsite-net/petsite/wwwroot/images/main_banner_text.png similarity index 100% rename from PetAdoptions/petsite/petsite/wwwroot/images/main_banner_text.png rename to src/applications/microservices/petsite-net/petsite/wwwroot/images/main_banner_text.png diff --git a/PetAdoptions/petsite/petsite/wwwroot/js/site.js b/src/applications/microservices/petsite-net/petsite/wwwroot/js/site.js similarity index 100% rename from PetAdoptions/petsite/petsite/wwwroot/js/site.js rename to src/applications/microservices/petsite-net/petsite/wwwroot/js/site.js diff --git a/PetAdoptions/petsite/petsite/wwwroot/js/site.min.js b/src/applications/microservices/petsite-net/petsite/wwwroot/js/site.min.js similarity index 100% rename from PetAdoptions/petsite/petsite/wwwroot/js/site.min.js rename to src/applications/microservices/petsite-net/petsite/wwwroot/js/site.min.js diff --git a/PetAdoptions/petsite/petsite/wwwroot/lib/bootstrap/LICENSE b/src/applications/microservices/petsite-net/petsite/wwwroot/lib/bootstrap/LICENSE similarity index 100% rename from PetAdoptions/petsite/petsite/wwwroot/lib/bootstrap/LICENSE rename to src/applications/microservices/petsite-net/petsite/wwwroot/lib/bootstrap/LICENSE diff --git a/PetAdoptions/petsite/petsite/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css b/src/applications/microservices/petsite-net/petsite/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css similarity index 100% rename from PetAdoptions/petsite/petsite/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css rename to src/applications/microservices/petsite-net/petsite/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css diff --git a/PetAdoptions/petsite/petsite/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map b/src/applications/microservices/petsite-net/petsite/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map similarity index 100% rename from PetAdoptions/petsite/petsite/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map rename to src/applications/microservices/petsite-net/petsite/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.css.map diff --git a/PetAdoptions/petsite/petsite/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css b/src/applications/microservices/petsite-net/petsite/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css similarity index 100% rename from PetAdoptions/petsite/petsite/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css rename to src/applications/microservices/petsite-net/petsite/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css diff --git a/PetAdoptions/petsite/petsite/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map b/src/applications/microservices/petsite-net/petsite/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map similarity index 100% rename from PetAdoptions/petsite/petsite/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map rename to src/applications/microservices/petsite-net/petsite/wwwroot/lib/bootstrap/dist/css/bootstrap-grid.min.css.map diff --git a/PetAdoptions/petsite/petsite/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css b/src/applications/microservices/petsite-net/petsite/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css similarity index 100% rename from PetAdoptions/petsite/petsite/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css rename to src/applications/microservices/petsite-net/petsite/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css diff --git a/PetAdoptions/petsite/petsite/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map b/src/applications/microservices/petsite-net/petsite/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map similarity index 100% rename from PetAdoptions/petsite/petsite/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map rename to src/applications/microservices/petsite-net/petsite/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.css.map diff --git a/PetAdoptions/petsite/petsite/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css b/src/applications/microservices/petsite-net/petsite/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css similarity index 100% rename from PetAdoptions/petsite/petsite/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css rename to src/applications/microservices/petsite-net/petsite/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css diff --git a/PetAdoptions/petsite/petsite/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map b/src/applications/microservices/petsite-net/petsite/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map similarity index 100% rename from PetAdoptions/petsite/petsite/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map rename to src/applications/microservices/petsite-net/petsite/wwwroot/lib/bootstrap/dist/css/bootstrap-reboot.min.css.map diff --git a/PetAdoptions/petsite/petsite/wwwroot/lib/bootstrap/dist/css/bootstrap.css b/src/applications/microservices/petsite-net/petsite/wwwroot/lib/bootstrap/dist/css/bootstrap.css similarity index 100% rename from PetAdoptions/petsite/petsite/wwwroot/lib/bootstrap/dist/css/bootstrap.css rename to src/applications/microservices/petsite-net/petsite/wwwroot/lib/bootstrap/dist/css/bootstrap.css diff --git a/PetAdoptions/petsite/petsite/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map b/src/applications/microservices/petsite-net/petsite/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map similarity index 100% rename from PetAdoptions/petsite/petsite/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map rename to src/applications/microservices/petsite-net/petsite/wwwroot/lib/bootstrap/dist/css/bootstrap.css.map diff --git a/PetAdoptions/petsite/petsite/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css b/src/applications/microservices/petsite-net/petsite/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css similarity index 100% rename from PetAdoptions/petsite/petsite/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css rename to src/applications/microservices/petsite-net/petsite/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css diff --git a/PetAdoptions/petsite/petsite/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map b/src/applications/microservices/petsite-net/petsite/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map similarity index 100% rename from PetAdoptions/petsite/petsite/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map rename to src/applications/microservices/petsite-net/petsite/wwwroot/lib/bootstrap/dist/css/bootstrap.min.css.map diff --git a/PetAdoptions/petsite/petsite/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js b/src/applications/microservices/petsite-net/petsite/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js similarity index 100% rename from PetAdoptions/petsite/petsite/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js rename to src/applications/microservices/petsite-net/petsite/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js diff --git a/PetAdoptions/petsite/petsite/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js.map b/src/applications/microservices/petsite-net/petsite/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js.map similarity index 100% rename from PetAdoptions/petsite/petsite/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js.map rename to src/applications/microservices/petsite-net/petsite/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.js.map diff --git a/PetAdoptions/petsite/petsite/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js b/src/applications/microservices/petsite-net/petsite/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js similarity index 100% rename from PetAdoptions/petsite/petsite/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js rename to src/applications/microservices/petsite-net/petsite/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js diff --git a/PetAdoptions/petsite/petsite/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js.map b/src/applications/microservices/petsite-net/petsite/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js.map similarity index 100% rename from PetAdoptions/petsite/petsite/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js.map rename to src/applications/microservices/petsite-net/petsite/wwwroot/lib/bootstrap/dist/js/bootstrap.bundle.min.js.map diff --git a/PetAdoptions/petsite/petsite/wwwroot/lib/bootstrap/dist/js/bootstrap.js b/src/applications/microservices/petsite-net/petsite/wwwroot/lib/bootstrap/dist/js/bootstrap.js similarity index 100% rename from PetAdoptions/petsite/petsite/wwwroot/lib/bootstrap/dist/js/bootstrap.js rename to src/applications/microservices/petsite-net/petsite/wwwroot/lib/bootstrap/dist/js/bootstrap.js diff --git a/PetAdoptions/petsite/petsite/wwwroot/lib/bootstrap/dist/js/bootstrap.js.map b/src/applications/microservices/petsite-net/petsite/wwwroot/lib/bootstrap/dist/js/bootstrap.js.map similarity index 100% rename from PetAdoptions/petsite/petsite/wwwroot/lib/bootstrap/dist/js/bootstrap.js.map rename to src/applications/microservices/petsite-net/petsite/wwwroot/lib/bootstrap/dist/js/bootstrap.js.map diff --git a/PetAdoptions/petsite/petsite/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js b/src/applications/microservices/petsite-net/petsite/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js similarity index 100% rename from PetAdoptions/petsite/petsite/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js rename to src/applications/microservices/petsite-net/petsite/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js diff --git a/PetAdoptions/petsite/petsite/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js.map b/src/applications/microservices/petsite-net/petsite/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js.map similarity index 100% rename from PetAdoptions/petsite/petsite/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js.map rename to src/applications/microservices/petsite-net/petsite/wwwroot/lib/bootstrap/dist/js/bootstrap.min.js.map diff --git a/PetAdoptions/petsite/petsite/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt b/src/applications/microservices/petsite-net/petsite/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt similarity index 100% rename from PetAdoptions/petsite/petsite/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt rename to src/applications/microservices/petsite-net/petsite/wwwroot/lib/jquery-validation-unobtrusive/LICENSE.txt diff --git a/PetAdoptions/petsite/petsite/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js b/src/applications/microservices/petsite-net/petsite/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js similarity index 100% rename from PetAdoptions/petsite/petsite/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js rename to src/applications/microservices/petsite-net/petsite/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.js diff --git a/PetAdoptions/petsite/petsite/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js b/src/applications/microservices/petsite-net/petsite/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js similarity index 100% rename from PetAdoptions/petsite/petsite/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js rename to src/applications/microservices/petsite-net/petsite/wwwroot/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js diff --git a/PetAdoptions/petsite/petsite/wwwroot/lib/jquery-validation/LICENSE.md b/src/applications/microservices/petsite-net/petsite/wwwroot/lib/jquery-validation/LICENSE.md similarity index 100% rename from PetAdoptions/petsite/petsite/wwwroot/lib/jquery-validation/LICENSE.md rename to src/applications/microservices/petsite-net/petsite/wwwroot/lib/jquery-validation/LICENSE.md diff --git a/PetAdoptions/petsite/petsite/wwwroot/lib/jquery-validation/dist/additional-methods.js b/src/applications/microservices/petsite-net/petsite/wwwroot/lib/jquery-validation/dist/additional-methods.js similarity index 100% rename from PetAdoptions/petsite/petsite/wwwroot/lib/jquery-validation/dist/additional-methods.js rename to src/applications/microservices/petsite-net/petsite/wwwroot/lib/jquery-validation/dist/additional-methods.js diff --git a/PetAdoptions/petsite/petsite/wwwroot/lib/jquery-validation/dist/additional-methods.min.js b/src/applications/microservices/petsite-net/petsite/wwwroot/lib/jquery-validation/dist/additional-methods.min.js similarity index 100% rename from PetAdoptions/petsite/petsite/wwwroot/lib/jquery-validation/dist/additional-methods.min.js rename to src/applications/microservices/petsite-net/petsite/wwwroot/lib/jquery-validation/dist/additional-methods.min.js diff --git a/PetAdoptions/petsite/petsite/wwwroot/lib/jquery-validation/dist/jquery.validate.js b/src/applications/microservices/petsite-net/petsite/wwwroot/lib/jquery-validation/dist/jquery.validate.js similarity index 100% rename from PetAdoptions/petsite/petsite/wwwroot/lib/jquery-validation/dist/jquery.validate.js rename to src/applications/microservices/petsite-net/petsite/wwwroot/lib/jquery-validation/dist/jquery.validate.js diff --git a/PetAdoptions/petsite/petsite/wwwroot/lib/jquery-validation/dist/jquery.validate.min.js b/src/applications/microservices/petsite-net/petsite/wwwroot/lib/jquery-validation/dist/jquery.validate.min.js similarity index 100% rename from PetAdoptions/petsite/petsite/wwwroot/lib/jquery-validation/dist/jquery.validate.min.js rename to src/applications/microservices/petsite-net/petsite/wwwroot/lib/jquery-validation/dist/jquery.validate.min.js diff --git a/PetAdoptions/petsite/petsite/wwwroot/lib/jquery/LICENSE.txt b/src/applications/microservices/petsite-net/petsite/wwwroot/lib/jquery/LICENSE.txt similarity index 100% rename from PetAdoptions/petsite/petsite/wwwroot/lib/jquery/LICENSE.txt rename to src/applications/microservices/petsite-net/petsite/wwwroot/lib/jquery/LICENSE.txt diff --git a/PetAdoptions/petsite/petsite/wwwroot/lib/jquery/dist/jquery.js b/src/applications/microservices/petsite-net/petsite/wwwroot/lib/jquery/dist/jquery.js similarity index 100% rename from PetAdoptions/petsite/petsite/wwwroot/lib/jquery/dist/jquery.js rename to src/applications/microservices/petsite-net/petsite/wwwroot/lib/jquery/dist/jquery.js diff --git a/PetAdoptions/petsite/petsite/wwwroot/lib/jquery/dist/jquery.min.js b/src/applications/microservices/petsite-net/petsite/wwwroot/lib/jquery/dist/jquery.min.js similarity index 100% rename from PetAdoptions/petsite/petsite/wwwroot/lib/jquery/dist/jquery.min.js rename to src/applications/microservices/petsite-net/petsite/wwwroot/lib/jquery/dist/jquery.min.js diff --git a/PetAdoptions/petsite/petsite/wwwroot/lib/jquery/dist/jquery.min.map b/src/applications/microservices/petsite-net/petsite/wwwroot/lib/jquery/dist/jquery.min.map similarity index 100% rename from PetAdoptions/petsite/petsite/wwwroot/lib/jquery/dist/jquery.min.map rename to src/applications/microservices/petsite-net/petsite/wwwroot/lib/jquery/dist/jquery.min.map diff --git a/src/cdk/.env.sample b/src/cdk/.env.sample new file mode 100644 index 00000000..53b48061 --- /dev/null +++ b/src/cdk/.env.sample @@ -0,0 +1,9 @@ +CONFIG_FILE_URL=https://raw.githubusercontent.com/aws-samples/one-observability-demo/refs/heads/feat/cdkpipeline/typedoc.json +CONFIG_BUCKET=your-config-bucket-name +ORGANIZATION_NAME=aws-samples +REPOSITORY_NAME=one-observability-demo +BRANCH_NAME=your-branch-name +WORKING_FOLDER=src/cdk +STACK_NAME=TestStack +AWS_REGION=us-east-1 +AWS_ACCOUNT_ID=123456789012 \ No newline at end of file diff --git a/PetAdoptions/cdk/pet_stack/.npmignore b/src/cdk/.npmignore similarity index 100% rename from PetAdoptions/cdk/pet_stack/.npmignore rename to src/cdk/.npmignore diff --git a/src/cdk/.vscode/launch.json b/src/cdk/.vscode/launch.json new file mode 100644 index 00000000..48b1653e --- /dev/null +++ b/src/cdk/.vscode/launch.json @@ -0,0 +1,24 @@ +{ + "version": "0.2.0", + "configurations": [ + { + "name": "Debug CDK Synth", + "type": "node", + "request": "launch", + "program": "${workspaceFolder}/node_modules/.bin/cdk", + "args": [ + "-a", + "npx ts-node --prefer-ts-exts bin/local.ts", + "synth" + ], + "cwd": "${workspaceFolder}", + "env": { + "NODE_OPTIONS": "--inspect" + }, + "console": "integratedTerminal", + "skipFiles": [ + "/**" + ] + } + ] +} \ No newline at end of file diff --git a/src/cdk/README.md b/src/cdk/README.md new file mode 100644 index 00000000..7bb2ba66 --- /dev/null +++ b/src/cdk/README.md @@ -0,0 +1,18 @@ + +# 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/src/cdk/bin/constants.ts b/src/cdk/bin/constants.ts new file mode 100644 index 00000000..31236dc7 --- /dev/null +++ b/src/cdk/bin/constants.ts @@ -0,0 +1,87 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +/** + * Export name constants for CloudFormation stack outputs. + * + * This module contains all the export names used for cross-stack references + * in the One Observability Workshop CDK application. + * + * @packageDocumentation + */ + +// VPC Export Names +export const VPC_ID_EXPORT_NAME = 'WorkshopVPC'; +export const VPC_CIDR_EXPORT_NAME = 'WorkshopVPCCidr'; +export const VPC_PRIVATE_SUBNETS_EXPORT_NAME = 'WorkshopVPCPrivateSubnets'; +export const VPC_PUBLIC_SUBNETS_EXPORT_NAME = 'WorkshopVPCPublicSubnets'; +export const VPC_ISOLATED_SUBNETS_EXPORT_NAME = 'WorkshopVPCIsolatedSubnets'; +export const VPC_AVAILABILITY_ZONES_EXPORT_NAME = 'WorkshopVPCAvailabilityZones'; +export const VPC_PRIVATE_SUBNET_CIDRS_EXPORT_NAME = 'WorkshopVPCPrivateSubnetCidrs'; +export const VPC_PUBLIC_SUBNET_CIDRS_EXPORT_NAME = 'WorkshopVPCPublicSubnetCidrs'; +export const VPC_ISOLATED_SUBNET_CIDRS_EXPORT_NAME = 'WorkshopVPCIsolatedSubnetCidrs'; + +// SNS/SQS Export Names +export const SNS_TOPIC_ARN_EXPORT_NAME = 'WorkshopSNSTopicArn'; +export const SQS_QUEUE_ARN_EXPORT_NAME = 'WorkshopSQSQueueArn'; +export const SQS_QUEUE_URL_EXPORT_NAME = 'WorkshopSQSQueueUrl'; + +// ECS Export Names +export const ECS_CLUSTER_ARN_EXPORT_NAME = 'WorkshopECSClusterArn'; +export const ECS_CLUSTER_NAME_EXPORT_NAME = 'WorkshopECSClusterName'; +export const ECS_SECURITY_GROUP_ID_EXPORT_NAME = 'WorkshopECSSecurityGroupId'; + +// EKS Export Names +export const EKS_CLUSTER_ARN_EXPORT_NAME = 'WorkshopEKSClusterArn'; +export const EKS_CLUSTER_NAME_EXPORT_NAME = 'WorkshopEKSClusterName'; +export const EKS_SECURITY_GROUP_ID_EXPORT_NAME = 'WorkshopEKSSecurityGroupId'; +export const EKS_KUBECTL_ROLE_ARN_EXPORT_NAME = 'WorkshopEKSKubectlRoleArn'; +export const EKS_OPEN_ID_CONNECT_PROVIDER_ARN_EXPORT_NAME = 'WorkshopEKSOpenIdConnectProviderArn'; +export const EKS_KUBECTL_SECURITY_GROUP_ID_EXPORT_NAME = 'WorkshopEKSKubectlSecurityGroupId'; +export const EKS_KUBECTL_LAMBDA_ROLE_ARN_EXPORT_NAME = 'WorkshopEKSKubectlLambdaRoleArn'; + +// Aurora Database Export Names +export const AURORA_CLUSTER_ARN_EXPORT_NAME = 'WorkshopAuroraClusterArn'; +export const AURORA_CLUSTER_ENDPOINT_EXPORT_NAME = 'WorkshopAuroraClusterEndpoint'; +export const AURORA_SECURITY_GROUP_ID_EXPORT_NAME = 'WorkshopAuroraSecurityGroupId'; +export const AURORA_ADMIN_SECRET_ARN_EXPORT_NAME = 'WorkshopAuroraAdminSecretArn'; //pragma: allowlist secret + +// DynamoDB Export Names +export const DYNAMODB_TABLE_ARN_EXPORT_NAME = 'WorkshopDynamoDBTableArn'; +export const DYNAMODB_TABLE_NAME_EXPORT_NAME = 'WorkshopDynamoDBTableName'; + +// OpenSearch Serverless Export Names +export const OPENSEARCH_COLLECTION_ARN_EXPORT_NAME = 'WorkshopOpenSearchCollectionArn'; +export const OPENSEARCH_COLLECTION_ID_EXPORT_NAME = 'WorkshopOpenSearchCollectionId'; +export const OPENSEARCH_COLLECTION_ENDPOINT_EXPORT_NAME = 'WorkshopOpenSearchCollectionEndpoint'; + +// OpenSearch Application Export Names +export const OPENSEARCH_APPLICATION_ARN_EXPORT_NAME = 'WorkshopOpenSearchApplicationArn'; +export const OPENSEARCH_APPLICATION_ID_EXPORT_NAME = 'WorkshopOpenSearchApplicationId'; + +// OpenSearch Ingestion Pipeline Export Names +export const OPENSEARCH_PIPELINE_ARN_EXPORT_NAME = 'WorkshopOpenSearchPipelineArn'; +export const OPENSEARCH_PIPELINE_ENDPOINT_EXPORT_NAME = 'WorkshopOpenSearchPipelineEndpoint'; +export const OPENSEARCH_PIPELINE_ROLE_ARN_EXPORT_NAME = 'WorkshopOpenSearchPipelineRoleArn'; + +// VPC Endpoint Export Names +export const VPC_ENDPOINT_APIGATEWAY_ID_EXPORT_NAME = 'WorkshopVPCEndpointApiGatewayId'; +export const VPC_ENDPOINT_DYNAMODB_ID_EXPORT_NAME = 'WorkshopVPCEndpointDynamoDbId'; +export const VPC_ENDPOINT_LAMBDA_ID_EXPORT_NAME = 'WorkshopVPCEndpointLambdaId'; +export const VPC_ENDPOINT_SERVICEDISCOVERY_ID_EXPORT_NAME = 'WorkshopVPCEndpointServiceDiscoveryId'; +export const VPC_ENDPOINT_DATA_SERVICEDISCOVERY_ID_EXPORT_NAME = 'WorkshopVPCEndpointDataServiceDiscoveryId'; + +// CloudMap Export Names +export const CLOUDMAP_NAMESPACE_ID_EXPORT_NAME = 'WorkshopCloudMapNamespaceId'; +export const CLOUDMAP_NAMESPACE_NAME_EXPORT_NAME = 'WorkshopCloudMapNamespaceName'; +export const CLOUDMAP_NAMESPACE_ARN_EXPORT_NAME = 'WorkshopCloudMapNamespaceArn'; + +// Assets Export Names +export const ASSETS_BUCKET_NAME_EXPORT_NAME = 'WorkshopAssetsBucketName'; +export const ASSETS_BUCKET_ARN_EXPORT_NAME = 'WorkshopAssetsBucketArn'; + +// EventBridge Export Names +export const EVENTBUS_ARN_EXPORT_NAME = 'WorkshopEventBusArn'; +export const EVENTBUS_NAME_EXPORT_NAME = 'WorkshopEventBusName'; diff --git a/src/cdk/bin/environment.ts b/src/cdk/bin/environment.ts new file mode 100644 index 00000000..511a021f --- /dev/null +++ b/src/cdk/bin/environment.ts @@ -0,0 +1,249 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +/** + * Environment configuration module for the One Observability Workshop. + * + * This module defines environment-specific constants and configuration values + * used throughout the CDK application for consistent deployment across different + * AWS environments and regions. + * + * @packageDocumentation + */ + +/** + * Environment configuration and defaults for the One Observability Workshop. + * + * This module provides configuration constants that can be overridden via environment + * variables or CDK context. It loads environment variables from a .env file and + * provides sensible defaults for the workshop deployment. + * + * @packageDocumentation + */ + +import { Runtime } from 'aws-cdk-lib/aws-lambda'; +import { Runtime as CanaryRuntime } from 'aws-cdk-lib/aws-synthetics'; +import { RetentionDays } from 'aws-cdk-lib/aws-logs'; +import { AuroraPostgresEngineVersion } from 'aws-cdk-lib/aws-rds'; +import * as dotenv from 'dotenv'; +import { MicroserviceApplicationPlacement } from '../lib/stages/applications'; +import { WorkshopLambdaFunctionProperties } from '../lib/constructs/lambda'; + +/** + * Host type enumeration for microservice deployment. + * Defines where microservices can be deployed. + */ +export enum HostType { + /** Amazon Elastic Container Service */ + ECS = 'ECS', + /** Amazon Elastic Kubernetes Service */ + EKS = 'EKS', +} + +/** + * Compute type enumeration for container workloads. + * Defines the compute platform for running containers. + */ +export enum ComputeType { + /** Amazon EC2 instances */ + EC2 = 'EC2', + /** AWS Fargate serverless compute */ + Fargate = 'Fargate', +} + +// Load environment variables from .env file +dotenv.config(); + +/** AWS Account ID from environment variable */ +export const ACCOUNT_ID = process.env.AWS_ACCOUNT_ID; + +/** AWS Region from environment variable */ +export const REGION = process.env.AWS_REGION; + +/** S3 bucket name for configuration storage */ +export const CONFIG_BUCKET = process.env.CONFIG_BUCKET; + +/** GitHub organization name, defaults to 'aws-samples' */ +export const ORGANIZATION_NAME = process.env.ORGANIZATION_NAME || 'aws-samples'; + +/** Repository name, defaults to 'one-observability-demo' */ +export const REPOSITORY_NAME = process.env.REPOSITORY_NAME || 'one-observability-demo'; + +/** Git branch name for the pipeline source */ +export const BRANCH_NAME = process.env.BRANCH_NAME || 'feat/cdkpipeline'; + +/** Working directory for CDK operations */ +export const WORKING_FOLDER = process.env.WORKING_FOLDER || 'src/cdk'; + +/** Default tags applied to all resources */ +export const TAGS = { + environment: 'non-prod', + application: 'One Observability Workshop', +}; + +/** Default retention period for logs */ +export const DEFAULT_RETENTION_DAYS = RetentionDays.ONE_WEEK; // TODO: Find a way to parametrize this + +/** Core infrastructure properties for the workshop */ +export const CORE_PROPERTIES = { + /** Whether to create a new VPC or use existing one */ + createVpc: process.env.CREATE_VPC == 'false' || true, + /** CIDR range for the VPC */ + vpcCider: process.env.VPC_CIDR || '10.0.0.0/16', + /** Existing VPC ID to use instead of creating new one */ + vpcId: process.env.VPC_ID || undefined, + /** Create CloudTrail and Cloudwatch logs for events */ + createCloudTrail: process.env.CREATE_CLOUDTRAIL == 'false' || true, + /** Default retention for logs in the core components */ + defaultRetentionDays: DEFAULT_RETENTION_DAYS, +}; + +/** Microservices definitions */ +/** Microservices definitions for the pet adoption application */ + +/** Pay for Adoption microservice configuration (Go implementation) */ +export const PAYFORADOPTION_GO = { + name: 'payforadoption-go', + dockerFilePath: 'src/applications/microservices/payforadoption-go', + hostType: HostType.ECS, + computeType: ComputeType.Fargate, + disableService: false, +}; + +/** Pet List Adoptions microservice configuration (Go implementation) */ +export const PETLISTADOPTIONS_PY = { + name: 'petlistadoption-py', + dockerFilePath: 'src/applications/microservices/petlistadoptions-py', + hostType: HostType.ECS, + computeType: ComputeType.Fargate, + disableService: false, +}; + +/** Pet Search microservice configuration (Java implementation) */ +export const PETSEARCH_JAVA = { + name: 'petsearch-java', + dockerFilePath: 'src/applications/microservices/petsearch-java', + hostType: HostType.ECS, + computeType: ComputeType.Fargate, + disableService: false, +}; + +/** Pet Site frontend application configuration (deployed on EKS) */ +export const PETSITE_NET = { + name: 'petsite-net', + dockerFilePath: 'src/applications/microservices/petsite-net/petsite', + hostType: HostType.EKS, + computeType: ComputeType.Fargate, + disableService: false, + manifestPath: 'lib/microservices/manifests/petsite-deployment.yaml', +}; + +/** Pet Status Updater microservice configuration */ +export const PETFOOD_RS = { + name: 'petfood-rs', + dockerFilePath: 'src/applications/microservices/petfood-rs', + hostType: HostType.ECS, + computeType: ComputeType.Fargate, + disableService: false, +}; + +/** Complete list of all microservice applications */ +export const APPLICATION_LIST = [PAYFORADOPTION_GO, PETLISTADOPTIONS_PY, PETSEARCH_JAVA, PETSITE_NET, PETFOOD_RS]; + +/** Map of microservice names to their deployment configurations */ +export const MICROSERVICES_PLACEMENT = new Map([ + [PAYFORADOPTION_GO.name, PAYFORADOPTION_GO], + [PETLISTADOPTIONS_PY.name, PETLISTADOPTIONS_PY], + [PETSEARCH_JAVA.name, PETSEARCH_JAVA], + [PETSITE_NET.name, PETSITE_NET], + [PETFOOD_RS.name, PETFOOD_RS], +]); + +/** Paths to pet image assets for seeding the application */ +export const PET_IMAGES = [ + '../../static/images/bunnies.zip', + '../../static/images/kitten.zip', + '../../static/images/puppies.zip', +]; + +/** Prefix for AWS Systems Manager Parameter Store parameters */ +export const PARAMETER_STORE_PREFIX = '/petstore'; + +/** Lambda function configuration for pet status updater */ +export const STATUS_UPDATER_FUNCTION = { + name: 'petupdater-node', + runtime: Runtime.NODEJS_22_X, + depsLockFilePath: '../applications/lambda/petstatusupdater-node/package-lock.json', + entry: '../applications/lambda/petstatusupdater-node/index.js', + memorySize: 128, + handler: 'handler', + enableSchedule: false, +}; + +export const TRAFFIC_GENERATOR_FUNCTION = { + name: 'traffic-generator-node', + runtime: Runtime.NODEJS_22_X, + depsLockFilePath: '../applications/lambda/traffic-generator-node/package-lock.json', + entry: '../applications/lambda/traffic-generator-node/index.js', + memorySize: 128, + handler: 'handler', + scheduleExpression: 'rate(1 minute)', + enableSchedule: true, +}; + +export const PETFOOD_IMAGE_GENERATOR_FUNCTION = { + name: 'petfood-image-generator-python', + runtime: Runtime.PYTHON_3_13, + entry: '../applications/lambda/petfood-image-generator-python', + index: 'lambda_function.py', + memorySize: 128, + handler: 'lambda_handler', + enableSchedule: false, +}; + +export const PETFOOD_CLEANUP_PROCESSOR_FUNCTION = { + name: 'petfood-cleanup-processor-node', + runtime: Runtime.NODEJS_22_X, + depsLockFilePath: '../applications/lambda/petfood-cleanup-processor-node/package-lock.json', + entry: '../applications/lambda/petfood-cleanup-processor-node/index.js', + memorySize: 128, + handler: 'handler', + enableSchedule: false, +}; + +/** Map of Lambda function names to their configurations */ +export const LAMBDA_FUNCTIONS = new Map([ + [STATUS_UPDATER_FUNCTION.name, STATUS_UPDATER_FUNCTION], + [TRAFFIC_GENERATOR_FUNCTION.name, TRAFFIC_GENERATOR_FUNCTION], + [PETFOOD_CLEANUP_PROCESSOR_FUNCTION.name, PETFOOD_CLEANUP_PROCESSOR_FUNCTION], + [PETFOOD_IMAGE_GENERATOR_FUNCTION.name, PETFOOD_IMAGE_GENERATOR_FUNCTION], +]); + +export const PETSITE_CANARY = { + name: 'petsite-canary', + runtime: CanaryRuntime.SYNTHETICS_NODEJS_PUPPETEER_9_1, + scheduleExpression: 'rate(1 minute)', + handler: 'index.handler', + path: '../applications/canaries/petsite-canary', +}; + +export const HOUSEKEEPING_CANARY = { + name: 'housekeeping-canary', + runtime: CanaryRuntime.SYNTHETICS_NODEJS_PUPPETEER_9_1, + scheduleExpression: 'rate(30 minutes)', + handler: 'index.handler', + path: '../applications/canaries/housekeeping', +}; + +export const CANARY_FUNCTIONS = new Map([ + [PETSITE_CANARY.name, PETSITE_CANARY], + [HOUSEKEEPING_CANARY.name, HOUSEKEEPING_CANARY], +]); + +/** Maximum number of Availability Zones to use for high availability */ +export const MAX_AVAILABILITY_ZONES = 2; + +/** Aurora PostgreSQL engine version for the workshop database */ +export const AURORA_POSTGRES_VERSION = AuroraPostgresEngineVersion.VER_16_8; diff --git a/src/cdk/bin/local.ts b/src/cdk/bin/local.ts new file mode 100644 index 00000000..f9f34ba4 --- /dev/null +++ b/src/cdk/bin/local.ts @@ -0,0 +1,109 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +/** + * Local development deployment entry point for the One Observability Workshop. + * + * This file creates individual CDK stacks for local development and testing, + * bypassing the pipeline deployment model. It's useful for rapid development + * and debugging of individual components. + * + * The stacks deployed include: + * - Core infrastructure (VPC, security groups, etc.) + * - Container applications (ECS/EKS services) + * - Storage services (S3, Aurora, DynamoDB) + * - Compute services (Lambda functions, EC2) + * - Microservices (Pet store application components) + * + * @packageDocumentation + */ + +import { App, Aspects } from 'aws-cdk-lib'; +import { CoreStack } from '../lib/stages/core'; +import { + APPLICATION_LIST, + AURORA_POSTGRES_VERSION, + CANARY_FUNCTIONS, + CORE_PROPERTIES, + LAMBDA_FUNCTIONS, + MICROSERVICES_PLACEMENT, + PET_IMAGES, + TAGS, +} from './environment'; +import { ContainersStack } from '../lib/stages/containers'; +import { AwsSolutionsChecks } from 'cdk-nag'; +import { StorageStack } from '../lib/stages/storage'; +import { ComputeStack } from '../lib/stages/compute'; +import { MicroservicesStack } from '../lib/stages/applications'; +import { Utilities } from '../lib/utils/utilities'; + +/** CDK Application instance for local deployment */ +const app = new App(); + +/** Deploy core infrastructure stack with networking and security components */ +const core = new CoreStack(app, 'DevCoreStack', { + ...CORE_PROPERTIES, + tags: TAGS, +}); + +/** Validate required environment variables */ +const s3BucketName = process.env.CONFIG_BUCKET; +if (!s3BucketName) { + throw new Error('CONFIG_BUCKET environment variable is not set'); +} + +const branch_name = process.env.BRANCH_NAME; +if (!branch_name) { + throw new Error('BRANCH_NAME environment variable is not set'); +} + +/** Deploy container applications stack with ECS and EKS services */ +new ContainersStack(app, 'DevApplicationsStack', { + source: { + bucketName: s3BucketName, + bucketKey: `repo/refs/heads/${branch_name}/repo.zip`, + }, + tags: TAGS, + applicationList: APPLICATION_LIST, +}); + +/** Deploy storage stack with S3, Aurora, and DynamoDB */ +new StorageStack(app, 'DevStorageStack', { + tags: TAGS, + assetsProperties: { + seedPaths: PET_IMAGES, + }, + auroraDatabaseProperties: { + engineVersion: AURORA_POSTGRES_VERSION, + }, +}).addDependency(core, 'Network is needed'); + +/** Deploy compute stack with Lambda functions and EC2 resources */ +const compute = new ComputeStack(app, 'DevComputeStack', { + tags: TAGS, +}); + +compute.addDependency(core, 'Network is needed'); + +/** Deploy microservices stack with pet store application components */ +const microservices = new MicroservicesStack(app, 'DevMicroservicesStack', { + tags: TAGS, + microservicesPlacement: MICROSERVICES_PLACEMENT, + lambdaFunctions: LAMBDA_FUNCTIONS, + env: { + account: process.env.AWS_ACCOUNT_ID, + region: process.env.AWS_REGION, + }, + canaries: CANARY_FUNCTIONS, +}); +microservices.addDependency(compute, 'Need to know where to run'); + +/** Tag all resources to indicate local deployment */ +Utilities.TagConstruct(app, { + LocalDeployment: 'true', +}); + +/** Add CDK-nag compliance checks for AWS Solutions best practices */ +Aspects.of(app).add(new AwsSolutionsChecks({ verbose: true })); diff --git a/src/cdk/bin/workshop.ts b/src/cdk/bin/workshop.ts new file mode 100644 index 00000000..7dc12b81 --- /dev/null +++ b/src/cdk/bin/workshop.ts @@ -0,0 +1,81 @@ +#!/usr/bin/env node + +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +/** + * Entry point for the One Observability Workshop CDK application. + * + * This file creates the main CDK app and instantiates the pipeline stack + * with configuration from environment variables and CDK context. + * The pipeline deploys the complete workshop infrastructure including: + * - Core networking and security components + * - Container orchestration platforms (ECS/EKS) + * - Storage services (S3, Aurora, DynamoDB) + * - Compute services (Lambda, EC2) + * - Sample microservices for the pet adoption application + * + * Configuration is resolved from CDK context first, then falls back to + * environment variables for flexibility in different deployment scenarios. + * + * @packageDocumentation + */ + +import { App, Aspects } from 'aws-cdk-lib'; +import { CDKPipeline } from '../lib/pipeline'; +import { AwsSolutionsChecks } from 'cdk-nag'; +import { Utilities } from '../lib/utils/utilities'; +import { + CONFIG_BUCKET, + REGION, + ACCOUNT_ID, + BRANCH_NAME, + ORGANIZATION_NAME, + REPOSITORY_NAME, + WORKING_FOLDER, + TAGS, + CORE_PROPERTIES, + DEFAULT_RETENTION_DAYS, + APPLICATION_LIST, + PET_IMAGES, + MICROSERVICES_PLACEMENT, + LAMBDA_FUNCTIONS, + CANARY_FUNCTIONS, +} from './environment'; + +/** Main CDK application instance */ +const app = new App(); + +/** + * Create the main pipeline stack for the One Observability Workshop. + * Configuration values are resolved from CDK context first, then fall back to environment variables. + */ +new CDKPipeline(app, 'OneObservability', { + configBucketName: app.node.tryGetContext('configBucketName') || CONFIG_BUCKET, + branchName: app.node.tryGetContext('branchName') || BRANCH_NAME, + organizationName: app.node.tryGetContext('organizationName') || ORGANIZATION_NAME, + repositoryName: app.node.tryGetContext('repositoryName') || REPOSITORY_NAME, + workingFolder: app.node.tryGetContext('workingFolder') || WORKING_FOLDER, + env: { + account: ACCOUNT_ID, + region: REGION, + }, + tags: TAGS, + coreStageProperties: CORE_PROPERTIES, + defaultRetentionPeriod: DEFAULT_RETENTION_DAYS, + applicationList: APPLICATION_LIST, + petImagesPaths: PET_IMAGES, + microservicesProperties: { + microservicesPlacement: MICROSERVICES_PLACEMENT, + lambdaFunctions: LAMBDA_FUNCTIONS, + canaries: CANARY_FUNCTIONS, + }, +}); + +/** Apply tags to all resources in the application */ +Utilities.TagConstruct(app, TAGS); + +/** Add CDK-nag compliance checks for AWS Solutions best practices */ +Aspects.of(app).add(new AwsSolutionsChecks({ verbose: true })); diff --git a/src/cdk/cdk.json b/src/cdk/cdk.json new file mode 100644 index 00000000..f163b825 --- /dev/null +++ b/src/cdk/cdk.json @@ -0,0 +1,94 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/workshop.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-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-kms:applyImportedAliasPermissionsToPrincipal": 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, + "@aws-cdk/aws-s3:keepNotificationInImportedBucket": false, + "@aws-cdk/core:explicitStackTags": true, + "@aws-cdk/aws-ecs:enableImdsBlockingDeprecatedFeature": false, + "@aws-cdk/aws-ecs:disableEcsImdsBlocking": true, + "@aws-cdk/aws-ecs:reduceEc2FargateCloudWatchPermissions": true, + "@aws-cdk/aws-dynamodb:resourcePolicyPerReplica": true, + "@aws-cdk/aws-ec2:ec2SumTImeoutEnabled": true, + "@aws-cdk/aws-appsync:appSyncGraphQLAPIScopeLambdaPermission": true, + "@aws-cdk/aws-rds:setCorrectValueForDatabaseInstanceReadReplicaInstanceResourceId": true, + "@aws-cdk/core:cfnIncludeRejectComplexResourceUpdateCreatePolicyIntrinsics": true, + "@aws-cdk/aws-lambda-nodejs:sdkV3ExcludeSmithyPackages": true, + "@aws-cdk/aws-stepfunctions-tasks:fixRunEcsTaskPolicy": true, + "@aws-cdk/aws-ec2:bastionHostUseAmazonLinux2023ByDefault": true, + "@aws-cdk/aws-route53-targets:userPoolDomainNameMethodWithoutCustomResource": true, + "@aws-cdk/aws-elasticloadbalancingV2:albDualstackWithoutPublicIpv4SecurityGroupRulesDefault": true, + "@aws-cdk/aws-iam:oidcRejectUnauthorizedConnections": true, + "@aws-cdk/core:enableAdditionalMetadataCollection": true, + "@aws-cdk/aws-lambda:createNewPoliciesWithAddToRolePolicy": false, + "@aws-cdk/aws-s3:setUniqueReplicationRoleName": true, + "@aws-cdk/aws-events:requireEventBusPolicySid": true, + "@aws-cdk/core:aspectPrioritiesMutating": true, + "@aws-cdk/aws-dynamodb:retainTableReplica": true, + "@aws-cdk/aws-stepfunctions:useDistributedMapResultWriterV2": true, + "@aws-cdk/s3-notifications:addS3TrustKeyPolicyForSnsSubscriptions": true, + "@aws-cdk/aws-ec2:requirePrivateSubnetsForEgressOnlyInternetGateway": true, + "@aws-cdk/aws-s3:publicAccessBlockedByDefault": true, + "@aws-cdk/aws-lambda:useCdkManagedLogGroup": true + } +} diff --git a/src/cdk/index.ts b/src/cdk/index.ts new file mode 100644 index 00000000..2120b34e --- /dev/null +++ b/src/cdk/index.ts @@ -0,0 +1,18 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +/** + * One Observability Workshop CDK Library. + * + * This module exports the main constructs and utilities for deploying + * the One Observability Workshop infrastructure on AWS. + * + * @packageDocumentation + */ + +export * from './lib/pipeline'; +export * from './lib/utils/utilities'; +export * from './lib/constructs/network'; +export * from './lib/constructs/cloudtrail'; diff --git a/PetAdoptions/cdk/.gitkeep b/src/cdk/lib/constructs/.gitkeep similarity index 100% rename from PetAdoptions/cdk/.gitkeep rename to src/cdk/lib/constructs/.gitkeep diff --git a/src/cdk/lib/constructs/assets.ts b/src/cdk/lib/constructs/assets.ts new file mode 100644 index 00000000..5a2915a8 --- /dev/null +++ b/src/cdk/lib/constructs/assets.ts @@ -0,0 +1,154 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +import { RemovalPolicy, Stack, CfnOutput, Fn } from 'aws-cdk-lib'; +import { Bucket, IBucket } from 'aws-cdk-lib/aws-s3'; +import { Construct } from 'constructs'; +import { BucketDeployment, Source } from 'aws-cdk-lib/aws-s3-deployment'; +import { NagSuppressions } from 'cdk-nag'; +import { Utilities } from '../utils/utilities'; +import { PARAMETER_STORE_PREFIX } from '../../bin/environment'; +import { ASSETS_BUCKET_NAME_EXPORT_NAME, ASSETS_BUCKET_ARN_EXPORT_NAME } from '../../bin/constants'; + +/** + * Properties for configuring Assets construct + * @interface AssetsProperties + */ +export interface AssetsProperties { + /** + * Array of local file paths to deploy to the S3 bucket as seed data + * Files will be deployed to the 'petimages' prefix in the bucket + * @optional + */ + seedPaths?: string[]; +} + +/** + * AWS CDK Construct that creates S3 bucket for pet adoption assets with optional seed data deployment + * @class Assets + * @extends Construct + */ +export class WorkshopAssets extends Construct { + /** + * The S3 bucket for storing pet adoption assets + * @public + */ + public bucket: Bucket; + + /** + * Creates a new Assets construct with S3 bucket and optional seed data deployment + * If seedPaths are provided, deploys local assets to the bucket under 'petimages' prefix + * @param scope - The parent construct + * @param id - The construct ID + * @param properties - Configuration properties for the construct + */ + constructor(scope: Construct, id: string, properties?: AssetsProperties) { + super(scope, id); + + this.bucket = new Bucket(this, 'petadoptionBucket', { + publicReadAccess: false, + autoDeleteObjects: true, + removalPolicy: RemovalPolicy.DESTROY, + enforceSSL: true, + }); + + NagSuppressions.addResourceSuppressions(this.bucket, [ + { + id: 'AwsSolutions-S1', + reason: 'Bucket doesn not need server access logs', + }, + ]); + + if (properties?.seedPaths) { + const sources = properties.seedPaths.map((path: string) => { + return Source.asset(path); + }); + + const deployment = new BucketDeployment(this, 'petimagesdeployment', { + sources: sources, + destinationBucket: this.bucket, + retainOnDelete: false, + }); + + NagSuppressions.addResourceSuppressions( + deployment.handlerRole, + [ + { + id: 'AwsSolutions-IAM4', + reason: 'AWS Managed policies is acceptable for the cleanup lambda', + }, + { + id: 'AwsSolutions-IAM5', + reason: 'Star resource needed to cleanup the bucket', + }, + ], + true, + ); + + // TODO: Refine the suppression if possible + NagSuppressions.addStackSuppressions(Stack.of(this), [ + { + id: 'AwsSolutions-L1', + reason: 'The construct manages the lambda runtime version', + }, + ]); + } + + // Create CloudFormation outputs for Assets resources + this.createAssetsOutputs(); + } + + /** + * Creates CloudFormation outputs for the S3 bucket resources + */ + private createAssetsOutputs(): void { + new CfnOutput(this, 'AssetsBucketNameOutput', { + value: this.bucket.bucketName, + exportName: ASSETS_BUCKET_NAME_EXPORT_NAME, + description: 'Workshop Assets S3 Bucket Name', + }); + + new CfnOutput(this, 'AssetsBucketArnOutput', { + value: this.bucket.bucketArn, + exportName: ASSETS_BUCKET_ARN_EXPORT_NAME, + description: 'Workshop Assets S3 Bucket ARN', + }); + + Utilities.createSsmParameters( + this, + PARAMETER_STORE_PREFIX, + new Map( + Object.entries({ + s3bucketname: this.bucket.bucketName, + }), + ), + ); + } + + /** + * Imports an S3 bucket from CloudFormation exports created by WorkshopAssets + * + * This static method reconstructs a bucket instance from CloudFormation exports, + * allowing other stacks to reference and use the bucket created by the core infrastructure. + * + * @param scope - The construct scope where the bucket will be imported + * @param id - The construct identifier for the imported bucket + * @returns The imported bucket instance + * + * @example + * ```typescript + * const bucket = WorkshopAssets.importBucketFromExports(this, 'ImportedBucket'); + * // Use bucket.bucketName, bucket.bucketArn, etc. + * ``` + */ + public static importBucketFromExports(scope: Construct, id: string): IBucket { + const bucketName = Fn.importValue(ASSETS_BUCKET_NAME_EXPORT_NAME); + const bucketArn = Fn.importValue(ASSETS_BUCKET_ARN_EXPORT_NAME); + + return Bucket.fromBucketAttributes(scope, id, { + bucketName: bucketName, + bucketArn: bucketArn, + }); + } +} diff --git a/src/cdk/lib/constructs/canary.ts b/src/cdk/lib/constructs/canary.ts new file mode 100644 index 00000000..43d4105c --- /dev/null +++ b/src/cdk/lib/constructs/canary.ts @@ -0,0 +1,122 @@ +import { Duration, Stack } from 'aws-cdk-lib'; +import { Effect, ManagedPolicy, Policy, PolicyStatement } from 'aws-cdk-lib/aws-iam'; +import { RetentionDays } from 'aws-cdk-lib/aws-logs'; +import { IBucket } from 'aws-cdk-lib/aws-s3'; +import { Canary, Code, ResourceToReplicateTags, Runtime, Schedule, Test } from 'aws-cdk-lib/aws-synthetics'; +import { Construct } from 'constructs'; +import { HOUSEKEEPING_CANARY, PETSITE_CANARY } from '../../bin/environment'; +import { NagSuppressions } from 'cdk-nag'; + +export interface WorkshopCanaryProperties { + artifactsBucket?: IBucket; + runtime: Runtime; + scheduleExpression?: string; + handler: string; + path: string; + logRetentionDays?: RetentionDays; + name: string; +} + +export const CanaryNames = { + /** Pet status updater function name */ + Petsite: PETSITE_CANARY.name, + HouseKeeping: HOUSEKEEPING_CANARY.name, +} as const; + +export abstract class WorkshopCanary extends Construct { + public canary: Canary; + constructor(scope: Construct, id: string, properties: WorkshopCanaryProperties) { + super(scope, id); + + this.canary = new Canary(this, `canary-${id}`, { + canaryName: properties.name, + runtime: properties.runtime, + schedule: Schedule.expression(properties.scheduleExpression || 'rate(5 minutes)'), + test: Test.custom({ + handler: properties.handler, + code: Code.fromAsset(properties.path), + }), + activeTracing: true, + artifactsBucketLocation: properties.artifactsBucket + ? { + bucket: properties.artifactsBucket, + prefix: `canary-${id}`, + } + : undefined, + environmentVariables: this.getEnvironmentVariables(properties), + provisionedResourceCleanup: true, + resourcesToReplicateTags: [ResourceToReplicateTags.LAMBDA_FUNCTION], + artifactsBucketLifecycleRules: [ + { + expiration: Duration.days(properties.logRetentionDays?.valueOf() || 30), + }, + ], + timeToLive: Duration.minutes(5), + }); + + const parameterStorePolicy = new Policy(this, `${id}-paramterstore-policy`, { + statements: [WorkshopCanary.getDefaultSSMPolicy(this, '/petstore/')], + roles: [this.canary.role], + }); + + this.canary.role.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName('AWSXRayDaemonWriteAccess')); + + NagSuppressions.addResourceSuppressions( + this.canary.role, + [ + { + id: 'AwsSolutions-IAM5', + reason: 'Suppress wildcard permissions created by the Canary Construct', + }, + { + id: 'AwsSolutions-IAM4', + reason: 'XRay managed polices are acceptable', + }, + ], + true, + ); + NagSuppressions.addResourceSuppressions( + parameterStorePolicy, + [ + { + id: 'AwsSolutions-IAM5', + reason: 'This allows the canary to read parameters for the application and perform multiple actions', + appliesTo: [ + `Resource::arn:aws:ssm:${Stack.of(this).region}:${Stack.of(this).account}:parameter/petstore/*`, + ], + }, + ], + true, + ); + } + + public static getDefaultSSMPolicy(scope: Construct, prefix?: string) { + const cleanPrefix = (prefix || '/petstore/').startsWith('/') + ? (prefix || '/petstore/').slice(1) + : prefix || '/petstore/'; + const readSMParametersPolicy = new PolicyStatement({ + effect: Effect.ALLOW, + actions: ['ssm:GetParametersByPath', 'ssm:GetParameters', 'ssm:GetParameter'], + resources: [`arn:aws:ssm:${Stack.of(scope).region}:${Stack.of(scope).account}:parameter/${cleanPrefix}*`], + }); + + return readSMParametersPolicy; + } + + /** + * Creates CloudFormation outputs for the Lambda function. + * Must be implemented by concrete subclasses. + * + * @param properties - Function configuration properties + */ + abstract createOutputs(properties: WorkshopCanaryProperties): void; + + /** + * Returns environment variables for the Lambda function. + * Must be implemented by concrete subclasses. + * + * @param properties - Function configuration properties + * @returns Map of environment variable names to values + */ + abstract getEnvironmentVariables(properties: WorkshopCanaryProperties): { [key: string]: string } | undefined; +} diff --git a/src/cdk/lib/constructs/cloudtrail.ts b/src/cdk/lib/constructs/cloudtrail.ts new file mode 100644 index 00000000..b871e957 --- /dev/null +++ b/src/cdk/lib/constructs/cloudtrail.ts @@ -0,0 +1,109 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +/** + * CloudTrail construct for the One Observability Workshop. + * + * This module provides a CloudTrail trail with CloudWatch logs integration + * and anomaly detection capabilities for monitoring AWS API activity. + * + * @packageDocumentation + */ + +import { Construct } from 'constructs'; +import { Trail, InsightType } from 'aws-cdk-lib/aws-cloudtrail'; +import { LogGroup, RetentionDays } from 'aws-cdk-lib/aws-logs'; +import { Role, ServicePrincipal, PolicyStatement, PolicyDocument } from 'aws-cdk-lib/aws-iam'; +import { Names, RemovalPolicy } from 'aws-cdk-lib'; +import { NagSuppressions } from 'cdk-nag'; + +/** + * Configuration properties for the WorkshopCloudTrail construct. + */ +export interface WorkshopCloudTrailProperties { + /** Name identifier for the CloudTrail resources */ + name: string; + /** Whether to include S3 data events in the trail */ + includeS3DataEvents?: boolean; + /** Whether to include Lambda events in the trail */ + includeLambdaEvents?: boolean; + /** CloudWatch log retention period in days */ + logRetentionDays?: RetentionDays; +} + +/** + * A CDK construct that creates a CloudTrail trail with CloudWatch logs + * integration and anomaly detection for the observability workshop. + */ +export class WorkshopCloudTrail extends Construct { + /** The CloudTrail trail instance */ + public readonly trail: Trail; + /** The CloudWatch log group for trail events */ + public readonly logGroup: LogGroup; + + /** + * Creates a new WorkshopCloudTrail construct. + * + * @param scope - The parent construct + * @param id - The construct identifier + * @param properties - Configuration properties for the CloudTrail + */ + constructor(scope: Construct, id: string, properties: WorkshopCloudTrailProperties) { + super(scope, id); + + const logName = Names.uniqueResourceName(this, {}); + // Create CloudWatch log group for CloudTrail + this.logGroup = new LogGroup(this, 'CloudTrailLogGroup', { + retention: properties.logRetentionDays || RetentionDays.ONE_WEEK, + logGroupName: `/aws/cloudtrail/${logName}`, + removalPolicy: RemovalPolicy.DESTROY, + }); + + // Create IAM role for CloudTrail to write to CloudWatch Logs + new Role(this, 'CloudTrailRole', { + assumedBy: new ServicePrincipal('cloudtrail.amazonaws.com'), + inlinePolicies: { + CloudWatchLogsPolicy: new PolicyDocument({ + statements: [ + new PolicyStatement({ + actions: ['logs:CreateLogStream', 'logs:PutLogEvents'], + resources: [this.logGroup.logGroupArn], + }), + ], + }), + }, + }); + + // Create CloudTrail trail + this.trail = new Trail(this, 'Trail', { + trailName: properties.name, + cloudWatchLogGroup: this.logGroup, + includeGlobalServiceEvents: true, + isMultiRegionTrail: false, + enableFileValidation: true, + sendToCloudWatchLogs: true, + insightTypes: [InsightType.API_CALL_RATE, InsightType.API_ERROR_RATE], + }); + + if (properties.includeS3DataEvents) { + this.trail.logAllS3DataEvents(); + } + + if (properties.includeLambdaEvents) { + this.trail.logAllLambdaDataEvents(); + } + + NagSuppressions.addResourceSuppressions( + this.trail, + [ + { + id: 'AwsSolutions-S1', + reason: 'CloudTrail Bucket, access logs are not required for the workshop', + }, + ], + true, + ); + } +} diff --git a/src/cdk/lib/constructs/cloudwatch.ts b/src/cdk/lib/constructs/cloudwatch.ts new file mode 100644 index 00000000..d2eb7600 --- /dev/null +++ b/src/cdk/lib/constructs/cloudwatch.ts @@ -0,0 +1,85 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +/** + * CloudWatch construct for the One Observability Workshop. + * + * This module provides CloudWatch settings configuration. + * + * @packageDocumentation + */ + +import { Construct } from 'constructs'; +import { Stack } from 'aws-cdk-lib'; +import { CfnResourcePolicy } from 'aws-cdk-lib/aws-logs'; +import { CfnTransactionSearchConfig } from 'aws-cdk-lib/aws-xray'; + +/** + * Configuration properties for the CloudWatchTransactionSearch construct. + */ +export interface CloudWatchTransactionSearchProperties { + /** Indexing percentage for transaction search (0-100) */ + indexingPercentage?: number; +} + +/** + * A CDK construct that creates CloudWatch Transaction Search configuration + * with CloudWatch logs resource policy for the observability workshop. + */ +export class CloudWatchTransactionSearch extends Construct { + /** The CloudWatch resource policy for X-Ray access */ + public readonly resourcePolicy: CfnResourcePolicy; + /** The X-Ray transaction search configuration */ + public readonly transactionSearchConfig: CfnTransactionSearchConfig; + + /** + * Creates a new CloudWatch TransactionSearch construct. + * + * @param scope - The parent construct + * @param id - The construct identifier + * @param properties - Configuration properties for CloudWatch Transaction Search + */ + constructor(scope: Construct, id: string, properties?: CloudWatchTransactionSearchProperties) { + super(scope, id); + + const stack = Stack.of(this); + + // CloudWatch Transaction Search setup + this.resourcePolicy = new CfnResourcePolicy(this, 'TransactionSearchLogResourcePolicy', { + policyName: 'TransactionSearchAccess', + policyDocument: JSON.stringify({ + Version: '2012-10-17', + Statement: [ + { + Sid: 'TransactionSearchXRayAccess', + Effect: 'Allow', + Principal: { + Service: 'xray.amazonaws.com', + }, + Action: 'logs:PutLogEvents', + Resource: [ + `arn:${stack.partition}:logs:${stack.region}:${stack.account}:log-group:aws/spans:*`, + `arn:${stack.partition}:logs:${stack.region}:${stack.account}:log-group:/aws/application-signals/data:*`, + ], + Condition: { + ArnLike: { + 'aws:SourceArn': `arn:${stack.partition}:xray:${stack.region}:${stack.account}:*`, + }, + StringEquals: { + 'aws:SourceAccount': stack.account, + }, + }, + }, + ], + }), + }); + + this.transactionSearchConfig = new CfnTransactionSearchConfig(this, 'TransactionSearchConfig', { + indexingPercentage: properties?.indexingPercentage || 1, + }); + + this.transactionSearchConfig.addDependency(this.resourcePolicy); + } +} \ No newline at end of file diff --git a/src/cdk/lib/constructs/database.ts b/src/cdk/lib/constructs/database.ts new file mode 100644 index 00000000..64542ee5 --- /dev/null +++ b/src/cdk/lib/constructs/database.ts @@ -0,0 +1,217 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +import { CfnOutput, Duration, Fn, RemovalPolicy } from 'aws-cdk-lib'; +import { + InstanceClass, + InstanceSize, + InstanceType, + ISecurityGroup, + IVpc, + Peer, + Port, + SecurityGroup, +} from 'aws-cdk-lib/aws-ec2'; +import { RetentionDays } from 'aws-cdk-lib/aws-logs'; +import { + AuroraPostgresEngineVersion, + ClusterInstance, + DatabaseCluster, + DatabaseClusterEngine, + DatabaseInsightsMode, + IDatabaseCluster, + ParameterGroup, + PerformanceInsightRetention, +} from 'aws-cdk-lib/aws-rds'; +import { NagSuppressions } from 'cdk-nag'; +import { Construct } from 'constructs'; +import { + AURORA_CLUSTER_ARN_EXPORT_NAME, + AURORA_CLUSTER_ENDPOINT_EXPORT_NAME, + AURORA_SECURITY_GROUP_ID_EXPORT_NAME, + AURORA_ADMIN_SECRET_ARN_EXPORT_NAME, +} from '../../bin/constants'; +import { ISecret, Secret } from 'aws-cdk-lib/aws-secretsmanager'; +import { Utilities } from '../utils/utilities'; +import { PARAMETER_STORE_PREFIX } from '../../bin/environment'; + +const databaseName = 'adoptions'; + +/** + * Properties for configuring Aurora PostgreSQL database cluster + * @interface AuroraDBProperties + */ +export interface AuroraDBProperties { + /** RDS username for database authentication */ + rdsUsername?: string; + /** VPC where the database cluster will be deployed */ + vpc?: IVpc; + /** Aurora PostgreSQL engine version */ + engineVersion?: AuroraPostgresEngineVersion; + /** Parameter group for database configuration */ + parameterGroup?: ParameterGroup; + /** Default retention period for CloudWatch logs */ + defaultRetentionDays?: RetentionDays; +} + +/** + * AWS CDK Construct that creates Aurora PostgreSQL database cluster with security group + * @class AuroraDatabase + * @extends Construct + */ +export class AuroraDatabase extends Construct { + /** The Aurora PostgreSQL database cluster */ + public cluster: DatabaseCluster; + /** Security group for database access */ + public databaseSecurityGroup: SecurityGroup; + + /** + * Creates a new AuroraDatabase construct with serverless v2 configuration + * @param scope - The parent construct + * @param id - The construct ID + * @param properties - Configuration properties for the database cluster + */ + constructor(scope: Construct, id: string, properties: AuroraDBProperties) { + super(scope, id); + + if (!properties.vpc) { + throw new Error('VPC is required for Aurora PostgreSQL cluster'); + } + + this.databaseSecurityGroup = new SecurityGroup(this, 'dbSecurityGroup', { + vpc: properties.vpc, + description: 'Aurora Postgres Cluster Security Group', + }); + + /** Add ingress rules to Security Group for private Subnets */ + for (const subnet of properties.vpc.privateSubnets) { + this.databaseSecurityGroup.addIngressRule(Peer.ipv4(subnet.ipv4CidrBlock), Port.POSTGRES); + } + + this.cluster = new DatabaseCluster(this, 'Database', { + engine: DatabaseClusterEngine.auroraPostgres({ + version: properties.engineVersion || AuroraPostgresEngineVersion.VER_16_8, + }), + parameterGroup: + properties.parameterGroup || + ParameterGroup.fromParameterGroupName(this, 'ParameterGroup', 'default.aurora-postgresql16'), + vpc: properties.vpc, + securityGroups: [this.databaseSecurityGroup], + defaultDatabaseName: databaseName, + databaseInsightsMode: DatabaseInsightsMode.ADVANCED, + performanceInsightRetention: PerformanceInsightRetention.MONTHS_15, + writer: ClusterInstance.provisioned('writer', { + autoMinorVersionUpgrade: true, + instanceType: InstanceType.of(InstanceClass.T4G, InstanceSize.MEDIUM), + }), + readers: [ + ClusterInstance.provisioned('reader1', { + promotionTier: 1, + instanceType: InstanceType.of(InstanceClass.T4G, InstanceSize.MEDIUM), + autoMinorVersionUpgrade: true, + }), + ], + serverlessV2MaxCapacity: 1, + serverlessV2MinCapacity: 0.5, + iamAuthentication: true, + cloudwatchLogsExports: ['postgresql'], + cloudwatchLogsRetention: properties.defaultRetentionDays || RetentionDays.ONE_WEEK, + removalPolicy: RemovalPolicy.DESTROY, + vpcSubnets: { + subnetGroupName: 'Isolated', + }, + storageEncrypted: true, + backup: { + retention: Duration.days(1), // To minimize cost + }, + }); + + NagSuppressions.addResourceSuppressions(this.cluster, [ + { + id: 'AwsSolutions-RDS10', + reason: 'Demo purposes only', + }, + ]); + + NagSuppressions.addResourceSuppressions( + this.cluster, + [ + { + id: 'AwsSolutions-SMG4', + reason: 'Demo purposes only', + }, + { + id: 'AwsSolutions-IAM4', + reason: 'Log Retention lambda using managed policies is acceptable', + }, + ], + true, + ); + + this.createExports(); + this.createOutputs(); + } + + private createExports(): void { + new CfnOutput(this, 'ClusterArn', { + value: this.cluster.clusterArn, + exportName: AURORA_CLUSTER_ARN_EXPORT_NAME, + }); + + new CfnOutput(this, 'ClusterEndpoint', { + value: this.cluster.clusterEndpoint.hostname, + exportName: AURORA_CLUSTER_ENDPOINT_EXPORT_NAME, + }); + + new CfnOutput(this, 'SecurityGroupId', { + value: this.databaseSecurityGroup.securityGroupId, + exportName: AURORA_SECURITY_GROUP_ID_EXPORT_NAME, + }); + + new CfnOutput(this, 'AdminSecretArn', { + value: this.cluster.secret!.secretArn, + exportName: AURORA_ADMIN_SECRET_ARN_EXPORT_NAME, + }); + } + + public static importFromExports( + scope: Construct, + id: string, + ): { cluster: IDatabaseCluster; securityGroup: ISecurityGroup; adminSecret: ISecret } { + const clusterArn = Fn.importValue(AURORA_CLUSTER_ARN_EXPORT_NAME); + const clusterEndpoint = Fn.importValue(AURORA_CLUSTER_ENDPOINT_EXPORT_NAME); + const securityGroupId = Fn.importValue(AURORA_SECURITY_GROUP_ID_EXPORT_NAME); + const adminSecretArn = Fn.importValue(AURORA_ADMIN_SECRET_ARN_EXPORT_NAME); + + const cluster = DatabaseCluster.fromDatabaseClusterAttributes(scope, `${id}-Cluster`, { + clusterIdentifier: clusterArn.split(':')[6], + clusterEndpointAddress: clusterEndpoint, + }); + + const securityGroup = SecurityGroup.fromSecurityGroupId(scope, `${id}-SecurityGroup`, securityGroupId); + + const adminSecret = Secret.fromSecretCompleteArn(scope, `${id}-AdminSecret`, adminSecretArn); + + return { cluster, securityGroup, adminSecret }; + } + + createOutputs(): void { + if (this.cluster.secret) { + Utilities.createSsmParameters( + this, + PARAMETER_STORE_PREFIX, + new Map( + Object.entries({ + rdssecretarn: this.cluster.secret?.secretArn, + 'rds-reader-endpoint': this.cluster.clusterReadEndpoint.hostname, + 'rds-writer-endpoint': this.cluster.clusterEndpoint.hostname, + 'rds-database-name': databaseName, + }), + ), + ); + } else { + throw new Error('Cluster secret is not available'); + } + } +} diff --git a/src/cdk/lib/constructs/dynamodb.ts b/src/cdk/lib/constructs/dynamodb.ts new file mode 100644 index 00000000..9e0d412f --- /dev/null +++ b/src/cdk/lib/constructs/dynamodb.ts @@ -0,0 +1,215 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +import { CfnOutput, Fn, RemovalPolicy } from 'aws-cdk-lib'; +import { TreatMissingData, ComparisonOperator } from 'aws-cdk-lib/aws-cloudwatch'; +import { AttributeType, ITable, Table } from 'aws-cdk-lib/aws-dynamodb'; +import { NagSuppressions } from 'cdk-nag'; +import { Construct } from 'constructs'; +import { DYNAMODB_TABLE_ARN_EXPORT_NAME, DYNAMODB_TABLE_NAME_EXPORT_NAME } from '../../bin/constants'; +import { Utilities } from '../utils/utilities'; +import { PARAMETER_STORE_PREFIX } from '../../bin/environment'; + +/** + * Properties for configuring DynamoDatabase construct + * @interface DynamoDbProperties + */ +export interface DynamoDatabaseProperties { + /** + * Threshold value for CloudWatch alarms on throttle events + * @default 0 + */ + alarmThreshold?: number; + /** + * Number of evaluation periods for CloudWatch alarms + * @default 1 + */ + evaluationPeriods?: number; +} + +/** + * AWS CDK Construct that creates DynamoDatabase table with CloudWatch alarms for pet adoption + * @class DynamoDb + * @extends Construct + */ +export class DynamoDatabase extends Construct { + /** + * The DynamoDatabase table for storing pet adoption data + * @public + */ + public petAdoptionTable: Table; + public petFoodsTable: Table; + public petFoodsCartTable: Table; + + /** + * Creates a new DynamoDatabase construct with table and monitoring alarms + * Creates read and write throttle event alarms with configurable thresholds + * @param scope - The parent construct + * @param id - The construct ID + * @param properties - Configuration properties for the construct (required) + */ + constructor(scope: Construct, id: string, properties?: DynamoDatabaseProperties) { + super(scope, id); + + this.petAdoptionTable = new Table(this, 'ddbPetadoption', { + partitionKey: { + name: 'pettype', + type: AttributeType.STRING, + }, + sortKey: { + name: 'petid', + type: AttributeType.STRING, + }, + removalPolicy: RemovalPolicy.DESTROY, + }); + + this.petAdoptionTable + .metric('WriteThrottleEvents', { statistic: 'avg' }) + .createAlarm(this, 'WriteThrottleEvents-BasicAlarm', { + threshold: properties?.alarmThreshold || 0, + treatMissingData: TreatMissingData.NOT_BREACHING, + comparisonOperator: ComparisonOperator.GREATER_THAN_THRESHOLD, + evaluationPeriods: properties?.evaluationPeriods || 1, + alarmName: `${this.petAdoptionTable.tableName}-WriteThrottleEvents-BasicAlarm`, + }); + + this.petAdoptionTable + .metric('ReadThrottleEvents', { statistic: 'avg' }) + .createAlarm(this, 'ReadThrottleEvents-BasicAlarm', { + threshold: properties?.alarmThreshold || 0, + treatMissingData: TreatMissingData.NOT_BREACHING, + comparisonOperator: ComparisonOperator.GREATER_THAN_THRESHOLD, + evaluationPeriods: properties?.evaluationPeriods || 1, + alarmName: `${this.petAdoptionTable.tableName}-ReadThrottleEvents-BasicAlarm`, + }); + + this.petFoodsTable = new Table(this, 'ddbPetFoods', { + partitionKey: { + name: 'id', + type: AttributeType.STRING, + }, + removalPolicy: RemovalPolicy.DESTROY, + }); + + this.petFoodsTable.addGlobalSecondaryIndex({ + indexName: 'PetTypeIndex', + partitionKey: { + name: 'pet_type', + type: AttributeType.STRING, + }, + sortKey: { + name: 'name', + type: AttributeType.STRING, + }, + }); + + this.petFoodsTable.addGlobalSecondaryIndex({ + indexName: 'FoodTypeIndex', + partitionKey: { + name: 'food_type', + type: AttributeType.STRING, + }, + sortKey: { + name: 'price', + type: AttributeType.NUMBER, + }, + }); + + this.petFoodsCartTable = new Table(this, 'ddbPetFoodsCart', { + partitionKey: { + name: 'user_id', + type: AttributeType.STRING, + }, + removalPolicy: RemovalPolicy.DESTROY, + }); + + NagSuppressions.addResourceSuppressions( + [this.petAdoptionTable, this.petFoodsCartTable, this.petFoodsTable], + [ + { + id: 'AwsSolutions-DDB3', + reason: 'Point-in-time Recovery not required for this table', + }, + ], + true, + ); + + this.createExports(); + this.createOutputs(); + } + + private createExports(): void { + new CfnOutput(this, 'TableArn', { + value: this.petAdoptionTable.tableArn, + exportName: DYNAMODB_TABLE_ARN_EXPORT_NAME, + }); + + new CfnOutput(this, 'TableName', { + value: this.petAdoptionTable.tableName, + exportName: DYNAMODB_TABLE_NAME_EXPORT_NAME, + }); + + new CfnOutput(this, 'PetFoodsTableArn', { + value: this.petFoodsTable.tableArn, + exportName: `${DYNAMODB_TABLE_ARN_EXPORT_NAME}-PetFoods`, + }); + + new CfnOutput(this, 'PetFoodsTableName', { + value: this.petFoodsTable.tableName, + exportName: `${DYNAMODB_TABLE_NAME_EXPORT_NAME}-PetFoods`, + }); + + new CfnOutput(this, 'PetFoodsCartTableArn', { + value: this.petFoodsCartTable.tableArn, + exportName: `${DYNAMODB_TABLE_ARN_EXPORT_NAME}-PetFoodsCart`, + }); + + new CfnOutput(this, 'PetFoodsCartTableName', { + value: this.petFoodsCartTable.tableName, + exportName: `${DYNAMODB_TABLE_NAME_EXPORT_NAME}-PetFoodsCart`, + }); + } + + public static importFromExports( + scope: Construct, + id: string, + ): { table: ITable; petFoodsTable: ITable; petFoodsCartTable: ITable } { + const tableArn = Fn.importValue(DYNAMODB_TABLE_ARN_EXPORT_NAME); + const petFoodsTableArn = Fn.importValue(`${DYNAMODB_TABLE_ARN_EXPORT_NAME}-PetFoods`); + const petFoodsCartTableArn = Fn.importValue(`${DYNAMODB_TABLE_ARN_EXPORT_NAME}-PetFoodsCart`); + + const petAdoptionsTable = Table.fromTableAttributes(scope, `${id}-Table`, { + tableArn: tableArn, + }); + + const petFoodsTable = Table.fromTableAttributes(scope, `${id}-PetFoodsTable`, { + tableArn: petFoodsTableArn, + grantIndexPermissions: true, + }); + + const petFoodsCartTable = Table.fromTableAttributes(scope, `${id}-PetFoodsCartTable`, { + tableArn: petFoodsCartTableArn, + }); + + return { table: petAdoptionsTable, petFoodsTable, petFoodsCartTable }; + } + + createOutputs(): void { + if (this.petAdoptionTable && this.petFoodsTable && this.petFoodsCartTable) { + Utilities.createSsmParameters( + this, + PARAMETER_STORE_PREFIX, + new Map( + Object.entries({ + dynamodbtablename: this.petAdoptionTable.tableName, + foods_table_name: this.petFoodsTable.tableName, + carts_table_name: this.petFoodsCartTable.tableName, + }), + ), + ); + } else { + throw new Error('Tables are not available'); + } + } +} diff --git a/src/cdk/lib/constructs/ecs-service.ts b/src/cdk/lib/constructs/ecs-service.ts new file mode 100644 index 00000000..8d78d615 --- /dev/null +++ b/src/cdk/lib/constructs/ecs-service.ts @@ -0,0 +1,516 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +import { Effect, IRole, PolicyStatement, Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam'; +import { Microservice, MicroserviceProperties } from './microservice'; +import { ComputeType } from '../../bin/environment'; +import { + AwsLogDriver, + ContainerDefinition, + ContainerImage, + Ec2TaskDefinition, + FargateTaskDefinition, + Protocol, + TaskDefinition, + Ec2Service, + FargateService, + BaseService, + FireLensLogDriver, + FirelensLogRouterType, +} from 'aws-cdk-lib/aws-ecs'; +import { + ApplicationLoadBalancedEc2Service, + ApplicationLoadBalancedFargateService, + ApplicationLoadBalancedServiceBase, +} from 'aws-cdk-lib/aws-ecs-patterns'; +import { Construct } from 'constructs'; +import { LogGroup, RetentionDays } from 'aws-cdk-lib/aws-logs'; +import { RemovalPolicy, Stack, Fn } from 'aws-cdk-lib'; +import { NagSuppressions } from 'cdk-nag'; +import { Port, Peer, SubnetType } from 'aws-cdk-lib/aws-ec2'; +import { IPrivateDnsNamespace } from 'aws-cdk-lib/aws-servicediscovery'; +import { OpenSearchCollection } from './opensearch-collection'; +import { OpenSearchPipeline } from './opensearch-pipeline'; + +export interface EcsServiceProperties extends MicroserviceProperties { + cpu: number; + memoryLimitMiB: number; + desiredTaskCount: number; + cloudMapNamespace?: IPrivateDnsNamespace; + openSearchCollection?: + | OpenSearchCollection + | { + collectionArn: string; + collectionEndpoint: string; + }; + /** + * OpenSearch ingestion pipeline for log routing + * When provided, logs will be sent to the pipeline instead of directly to OpenSearch + * Mutually exclusive with openSearchCollection + */ + openSearchPipeline?: + | OpenSearchPipeline + | { + pipelineEndpoint: string; + pipelineArn?: string; + pipelineRoleArn?: string; + }; + additionalEnvironment?: { [key: string]: string }; +} + +export abstract class EcsService extends Microservice { + public readonly taskDefinition: TaskDefinition; + public readonly loadBalancedService?: ApplicationLoadBalancedServiceBase; + public readonly service?: BaseService; + public readonly container: ContainerDefinition; + public readonly taskRole: IRole; + + constructor(scope: Construct, id: string, properties: EcsServiceProperties) { + super(scope, id, properties); + + // Validate mutual exclusivity of openSearchCollection and openSearchPipeline + if (properties.openSearchCollection && properties.openSearchPipeline) { + throw new Error( + 'openSearchCollection and openSearchPipeline are mutually exclusive. Please specify only one.', + ); + } + + const result = this.configureECSService(properties); + this.taskDefinition = result.taskDefinition; + this.loadBalancedService = result.loadBalancedService; + this.service = result.service; + this.container = result.container; + this.taskRole = result.taskRole; + + this.addPermissions(properties); + this.createOutputs(properties); + } + + configureEKSService(): void { + throw new Error('Method not implemented.'); + } + + configureECSService(properties: EcsServiceProperties) { + let loadBalancedService: ApplicationLoadBalancedServiceBase | undefined; + let service: BaseService | undefined; + + // Create CloudWatch log group + const logGroup = new LogGroup(this, 'ecs-log-group', { + logGroupName: properties.logGroupName || `/ecs/${properties.name}`, + removalPolicy: RemovalPolicy.DESTROY, + retention: properties.logRetentionDays || RetentionDays.ONE_WEEK, + }); + + // Configure logging based on whether OpenSearch collection or pipeline is provided + const logging = + properties.openSearchCollection || properties.openSearchPipeline + ? // Use FireLens for routing to OpenSearch collection or pipeline + this.createFireLensLogDriver(properties) + : // Use standard CloudWatch logging + new AwsLogDriver({ + streamPrefix: 'logs', + logGroup: logGroup, + }); + + const taskRole = new Role(this, `taskRole`, { + assumedBy: new ServicePrincipal('ecs-tasks.amazonaws.com'), + }); + + const taskDefinition = + properties.computeType == ComputeType.Fargate + ? new FargateTaskDefinition(this, 'taskDefinition', { + cpu: properties.cpu, // TODO: Some math is needed here so the value includes Container + Sidecars + taskRole: taskRole, + memoryLimitMiB: properties.memoryLimitMiB, + }) + : new Ec2TaskDefinition(this, 'taskDefinition', { + taskRole: taskRole, + enableFaultInjection: true, + }); + + // Add execution role permissions + const executionRoleActions = [ + 'ecr:GetAuthorizationToken', + 'ecr:BatchCheckLayerAvailability', + 'ecr:GetDownloadUrlForLayer', + 'ecr:BatchGetImage', + 'logs:CreateLogStream', + 'logs:PutLogEvents', + ]; + + // Add OpenSearch permissions only if collection is provided AND pipeline is not used + // When using pipeline, the pipeline handles OpenSearch access, not the ECS task + if (properties.openSearchCollection && !properties.openSearchPipeline) { + executionRoleActions.push( + 'aoss:WriteDocument', + 'aoss:CreateIndex', + 'aoss:DescribeIndex', + 'aoss:UpdateIndex', + 'es:ESHttpPost', + 'es:ESHttpPut', + ); + } + + // Note: Pipeline permissions are handled in the task role, not execution role + + taskDefinition.addToExecutionRolePolicy( + new PolicyStatement({ + effect: Effect.ALLOW, + actions: executionRoleActions, + resources: ['*'], + }), + ); + + const image = ContainerImage.fromRegistry(properties.repositoryURI); + + // Merge default environment variables with additional ones + const defaultEnvironment = { + // clear text, not for sensitive data + AWS_REGION: Stack.of(this).region, + }; + + const environment = { + ...defaultEnvironment, + ...(properties.additionalEnvironment || {}), + }; + + const container = taskDefinition.addContainer('container', { + image: image, + memoryLimitMiB: 512, + cpu: 256, + logging, + environment, + }); + + container.addPortMappings({ + containerPort: properties.containerPort || 80, + protocol: Protocol.TCP, + }); + + // Add FireLens log router container if OpenSearch collection or pipeline is provided + if (properties.openSearchCollection || properties.openSearchPipeline) { + this.addFireLensLogRouter(taskDefinition, properties); + } + + if (!properties.disableService) { + if (properties.createLoadBalancer === false) { + // Create service without load balancer + service = + properties.computeType == ComputeType.Fargate + ? new FargateService(this, 'ecs-service-fargate-no-lb', { + cluster: properties.ecsCluster!, + taskDefinition: taskDefinition as FargateTaskDefinition, + desiredCount: properties.desiredTaskCount, + serviceName: properties.name, + securityGroups: properties.securityGroup ? [properties.securityGroup] : undefined, + assignPublicIp: false, + cloudMapOptions: properties.cloudMapNamespace + ? { name: properties.name, cloudMapNamespace: properties.cloudMapNamespace } + : undefined, + }) + : new Ec2Service(this, 'ecs-service-ec2-no-lb', { + cluster: properties.ecsCluster!, + taskDefinition: taskDefinition as Ec2TaskDefinition, + desiredCount: properties.desiredTaskCount, + serviceName: properties.name, + cloudMapOptions: properties.cloudMapNamespace + ? { name: properties.name, cloudMapNamespace: properties.cloudMapNamespace } + : undefined, + }); + } else { + if (properties.computeType == ComputeType.Fargate) { + loadBalancedService = new ApplicationLoadBalancedFargateService(this, 'ecs-service-fargate', { + cluster: properties.ecsCluster, + taskDefinition: taskDefinition as FargateTaskDefinition, + publicLoadBalancer: false, + desiredCount: properties.desiredTaskCount, + listenerPort: properties.listenerPort || 80, + securityGroups: properties.securityGroup ? [properties.securityGroup] : undefined, + openListener: false, + assignPublicIp: false, + serviceName: properties.name, + loadBalancerName: `LB-${properties.name}`, + }); + + if (properties.healthCheck) { + loadBalancedService.targetGroup.configureHealthCheck({ + path: properties.healthCheck, + }); + } + + // Allow load balancer to communicate with ECS tasks + if (properties.securityGroup) { + properties.securityGroup.addIngressRule( + loadBalancedService.loadBalancer.connections.securityGroups[0], + Port.tcp(properties.containerPort || 80), + 'Allow load balancer to reach ECS tasks', + ); + } + + // Allow traffic from specified subnet type to load balancer + if (properties.vpc && loadBalancedService) { + const subnets = + properties.subnetType === SubnetType.PUBLIC + ? properties.vpc.publicSubnets + : properties.vpc.privateSubnets; + for (const [index, subnet] of subnets.entries()) { + loadBalancedService.loadBalancer.connections.allowFrom( + Peer.ipv4(subnet.ipv4CidrBlock), + Port.tcp(properties.listenerPort || 80), + `Allow traffic from ${properties.subnetType || 'private'} subnet ${index + 1}`, + ); + } + } + } else { + loadBalancedService = new ApplicationLoadBalancedEc2Service(this, 'ecs-service-ec2', { + cluster: properties.ecsCluster, + taskDefinition: taskDefinition as FargateTaskDefinition, + publicLoadBalancer: false, + desiredCount: properties.desiredTaskCount, + listenerPort: properties.listenerPort || 80, + openListener: false, + serviceName: properties.name, + loadBalancerName: `LB-${properties.name}`, + }); + + if (properties.healthCheck) { + loadBalancedService.targetGroup.configureHealthCheck({ + path: properties.healthCheck, + }); + } + + // Allow load balancer to communicate with ECS tasks + if (properties.securityGroup) { + properties.securityGroup.addIngressRule( + loadBalancedService.loadBalancer.connections.securityGroups[0], + Port.tcp(properties.containerPort || 80), + 'Allow load balancer to reach ECS tasks', + ); + } + + // Allow traffic from specified subnet type to load balancer + if (properties.vpc && loadBalancedService) { + const subnets = + properties.subnetType === SubnetType.PUBLIC + ? properties.vpc.publicSubnets + : properties.vpc.privateSubnets; + for (const [index, subnet] of subnets.entries()) { + loadBalancedService.loadBalancer.connections.allowFrom( + Peer.ipv4(subnet.ipv4CidrBlock), + Port.tcp(properties.listenerPort || 80), + `Allow traffic from ${properties.subnetType || 'private'} subnet ${index + 1}`, + ); + } + } + } + } + } + + NagSuppressions.addResourceSuppressions(taskDefinition, [ + { + id: 'AwsSolutions-ECS2', + reason: 'AWS_REGION is required by OTEL. TODO: Replace with proper environment variables ', + }, + ]); + + if (taskDefinition.executionRole) { + NagSuppressions.addResourceSuppressions( + taskDefinition.executionRole, + [ + { + id: 'AwsSolutions-IAM5', + reason: 'Allowing * for ECR pull and log access', + }, + ], + true, + ); + } + + if (taskDefinition.taskRole && properties.openSearchCollection) { + NagSuppressions.addResourceSuppressions( + taskDefinition.taskRole, + [ + { + id: 'AwsSolutions-IAM5', + reason: 'OpenSearch Serverless requires broad permissions for log ingestion', + }, + ], + true, + ); + } + + if (taskDefinition.taskRole && properties.openSearchPipeline) { + NagSuppressions.addResourceSuppressions( + taskDefinition.taskRole, + [ + { + id: 'AwsSolutions-IAM5', + reason: 'OpenSearch Ingestion Service requires permissions for pipeline access', + }, + ], + true, + ); + } + + if (loadBalancedService) { + NagSuppressions.addResourceSuppressions(loadBalancedService.loadBalancer, [ + { + id: 'AwsSolutions-ELB2', + reason: 'Disabled access logs for now', + }, + ]); + } + + return { taskDefinition, loadBalancedService, service, container, taskRole }; + } + + private createFireLensLogDriver(properties: EcsServiceProperties): FireLensLogDriver { + if (properties.openSearchPipeline) { + // Configure for OpenSearch Ingestion Pipeline using HTTP output + const pipeline = properties.openSearchPipeline; + const pipelineEndpoint = + 'pipelineEndpoint' in pipeline + ? pipeline.pipelineEndpoint + : (pipeline as OpenSearchPipeline).pipeline.attrIngestEndpointUrls[0]; + + // Extract host from pipeline endpoint + // Pipeline endpoints from OSI are typically just hostnames without https:// + const hostAndPath = Fn.split('/', pipelineEndpoint); + const host = Fn.select(0, hostAndPath); + + // Get the pipeline role ARN if available + let pipelineRoleArn: string | undefined; + if ('pipelineRoleArn' in pipeline) { + // This is an imported pipeline object with pipelineRoleArn property + pipelineRoleArn = pipeline.pipelineRoleArn; + } else if ('pipelineRole' in pipeline) { + // This is an OpenSearchPipeline construct with pipelineRole property + pipelineRoleArn = (pipeline as OpenSearchPipeline).pipelineRole.roleArn; + } + + const httpOptions: { [key: string]: string } = { + Name: 'http', + Match: '*', + Host: host, + Port: '443', + uri: '/log/ingest', + format: 'json', + aws_auth: 'true', + aws_region: Stack.of(this).region, + aws_service: 'osis', + tls: 'on', + 'tls.verify': 'off', + Retry_Limit: '3', + Log_Level: 'trace', + }; + + // Don't use aws_role_arn - let the ECS task role handle authentication directly + + return new FireLensLogDriver({ + options: httpOptions, + }); + } else if (properties.openSearchCollection) { + // Configure for direct OpenSearch Collection + const collection = properties.openSearchCollection; + const openSearchEndpoint = + 'collection' in collection + ? collection.collection.attrCollectionEndpoint + : collection.collectionEndpoint; + + // Use CloudFormation functions to strip https:// prefix at deployment time + const openSearchHostWithoutProtocol = Fn.select(1, Fn.split('https://', openSearchEndpoint)); + + return new FireLensLogDriver({ + options: { + Name: 'opensearch', + Host: openSearchHostWithoutProtocol, + Port: '443', + aws_auth: 'On', + AWS_Region: Stack.of(this).region, + AWS_Service_Name: 'aoss', + Index: `${properties.name}-logs`, + tls: 'On', + Suppress_Type_Name: 'On', + Trace_Error: 'On', + Trace_Output: 'On', + }, + }); + } else { + throw new Error('Either openSearchCollection or openSearchPipeline must be provided for FireLens logging'); + } + } + + private addFireLensLogRouter(taskDefinition: TaskDefinition, properties: EcsServiceProperties): void { + // Add FireLens log router using the task definition method + taskDefinition.addFirelensLogRouter('log-router', { + image: ContainerImage.fromRegistry('public.ecr.aws/aws-observability/aws-for-fluent-bit:stable'), + memoryLimitMiB: 512, + cpu: 256, + essential: true, + logging: new AwsLogDriver({ + streamPrefix: 'firelens', + logGroup: new LogGroup(this, 'firelens-log-group', { + logGroupName: `/ecs/firelens/${properties.name}`, + removalPolicy: RemovalPolicy.DESTROY, + retention: RetentionDays.ONE_WEEK, + }), + }), + firelensConfig: { + type: FirelensLogRouterType.FLUENTBIT, + options: { + enableECSLogMetadata: true, + }, + }, + }); + + // Add task role permissions based on configuration + if (properties.openSearchCollection && !properties.openSearchPipeline) { + // Add permissions for direct OpenSearch access only when not using pipeline + const collection = properties.openSearchCollection; + const collectionArn = 'collection' in collection ? collection.collection.attrArn : collection.collectionArn; + + taskDefinition.taskRole.addToPrincipalPolicy( + new PolicyStatement({ + effect: Effect.ALLOW, + actions: [ + 'aoss:WriteDocument', + 'aoss:CreateIndex', + 'aoss:DescribeIndex', + 'es:ESHttpPost', + 'es:ESHttpPut', + ], + resources: [collectionArn], + }), + ); + } else if (properties.openSearchPipeline) { + // For OpenSearch Ingestion Service pipeline, use HTTP calls with AWS SigV4 auth + // The ECS task role will authenticate directly with the pipeline endpoint + const pipeline = properties.openSearchPipeline; + const pipelineArn = + 'pipelineEndpoint' in pipeline + ? `arn:aws:osis:${Stack.of(this).region}:${Stack.of(this).account}:pipeline/*` + : (pipeline as OpenSearchPipeline).pipeline.attrPipelineArn; + + // Add permissions for pipeline ingestion via HTTP + taskDefinition.taskRole.addToPrincipalPolicy( + new PolicyStatement({ + effect: Effect.ALLOW, + actions: ['osis:Ingest'], + resources: [pipelineArn], + }), + ); + + // Add permissions for AWS SigV4 signing for HTTP requests to the pipeline + taskDefinition.taskRole.addToPrincipalPolicy( + new PolicyStatement({ + effect: Effect.ALLOW, + actions: [ + 'sts:GetCallerIdentity', // Required for SigV4 signing + ], + resources: ['*'], + }), + ); + } + } +} diff --git a/src/cdk/lib/constructs/ecs.ts b/src/cdk/lib/constructs/ecs.ts new file mode 100644 index 00000000..dbe99cda --- /dev/null +++ b/src/cdk/lib/constructs/ecs.ts @@ -0,0 +1,212 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +/** + * Amazon ECS cluster construct for the One Observability Workshop. + * + * This module provides a CDK construct for creating and managing an Amazon ECS cluster + * with EC2 capacity providers, auto scaling groups, and enhanced container insights. + * The cluster is configured for optimal observability and monitoring capabilities. + * + * @packageDocumentation + */ + +import { Construct } from 'constructs'; +import { CfnOutput, Fn } from 'aws-cdk-lib'; +import { AsgCapacityProvider, Cluster, ContainerInsights, EcsOptimizedImage, ICluster } from 'aws-cdk-lib/aws-ecs'; +import { InstanceClass, InstanceSize, InstanceType, ISecurityGroup, SecurityGroup, IVpc } from 'aws-cdk-lib/aws-ec2'; +import { AutoScalingGroup, BlockDeviceVolume, ScalingEvents } from 'aws-cdk-lib/aws-autoscaling'; +import { ITopic } from 'aws-cdk-lib/aws-sns'; +import { NagSuppressions } from 'cdk-nag'; +import { + ECS_CLUSTER_ARN_EXPORT_NAME, + ECS_CLUSTER_NAME_EXPORT_NAME, + ECS_SECURITY_GROUP_ID_EXPORT_NAME, +} from '../../bin/constants'; +import { OpenSearchPipeline } from './opensearch-pipeline'; + +/** + * Properties for configuring the ECS cluster construct. + */ +export interface EcsProperties { + /** VPC where the ECS cluster will be deployed */ + vpc: IVpc; + /** SNS topic for auto scaling notifications */ + topic: ITopic; + /** Number of EC2 instances for the ECS cluster capacity */ + ecsEc2Capacity?: number; + /** EC2 instance type for the ECS cluster nodes */ + ecsEc2InstanceType?: string; + /** OpenSearch ingestion pipeline for log routing */ + openSearchPipeline?: OpenSearchPipeline; +} + +/** + * A CDK construct that creates an Amazon ECS cluster with EC2 capacity. + * + * This construct sets up: + * - ECS cluster with enhanced container insights + * - Auto Scaling Group with ECS-optimized AMI + * - Security group for cluster resources + * - Capacity provider for efficient resource management + * - CloudFormation exports for cross-stack references + * + * The cluster is configured with best practices for security, monitoring, + * and cost optimization. + */ +export class WorkshopEcs extends Construct { + /** The ECS cluster instance */ + public readonly cluster: Cluster; + /** Auto Scaling Group managing the EC2 capacity */ + public readonly autoScalingGroup: AutoScalingGroup; + /** Security group for ECS cluster resources */ + public readonly securityGroup: SecurityGroup; + /** OpenSearch ingestion pipeline for log routing (optional) */ + public readonly openSearchPipeline?: OpenSearchPipeline; + + /** + * Creates a new WorkshopEcs construct. + * + * @param scope - The parent construct + * @param id - The construct identifier + * @param properties - Configuration properties for the ECS cluster + */ + constructor(scope: Construct, id: string, properties: EcsProperties) { + super(scope, id); + + // Store the pipeline reference for use by ECS services + this.openSearchPipeline = properties.openSearchPipeline; + + this.securityGroup = new SecurityGroup(this, 'SecurityGroup', { + vpc: properties.vpc, + description: 'Security group for ECS cluster resources', + allowAllOutbound: true, + securityGroupName: `${id}-ecs-security-group`, + }); + + this.cluster = new Cluster(this, 'Cluster', { + containerInsightsV2: ContainerInsights.ENHANCED, + vpc: properties.vpc, + clusterName: `${id}-cluster`, + }); + + this.autoScalingGroup = new AutoScalingGroup(this, 'AutoScalingGroup', { + vpc: properties.vpc, + machineImage: EcsOptimizedImage.amazonLinux2023(), + minCapacity: properties.ecsEc2Capacity || 0, + maxCapacity: properties.ecsEc2Capacity || 2, + desiredCapacity: properties.ecsEc2Capacity || 2, + instanceType: properties.ecsEc2InstanceType + ? new InstanceType(properties.ecsEc2InstanceType) + : InstanceType.of(InstanceClass.T3, InstanceSize.MEDIUM), + blockDevices: [ + { + deviceName: '/dev/xvda', + volume: BlockDeviceVolume.ebs(30, { encrypted: true }), + }, + ], + notifications: [ + { + topic: properties.topic, + scalingEvents: ScalingEvents.ALL, + }, + ], + autoScalingGroupName: `${id}-ecs-asg`, + }); + + this.cluster.addAsgCapacityProvider( + new AsgCapacityProvider(this, 'AsgCapacityProvider', { + autoScalingGroup: this.autoScalingGroup, + }), + ); + + NagSuppressions.addResourceSuppressions( + this.autoScalingGroup, + [ + { + id: 'AwsSolutions-IAM5', + reason: 'Autoscaling group needs access to all ECS tasks', + }, + ], + true, + ); + + NagSuppressions.addResourceSuppressions(this.cluster, [ + { + id: 'AwsSolutions-ECS4', + reason: 'Containers insights v2 is enabled, false positive', + }, + ]); + + this.createExports(); + } + + /** + * Creates CloudFormation exports for the ECS cluster resources. + * These exports allow other stacks to reference the cluster. + */ + private createExports(): void { + new CfnOutput(this, 'ClusterArn', { + value: this.cluster.clusterArn, + exportName: ECS_CLUSTER_ARN_EXPORT_NAME, + }); + + new CfnOutput(this, 'ClusterName', { + value: this.cluster.clusterName, + exportName: ECS_CLUSTER_NAME_EXPORT_NAME, + }); + + new CfnOutput(this, 'SecurityGroupId', { + value: this.securityGroup.securityGroupId, + exportName: ECS_SECURITY_GROUP_ID_EXPORT_NAME, + }); + } + + /** + * Imports an ECS cluster from CloudFormation exports. + * + * This static method allows other stacks to reference an ECS cluster + * that was created by this construct and exported via CloudFormation. + * + * @param scope - The construct scope where the cluster will be imported + * @param id - The construct identifier for the imported resources + * @param vpc - The VPC where the cluster is deployed + * @returns Object containing the imported cluster, security group, and optional pipeline + */ + public static importFromExports( + scope: Construct, + id: string, + vpc: IVpc, + ): { cluster: ICluster; securityGroup: ISecurityGroup; openSearchPipeline?: { pipelineEndpoint: string; pipelineArn: string; pipelineRoleArn: string } } { + const clusterName = Fn.importValue(ECS_CLUSTER_NAME_EXPORT_NAME); + const securityGroupId = Fn.importValue(ECS_SECURITY_GROUP_ID_EXPORT_NAME); + + const cluster = Cluster.fromClusterAttributes(scope, `${id}-Cluster`, { + clusterName: clusterName, + vpc: vpc, + }); + + const securityGroup = SecurityGroup.fromSecurityGroupId(scope, `${id}-SecurityGroup`, securityGroupId); + + // Import OpenSearch pipeline information if available + // This provides backward compatibility - if pipeline exports don't exist, + // the import will gracefully handle the missing values + let openSearchPipeline: { pipelineEndpoint: string; pipelineArn: string; pipelineRoleArn: string } | undefined; + + try { + const pipelineImports = OpenSearchPipeline.importFromExports(); + openSearchPipeline = { + pipelineEndpoint: pipelineImports.pipelineEndpoint, + pipelineArn: pipelineImports.pipelineArn, + pipelineRoleArn: pipelineImports.pipelineRoleArn, + }; + } catch (error) { + // Pipeline exports don't exist - this is fine for backward compatibility + openSearchPipeline = undefined; + } + + return { cluster, securityGroup, openSearchPipeline }; + } +} diff --git a/src/cdk/lib/constructs/eks-deployment.ts b/src/cdk/lib/constructs/eks-deployment.ts new file mode 100644 index 00000000..50d1fddc --- /dev/null +++ b/src/cdk/lib/constructs/eks-deployment.ts @@ -0,0 +1,48 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +import { CfnPodIdentityAssociation, KubernetesManifest } from 'aws-cdk-lib/aws-eks'; +import { Construct } from 'constructs'; +import { Microservice, MicroserviceProperties } from './microservice'; +import { Role } from 'aws-cdk-lib/aws-iam'; + +export interface EKSDeploymentProperties extends MicroserviceProperties { + manifestPath?: string; + skipValidation?: boolean; + disableService?: boolean; + name: string; +} + +export abstract class EKSDeployment extends Microservice { + public manifest: KubernetesManifest; + public serviceAccountRole?: Role; + public namespace?: string; + public serviceAccountName?: string; + public podIdentityAssociation?: CfnPodIdentityAssociation; + + constructor(scope: Construct, id: string, properties: EKSDeploymentProperties) { + super(scope, id, properties); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- this is how KubnernetesManifests defines it + abstract prepareManifest(properties: EKSDeploymentProperties): Record[]; + + configureEKSService(properties: EKSDeploymentProperties): KubernetesManifest { + if (!properties.eksCluster) { + throw new Error('eksCluster is required'); + } + + if (!properties.manifestPath) { + throw new Error('manifestPath is required'); + } + + return new KubernetesManifest(this, 'KubernetesManifests', { + overwrite: true, + prune: true, + skipValidation: properties.skipValidation, + manifest: this.prepareManifest(properties), + cluster: properties.eksCluster, + }); + } +} diff --git a/src/cdk/lib/constructs/eks.ts b/src/cdk/lib/constructs/eks.ts new file mode 100644 index 00000000..6a16a21c --- /dev/null +++ b/src/cdk/lib/constructs/eks.ts @@ -0,0 +1,288 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +import { Construct } from 'constructs'; +import { CfnOutput, Fn, Stack } from 'aws-cdk-lib'; +import { + InstanceClass, + InstanceSize, + InstanceType, + ISecurityGroup, + SecurityGroup, + SubnetType, + IVpc, +} from 'aws-cdk-lib/aws-ec2'; +import { + Cluster, + ICluster, + AuthenticationMode, + KubernetesVersion, + ClusterLoggingTypes, + Addon, + CfnAddon, + NodegroupAmiType, + AlbControllerVersion, + CfnNodegroup, +} from 'aws-cdk-lib/aws-eks'; +import { KubectlV33Layer } from '@aws-cdk/lambda-layer-kubectl-v33'; +import { ManagedPolicy, Role, ServicePrincipal, OpenIdConnectProvider } from 'aws-cdk-lib/aws-iam'; +import { NagSuppressions } from 'cdk-nag'; +import { + EKS_CLUSTER_ARN_EXPORT_NAME, + EKS_CLUSTER_NAME_EXPORT_NAME, + EKS_SECURITY_GROUP_ID_EXPORT_NAME, + EKS_KUBECTL_ROLE_ARN_EXPORT_NAME, + EKS_OPEN_ID_CONNECT_PROVIDER_ARN_EXPORT_NAME, + EKS_KUBECTL_SECURITY_GROUP_ID_EXPORT_NAME, + EKS_KUBECTL_LAMBDA_ROLE_ARN_EXPORT_NAME, +} from '../../bin/constants'; + +export interface EksProperties { + vpc: IVpc; + eksEc2Capacity?: number; + eksEc2InstanceType?: string; +} + +export class WorkshopEks extends Construct { + public readonly cluster: Cluster; + + constructor(scope: Construct, id: string, properties: EksProperties) { + super(scope, id); + + this.cluster = new Cluster(this, 'Cluster', { + vpc: properties.vpc, + authenticationMode: AuthenticationMode.API_AND_CONFIG_MAP, + version: KubernetesVersion.V1_33, + kubectlLayer: new KubectlV33Layer(this, 'kubectl'), + defaultCapacity: properties.eksEc2Capacity || 2, + defaultCapacityInstance: properties.eksEc2InstanceType + ? new InstanceType(properties.eksEc2InstanceType) + : InstanceType.of(InstanceClass.T3, InstanceSize.MEDIUM), + clusterLogging: [ + ClusterLoggingTypes.API, + ClusterLoggingTypes.AUDIT, + ClusterLoggingTypes.AUTHENTICATOR, + ClusterLoggingTypes.CONTROLLER_MANAGER, + ClusterLoggingTypes.SCHEDULER, + ], + albController: { + version: AlbControllerVersion.V2_8_2, + }, + vpcSubnets: [ + { + subnetType: SubnetType.PRIVATE_WITH_EGRESS, + }, + ], + clusterName: `${id}-cluster`, + }); + + this.setupAddons(); + this.setupSuppressions(); + this.createExports(); + } + + private setupAddons(): void { + const cfnNodeGroup = this.cluster.defaultNodegroup?.node.defaultChild as CfnNodegroup; + cfnNodeGroup.addPropertyOverride('AmiType', NodegroupAmiType.AL2023_X86_64_STANDARD); + + new Addon(this, 'coreDNSAddon', { + cluster: this.cluster, + addonName: 'coredns', + preserveOnDelete: false, + }); + + new Addon(this, 'nodeMonitoringAgentAddon', { + cluster: this.cluster, + addonName: 'eks-node-monitoring-agent', + preserveOnDelete: false, + }); + + new Addon(this, 'podIdentityAgentAddon', { + cluster: this.cluster, + addonName: 'eks-pod-identity-agent', + preserveOnDelete: false, + }); + + new Addon(this, 'guardDutyAddon', { + cluster: this.cluster, + addonName: 'aws-guardduty-agent', + preserveOnDelete: false, + }); + + const iamRoleCloudwatchAddon = new Role(this, 'CloudwatchAddonRole', { + description: 'Allows pods running in Amazon EKS cluster to access AWS resources.', + managedPolicies: [ + ManagedPolicy.fromAwsManagedPolicyName('AmazonEKSClusterPolicy'), + ManagedPolicy.fromAwsManagedPolicyName('CloudWatchAgentServerPolicy'), + ], + assumedBy: new ServicePrincipal('pods.eks.amazonaws.com').withSessionTags(), + }); + + const cloudwatchAddon = new Addon(this, 'cloudwatchObservabilityAddon', { + cluster: this.cluster, + addonName: 'amazon-cloudwatch-observability', + preserveOnDelete: false, + }); + + const cfnCloudWatchAddon = cloudwatchAddon.node.defaultChild as CfnAddon; + cfnCloudWatchAddon.addPropertyOverride('PodIdentityAssociations', [ + { + ServiceAccount: 'cloudwatch-agent', + RoleArn: iamRoleCloudwatchAddon.roleArn, + }, + ]); + + const iamRoleNetworkFlowAgent = new Role(this, 'NetworkFlowAgentRole', { + description: 'Allows pods running in Amazon EKS cluster to access AWS resources.', + managedPolicies: [ + ManagedPolicy.fromAwsManagedPolicyName('AmazonEKSClusterPolicy'), + ManagedPolicy.fromAwsManagedPolicyName('CloudWatchNetworkFlowMonitorAgentPublishPolicy'), + ], + assumedBy: new ServicePrincipal('pods.eks.amazonaws.com').withSessionTags(), + }); + + const networkFlowAgentAddon = new Addon(this, 'networkFlowMonitoringAgentAddon', { + cluster: this.cluster, + addonName: 'aws-network-flow-monitoring-agent', + preserveOnDelete: false, + }); + + const cfnNetworkFlowAddon = networkFlowAgentAddon.node.defaultChild as CfnAddon; + cfnNetworkFlowAddon.addPropertyOverride('PodIdentityAssociations', [ + { + ServiceAccount: 'aws-network-flow-monitor-agent-service-account', + RoleArn: iamRoleNetworkFlowAgent.roleArn, + }, + ]); + } + + private setupSuppressions(): void { + NagSuppressions.addResourceSuppressions( + this.cluster, + [ + { + id: 'AwsSolutions-IAM4', + reason: 'CDK EKS automation requires multiple * access', + }, + { + id: 'AwsSolutions-IAM5', + reason: 'EKS Creation Role requires star access', + }, + { + id: 'AwsSolutions-EKS1', + reason: 'Public API are needed to manage the cluster from CloudShell in the workshop', + }, + ], + true, + ); + + NagSuppressions.addStackSuppressions( + Stack.of(this), + [ + { + id: 'AwsSolutions-IAM4', + reason: 'CDK EKS automation requires multiple * access', + }, + { + id: 'AwsSolutions-IAM5', + reason: 'EKS Creation Role requires star access', + }, + { + id: 'AwsSolutions-SF1', + reason: 'Step functions are implemented by CDK EKS Construct and out of scope', + }, + { + id: 'AwsSolutions-SF2', + reason: 'Step functions are implemented by CDK EKS Construct and out of scope', + }, + { + id: 'AwsSolutions-L1', + reason: 'Lambda functions are implemented by CDK EKS Construct and out of scope', + }, + ], + true, + ); + } + + private createExports(): void { + new CfnOutput(this, 'ClusterArn', { + value: this.cluster.clusterArn, + exportName: EKS_CLUSTER_ARN_EXPORT_NAME, + }); + + new CfnOutput(this, 'ClusterName', { + value: this.cluster.clusterName, + exportName: EKS_CLUSTER_NAME_EXPORT_NAME, + }); + + new CfnOutput(this, 'SecurityGroupId', { + value: this.cluster.clusterSecurityGroupId, + exportName: EKS_SECURITY_GROUP_ID_EXPORT_NAME, + }); + + new CfnOutput(this, 'KubectlRoleArn', { + value: this.cluster.kubectlRole!.roleArn, + exportName: EKS_KUBECTL_ROLE_ARN_EXPORT_NAME, + }); + + new CfnOutput(this, 'OpenIdConnectProviderArn', { + value: this.cluster.openIdConnectProvider.openIdConnectProviderArn, + exportName: EKS_OPEN_ID_CONNECT_PROVIDER_ARN_EXPORT_NAME, + }); + + new CfnOutput(this, 'KubectlSecurityGroupId', { + value: this.cluster.kubectlSecurityGroup!.securityGroupId, + exportName: EKS_KUBECTL_SECURITY_GROUP_ID_EXPORT_NAME, + }); + + new CfnOutput(this, 'KubectlLambdaRoleArn', { + value: this.cluster.kubectlLambdaRole!.roleArn, + exportName: EKS_KUBECTL_LAMBDA_ROLE_ARN_EXPORT_NAME, + }); + } + + public static importFromExports( + scope: Construct, + id: string, + ): { cluster: ICluster; securityGroup: ISecurityGroup } { + const clusterName = Fn.importValue(EKS_CLUSTER_NAME_EXPORT_NAME); + const securityGroupId = Fn.importValue(EKS_SECURITY_GROUP_ID_EXPORT_NAME); + const kubectlRoleArn = Fn.importValue(EKS_KUBECTL_ROLE_ARN_EXPORT_NAME); + const kubectlLambdaRoleArn = Fn.importValue(EKS_KUBECTL_LAMBDA_ROLE_ARN_EXPORT_NAME); + const openIdConnectProviderArn = Fn.importValue(EKS_OPEN_ID_CONNECT_PROVIDER_ARN_EXPORT_NAME); + const kubectlSecurityGroupId = Fn.importValue(EKS_KUBECTL_SECURITY_GROUP_ID_EXPORT_NAME); + const kubectlRole = Role.fromRoleArn(scope, `${id}-KubectlRole`, kubectlRoleArn); + const kubectlLambdaRole = Role.fromRoleArn(scope, `${id}-KubectlLambdaRole`, kubectlLambdaRoleArn); + const kubectlSecurityGroup = SecurityGroup.fromSecurityGroupId( + scope, + `${id}-KubectlSecurityGroup`, + kubectlSecurityGroupId, + ); + const openIdConnectProvider = OpenIdConnectProvider.fromOpenIdConnectProviderArn( + scope, + `${id}-OpenIdConnectProvider`, + openIdConnectProviderArn, + ); + + const clusterSecurityGroup = SecurityGroup.fromSecurityGroupId( + scope, + `${id}-ClusterSecurityGroup`, + securityGroupId, + ); + + const cluster = Cluster.fromClusterAttributes(scope, `${id}-Cluster`, { + clusterName: clusterName, + kubectlRoleArn: kubectlRole.roleArn, + kubectlLambdaRole: kubectlLambdaRole, + openIdConnectProvider: openIdConnectProvider, + kubectlSecurityGroupId: kubectlSecurityGroup.securityGroupId, + kubectlLayer: new KubectlV33Layer(scope, 'kubectl'), + clusterSecurityGroupId: clusterSecurityGroup.securityGroupId, + }); + + const securityGroup = SecurityGroup.fromSecurityGroupId(scope, `${id}-SecurityGroup`, securityGroupId); + + return { cluster, securityGroup }; + } +} diff --git a/src/cdk/lib/constructs/eventbus.ts b/src/cdk/lib/constructs/eventbus.ts new file mode 100644 index 00000000..777be909 --- /dev/null +++ b/src/cdk/lib/constructs/eventbus.ts @@ -0,0 +1,84 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +import { CfnOutput, Fn } from 'aws-cdk-lib'; +import { EventBus, IEventBus } from 'aws-cdk-lib/aws-events'; +import { Construct } from 'constructs'; +import { EVENTBUS_ARN_EXPORT_NAME, EVENTBUS_NAME_EXPORT_NAME } from '../../bin/constants'; + +/** + * Properties for configuring EventBusResources construct + * @interface EventBusResourcesProperties + */ +export interface EventBusResourcesProperties { + /** + * The name of the EventBus + * @default 'workshop-eventbus' + */ + eventBusName?: string; + /** + * Description for the EventBus + * @default 'Workshop EventBus for cross-service communication' + */ + description?: string; +} + +/** + * AWS CDK Construct that creates EventBridge EventBus resources + * @class EventBusResources + * @extends Construct + */ +export class EventBusResources extends Construct { + /** + * The EventBridge EventBus + * @public + */ + public eventBus: EventBus; + + /** + * Creates a new EventBusResources construct + * @param scope - The parent construct + * @param id - The construct ID + * @param properties - Configuration properties for the construct + */ + constructor(scope: Construct, id: string, properties?: EventBusResourcesProperties) { + super(scope, id); + + this.eventBus = new EventBus(this, 'WorkshopEventBus', { + eventBusName: properties?.eventBusName || 'workshop-eventbus', + description: properties?.description || 'Workshop EventBus for cross-service communication', + }); + + // Create CloudFormation outputs for EventBus resources + this.createEventBusOutputs(); + } + + /** + * Imports EventBus resources from CloudFormation exports created by EventBusResources + * + * @param scope - The construct scope where the resources will be imported + * @param id - The construct identifier for the imported resources + * @returns Object containing the imported EventBus + */ + public static importFromExports(scope: Construct, id: string): { eventBus: IEventBus } { + const eventBusArn = Fn.importValue(EVENTBUS_ARN_EXPORT_NAME); + const eventBus = EventBus.fromEventBusArn(scope, `${id}-EventBus`, eventBusArn); + + return { eventBus }; + } + + /** + * Creates CloudFormation outputs for EventBus resources + */ + private createEventBusOutputs() { + new CfnOutput(this, 'EventBusArn', { + value: this.eventBus.eventBusArn, + exportName: EVENTBUS_ARN_EXPORT_NAME, + }); + new CfnOutput(this, 'EventBusName', { + value: this.eventBus.eventBusName, + exportName: EVENTBUS_NAME_EXPORT_NAME, + }); + } +} diff --git a/src/cdk/lib/constructs/index.ts b/src/cdk/lib/constructs/index.ts new file mode 100644 index 00000000..311f268c --- /dev/null +++ b/src/cdk/lib/constructs/index.ts @@ -0,0 +1,17 @@ +export * from './assets'; +export * from './cloudtrail'; +export * from './database'; +export * from './dynamodb'; +export * from './ecs'; +export * from './ecs-service'; +export * from './eks'; +export * from './eks-deployment'; +export * from './eventbus'; +export * from './lambda'; +export * from './microservice'; +export * from './network'; +export * from './opensearch-application'; +export * from './opensearch-collection'; +export * from './queue'; +export * from './vpc-endpoints'; +export * from './canary'; diff --git a/src/cdk/lib/constructs/lambda.ts b/src/cdk/lib/constructs/lambda.ts new file mode 100644 index 00000000..d81cc7ee --- /dev/null +++ b/src/cdk/lib/constructs/lambda.ts @@ -0,0 +1,321 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +/** + * Lambda function constructs for the One Observability Workshop. + * + * This module provides abstract base classes and interfaces for creating + * Lambda functions with consistent configuration and best practices for + * observability, security, and performance. + * + * @packageDocumentation + */ + +import { Runtime, Function, ILayerVersion, Architecture } from 'aws-cdk-lib/aws-lambda'; +import { BundlingOptions, NodejsFunction } from 'aws-cdk-lib/aws-lambda-nodejs'; +import { LogGroup, RetentionDays } from 'aws-cdk-lib/aws-logs'; +import { Rule, Schedule } from 'aws-cdk-lib/aws-events'; +import { LambdaFunction } from 'aws-cdk-lib/aws-events-targets'; +import { Construct } from 'constructs'; +import { + PETFOOD_CLEANUP_PROCESSOR_FUNCTION, + PETFOOD_IMAGE_GENERATOR_FUNCTION, + STATUS_UPDATER_FUNCTION, + TRAFFIC_GENERATOR_FUNCTION, +} from '../../bin/environment'; +import { Queue } from 'aws-cdk-lib/aws-sqs'; +import { PythonFunction } from '@aws-cdk/aws-lambda-python-alpha'; +import { RemovalPolicy } from 'aws-cdk-lib'; + +/** + * Gets the OpenTelemetry Python layer ARN for the specified region. + * @param region - AWS region + * @returns Complete ARN for the OpenTelemetry Python layer + */ +export function getOpenTelemetryPythonLayerArn(region: string): string { + const layerMappings: Record = { + 'us-east-1': { account: '615299751070', version: '16' }, + 'us-east-2': { account: '615299751070', version: '13' }, + 'us-west-1': { account: '615299751070', version: '20' }, + 'us-west-2': { account: '615299751070', version: '20' }, + 'af-south-1': { account: '904233096616', version: '10' }, + 'ap-east-1': { account: '888577020596', version: '10' }, + 'ap-south-2': { account: '796973505492', version: '10' }, + 'ap-southeast-3': { account: '039612877180', version: '10' }, + 'ap-southeast-4': { account: '713881805771', version: '10' }, + 'ap-southeast-5': { account: '152034782359', version: '1' }, + 'ap-southeast-7': { account: '980416031188', version: '1' }, + 'ap-south-1': { account: '615299751070', version: '13' }, + 'ap-northeast-3': { account: '615299751070', version: '12' }, + 'ap-northeast-2': { account: '615299751070', version: '13' }, + 'ap-southeast-1': { account: '615299751070', version: '12' }, + 'ap-southeast-2': { account: '615299751070', version: '13' }, + 'ap-northeast-1': { account: '615299751070', version: '13' }, + 'ca-central-1': { account: '615299751070', version: '13' }, + 'ca-west-1': { account: '595944127152', version: '1' }, + 'eu-central-1': { account: '615299751070', version: '13' }, + 'eu-west-1': { account: '615299751070', version: '13' }, + 'eu-west-2': { account: '615299751070', version: '13' }, + 'eu-south-1': { account: '257394471194', version: '10' }, + 'eu-west-3': { account: '615299751070', version: '13' }, + 'eu-south-2': { account: '490004653786', version: '10' }, + 'eu-north-1': { account: '615299751070', version: '13' }, + 'eu-central-2': { account: '156041407956', version: '10' }, + 'il-central-1': { account: '746669239226', version: '10' }, + 'me-south-1': { account: '980921751758', version: '10' }, + 'me-central-1': { account: '739275441131', version: '10' }, + 'sa-east-1': { account: '615299751070', version: '13' }, + 'mx-central-1': { account: '610118373846', version: '1' }, + }; + + const mapping = layerMappings[region]; + if (!mapping) { + throw new Error(`OpenTelemetry Python layer not available in region: ${region}`); + } + + return `arn:aws:lambda:${region}:${mapping.account}:layer:AWSOpenTelemetryDistroPython:${mapping.version}`; +} + +/** + * Gets the Lambda Insights layer ARN for the specified region. + * @param region - AWS region + * @returns Complete ARN for the Lambda Insights layer + */ +export function getLambdaInsightsLayerArn(region: string): string { + const layerMappings: Record = { + 'us-east-1': { account: '580247275435', version: '56' }, + 'us-east-2': { account: '580247275435', version: '56' }, + 'us-west-1': { account: '580247275435', version: '56' }, + 'us-west-2': { account: '580247275435', version: '56' }, + 'af-south-1': { account: '012438385374', version: '47' }, + 'ap-southeast-7': { account: '761018874580', version: '3' }, + 'ap-east-1': { account: '519774774795', version: '47' }, + 'ap-south-2': { account: '891564319516', version: '29' }, + 'ap-southeast-3': { account: '439286490199', version: '33' }, + 'ap-southeast-5': { account: '590183865173', version: '4' }, + 'ap-southeast-4': { account: '158895979263', version: '24' }, + 'ap-south-1': { account: '580247275435', version: '54' }, + 'ap-northeast-3': { account: '194566237122', version: '37' }, + 'ap-northeast-2': { account: '580247275435', version: '55' }, + 'ap-southeast-1': { account: '580247275435', version: '56' }, + 'ap-southeast-2': { account: '580247275435', version: '56' }, + 'ap-northeast-1': { account: '580247275435', version: '83' }, + 'ca-central-1': { account: '580247275435', version: '55' }, + 'ca-west-1': { account: '946466191631', version: '16' }, + 'cn-north-1': { account: '488211338238', version: '46', partition: 'aws-cn' }, + 'cn-northwest-1': { account: '488211338238', version: '46', partition: 'aws-cn' }, + 'eu-central-1': { account: '580247275435', version: '56' }, + 'eu-west-1': { account: '580247275435', version: '56' }, + 'eu-west-2': { account: '580247275435', version: '56' }, + 'eu-south-1': { account: '339249233099', version: '47' }, + 'eu-west-3': { account: '580247275435', version: '55' }, + 'eu-south-2': { account: '352183217350', version: '31' }, + 'eu-north-1': { account: '580247275435', version: '53' }, + 'eu-central-2': { account: '033019950311', version: '30' }, + 'il-central-1': { account: '459530977127', version: '23' }, + 'mx-central-1': { account: '879381266642', version: '3' }, + 'me-south-1': { account: '285320876703', version: '47' }, + 'me-central-1': { account: '732604637566', version: '30' }, + 'sa-east-1': { account: '580247275435', version: '55' }, + 'us-gov-east-1': { account: '122132214140', version: '24', partition: 'aws-us-gov' }, + 'us-gov-west-1': { account: '751350123760', version: '24', partition: 'aws-us-gov' }, + }; + + const mapping = layerMappings[region]; + if (!mapping) { + throw new Error(`Lambda Insights layer not available in region: ${region}`); + } + + const partition = mapping.partition || 'aws'; + return `arn:${partition}:lambda:${region}:${mapping.account}:layer:LambdaInsightsExtension:${mapping.version}`; +} + +/** + * Properties for configuring a workshop Lambda function. + */ +export interface WorkshopLambdaFunctionProperties { + /** Unique name for the Lambda function */ + name: string; + /** Runtime environment for the function */ + runtime: Runtime; + /** Path to the dependencies lock file (for Node.js functions) */ + depsLockFilePath?: string; + /** Entry point file for the function code */ + entry: string; + /** Memory allocation for the function in MB */ + memorySize: number; + /** Handler method name within the entry file */ + handler?: string; + /** The path (relative to entry) to the index file containing the exported handler. */ + index?: string; + /** Log retention period for CloudWatch logs */ + logRetentionDays?: RetentionDays; + /** Description of the function's purpose */ + description?: string; + /** + * The schedule expression for traffic generation + * @default 'rate(5 minute)' + */ + scheduleExpression?: string; + /** + * Whether to enable the EventBridge schedule + * @default false + */ + enableSchedule?: boolean; +} + +/** + * Predefined Lambda function names used throughout the workshop. + */ +export const LambdaFunctionNames = { + /** Pet status updater function name */ + StatusUpdater: STATUS_UPDATER_FUNCTION.name, + TrafficGenerator: TRAFFIC_GENERATOR_FUNCTION.name, + PetfoodImageGenerator: PETFOOD_IMAGE_GENERATOR_FUNCTION.name, + PetfoodCleanupProcessor: PETFOOD_CLEANUP_PROCESSOR_FUNCTION.name, +} as const; + +/** + * Abstract base class for workshop Lambda functions. + * + * This class provides a common foundation for creating Lambda functions + * with consistent configuration, observability features, and security + * best practices. Concrete implementations must provide specific + * permissions, environment variables, and bundling configurations. + */ +export abstract class WokshopLambdaFunction extends Construct { + /** The Lambda function instance */ + public function: Function; + + /** + * Creates a new workshop Lambda function. + * + * @param scope - The parent construct + * @param id - The construct identifier + * @param properties - Configuration properties for the function + * @throws Error if the runtime is not supported + */ + constructor(scope: Construct, id: string, properties: WorkshopLambdaFunctionProperties) { + super(scope, id); + + const logGroup = new LogGroup(this, 'LogGroup', { + logGroupName: `/aws/lambda/${properties.name}`, + retention: properties.logRetentionDays ?? RetentionDays.ONE_DAY, + removalPolicy: RemovalPolicy.DESTROY, + }); + + if (properties.runtime.name.startsWith('nodejs')) { + /** NodeJS Lambda function */ + if (!properties.handler) { + throw new Error('Handler must be specified for Node.js functions'); + } + + this.function = new NodejsFunction(this, `${properties.name}-function`, { + runtime: properties.runtime, + architecture: Architecture.X86_64, + depsLockFilePath: properties.depsLockFilePath, + entry: properties.entry, + handler: properties.handler, + memorySize: properties.memorySize, + logGroup: logGroup, + layers: this.getLayers(properties), + environment: this.getEnvironmentVariables(properties), + bundling: this.getBundling(properties), + deadLetterQueueEnabled: true, + deadLetterQueue: new Queue(this, 'DeadLetterQueue', { + queueName: `${properties.name}-dlq`, + enforceSSL: true, + }), + }); + } else if (properties.runtime.name.startsWith('python')) { + /** Python Lambda function */ + if (!properties.index) { + throw new Error('Index must be specified for Python functions'); + } + + this.function = new PythonFunction(this, `${properties.name}-function`, { + runtime: properties.runtime, + architecture: Architecture.X86_64, + index: properties.index, + entry: properties.entry, + memorySize: properties.memorySize, + logGroup: logGroup, + layers: this.getLayers(properties), + environment: this.getEnvironmentVariables(properties), + deadLetterQueueEnabled: true, + deadLetterQueue: new Queue(this, 'DeadLetterQueue', { + queueName: `${properties.name}-dlq`, + enforceSSL: true, + }), + }); + } else { + throw new Error(`Runtime ${properties.runtime.name} not supported`); + } + + if (properties.enableSchedule && properties.scheduleExpression) { + this.scheduleFunction(properties.scheduleExpression); + } + + this.addFunctionPermissions(properties); + } + + /** + * Use event bridge to schedule the function execution using the specified + * schedule expression + * @param scheduleExpression + */ + scheduleFunction(scheduleExpression: string) { + const rule = new Rule(this, 'ScheduleRule', { + schedule: Schedule.expression(scheduleExpression), + }); + + rule.addTarget(new LambdaFunction(this.function)); + } + + /** + * Adds IAM permissions required by the Lambda function. + * Must be implemented by concrete subclasses. + * + * @param properties - Function configuration properties + */ + abstract addFunctionPermissions(properties: WorkshopLambdaFunctionProperties): void; + + /** + * Creates CloudFormation outputs for the Lambda function. + * Must be implemented by concrete subclasses. + * + * @param properties - Function configuration properties + */ + abstract createOutputs(properties: WorkshopLambdaFunctionProperties): void; + + /** + * Returns environment variables for the Lambda function. + * Must be implemented by concrete subclasses. + * + * @param properties - Function configuration properties + * @returns Map of environment variable names to values + */ + abstract getEnvironmentVariables( + properties: WorkshopLambdaFunctionProperties, + ): { [key: string]: string } | undefined; + + /** + * Returns Lambda layers to be attached to the function. + * Must be implemented by concrete subclasses. + * + * @param properties - Function configuration properties + * @returns Array of Lambda layer versions + */ + abstract getLayers(properties: WorkshopLambdaFunctionProperties): ILayerVersion[]; + + /** + * Returns bundling options for the Lambda function code. + * Must be implemented by concrete subclasses. + * + * @param properties - Function configuration properties + * @returns Bundling configuration options + */ + abstract getBundling(properties: WorkshopLambdaFunctionProperties): BundlingOptions; +} diff --git a/src/cdk/lib/constructs/microservice.ts b/src/cdk/lib/constructs/microservice.ts new file mode 100644 index 00000000..73977855 --- /dev/null +++ b/src/cdk/lib/constructs/microservice.ts @@ -0,0 +1,98 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +import { ISecurityGroup, IVpc, SubnetType } from 'aws-cdk-lib/aws-ec2'; +import { Construct } from 'constructs'; +import { ICluster as IEKSCluster } from 'aws-cdk-lib/aws-eks'; +import { ICluster as IECSCluster } from 'aws-cdk-lib/aws-ecs'; +import { RetentionDays } from 'aws-cdk-lib/aws-logs'; +import { + PAYFORADOPTION_GO, + PETLISTADOPTIONS_PY, + PETSEARCH_JAVA, + PETSITE_NET, + PETFOOD_RS, + HostType, + ComputeType, +} from '../../bin/environment'; +import { Effect, PolicyStatement } from 'aws-cdk-lib/aws-iam'; +import { Stack } from 'aws-cdk-lib'; + +export const MicroservicesNames = { + PayForAdoption: PAYFORADOPTION_GO.name, + PetListAdoptions: PETLISTADOPTIONS_PY.name, + PetSearch: PETSEARCH_JAVA.name, + PetSite: PETSITE_NET.name, + PetFood: PETFOOD_RS.name, +} as const; + +export interface MicroserviceProperties { + hostType: HostType; + computeType: ComputeType; + tags?: { [key: string]: string }; + securityGroup?: ISecurityGroup; + vpc?: IVpc; + eksCluster?: IEKSCluster; + ecsCluster?: IECSCluster; + /** Default Log Retention */ + logRetentionDays?: RetentionDays; + name: string; + repositoryURI: string; + disableService?: boolean; + logGroupName?: string; + healthCheck?: string; + subnetType?: SubnetType; + listenerPort?: number; + containerPort?: number; + createLoadBalancer?: boolean; +} + +export abstract class Microservice extends Construct { + constructor(scope: Construct, id: string, properties: MicroserviceProperties) { + super(scope, id); + + if (properties.hostType == HostType.ECS && !properties.ecsCluster) { + throw new Error('ecsCluster is required if host type is ECS'); + } + if (properties.hostType == HostType.EKS && !properties.eksCluster) { + throw new Error('eksCluster is required if host type is EKS'); + } + } + + abstract configureEKSService(properties: MicroserviceProperties): void; + + abstract configureECSService(properties: MicroserviceProperties): void; + + abstract addPermissions(properties: MicroserviceProperties): void; + + abstract createOutputs(properties: MicroserviceProperties): void; + + readonly ddbSeedPolicy = new PolicyStatement({ + effect: Effect.ALLOW, + actions: ['dynamodb:BatchWriteItem', 'dynamodb:ListTables', 'dynamodb:Scan', 'dynamodb:Query'], + resources: ['*'], + }); + + public static getDefaultEventBridgePolicy(scope: Construct) { + const publishEventPolicy = new PolicyStatement({ + effect: Effect.ALLOW, + actions: ['events:PutEvents'], + resources: [`arn:aws:events:${Stack.of(scope).region}:${Stack.of(scope).account}:event-bus/default`], + }); + return publishEventPolicy; + } + + public static getDefaultSSMPolicy(scope: Construct, prefix?: string) { + const cleanPrefix = (prefix || '/petstore/').startsWith('/') + ? (prefix || '/petstore/').slice(1) + : prefix || '/petstore/'; + const readSMParametersPolicy = new PolicyStatement({ + effect: Effect.ALLOW, + actions: ['ssm:GetParametersByPath', 'ssm:GetParameters', 'ssm:GetParameter'], + resources: [`arn:aws:ssm:${Stack.of(scope).region}:${Stack.of(scope).account}:parameter/${cleanPrefix}*`], + }); + + return readSMParametersPolicy; + } +} diff --git a/src/cdk/lib/constructs/network.ts b/src/cdk/lib/constructs/network.ts new file mode 100644 index 00000000..84f5a1a8 --- /dev/null +++ b/src/cdk/lib/constructs/network.ts @@ -0,0 +1,339 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +import { Construct } from 'constructs'; +import { + Vpc, + IpAddresses, + FlowLog, + FlowLogDestination, + FlowLogResourceType, + LogFormat, + SubnetType, + IVpc, +} from 'aws-cdk-lib/aws-ec2'; +import { LogGroup, RetentionDays } from 'aws-cdk-lib/aws-logs'; +import { Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam'; +import { + CfnResolverQueryLoggingConfig, + CfnResolverQueryLoggingConfigAssociation, +} from 'aws-cdk-lib/aws-route53resolver'; +import { CfnOutput, Fn, RemovalPolicy } from 'aws-cdk-lib'; +import { VpcEndpoints } from './vpc-endpoints'; +import { MAX_AVAILABILITY_ZONES } from '../../bin/environment'; +import { + VPC_AVAILABILITY_ZONES_EXPORT_NAME, + VPC_CIDR_EXPORT_NAME, + VPC_ID_EXPORT_NAME, + VPC_ISOLATED_SUBNETS_EXPORT_NAME, + VPC_PRIVATE_SUBNETS_EXPORT_NAME, + VPC_PUBLIC_SUBNETS_EXPORT_NAME, + VPC_PRIVATE_SUBNET_CIDRS_EXPORT_NAME, + VPC_PUBLIC_SUBNET_CIDRS_EXPORT_NAME, + VPC_ISOLATED_SUBNET_CIDRS_EXPORT_NAME, + CLOUDMAP_NAMESPACE_ID_EXPORT_NAME, + CLOUDMAP_NAMESPACE_NAME_EXPORT_NAME, + CLOUDMAP_NAMESPACE_ARN_EXPORT_NAME, +} from '../../bin/constants'; +import { PrivateDnsNamespace, IPrivateDnsNamespace } from 'aws-cdk-lib/aws-servicediscovery'; +import { Utilities } from '../utils/utilities'; + +/** + * Properties for the WorkshopNetwork construct + */ +export interface WorkshopNetworkProperties { + /** The name identifier for the network resources */ + name: string; + /** The CIDR range for the VPC (e.g., '10.0.0.0/16') */ + cidrRange: string; + /** Whether to enable VPC Flow Logs */ + enableFlowLogs?: boolean; + /** Whether to enable DNS Query Resolver Logs for the VPC*/ + enableDnsQueryResolverLogs?: boolean; + /** Default Log Retention */ + logRetentionDays?: RetentionDays; +} + +/** + * A CDK construct that creates a VPC with public and private subnets, + * NAT gateway, and VPC Flow Logs for the observability workshop + */ +export class WorkshopNetwork extends Construct { + /** The VPC instance created by this construct */ + public readonly vpc: Vpc; + /** The VPC endpoints created by this construct */ + public readonly vpcEndpoints: VpcEndpoints; + /** Cloud Map domain */ + public readonly cloudMapNamespace: PrivateDnsNamespace; + + /** + * Creates a new WorkshopNetwork construct + * @param scope - The parent construct + * @param id - The construct identifier + * @param properties - Configuration properties for the network + */ + constructor(scope: Construct, id: string, properties: WorkshopNetworkProperties) { + super(scope, id); + + // Create a VPC with public and private subnets + // The VPC where all the microservices will be deployed into + this.vpc = new Vpc(this, 'VPC-' + properties.name, { + ipAddresses: IpAddresses.cidr(properties.cidrRange), + natGateways: 1, + maxAzs: MAX_AVAILABILITY_ZONES, + subnetConfiguration: [ + { + name: 'Public', + cidrMask: 24, + subnetType: SubnetType.PUBLIC, + mapPublicIpOnLaunch: false, + }, + { + name: 'Private', + cidrMask: 24, + subnetType: SubnetType.PRIVATE_WITH_EGRESS, + }, + { + name: 'Isolated', + cidrMask: 28, + subnetType: SubnetType.PRIVATE_ISOLATED, + }, + ], + }); + + /** Add tags for EKS auto-discovery */ + for (const subnet of this.vpc.publicSubnets) { + Utilities.TagConstruct(subnet, { + 'kubernetes.io/role/elb': '1', + }); + } + for (const subnet of this.vpc.privateSubnets) { + Utilities.TagConstruct(subnet, { + 'kubernetes.io/role/internal-elb': '1', + }); + } + + if (properties.enableFlowLogs) { + this.enableFlowLogs(properties.logRetentionDays || RetentionDays.ONE_WEEK); + } + + if (properties.enableDnsQueryResolverLogs) { + this.enableDnsQueryResolverLogs(properties.logRetentionDays || RetentionDays.ONE_WEEK); + } + + // Create VPC endpoints + this.vpcEndpoints = new VpcEndpoints(this, 'VpcEndpoints', { + vpc: this.vpc, + }); + + // Create Cloudmap namespace + this.cloudMapNamespace = new PrivateDnsNamespace(this, 'CloudMapNamespace', { + name: `${properties.name}-space`, + description: 'Cloud Map namespace for ' + properties.name, + vpc: this.vpc, + }); + + // Create CloudFormation outputs for VPC resources + this.createVpcOutputs(); + this.createCloudMapOutputs(); + } + + /** + * Enables DNS query resolver logs for the VPC + * @param retention - Log retention period + */ + private enableDnsQueryResolverLogs(retention: RetentionDays) { + const resolverLogGroup = new LogGroup(this, 'ResolverLogGroup', { + logGroupName: '/aws/vpc/dns-query-resolver-logs/' + this.vpc.vpcId, + retention: retention, + }); + + const cfnResovlerQueryConfig = new CfnResolverQueryLoggingConfig(this, 'ResolverQueryLogConfig', { + destinationArn: resolverLogGroup.logGroupArn, + name: 'ResolverQueryLogConfig', + }); + + new CfnResolverQueryLoggingConfigAssociation(this, 'ResolverQueryLogConfigAssociation', { + resolverQueryLogConfigId: cfnResovlerQueryConfig.ref, + resourceId: this.vpc.vpcId, + }); + } + + /** + * Enables VPC Flow Logs with comprehensive log format + * @param retention - Log retention period + */ + private enableFlowLogs(retention: RetentionDays) { + const flowLogGroup = new LogGroup(this, 'FlowLogGroup', { + logGroupName: '/aws/vpcflowlogs/' + this.vpc.vpcId, + retention: retention, + removalPolicy: RemovalPolicy.DESTROY, + }); + + const role = new Role(this, 'VPCFlowLogRole', { + assumedBy: new ServicePrincipal('vpc-flow-logs.amazonaws.com'), + }); + + new FlowLog(this, 'VPCFlowLog', { + destination: FlowLogDestination.toCloudWatchLogs(flowLogGroup, role), + resourceType: FlowLogResourceType.fromVpc(this.vpc), + logFormat: [ + LogFormat.ACCOUNT_ID, + LogFormat.ACTION, + LogFormat.AZ_ID, + LogFormat.BYTES, + LogFormat.DST_ADDR, + LogFormat.DST_PORT, + LogFormat.END_TIMESTAMP, + LogFormat.FLOW_DIRECTION, + LogFormat.INSTANCE_ID, + LogFormat.INTERFACE_ID, + LogFormat.LOG_STATUS, + LogFormat.PACKETS, + LogFormat.PKT_DST_AWS_SERVICE, + LogFormat.PKT_DST_ADDR, + LogFormat.PKT_SRC_AWS_SERVICE, + LogFormat.PKT_SRC_ADDR, + LogFormat.PROTOCOL, + LogFormat.REGION, + LogFormat.SRC_ADDR, + LogFormat.SRC_PORT, + LogFormat.START_TIMESTAMP, + LogFormat.SUBLOCATION_ID, + LogFormat.SUBLOCATION_TYPE, + LogFormat.SUBNET_ID, + LogFormat.TCP_FLAGS, + LogFormat.TRAFFIC_PATH, + LogFormat.TRAFFIC_TYPE, + LogFormat.VERSION, + LogFormat.VPC_ID, + ], + }); + } + + /** + * Imports a VPC from CloudFormation exports created by WorkshopNetwork + * + * This static method reconstructs a VPC instance from CloudFormation exports, + * allowing other stacks to reference and use the VPC created by the core infrastructure. + * + * @param scope - The construct scope where the VPC will be imported + * @param id - The construct identifier for the imported VPC + * @returns The imported VPC instance with all subnet and availability zone information + * + * @example + * ```typescript + * const vpc = WorkshopNetwork.importVpcFromExports(this, 'ImportedVpc'); + * // Use vpc.privateSubnets, vpc.publicSubnets, etc. + * ``` + */ + public static importVpcFromExports(scope: Construct, id: string): IVpc { + const vpcId = Fn.importValue(VPC_ID_EXPORT_NAME); + const availabilityZones = Fn.importListValue(VPC_AVAILABILITY_ZONES_EXPORT_NAME, MAX_AVAILABILITY_ZONES, ','); + + const publicSubnetIds = Fn.importListValue(VPC_PUBLIC_SUBNETS_EXPORT_NAME, MAX_AVAILABILITY_ZONES, ','); + const privateSubnetIds = Fn.importListValue(VPC_PRIVATE_SUBNETS_EXPORT_NAME, MAX_AVAILABILITY_ZONES, ','); + const isolatedSubnetIds = Fn.importListValue(VPC_ISOLATED_SUBNETS_EXPORT_NAME, MAX_AVAILABILITY_ZONES, ','); + + const publicSubnetCidrs = Fn.importListValue(VPC_PUBLIC_SUBNET_CIDRS_EXPORT_NAME, MAX_AVAILABILITY_ZONES, ','); + const privateSubnetCidrs = Fn.importListValue( + VPC_PRIVATE_SUBNET_CIDRS_EXPORT_NAME, + MAX_AVAILABILITY_ZONES, + ',', + ); + const isolatedSubnetCidrs = Fn.importListValue( + VPC_ISOLATED_SUBNET_CIDRS_EXPORT_NAME, + MAX_AVAILABILITY_ZONES, + ',', + ); + + const vpcCidrBlock = Fn.importValue(VPC_CIDR_EXPORT_NAME); + + return Vpc.fromVpcAttributes(scope, id, { + vpcId: vpcId, + availabilityZones: availabilityZones, + privateSubnetIds: privateSubnetIds, + publicSubnetIds: publicSubnetIds, + isolatedSubnetIds: isolatedSubnetIds, + privateSubnetIpv4CidrBlocks: privateSubnetCidrs, + publicSubnetIpv4CidrBlocks: publicSubnetCidrs, + isolatedSubnetIpv4CidrBlocks: isolatedSubnetCidrs, + vpcCidrBlock: vpcCidrBlock, + }); + } + + /** + * Creates CloudFormation outputs for VPC resources + */ + private createVpcOutputs() { + new CfnOutput(this, 'VpcId', { value: this.vpc.vpcId, exportName: VPC_ID_EXPORT_NAME }); + new CfnOutput(this, 'VpcCidr', { value: this.vpc.vpcCidrBlock, exportName: VPC_CIDR_EXPORT_NAME }); + new CfnOutput(this, 'VpcPrivateSubnets', { + value: this.vpc.privateSubnets.map((s) => s.subnetId).join(','), + exportName: VPC_PRIVATE_SUBNETS_EXPORT_NAME, + }); + new CfnOutput(this, 'VpcPublicSubnets', { + value: this.vpc.publicSubnets.map((s) => s.subnetId).join(','), + exportName: VPC_PUBLIC_SUBNETS_EXPORT_NAME, + }); + new CfnOutput(this, 'VpcIsolatedSubnets', { + value: this.vpc.isolatedSubnets.map((s) => s.subnetId).join(','), + exportName: VPC_ISOLATED_SUBNETS_EXPORT_NAME, + }); + new CfnOutput(this, 'VpcAvailabilityZones', { + value: this.vpc.availabilityZones.join(','), + exportName: VPC_AVAILABILITY_ZONES_EXPORT_NAME, + }); + new CfnOutput(this, 'VpcPrivateSubnetCidrs', { + value: this.vpc.privateSubnets.map((s) => s.ipv4CidrBlock).join(','), + exportName: VPC_PRIVATE_SUBNET_CIDRS_EXPORT_NAME, + }); + new CfnOutput(this, 'VpcPublicSubnetCidrs', { + value: this.vpc.publicSubnets.map((s) => s.ipv4CidrBlock).join(','), + exportName: VPC_PUBLIC_SUBNET_CIDRS_EXPORT_NAME, + }); + new CfnOutput(this, 'VpcIsolatedSubnetCidrs', { + value: this.vpc.isolatedSubnets.map((s) => s.ipv4CidrBlock).join(','), + exportName: VPC_ISOLATED_SUBNET_CIDRS_EXPORT_NAME, + }); + } + + /** + * Creates CloudFormation outputs for CloudMap namespace resources + */ + private createCloudMapOutputs() { + new CfnOutput(this, 'CloudMapNamespaceId', { + value: this.cloudMapNamespace.namespaceId, + exportName: CLOUDMAP_NAMESPACE_ID_EXPORT_NAME, + }); + + new CfnOutput(this, 'CloudMapNamespaceName', { + value: this.cloudMapNamespace.namespaceName, + exportName: CLOUDMAP_NAMESPACE_NAME_EXPORT_NAME, + }); + + new CfnOutput(this, 'CloudMapNamespaceArn', { + value: this.cloudMapNamespace.namespaceArn, + exportName: CLOUDMAP_NAMESPACE_ARN_EXPORT_NAME, + }); + } + + /** + * Imports a CloudMap namespace from CloudFormation exports + * @param scope - The construct scope + * @param id - The construct identifier + * @returns The imported CloudMap namespace + */ + public static importCloudMapNamespaceFromExports(scope: Construct, id: string): IPrivateDnsNamespace { + const namespaceId = Fn.importValue(CLOUDMAP_NAMESPACE_ID_EXPORT_NAME); + const namespaceName = Fn.importValue(CLOUDMAP_NAMESPACE_NAME_EXPORT_NAME); + const namespaceArn = Fn.importValue(CLOUDMAP_NAMESPACE_ARN_EXPORT_NAME); + + return PrivateDnsNamespace.fromPrivateDnsNamespaceAttributes(scope, id, { + namespaceId: namespaceId, + namespaceName: namespaceName, + namespaceArn: namespaceArn, + }); + } +} diff --git a/src/cdk/lib/constructs/opensearch-application.ts b/src/cdk/lib/constructs/opensearch-application.ts new file mode 100644 index 00000000..772b0e59 --- /dev/null +++ b/src/cdk/lib/constructs/opensearch-application.ts @@ -0,0 +1,140 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +import { CfnOutput, Fn } from 'aws-cdk-lib'; +import { CfnApplication } from 'aws-cdk-lib/aws-opensearchservice'; +import { Construct } from 'constructs'; +import { OPENSEARCH_APPLICATION_ARN_EXPORT_NAME, OPENSEARCH_APPLICATION_ID_EXPORT_NAME } from '../../bin/constants'; +import { OpenSearchCollection } from './opensearch-collection'; +import { Utilities } from '../utils/utilities'; +import { PARAMETER_STORE_PREFIX } from '../../bin/environment'; + +/** + * Properties for configuring OpenSearchApplication construct + * @interface OpenSearchApplicationProperties + */ +export interface OpenSearchApplicationProperties { + /** + * Name of the OpenSearch UI Application + * @default 'petadoption-ui-app' + */ + applicationName?: string; + /** + * The OpenSearch collection to use as data source + */ + collection: OpenSearchCollection; + /** + * Application configuration settings + * @optional + */ + appConfig?: { [key: string]: string }; + /** + * IAM Identity Center options + * @optional + */ + iamIdentityCenterOptions?: { + enabled?: boolean; + identityStoreId?: string; + }; +} + +/** + * AWS CDK Construct that creates OpenSearch UI Application for pet adoption data visualization + * @class OpenSearchApplication + * @extends Construct + */ +export class OpenSearchApplication extends Construct { + /** + * The OpenSearch UI Application for visualizing pet adoption data + * @public + */ + public application: CfnApplication; + + /** + * The application endpoint URL + * @public + */ + public applicationEndpoint: string; + + /** + * Creates a new OpenSearchApplication construct with UI application + * @param scope - The parent construct + * @param id - The construct ID + * @param properties - Configuration properties for the construct (required) + */ + constructor(scope: Construct, id: string, properties: OpenSearchApplicationProperties) { + super(scope, id); + + const applicationName = properties.applicationName || 'petadoption-opensearch-ui'; + const collection = properties.collection; + + // Create the OpenSearch UI Application + this.application = new CfnApplication(this, 'Application', { + name: applicationName, + appConfigs: [ + { + key: 'opensearchDashboards.dashboardAdmin.users', + value: '*', + }, + ], + dataSources: [ + { + dataSourceArn: collection.collection.attrArn, + }, + ], + iamIdentityCenterOptions: properties.iamIdentityCenterOptions || { + enabled: false, + }, + }); + + // Add dependency to ensure collection is created before application + this.application.addDependency(collection.collection); + this.application.addDependency(collection.accessPolicy); + + this.createExports(); + this.createOutputs(); + } + + private createExports(): void { + new CfnOutput(this, 'ApplicationArn', { + value: this.application.attrArn, + exportName: OPENSEARCH_APPLICATION_ARN_EXPORT_NAME, + }); + + new CfnOutput(this, 'ApplicationId', { + value: this.application.attrId, + exportName: OPENSEARCH_APPLICATION_ID_EXPORT_NAME, + }); + } + + public static importFromExports(): { + applicationArn: string; + applicationId: string; + } { + const applicationArn = Fn.importValue(OPENSEARCH_APPLICATION_ARN_EXPORT_NAME); + const applicationId = Fn.importValue(OPENSEARCH_APPLICATION_ID_EXPORT_NAME); + + return { + applicationArn, + applicationId, + }; + } + + createOutputs(): void { + if (this.application) { + Utilities.createSsmParameters( + this, + PARAMETER_STORE_PREFIX, + new Map( + Object.entries({ + opensearchapplicationarn: this.application.attrArn, + opensearchapplicationid: this.application.attrId, + }), + ), + ); + } else { + throw new Error('OpenSearch Ui is not available'); + } + } +} diff --git a/src/cdk/lib/constructs/opensearch-collection.ts b/src/cdk/lib/constructs/opensearch-collection.ts new file mode 100644 index 00000000..0088cfc8 --- /dev/null +++ b/src/cdk/lib/constructs/opensearch-collection.ts @@ -0,0 +1,283 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +import { CfnOutput, Fn, Stack } from 'aws-cdk-lib'; +import { CfnCollection, CfnSecurityPolicy, CfnAccessPolicy } from 'aws-cdk-lib/aws-opensearchserverless'; +import { IRole } from 'aws-cdk-lib/aws-iam'; +import { Construct } from 'constructs'; +import { + OPENSEARCH_COLLECTION_ARN_EXPORT_NAME, + OPENSEARCH_COLLECTION_ID_EXPORT_NAME, + OPENSEARCH_COLLECTION_ENDPOINT_EXPORT_NAME, +} from '../../bin/constants'; +import { Utilities } from '../utils/utilities'; +import { PARAMETER_STORE_PREFIX } from '../../bin/environment'; + +/** + * Properties for configuring OpenSearchCollection construct + * @interface OpenSearchCollectionProperties + */ +export interface OpenSearchCollectionProperties { + /** + * Name of the OpenSearch Serverless collection + * @default 'petadoption-collection' + */ + collectionName?: string; + /** + * Description for the OpenSearch Serverless collection + * @default 'Pet adoption data collection' + */ + description?: string; + /** + * Type of the OpenSearch Serverless collection + * @default 'TIMESERIES' + */ + type?: 'SEARCH' | 'TIMESERIES' | 'VECTORSEARCH'; + /** + * Roles that need access to ingest data into the OpenSearch collection + * @optional + */ + ingestionRoles?: IRole[]; +} + +/** + * AWS CDK Construct that creates OpenSearch Serverless collection with CloudWatch alarms for pet adoption + * @class OpenSearchCollection + * @extends Construct + */ +export class OpenSearchCollection extends Construct { + /** + * The OpenSearch Serverless collection for storing pet adoption data + * @public + */ + public collection: CfnCollection; + + /** + * The security policy for the collection + * @public + */ + public securityPolicy: CfnSecurityPolicy; + + /** + * The access policy for the collection + * @public + */ + public accessPolicy: CfnAccessPolicy; + + /** + * Creates a new OpenSearchCollection construct with collection + * @param scope - The parent construct + * @param id - The construct ID + * @param properties - Configuration properties for the construct (optional) + */ + constructor(scope: Construct, id: string, properties?: OpenSearchCollectionProperties) { + super(scope, id); + + const collectionName = properties?.collectionName || 'pet-collection'; + const description = properties?.description || 'Pet adoption data collection'; + const type = properties?.type || 'TIMESERIES'; + + // Create security policy for encryption + this.securityPolicy = new CfnSecurityPolicy(this, 'SecurityPolicy', { + name: `${collectionName}-sec-policy`, + type: 'encryption', + policy: JSON.stringify({ + Rules: [ + { + ResourceType: 'collection', + Resource: [`collection/${collectionName}`], + }, + ], + AWSOwnedKey: true, + }), + }); + + // Create network security policy + const networkSecurityPolicy = new CfnSecurityPolicy(this, 'NetworkSecurityPolicy', { + name: `${collectionName}-net-policy`, + type: 'network', + policy: JSON.stringify([ + { + Rules: [ + { + ResourceType: 'collection', + Resource: [`collection/${collectionName}`], + }, + { + ResourceType: 'dashboard', + Resource: [`collection/${collectionName}`], + }, + ], + AllowFromPublic: true, + }, + ]), + }); + + // Create the OpenSearch Serverless collection + this.collection = new CfnCollection(this, 'Collection', { + name: collectionName, + description: description, + type: type, + }); + + // Add dependencies + this.collection.addDependency(this.securityPolicy); + this.collection.addDependency(networkSecurityPolicy); + + // Get the AWS account ID from the stack + const accountId = Stack.of(this).account; + + // Allow all principals in the account to access the collection. + const principals: string[] = [`arn:aws:iam::${accountId}:root`]; + + // Create access policy for the collection + this.accessPolicy = new CfnAccessPolicy(this, 'AccessPolicy', { + name: `${collectionName}-acc-policy`, + type: 'data', + policy: JSON.stringify([ + { + Rules: [ + { + ResourceType: 'collection', + Resource: [`collection/${collectionName}`], + Permission: [ + 'aoss:CreateCollectionItems', + 'aoss:DeleteCollectionItems', + 'aoss:UpdateCollectionItems', + 'aoss:DescribeCollectionItems', + ], + }, + { + ResourceType: 'index', + Resource: [`index/${collectionName}/*`], + Permission: [ + 'aoss:CreateIndex', + 'aoss:DeleteIndex', + 'aoss:UpdateIndex', + 'aoss:DescribeIndex', + 'aoss:ReadDocument', + 'aoss:WriteDocument', + ], + }, + ], + Principal: principals, + }, + ]), + }); + + // Add dependency for access policy + this.accessPolicy.addDependency(this.collection); + + this.createExports(); + this.createOutputs(); + } + + private createExports(): void { + new CfnOutput(this, 'CollectionArn', { + value: this.collection.attrArn, + exportName: OPENSEARCH_COLLECTION_ARN_EXPORT_NAME, + }); + + new CfnOutput(this, 'CollectionId', { + value: this.collection.attrId, + exportName: OPENSEARCH_COLLECTION_ID_EXPORT_NAME, + }); + + new CfnOutput(this, 'CollectionEndpoint', { + value: this.collection.attrCollectionEndpoint, + exportName: OPENSEARCH_COLLECTION_ENDPOINT_EXPORT_NAME, + }); + + // Export access policy name for updates + new CfnOutput(this, 'AccessPolicyName', { + value: this.accessPolicy.name!, + exportName: `${OPENSEARCH_COLLECTION_ARN_EXPORT_NAME}-AccessPolicy`, + }); + } + + public static importFromExports(): { + collectionArn: string; + collectionId: string; + collectionEndpoint: string; + } { + const collectionArn = Fn.importValue(OPENSEARCH_COLLECTION_ARN_EXPORT_NAME); + const collectionId = Fn.importValue(OPENSEARCH_COLLECTION_ID_EXPORT_NAME); + const collectionEndpoint = Fn.importValue(OPENSEARCH_COLLECTION_ENDPOINT_EXPORT_NAME); + + return { + collectionArn, + collectionId, + collectionEndpoint, + }; + } + + /** + * Add additional ingestion roles to the access policy + * @param roles - Array of IAM roles to grant access + */ + public addIngestionRoles(roles: IRole[]): void { + const accountId = Stack.of(this).account; + const collectionName = this.collection.name; + + // Get current principals + const currentPolicy = JSON.parse(this.accessPolicy.policy as string); + const currentPrincipals = currentPolicy[0].Principal || []; + + // Add new role ARNs + roles.forEach(role => { + if (!currentPrincipals.includes(role.roleArn)) { + currentPrincipals.push(role.roleArn); + } + }); + + // Update the access policy + this.accessPolicy.policy = JSON.stringify([ + { + Rules: [ + { + ResourceType: 'collection', + Resource: [`collection/${collectionName}`], + Permission: [ + 'aoss:CreateCollectionItems', + 'aoss:DeleteCollectionItems', + 'aoss:UpdateCollectionItems', + 'aoss:DescribeCollectionItems', + ], + }, + { + ResourceType: 'index', + Resource: [`index/${collectionName}/*`], + Permission: [ + 'aoss:CreateIndex', + 'aoss:DeleteIndex', + 'aoss:UpdateIndex', + 'aoss:DescribeIndex', + 'aoss:ReadDocument', + 'aoss:WriteDocument', + ], + }, + ], + Principal: currentPrincipals, + }, + ]); + } + + createOutputs(): void { + if (this.collection) { + Utilities.createSsmParameters( + this, + PARAMETER_STORE_PREFIX, + new Map( + Object.entries({ + opensearchcollectionarn: this.collection.attrArn, + opensearchcollectionid: this.collection.attrId, + opensearchcollectionendpoint: this.collection.attrCollectionEndpoint, + }), + ), + ); + } else { + throw new Error('OpenSearch collection is not available'); + } + } +} diff --git a/src/cdk/lib/constructs/opensearch-pipeline.ts b/src/cdk/lib/constructs/opensearch-pipeline.ts new file mode 100644 index 00000000..63924dad --- /dev/null +++ b/src/cdk/lib/constructs/opensearch-pipeline.ts @@ -0,0 +1,327 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +import { CfnOutput, Fn, Stack } from 'aws-cdk-lib'; +import { CfnPipeline } from 'aws-cdk-lib/aws-osis'; +import { Role, ServicePrincipal, PolicyStatement, Effect } from 'aws-cdk-lib/aws-iam'; +import { LogGroup, RetentionDays } from 'aws-cdk-lib/aws-logs'; +import { Construct } from 'constructs'; +import { + OPENSEARCH_PIPELINE_ARN_EXPORT_NAME, + OPENSEARCH_PIPELINE_ENDPOINT_EXPORT_NAME, + OPENSEARCH_PIPELINE_ROLE_ARN_EXPORT_NAME, +} from '../../bin/constants'; +import { Utilities } from '../utils/utilities'; +import { PARAMETER_STORE_PREFIX } from '../../bin/environment'; +import { OpenSearchCollection } from './opensearch-collection'; + +/** + * Properties for configuring OpenSearchPipeline construct + * @interface OpenSearchPipelineProperties + */ +export interface OpenSearchPipelineProperties { + /** + * Name of the OpenSearch Ingestion pipeline + * @default 'pet-logs-pipeline' + */ + pipelineName?: string; + + /** + * OpenSearch collection to send logs to + */ + openSearchCollection: + | OpenSearchCollection + | { + collectionArn: string; + collectionEndpoint: string; + }; + + /** + * Log buffer configuration + * @default { flushInterval: 60, batchSize: 1000 } + */ + bufferOptions?: { + flushInterval?: number; + batchSize?: number; + }; + + /** + * Index template for log organization + * @default 'logs-{yyyy.MM.dd}' + */ + indexTemplate?: string; + + /** + * Minimum and maximum pipeline capacity units + * @default { min: 1, max: 4 } + */ + capacityLimits?: { + min?: number; + max?: number; + }; +} + +/** + * AWS CDK Construct that creates OpenSearch Ingestion pipeline for log processing + * @class OpenSearchPipeline + * @extends Construct + */ +export class OpenSearchPipeline extends Construct { + /** + * The OpenSearch Ingestion pipeline for processing logs + * @public + */ + public readonly pipeline: CfnPipeline; + + /** + * The IAM role for the pipeline + * @public + */ + public readonly pipelineRole: Role; + + /** + * The pipeline endpoint URL + * @public + */ + public readonly pipelineEndpoint: string; + + /** + * Creates a new OpenSearchPipeline construct + * @param scope - The parent construct + * @param id - The construct ID + * @param properties - Configuration properties for the construct + */ + constructor(scope: Construct, id: string, properties: OpenSearchPipelineProperties) { + super(scope, id); + + // Validate required properties + if (!properties.openSearchCollection) { + throw new Error('OpenSearch collection is required for the pipeline'); + } + + // Set default values + const pipelineName = properties.pipelineName || 'pet-logs-pipeline'; + const bufferOptions = { + flushInterval: properties.bufferOptions?.flushInterval || 60, + batchSize: properties.bufferOptions?.batchSize || 1000, + }; + const indexTemplate = properties.indexTemplate || `${pipelineName}-logs`; + const capacityLimits = { + min: properties.capacityLimits?.min || 1, + max: properties.capacityLimits?.max || 4, + }; + + // Extract collection information + const collectionEndpoint = + 'collectionEndpoint' in properties.openSearchCollection + ? properties.openSearchCollection.collectionEndpoint + : properties.openSearchCollection.collection.attrCollectionEndpoint; + + const collectionArn = + 'collectionArn' in properties.openSearchCollection + ? properties.openSearchCollection.collectionArn + : properties.openSearchCollection.collection.attrArn; + + // Create IAM role for the pipeline + this.pipelineRole = new Role(this, 'PipelineRole', { + assumedBy: new ServicePrincipal('osis-pipelines.amazonaws.com'), + description: `IAM role for OpenSearch Ingestion pipeline ${pipelineName}`, + }); + + // Add permissions for OpenSearch Serverless access + this.pipelineRole.addToPolicy( + new PolicyStatement({ + effect: Effect.ALLOW, + actions: [ + 'aoss:*', // Required for pipeline to connect to collection + ], + resources: [collectionArn, `${collectionArn}/*`], + }), + ); + + // Add CloudWatch logging permissions + this.pipelineRole.addToPolicy( + new PolicyStatement({ + effect: Effect.ALLOW, + actions: ['logs:CreateLogStream', 'logs:PutLogEvents', 'logs:CreateLogGroup'], + resources: [ + `arn:aws:logs:${Stack.of(this).region}:${Stack.of(this).account}:log-group:/aws/vendedlogs/opensearch-ingestion/${pipelineName}*`, + ], + }), + ); + + // Add EventBridge permissions for pipeline lifecycle events + this.pipelineRole.addToPolicy( + new PolicyStatement({ + effect: Effect.ALLOW, + actions: ['events:PutEvents', 'events:DescribeRule', 'events:ListTargetsByRule'], + resources: [ + `arn:aws:events:${Stack.of(this).region}:${Stack.of(this).account}:event-bus/default`, + `arn:aws:events:${Stack.of(this).region}:${Stack.of(this).account}:rule/*`, + ], + }), + ); + + // Create CloudWatch log group for pipeline logs + // OpenSearch Ingestion requires log groups to use /aws/vendedlogs/ prefix + const logGroup = new LogGroup(this, 'PipelineLogGroup', { + logGroupName: `/aws/vendedlogs/opensearch-ingestion/${pipelineName}`, + retention: RetentionDays.ONE_WEEK, + }); + + // Generate pipeline configuration YAML + const pipelineConfiguration = this.generatePipelineConfiguration( + collectionEndpoint, + indexTemplate, + bufferOptions, + this.pipelineRole.roleArn, + ); + + // Create the OpenSearch Ingestion pipeline + this.pipeline = new CfnPipeline(this, 'Pipeline', { + pipelineName: pipelineName, + pipelineConfigurationBody: pipelineConfiguration, + minUnits: capacityLimits.min, + maxUnits: capacityLimits.max, + // Configure log publishing for pipeline monitoring + logPublishingOptions: { + isLoggingEnabled: true, + cloudWatchLogDestination: { + logGroup: `/aws/vendedlogs/opensearch-ingestion/${pipelineName}`, + }, + }, + // Add tags for resource management + tags: [ + { + key: 'Name', + value: pipelineName, + }, + { + key: 'Purpose', + value: 'LogIngestion', + }, + { + key: 'Component', + value: 'OpenSearchPipeline', + }, + ], + }); + + // Add dependencies to ensure resources are created in correct order + this.pipeline.node.addDependency(this.pipelineRole); + this.pipeline.node.addDependency(logGroup); + + // Set the pipeline endpoint (extract first endpoint from the array) + // The attrIngestEndpointUrls returns an array, so we need to get the first element + this.pipelineEndpoint = Fn.select(0, this.pipeline.attrIngestEndpointUrls); + + this.createExports(); + this.createOutputs(); + } + + /** + * Generates the pipeline configuration YAML for OpenSearch Ingestion + * Configures HTTP source, JSON parser processor, and OpenSearch Serverless sink + * @private + */ + private generatePipelineConfiguration( + collectionEndpoint: string, + indexTemplate: string, + bufferOptions: { flushInterval: number; batchSize: number }, + roleArn: string, + ): string { + // Strip https:// from collection endpoint for OpenSearch sink + const cleanEndpoint = collectionEndpoint.replace('https://', ''); + + // Generate YAML configuration for OSI pipeline with minimal processing + // FluentBit already sends JSON format, so we don't need to parse it + const yamlConfig = `version: "2" +log-pipeline: + source: + http: + path: "/log/ingest" + processor: + - parse_json: + source: "log" + destination: "parsed_log" + parse_when: '/log != null and /log != ""' + - add_entries: + entries: + - key: "pipeline_version" + value: "1.0" + sink: + - opensearch: + hosts: ["${cleanEndpoint}"] + index: "${indexTemplate}" + aws: + region: "${Stack.of(this).region}" + sts_role_arn: "${roleArn}" + serverless: true`; + + return yamlConfig; + } + + /** + * Creates CloudFormation exports for the pipeline + * @private + */ + private createExports(): void { + new CfnOutput(this, 'PipelineArn', { + value: this.pipeline.attrPipelineArn, + exportName: OPENSEARCH_PIPELINE_ARN_EXPORT_NAME, + }); + + new CfnOutput(this, 'PipelineEndpoint', { + value: this.pipelineEndpoint, + exportName: OPENSEARCH_PIPELINE_ENDPOINT_EXPORT_NAME, + }); + + new CfnOutput(this, 'PipelineRoleArn', { + value: this.pipelineRole.roleArn, + exportName: OPENSEARCH_PIPELINE_ROLE_ARN_EXPORT_NAME, + }); + } + + /** + * Imports pipeline information from CloudFormation exports + * @static + */ + public static importFromExports(): { + pipelineArn: string; + pipelineEndpoint: string; + pipelineRoleArn: string; + } { + const pipelineArn = Fn.importValue(OPENSEARCH_PIPELINE_ARN_EXPORT_NAME); + const pipelineEndpoint = Fn.importValue(OPENSEARCH_PIPELINE_ENDPOINT_EXPORT_NAME); + const pipelineRoleArn = Fn.importValue(OPENSEARCH_PIPELINE_ROLE_ARN_EXPORT_NAME); + + return { + pipelineArn, + pipelineEndpoint, + pipelineRoleArn, + }; + } + + /** + * Creates SSM parameter outputs for the pipeline + * @private + */ + private createOutputs(): void { + if (this.pipeline) { + Utilities.createSsmParameters( + this, + PARAMETER_STORE_PREFIX, + new Map( + Object.entries({ + opensearchpipelinearn: this.pipeline.attrPipelineArn, + opensearchpipelineendpoint: this.pipelineEndpoint, + opensearchpipelinerolearn: this.pipelineRole.roleArn, + }), + ), + ); + } else { + throw new Error('OpenSearch pipeline is not available'); + } + } +} diff --git a/src/cdk/lib/constructs/queue.ts b/src/cdk/lib/constructs/queue.ts new file mode 100644 index 00000000..d9fd6854 --- /dev/null +++ b/src/cdk/lib/constructs/queue.ts @@ -0,0 +1,119 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +import { CfnOutput, Duration, Fn } from 'aws-cdk-lib'; +import { ITopic, Topic } from 'aws-cdk-lib/aws-sns'; +import { EmailSubscription } from 'aws-cdk-lib/aws-sns-subscriptions'; +import { IQueue, Queue } from 'aws-cdk-lib/aws-sqs'; +import { NagSuppressions } from 'cdk-nag'; +import { Construct } from 'constructs'; +import { SNS_TOPIC_ARN_EXPORT_NAME, SQS_QUEUE_ARN_EXPORT_NAME, SQS_QUEUE_URL_EXPORT_NAME } from '../../bin/constants'; + +/** + * Properties for configuring QueueResources construct + * @interface QueueResourcesProperties + */ +export interface QueueResourcesProperties { + /** + * The visibility timeout for the SQS queue in seconds + * @default 300 + */ + visibilityTimeout?: number; + /** + * Email address for SNS topic subscription + * @default 'someone@example.com' + */ + snsTopicEmail?: string; +} + +/** + * AWS CDK Construct that creates SQS queue and SNS topic resources for pet adoption + * @class QueueResources + * @extends Construct + */ +export class QueueResources extends Construct { + /** + * The SQS queue for pet adoption messages + * @public + */ + public queue: Queue; + /** + * The SNS topic for pet adoption notifications + * @public + */ + public topic: Topic; + + /** + * Creates a new QueueResources construct + * @param scope - The parent construct + * @param id - The construct ID + * @param properties - Configuration properties for the construct + */ + constructor(scope: Construct, id: string, properties?: QueueResourcesProperties) { + super(scope, id); + this.queue = new Queue(this, 'sqs_petadoption', { + visibilityTimeout: Duration.seconds(properties?.visibilityTimeout || 300), + enforceSSL: true, + }); + + NagSuppressions.addResourceSuppressions( + this.queue, + [ + { + id: 'AwsSolutions-SQS3', + reason: 'DLQ is not enabled for this workshop', + }, + ], + true, + ); + + this.topic = new Topic(this, 'topic_petadoption', { + enforceSSL: true, + displayName: 'Pet Adoption Notifications', + }); + this.topic.addSubscription(new EmailSubscription(properties?.snsTopicEmail || 'someone@example.com')); + + // Create CloudFormation outputs for queue resources + this.createQueueOutputs(); + } + + /** + * Imports queue resources from CloudFormation exports created by QueueResources + * + * @param scope - The construct scope where the resources will be imported + * @param id - The construct identifier for the imported resources + * @returns Object containing the imported SNS topic and SQS queue + */ + public static importFromExports(scope: Construct, id: string): { topic: ITopic; queue: IQueue } { + const topicArn = Fn.importValue(SNS_TOPIC_ARN_EXPORT_NAME); + const queueArn = Fn.importValue(SQS_QUEUE_ARN_EXPORT_NAME); + const queueUrl = Fn.importValue(SQS_QUEUE_URL_EXPORT_NAME); + + const topic = Topic.fromTopicArn(scope, `${id}-Topic`, topicArn); + const queue = Queue.fromQueueAttributes(scope, `${id}-Queue`, { + queueArn: queueArn, + queueUrl: queueUrl, + }); + + return { topic, queue }; + } + + /** + * Creates CloudFormation outputs for queue resources + */ + private createQueueOutputs() { + new CfnOutput(this, 'SNSTopicArn', { + value: this.topic.topicArn, + exportName: SNS_TOPIC_ARN_EXPORT_NAME, + }); + new CfnOutput(this, 'SQSQueueArn', { + value: this.queue.queueArn, + exportName: SQS_QUEUE_ARN_EXPORT_NAME, + }); + new CfnOutput(this, 'SQSQueueUrl', { + value: this.queue.queueUrl, + exportName: SQS_QUEUE_URL_EXPORT_NAME, + }); + } +} diff --git a/src/cdk/lib/constructs/vpc-endpoints.ts b/src/cdk/lib/constructs/vpc-endpoints.ts new file mode 100644 index 00000000..4f326b8c --- /dev/null +++ b/src/cdk/lib/constructs/vpc-endpoints.ts @@ -0,0 +1,139 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +import { Construct } from 'constructs'; +import { IVpc, InterfaceVpcEndpoint, InterfaceVpcEndpointAwsService, IInterfaceVpcEndpoint } from 'aws-cdk-lib/aws-ec2'; +import { CfnOutput, Fn } from 'aws-cdk-lib'; +import { + VPC_ENDPOINT_APIGATEWAY_ID_EXPORT_NAME, + VPC_ENDPOINT_DYNAMODB_ID_EXPORT_NAME, + VPC_ENDPOINT_LAMBDA_ID_EXPORT_NAME, + VPC_ENDPOINT_SERVICEDISCOVERY_ID_EXPORT_NAME, + VPC_ENDPOINT_DATA_SERVICEDISCOVERY_ID_EXPORT_NAME, +} from '../../bin/constants'; + +export interface VpcEndpointsProperties { + vpc: IVpc; +} + +export class VpcEndpoints extends Construct { + public readonly apiGatewayEndpoint: InterfaceVpcEndpoint; + public readonly dynamoDbEndpoint: InterfaceVpcEndpoint; + public readonly lambdaEndpoint: InterfaceVpcEndpoint; + public readonly serviceDiscoveryEndpoint: InterfaceVpcEndpoint; + public readonly dataServiceDiscoveryEndpoint: InterfaceVpcEndpoint; + + constructor(scope: Construct, id: string, properties: VpcEndpointsProperties) { + super(scope, id); + + this.apiGatewayEndpoint = new InterfaceVpcEndpoint(this, 'ApiGatewayEndpoint', { + vpc: properties.vpc, + service: InterfaceVpcEndpointAwsService.APIGATEWAY, + subnets: { subnets: properties.vpc.privateSubnets }, + privateDnsEnabled: false, + }); + + this.dynamoDbEndpoint = new InterfaceVpcEndpoint(this, 'DynamoDbEndpoint', { + vpc: properties.vpc, + service: InterfaceVpcEndpointAwsService.DYNAMODB, + subnets: { subnets: properties.vpc.privateSubnets }, + privateDnsEnabled: false, // Not Supported by DynamoDB + }); + + this.lambdaEndpoint = new InterfaceVpcEndpoint(this, 'LambdaEndpoint', { + vpc: properties.vpc, + service: InterfaceVpcEndpointAwsService.LAMBDA, + subnets: { subnets: properties.vpc.privateSubnets }, + privateDnsEnabled: false, + }); + + this.serviceDiscoveryEndpoint = new InterfaceVpcEndpoint(this, 'ServiceDiscoveryEndpoint', { + vpc: properties.vpc, + service: InterfaceVpcEndpointAwsService.CLOUD_MAP_SERVICE_DISCOVERY, + subnets: { subnets: properties.vpc.privateSubnets }, + privateDnsEnabled: false, + }); + + this.dataServiceDiscoveryEndpoint = new InterfaceVpcEndpoint(this, 'DataServiceDiscoveryEndpoint', { + vpc: properties.vpc, + service: InterfaceVpcEndpointAwsService.CLOUD_MAP_DATA_SERVICE_DISCOVERY, + subnets: { subnets: properties.vpc.privateSubnets }, + privateDnsEnabled: false, + }); + + this.createOutputs(); + } + + private createOutputs() { + new CfnOutput(this, 'ApiGatewayEndpointId', { + value: this.apiGatewayEndpoint.vpcEndpointId, + exportName: VPC_ENDPOINT_APIGATEWAY_ID_EXPORT_NAME, + }); + + new CfnOutput(this, 'DynamoDbEndpointId', { + value: this.dynamoDbEndpoint.vpcEndpointId, + exportName: VPC_ENDPOINT_DYNAMODB_ID_EXPORT_NAME, + }); + + new CfnOutput(this, 'LambdaEndpointId', { + value: this.lambdaEndpoint.vpcEndpointId, + exportName: VPC_ENDPOINT_LAMBDA_ID_EXPORT_NAME, + }); + + new CfnOutput(this, 'ServiceDiscoveryEndpointId', { + value: this.serviceDiscoveryEndpoint.vpcEndpointId, + exportName: VPC_ENDPOINT_SERVICEDISCOVERY_ID_EXPORT_NAME, + }); + + new CfnOutput(this, 'DataServiceDiscoveryEndpointId', { + value: this.dataServiceDiscoveryEndpoint.vpcEndpointId, + exportName: VPC_ENDPOINT_DATA_SERVICEDISCOVERY_ID_EXPORT_NAME, + }); + } + + /** + * Imports VPC endpoints from CloudFormation exports + * @param scope - The construct scope where the endpoints will be imported + * @param id - The construct identifier for the imported endpoints + * @returns Object containing the imported VPC endpoint interfaces + */ + public static importFromExports(scope: Construct, id: string) { + const apiGatewayEndpointId = Fn.importValue(VPC_ENDPOINT_APIGATEWAY_ID_EXPORT_NAME); + const dynamoDatabaseEndpointId = Fn.importValue(VPC_ENDPOINT_DYNAMODB_ID_EXPORT_NAME); + const lambdaEndpointId = Fn.importValue(VPC_ENDPOINT_LAMBDA_ID_EXPORT_NAME); + const serviceDiscoveryEndpointId = Fn.importValue(VPC_ENDPOINT_SERVICEDISCOVERY_ID_EXPORT_NAME); + const dataServiceDiscoveryEndpointId = Fn.importValue(VPC_ENDPOINT_DATA_SERVICEDISCOVERY_ID_EXPORT_NAME); + + return { + apiGatewayEndpoint: InterfaceVpcEndpoint.fromInterfaceVpcEndpointAttributes(scope, `${id}-ApiGateway`, { + vpcEndpointId: apiGatewayEndpointId, + port: 443, + }) as IInterfaceVpcEndpoint, + dynamoDbEndpoint: InterfaceVpcEndpoint.fromInterfaceVpcEndpointAttributes(scope, `${id}-DynamoDb`, { + vpcEndpointId: dynamoDatabaseEndpointId, + port: 443, + }) as IInterfaceVpcEndpoint, + lambdaEndpoint: InterfaceVpcEndpoint.fromInterfaceVpcEndpointAttributes(scope, `${id}-Lambda`, { + vpcEndpointId: lambdaEndpointId, + port: 443, + }) as IInterfaceVpcEndpoint, + serviceDiscoveryEndpoint: InterfaceVpcEndpoint.fromInterfaceVpcEndpointAttributes( + scope, + `${id}-ServiceDiscovery`, + { + vpcEndpointId: serviceDiscoveryEndpointId, + port: 443, + }, + ) as IInterfaceVpcEndpoint, + dataServiceDiscoveryEndpoint: InterfaceVpcEndpoint.fromInterfaceVpcEndpointAttributes( + scope, + `${id}-DataServiceDiscovery`, + { + vpcEndpointId: dataServiceDiscoveryEndpointId, + port: 443, + }, + ) as IInterfaceVpcEndpoint, + }; + } +} diff --git a/src/cdk/lib/microservices/manifests/petsite-deployment.yaml b/src/cdk/lib/microservices/manifests/petsite-deployment.yaml new file mode 100644 index 00000000..59faa875 --- /dev/null +++ b/src/cdk/lib/microservices/manifests/petsite-deployment.yaml @@ -0,0 +1,70 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +--- +apiVersion: v1 +kind: Namespace +metadata: + name: {{ NAMESPACE }} +--- +apiVersion: v1 +kind: ServiceAccount +metadata: + name: {{ SERVICE_ACCOUNT_NAME }} + namespace: {{ NAMESPACE }} +--- +apiVersion: v1 +kind: Service +metadata: + name: service-petsite + namespace: {{ NAMESPACE }} + annotations: + scrape: 'true' + prometheus.io/scrape: 'true' +spec: + ports: + - port: 80 + nodePort: 30300 + targetPort: 80 + protocol: TCP + type: NodePort + selector: + app: petsite +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: petsite-deployment + namespace: {{ NAMESPACE }} +spec: + selector: + matchLabels: + app: petsite + replicas: 2 + template: + metadata: + labels: + app: petsite + annotations: + instrumentation.opentelemetry.io/inject-dotnet: "true" + spec: + serviceAccountName: {{ SERVICE_ACCOUNT_NAME }} + containers: + - image: '{{ ECR_IMAGE_URL }}' + imagePullPolicy: Always + name: petsite + ports: + - containerPort: 80 + protocol: TCP +--- +apiVersion: elbv2.k8s.aws/v1beta1 +kind: TargetGroupBinding +metadata: + name: petsite-tgb + namespace: {{ NAMESPACE }} +spec: + serviceRef: + name: service-petsite + port: 80 + targetGroupARN: {{ TARGET_GROUP_ARN }} + targetType: ip + diff --git a/src/cdk/lib/microservices/pay-for-adoption.ts b/src/cdk/lib/microservices/pay-for-adoption.ts new file mode 100644 index 00000000..94297b29 --- /dev/null +++ b/src/cdk/lib/microservices/pay-for-adoption.ts @@ -0,0 +1,101 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +import { IDatabaseCluster } from 'aws-cdk-lib/aws-rds'; +import { EcsService, EcsServiceProperties } from '../constructs/ecs-service'; +import { Construct } from 'constructs'; +import { ISecret } from 'aws-cdk-lib/aws-secretsmanager'; +import { ManagedPolicy, Policy, PolicyDocument } from 'aws-cdk-lib/aws-iam'; +import { PARAMETER_STORE_PREFIX } from '../../bin/environment'; +import { Utilities } from '../utils/utilities'; +import { NagSuppressions } from 'cdk-nag'; +import { ITable } from 'aws-cdk-lib/aws-dynamodb'; + +export interface PayForAdoptionServiceProperties extends EcsServiceProperties { + database: IDatabaseCluster; + secret: ISecret; + table: ITable; +} + +export class PayForAdoptionService extends EcsService { + constructor(scope: Construct, id: string, properties: PayForAdoptionServiceProperties) { + super(scope, id, properties); + + Utilities.TagConstruct(this, { + 'app:owner': 'petstore', + 'app:project': 'workshop', + 'app:name': properties.name, + 'app:computType': properties.computeType, + 'app:hostType:': properties.hostType, + }); + } + + addPermissions(properties: PayForAdoptionServiceProperties): void { + properties.secret?.grantRead(this.taskRole); + + this.taskRole.addManagedPolicy( + ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonECSTaskExecutionRolePolicy'), + ); + + this.taskRole.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName('AWSXRayDaemonWriteAccess')); + + const taskPolicy = new Policy(this, 'taskPolicy', { + policyName: 'PayForAdoptionTaskPolicy', + document: new PolicyDocument({ + statements: [EcsService.getDefaultSSMPolicy(this, PARAMETER_STORE_PREFIX)], + }), + roles: [this.taskRole], + }); + + properties.table.grantReadWriteData(this.taskRole); + + NagSuppressions.addResourceSuppressions( + taskPolicy, + [ + { + id: 'AwsSolutions-IAM4', + reason: 'Managed Policies are acceptable for the task role', + }, + { + id: 'AwsSolutions-IAM5', + reason: 'Permissions are acceptable for the task role', + }, + ], + true, + ); + + NagSuppressions.addResourceSuppressions( + this.taskRole, + [ + { + id: 'AwsSolutions-IAM4', + reason: 'Managed Policies are acceptable for the task role', + }, + { + id: 'AwsSolutions-IAM5', + reason: 'Permissions are acceptable for the task role', + }, + ], + true, + ); + } + + createOutputs(properties: PayForAdoptionServiceProperties): void { + if (!this.loadBalancedService && !properties.disableService) { + throw new Error('Service is not defined'); + } else { + Utilities.createSsmParameters( + this, + PARAMETER_STORE_PREFIX, + new Map( + Object.entries({ + paymentapiurl: `http://${this.loadBalancedService?.loadBalancer.loadBalancerDnsName}/api/home/completeadoption`, + payforadoptionmetricsurl: `http://${this.loadBalancedService?.loadBalancer.loadBalancerDnsName}/metrics`, + cleanupadoptionsurl: `http://${this.loadBalancedService?.loadBalancer.loadBalancerDnsName}/api/home/cleanupadoptions`, + }), + ), + ); + } + } +} diff --git a/src/cdk/lib/microservices/pet-search.ts b/src/cdk/lib/microservices/pet-search.ts new file mode 100644 index 00000000..fb60fb30 --- /dev/null +++ b/src/cdk/lib/microservices/pet-search.ts @@ -0,0 +1,121 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +import { IDatabaseCluster } from 'aws-cdk-lib/aws-rds'; +import { EcsService, EcsServiceProperties } from '../constructs/ecs-service'; +import { ISecret } from 'aws-cdk-lib/aws-secretsmanager'; +import { Construct } from 'constructs'; +import { ManagedPolicy, Policy, PolicyDocument } from 'aws-cdk-lib/aws-iam'; +import { PARAMETER_STORE_PREFIX } from '../../bin/environment'; +import { NagSuppressions } from 'cdk-nag'; +import { Utilities } from '../utils/utilities'; +import { ITable } from 'aws-cdk-lib/aws-dynamodb'; +import { ApplicationSignalsIntegration, JavaInstrumentationVersion } from '@aws-cdk/aws-applicationsignals-alpha'; +import { IBucket } from 'aws-cdk-lib/aws-s3'; + +export interface PetSearchServiceProperties extends EcsServiceProperties { + database: IDatabaseCluster; + secret: ISecret; + table: ITable; + bucket: IBucket; +} + +export class PetSearchService extends EcsService { + constructor(scope: Construct, id: string, properties: PetSearchServiceProperties) { + super(scope, id, properties); + Utilities.TagConstruct(this, { + 'app:owner': 'petstore', + 'app:project': 'workshop', + 'app:name': properties.name, + 'app:computType': properties.computeType, + 'app:hostType:': properties.hostType, + }); + + new ApplicationSignalsIntegration(this, 'petsearch-integration', { + taskDefinition: this.taskDefinition, + instrumentation: { + sdkVersion: JavaInstrumentationVersion.V2_10_0, + }, + serviceName: `${properties.name}-Service`, + cloudWatchAgentSidecar: { + containerName: 'ecs-cwagent', + enableLogging: true, + cpu: 256, + memoryLimitMiB: 512, + }, + }); + + NagSuppressions.addResourceSuppressions(this.taskDefinition, [ + { + id: 'AwsSolutions-ECS7', + reason: 'False positive, the Application Signal container has logging enabled as a sidecar', + }, + ]); + } + + addPermissions(properties: PetSearchServiceProperties): void { + this.taskRole.addManagedPolicy( + ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonECSTaskExecutionRolePolicy'), + ); + + this.taskRole.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName('AWSXRayDaemonWriteAccess')); + + const taskPolicy = new Policy(this, 'taskPolicy', { + policyName: 'PetSearchTaskPolicy', + document: new PolicyDocument({ + statements: [EcsService.getDefaultSSMPolicy(this, PARAMETER_STORE_PREFIX)], + }), + roles: [this.taskRole], + }); + + properties.table.grantReadData(this.taskRole); + properties.bucket.grantRead(this.taskRole); + + NagSuppressions.addResourceSuppressions( + taskPolicy, + [ + { + id: 'AwsSolutions-IAM4', + reason: 'Managed Policies are acceptable for the task role', + }, + { + id: 'AwsSolutions-IAM5', + reason: 'Permissions are acceptable for the task role', + }, + ], + true, + ); + + NagSuppressions.addResourceSuppressions( + this.taskRole, + [ + { + id: 'AwsSolutions-IAM4', + reason: 'Managed Policies are acceptable for the task role', + }, + { + id: 'AwsSolutions-IAM5', + reason: 'Permissions are acceptable for the task role', + }, + ], + true, + ); + } + + createOutputs(properties: PetSearchServiceProperties): void { + if (!this.loadBalancedService && !properties.disableService) { + throw new Error('Service is not defined'); + } else { + Utilities.createSsmParameters( + this, + PARAMETER_STORE_PREFIX, + new Map( + Object.entries({ + searchapiurl: `http://${this.loadBalancedService?.loadBalancer.loadBalancerDnsName}/api/search?`, + }), + ), + ); + } + } +} diff --git a/src/cdk/lib/microservices/petfood.ts b/src/cdk/lib/microservices/petfood.ts new file mode 100644 index 00000000..0b8a0964 --- /dev/null +++ b/src/cdk/lib/microservices/petfood.ts @@ -0,0 +1,124 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +import { EcsService, EcsServiceProperties } from '../constructs/ecs-service'; +import { Construct } from 'constructs'; +import { ManagedPolicy, Policy, PolicyDocument } from 'aws-cdk-lib/aws-iam'; +import { PARAMETER_STORE_PREFIX } from '../../bin/environment'; +import { NagSuppressions } from 'cdk-nag'; +import { Utilities } from '../utils/utilities'; +import { ITable } from 'aws-cdk-lib/aws-dynamodb'; +import { IBucket } from 'aws-cdk-lib/aws-s3'; + +export interface PetFoodProperties extends EcsServiceProperties { + petFoodTable: ITable; + petFoodCartTable: ITable; + assetsBucket: IBucket; +} + +export class PetFoodECSService extends EcsService { + constructor(scope: Construct, id: string, properties: PetFoodProperties) { + super(scope, id, properties); + + // new ApplicationSignalsIntegration(this, 'petlist-integration', { + // taskDefinition: this.taskDefinition, + // instrumentation: { + // sdkVersion: RustInstr.V0_9_0, + // }, + // serviceName: `${properties.name}-Service`, + // cloudWatchAgentSidecar: { + // containerName: 'ecs-cwagent', + // enableLogging: true, + // cpu: 256, + // memoryLimitMiB: 512, + // }, + // }); + + // NagSuppressions.addResourceSuppressions(this.taskDefinition, [ + // { + // id: 'AwsSolutions-ECS7', + // reason: 'False positive, the Application Signal container has logging enabled as a sidecar', + // }, + // ]); + + // Utilities.TagConstruct(this, { + // 'app:owner': 'petstore', + // 'app:project': 'workshop', + // 'app:name': properties.name, + // 'app:computType': properties.computeType, + // 'app:hostType:': properties.hostType, + // }); + } + + addPermissions(properties: PetFoodProperties): void { + properties.petFoodTable.grantReadWriteData(this.taskRole); + properties.petFoodCartTable.grantReadWriteData(this.taskRole); + properties.assetsBucket.grantReadWrite(this.taskRole); + + this.taskRole.addManagedPolicy( + ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonECSTaskExecutionRolePolicy'), + ); + + this.taskRole.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName('AWSXRayDaemonWriteAccess')); + + const taskPolicy = new Policy(this, 'taskPolicy', { + policyName: 'PetFoodTaskPolicy', + document: new PolicyDocument({ + statements: [ + EcsService.getDefaultSSMPolicy(this, PARAMETER_STORE_PREFIX), + EcsService.getDefaultEventBridgePolicy(this), + ], + }), + roles: [this.taskRole], + }); + + NagSuppressions.addResourceSuppressions( + taskPolicy, + [ + { + id: 'AwsSolutions-IAM4', + reason: 'Managed Policies are acceptable for the task role', + }, + { + id: 'AwsSolutions-IAM5', + reason: 'Permissions are acceptable for the task role', + }, + ], + true, + ); + + NagSuppressions.addResourceSuppressions( + this.taskRole, + [ + { + id: 'AwsSolutions-IAM4', + reason: 'Managed Policies are acceptable for the task role', + }, + { + id: 'AwsSolutions-IAM5', + reason: 'Permissions are acceptable for the task role', + }, + ], + true, + ); + } + + createOutputs(properties: PetFoodProperties): void { + if (!this.loadBalancedService && !properties.disableService) { + throw new Error('Service is not defined'); + } else { + Utilities.createSsmParameters( + this, + PARAMETER_STORE_PREFIX, + new Map( + Object.entries({ + petfoodapiurl: `http://${this.loadBalancedService?.loadBalancer.loadBalancerDnsName}/api/foods`, + petfoodmetricsurl: `http://${this.loadBalancedService?.loadBalancer.loadBalancerDnsName}/metrics`, + petfoodcarturl: `http://${this.loadBalancedService?.loadBalancer.loadBalancerDnsName}/api/cart`, + }), + ), + ); + } + } +} diff --git a/src/cdk/lib/microservices/petlist-adoptions.ts b/src/cdk/lib/microservices/petlist-adoptions.ts new file mode 100644 index 00000000..adcbfd4f --- /dev/null +++ b/src/cdk/lib/microservices/petlist-adoptions.ts @@ -0,0 +1,118 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +import { IDatabaseCluster } from 'aws-cdk-lib/aws-rds'; +import { EcsService, EcsServiceProperties } from '../constructs/ecs-service'; +import { ISecret } from 'aws-cdk-lib/aws-secretsmanager'; +import { Construct } from 'constructs'; +import { ManagedPolicy, Policy, PolicyDocument } from 'aws-cdk-lib/aws-iam'; +import { PARAMETER_STORE_PREFIX } from '../../bin/environment'; +import { NagSuppressions } from 'cdk-nag'; +import { Utilities } from '../utils/utilities'; +import { ApplicationSignalsIntegration, PythonInstrumentationVersion } from '@aws-cdk/aws-applicationsignals-alpha'; + +export interface ListAdoptionsServiceProperties extends EcsServiceProperties { + database: IDatabaseCluster; + secret: ISecret; +} + +export class ListAdoptionsService extends EcsService { + constructor(scope: Construct, id: string, properties: ListAdoptionsServiceProperties) { + super(scope, id, properties); + + new ApplicationSignalsIntegration(this, 'petlist-integration', { + taskDefinition: this.taskDefinition, + instrumentation: { + sdkVersion: PythonInstrumentationVersion.V0_9_0, + }, + serviceName: `${properties.name}-Service`, + cloudWatchAgentSidecar: { + containerName: 'ecs-cwagent', + enableLogging: true, + cpu: 256, + memoryLimitMiB: 512, + }, + }); + + NagSuppressions.addResourceSuppressions(this.taskDefinition, [ + { + id: 'AwsSolutions-ECS7', + reason: 'False positive, the Application Signal container has logging enabled as a sidecar', + }, + ]); + + Utilities.TagConstruct(this, { + 'app:owner': 'petstore', + 'app:project': 'workshop', + 'app:name': properties.name, + 'app:computType': properties.computeType, + 'app:hostType:': properties.hostType, + }); + } + + addPermissions(properties: ListAdoptionsServiceProperties): void { + properties.secret?.grantRead(this.taskRole); + + this.taskRole.addManagedPolicy( + ManagedPolicy.fromAwsManagedPolicyName('service-role/AmazonECSTaskExecutionRolePolicy'), + ); + + this.taskRole.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName('AWSXRayDaemonWriteAccess')); + + const taskPolicy = new Policy(this, 'taskPolicy', { + policyName: 'ListdoptionTaskPolicy', + document: new PolicyDocument({ + statements: [EcsService.getDefaultSSMPolicy(this, PARAMETER_STORE_PREFIX)], + }), + roles: [this.taskRole], + }); + + NagSuppressions.addResourceSuppressions( + taskPolicy, + [ + { + id: 'AwsSolutions-IAM4', + reason: 'Managed Policies are acceptable for the task role', + }, + { + id: 'AwsSolutions-IAM5', + reason: 'Permissions are acceptable for the task role', + }, + ], + true, + ); + + NagSuppressions.addResourceSuppressions( + this.taskRole, + [ + { + id: 'AwsSolutions-IAM4', + reason: 'Managed Policies are acceptable for the task role', + }, + { + id: 'AwsSolutions-IAM5', + reason: 'Permissions are acceptable for the task role', + }, + ], + true, + ); + } + + createOutputs(properties: ListAdoptionsServiceProperties): void { + if (!this.loadBalancedService && !properties.disableService) { + throw new Error('Service is not defined'); + } else { + Utilities.createSsmParameters( + this, + PARAMETER_STORE_PREFIX, + new Map( + Object.entries({ + petlistadoptionsurl: `http://${this.loadBalancedService?.loadBalancer.loadBalancerDnsName}/api/adoptionlist/`, + petlistadoptionsmetricsurl: `http://${this.loadBalancedService?.loadBalancer.loadBalancerDnsName}/metrics`, + }), + ), + ); + } + } +} diff --git a/src/cdk/lib/microservices/petsite.ts b/src/cdk/lib/microservices/petsite.ts new file mode 100644 index 00000000..339d6c32 --- /dev/null +++ b/src/cdk/lib/microservices/petsite.ts @@ -0,0 +1,272 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +import { Construct } from 'constructs'; +import { EKSDeployment, EKSDeploymentProperties } from '../constructs/eks-deployment'; +import { Microservice, MicroserviceProperties } from '../constructs/microservice'; +import { readFileSync } from 'node:fs'; +import * as yaml from 'yaml'; +import * as nunjucks from 'nunjucks'; +import { ManagedPolicy, Policy, PolicyDocument, Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam'; +import { CfnPodIdentityAssociation } from 'aws-cdk-lib/aws-eks'; +import { + ApplicationLoadBalancer, + ApplicationProtocol, + ApplicationTargetGroup, + TargetType, +} from 'aws-cdk-lib/aws-elasticloadbalancingv2'; +import { + CachedMethods, + CachePolicy, + Distribution, + OriginProtocolPolicy, + OriginRequestPolicy, + SecurityPolicyProtocol, + ViewerProtocolPolicy, +} from 'aws-cdk-lib/aws-cloudfront'; +import { LoadBalancerV2Origin } from 'aws-cdk-lib/aws-cloudfront-origins'; +import { NagSuppressions } from 'cdk-nag'; +import { Utilities } from '../utils/utilities'; +import { PARAMETER_STORE_PREFIX } from '../../bin/environment'; +import { Peer, Port, PrefixList } from 'aws-cdk-lib/aws-ec2'; +import { Bucket, ObjectOwnership } from 'aws-cdk-lib/aws-s3'; +import { Duration, RemovalPolicy } from 'aws-cdk-lib'; + +export class PetSite extends EKSDeployment { + public readonly loadBalancer: ApplicationLoadBalancer; + public readonly targetGroup: ApplicationTargetGroup; + public readonly distribution: Distribution; + constructor(scope: Construct, id: string, properties: EKSDeploymentProperties) { + super(scope, id, properties); + this.loadBalancer = new ApplicationLoadBalancer(scope, 'loadBalancer', { + vpc: properties.vpc!, + internetFacing: true, + loadBalancerName: `LB-${properties.name}`, + vpcSubnets: { + subnets: properties.vpc!.publicSubnets, + }, + }); + + const cloudFrontPrefixList = PrefixList.fromLookup(this, 'cloudfront-prefix-list', { + prefixListName: 'com.amazonaws.global.cloudfront.origin-facing', + }); + // Allow CloudFront to access the load balancer + this.loadBalancer.connections.allowFrom( + Peer.prefixList(cloudFrontPrefixList.prefixListId), + Port.tcp(80), + 'Allow CloudFront access', + ); + + this.targetGroup = new ApplicationTargetGroup(scope, 'targetGroup', { + port: properties.listenerPort || 80, + vpc: properties.vpc!, + protocol: ApplicationProtocol.HTTP, + targetGroupName: `TG-${properties.name}`, + targetType: TargetType.IP, + healthCheck: { + path: properties.healthCheck, + }, + }); + + this.loadBalancer.addListener('listener', { + port: properties.listenerPort || 80, + protocol: ApplicationProtocol.HTTP, + defaultTargetGroups: [this.targetGroup], + open: false, + }); + + // TODO: Autodelete is not working for this bucket + const cloudfrontAccessBucket = new Bucket(this, 'CloudfrontAccessLogs', { + removalPolicy: RemovalPolicy.RETAIN, + enforceSSL: true, + objectOwnership: ObjectOwnership.BUCKET_OWNER_PREFERRED, + }); + + this.distribution = new Distribution(this, 'Distribution', { + defaultBehavior: { + origin: new LoadBalancerV2Origin(this.loadBalancer, { + protocolPolicy: OriginProtocolPolicy.HTTP_ONLY, + }), + viewerProtocolPolicy: ViewerProtocolPolicy.REDIRECT_TO_HTTPS, + originRequestPolicy: OriginRequestPolicy.ALL_VIEWER, + cachePolicy: CachePolicy.CACHING_DISABLED, + allowedMethods: CachedMethods.CACHE_GET_HEAD_OPTIONS, + }, + enableLogging: true, + logBucket: cloudfrontAccessBucket, + minimumProtocolVersion: SecurityPolicyProtocol.TLS_V1_2_2021, + errorResponses: [ + { + httpStatus: 404, + ttl: Duration.minutes(5), + }, + { + httpStatus: 403, + ttl: Duration.minutes(5), + }, + ], + enableIpv6: false, + }); + + // Allow load balancer to reach EKS nodes + this.loadBalancer.connections.allowTo( + properties.eksCluster!.clusterSecurityGroup, + Port.tcp(properties.listenerPort || 80), + 'Allow Load Balancer to EKS nodes', + ); + + this.namespace = 'petsite'; + this.serviceAccountName = 'petsite-sa'; + this.prepareManifest(properties); + this.manifest = this.configureEKSService(properties); + this.addPermissions(properties); + + this.createOutputs(); + + NagSuppressions.addResourceSuppressions( + this.loadBalancer, + [ + { + id: 'AwsSolutions-ELB2', + reason: 'Access logs not required for this workshop', + }, + ], + true, + ); + + NagSuppressions.addResourceSuppressions( + this.loadBalancer, + [ + { + id: 'AwsSolutions-EC23', + reason: 'Public Load balancer requires access from anywhere', + }, + ], + true, + ); + + NagSuppressions.addResourceSuppressions( + cloudfrontAccessBucket, + [ + { + id: 'AwsSolutions-S1', + reason: 'Cloudfront access log bucket', + }, + ], + true, + ); + + NagSuppressions.addResourceSuppressions( + this.distribution, + [ + { + id: 'AwsSolutions-CFR4', + reason: 'Using default Cloudfront certificate in the workshop is acceptable', + }, + { + id: 'AwsSolutions-CFR5', + reason: 'Using default Cloudfront certificate in the workshop is acceptable', + }, + ], + true, + ); + + Utilities.TagConstruct(this, { + 'app:owner': 'petstore', + 'app:project': 'workshop', + 'app:name': properties.name, + 'app:computType': properties.computeType, + 'app:hostType:': properties.hostType, + }); + } + + // eslint-disable-next-line @typescript-eslint/no-explicit-any -- this is how KubnernetesManifests defines it + prepareManifest(properties: EKSDeploymentProperties): Record[] { + if (!properties.manifestPath) { + throw new Error('manifestPath is required'); + } + + const manifestTemplate = readFileSync(properties.manifestPath, 'utf8'); + nunjucks.configure({ autoescape: true }); + + const deploymentYaml = nunjucks.renderString(manifestTemplate, { + ECR_IMAGE_URL: properties.repositoryURI, + SUBNETS: properties.vpc?.publicSubnets, + NAMESPACE: this.namespace, + SERVICE_ACCOUNT_NAME: this.serviceAccountName, + TARGET_GROUP_ARN: this.targetGroup.targetGroupArn, + }); + return yaml.parseAllDocuments(deploymentYaml).map((document) => document.toJS()); + } + + configureECSService(): void { + // Not applicable + } + addPermissions(properties: MicroserviceProperties): void { + this.serviceAccountRole = new Role(this, 'serviceAccountRole', { + assumedBy: new ServicePrincipal('pods.eks.amazonaws.com').withSessionTags(), + }); + + this.podIdentityAssociation = new CfnPodIdentityAssociation(this, 'podIdentityAssociation', { + clusterName: properties.eksCluster!.clusterName, + namespace: this.namespace || 'default', + roleArn: this.serviceAccountRole.roleArn, + serviceAccount: this.serviceAccountName || `${properties.name}-sa`, + }); + + const servicePolicy = new Policy(this, 'PetSitePolicy', { + policyName: 'PetSiteAccessPolicy', + document: new PolicyDocument({ + statements: [Microservice.getDefaultSSMPolicy(this, PARAMETER_STORE_PREFIX)], + }), + roles: [this.serviceAccountRole], + }); + + this.serviceAccountRole.addManagedPolicy(ManagedPolicy.fromAwsManagedPolicyName('AWSXRayDaemonWriteAccess')); + + NagSuppressions.addResourceSuppressions( + servicePolicy, + [ + { + id: 'AwsSolutions-IAM4', + reason: 'Managed Policies are acceptable for the pod role', + }, + { + id: 'AwsSolutions-IAM5', + reason: 'Permissions are acceptable for the pod role', + }, + ], + true, + ); + + NagSuppressions.addResourceSuppressions( + this.serviceAccountRole, + [ + { + id: 'AwsSolutions-IAM4', + reason: 'Managed Policies are acceptable for the pod role', + }, + { + id: 'AwsSolutions-IAM5', + reason: 'Permissions are acceptable for the pod role', + }, + ], + true, + ); + } + createOutputs(): void { + if (this.loadBalancer) { + Utilities.createSsmParameters( + this, + PARAMETER_STORE_PREFIX, + new Map( + Object.entries({ + petsiteurl: `https://${this.distribution.distributionDomainName}`, + imagescdnurl: `https://${this.distribution.distributionDomainName}/images`, + }), + ), + ); + } + } +} diff --git a/src/cdk/lib/pipeline.ts b/src/cdk/lib/pipeline.ts new file mode 100644 index 00000000..2c7225e7 --- /dev/null +++ b/src/cdk/lib/pipeline.ts @@ -0,0 +1,373 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +/** + * CDK Pipeline module for the One Observability Workshop. + * + * This module defines the main CI/CD pipeline that deploys the workshop infrastructure + * across multiple stages including core networking, applications, storage, compute, and microservices. + * + * @packageDocumentation + */ + +import { CfnOutput, Stack, StackProps } from 'aws-cdk-lib'; +import { BuildSpec, LinuxBuildImage } from 'aws-cdk-lib/aws-codebuild'; +import { PipelineType } from 'aws-cdk-lib/aws-codepipeline'; +import { IRole, Policy, PolicyStatement, Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam'; +import { BlockPublicAccess, Bucket, BucketEncryption } from 'aws-cdk-lib/aws-s3'; +import { CodeBuildStep, CodePipeline, CodePipelineSource } from 'aws-cdk-lib/pipelines'; +import { NagSuppressions } from 'cdk-nag'; +import { Construct } from 'constructs'; +import { CoreStage, CoreStageProperties } from './stages/core'; +import { Utilities } from './utils/utilities'; +import { RetentionDays } from 'aws-cdk-lib/aws-logs'; +import { S3Trigger } from 'aws-cdk-lib/aws-codepipeline-actions'; +import { ContainerDefinition, ContainersPipelineStage } from './stages/containers'; +import { StorageStage } from './stages/storage'; +import { AuroraPostgresEngineVersion } from 'aws-cdk-lib/aws-rds'; +import { ComputeStage } from './stages/compute'; +import { MicroservicesStage, MicroserviceApplicationsProperties } from './stages/applications'; + +/** + * Properties for configuring the CDK Pipeline stack. + * + * This interface extends StackProps and includes all necessary configuration + * for deploying the One Observability Workshop infrastructure pipeline. + */ +export interface CDKPipelineProperties extends StackProps { + /** S3 bucket name containing the source code repository */ + configBucketName: string; + /** Git branch name to deploy from */ + branchName: string; + /** Organization name for resource naming */ + organizationName: string; + /** Repository name for the source code */ + repositoryName: string; + /** Working folder path within the repository */ + workingFolder: string; + /** Optional tags to apply to all resources */ + tags?: { [key: string]: string }; + /** Optional properties for the core infrastructure stage */ + coreStageProperties?: CoreStageProperties; + /** Default log retention period for CloudWatch logs */ + defaultRetentionPeriod?: RetentionDays; + /** List of container application definitions to deploy */ + applicationList: ContainerDefinition[]; + /** Paths to pet store images for seeding the application */ + petImagesPaths: string[]; + /** PostgreSQL engine version for Aurora database */ + postgresEngineVersion?: AuroraPostgresEngineVersion; + /** Properties for microservices deployment stage */ + microservicesProperties: MicroserviceApplicationsProperties; +} + +/** + * CDK Pipeline stack for the One Observability Workshop. + * + * This stack creates a complete CI/CD pipeline that deploys the workshop infrastructure + * in multiple stages: + * - Core: Networking, security, and foundational services + * - Applications: Container-based applications (ECS/EKS) + * - Backend: Storage (S3, Aurora, DynamoDB) and compute (Lambda, EC2) + * - Microservices: Sample microservices for the pet store application + * + * The pipeline uses AWS CodePipeline with CodeBuild for synthesis and deployment, + * with proper security controls and artifact management. + */ +export class CDKPipeline extends Stack { + /** + * The IAM role used by the pipeline for executing pipeline actions. + * This role has permissions to access the source S3 bucket and other required resources. + */ + readonly pipelineRole: IRole; + + /** + * Creates a new CDK Pipeline stack. + * + * @param scope - The parent construct + * @param id - The construct identifier + * @param properties - Configuration properties for the pipeline + */ + constructor(scope: Construct, id: string, properties: CDKPipelineProperties) { + super(scope, id, properties); + + // Create a CodePipeline source using the Specified S3 Bucket + const configBucket = Bucket.fromBucketName(this, 'ConfigBucket', properties.configBucketName); + const bucketKey = `repo/refs/heads/${properties.branchName}/repo.zip`; + + // Use the configuration file as the pipeline trigger + const bucketSource = CodePipelineSource.s3(configBucket, bucketKey, { + trigger: S3Trigger.POLL, + }); + /** + * Create an S3 bucket to store the pipeline artifacts. + * The bucket has encryption at rest using a CMK and enforces encryption in transit. + * Versioning is enabled on the bucket for audit and recovery purposes. + */ + const pipelineArtifactBucket = new Bucket(this, 'ArtifactBucket', { + enforceSSL: true, + versioned: true, + encryption: BucketEncryption.S3_MANAGED, + blockPublicAccess: BlockPublicAccess.BLOCK_ALL, + }); + + /** + * Add CDK-nag suppressions for the artifact bucket. + */ + NagSuppressions.addResourceSuppressions(pipelineArtifactBucket, [ + { + id: 'AwsSolutions-S1', + reason: 'Temporary artifact bucket, access logs are not needed', + }, + ]); + + /** + * Create the IAM role for the pipeline. + */ + this.pipelineRole = new Role(this, 'PipelineRole', { + assumedBy: new ServicePrincipal('codepipeline.amazonaws.com'), + }); + + /** + * Grant access to the source bucket for the pipeline role. + */ + configBucket.grantRead(this.pipelineRole); + + const synthStep = new CodeBuildStep('Synth', { + input: bucketSource, + primaryOutputDirectory: `${properties.workingFolder}/cdk.out`, + installCommands: ['npm i -g aws-cdk'], + // Using globally installed CDK due to this issue https://github.com/aws/aws-cdk/issues/28519 + commands: ['. ./.env', `cd ${properties.workingFolder}`, 'npm ci', 'npm run build', 'cdk synth --all'], + buildEnvironment: { + buildImage: LinuxBuildImage.STANDARD_7_0, + }, + partialBuildSpec: BuildSpec.fromObject({ + phases: { + install: { + 'runtime-versions': { + nodejs: '22.x', + }, + }, + }, + }), + }); + /** + * Create the CodePipeline with the following configuration: + * - Synthesis step that builds and synthesizes the CDK app + * - Custom artifact bucket with encryption + * - VPC integration for network isolation + * - Cross-account key support for multi-account deployments + */ + const pipeline = new CodePipeline(this, 'Pipeline', { + synth: synthStep, + artifactBucket: pipelineArtifactBucket, + crossAccountKeys: true, + pipelineName: `${id}-pipeline`, + usePipelineRoleForActions: true, + pipelineType: PipelineType.V2, + role: this.pipelineRole, + codeBuildDefaults: { + buildEnvironment: { + buildImage: LinuxBuildImage.STANDARD_7_0, + privileged: true, + environmentVariables: { + NODE_VERSION: { + value: '22.x', + }, + }, + }, + }, + }); + + const coreWave = pipeline.addWave('Core'); + + let stageSequence = 1; + const coreStageTags = { + ...properties.tags, + parent: this.stackName, + sequence: (stageSequence++).toString(), + }; + const coreProperties = properties.coreStageProperties + ? { ...properties.coreStageProperties, tags: coreStageTags } + : { tags: coreStageTags }; + + const coreStage = new CoreStage(this, 'Core', coreProperties); + coreWave.addStage(coreStage); + + const applicationsStageTags = { + ...properties.tags, + parent: this.stackName, + sequence: (stageSequence++).toString(), + }; + coreWave.addStage( + new ContainersPipelineStage(this, 'Applications', { + applicationList: properties.applicationList, + tags: applicationsStageTags, + source: { + bucketName: properties.configBucketName, + bucketKey: bucketKey, + }, + env: properties.env, + }), + ); + + const backendWave = pipeline.addWave('Backend'); + + const storageStage = new StorageStage(this, 'Storage', { + assetsProperties: { + seedPaths: properties.petImagesPaths, + }, + auroraDatabaseProperties: { + engineVersion: properties.postgresEngineVersion, + }, + tags: { + ...properties.tags, + parent: this.stackName, + sequence: (stageSequence++).toString(), + }, + env: properties.env, + }); + + backendWave.addStage(storageStage, { + post: [storageStage.getDDBSeedingStep(this, configBucket)], + }); + + const computeStage = new ComputeStage(this, 'Compute', { + tags: { + ...properties.tags, + parent: this.stackName, + sequence: (stageSequence++).toString(), + }, + env: properties.env, + }); + + backendWave.addStage(computeStage); + + const microservicesStageTags = { + ...properties.tags, + parent: this.stackName, + sequence: (stageSequence++).toString(), + }; + + pipeline.addStage( + new MicroservicesStage(this, 'Microservices', { + ...properties.microservicesProperties, + tags: microservicesStageTags, + env: properties.env, + }), + ); + + /** + * Build the pipeline to add suppressions and customizations. + * This is required before adding additional configurations. + * @see https://github.com/cdklabs/cdk-nag?tab=readme-ov-file#suppressing-aws-cdk-libpipelines-violations + */ + pipeline.buildPipeline(); + + /** + * Grant access to describe Prefix lists + */ + if (pipeline.synthProject.role) { + new Policy(this, 'CloudFormationPolicy', { + statements: [ + new PolicyStatement({ + actions: [ + 'cloudformation:DescribeStacks', + 'cloudformation:ListResources', + 'ec2:DescribeManagedPrefixLists', + 'ec2:GetManagedPrefixListEntries', + ], + resources: ['*'], + }), + ], + roles: [pipeline.synthProject.role], + }); + } + + /** + * Add CDK-nag suppressions for the pipeline role. + */ + NagSuppressions.addResourceSuppressions( + pipeline.pipeline.role, + [ + { + id: 'AwsSolutions-IAM5', + reason: 'The pipeline role is not scoped to a specific resource', + }, + ], + true, + ); + + /** + * Add CDK-nag suppressions for the synth project. + */ + NagSuppressions.addResourceSuppressions( + pipeline.synthProject, + [ + { + id: 'AwsSolutions-IAM5', + reason: 'The pipeline role is not scoped to a specific resource', + }, + { + id: 'AwsSolutions-IAM4', + reason: 'AWS Managed policy is acceptable here', + appliesTo: ['Policy::arn::iam::aws:policy/AWSCodeArtifactReadOnlyAccess'], + }, + ], + true, + ); + + /** + * Add CDK-nag suppressions for the self-mutation project. + */ + NagSuppressions.addResourceSuppressions( + pipeline.selfMutationProject, + [ + { + id: 'AwsSolutions-IAM5', + reason: 'Ephemeral Synth Project not limited to specific resource or action', + }, + ], + true, + ); + + /** + * Add stack-level CDK-nag suppressions. + * Added as stack suppression since path can change based on the context and repo name. + * Suppression can also be limited by path but must be updated every time the repo changes. + */ + NagSuppressions.addStackSuppressions( + this, + [ + { + id: 'AwsSolutions-IAM5', + reason: 'Ephemeral Synth Project not limited to specific resource or action', + }, + { + id: 'AwsSolutions-CB4', + reason: 'CDK Pipelines uses CMKs with cross account / region. Omitted for simplicity.', + }, + ], + true, + ); + + /** + * Generate PipelineArn Output with the self-mutating Pipeline ARN + */ + new CfnOutput(this, 'PipelineArn', { + value: pipeline.pipeline.pipelineArn, + exportName: 'PipelineArn', + }); + + /** + * Tag all child resources of the application + */ + + if (properties.tags) { + Utilities.TagConstruct(this, properties.tags); + } + } +} diff --git a/src/cdk/lib/serverless/canaries/housekeeping/housekeeping.ts b/src/cdk/lib/serverless/canaries/housekeeping/housekeeping.ts new file mode 100644 index 00000000..5160048d --- /dev/null +++ b/src/cdk/lib/serverless/canaries/housekeeping/housekeeping.ts @@ -0,0 +1,18 @@ +import { Construct } from 'constructs'; +import { WorkshopCanary, WorkshopCanaryProperties } from '../../../constructs/canary'; + +interface HouseKeepingCanaryProperties extends WorkshopCanaryProperties { + urlParameterName: string; +} + +export class HouseKeepingCanary extends WorkshopCanary { + constructor(scope: Construct, id: string, properties: HouseKeepingCanaryProperties) { + super(scope, id, properties); + } + createOutputs(): void {} + getEnvironmentVariables(properties: HouseKeepingCanaryProperties): { [key: string]: string } | undefined { + return { + PETSITE_URL_PARAMETER_NAME: properties.urlParameterName, + }; + } +} diff --git a/src/cdk/lib/serverless/canaries/traffic-generator/traffic-generator.ts b/src/cdk/lib/serverless/canaries/traffic-generator/traffic-generator.ts new file mode 100644 index 00000000..d6a29912 --- /dev/null +++ b/src/cdk/lib/serverless/canaries/traffic-generator/traffic-generator.ts @@ -0,0 +1,18 @@ +import { Construct } from 'constructs'; +import { WorkshopCanary, WorkshopCanaryProperties } from '../../../constructs/canary'; + +interface TrafficGeneratorCanaryProperties extends WorkshopCanaryProperties { + urlParameterName: string; +} + +export class TrafficGeneratorCanary extends WorkshopCanary { + constructor(scope: Construct, id: string, properties: TrafficGeneratorCanaryProperties) { + super(scope, id, properties); + } + createOutputs(): void {} + getEnvironmentVariables(properties: TrafficGeneratorCanaryProperties): { [key: string]: string } | undefined { + return { + PETSITE_URL_PARAMETER_NAME: properties.urlParameterName, + }; + } +} diff --git a/src/cdk/lib/serverless/functions/petfood/cleanup-processor.ts b/src/cdk/lib/serverless/functions/petfood/cleanup-processor.ts new file mode 100644 index 00000000..bdbea667 --- /dev/null +++ b/src/cdk/lib/serverless/functions/petfood/cleanup-processor.ts @@ -0,0 +1,111 @@ +import { Construct } from 'constructs'; +import { + WokshopLambdaFunction, + WorkshopLambdaFunctionProperties, + getLambdaInsightsLayerArn, + getOpenTelemetryPythonLayerArn, +} from '../../../constructs/lambda'; +import { IBucket } from 'aws-cdk-lib/aws-s3'; +import { IEventBus, Rule } from 'aws-cdk-lib/aws-events'; +import { ILayerVersion, LayerVersion } from 'aws-cdk-lib/aws-lambda'; +import { BundlingOptions } from 'aws-cdk-lib/aws-lambda-nodejs'; +import { Stack } from 'aws-cdk-lib'; +import { ITable } from 'aws-cdk-lib/aws-dynamodb'; +import { Effect, Policy, PolicyStatement } from 'aws-cdk-lib/aws-iam'; +import { LambdaFunction } from 'aws-cdk-lib/aws-events-targets'; +import { NagSuppressions } from 'cdk-nag'; + +export interface PetfoodCleanupProcessorProperties extends WorkshopLambdaFunctionProperties { + bedrockModelId?: string; + imageBucket: IBucket; + eventBridgeBus?: IEventBus; + petfoodTable: ITable; +} + +export class PetfoodCleanupProcessorFunction extends WokshopLambdaFunction { + constructor(scope: Construct, id: string, properties: PetfoodCleanupProcessorProperties) { + properties = { ...properties, description: 'Generate pet food image' }; + + super(scope, id, properties); + + new Rule(this, 'PetfoodCleanupProcessorRule', { + eventBus: properties.eventBridgeBus!, + eventPattern: { + source: ['petfood.service'], + detailType: ['ItemDiscontinued'], + }, + targets: [new LambdaFunction(this.function, {})], + }); + } + + addFunctionPermissions(properties: PetfoodCleanupProcessorProperties): void { + const functionPolicy = new Policy(this, 'PetfoodImageGeneratorPolicy', { + statements: [ + new PolicyStatement({ + effect: Effect.ALLOW, + actions: ['bedrock:InvokeModel'], + resources: ['*'], + }), + new PolicyStatement({ + effect: Effect.ALLOW, + actions: ['s3:PutObject', 's3:PutObjectAcl', 's3:GetObject', 's3:DeleteObject', 's3:HeadObject'], + resources: [properties.imageBucket.bucketArn + '/*'], + }), + new PolicyStatement({ + effect: Effect.ALLOW, + actions: ['dynamodb:UpdateItem', 'dynamodb:GetItem', 'dynamodb:DeleteItem'], + resources: [properties.petfoodTable.tableArn], + }), + ], + roles: [this.function.role!], + }); + + NagSuppressions.addResourceSuppressions( + [this.function.role!, functionPolicy], + [ + { + id: 'AwsSolutions-IAM4', + reason: 'Managed Policies are acceptable for the task role', + }, + { + id: 'AwsSolutions-IAM5', + reason: 'Permissions are acceptable for the task role', + }, + ], + true, + ); + } + createOutputs(): void {} + getEnvironmentVariables(properties: PetfoodCleanupProcessorProperties): { [key: string]: string } | undefined { + return { + LOG_LEVEL: 'INFO', + FOOD_TABLE_NAME: properties.petfoodTable.tableName, + S3_BUCKET_NAME: properties.imageBucket.bucketName, + BEDROCK_MODEL_ID: properties.bedrockModelId || 'amazon.titan-image-generator-v2:0', + OTEL_PYTHON_DISABLED_INSTRUMENTATIONS: 'none', + OTEL_AWS_APPLICATION_SIGNALS_ENABLED: 'true', + OTEL_METRICS_EXPORTER: 'none', + OTEL_LOGS_EXPORTER: 'none', + OTEL_SERVICE_NAME: properties.name, + OTEL_SERVICE_VERSION: '0.1.0', + AWS_LAMBDA_EXEC_WRAPPER: '/opt/otel-instrument', + }; + } + getLayers(): ILayerVersion[] { + return [ + LayerVersion.fromLayerVersionArn( + this, + 'LambdaInsightsLayer', + getLambdaInsightsLayerArn(Stack.of(this).region), + ), + LayerVersion.fromLayerVersionArn( + this, + 'OpenTelemetryLayer', + getOpenTelemetryPythonLayerArn(Stack.of(this).region), + ), + ]; + } + getBundling(): BundlingOptions { + return {}; + } +} diff --git a/src/cdk/lib/serverless/functions/petfood/image-generator.ts b/src/cdk/lib/serverless/functions/petfood/image-generator.ts new file mode 100644 index 00000000..6604efa8 --- /dev/null +++ b/src/cdk/lib/serverless/functions/petfood/image-generator.ts @@ -0,0 +1,119 @@ +import { Construct } from 'constructs'; +import { + WokshopLambdaFunction, + WorkshopLambdaFunctionProperties, + getOpenTelemetryPythonLayerArn, + getLambdaInsightsLayerArn, +} from '../../../constructs/lambda'; +import { IBucket } from 'aws-cdk-lib/aws-s3'; +import { IEventBus, Rule } from 'aws-cdk-lib/aws-events'; +import { ILayerVersion, LayerVersion } from 'aws-cdk-lib/aws-lambda'; +import { BundlingOptions } from 'aws-cdk-lib/aws-lambda-nodejs'; +import { Stack } from 'aws-cdk-lib'; +import { ITable } from 'aws-cdk-lib/aws-dynamodb'; +import { Effect, Policy, PolicyStatement } from 'aws-cdk-lib/aws-iam'; +import { LambdaFunction } from 'aws-cdk-lib/aws-events-targets'; +import { NagSuppressions } from 'cdk-nag'; + +export interface PetfoodImageGeneratorProperties extends WorkshopLambdaFunctionProperties { + bedrockModelId?: string; + imageBucket: IBucket; + eventBridgeBus?: IEventBus; + petfoodTable: ITable; +} + +export class PetfoodImageGeneratorFunction extends WokshopLambdaFunction { + constructor(scope: Construct, id: string, properties: PetfoodImageGeneratorProperties) { + properties = { ...properties, description: 'Generate pet food image' }; + + super(scope, id, properties); + + new Rule(this, 'PetfoodImageGeneratorUpdatedRule', { + eventBus: properties.eventBridgeBus!, + eventPattern: { + source: ['petfood.service'], + detailType: ['FoodItemUpdated'], + }, + targets: [new LambdaFunction(this.function, {})], + }); + + new Rule(this, 'PetfoodImageGeneratorCreatedRule', { + eventBus: properties.eventBridgeBus!, + eventPattern: { + source: ['petfood.service'], + detailType: ['FoodItemCreated'], + }, + targets: [new LambdaFunction(this.function, {})], + }); + } + + addFunctionPermissions(properties: PetfoodImageGeneratorProperties): void { + const functionPolicy = new Policy(this, 'PetfoodImageGeneratorPolicy', { + statements: [ + new PolicyStatement({ + effect: Effect.ALLOW, + actions: ['bedrock:InvokeModel'], + resources: ['*'], + }), + new PolicyStatement({ + effect: Effect.ALLOW, + actions: ['s3:PutObject', 's3:PutObjectAcl', 's3:GetObject'], + resources: [properties.imageBucket.bucketArn + '/*'], + }), + new PolicyStatement({ + effect: Effect.ALLOW, + actions: ['dynamodb:UpdateItem', 'dynamodb:GetItem'], + resources: [properties.petfoodTable.tableArn], + }), + ], + roles: [this.function.role!], + }); + NagSuppressions.addResourceSuppressions( + [this.function.role!, functionPolicy], + [ + { + id: 'AwsSolutions-IAM4', + reason: 'Managed Policies are acceptable for the task role', + }, + { + id: 'AwsSolutions-IAM5', + reason: 'Permissions are acceptable for the task role', + }, + ], + true, + ); + } + createOutputs(): void {} + getEnvironmentVariables(properties: PetfoodImageGeneratorProperties): { [key: string]: string } | undefined { + return { + LOG_LEVEL: 'INFO', + FOOD_TABLE_NAME: properties.petfoodTable.tableName, + S3_BUCKET_NAME: properties.imageBucket.bucketName, + BEDROCK_MODEL_ID: properties.bedrockModelId || 'amazon.titan-image-generator-v2:0', + OTEL_PYTHON_DISABLED_INSTRUMENTATIONS: 'none', + OTEL_AWS_APPLICATION_SIGNALS_ENABLED: 'true', + OTEL_METRICS_EXPORTER: 'none', + OTEL_LOGS_EXPORTER: 'none', + OTEL_SERVICE_NAME: properties.name, + OTEL_SERVICE_VERSION: '0.1.0', + AWS_LAMBDA_EXEC_WRAPPER: '/opt/otel-instrument', + }; + } + getLayers(): ILayerVersion[] { + return [ + LayerVersion.fromLayerVersionArn( + this, + 'LambdaInsightsLayer', + getLambdaInsightsLayerArn(Stack.of(this).region), + ), + LayerVersion.fromLayerVersionArn( + this, + 'OpenTelemetryLayer', + getOpenTelemetryPythonLayerArn(Stack.of(this).region), + ), + ]; + } + getBundling(): BundlingOptions { + return {}; + } +} diff --git a/src/cdk/lib/serverless/functions/status-updater/status-updater.ts b/src/cdk/lib/serverless/functions/status-updater/status-updater.ts new file mode 100644 index 00000000..0bc03592 --- /dev/null +++ b/src/cdk/lib/serverless/functions/status-updater/status-updater.ts @@ -0,0 +1,184 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +import { ITable } from 'aws-cdk-lib/aws-dynamodb'; +import { + WokshopLambdaFunction, + WorkshopLambdaFunctionProperties, + getLambdaInsightsLayerArn, +} from '../../../constructs/lambda'; +import { Construct } from 'constructs'; +import { ManagedPolicy, PolicyDocument, Effect, PolicyStatement, StarPrincipal } from 'aws-cdk-lib/aws-iam'; +import { ILayerVersion, LayerVersion } from 'aws-cdk-lib/aws-lambda'; +import { RemovalPolicy, Stack } from 'aws-cdk-lib'; +import { BundlingOptions } from 'aws-cdk-lib/aws-lambda-nodejs'; +import { + EndpointType, + LambdaRestApi, + LogGroupLogDestination, + MethodLoggingLevel, + RequestAuthorizer, +} from 'aws-cdk-lib/aws-apigateway'; +import { NagSuppressions } from 'cdk-nag'; +import { LogGroup, RetentionDays } from 'aws-cdk-lib/aws-logs'; +import { IVpcEndpoint } from 'aws-cdk-lib/aws-ec2'; +import { Utilities } from '../../../utils/utilities'; +import { PARAMETER_STORE_PREFIX } from '../../../../bin/environment'; + +export interface StatusUpdaterServiceProperties extends WorkshopLambdaFunctionProperties { + table: ITable; + vpcEndpoint?: IVpcEndpoint; +} + +export class StatusUpdatedService extends WokshopLambdaFunction { + public api: LambdaRestApi; + constructor(scope: Construct, id: string, properties: StatusUpdaterServiceProperties) { + properties = { ...properties, description: 'Update Pet availability status' }; + + super(scope, id, properties); + + const accesLogs = new LogGroup(this, 'access-logs', { + logGroupName: `/aws/apigw/${properties.name}-api/access-logs`, + retention: properties.logRetentionDays || RetentionDays.ONE_WEEK, + removalPolicy: RemovalPolicy.DESTROY, + }); + + const authorizer = new RequestAuthorizer(this, `${properties.name}-authorizer`, { + handler: this.function, + identitySources: ['method.request.header.Authorization'], + resultsCacheTtl: undefined, + authorizerName: `${properties.name}-authorizer`, + }); + + this.api = new LambdaRestApi(this, `${properties.name}-api`, { + handler: this.function, + description: 'Update Pet availability status', + proxy: true, + endpointConfiguration: { + types: [EndpointType.PRIVATE], + }, + policy: new PolicyDocument({ + statements: [ + new PolicyStatement({ + effect: Effect.ALLOW, + principals: [new StarPrincipal()], + actions: ['execute-api:Invoke'], + resources: ['*'], + conditions: { + StringEquals: { + 'aws:sourceVpce': properties.vpcEndpoint?.vpcEndpointId || 'vpce-*', + }, + }, + }), + ], + }), + cloudWatchRole: true, + deployOptions: { + tracingEnabled: true, + loggingLevel: MethodLoggingLevel.INFO, + stageName: 'prod', + accessLogDestination: new LogGroupLogDestination(accesLogs), + }, + defaultMethodOptions: { + methodResponses: [], + authorizer: authorizer, + }, + }); + + this.api.addRequestValidator(`${properties.name}-req-validator`, { + validateRequestBody: true, + validateRequestParameters: true, + }); + + NagSuppressions.addResourceSuppressions( + this.api, + [ + { + id: 'AwsSolutions-IAM4', + reason: 'Cloudwatch Managed Policy is acceptable for Service Role', + appliesTo: [ + 'Policy::arn::iam::aws:policy/service-role/AmazonAPIGatewayPushToCloudWatchLogs', + ], + }, + ], + true, + ); + + NagSuppressions.addResourceSuppressions( + this.api, + [ + { + id: 'AwsSolutions-COG4', + reason: 'Private API. Authentication is not required for now as the private zone is considered trusted', + }, + ], + true, + ); + + this.createOutputs(); + } + addFunctionPermissions(properties: StatusUpdaterServiceProperties): void { + if (this.function) { + this.function.role?.addManagedPolicy( + ManagedPolicy.fromAwsManagedPolicyName('CloudWatchLambdaInsightsExecutionRolePolicy'), + ); + this.function.role?.addManagedPolicy( + ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'), + ); + + properties.table.grantReadWriteData(this.function); + + NagSuppressions.addResourceSuppressions( + this.function.role!, + [ + { + id: 'AwsSolutions-IAM4', + reason: 'Managed Policies are acceptable for the task role', + }, + { + id: 'AwsSolutions-IAM5', + reason: 'Permissions are acceptable for the task role', + }, + ], + true, + ); + } + } + createOutputs(): void { + if (this.api) { + Utilities.createSsmParameters( + this, + PARAMETER_STORE_PREFIX, + new Map( + Object.entries({ + updateadoptionstatusurl: this.api.url, + }), + ), + ); + } else { + throw new Error('Service is not defined'); + } + } + getEnvironmentVariables(properties: StatusUpdaterServiceProperties): { [key: string]: string } | undefined { + // No environment variables to create + return { + TABLE_NAME: properties.table.tableName, + }; + } + getLayers(): ILayerVersion[] { + return [ + LayerVersion.fromLayerVersionArn( + this, + 'LambdaInsightsLayer', + getLambdaInsightsLayerArn(Stack.of(this).region), + ), + ]; + } + getBundling(): BundlingOptions { + return { + externalModules: [], + nodeModules: ['aws-xray-sdk-core', '@aws-sdk/client-dynamodb', '@aws-sdk/lib-dynamodb'], + }; + } +} diff --git a/src/cdk/lib/serverless/functions/traffic-generator/traffic-generator.ts b/src/cdk/lib/serverless/functions/traffic-generator/traffic-generator.ts new file mode 100644 index 00000000..71fa2ebb --- /dev/null +++ b/src/cdk/lib/serverless/functions/traffic-generator/traffic-generator.ts @@ -0,0 +1,105 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +import { + WokshopLambdaFunction, + WorkshopLambdaFunctionProperties, + getLambdaInsightsLayerArn, +} from '../../../constructs/lambda'; +import { Construct } from 'constructs'; +import { ManagedPolicy, PolicyDocument, Effect, PolicyStatement, Policy } from 'aws-cdk-lib/aws-iam'; +import { ILayerVersion, LayerVersion } from 'aws-cdk-lib/aws-lambda'; +import { Arn, ArnFormat, Stack } from 'aws-cdk-lib'; +import { BundlingOptions } from 'aws-cdk-lib/aws-lambda-nodejs'; +import { LambdaRestApi } from 'aws-cdk-lib/aws-apigateway'; +import { NagSuppressions } from 'cdk-nag'; +import { Canary } from 'aws-cdk-lib/aws-synthetics'; + +export interface TrafficGeneratorFunctionProperties extends WorkshopLambdaFunctionProperties { + trafficCanary: Canary; +} + +export class TrafficGeneratorFunction extends WokshopLambdaFunction { + public api: LambdaRestApi; + constructor(scope: Construct, id: string, properties: TrafficGeneratorFunctionProperties) { + super(scope, id, properties); + + this.createOutputs(); + } + addFunctionPermissions(properties: TrafficGeneratorFunctionProperties): void { + if (this.function) { + this.function.role?.addManagedPolicy( + ManagedPolicy.fromAwsManagedPolicyName('CloudWatchLambdaInsightsExecutionRolePolicy'), + ); + this.function.role?.addManagedPolicy( + ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSLambdaBasicExecutionRole'), + ); + + new Policy(this, 'TrafficGeneratorPolicy', { + policyName: 'TrafficGeneratorPolicy', + document: new PolicyDocument({ + statements: [ + new PolicyStatement({ + effect: Effect.ALLOW, + actions: ['lambda:InvokeFunction'], + resources: [this.getCanaryFunctionArn(properties.trafficCanary)], + }), + ], + }), + roles: [this.function.role!], + }); + + NagSuppressions.addResourceSuppressions( + this.function.role!, + [ + { + id: 'AwsSolutions-IAM4', + reason: 'Managed Policies are acceptable for the task role', + }, + { + id: 'AwsSolutions-IAM5', + reason: 'Permissions are acceptable for the task role', + }, + ], + true, + ); + } + } + createOutputs(): void {} + getEnvironmentVariables(properties: TrafficGeneratorFunctionProperties): { [key: string]: string } | undefined { + // No environment variables to create + return { + FUNCTION_ARN: this.getCanaryFunctionArn(properties.trafficCanary), + }; + } + + getCanaryFunctionArn(canary: Canary) { + return Arn.format( + { + service: 'lambda', + resource: 'function', + arnFormat: ArnFormat.COLON_RESOURCE_NAME, + resourceName: canary.canaryName, + }, + Stack.of(this), + ); + } + + getBundling(): BundlingOptions { + return { + externalModules: [], + nodeModules: ['@aws-sdk/client-lambda'], + }; + } + + getLayers(): ILayerVersion[] { + return [ + LayerVersion.fromLayerVersionArn( + this, + 'LambdaInsightsLayer', + getLambdaInsightsLayerArn(Stack.of(this).region), + ), + ]; + } +} diff --git a/src/cdk/lib/stages/applications.ts b/src/cdk/lib/stages/applications.ts new file mode 100644 index 00000000..e6a818b6 --- /dev/null +++ b/src/cdk/lib/stages/applications.ts @@ -0,0 +1,359 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import { RemovalPolicy, Stack, StackProps, Stage } from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import { Utilities } from '../utils/utilities'; +import { WorkshopNetwork } from '../constructs/network'; +import { WorkshopEcs } from '../constructs/ecs'; +import { Microservice, MicroservicesNames } from '../constructs/microservice'; +import { ComputeType, HostType, PARAMETER_STORE_PREFIX } from '../../bin/environment'; +import { PayForAdoptionService } from '../microservices/pay-for-adoption'; +import { AuroraDatabase } from '../constructs/database'; +import { DynamoDatabase } from '../constructs/dynamodb'; +import { ListAdoptionsService } from '../microservices/petlist-adoptions'; +import { PetSearchService } from '../microservices/pet-search'; +import { LambdaFunctionNames, WorkshopLambdaFunctionProperties } from '../constructs/lambda'; +import { StatusUpdatedService } from '../serverless/functions/status-updater/status-updater'; +import { VpcEndpoints } from '../constructs/vpc-endpoints'; +import { PetSite } from '../microservices/petsite'; +import { WorkshopEks } from '../constructs/eks'; +import { SubnetType } from 'aws-cdk-lib/aws-ec2'; +import { OpenSearchCollection } from '../constructs/opensearch-collection'; +import { WorkshopAssets } from '../constructs/assets'; +import { EventBusResources } from '../constructs/eventbus'; +import { PetFoodECSService } from '../microservices/petfood'; +import { CanaryNames, WorkshopCanaryProperties } from '../constructs/canary'; +import { TrafficGeneratorFunction } from '../serverless/functions/traffic-generator/traffic-generator'; +import { Bucket } from 'aws-cdk-lib/aws-s3'; +import { HouseKeepingCanary } from '../serverless/canaries/housekeeping/housekeeping'; +import { TrafficGeneratorCanary } from '../serverless/canaries/traffic-generator/traffic-generator'; +import { NagSuppressions } from 'cdk-nag'; +import { PetfoodCleanupProcessorFunction } from '../serverless/functions/petfood/cleanup-processor'; +import { PetfoodImageGeneratorFunction } from '../serverless/functions/petfood/image-generator'; + +export interface MicroserviceApplicationPlacement { + hostType: HostType; + computeType: ComputeType; + disableService: boolean; + manifestPath?: string; +} + +interface ImportedResources { + vpcExports: any; + ecsExports: any; + eksExports: any; + rdsExports: any; + dynamodbExports: any; + vpcEndpoints: any; + cloudMap: any; + openSearchExports: any; + assetsBucket: any; + eventBusExports: any; + baseURI: string; +} + +export interface MicroserviceApplicationsProperties extends StackProps { + /** Tags to apply to all resources in the stage */ + tags?: { [key: string]: string }; + microservicesPlacement: Map; + lambdaFunctions: Map; + canaries: Map; +} + +export class MicroservicesStage extends Stage { + public stack: MicroservicesStack; + constructor(scope: Construct, id: string, properties: MicroserviceApplicationsProperties) { + super(scope, id, properties); + + this.stack = new MicroservicesStack(this, 'Microservice', properties); + + if (properties.tags) { + Utilities.TagConstruct(this.stack, properties.tags); + } + } +} + +export class MicroservicesStack extends Stack { + public microservices: Map; + + constructor(scope: Construct, id: string, properties: MicroserviceApplicationsProperties) { + super(scope, id, properties); + + // Import all required resources + const imports = this.importResources(); + + // Create microservices + this.createMicroservices(properties, imports); + + // Create canaries and Lambda functions + this.createCanariesAndLambdas(properties, imports); + + Utilities.SuppressLogRetentionNagWarnings(this); + Utilities.SuppressKubectlProviderNagWarnings(this); + } + + private importResources() { + const vpcExports = WorkshopNetwork.importVpcFromExports(this, 'WorkshopVpc'); + const ecsExports = WorkshopEcs.importFromExports(this, 'WorkshopEcs', vpcExports); + const eksExports = WorkshopEks.importFromExports(this, 'WorkshopEks'); + const rdsExports = AuroraDatabase.importFromExports(this, 'AuroraDatabase'); + const dynamodbExports = DynamoDatabase.importFromExports(this, 'DynamoDatabase'); + const vpcEndpoints = VpcEndpoints.importFromExports(this, 'VpcEndpoints'); + const cloudMap = WorkshopNetwork.importCloudMapNamespaceFromExports(this, 'CloudMapNamespace'); + const openSearchExports = OpenSearchCollection.importFromExports(); + const assetsBucket = WorkshopAssets.importBucketFromExports(this, 'WorkshopAssets'); + const eventBusExports = EventBusResources.importFromExports(this, 'EventBusResources'); + const baseURI = `${Stack.of(this).account}.dkr.ecr.${Stack.of(this).region}.amazonaws.com`; + + return { + vpcExports, + ecsExports, + eksExports, + rdsExports, + dynamodbExports, + vpcEndpoints, + cloudMap, + openSearchExports, + assetsBucket, + eventBusExports, + baseURI, + }; + } + + private createMicroservices(properties: MicroserviceApplicationsProperties, imports: ImportedResources) { + this.microservices = new Map(); + + for (const name of properties.microservicesPlacement.keys()) { + const service = properties.microservicesPlacement.get(name); + let svc; + + if (name == MicroservicesNames.PayForAdoption) { + if (service?.hostType == HostType.ECS) { + svc = new PayForAdoptionService(this, name, { + hostType: service.hostType, + computeType: service.computeType, + securityGroup: imports.ecsExports.securityGroup, + ecsCluster: imports.ecsExports.cluster, + disableService: service.disableService, + cpu: 1024, + memoryLimitMiB: 2048, + desiredTaskCount: 2, + name: name, + repositoryURI: `${imports.baseURI}/${name}`, + database: imports.rdsExports.cluster, + secret: imports.rdsExports.adminSecret, + table: imports.dynamodbExports.table, + healthCheck: '/health/status', + vpc: imports.vpcExports, + subnetType: SubnetType.PRIVATE_WITH_EGRESS, + createLoadBalancer: true, + cloudMapNamespace: imports.cloudMap, + }); + } else { + throw new Error(`EKS is not supported for ${name}`); + } + if (svc) { + this.microservices.set(name, svc); + } + } + if (name == MicroservicesNames.PetListAdoptions) { + if (service?.hostType == HostType.ECS) { + svc = new ListAdoptionsService(this, name, { + hostType: service.hostType, + computeType: service.computeType, + securityGroup: imports.ecsExports.securityGroup, + ecsCluster: imports.ecsExports.cluster, + disableService: service.disableService, + cpu: 1024, + memoryLimitMiB: 2048, + desiredTaskCount: 2, + name: name, + repositoryURI: `${imports.baseURI}/${name}`, + database: imports.rdsExports.cluster, + secret: imports.rdsExports.adminSecret, + healthCheck: '/health/status', + vpc: imports.vpcExports, + subnetType: SubnetType.PRIVATE_WITH_EGRESS, + createLoadBalancer: true, + cloudMapNamespace: imports.cloudMap, + }); + } else { + throw new Error(`EKS is not supported for ${name}`); + } + if (svc) { + this.microservices.set(name, svc); + } + } + if (name == MicroservicesNames.PetSearch) { + if (service?.hostType == HostType.ECS) { + svc = new PetSearchService(this, name, { + hostType: service.hostType, + computeType: service.computeType, + securityGroup: imports.ecsExports.securityGroup, + ecsCluster: imports.ecsExports.cluster, + disableService: service.disableService, + cpu: 1024, + memoryLimitMiB: 2048, + desiredTaskCount: 2, + name: name, + repositoryURI: `${imports.baseURI}/${name}`, + database: imports.rdsExports.cluster, + secret: imports.rdsExports.adminSecret, + healthCheck: '/health/status', + vpc: imports.vpcExports, + subnetType: SubnetType.PRIVATE_WITH_EGRESS, + createLoadBalancer: true, + cloudMapNamespace: imports.cloudMap, + table: imports.dynamodbExports.table, + bucket: imports.assetsBucket, + }); + } else { + throw new Error(`EKS is not supported for ${name}`); + } + if (svc) { + this.microservices.set(name, svc); + } + } + if (name == MicroservicesNames.PetFood) { + if (service?.hostType == HostType.ECS) { + svc = new PetFoodECSService(this, name, { + hostType: service.hostType, + computeType: service.computeType, + securityGroup: imports.ecsExports.securityGroup, + ecsCluster: imports.ecsExports.cluster, + disableService: service.disableService, + cpu: 1024, + memoryLimitMiB: 2048, + desiredTaskCount: 2, + name: name, + repositoryURI: `${imports.baseURI}/${name}`, + healthCheck: '/health/status', + vpc: imports.vpcExports, + subnetType: SubnetType.PRIVATE_WITH_EGRESS, + createLoadBalancer: true, + cloudMapNamespace: imports.cloudMap, + petFoodTable: imports.dynamodbExports.petFoodsTable, + petFoodCartTable: imports.dynamodbExports.petFoodsCartTable, + additionalEnvironment: { + PETFOOD_ENABLE_JSON_LOGGING: 'true', + PETFOOD_OTLP_ENDPOINT: 'http://localhost:4317', + AWS_REGION: Stack.of(this).region, + PETFOOD_FOODS_TABLE_NAME: imports.dynamodbExports.petFoodsTable.tableName, + PETFOOD_CARTS_TABLE_NAME: imports.dynamodbExports.petFoodsCartTable.tableName, + }, + assetsBucket: imports.assetsBucket, + containerPort: 8080, + // Use pipeline if available, otherwise fall back to direct collection access + ...(imports.ecsExports.openSearchPipeline + ? { openSearchPipeline: imports.ecsExports.openSearchPipeline } + : { openSearchCollection: imports.openSearchExports }), + }); + } else { + throw new Error(`EKS is not supported for ${name}`); + } + if (svc) { + this.microservices.set(name, svc); + } + } + if (name == MicroservicesNames.PetSite) { + if (service?.hostType == HostType.EKS) { + svc = new PetSite(this, name, { + hostType: service.hostType, + computeType: service.computeType, + securityGroup: imports.eksExports.securityGroup, + eksCluster: imports.eksExports.cluster, + disableService: service.disableService, + name: name, + repositoryURI: `${imports.baseURI}/${name}`, + manifestPath: service.manifestPath, + vpc: imports.vpcExports, + subnetType: SubnetType.PRIVATE_WITH_EGRESS, + listenerPort: 80, + healthCheck: '/health/status', + }); + } else { + throw new Error(`ECS is not supported for ${name}`); + } + if (svc) { + this.microservices.set(name, svc); + } + } + } + } + + private createCanariesAndLambdas(properties: MicroserviceApplicationsProperties, imports: ImportedResources) { + const canaryArtifactBucket = new Bucket(this, 'CanaryArtifacts', { + autoDeleteObjects: true, + removalPolicy: RemovalPolicy.DESTROY, + enforceSSL: true, + }); + + let trafficCanary; + for (const name of properties.canaries.keys()) { + const canaryProperties = properties.canaries.get(name) as WorkshopCanaryProperties; + + if (name == CanaryNames.Petsite) { + trafficCanary = new TrafficGeneratorCanary(this, name, { + ...canaryProperties, + artifactsBucket: canaryArtifactBucket, + urlParameterName: `${PARAMETER_STORE_PREFIX}/petsiteurl`, + }); + } + if (name == CanaryNames.HouseKeeping) { + new HouseKeepingCanary(this, name, { + ...canaryProperties, + artifactsBucket: canaryArtifactBucket, + urlParameterName: `${PARAMETER_STORE_PREFIX}/petsiteurl`, + }); + } + } + + if (!trafficCanary) { + throw new Error('Traffic canary not found'); + } + + for (const name of properties.lambdaFunctions.keys()) { + const lambdafunction = properties.lambdaFunctions.get(name) as WorkshopLambdaFunctionProperties; + + if (name == LambdaFunctionNames.StatusUpdater) { + new StatusUpdatedService(this, name, { + ...lambdafunction, + table: imports.dynamodbExports.table, + vpcEndpoint: imports.vpcEndpoints.apiGatewayEndpoint, + }); + } + if (name == LambdaFunctionNames.TrafficGenerator) { + new TrafficGeneratorFunction(this, name, { + ...lambdafunction, + trafficCanary: trafficCanary.canary, + }); + } + if (name == LambdaFunctionNames.PetfoodCleanupProcessor) { + new PetfoodCleanupProcessorFunction(this, name, { + ...lambdafunction, + imageBucket: imports.assetsBucket, + eventBridgeBus: imports.eventBusExports.eventBus, + petfoodTable: imports.dynamodbExports.petFoodsTable, + }); + } + if (name == LambdaFunctionNames.PetfoodImageGenerator) { + new PetfoodImageGeneratorFunction(this, name, { + ...lambdafunction, + imageBucket: imports.assetsBucket, + eventBridgeBus: imports.eventBusExports.eventBus, + petfoodTable: imports.dynamodbExports.petFoodsTable, + }); + } + } + + NagSuppressions.addResourceSuppressions(canaryArtifactBucket, [ + { + id: 'AwsSolutions-S1', + reason: 'This bucket is used for canary artifacts and does not need access logs', + }, + ]); + } +} diff --git a/src/cdk/lib/stages/compute.ts b/src/cdk/lib/stages/compute.ts new file mode 100644 index 00000000..99ca6e08 --- /dev/null +++ b/src/cdk/lib/stages/compute.ts @@ -0,0 +1,84 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +import { Stack, StackProps, Stage } from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import { Utilities } from '../utils/utilities'; +import { WorkshopNetwork } from '../constructs/network'; +import { QueueResources } from '../constructs/queue'; +import { WorkshopEcs } from '../constructs/ecs'; +import { WorkshopEks } from '../constructs/eks'; +import { OpenSearchCollection } from '../constructs/opensearch-collection'; +import { OpenSearchPipeline } from '../constructs/opensearch-pipeline'; + +export interface ComputeProperties extends StackProps { + /** Tags to apply to all resources in the stage */ + tags?: { [key: string]: string }; + ecsEc2Capacity?: number; + ecsEc2InstanceType?: string; + eksEc2Capacity?: number; + eksEc2InstanceType?: string; +} + +export class ComputeStage extends Stage { + public stack: ComputeStack; + constructor(scope: Construct, id: string, properties: ComputeProperties) { + super(scope, id, properties); + + this.stack = new ComputeStack(this, 'ComputeStack', properties); + + if (properties.tags) { + Utilities.TagConstruct(this.stack, properties.tags); + } + } +} + +/** + * Stack for compute resources including ECS cluster and auto scaling group + */ +export class ComputeStack extends Stack { + /** ECS construct */ + public ecs: WorkshopEcs; + /** EKS construct */ + public eks: WorkshopEks; + /** OpenSearch ingestion pipeline */ + public openSearchPipeline: OpenSearchPipeline; + + /** + * Creates a new ComputeStack + * @param scope - The parent construct + * @param id - The construct id + * @param properties - Stack properties including EC2 configuration + */ + constructor(scope: Construct, id: string, properties?: ComputeProperties) { + super(scope, id, properties); + + const vpc = WorkshopNetwork.importVpcFromExports(this, 'WorkshopVpc'); + const { topic } = QueueResources.importFromExports(this, 'ImportedQueueResources'); + + // Import OpenSearch collection + const openSearchCollection = OpenSearchCollection.importFromExports(); + + // Create OpenSearch ingestion pipeline + this.openSearchPipeline = new OpenSearchPipeline(this, 'LogsIngestionPipeline', { + pipelineName: 'petsite-logs-pipeline', + openSearchCollection: openSearchCollection, + indexTemplate: 'pet-collection-logs', + }); + + this.ecs = new WorkshopEcs(this, 'PetsiteECS', { + vpc, + topic, + openSearchPipeline: this.openSearchPipeline, + ecsEc2Capacity: properties?.ecsEc2Capacity, + ecsEc2InstanceType: properties?.ecsEc2InstanceType, + }); + + this.eks = new WorkshopEks(this, 'PetsiteEKS', { + vpc, + eksEc2Capacity: properties?.eksEc2Capacity, + eksEc2InstanceType: properties?.eksEc2InstanceType, + }); + } +} diff --git a/src/cdk/lib/stages/containers.ts b/src/cdk/lib/stages/containers.ts new file mode 100644 index 00000000..604d29af --- /dev/null +++ b/src/cdk/lib/stages/containers.ts @@ -0,0 +1,287 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +import { Arn, ArnFormat, RemovalPolicy, Stack, StackProps, Stage, Duration } from 'aws-cdk-lib'; +import { Artifact, Pipeline, PipelineType } from 'aws-cdk-lib/aws-codepipeline'; +import { Repository, TagMutability } from 'aws-cdk-lib/aws-ecr'; +import { Construct } from 'constructs'; +import { NagSuppressions } from 'cdk-nag'; +import { + EcrBuildAndPublishAction, + RegistryType, + S3SourceAction, + S3Trigger, +} from 'aws-cdk-lib/aws-codepipeline-actions'; +import { Bucket } from 'aws-cdk-lib/aws-s3'; +import { CompositePrincipal, Policy, PolicyStatement, Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam'; +import { Function, Runtime, Code } from 'aws-cdk-lib/aws-lambda'; +import { Rule, EventPattern } from 'aws-cdk-lib/aws-events'; +import { LambdaFunction } from 'aws-cdk-lib/aws-events-targets'; + +/** + * Definition for an application to be built and deployed + */ +export interface ContainerDefinition { + /** The name of the application */ + name: string; + /** Path to the Dockerfile for building the application */ + dockerFilePath: string; +} + +/** + * Properties for S3 source configuration + */ +export interface S3SourceProperties { + /** Name of the S3 bucket containing source code */ + bucketName: string; + /** Key/path to the source code object in S3 */ + bucketKey: string; +} + +/** + * Properties for the Containers Pipeline Stage + */ +export interface ContainersPipelineStageProperties extends StackProps { + /** S3 source configuration */ + source: S3SourceProperties; + /** List of applications to build and deploy */ + applicationList: ContainerDefinition[]; +} + +/** + * CDK Stage for the Containers Pipeline + */ +export class ContainersPipelineStage extends Stage { + /** + * Creates a new Containers Pipeline Stage + * @param scope - The scope in which to define this construct + * @param id - The scoped construct ID + * @param properties - Configuration properties for the stage + */ + constructor(scope: Construct, id: string, properties?: ContainersPipelineStageProperties) { + super(scope, id); + new ContainersStack(this, 'ContainersStack', properties); + } +} + +/** + * Stack containing the containers build pipeline and ECR repositories + */ +export class ContainersStack extends Stack { + /** Map of application names to their ECR repositories */ + public applicationRepositories: Map = new Map(); + /** The CodePipeline for building applications */ + public pipeline: Pipeline; + + /** + * Creates a new Containers Stack + * @param scope - The scope in which to define this construct + * @param id - The scoped construct ID + * @param properties - Configuration properties for the stack + * @throws Error when source or applicationList properties are missing + */ + constructor(scope: Construct, id: string, properties?: ContainersPipelineStageProperties) { + super(scope, id, properties); + + if (!properties?.source || !properties?.applicationList) { + throw new Error('Source and applicationList are required'); + } + + const pipelineRole = new Role(this, 'PipelineRole', { + assumedBy: new ServicePrincipal('codepipeline.amazonaws.com'), + }); + + const codeBuildRole = new Role(this, 'CodeBuildRole', { + assumedBy: new CompositePrincipal(new ServicePrincipal('codebuild.amazonaws.com'), pipelineRole), + }); + + // Create ECR repositories for each application + for (const app of properties.applicationList) { + const repository = new Repository(this, `${app.name}Repository`, { + repositoryName: app.name.toLowerCase(), + imageScanOnPush: true, + emptyOnDelete: true, + imageTagMutability: TagMutability.MUTABLE, + removalPolicy: RemovalPolicy.DESTROY, + }); + + repository.grantPullPush(codeBuildRole); + NagSuppressions.addResourceSuppressions(repository, [ + { + id: 'AwsSolutions-ECR1', + reason: 'This is a sample application, so no access logging is required', + }, + ]); + + this.applicationRepositories.set(app.name, repository); + } + + // Create CodePipeline + this.pipeline = new Pipeline(this, 'ContainersPipeline', { + restartExecutionOnUpdate: true, + pipelineType: PipelineType.V2, + usePipelineRoleForActions: true, + role: pipelineRole, + pipelineName: `${this.stackName}-pipeline`, + }); + + const sourceOutput = new Artifact(); + const sourceBucket = Bucket.fromBucketName(this, 'SourceBucket', properties.source.bucketName); + + const pipelineLogArn = Arn.format( + { + service: 'logs', + resource: 'log-group', + resourceName: '/aws/codepipeline/*', + arnFormat: ArnFormat.COLON_RESOURCE_NAME, + account: this.account, + region: this.region, + partition: 'aws', + }, + Stack.of(this), + ); + + const cloudWatchPolicy = new Policy(this, 'CloudwatchPolicy', { + statements: [ + new PolicyStatement({ + actions: ['logs:CreateLogGroup', 'logs:CreateLogStream', 'logs:PutLogEvents'], + resources: [pipelineLogArn], + }), + ], + roles: [pipelineRole], + }); + + sourceBucket.grantRead(pipelineRole); + this.pipeline.node.addDependency(cloudWatchPolicy); + + const sourceAction = new S3SourceAction({ + actionName: 'Source', + bucket: sourceBucket, + bucketKey: properties.source.bucketKey, + output: sourceOutput, + trigger: S3Trigger.POLL, + }); + + this.pipeline.addStage({ + stageName: 'Source', + actions: [sourceAction], + }); + + // Create build steps for each application (parallel execution) + const buildSteps = properties.applicationList.map((app) => { + const repository = this.applicationRepositories.get(app.name)!; + + return new EcrBuildAndPublishAction({ + actionName: `Build-${app.name}`, + repositoryName: repository.repositoryName, + registryType: RegistryType.PRIVATE, + dockerfileDirectoryPath: app.dockerFilePath, + input: sourceOutput, + imageTags: ['latest'], + role: codeBuildRole, + }); + }); + + // Add build stage with all steps running in parallel + this.pipeline.addStage({ + stageName: 'build', + actions: buildSteps, + }); + + NagSuppressions.addResourceSuppressions( + this.pipeline.artifactBucket, + [ + { + id: 'AwsSolutions-S1', + reason: 'Artifact Bucket for application pipeline, access logs not needed', + }, + ], + true, + ); + + NagSuppressions.addResourceSuppressions( + [codeBuildRole, this.pipeline.role], + [ + { + id: 'AwsSolutions-IAM5', + reason: 'Allow access to repositories and Artifact bucket', + }, + ], + true, + ); + + NagSuppressions.addResourceSuppressions( + cloudWatchPolicy, + [ + { + id: 'AwsSolutions-IAM5', + reason: 'Allow access to Cloudwatch Log Groups for pipeline execution', + }, + ], + true, + ); + + // Create retry mechanism + this.createRetryMechanism(); + } + + /** + * Creates a Lambda function and EventBridge rule to retry failed pipeline actions + */ + private createRetryMechanism(): void { + const retryLambdaRole = new Role(this, 'RetryLambdaRole', { + assumedBy: new ServicePrincipal('lambda.amazonaws.com'), + managedPolicies: [{ managedPolicyArn: 'arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole' }], + }); + + retryLambdaRole.addToPolicy( + new PolicyStatement({ + actions: [ + 'codepipeline:RetryStageExecution', + 'codepipeline:GetPipelineExecution', + 'codepipeline:GetPipelineState', + 'codepipeline:ListPipelineExecutions', + ], + resources: [this.pipeline.pipelineArn, `${this.pipeline.pipelineArn}/*`], + }), + ); + + const retryFunction = new Function(this, 'PipelineRetryFunction', { + runtime: Runtime.PYTHON_3_13, + handler: 'index.handler', + role: retryLambdaRole, + timeout: Duration.minutes(1), + code: Code.fromAsset('../applications/lambda/pipeline-retry-python'), + }); + + new Rule(this, 'PipelineFailureRule', { + eventPattern: { + source: ['aws.codepipeline'], + detailType: ['CodePipeline Stage Execution State Change'], + detail: { + state: ['FAILED'], + pipeline: [this.pipeline.pipelineName], + stage: ['build'], + }, + } as EventPattern, + targets: [new LambdaFunction(retryFunction)], + }); + + NagSuppressions.addResourceSuppressions( + retryLambdaRole, + [ + { + id: 'AwsSolutions-IAM5', + reason: 'Lambda needs access to retry pipeline executions', + }, + { + id: 'AwsSolutions-IAM4', + reason: 'Managed policy is acceptable for Lambda', + appliesTo: ['Policy::arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole'], + }, + ], + true, + ); + } +} diff --git a/src/cdk/lib/stages/core.ts b/src/cdk/lib/stages/core.ts new file mode 100644 index 00000000..f8f178cd --- /dev/null +++ b/src/cdk/lib/stages/core.ts @@ -0,0 +1,143 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +/** + * Core infrastructure stage and stack for the One Observability Workshop. + * + * This module defines the core infrastructure components including VPC setup, + * networking configuration, and foundational resources needed for the workshop. + * + * @packageDocumentation + */ + +import { Stack, StackProps, Stage } from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import { Utilities } from '../utils/utilities'; +import { IVpc, Vpc } from 'aws-cdk-lib/aws-ec2'; +import { WorkshopNetwork } from '../constructs/network'; +import { RetentionDays } from 'aws-cdk-lib/aws-logs'; +import { WorkshopCloudTrail } from '../constructs/cloudtrail'; +import { QueueResources, QueueResourcesProperties } from '../constructs/queue'; +import { EventBusResources, EventBusResourcesProperties } from '../constructs/eventbus'; +import { CfnDiscovery } from 'aws-cdk-lib/aws-applicationsignals'; +import { CloudWatchTransactionSearch, CloudWatchTransactionSearchProperties } from '../constructs/cloudwatch'; + +/** + * Configuration properties for the CoreStage. + */ +export interface CoreStageProperties extends StackProps { + /** Tags to apply to all resources in the stage */ + tags?: { [key: string]: string }; + /** Whether to create a new VPC (default: true) */ + createVpc?: boolean; + /** CIDR range for the VPC if creating a new one */ + vpcCidr?: string; + /** Existing VPC ID to use instead of creating new one */ + vpcId?: string; + /** Whether to create a CloudTrail trail (default: true) */ + createCloudTrail?: boolean; + /** Default Retention Period for logs */ + defaultRetentionDays?: RetentionDays; + /** Queue Resources */ + queueProperties?: QueueResourcesProperties; + /** EventBus Resources */ + eventBusProperties?: EventBusResourcesProperties; + /** CloudWatch Resources */ + cloudWatchProperties?: CloudWatchTransactionSearchProperties; +} + +/** + * Core deployment stage containing the foundational infrastructure stack. + * + * This stage creates the core infrastructure needed for the One Observability + * Workshop, including networking components and base resources. + */ +export class CoreStage extends Stage { + /** The core infrastructure stack */ + public readonly coreStack: CoreStack; + + /** + * Creates a new CoreStage. + * + * @param scope - The parent construct + * @param id - The stage identifier + * @param properties - Configuration properties for the stage + */ + constructor(scope: Construct, id: string, properties: CoreStageProperties) { + super(scope, id); + + this.coreStack = new CoreStack(this, `Stack`, properties); + if (properties.tags) { + Utilities.TagConstruct(this.coreStack, properties.tags); + } + } +} + +/** + * Core infrastructure stack containing VPC and networking resources. + * + * This stack sets up the foundational networking infrastructure for the workshop, + * either by creating a new VPC or using an existing one. + */ +export class CoreStack extends Stack { + /** The VPC instance used by the workshop */ + public readonly vpc: IVpc; + /** Whether the VPC is externally managed (not created by this stack) */ + public readonly externalVpc: boolean; + + /** + * Creates a new CoreStack. + * + * @param scope - The parent construct + * @param id - The stack identifier + * @param properties - Configuration properties for the stack + * @throws Error when neither createVpc nor vpcId is properly specified + */ + constructor(scope: Construct, id: string, properties: CoreStageProperties) { + super(scope, id, properties); + + /** Add Queue resources */ + new QueueResources(this, 'QueueResources', properties.queueProperties); + + /** Add EventBus resources */ + new EventBusResources(this, 'EventBusResources', properties.eventBusProperties); + + /** Enable CloudWatch Application Signals Discovery */ + new CfnDiscovery(this, 'ApplicationSignals', {}); + + /** CloudWatch Transaction Search setup **/ + new CloudWatchTransactionSearch(this, 'CloudWatchTransactionSearch', properties.cloudWatchProperties); + + if (!properties.createVpc || properties.createVpc) { + // Create a new VPC with workshop networking configuration + this.externalVpc = false; + const vpc = new WorkshopNetwork(this, 'vpc', { + name: 'Workshop', + cidrRange: properties.vpcCidr || '10.0.0.0/16', + logRetentionDays: properties.defaultRetentionDays || RetentionDays.ONE_WEEK, + enableDnsQueryResolverLogs: true, + enableFlowLogs: true, + }); + this.vpc = vpc.vpc; + } else if (properties.vpcId) { + // Use an existing VPC + this.vpc = Vpc.fromLookup(this, 'vpc', { + vpcId: properties.vpcId, + }); + this.externalVpc = true; + } else { + throw new Error('Either createVpc or vpcId must be specified'); + } + + if (properties.createCloudTrail == true) { + // Create CloudTrail trail + new WorkshopCloudTrail(this, 'cloudtrail', { + name: 'workshop-trail', + includeS3DataEvents: true, + logRetentionDays: properties.defaultRetentionDays || RetentionDays.ONE_WEEK, + }); + } + } +} diff --git a/src/cdk/lib/stages/index.ts b/src/cdk/lib/stages/index.ts new file mode 100644 index 00000000..38102a65 --- /dev/null +++ b/src/cdk/lib/stages/index.ts @@ -0,0 +1,5 @@ +export * from './applications'; +export * from './compute'; +export * from './containers'; +export * from './core'; +export * from './storage'; diff --git a/src/cdk/lib/stages/storage.ts b/src/cdk/lib/stages/storage.ts new file mode 100644 index 00000000..3ef5f269 --- /dev/null +++ b/src/cdk/lib/stages/storage.ts @@ -0,0 +1,124 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +import { Stack, StackProps, Stage } from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import { AssetsProperties, WorkshopAssets } from '../constructs/assets'; +import { DynamoDatabase as DynamoDatabase, DynamoDatabaseProperties } from '../constructs/dynamodb'; +import { Utilities } from '../utils/utilities'; +import { AuroraDatabase, AuroraDBProperties } from '../constructs/database'; +import { WorkshopNetwork } from '../constructs/network'; +import { OpenSearchCollection, OpenSearchCollectionProperties } from '../constructs/opensearch-collection'; +import { OpenSearchApplication, OpenSearchApplicationProperties } from '../constructs/opensearch-application'; +import { CodeBuildStep } from 'aws-cdk-lib/pipelines'; +import { ManagedPolicy, Policy, PolicyStatement, Role, ServicePrincipal } from 'aws-cdk-lib/aws-iam'; +import { NagSuppressions } from 'cdk-nag'; +import { IBucket } from 'aws-cdk-lib/aws-s3'; + +export interface StorageProperties extends StackProps { + assetsProperties?: AssetsProperties; + dynamoDatabaseProperties?: DynamoDatabaseProperties; + auroraDatabaseProperties?: AuroraDBProperties; + opensearchCollectionProperties?: OpenSearchCollectionProperties; + opensearchApplicationProperties?: Omit; + /** Tags to apply to all resources in the stage */ + tags?: { [key: string]: string }; +} + +export class StorageStage extends Stage { + public stack: StorageStack; + constructor(scope: Construct, id: string, properties: StorageProperties) { + super(scope, id, properties); + this.stack = new StorageStack(this, 'StorageStack', properties); + + if (properties.tags) { + Utilities.TagConstruct(this.stack, properties.tags); + } + } + public getDDBSeedingStep(scope: Stack, artifactBucket: IBucket) { + const seedingRole = new Role(scope, 'DDBSeedingRole', { + assumedBy: new ServicePrincipal('codebuild.amazonaws.com'), + description: 'CodeBuild role for DynamoDB seeding', + managedPolicies: [ManagedPolicy.fromAwsManagedPolicyName('AmazonDynamoDBFullAccess_v2')], + }); + + artifactBucket.grantRead(seedingRole); + new Policy(scope, 'DDBSeedingPolicy', { + roles: [seedingRole], + statements: [ + new PolicyStatement({ + actions: ['ssm:GetParameter'], + resources: ['*'], + }), + ], + }); + + // Seeding action role needs access to retrieve the table + // name from Parameter store, and full access to dynamodb + + const seedStep = new CodeBuildStep('DDBSeeding', { + commands: [ + 'cd src/cdk', + 'TABLE_NAME=$(./scripts/get-parameter.sh dynamodbtablename)', + './scripts/seed-dynamodb.sh $TABLE_NAME', + ], + buildEnvironment: { + privileged: false, + }, + role: seedingRole, + }); + + NagSuppressions.addResourceSuppressions( + seedingRole, + [ + { + id: 'AwsSolutions-IAM4', + reason: 'AWS Managed policies is acceptable for the DynamoDB Seeding action', + }, + ], + true, + ); + + return seedStep; + } +} + +export class StorageStack extends Stack { + public readonly dynamoDatabase: DynamoDatabase; + public readonly auroraDatabase: AuroraDatabase; + public readonly workshopAssets: WorkshopAssets; + constructor(scope: Construct, id: string, properties: StorageProperties) { + super(scope, id, properties); + + const vpc = WorkshopNetwork.importVpcFromExports(this, 'vpc'); + + /** Add Assets resources */ + this.workshopAssets = new WorkshopAssets(this, 'WorkshopAssets', properties.assetsProperties); + + /** Add DynamoDB resource */ + this.dynamoDatabase = new DynamoDatabase(this, 'DynamoDb', properties.dynamoDatabaseProperties); + + const databaseProperties = properties.auroraDatabaseProperties || {}; + if (databaseProperties) { + databaseProperties.vpc = vpc; + } + /** Add Database resource */ + this.auroraDatabase = new AuroraDatabase(this, 'AuroraDatabase', databaseProperties); + + /** Add OpenSearch Collection resource */ + const openSearchCollection = new OpenSearchCollection( + this, + 'OpenSearchCollection', + properties.opensearchCollectionProperties, + ); + + /** Add OpenSearch Application resource */ + new OpenSearchApplication(this, 'OpenSearchUiApplication', { + collection: openSearchCollection, + ...properties.opensearchApplicationProperties, + }); + + Utilities.SuppressLogRetentionNagWarnings(this); + } +} diff --git a/src/cdk/lib/utils/utilities.ts b/src/cdk/lib/utils/utilities.ts new file mode 100644 index 00000000..912e579b --- /dev/null +++ b/src/cdk/lib/utils/utilities.ts @@ -0,0 +1,934 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ +/** + * Utilities module provides helper functions for common CDK operations. + * + * This module contains static utility functions for tagging resources and + * retrieving CDK lookup role ARNs. + * + * @packageDocumentation + */ +import { Annotations, CfnOutput, CfnResource, Tags } from 'aws-cdk-lib'; +import { Construct } from 'constructs'; +import { NagSuppressions } from 'cdk-nag'; +import { Policy, Role } from 'aws-cdk-lib/aws-iam'; +import { StringParameter } from 'aws-cdk-lib/aws-ssm'; + +/** + * List of AWS CloudFormation resource types that support tagging. + * This list is used to determine which resources can have tags applied directly. + * + * @internal + */ +const TAGGABLE_RESOURCE_TYPE_LIST = new Set([ + 'AWS::ACMPCA::CertificateAuthority', + 'AWS::APS::RuleGroupsNamespace', + 'AWS::APS::Scraper', + 'AWS::APS::Workspace', + 'AWS::AccessAnalyzer::Analyzer', + 'AWS::AmazonMQ::Configuration', + 'AWS::Amplify::App', + 'AWS::Amplify::Branch', + 'AWS::AmplifyUIBuilder::Component', + 'AWS::AmplifyUIBuilder::Form', + 'AWS::AmplifyUIBuilder::Theme', + 'AWS::ApiGateway::ApiKey', + 'AWS::ApiGateway::ClientCertificate', + 'AWS::ApiGateway::DomainName', + 'AWS::ApiGateway::DomainNameAccessAssociation', + 'AWS::ApiGateway::DomainNameV2', + 'AWS::ApiGateway::RestApi', + 'AWS::ApiGateway::Stage', + 'AWS::ApiGateway::UsagePlan', + 'AWS::ApiGateway::VpcLink', + 'AWS::ApiGatewayV2::Api', + 'AWS::ApiGatewayV2::DomainName', + 'AWS::ApiGatewayV2::VpcLink', + 'AWS::AppConfig::Application', + 'AWS::AppConfig::ConfigurationProfile', + 'AWS::AppConfig::Deployment', + 'AWS::AppConfig::DeploymentStrategy', + 'AWS::AppConfig::Environment', + 'AWS::AppConfig::Extension', + 'AWS::AppConfig::ExtensionAssociation', + 'AWS::AppFlow::Flow', + 'AWS::AppIntegrations::Application', + 'AWS::AppIntegrations::DataIntegration', + 'AWS::AppIntegrations::EventIntegration', + 'AWS::AppRunner::AutoScalingConfiguration', + 'AWS::AppRunner::ObservabilityConfiguration', + 'AWS::AppRunner::Service', + 'AWS::AppRunner::VpcConnector', + 'AWS::AppRunner::VpcIngressConnection', + 'AWS::AppStream::AppBlock', + 'AWS::AppStream::AppBlockBuilder', + 'AWS::AppStream::Application', + 'AWS::AppSync::Api', + 'AWS::AppSync::ChannelNamespace', + 'AWS::AppSync::DomainName', + 'AWS::AppSync::GraphQLApi', + 'AWS::AppTest::TestCase', + 'AWS::ApplicationInsights::Application', + 'AWS::ApplicationSignals::ServiceLevelObjective', + 'AWS::Athena::CapacityReservation', + 'AWS::Athena::DataCatalog', + 'AWS::Athena::WorkGroup', + 'AWS::AuditManager::Assessment', + 'AWS::AutoScaling::AutoScalingGroup', + 'AWS::B2BI::Capability', + 'AWS::B2BI::Partnership', + 'AWS::B2BI::Profile', + 'AWS::B2BI::Transformer', + 'AWS::BCMDataExports::Export', + 'AWS::Backup::BackupPlan', + 'AWS::Backup::BackupVault', + 'AWS::Backup::Framework', + 'AWS::Backup::LogicallyAirGappedBackupVault', + 'AWS::Backup::ReportPlan', + 'AWS::Backup::RestoreTestingPlan', + 'AWS::BackupGateway::Hypervisor', + 'AWS::Batch::ComputeEnvironment', + 'AWS::Batch::ConsumableResource', + 'AWS::Batch::JobDefinition', + 'AWS::Batch::JobQueue', + 'AWS::Batch::SchedulingPolicy', + 'AWS::Bedrock::Agent', + 'AWS::Bedrock::AgentAlias', + 'AWS::Bedrock::ApplicationInferenceProfile', + 'AWS::Bedrock::Blueprint', + 'AWS::Bedrock::DataAutomationProject', + 'AWS::Bedrock::Flow', + 'AWS::Bedrock::FlowAlias', + 'AWS::Bedrock::Guardrail', + 'AWS::Bedrock::KnowledgeBase', + 'AWS::Bedrock::Prompt', + 'AWS::Bedrock::PromptVersion', + 'AWS::BillingConductor::BillingGroup', + 'AWS::BillingConductor::CustomLineItem', + 'AWS::BillingConductor::PricingPlan', + 'AWS::BillingConductor::PricingRule', + 'AWS::Budgets::BudgetsAction', + 'AWS::CE::AnomalyMonitor', + 'AWS::CE::AnomalySubscription', + 'AWS::CE::CostCategory', + 'AWS::Cassandra::Keyspace', + 'AWS::Cassandra::Table', + 'AWS::Chatbot::CustomAction', + 'AWS::Chatbot::MicrosoftTeamsChannelConfiguration', + 'AWS::Chatbot::SlackChannelConfiguration', + 'AWS::CleanRooms::AnalysisTemplate', + 'AWS::CleanRooms::Collaboration', + 'AWS::CleanRooms::ConfiguredTable', + 'AWS::CleanRooms::ConfiguredTableAssociation', + 'AWS::CleanRooms::IdMappingTable', + 'AWS::CleanRooms::IdNamespaceAssociation', + 'AWS::CleanRooms::Membership', + 'AWS::CleanRooms::PrivacyBudgetTemplate', + 'AWS::CleanRoomsML::TrainingDataset', + 'AWS::CloudFormation::Stack', + 'AWS::CloudFormation::StackSet', + 'AWS::CloudFront::AnycastIpList', + 'AWS::CloudFront::ConnectionGroup', + 'AWS::CloudFront::Distribution', + 'AWS::CloudFront::DistributionTenant', + 'AWS::CloudFront::VpcOrigin', + 'AWS::CloudTrail::Channel', + 'AWS::CloudTrail::Dashboard', + 'AWS::CloudTrail::EventDataStore', + 'AWS::CloudTrail::Trail', + 'AWS::CloudWatch::Alarm', + 'AWS::CloudWatch::CompositeAlarm', + 'AWS::CloudWatch::MetricStream', + 'AWS::CodeArtifact::Domain', + 'AWS::CodeArtifact::PackageGroup', + 'AWS::CodeArtifact::Repository', + 'AWS::CodeBuild::Fleet', + 'AWS::CodeConnections::Connection', + 'AWS::CodeDeploy::Application', + 'AWS::CodeGuruProfiler::ProfilingGroup', + 'AWS::CodePipeline::CustomActionType', + 'AWS::CodePipeline::Pipeline', + 'AWS::CodeStarConnections::Connection', + 'AWS::CodeStarConnections::RepositoryLink', + 'AWS::Cognito::IdentityPool', + 'AWS::Cognito::UserPool', + 'AWS::Comprehend::DocumentClassifier', + 'AWS::Comprehend::Flywheel', + 'AWS::Config::AggregationAuthorization', + 'AWS::Config::ConfigurationAggregator', + 'AWS::Config::StoredQuery', + 'AWS::Connect::AgentStatus', + 'AWS::Connect::ContactFlow', + 'AWS::Connect::ContactFlowModule', + 'AWS::Connect::EmailAddress', + 'AWS::Connect::EvaluationForm', + 'AWS::Connect::HoursOfOperation', + 'AWS::Connect::Instance', + 'AWS::Connect::PhoneNumber', + 'AWS::Connect::Prompt', + 'AWS::Connect::Queue', + 'AWS::Connect::QuickConnect', + 'AWS::Connect::RoutingProfile', + 'AWS::Connect::Rule', + 'AWS::Connect::SecurityProfile', + 'AWS::Connect::TaskTemplate', + 'AWS::Connect::TrafficDistributionGroup', + 'AWS::Connect::User', + 'AWS::Connect::UserHierarchyGroup', + 'AWS::Connect::View', + 'AWS::ConnectCampaigns::Campaign', + 'AWS::ConnectCampaignsV2::Campaign', + 'AWS::ControlTower::EnabledBaseline', + 'AWS::ControlTower::EnabledControl', + 'AWS::ControlTower::LandingZone', + 'AWS::CustomerProfiles::CalculatedAttributeDefinition', + 'AWS::CustomerProfiles::Domain', + 'AWS::CustomerProfiles::EventStream', + 'AWS::CustomerProfiles::EventTrigger', + 'AWS::CustomerProfiles::Integration', + 'AWS::CustomerProfiles::ObjectType', + 'AWS::CustomerProfiles::SegmentDefinition', + 'AWS::DMS::DataMigration', + 'AWS::DMS::DataProvider', + 'AWS::DMS::InstanceProfile', + 'AWS::DMS::MigrationProject', + 'AWS::DMS::ReplicationConfig', + 'AWS::DSQL::Cluster', + 'AWS::DataBrew::Dataset', + 'AWS::DataBrew::Job', + 'AWS::DataBrew::Project', + 'AWS::DataBrew::Recipe', + 'AWS::DataBrew::Ruleset', + 'AWS::DataBrew::Schedule', + 'AWS::DataPipeline::Pipeline', + 'AWS::DataSync::Agent', + 'AWS::DataSync::LocationAzureBlob', + 'AWS::DataSync::LocationEFS', + 'AWS::DataSync::LocationFSxLustre', + 'AWS::DataSync::LocationFSxONTAP', + 'AWS::DataSync::LocationFSxOpenZFS', + 'AWS::DataSync::LocationFSxWindows', + 'AWS::DataSync::LocationHDFS', + 'AWS::DataSync::LocationNFS', + 'AWS::DataSync::LocationObjectStorage', + 'AWS::DataSync::LocationS3', + 'AWS::DataSync::LocationSMB', + 'AWS::DataSync::StorageSystem', + 'AWS::DataSync::Task', + 'AWS::DataZone::Domain', + 'AWS::Deadline::Farm', + 'AWS::Deadline::Fleet', + 'AWS::Deadline::LicenseEndpoint', + 'AWS::Deadline::Queue', + 'AWS::Detective::Graph', + 'AWS::DocDBElastic::Cluster', + 'AWS::DynamoDB::Table', + 'AWS::EC2::CapacityReservation', + 'AWS::EC2::CapacityReservationFleet', + 'AWS::EC2::CarrierGateway', + 'AWS::EC2::CustomerGateway', + 'AWS::EC2::DHCPOptions', + 'AWS::EC2::EIP', + 'AWS::EC2::FlowLog', + 'AWS::EC2::IPAM', + 'AWS::EC2::IPAMPool', + 'AWS::EC2::IPAMResourceDiscovery', + 'AWS::EC2::IPAMResourceDiscoveryAssociation', + 'AWS::EC2::IPAMScope', + 'AWS::EC2::Instance', + 'AWS::EC2::InstanceConnectEndpoint', + 'AWS::EC2::InternetGateway', + 'AWS::EC2::KeyPair', + 'AWS::EC2::LocalGatewayRouteTable', + 'AWS::EC2::LocalGatewayRouteTableVPCAssociation', + 'AWS::EC2::LocalGatewayRouteTableVirtualInterfaceGroupAssociation', + 'AWS::EC2::NatGateway', + 'AWS::EC2::NetworkAcl', + 'AWS::EC2::NetworkInsightsAccessScope', + 'AWS::EC2::NetworkInsightsAccessScopeAnalysis', + 'AWS::EC2::NetworkInsightsAnalysis', + 'AWS::EC2::NetworkInsightsPath', + 'AWS::EC2::NetworkInterface', + 'AWS::EC2::PlacementGroup', + 'AWS::EC2::PrefixList', + 'AWS::EC2::RouteServer', + 'AWS::EC2::RouteServerEndpoint', + 'AWS::EC2::RouteServerPeer', + 'AWS::EC2::RouteTable', + 'AWS::EC2::SecurityGroup', + 'AWS::EC2::Subnet', + 'AWS::EC2::TransitGateway', + 'AWS::EC2::TransitGatewayAttachment', + 'AWS::EC2::TransitGatewayConnect', + 'AWS::EC2::TransitGatewayMulticastDomain', + 'AWS::EC2::TransitGatewayPeeringAttachment', + 'AWS::EC2::TransitGatewayRouteTable', + 'AWS::EC2::TransitGatewayVpcAttachment', + 'AWS::EC2::VPC', + 'AWS::EC2::VPCBlockPublicAccessExclusion', + 'AWS::EC2::VPCEndpoint', + 'AWS::EC2::VPCEndpointService', + 'AWS::EC2::VPCPeeringConnection', + 'AWS::EC2::VPNConnection', + 'AWS::EC2::VPNGateway', + 'AWS::EC2::VerifiedAccessEndpoint', + 'AWS::EC2::VerifiedAccessGroup', + 'AWS::EC2::VerifiedAccessInstance', + 'AWS::EC2::VerifiedAccessTrustProvider', + 'AWS::EC2::Volume', + 'AWS::ECR::PublicRepository', + 'AWS::ECR::Repository', + 'AWS::ECS::CapacityProvider', + 'AWS::ECS::Cluster', + 'AWS::ECS::Service', + 'AWS::ECS::TaskDefinition', + 'AWS::ECS::TaskSet', + 'AWS::EFS::AccessPoint', + 'AWS::EFS::FileSystem', + 'AWS::EKS::AccessEntry', + 'AWS::EKS::Addon', + 'AWS::EKS::Cluster', + 'AWS::EKS::FargateProfile', + 'AWS::EKS::IdentityProviderConfig', + 'AWS::EKS::Nodegroup', + 'AWS::EKS::PodIdentityAssociation', + 'AWS::EMR::Studio', + 'AWS::EMR::WALWorkspace', + 'AWS::EMRContainers::VirtualCluster', + 'AWS::EMRServerless::Application', + 'AWS::ElastiCache::ParameterGroup', + 'AWS::ElastiCache::ServerlessCache', + 'AWS::ElastiCache::SubnetGroup', + 'AWS::ElastiCache::User', + 'AWS::ElastiCache::UserGroup', + 'AWS::ElasticBeanstalk::Environment', + 'AWS::ElasticLoadBalancingV2::LoadBalancer', + 'AWS::ElasticLoadBalancingV2::TargetGroup', + 'AWS::ElasticLoadBalancingV2::TrustStore', + 'AWS::EntityResolution::IdMappingWorkflow', + 'AWS::EntityResolution::IdNamespace', + 'AWS::EntityResolution::MatchingWorkflow', + 'AWS::EntityResolution::SchemaMapping', + 'AWS::EventSchemas::Discoverer', + 'AWS::EventSchemas::Registry', + 'AWS::EventSchemas::Schema', + 'AWS::Events::EventBus', + 'AWS::Evidently::Experiment', + 'AWS::Evidently::Feature', + 'AWS::Evidently::Launch', + 'AWS::Evidently::Project', + 'AWS::Evidently::Segment', + 'AWS::FIS::ExperimentTemplate', + 'AWS::FMS::Policy', + 'AWS::FMS::ResourceSet', + 'AWS::FSx::DataRepositoryAssociation', + 'AWS::FinSpace::Environment', + 'AWS::Forecast::DatasetGroup', + 'AWS::FraudDetector::List', + 'AWS::GameLift::Alias', + 'AWS::GameLift::Build', + 'AWS::GameLift::ContainerFleet', + 'AWS::GameLift::ContainerGroupDefinition', + 'AWS::GameLift::Fleet', + 'AWS::GameLift::GameServerGroup', + 'AWS::GameLift::GameSessionQueue', + 'AWS::GameLift::Location', + 'AWS::GameLift::MatchmakingConfiguration', + 'AWS::GameLift::MatchmakingRuleSet', + 'AWS::GameLift::Script', + 'AWS::GlobalAccelerator::Accelerator', + 'AWS::GlobalAccelerator::CrossAccountAttachment', + 'AWS::Glue::Crawler', + 'AWS::Glue::Job', + 'AWS::Glue::Registry', + 'AWS::Glue::Schema', + 'AWS::Glue::Trigger', + 'AWS::Glue::UsageProfile', + 'AWS::GreengrassV2::ComponentVersion', + 'AWS::GreengrassV2::Deployment', + 'AWS::GroundStation::Config', + 'AWS::GroundStation::DataflowEndpointGroup', + 'AWS::GroundStation::MissionProfile', + 'AWS::GuardDuty::Detector', + 'AWS::GuardDuty::Filter', + 'AWS::GuardDuty::IPSet', + 'AWS::GuardDuty::MalwareProtectionPlan', + 'AWS::GuardDuty::PublishingDestination', + 'AWS::GuardDuty::ThreatIntelSet', + 'AWS::HealthImaging::Datastore', + 'AWS::HealthLake::FHIRDatastore', + 'AWS::IAM::OIDCProvider', + 'AWS::IAM::Role', + 'AWS::IAM::SAMLProvider', + 'AWS::IAM::ServerCertificate', + 'AWS::IAM::User', + 'AWS::IVS::Channel', + 'AWS::IVS::EncoderConfiguration', + 'AWS::IVS::IngestConfiguration', + 'AWS::IVS::PlaybackKeyPair', + 'AWS::IVS::PlaybackRestrictionPolicy', + 'AWS::IVS::PublicKey', + 'AWS::IVS::RecordingConfiguration', + 'AWS::IVS::Stage', + 'AWS::IVS::StorageConfiguration', + 'AWS::IVS::StreamKey', + 'AWS::IVSChat::LoggingConfiguration', + 'AWS::IVSChat::Room', + 'AWS::ImageBuilder::Component', + 'AWS::ImageBuilder::ContainerRecipe', + 'AWS::ImageBuilder::DistributionConfiguration', + 'AWS::ImageBuilder::Image', + 'AWS::ImageBuilder::ImagePipeline', + 'AWS::ImageBuilder::ImageRecipe', + 'AWS::ImageBuilder::InfrastructureConfiguration', + 'AWS::ImageBuilder::LifecyclePolicy', + 'AWS::ImageBuilder::Workflow', + 'AWS::InspectorV2::CisScanConfiguration', + 'AWS::InternetMonitor::Monitor', + 'AWS::Invoicing::InvoiceUnit', + 'AWS::IoT::Authorizer', + 'AWS::IoT::BillingGroup', + 'AWS::IoT::CACertificate', + 'AWS::IoT::CertificateProvider', + 'AWS::IoT::Command', + 'AWS::IoT::CustomMetric', + 'AWS::IoT::Dimension', + 'AWS::IoT::DomainConfiguration', + 'AWS::IoT::FleetMetric', + 'AWS::IoT::JobTemplate', + 'AWS::IoT::Policy', + 'AWS::IoT::ProvisioningTemplate', + 'AWS::IoT::RoleAlias', + 'AWS::IoT::ScheduledAudit', + 'AWS::IoT::SecurityProfile', + 'AWS::IoT::SoftwarePackage', + 'AWS::IoT::SoftwarePackageVersion', + 'AWS::IoT::ThingGroup', + 'AWS::IoT::ThingType', + 'AWS::IoT::TopicRule', + 'AWS::IoTAnalytics::Channel', + 'AWS::IoTAnalytics::Dataset', + 'AWS::IoTAnalytics::Datastore', + 'AWS::IoTAnalytics::Pipeline', + 'AWS::IoTCoreDeviceAdvisor::SuiteDefinition', + 'AWS::IoTEvents::AlarmModel', + 'AWS::IoTEvents::DetectorModel', + 'AWS::IoTEvents::Input', + 'AWS::IoTFleetWise::Campaign', + 'AWS::IoTFleetWise::DecoderManifest', + 'AWS::IoTFleetWise::Fleet', + 'AWS::IoTFleetWise::ModelManifest', + 'AWS::IoTFleetWise::SignalCatalog', + 'AWS::IoTFleetWise::StateTemplate', + 'AWS::IoTFleetWise::Vehicle', + 'AWS::IoTSiteWise::Asset', + 'AWS::IoTSiteWise::AssetModel', + 'AWS::IoTSiteWise::Dashboard', + 'AWS::IoTSiteWise::Dataset', + 'AWS::IoTSiteWise::Gateway', + 'AWS::IoTSiteWise::Portal', + 'AWS::IoTSiteWise::Project', + 'AWS::IoTTwinMaker::ComponentType', + 'AWS::IoTTwinMaker::Entity', + 'AWS::IoTTwinMaker::Scene', + 'AWS::IoTTwinMaker::SyncJob', + 'AWS::IoTTwinMaker::Workspace', + 'AWS::IoTWireless::Destination', + 'AWS::IoTWireless::DeviceProfile', + 'AWS::IoTWireless::FuotaTask', + 'AWS::IoTWireless::MulticastGroup', + 'AWS::IoTWireless::NetworkAnalyzerConfiguration', + 'AWS::IoTWireless::PartnerAccount', + 'AWS::IoTWireless::ServiceProfile', + 'AWS::IoTWireless::TaskDefinition', + 'AWS::IoTWireless::WirelessDevice', + 'AWS::IoTWireless::WirelessDeviceImportTask', + 'AWS::IoTWireless::WirelessGateway', + 'AWS::KMS::Key', + 'AWS::KMS::ReplicaKey', + 'AWS::KafkaConnect::Connector', + 'AWS::KafkaConnect::CustomPlugin', + 'AWS::KafkaConnect::WorkerConfiguration', + 'AWS::Kendra::DataSource', + 'AWS::Kendra::Faq', + 'AWS::Kendra::Index', + 'AWS::KendraRanking::ExecutionPlan', + 'AWS::Kinesis::Stream', + 'AWS::KinesisAnalyticsV2::Application', + 'AWS::KinesisVideo::SignalingChannel', + 'AWS::KinesisVideo::Stream', + 'AWS::Lambda::CodeSigningConfig', + 'AWS::Lambda::EventSourceMapping', + 'AWS::Lambda::Function', + 'AWS::LaunchWizard::Deployment', + 'AWS::Lex::Bot', + 'AWS::Lightsail::Alarm', + 'AWS::Lightsail::Bucket', + 'AWS::Lightsail::Certificate', + 'AWS::Lightsail::Container', + 'AWS::Lightsail::Database', + 'AWS::Lightsail::Disk', + 'AWS::Lightsail::Distribution', + 'AWS::Lightsail::Instance', + 'AWS::Lightsail::LoadBalancer', + 'AWS::Lightsail::LoadBalancerTlsCertificate', + 'AWS::Lightsail::StaticIp', + 'AWS::Location::APIKey', + 'AWS::Location::GeofenceCollection', + 'AWS::Location::Map', + 'AWS::Location::PlaceIndex', + 'AWS::Location::RouteCalculator', + 'AWS::Location::Tracker', + 'AWS::Logs::Delivery', + 'AWS::Logs::DeliveryDestination', + 'AWS::Logs::DeliverySource', + 'AWS::Logs::LogGroup', + 'AWS::LookoutEquipment::InferenceScheduler', + 'AWS::M2::Application', + 'AWS::M2::Environment', + 'AWS::MSK::Cluster', + 'AWS::MSK::Replicator', + 'AWS::MSK::ServerlessCluster', + 'AWS::MSK::VpcConnection', + 'AWS::MWAA::Environment', + 'AWS::Macie::AllowList', + 'AWS::Macie::CustomDataIdentifier', + 'AWS::Macie::FindingsFilter', + 'AWS::ManagedBlockchain::Accessor', + 'AWS::MediaLive::ChannelPlacementGroup', + 'AWS::MediaLive::CloudWatchAlarmTemplate', + 'AWS::MediaLive::CloudWatchAlarmTemplateGroup', + 'AWS::MediaLive::Cluster', + 'AWS::MediaLive::EventBridgeRuleTemplate', + 'AWS::MediaLive::EventBridgeRuleTemplateGroup', + 'AWS::MediaLive::Multiplex', + 'AWS::MediaLive::Network', + 'AWS::MediaLive::SdiSource', + 'AWS::MediaLive::SignalMap', + 'AWS::MediaPackage::Asset', + 'AWS::MediaPackage::Channel', + 'AWS::MediaPackage::OriginEndpoint', + 'AWS::MediaPackage::PackagingConfiguration', + 'AWS::MediaPackage::PackagingGroup', + 'AWS::MediaPackageV2::Channel', + 'AWS::MediaPackageV2::ChannelGroup', + 'AWS::MediaPackageV2::OriginEndpoint', + 'AWS::MediaTailor::Channel', + 'AWS::MediaTailor::LiveSource', + 'AWS::MediaTailor::PlaybackConfiguration', + 'AWS::MediaTailor::SourceLocation', + 'AWS::MediaTailor::VodSource', + 'AWS::MemoryDB::ACL', + 'AWS::MemoryDB::Cluster', + 'AWS::MemoryDB::MultiRegionCluster', + 'AWS::MemoryDB::ParameterGroup', + 'AWS::MemoryDB::SubnetGroup', + 'AWS::MemoryDB::User', + 'AWS::Neptune::DBCluster', + 'AWS::Neptune::DBClusterParameterGroup', + 'AWS::Neptune::DBParameterGroup', + 'AWS::Neptune::DBSubnetGroup', + 'AWS::NeptuneGraph::Graph', + 'AWS::NetworkFirewall::Firewall', + 'AWS::NetworkFirewall::FirewallPolicy', + 'AWS::NetworkFirewall::RuleGroup', + 'AWS::NetworkFirewall::TLSInspectionConfiguration', + 'AWS::NetworkManager::ConnectAttachment', + 'AWS::NetworkManager::ConnectPeer', + 'AWS::NetworkManager::CoreNetwork', + 'AWS::NetworkManager::Device', + 'AWS::NetworkManager::DirectConnectGatewayAttachment', + 'AWS::NetworkManager::GlobalNetwork', + 'AWS::NetworkManager::Link', + 'AWS::NetworkManager::Site', + 'AWS::NetworkManager::SiteToSiteVpnAttachment', + 'AWS::NetworkManager::TransitGatewayPeering', + 'AWS::NetworkManager::TransitGatewayRouteTableAttachment', + 'AWS::NetworkManager::VpcAttachment', + 'AWS::Notifications::NotificationConfiguration', + 'AWS::NotificationsContacts::EmailContact', + 'AWS::OSIS::Pipeline', + 'AWS::Oam::Link', + 'AWS::Oam::Sink', + 'AWS::Omics::AnnotationStore', + 'AWS::Omics::ReferenceStore', + 'AWS::Omics::RunGroup', + 'AWS::Omics::SequenceStore', + 'AWS::Omics::VariantStore', + 'AWS::Omics::Workflow', + 'AWS::OpenSearchServerless::Collection', + 'AWS::OpenSearchService::Application', + 'AWS::Organizations::Account', + 'AWS::Organizations::OrganizationalUnit', + 'AWS::Organizations::Policy', + 'AWS::Organizations::ResourcePolicy', + 'AWS::PCAConnectorAD::Connector', + 'AWS::PCAConnectorAD::DirectoryRegistration', + 'AWS::PCAConnectorAD::Template', + 'AWS::PCAConnectorSCEP::Challenge', + 'AWS::PCAConnectorSCEP::Connector', + 'AWS::PCS::Cluster', + 'AWS::PCS::ComputeNodeGroup', + 'AWS::PCS::Queue', + 'AWS::Panorama::ApplicationInstance', + 'AWS::Panorama::Package', + 'AWS::PaymentCryptography::Key', + 'AWS::Pinpoint::InAppTemplate', + 'AWS::Pipes::Pipe', + 'AWS::Proton::EnvironmentAccountConnection', + 'AWS::Proton::EnvironmentTemplate', + 'AWS::Proton::ServiceTemplate', + 'AWS::QBusiness::Application', + 'AWS::QBusiness::DataAccessor', + 'AWS::QBusiness::DataSource', + 'AWS::QBusiness::Index', + 'AWS::QBusiness::Plugin', + 'AWS::QBusiness::Retriever', + 'AWS::QBusiness::WebExperience', + 'AWS::QLDB::Stream', + 'AWS::QuickSight::Analysis', + 'AWS::QuickSight::CustomPermissions', + 'AWS::QuickSight::Dashboard', + 'AWS::QuickSight::DataSet', + 'AWS::QuickSight::DataSource', + 'AWS::QuickSight::Folder', + 'AWS::QuickSight::Template', + 'AWS::QuickSight::Theme', + 'AWS::QuickSight::VPCConnection', + 'AWS::RAM::Permission', + 'AWS::RAM::ResourceShare', + 'AWS::RDS::CustomDBEngineVersion', + 'AWS::RDS::DBCluster', + 'AWS::RDS::DBClusterParameterGroup', + 'AWS::RDS::DBInstance', + 'AWS::RDS::DBParameterGroup', + 'AWS::RDS::DBProxy', + 'AWS::RDS::DBProxyEndpoint', + 'AWS::RDS::DBShardGroup', + 'AWS::RDS::DBSubnetGroup', + 'AWS::RDS::EventSubscription', + 'AWS::RDS::GlobalCluster', + 'AWS::RDS::Integration', + 'AWS::RDS::OptionGroup', + 'AWS::RUM::AppMonitor', + 'AWS::Rbin::Rule', + 'AWS::Redshift::Cluster', + 'AWS::Redshift::ClusterParameterGroup', + 'AWS::Redshift::Integration', + 'AWS::RedshiftServerless::Namespace', + 'AWS::RedshiftServerless::Workgroup', + 'AWS::RefactorSpaces::Application', + 'AWS::RefactorSpaces::Environment', + 'AWS::RefactorSpaces::Route', + 'AWS::RefactorSpaces::Service', + 'AWS::Rekognition::Collection', + 'AWS::Rekognition::StreamProcessor', + 'AWS::ResilienceHub::App', + 'AWS::ResilienceHub::ResiliencyPolicy', + 'AWS::ResourceExplorer2::Index', + 'AWS::ResourceExplorer2::View', + 'AWS::ResourceGroups::Group', + 'AWS::RolesAnywhere::CRL', + 'AWS::RolesAnywhere::Profile', + 'AWS::RolesAnywhere::TrustAnchor', + 'AWS::Route53::HealthCheck', + 'AWS::Route53::HostedZone', + 'AWS::Route53Profiles::Profile', + 'AWS::Route53Profiles::ProfileAssociation', + 'AWS::Route53RecoveryControl::Cluster', + 'AWS::Route53RecoveryControl::ControlPanel', + 'AWS::Route53RecoveryControl::SafetyRule', + 'AWS::Route53RecoveryReadiness::Cell', + 'AWS::Route53RecoveryReadiness::ReadinessCheck', + 'AWS::Route53RecoveryReadiness::RecoveryGroup', + 'AWS::Route53RecoveryReadiness::ResourceSet', + 'AWS::Route53Resolver::FirewallDomainList', + 'AWS::Route53Resolver::FirewallRuleGroup', + 'AWS::Route53Resolver::FirewallRuleGroupAssociation', + 'AWS::Route53Resolver::OutpostResolver', + 'AWS::Route53Resolver::ResolverQueryLoggingConfig', + 'AWS::Route53Resolver::ResolverRule', + 'AWS::S3::AccessGrant', + 'AWS::S3::AccessGrantsInstance', + 'AWS::S3::AccessGrantsLocation', + 'AWS::S3::Bucket', + 'AWS::S3::StorageLens', + 'AWS::S3::StorageLensGroup', + 'AWS::S3Outposts::Bucket', + 'AWS::SES::MailManagerAddonInstance', + 'AWS::SES::MailManagerAddonSubscription', + 'AWS::SES::MailManagerArchive', + 'AWS::SES::MailManagerIngressPoint', + 'AWS::SES::MailManagerRelay', + 'AWS::SES::MailManagerRuleSet', + 'AWS::SES::MailManagerTrafficPolicy', + 'AWS::SNS::Topic', + 'AWS::SQS::Queue', + 'AWS::SSM::Document', + 'AWS::SSM::Parameter', + 'AWS::SSM::PatchBaseline', + 'AWS::SSMContacts::Rotation', + 'AWS::SSMIncidents::ReplicationSet', + 'AWS::SSMIncidents::ResponsePlan', + 'AWS::SSMQuickSetup::ConfigurationManager', + 'AWS::SSO::Application', + 'AWS::SSO::Instance', + 'AWS::SSO::PermissionSet', + 'AWS::SageMaker::Cluster', + 'AWS::SageMaker::DataQualityJobDefinition', + 'AWS::SageMaker::Endpoint', + 'AWS::SageMaker::FeatureGroup', + 'AWS::SageMaker::Image', + 'AWS::SageMaker::InferenceComponent', + 'AWS::SageMaker::InferenceExperiment', + 'AWS::SageMaker::MlflowTrackingServer', + 'AWS::SageMaker::ModelBiasJobDefinition', + 'AWS::SageMaker::ModelCard', + 'AWS::SageMaker::ModelExplainabilityJobDefinition', + 'AWS::SageMaker::ModelPackage', + 'AWS::SageMaker::ModelPackageGroup', + 'AWS::SageMaker::ModelQualityJobDefinition', + 'AWS::SageMaker::PartnerApp', + 'AWS::SageMaker::Pipeline', + 'AWS::SageMaker::Project', + 'AWS::SageMaker::Space', + 'AWS::SageMaker::StudioLifecycleConfig', + 'AWS::Scheduler::ScheduleGroup', + 'AWS::SecretsManager::Secret', + 'AWS::SecurityHub::AutomationRule', + 'AWS::SecurityHub::ConfigurationPolicy', + 'AWS::SecurityHub::Hub', + 'AWS::SecurityLake::DataLake', + 'AWS::SecurityLake::Subscriber', + 'AWS::ServiceCatalogAppRegistry::Application', + 'AWS::ServiceCatalogAppRegistry::AttributeGroup', + 'AWS::Shield::Protection', + 'AWS::Shield::ProtectionGroup', + 'AWS::Signer::SigningProfile', + 'AWS::StepFunctions::Activity', + 'AWS::StepFunctions::StateMachine', + 'AWS::Synthetics::Canary', + 'AWS::Synthetics::Group', + 'AWS::SystemsManagerSAP::Application', + 'AWS::Timestream::Database', + 'AWS::Timestream::InfluxDBInstance', + 'AWS::Timestream::ScheduledQuery', + 'AWS::Timestream::Table', + 'AWS::Transfer::Agreement', + 'AWS::Transfer::Certificate', + 'AWS::Transfer::Connector', + 'AWS::Transfer::Profile', + 'AWS::Transfer::Server', + 'AWS::Transfer::User', + 'AWS::Transfer::WebApp', + 'AWS::Transfer::Workflow', + 'AWS::VoiceID::Domain', + 'AWS::VpcLattice::AccessLogSubscription', + 'AWS::VpcLattice::Listener', + 'AWS::VpcLattice::ResourceConfiguration', + 'AWS::VpcLattice::ResourceGateway', + 'AWS::VpcLattice::Rule', + 'AWS::VpcLattice::Service', + 'AWS::VpcLattice::ServiceNetwork', + 'AWS::VpcLattice::ServiceNetworkResourceAssociation', + 'AWS::VpcLattice::ServiceNetworkServiceAssociation', + 'AWS::VpcLattice::ServiceNetworkVpcAssociation', + 'AWS::VpcLattice::TargetGroup', + 'AWS::WAFv2::IPSet', + 'AWS::WAFv2::RegexPatternSet', + 'AWS::WAFv2::RuleGroup', + 'AWS::WAFv2::WebACL', + 'AWS::Wisdom::AIAgent', + 'AWS::Wisdom::AIGuardrail', + 'AWS::Wisdom::AIPrompt', + 'AWS::Wisdom::Assistant', + 'AWS::Wisdom::AssistantAssociation', + 'AWS::Wisdom::KnowledgeBase', + 'AWS::Wisdom::MessageTemplate', + 'AWS::WorkSpaces::ConnectionAlias', + 'AWS::WorkSpaces::WorkspacesPool', + 'AWS::WorkSpacesThinClient::Environment', + 'AWS::WorkSpacesWeb::BrowserSettings', + 'AWS::WorkSpacesWeb::DataProtectionSettings', + 'AWS::WorkSpacesWeb::IdentityProvider', + 'AWS::WorkSpacesWeb::IpAccessSettings', + 'AWS::WorkSpacesWeb::NetworkSettings', + 'AWS::WorkSpacesWeb::Portal', + 'AWS::WorkSpacesWeb::TrustStore', + 'AWS::WorkSpacesWeb::UserAccessLoggingSettings', + 'AWS::WorkSpacesWeb::UserSettings', + 'AWS::XRay::Group', + 'AWS::XRay::SamplingRule', +]); + +const EXCEPTIONS = new Set(['AWS::SSM::Parameter', 'AWS::EKS::Nodegroup']); + +/** + * Utility class providing helper functions for common CDK operations. + * + * This class contains static methods for tagging resources and retrieving + * CDK lookup role ARNs. + */ +export const Utilities = { + /** + * Adds tags to a construct and all its children recursively. + * + * This method applies tags to both the CDK construct and the underlying CloudFormation + * resources. For CloudFormation resources, it checks if the resource type supports + * tagging before applying tags. + * + * @param object - The construct to which tags will be applied + * @param tags - Map of tag keys and values to apply + */ + TagConstruct(object: Construct, tags: { [key: string]: string }) { + // Apply tags to the construct + for (const [key, value] of Object.entries(tags)) { + Tags.of(object).add(key, value); + } + + // For CfnResource objects, we need to explicitly set the Tags property + if (object instanceof CfnResource) { + let cfnTags = Object.entries(tags).map(([key, value]) => ({ + Key: key, + Value: value, + })); + + // Special handling for AutoScaling Groups + if (object.cfnResourceType === 'AWS::AutoScaling::AutoScalingGroup') { + cfnTags = Object.entries(tags).map(([key, value]) => ({ + Key: key, + Value: value, + PropagateAtLaunch: true, + })); + } + + // Only tag the resource if it's on the allowed list + if (cfnTags.length > 0 && TAGGABLE_RESOURCE_TYPE_LIST.has(object.cfnResourceType)) { + if (EXCEPTIONS.has(object.cfnResourceType)) { + // For these resource types, Tags must by added as a Map not an array or CFN will fail to deploy + + object.addPropertyOverride('Tags', tags); + } else { + object.addPropertyOverride('Tags', cfnTags); + } + } else { + Annotations.of(object).addInfo( + `Skipping tags on resource ${object.logicalId} of type ${object.cfnResourceType}`, + ); + } + } + + // Recursively tag all child constructs + for (const child of object.node.children) { + this.TagConstruct(child, tags); + } + }, + + /** + * Recursively searches for child nodes in a construct by resource type and partial name match. + * + * @param construct - The root construct to search within + * @param partialName - Partial match string for the resource name + * @param resourceType - Optional CloudFormation resource type to search for (e.g., 'AWS::Lambda::Function') + * @returns Array of matching constructs + */ + FindChildNodes(construct: Construct, partialName: string, resourceType?: string): Construct[] { + const matches: Construct[] = []; + + function searchRecursively(node: Construct) { + // Check if current node is a CfnResource with matching name and optionally matching type + if ( + node instanceof CfnResource && + node.toString().includes(partialName) && + (!resourceType || node.cfnResourceType === resourceType) + ) { + matches.push(node); + } + + // Recursively search all children + for (const child of node.node.children) { + searchRecursively(child); + } + } + + searchRecursively(construct); + return matches; + }, + + /** + * Applies NAG suppressions to log retention resources in a construct. + * + * @param construct - The construct to search for log retention resources + */ + SuppressLogRetentionNagWarnings(construct: Construct) { + const logRetentionRole = this.FindChildNodes(construct, 'LogRetention', 'AWS::IAM::Role'); + for (const role of logRetentionRole) { + const serviceRole = role as Role; + NagSuppressions.addResourceSuppressions( + serviceRole, + [ + { + id: 'AwsSolutions-IAM4', + reason: 'Log Retention lambda using managed policies is acceptable', + }, + ], + true, + ); + } + + const logRetentionPolicy = this.FindChildNodes(construct, 'LogRetention', 'AWS::IAM::Policy'); + for (const policy of logRetentionPolicy) { + const serviceRole = policy as Policy; + NagSuppressions.addResourceSuppressions( + serviceRole, + [ + { + id: 'AwsSolutions-IAM5', + reason: 'Log Retention lambda using wildcard is acceptable', + }, + ], + true, + ); + } + }, + + SuppressKubectlProviderNagWarnings(construct: Construct) { + const kubectlProvider = this.FindChildNodes(construct, 'KubectlProvider'); + for (const resource of kubectlProvider) { + NagSuppressions.addResourceSuppressions( + resource, + [ + { + id: 'AwsSolutions-IAM4', + reason: 'kubectl lambda using managed policies is acceptable', + }, + { + id: 'AwsSolutions-IAM5', + reason: 'Kubectl lambda using wildcard is acceptable', + }, + { + id: 'AwsSolutions-L1', + reason: 'Kubectl lambda managed by EKS Construct', + }, + ], + true, + ); + } + }, + + createSsmParameters(scope: Construct, prefix: string, parameters: Map) { + for (const [key, value] of parameters.entries()) { + //const id = key.replace('/', '_'); + const fullKey = `${prefix}/${key}`; + new StringParameter(scope, fullKey, { parameterName: fullKey, stringValue: value }); + } + }, + + createOuputs(scope: Construct, parameters: Map) { + for (const [key, value] of parameters.entries()) { + new CfnOutput(scope, key, { value: value }); + } + }, +}; diff --git a/src/cdk/package-lock.json b/src/cdk/package-lock.json new file mode 100644 index 00000000..7051f28f --- /dev/null +++ b/src/cdk/package-lock.json @@ -0,0 +1,7741 @@ +{ + "name": "cdk", + "version": "2.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "cdk", + "version": "2.0.0", + "dependencies": { + "@aws-cdk/aws-applicationsignals-alpha": "^2.213.0-alpha.0", + "@aws-cdk/aws-lambda-python-alpha": "2.213.0-alpha.0", + "@aws-cdk/lambda-layer-kubectl-v33": "^2.0.0", + "@types/yaml": "^1.9.7", + "aws-cdk": "^2.1027.0", + "aws-cdk-lib": "2.213.0", + "cdk-nag": "^2.37.12", + "constructs": "^10.4.2", + "dotenv": "^17.2.1", + "nunjucks": "^3.2.4", + "yaml": "^2.8.1" + }, + "bin": { + "cdk": "bin/workshop.js" + }, + "devDependencies": { + "@types/jest": "^30.0.0", + "@types/node": "^24.3.0", + "@types/nunjucks": "^3.2.6", + "@typescript-eslint/eslint-plugin": "^8.41.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-jest": "^29.0.1", + "eslint-plugin-prettier": "^5.5.4", + "eslint-plugin-unicorn": "^60.0.0", + "http-server": "^14.1.1", + "jest": "^30.1.1", + "jest-junit": "^16.0.0", + "ts-jest": "^29.4.1", + "ts-node": "^10.9.2", + "typedoc": "^0.28.11", + "typedoc-github-theme": "^0.3.1", + "typedoc-github-wiki-theme": "^2.1.0", + "typedoc-plugin-markdown": "^4.8.1", + "typescript": "~5.9.2" + } + }, + "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, + "license": "Apache-2.0", + "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.242", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.242.tgz", + "integrity": "sha512-4c1bAy2ISzcdKXYS1k4HYZsNrgiwbiDzj36ybwFVxEWZXVAP0dimQTCaB9fxu7sWzEjw3d+eaw6Fon+QTfTIpQ==", + "license": "Apache-2.0" + }, + "node_modules/@aws-cdk/asset-node-proxy-agent-v6": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.0.tgz", + "integrity": "sha512-7bY3J8GCVxLupn/kNmpPc5VJz8grx+4RKfnnJiO1LG+uxkZfANZG3RMHhE+qQxxwkyQ9/MfPtTpf748UhR425A==", + "license": "Apache-2.0" + }, + "node_modules/@aws-cdk/aws-applicationsignals-alpha": { + "version": "2.213.0-alpha.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/aws-applicationsignals-alpha/-/aws-applicationsignals-alpha-2.213.0-alpha.0.tgz", + "integrity": "sha512-G3wO/3xRWm73nelDwriG7EqRdV+gZsuvm6kBLPTAk1I5IqOrx4M3S/GdNWX3dWIKG+G54EjSWV6NLLRQAMP8BA==", + "license": "Apache-2.0", + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.213.0", + "constructs": "^10.0.0" + } + }, + "node_modules/@aws-cdk/aws-lambda-python-alpha": { + "version": "2.213.0-alpha.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/aws-lambda-python-alpha/-/aws-lambda-python-alpha-2.213.0-alpha.0.tgz", + "integrity": "sha512-ag2gXllOIMYtjlrEULaglMl6cJo+xgoIEkKZDwOzNvVdlnPjFPRXVBjczt50LPyzkZSdAXZsGJUz3cuKnRKGTg==", + "license": "Apache-2.0", + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "aws-cdk-lib": "^2.213.0", + "constructs": "^10.0.0" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema": { + "version": "48.3.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-48.3.0.tgz", + "integrity": "sha512-6R7aMNXeVnXGMOXgspwMmpVi1rmgRReGKYt7yl22ZKNZN/whJqdnV2C6O2CI2lnj5Th/SlgQWsuEQOMM3TI5RQ==", + "bundleDependencies": [ + "jsonschema", + "semver" + ], + "license": "Apache-2.0", + "dependencies": { + "jsonschema": "~1.4.1", + "semver": "^7.7.2" + }, + "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.2", + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@aws-cdk/lambda-layer-kubectl-v33": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/lambda-layer-kubectl-v33/-/lambda-layer-kubectl-v33-2.0.0.tgz", + "integrity": "sha512-osA3wkwWK2OfpymTcCZKhgaKSca9PQSr+7xi+UevKFRHtMdxHgygC345hdDpCtZlMmX9pKjtFpRUxeRrbGHMEw==", + "license": "Apache-2.0", + "peerDependencies": { + "aws-cdk-lib": "^2.94.0", + "constructs": "^10.0.5" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.0.tgz", + "integrity": "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", + "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", + "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.28.0", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.0.tgz", + "integrity": "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.0", + "@babel/types": "^7.28.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.2.tgz", + "integrity": "sha512-/V9771t+EgXz62aCcyofnQhGM8DQACbRhvzKFsXKC9QM+5MadF8ZmIm0crDMaz3+o0h0zXfJnd4EhbYbxsrcFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.0.tgz", + "integrity": "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.0" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", + "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", + "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.0.tgz", + "integrity": "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.2", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.2.tgz", + "integrity": "sha512-ruv7Ae4J5dUYULmeXw1gmb7rYRz57OWCPM57pHojnLq/3Z1CK2lNSLTCVjxVk1F/TZHwOZZrOWi0ur95BbLxNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@emnapi/core": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.5.0.tgz", + "integrity": "sha512-sbP8GzB1WDzacS8fgNPpHlp6C9VZe+SJP3F90W9rLemaQj2PzIuTEl1qDOYQf58YIpyjViI24y9aPWCjEzY2cg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.1.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.5.0.tgz", + "integrity": "sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.1.0.tgz", + "integrity": "sha512-WI0DdZ8xFSbgMjR1sFsKABJ/C5OnRrjT06JXbZKexJGrDuPTzZdDYfFlsgcCXCyf+suG5QU2e/y1Wo2V/OapLQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.7.0.tgz", + "integrity": "sha512-dyybb3AcajC7uha6CvhdVRJqaKyn7w2YKqKyAN37NKYgZT36w+iRb0Dymmc5qEJ549c/S31cMMSFd75bteCpCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.1.tgz", + "integrity": "sha512-CCZCDJuduB9OUkFkY2IgppNZMi2lBQgD2qzwXkEia16cge2pijY/aXi96CJMquDMn3nJdlPV1A5KrJEXwfLNzQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.0", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz", + "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@eslint/object-schema": "^2.1.6", + "debug": "^4.3.1", + "minimatch": "^3.1.2" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.0.tgz", + "integrity": "sha512-ViuymvFmcJi04qdZeDc2whTHryouGcDlaxPqarTD0ZE10ISpxGUVZGZDx4w01upyIynL3iu6IXH2bS1NhclQMw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.15.1", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.1.tgz", + "integrity": "sha512-bkOp+iumZCCbt1K1CmWf0R9pM5yKpDv+ZXtvSyQpudrI9kuFLp+bM2WOPXImuD/ceQuaa8f5pj93Y7zyECIGNA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.1.tgz", + "integrity": "sha512-gtF186CXhIl1p4pJNGZw8Yc6RlshoePRvE0X91oPGb3vZ8pM3qOS9W9NGPat9LziaBV7XrJWGylNQXkGcnM3IQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/eslintrc/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0", + "peer": true + }, + "node_modules/@eslint/eslintrc/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/@eslint/js": { + "version": "9.32.0", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.32.0.tgz", + "integrity": "sha512-BBpRFZK3eX6uMLKz8WxFOBIFFcGFJ/g8XuwjTHCqHROSIsopI+ddn/d5Cfh36+7+e5edVS8dbSHnBNhrLEX0zg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.6.tgz", + "integrity": "sha512-RBMg5FRL0I0gs51M/guSAj5/e14VQ4tpZnQNWwuDT66P14I43ItmPfIZRhO9fUVIPOAQXU47atlywZ/czoqFPA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.4.tgz", + "integrity": "sha512-Ul5l+lHEcw3L5+k8POx6r74mxEYKG5kOb6Xpy2gCRW6zweT6TEhAf8vhxGgjhqrd/VO/Dirhsb+1hNpD1ue9hw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.15.1", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@gerrit0/mini-shiki": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@gerrit0/mini-shiki/-/mini-shiki-3.9.2.tgz", + "integrity": "sha512-Tvsj+AOO4Z8xLRJK900WkyfxHsZQu+Zm1//oT1w443PO6RiYMoq/4NGOhaNuZoUMYsjKIAPVQ6eOFMddj6yphQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/engine-oniguruma": "^3.9.2", + "@shikijs/langs": "^3.9.2", + "@shikijs/themes": "^3.9.2", + "@shikijs/types": "^3.9.2", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.1", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", + "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.6", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.6.tgz", + "integrity": "sha512-YuI2ZHQL78Q5HbhDiBA1X4LmYdXCKCMQIfw0pw7piHJwyREFebJUvrQN4cMssyES6x+vfUbx1CIpaQUKYdQZOw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@humanfs/core": "^0.19.1", + "@humanwhocodes/retry": "^0.3.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node/node_modules/@humanwhocodes/retry": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.3.1.tgz", + "integrity": "sha512-JBxkERygn7Bv/GbN5Rv8Ul6LVknS+5Bp6RgDC/O8gEBU/yeH5Ui5C/OlWrTb6qct7LjjfT6Re2NxB0ln0yYybA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@isaacs/cliui": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", + "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^5.1.2", + "string-width-cjs": "npm:string-width@^4.2.0", + "strip-ansi": "^7.0.1", + "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", + "wrap-ansi": "^8.1.0", + "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz", + "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/@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, + "license": "ISC", + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@jest/console": { + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-30.1.1.tgz", + "integrity": "sha512-f7TGqR1k4GtN5pyFrKmq+ZVndesiwLU33yDpJIGMS9aW+j6hKjue7ljeAdznBsH9kAnxUWe2Y+Y3fLV/FJt3gA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.5", + "@types/node": "*", + "chalk": "^4.1.2", + "jest-message-util": "30.1.0", + "jest-util": "30.0.5", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/core": { + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-30.1.1.tgz", + "integrity": "sha512-3ncU9peZ3D2VdgRkdZtUceTrDgX5yiDRwAFjtxNfU22IiZrpVWlv/FogzDLYSJQptQGfFo3PcHK86a2oG6WUGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.1.1", + "@jest/pattern": "30.0.1", + "@jest/reporters": "30.1.1", + "@jest/test-result": "30.1.1", + "@jest/transform": "30.1.1", + "@jest/types": "30.0.5", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-changed-files": "30.0.5", + "jest-config": "30.1.1", + "jest-haste-map": "30.1.0", + "jest-message-util": "30.1.0", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.1.0", + "jest-resolve-dependencies": "30.1.1", + "jest-runner": "30.1.1", + "jest-runtime": "30.1.1", + "jest-snapshot": "30.1.1", + "jest-util": "30.0.5", + "jest-validate": "30.1.0", + "jest-watcher": "30.1.1", + "micromatch": "^4.0.8", + "pretty-format": "30.0.5", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/diff-sequences": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/diff-sequences/-/diff-sequences-30.0.1.tgz", + "integrity": "sha512-n5H8QLDJ47QqbCNn5SuFjCRDrOLEZ0h8vAHCK5RL9Ls7Xa8AQLa/YxAc9UjFqoEDM48muwtBGjtMY5cr0PLDCw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/environment": { + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.1.1.tgz", + "integrity": "sha512-yWHbU+3j7ehQE+NRpnxRvHvpUhoohIjMePBbIr8lfe0cWVb0WeTf80DNux1GPJa18CDHiIU5DtksGUfxcDE+Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.1.1", + "@jest/types": "30.0.5", + "@types/node": "*", + "jest-mock": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect": { + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-30.1.1.tgz", + "integrity": "sha512-3vHIHsF+qd3D8FU2c7U5l3rg1fhDwAYcGyHyZAi94YIlTwcJ+boNhRyJf373cl4wxbOX+0Q7dF40RTrTFTSuig==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "30.1.1", + "jest-snapshot": "30.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/expect-utils": { + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-30.1.1.tgz", + "integrity": "sha512-5YUHr27fpJ64dnvtu+tt11ewATynrHkGYD+uSFgRr8V2eFJis/vEXgToyLwccIwqBihVfz9jwio+Zr1ab1Zihw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/fake-timers": { + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.1.1.tgz", + "integrity": "sha512-fK/25dNgBNYPw3eLi2CRs57g1H04qBAFNMsUY3IRzkfx/m4THe0E1zF+yGQBOMKKc2XQVdc9EYbJ4hEm7/2UtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.5", + "@sinonjs/fake-timers": "^13.0.0", + "@types/node": "*", + "jest-message-util": "30.1.0", + "jest-mock": "30.0.5", + "jest-util": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/get-type": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/@jest/get-type/-/get-type-30.1.0.tgz", + "integrity": "sha512-eMbZE2hUnx1WV0pmURZY9XoXPkUYjpc55mb0CrhtdWLtzMQPFvu/rZkTLZFTsdaVQa+Tr4eWAteqcUzoawq/uA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/globals": { + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-30.1.1.tgz", + "integrity": "sha512-NNUUkHT2TU/xztZl6r1UXvJL+zvCwmZsQDmK69fVHHcB9fBtlu3FInnzOve/ZoyKnWY8JXWJNT+Lkmu1+ubXUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.1.1", + "@jest/expect": "30.1.1", + "@jest/types": "30.0.5", + "jest-mock": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/reporters": { + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-30.1.1.tgz", + "integrity": "sha512-Hb2Bq80kahOC6Sv2waEaH1rEU6VdFcM6WHaRBWQF9tf30+nJHxhl/Upbgo9+25f0mOgbphxvbwSMjSgy9gW/FA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "30.1.1", + "@jest/test-result": "30.1.1", + "@jest/transform": "30.1.1", + "@jest/types": "30.0.5", + "@jridgewell/trace-mapping": "^0.3.25", + "@types/node": "*", + "chalk": "^4.1.2", + "collect-v8-coverage": "^1.0.2", + "exit-x": "^0.2.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^5.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "30.1.0", + "jest-util": "30.0.5", + "jest-worker": "30.1.0", + "slash": "^3.0.0", + "string-length": "^4.0.2", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/snapshot-utils": { + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/@jest/snapshot-utils/-/snapshot-utils-30.1.1.tgz", + "integrity": "sha512-TkVBc9wuN22TT8hESRFmjjg/xIMu7z0J3UDYtIRydzCqlLPTB7jK1DDBKdnTUZ4zL3z3rnPpzV6rL1Uzh87sXg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.5", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "natural-compare": "^1.4.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/source-map": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-30.0.1.tgz", + "integrity": "sha512-MIRWMUUR3sdbP36oyNyhbThLHyJ2eEDClPCiHVbrYAe5g3CHRArIVpBw7cdSB5fr+ofSfIb2Tnsw8iEHL0PYQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "callsites": "^3.1.0", + "graceful-fs": "^4.2.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-result": { + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-30.1.1.tgz", + "integrity": "sha512-bMdj7fNu8iZuBPSnbVir5ezvWmVo4jrw7xDE+A33Yb3ENCoiJK9XgOLgal+rJ9XSKjsL7aPUMIo87zhN7I5o2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.1.1", + "@jest/types": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "collect-v8-coverage": "^1.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/test-sequencer": { + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-30.1.1.tgz", + "integrity": "sha512-yruRdLXSA3HYD/MTNykgJ6VYEacNcXDFRMqKVAwlYegmxICUiT/B++CNuhJnYJzKYks61iYnjVsMwbUqmmAYJg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.1.1", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.1.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/transform": { + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-30.1.1.tgz", + "integrity": "sha512-PHIA2AbAASBfk6evkNifvmx9lkOSkmvaQoO6VSpuL8+kQqDMHeDoJ7RU3YP1wWAMD7AyQn9UL5iheuFYCC4lqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/types": "30.0.5", + "@jridgewell/trace-mapping": "^0.3.25", + "babel-plugin-istanbul": "^7.0.0", + "chalk": "^4.1.2", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.1.0", + "jest-regex-util": "30.0.1", + "jest-util": "30.0.5", + "micromatch": "^4.0.8", + "pirates": "^4.0.7", + "slash": "^3.0.0", + "write-file-atomic": "^5.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jest/types": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.0.5.tgz", + "integrity": "sha512-aREYa3aku9SSnea4aX6bhKn4bgv3AXkgijoQgbYV3yvbiGt6z+MQ85+6mIhx9DsKW2BuB/cLR/A+tcMThx+KLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.12", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.12.tgz", + "integrity": "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz", + "integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.29", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.29.tgz", + "integrity": "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } + }, + "node_modules/@shikijs/engine-oniguruma": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.9.2.tgz", + "integrity": "sha512-Vn/w5oyQ6TUgTVDIC/BrpXwIlfK6V6kGWDVVz2eRkF2v13YoENUvaNwxMsQU/t6oCuZKzqp9vqtEtEzKl9VegA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.9.2", + "@shikijs/vscode-textmate": "^10.0.2" + } + }, + "node_modules/@shikijs/langs": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.9.2.tgz", + "integrity": "sha512-X1Q6wRRQXY7HqAuX3I8WjMscjeGjqXCg/Sve7J2GWFORXkSrXud23UECqTBIdCSNKJioFtmUGJQNKtlMMZMn0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.9.2" + } + }, + "node_modules/@shikijs/themes": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.9.2.tgz", + "integrity": "sha512-6z5lBPBMRfLyyEsgf6uJDHPa6NAGVzFJqH4EAZ+03+7sedYir2yJBRu2uPZOKmj43GyhVHWHvyduLDAwJQfDjA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/types": "3.9.2" + } + }, + "node_modules/@shikijs/types": { + "version": "3.9.2", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.9.2.tgz", + "integrity": "sha512-/M5L0Uc2ljyn2jKvj4Yiah7ow/W+DJSglVafvWAJ/b8AZDeeRAdMu3c2riDzB7N42VD+jSnWxeP9AKtd4TfYVw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "@types/hast": "^3.0.4" + } + }, + "node_modules/@shikijs/vscode-textmate": { + "version": "10.0.2", + "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-10.0.2.tgz", + "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.38", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.38.tgz", + "integrity": "sha512-HpkxMmc2XmZKhvaKIZZThlHmx1L0I/V1hWK1NubtlFnr6ZqdiOpV72TKudZUNQjZNsyDBay72qFEhEvb+bcwcA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "type-detect": "4.0.8" + } + }, + "node_modules/@sinonjs/fake-timers": { + "version": "13.0.5", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", + "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/@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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.0.tgz", + "integrity": "sha512-VyyPYFlOMNylG45GoAe0xDoLwWuowvf92F9kySqzYh8vmYm7D2u4iUJKa1tOUpS70Ku13ASrOkS4ScXFsTaCNQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "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, + "license": "MIT", + "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.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.20.7", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.7.tgz", + "integrity": "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.20.7" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" + } + }, + "node_modules/@types/jest": { + "version": "30.0.0", + "resolved": "https://registry.npmjs.org/@types/jest/-/jest-30.0.0.tgz", + "integrity": "sha512-XTYugzhuwqWjws0CVz8QpM36+T+Dz5mTEBKhNs/esGLnCIlGdRy+Dq78NRjd7ls7r8BC8ZRMOrKlkO1hU0JOwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "expect": "^30.0.0", + "pretty-format": "^30.0.0" + } + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.3.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.3.0.tgz", + "integrity": "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.10.0" + } + }, + "node_modules/@types/nunjucks": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@types/nunjucks/-/nunjucks-3.2.6.tgz", + "integrity": "sha512-pHiGtf83na1nCzliuAdq8GowYiXvH5l931xZ0YEHaLMNFgynpEqx+IPStlu7UaDkehfvl01e4x/9Tpwhy7Ue3w==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/yaml": { + "version": "1.9.7", + "resolved": "https://registry.npmjs.org/@types/yaml/-/yaml-1.9.7.tgz", + "integrity": "sha512-8WMXRDD1D+wCohjfslHDgICd2JtMATZU8CkhH8LVJqcJs6dyYj5TGptzP8wApbmEullGBSsCEzzap73DQ1HJaA==", + "deprecated": "This is a stub types definition. yaml provides its own type definitions, so you do not need this installed.", + "license": "MIT", + "dependencies": { + "yaml": "*" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.41.0.tgz", + "integrity": "sha512-8fz6oa6wEKZrhXWro/S3n2eRJqlRcIa6SlDh59FXJ5Wp5XRZ8B9ixpJDcjadHq47hMx0u+HW6SNa6LjJQ6NLtw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.10.0", + "@typescript-eslint/scope-manager": "8.41.0", + "@typescript-eslint/type-utils": "8.41.0", + "@typescript-eslint/utils": "8.41.0", + "@typescript-eslint/visitor-keys": "8.41.0", + "graphemer": "^1.4.0", + "ignore": "^7.0.0", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.41.0", + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.41.0.tgz", + "integrity": "sha512-gTtSdWX9xiMPA/7MV9STjJOOYtWwIJIYxkQxnSV1U3xcE+mnJSH3f6zI0RYP+ew66WSlZ5ed+h0VCxsvdC1jJg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@typescript-eslint/scope-manager": "8.41.0", + "@typescript-eslint/types": "8.41.0", + "@typescript-eslint/typescript-estree": "8.41.0", + "@typescript-eslint/visitor-keys": "8.41.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.41.0.tgz", + "integrity": "sha512-b8V9SdGBQzQdjJ/IO3eDifGpDBJfvrNTp2QD9P2BeqWTGrRibgfgIlBSw6z3b6R7dPzg752tOs4u/7yCLxksSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.41.0", + "@typescript-eslint/types": "^8.41.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.41.0.tgz", + "integrity": "sha512-n6m05bXn/Cd6DZDGyrpXrELCPVaTnLdPToyhBoFkLIMznRUQUEQdSp96s/pcWSQdqOhrgR1mzJ+yItK7T+WPMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.41.0", + "@typescript-eslint/visitor-keys": "8.41.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.41.0.tgz", + "integrity": "sha512-TDhxYFPUYRFxFhuU5hTIJk+auzM/wKvWgoNYOPcOf6i4ReYlOoYN8q1dV5kOTjNQNJgzWN3TUUQMtlLOcUgdUw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.41.0.tgz", + "integrity": "sha512-63qt1h91vg3KsjVVonFJWjgSK7pZHSQFKH6uwqxAH9bBrsyRhO6ONoKyXxyVBzG1lJnFAJcKAcxLS54N1ee1OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.41.0", + "@typescript-eslint/typescript-estree": "8.41.0", + "@typescript-eslint/utils": "8.41.0", + "debug": "^4.3.4", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.41.0.tgz", + "integrity": "sha512-9EwxsWdVqh42afLbHP90n2VdHaWU/oWgbH2P0CfcNfdKL7CuKpwMQGjwev56vWu9cSKU7FWSu6r9zck6CVfnag==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.41.0.tgz", + "integrity": "sha512-D43UwUYJmGhuwHfY7MtNKRZMmfd8+p/eNSfFe6tH5mbVDto+VQCayeAt35rOx3Cs6wxD16DQtIKw/YXxt5E0UQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.41.0", + "@typescript-eslint/tsconfig-utils": "8.41.0", + "@typescript-eslint/types": "8.41.0", + "@typescript-eslint/visitor-keys": "8.41.0", + "debug": "^4.3.4", + "fast-glob": "^3.3.2", + "is-glob": "^4.0.3", + "minimatch": "^9.0.4", + "semver": "^7.6.0", + "ts-api-utils": "^2.1.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.41.0.tgz", + "integrity": "sha512-udbCVstxZ5jiPIXrdH+BZWnPatjlYwJuJkDA4Tbo3WyYLh8NvB+h/bKeSZHDOFKfphsZYJQqaFtLeXEqurQn1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.7.0", + "@typescript-eslint/scope-manager": "8.41.0", + "@typescript-eslint/types": "8.41.0", + "@typescript-eslint/typescript-estree": "8.41.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0", + "typescript": ">=4.8.4 <6.0.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.41.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.41.0.tgz", + "integrity": "sha512-+GeGMebMCy0elMNg67LRNoVnUFPIm37iu5CmHESVx56/9Jsfdpsvbv605DQ81Pi/x11IdKUsS5nzgTYbCQU9fg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.41.0", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "dev": true, + "license": "ISC" + }, + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/a-sync-waterfall": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/a-sync-waterfall/-/a-sync-waterfall-1.0.1.tgz", + "integrity": "sha512-RYTOHHdWipFUliRFMCS4X2Yn2X8M87V/OpSqWzKKOGhzqyUxzyVmhHDH9sAvG+ZuQf/TAOFsLCpMw09I1ufUnA==", + "license": "MIT" + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peer": true, + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.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, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/ajv": { + "version": "6.12.6", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", + "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "ISC", + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/asap": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz", + "integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==", + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/aws-cdk": { + "version": "2.1027.0", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1027.0.tgz", + "integrity": "sha512-oo2d1o8L1GBmAG4cDzIloEBOkijf1VzpZKJJguAodUCffDDOaAd6zjEbQbedb3SY+Vg+8m9OUOEXzeLMMg3GPQ==", + "license": "Apache-2.0", + "bin": { + "cdk": "bin/cdk" + }, + "engines": { + "node": ">= 18.0.0" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/aws-cdk-lib": { + "version": "2.213.0", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.213.0.tgz", + "integrity": "sha512-U6/1RnVu9R7bpb9gJoEl0tie4+f7/5MIWkolWhsoaaEjQ3XwKicaLxzGPZaU4hShDOG+pp6+1Lp9pofOdV0t8A==", + "bundleDependencies": [ + "@balena/dockerignore", + "case", + "fs-extra", + "ignore", + "jsonschema", + "minimatch", + "punycode", + "semver", + "table", + "yaml", + "mime-types" + ], + "license": "Apache-2.0", + "dependencies": { + "@aws-cdk/asset-awscli-v1": "2.2.242", + "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.0", + "@aws-cdk/cloud-assembly-schema": "^48.3.0", + "@balena/dockerignore": "^1.0.2", + "case": "1.6.3", + "fs-extra": "^11.3.1", + "ignore": "^5.3.2", + "jsonschema": "^1.5.0", + "mime-types": "^2.1.35", + "minimatch": "^3.1.2", + "punycode": "^2.3.1", + "semver": "^7.7.2", + "table": "^6.9.0", + "yaml": "1.10.2" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "constructs": "^10.0.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.17.1", + "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": "1.0.2", + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/brace-expansion": { + "version": "1.1.12", + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "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/concat-map": { + "version": "0.0.1", + "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.0.6", + "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.1", + "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": "3.1.2", + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "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.2", + "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.2", + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/babel-jest": { + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-30.1.1.tgz", + "integrity": "sha512-1bZfC/V03qBCzASvZpNFhx3Ouj6LgOd4KFJm4br/fYOS+tSSvVCE61QmcAVbMTwq/GoB7KN4pzGMoyr9cMxSvQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/transform": "30.1.1", + "@types/babel__core": "^7.20.5", + "babel-plugin-istanbul": "^7.0.0", + "babel-preset-jest": "30.0.1", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "slash": "^3.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0" + } + }, + "node_modules/babel-plugin-istanbul": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-7.0.0.tgz", + "integrity": "sha512-C5OzENSx/A+gt7t4VH1I2XsflxyPUmXRFPKBxt33xncdOmq7oROVM3bZv9Ysjjkv8OJYDMa+tKuKMvqU/H3xdw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-instrument": "^6.0.2", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/babel-plugin-jest-hoist": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-30.0.1.tgz", + "integrity": "sha512-zTPME3pI50NsFW8ZBaVIOeAxzEY7XHlmWeXXu9srI+9kNfzCUTy8MFan46xOGZY8NZThMqq+e3qZUKsvXbasnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.3", + "@types/babel__core": "^7.20.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/babel-preset-current-node-syntax": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.1.1.tgz", + "integrity": "sha512-23fWKohMTvS5s0wwJKycOe0dBdCwQ6+iiLaNR9zy8P13mtFRFM9qLLX6HJX5DL2pi/FNDf3fCQHM4FIMoHH/7w==", + "dev": true, + "license": "MIT", + "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 || ^8.0.0-0" + } + }, + "node_modules/babel-preset-jest": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-30.0.1.tgz", + "integrity": "sha512-+YHejD5iTWI46cZmcc/YtX4gaKBtdqCHCVfuVinizVpbmyjO3zYmeuyFdfA8duRqQZfgCAMlsfmkVbJ+e2MAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "babel-plugin-jest-hoist": "30.0.1", + "babel-preset-current-node-syntax": "^1.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.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, + "license": "MIT" + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.25.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.1.tgz", + "integrity": "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw==", + "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" + } + ], + "license": "MIT", + "dependencies": { + "caniuse-lite": "^1.0.30001726", + "electron-to-chromium": "^1.5.173", + "node-releases": "^2.0.19", + "update-browserslist-db": "^1.1.3" + }, + "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, + "license": "MIT", + "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, + "license": "Apache-2.0", + "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==", + "dev": true, + "license": "MIT" + }, + "node_modules/builtin-modules": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-5.0.0.tgz", + "integrity": "sha512-bkXY9WsVpY7CvMhKSR6pZilZu9Ln5WDrKVBUXf2S443etkmEO4V58heTecXcUIsNsi4Rx8JUO4NfX1IcQl4deg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001731", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001731.tgz", + "integrity": "sha512-lDdp2/wrOmTRWuoB5DpfNkC0rJDU8DqRa6nYL6HK6sytw70QMopt/NIc/9SM7ylItlBWfACXk0tEn37UWM/+mg==", + "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" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/cdk-nag": { + "version": "2.37.12", + "resolved": "https://registry.npmjs.org/cdk-nag/-/cdk-nag-2.37.12.tgz", + "integrity": "sha512-nQnHdZeQHgl4niqcRGCtxltKn0hBFg77V/jnKl5yw3MZr68xqNYhf57sdc/63QaxhvK0wnFDkgEBB9Diz+a2+A==", + "license": "Apache-2.0", + "peerDependencies": { + "aws-cdk-lib": "^2.176.0", + "constructs": "^10.0.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, + "license": "MIT", + "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/change-case": { + "version": "5.4.4", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-5.4.4.tgz", + "integrity": "sha512-HRQyTk2/YPEkt9TnUPbOpr64Uw3KOicFWPVBb+xiHvd6eBx/qPr9xqfBFDT8P2vWsvvz4jbEkfDe71W3VyNu2w==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "node_modules/ci-info": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.3.0.tgz", + "integrity": "sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/cjs-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.1.0.tgz", + "integrity": "sha512-UX0OwmYRYQQetfrLEZeewIFFI+wSTofC+pMBLNuH3RUuu/xzG1oz84UCEDOSoQlN3fZ4+AzmV50ZYvGqkMh9yA==", + "dev": true, + "license": "MIT" + }, + "node_modules/clean-regexp": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz", + "integrity": "sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/clean-regexp/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, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "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, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/cliui/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, + "license": "MIT" + }, + "node_modules/cliui/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, + "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/cliui/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, + "license": "MIT", + "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/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "node_modules/commander": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", + "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "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, + "license": "MIT" + }, + "node_modules/constructs": { + "version": "10.4.2", + "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.4.2.tgz", + "integrity": "sha512-wsNxBlAott2qg8Zv87q3eYZYgheb9lchtBfjHzzLHtXbttwSrHPs1NNQbBrmbb1YZvYg2+Vh0Dor76w4mFxJkA==", + "license": "Apache-2.0" + }, + "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, + "license": "MIT" + }, + "node_modules/core-js-compat": { + "version": "3.44.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.44.0.tgz", + "integrity": "sha512-JepmAj2zfl6ogy34qfWtcE7nHKAJnKsQFRn++scjVS2bZFllwptzw61BZcZFYBPpUznLfAvh0LGhxKppk04ClA==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.25.1" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/corser": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/corser/-/corser-2.0.1.tgz", + "integrity": "sha512-utCYNzRSQIZNPIcGZdQc92UVJYAhtGAteCFg0yRaFm8f0P+CPtyGyHXJcGXnffjCybUCEx3FQ2G7U3/o9eIkVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.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, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/dedent": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.6.0.tgz", + "integrity": "sha512-F1Z+5UCFpmQUzJa11agbyPVMbpgT/qA3/SKyJ1jyBgm7dUcUEa8v9JwDkerSQXfakBwFljIxhOJqGkjUwZ9FSA==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT", + "peer": 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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dotenv": { + "version": "17.2.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.1.tgz", + "integrity": "sha512-kQhDYKZecqnM0fCnzI5eIv5L4cAe/iRI+HqMbO/hbRdTAeXDG+M9FjipUxNfbARuEg4iHIbhnhs78BCHNbSxEQ==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/eastasianwidth": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", + "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.192", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.192.tgz", + "integrity": "sha512-rP8Ez0w7UNw/9j5eSXCe10o1g/8B1P5SM90PCCMVkIRQn2R0LEHWz4Eh9RnxkniuDe1W0cTSOB3MLlkTGDcuCg==", + "dev": true, + "license": "ISC" + }, + "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, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "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, + "license": "MIT", + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.32.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.32.0.tgz", + "integrity": "sha512-LSehfdpgMeWcTZkWZVIJl+tkZ2nuSkyyB9C27MZqFWXuph7DvaowgcTvKqxvpLW1JZIk8PN7hFY3Rj9LQ7m7lg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.0", + "@eslint/config-helpers": "^0.3.0", + "@eslint/core": "^0.15.0", + "@eslint/eslintrc": "^3.3.1", + "@eslint/js": "9.32.0", + "@eslint/plugin-kit": "^0.3.4", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-config-prettier": { + "version": "10.1.8", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-10.1.8.tgz", + "integrity": "sha512-82GZUjRS0p/jganf6q1rEO25VSoHH0hKPCTrgillPjdI/3bgBhAE1QzHrHTizjpRvy6pGAvKjDJtk2pF9NDq8w==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "funding": { + "url": "https://opencollective.com/eslint-config-prettier" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-plugin-jest": { + "version": "29.0.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-29.0.1.tgz", + "integrity": "sha512-EE44T0OSMCeXhDrrdsbKAhprobKkPtJTbQz5yEktysNpHeDZTAL1SfDTNKmcFfJkY6yrQLtTKZALrD3j/Gpmiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^8.0.0" + }, + "engines": { + "node": "^20.12.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^8.0.0", + "eslint": "^8.57.0 || ^9.0.0", + "jest": "*" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + }, + "jest": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.4", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.4.tgz", + "integrity": "sha512-swNtI95SToIz05YINMA6Ox5R057IMAmWZ26GqPxusAp1TZzj+IdY9tXNWWD3vkF/wEqydCONcwjTFpxybBqZsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.0", + "synckit": "^0.11.7" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-unicorn": { + "version": "60.0.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-60.0.0.tgz", + "integrity": "sha512-QUzTefvP8stfSXsqKQ+vBQSEsXIlAiCduS/V1Em+FKgL9c21U/IIm20/e3MFy1jyCf14tHAhqC1sX8OTy6VUCg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "@eslint-community/eslint-utils": "^4.7.0", + "@eslint/plugin-kit": "^0.3.3", + "change-case": "^5.4.4", + "ci-info": "^4.3.0", + "clean-regexp": "^1.0.0", + "core-js-compat": "^3.44.0", + "esquery": "^1.6.0", + "find-up-simple": "^1.0.1", + "globals": "^16.3.0", + "indent-string": "^5.0.0", + "is-builtin-module": "^5.0.0", + "jsesc": "^3.1.0", + "pluralize": "^8.0.0", + "regexp-tree": "^0.1.27", + "regjsparser": "^0.12.0", + "semver": "^7.7.2", + "strip-indent": "^4.0.0" + }, + "engines": { + "node": "^20.10.0 || >=21.0.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1" + }, + "peerDependencies": { + "eslint": ">=9.29.0" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/globals": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-16.3.0.tgz", + "integrity": "sha512-bqWEnJ1Nt3neqx2q5SFfGS8r/ahumIakg3HcwtNlrVlwXIeNumWn/c7Pn/wKzGhf6SaW6H6uWXLqC30STCMchQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-unicorn/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/eslint/node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "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, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.6.0.tgz", + "integrity": "sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true, + "license": "MIT" + }, + "node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/execa/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/exit-x": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/exit-x/-/exit-x-0.2.2.tgz", + "integrity": "sha512-+I6B/IkJc1o/2tiURyz/ivu/O0nKNEArIUB5O7zBrlDVJr22SCLH3xTeEry428LvFhRzIA1g8izguxJ/gbNcVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expect": { + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/expect/-/expect-30.1.1.tgz", + "integrity": "sha512-OKe7cdic4qbfWd/CcgwJvvCrNX2KWfuMZee9AfJHL1gTYmvqjBjZG1a2NwfhspBzxzlXwsN75WWpKTYfsJpBxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/expect-utils": "30.1.1", + "@jest/get-type": "30.1.0", + "jest-matcher-utils": "30.1.1", + "jest-message-util": "30.1.0", + "jest-mock": "30.0.5", + "jest-util": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "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, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/fastq": { + "version": "1.19.1", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", + "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "reusify": "^1.0.4" + } + }, + "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, + "license": "Apache-2.0", + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up-simple": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz", + "integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true, + "license": "ISC", + "peer": true + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/foreground-child": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", + "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", + "dev": true, + "license": "ISC", + "dependencies": { + "cross-spawn": "^7.0.6", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "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, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "license": "ISC", + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "peer": true, + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "license": "ISC" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/handlebars": { + "version": "4.7.8", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz", + "integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.5", + "neo-async": "^2.6.2", + "source-map": "^0.6.1", + "wordwrap": "^1.0.0" + }, + "bin": { + "handlebars": "bin/handlebars" + }, + "engines": { + "node": ">=0.4.7" + }, + "optionalDependencies": { + "uglify-js": "^3.1.4" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "license": "MIT", + "bin": { + "he": "bin/he" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-3.0.0.tgz", + "integrity": "sha512-oWv4T4yJ52iKrufjnyZPkrN0CH3QnrUqdB6In1g5Fe1mia8GmF36gnfNySxoZtxD5+NmYw1EElVXiBk93UeskA==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "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, + "license": "MIT" + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-server": { + "version": "14.1.1", + "resolved": "https://registry.npmjs.org/http-server/-/http-server-14.1.1.tgz", + "integrity": "sha512-+cbxadF40UXd9T01zUHgA+rlo2Bg1Srer4+B4NwIHdaGxAGGv59nYRnGGDJ9LBk7alpS0US+J+bLLdQOOkJq4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "basic-auth": "^2.0.1", + "chalk": "^4.1.2", + "corser": "^2.0.1", + "he": "^1.2.0", + "html-encoding-sniffer": "^3.0.0", + "http-proxy": "^1.18.1", + "mime": "^1.6.0", + "minimist": "^1.2.6", + "opener": "^1.5.1", + "portfinder": "^1.0.28", + "secure-compare": "3.0.1", + "union": "~0.5.0", + "url-join": "^4.0.1" + }, + "bin": { + "http-server": "bin/http-server" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-fresh/node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=4" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-5.0.0.tgz", + "integrity": "sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "ISC", + "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, + "license": "ISC" + }, + "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, + "license": "MIT" + }, + "node_modules/is-builtin-module": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-5.0.0.tgz", + "integrity": "sha512-f4RqJKBUe5rQkJ2eJEJBXSticB3hGbN9j0yxxMQFqIW89Jp9WYFtzfTcRlstDKVUTRzSOTLKRfO9vIztenwtxA==", + "dev": true, + "license": "MIT", + "dependencies": { + "builtin-modules": "^5.0.0" + }, + "engines": { + "node": ">=18.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "ISC" + }, + "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, + "license": "BSD-3-Clause", + "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, + "license": "BSD-3-Clause", + "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.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "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, + "license": "BSD-3-Clause", + "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": "5.0.6", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-5.0.6.tgz", + "integrity": "sha512-yg2d+Em4KizZC5niWhQaIomgf5WlL4vOOjZ5xGCmF8SnPE/mDWWXgvRExdcpCgh9lLRRa1/fSYp2ymmbJ1pI+A==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.23", + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-reports": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.2.0.tgz", + "integrity": "sha512-HGYWWS/ehqTV3xN10i23tkPkpH46MLCIMFNCaaKNavAXTF1RkqxawEPtnjnGZ6XKSInBKkiOA5BKS+aZiY3AvA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jackspeak": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", + "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/jest": { + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/jest/-/jest-30.1.1.tgz", + "integrity": "sha512-yC3JvpP/ZcAZX5rYCtXO/g9k6VTCQz0VFE2v1FpxytWzUqfDtu0XL/pwnNvptzYItvGwomh1ehomRNMOyhCJKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.1.1", + "@jest/types": "30.0.5", + "import-local": "^3.2.0", + "jest-cli": "30.1.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.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": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-30.0.5.tgz", + "integrity": "sha512-bGl2Ntdx0eAwXuGpdLdVYVr5YQHnSZlQ0y9HVDu565lCUAe9sj6JOtBbMmBBikGIegne9piDDIOeiLVoqTkz4A==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^5.1.1", + "jest-util": "30.0.5", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-circus": { + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-30.1.1.tgz", + "integrity": "sha512-M3Vd4x5wD7eSJspuTvRF55AkOOBndRxgW3gqQBDlFvbH3X+ASdi8jc+EqXEeAFd/UHulVYIlC4XKJABOhLw6UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.1.1", + "@jest/expect": "30.1.1", + "@jest/test-result": "30.1.1", + "@jest/types": "30.0.5", + "@types/node": "*", + "chalk": "^4.1.2", + "co": "^4.6.0", + "dedent": "^1.6.0", + "is-generator-fn": "^2.1.0", + "jest-each": "30.1.0", + "jest-matcher-utils": "30.1.1", + "jest-message-util": "30.1.0", + "jest-runtime": "30.1.1", + "jest-snapshot": "30.1.1", + "jest-util": "30.0.5", + "p-limit": "^3.1.0", + "pretty-format": "30.0.5", + "pure-rand": "^7.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-cli": { + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-30.1.1.tgz", + "integrity": "sha512-xm9llxuh5OoI5KZaYzlMhklryHBwg9LZy/gEaaMlXlxb+cZekGNzukU0iblbDo3XOBuN6N0CgK4ykgNRYSEb6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/core": "30.1.1", + "@jest/test-result": "30.1.1", + "@jest/types": "30.0.5", + "chalk": "^4.1.2", + "exit-x": "^0.2.2", + "import-local": "^3.2.0", + "jest-config": "30.1.1", + "jest-util": "30.0.5", + "jest-validate": "30.1.0", + "yargs": "^17.7.2" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-config": { + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-30.1.1.tgz", + "integrity": "sha512-xuPGUGDw+9fPPnGmddnLnHS/mhKUiJOW7K65vErYmglEPKq65NKwSRchkQ7iv6gqjs2l+YNEsAtbsplxozdOWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@jest/get-type": "30.1.0", + "@jest/pattern": "30.0.1", + "@jest/test-sequencer": "30.1.1", + "@jest/types": "30.0.5", + "babel-jest": "30.1.1", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "deepmerge": "^4.3.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-circus": "30.1.1", + "jest-docblock": "30.0.1", + "jest-environment-node": "30.1.1", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.1.0", + "jest-runner": "30.1.1", + "jest-util": "30.0.5", + "jest-validate": "30.1.0", + "micromatch": "^4.0.8", + "parse-json": "^5.2.0", + "pretty-format": "30.0.5", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "esbuild-register": ">=3.4.0", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "esbuild-register": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-diff": { + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-30.1.1.tgz", + "integrity": "sha512-LUU2Gx8EhYxpdzTR6BmjL1ifgOAQJQELTHOiPv9KITaKjZvJ9Jmgigx01tuZ49id37LorpGc9dPBPlXTboXScw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/diff-sequences": "30.0.1", + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "pretty-format": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-docblock": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-30.0.1.tgz", + "integrity": "sha512-/vF78qn3DYphAaIc3jy4gA7XSAz167n9Bm/wn/1XhTLW7tTBIzXtCJpb/vcmc73NIIeeohCbdL94JasyXUZsGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "detect-newline": "^3.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-each": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-30.1.0.tgz", + "integrity": "sha512-A+9FKzxPluqogNahpCv04UJvcZ9B3HamqpDNWNKDjtxVRYB8xbZLFuCr8JAJFpNp83CA0anGQFlpQna9Me+/tQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.0.5", + "chalk": "^4.1.2", + "jest-util": "30.0.5", + "pretty-format": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-node": { + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-30.1.1.tgz", + "integrity": "sha512-IaMoaA6saxnJimqCppUDqKck+LKM0Jg+OxyMUIvs1yGd2neiC22o8zXo90k04+tO+49OmgMR4jTgM5e4B0S62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.1.1", + "@jest/fake-timers": "30.1.1", + "@jest/types": "30.0.5", + "@types/node": "*", + "jest-mock": "30.0.5", + "jest-util": "30.0.5", + "jest-validate": "30.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-30.1.0.tgz", + "integrity": "sha512-JLeM84kNjpRkggcGpQLsV7B8W4LNUWz7oDNVnY1Vjj22b5/fAb3kk3htiD+4Na8bmJmjJR7rBtS2Rmq/NEcADg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.5", + "@types/node": "*", + "anymatch": "^3.1.3", + "fb-watchman": "^2.0.2", + "graceful-fs": "^4.2.11", + "jest-regex-util": "30.0.1", + "jest-util": "30.0.5", + "jest-worker": "30.1.0", + "micromatch": "^4.0.8", + "walker": "^1.0.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.3" + } + }, + "node_modules/jest-haste-map/node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/jest-junit": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/jest-junit/-/jest-junit-16.0.0.tgz", + "integrity": "sha512-A94mmw6NfJab4Fg/BlvVOUXzXgF0XIH6EmTgJ5NDPp4xoKq0Kr7sErb+4Xs9nZvu58pJojz5RFGpqnZYJTrRfQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mkdirp": "^1.0.4", + "strip-ansi": "^6.0.1", + "uuid": "^8.3.2", + "xml": "^1.0.1" + }, + "engines": { + "node": ">=10.12.0" + } + }, + "node_modules/jest-leak-detector": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-30.1.0.tgz", + "integrity": "sha512-AoFvJzwxK+4KohH60vRuHaqXfWmeBATFZpzpmzNmYTtmRMiyGPVhkXpBqxUQunw+dQB48bDf4NpUs6ivVbRv1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "pretty-format": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-30.1.1.tgz", + "integrity": "sha512-SuH2QVemK48BNTqReti6FtjsMPFsSOD/ZzRxU1TttR7RiRsRSe78d03bb4Cx6D4bQC/80Q8U4VnaaAH9FlbZ9w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "chalk": "^4.1.2", + "jest-diff": "30.1.1", + "pretty-format": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-message-util": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.1.0.tgz", + "integrity": "sha512-HizKDGG98cYkWmaLUHChq4iN+oCENohQLb7Z5guBPumYs+/etonmNFlg1Ps6yN9LTPyZn+M+b/9BbnHx3WTMDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.0.5", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "micromatch": "^4.0.8", + "pretty-format": "30.0.5", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-mock": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.0.5.tgz", + "integrity": "sha512-Od7TyasAAQX/6S+QCbN6vZoWOMwlTtzzGuxJku1GhGanAjz9y+QsQkpScDmETvdc9aSXyJ/Op4rhpMYBWW91wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.5", + "@types/node": "*", + "jest-util": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.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, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-30.1.0.tgz", + "integrity": "sha512-hASe7D/wRtZw8Cm607NrlF7fi3HWC5wmA5jCVc2QjQAB2pTwP9eVZILGEi6OeSLNUtE1zb04sXRowsdh5CUjwA==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.1.0", + "jest-pnp-resolver": "^1.2.3", + "jest-util": "30.0.5", + "jest-validate": "30.1.0", + "slash": "^3.0.0", + "unrs-resolver": "^1.7.11" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-30.1.1.tgz", + "integrity": "sha512-tRtaaoH8Ws1Gn1o/9pedt19dvVgr81WwdmvJSP9Ow3amOUOP2nN9j94u5jC9XlIfa2Q1FQKIWWQwL4ajqsjCGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "jest-regex-util": "30.0.1", + "jest-snapshot": "30.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runner": { + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-30.1.1.tgz", + "integrity": "sha512-ATe6372SOfJvCRExtCAr06I4rGujwFdKg44b6i7/aOgFnULwjxzugJ0Y4AnG+jeSeQi8dU7R6oqLGmsxRUbErQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/console": "30.1.1", + "@jest/environment": "30.1.1", + "@jest/test-result": "30.1.1", + "@jest/transform": "30.1.1", + "@jest/types": "30.0.5", + "@types/node": "*", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "exit-x": "^0.2.2", + "graceful-fs": "^4.2.11", + "jest-docblock": "30.0.1", + "jest-environment-node": "30.1.1", + "jest-haste-map": "30.1.0", + "jest-leak-detector": "30.1.0", + "jest-message-util": "30.1.0", + "jest-resolve": "30.1.0", + "jest-runtime": "30.1.1", + "jest-util": "30.0.5", + "jest-watcher": "30.1.1", + "jest-worker": "30.1.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-runtime": { + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-30.1.1.tgz", + "integrity": "sha512-7sOyR0Oekw4OesQqqBHuYJRB52QtXiq0NNgLRzVogiMSxKCMiliUd6RrXHCnG5f12Age/ggidCBiQftzcA9XKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.1.1", + "@jest/fake-timers": "30.1.1", + "@jest/globals": "30.1.1", + "@jest/source-map": "30.0.1", + "@jest/test-result": "30.1.1", + "@jest/transform": "30.1.1", + "@jest/types": "30.0.5", + "@types/node": "*", + "chalk": "^4.1.2", + "cjs-module-lexer": "^2.1.0", + "collect-v8-coverage": "^1.0.2", + "glob": "^10.3.10", + "graceful-fs": "^4.2.11", + "jest-haste-map": "30.1.0", + "jest-message-util": "30.1.0", + "jest-mock": "30.0.5", + "jest-regex-util": "30.0.1", + "jest-resolve": "30.1.0", + "jest-snapshot": "30.1.1", + "jest-util": "30.0.5", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot": { + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-30.1.1.tgz", + "integrity": "sha512-7/iBEzoJqEt2TjkQY+mPLHP8cbPhLReZVkkxjTMzIzoTC4cZufg7HzKo/n9cIkXKj2LG0x3mmBHsZto+7TOmFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.27.4", + "@babel/generator": "^7.27.5", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-syntax-typescript": "^7.27.1", + "@babel/types": "^7.27.3", + "@jest/expect-utils": "30.1.1", + "@jest/get-type": "30.1.0", + "@jest/snapshot-utils": "30.1.1", + "@jest/transform": "30.1.1", + "@jest/types": "30.0.5", + "babel-preset-current-node-syntax": "^1.1.0", + "chalk": "^4.1.2", + "expect": "30.1.1", + "graceful-fs": "^4.2.11", + "jest-diff": "30.1.1", + "jest-matcher-utils": "30.1.1", + "jest-message-util": "30.1.0", + "jest-util": "30.0.5", + "pretty-format": "30.0.5", + "semver": "^7.7.2", + "synckit": "^0.11.8" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jest-util": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.0.5.tgz", + "integrity": "sha512-pvyPWssDZR0FlfMxCBoc0tvM8iUEskaRFALUtGQYzVEAqisAztmy+R8LnU14KT4XA0H/a5HMVTXat1jLne010g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.0.5", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-util/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-validate": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-30.1.0.tgz", + "integrity": "sha512-7P3ZlCFW/vhfQ8pE7zW6Oi4EzvuB4sgR72Q1INfW9m0FGo0GADYlPwIkf4CyPq7wq85g+kPMtPOHNAdWHeBOaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/get-type": "30.1.0", + "@jest/types": "30.0.5", + "camelcase": "^6.3.0", + "chalk": "^4.1.2", + "leven": "^3.1.0", + "pretty-format": "30.0.5" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-watcher": { + "version": "30.1.1", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-30.1.1.tgz", + "integrity": "sha512-CrAQ73LlaS6KGQQw6NBi71g7qvP7scy+4+2c0jKX6+CWaYg85lZiig5nQQVTsS5a5sffNPL3uxXnaE9d7v9eQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/test-result": "30.1.1", + "@jest/types": "30.0.5", + "@types/node": "*", + "ansi-escapes": "^4.3.2", + "chalk": "^4.1.2", + "emittery": "^0.13.1", + "jest-util": "30.0.5", + "string-length": "^4.0.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-worker": { + "version": "30.1.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-30.1.0.tgz", + "integrity": "sha512-uvWcSjlwAAgIu133Tt77A05H7RIk3Ho8tZL50bQM2AkvLdluw9NG48lRCl3Dt+MOH719n/0nnb5YxUwcuJiKRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "@ungap/structured-clone": "^1.3.0", + "jest-util": "30.0.5", + "merge-stream": "^2.0.0", + "supports-color": "^8.1.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.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, + "license": "MIT", + "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, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "3.14.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", + "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT", + "peer": true + }, + "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, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT", + "peer": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT", + "peer": 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, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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, + "license": "MIT" + }, + "node_modules/linkify-it": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-5.0.0.tgz", + "integrity": "sha512-5aHCbzQRADcdP+ATqnDuhhJ/MRIqDkZX5pyjFHRRysS8vZ5AbqGEoFIb6pYHPZ+L/OC2Lc+xT8uHVVR5CAK/wQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "uc.micro": "^2.0.0" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT" + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT", + "peer": 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, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/lunr": { + "version": "2.3.9", + "resolved": "https://registry.npmjs.org/lunr/-/lunr-2.3.9.tgz", + "integrity": "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "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, + "license": "ISC" + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/markdown-it": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.1.0.tgz", + "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1", + "entities": "^4.4.0", + "linkify-it": "^5.0.0", + "mdurl": "^2.0.0", + "punycode.js": "^2.3.1", + "uc.micro": "^2.1.0" + }, + "bin": { + "markdown-it": "bin/markdown-it.mjs" + } + }, + "node_modules/markdown-it/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mdurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-2.0.0.tgz", + "integrity": "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "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, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "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, + "license": "MIT" + }, + "node_modules/napi-postinstall": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.3.tgz", + "integrity": "sha512-uTp172LLXSxuSYHv/kou+f6KW3SMppU9ivthaVTXian9sOt3XM/zHYHpRZiLgQoxeWfYUnslNWQHF1+G71xcow==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "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, + "license": "MIT" + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.19", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.19.tgz", + "integrity": "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/nunjucks": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/nunjucks/-/nunjucks-3.2.4.tgz", + "integrity": "sha512-26XRV6BhkgK0VOxfbU5cQI+ICFUtMLixv1noZn1tGU38kQH5A5nmmbk/O45xdyBhD1esk47nKrY0mvQpZIhRjQ==", + "license": "BSD-2-Clause", + "dependencies": { + "a-sync-waterfall": "^1.0.0", + "asap": "^2.0.3", + "commander": "^5.1.0" + }, + "bin": { + "nunjucks-precompile": "bin/precompile" + }, + "engines": { + "node": ">= 6.9.0" + }, + "peerDependencies": { + "chokidar": "^3.3.0" + }, + "peerDependenciesMeta": { + "chokidar": { + "optional": true + } + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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, + "license": "ISC", + "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, + "license": "MIT", + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true, + "license": "(WTFPL OR MIT)", + "bin": { + "opener": "bin/opener-bin.js" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/package-json-from-dist": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", + "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", + "dev": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/portfinder": { + "version": "1.0.37", + "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.37.tgz", + "integrity": "sha512-yuGIEjDAYnnOex9ddMnKZEMFE0CcGo6zbfzDklkmT1m5z734ss6JMzN9rNB3+RR7iS+F10D4/BVIaXOyh8PQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "async": "^3.2.6", + "debug": "^4.3.6" + }, + "engines": { + "node": ">= 10.12" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/prettier": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz", + "integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/prettier-linter-helpers": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", + "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/pretty-format": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.0.5.tgz", + "integrity": "sha512-D1tKtYvByrBkFLe2wHJl2bwMJIiT8rW+XA+TiataH79/FszLQMrpGEvzUVkzPau7OCO0Qnrhpe87PqtOAIB8Yw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/punycode.js": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode.js/-/punycode.js-2.3.1.tgz", + "integrity": "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/pure-rand": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-7.0.1.tgz", + "integrity": "sha512-oTUZM/NAZS8p7ANR3SHh30kXB+zK2r2BPcEn/awJIbOvq82WoMN4p62AWWp3Hhw50G0xMsw1mhIBLqHw64EcNQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, + "node_modules/qs": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz", + "integrity": "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/queue-microtask": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", + "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "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, + "license": "MIT" + }, + "node_modules/regexp-tree": { + "version": "0.1.27", + "resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz", + "integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==", + "dev": true, + "license": "MIT", + "bin": { + "regexp-tree": "bin/regexp-tree" + } + }, + "node_modules/regjsparser": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", + "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "jsesc": "~3.0.2" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "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, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/resolve-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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/reusify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", + "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", + "dev": true, + "license": "MIT", + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" + } + }, + "node_modules/run-parallel": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", + "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "dependencies": { + "queue-microtask": "^1.2.2" + } + }, + "node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true, + "license": "MIT" + }, + "node_modules/secure-compare": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/secure-compare/-/secure-compare-3.0.1.tgz", + "integrity": "sha512-AckIIV90rPDcBcglUwXPF3kg0P0qmPsPXAj6BBEENQE1p5yA1xfmDJzfi1Tappj37Pv2mVbKpL3Z1T+Nn7k1Qw==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "ISC", + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "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==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "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, + "license": "MIT", + "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, + "license": "BSD-3-Clause" + }, + "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, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/stack-utils/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, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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, + "license": "MIT", + "dependencies": { + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/string-width": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", + "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eastasianwidth": "^0.2.0", + "emoji-regex": "^9.2.2", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/string-width-cjs": { + "name": "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, + "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/string-width-cjs/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, + "license": "MIT" + }, + "node_modules/string-width/node_modules/ansi-regex": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz", + "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/string-width/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/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, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi-cjs": { + "name": "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, + "license": "MIT", + "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, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/strip-indent": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-4.0.0.tgz", + "integrity": "sha512-mnVSV2l+Zv6BLpSD/8V87CW/y9EmmbYzGCIavsnsI6/nwn26DwffM/yztm30Z/I2DY9wdS3vXVCMnHDgZaVNoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "min-indent": "^1.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "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, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/synckit": { + "version": "0.11.11", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.11.tgz", + "integrity": "sha512-MeQTA1r0litLUf0Rp/iisCaL8761lKAZHaimlbGK4j0HysC4PLfqygQj9srcs0m2RdtDYnF8UuYyKpbjHYp7Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } + }, + "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, + "license": "ISC", + "dependencies": { + "@istanbuljs/schema": "^0.1.2", + "glob": "^7.1.4", + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/test-exclude/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, + "license": "ISC", + "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/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "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, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-api-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", + "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/ts-jest": { + "version": "29.4.1", + "resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.4.1.tgz", + "integrity": "sha512-SaeUtjfpg9Uqu8IbeDKtdaS0g8lS6FT6OzM3ezrDfErPJPHNDo/Ey+VFGP1bQIDfagYDLyRpd7O15XpG1Es2Uw==", + "dev": true, + "license": "MIT", + "dependencies": { + "bs-logger": "^0.2.6", + "fast-json-stable-stringify": "^2.1.0", + "handlebars": "^4.7.8", + "json5": "^2.2.3", + "lodash.memoize": "^4.1.2", + "make-error": "^1.3.6", + "semver": "^7.7.2", + "type-fest": "^4.41.0", + "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 || ^30.0.0", + "@jest/types": "^29.0.0 || ^30.0.0", + "babel-jest": "^29.0.0 || ^30.0.0", + "jest": "^29.0.0 || ^30.0.0", + "jest-util": "^29.0.0 || ^30.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 + }, + "jest-util": { + "optional": true + } + } + }, + "node_modules/ts-jest/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ts-jest/node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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, + "license": "MIT", + "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/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "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, + "license": "MIT", + "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, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typedoc": { + "version": "0.28.11", + "resolved": "https://registry.npmjs.org/typedoc/-/typedoc-0.28.11.tgz", + "integrity": "sha512-1FqgrrUYGNuE3kImAiEDgAVVVacxdO4ZVTKbiOVDGkoeSB4sNwQaDpa8mta+Lw5TEzBFmGXzsg0I1NLRIoaSFw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@gerrit0/mini-shiki": "^3.9.0", + "lunr": "^2.3.9", + "markdown-it": "^14.1.0", + "minimatch": "^9.0.5", + "yaml": "^2.8.0" + }, + "bin": { + "typedoc": "bin/typedoc" + }, + "engines": { + "node": ">= 18", + "pnpm": ">= 10" + }, + "peerDependencies": { + "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x" + } + }, + "node_modules/typedoc-github-theme": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/typedoc-github-theme/-/typedoc-github-theme-0.3.1.tgz", + "integrity": "sha512-j6PmkAGmf/MGCzYjQcUH6jS9djPsNl/IoTXooxC+MoeMkBhbmPyKJlpR6Lw12BLoe2OYpYA2J1KMktUJXp/8Sw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "typedoc": "~0.28.0" + } + }, + "node_modules/typedoc-github-wiki-theme": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/typedoc-github-wiki-theme/-/typedoc-github-wiki-theme-2.1.0.tgz", + "integrity": "sha512-5j4vuoGwLn8PM1HeXocbwUF0Ra2qTFLeqsQwBQsLBPIx7Tl/lxkas1qIPFg/InOYwy9Y3Pn4xSjh+c/KM+jh6Q==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "typedoc-plugin-markdown": ">=4.3.0" + } + }, + "node_modules/typedoc-plugin-markdown": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/typedoc-plugin-markdown/-/typedoc-plugin-markdown-4.8.1.tgz", + "integrity": "sha512-ug7fc4j0SiJxSwBGLncpSo8tLvrT9VONvPUQqQDTKPxCoFQBADLli832RGPtj6sfSVJebNSrHZQRUdEryYH/7g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "typedoc": "0.28.x" + } + }, + "node_modules/typedoc/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/typedoc/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/typescript": { + "version": "5.9.2", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.2.tgz", + "integrity": "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uc.micro": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-2.1.0.tgz", + "integrity": "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A==", + "dev": true, + "license": "MIT" + }, + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "bin": { + "uglifyjs": "bin/uglifyjs" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/undici-types": { + "version": "7.10.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz", + "integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/union": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/union/-/union-0.5.0.tgz", + "integrity": "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA==", + "dev": true, + "dependencies": { + "qs": "^6.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", + "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" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "peer": true, + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/url-join": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-4.0.1.tgz", + "integrity": "sha512-jk1+QP6ZJqyOiuEI9AEWQfju/nB2Pw466kbA0LEZljHwKeMgd9WrAEgEGxjPDD2+TNbbb37rTyhEfrCXfuKXnA==", + "dev": true, + "license": "MIT" + }, + "node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, + "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, + "license": "MIT" + }, + "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, + "license": "ISC", + "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, + "license": "Apache-2.0", + "dependencies": { + "makeerror": "1.0.12" + } + }, + "node_modules/whatwg-encoding": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-2.0.0.tgz", + "integrity": "sha512-p41ogyeMUrw3jWclHWTQg1k05DSVXPLcVxRTYsXUk+ZooOCZLcoYgPZ/HL/D/N+uQPOtcp1me1WhBEaX02mhWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=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, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/wordwrap": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", + "integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", + "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.1.0", + "string-width": "^5.0.1", + "strip-ansi": "^7.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrap-ansi-cjs": { + "name": "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, + "license": "MIT", + "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/wrap-ansi-cjs/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, + "license": "MIT" + }, + "node_modules/wrap-ansi-cjs/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, + "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/wrap-ansi/node_modules/ansi-regex": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.0.tgz", + "integrity": "sha512-TKY5pyBkHyADOPYlRT9Lx6F544mPl0vS5Ew7BJ45hA08Q+t3GjbueLliBWN3sMICk6+y7HdyxSzC4bWS8baBdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/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, + "license": "ISC" + }, + "node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", + "dev": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==", + "dev": true, + "license": "MIT" + }, + "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, + "license": "ISC", + "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, + "license": "ISC" + }, + "node_modules/yaml": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz", + "integrity": "sha512-lcYcMxX2PO9XMGvAJkJ3OsNMw+/7FKes7/hgerGUYWIoWu5j/+YQqcZr5JnPZWzOsEBgMbSbiSTn/dv/69Mkpw==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + } + }, + "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, + "license": "MIT", + "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, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/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, + "license": "MIT" + }, + "node_modules/yargs/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, + "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/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "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, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/src/cdk/package.json b/src/cdk/package.json new file mode 100644 index 00000000..922260c3 --- /dev/null +++ b/src/cdk/package.json @@ -0,0 +1,48 @@ +{ + "name": "cdk", + "version": "2.0.0", + "bin": { + "cdk": "bin/workshop.js" + }, + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "test": "jest", + "cdk": "cdk", + "docs": "typedoc", + "docs:wiki": "typedoc --out ./wiki-docs" + }, + "devDependencies": { + "@types/jest": "^30.0.0", + "@types/node": "^24.3.0", + "@types/nunjucks": "^3.2.6", + "@typescript-eslint/eslint-plugin": "^8.41.0", + "eslint-config-prettier": "^10.1.8", + "eslint-plugin-jest": "^29.0.1", + "eslint-plugin-prettier": "^5.5.4", + "eslint-plugin-unicorn": "^60.0.0", + "http-server": "^14.1.1", + "jest": "^30.1.1", + "jest-junit": "^16.0.0", + "ts-jest": "^29.4.1", + "ts-node": "^10.9.2", + "typedoc": "^0.28.11", + "typedoc-github-theme": "^0.3.1", + "typedoc-github-wiki-theme": "^2.1.0", + "typedoc-plugin-markdown": "^4.8.1", + "typescript": "~5.9.2" + }, + "dependencies": { + "@aws-cdk/aws-applicationsignals-alpha": "^2.213.0-alpha.0", + "@aws-cdk/aws-lambda-python-alpha": "2.213.0-alpha.0", + "@aws-cdk/lambda-layer-kubectl-v33": "^2.0.0", + "@types/yaml": "^1.9.7", + "aws-cdk": "^2.1027.0", + "aws-cdk-lib": "2.213.0", + "cdk-nag": "^2.37.12", + "constructs": "^10.4.2", + "dotenv": "^17.2.1", + "nunjucks": "^3.2.4", + "yaml": "^2.8.1" + } +} diff --git a/src/cdk/scripts/deploy-check.sh b/src/cdk/scripts/deploy-check.sh new file mode 100755 index 00000000..a2e19fff --- /dev/null +++ b/src/cdk/scripts/deploy-check.sh @@ -0,0 +1,87 @@ +#!/bin/bash + +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -e + +# Parse command line arguments +PUSH_FLAG=false +if [[ "$1" == "--push" ]]; then + PUSH_FLAG=true +fi + +ENV_FILE=".env" + +# Check if .env file exists +if [[ ! -f "$ENV_FILE" ]]; then + echo "Error: .env file must exist at $ENV_FILE" + exit 1 +fi + +# Load environment variables +source "$ENV_FILE" + +# Check AWS credentials +if ! aws sts get-caller-identity &>/dev/null; then + echo "Error: Valid AWS credentials not found. Please assume a role in the target account." + exit 1 +fi + +# Print current AWS info +CALLER_IDENTITY=$(aws sts get-caller-identity) +ROLE_ARN=$(echo "$CALLER_IDENTITY" | jq -r '.Arn') +ACCOUNT_ID=$(echo "$CALLER_IDENTITY" | jq -r '.Account') +REGION=$(aws configure get region) + +echo "Current AWS Role: $ROLE_ARN" +echo "Account ID: $ACCOUNT_ID" +echo "Region: $REGION" + +# Check if bucket exists +if ! aws s3api head-bucket --bucket "$CONFIG_BUCKET" 2>/dev/null; then + if [[ "$PUSH_FLAG" == true ]]; then + aws s3 mb "s3://$CONFIG_BUCKET" + aws s3api put-bucket-versioning --bucket "$CONFIG_BUCKET" --versioning-configuration Status=Enabled + echo "Bucket created successfully with versioning enabled." + else + read -p "Bucket $CONFIG_BUCKET does not exist. Create it? (y/n): " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + aws s3 mb "s3://$CONFIG_BUCKET" + aws s3api put-bucket-versioning --bucket "$CONFIG_BUCKET" --versioning-configuration Status=Enabled + echo "Bucket created successfully with versioning enabled." + else + echo "Bucket creation cancelled." + exit 1 + fi + fi +fi + +# Check if repo.zip exists or push flag is set +OBJECT_KEY="repo/refs/heads/${BRANCH_NAME}/repo.zip" +if [[ "$PUSH_FLAG" == true ]] || ! aws s3api head-object --bucket "$CONFIG_BUCKET" --key "$OBJECT_KEY" &>/dev/null; then + if [[ "$PUSH_FLAG" == true ]]; then + echo "Push flag detected. Overriding repository content..." + TEMP_REMOTE="temp-s3-remote" + git remote add "$TEMP_REMOTE" "s3+zip://${CONFIG_BUCKET}/repo" + git push "$TEMP_REMOTE" "$BRANCH_NAME" + git remote remove "$TEMP_REMOTE" + echo "Repository pushed to S3 successfully." + else + read -p "Object $OBJECT_KEY does not exist. Create it? (y/n): " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + TEMP_REMOTE="temp-s3-remote" + git remote add "$TEMP_REMOTE" "s3+zip://${CONFIG_BUCKET}/repo" + git push "$TEMP_REMOTE" "$BRANCH_NAME" + git remote remove "$TEMP_REMOTE" + echo "Repository pushed to S3 successfully." + else + echo "Repository upload cancelled." + exit 1 + fi + fi +fi + +echo "All checks completed successfully." \ No newline at end of file diff --git a/src/cdk/scripts/get-parameter.sh b/src/cdk/scripts/get-parameter.sh new file mode 100755 index 00000000..debace42 --- /dev/null +++ b/src/cdk/scripts/get-parameter.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +ENV_FILE="$SCRIPT_DIR/../bin/environment.ts" +PARAMETER_KEY="$1" + +if [[ -z "$PARAMETER_KEY" ]]; then + echo "-1" + exit 0 +fi + +# Check AWS credentials +if ! aws sts get-caller-identity &>/dev/null 2>&1; then + echo "-2" + exit 0 +fi + +# Extract PARAMETER_STORE_PREFIX from environment.ts +PARAMETER_STORE_PREFIX=$(grep "PARAMETER_STORE_PREFIX = " "$ENV_FILE" 2>/dev/null | sed "s/.*= '\(.*\)';/\1/") + +if [[ -z "$PARAMETER_STORE_PREFIX" ]]; then + echo "-1" + exit 0 +fi + +FULL_PARAMETER_NAME="${PARAMETER_STORE_PREFIX}/${PARAMETER_KEY}" + +# Try to get parameter, handle errors silently +RESULT=$(aws ssm get-parameter --name "$FULL_PARAMETER_NAME" --query 'Parameter.Value' --output text 2>/dev/null) +EXIT_CODE=$? + +if [[ $EXIT_CODE -eq 0 ]]; then + echo "$RESULT" +elif [[ $EXIT_CODE -eq 255 ]] && aws ssm get-parameter --name "$FULL_PARAMETER_NAME" 2>&1 | grep -q "AccessDenied\|UnauthorizedOperation"; then + echo "-2" +else + echo "-1" +fi \ No newline at end of file diff --git a/src/cdk/scripts/redeploy-app.sh b/src/cdk/scripts/redeploy-app.sh new file mode 100755 index 00000000..5fa2d804 --- /dev/null +++ b/src/cdk/scripts/redeploy-app.sh @@ -0,0 +1,230 @@ +#!/bin/bash + +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -e + +# Colors for output +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +BLUE='\033[0;34m' +NC='\033[0m' # No Color + +# Get script directory +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +CDK_DIR="$(dirname "$SCRIPT_DIR")" +REPO_ROOT="$(dirname "$(dirname "$CDK_DIR")")" + +echo -e "${BLUE}One Observability Demo - Application Redeployment Script${NC}" +echo "==================================================" + +# Check AWS CLI +if ! command -v aws &> /dev/null; then + echo -e "${RED}Error: AWS CLI is not installed${NC}" + exit 1 +fi + +# Detect OCI runner +OCI_RUNNER="" +if command -v docker &> /dev/null; then + OCI_RUNNER="docker" +elif command -v finch &> /dev/null; then + OCI_RUNNER="finch" +elif command -v podman &> /dev/null; then + OCI_RUNNER="podman" +else + echo -e "${RED}Error: No OCI runner found. Please install docker, finch, or podman${NC}" + exit 1 +fi + +echo -e "${GREEN}Using OCI runner: $OCI_RUNNER${NC}" + +# Check if logged in +if ! aws sts get-caller-identity &> /dev/null; then + echo -e "${RED}Error: Not logged into AWS. Please configure your credentials${NC}" + exit 1 +fi + +# Get AWS account and region +AWS_ACCOUNT=$(aws sts get-caller-identity --query Account --output text) +if [ -z "$AWS_REGION" ]; then + AWS_REGION="us-east-1" +fi + +echo -e "${GREEN}Using AWS Account: $AWS_ACCOUNT${NC}" +echo -e "${GREEN}Using AWS Region: $AWS_REGION${NC}" +echo + +# Parse applications from environment.ts +parse_applications() { + local env_file="$CDK_DIR/bin/environment.ts" + if [ ! -f "$env_file" ]; then + echo -e "${RED}Error: environment.ts not found at $env_file${NC}" + exit 1 + fi + + # Extract application definitions + grep -A 6 "export const.*= {" "$env_file" | grep -E "(name:|dockerFilePath:|hostType:)" | \ + awk ' + /name:/ { name = $2; gsub(/[",]/, "", name) } + /dockerFilePath:/ { path = $2; gsub(/[",]/, "", path) } + /hostType:/ { + host = $2; gsub(/[",]/, "", host); gsub(/HostType\./, "", host) + if (name && path && host) { + print name ":" path ":" host + name = ""; path = ""; host = "" + } + }' +} + +# Get applications +APPS=() +while IFS= read -r line; do + APPS+=("$line") +done < <(parse_applications) + +if [ ${#APPS[@]} -eq 0 ]; then + echo -e "${RED}Error: No applications found in environment.ts${NC}" + exit 1 +fi + +# Display applications +echo -e "${YELLOW}Available applications:${NC}" +for i in "${!APPS[@]}"; do + IFS=':' read -r name path host <<< "${APPS[$i]}" + echo " $((i+1)). $name (Host: $host)" +done +echo + +# Get user selection +while true; do + read -p "Select application to redeploy (1-${#APPS[@]}): " selection + if [[ "$selection" =~ ^[0-9]+$ ]] && [ "$selection" -ge 1 ] && [ "$selection" -le ${#APPS[@]} ]; then + break + fi + echo -e "${RED}Invalid selection. Please enter a number between 1 and ${#APPS[@]}${NC}" +done + +# Parse selected application +IFS=':' read -r APP_NAME DOCKER_PATH HOST_TYPE <<< "${APPS[$((selection-1))]}" + +# Clean any remaining quotes +APP_NAME=$(echo "$APP_NAME" | tr -d "'\"") +DOCKER_PATH=$(echo "$DOCKER_PATH" | tr -d "'\"") +HOST_TYPE=$(echo "$HOST_TYPE" | tr -d "'\"") + +echo -e "${GREEN}Selected: $APP_NAME${NC}" +echo -e "${GREEN}Docker path: $DOCKER_PATH${NC}" +echo -e "${GREEN}Host type: $HOST_TYPE${NC}" +echo + +# Build and push container +echo -e "${YELLOW}Building and pushing container...${NC}" +ECR_REPO="$AWS_ACCOUNT.dkr.ecr.$AWS_REGION.amazonaws.com/$APP_NAME" + +# Login to ECR +aws ecr get-login-password --region "$AWS_REGION" | $OCI_RUNNER login --username AWS --password-stdin "$ECR_REPO" + +# Build and push +cd "$REPO_ROOT/$DOCKER_PATH" +if [ "$OCI_RUNNER" = "docker" ]; then + # Use buildx for cross-platform build (ARM to x86/amd64) + docker buildx build --platform linux/amd64 -t "$ECR_REPO:latest" --push . +else + # For finch/podman, use regular build with platform flag + $OCI_RUNNER build --platform linux/amd64 -t "$APP_NAME:latest" . + $OCI_RUNNER tag "$APP_NAME:latest" "$ECR_REPO:latest" + $OCI_RUNNER push "$ECR_REPO:latest" +fi + +echo -e "${GREEN}Container pushed successfully!${NC}" +echo + +# Handle deployment based on host type +if [ "$HOST_TYPE" = "ECS" ]; then + echo -e "${YELLOW}Handling ECS deployment...${NC}" + + # Get ECS clusters + CLUSTERS=() + while IFS= read -r line; do + CLUSTERS+=("$line") + done < <(aws ecs list-clusters --query 'clusterArns[]' --output text | xargs -n1 basename) + + if [ ${#CLUSTERS[@]} -eq 0 ]; then + echo -e "${RED}No ECS clusters found${NC}" + exit 1 + fi + + # Select cluster + if [ ${#CLUSTERS[@]} -eq 1 ]; then + CLUSTER="${CLUSTERS[0]}" + echo -e "${GREEN}Using cluster: $CLUSTER${NC}" + else + echo -e "${YELLOW}Available clusters:${NC}" + for i in "${!CLUSTERS[@]}"; do + echo " $((i+1)). ${CLUSTERS[$i]}" + done + + while true; do + read -p "Select cluster (1-${#CLUSTERS[@]}): " cluster_selection + if [[ "$cluster_selection" =~ ^[0-9]+$ ]] && [ "$cluster_selection" -ge 1 ] && [ "$cluster_selection" -le ${#CLUSTERS[@]} ]; then + CLUSTER="${CLUSTERS[$((cluster_selection-1))]}" + break + fi + echo -e "${RED}Invalid selection${NC}" + done + fi + + # Get services + SERVICES=() + while IFS= read -r line; do + SERVICES+=("$line") + done < <(aws ecs list-services --cluster "$CLUSTER" --query 'serviceArns[]' --output text | xargs -n1 basename) + + if [ ${#SERVICES[@]} -eq 0 ]; then + echo -e "${RED}No services found in cluster $CLUSTER${NC}" + exit 1 + fi + + echo -e "${YELLOW}Available services:${NC}" + for i in "${!SERVICES[@]}"; do + echo " $((i+1)). ${SERVICES[$i]}" + done + + while true; do + read -p "Select service (1-${#SERVICES[@]}): " service_selection + if [[ "$service_selection" =~ ^[0-9]+$ ]] && [ "$service_selection" -ge 1 ] && [ "$service_selection" -le ${#SERVICES[@]} ]; then + SERVICE="${SERVICES[$((service_selection-1))]}" + break + fi + echo -e "${RED}Invalid selection${NC}" + done + + # Force new deployment + echo -e "${YELLOW}Forcing service redeployment...${NC}" + aws ecs update-service --cluster "$CLUSTER" --service "$SERVICE" --force-new-deployment > /dev/null + + echo -e "${GREEN}Service redeployment initiated for $SERVICE in cluster $CLUSTER${NC}" + echo -e "${BLUE}You can monitor the deployment in the AWS Console or with:${NC}" + echo "aws ecs describe-services --cluster $CLUSTER --services $SERVICE" + +elif [ "$HOST_TYPE" = "EKS" ]; then + echo -e "${YELLOW}EKS deployment detected${NC}" + echo -e "${BLUE}Manual restart required using kubectl:${NC}" + echo + echo -e "${GREEN}Example command:${NC}" + echo "kubectl rollout restart deployment/$APP_NAME" + echo + echo -e "${BLUE}Or if using a different deployment name:${NC}" + echo "kubectl get deployments" + echo "kubectl rollout restart deployment/" + +else + echo -e "${RED}Unknown host type: $HOST_TYPE${NC}" + exit 1 +fi + +echo +echo -e "${GREEN}Redeployment process completed!${NC}" diff --git a/src/cdk/scripts/seed-dynamodb.sh b/src/cdk/scripts/seed-dynamodb.sh new file mode 100755 index 00000000..43997ace --- /dev/null +++ b/src/cdk/scripts/seed-dynamodb.sh @@ -0,0 +1,80 @@ +#!/bin/bash + +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SEED_FILE="$SCRIPT_DIR/seed.json" +TABLE_NAME="$1" + +# Check if seed.json exists +if [[ ! -f "$SEED_FILE" ]]; then + echo "Error: seed.json file not found at $SEED_FILE" + exit 1 +fi + +# Check AWS credentials +if ! aws sts get-caller-identity &>/dev/null; then + echo "Error: Valid AWS credentials not found. Please configure AWS CLI." + exit 1 +fi + +if [[ -n "$TABLE_NAME" ]]; then + SELECTED_TABLE="$TABLE_NAME" +else + echo "Fetching DynamoDB tables..." + TABLES=$(aws dynamodb list-tables --query 'TableNames' --output text) + + if [[ -z "$TABLES" ]]; then + echo "No DynamoDB tables found in the current region." + exit 1 + fi + + # Find table containing "Petadoption" + DEFAULT_TABLE="" + TABLE_ARRAY=($TABLES) + for table in "${TABLE_ARRAY[@]}"; do + if [[ "$table" == *"Petadoption"* ]]; then + DEFAULT_TABLE="$table" + break + fi + done + + echo "Available DynamoDB tables:" + select table in $TABLES; do + if [[ -n "$table" ]]; then + SELECTED_TABLE="$table" + break + else + echo "Invalid selection. Please try again." + fi + done + + # Set default if Petadoption table found + if [[ -n "$DEFAULT_TABLE" && "$SELECTED_TABLE" != "$DEFAULT_TABLE" ]]; then + read -p "Use $DEFAULT_TABLE instead? (y/n): " -n 1 -r + echo + if [[ $REPLY =~ ^[Yy]$ ]]; then + SELECTED_TABLE="$DEFAULT_TABLE" + fi + fi +fi + +echo "Seeding table: $SELECTED_TABLE" + +# Read and process seed data +jq -c '.[]' "$SEED_FILE" | while read -r item; do + petid=$(echo "$item" | jq -r '.petid') + echo "Inserting item with petid: $petid" + + # Convert JSON to DynamoDB format + dynamo_item=$(echo "$item" | jq 'with_entries(select(.value != null and .key != null)) | with_entries(.value = {S: (.value | tostring)})') + + aws dynamodb put-item \ + --table-name "$SELECTED_TABLE" \ + --item "$dynamo_item" +done + +echo "Successfully seeded $SELECTED_TABLE with data from $SEED_FILE" \ No newline at end of file diff --git a/src/cdk/scripts/seed.json b/src/cdk/scripts/seed.json new file mode 100644 index 00000000..33641e3b --- /dev/null +++ b/src/cdk/scripts/seed.json @@ -0,0 +1,236 @@ + [ + { + "pettype": "bunny", + "petid": "023", + "availability": "yes", + "cuteness_rate": "5", + "image": "b4", + "petcolor": "white", + "price": "75" + }, + { + "pettype": "bunny", + "petid": "024", + "availability": "yes", + "cuteness_rate": "4", + "image": "b3", + "petcolor": "brown", + "price": "45" + }, + { + "pettype": "bunny", + "petid": "025", + "availability": "yes", + "cuteness_rate": "5", + "image": "b2", + "petcolor": "brown", + "price": "55" + }, + { + "pettype": "bunny", + "petid": "026", + "availability": "yes", + "cuteness_rate": "4", + "image": "b1", + "petcolor": "brown", + "price": "95" + }, + { + "pettype": "kitten", + "petid": "016", + "availability": "yes", + "cuteness_rate": "4", + "image": "k1", + "petcolor": "white", + "price": "79" + }, + { + "pettype": "kitten", + "petid": "017", + "availability": "yes", + "cuteness_rate": "5", + "image": "k3", + "petcolor": "white", + "price": "89" + }, + { + "pettype": "kitten", + "petid": "018", + "availability": "yes", + "cuteness_rate": "4", + "image": "k6", + "petcolor": "black", + "price": "99" + }, + { + "pettype": "kitten", + "petid": "019", + "availability": "yes", + "cuteness_rate": "4", + "image": "k2", + "petcolor": "grey", + "price": "99" + }, + { + "pettype": "kitten", + "petid": "020", + "availability": "yes", + "cuteness_rate": "5", + "image": "k5", + "petcolor": "brown", + "price": "89" + }, + { + "pettype": "kitten", + "petid": "021", + "availability": "yes", + "cuteness_rate": "4", + "image": "k4", + "petcolor": "brown", + "price": "65" + }, + { + "pettype": "kitten", + "petid": "022", + "availability": "yes", + "cuteness_rate": "5", + "image": "k7", + "petcolor": "black", + "price": "75" + }, + { + "pettype": "puppy", + "petid": "001", + "availability": "yes", + "cuteness_rate": "5", + "image": "p9", + "petcolor": "black", + "price": "89" + }, + { + "pettype": "puppy", + "petid": "002", + "availability": "yes", + "cuteness_rate": "5", + "image": "p10", + "petcolor": "black", + "price": "99" + }, + { + "pettype": "puppy", + "petid": "003", + "availability": "yes", + "cuteness_rate": "5", + "image": "p1", + "petcolor": "brown", + "price": "99" + }, + { + "pettype": "puppy", + "petid": "004", + "availability": "yes", + "cuteness_rate": "5", + "image": "p11", + "petcolor": "black", + "price": "89" + }, + { + "pettype": "puppy", + "petid": "005", + "availability": "yes", + "cuteness_rate": "3", + "image": "p3", + "petcolor": "brown", + "price": "59" + }, + { + "pettype": "puppy", + "petid": "006", + "availability": "yes", + "cuteness_rate": "3", + "image": "p6", + "petcolor": "brown", + "price": "100" + }, + { + "pettype": "puppy", + "petid": "007", + "availability": "yes", + "cuteness_rate": "4", + "image": "p4", + "petcolor": "brown", + "price": "70" + }, + { + "pettype": "puppy", + "petid": "008", + "availability": "yes", + "cuteness_rate": "5", + "image": "p7", + "petcolor": "brown", + "price": "600" + }, + { + "pettype": "puppy", + "petid": "009", + "availability": "yes", + "cuteness_rate": "5", + "image": "p12", + "petcolor": "black", + "price": "60" + }, + { + "pettype": "puppy", + "petid": "010", + "availability": "yes", + "cuteness_rate": "3", + "image": "p8", + "petcolor": "brown", + "price": "80" + }, + { + "pettype": "puppy", + "petid": "011", + "availability": "yes", + "cuteness_rate": "4", + "image": "p14", + "petcolor": "white", + "price": "59" + }, + { + "pettype": "puppy", + "petid": "012", + "availability": "yes", + "cuteness_rate": "5", + "image": "p15", + "petcolor": "white", + "price": "79" + }, + { + "pettype": "puppy", + "petid": "013", + "availability": "yes", + "cuteness_rate": "4", + "image": "p2", + "petcolor": "white", + "price": "99" + }, + { + "pettype": "puppy", + "petid": "014", + "availability": "yes", + "cuteness_rate": "5", + "image": "p13", + "petcolor": "black", + "price": "79" + }, + { + "pettype": "puppy", + "petid": "015", + "availability": "yes", + "cuteness_rate": "4", + "image": "p5", + "petcolor": "white", + "price": "79" + } +] \ No newline at end of file diff --git a/src/cdk/test/cdk.test.ts b/src/cdk/test/cdk.test.ts new file mode 100644 index 00000000..4d2de8ed --- /dev/null +++ b/src/cdk/test/cdk.test.ts @@ -0,0 +1,32 @@ +/* +Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +SPDX-License-Identifier: Apache-2.0 +*/ + +/** + * Unit tests for the One Observability Workshop CDK constructs. + * + * This file contains test cases to validate the CDK infrastructure + * components and ensure they meet the expected specifications. + * + * @packageDocumentation + */ + +// import * as cdk from 'aws-cdk-lib'; +// import { Template } from 'aws-cdk-lib/assertions'; +// import * as Cdk from '../lib/cdk-stack'; + +/** + * Example test case for SQS Queue creation. + * This test is currently commented out as it's a template example. + */ +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/PetAdoptions/cdk/pet_stack/tsconfig.json b/src/cdk/tsconfig.json similarity index 67% rename from PetAdoptions/cdk/pet_stack/tsconfig.json rename to src/cdk/tsconfig.json index 37bbd496..fa55b1fe 100644 --- a/PetAdoptions/cdk/pet_stack/tsconfig.json +++ b/src/cdk/tsconfig.json @@ -1,8 +1,10 @@ { "compilerOptions": { - "target":"ES2021", + "target": "ES2020", "module": "commonjs", - "lib": ["es2021"], + "lib": [ + "es2020" + ], "declaration": true, "strict": true, "noImplicitAny": true, @@ -16,8 +18,13 @@ "inlineSourceMap": true, "inlineSources": true, "experimentalDecorators": true, - "strictPropertyInitialization":false, - "typeRoots": ["./node_modules/@types"] + "strictPropertyInitialization": false, + "typeRoots": [ + "./node_modules/@types" + ] }, - "exclude": ["cdk.out"] -} + "exclude": [ + "node_modules", + "cdk.out" + ] +} \ No newline at end of file diff --git a/src/cdk/typedoc.json b/src/cdk/typedoc.json new file mode 100644 index 00000000..19038a40 --- /dev/null +++ b/src/cdk/typedoc.json @@ -0,0 +1,67 @@ +{ + "entryPoints": ["index.ts"], + "out": "./wiki-docs", + "theme": "default", + "name": "One Observability Workshop CDK", + "includeVersion": true, + "excludePrivate": false, + "excludeProtected": false, + "excludeExternals": true, + "exclude": ["../../node_modules/**", "../../.git/**", "../../src/**", "../../PetAdoptions/**"], + "readme": "../../README.md", + "media": "../../diagrams", + "includes": "../../", + "includeDeclarations": true, + "plugin": ["typedoc-plugin-markdown"], + "entryPointStrategy": "expand", + "entryDocuments": { + "CONTRIBUTING.md": "../../CONTRIBUTING.md", + "ARCHITECTURE.md": "../../ARCHITECTURE.md", + "ASH_SCAN.md": "../../ASH_SCAN.md" + }, + "gitRevision": "main", + "hideGenerator": true, + "sort": ["source-order"], + "kindSortOrder": [ + "Document", + "Project", + "Module", + "Namespace", + "Enum", + "EnumMember", + "Class", + "Interface", + "TypeAlias", + "Constructor", + "Property", + "Variable", + "Function", + "Accessor", + "Method", + "Parameter", + "TypeParameter", + "TypeLiteral", + "CallSignature", + "ConstructorSignature", + "IndexSignature", + "GetSignature", + "SetSignature" + ], + "categorizeByGroup": true, + "defaultCategory": "Other", + "categoryOrder": [ + "Constructs", + "Interfaces", + "Types", + "Functions", + "Variables", + "*" + ], + "tsconfig": "./tsconfig.json", + "skipErrorChecking": true, + "validation": { + "notExported": false, + "invalidLink": false, + "notDocumented": false + } +} \ No newline at end of file diff --git a/src/templates/README.md b/src/templates/README.md new file mode 100644 index 00000000..40db3465 --- /dev/null +++ b/src/templates/README.md @@ -0,0 +1,41 @@ + +# CloudFormation Templates + +This folder contains CloudFormation templates for the One Observability Demo project. + +## Templates + +- **[codebuild-deployment-template-simplified.yaml](./codebuild-deployment-template.yaml)** - CodeBuild CDK deployment template with intelligent retry handling + +## Documentation + +For comprehensive documentation, usage instructions, architecture diagrams, and implementation details, see: + +**📖 [CodeBuild CDK Deployment Template Documentation](../../docs/codebuild-cdk-deployment-template.md)** + +This documentation includes: +- Architecture diagrams +- Deployment flow charts +- Implementation details +- Retry handling mechanisms +- Usage examples +- Troubleshooting guides +- Security considerations + +## Quick Start + +```bash +aws cloudformation create-stack \ + --stack-name MyWorkshop-CDK-Deployment \ + --template-body file://codebuild-deployment-template-simplified.yaml \ + --capabilities CAPABILITY_NAMED_IAM \ + --parameters \ + ParameterKey=pOrganizationName,ParameterValue=my-org \ + ParameterKey=pRepositoryName,ParameterValue=my-cdk-project \ + ParameterKey=pBranchName,ParameterValue=main +``` + +For detailed parameter descriptions and advanced usage, refer to the [full documentation](../../docs/codebuild-cdk-deployment-template.md). diff --git a/src/templates/codebuild-deployment-template.yaml b/src/templates/codebuild-deployment-template.yaml new file mode 100644 index 00000000..301cc971 --- /dev/null +++ b/src/templates/codebuild-deployment-template.yaml @@ -0,0 +1,1340 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: MIT-0 +# Workshop deployment template for CDK projects - CloudFormation +# Version: 1.0.0 - Simplified + +AWSTemplateFormatVersion: '2010-09-09' +Description: | + Automates CDK project deployment for workshop environments. Provisions S3 storage, CodeBuild project, and monitoring resources to bootstrap AWS accounts and deploy CDK applications consistently. Includes error handling and cleanup mechanisms for failed deployments. For more details, see: https://github.com/aws-samples/one-observability-demo + +Metadata: + AWS::CloudFormation::Interface: + ParameterGroups: + - Label: + default: Repository Configuration + Parameters: + - pConfigFileUrl + - pOrganizationName + - pRepositoryName + - pBranchName + - pWorkingFolder + - Label: + default: User Defined Tags + Parameters: + - pUserDefinedTagKey1 + - pUserDefinedTagValue1 + - pUserDefinedTagKey2 + - pUserDefinedTagValue2 + - pUserDefinedTagKey3 + - pUserDefinedTagValue3 + - pUserDefinedTagKey4 + - pUserDefinedTagValue4 + - pUserDefinedTagKey5 + - pUserDefinedTagValue5 + +Parameters: + pConfigFileUrl: + Type: String + Description: URL to the initial configuration file + AllowedPattern: ^https?://.*$ + ConstraintDescription: Must be a valid HTTP or HTTPS URL + Default: https://raw.githubusercontent.com/aws-samples/one-observability-demo/refs/heads/feat/cdkpipeline/typedoc.json + + pOrganizationName: + Type: String + Description: GitHub/CodeCommit organization name + AllowedPattern: '[A-Za-z0-9_.-]+' + ConstraintDescription: Must contain only alphanumeric characters, underscores, periods, or hyphens + Default: aws-samples + + pRepositoryName: + Type: String + Description: Repository containing the CDK code + AllowedPattern: '[A-Za-z0-9_.-]+' + ConstraintDescription: Must contain only alphanumeric characters, underscores, periods, or hyphens + Default: one-observability-demo + + pBranchName: + Type: String + Description: Branch to deploy from + Default: feat/cdkpipeline + AllowedPattern: '[A-Za-z0-9_.-/]+' + ConstraintDescription: Must contain only alphanumeric characters, underscores, + periods, slashes, or hyphens + + pWorkingFolder: + Type: String + Description: Working folder for deployment + Default: src/cdk + AllowedPattern: '[A-Za-z0-9_.-/]+' + ConstraintDescription: Must contain only alphanumeric characters, underscores, periods, or hyphens + + pApplicationName: + Type: String + Description: Application name used for tagging deployed stacks + Default: One Observability Workshop + AllowedPattern: ^[A-Za-z0-9\s_.-]+$ + ConstraintDescription: Must contain only alphanumeric characters, spaces, underscores, periods, or hyphens + + pUserDefinedTagKey1: + Type: String + Description: Key for user defined tag 1 + AllowedPattern: ^(?!aws:)[A-Za-z0-9\s_.:=+-@/]{0,128} + ConstraintDescription: 'Must match the allowable values for a Tag Key. This can + only contain alphanumeric characters or special characters ( _ . : / = + - + or @) up to 128 characters' + Default: WorkshopName + + pUserDefinedTagValue1: + Type: String + Description: Value for user defined tag 1 + AllowedPattern: ^[\w\s_.:=+-@/]{0,256}$ + ConstraintDescription: 'Must match the allowable values for a Tag Value. This + can only contain alphanumeric characters or special characters ( _ . : / = + + - or @) up to 256 characters' + Default: one-observability-demo + + pUserDefinedTagKey2: + Type: String + Description: Key for user defined tag 2 + AllowedPattern: ^(?!aws:)[A-Za-z0-9\s_.:=+-@/]{0,128} + ConstraintDescription: 'Must match the allowable values for a Tag Key. This can + only contain alphanumeric characters or special characters ( _ . : / = + - + or @) up to 128 characters' + Default: Version + + pUserDefinedTagValue2: + Type: String + Description: Value for user defined tag 2 + AllowedPattern: ^[\w\s_.:=+-@/]{0,256}$ + ConstraintDescription: 'Must match the allowable values for a Tag Value. This + can only contain alphanumeric characters or special characters ( _ . : / = + + - or @) up to 256 characters' + Default: 1.0.0 + + pUserDefinedTagKey3: + Type: String + Description: Key for user defined tag 3 + AllowedPattern: ^(?!aws:)[A-Za-z0-9\s_.:=+-@/]{0,128} + ConstraintDescription: 'Must match the allowable values for a Tag Key. This can + only contain alphanumeric characters or special characters ( _ . : / = + - + or @) up to 128 characters' + + pUserDefinedTagValue3: + Type: String + Description: Value for user defined tag 3 + AllowedPattern: ^[\w\s_.:=+-@/]{0,256}$ + ConstraintDescription: 'Must match the allowable values for a Tag Value. This + can only contain alphanumeric characters or special characters ( _ . : / = + + - or @) up to 256 characters' + + pUserDefinedTagKey4: + Type: String + Description: Key for user defined tag 4 + AllowedPattern: ^(?!aws:)[A-Za-z0-9\s_.:=+-@/]{0,128} + ConstraintDescription: 'Must match the allowable values for a Tag Key. This can + only contain alphanumeric characters or special characters ( _ . : / = + - + or @) up to 128 characters' + + pUserDefinedTagValue4: + Type: String + Description: Value for user defined tag 4 + AllowedPattern: ^[\w\s_.:=+-@/]{0,256}$ + ConstraintDescription: 'Must match the allowable values for a Tag Value. This + can only contain alphanumeric characters or special characters ( _ . : / = + + - or @) up to 256 characters' + + pUserDefinedTagKey5: + Type: String + Description: Key for user defined tag 5 + AllowedPattern: ^(?!aws:)[A-Za-z0-9\s_.:=+-@/]{0,128} + ConstraintDescription: 'Must match the allowable values for a Tag Key. This can + only contain alphanumeric characters or special characters ( _ . : / = + - + or @) up to 128 characters' + + pUserDefinedTagValue5: + Type: String + Description: Value for user defined tag 5 + AllowedPattern: ^[\w\s_.:=+-@/]{0,256}$ + ConstraintDescription: 'Must match the allowable values for a Tag Value. This + can only contain alphanumeric characters or special characters ( _ . : / = + + - or @) up to 256 characters' + +Conditions: + # Conditions to control creation of user-defined tags + cCreateTag1: !And + - !Not + - !Equals + - !Ref pUserDefinedTagKey1 + - '' + - !Not + - !Equals + - !Ref pUserDefinedTagValue1 + - '' + + cCreateTag2: !And + - !Not + - !Equals + - !Ref pUserDefinedTagKey2 + - '' + - !Not + - !Equals + - !Ref pUserDefinedTagValue2 + - '' + + cCreateTag3: !And + - !Not + - !Equals + - !Ref pUserDefinedTagKey3 + - '' + - !Not + - !Equals + - !Ref pUserDefinedTagValue3 + - '' + + cCreateTag4: !And + - !Not + - !Equals + - !Ref pUserDefinedTagKey4 + - '' + - !Not + - !Equals + - !Ref pUserDefinedTagValue4 + - '' + + cCreateTag5: !And + - !Not + - !Equals + - !Ref pUserDefinedTagKey5 + - '' + - !Not + - !Equals + - !Ref pUserDefinedTagValue5 + - '' + +Resources: + # Lambda execution role for resource cleanup + rResourceCleanupRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: lambda.amazonaws.com + Action: sts:AssumeRole + ManagedPolicyArns: + - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole + Policies: + - PolicyName: ResourceCleanup + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - s3:ListBucket + - s3:DeleteObject + - s3:ListBucketVersions + - s3:DeleteObjectVersion + Resource: + - !Sub ${rConfigBucket.Arn}/* + - !GetAtt rConfigBucket.Arn + + # Lambda function to clean up resources + rResourceCleanupFunction: + Type: AWS::Lambda::Function + Properties: + Runtime: python3.13 + Handler: index.handler + Role: !GetAtt rResourceCleanupRole.Arn + Timeout: 300 + Code: + ZipFile: | + import boto3 + import cfnresponse + def handler(event, context): + try: + if event['RequestType'] == 'Delete': + # Clean up S3 bucket + s3 = boto3.client('s3') + bucket = event['ResourceProperties']['BucketName'] + paginator = s3.get_paginator('list_object_versions') + for page in paginator.paginate(Bucket=bucket): + delete_keys = [] + if 'Versions' in page: + delete_keys.extend([{'Key': obj['Key'], 'VersionId': obj['VersionId']} for obj in page['Versions']]) + if 'DeleteMarkers' in page: + delete_keys.extend([{'Key': obj['Key'], 'VersionId': obj['VersionId']} for obj in page['DeleteMarkers']]) + if delete_keys: + s3.delete_objects(Bucket=bucket, Delete={'Objects': delete_keys}) + cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) + except Exception as e: + cfnresponse.send(event, context, cfnresponse.FAILED, {}, str(e)) + + # Create S3 bucket to store configuration files + rConfigBucket: + Type: AWS::S3::Bucket + Properties: + VersioningConfiguration: + Status: Enabled + BucketEncryption: + ServerSideEncryptionConfiguration: + - ServerSideEncryptionByDefault: + SSEAlgorithm: AES256 + PublicAccessBlockConfiguration: + BlockPublicAcls: true + BlockPublicPolicy: true + IgnorePublicAcls: true + RestrictPublicBuckets: true + Tags: + - Key: Name + Value: !Sub ${AWS::StackName}-config-bucket + - !If + - cCreateTag1 + - Key: !Ref pUserDefinedTagKey1 + Value: !Ref pUserDefinedTagValue1 + - !Ref AWS::NoValue + - !If + - cCreateTag2 + - Key: !Ref pUserDefinedTagKey2 + Value: !Ref pUserDefinedTagValue2 + - !Ref AWS::NoValue + - !If + - cCreateTag3 + - Key: !Ref pUserDefinedTagKey3 + Value: !Ref pUserDefinedTagValue3 + - !Ref AWS::NoValue + - !If + - cCreateTag4 + - Key: !Ref pUserDefinedTagKey4 + Value: !Ref pUserDefinedTagValue4 + - !Ref AWS::NoValue + - !If + - cCreateTag5 + - Key: !Ref pUserDefinedTagKey5 + Value: !Ref pUserDefinedTagValue5 + - !Ref AWS::NoValue + + # Custom resource to clean up resources on deletion + rResourceCleanup: + Type: AWS::CloudFormation::CustomResource + Properties: + ServiceToken: !GetAtt rResourceCleanupFunction.Arn + BucketName: !Ref rConfigBucket + StackName: !Ref AWS::StackName + + # Bucket policy to enforce HTTPS-only access + rConfigBucketPolicy: + Type: AWS::S3::BucketPolicy + Properties: + Bucket: !Ref rConfigBucket + PolicyDocument: + Statement: + - Sid: DenyInsecureConnections + Effect: Deny + Principal: '*' + Action: s3:* + Resource: + - !GetAtt rConfigBucket.Arn + - !Sub ${rConfigBucket.Arn}/* + Condition: + Bool: + aws:SecureTransport: 'false' + # Create IAM role for CodeBuild project + rCodeBuildServiceRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: codebuild.amazonaws.com + Action: sts:AssumeRole + ManagedPolicyArns: + - !Sub arn:${AWS::Partition}:iam::aws:policy/AdministratorAccess + Policies: + - PolicyName: EnhancedPipelineMonitoring + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - codepipeline:ListPipelineExecutions + - codepipeline:GetPipelineExecution + - codepipeline:GetPipelineState + - codepipeline:ListActionExecutions + Resource: '*' + Condition: + StringEquals: + 'aws:RequestedRegion': !Ref AWS::Region + Tags: + - Key: Name + Value: !Sub ${AWS::StackName}-codebuild-role + - !If + - cCreateTag1 + - Key: !Ref pUserDefinedTagKey1 + Value: !Ref pUserDefinedTagValue1 + - !Ref AWS::NoValue + - !If + - cCreateTag2 + - Key: !Ref pUserDefinedTagKey2 + Value: !Ref pUserDefinedTagValue2 + - !Ref AWS::NoValue + - !If + - cCreateTag3 + - Key: !Ref pUserDefinedTagKey3 + Value: !Ref pUserDefinedTagValue3 + - !Ref AWS::NoValue + - !If + - cCreateTag4 + - Key: !Ref pUserDefinedTagKey4 + Value: !Ref pUserDefinedTagValue4 + - !Ref AWS::NoValue + - !If + - cCreateTag5 + - Key: !Ref pUserDefinedTagKey5 + Value: !Ref pUserDefinedTagValue5 + - !Ref AWS::NoValue + + # Create wait condition handle for signaling completion + rCDKDeploymentWaitConditionHandle: + Type: AWS::CloudFormation::WaitConditionHandle + + # Create wait condition to ensure CodeBuild project completes successfully + rCDKDeploymentWaitCondition: + Type: AWS::CloudFormation::WaitCondition + Properties: + Handle: !Ref rCDKDeploymentWaitConditionHandle + Timeout: 3600 + Count: 1 + + # Create CodeBuild project to orchestrate CDK deployment + rCDKDeploymentProject: + Type: AWS::CodeBuild::Project + Properties: + Name: !Sub ${AWS::StackName}-cdk-deployment + Artifacts: + Type: NO_ARTIFACTS + Environment: + Type: LINUX_CONTAINER + ComputeType: BUILD_GENERAL1_SMALL + Image: aws/codebuild/amazonlinux2-x86_64-standard:5.0 + PrivilegedMode: true + EnvironmentVariables: + - Name: CONFIG_FILE_URL + Value: !Ref pConfigFileUrl + Type: PLAINTEXT + - Name: CONFIG_BUCKET + Value: !Ref rConfigBucket + Type: PLAINTEXT + - Name: ORGANIZATION_NAME + Value: !Ref pOrganizationName + Type: PLAINTEXT + - Name: REPOSITORY_NAME + Value: !Ref pRepositoryName + Type: PLAINTEXT + - Name: BRANCH_NAME + Value: !Ref pBranchName + Type: PLAINTEXT + - Name: WORKING_FOLDER + Value: !Ref pWorkingFolder + Type: PLAINTEXT + - Name: STACK_NAME + Value: !Ref AWS::StackName + Type: PLAINTEXT + - Name: AWS_REGION + Value: !Ref AWS::Region + Type: PLAINTEXT + - Name: AWS_ACCOUNT_ID + Value: !Ref AWS::AccountId + Type: PLAINTEXT + ServiceRole: !GetAtt rCodeBuildServiceRole.Arn + Source: + Type: NO_SOURCE + BuildSpec: | + version: 0.2 + phases: + install: + runtime-versions: + nodejs: 22 + python: 3.12 + commands: + - npm install -g aws-cdk + - pip3 install git-remote-s3 + pre_build: + commands: + # Configure git with generic user info + - git config --global user.email "codebuild@aws.amazon.com" + - git config --global user.name "AWS CodeBuild" + # Clone repository with shallow clone + - git clone --depth 1 --branch $BRANCH_NAME https://github.com/$ORGANIZATION_NAME/$REPOSITORY_NAME.git ./repo + - cd ./repo + # Download configuration file to repository root + - curl -o ./config.json "$CONFIG_FILE_URL" + # Create .env file with all environment variables + - | + cat > .env << EOF + CONFIG_FILE_URL=$CONFIG_FILE_URL + CONFIG_BUCKET=$CONFIG_BUCKET + ORGANIZATION_NAME=$ORGANIZATION_NAME + REPOSITORY_NAME=$REPOSITORY_NAME + BRANCH_NAME=$BRANCH_NAME + WORKING_FOLDER=$WORKING_FOLDER + STACK_NAME=$STACK_NAME + AWS_REGION=$AWS_REGION + AWS_ACCOUNT_ID=$AWS_ACCOUNT_ID + EOF + # Add config file and .env file to repository + - git add config.json .env -f + - git commit -m "Add configuration file from $CONFIG_FILE_URL and environment variables" + # Configure S3 remote with s3+zip protocol for CodePipeline + - git remote add s3 s3+zip://$CONFIG_BUCKET/repo + - git push s3 $BRANCH_NAME + # Check if account is bootstrapped + - | + STACK_EXISTS=$(aws cloudformation list-stacks --query 'StackSummaries[?StackName==`CDKToolkit` && StackStatus!=`DELETE_COMPLETE`].StackName' --output text) + if [ -z "$STACK_EXISTS" ]; then + echo "Account not bootstrapped, bootstrapping now..." + cdk bootstrap aws://${AWS_ACCOUNT_ID}/${AWS_REGION} + else + STACK_STATUS=$(aws cloudformation list-stacks --query 'StackSummaries[?StackName==`CDKToolkit` && StackStatus!=`DELETE_COMPLETE`].StackStatus' --output text) + if [ "$STACK_STATUS" = "ROLLBACK_COMPLETE" ]; then + echo "CDK bootstrap stack in ROLLBACK_COMPLETE state, cleaning up resources..." + # Force remove ECR and S3 buckets + aws ecr describe-repositories --query 'repositories[?repositoryName.starts_with(@, `cdk-`)].[repositoryName]' --output text | xargs -I {} aws ecr delete-repository --repository-name {} --force || true + aws s3 ls | grep cdk- | awk '{print $3}' | xargs -I {} aws s3 rb s3://{} --force || true + # Delete the stack + aws cloudformation delete-stack --stack-name CDKToolkit + aws cloudformation wait stack-delete-complete --stack-name CDKToolkit + # Bootstrap again + cdk bootstrap aws://${AWS_ACCOUNT_ID}/${AWS_REGION} + else + echo "CDK bootstrap stack exists with status: $STACK_STATUS" + fi + fi + build: + commands: + - cd $WORKING_FOLDER + # Install dependencies and synthesize CDK + - npm install + - cdk synth + - cdk deploy --require-approval never --outputs-file cdk-outputs.json + # Extract pipeline ARN for later use + - PIPELINE_ARN=$(cat cdk-outputs.json | jq -r '.[] | select(has("PipelineArn")) | .PipelineArn') + - | + if [ -z "$PIPELINE_ARN" ] || [ "$PIPELINE_ARN" = "null" ]; then + echo "ERROR: Pipeline ARN is empty or null. CDK deployment failed to create pipeline." + exit 1 + fi + - | + echo "Pipeline ARN: $PIPELINE_ARN" + - PIPELINE_NAME=$(echo $PIPELINE_ARN | cut -d':' -f6) + - | + echo "Pipeline Name: $PIPELINE_NAME" + # Wait for pipeline to complete using direct status checking with retry handling + - | + echo "Waiting for pipeline ${PIPELINE_NAME} execution to complete..." + TIMEOUT=3600 # 1 hour timeout + ELAPSED=0 + SLEEP_INTERVAL=30 + INITIAL_EXECUTION_ID="" + RETRY_COUNT=0 + MAX_RETRIES=3 + RETRY_LOOP_COUNT=0 + MAX_RETRY_LOOPS=10 + + # Get the initial execution ID to track retries + INITIAL_EXECUTION_ID=$(aws codepipeline list-pipeline-executions \ + --pipeline-name "$PIPELINE_NAME" \ + --max-items 1 \ + --query 'pipelineExecutionSummaries[0].pipelineExecutionId' \ + --output text) + + echo "Initial pipeline execution ID: $INITIAL_EXECUTION_ID" + + while [ $ELAPSED -lt $TIMEOUT ]; do + # Get the most recent pipeline execution details + EXECUTION_DETAILS=$(aws codepipeline list-pipeline-executions \ + --pipeline-name "$PIPELINE_NAME" \ + --max-items 1 \ + --query 'pipelineExecutionSummaries[0].[pipelineExecutionId,status]' \ + --output text) + + CURRENT_EXECUTION_ID=$(echo "$EXECUTION_DETAILS" | cut -f1) + EXECUTION_STATUS=$(echo "$EXECUTION_DETAILS" | cut -f2) + + # Check if this is a new execution (retry scenario) + if [ "$CURRENT_EXECUTION_ID" != "$INITIAL_EXECUTION_ID" ]; then + RETRY_COUNT=$((RETRY_COUNT + 1)) + echo "Detected new pipeline execution (retry #$RETRY_COUNT): $CURRENT_EXECUTION_ID" + INITIAL_EXECUTION_ID="$CURRENT_EXECUTION_ID" + RETRY_LOOP_COUNT=0 # Reset retry loop counter for new execution + echo "Continuing to monitor new execution..." + fi + + echo "Current pipeline execution status: $EXECUTION_STATUS (ID: $CURRENT_EXECUTION_ID)" + + case "$EXECUTION_STATUS" in + "Succeeded") + echo "Pipeline execution completed successfully!" + if [ $RETRY_COUNT -gt 0 ]; then + echo "Success achieved after $RETRY_COUNT retry(ies)" + fi + echo "Signaling CloudFormation SUCCESS" + WAIT_HANDLE_URL=$(aws cloudformation describe-stack-resource --stack-name $STACK_NAME --logical-resource-id rCDKDeploymentWaitConditionHandle --query 'StackResourceDetail.PhysicalResourceId' --output text --region $AWS_REGION) + curl -X PUT -H 'Content-Type:' --data-binary '{"Status" : "SUCCESS","Reason" : "Pipeline completed successfully","UniqueId" : "'$(uuidgen)'","Data" : "Pipeline execution finished"}' "$WAIT_HANDLE_URL" + break + ;; + "Failed") + echo "Pipeline execution failed with status: $EXECUTION_STATUS" + RETRY_LOOP_COUNT=$((RETRY_LOOP_COUNT + 1)) + echo "Retry loop count: $RETRY_LOOP_COUNT/$MAX_RETRY_LOOPS" + + if [ $RETRY_LOOP_COUNT -lt $MAX_RETRY_LOOPS ]; then + echo "Waiting for potential retry... (loop $RETRY_LOOP_COUNT of $MAX_RETRY_LOOPS)" + sleep 60 + ELAPSED=$((ELAPSED + 60)) + continue + else + echo "Maximum retry loops ($MAX_RETRY_LOOPS) reached without new execution. Build failed." + exit 1 + fi + ;; + "Cancelled"|"Stopped") + echo "Pipeline execution was cancelled or stopped: $EXECUTION_STATUS" + echo "This may indicate manual intervention or system issues." + exit 1 + ;; + "Superseded") + echo "Pipeline execution was superseded by a newer execution" + echo "Continuing to monitor the newer execution..." + sleep $SLEEP_INTERVAL + ELAPSED=$((ELAPSED + SLEEP_INTERVAL)) + ;; + "InProgress") + echo "Pipeline execution in progress..." + sleep $SLEEP_INTERVAL + ELAPSED=$((ELAPSED + SLEEP_INTERVAL)) + ;; + "Stopping") + echo "Pipeline execution is stopping..." + sleep $SLEEP_INTERVAL + ELAPSED=$((ELAPSED + SLEEP_INTERVAL)) + ;; + *) + echo "Unknown pipeline status: $EXECUTION_STATUS" + echo "Continuing to monitor..." + sleep $SLEEP_INTERVAL + ELAPSED=$((ELAPSED + SLEEP_INTERVAL)) + ;; + esac + + # Additional safety check for stuck executions + if [ $ELAPSED -gt 0 ] && [ $((ELAPSED % 300)) -eq 0 ]; then + echo "Progress check: $((ELAPSED / 60)) minutes elapsed, status: $EXECUTION_STATUS" + + # Get detailed stage information for better visibility + aws codepipeline get-pipeline-state \ + --name "$PIPELINE_NAME" \ + --query 'stageStates[*].[stageName,latestExecution.status]' \ + --output table || echo "Could not retrieve detailed stage information" + fi + done + + if [ $ELAPSED -ge $TIMEOUT ]; then + echo "Timeout reached after $((TIMEOUT / 60)) minutes" + echo "Final pipeline status: $EXECUTION_STATUS" + echo "Total retries attempted: $RETRY_COUNT" + + # Get final pipeline state for debugging + echo "Final pipeline state:" + aws codepipeline get-pipeline-state \ + --name "$PIPELINE_NAME" \ + --query 'stageStates[*].[stageName,latestExecution.status,latestExecution.errorDetails.message]' \ + --output table || echo "Could not retrieve final pipeline state" + + exit 1 + fi + post_build: + commands: + - | + if [ "$CODEBUILD_BUILD_SUCCEEDING" = "0" ]; then + echo "Build failed, cleaning up S3 bucket for rollback" + aws s3 rm s3://$CONFIG_BUCKET --recursive + echo "Triggering CDK stack cleanup via Step Function" + aws stepfunctions start-execution \ + --state-machine-arn "arn:aws:states:$AWS_REGION:$AWS_ACCOUNT_ID:stateMachine:$STACK_NAME-cdk-cleanup" \ + --input '{}' || echo "Failed to trigger cleanup, continuing..." + WAIT_HANDLE_URL=$(aws cloudformation describe-stack-resource --stack-name $STACK_NAME --logical-resource-id rCDKDeploymentWaitConditionHandle --query 'StackResourceDetail.PhysicalResourceId' --output text --region $AWS_REGION) + curl -X PUT -H 'Content-Type:' --data-binary '{"Status" : "FAILURE","Reason" : "Build failed","UniqueId" : "'$(uuidgen)'","Data" : "Build execution failed"}' "$WAIT_HANDLE_URL" + else + echo "Build completed - signal already sent during pipeline monitoring" + fi + Tags: + - Key: Name + Value: !Sub ${AWS::StackName}-cdk-deployment + - !If + - cCreateTag1 + - Key: !Ref pUserDefinedTagKey1 + Value: !Ref pUserDefinedTagValue1 + - !Ref AWS::NoValue + - !If + - cCreateTag2 + - Key: !Ref pUserDefinedTagKey2 + Value: !Ref pUserDefinedTagValue2 + - !Ref AWS::NoValue + - !If + - cCreateTag3 + - Key: !Ref pUserDefinedTagKey3 + Value: !Ref pUserDefinedTagValue3 + - !Ref AWS::NoValue + - !If + - cCreateTag4 + - Key: !Ref pUserDefinedTagKey4 + Value: !Ref pUserDefinedTagValue4 + - !Ref AWS::NoValue + - !If + - cCreateTag5 + - Key: !Ref pUserDefinedTagKey5 + Value: !Ref pUserDefinedTagValue5 + - !Ref AWS::NoValue + # Start the CodeBuild project to begin deployment + rStartDeployment: + Type: Custom::StartDeployment + DependsOn: + - rConfigBucket + - rCDKDeploymentWaitConditionHandle + Properties: + ServiceToken: !GetAtt rStartDeploymentFunction.Arn + CodeBuildProjectName: !Ref rCDKDeploymentProject + + # Create Lambda function to start the CodeBuild project + rStartDeploymentFunction: + Type: AWS::Lambda::Function + Properties: + Runtime: python3.13 + Handler: index.handler + Role: !GetAtt rStartDeploymentFunctionRole.Arn + Code: + ZipFile: | + import boto3 + import cfnresponse + import time + + def handler(event, context): + if event['RequestType'] in ['Create', 'Update']: + try: + codebuild = boto3.client('codebuild') + project_name = event['ResourceProperties']['CodeBuildProjectName'] + + # Start the CodeBuild project + response = codebuild.start_build(projectName=project_name) + build_id = response['build']['id'] + + # Wait a few seconds to ensure the build starts + time.sleep(5) + + cfnresponse.send(event, context, cfnresponse.SUCCESS, { + 'BuildId': build_id + }) + except Exception as e: + print(f"Error starting CodeBuild project: {str(e)}") + cfnresponse.send(event, context, cfnresponse.FAILED, { + 'Error': str(e) + }) + else: # Delete + cfnresponse.send(event, context, cfnresponse.SUCCESS, {}) + Timeout: 30 + Tags: + - Key: Name + Value: !Sub ${AWS::StackName}-start-deployment-function + - !If + - cCreateTag1 + - Key: !Ref pUserDefinedTagKey1 + Value: !Ref pUserDefinedTagValue1 + - !Ref AWS::NoValue + - !If + - cCreateTag2 + - Key: !Ref pUserDefinedTagKey2 + Value: !Ref pUserDefinedTagValue2 + - !Ref AWS::NoValue + - !If + - cCreateTag3 + - Key: !Ref pUserDefinedTagKey3 + Value: !Ref pUserDefinedTagValue3 + - !Ref AWS::NoValue + - !If + - cCreateTag4 + - Key: !Ref pUserDefinedTagKey4 + Value: !Ref pUserDefinedTagValue4 + - !Ref AWS::NoValue + - !If + - cCreateTag5 + - Key: !Ref pUserDefinedTagKey5 + Value: !Ref pUserDefinedTagValue5 + - !Ref AWS::NoValue + + # Create IAM role for start deployment Lambda function + rStartDeploymentFunctionRole: + Type: AWS::IAM::Role + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: lambda.amazonaws.com + Action: sts:AssumeRole + ManagedPolicyArns: + - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole + Policies: + - PolicyName: CodeBuildAccess + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - codebuild:StartBuild + Resource: !GetAtt rCDKDeploymentProject.Arn + Tags: + - Key: Name + Value: !Sub ${AWS::StackName}-start-deployment-role + - !If + - cCreateTag1 + - Key: !Ref pUserDefinedTagKey1 + Value: !Ref pUserDefinedTagValue1 + - !Ref AWS::NoValue + - !If + - cCreateTag2 + - Key: !Ref pUserDefinedTagKey2 + Value: !Ref pUserDefinedTagValue2 + - !Ref AWS::NoValue + - !If + - cCreateTag3 + - Key: !Ref pUserDefinedTagKey3 + Value: !Ref pUserDefinedTagValue3 + - !Ref AWS::NoValue + - !If + - cCreateTag4 + - Key: !Ref pUserDefinedTagKey4 + Value: !Ref pUserDefinedTagValue4 + - !Ref AWS::NoValue + - !If + - cCreateTag5 + - Key: !Ref pUserDefinedTagKey5 + Value: !Ref pUserDefinedTagValue5 + - !Ref AWS::NoValue + + # Step Function for CDK Stack Cleanup + rCDKCleanupStateMachine: + Type: AWS::StepFunctions::StateMachine + DeletionPolicy: Retain + UpdateReplacePolicy: Retain + Properties: + StateMachineName: !Sub ${AWS::StackName}-cdk-cleanup + RoleArn: !GetAtt rCDKCleanupRole.Arn + DefinitionString: !Sub | + { + "Comment": "CDK Stack Cleanup State Machine", + "StartAt": "ListTaggedStacks", + "States": { + "ListTaggedStacks": { + "Type": "Task", + "Resource": "arn:aws:states:::lambda:invoke", + "Parameters": { + "FunctionName": "${rCDKStackListerFunction}", + "Payload": {} + }, + "ResultPath": "$.stackList", + "Next": "CheckStacksFound" + }, + "CheckStacksFound": { + "Type": "Choice", + "Choices": [ + { + "Variable": "$.stackList.Payload.stacks", + "IsPresent": true, + "Next": "DeleteStacks" + } + ], + "Default": "NoStacksToClean" + }, + "DeleteStacks": { + "Type": "Map", + "ItemsPath": "$.stackList.Payload.stacks", + "MaxConcurrency": 1, + "Iterator": { + "StartAt": "DescribeStack", + "States": { + "DescribeStack": { + "Type": "Task", + "Resource": "arn:aws:states:::aws-sdk:cloudformation:describeStacks", + "Parameters": { + "StackName.$": "$" + }, + "Assign": { + "stackInfo.$": "$.Stacks[0]" + }, + "Next": "DeleteStack", + "Catch": [ + { + "ErrorEquals": ["CloudFormation.CloudFormationException"], + "Next": "StackAlreadyDeleted" + } + ] + }, + "DeleteStack": { + "Type": "Task", + "Resource": "arn:aws:states:::aws-sdk:cloudformation:deleteStack", + "Parameters": { + "StackName.$": "$stackInfo.StackName" + }, + "Next": "WaitForDeletion", + "Catch": [ + { + "ErrorEquals": ["States.ALL"], + "Next": "DeletionFailed" + } + ] + }, + "WaitForDeletion": { + "Type": "Wait", + "Seconds": 30, + "Next": "CheckDeletionStatus" + }, + "CheckDeletionStatus": { + "Type": "Task", + "Resource": "arn:aws:states:::aws-sdk:cloudformation:describeStacks", + "Parameters": { + "StackName.$": "$stackInfo.StackName" + }, + "ResultPath": "$.stackStatus", + "Next": "EvaluateDeletionStatus", + "Catch": [ + { + "ErrorEquals": ["CloudFormation.ValidationError", "CloudFormation.CloudFormationException"], + "Next": "DeletionComplete" + } + ] + }, + "EvaluateDeletionStatus": { + "Type": "Choice", + "Choices": [ + { + "Variable": "$.stackStatus.Stacks[0].StackStatus", + "StringEquals": "DELETE_IN_PROGRESS", + "Next": "WaitForDeletion" + }, + { + "Variable": "$.stackStatus.Stacks[0].StackStatus", + "StringEquals": "DELETE_COMPLETE", + "Next": "DeletionComplete" + } + ], + "Default": "DeletionFailed" + }, + "DeletionComplete": { + "Type": "Pass", + "Result": "Stack deleted successfully", + "End": true + }, + "DeletionFailed": { + "Type": "Pass", + "Result": "Stack deletion failed or cancelled", + "End": true + }, + "StackAlreadyDeleted": { + "Type": "Pass", + "Result": "Stack already deleted or does not exist", + "End": true + } + } + }, + "Next": "CleanupComplete" + }, + "NoStacksToClean": { + "Type": "Pass", + "Result": "No tagged stacks found to clean up", + "End": true + }, + "CleanupComplete": { + "Type": "Task", + "Resource": "arn:aws:states:::lambda:invoke", + "Parameters": { + "FunctionName": "${rCleanupCompletionFunction}", + "Payload": { + "StackName": "${AWS::StackName}" + } + }, + "End": true + } + } + } + + # IAM Role for Step Function + rCDKCleanupRole: + Type: AWS::IAM::Role + DeletionPolicy: Retain + UpdateReplacePolicy: Retain + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: states.amazonaws.com + Action: sts:AssumeRole + Policies: + - PolicyName: CDKCleanupPolicy + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - lambda:InvokeFunction + Resource: !GetAtt rCDKStackListerFunction.Arn + - Effect: Allow + Action: + - cloudformation:DescribeStacks + Resource: "*" + - Effect: Allow + Action: + - cloudformation:DeleteStack + Resource: "*" + Condition: + StringEquals: + "aws:ResourceTag/application": !Ref pApplicationName + - Effect: Allow + Action: + - lambda:InvokeFunction + Resource: !GetAtt rCleanupCompletionFunction.Arn + + # Lambda function to list CDK stacks by tag + rCDKStackListerFunction: + Type: AWS::Lambda::Function + DeletionPolicy: Retain + UpdateReplacePolicy: Retain + Properties: + FunctionName: !Sub ${AWS::StackName}-cdk-stack-lister + Runtime: python3.13 + Handler: index.handler + Role: !GetAtt rCDKStackListerRole.Arn + Timeout: 60 + Code: + ZipFile: !Sub | + import boto3 + + def handler(event, context): + cf_client = boto3.client('cloudformation') + stacks = [] + + try: + paginator = cf_client.get_paginator('list_stacks') + for page in paginator.paginate(StackStatusFilter=['CREATE_COMPLETE', 'UPDATE_COMPLETE', 'DELETE_FAILED', 'ROLLBACK_COMPLETE', 'ROLLBACK_FAILED']): + for stack in page['StackSummaries']: + # Skip nested stacks + if 'ParentId' in stack: + continue + + try: + # Get stack tags + stack_detail = cf_client.describe_stacks(StackName=stack['StackName']) + tags = stack_detail['Stacks'][0].get('Tags', []) + + # Check if stack has the required application tag + has_app_tag = False + sequence_value = 1 # Default sequence value + + for tag in tags: + if tag['Key'] == 'application' and tag['Value'] == '${pApplicationName}': + has_app_tag = True + elif tag['Key'] == 'sequence': + try: + sequence_value = int(tag['Value']) + except ValueError: + sequence_value = 1 + + if has_app_tag: + stacks.append({ + 'name': stack['StackName'], + 'sequence': sequence_value + }) + except Exception as e: + print(f"Error checking stack {stack['StackName']}: {str(e)}") + continue + + # Sort stacks by sequence in descending order (highest first) + stacks.sort(key=lambda x: x['sequence'], reverse=True) + + # Return just the stack names in sorted order + return {'stacks': [stack['name'] for stack in stacks]} + except Exception as e: + print(f"Error listing stacks: {str(e)}") + return {'stacks': []} + + # IAM Role for CDK Stack Lister Lambda + rCDKStackListerRole: + Type: AWS::IAM::Role + DeletionPolicy: Retain + UpdateReplacePolicy: Retain + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: lambda.amazonaws.com + Action: sts:AssumeRole + ManagedPolicyArns: + - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole + Policies: + - PolicyName: CDKStackListerPolicy + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - cloudformation:ListStacks + - cloudformation:DescribeStacks + Resource: "*" + + # Lambda function to clean up retained resources after Step Function completes + rCleanupCompletionFunction: + Type: AWS::Lambda::Function + DeletionPolicy: Retain + UpdateReplacePolicy: Retain + Properties: + FunctionName: !Sub ${AWS::StackName}-cleanup-completion + Runtime: python3.13 + Handler: index.handler + Role: !GetAtt rCleanupCompletionRole.Arn + Timeout: 300 + Code: + ZipFile: !Sub | + import boto3 + import time + + def handler(event, context): + stack_name = event.get('StackName') + if not stack_name: + print("No stack name provided") + return + + # Wait a bit to ensure all cleanup operations are complete + time.sleep(30) + + try: + cf_client = boto3.client('cloudformation') + + # List of resources to delete (in order) + resources_to_delete = [ + f"{stack_name}-cdk-cleanup", # Step Function + f"{stack_name}-cdk-stack-lister", # Lambda function + ] + + # Delete Step Function + try: + sf_client = boto3.client('stepfunctions') + sf_arn = f"arn:aws:states:${AWS::Region}:${AWS::AccountId}:stateMachine:{stack_name}-cdk-cleanup" + sf_client.delete_state_machine(stateMachineArn=sf_arn) + print(f"Deleted Step Function: {sf_arn}") + except Exception as e: + print(f"Error deleting Step Function: {str(e)}") + + # Delete Lambda function + try: + lambda_client = boto3.client('lambda') + lambda_client.delete_function(FunctionName=f"{stack_name}-cdk-stack-lister") + print(f"Deleted Lambda function: {stack_name}-cdk-stack-lister") + except Exception as e: + print(f"Error deleting Lambda function: {str(e)}") + + # Delete EventBridge rules + try: + events_client = boto3.client('events') + + # Get rules that match our stack + rules = events_client.list_rules()['Rules'] + for rule in rules: + if stack_name in rule.get('Description', '') or stack_name in rule['Name']: + try: + # Remove targets first + targets = events_client.list_targets_by_rule(Rule=rule['Name']) + if targets['Targets']: + target_ids = [target['Id'] for target in targets['Targets']] + events_client.remove_targets(Rule=rule['Name'], Ids=target_ids) + + # Delete rule + events_client.delete_rule(Name=rule['Name']) + print(f"Deleted EventBridge rule: {rule['Name']}") + except Exception as e: + print(f"Error deleting rule {rule['Name']}: {str(e)}") + except Exception as e: + print(f"Error deleting EventBridge rules: {str(e)}") + + # Delete IAM roles (these will be deleted automatically when no longer referenced) + print("Cleanup completion function executed successfully") + + # Finally, delete this function itself + try: + lambda_client.delete_function(FunctionName=f"{stack_name}-cleanup-completion") + print("Deleted cleanup completion function") + except Exception as e: + print(f"Error deleting cleanup completion function: {str(e)}") + + except Exception as e: + print(f"Error in cleanup completion: {str(e)}") + + # IAM Role for Cleanup Completion Lambda + rCleanupCompletionRole: + Type: AWS::IAM::Role + DeletionPolicy: Retain + UpdateReplacePolicy: Retain + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: lambda.amazonaws.com + Action: sts:AssumeRole + ManagedPolicyArns: + - !Sub arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole + Policies: + - PolicyName: CleanupCompletionPolicy + PolicyDocument: + Version: '2012-10-17' + Statement: + # Step Function deletion - specific to this stack's state machine + - Effect: Allow + Action: + - states:DeleteStateMachine + Resource: !Sub "arn:aws:states:${AWS::Region}:${AWS::AccountId}:stateMachine:${AWS::StackName}-cdk-cleanup" + # Lambda function deletion - specific to this stack's functions + - Effect: Allow + Action: + - lambda:DeleteFunction + Resource: + - !Sub "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${AWS::StackName}-cdk-stack-lister" + - !Sub "arn:aws:lambda:${AWS::Region}:${AWS::AccountId}:function:${AWS::StackName}-cleanup-completion" + # EventBridge rules - list and manage rules for this stack + - Effect: Allow + Action: + - events:ListRules + Resource: "*" + - Effect: Allow + Action: + - events:ListTargetsByRule + - events:RemoveTargets + - events:DeleteRule + Resource: !Sub "arn:aws:events:${AWS::Region}:${AWS::AccountId}:rule/*" + Condition: + StringLike: + "events:source": "aws.cloudformation" + # IAM role management - only for roles created by this stack + - Effect: Allow + Action: + - iam:DeleteRole + - iam:DetachRolePolicy + - iam:DeleteRolePolicy + Resource: !Sub "arn:aws:iam::${AWS::AccountId}:role/${AWS::StackName}-*" + + # EventBridge Rule for Stack Deletion + rStackDeletionRule: + Type: AWS::Events::Rule + DeletionPolicy: Retain + UpdateReplacePolicy: Retain + Properties: + Description: Trigger cleanup when stack is being deleted + EventPattern: + source: ["aws.cloudformation"] + detail-type: ["CloudFormation Stack Status Change"] + detail: + stack-id: [!Ref "AWS::StackId"] + status-details: + status: ["DELETE_IN_PROGRESS"] + State: ENABLED + Targets: + - Arn: !GetAtt rCDKCleanupStateMachine.Arn + Id: "CDKCleanupTarget" + RoleArn: !GetAtt rEventBridgeRole.Arn + + # EventBridge Rule for CodeBuild Failure + rCodeBuildFailureRule: + Type: AWS::Events::Rule + DeletionPolicy: Retain + UpdateReplacePolicy: Retain + Properties: + Description: Trigger cleanup when CodeBuild fails + EventPattern: + source: ["aws.codebuild"] + detail-type: ["CodeBuild Build State Change"] + detail: + project-name: [!Ref rCDKDeploymentProject] + build-status: ["FAILED", "FAULT", "STOPPED", "TIMED_OUT"] + State: ENABLED + Targets: + - Arn: !GetAtt rCDKCleanupStateMachine.Arn + Id: "CDKCleanupTarget" + RoleArn: !GetAtt rEventBridgeRole.Arn + + # IAM Role for EventBridge to invoke Step Function + rEventBridgeRole: + Type: AWS::IAM::Role + DeletionPolicy: Retain + UpdateReplacePolicy: Retain + Properties: + AssumeRolePolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Principal: + Service: events.amazonaws.com + Action: sts:AssumeRole + Policies: + - PolicyName: StepFunctionExecutionPolicy + PolicyDocument: + Version: '2012-10-17' + Statement: + - Effect: Allow + Action: + - states:StartExecution + Resource: !GetAtt rCDKCleanupStateMachine.Arn + + +Outputs: + oConfigBucketName: + Description: Name of the S3 bucket storing configuration files + Value: !Ref rConfigBucket + Export: + Name: !Sub ${AWS::StackName}-ConfigBucketName + + oCodeBuildProjectName: + Description: Name of the CodeBuild project orchestrating CDK deployment + Value: !Ref rCDKDeploymentProject + Export: + Name: !Sub ${AWS::StackName}-CodeBuildProjectName + + oDeploymentStatus: + Description: Status of the CDK deployment + Value: !Sub Check CodeBuild project ${rCDKDeploymentProject} for deployment status + Export: + Name: !Sub ${AWS::StackName}-DeploymentStatus + + oCDKCleanupStateMachine: + Description: ARN of the Step Function that handles CDK stack cleanup + Value: !GetAtt rCDKCleanupStateMachine.Arn + Export: + Name: !Sub ${AWS::StackName}-CDKCleanupStateMachine + + oRepositoryInfo: + Description: Repository information used for deployment + Value: !Sub 'Organization: ${pOrganizationName}, Repository: ${pRepositoryName}, + Branch: ${pBranchName}' + Export: + Name: !Sub ${AWS::StackName}-RepositoryInfo diff --git a/PetAdoptions/trafficgenerator/.idea/.gitignore b/src/use-cases/.gitkeep similarity index 100% rename from PetAdoptions/trafficgenerator/.idea/.gitignore rename to src/use-cases/.gitkeep diff --git a/PetAdoptions/cdk/pet_stack/resources/bunnies.zip b/static/images/bunnies.zip similarity index 100% rename from PetAdoptions/cdk/pet_stack/resources/bunnies.zip rename to static/images/bunnies.zip diff --git a/PetAdoptions/cdk/pet_stack/resources/kitten.zip b/static/images/kitten.zip similarity index 100% rename from PetAdoptions/cdk/pet_stack/resources/kitten.zip rename to static/images/kitten.zip diff --git a/PetAdoptions/cdk/pet_stack/resources/puppies.zip b/static/images/puppies.zip similarity index 100% rename from PetAdoptions/cdk/pet_stack/resources/puppies.zip rename to static/images/puppies.zip diff --git a/typedoc.json b/typedoc.json new file mode 100644 index 00000000..a1c2458b --- /dev/null +++ b/typedoc.json @@ -0,0 +1,61 @@ +{ + "entryPoints": ["src/cdk/lib"], + "entryPointStrategy": "expand", + "out": "./wiki-docs", + "theme": "default", + "name": "One Observability Workshop", + "includeVersion": true, + "excludePrivate": true, + "excludeProtected": false, + "excludeExternals": true, + "readme": "./README.md", + "plugin": ["typedoc-plugin-markdown"], + "projectDocuments": ["CONTRIBUTING.md", "docs/codebuild-cdk-deployment-template.md"], + "exclude": ["**/*.d.ts", "**/node_modules/**", "**/cdk.out/**"], + "gitRevision": "main", + "hideGenerator": true, + "sort": ["source-order"], + "kindSortOrder": [ + "Document", + "Project", + "Module", + "Namespace", + "Enum", + "EnumMember", + "Class", + "Interface", + "TypeAlias", + "Constructor", + "Property", + "Variable", + "Function", + "Accessor", + "Method", + "Parameter", + "TypeParameter", + "TypeLiteral", + "CallSignature", + "ConstructorSignature", + "IndexSignature", + "GetSignature", + "SetSignature" + ], + "categorizeByGroup": true, + "defaultCategory": "Other", + "categoryOrder": [ + "Constructs", + "Interfaces", + "Types", + "Functions", + "Variables", + "*" + ], + "tsconfig": "src/cdk/tsconfig.json", + "treatWarningsAsErrors": false, + "validation": { + "notExported": false, + "invalidLink": false, + "notDocumented": false + }, + "blockTags": ["@optional", "@extends", "@param", "@returns", "@throws", "@see", "@example", "@deprecated", "@since", "@author", "@default"] +}