diff --git a/.env.example b/.env.example index ae3cf46..a232761 100644 --- a/.env.example +++ b/.env.example @@ -1,18 +1,26 @@ -# supercut director — LLM provider (OpenAI-compatible). Copy to .env and fill in. +# supercut director — LLM provider. Copy to .env and fill in. # Only `supercut generate` needs this; `record` and `render` work without a key. +# generate sends crawled DOM text, optional screenshots, and optional repo notes +# to the configured provider. Use trusted apps/providers only. -# ── DeepSeek (recommended) ─────────────────────────────────────────────── +# ── Provider selection ────────────────────────────────────────────────── +# Optional when exactly one provider key is set. Required to disambiguate if +# multiple provider keys exist. +# SUPERCUT_PROVIDER=deepseek # deepseek | openrouter | custom + +# ── DeepSeek (text-only; DOM-only analysis, no screenshot QC) ─────────── # Get a key: https://platform.deepseek.com/api_keys DEEPSEEK_API_KEY= +# SUPERCUT_MODEL=deepseek-v4-pro # or deepseek-v4-flash +# SUPERCUT_VISION=false # DeepSeek text-only; true is rejected -# Model: deepseek-v4-pro (smart, recommended for the director) or -# deepseek-v4-flash (cheaper/faster). Both are text-only. -SUPERCUT_MODEL=deepseek-v4-pro - -# ── OR OpenRouter (one key, many models incl. vision) ──────────────────── +# ── OpenRouter (can use vision-capable models) ────────────────────────── # OPENROUTER_API_KEY= # SUPERCUT_MODEL=anthropic/claude-sonnet-4.6 +# SUPERCUT_VISION=true -# ── Advanced overrides (usually leave blank) ───────────────────────────── -# SUPERCUT_LLM_BASE_URL= # custom endpoint -# SUPERCUT_VISION= # "true"/"false" to force vision on/off +# ── Custom OpenAI-compatible endpoint ─────────────────────────────────── +# SUPERCUT_API_KEY= +# SUPERCUT_LLM_BASE_URL=https://your-compatible-endpoint.example/api/v1 +# SUPERCUT_MODEL=your-model-name # required for custom endpoints +# SUPERCUT_VISION=false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..d51624d --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,36 @@ +name: CI + +on: + pull_request: + push: + branches: [main] + +permissions: + contents: read + +jobs: + typecheck-and-unit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: 22 + cache: npm + - run: npm ci + - run: npm run typecheck + - run: npm test -- --run test/cursor.test.ts test/director.test.ts test/director-validation.test.ts test/schema.test.ts test/config.test.ts test/url-policy.test.ts test/redaction.test.ts test/plan.test.ts + - run: npm audit --audit-level=moderate + + browser-e2e: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0 + with: + node-version: 22 + cache: npm + - run: npm ci + - run: npx playwright install --with-deps chromium + - run: sudo apt-get update && sudo apt-get install -y ffmpeg + - run: npm test -- --run test/record.e2e.test.ts test/generate.e2e.test.ts diff --git a/README.md b/README.md index 71fed49..97977f7 100644 --- a/README.md +++ b/README.md @@ -4,27 +4,130 @@ Institutional-grade, max-60-second launch videos generated from your **real** product — not an HTML mockup. A scripted browser performs your app on camera; -a cinematic renderer adds the Screen Studio look (spring zoom-to-cursor, -motion blur, padded background, music on the beat grid); an AI director writes -the script and quality-checks the footage. +a cinematic renderer adds the Screen Studio look: spring zoom-to-cursor, +motion blur, padded background, and a polished final export. -**Status: pre-release, under active construction.** The design doc and build -plan are complete; stages are landing in order. Nothing to install yet. +**Status: pre-release.** The core record/render/generate pipeline exists, but +it is still being hardened. Use it on trusted apps and trusted recipes. +```text + your app URL ──▶ ① analyze pick the 2-4 money moments (LLM) + ② script write the filming recipe (LLM, schema-validated) + ③ record deterministic browser executor performs it + ④ qc deterministic + optional vision checks, bounded retakes + ⑤ render cinematic compositing ──▶ final.mp4 (≤60s target) ``` - your app URL ──▶ ① analyze pick the 3-4 money moments (LLM) - ② script write the filming recipe, beat-aligned (LLM, schema-validated) - ③ record a deterministic browser executor performs it (pure code) - ④ qc vision checks the footage, refilms what's bad (bounded loop) - ⑤ render cinematic compositing + music ──▶ launch.mp4 (≤60s, 1080p60) + +## Install for local development + +```bash +npm install +npm run typecheck +npm run test:fast +``` + +For browser/video tests you also need Chromium + ffmpeg: + +```bash +npx playwright install chromium +npm run test:e2e +``` + +## CLI + +```bash +npm run build +node dist/cli/index.js doctor +node dist/cli/index.js record --recipe examples/demo.recipe.json --out out/take --seed 1 +node dist/cli/index.js render --take out/take --out out/final.mp4 +node dist/cli/index.js generate --url https://your-app.example --out out/generate --yes +``` + +### Private/local apps + +`generate` blocks localhost, RFC1918, link-local, and cloud metadata addresses +by default. If you intentionally want to film a local development app, opt in: + +```bash +node dist/cli/index.js generate --url http://127.0.0.1:3000 --allow-private-network --yes +``` + +Do not use recipes or URLs you do not trust. They drive real browser navigation. + +## LLM provider setup + +Copy `.env.example` to `.env` or pass `--env-file `. + +```bash +cp .env.example .env +``` + +DeepSeek is text-only in this project, so Supercut disables screenshots and +vision QC for DeepSeek by default: + +```env +SUPERCUT_PROVIDER=deepseek +DEEPSEEK_API_KEY=... +SUPERCUT_MODEL=deepseek-v4-pro +``` + +OpenRouter/custom OpenAI-compatible providers can use vision-capable models: + +```env +SUPERCUT_PROVIDER=openrouter +OPENROUTER_API_KEY=... +SUPERCUT_MODEL=anthropic/claude-sonnet-4.6 +SUPERCUT_VISION=true +``` + +For `SUPERCUT_PROVIDER=custom`, set both `SUPERCUT_LLM_BASE_URL` and +`SUPERCUT_MODEL`; Supercut will not guess an OpenRouter model for your endpoint. + +If multiple provider keys are present, set `SUPERCUT_PROVIDER` explicitly. +Ambiguous provider configuration fails loudly rather than guessing. + +## Privacy warning + +`supercut generate` may send crawled DOM text, element labels/selectors, +optional screenshots, and optional repo notes (`--repo`) to the configured LLM +provider. It can also persist sensitive frames, recipes, and director reports in +`out/`. Review those artifacts before sharing them. + +Use `record` + `render` for a no-LLM workflow. + +## Event-log contract + +The public boundary is: + +```text +recipe.json ──▶ record ──▶ take directory + ├─ events.json + ├─ frames-index.json + └─ frames/*.png + +take directory ──▶ render ──▶ final.mp4 +``` + +Schemas reject unsupported URL schemes, malformed known events, non-monotonic +timelines, oversized event logs, and impossible camera/zoom boxes. + +## Project principles + +- Real product footage beats mockups. +- The event log is a public contract. +- Non-AI recorder/render paths must remain useful without an API key. +- Defaults should fail loudly on unsafe or ambiguous config. + +## Contributing + +```bash +npm run typecheck +npm run test:fast +npm run test:e2e +npm audit --audit-level=moderate ``` -- Real footage only — no fake UI renders, ever -- The event-log JSON between recorder and renderer is a public contract; any - recorder can feed it -- Stages ③ and ⑤ run standalone with zero API key (`supercut record`, - `supercut render`) -- MIT, CC0-only bundled music, macOS + Linux +Keep PRs focused and add tests for behavior changes. ## License diff --git a/package-lock.json b/package-lock.json index cc75896..2d49317 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,16 +19,50 @@ "@types/node": "^20.17.0", "tsx": "^4.19.0", "typescript": "^5.6.0", - "vitest": "^2.1.0" + "vitest": "^4.1.8" }, "engines": { "node": ">=20" } }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.0.tgz", - "integrity": "sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", "cpu": [ "ppc64" ], @@ -43,9 +77,9 @@ } }, "node_modules/@esbuild/android-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.0.tgz", - "integrity": "sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", "cpu": [ "arm" ], @@ -60,9 +94,9 @@ } }, "node_modules/@esbuild/android-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.0.tgz", - "integrity": "sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", "cpu": [ "arm64" ], @@ -77,9 +111,9 @@ } }, "node_modules/@esbuild/android-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.0.tgz", - "integrity": "sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", "cpu": [ "x64" ], @@ -94,9 +128,9 @@ } }, "node_modules/@esbuild/darwin-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.0.tgz", - "integrity": "sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", "cpu": [ "arm64" ], @@ -111,9 +145,9 @@ } }, "node_modules/@esbuild/darwin-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.0.tgz", - "integrity": "sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", "cpu": [ "x64" ], @@ -128,9 +162,9 @@ } }, "node_modules/@esbuild/freebsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.0.tgz", - "integrity": "sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", "cpu": [ "arm64" ], @@ -145,9 +179,9 @@ } }, "node_modules/@esbuild/freebsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.0.tgz", - "integrity": "sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", "cpu": [ "x64" ], @@ -162,9 +196,9 @@ } }, "node_modules/@esbuild/linux-arm": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.0.tgz", - "integrity": "sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", "cpu": [ "arm" ], @@ -179,9 +213,9 @@ } }, "node_modules/@esbuild/linux-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.0.tgz", - "integrity": "sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", "cpu": [ "arm64" ], @@ -196,9 +230,9 @@ } }, "node_modules/@esbuild/linux-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.0.tgz", - "integrity": "sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", "cpu": [ "ia32" ], @@ -213,9 +247,9 @@ } }, "node_modules/@esbuild/linux-loong64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.0.tgz", - "integrity": "sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", "cpu": [ "loong64" ], @@ -230,9 +264,9 @@ } }, "node_modules/@esbuild/linux-mips64el": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.0.tgz", - "integrity": "sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", "cpu": [ "mips64el" ], @@ -247,9 +281,9 @@ } }, "node_modules/@esbuild/linux-ppc64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.0.tgz", - "integrity": "sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", "cpu": [ "ppc64" ], @@ -264,9 +298,9 @@ } }, "node_modules/@esbuild/linux-riscv64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.0.tgz", - "integrity": "sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", "cpu": [ "riscv64" ], @@ -281,9 +315,9 @@ } }, "node_modules/@esbuild/linux-s390x": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.0.tgz", - "integrity": "sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", "cpu": [ "s390x" ], @@ -298,9 +332,9 @@ } }, "node_modules/@esbuild/linux-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.0.tgz", - "integrity": "sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", "cpu": [ "x64" ], @@ -315,9 +349,9 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.0.tgz", - "integrity": "sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", "cpu": [ "arm64" ], @@ -332,9 +366,9 @@ } }, "node_modules/@esbuild/netbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.0.tgz", - "integrity": "sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", "cpu": [ "x64" ], @@ -349,9 +383,9 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.0.tgz", - "integrity": "sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", "cpu": [ "arm64" ], @@ -366,9 +400,9 @@ } }, "node_modules/@esbuild/openbsd-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.0.tgz", - "integrity": "sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", "cpu": [ "x64" ], @@ -383,9 +417,9 @@ } }, "node_modules/@esbuild/openharmony-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.0.tgz", - "integrity": "sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", "cpu": [ "arm64" ], @@ -400,9 +434,9 @@ } }, "node_modules/@esbuild/sunos-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.0.tgz", - "integrity": "sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", "cpu": [ "x64" ], @@ -417,9 +451,9 @@ } }, "node_modules/@esbuild/win32-arm64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.0.tgz", - "integrity": "sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", "cpu": [ "arm64" ], @@ -434,9 +468,9 @@ } }, "node_modules/@esbuild/win32-ia32": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.0.tgz", - "integrity": "sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", "cpu": [ "ia32" ], @@ -451,9 +485,9 @@ } }, "node_modules/@esbuild/win32-x64": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.0.tgz", - "integrity": "sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", "cpu": [ "x64" ], @@ -474,24 +508,39 @@ "dev": true, "license": "MIT" }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.61.1.tgz", - "integrity": "sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==", - "cpu": [ - "arm" - ], + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.5.tgz", + "integrity": "sha512-AWPoBRJ9tsnVhor4sjO7rkni+7p+2IAEFj6cx06UgP10jkQHqay/36uRV/bFkgrh18D9vb4cr8Q0Pthskgzy+Q==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "android" - ] + "dependencies": { + "@tybys/wasm-util": "^0.10.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.133.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.133.0.tgz", + "integrity": "sha512-KzkdCd6Uxqnf6l3HOw1xfatAlUURA0g14cvBYFyJ5SaNOQbOUvBr9PKArcPcrNIeRsBdgcUzOGrhKveVpvOIGA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.61.1.tgz", - "integrity": "sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.3.tgz", + "integrity": "sha512-454rs7jHngixp/NMxd5srYD57OnzSlZ/eFTETjORQHLwJG1lRtmNOJcBerZlfu4GjKqeq8aCCIQrMdHyhI51Hw==", "cpu": [ "arm64" ], @@ -500,12 +549,15 @@ "optional": true, "os": [ "android" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.61.1.tgz", - "integrity": "sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.3.tgz", + "integrity": "sha512-PcAhP+ynjURNyy8SKGl5DQP94aGuB/7JrXJb/t7P+hanXvQVMWzUvRRhBAcg/lNRadBhoUPqSoP4xw5tR/KBEA==", "cpu": [ "arm64" ], @@ -514,12 +566,15 @@ "optional": true, "os": [ "darwin" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.61.1.tgz", - "integrity": "sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.3.tgz", + "integrity": "sha512-9YpfeUvSE2RS7wysJ81uOZkXJz7f7Q55H2Gvp3VEw/EsahqDtrphrZ0EwDLK5vvKOzaCrBsjF8JmnMLcUt78Gg==", "cpu": [ "x64" ], @@ -528,26 +583,15 @@ "optional": true, "os": [ "darwin" - ] - }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.61.1.tgz", - "integrity": "sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==", - "cpu": [ - "arm64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.61.1.tgz", - "integrity": "sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.3.tgz", + "integrity": "sha512-yB1IlAsSNHncV6SCTL27/MVGR5htvQsoGxIv5KMGXALp+Ll1wYsn+x98M9MW7qa+NdSbvrrY7ANI4wLJ0n1e6g==", "cpu": [ "x64" ], @@ -556,26 +600,15 @@ "optional": true, "os": [ "freebsd" - ] - }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.61.1.tgz", - "integrity": "sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==", - "cpu": [ - "arm" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.61.1.tgz", - "integrity": "sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.3.tgz", + "integrity": "sha512-Yi30IVAAfLUCy2MseFjbB1jAMDl1VMCAas5StnYp8da9+CKvMd2H2cbEjWcw5NPaPqzvYkVIaF1nNUG+b7u/sw==", "cpu": [ "arm" ], @@ -584,26 +617,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.61.1.tgz", - "integrity": "sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==", - "cpu": [ - "arm64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.61.1.tgz", - "integrity": "sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.3.tgz", + "integrity": "sha512-jsO7R8To+AdlYgUmN5sHSCZbfhtMBkO0WUx8iORQnPcMMdgr7qM2DQmMwgabs3GhNztdmoKkMKQFHD6DTMCIQw==", "cpu": [ "arm64" ], @@ -612,54 +634,32 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.61.1.tgz", - "integrity": "sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==", - "cpu": [ - "loong64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.61.1.tgz", - "integrity": "sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.3.tgz", + "integrity": "sha512-VWkUHwWriDciit80wleYwKILoR/KMvxh/IdwS/paX+ZgpuRpCrKLUdadJbc0NpBEiyhpYawsJ73j9aCvOH+f7Q==", "cpu": [ - "loong64" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.61.1.tgz", - "integrity": "sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==", - "cpu": [ - "ppc64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.61.1.tgz", - "integrity": "sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==", + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.3.tgz", + "integrity": "sha512-5f1laC0SlIR0yDbFCd8acUhvJIag6N3zC5P7oUPN6wX0aOma+uKJ0wBDH5aq7I1PVI2ttTlhJwzwRIBnLiSGEg==", "cpu": [ "ppc64" ], @@ -668,40 +668,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.61.1.tgz", - "integrity": "sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.61.1.tgz", - "integrity": "sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==", - "cpu": [ - "riscv64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.61.1.tgz", - "integrity": "sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.3.tgz", + "integrity": "sha512-Iq4ko0r4XsgbrF/LunNgHtAGLRRVE2kXonAXQ/MV0mC6jQpMOhW1SvtZja2EhC/kd05++bP78dsqBeIQyYJ6Yg==", "cpu": [ "s390x" ], @@ -710,12 +685,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.1.tgz", - "integrity": "sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.3.tgz", + "integrity": "sha512-B8m6tD5+/N5FeNQFbKlLA/2yVq9ycQP1SeedyEYYKWBNR3ZQbkvIUcNnDNM03lO1l5F2roiiFJGgvoLLyZXtSg==", "cpu": [ "x64" ], @@ -724,12 +702,15 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.61.1.tgz", - "integrity": "sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.3.tgz", + "integrity": "sha512-pSdpdUJHkuCxun9LE7jvgUB9qsRgaiyNNCX7m/AvHTcq67AiT/Yhoxvw5zPfhrM8k/BfP8ce/hMOpthKDpEUow==", "cpu": [ "x64" ], @@ -738,26 +719,15 @@ "optional": true, "os": [ "linux" - ] - }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.61.1.tgz", - "integrity": "sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==", - "cpu": [ - "x64" ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.61.1.tgz", - "integrity": "sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==", + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.3.tgz", + "integrity": "sha512-OXXS3RKJgX2uLwM+gYyuH5omcH8fL1LJs96pZGgtetVCahON57+d4SJHzTgZiOjxgGkSnpXpOsWuPDGAKAigEg==", "cpu": [ "arm64" ], @@ -766,40 +736,51 @@ "optional": true, "os": [ "openharmony" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.61.1.tgz", - "integrity": "sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==", + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.3.tgz", + "integrity": "sha512-JTtb8BWFynicNSoPrehsCzBtOKjZ6jhMiPFEmOiuXg1Fl8dn2KHQob+GuPSGR0dryQa1PQJbzjF3dqO/whhjLg==", "cpu": [ - "arm64" + "wasm32" ], "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.61.1.tgz", - "integrity": "sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==", + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.3.tgz", + "integrity": "sha512-gEdFFEN70A/jxb2svrWsN3aDL7OUtmvlOy+6fa2jxG8K0wQ1ZbdeLGnidov6Yu5/733dI5ySfzFlQ/cb0bSz1g==", "cpu": [ - "ia32" + "arm64" ], "dev": true, "license": "MIT", "optional": true, "os": [ "win32" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.61.1.tgz", - "integrity": "sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==", + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.3.tgz", + "integrity": "sha512-eXB7CHuaQdqmJcc3koCNtNPmT/bj2gc999kUFgBxG8Ac0NdgXc4rkCHhqrgrhN3zddvvvrgzj1e90SuSfmyIXA==", "cpu": [ "x64" ], @@ -808,21 +789,53 @@ "optional": true, "os": [ "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.61.1.tgz", - "integrity": "sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==", - "cpu": [ - "x64" ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", "dev": true, "license": "MIT", "optional": true, - "os": [ - "win32" - ] + "dependencies": { + "tslib": "^2.4.0" + } + }, + "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", @@ -842,38 +855,40 @@ } }, "node_modules/@vitest/expect": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.9.tgz", - "integrity": "sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.8.tgz", + "integrity": "sha512-h3nDO677RDLEGlBxyQ5CW8RlMThSKSRLUePLOx09gNIWRL40edgA1GCZSZgf1W55MFAG6/Sw14KeaAnqv0NKdQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.9", - "@vitest/utils": "2.1.9", - "chai": "^5.1.2", - "tinyrainbow": "^1.2.0" + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/mocker": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.9.tgz", - "integrity": "sha512-tVL6uJgoUdi6icpxmdrn5YNo3g3Dxv+IHJBr0GXHaEdTcw3F+cPKnsXFhli6nO+f/6SDKPHEK1UN+k+TQv0Ehg==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.8.tgz", + "integrity": "sha512-LEiN/xe4OSIbKe9HQIp5OC24agGD9J5CnmMgsLohVVoOPWL9a2sBoR6VBx43jQZb7Kr1l4RCuyCJzcAa0+dojw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "2.1.9", + "@vitest/spy": "4.1.8", "estree-walker": "^3.0.3", - "magic-string": "^0.30.12" + "magic-string": "^0.30.21" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "msw": "^2.4.9", - "vite": "^5.0.0" + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "msw": { @@ -885,70 +900,68 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", - "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.8.tgz", + "integrity": "sha512-9GasEBxpZ1VYIpqHf/0+YGg121uSNwCKOJqIrTwWP/TB7DmFCiaBpNl3aPZzoLWfWkuqhbH8vJIVobZkvdo2cA==", "dev": true, "license": "MIT", "dependencies": { - "tinyrainbow": "^1.2.0" + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/runner": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.9.tgz", - "integrity": "sha512-ZXSSqTFIrzduD63btIfEyOmNcBmQvgOVsPNPe0jYtESiXkhd8u2erDLnMxmGrDCwHCCHE7hxwRDCT3pt0esT4g==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.8.tgz", + "integrity": "sha512-EmVxeBAfMJvycdjd6Hm+RbFBbA9fKvo0Kx37hNpBYoYeavH3RNsBXWDooR1mgD52dCrxIIuP7UotpfiwOikvcg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "2.1.9", - "pathe": "^1.1.2" + "@vitest/utils": "4.1.8", + "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/snapshot": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.9.tgz", - "integrity": "sha512-oBO82rEjsxLNJincVhLhaxxZdEtV0EFHMK5Kmx5sJ6H9L183dHECjiefOAdnqpIgT5eZwT04PoggUnW88vOBNQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.8.tgz", + "integrity": "sha512-acfZboRmAIf05DEKcBQy33VXojFJjtUdLyo7oOmV9kebb2xdU01UknNiPuPZoJZQyO7DF0gZdTGTpeAzET9QPQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.9", - "magic-string": "^0.30.12", - "pathe": "^1.1.2" + "@vitest/pretty-format": "4.1.8", + "@vitest/utils": "4.1.8", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/spy": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.9.tgz", - "integrity": "sha512-E1B35FwzXXTs9FHNK6bDszs7mtydNi5MIfUWpceJ8Xbfb1gBMscAnwLbEu+B44ed6W3XjL9/ehLPHR1fkf1KLQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.8.tgz", + "integrity": "sha512-6EevtBp6OZOPF7bmz36HrGMeP3txgVSrgebWxHOafDXGkhIzfXK14f8KF6MuFfgXXUeHxmpD3BQxkV00/3s5mA==", "dev": true, "license": "MIT", - "dependencies": { - "tinyspy": "^3.0.2" - }, "funding": { "url": "https://opencollective.com/vitest" } }, "node_modules/@vitest/utils": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.9.tgz", - "integrity": "sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.8.tgz", + "integrity": "sha512-uOJamYALNhfJ6iolExyQM40yIQwDqYnkKtQ5VCiSe17E33H0aQ/u+1GlRuz4LZBk6Mm3sg90G9hEbmEt37C1Zg==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "2.1.9", - "loupe": "^3.1.2", - "tinyrainbow": "^1.2.0" + "@vitest/pretty-format": "4.1.8", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" }, "funding": { "url": "https://opencollective.com/vitest" @@ -964,82 +977,44 @@ "node": ">=12" } }, - "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==", + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", "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/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true, - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } + "license": "MIT" }, - "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==", + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", "dev": true, - "license": "MIT", + "license": "Apache-2.0", "engines": { - "node": ">=6" + "node": ">=8" } }, "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==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", "dev": true, "license": "MIT" }, "node_modules/esbuild": { - "version": "0.28.0", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.0.tgz", - "integrity": "sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==", + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -1050,32 +1025,32 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.28.0", - "@esbuild/android-arm": "0.28.0", - "@esbuild/android-arm64": "0.28.0", - "@esbuild/android-x64": "0.28.0", - "@esbuild/darwin-arm64": "0.28.0", - "@esbuild/darwin-x64": "0.28.0", - "@esbuild/freebsd-arm64": "0.28.0", - "@esbuild/freebsd-x64": "0.28.0", - "@esbuild/linux-arm": "0.28.0", - "@esbuild/linux-arm64": "0.28.0", - "@esbuild/linux-ia32": "0.28.0", - "@esbuild/linux-loong64": "0.28.0", - "@esbuild/linux-mips64el": "0.28.0", - "@esbuild/linux-ppc64": "0.28.0", - "@esbuild/linux-riscv64": "0.28.0", - "@esbuild/linux-s390x": "0.28.0", - "@esbuild/linux-x64": "0.28.0", - "@esbuild/netbsd-arm64": "0.28.0", - "@esbuild/netbsd-x64": "0.28.0", - "@esbuild/openbsd-arm64": "0.28.0", - "@esbuild/openbsd-x64": "0.28.0", - "@esbuild/openharmony-arm64": "0.28.0", - "@esbuild/sunos-x64": "0.28.0", - "@esbuild/win32-arm64": "0.28.0", - "@esbuild/win32-ia32": "0.28.0", - "@esbuild/win32-x64": "0.28.0" + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.1" } }, "node_modules/estree-walker": { @@ -1098,6 +1073,24 @@ "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.2", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", @@ -1112,66 +1105,317 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/loupe": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", - "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", "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", + "license": "MPL-2.0", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" - } - }, - "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==", + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], "dev": true, - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "node_modules/pathe": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", - "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], "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==", + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "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/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/obug": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.2.tgz", + "integrity": "sha512-AWGB9WFcRXOQs48Z/udjI5ZcZMHXwX8XPByNpOydgcGsDLIzjGizhoMWJyKAWze7AVW/2W1i+/gPX4YtKe5cyg==", "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], "license": "MIT", "engines": { - "node": ">= 14.16" + "node": ">=12.20.0" } }, + "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/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -1179,6 +1423,19 @@ "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/playwright": { "version": "1.60.0", "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz", @@ -1238,49 +1495,38 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/rollup": { - "version": "4.61.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.61.1.tgz", - "integrity": "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==", + "node_modules/rolldown": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.3.tgz", + "integrity": "sha512-i00lAJ2ks1BYr7rjNjKC7BcqAS7nVfiT3QX1SI5aY+AFHblCmaUf9OE9dbdzDvW6dJxbi2ZCZiy9v3CcwOiX3g==", "dev": true, "license": "MIT", "dependencies": { - "@types/estree": "1.0.9" + "@oxc-project/types": "=0.133.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { - "rollup": "dist/bin/rollup" + "rolldown": "bin/cli.mjs" }, "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" + "node": "^20.19.0 || >=22.12.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.61.1", - "@rollup/rollup-android-arm64": "4.61.1", - "@rollup/rollup-darwin-arm64": "4.61.1", - "@rollup/rollup-darwin-x64": "4.61.1", - "@rollup/rollup-freebsd-arm64": "4.61.1", - "@rollup/rollup-freebsd-x64": "4.61.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.61.1", - "@rollup/rollup-linux-arm-musleabihf": "4.61.1", - "@rollup/rollup-linux-arm64-gnu": "4.61.1", - "@rollup/rollup-linux-arm64-musl": "4.61.1", - "@rollup/rollup-linux-loong64-gnu": "4.61.1", - "@rollup/rollup-linux-loong64-musl": "4.61.1", - "@rollup/rollup-linux-ppc64-gnu": "4.61.1", - "@rollup/rollup-linux-ppc64-musl": "4.61.1", - "@rollup/rollup-linux-riscv64-gnu": "4.61.1", - "@rollup/rollup-linux-riscv64-musl": "4.61.1", - "@rollup/rollup-linux-s390x-gnu": "4.61.1", - "@rollup/rollup-linux-x64-gnu": "4.61.1", - "@rollup/rollup-linux-x64-musl": "4.61.1", - "@rollup/rollup-openbsd-x64": "4.61.1", - "@rollup/rollup-openharmony-arm64": "4.61.1", - "@rollup/rollup-win32-arm64-msvc": "4.61.1", - "@rollup/rollup-win32-ia32-msvc": "4.61.1", - "@rollup/rollup-win32-x64-gnu": "4.61.1", - "@rollup/rollup-win32-x64-msvc": "4.61.1", - "fsevents": "~2.3.2" + "@rolldown/binding-android-arm64": "1.0.3", + "@rolldown/binding-darwin-arm64": "1.0.3", + "@rolldown/binding-darwin-x64": "1.0.3", + "@rolldown/binding-freebsd-x64": "1.0.3", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.3", + "@rolldown/binding-linux-arm64-gnu": "1.0.3", + "@rolldown/binding-linux-arm64-musl": "1.0.3", + "@rolldown/binding-linux-ppc64-gnu": "1.0.3", + "@rolldown/binding-linux-s390x-gnu": "1.0.3", + "@rolldown/binding-linux-x64-gnu": "1.0.3", + "@rolldown/binding-linux-x64-musl": "1.0.3", + "@rolldown/binding-openharmony-arm64": "1.0.3", + "@rolldown/binding-wasm32-wasi": "1.0.3", + "@rolldown/binding-win32-arm64-msvc": "1.0.3", + "@rolldown/binding-win32-x64-msvc": "1.0.3" } }, "node_modules/siginfo": { @@ -1308,9 +1554,9 @@ "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==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", "dev": true, "license": "MIT" }, @@ -1322,42 +1568,50 @@ "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/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==", + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", "dev": true, "license": "MIT", "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": ">=18" } }, - "node_modules/tinyrainbow": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", - "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "dev": true, "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, "engines": { - "node": ">=14.0.0" + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" } }, - "node_modules/tinyspy": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", - "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", "dev": true, "license": "MIT", "engines": { "node": ">=14.0.0" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, "node_modules/tsx": { "version": "4.22.4", "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz", @@ -1414,21 +1668,23 @@ "license": "MIT" }, "node_modules/vite": { - "version": "5.4.21", - "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", - "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "version": "8.0.16", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.16.tgz", + "integrity": "sha512-h9bXPmJichP5fLmVQo3PyaGSDE2n3aPuomeAlVRm0JLmt4rY6zmPKd59HYI4LNW8oTK7tlTsuC7l/m7awx9Jcw==", "dev": true, "license": "MIT", "dependencies": { - "esbuild": "^0.21.3", - "postcss": "^8.4.43", - "rollup": "^4.20.0" + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.3", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^20.19.0 || >=22.12.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -1437,23 +1693,33 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^18.0.0 || >=20.0.0", - "less": "*", - "lightningcss": "^1.21.0", - "sass": "*", - "sass-embedded": "*", - "stylus": "*", - "sugarss": "*", - "terser": "^5.4.0" + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.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 }, - "less": { + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { "optional": true }, - "lightningcss": { + "jiti": { + "optional": true + }, + "less": { "optional": true }, "sass": { @@ -1470,530 +1736,104 @@ }, "terser": { "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true } } }, - "node_modules/vite-node": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.9.tgz", - "integrity": "sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==", - "dev": true, - "license": "MIT", - "dependencies": { - "cac": "^6.7.14", - "debug": "^4.3.7", - "es-module-lexer": "^1.5.4", - "pathe": "^1.1.2", - "vite": "^5.0.0" - }, - "bin": { - "vite-node": "vite-node.mjs" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" - }, - "funding": { - "url": "https://opencollective.com/vitest" - } - }, - "node_modules/vite/node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", - "cpu": [ - "arm" - ], + "node_modules/vite/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": [ - "android" + "darwin" ], "engines": { - "node": ">=12" + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "node_modules/vite/node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", - "cpu": [ - "arm64" - ], + "node_modules/vitest": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.8.tgz", + "integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==", "dev": true, "license": "MIT", - "optional": true, - "os": [ - "android" - ], + "dependencies": { + "@vitest/expect": "4.1.8", + "@vitest/mocker": "4.1.8", + "@vitest/pretty-format": "4.1.8", + "@vitest/runner": "4.1.8", + "@vitest/snapshot": "4.1.8", + "@vitest/spy": "4.1.8", + "@vitest/utils": "4.1.8", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=12" - } - }, - "node_modules/vite/node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=12" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" - } - }, - "node_modules/vite/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/vitest": { - "version": "2.1.9", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.9.tgz", - "integrity": "sha512-MSmPM9REYqDGBI8439mA4mWhV5sKmDlBKWIYbA3lRb2PTHACE0mgKwA8yQ2xq9vxDTuk4iPrECBAEW2aoFXY0Q==", - "dev": true, - "license": "MIT", - "dependencies": { - "@vitest/expect": "2.1.9", - "@vitest/mocker": "2.1.9", - "@vitest/pretty-format": "^2.1.9", - "@vitest/runner": "2.1.9", - "@vitest/snapshot": "2.1.9", - "@vitest/spy": "2.1.9", - "@vitest/utils": "2.1.9", - "chai": "^5.1.2", - "debug": "^4.3.7", - "expect-type": "^1.1.0", - "magic-string": "^0.30.12", - "pathe": "^1.1.2", - "std-env": "^3.8.0", - "tinybench": "^2.9.0", - "tinyexec": "^0.3.1", - "tinypool": "^1.0.1", - "tinyrainbow": "^1.2.0", - "vite": "^5.0.0", - "vite-node": "2.1.9", - "why-is-node-running": "^2.3.0" - }, - "bin": { - "vitest": "vitest.mjs" - }, - "engines": { - "node": "^18.0.0 || >=20.0.0" + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" }, "funding": { "url": "https://opencollective.com/vitest" }, "peerDependencies": { "@edge-runtime/vm": "*", - "@types/node": "^18.0.0 || >=20.0.0", - "@vitest/browser": "2.1.9", - "@vitest/ui": "2.1.9", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.8", + "@vitest/browser-preview": "4.1.8", + "@vitest/browser-webdriverio": "4.1.8", + "@vitest/coverage-istanbul": "4.1.8", + "@vitest/coverage-v8": "4.1.8", + "@vitest/ui": "4.1.8", "happy-dom": "*", - "jsdom": "*" + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "@edge-runtime/vm": { "optional": true }, + "@opentelemetry/api": { + "optional": true + }, "@types/node": { "optional": true }, - "@vitest/browser": { + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { "optional": true }, "@vitest/ui": { @@ -2004,6 +1844,9 @@ }, "jsdom": { "optional": true + }, + "vite": { + "optional": false } } }, diff --git a/package.json b/package.json index 951cfee..1beba00 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,9 @@ "test": "vitest run", "test:watch": "vitest", "typecheck": "tsc --noEmit", - "dev": "tsx src/cli/index.ts" + "dev": "tsx src/cli/index.ts", + "test:fast": "vitest run test/cursor.test.ts test/director.test.ts test/director-validation.test.ts test/schema.test.ts test/config.test.ts test/url-policy.test.ts test/redaction.test.ts test/plan.test.ts", + "test:e2e": "vitest run test/record.e2e.test.ts test/generate.e2e.test.ts" }, "dependencies": { "playwright": "^1.53.0", @@ -29,6 +31,6 @@ "@types/node": "^20.17.0", "tsx": "^4.19.0", "typescript": "^5.6.0", - "vitest": "^2.1.0" + "vitest": "^4.1.8" } } diff --git a/src/capture/cursor.ts b/src/capture/cursor.ts index 6055cdd..6941526 100644 --- a/src/capture/cursor.ts +++ b/src/capture/cursor.ts @@ -32,8 +32,7 @@ export function makeRng(seed: number): () => number { /** Fitts's law movement time in ms, before clamping. */ export function fittsMs(distancePx: number, targetWidthPx: number): number { - // tuned 2026-06-11 after Brayden's v0 verdict: "cursor moving is a little - // bit too fast" — slower, deliberate presenter pace + // Slower, deliberate presenter pace rather than twitchy cursor motion. const a = 220, b = 170; return a + b * Math.log2(distancePx / Math.max(targetWidthPx, 8) + 1); } diff --git a/src/capture/executor.ts b/src/capture/executor.ts index 6fa1e82..622abd8 100644 --- a/src/capture/executor.ts +++ b/src/capture/executor.ts @@ -17,7 +17,7 @@ * are canonical (design doc, stage 3). On a local fixture nothing overruns, * so the scheduled timeline is byte-identical across runs. * - * Capture path per spike verdict (spikes/RESULTS.md): CDP screencast PNG at + * Capture path: CDP screencast PNG at * 2x DPR, ack-throttled, frames streamed straight to disk. */ import { mkdirSync, writeFileSync } from "node:fs"; @@ -26,6 +26,7 @@ import { join } from "node:path"; import { chromium, type CDPSession, type Page } from "playwright"; import type { EventLog, KnownEvent, Recipe, Scene, Action } from "../schema/index.js"; import { cursorPath, makeRng, type CursorPoint } from "./cursor.js"; +import { assertSafeNavigationUrl } from "../security/url-policy.js"; const VIEWPORT = { width: 1920, height: 1080 }; const DPR = 2; @@ -43,6 +44,8 @@ export interface RecordOptions { seed?: number; /** Skip screencast (faster scheduling-only tests). */ captureFrames?: boolean; + /** allow localhost/RFC1918/cloud-metadata navigation; off by default for safety */ + allowPrivateNetwork?: boolean; } export interface RecordResult { @@ -65,11 +68,25 @@ function roundToFrame(ms: number): number { const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); +async function assertRecipeNavigationPolicy(recipe: Recipe, allowPrivateNetwork: boolean): Promise { + for (const scene of recipe.scenes) { + await assertSafeNavigationUrl(scene.entry.url, { allowPrivateNetwork }); + for (const action of [...scene.entry.prelude, ...scene.actions]) { + if (action.kind === "goto" && action.url) { + await assertSafeNavigationUrl(action.url, { allowPrivateNetwork }); + } + } + } +} + export async function record(opts: RecordOptions): Promise { const { recipe, outDir } = opts; const captureFrames = opts.captureFrames ?? true; + const allowPrivateNetwork = opts.allowPrivateNetwork ?? false; const rng = makeRng(opts.seed ?? 1); + await assertRecipeNavigationPolicy(recipe, allowPrivateNetwork); + mkdirSync(join(outDir, "frames"), { recursive: true }); // launch is the only setup outside try/finally; everything else (newPage, @@ -139,7 +156,9 @@ export async function record(opts: RecordOptions): Promise { switch (a.kind) { case "goto": { if (!a.url) throw new Error("goto action requires url"); - await page.goto(a.url, { timeout: ACTION_TIMEOUT_MS, waitUntil: "load" }); + await assertSafeNavigationUrl(a.url, { allowPrivateNetwork }); + const response = await page.goto(a.url, { timeout: ACTION_TIMEOUT_MS, waitUntil: "load" }); + await assertSafeNavigationUrl(a.url, { allowPrivateNetwork, finalUrl: response?.url() ?? page.url() }); break; } case "wait": @@ -254,7 +273,9 @@ export async function record(opts: RecordOptions): Promise { // navigate to first scene's entry before starting capture, so frame 0 is content const firstScene = recipe.scenes[0]; if (!firstScene) throw new Error("recipe has no scenes"); - await page.goto(firstScene.entry.url, { timeout: ACTION_TIMEOUT_MS, waitUntil: "load" }); + await assertSafeNavigationUrl(firstScene.entry.url, { allowPrivateNetwork }); + const firstResponse = await page.goto(firstScene.entry.url, { timeout: ACTION_TIMEOUT_MS, waitUntil: "load" }); + await assertSafeNavigationUrl(firstScene.entry.url, { allowPrivateNetwork, finalUrl: firstResponse?.url() ?? page.url() }); await sleep(SETTLE_MS); // `load` ≠ ready: let hydration/fonts/paints settle if (captureFrames) { @@ -290,9 +311,11 @@ export async function record(opts: RecordOptions): Promise { try { if (i > 0) { - await page.goto(scene.entry.url, { timeout: ACTION_TIMEOUT_MS, waitUntil: "load" }); + await assertSafeNavigationUrl(scene.entry.url, { allowPrivateNetwork }); + const response = await page.goto(scene.entry.url, { timeout: ACTION_TIMEOUT_MS, waitUntil: "load" }); + await assertSafeNavigationUrl(scene.entry.url, { allowPrivateNetwork, finalUrl: response?.url() ?? page.url() }); await sleep(SETTLE_MS); - // timestamp canon (PR #1 review): when nav finishes EARLY, dwell out + // Timestamp canon: when nav finishes early, dwell out // the unused allowance in WALL time so pixels and schedule stay in // lockstep — advancing only the clock made the footage run ~1s ahead // of every logged event after a fast local navigation @@ -307,8 +330,7 @@ export async function record(opts: RecordOptions): Promise { } for (const a of scene.entry.prelude) await runAction(a); for (const a of scene.actions) await runAction(a); - // hold the scene's final frame (PR #1 review: was validated + budgeted - // by the schema but never executed) + // Hold the scene's final frame; it is validated and budgeted by the schema. if (scene.hold_ms > 0) { await sleep(scene.hold_ms); clock += scene.hold_ms; @@ -350,6 +372,10 @@ export async function record(opts: RecordOptions): Promise { }; writeFileSync(join(outDir, "events.json"), JSON.stringify(eventLog, null, 2)); + // CDP screencast timestamps can arrive/write with tiny ordering jitter across + // platforms. The renderer consumes by source timestamp, not filename order, + // so persist a monotonic index instead of failing later in render. + frameIndex.sort((a, b) => a.t_source - b.t_source); writeFileSync(join(outDir, "frames-index.json"), JSON.stringify(frameIndex)); return { eventLog, frameCount: frameIndex.length, failedScenes, aborted, outDir }; diff --git a/src/cli/doctor.ts b/src/cli/doctor.ts index dcde2fc..a68a36c 100644 --- a/src/cli/doctor.ts +++ b/src/cli/doctor.ts @@ -65,7 +65,7 @@ const checks: Check[] = [ { // render needs the FULL chromium channel (the headless shell has no // WebCodecs) — a doctor that only checks the shell passes while render - // cannot launch (review: P2) + // cannot launch name: "full chromium (render)", run: async () => { try { diff --git a/src/cli/index.ts b/src/cli/index.ts index 14cb0ce..9ad591a 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -34,10 +34,11 @@ async function main(): Promise { recipe: { type: "string" }, out: { type: "string" }, seed: { type: "string" }, + "allow-private-network": { type: "boolean" }, }, }); if (!values.recipe) { - console.error("usage: supercut record --recipe [--out ] [--seed ]"); + console.error("usage: supercut record --recipe [--out ] [--seed ] [--allow-private-network]"); return 1; } const { readFileSync } = await import("node:fs"); @@ -48,7 +49,12 @@ async function main(): Promise { const outDir = values.out ?? "out/take"; console.log(`recording ${recipe.scenes.length} scene(s) from ${recipe.app_url} → ${outDir}`); const t0 = Date.now(); - const res = await record({ recipe, outDir, seed: values.seed ? Number(values.seed) : 1 }); + const seed = values.seed === undefined ? 1 : Number(values.seed); + if (!Number.isInteger(seed) || seed < 0) { + console.error(`invalid --seed "${values.seed}" (expected a non-negative integer)`); + return 1; + } + const res = await record({ recipe, outDir, seed, ...(values["allow-private-network"] ? { allowPrivateNetwork: true } : {}) }); console.log( `done in ${((Date.now() - t0) / 1000).toFixed(1)}s — ${res.frameCount} frames, ` + `${res.eventLog.events.length} events` + @@ -97,22 +103,37 @@ async function main(): Promise { seed: { type: "string" }, model: { type: "string" }, "no-vision": { type: "boolean" }, + "env-file": { type: "string" }, + "allow-private-network": { type: "boolean" }, + yes: { type: "boolean" }, }, }); if (!values.url) { console.error( "usage: supercut generate --url [--repo ] [--out ] " + - "[--bg ] [--seed ] [--model ] [--no-vision]", + "[--bg ] [--seed ] [--model ] [--env-file ] [--allow-private-network] [--yes] [--no-vision]", ); return 1; } const { loadDotEnv, resolveProvider } = await import("../director/config.js"); const { generate } = await import("../director/generate.js"); - loadDotEnv(); // read .env at repo root (DEEPSEEK_API_KEY etc.) - if (values.model) process.env.SUPERCUT_MODEL = values.model; // --model overrides .env + const envLoad = loadDotEnv(values["env-file"] ?? ".env"); + if (process.env.SUPERCUT_VERBOSE && envLoad.reason) console.error(`env: ${envLoad.path} ${envLoad.reason}`); + const seed = values.seed === undefined ? undefined : Number(values.seed); + if (seed !== undefined && (!Number.isInteger(seed) || seed < 0)) { + console.error(`invalid --seed "${values.seed}" (expected a non-negative integer)`); + return 1; + } + if (!values.yes) { + console.error( + "generate sends crawled DOM text, optional screenshots, and optional repo notes to the configured LLM provider. " + + "Re-run with --yes to acknowledge, or use record/render without an LLM.", + ); + return 1; + } let provider; try { - provider = resolveProvider(); + provider = resolveProvider(process.env, { ...(values.model ? { model: values.model } : {}) }); } catch (err) { console.error( `${err instanceof Error ? err.message : err}\n` + @@ -129,7 +150,8 @@ async function main(): Promise { vision: values["no-vision"] ? false : provider.vision, ...(values.repo ? { repoPath: values.repo } : {}), ...(values.bg ? { background: values.bg } : {}), - ...(values.seed ? { seed: Number(values.seed) } : {}), + ...(seed !== undefined ? { seed } : {}), + ...(values["allow-private-network"] ? { allowPrivateNetwork: true } : {}), }); console.log(`\nsupercut: ${res.outFile} (${res.recipe.scenes.length} scenes, ${res.retakes} re-take(s))`); return 0; diff --git a/src/director/analyze.ts b/src/director/analyze.ts index 4223d89..86d26a0 100644 --- a/src/director/analyze.ts +++ b/src/director/analyze.ts @@ -8,6 +8,7 @@ import { z } from "zod"; import { extractJson, type ChatPart, type LlmClient } from "./llm.js"; import type { PageDigest } from "./inventory.js"; +import { redactForPrompt } from "../security/redaction.js"; export const appAnalysis = z.object({ product_summary: z.string().min(10).max(600), @@ -27,14 +28,38 @@ export const appAnalysis = z.object({ export type AppAnalysis = z.infer; +export function validateAnalysis(raw: unknown, digests: PageDigest[]): AppAnalysis { + const parsed = appAnalysis.parse(raw); + const byPage = new Map(digests.map((d) => [d.url, new Set(d.inventory.map((i) => i.selector))])); + for (const moment of parsed.money_moments) { + const selectors = byPage.get(moment.page_url); + if (!selectors) { + throw new Error(`money moment "${moment.title}" page_url "${moment.page_url}" is not a crawled page`); + } + for (const selector of moment.elements) { + if (!selectors.has(selector)) { + throw new Error(`money moment "${moment.title}" selector "${selector}" is not in the inventory for ${moment.page_url}`); + } + } + } + return parsed; +} + function digestText(d: PageDigest): string { const inv = d.inventory - .map((i) => ` ${i.selector} [${i.tag}] "${i.text}"${i.href ? ` → ${i.href}` : ""}${i.hidden ? " (HIDDEN until revealed)" : ""}`) + .map((i) => ` ${i.selector} [${i.tag}] "${redactForPrompt(i.text)}"${i.href ? ` → ${redactForPrompt(i.href)}` : ""}${i.hidden ? " (HIDDEN until revealed)" : ""}`) .join("\n"); return `PAGE ${d.url}\ntitle: ${d.title}\nheadings: ${d.headings.join(" | ")}\nelements:\n${inv}`; } -const SYSTEM = `You are the director of a 60-second product launch video. You study a web product and pick the 2-4 "money moments" — the interactions that make a viewer instantly understand why this product is good. Prefer moments with visible payoff (something appears, changes, or completes). Respond ONLY with a JSON object matching: +const SYSTEM = `You are the director of a 60-second Screen-Studio-style product launch video, not a random website tour. You study a web product and pick the 2-4 "money moments" — the interactions that make a viewer instantly understand why this product is good. + +Order money_moments as the exact video storyboard: +1. hook: the clearest landing/first-impression value moment +2. proof/depth: the core workflow or differentiator +3. payoff: the most visual result, completion, dashboard, or CTA + +Prefer moments with visible payoff (something appears, changes, or completes). Do not order by crawl order unless that is also the best viewer story. Respond ONLY with a JSON object matching: { "product_summary": string, "money_moments": [{ "title": string, "why": string, "page_url": string (one of the crawled page URLs), "elements": [selector strings COPIED EXACTLY from the element inventory] }] }`; export async function analyzeApp( @@ -63,7 +88,7 @@ export async function analyzeApp( : parts; const raw = await llm.chat({ system: SYSTEM, user, json: true }); try { - return appAnalysis.parse(extractJson(raw)); + return validateAnalysis(extractJson(raw), digests); } catch (err) { feedback = err instanceof Error ? err.message.slice(0, 500) : String(err); } diff --git a/src/director/config.ts b/src/director/config.ts index d0108ef..276b857 100644 --- a/src/director/config.ts +++ b/src/director/config.ts @@ -1,72 +1,139 @@ /** - * Provider config — resolves which LLM the director uses from the - * environment (a `.env` file or real env vars). OpenAI-compatible: works with - * OpenRouter, DeepSeek, or any compatible endpoint. - * - * SUPERCUT_API_KEY the key (or OPENROUTER_API_KEY / DEEPSEEK_API_KEY) - * SUPERCUT_LLM_BASE_URL endpoint (auto-set for DeepSeek) - * SUPERCUT_MODEL model id - * SUPERCUT_VISION "true"/"false" — force vision on/off - * - * Vision capability matters: the analyze + QC stages can use screenshots, but - * text-only models (e.g. deepseek-chat) can't. When vision is off the director - * reads the DOM/inventory instead, and the vision-QC pass is skipped. + * Provider config — resolves which LLM the director uses from explicit env + * input (a loaded `.env` file or real env vars). OpenAI-compatible: works with + * OpenRouter, DeepSeek, or a custom endpoint. */ -import { OpenRouterClient, type LlmClient } from "./llm.js"; +import { existsSync } from "node:fs"; +import { OpenAICompatibleClient, type LlmClient } from "./llm.js"; + +export type ProviderName = "deepseek" | "openrouter" | "custom"; + +export interface ProviderEnv { + SUPERCUT_PROVIDER?: string; + SUPERCUT_API_KEY?: string; + OPENROUTER_API_KEY?: string; + DEEPSEEK_API_KEY?: string; + SUPERCUT_LLM_BASE_URL?: string; + SUPERCUT_MODEL?: string; + SUPERCUT_VISION?: string; +} + +export interface ProviderOverrides { + provider?: ProviderName; + model?: string; + baseUrl?: string; + vision?: boolean; +} export interface ResolvedProvider { client: LlmClient; + provider: ProviderName; + model: string; + baseUrl: string; vision: boolean; summary: string; } const DEEPSEEK_BASE = "https://api.deepseek.com"; +const OPENROUTER_BASE = "https://openrouter.ai/api/v1"; +const DEFAULT_OPENROUTER_MODEL = "anthropic/claude-sonnet-4.6"; +const DEFAULT_DEEPSEEK_MODEL = "deepseek-v4-pro"; + +function parseProvider(value: string | undefined): ProviderName | undefined { + if (!value) return undefined; + const v = value.toLowerCase(); + if (v === "deepseek" || v === "openrouter" || v === "custom") return v; + throw new Error(`unknown SUPERCUT_PROVIDER "${value}" (expected deepseek, openrouter, or custom)`); +} + +function parseVision(value: string | undefined): boolean | undefined { + if (value === undefined) return undefined; + const v = value.toLowerCase(); + if (v === "true" || v === "1" || v === "yes") return true; + if (v === "false" || v === "0" || v === "no") return false; + throw new Error(`invalid SUPERCUT_VISION "${value}" (expected true or false)`); +} -export function resolveProvider(): ResolvedProvider { - const deepseekKey = process.env.DEEPSEEK_API_KEY; - const orKey = process.env.OPENROUTER_API_KEY; - const genericKey = process.env.SUPERCUT_API_KEY; - const apiKey = genericKey || deepseekKey || orKey || ""; - if (!apiKey) { +export function resolveProvider( + env: ProviderEnv = process.env, + overrides: ProviderOverrides = {}, +): ResolvedProvider { + const explicitProvider = overrides.provider ?? parseProvider(env.SUPERCUT_PROVIDER); + const providerKeys = [ + env.DEEPSEEK_API_KEY ? "deepseek" : "", + env.OPENROUTER_API_KEY ? "openrouter" : "", + ].filter(Boolean); + + if (!explicitProvider && providerKeys.length > 1 && !env.SUPERCUT_API_KEY) { + throw new Error("multiple provider keys found; set SUPERCUT_PROVIDER to deepseek or openrouter"); + } + + let provider: ProviderName; + if (explicitProvider) provider = explicitProvider; + else if (env.DEEPSEEK_API_KEY) provider = "deepseek"; + else if (env.OPENROUTER_API_KEY) provider = "openrouter"; + else if (env.SUPERCUT_API_KEY || overrides.baseUrl || env.SUPERCUT_LLM_BASE_URL) provider = "custom"; + else { throw new Error( "no API key found. Set DEEPSEEK_API_KEY (or OPENROUTER_API_KEY / SUPERCUT_API_KEY) " + - "in a .env file at the repo root. See .env.example.", + "in a .env file. See .env.example.", ); } - // explicit base url wins; otherwise infer DeepSeek from which key was set - const usingDeepseek = !!deepseekKey && !orKey && !process.env.SUPERCUT_LLM_BASE_URL; - const baseUrl = process.env.SUPERCUT_LLM_BASE_URL || (usingDeepseek ? DEEPSEEK_BASE : undefined); + const apiKey = + provider === "deepseek" ? env.DEEPSEEK_API_KEY || env.SUPERCUT_API_KEY || "" : + provider === "openrouter" ? env.OPENROUTER_API_KEY || env.SUPERCUT_API_KEY || "" : + env.SUPERCUT_API_KEY || env.DEEPSEEK_API_KEY || env.OPENROUTER_API_KEY || ""; + if (!apiKey) throw new Error(`no API key found for provider ${provider}`); + + const baseUrl = overrides.baseUrl ?? env.SUPERCUT_LLM_BASE_URL ?? ( + provider === "deepseek" ? DEEPSEEK_BASE : provider === "openrouter" ? OPENROUTER_BASE : "" + ); + if (!baseUrl) throw new Error("SUPERCUT_LLM_BASE_URL is required when SUPERCUT_PROVIDER=custom"); - // deepseek-v4-pro: smarter understanding for the ~3 director calls/video. - // Swap to deepseek-v4-flash via SUPERCUT_MODEL for cheaper/faster. Both are - // text-only (V4 native vision not shipped as of 2026-06), so vision stays off. - const model = - process.env.SUPERCUT_MODEL || (usingDeepseek ? "deepseek-v4-pro" : undefined); + const model = overrides.model ?? env.SUPERCUT_MODEL ?? ( + provider === "deepseek" ? DEFAULT_DEEPSEEK_MODEL : + provider === "openrouter" ? DEFAULT_OPENROUTER_MODEL : "" + ); + if (!model) throw new Error("SUPERCUT_MODEL is required when SUPERCUT_PROVIDER=custom"); - // vision: explicit override, else off for DeepSeek/text-only, on otherwise - const visionEnv = process.env.SUPERCUT_VISION?.toLowerCase(); - const vision = - visionEnv === "true" ? true : visionEnv === "false" ? false : !usingDeepseek; + const envVision = parseVision(env.SUPERCUT_VISION); + const vision = overrides.vision ?? envVision ?? (provider !== "deepseek"); + if (provider === "deepseek" && vision) { + throw new Error("vision cannot be enabled for DeepSeek text-only models; use OpenRouter/custom vision model or SUPERCUT_VISION=false"); + } - const client = new OpenRouterClient({ + const client = new OpenAICompatibleClient({ apiKey, - ...(model ? { model } : {}), - ...(baseUrl ? { baseUrl } : {}), + model, + baseUrl, + providerLabel: provider, + vision, }); return { client, + provider, + model, + baseUrl, vision, - summary: `${client.label} @ ${baseUrl ?? "openrouter"} · vision ${vision ? "on" : "off (DOM-only)"}`, + summary: `${client.label} @ ${baseUrl} · vision ${vision ? "on" : "off (DOM-only)"}`, }; } -/** Best-effort .env loader (Node 20.12+ / 22). No-op if absent. */ -export function loadDotEnv(path = ".env"): void { +export interface DotEnvLoadResult { + path: string; + loaded: boolean; + reason?: string; +} + +/** Best-effort .env loader (Node 20.12+ / 22). Does not silently pretend success. */ +export function loadDotEnv(path = ".env"): DotEnvLoadResult { + if (!existsSync(path)) return { path, loaded: false, reason: "not found" }; try { (process as unknown as { loadEnvFile: (p: string) => void }).loadEnvFile(path); - } catch { - /* no .env, or older Node — rely on real env vars */ + return { path, loaded: true }; + } catch (err) { + return { path, loaded: false, reason: err instanceof Error ? err.message : String(err) }; } } diff --git a/src/director/generate.ts b/src/director/generate.ts index f324de7..2e1bc4d 100644 --- a/src/director/generate.ts +++ b/src/director/generate.ts @@ -22,6 +22,7 @@ import { crawlApp, type PageDigest } from "./inventory.js"; import type { LlmClient } from "./llm.js"; import { applyVerdicts, deterministicChecks, visionQc, type SceneVerdict } from "./qc.js"; import { writeRecipe } from "./script.js"; +import { assertSafeNavigationUrl } from "../security/url-policy.js"; const exec = promisify(execFile); const MAX_RETAKES = 3; @@ -39,6 +40,8 @@ export interface GenerateOptions { vision?: boolean; /** @deprecated use vision:false */ noVision?: boolean; + /** allow localhost/RFC1918/cloud-metadata navigation; off by default for safety */ + allowPrivateNetwork?: boolean; log?: (msg: string) => void; } @@ -50,12 +53,14 @@ export interface GenerateResult { verdictLog: SceneVerdict[][]; } -async function preflight(url: string): Promise { +async function preflight(url: string, allowPrivateNetwork: boolean): Promise { + await assertSafeNavigationUrl(url, { allowPrivateNetwork }); // app reachable — error in seconds, never after 10 minutes of work const ctrl = new AbortController(); const timer = setTimeout(() => ctrl.abort(), 5000); try { const res = await fetch(url, { signal: ctrl.signal }); + await assertSafeNavigationUrl(url, { allowPrivateNetwork, finalUrl: res.url }); if (res.status >= 500) throw new Error(`app at ${url} responded ${res.status}`); } catch (err) { throw new Error( @@ -91,10 +96,10 @@ export async function generate(opts: GenerateOptions): Promise { mkdirSync(opts.outDir, { recursive: true }); log("preflight…"); - await preflight(opts.url); + await preflight(opts.url, opts.allowPrivateNetwork ?? false); log(`① analyze: crawling app…${vision ? "" : " (DOM-only, text model)"}`); - const digests: PageDigest[] = await crawlApp(opts.url, { maxPages: 3, screenshots: vision }); + const digests: PageDigest[] = await crawlApp(opts.url, { maxPages: 3, screenshots: vision, allowPrivateNetwork: opts.allowPrivateNetwork ?? false }); log(` crawled ${digests.length} page(s), ${digests.reduce((n, d) => n + d.inventory.length, 0)} interactable elements`); const notes = opts.repoPath ? repoNotes(opts.repoPath) : undefined; @@ -116,7 +121,7 @@ export async function generate(opts: GenerateOptions): Promise { takeDir = join(opts.outDir, `take-${retakes}`); rmSync(takeDir, { recursive: true, force: true }); log(`③ record: take ${retakes} (${recipe.scenes.length} scenes)…`); - result = await record({ recipe, outDir: takeDir, seed: opts.seed ?? 1 }); + result = await record({ recipe, outDir: takeDir, seed: opts.seed ?? 1, allowPrivateNetwork: opts.allowPrivateNetwork ?? false }); if (result.aborted) { throw new Error( `capture aborted: scenes failed [${result.failedScenes.join(", ")}] — app state may not match the recipe`, @@ -142,7 +147,7 @@ export async function generate(opts: GenerateOptions): Promise { if (retakes >= MAX_RETAKES) { log(` re-take budget exhausted (${MAX_RETAKES}) — proceeding with the take as recorded`); } - // PR #2 review: do NOT adopt the patched recipe here. `takeDir` was + // Do NOT adopt the patched recipe here. `takeDir` was // recorded from the CURRENT `recipe`; writing applied.recipe would make // recipe.json/report describe scenes/holds that were never filmed (and // for cuts, omit a scene that is still in the rendered video). The diff --git a/src/director/inventory.ts b/src/director/inventory.ts index 17634aa..c5024a1 100644 --- a/src/director/inventory.ts +++ b/src/director/inventory.ts @@ -5,6 +5,7 @@ * construction: it fails the whitelist check and bounces back for retry. */ import { chromium, type Browser, type Page } from "playwright"; +import { assertSafeNavigationUrl } from "../security/url-policy.js"; export interface InventoryItem { /** Playwright-compatible selector, verified to resolve on the page */ @@ -31,7 +32,7 @@ const cssEscape = (s: string) => s.replace(/["\\]/g, "\\$&"); // links the crawler must NOT navigate to: file downloads (PDF/zip/images/docs), // and non-http protocols. Navigating to a PDF triggers a download that crashes -// page.goto (found on a real run, 2026-06-12). +// page.goto. const NON_HTML_EXT = /\.(pdf|zip|tar|gz|dmg|exe|pkg|csv|xlsx?|docx?|pptx?|png|jpe?g|gif|svg|webp|mp4|mov|webm|mp3|wav|woff2?|ttf)$/i; @@ -93,7 +94,7 @@ async function digestPage(page: Page, withScreenshot: boolean): Promise 1) { if (!box) continue; // can't disambiguate a hidden duplicate — skip, don't guess - // pick the CLOSEST nth-match (PR #2 review: a strict ±2px test can miss + // Pick the closest nth-match; a strict ±2px test can miss // on sub-pixel rendering and silently fall back to nth=1 = wrong element). // Cap the accepted distance so we never inventory a wildly-off element. const MAX_OFFSET_PX = 20; @@ -136,11 +137,13 @@ async function digestPage(page: Page, withScreenshot: boolean): Promise { const maxPages = opts.maxPages ?? 3; const screenshots = opts.screenshots ?? true; const origin = new URL(appUrl).origin; + const allowPrivateNetwork = opts.allowPrivateNetwork ?? false; + await assertSafeNavigationUrl(appUrl, { allowPrivateNetwork }); const browser: Browser = await chromium.launch({ headless: true }); try { @@ -163,7 +166,7 @@ export async function crawlApp( const queue = [appUrl]; while (queue.length > 0 && digests.length < maxPages) { const target = queue.shift()!; - // pathname + search (PR #2 review): pathname-only collapses query-routed + // Pathname + search: pathname-only collapses query-routed // pages (/search?q=a vs ?q=b) and SPA filter/detail views, so the crawler // would skip real money-moment pages. Hash is excluded (same document). const u = new URL(target); @@ -174,7 +177,9 @@ export async function crawlApp( // a single bad page (download, timeout, redirect off-origin) must not // kill the whole crawl — skip it and keep going try { - await page.goto(target, { timeout: 15_000, waitUntil: "load" }); + await assertSafeNavigationUrl(target, { allowPrivateNetwork }); + const response = await page.goto(target, { timeout: 15_000, waitUntil: "load" }); + await assertSafeNavigationUrl(target, { allowPrivateNetwork, finalUrl: response?.url() ?? page.url() }); await page.waitForTimeout(400); // settle: load ≠ ready } catch (err) { if (digests.length === 0 && queue.length === 0) throw err; // start page must load @@ -188,6 +193,7 @@ export async function crawlApp( try { const linked = new URL(item.href, target); if (linked.origin === origin && isCrawlable(linked) && !visited.has(linked.pathname + linked.search)) { + await assertSafeNavigationUrl(linked.href, { allowPrivateNetwork }); queue.push(linked.href); } } catch { diff --git a/src/director/llm.ts b/src/director/llm.ts index 183a674..aa8daa2 100644 --- a/src/director/llm.ts +++ b/src/director/llm.ts @@ -1,6 +1,7 @@ /** - * LLM access for the director stages — OpenRouter-first (one key, many - * models; Engineering Decision #4), plain fetch, zero SDK dependencies. + * LLM access for the director stages — OpenAI-compatible, plain fetch, zero + * SDK dependencies. Works with OpenRouter, DeepSeek, or a custom compatible + * endpoint selected in config.ts. * * Every AI touchpoint in supercut goes through this interface, so tests can * inject a stub and the whole generate pipeline runs without any API key. @@ -23,30 +24,36 @@ export interface LlmClient { readonly label: string; } -export interface OpenRouterConfig { +export interface OpenAICompatibleConfig { apiKey: string; - /** override with SUPERCUT_MODEL; needs vision for analyze + QC */ - model?: string; - baseUrl?: string; + model: string; + baseUrl: string; + providerLabel: string; + /** whether this provider/model accepts image parts */ + vision: boolean; } -const DEFAULT_MODEL = "anthropic/claude-sonnet-4.6"; - -export class OpenRouterClient implements LlmClient { +export class OpenAICompatibleClient implements LlmClient { private readonly apiKey: string; private readonly model: string; private readonly baseUrl: string; + private readonly vision: boolean; readonly label: string; - constructor(cfg: OpenRouterConfig) { - if (!cfg.apiKey) throw new Error("OpenRouter API key is empty"); + constructor(cfg: OpenAICompatibleConfig) { + if (!cfg.apiKey) throw new Error(`${cfg.providerLabel} API key is empty`); this.apiKey = cfg.apiKey; - this.model = cfg.model ?? process.env.SUPERCUT_MODEL ?? DEFAULT_MODEL; - this.baseUrl = cfg.baseUrl ?? "https://openrouter.ai/api/v1"; - this.label = `openrouter:${this.model}`; + this.model = cfg.model; + this.baseUrl = cfg.baseUrl.replace(/\/$/, ""); + this.vision = cfg.vision; + this.label = `${cfg.providerLabel}:${this.model}`; } async chat(opts: ChatOptions): Promise { + if (!this.vision && opts.user.some((p) => p.type === "image")) { + throw new Error(`${this.label} is text-only; refusing to send image parts`); + } + const content = opts.user.map((p) => p.type === "text" ? { type: "text" as const, text: p.text } @@ -66,8 +73,6 @@ export class OpenRouterClient implements LlmClient { for (let attempt = 0; attempt < 4; attempt++) { let res: Response; try { - // explicit 4-min timeout so a stalled connection (proxy half-open, - // slow reasoning model) fails cleanly instead of hanging forever res = await fetch(`${this.baseUrl}/chat/completions`, { method: "POST", headers: { @@ -79,8 +84,6 @@ export class OpenRouterClient implements LlmClient { signal: AbortSignal.timeout(240_000), }); } catch (err) { - // network-level throw ("fetch failed", timeout, proxy reset) — transient, - // retry with backoff. Surface the underlying cause for diagnosis. const cause = (err as { cause?: { code?: string; message?: string } })?.cause; lastErr = `network: ${cause?.code ?? ""} ${cause?.message ?? (err instanceof Error ? err.message : String(err))}`.trim(); await new Promise((r) => setTimeout(r, 1500 * (attempt + 1))); @@ -91,15 +94,11 @@ export class OpenRouterClient implements LlmClient { choices?: { message?: { content?: string; reasoning_content?: string } }[]; }; const msg = data.choices?.[0]?.message; - // reasoning models (deepseek-v4) may put the answer in content; fall - // back to reasoning_content only if content is empty const text = msg?.content || msg?.reasoning_content; if (!text) throw new Error(`LLM returned an empty response (${this.label})`); return text; } const snippet = (await res.text()).slice(0, 300); - // auth/config errors fail FAST and clear (fail-fast preflight rule); - // only rate limits and server errors retry if (res.status === 401 || res.status === 403) { throw new Error(`LLM auth failed (${res.status}, ${this.label}) — check your API key. ${snippet}`); } @@ -113,6 +112,10 @@ export class OpenRouterClient implements LlmClient { } } +/** Backwards-compatible export name for older internal imports. */ +export const OpenRouterClient = OpenAICompatibleClient; +export type OpenRouterConfig = OpenAICompatibleConfig; + /** * Pull the first JSON object out of a model response — tolerates ```json * fences and prose around the object, balanced-brace scan. diff --git a/src/director/qc.ts b/src/director/qc.ts index b2c12ec..7f49e02 100644 --- a/src/director/qc.ts +++ b/src/director/qc.ts @@ -43,8 +43,8 @@ export function deterministicChecks(result: RecordResult): SceneVerdict[] { } // dead air: >4s between consecutive interaction events inside a scene. - // PR #2 review: this is INFORMATIONAL only (verdict "ok" + reason). hold_ms - // adds time at the END of a scene — it cannot compress a MID-scene gap, so + // Informational only: hold_ms adds time at the END of a scene and cannot + // compress a MID-scene gap, so // patching it was a no-op that just lengthened the scene. Mid-scene dead air // comes from observed overrun on a slow app, and no frozen-surface lever // (hold/zoom/cut) fixes it, nor does re-recording. We surface it in the diff --git a/src/director/script.ts b/src/director/script.ts index 07bdc8b..5a03cb7 100644 --- a/src/director/script.ts +++ b/src/director/script.ts @@ -12,6 +12,7 @@ import { parseRecipe, type Recipe } from "../schema/index.js"; import { extractJson, type ChatPart, type LlmClient } from "./llm.js"; import type { AppAnalysis } from "./analyze.js"; import type { PageDigest } from "./inventory.js"; +import { redactForPrompt } from "../security/redaction.js"; const SYSTEM = `You write filming scripts ("recipes") for supercut, which records a REAL web app with a browser robot and renders a cinematic 60-second launch video. Respond ONLY with a JSON recipe: @@ -32,10 +33,13 @@ const SYSTEM = `You write filming scripts ("recipes") for supercut, which record HARD RULES: - selectors: COPY EXACTLY from the provided element inventory. Never invent or modify one. - entry.url: only crawled page URLs. -- 2-4 scenes, 2-4 actions each, action duration_ms 1200-4000, hold_ms 400-1200. +- Create EXACTLY one scene per STORYBOARD beat, in the same order. Do not add a generic site-tour scene. +- Each scene's entry.url must equal that beat's page_url and must include at least one of that beat's money selectors. +- Do not use mid-scene "goto" actions; each scene starts from its entry.url so selector validation and capture stay coherent. +- 2-4 scenes, 2-4 actions each, action duration_ms 1200-4000, hold_ms 600-1400. - total of all durations + holds ≤ 50000 (one minute video with headroom). - "type" actions need realistic short text (an email, a search term — match the field). -- Order scenes as a story: hook → depth → payoff. End on the most visual screen. +- Order scenes as a Screen-Studio story: hook → proof/depth → payoff. End on the most visual screen. - depends_on only when a later scene NEEDS an earlier scene's state. - (HIDDEN until revealed) elements: only use them AFTER an earlier action in the SAME scene reveals them (e.g. click the button that opens the form, then type into its field).`; @@ -50,7 +54,7 @@ export async function writeRecipe( digests: PageDigest[], appUrl: string, ): Promise { - // per-page whitelist (PR #2 review): a global set would let a /dash selector + // Per-page whitelist: a global set would let a /dash selector // pass validation in a / scene, then capture waits forever for an element // that page can never show. Validate each scene's selectors against the // inventory of ITS entry.url page. (v1 caveat: a mid-scene `goto` to another @@ -58,12 +62,18 @@ export async function writeRecipe( const pageUrls = new Set(digests.map((d) => d.url)); const byPage = new Map>(); for (const d of digests) byPage.set(d.url, new Set(d.inventory.map((i) => i.selector))); + const storyboard = analysis.money_moments.map((m, index) => ({ + index: index + 1, + title: m.title, + pageUrl: m.page_url, + selectors: new Set(m.elements), + })); const inventoryText = digests .map( (d) => `PAGE ${d.url}\n` + - d.inventory.map((i) => ` ${i.selector} [${i.tag}] "${i.text}"${i.hidden ? " (HIDDEN until revealed)" : ""}`).join("\n"), + d.inventory.map((i) => ` ${i.selector} [${i.tag}] "${redactForPrompt(i.text)}"${i.hidden ? " (HIDDEN until revealed)" : ""}`).join("\n"), ) .join("\n\n"); @@ -75,6 +85,10 @@ export async function writeRecipe( analysis.money_moments .map((m) => `- ${m.title} (${m.page_url}): ${m.why} — elements: ${m.elements.join(", ")}`) .join("\n") + + `\n\nSTORYBOARD (mandatory; output exactly these beats in this order, one scene per beat):\n` + + analysis.money_moments + .map((m, i) => `${i + 1}. ${i === 0 ? "HOOK" : i === analysis.money_moments.length - 1 ? "PAYOFF" : "PROOF"} — ${m.title} @ ${m.page_url}; scene must use one of: ${m.elements.join(", ")}`) + .join("\n") + `\n\nELEMENT INVENTORY (the ONLY selectors you may use):\n${inventoryText}`, }, ]; @@ -88,20 +102,44 @@ export async function writeRecipe( try { const recipe = parseRecipe(extractJson(raw)); + if (recipe.scenes.length !== storyboard.length) { + throw new Error( + `recipe has ${recipe.scenes.length} scene(s), but storyboard requires exactly ${storyboard.length} scene(s) ` + + `(one per money moment, in order)`, + ); + } // whitelist gates — the anti-hallucination contract - for (const scene of recipe.scenes) { + for (const [i, scene] of recipe.scenes.entries()) { + const beat = storyboard[i]!; + if (scene.entry.url !== beat.pageUrl) { + throw new Error( + `scene ${i + 1} "${scene.name}" entry.url "${scene.entry.url}" does not match storyboard beat ` + + `"${beat.title}" page_url "${beat.pageUrl}"`, + ); + } if (!pageUrls.has(scene.entry.url)) { throw new Error(`scene "${scene.name}" entry.url "${scene.entry.url}" is not a crawled page (allowed: ${[...pageUrls].join(", ")})`); } const pageSelectors = byPage.get(scene.entry.url)!; + let usesMoneySelector = false; for (const a of [...scene.entry.prelude, ...scene.actions]) { + if (a.kind === "goto") { + throw new Error(`scene "${scene.name}" uses a mid-scene goto; use a new scene entry.url instead`); + } if (a.selector && !pageSelectors.has(a.selector)) { throw new Error( `selector "${a.selector}" in scene "${scene.name}" is not on its entry page ${scene.entry.url} — ` + `use only selectors listed under that page in the inventory`, ); } + if (a.selector && beat.selectors.has(a.selector)) usesMoneySelector = true; + } + if (!usesMoneySelector) { + throw new Error( + `scene ${i + 1} "${scene.name}" does not film storyboard beat "${beat.title}" — ` + + `include at least one of: ${[...beat.selectors].join(", ")}`, + ); } } return { recipe, attempts: attempt }; diff --git a/src/render/host-page.ts b/src/render/host-page.ts index a0f6f46..7b53688 100644 --- a/src/render/host-page.ts +++ b/src/render/host-page.ts @@ -1,7 +1,7 @@ /** * The render host page — a dumb, fast executor served on localhost * (WebCodecs needs a secure context; the headless SHELL has no WebCodecs at - * all, so this page runs in full Chromium — see spikes/RESULTS.md). + * all, so this page runs in full Chromium). * * It fetches the precomputed render-plan.json (all the smart math already * done in tested TS) and only does mechanical work per output frame: @@ -10,7 +10,7 @@ * source frame → cursor] → VideoFrame → H.264 (annexb) → POST /result * * Plain JS in a template string: it is served as a real page, so no TS/esbuild - * helper traps (the tsx __name lesson from the spikes). + * helper traps. */ export const HOST_PAGE = ` supercut render host @@ -20,8 +20,9 @@ const log = (m) => console.log("[render] " + m); async function main() { const TOKEN = new URLSearchParams(location.search).get("t") || ""; + const authed = (u) => u + (u.includes("?") ? "&" : "?") + "t=" + encodeURIComponent(TOKEN); const fetchOk = async (u) => { - const r = await fetch(u); + const r = await fetch(authed(u)); if (!r.ok) throw new Error("fetch " + u + " failed: HTTP " + r.status); return r; }; @@ -46,17 +47,23 @@ async function main() { const ctx = canvas.getContext("2d"); // motion-blur accumulator: 'lighter' (additive) at 1/8 alpha per subframe is // a TRUE average — 8 × src-over at 1/8 alpha only reaches ~66% opacity and - // washes the content dark (found in first render QC, 2026-06-11) + // washes the content dark. const accumCanvas = new OffscreenCanvas(W, H); const actx = accumCanvas.getContext("2d"); // --- encoder: H.264 annexb so Node can mux the raw stream with ffmpeg -c copy --- const chunks = []; + const MAX_ENCODED_BYTES = 1.5e9; + let totalEncodedBytes = 0; let encodeError = null; const encoder = new VideoEncoder({ output: (chunk) => { const buf = new Uint8Array(chunk.byteLength); chunk.copyTo(buf); + totalEncodedBytes += buf.length; + if (totalEncodedBytes > MAX_ENCODED_BYTES) { + throw new Error("encoded result exceeds 1.5GB cap"); + } chunks.push(buf); }, error: (e) => { encodeError = e; }, @@ -68,8 +75,8 @@ async function main() { bitrate: 10_000_000, avc: { format: "annexb" }, }; - // probe BEFORE configuring: a clear one-line failure beats a cryptic - // mid-render encoder error (adversarial review: no capability probing) + // Probe BEFORE configuring: a clear one-line failure beats a cryptic + // mid-render encoder error. const support = await VideoEncoder.isConfigSupported(encoderConfig); if (!support.supported) { throw new Error("H.264 (avc1.640028) encoding not supported by this Chromium — cannot render"); @@ -97,8 +104,7 @@ async function main() { c.closePath(); } - // macOS pointer, redrawn properly (v0's hand-sketched arrow read as cheap — - // Brayden: "the cursor is a bit cringe"). Accurate proportions, rounded + // macOS pointer with accurate proportions, rounded // joins, soft drop shadow, micro-squeeze on click. Tip at (0,0). function drawCursor(c, x, y, pulse) { c.save(); @@ -164,7 +170,7 @@ async function main() { // adaptive blur: pass count scales with corner displacement across the // shutter so ghost spacing stays ≲1px at any camera speed (the residual - // "weird border" rings Brayden still saw on v5 were 8 discrete copies of + // Border rings come from 8 discrete copies of // fast frames + 8 stacked shadows) const [z0, ox0, oy0] = camAt(0); const [z1, ox1, oy1] = camAt(1); @@ -223,7 +229,7 @@ async function main() { } // window shadow: drawn ONCE per frame at mid-shutter — it is already a // 72px blur, so motion-blurring it is invisible, but stacking copies of - // it was the big concentric banding (QC round: v5 residual rings) + // it creates concentric banding. { const [z, offX, offY] = camAt(0.5); ctx.setTransform(z, 0, 0, z, offX, offY); @@ -238,7 +244,7 @@ async function main() { } ctx.drawImage(accumCanvas, 0, 0); // vignette pulls the eye to the window — fades out as the camera zooms in - // (a fixed vignette grays the corners of bright content at zoom, QC round 3) + // (a fixed vignette grays the corners of bright content at zoom) const zNow = camera[(f * SUB + (SUB - 1)) * 3]; const vigA = Math.max(0, Math.min(1, (1.55 - zNow) / 0.55)) * background.vignette; if (vigA > 0.01) { @@ -250,7 +256,7 @@ async function main() { } // 3) cursor: drawn SHARP on the final composite (dark pixels vanish in the - // additive blur layer — found in QC round 2). It still tracks the camera: + // additive blur layer). It still tracks the camera: // position + scale from the last subframe's transform. { const base = (f * SUB + (SUB - 1)) * 3; @@ -260,7 +266,7 @@ async function main() { ctx.save(); ctx.translate(z * cur[0] + offX, z * cur[1] + offY); // damped scale (sqrt z): full proportional growth read as distracting - // (PR #1 review) but a fully fixed cursor detaches from the content — + // but a fully fixed cursor detaches from the content — // sqrt keeps it cohesive while barely growing (~1.2x at max zoom) const cs = Math.sqrt(z); ctx.scale(cs, cs); @@ -272,7 +278,7 @@ async function main() { encoder.encode(vf, { keyFrame: f % 120 === 0 }); vf.close(); if (encodeError) throw encodeError; - // real backpressure: DRAIN the queue, don't nap once and hope (review: P2) + // Real backpressure: drain the queue, do not nap once and hope. while (encoder.encodeQueueSize > 4) { await new Promise((r) => setTimeout(r, 8)); if (encodeError) throw encodeError; diff --git a/src/render/index.ts b/src/render/index.ts index 13b1da4..79a835d 100644 --- a/src/render/index.ts +++ b/src/render/index.ts @@ -10,8 +10,10 @@ */ import { execFile } from "node:child_process"; import { randomBytes } from "node:crypto"; -import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs"; +import { createWriteStream, existsSync, mkdirSync, readdirSync, readFileSync, statSync } from "node:fs"; import { createServer } from "node:http"; +import { Transform } from "node:stream"; +import { pipeline } from "node:stream/promises"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; @@ -27,7 +29,7 @@ export interface RenderOptions { outFile: string; /** palette name (aurora|midnight|dusk|paper) or a path to a wallpaper image */ background?: string; - /** ms; encoding 60s of footage measured ~36s in the spike — 5 min is generous */ + /** ms; encoding 60s of footage is expected to finish well within 5 min */ timeoutMs?: number; } @@ -43,7 +45,7 @@ export async function renderTake(opts: RenderOptions): Promise { const timeoutMs = opts.timeoutMs ?? 300_000; const t0 = Date.now(); - // fail BEFORE the expensive work: output dir + take shape (review: P2) + // Fail before expensive work: output dir + take shape. mkdirSync(dirname(outFile), { recursive: true }); const log = parseEventLog(JSON.parse(readFileSync(join(takeDir, "events.json"), "utf8"))); const rawIndex = JSON.parse(readFileSync(join(takeDir, "frames-index.json"), "utf8")); @@ -56,10 +58,11 @@ export async function renderTake(opts: RenderOptions): Promise { if (!existsSync(bgSpec)) { const assetsDir = fileURLToPath(new URL("../../assets", import.meta.url)); if (existsSync(assetsDir)) { - const needle = bgSpec.toLowerCase().replace(/[^a-z0-9]/g, ""); - const hit = readdirSync(assetsDir).find((f) => - f.toLowerCase().replace(/[^a-z0-9]/g, "").includes(needle), - ); + const requested = bgSpec.toLowerCase().replace(/\.[a-z0-9]+$/, ""); + const hit = readdirSync(assetsDir).find((f) => { + const lower = f.toLowerCase(); + return lower === bgSpec.toLowerCase() || lower.replace(/\.[a-z0-9]+$/, "") === requested; + }); if (hit) bgSpec = join(assetsDir, hit); } } @@ -72,19 +75,33 @@ export async function renderTake(opts: RenderOptions): Promise { const planJson = JSON.stringify(plan); const token = randomBytes(16).toString("hex"); - let resultBuf: Buffer | null = null; + const rawPath = join(takeDir, "encoded.h264"); + let encodedBytes = 0; + let resultReady = false; + let rejectResult!: (err: Error) => void; let resolveResult!: () => void; - const resultReceived = new Promise((r) => (resolveResult = r)); + const resultReceived = new Promise((r, rej) => { resolveResult = r; rejectResult = rej; }); const server = createServer((req, res) => { - const url = (req.url ?? "/").split("?")[0]!; + const rawUrl = req.url ?? "/"; + const parsedUrl = new URL(rawUrl, "http://127.0.0.1"); + const url = parsedUrl.pathname; + const authorized = parsedUrl.searchParams.get("t") === token || req.headers["x-render-token"] === token; + const requireToken = (): boolean => { + if (authorized) return true; + res.writeHead(403); + res.end(); + return false; + }; if (url === "/" || url === "/host.html") { res.writeHead(200, { "content-type": "text/html" }); res.end(HOST_PAGE); } else if (url === "/take/render-plan.json") { + if (!requireToken()) return; res.writeHead(200, { "content-type": "application/json" }); res.end(planJson); } else if (url.startsWith("/take/frames/")) { + if (!requireToken()) return; try { const name = url.slice("/take/frames/".length).replace(/[^0-9a-zA-Z._-]/g, ""); const buf = readFileSync(join(takeDir, "frames", name)); @@ -95,6 +112,7 @@ export async function renderTake(opts: RenderOptions): Promise { res.end(); } } else if (url === "/take/bg" && bgIsImage) { + if (!requireToken()) return; const ext = bgSpec.toLowerCase(); const mime = ext.endsWith(".png") ? "image/png" : ext.endsWith(".webp") ? "image/webp" : "image/jpeg"; res.writeHead(200, { "content-type": mime }); @@ -102,28 +120,33 @@ export async function renderTake(opts: RenderOptions): Promise { } else if (url === "/result" && req.method === "POST") { // only OUR page may deliver the result (token minted per render), // and a runaway encoder can't OOM Node (size cap) - if (req.headers["x-render-token"] !== token) { - res.writeHead(403); - res.end(); - return; - } + if (!requireToken()) return; const MAX_RESULT_BYTES = 1.5e9; let received = 0; - const parts: Buffer[] = []; - req.on("data", (c: Buffer) => { - received += c.length; - if (received > MAX_RESULT_BYTES) { - req.destroy(new Error("encoded result exceeds 1.5GB cap")); - return; + const sizeLimiter = new Transform({ + transform(chunk: Buffer, _encoding, callback) { + received += chunk.length; + if (received > MAX_RESULT_BYTES) { + callback(new Error("encoded result exceeds 1.5GB cap")); + return; + } + callback(null, chunk); } - parts.push(c); - }); - req.on("end", () => { - resultBuf = Buffer.concat(parts); - res.writeHead(200); - res.end("ok"); - resolveResult(); }); + pipeline(req, sizeLimiter, createWriteStream(rawPath)) + .then(() => { + encodedBytes = received; + resultReady = true; + res.writeHead(200); + res.end("ok"); + resolveResult(); + }) + .catch((err) => { + const e = err instanceof Error ? err : new Error(String(err)); + res.writeHead(500); + res.end(e.message); + rejectResult(e); + }); } else { res.writeHead(404); res.end(); @@ -132,7 +155,7 @@ export async function renderTake(opts: RenderOptions): Promise { await new Promise((r) => server.listen(0, "127.0.0.1", r)); const port = (server.address() as { port: number }).port; - // full Chromium: the stripped headless shell has no WebCodecs (spike gotcha #2) + // Full Chromium: the stripped headless shell has no WebCodecs. const browser = await chromium.launch({ headless: true, channel: "chromium" }); try { const page = await browser.newPage(); @@ -156,7 +179,7 @@ export async function renderTake(opts: RenderOptions): Promise { for (;;) { await new Promise((r) => setTimeout(r, 500)); if (fatal) throw new Error(fatal); - if (resultBuf) return; + if (resultReady) return; } })(), ]); @@ -165,16 +188,13 @@ export async function renderTake(opts: RenderOptions): Promise { await new Promise((r) => server.close(() => r())); } - if (!resultBuf || (resultBuf as Buffer).length === 0) { + if (!resultReady || encodedBytes === 0) { throw new Error("render produced no encoded output"); } - const encoded: Buffer = resultBuf; // mux raw annexb H.264 → MP4. ffmpeg is a muxer here, never an effects engine. - const rawPath = join(takeDir, "encoded.h264"); - writeFileSync(rawPath, encoded); // -r BEFORE -i: raw annexb has no timestamps; this assigns them at 60fps. - // (-framerate alone misparses → 120fps/wrong duration, found 2026-06-11.) + // (-framerate alone can misparse to the wrong duration.) await exec("ffmpeg", [ "-y", "-f", "h264", @@ -188,7 +208,7 @@ export async function renderTake(opts: RenderOptions): Promise { return { outFile, frames: plan.frames, - encodedBytes: encoded.length, + encodedBytes, wallMs: Date.now() - t0, }; } diff --git a/src/render/plan.ts b/src/render/plan.ts index 2ad6df9..fbf14cc 100644 --- a/src/render/plan.ts +++ b/src/render/plan.ts @@ -57,8 +57,8 @@ export interface BackgroundStyle { /** * Curated palettes. "aurora" is the default — the soft blurred - * pastel-mesh look of OpenAI-style launch videos (Brayden's reference, - * 2026-06-11). Apple wallpapers can't be bundled (copyright); users get the + * pastel-mesh look of modern launch videos. Apple wallpapers cannot be + * bundled (copyright); users get the * same vibe via --bg . */ export const PALETTES: Record = { @@ -146,8 +146,7 @@ const ZOOM_TARGET = 1.48; const ZOOM_LEAD_MS = 600; // camera starts moving before the click lands const ZOOM_DWELL_MS = 1500; // stays on target after the event /** segments closer than this bridge into ONE held zoom — the camera glides - * between targets instead of pumping out/in per click (Brayden: "everything - * is just moving too much... the screen is kind of shaking", 2026-06-11) */ + * between targets instead of pumping out/in per click. */ const MERGE_GAP_MS = 2600; const TAIL_MS = 600; const PULSE_MS = 350; @@ -183,7 +182,7 @@ export function buildRenderPlan( ): RenderPlan { if (frameIndex.length === 0) throw new Error("render plan: empty frame index"); // frame index is external input — one malformed entry can otherwise request - // absurd allocations or break the nearest-hold walk (review: P1 bounds) + // absurd allocations or break the nearest-hold walk. let prevT = -1; for (const [i, e] of frameIndex.entries()) { if (typeof e?.file !== "string" || e.file.length === 0 || typeof e?.t_source !== "number") { @@ -218,7 +217,7 @@ export function buildRenderPlan( } const frames = Math.ceil((lastT + TAIL_MS) / frameMs); // hard ceiling: product max is 60s; 2 min of slack covers overruns — beyond - // that a corrupt timestamp is asking us to allocate the moon (review: P1) + // that a corrupt timestamp is asking us to allocate the moon. const MAX_TAKE_MS = 120_000; if (lastT > MAX_TAKE_MS) { throw new Error( @@ -276,7 +275,7 @@ export function buildRenderPlan( // ---- spring integration at subframe resolution ---- // 180° shutter: integrate 2×SUBFRAMES steps per frame but RECORD only the // first half — blur spans half the frame interval, halving ghost spacing - // (the "onion ring" edge artifact Brayden spotted, 2026-06-11) + // (prevents onion-ring edge artifacts) const STEPS = SUBFRAMES * 2; const dt = frameMs / 1000 / STEPS; const state = { z: 1, fx: center.x, fy: center.y, vz: 0, vfx: 0, vfy: 0 }; diff --git a/src/schema/event-log.ts b/src/schema/event-log.ts index 107644a..a333bef 100644 --- a/src/schema/event-log.ts +++ b/src/schema/event-log.ts @@ -2,29 +2,18 @@ import { z } from "zod"; /** * Event-Log Schema v0 — the public contract. - * - * Emitted by any recorder (supercut's Playwright executor, or third-party) - * alongside raw footage. The renderer consumes ONLY this file plus the video. - * - * recorder ──▶ footage.raw + events.json ──▶ renderer ──▶ final.mp4 - * - * Coordinates are CSS (logical) pixels in viewport space; the renderer - * multiplies by `viewport.dpr` to sample raw frames. - * - * `t` is the SCHEDULED timestamp (ms since first frame, frame 0 = t 0) and is - * deterministic by construction. `observed_t` is optional wall-clock metadata, - * excluded from determinism comparisons. - * - * Unknown event types MUST be ignored by consumers (forward compatibility) — - * use `parseEventLog`, which strips unknown-type events instead of failing. */ -const bbox = z.tuple([z.number(), z.number(), z.number(), z.number()]); // x, y, w, h -const point = z.tuple([z.number(), z.number()]); +export const MAX_EVENTS = 5_000; +export const MAX_CURSOR_POINTS = 20_000; + +const finite = z.number().finite(); +const bbox = z.tuple([finite, finite, finite.positive(), finite.positive()]); // x, y, w, h +const point = z.tuple([finite, finite]); const baseEvent = { - t: z.number().nonnegative(), - observed_t: z.number().nonnegative().optional(), + t: finite.nonnegative(), + observed_t: finite.nonnegative().optional(), }; export const clickEvent = z.object({ @@ -33,7 +22,7 @@ export const clickEvent = z.object({ bbox, selector: z.string(), point, -}); +}).strict(); export const typeEvent = z.object({ ...baseEvent, @@ -41,34 +30,34 @@ export const typeEvent = z.object({ bbox, selector: z.string(), textLen: z.number().int().nonnegative(), -}); +}).strict(); export const scrollEvent = z.object({ ...baseEvent, type: z.literal("scroll"), from: point, to: point, -}); +}).strict(); export const hoverEvent = z.object({ ...baseEvent, type: z.literal("hover"), bbox, selector: z.string(), -}); +}).strict(); export const sceneEvent = z.object({ ...baseEvent, type: z.literal("scene"), name: z.string(), priority: z.number().int().min(1), -}); +}).strict(); export const cursorPathEvent = z.object({ ...baseEvent, type: z.literal("cursor_path"), - points: z.array(z.tuple([z.number(), z.number(), z.number()])), // [t, x, y] -}); + points: z.array(z.tuple([finite.nonnegative(), finite, finite])).max(MAX_CURSOR_POINTS, "too many cursor points"), +}).strict(); export const knownEvent = z.discriminatedUnion("type", [ clickEvent, @@ -84,11 +73,11 @@ export const eventLog = z.object({ viewport: z.object({ width: z.number().int().positive(), height: z.number().int().positive(), - dpr: z.number().positive(), - }), - fps: z.number().int().positive(), - events: z.array(knownEvent), -}); + dpr: finite.positive(), + }).strict(), + fps: z.number().int().positive().max(240), + events: z.array(knownEvent).max(MAX_EVENTS, "too many events"), +}).strict(); export type EventLog = z.infer; export type KnownEvent = z.infer; @@ -102,13 +91,33 @@ const KNOWN_EVENT_TYPES = new Set([ "cursor_path", ]); +function enforceMonotonic(events: KnownEvent[]): void { + let prev = -1; + for (const e of events) { + // cursor_path is global metadata emitted at t=0 after all scene/action + // events by the built-in recorder; validate its internal point timeline but + // do not make the container event participate in event-order monotonicity. + if (e.type !== "cursor_path") { + if (e.t < prev) throw new Error(`event timestamps must be monotonic; ${e.type} at ${e.t} came after ${prev}`); + prev = e.t; + } + if (e.type === "cursor_path") { + let pointPrev = -1; + for (const [t] of e.points) { + if (t < pointPrev) throw new Error("cursor_path points must be monotonic"); + pointPrev = t; + } + } + } +} + /** * Parse an event log from untrusted JSON. Unknown event types are silently * dropped (forward compatibility); malformed KNOWN events still fail loudly. */ export function parseEventLog(raw: unknown): EventLog { const envelope = z - .object({ events: z.array(z.object({ type: z.string() }).passthrough()) }) + .object({ events: z.array(z.object({ type: z.string() }).passthrough()).max(MAX_EVENTS, "too many events") }) .passthrough() .parse(raw); @@ -117,5 +126,7 @@ export function parseEventLog(raw: unknown): EventLog { events: envelope.events.filter((e) => KNOWN_EVENT_TYPES.has(e.type)), }; - return eventLog.parse(filtered); + const parsed = eventLog.parse(filtered); + enforceMonotonic(parsed.events); + return parsed; } diff --git a/src/schema/recipe.ts b/src/schema/recipe.ts index 588ed6a..577c14b 100644 --- a/src/schema/recipe.ts +++ b/src/schema/recipe.ts @@ -22,6 +22,9 @@ export const MAX_BUDGET_MS = 60_000; export const MIN_ACTION_MS = 200; /** recipes drive a real local browser — never allow file:/javascript:/etc. */ +const finite = z.number().finite(); +const positiveFinite = finite.positive(); + const httpUrl = z .string() .url() @@ -40,8 +43,9 @@ export const action = z duration_ms: z.number().int().min(MIN_ACTION_MS), /** Where the camera should look during this action (CSS px bbox). * PATCHABLE by QC. */ - zoom: z.tuple([z.number(), z.number(), z.number(), z.number()]).optional(), + zoom: z.tuple([finite.nonnegative(), finite.nonnegative(), positiveFinite, positiveFinite]).optional(), }) + .strict() .superRefine((a, ctx) => { // per-kind requirements — fail at parse time, never mid-capture if ((a.kind === "click" || a.kind === "hover" || a.kind === "type") && !a.selector) { @@ -62,19 +66,19 @@ export const scene = z.object({ entry: z.object({ url: httpUrl, prelude: z.array(action).default([]), - }), + }).strict(), depends_on: z.array(z.string()).default([]), actions: z.array(action).min(1), /** Extra hold on the scene's last frame, ms. PATCHABLE by QC. */ - hold_ms: z.number().int().nonnegative().default(0), -}); + hold_ms: z.number().int().nonnegative().max(MAX_BUDGET_MS).default(0), +}).strict(); export const recipe = z.object({ version: z.literal(0), app_url: httpUrl, music_track: z.string().min(1), scenes: z.array(scene).min(1), -}); +}).strict(); export type Recipe = z.infer; export type Scene = z.infer; diff --git a/src/security/redaction.ts b/src/security/redaction.ts new file mode 100644 index 0000000..c247e83 --- /dev/null +++ b/src/security/redaction.ts @@ -0,0 +1,13 @@ +/** Conservative prompt redaction for common secrets and direct identifiers. */ +const EMAIL = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi; +const SECRET_ASSIGNMENT = /\b(api[_-]?key|token|password|secret|bearer)\s*[:=]\s*([^\s,;"']+)/gi; +const OPENAI_STYLE_KEY = /\bsk-[A-Za-z0-9_-]{10,}\b/g; +const LONG_HEX = /\b[a-f0-9]{32,}\b/gi; + +export function redactForPrompt(text: string): string { + return text + .replace(EMAIL, "[REDACTED_EMAIL]") + .replace(OPENAI_STYLE_KEY, "[REDACTED_KEY]") + .replace(LONG_HEX, "[REDACTED_TOKEN]") + .replace(SECRET_ASSIGNMENT, (_m, key: string) => `${key}=[REDACTED]`); +} diff --git a/src/security/url-policy.ts b/src/security/url-policy.ts new file mode 100644 index 0000000..78f06e2 --- /dev/null +++ b/src/security/url-policy.ts @@ -0,0 +1,68 @@ +import { lookup } from "node:dns/promises"; +import { isIP } from "node:net"; + +export interface NavigationPolicyOptions { + allowPrivateNetwork?: boolean; + /** optional final URL after following redirects; checked with stricter redirect error */ + finalUrl?: string; +} + +function ipToLong(ip: string): number | null { + if (isIP(ip) !== 4) return null; + return ip.split(".").reduce((n, part) => (n << 8) + Number(part), 0) >>> 0; +} + +function inCidr(ip: string, base: string, bits: number): boolean { + const n = ipToLong(ip); + const b = ipToLong(base); + if (n === null || b === null) return false; + const mask = bits === 0 ? 0 : (0xffffffff << (32 - bits)) >>> 0; + return (n & mask) === (b & mask); +} + +function isPrivateHostname(hostname: string): boolean { + const h = hostname.toLowerCase().replace(/^\[(.*)\]$/, "$1").replace(/\.$/, ""); + if (h === "localhost" || h.endsWith(".localhost")) return true; + if (h === "0.0.0.0") return true; + if (isIP(h) === 6) return h === "::1" || h.startsWith("fc") || h.startsWith("fd") || h.startsWith("fe80:"); + if (isIP(h) === 4) { + return ( + inCidr(h, "10.0.0.0", 8) || + inCidr(h, "127.0.0.0", 8) || + inCidr(h, "169.254.0.0", 16) || + inCidr(h, "172.16.0.0", 12) || + inCidr(h, "192.168.0.0", 16) + ); + } + return false; +} + +async function resolvesPrivate(hostname: string): Promise { + if (isPrivateHostname(hostname)) return true; + try { + const addrs = await lookup(hostname, { all: true, verbatim: true }); + return addrs.some((a) => isPrivateHostname(a.address)); + } catch { + return false; + } +} + +async function checkOne(raw: string, opts: NavigationPolicyOptions, redirect: boolean): Promise { + let url: URL; + try { + url = new URL(raw); + } catch { + throw new Error(`invalid navigation URL: ${raw}`); + } + if (url.protocol !== "http:" && url.protocol !== "https:") { + throw new Error(`navigation URL must be http(s): ${raw}`); + } + if (!opts.allowPrivateNetwork && await resolvesPrivate(url.hostname)) { + throw new Error(`${redirect ? "redirect target" : "navigation URL"} is on a private network: ${raw}`); + } +} + +export async function assertSafeNavigationUrl(raw: string, opts: NavigationPolicyOptions = {}): Promise { + await checkOne(raw, opts, false); + if (opts.finalUrl && opts.finalUrl !== raw) await checkOne(opts.finalUrl, opts, true); +} diff --git a/test/config.test.ts b/test/config.test.ts new file mode 100644 index 0000000..cd058bb --- /dev/null +++ b/test/config.test.ts @@ -0,0 +1,73 @@ +import { afterEach, describe, expect, it } from "vitest"; +import { resolveProvider, type ProviderEnv } from "../src/director/config.js"; + +function resolved(env: ProviderEnv) { + return resolveProvider(env); +} + +describe("provider resolution", () => { + const oldEnv = { ...process.env }; + afterEach(() => { + process.env = { ...oldEnv }; + }); + + it("resolves DeepSeek explicitly even when a custom DeepSeek base URL is set", () => { + const p = resolved({ + DEEPSEEK_API_KEY: "deepseek-key", + SUPERCUT_LLM_BASE_URL: "https://api.deepseek.com", + }); + + expect(p.provider).toBe("deepseek"); + expect(p.vision).toBe(false); + expect(p.model).toBe("deepseek-v4-pro"); + expect(p.summary).toContain("deepseek:deepseek-v4-pro @ https://api.deepseek.com"); + }); + + it("fails loudly when provider-specific keys are mixed without an explicit provider", () => { + expect(() => resolved({ DEEPSEEK_API_KEY: "deepseek", OPENROUTER_API_KEY: "or" })).toThrow( + /multiple provider keys/i, + ); + }); + + it("lets SUPERCUT_PROVIDER disambiguate mixed keys", () => { + const p = resolved({ + SUPERCUT_PROVIDER: "openrouter", + DEEPSEEK_API_KEY: "deepseek", + OPENROUTER_API_KEY: "or", + SUPERCUT_MODEL: "anthropic/claude-sonnet-4.6", + }); + + expect(p.provider).toBe("openrouter"); + expect(p.vision).toBe(true); + expect(p.model).toBe("anthropic/claude-sonnet-4.6"); + }); + + it("rejects forcing vision on for DeepSeek text-only models", () => { + expect(() => resolved({ DEEPSEEK_API_KEY: "deepseek", SUPERCUT_VISION: "true" })).toThrow(/vision.*deepseek/i); + }); + + it("does not mutate process.env when an explicit model override is passed", () => { + process.env.SUPERCUT_MODEL = "original"; + const p = resolveProvider({ DEEPSEEK_API_KEY: "deepseek" }, { model: "deepseek-v4-flash" }); + expect(p.model).toBe("deepseek-v4-flash"); + expect(process.env.SUPERCUT_MODEL).toBe("original"); + }); + + it("requires an explicit model for custom OpenAI-compatible endpoints", () => { + expect(() => + resolved({ + SUPERCUT_PROVIDER: "custom", + SUPERCUT_API_KEY: "custom-key", + SUPERCUT_LLM_BASE_URL: "https://llm.example.com/v1", + }), + ).toThrow(/SUPERCUT_MODEL.*custom/i); + + const p = resolved({ + SUPERCUT_PROVIDER: "custom", + SUPERCUT_API_KEY: "custom-key", + SUPERCUT_LLM_BASE_URL: "https://llm.example.com/v1", + SUPERCUT_MODEL: "local-model", + }); + expect(p.model).toBe("local-model"); + }); +}); diff --git a/test/director-validation.test.ts b/test/director-validation.test.ts new file mode 100644 index 0000000..61f4854 --- /dev/null +++ b/test/director-validation.test.ts @@ -0,0 +1,46 @@ +import { describe, expect, it } from "vitest"; +import { validateAnalysis } from "../src/director/analyze.js"; +import type { PageDigest } from "../src/director/inventory.js"; + +const digests: PageDigest[] = [ + { + url: "http://127.0.0.1:9999/", + title: "Home", + headings: ["Home"], + inventory: [ + { selector: "#cta", tag: "button", text: "Start", bbox: { x: 1, y: 2, w: 3, h: 4 } }, + ], + }, +]; + +describe("analysis validation", () => { + it("rejects money moments for non-crawled pages", () => { + expect(() => + validateAnalysis( + { + product_summary: "A useful product with dashboard analytics.", + money_moments: [ + { title: "Fake", why: "not crawled", page_url: "http://127.0.0.1:9999/admin", elements: ["#cta"] }, + { title: "Start", why: "real moment", page_url: "http://127.0.0.1:9999/", elements: ["#cta"] }, + ], + }, + digests, + ), + ).toThrow(/not a crawled page/i); + }); + + it("rejects selectors not inventoried on the referenced page", () => { + expect(() => + validateAnalysis( + { + product_summary: "A useful product with dashboard analytics.", + money_moments: [ + { title: "Fake", why: "fake selector", page_url: "http://127.0.0.1:9999/", elements: ["#missing"] }, + { title: "Start", why: "real moment", page_url: "http://127.0.0.1:9999/", elements: ["#cta"] }, + ], + }, + digests, + ), + ).toThrow(/not in the inventory/i); + }); +}); diff --git a/test/director.test.ts b/test/director.test.ts index 60f0f48..bbfc933 100644 --- a/test/director.test.ts +++ b/test/director.test.ts @@ -62,6 +62,14 @@ function validRecipeJson(selector: string): string { actions: [{ kind: "click", selector, duration_ms: 1500 }], hold_ms: 400, }, + { + name: "email-payoff", + priority: 2, + entry: { url: "http://127.0.0.1:9999/", prelude: [] }, + depends_on: [], + actions: [{ kind: "type", selector: "#email", text: "founder@example.com", duration_ms: 1500 }], + hold_ms: 600, + }, ], }); } @@ -105,6 +113,40 @@ describe("script stage — the anti-hallucination gates", () => { expect(attempts).toBe(2); }); + it("rejects recipes that skip storyboard beats", async () => { + const oneScene = JSON.parse(validRecipeJson("#cta")) as { scenes: unknown[] }; + oneScene.scenes = oneScene.scenes.slice(0, 1); + const llm = new StubLlm([JSON.stringify(oneScene), validRecipeJson("#cta")]); + const { attempts } = await writeRecipe(llm, analysis, digests, "http://127.0.0.1:9999"); + expect(attempts).toBe(2); + const retryText = llm.prompts[1]!.user.map((p) => (p.type === "text" ? p.text : "")).join(" "); + expect(retryText).toContain("one per money moment"); + }); + + it("rejects scenes that ignore the ordered money moment selector", async () => { + const wrongBeat = JSON.parse(validRecipeJson("#cta")) as { + scenes: { actions: { selector: string; kind: string; text?: string }[] }[]; + }; + wrongBeat.scenes[1]!.actions[0] = { kind: "click", selector: "#cta", duration_ms: 1500 }; + const llm = new StubLlm([JSON.stringify(wrongBeat), validRecipeJson("#cta")]); + const { attempts } = await writeRecipe(llm, analysis, digests, "http://127.0.0.1:9999"); + expect(attempts).toBe(2); + const retryText = llm.prompts[1]!.user.map((p) => (p.type === "text" ? p.text : "")).join(" "); + expect(retryText).toContain("does not film storyboard beat"); + }); + + it("rejects mid-scene goto actions that make the footage a random tour", async () => { + const withGoto = JSON.parse(validRecipeJson("#cta")) as { + scenes: { actions: { kind: string; url?: string; duration_ms: number; selector?: string; text?: string }[] }[]; + }; + withGoto.scenes[0]!.actions.unshift({ kind: "goto", url: "http://127.0.0.1:9999/dash", duration_ms: 1200 }); + const llm = new StubLlm([JSON.stringify(withGoto), validRecipeJson("#cta")]); + const { attempts } = await writeRecipe(llm, analysis, digests, "http://127.0.0.1:9999"); + expect(attempts).toBe(2); + const retryText = llm.prompts[1]!.user.map((p) => (p.type === "text" ? p.text : "")).join(" "); + expect(retryText).toContain("mid-scene goto"); + }); + it("gives up loudly after 4 failed attempts", async () => { const bad = validRecipeJson("#nope"); const llm = new StubLlm([bad, bad, bad, bad]); diff --git a/test/generate.e2e.test.ts b/test/generate.e2e.test.ts index bf6429f..4f14a11 100644 --- a/test/generate.e2e.test.ts +++ b/test/generate.e2e.test.ts @@ -41,7 +41,7 @@ afterAll(async () => { describe("inventory crawler on the fixture app", () => { it("extracts real, resolvable selectors and follows same-origin links", async () => { - const digests = await crawlApp(app.url, { maxPages: 3, screenshots: false }); + const digests = await crawlApp(app.url, { maxPages: 3, screenshots: false, allowPrivateNetwork: true }); expect(digests.length).toBeGreaterThanOrEqual(1); const selectors = digests[0]!.inventory.map((i) => i.selector); expect(selectors).toContain("#cta"); @@ -112,6 +112,7 @@ describe("generate E2E (stubbed brain, real pipeline)", () => { url: app.url, outDir, seed: 7, + allowPrivateNetwork: true, log: () => {}, }); @@ -128,7 +129,7 @@ describe("generate E2E (stubbed brain, real pipeline)", () => { it("fails fast on an unreachable app URL (before any LLM call)", async () => { const llm = new ScriptedLlm(() => []); await expect( - generate({ llm, url: "http://127.0.0.1:1", outDir: mkdtempSync(join(tmpdir(), "supercut-dead-")), log: () => {} }), + generate({ llm, url: "http://127.0.0.1:1", outDir: mkdtempSync(join(tmpdir(), "supercut-dead-")), allowPrivateNetwork: true, log: () => {} }), ).rejects.toThrow(/cannot reach/); expect(llm.calls).toBe(0); }, 30_000); diff --git a/test/record.e2e.test.ts b/test/record.e2e.test.ts index d616fbc..f1597bd 100644 --- a/test/record.e2e.test.ts +++ b/test/record.e2e.test.ts @@ -84,8 +84,8 @@ describe("record E2E on fixture app", () => { const out2 = mkdtempSync(join(tmpdir(), "supercut-take2-")); dirs.push(out1, out2); - const r1 = await record({ recipe, outDir: out1, seed: 42 }); - const r2 = await record({ recipe, outDir: out2, seed: 42 }); + const r1 = await record({ recipe, outDir: out1, seed: 42, allowPrivateNetwork: true }); + const r2 = await record({ recipe, outDir: out2, seed: 42, allowPrivateNetwork: true }); // no failures on the fixture expect(r1.aborted).toBe(false); @@ -179,7 +179,7 @@ describe("record E2E on fixture app", () => { const out = mkdtempSync(join(tmpdir(), "supercut-fail-")); dirs.push(out); // captureFrames off: this test is about failure policy, not pixels - const res = await record({ recipe, outDir: out, seed: 1, captureFrames: false }); + const res = await record({ recipe, outDir: out, seed: 1, captureFrames: false, allowPrivateNetwork: true }); expect(res.failedScenes).toContain("broken"); expect(res.failedScenes).toContain("child-of-broken"); @@ -208,7 +208,7 @@ describe("record E2E on fixture app", () => { const out = mkdtempSync(join(tmpdir(), "supercut-partial-")); dirs.push(out); - const res = await record({ recipe, outDir: out, seed: 1, captureFrames: false }); + const res = await record({ recipe, outDir: out, seed: 1, captureFrames: false, allowPrivateNetwork: true }); expect(res.aborted).toBe(false); // 1 of 3 ≤ 50% → take survives expect(res.failedScenes).toEqual(["bad-mid"]); diff --git a/test/redaction.test.ts b/test/redaction.test.ts new file mode 100644 index 0000000..a46c387 --- /dev/null +++ b/test/redaction.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from "vitest"; +import { redactForPrompt } from "../src/security/redaction.js"; + +describe("prompt redaction", () => { + it("redacts common credentials and emails before LLM prompts", () => { + const redacted = redactForPrompt("email ada@example.com api_key=sk-1234567890abcdef token: secret-value password=hunter2"); + expect(redacted).not.toContain("ada@example.com"); + expect(redacted).not.toContain("sk-1234567890abcdef"); + expect(redacted).not.toContain("secret-value"); + expect(redacted).not.toContain("hunter2"); + expect(redacted).toContain("[REDACTED_EMAIL]"); + expect(redacted).toContain("api_key=[REDACTED]"); + }); +}); diff --git a/test/schema.test.ts b/test/schema.test.ts index 21b8c8c..4c7f838 100644 --- a/test/schema.test.ts +++ b/test/schema.test.ts @@ -203,3 +203,50 @@ describe("recipe hardening (PR #1 review)", () => { expect(() => parseRecipe(r)).toThrow(); }); }); + +describe("schema hardening", () => { + it("rejects unknown recipe fields instead of silently dropping hallucinated keys", () => { + const raw = makeRecipe(); + (raw.scenes[0] as unknown as Record).voiceover = "this field is not supported"; + expect(() => parseRecipe(raw)).toThrow(); + }); + + it("rejects invalid zoom boxes", () => { + const raw = makeRecipe(); + raw.scenes[0]!.actions[0]!.zoom = [-10, 0, -1, 0]; + expect(() => parseRecipe(raw)).toThrow(/zoom/i); + }); + + it("rejects event logs with too many events", () => { + const events = Array.from({ length: 5001 }, (_, i) => ({ type: "scene", t: i, name: `s${i}`, priority: 1 })); + expect(() => parseEventLog({ version: 0, viewport: { width: 1920, height: 1080, dpr: 2 }, fps: 60, events })).toThrow( + /too many events/i, + ); + }); + + it("rejects cursor paths with too many points", () => { + const points = Array.from({ length: 20001 }, (_, i) => [i, 1, 1]); + expect(() => + parseEventLog({ + version: 0, + viewport: { width: 1920, height: 1080, dpr: 2 }, + fps: 60, + events: [{ type: "cursor_path", t: 0, points }], + }), + ).toThrow(/too many cursor points/i); + }); + + it("rejects known events that go backwards in time", () => { + expect(() => + parseEventLog({ + version: 0, + viewport: { width: 1920, height: 1080, dpr: 2 }, + fps: 60, + events: [ + { type: "scene", t: 100, name: "a", priority: 1 }, + { type: "click", t: 50, bbox: [0, 0, 10, 10], selector: "#x", point: [5, 5] }, + ], + }), + ).toThrow(/monotonic/i); + }); +}); diff --git a/test/url-policy.test.ts b/test/url-policy.test.ts new file mode 100644 index 0000000..256b161 --- /dev/null +++ b/test/url-policy.test.ts @@ -0,0 +1,26 @@ +import { describe, expect, it } from "vitest"; +import { assertSafeNavigationUrl } from "../src/security/url-policy.js"; + +describe("navigation URL policy", () => { + it("blocks cloud metadata addresses by default", async () => { + await expect(assertSafeNavigationUrl("http://169.254.169.254/latest/meta-data/")).rejects.toThrow(/private network/i); + }); + + it("blocks localhost by default but allows it explicitly", async () => { + await expect(assertSafeNavigationUrl("http://127.0.0.1:3000/")).rejects.toThrow(/private network/i); + await expect(assertSafeNavigationUrl("http://127.0.0.1:3000/", { allowPrivateNetwork: true })).resolves.toBeUndefined(); + }); + + it("blocks bracketed IPv6 localhost and ULA literals by default", async () => { + await expect(assertSafeNavigationUrl("http://[::1]:3000/")).rejects.toThrow(/private network/i); + await expect(assertSafeNavigationUrl("http://[fd00::1]/")).rejects.toThrow(/private network/i); + }); + + it("rejects redirects to private networks", async () => { + await expect( + assertSafeNavigationUrl("https://example.com/start", { + finalUrl: "http://10.0.0.2/admin", + }), + ).rejects.toThrow(/redirect/i); + }); +});