diff --git a/.github/workflows/apiref-infra.yml b/.github/workflows/apiref-infra.yml new file mode 100644 index 00000000000..23cc6ed1265 --- /dev/null +++ b/.github/workflows/apiref-infra.yml @@ -0,0 +1,96 @@ +# https://help.github.com/en/categories/automating-your-workflow-with-github-actions + +name: "API Reference Infra" + +on: + workflow_dispatch: + pull_request: + paths: + - '.github/workflows/apiref-infra.yml' + - 'apigen/infra/**' + push: + branches: + - "2.2.x" + paths: + - '.github/workflows/apiref-infra.yml' + - 'apigen/infra/**' + +concurrency: apiref-infra + +jobs: + test: + name: "Test" + runs-on: "ubuntu-latest" + permissions: + contents: read + + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + with: + egress-policy: audit + + - name: "Checkout" + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + + - name: "Install Node" + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: "22" + cache: "npm" + cache-dependency-path: apigen/infra/package-lock.json + + - name: "Install dependencies" + working-directory: ./apigen/infra + run: "npm ci" + + - name: "TypeScript check" + working-directory: ./apigen/infra + run: "npm run check" + + - name: "Unit tests" + working-directory: ./apigen/infra + run: "npm test" + + - name: "CDK synth" + working-directory: ./apigen/infra + run: "npx cdk synth --all --quiet" + + deploy: + name: "Deploy" + runs-on: "ubuntu-latest" + needs: test + if: "github.event_name == 'push' && github.ref == 'refs/heads/2.2.x'" + permissions: + id-token: write + contents: read + + steps: + - name: Harden the runner (Audit all outbound calls) + uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 + with: + egress-policy: audit + + - name: "Checkout" + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + + - name: "Install Node" + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: "22" + cache: "npm" + cache-dependency-path: apigen/infra/package-lock.json + + - name: "Install dependencies" + working-directory: ./apigen/infra + run: "npm ci" + + - name: "Configure AWS credentials" + uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 + with: + role-to-assume: ${{ vars.APIREF_INFRA_DEPLOY_ROLE_ARN }} + aws-region: us-east-1 + + - name: "CDK deploy" + working-directory: ./apigen/infra + run: "npx cdk deploy --all --require-approval never" diff --git a/.github/workflows/apiref.yml b/.github/workflows/apiref.yml index 889ccbf5443..aa32551687d 100644 --- a/.github/workflows/apiref.yml +++ b/.github/workflows/apiref.yml @@ -11,6 +11,7 @@ on: - 'src/**' - 'composer.lock' - 'apigen/**' + - '!apigen/infra/**' - '.github/workflows/apiref.yml' env: @@ -64,43 +65,38 @@ jobs: - apigen if: github.repository_owner == 'phpstan' runs-on: "ubuntu-latest" + permissions: + id-token: write + contents: read steps: - name: Harden the runner (Audit all outbound calls) uses: step-security/harden-runner@5ef0c079ce82195b2a36a210272d6b661572d83e # v2.14.2 with: egress-policy: audit - - name: "Install Node" - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 - with: - node-version: "16" - - name: "Download docs" uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0 with: name: docs path: docs - - name: "Sync with S3" - uses: jakejarvis/s3-sync-action@be0c4ab89158cac4278689ebedd8407dd5f35a83 # v0.5.1 + - name: "Configure AWS credentials" + uses: aws-actions/configure-aws-credentials@d979d5b3a71173a29b74b5b88418bfda9437d885 # v6.1.1 with: - args: --exclude '.git*/*' --follow-symlinks - env: - SOURCE_DIR: './docs' - DEST_DIR: ${{ github.ref_name }} - AWS_REGION: 'eu-west-1' - AWS_S3_BUCKET: "web-apiref.phpstan.org" - AWS_ACCESS_KEY_ID: ${{ secrets.APIREF_AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.APIREF_AWS_SECRET_ACCESS_KEY }} + role-to-assume: ${{ vars.APIREF_DEPLOY_ROLE_ARN }} + aws-region: us-east-1 + + - name: "Sync with S3" + run: | + aws s3 sync ./docs "s3://${{ vars.APIREF_BUCKET }}/${{ github.ref_name }}" \ + --exclude '.git*/*' \ + --follow-symlinks - name: "Invalidate CloudFront" - uses: chetan/invalidate-cloudfront-action@12d242edc7752fca9140c2034be28792ad22c5a8 # v2.4.1 - env: - DISTRIBUTION: "E37G1C2KWNAPBD" - PATHS: '/${{ github.ref_name }}/*' - AWS_REGION: 'eu-west-1' - AWS_ACCESS_KEY_ID: ${{ secrets.APIREF_AWS_ACCESS_KEY_ID }} - AWS_SECRET_ACCESS_KEY: ${{ secrets.APIREF_AWS_SECRET_ACCESS_KEY }} + run: | + aws cloudfront create-invalidation \ + --distribution-id "${{ vars.APIREF_DISTRIBUTION_ID }}" \ + --paths "/${{ github.ref_name }}/*" - uses: peter-evans/repository-dispatch@ff45666b9427631e3450c54a1bcbee4d9ff4d7c0 # v3.0.0 with: diff --git a/apigen/infra/.gitignore b/apigen/infra/.gitignore new file mode 100644 index 00000000000..cd37b1e3441 --- /dev/null +++ b/apigen/infra/.gitignore @@ -0,0 +1,7 @@ +node_modules +*.js +!functions/*.js +*.d.ts +cdk.out +.cdk.staging +coverage diff --git a/apigen/infra/CLAUDE.md b/apigen/infra/CLAUDE.md new file mode 100644 index 00000000000..82e182cd54b --- /dev/null +++ b/apigen/infra/CLAUDE.md @@ -0,0 +1,133 @@ +# apiref.phpstan.org infrastructure (CDK) + +AWS CDK app (TypeScript) that defines the production infra for +[apiref.phpstan.org](https://apiref.phpstan.org) — the auto-generated ApiGen +reference for the PHPStan codebase. S3 origin, CloudFront distribution, edge +function for per-version landing-page redirects, security headers policy, ACM +cert, and the IAM roles that GitHub Actions assumes via OIDC. + +See `README.md` for the bootstrap, cutover, and cleanup runbook. + +This stack mirrors the main-site infra at +[`phpstan-dist`/website/infra](https://github.com/phpstan/phpstan/tree/2.2.x/website/infra) +— same patterns, same conventions; reach for that repo first when looking for +prior art. + +## Stacks + +Both stacks deploy to `us-east-1` (required for CloudFront + ACM). + +| Stack | Defined in | Resources | +| --- | --- | --- | +| `PhpstanApirefOidcRoles` | `lib/oidc-roles-stack.ts` | `phpstan-apiref-infra-deploy` IAM role used by `apiref-infra.yml`. **Reuses** the account-wide OIDC provider — does NOT create a new one (IAM rejects duplicates of the same provider URL). | +| `PhpstanApirefWebsite` | `lib/apiref-stack.ts` | Private S3 bucket (OAC, versioned), CloudFront distribution, CF Function 2.0, Response Headers Policy, DNS-validated ACM cert for `apiref.phpstan.org`, and `phpstan-apiref-deploy` IAM role used by `apiref.yml`. | + +`bin/infra.ts` is the CDK app entrypoint. It hard-codes the account/region/repo/zone constants and reads one CDK context flag, `productionAlias`, that toggles whether `apiref.phpstan.org` is attached to the distribution. + +## The `productionAlias` flag + +Defined in `cdk.json` under `context`, default `false`. + +- `false` (pre-cutover): distribution has no aliases and no ACM cert attached. CloudFormation can create the distribution without conflict even while the legacy `E37G1C2KWNAPBD` still owns the alias. The distribution serves on its `*.cloudfront.net` domain for pre-cutover testing. +- `true` (post-cutover): distribution carries `apiref.phpstan.org` and uses the ACM cert. + +The CDK code generates `Aliases: null` and `ViewerCertificate: null` when `productionAlias: false`. CloudFormation treats both as absent. + +## Out-of-band resources + +The Route 53 record for `apiref.phpstan.org` is **not** managed by CDK. It was +created/updated out-of-band during the cutover (raw `change-resource-record-sets`), +and CloudFormation cannot UPSERT a record that already exists outside its own +state. Same pattern as apex/www on the main site. + +## Edge function + +`functions/apiref-version-redirects.js` is the CloudFront Function 2.0 source. +It's a lookup-table version of the legacy `apiref-phpstan-org-viewer-request` +JS 1.0 function — same job: 301-redirect bare version URIs (e.g. `/2.2.x` or +`/2.2.x/`) to that version's landing page (`/namespace-PHPStan.html`), +and `/` to the current "latest" (2.2.x in this migration). + +302 → 301 was an intentional change to match the main site's redirects. + +The lookup table `VERSION_REDIRECTS` is hand-curated. When a new release branch +is added (say 2.3.x), append three entries: `'/2.3.x'`, `'/2.3.x/'`, both +mapping to `/2.3.x/namespace-PHPStan.html`. If 2.3.x should become the new +latest, also update the `'/'` entry. Then `npm test` ensures the lookup table +size and `/` mapping stay in sync. + +The file ends with `if (typeof module !== 'undefined') module.exports = {...}` +so it can be imported by Node-based unit tests. In the CloudFront runtime +`module` is undefined, so the export is silently skipped. + +## Project layout + +``` +apigen/infra/ +├── bin/infra.ts # CDK app entrypoint — wires both stacks +├── lib/ +│ ├── oidc-roles-stack.ts # IAM role (reuses existing OIDC provider) +│ └── apiref-stack.ts # everything that serves traffic +├── functions/ +│ └── apiref-version-redirects.js # CloudFront Function 2.0 source +├── test/ +│ ├── apiref-version-redirects.test.ts # Vitest: 25 redirect cases +│ └── apiref-stack.test.ts # Vitest: 11 CDK assertions +├── cdk.json # CDK config + context (incl. productionAlias) +├── package.json +├── tsconfig.json +├── vitest.config.ts +├── README.md # bootstrap + cutover runbook (human-facing) +└── CLAUDE.md # this file +``` + +## Conventions + +Same as the main-site infra: + +- **Tabs for indentation** in TS, JSON, and JS files. +- **2-space indent** for YAML workflows. +- **Pin GitHub Actions to commit SHAs** with the version in a trailing comment — matches the repo style and what `step-security/harden-runner` audits. +- **No `module.exports` / ESM imports in `functions/*.js`** — they run in the CloudFront Function runtime, not Node. The only allowed exception is the `typeof module` guard for unit-test interop. +- Resource IDs in CDK use **PascalCase**. Resource *names* (`bucketName`, `roleName`, `functionName`, `responseHeadersPolicyName`) use **kebab-case** with the `phpstan-apiref-` prefix so they're easy to spot in the console. +- Output exports use the `PhpstanApiref…` prefix. + +## Commands + +```sh +npm ci # install (run after pulling) +npm run check # tsc --noEmit +npm test # vitest run — 36 tests (redirect fn + stack assertions) +npm run synth # cdk synth --all (no AWS creds needed) +npm run diff # cdk diff --all (needs AWS creds for the target account) +npm run deploy # cdk deploy --all +``` + +`npm test` is the gate before any deploy — the CI workflow runs `check` + `test` + `synth` in a `test` job and blocks `diff` and `deploy` on it via `needs: test`. + +## CI + +`.github/workflows/apiref-infra.yml` triggers on PRs and pushes that touch +`apigen/infra/**` or the workflow file itself. Three jobs (same as the main +site's `website-infra.yml`): + +1. `test` — `npm ci && npm run check && npm test && npx cdk synth --all` (no AWS creds). +2. `diff` (needs: `test`) — assumes `APIREF_INFRA_DEPLOY_ROLE_ARN` via OIDC, runs `cdk diff --all`, posts a sticky PR comment. +3. `deploy` (needs: `[test, diff]`, only on push to `2.2.x`) — assumes the same role, runs `cdk deploy --all --require-approval never`. + +The `apiref.yml` workflow (the actual content deploy) uses `paths-ignore` via the inline `!apigen/infra/**` form so infra-only edits don't kick off a (slow) ApiGen rebuild. + +## When to edit what + +- **New release branch** (need a `/X.Y.x` → `/X.Y.x/namespace-PHPStan.html` redirect) → add three entries to `VERSION_REDIRECTS` in `functions/apiref-version-redirects.js` plus three test cases in `test/apiref-version-redirects.test.ts`. If it's the new latest, update `'/'` too. +- **Changing security headers** → `lib/apiref-stack.ts` (`responseHeadersPolicy` block), not the function. +- **Adding cache behaviors or new functions** → `lib/apiref-stack.ts`. Extend `test/apiref-stack.test.ts`. +- **Changing the trust policy** (e.g. allowing another branch to deploy) → `lib/oidc-roles-stack.ts` for infra deploys, or `lib/apiref-stack.ts` for the content deploy role. +- **Cutover flag** → `cdk.json` `context.productionAlias`. Only flip after the cutover script has done its work. + +## What lives elsewhere + +- The ApiGen tool, theme, and PHP filters — `../` (`apigen/apigen.neon`, `apigen/src/`, `apigen/theme/`). +- The PHP source code that ApiGen reads — `../../src/`. +- The build + publish pipeline — `.github/workflows/apiref.yml`. +- The main-site (`phpstan.org`) infra — separate repo `phpstan/phpstan` (the "dist" repo), under `website/infra/`. Identical patterns; consult it first when wondering "how did we solve X for the main site?". diff --git a/apigen/infra/README.md b/apigen/infra/README.md new file mode 100644 index 00000000000..111458b60b6 --- /dev/null +++ b/apigen/infra/README.md @@ -0,0 +1,109 @@ +# apiref.phpstan.org infrastructure (CDK) + +CDK app that defines the AWS infrastructure for [apiref.phpstan.org](https://apiref.phpstan.org) +— the auto-generated ApiGen reference for the PHPStan codebase. Private S3 bucket +with OAC, CloudFront distribution, CloudFront Function 2.0 for per-version +landing-page redirects, Response Headers Policy, ACM cert, and the IAM role +assumed by `apiref.yml` via OIDC. + +Same shape as the main-site infra at [`phpstan-dist`/website/infra](https://github.com/phpstan/phpstan/tree/2.2.x/website/infra). + +## Stacks + +| Stack | Resources | +| --- | --- | +| `PhpstanApirefOidcRoles` | `phpstan-apiref-infra-deploy` role (used by `apiref-infra.yml`). Reuses the account-wide OIDC provider — does NOT create a new one. | +| `PhpstanApirefWebsite` | S3 bucket (OAC, private, versioned), CloudFront distribution, CF Function 2.0, Response Headers Policy, ACM cert, `phpstan-apiref-deploy` role used by `apiref.yml`. | + +Region: `us-east-1` (required for CloudFront + ACM). + +## The `productionAlias` flag + +Defined in `cdk.json` under `context`. Default `false`. + +- `false`: distribution carries no aliases, no ACM cert attached (serves on the CF default `*.cloudfront.net` domain). First deploy succeeds while `apiref.phpstan.org` is still owned by the legacy distribution `E37G1C2KWNAPBD`. +- `true`: distribution carries `apiref.phpstan.org` as its alias and uses the new ACM cert. Set after the manual cutover. + +## Out-of-band resources + +The Route 53 record for `apiref.phpstan.org` is **not** managed by CDK. The +cutover script UPSERTs the record (via raw `change-resource-record-sets`); CDK +isn't aware of it. Same pattern as apex/www on the main site. + +## Local development + +```sh +npm ci +npm run check # tsc --noEmit +npm test # vitest: 25 redirect-fn tests + 11 stack assertions +npm run synth # cdk synth --all +npm run diff # cdk diff --all (needs AWS creds for the target account) +``` + +## One-time bootstrap + +The CDK bootstrap roles for the AWS account already exist (created by the +phpstan-dist repo's CDK app). You only need to deploy the OIDC roles stack +once, from a maintainer's laptop with admin AWS credentials: + +```sh +npx cdk deploy PhpstanApirefOidcRoles +``` + +Note the `InfraDeployRoleArn` output and set the corresponding GitHub repo +variable. + +## GitHub repo variables to set (in phpstan/phpstan-src) + +After the first deploys, set these under Settings → Secrets and variables → Actions → Variables: + +| Variable | Value | Used by | +|---|---|---| +| `APIREF_INFRA_DEPLOY_ROLE_ARN` | `InfraDeployRoleArn` output of `PhpstanApirefOidcRoles` | `apiref-infra.yml` | +| `APIREF_DEPLOY_ROLE_ARN` | `DeployRoleArn` output of `PhpstanApirefWebsite` | `apiref.yml` | +| `APIREF_BUCKET` | `phpstan-apiref-web` | `apiref.yml` | +| `APIREF_DISTRIBUTION_ID` | `DistributionId` output of `PhpstanApirefWebsite` | `apiref.yml` | + +## Cutover runbook (legacy → new) + +This moves `apiref.phpstan.org` from the legacy distribution `E37G1C2KWNAPBD` +to the new CDK-managed distribution. Expect ~5–10 min of intermittent 403s on +`apiref.phpstan.org` while CloudFront edges propagate the alias swap. + +**Pre-cutover (with `productionAlias: false`):** + +1. Merge the PR that adds this `apigen/infra/` directory. `apiref-infra.yml` deploys both stacks. +2. Copy bucket contents: `aws s3 sync s3://web-apiref.phpstan.org/ s3://phpstan-apiref-web/` (~334 MB / 13.5k objects). +3. Smoke-test on the new distribution's CF domain (look up `DistributionDomain` output): + ```sh + D=$(aws cloudfront get-distribution --id --query 'Distribution.DomainName' --output text) + curl -sI "https://$D/" # 301 to /2.2.x/namespace-PHPStan.html + curl -sI "https://$D/2.2.x/namespace-PHPStan.html" # 200 + curl -sI "https://$D/1.9.x" # 301 to /1.9.x/namespace-PHPStan.html + curl -sI "https://$D/" | grep -iE 'strict-transport|x-content-type|x-frame|referrer-policy|x-xss' + ``` + Verify HSTS, XCTO, XFO=SAMEORIGIN, Referrer-Policy present; no X-XSS-Protection. + +**Cutover (with `productionAlias: true`):** + +The sequence (do Route 53 first — we learned the hard way on the main site that CloudFront's `AddAlias` does a DNS sanity check): + +1. UPSERT Route 53 `apiref.phpstan.org` CNAME → new distribution's CF domain. +2. Wait for Route 53 INSYNC (~30–60s). +3. Detach `apiref.phpstan.org` from `E37G1C2KWNAPBD` via `aws cloudfront update-distribution`. +4. Add `apiref.phpstan.org` to the new distribution (retry every 20s if CloudFront's DNS-check cache is stale). +5. Wait for new distribution `Deployed`. +6. Smoke-test against `https://apiref.phpstan.org/`. + +Then merge the PR that flips `productionAlias: true` in `cdk.json`. The +workflow's `cdk deploy` is a no-op for the alias (already attached by the +script) and just syncs CFN state. + +## Cleanup runbook (when stable for ~1 week) + +- Delete CloudFront distribution `E37G1C2KWNAPBD` (disable, wait, delete). +- Delete CloudFront Functions `apiref-phpstan-org-viewer-request` and `secure-headers-response` (the latter has no remaining users after `E37G1C2KWNAPBD` is gone). +- Delete the legacy ACM cert `arn:aws:acm:us-east-1:928192134594:certificate/18f4edec-8bec-4f52-a02b-a9738053b817` once unreferenced. +- Empty and delete S3 bucket `web-apiref.phpstan.org`. +- Delete the `APIREF_AWS_ACCESS_KEY_ID` and `APIREF_AWS_SECRET_ACCESS_KEY` GitHub secrets. +- (Optional follow-up) Migrate `update-playground-api.yml` and `update-playground-runner.yml` to OIDC — they're the last workflows in this repo still using `PLAYGROUND_RUNNER_AWS_*` static keys. diff --git a/apigen/infra/bin/infra.ts b/apigen/infra/bin/infra.ts new file mode 100644 index 00000000000..433a065933d --- /dev/null +++ b/apigen/infra/bin/infra.ts @@ -0,0 +1,49 @@ +#!/usr/bin/env node +import * as cdk from 'aws-cdk-lib'; +import { ApirefStack } from '../lib/apiref-stack'; +import { OidcRolesStack } from '../lib/oidc-roles-stack'; + +const app = new cdk.App(); + +const account = process.env.CDK_DEFAULT_ACCOUNT ?? '928192134594'; +const region = 'us-east-1'; +const env = { account, region }; + +const githubOrg = 'phpstan'; +const githubRepo = 'phpstan-src'; +const deployBranch = '2.2.x'; + +// Account-wide OIDC provider, created originally in the phpstan-dist repo's +// CDK app. We reference it by ARN — never instantiate a new one, IAM rejects +// duplicates of the same provider URL. +const oidcProviderArn = `arn:aws:iam::${account}:oidc-provider/token.actions.githubusercontent.com`; + +const hostedZoneId = 'Z3OJGVJEUUWZDN'; +const hostedZoneName = 'phpstan.org'; +const apirefDomain = 'apiref.phpstan.org'; + +const productionAlias = app.node.tryGetContext('productionAlias') === true; + +new OidcRolesStack(app, 'PhpstanApirefOidcRoles', { + env, + githubOrg, + githubRepo, + deployBranch, + oidcProviderArn, + description: 'IAM role for the apiref-infra GitHub Actions workflow (OIDC).', +}); + +new ApirefStack(app, 'PhpstanApirefWebsite', { + env, + githubOrg, + githubRepo, + deployBranch, + oidcProviderArn, + hostedZoneId, + hostedZoneName, + apirefDomain, + productionAlias, + description: `apiref.phpstan.org website (S3 + CloudFront + CF Function). productionAlias=${productionAlias}`, +}); + +app.synth(); diff --git a/apigen/infra/cdk.json b/apigen/infra/cdk.json new file mode 100644 index 00000000000..42cfa6b84e5 --- /dev/null +++ b/apigen/infra/cdk.json @@ -0,0 +1,30 @@ +{ + "app": "npx ts-node --prefer-ts-exts bin/infra.ts", + "watch": { + "include": ["**"], + "exclude": [ + "README.md", + "cdk*.json", + "**/*.d.ts", + "**/*.js", + "tsconfig.json", + "package*.json", + "node_modules", + "test" + ] + }, + "context": { + "productionAlias": false, + "@aws-cdk/aws-lambda:recognizeLayerVersion": true, + "@aws-cdk/core:checkSecretUsage": true, + "@aws-cdk/core:target-partitions": ["aws"], + "@aws-cdk/aws-iam:minimizePolicies": true, + "@aws-cdk/core:validateSnapshotRemovalPolicy": true, + "@aws-cdk/aws-s3:createDefaultLoggingPolicy": true, + "@aws-cdk/aws-cloudfront:defaultSecurityPolicyTLSv1.2_2021": true, + "@aws-cdk/aws-iam:standardizedServicePrincipals": true, + "@aws-cdk/aws-ec2:restrictDefaultSecurityGroup": true, + "@aws-cdk/core:includePrefixInUniqueNameGeneration": true, + "@aws-cdk/aws-lambda-nodejs:useLatestRuntimeVersion": true + } +} diff --git a/apigen/infra/functions/apiref-version-redirects.js b/apigen/infra/functions/apiref-version-redirects.js new file mode 100644 index 00000000000..69133d4d6d4 --- /dev/null +++ b/apigen/infra/functions/apiref-version-redirects.js @@ -0,0 +1,52 @@ +// CloudFront Function (runtime cloudfront-js-2.0), viewer-request. +// +// Replaces the legacy `apiref-phpstan-org-viewer-request` JS 1.0 function. +// Same job: redirect bare-version URIs (e.g. `/2.2.x` or `/2.2.x/`) to that +// version's landing page. `/` redirects to the current "latest" — bumped +// from 2.1.x to 2.2.x in this migration. +// +// When a new branch lands, add three entries here: `/X.Y.x`, `/X.Y.x/`, and +// update the `/` mapping if it should become the new latest. +// +// CloudFront runs this file directly and looks for a top-level `function handler`. +// The trailing `module.exports` is gated on `typeof module` so the same source +// can be imported into Node-based unit tests; in the CF runtime `module` is not +// defined, so the export is silently skipped. + +var VERSION_REDIRECTS = { + '/': '/2.2.x/namespace-PHPStan.html', + '/2.2.x': '/2.2.x/namespace-PHPStan.html', + '/2.2.x/': '/2.2.x/namespace-PHPStan.html', + '/2.1.x': '/2.1.x/namespace-PHPStan.html', + '/2.1.x/': '/2.1.x/namespace-PHPStan.html', + '/2.0.x': '/2.0.x/namespace-PHPStan.html', + '/2.0.x/': '/2.0.x/namespace-PHPStan.html', + '/1.12.x': '/1.12.x/namespace-PHPStan.html', + '/1.12.x/': '/1.12.x/namespace-PHPStan.html', + '/1.11.x': '/1.11.x/namespace-PHPStan.html', + '/1.11.x/': '/1.11.x/namespace-PHPStan.html', + '/1.10.x': '/1.10.x/namespace-PHPStan.html', + '/1.10.x/': '/1.10.x/namespace-PHPStan.html', + '/1.9.x': '/1.9.x/namespace-PHPStan.html', + '/1.9.x/': '/1.9.x/namespace-PHPStan.html', + // 1.8.x exists in the bucket but had no landing-page redirect in the + // legacy function; preserve that gap. +}; + +function handler(event) { + var target = VERSION_REDIRECTS[event.request.uri]; + if (target) { + return { + statusCode: 301, + statusDescription: 'Moved Permanently', + headers: { + location: { value: target }, + }, + }; + } + return event.request; +} + +if (typeof module !== 'undefined') { + module.exports = { handler: handler, VERSION_REDIRECTS: VERSION_REDIRECTS }; +} diff --git a/apigen/infra/lib/apiref-stack.ts b/apigen/infra/lib/apiref-stack.ts new file mode 100644 index 00000000000..2f70bf7495e --- /dev/null +++ b/apigen/infra/lib/apiref-stack.ts @@ -0,0 +1,181 @@ +import * as cdk from 'aws-cdk-lib'; +import * as acm from 'aws-cdk-lib/aws-certificatemanager'; +import * as cloudfront from 'aws-cdk-lib/aws-cloudfront'; +import * as origins from 'aws-cdk-lib/aws-cloudfront-origins'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import * as route53 from 'aws-cdk-lib/aws-route53'; +import * as s3 from 'aws-cdk-lib/aws-s3'; +import { Construct } from 'constructs'; +import * as path from 'node:path'; + +export interface ApirefStackProps extends cdk.StackProps { + readonly githubOrg: string; + readonly githubRepo: string; + readonly deployBranch: string; + readonly oidcProviderArn: string; + readonly hostedZoneId: string; + readonly hostedZoneName: string; + readonly apirefDomain: string; + readonly productionAlias: boolean; +} + +// The apiref.phpstan.org infrastructure: private S3 bucket served via +// CloudFront with OAC, a CloudFront Function 2.0 for per-version landing-page +// redirects, a Response Headers Policy for security headers, an ACM cert +// (DNS-validated), and the IAM role used by the apiref.yml workflow. +// +// The `productionAlias` flag toggles whether `apiref.phpstan.org` is attached +// to the distribution. We start at `false` so the first deploy succeeds while +// the alias still lives on the legacy distribution. After the cutover script +// moves the alias, we flip to `true` so CDK code matches reality. +// +// Route 53: the `apiref.phpstan.org` CNAME is created and managed out-of-band +// during the cutover (matches the apex/www pattern from the main site). +export class ApirefStack extends cdk.Stack { + readonly bucket: s3.Bucket; + readonly distribution: cloudfront.Distribution; + readonly deployRole: iam.Role; + + constructor(scope: Construct, id: string, props: ApirefStackProps) { + super(scope, id, props); + + this.bucket = new s3.Bucket(this, 'ApirefBucket', { + bucketName: 'phpstan-apiref-web', + blockPublicAccess: s3.BlockPublicAccess.BLOCK_ALL, + encryption: s3.BucketEncryption.S3_MANAGED, + versioned: true, + removalPolicy: cdk.RemovalPolicy.RETAIN, + enforceSSL: true, + }); + + const hostedZone = route53.HostedZone.fromHostedZoneAttributes(this, 'HostedZone', { + hostedZoneId: props.hostedZoneId, + zoneName: props.hostedZoneName, + }); + + const certificate = new acm.Certificate(this, 'Certificate', { + domainName: props.apirefDomain, + validation: acm.CertificateValidation.fromDns(hostedZone), + }); + + const edgeFunction = new cloudfront.Function(this, 'VersionRedirectsFunction', { + functionName: 'apiref-version-redirects', + comment: 'Viewer-request: per-version landing-page redirects for apiref.phpstan.org.', + runtime: cloudfront.FunctionRuntime.JS_2_0, + code: cloudfront.FunctionCode.fromFile({ + filePath: path.join(__dirname, '..', 'functions', 'apiref-version-redirects.js'), + }), + }); + + const responseHeadersPolicy = new cloudfront.ResponseHeadersPolicy(this, 'SecurityHeadersPolicy', { + responseHeadersPolicyName: 'apiref-security-headers', + comment: 'HSTS, X-Content-Type-Options, X-Frame-Options, Referrer-Policy for apiref.phpstan.org.', + securityHeadersBehavior: { + strictTransportSecurity: { + accessControlMaxAge: cdk.Duration.days(365), + includeSubdomains: true, + preload: true, + override: true, + }, + contentTypeOptions: { override: true }, + frameOptions: { + frameOption: cloudfront.HeadersFrameOption.SAMEORIGIN, + override: true, + }, + referrerPolicy: { + referrerPolicy: cloudfront.HeadersReferrerPolicy.STRICT_ORIGIN_WHEN_CROSS_ORIGIN, + override: true, + }, + }, + }); + + const domainNames = props.productionAlias ? [props.apirefDomain] : undefined; + const distributionCertificate = props.productionAlias ? certificate : undefined; + + this.distribution = new cloudfront.Distribution(this, 'Distribution', { + comment: `apiref.phpstan.org (productionAlias=${props.productionAlias})`, + domainNames, + certificate: distributionCertificate, + defaultRootObject: 'index.html', + minimumProtocolVersion: cloudfront.SecurityPolicyProtocol.TLS_V1_2_2021, + priceClass: cloudfront.PriceClass.PRICE_CLASS_100, + httpVersion: cloudfront.HttpVersion.HTTP2_AND_3, + enableIpv6: true, + defaultBehavior: { + origin: origins.S3BucketOrigin.withOriginAccessControl(this.bucket), + viewerProtocolPolicy: cloudfront.ViewerProtocolPolicy.REDIRECT_TO_HTTPS, + allowedMethods: cloudfront.AllowedMethods.ALLOW_GET_HEAD, + cachedMethods: cloudfront.CachedMethods.CACHE_GET_HEAD, + compress: true, + cachePolicy: cloudfront.CachePolicy.CACHING_OPTIMIZED, + responseHeadersPolicy, + functionAssociations: [{ + function: edgeFunction, + eventType: cloudfront.FunctionEventType.VIEWER_REQUEST, + }], + }, + }); + + this.deployRole = this.createDeployRole(props); + + new cdk.CfnOutput(this, 'BucketName', { + value: this.bucket.bucketName, + description: 'S3 bucket name for the apiref content', + exportName: 'PhpstanApirefBucketName', + }); + new cdk.CfnOutput(this, 'DistributionId', { + value: this.distribution.distributionId, + description: 'CloudFront distribution ID for apiref', + exportName: 'PhpstanApirefDistributionId', + }); + new cdk.CfnOutput(this, 'DistributionDomain', { + value: this.distribution.distributionDomainName, + description: 'CloudFront default domain (used for pre-cutover testing)', + }); + new cdk.CfnOutput(this, 'DeployRoleArn', { + value: this.deployRole.roleArn, + description: 'Role ARN for the apiref content GitHub Actions workflow', + exportName: 'PhpstanApirefDeployRoleArn', + }); + new cdk.CfnOutput(this, 'CertificateArn', { + value: certificate.certificateArn, + description: 'ACM cert for apiref.phpstan.org (attached only when productionAlias=true)', + }); + } + + private createDeployRole(props: ApirefStackProps): iam.Role { + const role = new iam.Role(this, 'DeployRole', { + roleName: 'phpstan-apiref-deploy', + description: 'Assumed by the apiref.yml GitHub Actions workflow to sync the bucket and invalidate CloudFront.', + assumedBy: new iam.FederatedPrincipal( + props.oidcProviderArn, + { + StringEquals: { + 'token.actions.githubusercontent.com:aud': 'sts.amazonaws.com', + }, + StringLike: { + 'token.actions.githubusercontent.com:sub': `repo:${props.githubOrg}/${props.githubRepo}:ref:refs/heads/${props.deployBranch}`, + }, + }, + 'sts:AssumeRoleWithWebIdentity', + ), + maxSessionDuration: cdk.Duration.hours(1), + }); + + this.bucket.grantReadWrite(role); + this.bucket.grantDelete(role); + + role.addToPolicy(new iam.PolicyStatement({ + actions: [ + 'cloudfront:CreateInvalidation', + 'cloudfront:GetInvalidation', + 'cloudfront:ListInvalidations', + ], + resources: [ + `arn:aws:cloudfront::${this.account}:distribution/${this.distribution.distributionId}`, + ], + })); + + return role; + } +} diff --git a/apigen/infra/lib/oidc-roles-stack.ts b/apigen/infra/lib/oidc-roles-stack.ts new file mode 100644 index 00000000000..e9e94d89fd7 --- /dev/null +++ b/apigen/infra/lib/oidc-roles-stack.ts @@ -0,0 +1,57 @@ +import * as cdk from 'aws-cdk-lib'; +import * as iam from 'aws-cdk-lib/aws-iam'; +import { Construct } from 'constructs'; + +export interface OidcRolesStackProps extends cdk.StackProps { + readonly githubOrg: string; + readonly githubRepo: string; + readonly deployBranch: string; + readonly oidcProviderArn: string; +} + +// IAM role used by the apiref-infra GitHub Actions workflow to run +// `cdk diff` / `cdk deploy`. Reuses the account-wide OIDC provider that +// already exists (created by the phpstan-dist repo's CDK app); we do NOT +// create a new `OpenIdConnectProvider` because IAM rejects duplicates. +export class OidcRolesStack extends cdk.Stack { + readonly infraDeployRole: iam.Role; + + constructor(scope: Construct, id: string, props: OidcRolesStackProps) { + super(scope, id, props); + + const subjectPrefix = `repo:${props.githubOrg}/${props.githubRepo}`; + const allowedSubjects = [ + `${subjectPrefix}:ref:refs/heads/${props.deployBranch}`, + `${subjectPrefix}:pull_request`, + ]; + + this.infraDeployRole = new iam.Role(this, 'InfraDeployRole', { + roleName: 'phpstan-apiref-infra-deploy', + description: 'Assumed by the apiref-infra GitHub Actions workflow to run cdk diff / cdk deploy.', + assumedBy: new iam.FederatedPrincipal( + props.oidcProviderArn, + { + StringEquals: { + 'token.actions.githubusercontent.com:aud': 'sts.amazonaws.com', + }, + StringLike: { + 'token.actions.githubusercontent.com:sub': allowedSubjects, + }, + }, + 'sts:AssumeRoleWithWebIdentity', + ), + maxSessionDuration: cdk.Duration.hours(1), + }); + + this.infraDeployRole.addToPolicy(new iam.PolicyStatement({ + actions: ['sts:AssumeRole'], + resources: [`arn:aws:iam::${this.account}:role/cdk-*`], + })); + + new cdk.CfnOutput(this, 'InfraDeployRoleArn', { + value: this.infraDeployRole.roleArn, + description: 'Role ARN for the apiref-infra GitHub Actions workflow', + exportName: 'PhpstanApirefInfraDeployRoleArn', + }); + } +} diff --git a/apigen/infra/package-lock.json b/apigen/infra/package-lock.json new file mode 100644 index 00000000000..b8fd040ef02 --- /dev/null +++ b/apigen/infra/package-lock.json @@ -0,0 +1,2295 @@ +{ + "name": "phpstan-apiref-infra", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "phpstan-apiref-infra", + "version": "1.0.0", + "license": "MIT", + "bin": { + "infra": "bin/infra.js" + }, + "devDependencies": { + "@types/node": "^22.10.0", + "aws-cdk": "^2.1010.0", + "aws-cdk-lib": "^2.180.0", + "constructs": "^10.4.2", + "ts-node": "^10.9.2", + "typescript": "^5.7.2", + "vitest": "^3.0.0" + } + }, + "node_modules/@aws-cdk/asset-awscli-v1": { + "version": "2.2.273", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-awscli-v1/-/asset-awscli-v1-2.2.273.tgz", + "integrity": "sha512-X57HYUtHt9BQrlrzUNcMyRsDUCoakYNnY6qh5lNwRCHPtQoTfXmuISkfLk0AjLkcbS5lw1LLTQFiQhTDXfiTvg==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@aws-cdk/asset-node-proxy-agent-v6": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@aws-cdk/asset-node-proxy-agent-v6/-/asset-node-proxy-agent-v6-2.1.1.tgz", + "integrity": "sha512-We4bmHaowOPHr+IQR4/FyTGjRfjgBj4ICMjtqmJeBDWad3Q/6St12NT07leNtyuukv2qMhtSZJQorD8KpKTwRA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@aws-cdk/cloud-assembly-schema": { + "version": "53.22.0", + "resolved": "https://registry.npmjs.org/@aws-cdk/cloud-assembly-schema/-/cloud-assembly-schema-53.22.0.tgz", + "integrity": "sha512-2GLhjjf7Db697rRyYt6rjN06fwbBIiewPo/jxSCyUN259ejF7NQQRwvrdAQb9Aq2jPaIIp1cfHSoEdXyA6Bb7Q==", + "bundleDependencies": [ + "jsonschema", + "semver" + ], + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "jsonschema": "~1.4.1", + "semver": "^7.7.4" + }, + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/jsonschema": { + "version": "1.4.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/@aws-cdk/cloud-assembly-schema/node_modules/semver": { + "version": "7.7.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.3.tgz", + "integrity": "sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.3.tgz", + "integrity": "sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.3.tgz", + "integrity": "sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.3.tgz", + "integrity": "sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.3.tgz", + "integrity": "sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.3.tgz", + "integrity": "sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.3.tgz", + "integrity": "sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.3.tgz", + "integrity": "sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.3.tgz", + "integrity": "sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.3.tgz", + "integrity": "sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.3.tgz", + "integrity": "sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.3.tgz", + "integrity": "sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.3.tgz", + "integrity": "sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.3.tgz", + "integrity": "sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.3.tgz", + "integrity": "sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.3.tgz", + "integrity": "sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.3.tgz", + "integrity": "sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.3.tgz", + "integrity": "sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.3.tgz", + "integrity": "sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.3.tgz", + "integrity": "sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.3.tgz", + "integrity": "sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.3.tgz", + "integrity": "sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.3.tgz", + "integrity": "sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.3.tgz", + "integrity": "sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.3.tgz", + "integrity": "sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", + "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.4.tgz", + "integrity": "sha512-Io0yyORnB6sikFlt8QW5K7slY4OjqNX9jmJQ02QDda8lyM6B5oNgVWoSoKPac8/kgnCUzuHQKrSLtu/uOqqrig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.4.tgz", + "integrity": "sha512-46ryTE9RZO/rfDd7pEqFl7etuyzekzEhUbTW3BvmeO/BcCMEgq59BKhek3dXDWgAj4oMK6OZi+vRr1wPW6qjEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.4", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.4.tgz", + "integrity": "sha512-IVNZik8IVRJRTr9fxlitMKeJeXFFFN0JaB9PHPGQ8NKQbGpfjlTx9zO4RefN8gp7eqjNy8nyK3NZmBzOPeIxtA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.4.tgz", + "integrity": "sha512-oukfKT9Mk41LreEW09vt45f8wx7DordoWUZMYdY/cyAk7w5TWkTRCNZYF7sX7n2wB7jyGAl74OxgwhPgKaqDMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.4", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.4.tgz", + "integrity": "sha512-dEYtS7qQP2CjU27QBC5oUOxLE/v5eLkGqPE0ZKEIDGMs4vKWe7IjgLOeauHsR0D5YuuycGRO5oSRXnwnmA78fQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.4.tgz", + "integrity": "sha512-vAfasCOe6AIK70iP5UD11Ac4siNUNJ9i/9PZ3NKx07sG6sUxeag1LWdNrMWeKKYBLlzuK+Gn65Yd5nyL6ds+nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.4.tgz", + "integrity": "sha512-fB2V0JFrQSMsCo9HiSq3Ezpdv4iYaXRG1Sx8edX3MwxfyNn83mKiGzOcH+Fkxt4MHxr3y42fQi1oeAInqgX2QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.4", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.5", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.5.tgz", + "integrity": "sha512-HEHNfbars9v4pgpW6SO1KSPkfoS0xVOM/9UzkJltjlsHZmJasxg8aXkuZa7SMf8vKGIBhpUsPluQSqhJFCqebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/aws-cdk": { + "version": "2.1121.0", + "resolved": "https://registry.npmjs.org/aws-cdk/-/aws-cdk-2.1121.0.tgz", + "integrity": "sha512-cG7CHt/SytYTfwrK+BUNQpqmS1dwhjt8z6ExKL6GK4n+8/6ZCwFzxlZWA/jUd2+Y9xPc+Q8cLKfMqGmgxEXbkg==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "cdk": "bin/cdk" + }, + "engines": { + "node": ">= 18.0.0" + } + }, + "node_modules/aws-cdk-lib": { + "version": "2.253.1", + "resolved": "https://registry.npmjs.org/aws-cdk-lib/-/aws-cdk-lib-2.253.1.tgz", + "integrity": "sha512-vy+hA15/ZfSQpivkNdlIn2ZDA2hesp3WJgmtIZJDFwu6xzwv7wH7glbAdu5xCHGcOjepOaTKZSvCPC6sN+0/Vw==", + "bundleDependencies": [ + "@balena/dockerignore", + "@aws-cdk/cloud-assembly-api", + "case", + "fs-extra", + "ignore", + "jsonschema", + "minimatch", + "punycode", + "semver", + "table", + "yaml", + "mime-types" + ], + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-cdk/asset-awscli-v1": "2.2.273", + "@aws-cdk/asset-node-proxy-agent-v6": "^2.1.1", + "@aws-cdk/cloud-assembly-api": "^2.2.2", + "@aws-cdk/cloud-assembly-schema": "^53.18.0", + "@balena/dockerignore": "^1.0.2", + "case": "1.6.3", + "fs-extra": "^11.3.3", + "ignore": "^5.3.2", + "jsonschema": "^1.5.0", + "mime-types": "^2.1.35", + "minimatch": "^10.2.3", + "punycode": "^2.3.1", + "semver": "^7.7.4", + "table": "^6.9.0", + "yaml": "1.10.3" + }, + "engines": { + "node": ">= 20.0.0" + }, + "peerDependencies": { + "constructs": "^10.5.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/@aws-cdk/cloud-assembly-api": { + "version": "2.2.2", + "bundleDependencies": [ + "jsonschema", + "semver" + ], + "dev": true, + "inBundle": true, + "license": "Apache-2.0", + "dependencies": { + "jsonschema": "~1.4.1", + "semver": "^7.7.4" + }, + "engines": { + "node": ">= 18.0.0" + }, + "peerDependencies": { + "@aws-cdk/cloud-assembly-schema": ">=53.15.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/@balena/dockerignore": { + "version": "1.0.2", + "dev": true, + "inBundle": true, + "license": "Apache-2.0" + }, + "node_modules/aws-cdk-lib/node_modules/ajv": { + "version": "8.18.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/aws-cdk-lib/node_modules/ansi-regex": { + "version": "5.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/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/aws-cdk-lib/node_modules/astral-regex": { + "version": "2.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/balanced-match": { + "version": "4.0.4", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/aws-cdk-lib/node_modules/brace-expansion": { + "version": "5.0.5", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/aws-cdk-lib/node_modules/case": { + "version": "1.6.3", + "dev": true, + "inBundle": true, + "license": "(MIT OR GPL-3.0-or-later)", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/color-convert": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/color-name": { + "version": "1.1.4", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/emoji-regex": { + "version": "8.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/fast-deep-equal": { + "version": "3.1.3", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/fast-uri": { + "version": "3.1.0", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "inBundle": true, + "license": "BSD-3-Clause" + }, + "node_modules/aws-cdk-lib/node_modules/fs-extra": { + "version": "11.3.3", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/aws-cdk-lib/node_modules/graceful-fs": { + "version": "4.2.11", + "dev": true, + "inBundle": true, + "license": "ISC" + }, + "node_modules/aws-cdk-lib/node_modules/ignore": { + "version": "5.3.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/aws-cdk-lib/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/aws-cdk-lib/node_modules/json-schema-traverse": { + "version": "1.0.0", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/jsonfile": { + "version": "6.2.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/jsonschema": { + "version": "1.5.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/aws-cdk-lib/node_modules/lodash.truncate": { + "version": "4.4.2", + "dev": true, + "inBundle": true, + "license": "MIT" + }, + "node_modules/aws-cdk-lib/node_modules/mime-db": { + "version": "1.52.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/mime-types": { + "version": "2.1.35", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/aws-cdk-lib/node_modules/minimatch": { + "version": "10.2.5", + "dev": true, + "inBundle": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/aws-cdk-lib/node_modules/punycode": { + "version": "2.3.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/aws-cdk-lib/node_modules/require-from-string": { + "version": "2.0.2", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/semver": { + "version": "7.7.4", + "dev": true, + "inBundle": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/aws-cdk-lib/node_modules/slice-ansi": { + "version": "4.0.0", + "dev": true, + "inBundle": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" + } + }, + "node_modules/aws-cdk-lib/node_modules/string-width": { + "version": "4.2.3", + "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/aws-cdk-lib/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/aws-cdk-lib/node_modules/table": { + "version": "6.9.0", + "dev": true, + "inBundle": true, + "license": "BSD-3-Clause", + "dependencies": { + "ajv": "^8.0.1", + "lodash.truncate": "^4.4.2", + "slice-ansi": "^4.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/universalify": { + "version": "2.0.1", + "dev": true, + "inBundle": true, + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/aws-cdk-lib/node_modules/yaml": { + "version": "1.10.3", + "dev": true, + "inBundle": true, + "license": "ISC", + "engines": { + "node": ">= 6" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/constructs": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/constructs/-/constructs-10.6.0.tgz", + "integrity": "sha512-TxHOnBO5zMo/G76ykzGF/wMpEHu257TbWiIxP9K0Yv/+t70UzgBQiTqjkAsWOPC6jW91DzJI0+ehQV6xDRNBuQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/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-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "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/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/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/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "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": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.14", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", + "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rollup": { + "version": "4.60.3", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.3.tgz", + "integrity": "sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.3", + "@rollup/rollup-android-arm64": "4.60.3", + "@rollup/rollup-darwin-arm64": "4.60.3", + "@rollup/rollup-darwin-x64": "4.60.3", + "@rollup/rollup-freebsd-arm64": "4.60.3", + "@rollup/rollup-freebsd-x64": "4.60.3", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.3", + "@rollup/rollup-linux-arm-musleabihf": "4.60.3", + "@rollup/rollup-linux-arm64-gnu": "4.60.3", + "@rollup/rollup-linux-arm64-musl": "4.60.3", + "@rollup/rollup-linux-loong64-gnu": "4.60.3", + "@rollup/rollup-linux-loong64-musl": "4.60.3", + "@rollup/rollup-linux-ppc64-gnu": "4.60.3", + "@rollup/rollup-linux-ppc64-musl": "4.60.3", + "@rollup/rollup-linux-riscv64-gnu": "4.60.3", + "@rollup/rollup-linux-riscv64-musl": "4.60.3", + "@rollup/rollup-linux-s390x-gnu": "4.60.3", + "@rollup/rollup-linux-x64-gnu": "4.60.3", + "@rollup/rollup-linux-x64-musl": "4.60.3", + "@rollup/rollup-openbsd-x64": "4.60.3", + "@rollup/rollup-openharmony-arm64": "4.60.3", + "@rollup/rollup-win32-arm64-msvc": "4.60.3", + "@rollup/rollup-win32-ia32-msvc": "4.60.3", + "@rollup/rollup-win32-x64-gnu": "4.60.3", + "@rollup/rollup-win32-x64-msvc": "4.60.3", + "fsevents": "~2.3.2" + } + }, + "node_modules/rollup/node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.16", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.16.tgz", + "integrity": "sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/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", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.3.tgz", + "integrity": "sha512-/4XH147Ui7OGTjg3HbdWe5arnZQSbfuRzdr9Ec7TQi5I7R+ir0Rlc9GIvD4v0XZurELqA035KVXJXpR61xhiTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.4.tgz", + "integrity": "sha512-LUCP5ev3GURDysTWiP47wRRUpLKMOfPh+yKTx3kVIEiu5KOMeqzpnYNsKyOoVrULivR8tLcks4+lga33Whn90A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.4", + "@vitest/mocker": "3.2.4", + "@vitest/pretty-format": "^3.2.4", + "@vitest/runner": "3.2.4", + "@vitest/snapshot": "3.2.4", + "@vitest/spy": "3.2.4", + "@vitest/utils": "3.2.4", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.4", + "@vitest/ui": "3.2.4", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/apigen/infra/package.json b/apigen/infra/package.json new file mode 100644 index 00000000000..4a94bba451f --- /dev/null +++ b/apigen/infra/package.json @@ -0,0 +1,29 @@ +{ + "name": "phpstan-apiref-infra", + "version": "1.0.0", + "license": "MIT", + "private": true, + "bin": { + "infra": "bin/infra.js" + }, + "scripts": { + "build": "tsc", + "watch": "tsc -w", + "check": "tsc -p tsconfig.json --noEmit", + "test": "vitest run", + "test:watch": "vitest", + "cdk": "cdk", + "synth": "cdk synth --all", + "diff": "cdk diff --all", + "deploy": "cdk deploy --all" + }, + "devDependencies": { + "@types/node": "^22.10.0", + "aws-cdk": "^2.1010.0", + "aws-cdk-lib": "^2.180.0", + "constructs": "^10.4.2", + "ts-node": "^10.9.2", + "typescript": "^5.7.2", + "vitest": "^3.0.0" + } +} diff --git a/apigen/infra/test/apiref-stack.test.ts b/apigen/infra/test/apiref-stack.test.ts new file mode 100644 index 00000000000..82b2cb9a59e --- /dev/null +++ b/apigen/infra/test/apiref-stack.test.ts @@ -0,0 +1,163 @@ +import { App } from 'aws-cdk-lib'; +import { Match, Template } from 'aws-cdk-lib/assertions'; +import { describe, expect, it } from 'vitest'; +import { ApirefStack } from '../lib/apiref-stack'; + +const baseProps = { + env: { account: '928192134594', region: 'us-east-1' }, + githubOrg: 'phpstan', + githubRepo: 'phpstan-src', + deployBranch: '2.2.x', + oidcProviderArn: 'arn:aws:iam::928192134594:oidc-provider/token.actions.githubusercontent.com', + hostedZoneId: 'Z3OJGVJEUUWZDN', + hostedZoneName: 'phpstan.org', + apirefDomain: 'apiref.phpstan.org', +}; + +function synth(productionAlias: boolean): Template { + const app = new App(); + const stack = new ApirefStack(app, 'TestApiref', { ...baseProps, productionAlias }); + return Template.fromStack(stack); +} + +describe('ApirefStack', () => { + describe('common (regardless of productionAlias)', () => { + const template = synth(false); + + it('creates a private S3 bucket with versioning and SSL enforcement', () => { + template.hasResourceProperties('AWS::S3::Bucket', { + BucketName: 'phpstan-apiref-web', + PublicAccessBlockConfiguration: { + BlockPublicAcls: true, + BlockPublicPolicy: true, + IgnorePublicAcls: true, + RestrictPublicBuckets: true, + }, + VersioningConfiguration: { Status: 'Enabled' }, + }); + }); + + it('denies insecure transport in the bucket policy', () => { + template.hasResourceProperties('AWS::S3::BucketPolicy', { + PolicyDocument: Match.objectLike({ + Statement: Match.arrayWith([ + Match.objectLike({ + Effect: 'Deny', + Condition: { Bool: { 'aws:SecureTransport': 'false' } }, + }), + ]), + }), + }); + }); + + it('creates an Origin Access Control', () => { + template.resourceCountIs('AWS::CloudFront::OriginAccessControl', 1); + }); + + it('creates the CloudFront Function on JS 2.0', () => { + template.hasResourceProperties('AWS::CloudFront::Function', { + Name: 'apiref-version-redirects', + FunctionConfig: Match.objectLike({ Runtime: 'cloudfront-js-2.0' }), + }); + }); + + it('creates a Response Headers Policy with HSTS, XCTO, XFO, Referrer-Policy and no X-XSS-Protection', () => { + template.hasResourceProperties('AWS::CloudFront::ResponseHeadersPolicy', { + ResponseHeadersPolicyConfig: Match.objectLike({ + Name: 'apiref-security-headers', + SecurityHeadersConfig: Match.objectLike({ + StrictTransportSecurity: Match.objectLike({ + AccessControlMaxAgeSec: 365 * 24 * 60 * 60, + IncludeSubdomains: true, + Preload: true, + Override: true, + }), + ContentTypeOptions: { Override: true }, + FrameOptions: { FrameOption: 'SAMEORIGIN', Override: true }, + ReferrerPolicy: { ReferrerPolicy: 'strict-origin-when-cross-origin', Override: true }, + }), + }), + }); + template.hasResourceProperties('AWS::CloudFront::ResponseHeadersPolicy', { + ResponseHeadersPolicyConfig: { + SecurityHeadersConfig: Match.not(Match.objectLike({ XSSProtection: Match.anyValue() })), + }, + }); + }); + + it('uses HTTP/2+3 and TLS 1.2_2021 minimum, with the function and headers policy attached', () => { + template.hasResourceProperties('AWS::CloudFront::Distribution', { + DistributionConfig: Match.objectLike({ + HttpVersion: 'http2and3', + IPV6Enabled: true, + DefaultCacheBehavior: Match.objectLike({ + ViewerProtocolPolicy: 'redirect-to-https', + Compress: true, + FunctionAssociations: Match.arrayWith([ + Match.objectLike({ EventType: 'viewer-request' }), + ]), + ResponseHeadersPolicyId: Match.anyValue(), + }), + }), + }); + }); + + it('issues a DNS-validated ACM cert for apiref.phpstan.org', () => { + template.hasResourceProperties('AWS::CertificateManager::Certificate', { + DomainName: 'apiref.phpstan.org', + ValidationMethod: 'DNS', + }); + }); + + it('creates the deploy role scoped to the phpstan-src 2.2.x branch via OIDC', () => { + template.hasResourceProperties('AWS::IAM::Role', { + RoleName: 'phpstan-apiref-deploy', + AssumeRolePolicyDocument: Match.objectLike({ + Statement: Match.arrayWith([ + Match.objectLike({ + Action: 'sts:AssumeRoleWithWebIdentity', + Condition: { + StringEquals: { 'token.actions.githubusercontent.com:aud': 'sts.amazonaws.com' }, + StringLike: { + 'token.actions.githubusercontent.com:sub': 'repo:phpstan/phpstan-src:ref:refs/heads/2.2.x', + }, + }, + }), + ]), + }), + }); + }); + + it('does not create any Route 53 records (apex/staging CNAME stays externally managed)', () => { + template.resourceCountIs('AWS::Route53::RecordSet', 0); + }); + }); + + describe('productionAlias: false (pre-cutover)', () => { + const template = synth(false); + + it('omits the alias and the ACM cert from the distribution (default CF cert is used)', () => { + const distributions = template.findResources('AWS::CloudFront::Distribution'); + const config = Object.values(distributions)[0].Properties.DistributionConfig; + // CDK synthesizes `null` for undefined optional properties; CFN treats it as absent. + expect(config.Aliases ?? null).toBeNull(); + expect(config.ViewerCertificate ?? null).toBeNull(); + }); + }); + + describe('productionAlias: true (post-cutover)', () => { + const template = synth(true); + + it('attaches apiref.phpstan.org as the alias and uses the ACM cert', () => { + template.hasResourceProperties('AWS::CloudFront::Distribution', { + DistributionConfig: Match.objectLike({ + Aliases: ['apiref.phpstan.org'], + ViewerCertificate: Match.objectLike({ + MinimumProtocolVersion: 'TLSv1.2_2021', + SslSupportMethod: 'sni-only', + }), + }), + }); + }); + }); +}); diff --git a/apigen/infra/test/apiref-version-redirects.test.ts b/apigen/infra/test/apiref-version-redirects.test.ts new file mode 100644 index 00000000000..779fb500360 --- /dev/null +++ b/apigen/infra/test/apiref-version-redirects.test.ts @@ -0,0 +1,75 @@ +import { describe, expect, it } from 'vitest'; +// eslint-disable-next-line @typescript-eslint/no-require-imports +const { handler, VERSION_REDIRECTS } = require('../functions/apiref-version-redirects.js'); + +interface CfEvent { + request: { + uri: string; + method?: string; + }; +} + +function event(uri: string): CfEvent { + return { request: { uri, method: 'GET' } }; +} + +describe('apiref-version-redirects handler', () => { + describe('version landing-page redirects', () => { + const cases: Array<[string, string]> = [ + ['/', '/2.2.x/namespace-PHPStan.html'], + ['/2.2.x', '/2.2.x/namespace-PHPStan.html'], + ['/2.2.x/', '/2.2.x/namespace-PHPStan.html'], + ['/2.1.x', '/2.1.x/namespace-PHPStan.html'], + ['/2.1.x/', '/2.1.x/namespace-PHPStan.html'], + ['/2.0.x', '/2.0.x/namespace-PHPStan.html'], + ['/2.0.x/', '/2.0.x/namespace-PHPStan.html'], + ['/1.12.x', '/1.12.x/namespace-PHPStan.html'], + ['/1.12.x/', '/1.12.x/namespace-PHPStan.html'], + ['/1.11.x', '/1.11.x/namespace-PHPStan.html'], + ['/1.11.x/', '/1.11.x/namespace-PHPStan.html'], + ['/1.10.x', '/1.10.x/namespace-PHPStan.html'], + ['/1.10.x/', '/1.10.x/namespace-PHPStan.html'], + ['/1.9.x', '/1.9.x/namespace-PHPStan.html'], + ['/1.9.x/', '/1.9.x/namespace-PHPStan.html'], + ]; + + for (const [uri, location] of cases) { + it(`${uri} -> 301 ${location}`, () => { + const result = handler(event(uri)); + expect(result.statusCode).toBe(301); + expect(result.statusDescription).toBe('Moved Permanently'); + expect(result.headers.location.value).toBe(location); + }); + } + + it('exposes the same lookup table to tests via module.exports', () => { + expect(VERSION_REDIRECTS['/']).toBe('/2.2.x/namespace-PHPStan.html'); + expect(Object.keys(VERSION_REDIRECTS)).toHaveLength(cases.length); + }); + + it('latest mapping points to 2.2.x (the post-migration default)', () => { + expect(VERSION_REDIRECTS['/']).toBe(VERSION_REDIRECTS['/2.2.x']); + }); + }); + + describe('pass-throughs', () => { + const passThrough = [ + '/2.2.x/namespace-PHPStan.html', + '/2.2.x/PHPStan/Analyser.html', + '/2.2.x/some/deep/path.html', + '/assets/style.css', + '/1.8.x', // 1.8.x is intentionally not in the redirect table + '/1.8.x/', + '/random', + '/random/path', + ]; + + for (const uri of passThrough) { + it(`${uri} passes through unchanged`, () => { + const result = handler(event(uri)); + expect(result.statusCode).toBeUndefined(); + expect(result.uri).toBe(uri); + }); + } + }); +}); diff --git a/apigen/infra/tsconfig.json b/apigen/infra/tsconfig.json new file mode 100644 index 00000000000..c2aae07b406 --- /dev/null +++ b/apigen/infra/tsconfig.json @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "commonjs", + "lib": ["es2022"], + "declaration": true, + "strict": true, + "noImplicitAny": true, + "strictNullChecks": true, + "noImplicitThis": true, + "alwaysStrict": true, + "noUnusedLocals": true, + "noUnusedParameters": false, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "inlineSourceMap": true, + "inlineSources": true, + "experimentalDecorators": true, + "strictPropertyInitialization": false, + "resolveJsonModule": true, + "esModuleInterop": true, + "skipLibCheck": true, + "typeRoots": ["./node_modules/@types"] + }, + "include": ["bin/**/*.ts", "lib/**/*.ts", "test/**/*.ts"], + "exclude": ["node_modules", "cdk.out"] +} diff --git a/apigen/infra/vitest.config.ts b/apigen/infra/vitest.config.ts new file mode 100644 index 00000000000..5ae75888702 --- /dev/null +++ b/apigen/infra/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + include: ['test/**/*.test.ts'], + }, +});