diff --git a/.commitlintrc.yml b/.github/.commitlintrc.yml similarity index 100% rename from .commitlintrc.yml rename to .github/.commitlintrc.yml diff --git a/.releaserc.json b/.github/.releaserc.json similarity index 98% rename from .releaserc.json rename to .github/.releaserc.json index f8d8ab1..1e189d3 100644 --- a/.releaserc.json +++ b/.github/.releaserc.json @@ -116,7 +116,7 @@ { "assets": [ "CHANGELOG.md", - "package.json" + ".github/package.json" ], "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" } diff --git a/.github/GITHUB_ACTIONS_CICD.md b/.github/GITHUB_ACTIONS_CICD.md new file mode 100644 index 0000000..cfa97b0 --- /dev/null +++ b/.github/GITHUB_ACTIONS_CICD.md @@ -0,0 +1,114 @@ +# GitHub Actions CI/CD Guide + +Reference for CI validation and deployment workflows in this repository. + +## Workflows + +- `ci.yml` — lint, tests, security/dependency checks, Terraform plan validation +- `staging.yml` — staging build/apply/deploy/smoke-test +- `release.yml` — semantic release and production deploy flow +- `_smoke-test.yml` — reusable post-deploy health check + +## `ci.yml` + +Triggers: + +- `pull_request` to `main` and `develop` +- `push` to `main` and `develop` +- `workflow_dispatch` + +Responsibilities: + +- Conventional commit check (PR) +- Backend lint + unit/integration tests +- Frontend lint + type-check + build +- Trivy + GitGuardian scan +- Snyk dependency audit +- Terraform fmt/validate/plan (no apply) +- Final `quality-gate` status + +## `staging.yml` + +Triggers: + +- successful `CI` workflow run on `develop` +- manual dispatch + +Responsibilities: + +- Build/push backend and frontend images to ECR +- Apply staging Terraform +- Force ECS rolling deploy for both services +- Run reusable smoke test against `vars.APP_URL` + +Config model: + +- Non-secret values loaded from `config/.env.staging` +- Secrets from GitHub Environment `staging` + +## `release.yml` + +Two flows: + +1. Semantic release flow + - Trigger: successful `CI` workflow run on `main` (or manual dispatch) + - Runs semantic-release (creates version tag/release) +2. Production deploy flow + - Trigger: tag push `v*` + - Builds/pushes images to ECR + - Runs Terraform apply for production + - Forces ECS deploy + - Runs smoke test against `vars.APP_URL` + +Config model: + +- Uses GitHub Environment `production` secrets + +## Required GitHub configuration + +### Repository secrets + +- `DATABASE_USER` +- `DATABASE_PASSWORD` +- `DATABASE_NAME` +- `DATABASE_PORT` +- `AWS_ROLE_TO_ASSUME` +- `GITGUARDIAN_API_KEY` +- `SNYK_TOKEN` + +### Environment `staging` secrets + +- `AWS_ROLE_TO_ASSUME` +- `TERRAFORM_STATE_BUCKET` +- `TERRAFORM_LOCK_TABLE` (compatibility input) +- `JWT_SECRET_KEY` + +### Environment `staging` vars + +- `APP_URL` + +### Environment `production` secrets + +- `AWS_ROLE_TO_ASSUME` +- `AWS_REGION` +- `TF_VERSION` +- `TERRAFORM_STATE_BUCKET` +- `TERRAFORM_LOCK_TABLE` (compatibility input) +- `JWT_SECRET_KEY` + +### Environment `production` vars + +- `APP_URL` + +## Terraform backend lock note + +Infrastructure init now uses `use_lockfile=true` for backend locking. + +`TERRAFORM_LOCK_TABLE` remains exposed in current workflow inputs for backward compatibility. + +## Related files + +- `.github/workflows/ci.yml` +- `.github/workflows/staging.yml` +- `.github/workflows/release.yml` +- `.github/workflows/_smoke-test.yml` diff --git a/.github/actions/aws-auth/action.yml b/.github/actions/aws-auth/action.yml index 3c2d8af..7f9940b 100644 --- a/.github/actions/aws-auth/action.yml +++ b/.github/actions/aws-auth/action.yml @@ -1,3 +1,14 @@ +############################################################################### +# composite action: aws-auth +# +# Authenticates the GitHub Actions runner with AWS by assuming an IAM role +# via OIDC (no long-lived credentials stored in secrets). +# +# Callers: ci.yml (terraform-plan), staging.yml (terraform-staging, +# deploy-staging), release.yml (terraform-production, +# deploy-production) +# Inputs: role-arn, aws-region +############################################################################### name: "AWS Authentication" description: "Authenticate with AWS using OIDC" diff --git a/.github/actions/docker-build/action.yml b/.github/actions/docker-build/action.yml index e530326..17633f7 100644 --- a/.github/actions/docker-build/action.yml +++ b/.github/actions/docker-build/action.yml @@ -1,3 +1,14 @@ +############################################################################### +# composite action: docker-build +# +# Builds a Docker image, optionally pushes it to a registry, and runs a +# Trivy vulnerability scan on the resulting image. +# +# Callers: staging.yml (build job), release.yml (build-production job) +# Inputs: context, dockerfile, image-name, registry, tags, build-args, +# scan, scan-severity, scan-exit-code, cache-scope, +# registry-username, registry-password +############################################################################### name: "Docker Build" description: "Build and push Docker image to registry" @@ -17,10 +28,12 @@ inputs: default: "ghcr.io" registry-username: description: "Registry username" - required: true + required: false + default: "" registry-password: description: "Registry password/token" - required: true + required: false + default: "" push: description: "Push to registry after build" required: false @@ -29,6 +42,30 @@ inputs: description: "Run Trivy vulnerability scan" required: false default: "true" + scan-severity: + description: "Trivy severities to include" + required: false + default: "CRITICAL,HIGH" + scan-exit-code: + description: "Trivy exit code on findings" + required: false + default: "0" + tags: + description: "docker/metadata-action tag rules" + required: false + default: "" + build-args: + description: "Additional Docker build args" + required: false + default: "" + cache-scope: + description: "GHA cache scope" + required: false + default: "" + platforms: + description: "Target platforms for image build" + required: false + default: "linux/amd64" cache: description: "Use GitHub Actions cache" required: false @@ -56,6 +93,7 @@ runs: uses: docker/setup-buildx-action@v2 - name: Login to registry + if: inputs.registry-username != '' && inputs.registry-password != '' uses: docker/login-action@v2 with: registry: ${{ inputs.registry }} @@ -78,11 +116,7 @@ runs: uses: docker/metadata-action@v4 with: images: ${{ inputs.registry }}/${{ inputs.image-name }} - tags: | - type=ref,event=branch - type=sha,prefix={{branch}}- - type=semver,pattern={{version}} - type=raw,value=latest,enable={{is_default_branch}} + tags: ${{ inputs.tags != '' && inputs.tags || 'type=ref,event=branch\ntype=sha,prefix={{branch}}-\ntype=semver,pattern={{version}}\ntype=raw,value=latest,enable={{is_default_branch}}' }} - name: Build and push Docker image id: build @@ -90,15 +124,17 @@ runs: with: context: ${{ inputs.context }} file: ${{ inputs.dockerfile }} + platforms: ${{ inputs.platforms }} push: ${{ inputs.push }} tags: ${{ steps.meta.outputs.tags }} labels: ${{ steps.meta.outputs.labels }} - cache-from: ${{ inputs.cache == 'true' && 'type=gha' || '' }} - cache-to: ${{ inputs.cache == 'true' && format('type=gha,mode=max,key={0}', steps.cache-key.outputs.key) || '' }} + cache-from: ${{ inputs.cache == 'true' && format('type=gha,scope={0}', inputs.cache-scope != '' && inputs.cache-scope || inputs.image-name) || '' }} + cache-to: ${{ inputs.cache == 'true' && format('type=gha,mode=max,scope={0}', inputs.cache-scope != '' && inputs.cache-scope || inputs.image-name) || '' }} build-args: | BUILD_DATE=$(date -u +'%Y-%m-%dT%H:%M:%SZ') VCS_REF=${{ github.sha }} VERSION=${{ github.ref_name }} + ${{ inputs.build-args }} - name: Run Trivy vulnerability scan id: trivy @@ -108,7 +144,8 @@ runs: image-ref: ${{ steps.meta.outputs.tags }} format: "sarif" output: "trivy-results.sarif" - severity: "CRITICAL,HIGH" + severity: ${{ inputs.scan-severity }} + exit-code: ${{ inputs.scan-exit-code }} continue-on-error: true - name: Upload Trivy results diff --git a/.github/actions/ecs/deploy/action.yml b/.github/actions/ecs/deploy/action.yml index 0cf9015..39be8de 100644 --- a/.github/actions/ecs/deploy/action.yml +++ b/.github/actions/ecs/deploy/action.yml @@ -1,3 +1,13 @@ +############################################################################### +# composite action: ecs/deploy +# +# Renders a new ECS task definition from a base JSON template with updated +# image URIs, registers the new task definition, and updates the ECS service +# to trigger a rolling deployment. +# +# Callers: release.yml (deploy-production job) +# Inputs: cluster, service, task-definition, container-name, image +############################################################################### name: "ECS Deploy" description: "Deploy updated task definition to ECS Fargate service" @@ -76,7 +86,6 @@ runs: service: ${{ inputs.service }} cluster: ${{ inputs.cluster }} wait-for-service-stability: ${{ inputs.wait-for-stability }} - wait-for-service-stability-timeout: ${{ inputs.timeout-seconds }} - name: Verify deployment shell: bash diff --git a/.github/actions/publish-test-results/action.yml b/.github/actions/publish-test-results/action.yml index 418f521..7bdc5c7 100644 --- a/.github/actions/publish-test-results/action.yml +++ b/.github/actions/publish-test-results/action.yml @@ -1,5 +1,23 @@ -name: "Publish Pytest Results" -description: "Upload pytest junit xml" +############################################################################### +# composite action: publish-test-results +# +# Uploads JUnit XML test result files as a workflow artifact so they are +# visible in the GitHub Actions UI and retained for post-run analysis. +# +# Callers: ci.yml (backend-ci job) +# Inputs: files (glob pattern), check_name (artifact display name) +############################################################################### +name: "Publish Test Results" +description: "Upload JUnit XML test results as a workflow artifact" + +inputs: + files: + description: "Glob pattern(s) for JUnit XML files (newline-separated)" + required: true + check_name: + description: "Artifact name shown in the Actions UI" + required: false + default: "Test Results" runs: using: "composite" @@ -7,5 +25,6 @@ runs: - name: Upload test results uses: actions/upload-artifact@v4 with: - name: pytest-results - path: backend/test-results.xml \ No newline at end of file + name: ${{ inputs.check_name }} + path: ${{ inputs.files }} + if-no-files-found: warn \ No newline at end of file diff --git a/.github/actions/terraform/apply/action.yml b/.github/actions/terraform/apply/action.yml index 1250147..0c3c9a9 100644 --- a/.github/actions/terraform/apply/action.yml +++ b/.github/actions/terraform/apply/action.yml @@ -1,5 +1,17 @@ +############################################################################### +# composite action: terraform/apply +# +# Self-contained Terraform workflow: init (with remote S3 state backend) → +# plan → apply. Designed for trusted deployment jobs; never used on PRs. +# The environment name is normalised ("production" → "prod") before loading +# the matching envs/.tfvars file. +# +# Callers: staging.yml (terraform-staging), release.yml (terraform-production) +# Inputs: working-directory, terraform-version, environment, aws-region, +# state-bucket, state-lock-table +############################################################################### name: "Terraform Apply" -description: "Apply Terraform changes to infrastructure" +description: "Init + plan + apply Terraform changes for a given environment" inputs: working-directory: @@ -7,11 +19,11 @@ inputs: required: false default: "./infra" terraform-version: - description: "Terraform version" + description: "Terraform version to install" required: false default: "1.5.0" environment: - description: "Environment (dev, staging, prod)" + description: "Target environment (staging | prod | production)" required: true aws-region: description: "AWS region" @@ -21,22 +33,20 @@ inputs: description: "S3 bucket for Terraform state" required: true state-lock-table: - description: "DynamoDB table for state locking" - required: true - plan-artifact-path: - description: "Path to Terraform plan artifact" - required: true + description: "(Deprecated) DynamoDB table for state locking" + required: false + default: "" outputs: apply-summary: - description: "Terraform apply summary" - value: ${{ steps.apply.outputs.summary }} + description: "Key Terraform outputs after apply" + value: ${{ steps.export.outputs.summary }} runs: using: "composite" steps: - name: Setup Terraform - uses: hashicorp/setup-terraform@v2 + uses: hashicorp/setup-terraform@v3 with: terraform_version: ${{ inputs.terraform-version }} @@ -44,36 +54,55 @@ runs: shell: bash working-directory: ${{ inputs.working-directory }} run: | + # Normalise: 'production' -> 'prod' for tfvars file lookup + ENV="${{ inputs.environment }}" + [[ "$ENV" == "production" ]] && ENV="prod" + echo "TF_ENV=$ENV" >> "$GITHUB_ENV" + terraform init \ -backend-config="bucket=${{ inputs.state-bucket }}" \ -backend-config="key=${{ inputs.environment }}/terraform.tfstate" \ -backend-config="region=${{ inputs.aws-region }}" \ - -backend-config="dynamodb_table=${{ inputs.state-lock-table }}" \ + -backend-config="use_lockfile=true" \ -upgrade + echo "Terraform init complete" + + - name: Terraform plan + id: plan + shell: bash + working-directory: ${{ inputs.working-directory }} + env: + TF_VAR_environment: ${{ inputs.environment }} + TF_VAR_aws_region: ${{ inputs.aws-region }} + run: | + PLAN_FILE="/tmp/tfplan.${{ inputs.environment }}" + terraform plan \ + -var-file="envs/${TF_ENV}.tfvars" \ + -out="$PLAN_FILE" \ + -input=false \ + -no-color + echo "plan-file=$PLAN_FILE" >> "$GITHUB_OUTPUT" + echo "Terraform plan complete" - name: Terraform apply id: apply shell: bash working-directory: ${{ inputs.working-directory }} run: | - if [ -f "${{ inputs.plan-artifact-path }}" ]; then - terraform apply -auto-approve "${{ inputs.plan-artifact-path }}" - else - echo "Error: Plan artifact not found at ${{ inputs.plan-artifact-path }}" - exit 1 - fi - - echo "✓ Terraform apply completed" + terraform apply -auto-approve "${{ steps.plan.outputs.plan-file }}" + echo "Terraform apply complete" - name: Export outputs + id: export shell: bash working-directory: ${{ inputs.working-directory }} run: | - terraform output -json > /tmp/tf-outputs-${{ inputs.environment }}.json - echo "Outputs saved to: /tmp/tf-outputs-${{ inputs.environment }}.json" + OUTPUT_FILE="/tmp/tf-outputs-${{ inputs.environment }}.json" + terraform output -json > "$OUTPUT_FILE" + echo "Outputs saved to: $OUTPUT_FILE" { echo 'summary</dev/null || echo "(no outputs)" echo 'EOF' - } >> $GITHUB_OUTPUT + } >> "$GITHUB_OUTPUT" diff --git a/.github/actions/terraform/plan/action.yml b/.github/actions/terraform/plan/action.yml index d15d3de..30e023c 100644 --- a/.github/actions/terraform/plan/action.yml +++ b/.github/actions/terraform/plan/action.yml @@ -1,3 +1,14 @@ +############################################################################### +# composite action: terraform/plan +# +# Runs terraform init (no backend) + terraform plan for a given environment +# and saves the plan output. Intended for review purposes only — never +# applies changes. +# +# Callers: ci.yml delegates to terraform/validate for the validate step, +# then runs an inline plan so output can be posted to PR comments. +# Inputs: working-directory, terraform-version, environment +############################################################################### name: "Terraform Plan" description: "Generate Terraform plan for infrastructure changes" @@ -21,8 +32,9 @@ inputs: description: "S3 bucket for Terraform state" required: true state-lock-table: - description: "DynamoDB table for state locking" - required: true + description: "(Deprecated) DynamoDB table for state locking" + required: false + default: "" outputs: plan-summary: @@ -33,7 +45,7 @@ runs: using: "composite" steps: - name: Setup Terraform - uses: hashicorp/setup-terraform@v2 + uses: hashicorp/setup-terraform@v3 with: terraform_version: ${{ inputs.terraform-version }} @@ -45,7 +57,7 @@ runs: -backend-config="bucket=${{ inputs.state-bucket }}" \ -backend-config="key=${{ inputs.environment }}/terraform.tfstate" \ -backend-config="region=${{ inputs.aws-region }}" \ - -backend-config="dynamodb_table=${{ inputs.state-lock-table }}" \ + -backend-config="use_lockfile=true" \ -upgrade - name: Terraform plan diff --git a/.github/actions/terraform/validate/action.yml b/.github/actions/terraform/validate/action.yml index 1dad62d..9075f1b 100644 --- a/.github/actions/terraform/validate/action.yml +++ b/.github/actions/terraform/validate/action.yml @@ -1,3 +1,13 @@ +############################################################################### +# composite action: terraform/validate +# +# Runs terraform fmt --check, terraform init -backend=false, and +# terraform validate to catch formatting and syntax errors without +# requiring remote state or AWS credentials. +# +# Callers: ci.yml (terraform-plan job, before the inline plan step) +# Inputs: working-directory, terraform-version +############################################################################### name: "Terraform Validate" description: "Validate Terraform configuration" @@ -15,7 +25,7 @@ runs: using: "composite" steps: - name: Setup Terraform - uses: hashicorp/setup-terraform@v2 + uses: hashicorp/setup-terraform@v3 with: terraform_version: ${{ inputs.terraform-version }} diff --git a/.github/package-lock.json b/.github/package-lock.json new file mode 100644 index 0000000..45783c6 --- /dev/null +++ b/.github/package-lock.json @@ -0,0 +1,7393 @@ +{ + "name": "mypythonproject1", + "version": "0.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "mypythonproject1", + "version": "0.0.0", + "devDependencies": { + "@commitlint/cli": "^19.3.0", + "@commitlint/config-conventional": "^19.3.0", + "@semantic-release/changelog": "^6.0.3", + "@semantic-release/commit-analyzer": "^13.0.0", + "@semantic-release/git": "^10.0.1", + "@semantic-release/github": "^11.0.0", + "@semantic-release/release-notes-generator": "^14.0.0", + "semantic-release": "^24.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@commitlint/cli": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-19.8.1.tgz", + "integrity": "sha512-LXUdNIkspyxrlV6VDHWBmCZRtkEVRpBKxi2Gtw3J54cGWhLCTouVD/Q6ZSaSvd2YaDObWK8mDjrz3TIKtaQMAA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/format": "^19.8.1", + "@commitlint/lint": "^19.8.1", + "@commitlint/load": "^19.8.1", + "@commitlint/read": "^19.8.1", + "@commitlint/types": "^19.8.1", + "tinyexec": "^1.0.0", + "yargs": "^17.0.0" + }, + "bin": { + "commitlint": "cli.js" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/config-conventional": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-19.8.1.tgz", + "integrity": "sha512-/AZHJL6F6B/G959CsMAzrPKKZjeEiAVifRyEwXxcT6qtqbPwGw+iQxmNS+Bu+i09OCtdNRW6pNpBvgPrtMr9EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^19.8.1", + "conventional-changelog-conventionalcommits": "^7.0.2" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/config-validator": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/config-validator/-/config-validator-19.8.1.tgz", + "integrity": "sha512-0jvJ4u+eqGPBIzzSdqKNX1rvdbSU1lPNYlfQQRIFnBgLy26BtC0cFnr7c/AyuzExMxWsMOte6MkTi9I3SQ3iGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^19.8.1", + "ajv": "^8.11.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/ensure": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-19.8.1.tgz", + "integrity": "sha512-mXDnlJdvDzSObafjYrOSvZBwkD01cqB4gbnnFuVyNpGUM5ijwU/r/6uqUmBXAAOKRfyEjpkGVZxaDsCVnHAgyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^19.8.1", + "lodash.camelcase": "^4.3.0", + "lodash.kebabcase": "^4.1.1", + "lodash.snakecase": "^4.1.1", + "lodash.startcase": "^4.4.0", + "lodash.upperfirst": "^4.3.1" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/execute-rule": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/execute-rule/-/execute-rule-19.8.1.tgz", + "integrity": "sha512-YfJyIqIKWI64Mgvn/sE7FXvVMQER/Cd+s3hZke6cI1xgNT/f6ZAz5heND0QtffH+KbcqAwXDEE1/5niYayYaQA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/format": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/format/-/format-19.8.1.tgz", + "integrity": "sha512-kSJj34Rp10ItP+Eh9oCItiuN/HwGQMXBnIRk69jdOwEW9llW9FlyqcWYbHPSGofmjsqeoxa38UaEA5tsbm2JWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^19.8.1", + "chalk": "^5.3.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/is-ignored": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/is-ignored/-/is-ignored-19.8.1.tgz", + "integrity": "sha512-AceOhEhekBUQ5dzrVhDDsbMaY5LqtN8s1mqSnT2Kz1ERvVZkNihrs3Sfk1Je/rxRNbXYFzKZSHaPsEJJDJV8dg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^19.8.1", + "semver": "^7.6.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/lint": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-19.8.1.tgz", + "integrity": "sha512-52PFbsl+1EvMuokZXLRlOsdcLHf10isTPlWwoY1FQIidTsTvjKXVXYb7AvtpWkDzRO2ZsqIgPK7bI98x8LRUEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/is-ignored": "^19.8.1", + "@commitlint/parse": "^19.8.1", + "@commitlint/rules": "^19.8.1", + "@commitlint/types": "^19.8.1" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/load": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-19.8.1.tgz", + "integrity": "sha512-9V99EKG3u7z+FEoe4ikgq7YGRCSukAcvmKQuTtUyiYPnOd9a2/H9Ak1J9nJA1HChRQp9OA/sIKPugGS+FK/k1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/config-validator": "^19.8.1", + "@commitlint/execute-rule": "^19.8.1", + "@commitlint/resolve-extends": "^19.8.1", + "@commitlint/types": "^19.8.1", + "chalk": "^5.3.0", + "cosmiconfig": "^9.0.0", + "cosmiconfig-typescript-loader": "^6.1.0", + "lodash.isplainobject": "^4.0.6", + "lodash.merge": "^4.6.2", + "lodash.uniq": "^4.5.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/message": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/message/-/message-19.8.1.tgz", + "integrity": "sha512-+PMLQvjRXiU+Ae0Wc+p99EoGEutzSXFVwQfa3jRNUZLNW5odZAyseb92OSBTKCu+9gGZiJASt76Cj3dLTtcTdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/parse": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/parse/-/parse-19.8.1.tgz", + "integrity": "sha512-mmAHYcMBmAgJDKWdkjIGq50X4yB0pSGpxyOODwYmoexxxiUCy5JJT99t1+PEMK7KtsCtzuWYIAXYAiKR+k+/Jw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/types": "^19.8.1", + "conventional-changelog-angular": "^7.0.0", + "conventional-commits-parser": "^5.0.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/read": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/read/-/read-19.8.1.tgz", + "integrity": "sha512-03Jbjb1MqluaVXKHKRuGhcKWtSgh3Jizqy2lJCRbRrnWpcM06MYm8th59Xcns8EqBYvo0Xqb+2DoZFlga97uXQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/top-level": "^19.8.1", + "@commitlint/types": "^19.8.1", + "git-raw-commits": "^4.0.0", + "minimist": "^1.2.8", + "tinyexec": "^1.0.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/resolve-extends": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-19.8.1.tgz", + "integrity": "sha512-GM0mAhFk49I+T/5UCYns5ayGStkTt4XFFrjjf0L4S26xoMTSkdCf9ZRO8en1kuopC4isDFuEm7ZOm/WRVeElVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/config-validator": "^19.8.1", + "@commitlint/types": "^19.8.1", + "global-directory": "^4.0.1", + "import-meta-resolve": "^4.0.0", + "lodash.mergewith": "^4.6.2", + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/rules": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-19.8.1.tgz", + "integrity": "sha512-Hnlhd9DyvGiGwjfjfToMi1dsnw1EXKGJNLTcsuGORHz6SS9swRgkBsou33MQ2n51/boIDrbsg4tIBbRpEWK2kw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@commitlint/ensure": "^19.8.1", + "@commitlint/message": "^19.8.1", + "@commitlint/to-lines": "^19.8.1", + "@commitlint/types": "^19.8.1" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/to-lines": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/to-lines/-/to-lines-19.8.1.tgz", + "integrity": "sha512-98Mm5inzbWTKuZQr2aW4SReY6WUukdWXuZhrqf1QdKPZBCCsXuG87c+iP0bwtD6DBnmVVQjgp4whoHRVixyPBg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/top-level": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/top-level/-/top-level-19.8.1.tgz", + "integrity": "sha512-Ph8IN1IOHPSDhURCSXBz44+CIu+60duFwRsg6HqaISFHQHbmBtxVw4ZrFNIYUzEP7WwrNPxa2/5qJ//NK1FGcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^7.0.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@commitlint/types": { + "version": "19.8.1", + "resolved": "https://registry.npmjs.org/@commitlint/types/-/types-19.8.1.tgz", + "integrity": "sha512-/yCrWGCoA1SVKOks25EGadP9Pnj0oAIHGpl2wH2M2Y46dPM2ueb8wyCVOD7O3WCTkaJ0IkKvzhl1JY7+uCT2Dw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/conventional-commits-parser": "^5.0.0", + "chalk": "^5.3.0" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@octokit/auth-token": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-6.0.0.tgz", + "integrity": "sha512-P4YJBPdPSpWTQ1NU4XYdvHvXJJDxM6YwpS0FZHRgP7YFkdVxsWcpWGy/NVqlAA7PcPCnMacXlRm1y2PFZRWL/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/core": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/@octokit/core/-/core-7.0.6.tgz", + "integrity": "sha512-DhGl4xMVFGVIyMwswXeyzdL4uXD5OGILGX5N8Y+f6W7LhC1Ze2poSNrkF/fedpVDHEEZ+PHFW0vL14I+mm8K3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/auth-token": "^6.0.0", + "@octokit/graphql": "^9.0.3", + "@octokit/request": "^10.0.6", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", + "before-after-hook": "^4.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/endpoint": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@octokit/endpoint/-/endpoint-11.0.3.tgz", + "integrity": "sha512-FWFlNxghg4HrXkD3ifYbS/IdL/mDHjh9QcsNyhQjN8dplUoZbejsdpmuqdA76nxj2xoWPs7p8uX2SNr9rYu0Ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/graphql": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/@octokit/graphql/-/graphql-9.0.3.tgz", + "integrity": "sha512-grAEuupr/C1rALFnXTv6ZQhFuL1D8G5y8CN04RgrO4FIPMrtm+mcZzFG7dcBm+nq+1ppNixu+Jd78aeJOYxlGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/request": "^10.0.6", + "@octokit/types": "^16.0.0", + "universal-user-agent": "^7.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/openapi-types": { + "version": "27.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-27.0.0.tgz", + "integrity": "sha512-whrdktVs1h6gtR+09+QsNk2+FO+49j6ga1c55YZudfEG+oKJVvJLQi3zkOm5JjiUXAagWK2tI2kTGKJ2Ys7MGA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/@octokit/plugin-paginate-rest/-/plugin-paginate-rest-13.2.1.tgz", + "integrity": "sha512-Tj4PkZyIL6eBMYcG/76QGsedF0+dWVeLhYprTmuFVVxzDW7PQh23tM0TP0z+1MvSkxB29YFZwnUX+cXfTiSdyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^15.0.1" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=6" + } + }, + "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/openapi-types": { + "version": "26.0.0", + "resolved": "https://registry.npmjs.org/@octokit/openapi-types/-/openapi-types-26.0.0.tgz", + "integrity": "sha512-7AtcfKtpo77j7Ts73b4OWhOZHTKo/gGY8bB3bNBQz4H+GRSWqx2yvj8TXRsbdTE0eRmYmXOEY66jM7mJ7LzfsA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@octokit/plugin-paginate-rest/node_modules/@octokit/types": { + "version": "15.0.2", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-15.0.2.tgz", + "integrity": "sha512-rR+5VRjhYSer7sC51krfCctQhVTmjyUMAaShfPB8mscVa8tSoLyon3coxQmXu0ahJoLVWl8dSGD/3OGZlFV44Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^26.0.0" + } + }, + "node_modules/@octokit/plugin-retry": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@octokit/plugin-retry/-/plugin-retry-8.1.0.tgz", + "integrity": "sha512-O1FZgXeiGb2sowEr/hYTr6YunGdSAFWnr2fyW39Ah85H8O33ELASQxcvOFF5LE6Tjekcyu2ms4qAzJVhSaJxTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", + "bottleneck": "^2.15.3" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": ">=7" + } + }, + "node_modules/@octokit/plugin-throttling": { + "version": "11.0.3", + "resolved": "https://registry.npmjs.org/@octokit/plugin-throttling/-/plugin-throttling-11.0.3.tgz", + "integrity": "sha512-34eE0RkFCKycLl2D2kq7W+LovheM/ex3AwZCYN8udpi6bxsyjZidb2McXs69hZhLmJlDqTSP8cH+jSRpiaijBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0", + "bottleneck": "^2.15.3" + }, + "engines": { + "node": ">= 20" + }, + "peerDependencies": { + "@octokit/core": "^7.0.0" + } + }, + "node_modules/@octokit/request": { + "version": "10.0.8", + "resolved": "https://registry.npmjs.org/@octokit/request/-/request-10.0.8.tgz", + "integrity": "sha512-SJZNwY9pur9Agf7l87ywFi14W+Hd9Jg6Ifivsd33+/bGUQIjNujdFiXII2/qSlN2ybqUHfp5xpekMEjIBTjlSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/endpoint": "^11.0.3", + "@octokit/request-error": "^7.0.2", + "@octokit/types": "^16.0.0", + "fast-content-type-parse": "^3.0.0", + "json-with-bigint": "^3.5.3", + "universal-user-agent": "^7.0.2" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/request-error": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/@octokit/request-error/-/request-error-7.1.0.tgz", + "integrity": "sha512-KMQIfq5sOPpkQYajXHwnhjCC0slzCNScLHs9JafXc4RAJI+9f+jNDlBNaIMTvazOPLgb4BnlhGJOTbnN0wIjPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/types": "^16.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@octokit/types": { + "version": "16.0.0", + "resolved": "https://registry.npmjs.org/@octokit/types/-/types-16.0.0.tgz", + "integrity": "sha512-sKq+9r1Mm4efXW1FCk7hFSeJo4QKreL/tTbR0rz/qx/r1Oa2VV83LTA/H/MuCOX7uCIJmQVRKBcbmWoySjAnSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/openapi-types": "^27.0.0" + } + }, + "node_modules/@pnpm/config.env-replace": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", + "integrity": "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@pnpm/network.ca-file": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", + "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "4.2.10" + }, + "engines": { + "node": ">=12.22.0" + } + }, + "node_modules/@pnpm/network.ca-file/node_modules/graceful-fs": { + "version": "4.2.10", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", + "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", + "dev": true, + "license": "ISC" + }, + "node_modules/@pnpm/npm-conf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-3.0.2.tgz", + "integrity": "sha512-h104Kh26rR8tm+a3Qkc5S4VLYint3FE48as7+/5oCEcKR2idC/pF1G6AhIXKI+eHPJa/3J9i5z0Al47IeGHPkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pnpm/config.env-replace": "^1.1.0", + "@pnpm/network.ca-file": "^1.0.1", + "config-chain": "^1.1.11" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@sec-ant/readable-stream": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@sec-ant/readable-stream/-/readable-stream-0.4.1.tgz", + "integrity": "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@semantic-release/changelog": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@semantic-release/changelog/-/changelog-6.0.3.tgz", + "integrity": "sha512-dZuR5qByyfe3Y03TpmCvAxCyTnp7r5XwtHRf/8vD9EAn4ZWbavUX8adMtXYzE86EVh0gyLA7lm5yW4IV30XUag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@semantic-release/error": "^3.0.0", + "aggregate-error": "^3.0.0", + "fs-extra": "^11.0.0", + "lodash": "^4.17.4" + }, + "engines": { + "node": ">=14.17" + }, + "peerDependencies": { + "semantic-release": ">=18.0.0" + } + }, + "node_modules/@semantic-release/commit-analyzer": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/@semantic-release/commit-analyzer/-/commit-analyzer-13.0.1.tgz", + "integrity": "sha512-wdnBPHKkr9HhNhXOhZD5a2LNl91+hs8CC2vsAVYxtZH3y0dV3wKn+uZSN61rdJQZ8EGxzWB3inWocBHV9+u/CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "conventional-changelog-angular": "^8.0.0", + "conventional-changelog-writer": "^8.0.0", + "conventional-commits-filter": "^5.0.0", + "conventional-commits-parser": "^6.0.0", + "debug": "^4.0.0", + "import-from-esm": "^2.0.0", + "lodash-es": "^4.17.21", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=20.8.1" + }, + "peerDependencies": { + "semantic-release": ">=20.1.0" + } + }, + "node_modules/@semantic-release/commit-analyzer/node_modules/conventional-changelog-angular": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-8.2.0.tgz", + "integrity": "sha512-4YB1zEXqB17oBI8yRsAs1T+ZhbdsOgJqkl6Trz+GXt/eKf1e4jnA0oW+sOd9BEENzEViuNW0DNoFFjSf3CeC5Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "compare-func": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@semantic-release/commit-analyzer/node_modules/conventional-commits-parser": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-6.3.0.tgz", + "integrity": "sha512-RfOq/Cqy9xV9bOA8N+ZH6DlrDR+5S3Mi0B5kACEjESpE+AviIpAptx9a9cFpWCCvgRtWT+0BbUw+e1BZfts9jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@simple-libs/stream-utils": "^1.2.0", + "meow": "^13.0.0" + }, + "bin": { + "conventional-commits-parser": "dist/cli/index.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@semantic-release/error": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@semantic-release/error/-/error-3.0.0.tgz", + "integrity": "sha512-5hiM4Un+tpl4cKw3lV4UgzJj+SmfNIDCLLw0TepzQxz9ZGV5ixnqkzIVF+3tp0ZHgcMKE+VNGHJjEeyFG2dcSw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.17" + } + }, + "node_modules/@semantic-release/git": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/@semantic-release/git/-/git-10.0.1.tgz", + "integrity": "sha512-eWrx5KguUcU2wUPaO6sfvZI0wPafUKAMNC18aXY4EnNcrZL86dEmpNVnC9uMpGZkmZJ9EfCVJBQx4pV4EMGT1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@semantic-release/error": "^3.0.0", + "aggregate-error": "^3.0.0", + "debug": "^4.0.0", + "dir-glob": "^3.0.0", + "execa": "^5.0.0", + "lodash": "^4.17.4", + "micromatch": "^4.0.0", + "p-reduce": "^2.0.0" + }, + "engines": { + "node": ">=14.17" + }, + "peerDependencies": { + "semantic-release": ">=18.0.0" + } + }, + "node_modules/@semantic-release/github": { + "version": "11.0.6", + "resolved": "https://registry.npmjs.org/@semantic-release/github/-/github-11.0.6.tgz", + "integrity": "sha512-ctDzdSMrT3H+pwKBPdyCPty6Y47X8dSrjd3aPZ5KKIKKWTwZBE9De8GtsH3TyAlw3Uyo2stegMx6rJMXKpJwJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@octokit/core": "^7.0.0", + "@octokit/plugin-paginate-rest": "^13.0.0", + "@octokit/plugin-retry": "^8.0.0", + "@octokit/plugin-throttling": "^11.0.0", + "@semantic-release/error": "^4.0.0", + "aggregate-error": "^5.0.0", + "debug": "^4.3.4", + "dir-glob": "^3.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.0", + "issue-parser": "^7.0.0", + "lodash-es": "^4.17.21", + "mime": "^4.0.0", + "p-filter": "^4.0.0", + "tinyglobby": "^0.2.14", + "url-join": "^5.0.0" + }, + "engines": { + "node": ">=20.8.1" + }, + "peerDependencies": { + "semantic-release": ">=24.1.0" + } + }, + "node_modules/@semantic-release/github/node_modules/@semantic-release/error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@semantic-release/error/-/error-4.0.0.tgz", + "integrity": "sha512-mgdxrHTLOjOddRVYIYDo0fR3/v61GNN1YGkfbrjuIKg/uMgCd+Qzo3UAXJ+woLQQpos4pl5Esuw5A7AoNlzjUQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@semantic-release/github/node_modules/aggregate-error": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-5.0.0.tgz", + "integrity": "sha512-gOsf2YwSlleG6IjRYG2A7k0HmBMEo6qVNk9Bp/EaLgAJT5ngH6PXbqa4ItvnEwCm/velL5jAnQgsHsWnjhGmvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^5.2.0", + "indent-string": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/github/node_modules/clean-stack": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-5.3.0.tgz", + "integrity": "sha512-9ngPTOhYGQqNVSfeJkYXHmF7AGWp4/nN5D/QqNQs3Dvxd1Kk/WpjHfNujKHYUQ/5CoGyOyFNoWSPk5afzP0QVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "5.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/github/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/@semantic-release/npm": { + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/@semantic-release/npm/-/npm-12.0.2.tgz", + "integrity": "sha512-+M9/Lb35IgnlUO6OSJ40Ie+hUsZLuph2fqXC/qrKn0fMvUU/jiCjpoL6zEm69vzcmaZJ8yNKtMBEKHWN49WBbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@semantic-release/error": "^4.0.0", + "aggregate-error": "^5.0.0", + "execa": "^9.0.0", + "fs-extra": "^11.0.0", + "lodash-es": "^4.17.21", + "nerf-dart": "^1.0.0", + "normalize-url": "^8.0.0", + "npm": "^10.9.3", + "rc": "^1.2.8", + "read-pkg": "^9.0.0", + "registry-auth-token": "^5.0.0", + "semver": "^7.1.2", + "tempy": "^3.0.0" + }, + "engines": { + "node": ">=20.8.1" + }, + "peerDependencies": { + "semantic-release": ">=20.1.0" + } + }, + "node_modules/@semantic-release/npm/node_modules/@semantic-release/error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@semantic-release/error/-/error-4.0.0.tgz", + "integrity": "sha512-mgdxrHTLOjOddRVYIYDo0fR3/v61GNN1YGkfbrjuIKg/uMgCd+Qzo3UAXJ+woLQQpos4pl5Esuw5A7AoNlzjUQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@semantic-release/npm/node_modules/aggregate-error": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-5.0.0.tgz", + "integrity": "sha512-gOsf2YwSlleG6IjRYG2A7k0HmBMEo6qVNk9Bp/EaLgAJT5ngH6PXbqa4ItvnEwCm/velL5jAnQgsHsWnjhGmvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^5.2.0", + "indent-string": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/npm/node_modules/clean-stack": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-5.3.0.tgz", + "integrity": "sha512-9ngPTOhYGQqNVSfeJkYXHmF7AGWp4/nN5D/QqNQs3Dvxd1Kk/WpjHfNujKHYUQ/5CoGyOyFNoWSPk5afzP0QVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "5.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/npm/node_modules/execa": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", + "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.6", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.1", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.2.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/@semantic-release/npm/node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/npm/node_modules/human-signals": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", + "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@semantic-release/npm/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/@semantic-release/npm/node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/npm/node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/npm/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/npm/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/@semantic-release/npm/node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/npm/node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@semantic-release/release-notes-generator": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@semantic-release/release-notes-generator/-/release-notes-generator-14.1.0.tgz", + "integrity": "sha512-CcyDRk7xq+ON/20YNR+1I/jP7BYKICr1uKd1HHpROSnnTdGqOTburi4jcRiTYz0cpfhxSloQO3cGhnoot7IEkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "conventional-changelog-angular": "^8.0.0", + "conventional-changelog-writer": "^8.0.0", + "conventional-commits-filter": "^5.0.0", + "conventional-commits-parser": "^6.0.0", + "debug": "^4.0.0", + "get-stream": "^7.0.0", + "import-from-esm": "^2.0.0", + "into-stream": "^7.0.0", + "lodash-es": "^4.17.21", + "read-package-up": "^11.0.0" + }, + "engines": { + "node": ">=20.8.1" + }, + "peerDependencies": { + "semantic-release": ">=20.1.0" + } + }, + "node_modules/@semantic-release/release-notes-generator/node_modules/conventional-changelog-angular": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-8.2.0.tgz", + "integrity": "sha512-4YB1zEXqB17oBI8yRsAs1T+ZhbdsOgJqkl6Trz+GXt/eKf1e4jnA0oW+sOd9BEENzEViuNW0DNoFFjSf3CeC5Q==", + "dev": true, + "license": "ISC", + "dependencies": { + "compare-func": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@semantic-release/release-notes-generator/node_modules/conventional-commits-parser": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-6.3.0.tgz", + "integrity": "sha512-RfOq/Cqy9xV9bOA8N+ZH6DlrDR+5S3Mi0B5kACEjESpE+AviIpAptx9a9cFpWCCvgRtWT+0BbUw+e1BZfts9jg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@simple-libs/stream-utils": "^1.2.0", + "meow": "^13.0.0" + }, + "bin": { + "conventional-commits-parser": "dist/cli/index.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@simple-libs/stream-utils": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@simple-libs/stream-utils/-/stream-utils-1.2.0.tgz", + "integrity": "sha512-KxXvfapcixpz6rVEB6HPjOUZT22yN6v0vI0urQSk1L8MlEWPDFCZkhw2xmkyoTGYeFw7tWTZd7e3lVzRZRN/EA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://ko-fi.com/dangreen" + } + }, + "node_modules/@sindresorhus/is": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", + "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/is?sponsor=1" + } + }, + "node_modules/@sindresorhus/merge-streams": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-4.0.0.tgz", + "integrity": "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@types/conventional-commits-parser": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@types/conventional-commits-parser/-/conventional-commits-parser-5.0.2.tgz", + "integrity": "sha512-BgT2szDXnVypgpNxOK8aL5SGjUdaQbC++WZNjF1Qge3Og2+zhHj+RWhmehLhYyvQwqAmvezruVfOf8+3m74W+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "25.3.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.3.3.tgz", + "integrity": "sha512-DpzbrH7wIcBaJibpKo9nnSQL0MTRdnWttGyE5haGwK86xgMOkFLp7vEyfQPGLOJh5wNYiJ3V9PmUMDhV9u8kkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/normalize-package-data": { + "version": "2.4.4", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz", + "integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==", + "dev": true, + "license": "MIT" + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/aggregate-error": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", + "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^2.0.0", + "indent-string": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "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==", + "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/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "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==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/argv-formatter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/argv-formatter/-/argv-formatter-1.0.0.tgz", + "integrity": "sha512-F2+Hkm9xFaRg+GkaNnbwXNDV5O6pnCFEmqyhvfC/Ic5LbgOWjJh3L+mN/s91rxVL3znE7DYVpW0GJFT+4YBgWw==", + "dev": true, + "license": "MIT" + }, + "node_modules/array-ify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/array-ify/-/array-ify-1.0.0.tgz", + "integrity": "sha512-c5AMf34bKdvPhQ7tBGhqkgKNUzMr4WUs+WDtC2ZUGOUncbxKMTvqxYctiseW3+L4bA8ec+GcZ6/A/FW4m8ukng==", + "dev": true, + "license": "MIT" + }, + "node_modules/before-after-hook": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/before-after-hook/-/before-after-hook-4.0.0.tgz", + "integrity": "sha512-q6tR3RPqIB1pMiTRMFcZwuG5T8vwp+vUvEG0vuI6B+Rikh5BfPp2fQ82c925FOs+b0lcFQ8CFrL+KbilfZFhOQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/bottleneck": { + "version": "2.19.5", + "resolved": "https://registry.npmjs.org/bottleneck/-/bottleneck-2.19.5.tgz", + "integrity": "sha512-VHiNCbI1lKdl44tGrhNfU3lup0Tj/ZBMJB5/2ZbNXRCPuRCO7ed2mgcK4r17y+KB2EfuYuRaVlwNbAeaWGSpbw==", + "dev": true, + "license": "MIT" + }, + "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/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/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "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/clean-stack": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", + "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/cli-highlight": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz", + "integrity": "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==", + "dev": true, + "license": "ISC", + "dependencies": { + "chalk": "^4.0.0", + "highlight.js": "^10.7.1", + "mz": "^2.4.0", + "parse5": "^5.1.1", + "parse5-htmlparser2-tree-adapter": "^6.0.0", + "yargs": "^16.0.0" + }, + "bin": { + "highlight": "bin/highlight" + }, + "engines": { + "node": ">=8.0.0", + "npm": ">=5.0.0" + } + }, + "node_modules/cli-highlight/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/cli-highlight/node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/cli-highlight/node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cli-highlight/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/cli-table3": { + "version": "0.6.5", + "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", + "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.0" + }, + "engines": { + "node": "10.* || >= 12.*" + }, + "optionalDependencies": { + "@colors/colors": "1.5.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/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/compare-func": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/compare-func/-/compare-func-2.0.0.tgz", + "integrity": "sha512-zHig5N+tPWARooBnb0Zx1MFcdfpyJrfTJ3Y5L+IFvUm8rM74hHz66z0gw0x4tijh5CorKkKUCnW82R2vmpeCRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-ify": "^1.0.0", + "dot-prop": "^5.1.0" + } + }, + "node_modules/config-chain": { + "version": "1.1.13", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", + "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" + } + }, + "node_modules/config-chain/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/conventional-changelog-angular": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-angular/-/conventional-changelog-angular-7.0.0.tgz", + "integrity": "sha512-ROjNchA9LgfNMTTFSIWPzebCwOGFdgkEq45EnvvrmSLvCtAw0HSmrCs7/ty+wAeYUZyNay0YMUNYFTRL72PkBQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "compare-func": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/conventional-changelog-conventionalcommits": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-7.0.2.tgz", + "integrity": "sha512-NKXYmMR/Hr1DevQegFB4MwfM5Vv0m4UIxKZTTYuD98lpTknaZlSRrDOG4X7wIXpGkfsYxZTghUN+Qq+T0YQI7w==", + "dev": true, + "license": "ISC", + "dependencies": { + "compare-func": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/conventional-changelog-writer": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/conventional-changelog-writer/-/conventional-changelog-writer-8.3.0.tgz", + "integrity": "sha512-l5hDOHjcTUVtnZJapoqXMCJ3IbyF6oV/vnxKL13AHulFH7mDp4PMJARxI7LWzob6UDDvhxIUWGTNUPW84JabQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@simple-libs/stream-utils": "^1.2.0", + "conventional-commits-filter": "^5.0.0", + "handlebars": "^4.7.7", + "meow": "^13.0.0", + "semver": "^7.5.2" + }, + "bin": { + "conventional-changelog-writer": "dist/cli/index.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/conventional-commits-filter": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/conventional-commits-filter/-/conventional-commits-filter-5.0.0.tgz", + "integrity": "sha512-tQMagCOC59EVgNZcC5zl7XqO30Wki9i9J3acbUvkaosCT6JX3EeFwJD7Qqp4MCikRnzS18WXV3BLIQ66ytu6+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/conventional-commits-parser": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/conventional-commits-parser/-/conventional-commits-parser-5.0.0.tgz", + "integrity": "sha512-ZPMl0ZJbw74iS9LuX9YIAiW8pfM5p3yh2o/NbXHbkFuZzY5jvdi5jFycEOkmBW5H5I7nA+D6f3UcsCLP2vvSEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-text-path": "^2.0.0", + "JSONStream": "^1.3.5", + "meow": "^12.0.1", + "split2": "^4.0.0" + }, + "bin": { + "conventional-commits-parser": "cli.mjs" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/conventional-commits-parser/node_modules/meow": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz", + "integrity": "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/convert-hrtime": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/convert-hrtime/-/convert-hrtime-5.0.0.tgz", + "integrity": "sha512-lOETlkIeYSJWcbbcvjRKGxVMXJR+8+OQb/mTPbA4ObPMytYIsUbuOE0Jzy60hjARYszq1id0j8KgVhC+WGZVTg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/cosmiconfig": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", + "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/cosmiconfig-typescript-loader": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-6.2.0.tgz", + "integrity": "sha512-GEN39v7TgdxgIoNcdkRE3uiAzQt3UXLyHbRHD6YoL048XAeOomyxaP+Hh/+2C6C2wYjxJ2onhJcsQp+L4YEkVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "jiti": "^2.6.1" + }, + "engines": { + "node": ">=v18" + }, + "peerDependencies": { + "@types/node": "*", + "cosmiconfig": ">=9", + "typescript": ">=5" + } + }, + "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/crypto-random-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz", + "integrity": "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^1.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/crypto-random-string/node_modules/type-fest": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", + "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/dargs": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/dargs/-/dargs-8.1.0.tgz", + "integrity": "sha512-wAV9QHOsNbwnWdNW2FYvE1P56wtgSbM+3SZcdGiWQILwVjACCXDCI3Ai8QlCjMDB8YK5zySiXZYBiwGmNY3lnw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "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/emojilib": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/emojilib/-/emojilib-2.4.0.tgz", + "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/env-ci": { + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/env-ci/-/env-ci-11.2.0.tgz", + "integrity": "sha512-D5kWfzkmaOQDioPmiviWAVtKmpPT4/iJmMVQxWxMPJTFyTkdc5JQUfc5iXEeWxcOdsYTKSAiA/Age4NUOqKsRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "execa": "^8.0.0", + "java-properties": "^1.0.2" + }, + "engines": { + "node": "^18.17 || >=20.6.1" + } + }, + "node_modules/env-ci/node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/env-ci/node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/env-ci/node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/env-ci/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/env-ci/node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/env-ci/node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/env-ci/node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/env-ci/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/env-ci/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/env-ci/node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/error-ex": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", + "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", + "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": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/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/fast-content-type-parse": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/fast-content-type-parse/-/fast-content-type-parse-3.0.0.tgz", + "integrity": "sha512-ZvLdcY8P+N8mGQJahJV5G4U88CSvT1rP8ApL6uETe88MBXrBHAkZlSEySdUlyztF7ccb+Znos3TFqaepHxdhBg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "MIT" + }, + "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" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/figures": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-6.1.0.tgz", + "integrity": "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-unicode-supported": "^2.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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": "7.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-7.0.0.tgz", + "integrity": "sha512-YyZM99iHrqLKjmt4LJDj58KI+fYyufRLBSYcqycxf//KpBk9FoewoGX0450m9nB44qrZnovzC2oeP5hUibxc/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^7.2.0", + "path-exists": "^5.0.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "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/find-versions": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-6.0.0.tgz", + "integrity": "sha512-2kCCtc+JvcZ86IGAz3Z2Y0A1baIz9fL31pH/0S1IqZr9Iwnjq8izfPtrCyQKO6TLMPELLsQMre7VDqeIKCsHkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver-regex": "^4.0.5", + "super-regex": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha512-OMcX/4IC/uqEPVgGeyfN22LJk6AZrMkRZHxcHBMBvHScDGgwTm2GT2Wkgtocyd3JfZffjj2kYUDXXII0Fk9W0g==", + "dev": true, + "license": "MIT", + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "node_modules/fs-extra": { + "version": "11.3.3", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.3.tgz", + "integrity": "sha512-VWSRii4t0AFm6ixFFmLLx1t7wS1gh+ckoa84aOeapGum0h+EZd1EhEumSB+ZdDLnEPuucsVB9oB7cxJHap6Afg==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/function-timeout": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/function-timeout/-/function-timeout-1.0.2.tgz", + "integrity": "sha512-939eZS4gJ3htTHAldmyyuzlrD58P03fHG49v2JfFXbV6OhvZKRC9j2yAtdHw/zrp2zXHuv05zMIy40F0ge7spA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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-stream": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-7.0.1.tgz", + "integrity": "sha512-3M8C1EOFN6r8AMUhwUAACIoXZJEOufDU5+0gFFN5uNs6XYOralD2Pqkl7m046va6x77FwposWXbAhPPIOus7mQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/git-log-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/git-log-parser/-/git-log-parser-1.2.1.tgz", + "integrity": "sha512-PI+sPDvHXNPl5WNOErAK05s3j0lgwUzMN6o8cyQrDaKfT3qd7TmNJKeXX+SknI5I0QhG5fVPAEwSY4tRGDtYoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "argv-formatter": "~1.0.0", + "spawn-error-forwarder": "~1.0.0", + "split2": "~1.0.0", + "stream-combiner2": "~1.1.1", + "through2": "~2.0.0", + "traverse": "0.6.8" + } + }, + "node_modules/git-log-parser/node_modules/split2": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-1.0.0.tgz", + "integrity": "sha512-NKywug4u4pX/AZBB1FCPzZ6/7O+Xhz1qMVbzTvvKvikjO99oPN87SkK08mEY9P63/5lWjK+wgOOgApnTg5r6qg==", + "dev": true, + "license": "ISC", + "dependencies": { + "through2": "~2.0.0" + } + }, + "node_modules/git-raw-commits": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/git-raw-commits/-/git-raw-commits-4.0.0.tgz", + "integrity": "sha512-ICsMM1Wk8xSGMowkOmPrzo2Fgmfo4bMHLNX6ytHjajRJUqvHOw/TFapQ+QG75c3X/tTDDhOSRPGC52dDbNM8FQ==", + "deprecated": "This package is no longer maintained. For the JavaScript API, please use @conventional-changelog/git-client instead.", + "dev": true, + "license": "MIT", + "dependencies": { + "dargs": "^8.0.0", + "meow": "^12.0.1", + "split2": "^4.0.0" + }, + "bin": { + "git-raw-commits": "cli.mjs" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/git-raw-commits/node_modules/meow": { + "version": "12.1.1", + "resolved": "https://registry.npmjs.org/meow/-/meow-12.1.1.tgz", + "integrity": "sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16.10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/global-directory": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", + "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ini": "4.1.1" + }, + "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/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/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/hook-std": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/hook-std/-/hook-std-4.0.0.tgz", + "integrity": "sha512-IHI4bEVOt3vRUDJ+bFA9VUJlo7SzvFARPNLw75pqSmAOP2HmTWfFJtPvLBrDrlgjEYXY9zs7SFdHPQaJShkSCQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/hosted-git-info": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-7.0.2.tgz", + "integrity": "sha512-puUZAUKT5m8Zzvs72XWy3HtvVbTWljRE66cP60bxJzAqf2DgICo7lYTY2IHUmLnNpjYvw5bvmoHvPc0QO2a62w==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "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-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", + "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", + "engines": { + "node": ">=4" + } + }, + "node_modules/import-from-esm": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/import-from-esm/-/import-from-esm-2.0.0.tgz", + "integrity": "sha512-YVt14UZCgsX1vZQ3gKjkWVdBdHQ6eu3MPU1TBgL1H5orXe2+jWD006WCPPtOuwlQm10NuzOW5WawiF1Q9veW8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.3.4", + "import-meta-resolve": "^4.0.0" + }, + "engines": { + "node": ">=18.20" + } + }, + "node_modules/import-meta-resolve": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/import-meta-resolve/-/import-meta-resolve-4.2.0.tgz", + "integrity": "sha512-Iqv2fzaTQN28s/FwZAoFq0ZSs/7hMAHJVX+w8PZl3cY19Pxk6jFFalxQoIfW2826i/fDLXv8IiEZRIT0lDuWcg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/index-to-position": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", + "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/ini": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", + "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/into-stream": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-7.0.0.tgz", + "integrity": "sha512-2dYz766i9HprMBasCMvHMuazJ7u4WzhJwo5kb3iPSiW/iRYV6uPari3zHoqZlnuaR7V1bEiNMxikhp37rdBXbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "from2": "^2.3.0", + "p-is-promise": "^3.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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-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-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-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/is-text-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-text-path/-/is-text-path-2.0.0.tgz", + "integrity": "sha512-+oDTluR6WEjdXEJMnC2z6A4FRwFoYuvShVVEGsS7ewc0UTi2QtAKMDJuL4BDEVt+5T7MjFo12RP8ghOM75oKJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "text-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "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/issue-parser": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/issue-parser/-/issue-parser-7.0.1.tgz", + "integrity": "sha512-3YZcUUR2Wt1WsapF+S/WiA2WmlW0cWAoPccMqne7AxEBhCdFeTPjfv/Axb8V2gyCgY3nRw+ksZ3xSUX+R47iAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash.capitalize": "^4.2.1", + "lodash.escaperegexp": "^4.1.2", + "lodash.isplainobject": "^4.0.6", + "lodash.isstring": "^4.0.1", + "lodash.uniqby": "^4.7.0" + }, + "engines": { + "node": "^18.17 || >=20.6.1" + } + }, + "node_modules/java-properties": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/java-properties/-/java-properties-1.0.2.tgz", + "integrity": "sha512-qjdpeo2yKlYTH7nFdK0vbZWuTCesk4o63v5iVOlhMQPfuIZQfW/HI35SjfhA+4qpg36rnFSvUK5b1m+ckIblQQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/jiti": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz", + "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "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.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-parse-better-errors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", + "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", + "dev": true, + "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": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-with-bigint": { + "version": "3.5.7", + "resolved": "https://registry.npmjs.org/json-with-bigint/-/json-with-bigint-3.5.7.tgz", + "integrity": "sha512-7ei3MdAI5+fJPVnKlW77TKNKwQ5ppSzWvhPuSuINT/GYW9ZOC1eRKOuhV9yHG5aEsUPj9BBx5JIekkmoLHxZOw==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonparse": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/jsonparse/-/jsonparse-1.3.1.tgz", + "integrity": "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg==", + "dev": true, + "engines": [ + "node >= 0.2.0" + ], + "license": "MIT" + }, + "node_modules/JSONStream": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/JSONStream/-/JSONStream-1.3.5.tgz", + "integrity": "sha512-E+iruNOY8VV9s4JEbe1aNEm6MiszPRr/UfcHMz0TQh1BXSxHK+ASV1R6W4HpjBhSeS+54PIsAMCBmwD06LLsqQ==", + "dev": true, + "license": "(MIT OR Apache-2.0)", + "dependencies": { + "jsonparse": "^1.2.0", + "through": ">=2.2.7 <3" + }, + "bin": { + "JSONStream": "bin.js" + }, + "engines": { + "node": "*" + } + }, + "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/load-json-file": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", + "integrity": "sha512-Kx8hMakjX03tiGTLAIdJ+lL0htKnXjEZN6hk/tozf/WOuYGdZBJrZ+rCJRbVCugsjB3jMLn9746NsQIf5VjBMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^4.0.0", + "pify": "^3.0.0", + "strip-bom": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/load-json-file/node_modules/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-aOIos8bujGN93/8Ox/jPLh7RwVnPEysynVFE+fQZyg6jKELEHwzgKdLRFHUgXJL6kylijVSBC4BvN9OmsB48Rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "error-ex": "^1.3.1", + "json-parse-better-errors": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", + "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.capitalize": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/lodash.capitalize/-/lodash.capitalize-4.2.1.tgz", + "integrity": "sha512-kZzYOKspf8XVX5AvmQF94gQW0lejFVgb80G85bU4ZWzoJ6C03PQg3coYAUpSTpQWelrZELd3XWgHzw4Ck5kaIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.escaperegexp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.escaperegexp/-/lodash.escaperegexp-4.1.2.tgz", + "integrity": "sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.isstring": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz", + "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.kebabcase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", + "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==", + "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" + }, + "node_modules/lodash.mergewith": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", + "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.startcase": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz", + "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.uniqby": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/lodash.uniqby/-/lodash.uniqby-4.7.0.tgz", + "integrity": "sha512-e/zcLx6CSbmaEgFHCA7BnoQKyCtKMxnuWrJygbwPs/AIn+IMKl66L8/s+wBUn5LRw2pZx3bUHibiV1b6aTWIww==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.upperfirst": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz", + "integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==", + "dev": true, + "license": "MIT" + }, + "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/make-asynchronous": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/make-asynchronous/-/make-asynchronous-1.1.0.tgz", + "integrity": "sha512-ayF7iT+44LXdxJLTrTd3TLQpFDDvPCBxXxbv+pMUSuHA5Q8zyAfwkRP6aHHwNVFBUFWtxAHqwNJxF8vMZLAbVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-event": "^6.0.0", + "type-fest": "^4.6.0", + "web-worker": "^1.5.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/marked": { + "version": "15.0.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", + "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", + "dev": true, + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/marked-terminal": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/marked-terminal/-/marked-terminal-7.3.0.tgz", + "integrity": "sha512-t4rBvPsHc57uE/2nJOLmMbZCQ4tgAccAED3ngXQqW6g+TxA488JzJ+FK3lQkzBQOI1mRV/r/Kq+1ZlJ4D0owQw==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "ansi-regex": "^6.1.0", + "chalk": "^5.4.1", + "cli-highlight": "^2.1.11", + "cli-table3": "^0.6.5", + "node-emoji": "^2.2.0", + "supports-hyperlinks": "^3.1.0" + }, + "engines": { + "node": ">=16.0.0" + }, + "peerDependencies": { + "marked": ">=1 <16" + } + }, + "node_modules/meow": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz", + "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/mime": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-4.1.0.tgz", + "integrity": "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa" + ], + "license": "MIT", + "bin": { + "mime": "bin/cli.js" + }, + "engines": { + "node": ">=16" + } + }, + "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/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/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/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "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/nerf-dart": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/nerf-dart/-/nerf-dart-1.0.0.tgz", + "integrity": "sha512-EZSPZB70jiVsivaBLYDCyntd5eH8NTSMOn3rB+HxwdmKThGELLdYv8qVIMWvZEFy9w8ZZpW9h9OB32l1rGtj7g==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-emoji": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.2.0.tgz", + "integrity": "sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/is": "^4.6.0", + "char-regex": "^1.0.2", + "emojilib": "^2.4.0", + "skin-tone": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/normalize-package-data": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-6.0.2.tgz", + "integrity": "sha512-V6gygoYb/5EmNI+MEGrWkC+e6+Rr7mTmfHrxDbLzxQogBkgzo76rkok0Am6thgSF7Mv2nLOajAJj5vDJZEFn7g==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^7.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/normalize-url": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.1.tgz", + "integrity": "sha512-JYc0DPlpGWB40kH5g07gGTrYuMqV653k3uBKY6uITPWds3M0ov3GaWGp9lbE3Bzngx8+XkfzgvASb9vk9JDFXQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm": { + "version": "10.9.4", + "resolved": "https://registry.npmjs.org/npm/-/npm-10.9.4.tgz", + "integrity": "sha512-OnUG836FwboQIbqtefDNlyR0gTHzIfwRfE3DuiNewBvnMnWEpB0VEXwBlFVgqpNzIgYo/MHh3d2Hel/pszapAA==", + "bundleDependencies": [ + "@isaacs/string-locale-compare", + "@npmcli/arborist", + "@npmcli/config", + "@npmcli/fs", + "@npmcli/map-workspaces", + "@npmcli/package-json", + "@npmcli/promise-spawn", + "@npmcli/redact", + "@npmcli/run-script", + "@sigstore/tuf", + "abbrev", + "archy", + "cacache", + "chalk", + "ci-info", + "cli-columns", + "fastest-levenshtein", + "fs-minipass", + "glob", + "graceful-fs", + "hosted-git-info", + "ini", + "init-package-json", + "is-cidr", + "json-parse-even-better-errors", + "libnpmaccess", + "libnpmdiff", + "libnpmexec", + "libnpmfund", + "libnpmhook", + "libnpmorg", + "libnpmpack", + "libnpmpublish", + "libnpmsearch", + "libnpmteam", + "libnpmversion", + "make-fetch-happen", + "minimatch", + "minipass", + "minipass-pipeline", + "ms", + "node-gyp", + "nopt", + "normalize-package-data", + "npm-audit-report", + "npm-install-checks", + "npm-package-arg", + "npm-pick-manifest", + "npm-profile", + "npm-registry-fetch", + "npm-user-validate", + "p-map", + "pacote", + "parse-conflict-json", + "proc-log", + "qrcode-terminal", + "read", + "semver", + "spdx-expression-parse", + "ssri", + "supports-color", + "tar", + "text-table", + "tiny-relative-date", + "treeverse", + "validate-npm-package-name", + "which", + "write-file-atomic" + ], + "dev": true, + "license": "Artistic-2.0", + "workspaces": [ + "docs", + "smoke-tests", + "mock-globals", + "mock-registry", + "workspaces/*" + ], + "dependencies": { + "@isaacs/string-locale-compare": "^1.1.0", + "@npmcli/arborist": "^8.0.1", + "@npmcli/config": "^9.0.0", + "@npmcli/fs": "^4.0.0", + "@npmcli/map-workspaces": "^4.0.2", + "@npmcli/package-json": "^6.2.0", + "@npmcli/promise-spawn": "^8.0.2", + "@npmcli/redact": "^3.2.2", + "@npmcli/run-script": "^9.1.0", + "@sigstore/tuf": "^3.1.1", + "abbrev": "^3.0.1", + "archy": "~1.0.0", + "cacache": "^19.0.1", + "chalk": "^5.4.1", + "ci-info": "^4.2.0", + "cli-columns": "^4.0.0", + "fastest-levenshtein": "^1.0.16", + "fs-minipass": "^3.0.3", + "glob": "^10.4.5", + "graceful-fs": "^4.2.11", + "hosted-git-info": "^8.1.0", + "ini": "^5.0.0", + "init-package-json": "^7.0.2", + "is-cidr": "^5.1.1", + "json-parse-even-better-errors": "^4.0.0", + "libnpmaccess": "^9.0.0", + "libnpmdiff": "^7.0.1", + "libnpmexec": "^9.0.1", + "libnpmfund": "^6.0.1", + "libnpmhook": "^11.0.0", + "libnpmorg": "^7.0.0", + "libnpmpack": "^8.0.1", + "libnpmpublish": "^10.0.1", + "libnpmsearch": "^8.0.0", + "libnpmteam": "^7.0.0", + "libnpmversion": "^7.0.0", + "make-fetch-happen": "^14.0.3", + "minimatch": "^9.0.5", + "minipass": "^7.1.1", + "minipass-pipeline": "^1.2.4", + "ms": "^2.1.2", + "node-gyp": "^11.2.0", + "nopt": "^8.1.0", + "normalize-package-data": "^7.0.0", + "npm-audit-report": "^6.0.0", + "npm-install-checks": "^7.1.1", + "npm-package-arg": "^12.0.2", + "npm-pick-manifest": "^10.0.0", + "npm-profile": "^11.0.1", + "npm-registry-fetch": "^18.0.2", + "npm-user-validate": "^3.0.0", + "p-map": "^7.0.3", + "pacote": "^19.0.1", + "parse-conflict-json": "^4.0.0", + "proc-log": "^5.0.0", + "qrcode-terminal": "^0.12.0", + "read": "^4.1.0", + "semver": "^7.7.2", + "spdx-expression-parse": "^4.0.0", + "ssri": "^12.0.0", + "supports-color": "^9.4.0", + "tar": "^6.2.1", + "text-table": "~0.2.0", + "tiny-relative-date": "^1.3.0", + "treeverse": "^3.0.0", + "validate-npm-package-name": "^6.0.1", + "which": "^5.0.0", + "write-file-atomic": "^6.0.0" + }, + "bin": { + "npm": "bin/npm-cli.js", + "npx": "bin/npx-cli.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.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/npm/node_modules/@isaacs/cliui": { + "version": "8.0.2", + "dev": true, + "inBundle": 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/npm/node_modules/@isaacs/cliui/node_modules/ansi-regex": { + "version": "6.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/emoji-regex": { + "version": "9.2.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/@isaacs/cliui/node_modules/string-width": { + "version": "5.1.2", + "dev": true, + "inBundle": 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/npm/node_modules/@isaacs/cliui/node_modules/strip-ansi": { + "version": "7.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/npm/node_modules/@isaacs/fs-minipass": { + "version": "4.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.4" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/npm/node_modules/@isaacs/string-locale-compare": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/@npmcli/agent": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/arborist": { + "version": "8.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@isaacs/string-locale-compare": "^1.1.0", + "@npmcli/fs": "^4.0.0", + "@npmcli/installed-package-contents": "^3.0.0", + "@npmcli/map-workspaces": "^4.0.1", + "@npmcli/metavuln-calculator": "^8.0.0", + "@npmcli/name-from-folder": "^3.0.0", + "@npmcli/node-gyp": "^4.0.0", + "@npmcli/package-json": "^6.0.1", + "@npmcli/query": "^4.0.0", + "@npmcli/redact": "^3.0.0", + "@npmcli/run-script": "^9.0.1", + "bin-links": "^5.0.0", + "cacache": "^19.0.1", + "common-ancestor-path": "^1.0.1", + "hosted-git-info": "^8.0.0", + "json-parse-even-better-errors": "^4.0.0", + "json-stringify-nice": "^1.1.4", + "lru-cache": "^10.2.2", + "minimatch": "^9.0.4", + "nopt": "^8.0.0", + "npm-install-checks": "^7.1.0", + "npm-package-arg": "^12.0.0", + "npm-pick-manifest": "^10.0.0", + "npm-registry-fetch": "^18.0.1", + "pacote": "^19.0.0", + "parse-conflict-json": "^4.0.0", + "proc-log": "^5.0.0", + "proggy": "^3.0.0", + "promise-all-reject-late": "^1.0.0", + "promise-call-limit": "^3.0.1", + "read-package-json-fast": "^4.0.0", + "semver": "^7.3.7", + "ssri": "^12.0.0", + "treeverse": "^3.0.0", + "walk-up-path": "^3.0.1" + }, + "bin": { + "arborist": "bin/index.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/config": { + "version": "9.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/map-workspaces": "^4.0.1", + "@npmcli/package-json": "^6.0.1", + "ci-info": "^4.0.0", + "ini": "^5.0.0", + "nopt": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "walk-up-path": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/fs": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/git": { + "version": "6.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/promise-spawn": "^8.0.0", + "ini": "^5.0.0", + "lru-cache": "^10.0.1", + "npm-pick-manifest": "^10.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "semver": "^7.3.5", + "which": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/installed-package-contents": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-bundled": "^4.0.0", + "npm-normalize-package-bin": "^4.0.0" + }, + "bin": { + "installed-package-contents": "bin/index.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/map-workspaces": { + "version": "4.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/name-from-folder": "^3.0.0", + "@npmcli/package-json": "^6.0.0", + "glob": "^10.2.2", + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/metavuln-calculator": { + "version": "8.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "cacache": "^19.0.0", + "json-parse-even-better-errors": "^4.0.0", + "pacote": "^20.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/metavuln-calculator/node_modules/pacote": { + "version": "20.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^6.0.0", + "@npmcli/installed-package-contents": "^3.0.0", + "@npmcli/package-json": "^6.0.0", + "@npmcli/promise-spawn": "^8.0.0", + "@npmcli/run-script": "^9.0.0", + "cacache": "^19.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^12.0.0", + "npm-packlist": "^9.0.0", + "npm-pick-manifest": "^10.0.0", + "npm-registry-fetch": "^18.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "sigstore": "^3.0.0", + "ssri": "^12.0.0", + "tar": "^6.1.11" + }, + "bin": { + "pacote": "bin/index.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/name-from-folder": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/node-gyp": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/package-json": { + "version": "6.2.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^6.0.0", + "glob": "^10.2.2", + "hosted-git-info": "^8.0.0", + "json-parse-even-better-errors": "^4.0.0", + "proc-log": "^5.0.0", + "semver": "^7.5.3", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/promise-spawn": { + "version": "8.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "which": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/query": { + "version": "4.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/redact": { + "version": "3.2.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@npmcli/run-script": { + "version": "9.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/node-gyp": "^4.0.0", + "@npmcli/package-json": "^6.0.0", + "@npmcli/promise-spawn": "^8.0.0", + "node-gyp": "^11.0.0", + "proc-log": "^5.0.0", + "which": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/npm/node_modules/@sigstore/protobuf-specs": { + "version": "0.4.3", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@sigstore/tuf": { + "version": "3.1.1", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.4.1", + "tuf-js": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/@tufjs/canonical-json": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/abbrev": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/agent-base": { + "version": "7.1.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/npm/node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/ansi-styles": { + "version": "6.2.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/npm/node_modules/aproba": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/archy": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/balanced-match": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/bin-links": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "cmd-shim": "^7.0.0", + "npm-normalize-package-bin": "^4.0.0", + "proc-log": "^5.0.0", + "read-cmd-shim": "^5.0.0", + "write-file-atomic": "^6.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/binary-extensions": { + "version": "2.3.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/brace-expansion": { + "version": "2.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/npm/node_modules/cacache": { + "version": "19.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/fs": "^4.0.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^7.0.2", + "ssri": "^12.0.0", + "tar": "^7.4.3", + "unique-filename": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/cacache/node_modules/chownr": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/npm/node_modules/cacache/node_modules/mkdirp": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/cacache/node_modules/tar": { + "version": "7.4.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/npm/node_modules/cacache/node_modules/yallist": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/npm/node_modules/chalk": { + "version": "5.4.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/npm/node_modules/chownr": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/ci-info": { + "version": "4.2.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/cidr-regex": { + "version": "4.1.3", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "ip-regex": "^5.0.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/npm/node_modules/cli-columns": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/npm/node_modules/cmd-shim": { + "version": "7.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/npm/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/common-ancestor-path": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/cross-spawn": { + "version": "7.0.6", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/cross-spawn/node_modules/which": { + "version": "2.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/cssesc": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/debug": { + "version": "4.4.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/npm/node_modules/diff": { + "version": "5.2.0", + "dev": true, + "inBundle": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/npm/node_modules/eastasianwidth": { + "version": "0.2.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/encoding": { + "version": "0.1.13", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/npm/node_modules/env-paths": { + "version": "2.2.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/npm/node_modules/err-code": { + "version": "2.0.3", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/exponential-backoff": { + "version": "3.1.2", + "dev": true, + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/npm/node_modules/fastest-levenshtein": { + "version": "1.0.16", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/npm/node_modules/foreground-child": { + "version": "3.3.1", + "dev": true, + "inBundle": 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/npm/node_modules/fs-minipass": { + "version": "3.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/glob": { + "version": "10.4.5", + "dev": true, + "inBundle": 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/npm/node_modules/graceful-fs": { + "version": "4.2.11", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/hosted-git-info": { + "version": "8.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/http-cache-semantics": { + "version": "4.2.0", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause" + }, + "node_modules/npm/node_modules/http-proxy-agent": { + "version": "7.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/npm/node_modules/https-proxy-agent": { + "version": "7.0.6", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/npm/node_modules/iconv-lite": { + "version": "0.6.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm/node_modules/ignore-walk": { + "version": "7.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minimatch": "^9.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/imurmurhash": { + "version": "0.1.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/npm/node_modules/ini": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/init-package-json": { + "version": "7.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/package-json": "^6.0.0", + "npm-package-arg": "^12.0.0", + "promzard": "^2.0.0", + "read": "^4.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4", + "validate-npm-package-name": "^6.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/ip-address": { + "version": "9.0.5", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/npm/node_modules/ip-regex": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/is-cidr": { + "version": "5.1.1", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "cidr-regex": "^4.1.1" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/npm/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/isexe": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/jackspeak": { + "version": "3.4.3", + "dev": true, + "inBundle": 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/npm/node_modules/jsbn": { + "version": "1.1.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/json-parse-even-better-errors": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/json-stringify-nice": { + "version": "1.1.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/jsonparse": { + "version": "1.3.1", + "dev": true, + "engines": [ + "node >= 0.2.0" + ], + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/just-diff": { + "version": "6.0.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/just-diff-apply": { + "version": "5.5.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/libnpmaccess": { + "version": "9.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-package-arg": "^12.0.0", + "npm-registry-fetch": "^18.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/libnpmdiff": { + "version": "7.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^8.0.1", + "@npmcli/installed-package-contents": "^3.0.0", + "binary-extensions": "^2.3.0", + "diff": "^5.1.0", + "minimatch": "^9.0.4", + "npm-package-arg": "^12.0.0", + "pacote": "^19.0.0", + "tar": "^6.2.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/libnpmexec": { + "version": "9.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^8.0.1", + "@npmcli/run-script": "^9.0.1", + "ci-info": "^4.0.0", + "npm-package-arg": "^12.0.0", + "pacote": "^19.0.0", + "proc-log": "^5.0.0", + "read": "^4.0.0", + "read-package-json-fast": "^4.0.0", + "semver": "^7.3.7", + "walk-up-path": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/libnpmfund": { + "version": "6.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^8.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/libnpmhook": { + "version": "11.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^18.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/libnpmorg": { + "version": "7.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^18.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/libnpmpack": { + "version": "8.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/arborist": "^8.0.1", + "@npmcli/run-script": "^9.0.1", + "npm-package-arg": "^12.0.0", + "pacote": "^19.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/libnpmpublish": { + "version": "10.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "ci-info": "^4.0.0", + "normalize-package-data": "^7.0.0", + "npm-package-arg": "^12.0.0", + "npm-registry-fetch": "^18.0.1", + "proc-log": "^5.0.0", + "semver": "^7.3.7", + "sigstore": "^3.0.0", + "ssri": "^12.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/libnpmsearch": { + "version": "8.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-registry-fetch": "^18.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/libnpmteam": { + "version": "7.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "aproba": "^2.0.0", + "npm-registry-fetch": "^18.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/libnpmversion": { + "version": "7.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^6.0.1", + "@npmcli/run-script": "^9.0.1", + "json-parse-even-better-errors": "^4.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.7" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/lru-cache": { + "version": "10.4.3", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/make-fetch-happen": { + "version": "14.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/agent": "^3.0.0", + "cacache": "^19.0.1", + "http-cache-semantics": "^4.1.1", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^1.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "ssri": "^12.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/make-fetch-happen/node_modules/negotiator": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/npm/node_modules/minimatch": { + "version": "9.0.5", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/minipass": { + "version": "7.1.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/npm/node_modules/minipass-collect": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/npm/node_modules/minipass-fetch": { + "version": "4.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^3.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/npm/node_modules/minipass-flush": { + "version": "1.0.5", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-pipeline": { + "version": "1.2.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-sized": { + "version": "1.0.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/minizlib": { + "version": "3.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^7.1.2" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/npm/node_modules/mkdirp": { + "version": "1.0.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/ms": { + "version": "2.1.3", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/mute-stream": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/node-gyp": { + "version": "11.2.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^14.0.3", + "nopt": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "tar": "^7.4.3", + "tinyglobby": "^0.2.12", + "which": "^5.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/chownr": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/mkdirp": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "bin": { + "mkdirp": "dist/cjs/src/bin.js" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/tar": { + "version": "7.4.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@isaacs/fs-minipass": "^4.0.0", + "chownr": "^3.0.0", + "minipass": "^7.1.2", + "minizlib": "^3.0.1", + "mkdirp": "^3.0.1", + "yallist": "^5.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/npm/node_modules/node-gyp/node_modules/yallist": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/npm/node_modules/nopt": { + "version": "8.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "abbrev": "^3.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/normalize-package-data": { + "version": "7.0.0", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "hosted-git-info": "^8.0.0", + "semver": "^7.3.5", + "validate-npm-package-license": "^3.0.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-audit-report": { + "version": "6.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-bundled": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-normalize-package-bin": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-install-checks": { + "version": "7.1.1", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "dependencies": { + "semver": "^7.1.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-normalize-package-bin": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-package-arg": { + "version": "12.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "hosted-git-info": "^8.0.0", + "proc-log": "^5.0.0", + "semver": "^7.3.5", + "validate-npm-package-name": "^6.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-packlist": { + "version": "9.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "ignore-walk": "^7.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-pick-manifest": { + "version": "10.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-install-checks": "^7.1.0", + "npm-normalize-package-bin": "^4.0.0", + "npm-package-arg": "^12.0.0", + "semver": "^7.3.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-profile": { + "version": "11.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "npm-registry-fetch": "^18.0.0", + "proc-log": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-registry-fetch": { + "version": "18.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/redact": "^3.0.0", + "jsonparse": "^1.3.1", + "make-fetch-happen": "^14.0.0", + "minipass": "^7.0.2", + "minipass-fetch": "^4.0.0", + "minizlib": "^3.0.1", + "npm-package-arg": "^12.0.0", + "proc-log": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/npm-user-validate": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "BSD-2-Clause", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/p-map": { + "version": "7.0.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm/node_modules/package-json-from-dist": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "BlueOak-1.0.0" + }, + "node_modules/npm/node_modules/pacote": { + "version": "19.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "@npmcli/git": "^6.0.0", + "@npmcli/installed-package-contents": "^3.0.0", + "@npmcli/package-json": "^6.0.0", + "@npmcli/promise-spawn": "^8.0.0", + "@npmcli/run-script": "^9.0.0", + "cacache": "^19.0.0", + "fs-minipass": "^3.0.0", + "minipass": "^7.0.2", + "npm-package-arg": "^12.0.0", + "npm-packlist": "^9.0.0", + "npm-pick-manifest": "^10.0.0", + "npm-registry-fetch": "^18.0.0", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1", + "sigstore": "^3.0.0", + "ssri": "^12.0.0", + "tar": "^6.1.11" + }, + "bin": { + "pacote": "bin/index.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/parse-conflict-json": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^4.0.0", + "just-diff": "^6.0.0", + "just-diff-apply": "^5.2.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/path-key": { + "version": "3.1.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/path-scurry": { + "version": "1.11.1", + "dev": true, + "inBundle": 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/npm/node_modules/postcss-selector-parser": { + "version": "7.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm/node_modules/proc-log": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/proggy": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/promise-all-reject-late": { + "version": "1.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/promise-call-limit": { + "version": "3.0.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/promise-retry": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/promzard": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "read": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/qrcode-terminal": { + "version": "0.12.0", + "dev": true, + "inBundle": true, + "bin": { + "qrcode-terminal": "bin/qrcode-terminal.js" + } + }, + "node_modules/npm/node_modules/read": { + "version": "4.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "mute-stream": "^2.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/read-cmd-shim": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/read-package-json-fast": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "json-parse-even-better-errors": "^4.0.0", + "npm-normalize-package-bin": "^4.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/retry": { + "version": "0.12.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/npm/node_modules/safer-buffer": { + "version": "2.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "optional": true + }, + "node_modules/npm/node_modules/semver": { + "version": "7.7.2", + "dev": true, + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/shebang-command": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/shebang-regex": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/signal-exit": { + "version": "4.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/npm/node_modules/sigstore": { + "version": "3.1.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^3.1.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.4.0", + "@sigstore/sign": "^3.1.0", + "@sigstore/tuf": "^3.1.0", + "@sigstore/verify": "^2.1.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/sigstore/node_modules/@sigstore/bundle": { + "version": "3.1.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/protobuf-specs": "^0.4.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/sigstore/node_modules/@sigstore/core": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/sigstore/node_modules/@sigstore/sign": { + "version": "3.1.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^3.1.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.4.0", + "make-fetch-happen": "^14.0.2", + "proc-log": "^5.0.0", + "promise-retry": "^2.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/sigstore/node_modules/@sigstore/verify": { + "version": "2.1.1", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "@sigstore/bundle": "^3.1.0", + "@sigstore/core": "^2.0.0", + "@sigstore/protobuf-specs": "^0.4.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/smart-buffer": { + "version": "4.2.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/npm/node_modules/socks": { + "version": "2.8.5", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/npm/node_modules/socks-proxy-agent": { + "version": "8.0.5", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/npm/node_modules/spdx-correct": { + "version": "3.2.0", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/spdx-correct/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/spdx-exceptions": { + "version": "2.5.0", + "dev": true, + "inBundle": true, + "license": "CC-BY-3.0" + }, + "node_modules/npm/node_modules/spdx-expression-parse": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/spdx-license-ids": { + "version": "3.0.21", + "dev": true, + "inBundle": true, + "license": "CC0-1.0" + }, + "node_modules/npm/node_modules/sprintf-js": { + "version": "1.1.3", + "dev": true, + "inBundle": true, + "license": "BSD-3-Clause" + }, + "node_modules/npm/node_modules/ssri": { + "version": "12.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/string-width": { + "version": "4.2.3", + "dev": true, + "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/npm/node_modules/string-width-cjs": { + "name": "string-width", + "version": "4.2.3", + "dev": true, + "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/npm/node_modules/strip-ansi": { + "version": "6.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/strip-ansi-cjs": { + "name": "strip-ansi", + "version": "6.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/supports-color": { + "version": "9.4.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/npm/node_modules/tar": { + "version": "6.2.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm/node_modules/tar/node_modules/fs-minipass": { + "version": "2.1.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/tar/node_modules/minizlib": { + "version": "2.1.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/npm/node_modules/tar/node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm/node_modules/text-table": { + "version": "0.2.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/tiny-relative-date": { + "version": "1.3.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/tinyglobby": { + "version": "0.2.14", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/npm/node_modules/tinyglobby/node_modules/fdir": { + "version": "6.4.6", + "dev": true, + "inBundle": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/npm/node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/npm/node_modules/treeverse": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/npm/node_modules/tuf-js": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "@tufjs/models": "3.0.1", + "debug": "^4.3.6", + "make-fetch-happen": "^14.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/tuf-js/node_modules/@tufjs/models": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "@tufjs/canonical-json": "2.0.0", + "minimatch": "^9.0.5" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/unique-filename": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "unique-slug": "^5.0.0" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/unique-slug": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/util-deprecate": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/validate-npm-package-license": { + "version": "3.0.4", + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/npm/node_modules/validate-npm-package-license/node_modules/spdx-expression-parse": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/npm/node_modules/validate-npm-package-name": { + "version": "6.0.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/walk-up-path": { + "version": "3.0.1", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/npm/node_modules/which": { + "version": "5.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/which/node_modules/isexe": { + "version": "3.1.1", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">=16" + } + }, + "node_modules/npm/node_modules/wrap-ansi": { + "version": "8.1.0", + "dev": true, + "inBundle": 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/npm/node_modules/wrap-ansi-cjs": { + "name": "wrap-ansi", + "version": "7.0.0", + "dev": true, + "inBundle": 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/npm/node_modules/wrap-ansi-cjs/node_modules/ansi-styles": { + "version": "4.3.0", + "dev": true, + "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/npm/node_modules/wrap-ansi/node_modules/ansi-regex": { + "version": "6.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "9.2.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/npm/node_modules/wrap-ansi/node_modules/string-width": { + "version": "5.1.2", + "dev": true, + "inBundle": 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/npm/node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "7.1.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/npm/node_modules/write-file-atomic": { + "version": "6.0.0", + "dev": true, + "inBundle": true, + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/npm/node_modules/yallist": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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-each-series": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-3.0.0.tgz", + "integrity": "sha512-lastgtAdoH9YaLyDa5i5z64q+kzOcQHsQ5SsZJD3q0VEyI8mq872S3geuNbRUQLVAE9siMfgKrpj7MloKFHruw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-event": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/p-event/-/p-event-6.0.1.tgz", + "integrity": "sha512-Q6Bekk5wpzW5qIyUP4gdMEujObYstZl6DMMOSenwBvV0BlE5LkDwkjs5yHbZmdCEq2o4RJx4tE1vwxFVf2FG1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-timeout": "^6.1.2" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-filter": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-filter/-/p-filter-4.1.0.tgz", + "integrity": "sha512-37/tPdZ3oJwHaS3gNJdenCDB3Tz26i9sjhnguBtvN0vYlRIiDNnvTWkuh+0hETV9rLPdJ3rlL3yVOYPIAnM8rw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-map": "^7.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-is-promise": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-3.0.0.tgz", + "integrity": "sha512-Wo8VsW4IRQSKVXsJCn7TomUaVtyfjVDn3nUP7kE967BQk0CwFpdbZs0X0uk5sW9mkBa9eNM7hCMaG93WUAwxYQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.4.tgz", + "integrity": "sha512-tkAQEw8ysMzmkhgw8k+1U/iPhWNhykKnSk4Rd5zLoPJCuJaGRPo6YposrZgaxHKzDHdDWWZvE/Sk7hsL2X/CpQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-reduce": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-2.1.0.tgz", + "integrity": "sha512-2USApvnsutq8uoxZBGbbWM0JIYLiEMJ9RlaN7fAzVNb9OZN0SHjjTTfIcb667XynS5Y1VhwDJVDa72TnPzAYWw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/p-timeout": { + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-6.1.4.tgz", + "integrity": "sha512-MyIV3ZA/PmyBN/ud8vV9XzwTrNtR4jFrObymZYnZqMmW0zA8Z17vnT0rBgFE/TlohB+YCHqXMgZzb3Csp49vqg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-try": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", + "integrity": "sha512-U1etNYuMJoIz3ZXSrrySFjsXQTWOx2/jdi86L+2pRvph/qMKL6sbcCYdH23fqsbm8TH2Gn0OybpT4eSFlCVHww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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", + "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/parse-ms": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/parse-ms/-/parse-ms-4.0.0.tgz", + "integrity": "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse5": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", + "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^6.0.1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.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-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/pkg-conf": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/pkg-conf/-/pkg-conf-2.1.0.tgz", + "integrity": "sha512-C+VUP+8jis7EsQZIhDYmS5qlNtjv2yP4SNtjXK9AP1ZcTRlnSfuumaTnRfYZnYgUUYVIKqL0fRvmUGDV2fmp6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^2.0.0", + "load-json-file": "^4.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pkg-conf/node_modules/find-up": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", + "integrity": "sha512-NWzkk0jSJtTt08+FBFMvXoeZnOJD+jTtsRmBYbAIzJdX6l7dLgR7CTubCM5/eDdPUBvLCeVasP1brfVR/9/EZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pkg-conf/node_modules/locate-path": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", + "integrity": "sha512-NCI2kiDkyR7VeEKm27Kda/iQHyKJe1Bu0FlTbYp3CqJu+9IFe9bLyAjMxf5ZDDbEg+iMPzB5zYyUTSm8wVTKmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^2.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pkg-conf/node_modules/p-limit": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", + "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pkg-conf/node_modules/p-locate": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", + "integrity": "sha512-nQja7m7gSKuewoVRen45CtVfODR3crN3goVQ0DDZ9N3yHxgpkuBhZqsaiotSQRrADUrne346peY7kT3TSACykg==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pkg-conf/node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/pretty-ms": { + "version": "9.3.0", + "resolved": "https://registry.npmjs.org/pretty-ms/-/pretty-ms-9.3.0.tgz", + "integrity": "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-ms": "^4.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "dev": true, + "license": "MIT" + }, + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", + "dev": true, + "license": "ISC" + }, + "node_modules/rc": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", + "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, + "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~1.3.0", + "minimist": "^1.2.0", + "strip-json-comments": "~2.0.1" + }, + "bin": { + "rc": "cli.js" + } + }, + "node_modules/rc/node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, + "license": "ISC" + }, + "node_modules/read-package-up": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-11.0.0.tgz", + "integrity": "sha512-MbgfoNPANMdb4oRBNg5eqLbB2t2r+o5Ua1pNt8BqGp4I0FJZhuVSOj3PaBPni4azWuSzEdNn2evevzVmEk1ohQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up-simple": "^1.0.0", + "read-pkg": "^9.0.0", + "type-fest": "^4.6.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-9.0.1.tgz", + "integrity": "sha512-9viLL4/n1BJUCT1NXVTdS1jtm80yDEgR5T4yCelII49Mbj0v1rZdKqj7zCiYdbB0CuCgdrvHcNogAKTFPBocFA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/normalize-package-data": "^2.4.3", + "normalize-package-data": "^6.0.0", + "parse-json": "^8.0.0", + "type-fest": "^4.6.0", + "unicorn-magic": "^0.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/read-pkg/node_modules/parse-json": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/registry-auth-token": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.1.1.tgz", + "integrity": "sha512-P7B4+jq8DeD2nMsAcdfaqHbssgHtZ7Z5+++a5ask90fvmJ8p5je4mOa+wzu+DB4vQ5tdJV/xywY+UnVFeQLV5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pnpm/npm-conf": "^3.0.2" + }, + "engines": { + "node": ">=14" + } + }, + "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/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "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/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/semantic-release": { + "version": "24.2.9", + "resolved": "https://registry.npmjs.org/semantic-release/-/semantic-release-24.2.9.tgz", + "integrity": "sha512-phCkJ6pjDi9ANdhuF5ElS10GGdAKY6R1Pvt9lT3SFhOwM4T7QZE7MLpBDbNruUx/Q3gFD92/UOFringGipRqZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@semantic-release/commit-analyzer": "^13.0.0-beta.1", + "@semantic-release/error": "^4.0.0", + "@semantic-release/github": "^11.0.0", + "@semantic-release/npm": "^12.0.2", + "@semantic-release/release-notes-generator": "^14.0.0-beta.1", + "aggregate-error": "^5.0.0", + "cosmiconfig": "^9.0.0", + "debug": "^4.0.0", + "env-ci": "^11.0.0", + "execa": "^9.0.0", + "figures": "^6.0.0", + "find-versions": "^6.0.0", + "get-stream": "^6.0.0", + "git-log-parser": "^1.2.0", + "hook-std": "^4.0.0", + "hosted-git-info": "^8.0.0", + "import-from-esm": "^2.0.0", + "lodash-es": "^4.17.21", + "marked": "^15.0.0", + "marked-terminal": "^7.3.0", + "micromatch": "^4.0.2", + "p-each-series": "^3.0.0", + "p-reduce": "^3.0.0", + "read-package-up": "^11.0.0", + "resolve-from": "^5.0.0", + "semver": "^7.3.2", + "semver-diff": "^5.0.0", + "signale": "^1.2.1", + "yargs": "^17.5.1" + }, + "bin": { + "semantic-release": "bin/semantic-release.js" + }, + "engines": { + "node": ">=20.8.1" + } + }, + "node_modules/semantic-release/node_modules/@semantic-release/error": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@semantic-release/error/-/error-4.0.0.tgz", + "integrity": "sha512-mgdxrHTLOjOddRVYIYDo0fR3/v61GNN1YGkfbrjuIKg/uMgCd+Qzo3UAXJ+woLQQpos4pl5Esuw5A7AoNlzjUQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/semantic-release/node_modules/aggregate-error": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-5.0.0.tgz", + "integrity": "sha512-gOsf2YwSlleG6IjRYG2A7k0HmBMEo6qVNk9Bp/EaLgAJT5ngH6PXbqa4ItvnEwCm/velL5jAnQgsHsWnjhGmvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "clean-stack": "^5.2.0", + "indent-string": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semantic-release/node_modules/clean-stack": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-5.3.0.tgz", + "integrity": "sha512-9ngPTOhYGQqNVSfeJkYXHmF7AGWp4/nN5D/QqNQs3Dvxd1Kk/WpjHfNujKHYUQ/5CoGyOyFNoWSPk5afzP0QVg==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "5.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semantic-release/node_modules/execa": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-9.6.1.tgz", + "integrity": "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sindresorhus/merge-streams": "^4.0.0", + "cross-spawn": "^7.0.6", + "figures": "^6.1.0", + "get-stream": "^9.0.0", + "human-signals": "^8.0.1", + "is-plain-obj": "^4.1.0", + "is-stream": "^4.0.1", + "npm-run-path": "^6.0.0", + "pretty-ms": "^9.2.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^4.0.0", + "yoctocolors": "^2.1.1" + }, + "engines": { + "node": "^18.19.0 || >=20.5.0" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/semantic-release/node_modules/execa/node_modules/get-stream": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-9.0.1.tgz", + "integrity": "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sec-ant/readable-stream": "^0.4.1", + "is-stream": "^4.0.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semantic-release/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/semantic-release/node_modules/hosted-git-info": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-8.1.0.tgz", + "integrity": "sha512-Rw/B2DNQaPBICNXEm8balFz9a6WpZrkCGpcWFpy7nCj+NyhSdqXipmfvtmWt9xGfp0wZnBxB+iVpLmQMYt47Tw==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^10.0.1" + }, + "engines": { + "node": "^18.17.0 || >=20.5.0" + } + }, + "node_modules/semantic-release/node_modules/human-signals": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-8.0.1.tgz", + "integrity": "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/semantic-release/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/semantic-release/node_modules/is-stream": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-4.0.1.tgz", + "integrity": "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semantic-release/node_modules/npm-run-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-6.0.0.tgz", + "integrity": "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0", + "unicorn-magic": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semantic-release/node_modules/p-reduce": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-3.0.0.tgz", + "integrity": "sha512-xsrIUgI0Kn6iyDYm9StOpOeK29XM1aboGji26+QEortiFST1hGZaUQOLhtEbqHErPpGW/aSz6allwK2qcptp0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semantic-release/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semantic-release/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/semantic-release/node_modules/strip-final-newline": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-4.0.0.tgz", + "integrity": "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semantic-release/node_modules/unicorn-magic": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.3.0.tgz", + "integrity": "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver-diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-5.0.0.tgz", + "integrity": "sha512-0HbGtOm+S7T6NGQ/pxJSJipJvc4DK3FcRVMRkhsIwJDJ4Jcz5DQC1cPPzB5GhzyHjwttW878HaWQq46CkL3cqg==", + "deprecated": "Deprecated as the semver package now supports this built-in.", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/semver-regex": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-4.0.5.tgz", + "integrity": "sha512-hunMQrEy1T6Jr2uEVjrAIqjwWcQTgOAcIM52C8MY1EZSD3DDNft04XzvYKPqjED65bNVVko0YI38nYeEHCX3yw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "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/signale": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/signale/-/signale-1.4.0.tgz", + "integrity": "sha512-iuh+gPf28RkltuJC7W5MRi6XAjTDCAPC/prJUpQoG4vIP3MJZ+GTydVnodXA7pwvTKb2cA0m9OFZW/cdWy/I/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^2.3.2", + "figures": "^2.0.0", + "pkg-conf": "^2.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/signale/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/signale/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/signale/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/signale/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/signale/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/signale/node_modules/figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/signale/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/signale/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/skin-tone": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/skin-tone/-/skin-tone-2.0.0.tgz", + "integrity": "sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "unicode-emoji-modifier-base": "^1.0.0" + }, + "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/spawn-error-forwarder": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/spawn-error-forwarder/-/spawn-error-forwarder-1.0.0.tgz", + "integrity": "sha512-gRjMgK5uFjbCvdibeGJuy3I5OYz6VLoVdsOJdA6wV0WlfQVLFueoqMxwwYD9RODdgb6oUIvlRlsyFSiQkMKu0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/spdx-correct": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", + "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-exceptions": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", + "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==", + "dev": true, + "license": "CC-BY-3.0" + }, + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/spdx-license-ids": { + "version": "3.0.23", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.23.tgz", + "integrity": "sha512-CWLcCCH7VLu13TgOH+r8p1O/Znwhqv/dbb6lqWy67G+pT1kHmeD/+V36AVb/vq8QMIQwVShJ6Ssl5FPh0fuSdw==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">= 10.x" + } + }, + "node_modules/stream-combiner2": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/stream-combiner2/-/stream-combiner2-1.1.1.tgz", + "integrity": "sha512-3PnJbYgS56AeWgtKF5jtJRT6uFJe56Z0Hc5Ngg/6sI6rIt8iiMBTa9cvdyFfpMQjaVHr8dusbNeFGIIonxOvKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "duplexer2": "~0.1.0", + "readable-stream": "^2.0.2" + } + }, + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "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-ansi/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": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "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": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/super-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/super-regex/-/super-regex-1.1.0.tgz", + "integrity": "sha512-WHkws2ZflZe41zj6AolvvmaTrWds/VuyeYr9iPVv/oQeaIoVxMKaushfFWpOGDT+GuBrM/sVqF8KUCYQlSSTdQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-timeout": "^1.0.1", + "make-asynchronous": "^1.0.1", + "time-span": "^5.1.0" + }, + "engines": { + "node": ">=18" + }, + "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/supports-hyperlinks": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.2.0.tgz", + "integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=14.18" + }, + "funding": { + "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" + } + }, + "node_modules/temp-dir": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-3.0.0.tgz", + "integrity": "sha512-nHc6S/bwIilKHNRgK/3jlhDoIHcp45YgyiwcAk46Tr0LfEqGBVpmiAyuiuxeVE44m3mXnEeVhaipLOEWmH+Njw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.16" + } + }, + "node_modules/tempy": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/tempy/-/tempy-3.2.0.tgz", + "integrity": "sha512-d79HhZya5Djd7am0q+W4RTsSU+D/aJzM+4Y4AGJGuGlgM2L6sx5ZvOYTmZjqPhrDrV6xJTtRSm1JCLj6V6LHLQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-stream": "^3.0.0", + "temp-dir": "^3.0.0", + "type-fest": "^2.12.2", + "unique-string": "^3.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tempy/node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tempy/node_modules/type-fest": { + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", + "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/text-extensions": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/text-extensions/-/text-extensions-2.4.0.tgz", + "integrity": "sha512-te/NtwBwfiNRLf9Ijqx3T0nlqZiQ2XrrtBvu+cLL8ZRrGkO0NHTug8MYFKyoSrv/sHTaSKfilUkizV6XhxMJ3g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, + "node_modules/time-span": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/time-span/-/time-span-5.1.0.tgz", + "integrity": "sha512-75voc/9G4rDIJleOo4jPvN4/YC4GRZrY8yy1uU4lwrB3XEQbWve8zXoO5No4eFrGcTAMYyoY67p8jRQdtA1HbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "convert-hrtime": "^5.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/tinyexec": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.2.tgz", + "integrity": "sha512-W/KYk+NFhkmsYpuHq5JykngiOCnxeVL8v8dFnqxSD8qEEdRfXk1SDM6JzNqcERbcGYj9tMrDQBYV9cjgnunFIg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/tinyglobby/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/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/traverse": { + "version": "0.6.8", + "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.8.tgz", + "integrity": "sha512-aXJDbk6SnumuaZSANd21XAo15ucCDE38H4fkqiGsc3MhCK+wOlZvLP9cB/TvpHT0mOyWgC4Z8EwRlzqYSUzdsA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "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/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "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.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/unicode-emoji-modifier-base": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unicode-emoji-modifier-base/-/unicode-emoji-modifier-base-1.0.0.tgz", + "integrity": "sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/unicorn-magic": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", + "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/unique-string": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", + "integrity": "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "crypto-random-string": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/universal-user-agent": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/universal-user-agent/-/universal-user-agent-7.0.3.tgz", + "integrity": "sha512-TmnEAEAsBJVZM/AADELsK76llnwcf9vMKuPz8JflO1frO8Lchitr0fNaN9d+Ap0BjKtqWqd/J17qeDnXh8CL2A==", + "dev": true, + "license": "ISC" + }, + "node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/url-join": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/url-join/-/url-join-5.0.0.tgz", + "integrity": "sha512-n2huDr9h9yzd6exQVnH/jU5mr+Pfx08LRXXZhkLLetAMESRj+anQsTAh940iMrIetKAmry9coFuZQ2jY8/p3WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/validate-npm-package-license": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", + "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "spdx-correct": "^3.0.0", + "spdx-expression-parse": "^3.0.0" + } + }, + "node_modules/web-worker": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/web-worker/-/web-worker-1.5.0.tgz", + "integrity": "sha512-RiMReJrTAiA+mBjGONMnjVDP2u3p9R1vkcGz6gDIrOMT3oGuYwX2WRMYI9ipkphSuE5XKEhydbhNEJh4NY9mlw==", + "dev": true, + "license": "Apache-2.0" + }, + "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/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": "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/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.4" + } + }, + "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/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": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/.github/package.json similarity index 100% rename from package.json rename to .github/package.json diff --git a/.github/scripts/backend-test.sh b/.github/scripts/backend-test.sh new file mode 100755 index 0000000..8064650 --- /dev/null +++ b/.github/scripts/backend-test.sh @@ -0,0 +1,44 @@ +#!/usr/bin/env bash +############################################################################### +# backend-test.sh — Backend lint and full test suite +# +# Runs four sequential checks against the backend Python package: +# 1. ruff check PEP 8 / import / style linting +# 2. ruff format --check formatting diff (non-destructive, fails if dirty) +# 3. pytest unit fast unit tests (no DB) with coverage report +# 4. pytest integration database-backed integration tests with coverage +# +# JUnit XML results are written to: +# backend/test-results-unit.xml +# backend/test-results-integration.xml +# These are picked up by the publish-test-results composite action. +# +# Usage: +# cd backend && bash ../.github/scripts/backend-test.sh +# +# Expected CWD: backend/ +# Dependencies: poetry (installs ruff, pytest, pytest-cov) +# Caller(s): ci.yml — backend-ci job / make lint / make test +############################################################################### +set -euo pipefail + +echo "::group::Ruff lint" +poetry run ruff check app/ --output-format=github +poetry run ruff format app/ --check +echo "::endgroup::" + +echo "::group::Unit tests" +poetry run pytest tests/unit -m unit -v \ + --cov=app --cov-report=xml --cov-report=html \ + --junitxml=test-results-unit.xml \ + --maxfail=5 --tb=short +echo "::endgroup::" + +echo "::group::Integration tests" +poetry run pytest tests/integration -m integration -v \ + --cov=app --cov-report=xml --cov-report=html \ + --junitxml=test-results-integration.xml \ + --maxfail=5 --tb=short +echo "::endgroup::" + +echo "✅ Backend tests passed" diff --git a/.github/scripts/frontend-ci.sh b/.github/scripts/frontend-ci.sh new file mode 100755 index 0000000..2c6dfca --- /dev/null +++ b/.github/scripts/frontend-ci.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +############################################################################### +# frontend-ci.sh — Frontend TypeScript type-check and production build +# +# Runs the two build-validation steps that follow ESLint in the CI pipeline. +# ESLint is intentionally excluded here; it runs as a separate workflow step +# with continue-on-error: true so its outcome can be captured independently +# by the quality-gate job. +# +# Steps: +# 1. npm run type-check TypeScript strict compilation check (no emit) +# 2. npm run build Vite / Angular production bundle +# +# Usage: +# cd frontend && bash ../.github/scripts/frontend-ci.sh +# +# Expected CWD: frontend/ +# Dependencies: npm (installed by caller via actions/setup-node) +# Caller(s): ci.yml — frontend-ci job / make frontend-build +############################################################################### +set -euo pipefail + +echo "::group::TypeScript type-check" +npm run type-check +echo "::endgroup::" + +echo "::group::Build" +npm run build +echo "::endgroup::" + +echo "✅ Frontend type-check and build passed" diff --git a/.github/scripts/quality-gate.sh b/.github/scripts/quality-gate.sh new file mode 100755 index 0000000..fd73814 --- /dev/null +++ b/.github/scripts/quality-gate.sh @@ -0,0 +1,63 @@ +#!/usr/bin/env bash +############################################################################### +# quality-gate.sh — Evaluate CI quality gate and block on failures +# +# Reads the result of each required CI job (passed as environment variables +# containing the GitHub Actions job-result string) and exits non-zero if any +# mandatory gate has failed. This script is the single blocking status check +# referenced by branch protection rules. +# +# Gate rules: +# backend-ci and frontend-ci must be "success" (failure blocks the gate) +# commitlint failure blocks; "skipped" is allowed on push +# terraform-plan failure blocks; "skipped" is allowed +# +# Usage: +# RESULT_BACKEND=success RESULT_FRONTEND=success \ +# RESULT_COMMITLINT=success RESULT_TF_PLAN=skipped \ +# bash .github/scripts/quality-gate.sh +# +# Environment variables (GitHub Actions job result strings): +# RESULT_COMMITLINT success | failure | cancelled | skipped +# RESULT_BACKEND success | failure | cancelled | skipped +# RESULT_FRONTEND success | failure | cancelled | skipped +# RESULT_TF_PLAN success | failure | cancelled | skipped +# +# Dependencies: bash 4+ +# Caller(s): ci.yml — quality-gate job +############################################################################### +set -euo pipefail + +echo "Quality Gate Results:" +printf " %-20s %s\n" "commitlint:" "${RESULT_COMMITLINT:-skipped}" +printf " %-20s %s\n" "backend-ci:" "${RESULT_BACKEND:-skipped}" +printf " %-20s %s\n" "frontend-ci:" "${RESULT_FRONTEND:-skipped}" +printf " %-20s %s\n" "tf-plan:" "${RESULT_TF_PLAN:-skipped}" + +fail=0 + +# Hard failures — these must pass +for result in "${RESULT_BACKEND:-}" "${RESULT_FRONTEND:-}"; do + if [[ "${result}" == "failure" ]]; then + echo "❌ Required job failed (backend-ci or frontend-ci)" + fail=1 + fi +done + +# Commitlint only runs on PRs; a 'skipped' result on push is fine +if [[ "${RESULT_COMMITLINT:-}" == "failure" ]]; then + echo "❌ Commitlint failed" + fail=1 +fi + +# Terraform plan failure blocks PRs with infra changes +if [[ "${RESULT_TF_PLAN:-}" == "failure" ]]; then + echo "❌ Terraform plan failed" + fail=1 +fi + +if [[ "${fail}" -eq 1 ]]; then + exit 1 +fi + +echo "✅ All gates passed" diff --git a/.github/scripts/smoke-test.sh b/.github/scripts/smoke-test.sh new file mode 100755 index 0000000..13d0db5 --- /dev/null +++ b/.github/scripts/smoke-test.sh @@ -0,0 +1,50 @@ +#!/usr/bin/env bash +############################################################################### +# smoke-test.sh — Post-deployment health-check smoke test +# +# Waits for the application to warm up, then probes the /health endpoint +# with configurable retries. Exits non-zero if the endpoint does not return +# HTTP 200 within the allotted attempts, causing the calling workflow job to +# fail and halting any subsequent deployment steps. +# +# Usage: +# APP_URL=https://your-app.example.com bash .github/scripts/smoke-test.sh +# make smoke-test APP_URL=https://your-app.example.com +# +# Environment variables: +# APP_URL REQUIRED — base URL of the deployed application +# WARMUP_SECS optional — seconds to wait before first probe (default: 15) +# MAX_RETRIES optional — number of probe attempts (default: 3) +# RETRY_DELAY optional — seconds between retries (default: 10) +# +# Dependencies: curl +# Caller(s): .github/workflows/_smoke-test.yml / make smoke-test +############################################################################### +set -euo pipefail + +: "${APP_URL:?APP_URL must be set}" + +WARMUP_SECS="${WARMUP_SECS:-15}" +MAX_RETRIES="${MAX_RETRIES:-3}" +RETRY_DELAY="${RETRY_DELAY:-10}" + +echo "⏳ Waiting ${WARMUP_SECS}s for service to warm up..." +sleep "${WARMUP_SECS}" + +STATUS="000" +for i in $(seq 1 "${MAX_RETRIES}"); do + STATUS=$(curl -s -o /dev/null -w "%{http_code}" "${APP_URL}/health" --max-time 15 || echo "000") + echo "Attempt ${i}/${MAX_RETRIES}: HTTP ${STATUS}" + [[ "${STATUS}" == "200" ]] && break + if [[ "${i}" -lt "${MAX_RETRIES}" ]]; then + echo "Retrying in ${RETRY_DELAY}s..." + sleep "${RETRY_DELAY}" + fi +done + +if [[ "${STATUS}" != "200" ]]; then + echo "❌ Health check failed after ${MAX_RETRIES} attempts (last HTTP status: ${STATUS})" + exit 1 +fi + +echo "✅ Smoke test passed (HTTP 200)" diff --git a/.github/workflows/_smoke-test.yml b/.github/workflows/_smoke-test.yml new file mode 100644 index 0000000..50143b2 --- /dev/null +++ b/.github/workflows/_smoke-test.yml @@ -0,0 +1,63 @@ +############################################################################### +# _smoke-test.yml — Reusable smoke test workflow +# +# Called by staging.yml and release.yml after a deployment to verify the +# service is healthy. All deployment-specific values are passed via inputs. +############################################################################### +name: Smoke Test + +on: + workflow_call: + inputs: + environment: + description: "GitHub Environment name (e.g. staging, production)" + required: true + type: string + app-url: + description: "Base URL of the deployed application" + required: true + type: string + warmup-seconds: + description: "Seconds to wait before probing the health endpoint" + required: false + type: number + default: 15 + deploy-sha: + description: "Commit SHA that was deployed (shown in summary)" + required: false + type: string + default: "" + deploy-version: + description: "Version / tag that was deployed (shown in summary)" + required: false + type: string + default: "" + +jobs: + smoke-test: + name: Smoke Test (${{ inputs.environment }}) + runs-on: ubuntu-latest + timeout-minutes: 5 + environment: ${{ inputs.environment }} + steps: + - uses: actions/checkout@v4 + + - name: Health check + env: + APP_URL: ${{ inputs.app-url }} + WARMUP_SECS: ${{ inputs.warmup-seconds }} + run: bash .github/scripts/smoke-test.sh + + - name: Write deployment summary + if: always() + run: | + { + echo "## ${{ inputs.environment }} Deployment" + echo "| Field | Value |" + echo "|---------|-------|" + echo "| Env | \`${{ inputs.environment }}\` |" + echo "| Version | \`${{ inputs.deploy-version != '' && inputs.deploy-version || 'N/A' }}\` |" + echo "| Commit | \`${{ inputs.deploy-sha != '' && inputs.deploy-sha || github.sha }}\` |" + echo "| URL | ${{ inputs.app-url }} |" + echo "| Status | ${{ job.status }} |" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ff64a21..d7f6278 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -124,10 +124,10 @@ jobs: # Secrets and variables → Variables) to match config/.env.test. # Reference: config/.env.test — DATABASE_USER / DATABASE_PASSWORD / DATABASE_NAME / DATABASE_PORT env: - POSTGRES_USER: ${{ vars.DATABASE_USER }} - POSTGRES_PASSWORD: ${{ vars.DATABASE_PASSWORD }} - POSTGRES_DB: ${{ vars.DATABASE_NAME }} - POSTGRES_PORT: ${{ vars.DATABASE_PORT }} + POSTGRES_USER: ${{ secrets.DATABASE_USER }} + POSTGRES_PASSWORD: ${{ secrets.DATABASE_PASSWORD }} + POSTGRES_DB: ${{ secrets.DATABASE_NAME }} + POSTGRES_PORT: ${{ secrets.DATABASE_PORT }} services: postgres: image: postgres:16-alpine @@ -136,12 +136,13 @@ jobs: POSTGRES_PASSWORD: ${{ env.POSTGRES_PASSWORD }} POSTGRES_DB: ${{ env.POSTGRES_DB }} options: >- - --health-cmd pg_isready + --health-cmd pg_is ready --health-interval 10s --health-timeout 5s --health-retries 5 ports: - ${{ env.POSTGRES_PORT }}:5432 + steps: - uses: actions/checkout@v4 @@ -154,7 +155,7 @@ jobs: - uses: actions/setup-python@v5 with: - python-version: "3.12" + python-version-file: backend/.python-version - name: Cache Poetry uses: actions/cache@v4 @@ -319,7 +320,7 @@ jobs: - name: Snyk Python id: snyk-python - uses: snyk/actions/python-3.12@master + uses: snyk/actions/python@master env: SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} with: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 97c29fb..c248e73 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -3,326 +3,239 @@ ############################################################################### # Two distinct flows gated by trigger type: # -# 1. CI succeeds on main → workflow_run (Semantic Release) -# • Runs semantic-release: analyses conventional commits since last tag, -# bumps version, writes CHANGELOG.md, creates git tag + GitHub Release. -# • Does NOT deploy — the tag creation above triggers flow 2. +# Flow 1 — Semantic Release (workflow_run: CI succeeds on main) +# Runs semantic-release: analyses conventional commits since last tag, +# bumps version, writes CHANGELOG.md, creates git tag + GitHub Release. +# Does NOT deploy — the tag creation triggers Flow 2. # -# 2. push tag → v* (Production Deploy) -# • Builds production images tagged vX.Y.Z + latest -# • Trivy image scan (CRITICAL blocks deploy) -# • Terraform apply — production environment -# • ECS deploy — production environment -# • Smoke test +# Flow 2 — Production Deploy (push tag v*) +# a. Build production images tagged vX.Y.Z + latest → ECR +# b. Trivy image scan (CRITICAL blocks deploy) +# c. Terraform apply — production environment +# d. ECS deploy — production environment +# e. Smoke test # # Concurrency: production deploy is NEVER cancelled mid-flight. -# ALL secrets are injected through the "production" GitHub Environment. +# ALL secrets come from the "production" GitHub Environment. ############################################################################### name: CD — Release & Production on: - # Flow 1 (semantic-release): trigger only after CI succeeds on main. - # Flow 2 (production deploy): triggered by the git tag that semantic-release creates. - workflow_run: - workflows: ["CI"] - branches: [main] - types: [completed] - push: - tags: - - "v*" - workflow_dispatch: - inputs: - tag: - description: "Tag to deploy (e.g. v1.2.3) — leave empty for latest" - required: false - type: string + workflow_run: + workflows: ["CI"] + branches: [main] + types: [completed] + push: + tags: + - "v*" + workflow_dispatch: + inputs: + tag: + description: "Tag to deploy (e.g. v1.2.3) — leave empty for latest" + required: false + type: string permissions: - contents: read + contents: read -# Semantic release: cancel if a newer main-push comes in -# Production deploy: NEVER cancel in-progress (cancel-in-progress: false) concurrency: - group: ${{ startsWith(github.ref, 'refs/tags/') && 'production-deploy' || 'semantic-release' }} - cancel-in-progress: ${{ !startsWith(github.ref, 'refs/tags/') }} + group: ${{ startsWith(github.ref, 'refs/tags/') && 'production-deploy' || 'semantic-release' }} + cancel-in-progress: ${{ !startsWith(github.ref, 'refs/tags/') }} jobs: - # ========================================================================= - # FLOW 1 — Semantic Release (push → main) - # ========================================================================= - autoversion: - name: Semantic Release - runs-on: ubuntu-latest - timeout-minutes: 10 - # Only run when CI has passed on main (workflow_run) or on manual dispatch. - # Production tag-push events are handled by the build-production flow below. - if: > - github.event_name == 'workflow_dispatch' || - (github.event_name == 'workflow_run' && - github.event.workflow_run.conclusion == 'success') - permissions: - contents: write # push tag + CHANGELOG commit - issues: write # close issues referenced in commits - pull-requests: write # comment on PRs - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - persist-credentials: true # semantic-release needs push access - # Check out the branch ref (not a detached SHA) so semantic-release - # can push the CHANGELOG commit and tag back to the branch. - ref: ${{ github.event.workflow_run.head_branch || 'main' }} - - - uses: actions/setup-node@v4 - with: - node-version: "20" - cache: "npm" - - - name: Install semantic-release - run: npm ci --ignore-scripts - - - name: Run semantic-release - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: npx semantic-release - - # ========================================================================= - # FLOW 2a — Build production images (push tag v*) - # ========================================================================= - build-production: - name: Build & Push (${{ matrix.service }}) - runs-on: ubuntu-latest - timeout-minutes: 20 - if: startsWith(github.ref, 'refs/tags/v') - environment: production - strategy: - matrix: - service: [backend, frontend] - fail-fast: true - permissions: - contents: read - packages: write # GHCR (interim; final push is to ECR below) - id-token: write # AWS OIDC - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ github.ref }} - - - name: Resolve version tag - id: ver - run: | - TAG="${GITHUB_REF#refs/tags/}" - echo "tag=$TAG" >> $GITHUB_OUTPUT - echo "sha_short=$(git rev-parse --short HEAD)" >> $GITHUB_OUTPUT - - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4 - with: - role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} - aws-region: ${{ secrets.AWS_REGION }} - - - name: Log in to Amazon ECR - id: ecr-login - uses: aws-actions/amazon-ecr-login@v2 - - - name: Set image metadata (ECR) - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ steps.ecr-login.outputs.registry }}/${{ matrix.service }} - tags: | - type=semver,pattern={{version}} - type=semver,pattern={{major}}.{{minor}} - type=raw,value=latest - type=sha,prefix=,format=short - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Build & push to ECR - uses: docker/build-push-action@v5 - with: - context: ./${{ matrix.service }} - file: ./${{ matrix.service }}/Dockerfile - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha,scope=${{ matrix.service }}-prod - cache-to: type=gha,scope=${{ matrix.service }}-prod,mode=max - build-args: | - BUILD_ENV=production - GIT_SHA=${{ steps.ver.outputs.sha_short }} - VERSION=${{ steps.ver.outputs.tag }} - - # ========================================================================= - # FLOW 2b — Image security scan (CRITICAL blocks deploy) - # ========================================================================= - scan-production: - name: Scan Production Image (${{ matrix.service }}) - runs-on: ubuntu-latest - timeout-minutes: 10 - if: startsWith(github.ref, 'refs/tags/v') - needs: [build-production] - environment: production - strategy: - matrix: - service: [backend, frontend] - fail-fast: false - permissions: - security-events: write - id-token: write - steps: - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4 - with: - role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} - aws-region: ${{ secrets.AWS_REGION }} - - - name: Log in to Amazon ECR - id: ecr-login - uses: aws-actions/amazon-ecr-login@v2 - - - name: Trivy image scan (FAIL on CRITICAL) - uses: aquasecurity/trivy-action@master - with: - image-ref: "${{ steps.ecr-login.outputs.registry }}/${{ matrix.service }}:${{ github.ref_name }}" - format: "sarif" - output: "trivy-${{ matrix.service }}-prod.sarif" - severity: "CRITICAL" - exit-code: "1" # Hard block on CRITICAL in production - - - name: Upload scan SARIF - if: always() - uses: github/codeql-action/upload-sarif@v3 - with: - sarif_file: "trivy-${{ matrix.service }}-prod.sarif" - category: "trivy-${{ matrix.service }}-production" - - # ========================================================================= - # FLOW 2c — Terraform apply — production - # ========================================================================= - terraform-production: - name: Terraform Apply (production) - runs-on: ubuntu-latest - timeout-minutes: 30 - if: startsWith(github.ref, 'refs/tags/v') - needs: [scan-production] - environment: production # Requires manual approval via GitHub Environments - permissions: - contents: read - id-token: write - steps: - - uses: actions/checkout@v4 - with: - ref: ${{ github.ref }} - - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4 - with: - role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} - aws-region: ${{ secrets.AWS_REGION }} - - - uses: hashicorp/setup-terraform@v3 - with: - terraform_version: ${{ secrets.TF_VERSION }} - - - name: Terraform init (remote state — production) - working-directory: infra - run: | - terraform init \ - -backend-config="bucket=${{ secrets.TERRAFORM_STATE_BUCKET }}" \ - -backend-config="key=production/terraform.tfstate" \ - -backend-config="region=${{ secrets.AWS_REGION }}" \ - -backend-config="dynamodb_table=${{ secrets.TERRAFORM_LOCK_TABLE }}" - - - name: Terraform plan (production) - working-directory: infra - env: - TF_VAR_environment: production - TF_VAR_aws_region: ${{ secrets.AWS_REGION }} - run: | - terraform plan \ - -var-file="envs/prod.tfvars" \ - -out=tfplan.prod \ - -no-color - - - name: Terraform apply (production) - working-directory: infra - run: terraform apply -auto-approve tfplan.prod - - # ========================================================================= - # FLOW 2d — ECS rolling deploy — production - # ========================================================================= - deploy-production: - name: Deploy to Production (${{ matrix.service }}) - runs-on: ubuntu-latest - timeout-minutes: 20 - if: startsWith(github.ref, 'refs/tags/v') - needs: [terraform-production] - environment: production - strategy: - matrix: - service: [backend, frontend] - fail-fast: false - permissions: - id-token: write - steps: - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4 - with: - role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} - aws-region: ${{ secrets.AWS_REGION }} - - - name: Log in to Amazon ECR - id: ecr-login - uses: aws-actions/amazon-ecr-login@v2 - - - name: Render new task definition - id: render-task-def - uses: aws-actions/amazon-ecs-render-task-definition@v1 - with: - task-definition: ${{ secrets.ECS_TASK_DEFINITION }} - container-name: ${{ matrix.service }} - image: "${{ steps.ecr-login.outputs.registry }}/${{ matrix.service }}:${{ github.ref_name }}" - - - name: Deploy to ECS - uses: aws-actions/amazon-ecs-deploy-task-definition@v1 - with: - task-definition: ${{ steps.render-task-def.outputs.task-definition }} - service: "${{ secrets.ECS_SERVICE_PREFIX }}-${{ matrix.service }}" - cluster: ${{ secrets.ECS_CLUSTER }} - wait-for-service-stability: true - - # ========================================================================= - # FLOW 2e — Smoke test (production) - # ========================================================================= - smoke-test-production: - name: Smoke Test (production) - runs-on: ubuntu-latest - timeout-minutes: 5 - if: startsWith(github.ref, 'refs/tags/v') - needs: [deploy-production] - environment: production - steps: - - name: Health check - run: | - echo "Waiting for cold-start..." - sleep 20 - STATUS=$(curl -s -o /dev/null -w "%{http_code}" \ - "${{ secrets.APP_URL }}/health" --max-time 15 || echo "000") - echo "Health check status: $STATUS" - [[ "$STATUS" == "200" ]] || { echo "❌ Production health check failed ($STATUS)"; exit 1; } - echo "✅ Production smoke test passed" - - - name: Write deployment summary - if: always() - run: | - { - echo "## Production Deployment" - echo "| Field | Value |" - echo "|---------|-------|" - echo "| Version | \`${{ github.ref_name }}\` |" - echo "| Commit | \`${{ github.sha }}\` |" - echo "| URL | ${{ secrets.APP_URL }} |" - echo "| Status | ${{ job.status }} |" - } >> $GITHUB_STEP_SUMMARY + autoversion: + name: Semantic Release + runs-on: ubuntu-latest + timeout-minutes: 10 + if: > + github.event_name == 'workflow_dispatch' || + (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') + permissions: + contents: write + issues: write + pull-requests: write + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + persist-credentials: true + ref: ${{ github.event.workflow_run.head_branch || 'main' }} + + - uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "npm" + cache-dependency-path: .github/package-lock.json + + - name: Install semantic-release + working-directory: .github + run: npm ci --ignore-scripts + + - name: Run semantic-release + working-directory: .github + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: npx semantic-release + + build-production: + name: Build & Push (${{ matrix.service }}) + runs-on: ubuntu-latest + timeout-minutes: 20 + if: startsWith(github.ref, 'refs/tags/v') + environment: production + strategy: + matrix: + service: [backend, frontend] + fail-fast: true + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.ref }} + + - name: Resolve version tag + id: ver + run: | + TAG="${GITHUB_REF#refs/tags/}" + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + echo "sha_short=$(git rev-parse --short HEAD)" >> "$GITHUB_OUTPUT" + + - name: Authenticate with AWS + id: aws + uses: ./.github/actions/aws-auth + with: + role-arn: ${{ secrets.AWS_ROLE_TO_ASSUME }} + aws-region: ${{ secrets.AWS_REGION }} + + - name: Log in to Amazon ECR + id: ecr-login + uses: aws-actions/amazon-ecr-login@v2 + + - name: Build, push & scan (blocking on CRITICAL) + uses: ./.github/actions/docker-build + with: + context: ./${{ matrix.service }} + dockerfile: ./${{ matrix.service }}/Dockerfile + image-name: mypythonproject1/${{ matrix.service }} + registry: ${{ steps.ecr-login.outputs.registry }} + tags: | + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest + type=sha,prefix=,format=short + build-args: | + BUILD_ENV=production + GIT_SHA=${{ steps.ver.outputs.sha_short }} + VERSION=${{ steps.ver.outputs.tag }} + platforms: linux/amd64 + scan: "true" + scan-severity: "CRITICAL" + scan-exit-code: "1" + cache-scope: ${{ matrix.service }}-prod + + terraform-production: + name: Terraform Apply (production) + runs-on: ubuntu-latest + timeout-minutes: 30 + if: startsWith(github.ref, 'refs/tags/v') + needs: [build-production] + environment: production + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.ref }} + + - name: Authenticate with AWS + uses: ./.github/actions/aws-auth + with: + role-arn: ${{ secrets.AWS_ROLE_TO_ASSUME }} + aws-region: ${{ secrets.AWS_REGION }} + + - name: Set Terraform runtime vars + run: | + AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) + REGION="${{ secrets.AWS_REGION }}" + echo "TF_VAR_ecr_repository_url=${AWS_ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/mypythonproject1/backend" >> "$GITHUB_ENV" + echo "TF_VAR_frontend_ecr_repository_url=${AWS_ACCOUNT_ID}.dkr.ecr.${REGION}.amazonaws.com/mypythonproject1/frontend" >> "$GITHUB_ENV" + echo "TF_VAR_jwt_secret_key=${{ secrets.JWT_SECRET_KEY }}" >> "$GITHUB_ENV" + + - name: Terraform apply + uses: ./.github/actions/terraform/apply + with: + working-directory: ./infra + terraform-version: ${{ secrets.TF_VERSION }} + environment: production + aws-region: ${{ secrets.AWS_REGION }} + state-bucket: ${{ secrets.TERRAFORM_STATE_BUCKET }} + state-lock-table: ${{ secrets.TERRAFORM_LOCK_TABLE }} + + deploy-production: + name: Deploy to Production (${{ matrix.service }}) + runs-on: ubuntu-latest + timeout-minutes: 20 + if: startsWith(github.ref, 'refs/tags/v') + needs: [terraform-production] + environment: production + strategy: + matrix: + service: [backend, frontend] + fail-fast: false + permissions: + id-token: write + steps: + - uses: actions/checkout@v4 + + - name: Authenticate with AWS + uses: ./.github/actions/aws-auth + with: + role-arn: ${{ secrets.AWS_ROLE_TO_ASSUME }} + aws-region: ${{ secrets.AWS_REGION }} + + - name: Force new ECS deployment + run: | + PROJECT_NAME=$(awk -F'=' '/^project_name[[:space:]]*=/{gsub(/["[:space:]]/,"",$2); print $2; exit}' infra/envs/prod.tfvars) + CLUSTER_NAME="${PROJECT_NAME}-cluster" + if [[ "${{ matrix.service }}" == "backend" ]]; then + SERVICE_NAME="${PROJECT_NAME}-service" + else + SERVICE_NAME="${PROJECT_NAME}-frontend-service" + fi + + aws ecs update-service \ + --cluster "${CLUSTER_NAME}" \ + --service "${SERVICE_NAME}" \ + --force-new-deployment \ + --region "${{ secrets.AWS_REGION }}" + + - name: Wait for service stability + timeout-minutes: 15 + run: | + PROJECT_NAME=$(awk -F'=' '/^project_name[[:space:]]*=/{gsub(/["[:space:]]/,"",$2); print $2; exit}' infra/envs/prod.tfvars) + CLUSTER_NAME="${PROJECT_NAME}-cluster" + if [[ "${{ matrix.service }}" == "backend" ]]; then + SERVICE_NAME="${PROJECT_NAME}-service" + else + SERVICE_NAME="${PROJECT_NAME}-frontend-service" + fi + + aws ecs wait services-stable \ + --cluster "${CLUSTER_NAME}" \ + --services "${SERVICE_NAME}" \ + --region "${{ secrets.AWS_REGION }}" + + smoke-test-production: + name: Smoke Test + needs: [deploy-production] + uses: ./.github/workflows/_smoke-test.yml + with: + environment: production + app-url: ${{ vars.APP_URL }} + warmup-seconds: 20 + deploy-version: ${{ github.ref_name }} + deploy-sha: ${{ github.sha }} + secrets: inherit diff --git a/.github/workflows/staging.yml b/.github/workflows/staging.yml index aceb660..2f5bcd3 100644 --- a/.github/workflows/staging.yml +++ b/.github/workflows/staging.yml @@ -2,271 +2,193 @@ # CD — Staging Deployment ############################################################################### # Responsibilities: -# • Build & push backend + frontend container images → GHCR -# • Trivy image scan (warn, non-blocking) -# • Terraform apply for staging environment -# • ECS rolling deploy (backend + frontend) -# • Smoke test after deployment +# - Build + push backend + frontend images to GHCR (Trivy scan, warn-only) +# - Terraform apply for staging environment +# - ECS rolling deploy (force-new-deployment, always pulls the 'staging' tag) +# - Smoke test after deployment # # Triggers: -# • workflow_run: CI workflow succeeds on develop (ensures quality gate passes first) -# • workflow_dispatch (manual re-deploy) +# - workflow_run: CI succeeds on develop +# - workflow_dispatch (manual re-deploy) # -# ALL secrets are injected via GitHub Environment "staging" — no plaintext. -# Non-secret config loaded from config/.env.staging. +# ALL secrets come from the GitHub Environment "staging". +# Non-secret config is loaded from config/.env.staging. ############################################################################### name: CD — Staging on: - # Only run after CI completes successfully on the develop branch. - # This ensures staging never deploys code that has not passed the quality gate. - workflow_run: - workflows: ["CI"] - branches: [develop] - types: [completed] - workflow_dispatch: - inputs: - force-deploy: - description: "Force deploy even if no code changes" - required: false - type: boolean - default: false + workflow_run: + workflows: ["CI"] + branches: [develop] + types: [completed] + workflow_dispatch: + inputs: + force-deploy: + description: "Force deploy even if no code changes" + required: false + type: boolean + default: false permissions: - contents: read - packages: write # Push to GHCR - id-token: write # AWS OIDC + contents: read + packages: write + id-token: write -# Only one staging deploy runs at a time; newer commits cancel older ones concurrency: - group: staging-deploy - cancel-in-progress: true + group: staging-deploy + cancel-in-progress: true jobs: - # ========================================================================= - # 1. BUILD — Docker images (backend + frontend) - # ========================================================================= - build: - name: Build & Push (${{ matrix.service }}) - runs-on: ubuntu-latest - timeout-minutes: 20 - # Gate: skip unless triggered manually OR the upstream CI run succeeded. - if: > - github.event_name == 'workflow_dispatch' || - (github.event_name == 'workflow_run' && - github.event.workflow_run.conclusion == 'success') - strategy: - matrix: - service: [backend, frontend] - fail-fast: true - outputs: - image-tag: ${{ steps.meta.outputs.version }} - steps: - - uses: actions/checkout@v4 - with: - # Check out the exact commit that passed CI, not the workflow file's HEAD. - ref: ${{ github.event.workflow_run.head_sha || github.sha }} - - - name: Load staging environment - run: | - grep -v '^\s*#' config/.env.staging | grep -v '^\s*$' >> $GITHUB_ENV - - - name: Set image metadata - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ matrix.service }} - tags: | - type=raw,value=staging - type=sha,prefix=staging-,format=short - type=ref,event=branch - - - name: Log in to GHCR - uses: docker/login-action@v3 - with: - registry: ghcr.io - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - - - name: Build & push - uses: docker/build-push-action@v5 - with: - context: ./${{ matrix.service }} - file: ./${{ matrix.service }}/Dockerfile - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - cache-from: type=gha,scope=${{ matrix.service }}-staging - cache-to: type=gha,scope=${{ matrix.service }}-staging,mode=max - build-args: | - BUILD_ENV=staging - GIT_SHA=${{ github.sha }} - - # ========================================================================= - # 2. IMAGE SECURITY SCAN (Trivy — warn only, non-blocking) - # ========================================================================= - scan: - name: Scan Images (${{ matrix.service }}) - runs-on: ubuntu-latest + build: + name: Build & Push (${{ matrix.service }}) + runs-on: ubuntu-latest + timeout-minutes: 20 + if: > + github.event_name == 'workflow_dispatch' || + (github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success') + environment: staging + strategy: + matrix: + service: [backend, frontend] + fail-fast: true + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event.workflow_run.head_sha || github.sha }} + + - name: Load staging environment + run: grep -v '^\s*#' config/.env.staging | grep -v '^\s*$' >> "$GITHUB_ENV" + + - name: Authenticate with AWS + uses: ./.github/actions/aws-auth + with: + role-arn: ${{ secrets.AWS_ROLE_TO_ASSUME }} + aws-region: ${{ env.AWS_REGION }} + + - name: Log in to Amazon ECR + id: ecr-login + uses: aws-actions/amazon-ecr-login@v2 + + - name: Build, push & scan + uses: ./.github/actions/docker-build + with: + context: ./${{ matrix.service }} + dockerfile: ./${{ matrix.service }}/Dockerfile + image-name: mypythonproject1/${{ matrix.service }} + registry: ${{ steps.ecr-login.outputs.registry }} + tags: | + type=raw,value=staging + type=sha,prefix=staging-,format=short + build-args: | + BUILD_ENV=staging + GIT_SHA=${{ github.event.workflow_run.head_sha || github.sha }} + platforms: linux/amd64 + scan: "true" + scan-severity: "CRITICAL,HIGH" + scan-exit-code: "0" + cache-scope: ${{ matrix.service }}-staging + + terraform-staging: + name: Terraform Apply (staging) + runs-on: ubuntu-latest + timeout-minutes: 30 + needs: [build] + environment: staging + permissions: + contents: read + id-token: write + steps: + - uses: actions/checkout@v4 + + - name: Load staging environment + run: grep -v '^\s*#' config/.env.staging | grep -v '^\s*$' >> "$GITHUB_ENV" + + - name: Authenticate with AWS + uses: ./.github/actions/aws-auth + with: + role-arn: ${{ secrets.AWS_ROLE_TO_ASSUME }} + aws-region: ${{ env.AWS_REGION }} + + - name: Set Terraform runtime vars + run: | + AWS_ACCOUNT_ID=$(aws sts get-caller-identity --query Account --output text) + echo "TF_VAR_ecr_repository_url=${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/mypythonproject1/backend" >> "$GITHUB_ENV" + echo "TF_VAR_frontend_ecr_repository_url=${AWS_ACCOUNT_ID}.dkr.ecr.${AWS_REGION}.amazonaws.com/mypythonproject1/frontend" >> "$GITHUB_ENV" + echo "TF_VAR_jwt_secret_key=${{ secrets.JWT_SECRET_KEY }}" >> "$GITHUB_ENV" + + - name: Terraform apply + uses: ./.github/actions/terraform/apply + with: + working-directory: ./infra + terraform-version: ${{ env.TF_VERSION }} + environment: staging + aws-region: ${{ env.AWS_REGION }} + state-bucket: ${{ secrets.TERRAFORM_STATE_BUCKET }} + state-lock-table: ${{ secrets.TERRAFORM_LOCK_TABLE }} + + deploy-staging: + name: Deploy to Staging (${{ matrix.service }}) + runs-on: ubuntu-latest + timeout-minutes: 20 + needs: [terraform-staging] + environment: staging + strategy: + matrix: + service: [backend, frontend] + fail-fast: false + permissions: + id-token: write + steps: + - uses: actions/checkout@v4 + + - name: Load staging environment + run: grep -v '^\s*#' config/.env.staging | grep -v '^\s*$' >> "$GITHUB_ENV" + + - name: Authenticate with AWS + uses: ./.github/actions/aws-auth + with: + role-arn: ${{ secrets.AWS_ROLE_TO_ASSUME }} + aws-region: ${{ env.AWS_REGION }} + + - name: Force new ECS deployment + run: | + PROJECT_NAME=$(awk -F'=' '/^project_name[[:space:]]*=/{gsub(/["[:space:]]/,"",$2); print $2; exit}' infra/envs/staging.tfvars) + CLUSTER_NAME="${PROJECT_NAME}-cluster" + if [[ "${{ matrix.service }}" == "backend" ]]; then + SERVICE_NAME="${PROJECT_NAME}-service" + else + SERVICE_NAME="${PROJECT_NAME}-frontend-service" + fi + + aws ecs update-service \ + --cluster "${CLUSTER_NAME}" \ + --service "${SERVICE_NAME}" \ + --force-new-deployment \ + --region "${{ env.AWS_REGION }}" + + - name: Wait for service stability timeout-minutes: 10 - needs: [build] - strategy: - matrix: - service: [backend, frontend] - fail-fast: false - steps: - - name: Load staging environment - run: | - grep -v '^\s*#' config/.env.staging | grep -v '^\s*$' >> $GITHUB_ENV - - - name: Trivy image scan - uses: aquasecurity/trivy-action@master - with: - image-ref: "${{ env.REGISTRY }}/${{ github.repository_owner }}/${{ matrix.service }}:staging-${{ github.event.workflow_run.head_sha || github.sha }}" - format: "sarif" - output: "trivy-${{ matrix.service }}.sarif" - severity: "CRITICAL,HIGH" - continue-on-error: true # Warn only — staging may have acceptable risk - - - name: Upload scan results - uses: github/codeql-action/upload-sarif@v3 - with: - sarif_file: "trivy-${{ matrix.service }}.sarif" - category: "trivy-${{ matrix.service }}-staging" - - # ========================================================================= - # 3. TERRAFORM APPLY — staging infrastrastructure - # ========================================================================= - terraform-staging: - name: Terraform Apply (staging) - runs-on: ubuntu-latest - timeout-minutes: 30 - needs: [build] - environment: staging # GitHub environment with approval gates if needed - permissions: - contents: read - id-token: write - steps: - - uses: actions/checkout@v4 - - - name: Load staging environment - run: | - grep -v '^\s*#' config/.env.staging | grep -v '^\s*$' >> $GITHUB_ENV - - - uses: hashicorp/setup-terraform@v3 - with: - terraform_version: ${{ env.TF_VERSION }} - - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4 - with: - role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} - aws-region: ${{ env.AWS_REGION }} - - - name: Terraform init (remote state) - working-directory: infra - run: | - terraform init \ - -backend-config="bucket=${{ secrets.TERRAFORM_STATE_BUCKET }}" \ - -backend-config="key=staging/terraform.tfstate" \ - -backend-config="region=${{ env.AWS_REGION }}" \ - -backend-config="dynamodb_table=${{ secrets.TERRAFORM_LOCK_TABLE }}" - - - name: Terraform plan (staging) - working-directory: infra - env: - TF_VAR_environment: staging - TF_VAR_aws_region: ${{ env.AWS_REGION }} - run: | - terraform plan \ - -var-file="envs/staging.tfvars" \ - -out=tfplan.staging \ - -no-color - - - name: Terraform apply (staging) - working-directory: infra - run: terraform apply -auto-approve tfplan.staging - - # ========================================================================= - # 4. DEPLOY — ECS rolling update (backend + frontend) - # ========================================================================= - deploy-staging: - name: Deploy to Staging (${{ matrix.service }}) - runs-on: ubuntu-latest - timeout-minutes: 20 - needs: [terraform-staging] - environment: staging - strategy: - matrix: - service: [backend, frontend] - fail-fast: false - permissions: - id-token: write - steps: - - name: Load staging environment - run: | - grep -v '^\s*#' config/.env.staging | grep -v '^\s*$' >> $GITHUB_ENV - - - name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4 - with: - role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} - aws-region: ${{ env.AWS_REGION }} - - - name: Update ECS service - run: | - aws ecs update-service \ - --cluster "${{ env.ECS_CLUSTER }}" \ - --service "${{ env.ECS_SERVICE_PREFIX }}-${{ matrix.service }}" \ - --force-new-deployment \ - --region "${{ env.AWS_REGION }}" - - - name: Wait for deployment stability - timeout-minutes: 10 - run: | - aws ecs wait services-stable \ - --cluster "${{ env.ECS_CLUSTER }}" \ - --services "${{ env.ECS_SERVICE_PREFIX }}-${{ matrix.service }}" \ - --region "${{ env.AWS_REGION }}" - - # ========================================================================= - # 5. SMOKE TEST - # ========================================================================= - smoke-test: - name: Smoke Test - runs-on: ubuntu-latest - timeout-minutes: 5 - needs: [deploy-staging] - environment: staging - steps: - - uses: actions/checkout@v4 - - - name: Load staging environment - run: | - grep -v '^\s*#' config/.env.staging | grep -v '^\s*$' >> $GITHUB_ENV - - - name: Health check - run: | - echo "Waiting for service to warm up..." - sleep 15 - STATUS=$(curl -s -o /dev/null -w "%{http_code}" \ - "${{ env.APP_URL }}/health" --max-time 10 || echo "000") - echo "Health check status: $STATUS" - [[ "$STATUS" == "200" ]] || { echo "❌ Health check failed ($STATUS)"; exit 1; } - echo "✅ Smoke test passed" - - - name: Write deployment summary - if: always() - run: | - { - echo "## Staging Deployment" - echo "| Field | Value |" - echo "|-------|-------|" - echo "| Commit | \`${{ github.event.workflow_run.head_sha || github.sha }}\` |" - echo "| Branch | \`${{ github.ref_name }}\` |" - echo "| URL | ${{ env.APP_URL }} |" - echo "| Status | ${{ job.status }} |" - } >> $GITHUB_STEP_SUMMARY + run: | + PROJECT_NAME=$(awk -F'=' '/^project_name[[:space:]]*=/{gsub(/["[:space:]]/,"",$2); print $2; exit}' infra/envs/staging.tfvars) + CLUSTER_NAME="${PROJECT_NAME}-cluster" + if [[ "${{ matrix.service }}" == "backend" ]]; then + SERVICE_NAME="${PROJECT_NAME}-service" + else + SERVICE_NAME="${PROJECT_NAME}-frontend-service" + fi + + aws ecs wait services-stable \ + --cluster "${CLUSTER_NAME}" \ + --services "${SERVICE_NAME}" \ + --region "${{ env.AWS_REGION }}" + + smoke-test: + name: Smoke Test + needs: [deploy-staging] + uses: ./.github/workflows/_smoke-test.yml + with: + environment: staging + app-url: ${{ vars.APP_URL }} + warmup-seconds: 15 + deploy-sha: ${{ github.event.workflow_run.head_sha || github.sha }} + secrets: inherit diff --git a/Makefile b/Makefile index 59a8f71..6fa0409 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,24 @@ +############################################################################### +# Makefile — Local developer CLI for mypythonproject1 +# +# Wraps common operations so engineers don't need to remember long commands: +# - Docker Compose local environment (docker-up / docker-down) +# - Backend / frontend development server (make dev) +# - Test runners (make test, backend-test-*, frontend-test, frontend-build) +# - Linting (make lint) +# - Terraform local plan/apply/destroy (make tf-* ENV=staging|prod) +# - Docker build + ECR push (make docker-build / docker-push ENV=...) +# - ECS rolling deploy (make ecs-deploy ENV=... IMAGE_TAG=...) +# - One-time AWS infra bootstrap (make bootstrap) +# - Environment variable setup (make setup-env ENV=...) +# +# All deployment targets require real AWS credentials in the shell. +# Terraform targets delegate to scripts/terraform-*.sh. +############################################################################### .PHONY: help install dev docker-up docker-down docker-logs \ - test backend-test frontend-test lint format clean \ + test backend-test frontend-test frontend-build lint clean \ + ensure-backend-venv ensure-frontend-deps \ + bootstrap setup-env \ tf-validate tf-plan tf-apply tf-destroy \ docker-build docker-push ecs-deploy deploy deploy-staging deploy-prod @@ -12,6 +31,40 @@ YELLOW := \033[0;33m RED := \033[0;31m NC := \033[0m # No Color +# ============================================================================ +# LOCAL ENV / TFVARS LOADING (dev by default) +# ============================================================================ + +ENV_FILE := deploy/.env +DEV_TFVARS_FILE := infra/envs/dev.tfvars + +-include $(ENV_FILE) + +# Prefer tfvars as source of truth for infra variables. +TFVARS_ENV := $(shell awk -F'=' '/^environment[[:space:]]*=/{gsub(/["[:space:]]/,"",$$2); print $$2; exit}' $(DEV_TFVARS_FILE) 2>/dev/null) +ENV ?= $(or $(TFVARS_ENV),dev) +TFVARS_FILE = infra/envs/$(ENV).tfvars + +TFVARS_PROJECT_NAME := $(shell awk -F'=' '/^project_name[[:space:]]*=/{gsub(/["[:space:]]/,"",$$2); print $$2; exit}' $(TFVARS_FILE) 2>/dev/null || awk -F'=' '/^project_name[[:space:]]*=/{gsub(/["[:space:]]/,"",$$2); print $$2; exit}' $(DEV_TFVARS_FILE) 2>/dev/null) +TFVARS_AWS_REGION := $(shell awk -F'=' '/^aws_region[[:space:]]*=/{gsub(/["[:space:]]/,"",$$2); print $$2; exit}' $(TFVARS_FILE) 2>/dev/null || awk -F'=' '/^aws_region[[:space:]]*=/{gsub(/["[:space:]]/,"",$$2); print $$2; exit}' $(DEV_TFVARS_FILE) 2>/dev/null) + +PROJECT_NAME := $(or $(TFVARS_PROJECT_NAME),$(PROJECT_NAME),mypythonproject1) +AWS_REGION := $(or $(TFVARS_AWS_REGION),$(AWS_REGION),us-east-1) +DOCKER_PLATFORM ?= linux/amd64 + +ECS_CLUSTER ?= $(PROJECT_NAME)-cluster-$(ENV) +ECS_SERVICE_BACKEND ?= backend-service-$(ENV) +ECS_SERVICE_FRONTEND ?= frontend-service-$(ENV) + +TERRAFORM_STATE_BUCKET ?= terraform-state-$(AWS_ACCOUNT_ID) +TERRAFORM_LOCK_TABLE ?= terraform-locks + +ifeq ($(strip $(AWS_ACCOUNT_ID)),) +AWS_ACCOUNT_ID := $(shell aws sts get-caller-identity --query Account --output text 2>/dev/null) +endif + +BACKEND_VENV_PY := backend/.venv/bin/python + # ============================================================================ # HELP & DOCUMENTATION # ============================================================================ @@ -41,15 +94,19 @@ help: @echo " $(YELLOW)make backend-test-integration$(NC) - Integration tests (real DB)" @echo " $(YELLOW)make backend-test-coverage$(NC) - Tests with coverage report" @echo " $(YELLOW)make backend-validate-tests$(NC) - Validate test configuration" - @echo " $(YELLOW)make frontend-test$(NC) - Frontend tests" - @echo " $(YELLOW)make lint$(NC) - Run linters (ruff, eslint)" - @echo " $(YELLOW)make format$(NC) - Format code (black, prettier)" + @echo " $(YELLOW)make frontend-test$(NC) - Frontend tests (Karma/Jasmine)" + @echo " $(YELLOW)make frontend-build$(NC) - TypeScript type-check + production build (mirrors CI)" + @echo " $(YELLOW)make lint$(NC) - Ruff lint+format-check (backend) + ESLint + type-check (frontend)" + @echo "" + @echo "$(GREEN)⚙️ SETUP$(NC)" + @echo " $(YELLOW)make bootstrap$(NC) - Terraform bootstrap (ECR, IAM, S3, optional DynamoDB lock table)" + @echo " $(YELLOW)make setup-env$(NC) - Export env vars for Terraform/deploy (ENV=staging|prod|dev)" @echo "" @echo "$(GREEN)🏗️ TERRAFORM$(NC)" @echo " $(YELLOW)make tf-validate$(NC) - Validate Terraform" - @echo " $(YELLOW)make tf-plan$(NC) - Plan infrastructure (ENV=staging|prod)" - @echo " $(YELLOW)make tf-apply$(NC) - Apply infrastructure (ENV=staging|prod)" - @echo " $(YELLOW)make tf-destroy$(NC) - Destroy infrastructure (ENV=staging|prod)" + @echo " $(YELLOW)make tf-plan$(NC) - Plan infrastructure (ENV=dev|staging|prod)" + @echo " $(YELLOW)make tf-apply$(NC) - Apply infrastructure (ENV=dev|staging|prod)" + @echo " $(YELLOW)make tf-destroy$(NC) - Destroy infrastructure (ENV=dev|staging|prod)" @echo "" @echo "$(GREEN)🐳 DOCKER BUILD & PUSH$(NC)" @echo " $(YELLOW)make docker-build$(NC) - Build Docker images (ENV=staging|prod)" @@ -75,7 +132,7 @@ help: docker-up: @echo "$(GREEN)🐳 Starting Docker Compose services...$(NC)" - cd deploy && docker-compose up -d + cd deploy && docker compose --env-file .env up -d @echo "" @echo "$(GREEN)✅ Services started!$(NC)" @echo "" @@ -88,34 +145,34 @@ docker-up: docker-down: @echo "$(YELLOW)⬇️ Stopping Docker Compose services...$(NC)" - cd deploy && docker-compose down + cd deploy && docker compose --env-file .env down @echo "$(GREEN)✅ Services stopped$(NC)" docker-restart: @echo "$(YELLOW)🔄 Restarting Docker Compose services...$(NC)" - cd deploy && docker-compose restart + cd deploy && docker compose --env-file .env restart @echo "$(GREEN)✅ Services restarted$(NC)" docker-logs: - cd deploy && docker-compose logs -f + cd deploy && docker compose --env-file .env logs -f docker-logs-backend: - cd deploy && docker-compose logs -f backend + cd deploy && docker compose --env-file .env logs -f backend docker-logs-frontend: - cd deploy && docker-compose logs -f frontend + cd deploy && docker compose --env-file .env logs -f frontend docker-logs-db: - cd deploy && docker-compose logs -f db + cd deploy && docker compose --env-file .env logs -f postgres docker-clean: @echo "$(RED)🧹 Removing Docker Compose volumes and containers...$(NC)" - cd deploy && docker-compose down -v + cd deploy && docker compose --env-file .env down -v @echo "$(GREEN)✅ Cleaned$(NC)" docker-ps: @echo "$(BLUE)Docker Compose Services:$(NC)" - cd deploy && docker-compose ps + cd deploy && docker compose --env-file .env ps # ============================================================================ # LOCAL DEVELOPMENT (Without Docker) @@ -123,8 +180,9 @@ docker-ps: install: @echo "$(GREEN)📦 Installing dependencies...$(NC)" - cd backend && pip install -e . && cd .. - cd frontend && npm install && cd .. + cd backend && pip install -e . + cd frontend && npm install + cd .github && npm ci --ignore-scripts @echo "$(GREEN)✅ Dependencies installed!$(NC)" dev: @@ -149,50 +207,78 @@ frontend: # TESTING & CODE QUALITY # ============================================================================ -test: backend-test-unit backend-test-integration frontend-test +ensure-backend-venv: + @if [ ! -x "$(BACKEND_VENV_PY)" ]; then \ + echo "$(RED)❌ Backend virtualenv not found at backend/.venv$(NC)"; \ + echo "$(YELLOW)Create it first: cd backend && python3 -m venv .venv && source .venv/bin/activate$(NC)"; \ + echo "$(YELLOW)Then install deps (dev): pip install -e . pytest pytest-cov pytest-asyncio$(NC)"; \ + exit 1; \ + fi + +ensure-frontend-deps: + @if [ ! -x "frontend/node_modules/.bin/ng" ]; then \ + echo "$(YELLOW)⚠️ Frontend dependencies not found. Installing with npm ci...$(NC)"; \ + cd frontend && npm ci; \ + fi + +test: ensure-backend-venv backend-test-unit backend-test-integration frontend-test @echo "$(GREEN)✅ All tests completed!$(NC)" -backend-test: backend-test-unit backend-test-integration +backend-test: ensure-backend-venv backend-test-unit backend-test-integration @echo "$(GREEN)✅ All backend tests passed!$(NC)" -backend-test-unit: +backend-test-unit: ensure-backend-venv @echo "$(GREEN)🧪 Running backend unit tests (no DB)...$(NC)" - cd backend && python -m pytest tests/unit -m unit -v --tb=short --cov=app --cov-report=html + cd backend && .venv/bin/python -m pytest -c pytest.ini tests/unit -m unit -v --tb=short --cov=app --cov-report=html @echo "$(GREEN)✅ Unit tests passed!$(NC)" @echo "$(BLUE)Coverage report: backend/htmlcov/index.html$(NC)" -backend-test-integration: +backend-test-integration: ensure-backend-venv @echo "$(GREEN)🧪 Running backend integration tests (with real DB)...$(NC)" - cd backend && python -m pytest tests/integration -m integration -v --tb=short + cd backend && .venv/bin/python -m pytest -c pytest.ini tests/integration -m integration -v --tb=short @echo "$(GREEN)✅ Integration tests passed!$(NC)" -backend-test-coverage: +backend-test-coverage: ensure-backend-venv @echo "$(GREEN)🧪 Running all backend tests with coverage report...$(NC)" - cd backend && python -m pytest tests/unit tests/integration -v --tb=short --cov=app --cov-report=html --cov-report=term-missing + cd backend && .venv/bin/python -m pytest -c pytest.ini tests/unit tests/integration -v --tb=short --cov=app --cov-report=html --cov-report=term-missing @echo "$(GREEN)✅ Tests completed!$(NC)" @echo "$(BLUE)Coverage report: backend/htmlcov/index.html$(NC)" -frontend-test: +frontend-test: ensure-frontend-deps @echo "$(GREEN)🧪 Running frontend tests...$(NC)" - cd frontend && npm run test -- --watch=false --coverage + cd frontend && npm exec -- ng test --watch=false --code-coverage @echo "$(GREEN)✅ Frontend tests passed!$(NC)" -backend-validate-tests: +backend-validate-tests: ensure-backend-venv @echo "$(GREEN)✓ Validating test configuration...$(NC)" - cd backend && python validate_tests.py + cd backend && .venv/bin/python validate_tests.py @echo "$(GREEN)✅ Test configuration valid!$(NC)" lint: @echo "$(GREEN)🔍 Running linters...$(NC)" - cd backend && ruff check app/ --output-format=github + cd backend && poetry run ruff check app/ --output-format=github + cd backend && poetry run ruff format app/ --check cd frontend && npm run lint + cd frontend && npm run type-check @echo "$(GREEN)✅ Linting complete!$(NC)" -format: - @echo "$(GREEN)✨ Formatting code...$(NC)" - cd backend && black app/ && ruff check app/ --fix - cd frontend && npm run format - @echo "$(GREEN)✅ Code formatted!$(NC)" +frontend-build: + @echo "$(GREEN)🔨 Running TypeScript type-check and production build...$(NC)" + cd frontend && npm run type-check + cd frontend && npm run build + @echo "$(GREEN)✅ Frontend build complete!$(NC)" + +# ============================================================================ +# ONE-TIME BOOTSTRAP & ENV SETUP +# ============================================================================ + +bootstrap: + @echo "$(GREEN)🚀 Bootstrapping AWS infrastructure (one-time setup)...$(NC)" + @bash scripts/bootstrap.sh + +setup-env: + @echo "$(GREEN)📝 Setting up environment for ENV=$(ENV)...$(NC)" + @ENV=$(ENV) AWS_REGION=$(AWS_REGION) TERRAFORM_STATE_BUCKET=$(TERRAFORM_STATE_BUCKET) TERRAFORM_LOCK_TABLE=$(TERRAFORM_LOCK_TABLE) bash scripts/setup-env.sh # ============================================================================ # TERRAFORM OPERATIONS @@ -203,19 +289,16 @@ tf-validate: @bash scripts/terraform-validate.sh tf-plan: - @if [ -z "$(ENV)" ]; then echo "$(RED)❌ ENV not set. Usage: make tf-plan ENV=staging$(NC)"; exit 1; fi @echo "$(GREEN)📋 Planning Terraform for ENV=$(ENV)...$(NC)" - @bash scripts/terraform-plan.sh + @ENV=$(ENV) AWS_REGION=$(AWS_REGION) TERRAFORM_STATE_BUCKET=$(TERRAFORM_STATE_BUCKET) TERRAFORM_LOCK_TABLE=$(TERRAFORM_LOCK_TABLE) bash scripts/terraform-plan.sh tf-apply: - @if [ -z "$(ENV)" ]; then echo "$(RED)❌ ENV not set. Usage: make tf-apply ENV=staging$(NC)"; exit 1; fi @echo "$(GREEN)🚀 Applying Terraform for ENV=$(ENV)...$(NC)" - @bash scripts/terraform-apply.sh + @ENV=$(ENV) AWS_REGION=$(AWS_REGION) TERRAFORM_STATE_BUCKET=$(TERRAFORM_STATE_BUCKET) TERRAFORM_LOCK_TABLE=$(TERRAFORM_LOCK_TABLE) bash scripts/terraform-apply.sh tf-destroy: - @if [ -z "$(ENV)" ]; then echo "$(RED)❌ ENV not set. Usage: make tf-destroy ENV=staging$(NC)"; exit 1; fi @echo "$(RED)⚠️ Destroying Terraform infrastructure for ENV=$(ENV)...$(NC)" - @bash scripts/terraform-destroy.sh + @ENV=$(ENV) AWS_REGION=$(AWS_REGION) TERRAFORM_STATE_BUCKET=$(TERRAFORM_STATE_BUCKET) TERRAFORM_LOCK_TABLE=$(TERRAFORM_LOCK_TABLE) bash scripts/terraform-destroy.sh # ============================================================================ # DOCKER BUILD & PUSH @@ -223,13 +306,21 @@ tf-destroy: docker-build: @if [ -z "$(ENV)" ]; then echo "$(RED)❌ ENV not set. Usage: make docker-build ENV=staging$(NC)"; exit 1; fi - @echo "$(GREEN)🔨 Building Docker images for ENV=$(ENV)...$(NC)" - @bash scripts/docker-build.sh + @echo "$(GREEN)🔨 Building Docker images for ENV=$(ENV) (platform=$(DOCKER_PLATFORM))...$(NC)" + docker buildx build --platform $(DOCKER_PLATFORM) --provenance=false --sbom=false --build-arg BUILD_ENV=$(ENV) -t mypythonproject1/backend:$(ENV) --load ./backend + docker buildx build --platform $(DOCKER_PLATFORM) --provenance=false --sbom=false --build-arg BUILD_ENV=$(ENV) -t mypythonproject1/frontend:$(ENV) --load ./frontend + @echo "$(GREEN)✅ Docker images built$(NC)" docker-push: @if [ -z "$(ENV)" ]; then echo "$(RED)❌ ENV not set. Usage: make docker-push ENV=staging$(NC)"; exit 1; fi - @echo "$(GREEN)📤 Pushing Docker images for ENV=$(ENV)...$(NC)" - @bash scripts/docker-build.sh push + @if [ -z "$(AWS_ACCOUNT_ID)" ]; then echo "$(RED)❌ Unable to detect AWS_ACCOUNT_ID — check AWS credentials$(NC)"; exit 1; fi + @echo "$(GREEN)📤 Pushing Docker images to ECR for ENV=$(ENV)...$(NC)" + aws ecr get-login-password --region $(AWS_REGION) | docker login --username AWS --password-stdin $(AWS_ACCOUNT_ID).dkr.ecr.$(AWS_REGION).amazonaws.com + docker tag mypythonproject1/backend:$(ENV) $(AWS_ACCOUNT_ID).dkr.ecr.$(AWS_REGION).amazonaws.com/mypythonproject1/backend:$(ENV) + docker tag mypythonproject1/frontend:$(ENV) $(AWS_ACCOUNT_ID).dkr.ecr.$(AWS_REGION).amazonaws.com/mypythonproject1/frontend:$(ENV) + docker push $(AWS_ACCOUNT_ID).dkr.ecr.$(AWS_REGION).amazonaws.com/mypythonproject1/backend:$(ENV) + docker push $(AWS_ACCOUNT_ID).dkr.ecr.$(AWS_REGION).amazonaws.com/mypythonproject1/frontend:$(ENV) + @echo "$(GREEN)✅ Images pushed to ECR$(NC)" # ============================================================================ # ECS DEPLOYMENT @@ -238,8 +329,18 @@ docker-push: ecs-deploy: @if [ -z "$(ENV)" ] || [ -z "$(IMAGE_TAG)" ]; then \ echo "$(RED)❌ Missing parameters. Usage: make ecs-deploy ENV=staging IMAGE_TAG=sha-abc123$(NC)"; exit 1; fi - @echo "$(GREEN)🚀 Deploying to ECS (ENV=$(ENV), IMAGE_TAG=$(IMAGE_TAG))...$(NC)" - @bash scripts/ecs-deploy.sh + @echo "$(GREEN)🚀 Deploying to ECS (cluster=$(ECS_CLUSTER), ENV=$(ENV), IMAGE_TAG=$(IMAGE_TAG))...$(NC)" + aws ecs update-service \ + --cluster $(ECS_CLUSTER) \ + --service $(ECS_SERVICE_BACKEND) \ + --force-new-deployment \ + --region $(AWS_REGION) + aws ecs update-service \ + --cluster $(ECS_CLUSTER) \ + --service $(ECS_SERVICE_FRONTEND) \ + --force-new-deployment \ + --region $(AWS_REGION) + @echo "$(GREEN)✅ ECS deployment triggered (run 'make setup-env ENV=$(ENV)' to verify cluster/service names)$(NC)" # ============================================================================ # FULL DEPLOYMENT ORCHESTRATION diff --git a/README.md b/README.md index fd525f6..aa5f62e 100644 --- a/README.md +++ b/README.md @@ -1,534 +1,154 @@ # MyPythonProject1 -A production-ready full-stack web application built with FastAPI, Angular, PostgreSQL, and deployed to AWS ECS Fargate via Terraform and GitHub Actions. +Production-ready full-stack app with FastAPI + Angular + PostgreSQL on AWS ECS Fargate, provisioned by Terraform and delivered through GitHub Actions. ---- +## High-level architecture -## Table of Contents +- Frontend: Angular app served by Nginx in ECS Fargate +- Backend: FastAPI service in ECS Fargate +- Database: PostgreSQL on AWS RDS +- Networking: ALB + VPC (public/private/db subnets) +- Runtime secrets: AWS Secrets Manager +- Infra state: S3 backend with native lockfile locking (`use_lockfile=true`) +- Images: Amazon ECR for both staging and production deploy flows -1. [Architecture Overview](#architecture-overview) -2. [Repository Structure](#repository-structure) -3. [Local Development Setup](#local-development-setup) -4. [Environment Configuration](#environment-configuration) -5. [Running Tests](#running-tests) -6. [CI/CD — GitHub Actions](#cicd--github-actions) -7. [Deploying to AWS](#deploying-to-aws) -8. [Conventional Commits & Versioning](#conventional-commits--versioning) -9. [Makefile Reference](#makefile-reference) -10. [Dependency Updates (Dependabot)](#dependency-updates-dependabot) +## Repository structure ---- - -## Architecture Overview - -``` -Internet - │ - ▼ -Route 53 (DNS) - │ - ▼ -Application Load Balancer ──────────────┐ -(HTTPS :443, HTTP :80 → redirect) │ - │ │ - ├─▶ ECS Fargate — Backend │ - │ (FastAPI :8000, private subnet) │ - │ │ │ - │ ▼ │ - │ RDS PostgreSQL 16 │ - │ (private DB subnet, Multi-AZ) │ - │ │ - └─▶ ECS Fargate — Frontend │ - (Nginx serving Angular, :80) │ - │ - AWS Secrets Manager ◄─────────────┘ - (DB password, JWT secret) - S3 + DynamoDB (Terraform state) - ECR (Docker images — production) - GHCR (Docker images — staging/CI) -``` - -### Technology Stack - -| Layer | Technology | -|---|---| -| Backend API | Python 3.12, FastAPI, SQLAlchemy, Alembic, Uvicorn | -| Frontend | Angular 19, TypeScript 5.6, Tailwind CSS 4 | -| Database | PostgreSQL 16 (RDS, Multi-AZ in production) | -| Infrastructure | Terraform 1.5, AWS ECS Fargate, ALB, VPC | -| CI | GitHub Actions — lint, test, security scan, tf plan | -| CD (staging) | GitHub Actions — build GHCR image, tf apply, ECS deploy | -| CD (production) | GitHub Actions — semantic-release, ECR image, ECS deploy | -| Secrets (runtime) | AWS Secrets Manager | -| Secrets (CI/CD) | GitHub Environment secrets | - ---- - -## Repository Structure - -``` +```text . -├── README.md ← you are here -├── Makefile ← unified developer CLI -├── package.json ← commitlint + semantic-release (CI tooling) -├── .commitlintrc.yml ← conventional commit rules -├── .releaserc.json ← semantic-release config -│ -├── backend/ ← FastAPI application -│ ├── app/ -│ │ ├── api/ ← route handlers (users, games, health) -│ │ ├── core/ ← config, security (JWT), logging -│ │ ├── db/ ← SQLAlchemy engine, session, base -│ │ ├── models/ ← ORM models -│ │ ├── schemas/ ← Pydantic request/response schemas -│ │ └── services/ ← business logic layer -│ ├── alembic/ ← database migration scripts -│ ├── tests/ -│ │ ├── unit/ ← fully mocked, no DB -│ │ └── integration/ ← real DB (auto-rollback) -│ ├── pyproject.toml -│ └── README.md ← backend-specific setup -│ -├── frontend/ ← Angular SPA -│ ├── src/app/ -│ │ ├── components/ ← feature components (login, dashboard, game) -│ │ ├── services/ ← API client, auth, game, user services -│ │ ├── core/ ← auth guard, HTTP interceptor -│ │ └── types/ ← TypeScript interfaces -│ ├── package.json -│ └── README.md ← frontend-specific setup -│ -├── infra/ ← Terraform infrastructure -│ ├── main.tf ← module composition + S3 backend -│ ├── variables.tf -│ ├── outputs.tf -│ ├── providers.tf -│ ├── modules/ ← network, rds, ecs, alb, iam -│ ├── envs/ -│ │ ├── staging.tfvars -│ │ └── prod.tfvars -│ └── README.md ← infra-specific setup and module reference -│ -├── config/ -│ ├── .env.dev ← local Docker Compose only (no secrets) -│ ├── .env.test ← test env + GitHub Actions CI vars -│ ├── .env.staging ← staging non-secret config + CI tooling -│ └── .env.production ← production non-secret config -│ -├── deploy/ -│ └── docker-compose.yml ← local full-stack development -│ -├── .github/ -│ ├── dependabot.yml ← automated dependency PRs -│ └── workflows/ -│ ├── ci.yml ← CI gate (lint, test, scan, tf plan) -│ ├── staging.yml ← CD staging (push → develop) -│ ├── release.yml ← CD production (push tag → v*) -│ └── dependabot-auto-merge.yml -│ -├── scripts/ ← shell helpers called by Makefile -└── docs/ ← architecture, onboarding, test guide +├── backend/ # FastAPI service + alembic + tests +├── frontend/ # Angular application +├── infra/ # Terraform root + modules + env tfvars +├── deploy/ # docker-compose local stack +├── config/ # env templates and ops guides +├── docs/ # architecture / onboarding / testing docs +└── .github/ # workflows and composite CI/CD actions ``` ---- - -## Local Development Setup +## Local development ### Prerequisites -| Tool | Minimum Version | Install | -|---|---|---| -| Docker Desktop | 24+ | https://docs.docker.com/desktop/ | -| Python | 3.12+ | `brew install python` | -| Poetry | 1.8+ | `pip install poetry` | -| Node.js | 20 LTS | `brew install node` | -| Terraform | 1.5+ | `brew install terraform` | -| AWS CLI | v2 | `brew install awscli` | -| Make | any | pre-installed on macOS/Linux | +- Docker Desktop +- Python 3.12+ +- Poetry +- Node.js 20+ +- Make -### 1. Clone the repository - -```bash -git clone https://github.com/your-org/mypythonproject1.git -cd mypythonproject1 -``` - -### 2. Install all dependencies +### Start full stack ```bash make install -# Equivalent to: -# cd backend && poetry install -# cd frontend && npm ci -# npm ci (root — installs commitlint + semantic-release) -``` - -### 3. Start with Docker Compose (recommended) - -This spins up PostgreSQL, the FastAPI backend, and the Angular dev server together: - -```bash -cp config/.env.dev deploy/.env # local env vars (safe, no secrets) +cp config/.env.dev deploy/.env docker compose -f deploy/docker-compose.yml up --build ``` -| Service | URL | -|---|---| -| Frontend | http://localhost:4200 | -| Backend API | http://localhost:8000 | -| OpenAPI docs | http://localhost:8000/docs | -| Health check | http://localhost:8000/health | -| PostgreSQL | localhost:5432 | +URLs: -### 4. Run backend only (faster iteration) +- Frontend: http://localhost:4200 +- Backend: http://localhost:8000 +- API docs: http://localhost:8000/docs +- Health: http://localhost:8000/health -```bash -# Terminal 1 — start postgres -docker compose -f deploy/docker-compose.yml up postgres -d - -# Terminal 2 — start backend -make backend -# visits http://localhost:8000/docs - -# Terminal 3 — start frontend -make frontend -# visits http://localhost:4200 -``` - -### 5. Apply database migrations - -```bash -cd backend -poetry run alembic upgrade head -``` - ---- - -## Environment Configuration - -All non-secret configuration lives in `config/`. Secrets are **never stored in files** — they are injected at runtime via AWS Secrets Manager (production) or GitHub Environment secrets (CI/CD). - -| File | Used by | Contents | -|---|---|---| -| `config/.env.dev` | Docker Compose local only | DB host, ports, debug=true | -| `config/.env.test` | pytest + GitHub Actions CI | test DB settings, CI tooling vars | -| `config/.env.staging` | GitHub Actions staging CD | staging endpoints, ECS names | -| `config/.env.production` | GitHub Actions production CD | production endpoints, ECS names | - -**Pattern used in GitHub Actions to load an env file:** -```yaml -- name: Load environment - run: grep -v '^\s*#' config/.env.test | grep -v '^\s*$' >> $GITHUB_ENV -``` - -**Never put these in config files:** -- Database passwords -- JWT secret keys -- AWS access keys -- API tokens - ---- - -## Running Tests +## Testing ```bash -# All tests make test - -# Backend only make backend-test - -# Backend — unit tests only (fast, no DB required) -cd backend && poetry run pytest tests/unit -m unit -v - -# Backend — integration tests only (requires running postgres) -cd backend && poetry run pytest tests/integration -m integration -v - -# With coverage report -cd backend && poetry run pytest --cov=app --cov-report=html -open backend/htmlcov/index.html - -# Frontend make frontend-test ``` -See [docs/TEST_ARCHITECTURE.md](docs/TEST_ARCHITECTURE.md) for the full testing strategy. - ---- - -## CI/CD — GitHub Actions +Backend split: -Three workflows handle the full CI/CD pipeline: - -``` -Feature branch → PR → develop → main - │ │ │ │ - │ │ │ └─ release.yml (semantic-release → v* tag → prod deploy) - │ │ └───────── staging.yml (build → tf apply staging → ECS deploy) - │ └──────────────── ci.yml (lint, test, scan, tf plan — quality gate) - └───────────────────────────── ci.yml (same, runs on PR) +```bash +cd backend +poetry run pytest tests/unit -m unit -v +poetry run pytest tests/integration -m integration -v ``` -### Workflow 1 — `ci.yml` (CI Gate) - -**Triggers:** `pull_request` → `main`/`develop`, `push` → `main`/`develop` +## CI/CD -| Job | What it does | -|---|---| -| `commitlint` | Validates all PR commits follow Conventional Commits spec | -| `changes` | Detects which paths changed (backend/frontend/infra) to skip unaffected jobs | -| `backend-ci` | Ruff lint, unit tests, integration tests against postgres service container | -| `frontend-ci` | ESLint, TypeScript type-check, Angular build | -| `security-scan` | Trivy filesystem scan (SARIF → GitHub Security), GitGuardian secret scan | -| `dependency-audit` | Snyk for Python + Node.js at `--severity-threshold=high` | -| `terraform-plan` | `terraform fmt` check, `validate`, `plan` for staging + prod (PR only, never applies) | -| `quality-gate` | Aggregates all results; single required status check for branch protection | +Detailed reference: `.github/GITHUB_ACTIONS_CICD.md` -### Workflow 2 — `staging.yml` (CD Staging) +- `ci.yml` + - Trigger: PR/push on `main` and `develop` + - Runs lint/tests/security/dependency audit + Terraform fmt/validate/plan + - Does not deploy +- `staging.yml` + - Trigger: successful CI workflow on `develop` or manual dispatch + - Builds/pushes backend+frontend images to ECR + - Applies Terraform for staging + - Forces ECS rollout and runs smoke test +- `release.yml` + - Flow 1: successful CI on `main` runs semantic-release + - Flow 2: `v*` tag builds/pushes ECR images, applies prod Terraform, deploys ECS, runs smoke test -**Triggers:** `push` to `develop` +## Environment configuration -| Job | What it does | -|---|---| -| `build` | Builds backend + frontend Docker images, pushes to GHCR tagged `staging` + `staging-` | -| `scan` | Trivy image scan (warn only, non-blocking for staging) | -| `terraform-staging` | `terraform apply` with `envs/staging.tfvars`, remote S3 state | -| `deploy-staging` | `aws ecs update-service --force-new-deployment`, waits for stability | -| `smoke-test` | Hits `$APP_URL/health`, fails the workflow if not HTTP 200 | +Non-secret configuration files: -### Workflow 3 — `release.yml` (CD Production) +- `config/.env.dev` (local docker compose) +- `config/.env.test` (tests/CI) +- `config/.env.staging` (staging workflow runtime config) +- `config/.env.production` (reference values) -**Triggers:** `push` to `main` → triggers semantic-release; the created `v*` tag → triggers production deploy +Operational docs: -| Job | What it does | -|---|---| -| `autoversion` | Analyses commits since last tag, bumps semver, writes `CHANGELOG.md`, creates GitHub Release + git tag | -| `build-production` | Builds images, pushes to **ECR** tagged `vX.Y.Z`, `latest`, `` | -| `scan-production` | Trivy image scan — **exits 1 on CRITICAL** (hard block for production) | -| `terraform-production` | `terraform apply` with `envs/prod.tfvars`, requires manual approval via GitHub Environment | -| `deploy-production` | ECS task definition render + deploy, waits for stability | -| `smoke-test-production` | Health check against production URL | +- `config/environment-setup.md` +- `config/secrets-management.md` ---- +## Manual infrastructure commands -## Deploying to AWS - -### First-time AWS Bootstrap - -Before any deployment you need to create the Terraform remote state resources and GitHub OIDC provider. Run this once per AWS account: - -```bash -# 1. Configure AWS CLI -aws configure -# AWS Access Key ID: ... -# AWS Secret Access Key: ... -# Default region: us-east-1 - -# 2. Create S3 bucket for Terraform state (replace with your bucket name) -aws s3api create-bucket \ - --bucket myproject-terraform-state \ - --region us-east-1 - -aws s3api put-bucket-versioning \ - --bucket myproject-terraform-state \ - --versioning-configuration Status=Enabled - -aws s3api put-bucket-encryption \ - --bucket myproject-terraform-state \ - --server-side-encryption-configuration \ - '{"Rules":[{"ApplyServerSideEncryptionByDefault":{"SSEAlgorithm":"AES256"}}]}' - -# 3. Create DynamoDB table for state locking -aws dynamodb create-table \ - --table-name terraform-locks \ - --attribute-definitions AttributeName=LockID,AttributeType=S \ - --key-schema AttributeName=LockID,KeyType=HASH \ - --billing-mode PAY_PER_REQUEST \ - --region us-east-1 - -# 4. Create GitHub OIDC provider in IAM (run once per account) -aws iam create-open-id-connect-provider \ - --url https://token.actions.githubusercontent.com \ - --client-id-list sts.amazonaws.com \ - --thumbprint-list 6938fd4d98bab03faadb97b34396831e3780aea1 -``` - -### GitHub Repository Secrets & Variables - -Go to **Settings → Secrets and variables** in your GitHub repo and configure: - -**Repository Variables** (non-secret, visible in logs): -| Variable | Example value | Used by | -|---|---|---| -| `DATABASE_USER` | `postgres` | `ci.yml` postgres service container | -| `DATABASE_NAME` | `testdb` | `ci.yml` postgres service container | -| `DATABASE_PORT` | `5432` | `ci.yml` postgres service container | - -**Repository Secrets** (encrypted, never logged): -| Secret | Description | -|---|---| -| `GITGUARDIAN_API_KEY` | GitGuardian secret scanning token | -| `SNYK_TOKEN` | Snyk vulnerability scanning token | - -**`staging` Environment Secrets** (Settings → Environments → staging): -| Secret | Description | -|---|---| -| `AWS_ROLE_TO_ASSUME` | ARN of IAM role for staging (`arn:aws:iam::123456:role/github-staging`) | -| `TERRAFORM_STATE_BUCKET` | S3 bucket name for Terraform state | -| `TERRAFORM_LOCK_TABLE` | DynamoDB table name for state locks | -| `DATABASE_PASSWORD` | RDS staging database password | - -**`production` Environment Secrets** (Settings → Environments → production): -| Secret | Description | -|---|---| -| `AWS_ROLE_TO_ASSUME` | ARN of IAM role for production | -| `AWS_REGION` | `us-east-1` | -| `TERRAFORM_STATE_BUCKET` | S3 bucket name | -| `TERRAFORM_LOCK_TABLE` | DynamoDB table name | -| `APP_URL` | `https://myproject.com` | -| `ECS_CLUSTER` | `myproject-prod-cluster` | -| `ECS_SERVICE_PREFIX` | `myproject-prod` | -| `ECS_TASK_DEFINITION` | path to task def JSON | -| `TF_VERSION` | `1.5.0` | - -> **Tip:** Set a required reviewer on the `production` environment under Settings → Environments → Required reviewers. This creates a manual approval gate before `terraform-production` and `deploy-production` run. - -### Deploying to Staging (automatic) +From repository root: ```bash -# Simply merge a PR into develop — staging.yml triggers automatically -git checkout develop -git merge feature/my-feature -git push origin develop - -# Watch the deployment at: -# https://github.com/your-org/mypythonproject1/actions/workflows/staging.yml -``` - -### Deploying to Production (tag-driven) - -```bash -# Merge your release branch into main — semantic-release creates the tag automatically -git checkout main -git merge develop -git push origin main - -# semantic-release will: -# 1. Analyse commits since last tag -# 2. Determine bump: feat → minor, fix → patch, BREAKING CHANGE → major -# 3. Write CHANGELOG.md and commit it -# 4. Create git tag (e.g. v1.3.0) -# 5. Create GitHub Release - -# The v1.3.0 tag push triggers release.yml → production deploy -# Monitor at: -# https://github.com/your-org/mypythonproject1/actions/workflows/release.yml -``` - -### Manual Terraform operations - -```bash -# Staging — plan only (safe, read-only) +make tf-validate ENV=staging make tf-plan ENV=staging - -# Staging — apply (will modify AWS resources) make tf-apply ENV=staging +``` -# Production — always plan first and review carefully -make tf-plan ENV=prod -make tf-apply ENV=prod +Destroy (destructive): -# Destroy (destructive — requires confirmation) +```bash make tf-destroy ENV=staging ``` ---- - -## Conventional Commits & Versioning - -All commits must follow [Conventional Commits](https://www.conventionalcommits.org/) spec. This is enforced by `commitlint` on every PR. +## AWS bootstrap -``` -(): - -[optional body] +At minimum, create: -[optional footer(s)] -``` +1. S3 bucket for Terraform state +2. GitHub OIDC provider in IAM +3. IAM roles assumed by GitHub Actions environments (`staging`, `production`) +4. ECR repositories for backend/frontend images -### Commit types and their effect on version bumps - -| Type | Semver bump | Example | -|---|---|---| -| `feat` | minor (`1.2.0 → 1.3.0`) | `feat(auth): add OAuth2 login` | -| `fix` | patch (`1.2.0 → 1.2.1`) | `fix(api): handle null user id` | -| `perf` | patch | `perf(db): add index on user email` | -| `refactor` | patch | `refactor(services): extract user factory` | -| `revert` | patch | `revert: feat(auth): add OAuth2 login` | -| `docs` | no release | `docs: update deployment guide` | -| `style` | no release | `style: fix trailing whitespace` | -| `test` | no release | `test: add user service unit tests` | -| `build` | no release | `build(deps): bump fastapi to 0.128` | -| `ci` | no release | `ci: add trivy scan step` | -| `chore` | no release | `chore: clean up old env files` | -| `BREAKING CHANGE` footer | major (`1.2.0 → 2.0.0`) | `feat!: remove v1 API endpoints` | - -### Example workflow +Run bootstrap: ```bash -# Good commit examples -git commit -m "feat(game): add multiplayer room support" -git commit -m "fix(auth): refresh token not invalidated on logout" -git commit -m "ci: increase backend test timeout to 10 minutes" - -# Breaking change -git commit -m "feat(api)!: rename /users to /accounts - -BREAKING CHANGE: all /users endpoints moved to /accounts" +make bootstrap ``` ---- - -## Makefile Reference +Optional inputs: ```bash -make help # list all targets - -# Development -make install # install backend + frontend + root deps -make dev # start all services via docker compose -make backend # run FastAPI dev server only -make frontend # run Angular dev server only - -# Testing -make test # run all tests -make backend-test # pytest (backend) -make frontend-test # ng test (frontend) -make lint # ruff + eslint -make format # ruff format + prettier - -# Infrastructure -make tf-validate ENV=staging # fmt check + validate -make tf-plan ENV=staging # plan (read-only, safe) -make tf-apply ENV=staging # apply (modifies AWS) -make tf-destroy ENV=staging # destroy (destructive) - -# Docker -make docker-build ENV=staging IMAGE_TAG=v1.0.0 -make docker-push ENV=staging IMAGE_TAG=v1.0.0 +GITHUB_ORG= GITHUB_REPO= AWS_REGION=us-east-1 make bootstrap ``` ---- +See `infra/README.md` for full bootstrap and IAM guidance. -## Dependency Updates (Dependabot) +## Versioning -Dependabot is configured (`.github/dependabot.yml`) to automatically open PRs for: +- Commits follow Conventional Commits +- `release.yml` uses semantic-release to generate version tags and release notes +- Production deploys are triggered by semantic tags (`v*`) -| Ecosystem | Location | Schedule | -|---|---|---| -| Poetry (Python) | `/backend` | Weekly, Mondays | -| npm (Node.js) | `/frontend` | Weekly, Mondays | -| Terraform providers | `/infra` | Weekly, Tuesdays | -| GitHub Actions | `/` | Weekly, Wednesdays | +## Common commands -Dependabot PRs for patch/minor updates are automatically merged by `dependabot-auto-merge.yml` after CI passes. Major version bumps require manual review. +```bash +make help +make lint +make format +make backend +make frontend +make dev +``` diff --git a/backend/Dockerfile b/backend/Dockerfile index 8f1fd60..e4475b7 100644 --- a/backend/Dockerfile +++ b/backend/Dockerfile @@ -1,5 +1,6 @@ # Multi-stage build for optimized production image -FROM python:3.12-slim as builder +ARG PYTHON_VERSION=3.12.1 +FROM python:${PYTHON_VERSION}-slim as builder WORKDIR /build @@ -18,7 +19,7 @@ RUN pip install --upgrade pip && \ poetry install --no-interaction --no-ansi --with dev --no-root # Production stage -FROM python:3.12-slim +FROM python:${PYTHON_VERSION}-slim WORKDIR /app diff --git a/backend/README.md b/backend/README.md index bb67250..43900dd 100644 --- a/backend/README.md +++ b/backend/README.md @@ -1,290 +1,82 @@ # Backend -FastAPI application with PostgreSQL, JWT authentication, SQLAlchemy ORM, and Alembic migrations. +FastAPI backend with SQLAlchemy, Alembic, JWT auth, and pytest. ---- - -## Table of Contents - -1. [Local Setup](#local-setup) -2. [Project Structure](#project-structure) -3. [Configuration](#configuration) -4. [Database Migrations](#database-migrations) -5. [API Reference](#api-reference) -6. [Running Tests](#running-tests) -7. [Docker](#docker) - ---- - -## Local Setup - -### Requirements - -- Python 3.12+ -- Poetry 1.8+ -- PostgreSQL 16 (or run via Docker Compose — recommended) - -### Install dependencies +## Local setup ```bash cd backend poetry install ``` -### Start with Docker Compose (recommended) - -From the project root: +From project root (recommended): ```bash cp config/.env.dev deploy/.env docker compose -f deploy/docker-compose.yml up postgres backend --build ``` +Endpoints: + - API: http://localhost:8000 -- OpenAPI docs: http://localhost:8000/docs -- ReDoc: http://localhost:8000/redoc +- Swagger: http://localhost:8000/docs +- Health: http://localhost:8000/health -### Start manually (if you have a local Postgres) +## Migrations ```bash cd backend - -# Copy and configure environment -cp ../config/.env.dev .env.local -# Edit .env.local — set DATABASE_HOST=localhost - -# Activate the poetry shell -poetry shell - -# Apply migrations -alembic upgrade head - -# Start dev server with hot reload -uvicorn app.main:app --reload --host 0.0.0.0 --port 8000 -``` - ---- - -## Project Structure - -``` -backend/ -├── main.py ← ASGI entry point (imports app from app/main.py) -├── pyproject.toml ← Python dependencies (Poetry) -├── pytest.ini ← pytest markers and options -├── alembic.ini ← Alembic migration config -│ -├── app/ -│ ├── main.py ← FastAPI app instance, lifespan, middleware -│ │ -│ ├── api/ ← Route handlers (thin — validate, delegate to service, return) -│ │ ├── health.py ← GET /health -│ │ ├── user.py ← POST /users, GET /users/me, etc. -│ │ └── game.py ← GET/POST/PUT/DELETE /games -│ │ -│ ├── core/ -│ │ ├── config.py ← pydantic-settings; reads env vars, validates at startup -│ │ ├── security.py ← JWT create/verify, bcrypt/argon2 password hashing -│ │ └── logging.py ← JSON structured logger -│ │ -│ ├── db/ -│ │ ├── base.py ← SQLAlchemy declarative base -│ │ └── session.py ← async engine + session factory, get_db() dependency -│ │ -│ ├── models/ ← SQLAlchemy ORM models (maps to DB tables) -│ │ ├── user.py -│ │ └── game.py -│ │ -│ ├── schemas/ ← Pydantic models (API contract — request/response shapes) -│ │ ├── user.py -│ │ └── game.py -│ │ -│ └── services/ ← Business logic (no HTTP context, no direct DB calls) -│ ├── user_service.py -│ └── game_service.py -│ -├── alembic/ -│ ├── env.py ← Alembic environment configuration -│ ├── script.py.mako ← Migration file template -│ └── versions/ ← Auto-generated migration scripts -│ -└── tests/ - ├── conftest.py ← Shared fixtures (DB engine, session, client, auth) - ├── unit/ ← Fully mocked tests — no DB, no network - │ ├── services/ - │ └── core/ - └── integration/ ← Real DB tests — auto-rollback per test - ├── api/ - └── db/ -``` - ---- - -## Configuration - -All configuration is read by `app/core/config.py` using `pydantic-settings`. It reads variables from the environment (or a `.env` file in development). - -| Variable | Default | Description | -|---|---|---| -| `ENVIRONMENT` | `development` | `development` / `test` / `staging` / `production` | -| `DEBUG` | `false` | Enable debug mode | -| `DATABASE_HOST` | `localhost` | Postgres host | -| `DATABASE_PORT` | `5432` | Postgres port | -| `DATABASE_NAME` | — | Database name | -| `DATABASE_USER` | — | Database user | -| `DATABASE_PASSWORD` | — | **Secret — never in config files** | -| `JWT_SECRET_KEY` | — | **Secret — never in config files** | -| `JWT_ALGORITHM` | `HS256` | JWT signing algorithm | -| `ACCESS_TOKEN_EXPIRE_MINUTES` | `15` | Access token lifetime | -| `REFRESH_TOKEN_EXPIRE_DAYS` | `7` | Refresh token lifetime | - -In production, `DATABASE_PASSWORD` and `JWT_SECRET_KEY` are fetched from **AWS Secrets Manager** at container startup, not from environment files. - -For local development, use `config/.env.dev` (already has safe test values for all required variables). - ---- - -## Database Migrations - -Migrations are managed by Alembic and stored in `alembic/versions/`. - -```bash -# Apply all pending migrations poetry run alembic upgrade head - -# Check current migration state -poetry run alembic current - -# Generate a new migration after changing a model -poetry run alembic revision --autogenerate -m "add score column to games" - -# Roll back one migration -poetry run alembic downgrade -1 - -# Roll back to a specific revision -poetry run alembic downgrade abc123 - -# Show migration history -poetry run alembic history --verbose +poetry run alembic revision --autogenerate -m "describe change" ``` -### Migration workflow - -1. Edit a model in `app/models/` -2. Run `alembic revision --autogenerate -m "description"` to generate the script -3. Review the generated file in `alembic/versions/` — never trust autogenerate blindly -4. Run `alembic upgrade head` to apply -5. Commit both the model change and the migration file together - ---- - -## API Reference - -The full interactive API docs are available at http://localhost:8000/docs when running locally. - -### Health - -``` -GET /health -→ 200 {"status": "ok", "environment": "development"} -``` - -### Authentication - -``` -POST /auth/register -Body: {"email": "user@example.com", "password": "StrongPass1!"} -→ 201 {"id": 1, "email": "user@example.com"} - -POST /auth/login -Body: {"email": "user@example.com", "password": "StrongPass1!"} -→ 200 {"access_token": "eyJ...", "refresh_token": "eyJ...", "token_type": "bearer"} - -POST /auth/refresh -Header: Authorization: Bearer -→ 200 {"access_token": "eyJ...", "token_type": "bearer"} - -POST /auth/logout -Header: Authorization: Bearer -→ 204 -``` - -### Users - -``` -GET /users/me -Header: Authorization: Bearer -→ 200 {"id": 1, "email": "user@example.com", "created_at": "..."} -``` - -### Games - -``` -GET /games ← list all games for authenticated user -POST /games ← create a new game -GET /games/{id} ← get a specific game -PUT /games/{id} ← update a game -DELETE /games/{id} ← delete a game -``` - ---- - -## Running Tests +## Testing ```bash cd backend - -# All tests poetry run pytest - -# Unit tests only (fast, no DB) poetry run pytest tests/unit -m unit -v - -# Integration tests only (requires postgres — start via docker compose) poetry run pytest tests/integration -m integration -v +``` -# With coverage -poetry run pytest --cov=app --cov-report=html --cov-report=term -open htmlcov/index.html - -# Specific test -poetry run pytest tests/unit/services/test_user_service.py::TestUserRegistration::test_register_success -v +Coverage: -# Watch mode (re-run on file changes) -poetry run pytest-watch +```bash +poetry run pytest --cov=app --cov-report=html +open htmlcov/index.html ``` -### Test markers +## Configuration -| Marker | Description | -|---|---| -| `@pytest.mark.unit` | No DB, no network — runs in < 1s per test | -| `@pytest.mark.integration` | Real DB with automatic rollback after each test | +Configuration is loaded from environment variables via `app/core/config.py`. -Unit tests mock all external dependencies. Integration tests use a real postgres container with transaction rollback per test — the DB is never dirty between tests. +Core variables: ---- +- `ENVIRONMENT` +- `DATABASE_HOST` / `DATABASE_PORT` / `DATABASE_NAME` / `DATABASE_USER` +- `DATABASE_PASSWORD` (secret) +- `JWT_SECRET_KEY` (secret) +- `JWT_ALGORITHM` -## Docker +In AWS, secrets come from AWS Secrets Manager through Terraform-provisioned runtime wiring. -```bash -# Build image -docker build -t myproject-backend:local ./backend +## Structure -# Run container (requires postgres) -docker run --rm \ - -e DATABASE_HOST=host.docker.internal \ - -e DATABASE_PORT=5432 \ - -e DATABASE_NAME=myproject \ - -e DATABASE_USER=postgres \ - -e DATABASE_PASSWORD=postgres \ - -p 8000:8000 \ - myproject-backend:local - -# Full stack via docker compose -docker compose -f deploy/docker-compose.yml up --build +```text +backend/ +├── app/ +│ ├── api/ +│ ├── core/ +│ ├── db/ +│ ├── models/ +│ ├── schemas/ +│ └── services/ +├── alembic/ +├── tests/ +└── pyproject.toml ``` -In CI/CD, images are built by GitHub Actions: -- **Staging:** pushed to GHCR tagged `staging-` (by `staging.yml`) -- **Production:** pushed to ECR tagged `vX.Y.Z` + `latest` (by `release.yml`) +## CI/CD notes + +- `ci.yml`: lint + unit/integration tests +- `staging.yml` and `release.yml`: build backend image and push to ECR diff --git a/backend/poetry.lock b/backend/poetry.lock index 2eb633c..3c2bb78 100644 --- a/backend/poetry.lock +++ b/backend/poetry.lock @@ -115,10 +115,7 @@ files = [ ] [package.dependencies] -cffi = [ - {version = ">=1.0.1", markers = "python_version < \"3.14\""}, - {version = ">=2.0.0b1", markers = "python_version >= \"3.14\""}, -] +cffi = {version = ">=1.0.1", markers = "python_version < \"3.14\""} [[package]] name = "asgi-lifespan" @@ -2008,21 +2005,6 @@ pygments = ">=2.13.0,<3.0.0" [package.extras] jupyter = ["ipywidgets (>=7.5.1,<9)"] -[[package]] -name = "rsa" -version = "4.2" -description = "Pure-Python RSA implementation" -optional = false -python-versions = "*" -groups = ["main"] -markers = "python_version >= \"3.14\"" -files = [ - {file = "rsa-4.2.tar.gz", hash = "sha256:aaefa4b84752e3e99bd8333a2e1e3e7a7da64614042bd66f775573424370108a"}, -] - -[package.dependencies] -pyasn1 = ">=0.1.3" - [[package]] name = "rsa" version = "4.9.1" @@ -2030,7 +2012,6 @@ description = "Pure-Python RSA implementation" optional = false python-versions = "<4,>=3.6" groups = ["main"] -markers = "python_version < \"3.14\"" files = [ {file = "rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762"}, {file = "rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75"}, @@ -2334,5 +2315,5 @@ watchdog = ["watchdog (>=2.3)"] [metadata] lock-version = "2.1" -python-versions = ">=3.12" -content-hash = "94cf2beae3873da697f339840b7d588097701ea1093768879f6392eda983c3b8" +python-versions = ">=3.12.1,<3.13" +content-hash = "bdc7224b3e418878293b38ae55cf86156d30ec0d7fb8e6b55346eadbe443c196" diff --git a/backend/pyproject.toml b/backend/pyproject.toml index 55f7885..b0efe2f 100644 --- a/backend/pyproject.toml +++ b/backend/pyproject.toml @@ -7,7 +7,7 @@ authors = [ ] license = {text = "MIT"} readme = "README.md" -requires-python = ">=3.12" +requires-python = ">=3.12.1,<3.13" dependencies = [ "requests (>=2.32.5,<3.0.0)", "fastapi (>=0.128.7,<0.129.0)", diff --git a/config/.env.dev.example b/config/.env.dev.example index c97eab0..c9d0d7a 100644 --- a/config/.env.dev.example +++ b/config/.env.dev.example @@ -32,7 +32,7 @@ DATABASE_POOL_TIMEOUT=30 DATABASE_ECHO=true # Security - Dev Keys (USE ONLY IN LOCAL DEV) -JWT_SECRET_KEY=dev-secret-key-change-in-production +JWT_SECRET_KEY=dev-only-secret-change-me JWT_ALGORITHM=HS256 JWT_EXPIRE_MINUTES=60 JWT_REFRESH_EXPIRE_DAYS=7 @@ -55,7 +55,6 @@ API_BASE_URL=http://localhost:8000 # Logging - Console output for development LOG_FORMAT=text LOG_OUTPUT=console -LOG_LEVEL=DEBUG # AWS - Not needed for local dev AWS_REGION=us-east-1 diff --git a/config/.env.prod.example b/config/.env.prod.example index e9149ab..9193e9d 100644 --- a/config/.env.prod.example +++ b/config/.env.prod.example @@ -1,18 +1,15 @@ # ============================================================================== -# Production Environment Variables +# Production Environment Configuration - Example Template # ============================================================================== -# DO NOT create or commit .env.prod file to git -# ALWAYS use AWS Secrets Manager for production secrets -# ============================================================================== -# This is a template showing what variables are expected in production -# Use: aws secretsmanager get-secret-value --secret-id myproject/prod +# Use this file as a reference for PRODUCTION values. +# Store sensitive values in AWS Secrets Manager / GitHub Secrets. # ============================================================================== ENVIRONMENT=production -PROJECT_NAME=myproject +PROJECT_NAME=mypythonproject1 APP_VERSION=1.0.0 DEBUG=false -LOG_LEVEL=WARN +LOG_LEVEL=WARNING # Backend Server BACKEND_HOST=0.0.0.0 @@ -23,30 +20,30 @@ API_TITLE=MyProject API API_DESCRIPTION=Production Environment API_VERSION=1.0.0 -# Database - AWS RDS (Multi-AZ) +# Database - non-secret connection metadata DATABASE_USER=postgres DATABASE_PASSWORD=*** USE SECRETS MANAGER *** DATABASE_HOST=rds-prod.xxx.us-east-1.rds.amazonaws.com DATABASE_PORT=5432 -DATABASE_NAME=myproject_prod +DATABASE_NAME=myproject_production DATABASE_POOL_SIZE=30 DATABASE_MAX_OVERFLOW=15 DATABASE_POOL_TIMEOUT=30 DATABASE_ECHO=false -# Security - STRONG RANDOM KEYS ONLY +# Security JWT_SECRET_KEY=*** USE SECRETS MANAGER *** JWT_ALGORITHM=HS256 JWT_EXPIRE_MINUTES=30 JWT_REFRESH_EXPIRE_DAYS=7 -# CORS - Production +# CORS ALLOWED_ORIGINS=https://myproject.com,https://www.myproject.com CORS_ALLOW_CREDENTIALS=true CORS_ALLOW_METHODS=GET,POST,PUT,DELETE,OPTIONS,PATCH CORS_ALLOW_HEADERS=Content-Type,Authorization -# Security Headers - Full enforcement +# Security Headers ENABLE_HTTPS_REDIRECT=true ENABLE_HSTS=true HSTS_MAX_AGE=31536000 @@ -56,24 +53,24 @@ SECURE_COOKIES=true FRONTEND_URL=https://myproject.com API_BASE_URL=https://api.myproject.com -# Logging - JSON to CloudWatch +# Logging LOG_FORMAT=json LOG_OUTPUT=console LOG_FILE_PATH=/var/log/app -# AWS Configuration +# AWS AWS_REGION=us-east-1 AWS_ACCESS_KEY_ID=*** USE IAM ROLE *** AWS_SECRET_ACCESS_KEY=*** USE IAM ROLE *** AWS_SECRETS_MANAGER_SECRET_NAME=myproject/prod -# Monitoring - Production +# Monitoring SENTRY_DSN=*** USE SECRETS MANAGER *** DATADOG_API_KEY=*** USE SECRETS MANAGER *** ENABLE_METRICS=true METRICS_PORT=9090 -# Rate Limiting - Strict for production +# Rate Limiting RATE_LIMIT_ENABLED=true RATE_LIMIT_REQUESTS=50 RATE_LIMIT_WINDOW_SECONDS=60 diff --git a/config/.env.staging.example b/config/.env.staging.example index fab64fa..0a9a700 100644 --- a/config/.env.staging.example +++ b/config/.env.staging.example @@ -1,8 +1,8 @@ # ============================================================================== -# Staging Environment Variables +# Staging Environment Configuration - Example Template # ============================================================================== -# Use this file for STAGING environment -# Manage this file via AWS Secrets Manager in production +# Use this file as a reference for STAGING values. +# Manage sensitive values via GitHub Secrets or AWS Secrets Manager. # ============================================================================== ENVIRONMENT=staging @@ -20,9 +20,8 @@ API_TITLE=MyProject API API_DESCRIPTION=Staging Environment API_VERSION=1.0.0 -# Database - AWS RDS +# Database - non-secret connection config (password from GitHub secret) DATABASE_USER=postgres -DATABASE_PASSWORD=changeme DATABASE_HOST=rds-staging.xxx.us-east-1.rds.amazonaws.com DATABASE_PORT=5432 DATABASE_NAME=myproject_staging @@ -31,19 +30,18 @@ DATABASE_MAX_OVERFLOW=10 DATABASE_POOL_TIMEOUT=30 DATABASE_ECHO=false -# Security - Generate strong keys -JWT_SECRET_KEY=changeme_with_strong_key +# Security - algorithm and expiry only (JWT key from GitHub secret) JWT_ALGORITHM=HS256 JWT_EXPIRE_MINUTES=60 JWT_REFRESH_EXPIRE_DAYS=7 -# CORS - Staging +# CORS ALLOWED_ORIGINS=https://staging.myproject.com CORS_ALLOW_CREDENTIALS=true CORS_ALLOW_METHODS=GET,POST,PUT,DELETE,OPTIONS,PATCH CORS_ALLOW_HEADERS=Content-Type,Authorization -# Security Headers - Enabled for staging +# Security Headers ENABLE_HTTPS_REDIRECT=true ENABLE_HSTS=true HSTS_MAX_AGE=31536000 @@ -53,24 +51,25 @@ SECURE_COOKIES=true FRONTEND_URL=https://staging.myproject.com API_BASE_URL=https://api-staging.myproject.com -# Logging - JSON format +# Logging LOG_FORMAT=json LOG_OUTPUT=console LOG_FILE_PATH=/var/log/app -# AWS Configuration +# AWS AWS_REGION=us-east-1 -AWS_ACCESS_KEY_ID=changeme -AWS_SECRET_ACCESS_KEY=changeme AWS_SECRETS_MANAGER_SECRET_NAME=myproject/staging # Monitoring -SENTRY_DSN=https://changeme@sentry.io/1234567 -DATADOG_API_KEY=changeme ENABLE_METRICS=true METRICS_PORT=9090 -# Rate Limiting - Standard for staging +# Rate Limiting RATE_LIMIT_ENABLED=true RATE_LIMIT_REQUESTS=100 RATE_LIMIT_WINDOW_SECONDS=60 + +# CI/CD Workflow Tooling +REGISTRY=ghcr.io +TF_VERSION=1.5.0 +PROJECT_ROOT=. diff --git a/config/environment-setup.md b/config/environment-setup.md index ef01d25..d313370 100644 --- a/config/environment-setup.md +++ b/config/environment-setup.md @@ -1,328 +1,88 @@ # Environment Configuration Guide -How to set up development, staging, and production environments. +How local, staging, and production environments are configured and promoted. -## 🌍 Environment Types +## Environment types -### Development (Local) -- **Purpose**: Developer machines -- **Config**: `.env.local` -- **Approval**: None -- **Persistence**: Local only +- Local: `config/.env.dev` copied to `deploy/.env` +- Staging: `infra/envs/staging.tfvars` + GitHub Environment `staging` +- Production: `infra/envs/prod.tfvars` + GitHub Environment `production` -### Staging -- **Purpose**: Pre-production testing -- **Config**: `infra/envs/staging.tfvars` + GitHub Secrets -- **Approval**: Optional -- **Persistence**: AWS resources (3+ days retention) +## Local development -### Production -- **Purpose**: User-facing application -- **Config**: `infra/envs/prod.tfvars` + GitHub Secrets -- **Approval**: Required (manual) -- **Persistence**: AWS resources (30+ days retention) - -## 📝 Development Setup - -### Step 1: Copy Environment Template ```bash -cp config/.env.example .env.local +cp config/.env.dev deploy/.env +docker compose -f deploy/docker-compose.yml up --build ``` -### Step 2: Edit Local Values -```bash -# Edit with your editor -nano .env.local - -# Or set individual variables -export DATABASE_URL="postgresql://postgres:mypass@localhost:5432/myproject" -``` +Health checks: -### Step 3: Source in Shell ```bash -# Option 1: Manual source -source .env.local - -# Option 2: Automatic (add to .bashrc/.zshrc) -if [ -f "$PWD/.env.local" ]; then - source .env.local -fi +curl http://localhost:8000/health +open http://localhost:4200 ``` -### Step 4: Verify -```bash -# Check variables are loaded -echo $DATABASE_URL -echo $JWT_SECRET_KEY -``` - -### Step 5: Run Locally -```bash -make dev -# Or individually: -make backend -make frontend -``` - -## 🌐 Staging Setup - -### Prerequisites -- AWS account with staging credentials -- Terraform installed -- GitHub CLI installed - -### Step 1: Create Staging Variables -```bash -# Copy Terraform example -cp config/terraform.tfvars.example infra/envs/staging.tfvars - -# Edit with staging values -nano infra/envs/staging.tfvars -``` - -### Step 2: Create GitHub Secrets -```bash -# Set AWS role for GitHub Actions -gh secret set AWS_ROLE_TO_ASSUME \ - --body "arn:aws:iam::123456789012:role/GitHubActionsRole" - -# Set Terraform state bucket -gh secret set TERRAFORM_STATE_BUCKET \ - --body "myproject-tf-state-staging" - -# Set Terraform lock table -gh secret set TERRAFORM_LOCK_TABLE \ - --body "terraform-locks-staging" -``` +## Staging setup -### Step 3: Create AWS Secrets Manager Secrets -```bash -# Database password -aws secretsmanager create-secret \ - --name /myproject/staging/db-password \ - --secret-string "staging-db-password-here" \ - --region us-east-1 +1. Configure `infra/envs/staging.tfvars` +2. Configure GitHub Environment `staging` secrets: + - `AWS_ROLE_TO_ASSUME` + - `TERRAFORM_STATE_BUCKET` + - `TERRAFORM_LOCK_TABLE` (compatibility input; lockfile backend is active) + - `JWT_SECRET_KEY` +3. Ensure `config/.env.staging` includes non-secret values: + - `AWS_REGION` + - `TF_VERSION` +4. Set Environment variable `APP_URL` for smoke tests -# JWT secret -aws secretsmanager create-secret \ - --name /myproject/staging/jwt-secret \ - --secret-string "staging-jwt-secret-here" \ - --region us-east-1 -``` +Deploy flow: -### Step 4: Plan Infrastructure -```bash -make tf-plan ENV=staging -``` +- Automatic on successful CI run for `develop` +- Or manual via `staging.yml` workflow dispatch -### Step 5: Apply Infrastructure -```bash -make tf-apply ENV=staging -``` +## Production setup -### Step 6: Deploy Application -```bash -make deploy ENV=staging IMAGE_TAG=v1.0.0 -``` +1. Configure `infra/envs/prod.tfvars` +2. Configure GitHub Environment `production` secrets: + - `AWS_ROLE_TO_ASSUME` + - `AWS_REGION` + - `TF_VERSION` + - `TERRAFORM_STATE_BUCKET` + - `TERRAFORM_LOCK_TABLE` (compatibility input; lockfile backend is active) + - `JWT_SECRET_KEY` +3. Set Environment variable `APP_URL` +4. Enable required reviewers/approval in GitHub Environment protection rules -## 🔒 Production Setup +Deploy flow: -### Prerequisites -- **CRITICAL**: All team approval processes in place -- Production AWS account -- Production GitHub Secrets -- Backup and disaster recovery plan verified +- Successful CI on `main` runs semantic-release +- Release tag `v*` triggers production apply/deploy/smoke-test flow -### Step 1: Create Production Variables -```bash -cp config/terraform.tfvars.example infra/envs/prod.tfvars +## Terraform backend behavior -nano infra/envs/prod.tfvars -# Important changes: -# - db_instance_class = "db.t4g.small" (or larger) -# - ecs_desired_count = 3 -# - ecs_max_capacity = 10 -# - multi_az = true -# - backup_retention_days = 30 -``` +Active backend locking strategy: -### Step 2: Create GitHub Secrets (Production) ```bash -# Repeat for production (with prod values) -gh secret set AWS_ROLE_TO_ASSUME \ - --body "arn:aws:iam::123456789012:role/GitHubActionsRoleProd" - -gh secret set TERRAFORM_STATE_BUCKET \ - --body "myproject-tf-state-prod" - -gh secret set TERRAFORM_LOCK_TABLE \ - --body "terraform-locks-prod" +terraform init \ + -backend-config="bucket=" \ + -backend-config="key=/terraform.tfstate" \ + -backend-config="region=" \ + -backend-config="use_lockfile=true" ``` -### Step 3: Create AWS Secrets (Production) -```bash -# Production database password (strong!) -aws secretsmanager create-secret \ - --name /myproject/prod/db-password \ - --secret-string "$(openssl rand -base64 32)" \ - --region us-east-1 +`dynamodb_table` is deprecated and should not be used for new setup. -# Production JWT secret -aws secretsmanager create-secret \ - --name /myproject/prod/jwt-secret \ - --secret-string "$(openssl rand -base64 32)" \ - --region us-east-1 -``` +## Manual infrastructure commands -### Step 4: Test Deployment (Staging First!) ```bash -# Always test in staging first -make deploy ENV=staging IMAGE_TAG=v1.0.0 - -# Monitor logs -aws logs tail /aws/ecs/myproject-staging --follow - -# Run smoke tests -# (your test suite here) - -# If everything good, then: -make deploy ENV=prod IMAGE_TAG=v1.0.0 -``` - -## 🔄 Environment-Specific Configuration - -### Development -```bash -# .env.local -DEBUG=true -LOG_LEVEL=DEBUG -ENVIRONMENT=local -DATABASE_URL=postgresql://postgres:local@localhost:5432/myproject -JWT_EXPIRATION_HOURS=24 -CORS_ORIGINS=["http://localhost:4200"] -``` - -### Staging -```bash -# infra/envs/staging.tfvars -environment = "staging" -ecs_desired_count = 2 -ecs_min_capacity = 1 -ecs_max_capacity = 4 -db_instance_class = "db.t4g.micro" -backup_retention_days = 7 -multi_az = false -``` - -### Production -```bash -# infra/envs/prod.tfvars -environment = "prod" -ecs_desired_count = 3 -ecs_min_capacity = 2 -ecs_max_capacity = 10 -db_instance_class = "db.t4g.small" -backup_retention_days = 30 -multi_az = true -``` - -## 📊 Database Configuration per Environment - -### Development (Local) -- Engine: PostgreSQL 16 (local) -- Storage: 1GB -- Backup: None -- Replication: None - -### Staging -- Engine: PostgreSQL 16 (AWS RDS) -- Storage: 20GB -- Backup: 7 days -- Replication: Single AZ - -### Production -- Engine: PostgreSQL 16 (AWS RDS) -- Storage: 100GB+ -- Backup: 30 days (incremental) -- Replication: Multi-AZ (high availability) - -## 🐳 Docker Configuration per Environment - -### Development -```bash -# Uses local docker-compose -make dev - -# Exposes: -# - Backend: http://localhost:8000 -# - Frontend: http://localhost:4200 -# - Database: localhost:5432 -``` - -### Staging/Production -```bash -# Uses ECR images -make docker-build ENV=staging IMAGE_TAG=v1.0.0 -make docker-push ENV=staging - -# Deployed to: -# ECS Cluster → Fargate tasks → ALB → CloudFront -``` - -## ✅ Pre-Deployment Checklist - -### Before Staging Deployment -- [ ] All tests passing locally (`make test`) -- [ ] Code formatted (`make format`) -- [ ] No linting errors (`make lint`) -- [ ] Staging secrets created in AWS Secrets Manager -- [ ] GitHub Secrets set for staging -- [ ] Terraform plan reviewed (`make tf-plan ENV=staging`) - -### Before Production Deployment -- [ ] All staging tests passing -- [ ] App running stable in staging (24+ hours) -- [ ] Production secrets created -- [ ] Approval from team lead obtained -- [ ] Backup of production database verified -- [ ] Rollback plan documented -- [ ] Monitoring and alerts configured - -## 🚨 Troubleshooting - -### Environment Variable Not Found -```bash -# Check if sourced -echo $DATABASE_URL - -# Source manually -source .env.local - -# Check .env.local exists -ls -la .env.local -``` - -### Different Config Between Envs -```bash -# Check which config is loaded -make tf-plan ENV=staging # Should use staging.tfvars - -# Verify path -cat infra/envs/staging.tfvars -``` - -### Secrets Not Injected -```bash -# Check in ECS console: -# Task Definitions → View JSON → containerDefinitions → environment - -# Or via AWS CLI: -aws ecs describe-task-definition --task-definition myproject-backend:1 \ - --query 'taskDefinition.containerDefinitions[0].environment' +make tf-validate ENV=staging +make tf-plan ENV=staging +make tf-apply ENV=staging ``` -## 📚 Related Docs +## Pre-deploy checklist -- [secrets-management.md](secrets-management.md) - How secrets are handled -- [../docs/DEPLOYMENT_GUIDE.md](../docs/DEPLOYMENT_GUIDE.md) - Deployment procedures -- [../docs/TROUBLESHOOTING.md](../docs/TROUBLESHOOTING.md) - Common issues +- Tests pass (`make test`) +- Lint passes (`make lint`) +- Required GitHub Environment secrets and vars are present +- Terraform plan reviewed for target environment diff --git a/config/secrets-management.md b/config/secrets-management.md index dfc072a..71abcba 100644 --- a/config/secrets-management.md +++ b/config/secrets-management.md @@ -1,265 +1,78 @@ -# Secrets Management - Complete Guide +# Secrets Management Guide -This document explains how secrets are managed across development, CI/CD, and production environments. +This repository separates runtime application secrets from CI/CD orchestration secrets. -## 🔐 Core Principle +## Core rules -**Secrets never appear in code.** Instead: -- **Development**: Local `.env.local` files (gitignored) -- **CI/CD**: GitHub Secrets (encrypted) -- **Runtime**: AWS Secrets Manager (encrypted) +- Never commit secrets to git +- Keep runtime secrets in AWS Secrets Manager +- Keep CI/CD orchestration secrets in GitHub Secrets (repo/environment) +- Keep only non-secret configuration in `config/.env.*` -## Secrets Types & Where They Live +## Secret locations by scope -### 1. GitHub Secrets (CI/CD Pipeline) +### GitHub Secrets (CI/CD) -Used by GitHub Actions workflows. Stored in repository settings. +Repository-level (used by `ci.yml`): -**Required Secrets:** -``` -AWS_ROLE_TO_ASSUME # ARN of GitHubActionsRole -TERRAFORM_STATE_BUCKET # S3 bucket for Terraform state -TERRAFORM_LOCK_TABLE # DynamoDB table for state locking -``` +- `DATABASE_USER` +- `DATABASE_PASSWORD` +- `DATABASE_NAME` +- `DATABASE_PORT` +- `AWS_ROLE_TO_ASSUME` +- `GITGUARDIAN_API_KEY` +- `SNYK_TOKEN` -**Setting Up:** -```bash -# Using GitHub CLI -gh secret set AWS_ROLE_TO_ASSUME --body "arn:aws:iam::123456789012:role/GitHubActionsRole" -gh secret set TERRAFORM_STATE_BUCKET --body "myproject-tf-state-staging" -gh secret set TERRAFORM_LOCK_TABLE --body "terraform-locks-staging" -``` +Environment `staging`: -**In Workflows:** -```yaml -- name: Configure AWS credentials - uses: aws-actions/configure-aws-credentials@v4 - with: - role-to-assume: ${{ secrets.AWS_ROLE_TO_ASSUME }} -``` +- `AWS_ROLE_TO_ASSUME` +- `TERRAFORM_STATE_BUCKET` +- `TERRAFORM_LOCK_TABLE` (compatibility input) +- `JWT_SECRET_KEY` -### 2. AWS Secrets Manager (Runtime) +Environment `production`: -Stores application secrets that are injected into ECS tasks. +- `AWS_ROLE_TO_ASSUME` +- `AWS_REGION` +- `TF_VERSION` +- `TERRAFORM_STATE_BUCKET` +- `TERRAFORM_LOCK_TABLE` (compatibility input) +- `JWT_SECRET_KEY` -**Secrets to Create:** -```bash -# Database password -aws secretsmanager create-secret \ - --name /myproject/staging/db-password \ - --secret-string "your-db-password" \ - --region us-east-1 +Environment variables (`vars`): -# JWT secret -aws secretsmanager create-secret \ - --name /myproject/staging/jwt-secret \ - --secret-string "your-jwt-secret" \ - --region us-east-1 +- `APP_URL` in `staging` +- `APP_URL` in `production` -# API keys or third-party credentials -aws secretsmanager create-secret \ - --name /myproject/staging/api-keys \ - --secret-string '{"key1": "value1", "key2": "value2"}' \ - --region us-east-1 -``` +### AWS Secrets Manager (runtime) -**In Terraform (variables.tf):** -```hcl -data "aws_secretsmanager_secret" "db_password" { - name = "/myproject/${var.environment}/db-password" -} +Runtime app secrets (for backend task runtime) should be stored as environment-scoped secrets, for example: -data "aws_secretsmanager_secret_version" "db_password" { - secret_id = data.aws_secretsmanager_secret.db_password.id -} +- `/myproject/staging/db-password` +- `/myproject/staging/jwt-secret` +- `/myproject/prod/db-password` +- `/myproject/prod/jwt-secret` -locals { - db_password = jsondecode(data.aws_secretsmanager_secret_version.db_password.secret_string) -} -``` +### Local development -**In ECS Task Definition (Terraform):** -```hcl -container_definitions = jsonencode([ - { - name = "backend" - image = aws_ecr_repository.backend.repository_url - environment = [ - { name = "DATABASE_URL", value = "postgresql://postgres:${local.db_password}@${aws_db_instance.postgres.endpoint}/myproject" }, - { name = "JWT_SECRET_KEY", value = local.jwt_secret }, - ] - logConfiguration = { - logDriver = "awslogs" - options = { - "awslogs-group" = aws_cloudwatch_log_group.ecs_logs.name - "awslogs-region" = var.aws_region - "awslogs-stream-prefix" = "ecs" - } - } - } -]) -``` +- Use `config/.env.dev` -> `deploy/.env` +- Keep local values local; `deploy/.env` must stay gitignored -### 3. Local Development (.env.local) +## Terraform backend note -For developers working locally. +Terraform backend locking is now based on `use_lockfile=true`. -**Setup:** -```bash -# Copy template -cp config/.env.example .env.local +The workflows still expose `TERRAFORM_LOCK_TABLE` input for compatibility, but new setup should prioritize lockfile backend behavior. -# Edit with your local values -# NEVER commit .env.local -``` +## Rotation guidance -**Example .env.local:** -```env -DATABASE_URL=postgresql://postgres:mypass@localhost:5432/myproject -JWT_SECRET_KEY=local-dev-secret -AWS_ACCESS_KEY_ID=local -AWS_SECRET_ACCESS_KEY=local -``` +- Rotate critical secrets regularly +- Rotate immediately after accidental exposure +- Document rotation date, owner, and impacted systems -**Usage in Backend:** -```python -import os -from dotenv import load_dotenv +## Incident response (secret exposure) -load_dotenv('.env.local') - -database_url = os.getenv('DATABASE_URL') -jwt_secret = os.getenv('JWT_SECRET_KEY') -``` - -**Usage in Frontend:** -```typescript -import { environment } from './environments/environment'; - -// Uses environment variables or .env.local -const apiUrl = process.env['NG_APP_API_URL'] || 'http://localhost:8000'; -``` - -**Usage in Scripts:** -```bash -#!/bin/bash -set -a -source .env.local -set +a - -# Now $DATABASE_URL and other vars are available -echo "Database: $DATABASE_URL" -``` - -## 🔄 Secret Flow by Environment - -### Development Flow -``` -Developer creates .env.local - ↓ -Runs: make dev - ↓ -Script sources .env.local - ↓ -Application reads environment variables - ↓ -Connects to local PostgreSQL with local secrets -``` - -### Staging/Production Flow via CI/CD -``` -Developer pushes code to develop/main - ↓ -GitHub Actions triggered - ↓ -Workflow reads GitHub Secrets - ↓ -Secrets passed to script via env variables - ↓ -Script runs make tf-plan / make tf-apply - ↓ -Terraform reads secrets from AWS Secrets Manager - ↓ -Terraform injects secrets into ECS task definition - ↓ -ECS starts container with environment variables - ↓ -Application reads from environment at runtime -``` - -## 🛡️ Best Practices - -### ✅ Do's - -- ✅ Store in `.env.local` for local development -- ✅ Store in GitHub Secrets for CI/CD infrastructure -- ✅ Store in AWS Secrets Manager for runtime -- ✅ Use `export` in scripts to pass through environment -- ✅ Rotate secrets regularly (every 90 days) -- ✅ Use environment-specific secrets (dev vs prod) -- ✅ Add `.env.local` to `.gitignore` (already done) -- ✅ Log secret names (not values) for debugging -- ✅ Use AWS IAM roles instead of hardcoded credentials - -### ❌ Don'ts - -- ❌ Never commit `.env.local` to git -- ❌ Never hardcode secrets in code -- ❌ Never put secrets in Makefile -- ❌ Never put secrets in shell scripts -- ❌ Never put secrets in Terraform code -- ❌ Never use same secrets across environments -- ❌ Never share secrets in Slack/Email -- ❌ Never log secret values - -## 🚨 Incident Response - -### If a Secret is Exposed - -1. **Immediately rotate the secret:** - ```bash - # Update in AWS Secrets Manager - aws secretsmanager put-secret-value \ - --secret-id /myproject/staging/db-password \ - --secret-string "new-password" - ``` - -2. **Update all places using it:** - - GitHub Actions workflows (if used) - - ECS task definitions - - Local `.env.local` files (manually for each developer) - - Third-party services (if applicable) - -3. **Audit access:** - ```bash - # Check CloudWatch logs - aws logs tail /aws/ecs/myproject --follow - ``` - -4. **Document the incident:** - - What was exposed - - When it was discovered - - What actions were taken - - Timeline - -## 📋 Secrets Checklist - -Before deploying to production: - -- [ ] All secrets in AWS Secrets Manager -- [ ] No secrets in Terraform code -- [ ] No secrets in shell scripts -- [ ] No secrets in Makefile -- [ ] All secrets follow naming convention: `/myproject/{env}/{secret-name}` -- [ ] GitHub Actions uses `${{ secrets.* }}` -- [ ] ECS task definition reads from environment -- [ ] `.env.local` added to `.gitignore` -- [ ] Team trained on secrets policy -- [ ] Secret rotation schedule established - -## 🔗 Related Documentation - -- [AWS Secrets Manager](https://docs.aws.amazon.com/secretsmanager/) -- [GitHub Encrypted Secrets](https://docs.github.com/en/actions/security-guides/encrypted-secrets) -- [Terraform AWS Secrets](https://registry.terraform.io/providers/hashicorp/aws/latest/docs/data-sources/secretsmanager_secret) -- [ECS Task IAM Roles](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_IAM_roles.html) +1. Revoke or rotate the exposed secret immediately +2. Update dependent systems and deployments +3. Redeploy affected services +4. Audit logs and document incident timeline diff --git a/deploy/DOCKER_COMPOSE_SETUP.md b/deploy/DOCKER_COMPOSE_SETUP.md deleted file mode 100644 index b804b6f..0000000 --- a/deploy/DOCKER_COMPOSE_SETUP.md +++ /dev/null @@ -1,385 +0,0 @@ -# Docker Compose Development Setup - -## Quick Start - -```bash -# 1. Start all services -make docker-up - -# 2. Access services -# - Frontend: http://localhost:4200 -# - Backend: http://localhost:8000 -# - Database: postgresql://localhost:5432 -# - Adminer: http://localhost:8080 (Database UI) - -# 3. View logs -make docker-logs - -# 4. Stop services -make docker-down -``` - -## Architecture - -``` -┌─────────────────────────────────────────────────────┐ -│ Docker Compose Network │ -├─────────────────────────────────────────────────────┤ -│ │ -│ ┌─────────────┐ ┌──────────────┐ │ -│ │ Frontend │ │ Backend │ │ -│ │ :4200 │ │ :8000 │ │ -│ └─────┬───────┘ └──────┬───────┘ │ -│ │ │ │ -│ └─────────┬───────┘ │ -│ │ │ -│ ┌─────▼────────┐ │ -│ │ │ │ -│ ┌───▼─────────────────┐ │ -│ │ PostgreSQL │ │ -│ │ Database :5432 │ │ -│ └─────────────────────┘ │ │ -│ │ -└─────────────────────────────────────────────────────┘ -``` - -## Services - -### PostgreSQL Database (db) - -- **Image**: postgres:16-alpine -- **Port**: 5432 -- **Credentials**: - - User: `postgres` - - Password: `postgres` (from .env) - - Database: `myproject_dev` -- **Volume**: `postgres_data:/var/lib/postgresql/data` -- **Health Check**: PostgreSQL readiness check every 10s - -### FastAPI Backend (backend) - -- **Port**: 8000 -- **Build**: `../backend/Dockerfile` -- **Environment**: All variables from `.env` file -- **Volumes**: - - `../backend:/app` - Source code (hot-reload) - - `/app/__pycache__` - Excluded from mount -- **Depends On**: `db` (healthy) -- **Health Check**: GET `/health` endpoint -- **Debug**: Auto-reload enabled when `BACKEND_RELOAD=true` - -### Angular Frontend (frontend) - -- **Port**: 4200 -- **Build**: `../frontend/Dockerfile` -- **Environment**: API_BASE_URL, FRONTEND_URL from `.env` -- **Volumes**: - - `../frontend:/app` - Source code - - `/app/node_modules` - Excluded from mount -- **Depends On**: `backend` service - -### Adminer (Database Management UI) - -- **Image**: adminer:latest -- **Port**: 8080 -- **Purpose**: Web-based database administration -- **Access**: http://localhost:8080 -- **Credentials**: Same as PostgreSQL service - -## Environment Configuration - -### Using .env File - -```bash -# 1. Create .env from template -cp .env.local.example .env - -# 2. Edit if needed (usually defaults are fine for local) -nano .env - -# 3. Docker Compose automatically loads .env -make docker-up -``` - -### Common .env Variables - -```bash -# Environment -ENVIRONMENT=development -DEBUG=true -LOG_LEVEL=DEBUG - -# Database -DATABASE_USER=postgres -DATABASE_PASSWORD=postgres -DATABASE_NAME=myproject_dev - -# API -API_BASE_URL=http://localhost:8000 -FRONTEND_URL=http://localhost:4200 - -# Secrets (safe for dev only!) -JWT_SECRET_KEY=dev-secret-key - -# Logging -LOG_FORMAT=text -LOG_OUTPUT=console -``` - -## Common Commands - -### Starting & Stopping - -```bash -# Start all services in background -make docker-up - -# Stop all services -make docker-down - -# Restart all services -make docker-restart - -# View running containers -make docker-ps -``` - -### Viewing Logs - -```bash -# All services -make docker-logs - -# Specific service -make docker-logs-backend -make docker-logs-frontend -make docker-logs-db - -# Follow logs in real-time -docker-compose -f deploy/docker-compose.yml logs -f backend - -# View last N lines -docker-compose -f deploy/docker-compose.yml logs --tail=100 backend -``` - -### Accessing Services - -```bash -# SSH into backend container -docker-compose -f deploy/docker-compose.yml exec backend bash - -# SSH into database -docker-compose -f deploy/docker-compose.yml exec db psql -U postgres -``` - -### Database Management - -```bash -# Connect to database -psql postgresql://postgres:postgres@localhost:5432/myproject_dev - -# View databases -\l - -# Connect to database -\c myproject_dev - -# List tables -\dt - -# Exit psql -\q - -# Or use Adminer UI at http://localhost:8080 -``` - -### Running Tests Inside Docker - -```bash -# Backend tests -docker-compose -f deploy/docker-compose.yml exec backend \ - pytest tests/ -v --tb=short - -# Integration tests -docker-compose -f deploy/docker-compose.yml exec backend \ - pytest tests/integration -v - -# E2E tests -docker-compose -f deploy/docker-compose.yml exec backend \ - pytest tests/e2e -v -``` - -## Troubleshooting - -### Port Already in Use - -```bash -# Find what's using port 8000 -lsof -i :8000 - -# Kill process -kill -9 - -# Or change port in .env -# Set BACKEND_PORT=8001 and rebuild -``` - -### Database Connection Failed - -```bash -# Check if db service is running -docker-compose -f deploy/docker-compose.yml ps - -# Check database logs -make docker-logs-db - -# Restart database -docker-compose -f deploy/docker-compose.yml restart db - -# Verify database is healthy -docker-compose -f deploy/docker-compose.yml exec db pg_isready -``` - -### Backend Won't Start - -```bash -# 1. Check logs -make docker-logs-backend - -# 2. Verify environment variables -docker-compose -f deploy/docker-compose.yml config | grep DATABASE - -# 3. Wait for db to be healthy -docker-compose -f deploy/docker-compose.yml exec db pg_isready - -# 4. Rebuild backend image -docker-compose -f deploy/docker-compose.yml build --no-cache backend -``` - -### Frontend Won't Load - -```bash -# Check frontend logs -make docker-logs-frontend - -# Check if Angular dev server is running -curl http://localhost:4200 - -# Restart frontend service -docker-compose -f deploy/docker-compose.yml restart frontend - -# Full rebuild -docker-compose -f deploy/docker-compose.yml build --no-cache frontend -``` - -## Cleaning Up - -### Remove Stopped Containers - -```bash -docker container prune -``` - -### Remove Unused Volumes - -```bash -docker volume prune -``` - -### Full Cleanup (WARNING: Removes all data!) - -```bash -# Stop and remove all containers, networks, volumes -make docker-clean - -# Or manually: -docker-compose -f deploy/docker-compose.yml down -v -``` - -## Advanced Usage - -### Rebuild Specific Service - -```bash -# Rebuild backend with no cache -docker-compose -f deploy/docker-compose.yml build --no-cache backend - -# Start with new build -docker-compose -f deploy/docker-compose.yml up backend -``` - -### Scale Services - -```bash -# Run 3 backend instances (with load balancing) -# Note: Need to remove port binding first -docker-compose -f deploy/docker-compose.yml up --scale backend=3 -``` - -### Custom Environment Override - -```bash -# Set custom value for single command -BACKEND_PORT=9000 docker-compose -f deploy/docker-compose.yml up backend -``` - -### Attach to Service - -```bash -# See real-time logs from backend -docker attach myproject_backend -``` - -## Performance Tips - -### 1. Use Volume Excludes - -The `docker-compose.yml` already excludes: -- `/app/__pycache__` -- `/app/node_modules` - -This prevents syncing Python/Node caches, improving performance. - -### 2. Increase Docker Resources - -If Docker is slow: -- Mac/Windows: Docker Desktop → Preferences → Resources → Increase CPUs/RAM -- Linux: Docker runs native, check system resources - -### 3. Use .dockerignore - -Frontend and backend both have `.dockerignore` files to exclude unnecessary files from Docker context. - -### 4. Enable BuildKit - -```bash -export DOCKER_BUILDKIT=1 -docker-compose -f deploy/docker-compose.yml build -``` - -## Production Differences - -This docker-compose setup is **development-focused**: - -| Feature | Development | Production | -|---------|-------------|-----------| -| Debug Mode | ✅ Enabled | ❌ Disabled | -| Hot Reload | ✅ Enabled | ❌ No reload | -| Logging | Text (console) | JSON (structured) | -| Health Checks | ✅ Yes | ✅ Yes | -| Volumes | Mounted (code sync) | ❌ No volumes | -| Adminer UI | ✅ Included | ❌ Removed | -| Workers | 1 | 4-8 | -| Resources | Limited | High | - -For production, use: -- ECS (AWS container orchestration) -- RDS (managed database) -- See [infra/README.md](../infra/README.md) for production setup - -## See Also - -- [Makefile](../Makefile) - All make targets -- [ENV_MANAGEMENT.md](../ENV_MANAGEMENT.md) - Environment variable guide -- [backend/README.md](../backend/README.md) - Backend setup -- [frontend/README.md](../frontend/README.md) - Frontend setup diff --git a/deploy/docker-compose.yml b/deploy/docker-compose.yml index d9a73b7..e673f40 100644 --- a/deploy/docker-compose.yml +++ b/deploy/docker-compose.yml @@ -1,26 +1,24 @@ -version: "3.9" - services: # ======================================================================== # PostgreSQL Database # ======================================================================== postgres: image: postgres:16-alpine - container_name: myproject_postgres + container_name: ${PROJECT_NAME}_postgres restart: unless-stopped ports: - - "5432:5432" + - "${POSTGRES_PORT}:${POSTGRES_CONTAINER_PORT}" environment: - POSTGRES_USER: ${DATABASE_USER:-postgres} - POSTGRES_PASSWORD: ${DATABASE_PASSWORD:-postgres} - POSTGRES_DB: ${DATABASE_NAME:-myproject_dev} - POSTGRES_INITDB_ARGS: "-c shared_buffers=256MB -c max_connections=200" + POSTGRES_USER: ${DATABASE_USER} + POSTGRES_PASSWORD: ${DATABASE_PASSWORD} + POSTGRES_DB: ${DATABASE_NAME} + POSTGRES_INITDB_ARGS: ${POSTGRES_INITDB_ARGS} volumes: - postgres_data:/var/lib/postgresql/data - ./init-db.sql:/docker-entrypoint-initdb.d/init.sql:ro - ./init-test-db.sql:/docker-entrypoint-initdb.d/test-init.sql:ro healthcheck: - test: ["CMD-SHELL", "pg_isready -U ${DATABASE_USER:-postgres}"] + test: ["CMD-SHELL", "pg_isready -U $$POSTGRES_USER -d $$POSTGRES_DB"] interval: 10s timeout: 5s retries: 5 @@ -37,50 +35,56 @@ services: context: ../backend dockerfile: Dockerfile args: - - PYTHON_VERSION=3.12 - container_name: myproject_backend + - PYTHON_VERSION=${PYTHON_VERSION} + container_name: ${PROJECT_NAME}_backend restart: unless-stopped ports: - - "8000:8000" + - "${BACKEND_PORT}:${BACKEND_PORT}" environment: # Application - ENVIRONMENT: ${ENVIRONMENT:-development} - DEBUG: ${DEBUG:-true} - LOG_LEVEL: ${LOG_LEVEL:-DEBUG} + ENVIRONMENT: ${ENVIRONMENT} + DEBUG: ${DEBUG} + LOG_LEVEL: ${LOG_LEVEL} # Backend Server - BACKEND_HOST: 0.0.0.0 - BACKEND_PORT: 8000 - BACKEND_RELOAD: ${BACKEND_RELOAD:-true} - BACKEND_WORKERS: 1 + BACKEND_HOST: ${BACKEND_HOST} + BACKEND_PORT: ${BACKEND_PORT} + BACKEND_RELOAD: ${BACKEND_RELOAD} + BACKEND_WORKERS: ${BACKEND_WORKERS} # API - API_TITLE: ${API_TITLE:-MyProject API} - API_VERSION: ${API_VERSION:-1.0.0} + API_TITLE: ${API_TITLE} + API_DESCRIPTION: ${API_DESCRIPTION} + API_VERSION: ${API_VERSION} # Database - DATABASE_USER: ${DATABASE_USER:-postgres} - DATABASE_PASSWORD: ${DATABASE_PASSWORD:-postgres} - DATABASE_HOST: postgres - DATABASE_PORT: 5432 - DATABASE_NAME: ${DATABASE_NAME:-myproject_dev} - DATABASE_POOL_SIZE: 5 - DATABASE_ECHO: ${DATABASE_ECHO:-true} + DATABASE_USER: ${DATABASE_USER} + DATABASE_PASSWORD: ${DATABASE_PASSWORD} + DATABASE_HOST: ${DATABASE_HOST} + DATABASE_PORT: ${DATABASE_PORT} + DATABASE_NAME: ${DATABASE_NAME} + DATABASE_POOL_SIZE: ${DATABASE_POOL_SIZE} + DATABASE_ECHO: ${DATABASE_ECHO} # Security - JWT_SECRET_KEY: ${JWT_SECRET_KEY:-dev-secret-key-change-in-production} - JWT_ALGORITHM: ${JWT_ALGORITHM:-HS256} + JWT_SECRET_KEY: ${JWT_SECRET_KEY} + JWT_ALGORITHM: ${JWT_ALGORITHM} + JWT_EXPIRE_MINUTES: ${JWT_EXPIRE_MINUTES} + ENABLE_HSTS: ${ENABLE_HSTS} # CORS - ALLOWED_ORIGINS: ${ALLOWED_ORIGINS:-http://localhost:4200,http://localhost:3000} + ALLOWED_ORIGINS: ${ALLOWED_ORIGINS} + CORS_ALLOW_CREDENTIALS: ${CORS_ALLOW_CREDENTIALS} + CORS_ALLOW_METHODS: ${CORS_ALLOW_METHODS} + CORS_ALLOW_HEADERS: ${CORS_ALLOW_HEADERS} # Frontend - FRONTEND_URL: ${FRONTEND_URL:-http://localhost:4200} - API_BASE_URL: ${API_BASE_URL:-http://localhost:8000} + FRONTEND_URL: ${FRONTEND_URL} + API_BASE_URL: ${API_BASE_URL} # Logging - LOG_FORMAT: ${LOG_FORMAT:-text} - LOG_OUTPUT: ${LOG_OUTPUT:-console} + LOG_FORMAT: ${LOG_FORMAT} + LOG_OUTPUT: ${LOG_OUTPUT} depends_on: postgres: condition: service_healthy @@ -109,18 +113,18 @@ services: dockerfile: Dockerfile args: - NODE_VERSION=20 - container_name: myproject_frontend + container_name: ${PROJECT_NAME}_frontend restart: unless-stopped ports: - - "4200:4200" + - "${FRONTEND_PORT}:${FRONTEND_PORT}" environment: # Angular - NG_HOST: 0.0.0.0 - NG_PORT: 4200 + NG_HOST: ${NG_HOST} + NG_PORT: ${NG_PORT} # API Configuration - API_BASE_URL: ${API_BASE_URL:-http://localhost:8000} - FRONTEND_URL: ${FRONTEND_URL:-http://localhost:4200} + API_BASE_URL: ${API_BASE_URL} + FRONTEND_URL: ${FRONTEND_URL} depends_on: - backend volumes: @@ -146,4 +150,4 @@ volumes: networks: myproject-network: driver: bridge - name: myproject-network + name: ${PROJECT_NAME}-network diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 2860b15..88b158f 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -1,249 +1,82 @@ # Architecture -This document describes the full system architecture of MyPythonProject1 — how services are structured, how they communicate, how infrastructure is provisioned, and how code moves from a developer's machine to production. +System architecture for application runtime, infrastructure, and delivery flow. + +## Runtime topology + +```text +Internet + -> ALB (public) + -> ECS Fargate services (private subnets) + - backend (FastAPI) + - frontend (Nginx + Angular) + -> RDS PostgreSQL (db subnets) + +Supporting services: + - AWS Secrets Manager (runtime secrets) + - Amazon ECR (container images) + - S3 backend state + lockfile locking (Terraform) + - CloudWatch Logs +``` ---- +## Application layers -## System Diagram +Backend (`backend/app`): -``` -Developer Workstation - │ - │ git push - ▼ -┌─────────────────────────────────────────────────────────────────┐ -│ GitHub Repository │ -│ │ -│ Branches: feature/* ──► develop ──► main ──► v* tag │ -│ │ -│ Workflows: │ -│ ci.yml ← runs on every PR and push to main/dev │ -│ staging.yml ← runs on push to develop │ -│ release.yml ← runs on push to main (release) + │ -│ push of v* tag (production deploy) │ -└──────────────┬──────────────────────────┬───────────────────────┘ - │ │ - Staging deploy Production deploy - │ │ - ▼ ▼ - ┌───────────────────┐ ┌─────────────────────────┐ - │ GHCR Docker Image │ │ ECR Docker Image │ - │ tag: staging-│ │ tag: v1.3.0, latest │ - └────────┬──────────┘ └────────────┬─────────────┘ - │ │ - ▼ ▼ - ┌─────────────────────────────────────────────────────┐ - │ AWS (us-east-1) │ - │ │ - │ ┌──────────────────────────────────────────────┐ │ - │ │ VPC 10.0.0.0/16 │ │ - │ │ │ │ - │ │ Public Subnets (2 AZs) │ │ - │ │ ┌─────────────────────────────────────┐ │ │ - │ │ │ Application Load Balancer (ALB) │ │ │ - │ │ │ :443 HTTPS → backend :8000 │ │ │ - │ │ │ :80 HTTP → 301 redirect │ │ │ - │ │ │ NAT Gateway (outbound egress) │ │ │ - │ │ └────────────────┬────────────────────┘ │ │ - │ │ │ │ │ - │ │ Private Subnets (2 AZs) │ │ - │ │ ┌────────────────▼────────────────────┐ │ │ - │ │ │ ECS Fargate Cluster │ │ │ - │ │ │ ┌──────────────────────────────┐ │ │ │ - │ │ │ │ backend service │ │ │ │ - │ │ │ │ FastAPI + Uvicorn :8000 │ │ │ │ - │ │ │ │ fetches secrets from SM │ │ │ │ - │ │ │ └──────────────────────────────┘ │ │ │ - │ │ │ ┌──────────────────────────────┐ │ │ │ - │ │ │ │ frontend service │ │ │ │ - │ │ │ │ Nginx serving Angular :80 │ │ │ │ - │ │ │ └──────────────────────────────┘ │ │ │ - │ │ └────────────────────────────────────┘ │ │ - │ │ │ │ - │ │ DB Subnets (isolated, no public route) │ │ - │ │ ┌─────────────────────────────────────┐ │ │ - │ │ │ RDS PostgreSQL 16 (Multi-AZ prod) │ │ │ - │ │ │ port 5432, encrypted at rest │ │ │ - │ │ └─────────────────────────────────────┘ │ │ - │ └──────────────────────────────────────────┘ │ - │ │ - │ Supporting services (not in VPC): │ - │ ┌──────────────────────────────────────────┐ │ - │ │ Secrets Manager — DB password, JWT key │ │ - │ │ ECR — production images │ │ - │ │ S3 + DynamoDB — Terraform state │ │ - │ │ IAM OIDC — GitHub Actions auth │ │ - │ │ CloudWatch Logs — ECS task logs │ │ - │ └──────────────────────────────────────────┘ │ - └─────────────────────────────────────────────────┘ -``` +- `api/`: route handlers +- `services/`: business logic +- `db/`: SQLAlchemy engine/session +- `models/` + `schemas/`: persistence and API contracts ---- +Frontend (`frontend/src/app`): -## Application Layers +- `components/`: UI features +- `services/`: API/auth/game/user clients +- `core/`: route guards and HTTP interceptor -### Backend (FastAPI) +## CI/CD flow -``` -app/ -├── api/ ← Route handlers (thin controllers — validate input, call service, return response) -│ ├── health.py ← GET /health (used by ALB health checks + smoke tests) -│ ├── user.py ← POST /users, GET /users/me, etc. -│ └── game.py ← CRUD + game logic endpoints -├── core/ -│ ├── config.py ← pydantic-settings reads env vars, validates types at startup -│ ├── security.py ← JWT creation/verification, password hashing (bcrypt/argon2) -│ └── logging.py ← JSON structured logging -├── db/ -│ ├── session.py ← async SQLAlchemy session factory, dependency injector -│ └── base.py ← declarative base, shared metadata -├── models/ ← SQLAlchemy ORM models (database schema) -├── schemas/ ← Pydantic models (API contract — request/response shapes) -└── services/ ← Business logic (no HTTP, no DB calls — pure functions + repo calls) -``` +```text +feature/* -> PR -> develop/main -**Request lifecycle:** -``` -HTTP request - → ALB - → ECS Fargate container (Uvicorn ASGI) - → FastAPI router (api/) - → Pydantic schema validation - → Service layer (services/) - → SQLAlchemy session (db/session.py) - → PostgreSQL - → Pydantic response schema - → HTTP response -``` +CI (`ci.yml`) + - lint/test/security/dependency checks + - terraform fmt/validate/plan -### Frontend (Angular) +Staging CD (`staging.yml`) + - triggered by successful CI on develop (or manual) + - build/push backend+frontend images to ECR + - terraform apply (staging) + - ECS force deployment + - smoke test +Release + Production CD (`release.yml`) + - successful CI on main -> semantic-release + - release tag `v*` -> production deploy flow + - build/push images to ECR + - terraform apply (production) + - ECS force deployment + - smoke test ``` -src/app/ -├── components/ ← Feature UI (login, register, dashboard, game) -├── services/ -│ ├── api.service.ts ← Base HTTP client with error handling -│ ├── auth.service.ts ← Login, logout, token storage -│ └── game.service.ts ← Game CRUD via API -├── core/ -│ ├── auth.guard.ts ← Redirects unauthenticated users to /login -│ └── http.interceptor.ts ← Attaches Bearer token to every API request -└── types/ ← Shared TypeScript interfaces -``` - -### Database (PostgreSQL) -- Migrations managed by **Alembic** (`backend/alembic/versions/`) -- Applied automatically on each deploy before ECS tasks start -- In production: Multi-AZ RDS, automated daily backups, encrypted at rest with KMS -- In staging: single-AZ RDS, daily backups, encrypted -- In tests: ephemeral PostgreSQL Docker container, auto-rollback after each test +## Secrets and configuration model ---- +- Runtime app secrets: AWS Secrets Manager +- CI/CD orchestration secrets: GitHub repository/environment secrets +- Non-secret pipeline config: environment files under `config/` -## Infrastructure (Terraform) +No static AWS credentials are stored in repository files. GitHub Actions uses OIDC to assume AWS roles. -Infrastructure is split into reusable modules: +## Terraform backend model -| Module | Resources | -|---|---| -| `modules/network` | VPC, public/private/DB subnets, IGW, NAT Gateway, route tables, NACLs | -| `modules/alb` | ALB, target groups, listeners (HTTP→HTTPS redirect, HTTPS forward) | -| `modules/ecs` | ECS cluster, backend + frontend task definitions, IAM execution role | -| `modules/rds` | RDS PostgreSQL instance, subnet group, security group, KMS key | -| `modules/iam` | GitHub OIDC provider, CI/CD IAM roles (staging/production) | +Terraform remote state is stored in S3 and uses `use_lockfile=true` for locking. -Environment-specific values live in `infra/envs/staging.tfvars` and `infra/envs/prod.tfvars`. The Terraform code itself is environment-agnostic. - -Remote state is stored in S3 with DynamoDB locking. The backend block uses **partial configuration** so bucket/key/region are injected at `terraform init` time by CI workflows: +Example init: ```bash terraform init \ - -backend-config="bucket=myproject-terraform-state" \ + -backend-config="bucket=" \ -backend-config="key=staging/terraform.tfstate" \ - -backend-config="region=us-east-1" \ - -backend-config="dynamodb_table=terraform-locks" -``` - ---- - -## Security Model - -### Secrets — Never in code - -| Secret type | Where stored | How accessed | -|---|---|---| -| DB password | AWS Secrets Manager | ECS task IAM role at container startup | -| JWT secret | AWS Secrets Manager | ECS task IAM role at container startup | -| CI/CD AWS credentials | GitHub OIDC (no static keys) | `aws-actions/configure-aws-credentials` | -| Staging secrets | GitHub Environment "staging" | Workflow `${{ secrets.* }}` | -| Production secrets | GitHub Environment "production" | Workflow `${{ secrets.* }}` with required reviewer approval | - -### Network — Least privilege - -- ECS tasks run in **private subnets** — no inbound internet access -- Only the ALB (in public subnet) is internet-facing -- RDS is in **DB subnets** — only the ECS security group can reach port 5432 -- All inter-service traffic is within the VPC -- All egress from private subnets routes through the NAT Gateway - -### IAM — OIDC, no static keys - -GitHub Actions authenticates to AWS using an **OIDC identity provider** — no IAM user access keys are stored anywhere. Each environment has a dedicated IAM role with the minimum permissions required for that environment. - ---- - -## CI/CD Flow — End to End - -``` -Developer writes code on feature branch - │ - │ git push origin feature/add-game-mode - ▼ -Pull Request opened → develop - │ - ├─ ci.yml triggered - │ commitlint: all commits follow Conventional Commits? - │ backend-ci: ruff lint → unit tests → integration tests - │ frontend-ci: eslint → tsc → ng build - │ security-scan: trivy fs + gitguardian - │ dependency-audit: snyk python + node - │ terraform-plan: staging + prod (read-only, comments plan on PR) - │ quality-gate: aggregates all — single required status check - │ - ├─ Code review + approval - │ - ▼ -PR merged to develop - │ - ├─ ci.yml triggered again (post-merge validation) - │ - └─ staging.yml triggered - build: docker build backend + frontend → GHCR - scan: trivy image scan (warn only) - terraform-staging: terraform apply envs/staging.tfvars - deploy-staging: ecs update-service --force-new-deployment - smoke-test: curl $APP_URL/health → must return 200 - - ▼ -When ready to release: merge develop → main - │ - ├─ ci.yml triggered - │ - └─ release.yml triggered (push to main) - autoversion: semantic-release analyses commits - → bumps version (e.g. 1.2.0 → 1.3.0) - → writes CHANGELOG.md - → commits + creates tag v1.3.0 - → creates GitHub Release - - ▼ -Tag v1.3.0 pushed - │ - └─ release.yml triggered (push tag v*) - build-production: docker build → ECR tagged v1.3.0 + latest - scan-production: trivy — CRITICAL = hard block - terraform-production: [manual approval required] → terraform apply prod - deploy-production: ecs task definition update + deploy - smoke-test-production: curl $PROD_URL/health → must return 200 + -backend-config="region=" \ + -backend-config="use_lockfile=true" ``` diff --git a/docs/ONBOARDING.md b/docs/ONBOARDING.md index 0ca6bdd..22936d6 100644 --- a/docs/ONBOARDING.md +++ b/docs/ONBOARDING.md @@ -1,279 +1,104 @@ -# Onboarding — New Developer Guide +# Onboarding Guide -Everything you need to go from zero to a running local environment and your first merged PR. Estimated time: **30–45 minutes**. - ---- +New developer path from clone to first successful PR. ## Prerequisites -Install these tools before starting: - -### macOS -```bash -# Homebrew -/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" - -# All required tools -brew install git python@3.12 node terraform awscli -brew install --cask docker - -# Open Docker Desktop before continuing -open /Applications/Docker.app -``` - -### Ubuntu / Debian -```bash -sudo apt-get update && sudo apt-get install -y \ - git python3.12 python3-venv nodejs npm \ - docker.io docker-compose +- Git +- Docker Desktop +- Python 3.12+ +- Poetry +- Node.js 20+ +- Terraform 1.5+ +- AWS CLI v2 (for infra/deploy work) -# Terraform -wget -O- https://apt.releases.hashicorp.com/gpg | gpg --dearmor \ - | sudo tee /usr/share/keyrings/hashicorp-archive-keyring.gpg -echo "deb [signed-by=/usr/share/keyrings/hashicorp-archive-keyring.gpg] \ - https://apt.releases.hashicorp.com $(lsb_release -cs) main" \ - | sudo tee /etc/apt/sources.list.d/hashicorp.list -sudo apt-get update && sudo apt-get install terraform - -# AWS CLI v2 -curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o awscliv2.zip -unzip awscliv2.zip && sudo ./aws/install -``` - -### Verify versions -```bash -git --version # 2.40+ -python3 --version # 3.12+ -node --version # 20+ -docker --version # 24+ -terraform --version # 1.5+ -aws --version # 2.x -``` - ---- - -## Step 1 — Clone & install +## Step 1 — Clone and install ```bash git clone https://github.com/your-org/mypythonproject1.git cd mypythonproject1 - -# Install all dependencies at once make install ``` -`make install` installs: -- Backend Python deps via Poetry (`backend/`) -- Frontend Node deps via npm (`frontend/`) -- Root CI tooling (commitlint + semantic-release) - ---- - ## Step 2 — Configure local environment ```bash cp config/.env.dev deploy/.env ``` -`config/.env.dev` has all non-secret local-dev settings pre-configured (local Postgres host, debug=true, test JWT key). You do not need to add any secrets to run the app locally. - ---- +This project keeps local non-secret defaults in `config/.env.dev`. -## Step 3 — Start the full stack +## Step 3 — Start local stack ```bash docker compose -f deploy/docker-compose.yml up --build ``` -Wait for all containers to be healthy, then open: +Verify: -| URL | What | -|---|---| -| http://localhost:4200 | Angular frontend | -| http://localhost:8000/docs | FastAPI OpenAPI docs | -| http://localhost:8000/health | Health check (`{"status":"ok"}`) | - -### Run database migrations -```bash -docker compose -f deploy/docker-compose.yml exec backend \ - alembic upgrade head -``` - ---- +- Frontend: http://localhost:4200 +- Backend docs: http://localhost:8000/docs +- Health: http://localhost:8000/health ## Step 4 — Run tests ```bash -# All tests make test - -# Backend only — unit tests (fast, no DB required) -cd backend && poetry run pytest tests/unit -m unit -v - -# Backend — integration tests (needs postgres running) -cd backend && poetry run pytest tests/integration -m integration -v - -# Frontend -cd frontend && npm test ``` ---- - -## Step 5 — Understand the branch strategy - -``` -main ← production-ready code only; never commit directly - └─ develop ← integration branch; merge feature branches here - └─ feature/your-feature ← your working branch -``` +Backend test split: -**Always branch from `develop`:** ```bash -git checkout develop -git pull origin develop -git checkout -b feature/my-new-feature +cd backend +poetry run pytest tests/unit -m unit -v +poetry run pytest tests/integration -m integration -v ``` ---- +## Step 5 — Branch and commit rules -## Step 6 — Write code & commit +Branch strategy: -Commits **must** follow [Conventional Commits](https://www.conventionalcommits.org/). This is enforced by `commitlint` on every PR. +- `main`: production +- `develop`: integration branch +- `feature/*`: work branches from `develop` -```bash -# Format: (): +Conventional commit format is required: -git commit -m "feat(game): add multiplayer room endpoints" -git commit -m "fix(auth): refresh token not invalidated on logout" -git commit -m "test: add user registration integration test" -git commit -m "docs: update local setup instructions" +```text +feat(scope): add feature +fix(scope): fix behavior +docs: update docs ``` -**Valid types:** `feat`, `fix`, `perf`, `refactor`, `revert`, `docs`, `style`, `test`, `build`, `ci`, `chore` +## Step 6 — Open PR to `develop` -If your commit fails commitlint, you'll see an error like: -``` -⧗ input: WIP: some stuff -✖ subject may not be empty [subject-empty] -✖ type may not be empty [type-empty] -``` +CI (`ci.yml`) runs lint/tests/security/dependency checks and Terraform plan checks. -Fix it with `git commit --amend -m "fix(auth): correct typo in error message"`. +Primary required status: `quality-gate`. ---- - -## Step 7 — Open a Pull Request → develop - -```bash -git push origin feature/my-new-feature -# Open PR on GitHub targeting the develop branch -``` +## Step 7 — Deployment behavior after merge -The CI pipeline (`ci.yml`) runs automatically. All jobs must pass before merging: +- Merge to `develop` + successful CI -> `staging.yml` deploys to staging +- Merge to `main` + successful CI -> `release.yml` runs semantic-release +- Tag `v*` -> production deployment flow in `release.yml` -| Check | Description | -|---|---| -| Commitlint | All commits in the PR follow Conventional Commits | -| Backend CI | Lint (ruff) + unit + integration tests | -| Frontend CI | ESLint + TypeScript + build | -| Security Scan | Trivy + GitGuardian | -| Dependency Audit | Snyk | -| Terraform Plan | Shows infra diff on PR (if `infra/` changed) | -| Quality Gate | Single required check summarising all above | +Both staging and production deployment flows build/push images to Amazon ECR. ---- - -## Step 8 — After merge to develop - -Your changes automatically deploy to **staging** via `staging.yml`: - -1. Docker images built and pushed to GHCR -2. Terraform applies any infra changes to staging -3. ECS rolls out new containers -4. Smoke test hits `/health` — if it fails, the deploy is marked failed - -Check the deployment at: -``` -https://github.com/your-org/mypythonproject1/actions/workflows/staging.yml -``` - -Staging URL: configured in `config/.env.staging` → `APP_URL`. - ---- - -## Common Commands Cheatsheet +## Common commands ```bash -# Start everything locally make dev - -# Run all tests -make test - -# Lint code make lint - -# Format code make format - -# Run only backend make backend - -# Run only frontend make frontend - -# View all make targets make help ``` ---- - -## Troubleshooting - -### `make install` fails on poetry -```bash -pip install --upgrade pip -pip install poetry -poetry --version -``` - -### Docker compose postgres fails to start -```bash -# Check if port 5432 is already in use -lsof -i :5432 -# Kill the conflicting process or change DATABASE_PORT in deploy/.env -``` - -### `alembic upgrade head` — "can't connect to postgres" -Make sure the postgres container is running: -```bash -docker compose -f deploy/docker-compose.yml ps -``` - -### Frontend — `npm ci` fails -```bash -# Clear cache and retry -cd frontend -rm -rf node_modules package-lock.json -npm install -``` - -### Commitlint fails on push -Edit the failing commit: -```bash -git commit --amend -m "fix(scope): proper conventional commit message" -``` -Or use interactive rebase to fix multiple commits: -```bash -git rebase -i HEAD~3 -``` - ---- - -## Getting Help +## Related docs -- Architecture overview: [docs/ARCHITECTURE.md](ARCHITECTURE.md) -- Test strategy: [docs/TEST_ARCHITECTURE.md](TEST_ARCHITECTURE.md) -- Infrastructure details: [infra/README.md](../infra/README.md) -- Backend API: [backend/README.md](../backend/README.md) -- Frontend: [frontend/README.md](../frontend/README.md) +- `README.md` +- `docs/ARCHITECTURE.md` +- `docs/TEST_ARCHITECTURE.md` +- `infra/README.md` +- `.github/GITHUB_ACTIONS_CICD.md` diff --git a/docs/TEST_ARCHITECTURE.md b/docs/TEST_ARCHITECTURE.md index 8c09b91..e55204b 100644 --- a/docs/TEST_ARCHITECTURE.md +++ b/docs/TEST_ARCHITECTURE.md @@ -1,318 +1,50 @@ # Test Architecture -Testing strategy for the FastAPI backend. Covers test types, fixture design, database isolation, and how tests run in CI. +Backend testing strategy and execution model. ---- +## Test layers -## Overview - -``` +```text tests/ -├── unit/ ← Fast, fully mocked — no database, no network, no docker required -│ ├── services/ ← Business logic tests (user_service, game_service) -│ └── core/ ← Utility tests (security, config) -│ -└── integration/ ← Real database — auto-rollback per test - ├── api/ ← Full HTTP request/response cycle tests - └── db/ ← Repository and data persistence tests +├── unit/ # fully mocked; no real DB/network +└── integration/ # real DB-backed API/repository tests ``` -### Test layer responsibilities - -| Layer | DB | Network | Speed | When to write | -|---|---|---|---|---| -| Unit | ❌ mocked | ❌ mocked | ~10ms/test | All business logic, utilities | -| Integration | ✅ real | ✅ real HTTP | ~100ms/test | API endpoints, DB queries | - ---- - -## Running Tests +## Running tests ```bash cd backend - -# All tests poetry run pytest - -# Unit tests only (fast, no DB) poetry run pytest tests/unit -m unit -v - -# Integration tests only (requires running postgres) poetry run pytest tests/integration -m integration -v - -# Single test -poetry run pytest tests/unit/services/test_user_service.py::TestUserRegistration::test_register_success -v - -# With coverage poetry run pytest --cov=app --cov-report=html --cov-report=term-missing -open htmlcov/index.html - -# Stop on first failure -poetry run pytest -x - -# Show local variables on failure -poetry run pytest -l - -# Parallelise (install pytest-xdist first) -poetry run pytest -n auto -``` - ---- - -## Pytest Markers - -Defined in `pytest.ini`: - -```ini -[pytest] -markers = - unit: mark test as a unit test (no database, no network) - integration: mark test as an integration test (requires database) -``` - -Usage: -```python -import pytest - -@pytest.mark.unit -def test_password_hash_is_not_plaintext(): - hashed = hash_password("secret123") - assert hashed != "secret123" - -@pytest.mark.integration -async def test_create_user_persists_to_db(db_session): - user = await create_user(db_session, email="a@b.com", password="pass") - result = await db_session.get(User, user.id) - assert result.email == "a@b.com" -``` - ---- - -## Fixture Reference - -All fixtures are defined in `tests/conftest.py`. - -### Database fixtures - -```python -@pytest.fixture(scope="session") -def test_engine(): - """Single SQLAlchemy engine for the entire test session. - Points to the test database (DATABASE_NAME=testdb from config/.env.test). - Creates all tables on setup, drops them after all tests complete. - """ - -@pytest.fixture(scope="function") -async def db_session(test_engine): - """Per-test transactional session. - Wraps each test in a transaction that is ROLLED BACK when the test ends, - leaving the database in a clean state for the next test. - No teardown logic needed in individual tests. - """ - -@pytest.fixture(scope="function") -async def db_session_with_commit(test_engine): - """Use when the test must commit (e.g. testing cascade deletes). - Truncates all tables after the test instead of rolling back. - """ -``` - -### HTTP client fixtures - -```python -@pytest.fixture -async def client(db_session): - """AsyncClient pointing at the full FastAPI app. - Injects db_session via dependency override — same session as the test, - so the test can verify DB state without committing. - """ - -@pytest.fixture -async def client_with_commit(db_session_with_commit): - """AsyncClient with a session that commits. - Use for tests that check persistent side effects. - """ -``` - -### Authentication fixtures - -```python -@pytest.fixture -def test_user_data(): - """Pre-defined user credentials dict. - Returns: {"email": "test@example.com", "password": "TestPassword1!"} - """ - -@pytest.fixture -async def created_user(db_session, test_user_data): - """Creates a real User row in the DB. - Rolled back after the test (via db_session). - """ - -@pytest.fixture -async def user_token(created_user): - """Returns a valid JWT access token for created_user.""" - -@pytest.fixture -async def authenticated_client(client, user_token): - """AsyncClient with Authorization: Bearer pre-set. - Use for tests that require an authenticated user. - """ -``` - ---- - -## Writing Unit Tests - -Unit tests mock all I/O. Never import database models or make network calls. - -```python -# tests/unit/services/test_user_service.py -import pytest -from unittest.mock import AsyncMock, MagicMock -from app.services.user_service import register_user, UserAlreadyExistsError - -@pytest.mark.unit -class TestUserRegistration: - - async def test_register_success(self): - # Arrange - mock_repo = AsyncMock() - mock_repo.find_by_email.return_value = None # user doesn't exist yet - mock_repo.create.return_value = MagicMock(id=1, email="new@example.com") - - # Act - result = await register_user( - repo=mock_repo, - email="new@example.com", - password="StrongPass1!" - ) - - # Assert - assert result.email == "new@example.com" - mock_repo.create.assert_called_once() - - async def test_register_duplicate_email_raises(self): - # Arrange - mock_repo = AsyncMock() - mock_repo.find_by_email.return_value = MagicMock() # user already exists - - # Act + Assert - with pytest.raises(UserAlreadyExistsError): - await register_user( - repo=mock_repo, - email="existing@example.com", - password="StrongPass1!" - ) ``` ---- +## Markers -## Writing Integration Tests +Defined in `backend/pytest.ini`: -Integration tests exercise the full stack: HTTP request → FastAPI router → service → database → response. +- `unit` +- `integration` -```python -# tests/integration/api/test_auth.py -import pytest -from httpx import AsyncClient +## Fixture strategy -@pytest.mark.integration -class TestAuthEndpoints: +Key fixtures in `backend/tests/conftest.py`: - async def test_register_and_login(self, client: AsyncClient): - # Register - resp = await client.post("/auth/register", json={ - "email": "integration@example.com", - "password": "StrongPass1!" - }) - assert resp.status_code == 201 - assert resp.json()["email"] == "integration@example.com" +- session-level test engine +- per-test transactional DB session rollback isolation +- async HTTP client fixture with dependency override +- auth/token helper fixtures for protected endpoint tests - # Login with same credentials - resp = await client.post("/auth/login", json={ - "email": "integration@example.com", - "password": "StrongPass1!" - }) - assert resp.status_code == 200 - assert "access_token" in resp.json() +## Isolation model - async def test_protected_route_requires_token(self, client: AsyncClient): - resp = await client.get("/users/me") - assert resp.status_code == 401 - - async def test_protected_route_with_valid_token( - self, authenticated_client: AsyncClient - ): - resp = await authenticated_client.get("/users/me") - assert resp.status_code == 200 - assert "email" in resp.json() -``` +Integration tests run in per-test transactions and roll back automatically to keep state clean between tests. ---- +## CI behavior -## Database Isolation Strategy +`ci.yml` runs backend tests in dedicated steps: -Each integration test runs inside a database transaction that is **rolled back** when the test ends. This means: - -- Tests are completely isolated — order doesn't matter -- No cleanup code required in individual tests -- The DB is always in a clean state -- Tests run at the same speed as real DB operations (they use real SQL) - -How it works internally: - -```python -@pytest.fixture(scope="function") -async def db_session(test_engine): - async with test_engine.connect() as conn: - await conn.begin() # start outer transaction - async with AsyncSession(bind=conn) as session: - await session.begin_nested() # savepoint - yield session # test runs here - await session.rollback() # rollback savepoint - await conn.rollback() # rollback outer transaction -``` - -The FastAPI `get_db()` dependency is overridden to use this same session: - -```python -app.dependency_overrides[get_db] = lambda: db_session -``` - ---- - -## CI Configuration - -Tests run in `ci.yml` with a real PostgreSQL service container: - -```yaml -services: - postgres: - image: postgres:16-alpine - env: - POSTGRES_USER: ${{ env.POSTGRES_USER }} - POSTGRES_PASSWORD: ${{ env.POSTGRES_PASSWORD }} - POSTGRES_DB: ${{ env.POSTGRES_DB }} - options: >- - --health-cmd pg_isready - --health-interval 10s - --health-timeout 5s - --health-retries 5 - ports: - - ${{ env.POSTGRES_PORT }}:5432 -``` - -The credentials match `config/.env.test` — the same file used locally. This means tests behave identically locally and in CI. - -Unit and integration tests run as separate steps so they can be distinguished in the CI summary: - -```yaml -- name: Unit tests - run: pytest tests/unit -m unit -v --junitxml=test-results-unit.xml - -- name: Integration tests - run: pytest tests/integration -m integration -v --junitxml=test-results-integration.xml -``` +- Unit tests +- Integration tests -Coverage is uploaded to Codecov after the integration test step. +Both are part of the required CI quality gate. diff --git a/frontend/Dockerfile b/frontend/Dockerfile index ea68c95..bc56907 100644 --- a/frontend/Dockerfile +++ b/frontend/Dockerfile @@ -1,5 +1,7 @@ # Build stage -FROM node:20-alpine AS builder +ARG NODE_VERSION=20 +ARG NGINX_TAG=alpine +FROM node:${NODE_VERSION}-alpine AS builder WORKDIR /app @@ -20,7 +22,7 @@ COPY src ./src RUN npm run build # Production stage -FROM nginx:alpine +FROM nginx:${NGINX_TAG} # Copy nginx config with SPA fallback (try_files → index.html) COPY nginx.conf /etc/nginx/conf.d/default.conf diff --git a/frontend/README.md b/frontend/README.md index 089eecc..1ac138b 100644 --- a/frontend/README.md +++ b/frontend/README.md @@ -1,301 +1,79 @@ # Frontend -Angular 19 single-page application with TypeScript, Tailwind CSS, and RxJS communicating with the FastAPI backend. +Angular frontend application (TypeScript + Tailwind) served by Nginx in containerized environments. ---- - -## Table of Contents - -1. [Local Setup](#local-setup) -2. [Project Structure](#project-structure) -3. [Environment Configuration](#environment-configuration) -4. [Available Scripts](#available-scripts) -5. [Architecture](#architecture) -6. [Running Tests](#running-tests) -7. [Building for Production](#building-for-production) -8. [Docker](#docker) - ---- - -## Local Setup - -### Requirements - -- Node.js 20 LTS+ -- npm 10+ - -### Install and run +## Local setup ```bash cd frontend npm ci npm start -# Opens http://localhost:4200 ``` -Or use the project-root shortcut: +Or from project root: ```bash make frontend ``` -The frontend dev server proxies API calls to `http://localhost:8000` by default (configured in `proxy.conf.json`). Make sure the backend is running. - -### Full stack (recommended) +Local URL: http://localhost:4200 -Start everything from the project root: +## Full-stack local run ```bash make dev -# Backend: http://localhost:8000 -# Frontend: http://localhost:4200 ``` ---- - -## Project Structure - -``` -frontend/ -├── angular.json ← Angular CLI workspace config -├── package.json ← Node dependencies -├── tsconfig.json ← TypeScript base config -├── tsconfig.app.json ← App-specific TypeScript config -├── tsconfig.spec.json ← Test TypeScript config -├── karma.conf.js ← Karma test runner config -├── eslint.config.js ← ESLint rules -├── postcss.config.mjs ← PostCSS (Tailwind) -│ -├── public/ -│ └── favicon.ico -│ -├── src/ -│ ├── index.html ← Root HTML shell -│ ├── main.ts ← Bootstraps Angular application -│ └── styles.css ← Global styles + Tailwind directives -│ -│ └── app/ -│ ├── app.component.ts ← Root component (RouterOutlet) -│ ├── app.routes.ts ← Route definitions -│ │ -│ ├── components/ ← Feature components (one folder per feature) -│ │ ├── login/ ← Login form -│ │ │ ├── login.component.ts -│ │ │ └── login.component.html -│ │ ├── register/ ← Registration form -│ │ ├── dashboard/ ← Authenticated home page -│ │ ├── game/ ← Game view and controls -│ │ └── shared/ ← Reusable UI components (buttons, modals) -│ │ -│ ├── services/ -│ │ ├── api.service.ts ← Base HTTP client; centralises URL, error handling -│ │ ├── auth.service.ts ← Login, logout, token storage, authentication state -│ │ ├── game.service.ts ← Game CRUD operations wrapping api.service -│ │ └── user.service.ts ← User profile operations -│ │ -│ ├── core/ -│ │ ├── auth.guard.ts ← Route guard: redirects to /login if not authenticated -│ │ └── http.interceptor.ts ← Attaches "Authorization: Bearer " to every request -│ │ -│ └── types/ ← Shared TypeScript interfaces -│ ├── auth.types.ts ← User, LoginRequest, TokenResponse -│ └── game.types.ts ← Game, CreateGameRequest, UpdateGameRequest -│ -└── Dockerfile ← Multi-stage build: ng build → nginx -``` - ---- - -## Environment Configuration +Expected local dependencies: -Angular uses `src/environments/` files to manage per-environment settings. +- Backend API at http://localhost:8000 -| File | Used when | -|---|---| -| `src/environments/environment.ts` | `ng serve` (local development) | -| `src/environments/environment.production.ts` | `ng build --configuration production` | - -Example `environment.ts`: -```typescript -export const environment = { - production: false, - apiUrl: 'http://localhost:8000' -}; -``` - -These files are **not secrets** — they contain only public configuration like the API base URL. Sensitive values (auth tokens, keys) are never stored in Angular code. - ---- - -## Available Scripts +## Testing and quality ```bash -# Development server with hot reload -npm start -# → http://localhost:4200 - -# Run unit tests with Karma/Jasmine -npm test - -# Run tests once (no watch — used in CI) -npm run test:ci - -# Type-check without emitting -npm run type-check - -# Lint with ESLint npm run lint - -# Auto-fix lint issues -npm run lint:fix - -# Production build -npm run build -# Output → dist/ - -# Analyse bundle size -npm run build -- --stats-json -npx webpack-bundle-analyzer dist/stats.json -``` - ---- - -## Architecture - -### Data flow - -``` -User interaction - │ - ▼ -Component (e.g. GameComponent) - │ calls - ▼ -Service (e.g. GameService) - │ calls - ▼ -ApiService.get/post/put/delete() - │ - ▼ -HttpClient (Angular) - │ - ├─ HttpInterceptor adds: Authorization: Bearer - │ - ▼ -Backend API (http://localhost:8000 or https://api.myproject.com) - │ - ▼ -Observable → Component subscribes → updates template -``` - -### Authentication flow - -``` -User submits login form - │ - ▼ -AuthService.login(email, password) - │ POSTs to /auth/login - ▼ -API returns { access_token, refresh_token } - │ - ▼ -AuthService stores tokens in localStorage - │ - ▼ -Router navigates to /dashboard - │ - ▼ -All subsequent requests: - HttpInterceptor reads token from AuthService - Adds Authorization header automatically -``` - -### Route protection - -```typescript -// app.routes.ts -{ - path: 'dashboard', - component: DashboardComponent, - canActivate: [AuthGuard] ← redirects to /login if not authenticated -} +npm run type-check +npm run test:ci ``` ---- - -## Running Tests +Coverage: ```bash -# Watch mode (interactive) -npm test - -# Single run (used in CI) -npm run test:ci - -# With coverage npm run test:ci -- --code-coverage -open coverage/index.html ``` -Tests use **Jasmine** as the test framework and **Karma** as the test runner. Each component and service has a corresponding `.spec.ts` file in the same folder. - -Example unit test: -```typescript -// auth.service.spec.ts -describe('AuthService', () => { - it('should store tokens after login', () => { - // arrange - const mockHttp = jasmine.createSpyObj('HttpClient', ['post']); - mockHttp.post.and.returnValue(of({ access_token: 'token123' })); - const service = new AuthService(mockHttp); - - // act - service.login('test@example.com', 'pass').subscribe(); - - // assert - expect(localStorage.getItem('access_token')).toBe('token123'); - }); -}); -``` - ---- - -## Building for Production +## Build ```bash npm run build -# Output: dist/ ``` -The production build: -- Enables Angular AOT compilation -- Minifies and tree-shakes all JavaScript -- Hashes file names for cache-busting -- Uses `environment.production.ts` +Build output is served by Nginx in Docker/ECS runtime. -The Docker image serves the `dist/` folder via Nginx with: -- `gzip` compression -- `Cache-Control` headers -- SPA fallback (`try_files $uri /index.html`) for Angular routing +## Environment files ---- +- `src/environments/environment.ts` (local) +- `src/environments/environment.production.ts` (production build) -## Docker - -```bash -# Build -docker build -t myproject-frontend:local ./frontend +These files only contain non-secret public config (for example API base URLs). -# Run -docker run --rm -p 80:80 myproject-frontend:local -# → http://localhost:80 +## Structure -# Multi-stage build internally: -# Stage 1: node:20-alpine → npm ci && ng build -# Stage 2: nginx:alpine → copies dist/ → serves on :80 +```text +frontend/ +├── src/ +│ ├── app/ +│ │ ├── components/ +│ │ ├── services/ +│ │ ├── core/ +│ │ └── types/ +│ └── environments/ +├── angular.json +├── package.json +└── Dockerfile ``` -In CI/CD: -- **Staging:** pushed to GHCR as `ghcr.io/your-org/frontend:staging-` by `staging.yml` -- **Production:** pushed to ECR as `.dkr.ecr.us-east-1.amazonaws.com/frontend:vX.Y.Z` by `release.yml` +## CI/CD notes + +- `ci.yml`: lint, type-check, build +- `staging.yml` and `release.yml`: build frontend image and push to ECR diff --git a/frontend/src/app/components/create-game/create-game.component.html b/frontend/src/app/components/create-game/create-game.component.html index c5d8d3f..8f2e5e9 100644 --- a/frontend/src/app/components/create-game/create-game.component.html +++ b/frontend/src/app/components/create-game/create-game.component.html @@ -41,7 +41,7 @@

Game Created! 🎉

- +

Your Game ID:

{{ gameId }}