From 3eae9298349bc0fd7cbdb0445983021224d19f9a Mon Sep 17 00:00:00 2001 From: brickerP Date: Mon, 20 Jul 2026 02:20:39 +0800 Subject: [PATCH 1/2] fix: harden release verification and resume accessibility --- .github/SECURITY.md | 26 + .github/dependabot.yml | 5 + .github/workflows/deploy-pages.yml | 104 ++- package-lock.json | 1261 +++++++++++++++++----------- package.json | 2 +- public/resume.pdf | Bin 15598 -> 50918 bytes scripts/verify-ci.mjs | 80 +- scripts/verify-dist.mjs | 13 +- scripts/verify-live.mjs | 209 +++++ scripts/verify-resume.mjs | 49 ++ scripts/verify-static.mjs | 7 +- 11 files changed, 1219 insertions(+), 537 deletions(-) create mode 100644 .github/SECURITY.md create mode 100644 scripts/verify-live.mjs create mode 100644 scripts/verify-resume.mjs diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000..cbcd702 --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,26 @@ +# Security Policy + +## Supported scope + +Only the version currently deployed from the default `main` branch is supported. +Older commits, branches, builds, and releases are not supported. + +This project is a static, client-side GitHub Pages experience. It has no +application accounts, server-side API, or project-operated data store. Reports +should concern security issues in the deployed site or files on `main`, rather +than general browser, GitHub Pages, or third-party service behavior. + +## Report a vulnerability + +Use GitHub's private [Report a vulnerability](https://github.com/BrickerP/BrickerP.github.io/security/advisories/new) +flow. Do not open a public issue or pull request containing exploit details. + +Include: + +- a description of the vulnerability and its potential impact; +- the affected URL, file, or code path; +- clear reproduction steps or a minimal proof of concept; and +- relevant browser, operating system, or runtime details. + +Do not include credentials or personal data in the report, and do not perform +destructive testing against the deployed site or services you do not own. diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a27f99e..56839da 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -19,6 +19,11 @@ updates: update-types: - "minor" - "patch" + ignore: + # Vite 8.1.5 produces a repeatable 10-channel 0s/48s seam delta in macOS Chrome. + - dependency-name: "vite" + versions: + - "8.x" - package-ecosystem: "github-actions" directory: "/" diff --git a/.github/workflows/deploy-pages.yml b/.github/workflows/deploy-pages.yml index b4de628..d30bfc5 100644 --- a/.github/workflows/deploy-pages.yml +++ b/.github/workflows/deploy-pages.yml @@ -15,17 +15,26 @@ concurrency: cancel-in-progress: true env: - DIST_ARTIFACT: site-dist-${{ github.sha }} + DIST_ARTIFACT: site-dist-${{ github.sha }}-${{ github.run_attempt }} jobs: build: name: Build production artifact runs-on: ubuntu-latest timeout-minutes: 10 + outputs: + dist-artifact-id: ${{ steps.upload-dist.outputs.artifact-id }} steps: - name: Checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - name: Review dependency changes + if: github.event_name == 'pull_request' + uses: actions/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294 # v5.0.0 + with: + fail-on-severity: moderate + fail-on-scopes: runtime, development, unknown + - name: Set up Node.js uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: @@ -50,6 +59,7 @@ jobs: run: npm run verify:dist - name: Upload immutable dist + id: upload-dist uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 with: name: ${{ env.DIST_ARTIFACT }} @@ -75,7 +85,7 @@ jobs: - name: Download immutable dist uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: ${{ env.DIST_ARTIFACT }} + artifact-ids: ${{ needs.build.outputs.dist-artifact-id }} path: dist - name: Install QA browser run: npx playwright install --with-deps chromium @@ -129,7 +139,7 @@ jobs: - name: Download immutable dist uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: ${{ env.DIST_ARTIFACT }} + artifact-ids: ${{ needs.build.outputs.dist-artifact-id }} path: dist - name: Install QA browser run: npx playwright install --with-deps chromium @@ -164,26 +174,81 @@ jobs: if-no-files-found: ignore retention-days: 14 + seam_macos: + name: macOS Chrome seam QA + needs: build + runs-on: macos-15 + timeout-minutes: 20 + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version-file: .node-version + cache: npm + - name: Install dependencies + run: npm ci + - name: Download immutable dist + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + artifact-ids: ${{ needs.build.outputs.dist-artifact-id }} + path: dist + - name: Verify loop seam in macOS Chrome + shell: bash + run: | + set -euo pipefail + mkdir -p .omx/handoff/ci + npm run preview -- --host 127.0.0.1 --port 4173 --strictPort > .omx/handoff/ci/seam-macos-preview.log 2>&1 & + preview_pid=$! + trap 'kill "$preview_pid" 2>/dev/null || true' EXIT + for attempt in {1..30}; do + curl --fail --silent http://127.0.0.1:4173/ > /dev/null && break + if ! kill -0 "$preview_pid" 2>/dev/null || [[ "$attempt" == 30 ]]; then + tail -100 .omx/handoff/ci/seam-macos-preview.log + exit 1 + fi + sleep 1 + done + PW_CHANNEL=chrome URL='http://127.0.0.1:4173/?qa=1' npm run verify:seam + - name: Upload macOS seam evidence + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: seam-macos-evidence-${{ github.run_attempt }} + path: | + .omx/handoff/** + playwright-report/** + test-results/** + include-hidden-files: true + if-no-files-found: ignore + retention-days: 14 + quality_gate: name: Quality gate if: always() needs: - browser - seam + - seam_macos runs-on: ubuntu-latest timeout-minutes: 2 env: BROWSER_RESULT: ${{ needs.browser.result }} SEAM_RESULT: ${{ needs.seam.result }} + MACOS_SEAM_RESULT: ${{ needs.seam_macos.result }} steps: - name: Require every QA lane run: | test "$BROWSER_RESULT" = success test "$SEAM_RESULT" = success + test "$MACOS_SEAM_RESULT" = success publish_pages_artifact: name: Prepare verified Pages artifact - needs: quality_gate + needs: + - build + - quality_gate if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' runs-on: ubuntu-latest timeout-minutes: 5 @@ -191,7 +256,7 @@ jobs: - name: Download verified dist uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: - name: ${{ env.DIST_ARTIFACT }} + artifact-ids: ${{ needs.build.outputs.dist-artifact-id }} path: dist - name: Upload GitHub Pages artifact uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 @@ -200,9 +265,12 @@ jobs: deploy: name: Deploy verified GitHub Pages artifact - needs: publish_pages_artifact + needs: + - build + - publish_pages_artifact if: github.event_name != 'pull_request' && github.ref == 'refs/heads/main' permissions: + contents: read pages: write id-token: write runs-on: ubuntu-latest @@ -214,3 +282,27 @@ jobs: - name: Deploy GitHub Pages id: deployment uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 + + - name: Checkout smoke test + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Set up Node.js for smoke test + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version-file: .node-version + + - name: Download immutable dist for live comparison + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + artifact-ids: ${{ needs.build.outputs.dist-artifact-id }} + path: dist + + - name: Verify live Pages origin + env: + DEPLOYMENT_URL: ${{ steps.deployment.outputs.page_url }} + DIST_DIR: dist + MAX_ATTEMPTS: 12 + REQUEST_TIMEOUT_MS: 8000 + RETRY_DELAY_MS: 5000 + SMOKE_DEADLINE_MS: 120000 + run: timeout 150s node scripts/verify-live.mjs diff --git a/package-lock.json b/package-lock.json index 3976f3f..89b8715 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,6 +8,9 @@ "name": "beijing-infinite-loop", "version": "1.0.0", "license": "MIT", + "engines": { + "node": "22.23.1" + }, "dependencies": { "three": "0.185.1" }, @@ -16,10 +19,7 @@ "@types/three": "0.185.1", "playwright": "^1.61.1", "typescript": "^5.6.3", - "vite": "8.1.5" - }, - "engines": { - "node": "22.23.1" + "vite": "6.4.3" } }, "node_modules/@dimforge/rapier3d-compat": { @@ -29,73 +29,112 @@ "dev": true, "license": "Apache-2.0" }, - "node_modules/@emnapi/core": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", - "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], "dev": true, "license": "MIT", "optional": true, - "dependencies": { - "@emnapi/wasi-threads": "1.2.2", - "tslib": "^2.4.0" + "os": [ + "aix" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@emnapi/runtime": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", - "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], "dev": true, "license": "MIT", "optional": true, - "dependencies": { - "tslib": "^2.4.0" + "os": [ + "android" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@emnapi/wasi-threads": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", - "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", "optional": true, - "dependencies": { - "tslib": "^2.4.0" + "os": [ + "android" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@napi-rs/wasm-runtime": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.6.tgz", - "integrity": "sha512-ZLv/JdUfkvOy9eCnnBaGfiO+XimbjebAeO+MRQqD/B+FR1tnRN0tpKSJHRbE8sFfS6aqsXZ67TQjfwfsxULVbg==", + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], "dev": true, "license": "MIT", "optional": true, - "dependencies": { - "@tybys/wasm-util": "^0.10.3" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/Brooooooklyn" - }, - "peerDependencies": { - "@emnapi/core": "^1.7.1", - "@emnapi/runtime": "^1.7.1" + "os": [ + "android" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@oxc-project/types": { - "version": "0.139.0", - "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.139.0.tgz", - "integrity": "sha512-r9gHphtCs+1M7J0pw6Sn/hh/Wpa/iQrOOkrNAlVLF/gHq+/CJmHIWKKUUhdWjcD6CIa8idarspCsASiXCXvFUw==", + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], "dev": true, "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/Boshen" + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" } }, - "node_modules/@rolldown/binding-android-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.1.5.tgz", - "integrity": "sha512-lZg8fqIv2v7FF237bwMgzGZEJvGL79/s5knJ/i6FmsGF4XXlzccZ4jb+TrFIxtSSxFtIpdsgrPZeMk1I9AFcyQ==", + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", "cpu": [ "arm64" ], @@ -103,16 +142,50 @@ "license": "MIT", "optional": true, "os": [ - "android" + "freebsd" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18" } }, - "node_modules/@rolldown/binding-darwin-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.1.5.tgz", - "integrity": "sha512-51Bnx9pNiMRKSUNtBfySkNJ9vMU9Hh3I1ozDd6gyPPYzaXCfnptUcEZxXGYFn+ul2dtcMUiqGR1Yai2K10uoTw==", + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", "cpu": [ "arm64" ], @@ -120,16 +193,118 @@ "license": "MIT", "optional": true, "os": [ - "darwin" + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18" } }, - "node_modules/@rolldown/binding-darwin-x64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.1.5.tgz", - "integrity": "sha512-Tm+gbfC0aHu1tBA/JvKQh32S0K6YgCHkiAF4/W6xX0K0RmNuc94VeK419dJoE65R5aRxmo+noZQSWrAMF6yb6g==", + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", "cpu": [ "x64" ], @@ -137,16 +312,33 @@ "license": "MIT", "optional": true, "os": [ - "darwin" + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18" } }, - "node_modules/@rolldown/binding-freebsd-x64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.1.5.tgz", - "integrity": "sha512-JMzDKCCXq93YccG5gz3hvOs1oXRKAf0XYpfOS88e+wZrC8Iugj6j68867vrYZkvpDDpKn/KoKORThmchMpF6TA==", + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", "cpu": [ "x64" ], @@ -154,35 +346,323 @@ "license": "MIT", "optional": true, "os": [ - "freebsd" + "netbsd" ], "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18" } }, - "node_modules/@rolldown/binding-linux-arm-gnueabihf": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.1.5.tgz", - "integrity": "sha512-uML21j2K5TfPGutKxub+M+nLjZIrWjXQ5Grx4lCe/nimTj9B4L63zHpjXLl4y0L3mcm2htEQIb06oCG/szerNw==", + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", "cpu": [ - "arm" + "loong64" ], "dev": true, + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-linux-arm64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.1.5.tgz", - "integrity": "sha512-navSiuTMogvnQoZoM/v+l3ZWo50/NTwSHSzheABx/RCnmUPaKwq9qSo4Br2OYRs21+Fz8uFqITZM3H4opOB0/Q==", + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", "cpu": [ - "arm64" + "ppc64" ], "dev": true, "libc": [ @@ -192,17 +672,14 @@ "optional": true, "os": [ "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-linux-arm64-musl": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.1.5.tgz", - "integrity": "sha512-lAryqH7IteztmCXQXk0etKj4wBQ7Gx5S6LjKhsgp9zb8I5bsuvU/2llH1hDQcjsFeqIsovMVN339/8pUDDBXxA==", + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", "cpu": [ - "arm64" + "ppc64" ], "dev": true, "libc": [ @@ -212,17 +689,14 @@ "optional": true, "os": [ "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-linux-ppc64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.1.5.tgz", - "integrity": "sha512-fsK/sNBnxzBlL4O1JNrZakVQxPspqpED5dLtNsZS9oOKmtSpdNIzxH2kkol5HYTWJN47sE20ztMJPxfZ89qGOg==", + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", "cpu": [ - "ppc64" + "riscv64" ], "dev": true, "libc": [ @@ -232,15 +706,29 @@ "optional": true, "os": [ "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] }, - "node_modules/@rolldown/binding-linux-s390x-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.1.5.tgz", - "integrity": "sha512-gLYb4BIadlfTOYT5gO503n8zQjXflgzpD0FcyKh0Mzx3rqCZKnHoJWV9xe1KXUJ5lx2JfcSHr/mhzS0PC/McAA==", + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", "cpu": [ "s390x" ], @@ -252,15 +740,12 @@ "optional": true, "os": [ "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-linux-x64-gnu": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.1.5.tgz", - "integrity": "sha512-FjcpEKUyJygHgs1o50VYNvkt5+7Le/VEdYt0AkRpkL33MnyQfwr8l5mXwMmfmTbyMPr5vJLC+8/Gd9gXnwU1QQ==", + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", "cpu": [ "x64" ], @@ -272,15 +757,12 @@ "optional": true, "os": [ "linux" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-linux-x64-musl": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.1.5.tgz", - "integrity": "sha512-Me+PfPI2TMeOQk0gYWfLQZtTktrmzbr8cDboqX83XKc7UrgAi55gF+2dUkWdxd19n55Essp2yeca+O9N5rBxHg==", + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", "cpu": [ "x64" ], @@ -292,15 +774,26 @@ "optional": true, "os": [ "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] }, - "node_modules/@rolldown/binding-openharmony-arm64": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.1.5.tgz", - "integrity": "sha512-yc5WrLzXks6zCQfn9Oxr8pORKyl/pF+QjHmW/Qx3qu0oyrrNC+y2JLTU1E2rcWYAmzlnqngWXHQjy51VzW70Vw==", + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", "cpu": [ "arm64" ], @@ -309,51 +802,40 @@ "optional": true, "os": [ "openharmony" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-wasm32-wasi": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.1.5.tgz", - "integrity": "sha512-VbQGPX2b4r48TAMIM2cjgluIM1HYutm4pcTEJsle7iEP7sB1dFqtPLBVbdLAZCxy1txCcPxf4QFf4v8uvltPqA==", + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", "cpu": [ - "wasm32" + "arm64" ], "dev": true, "license": "MIT", "optional": true, - "dependencies": { - "@emnapi/core": "1.11.1", - "@emnapi/runtime": "1.11.1", - "@napi-rs/wasm-runtime": "^1.1.6" - }, - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + "os": [ + "win32" + ] }, - "node_modules/@rolldown/binding-win32-arm64-msvc": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.1.5.tgz", - "integrity": "sha512-gHv82k63z4qpV5+Q1y/12KrK0ltWBukVDI8nZcbT7Tt/ZlOIVwppazneq0F93oDxTo3IgAMEDIoQh3E2n6mVsw==", + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", "cpu": [ - "arm64" + "ia32" ], "dev": true, "license": "MIT", "optional": true, "os": [ "win32" - ], - "engines": { - "node": "^20.19.0 || >=22.12.0" - } + ] }, - "node_modules/@rolldown/binding-win32-x64-msvc": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.1.5.tgz", - "integrity": "sha512-tTZuDBPw85tEN5PQi1pnEBzDy0Z49HtScLAbD5t6hyeU92A95pRWaSMw1GZZi/RwgSgUIl0xrSlXIT/9QzvYSA==", + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", "cpu": [ "x64" ], @@ -362,17 +844,21 @@ "optional": true, "os": [ "win32" - ], - "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==", + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], "dev": true, - "license": "MIT" + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, "node_modules/@tweenjs/tween.js": { "version": "23.1.3", @@ -381,16 +867,12 @@ "dev": true, "license": "MIT" }, - "node_modules/@tybys/wasm-util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.3.tgz", - "integrity": "sha512-F3fo1MYrRJYL3zER0OUOmkutjr1Vp23m7OsSgp7nq4SP6OqX6C/56XFIPAl5bt3zaBRjmW7SGz3u/6LwFpYcOg==", + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "dev": true, - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } + "license": "MIT" }, "node_modules/@types/node": { "version": "22.20.1", @@ -431,14 +913,46 @@ "dev": true, "license": "MIT" }, - "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==", + "node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "dev": true, - "license": "Apache-2.0", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, "engines": { - "node": ">=8" + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" } }, "node_modules/fdir": { @@ -481,279 +995,6 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, - "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": "MPL-2.0", - "dependencies": { - "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, - "license": "MPL-2.0", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">= 12.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/parcel" - } - }, - "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": "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, - "libc": [ - "glibc" - ], - "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, - "libc": [ - "musl" - ], - "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, - "libc": [ - "glibc" - ], - "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, - "libc": [ - "musl" - ], - "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/meshoptimizer": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz", @@ -876,38 +1117,49 @@ "node": "^10 || ^12 || >=14" } }, - "node_modules/rolldown": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.1.5.tgz", - "integrity": "sha512-t9z29cJjXf/vxQ8dyhCSpt6H6aSwHTk8cT5I3iy6SMXuFpk5mB6PL6XfC8PCwrPTx93udwKUm9HRteAlTGBLiA==", + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", "dev": true, "license": "MIT", "dependencies": { - "@oxc-project/types": "=0.139.0", - "@rolldown/pluginutils": "^1.0.0" + "@types/estree": "1.0.9" }, "bin": { - "rolldown": "bin/cli.mjs" + "rollup": "dist/bin/rollup" }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": ">=18.0.0", + "npm": ">=8.0.0" }, "optionalDependencies": { - "@rolldown/binding-android-arm64": "1.1.5", - "@rolldown/binding-darwin-arm64": "1.1.5", - "@rolldown/binding-darwin-x64": "1.1.5", - "@rolldown/binding-freebsd-x64": "1.1.5", - "@rolldown/binding-linux-arm-gnueabihf": "1.1.5", - "@rolldown/binding-linux-arm64-gnu": "1.1.5", - "@rolldown/binding-linux-arm64-musl": "1.1.5", - "@rolldown/binding-linux-ppc64-gnu": "1.1.5", - "@rolldown/binding-linux-s390x-gnu": "1.1.5", - "@rolldown/binding-linux-x64-gnu": "1.1.5", - "@rolldown/binding-linux-x64-musl": "1.1.5", - "@rolldown/binding-openharmony-arm64": "1.1.5", - "@rolldown/binding-wasm32-wasi": "1.1.5", - "@rolldown/binding-win32-arm64-msvc": "1.1.5", - "@rolldown/binding-win32-x64-msvc": "1.1.5" + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" } }, "node_modules/source-map-js": { @@ -943,14 +1195,6 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, - "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/typescript": { "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", @@ -973,23 +1217,24 @@ "license": "MIT" }, "node_modules/vite": { - "version": "8.1.5", - "resolved": "https://registry.npmjs.org/vite/-/vite-8.1.5.tgz", - "integrity": "sha512-7ULLwsCdYx/nRyrpiEwvqb5TFHrMVZyBt+rg/OAXT7rgj/z+DtTDyKFeLAdDkubDVDKD8jOsndmy7m55XcfUsw==", + "version": "6.4.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.4.3.tgz", + "integrity": "sha512-NTKlcQjlAK7MlQoyb6LgaqHc8sso/pVyUJYWMws3jg21uTJw/LddqIFPcPqP6PzpgbIcZyKI85sFE4HBrQDA8A==", "dev": true, "license": "MIT", "dependencies": { - "lightningcss": "^1.32.0", - "picomatch": "^4.0.5", - "postcss": "^8.5.17", - "rolldown": "~1.1.5", - "tinyglobby": "^0.2.17" + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" }, "bin": { "vite": "bin/vite.js" }, "engines": { - "node": "^20.19.0 || >=22.12.0" + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" }, "funding": { "url": "https://github.com/vitejs/vite?sponsor=1" @@ -998,15 +1243,14 @@ "fsevents": "~2.3.3" }, "peerDependencies": { - "@types/node": "^20.19.0 || >=22.12.0", - "@vitejs/devtools": "^0.3.0", - "esbuild": "^0.27.0 || ^0.28.0", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.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", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" @@ -1015,18 +1259,15 @@ "@types/node": { "optional": true }, - "@vitejs/devtools": { - "optional": true - }, - "esbuild": { - "optional": true - }, "jiti": { "optional": true }, "less": { "optional": true }, + "lightningcss": { + "optional": true + }, "sass": { "optional": true }, diff --git a/package.json b/package.json index c0c776f..fada157 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,6 @@ "@types/three": "0.185.1", "playwright": "^1.61.1", "typescript": "^5.6.3", - "vite": "8.1.5" + "vite": "6.4.3" } } diff --git a/public/resume.pdf b/public/resume.pdf index f35c02daf53ac67630dad92f1cc1d5a7130fa84a..2be99f6feb9c97e0b5cd1a77f132201e0e46e236 100644 GIT binary patch literal 50918 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 PX7XI@&6|y*&Dj3~OI|JU delta 865 zcmZWnO>5Lp6lG>Qo#fSBDK5s=N1>q5Chy)P^Aenn;74^(5UVZ(OP$&bMA}4WGNN{o zg-a3J-mUlt+(>652qJFX3gSj^<4XMly6`0nO_R5JIq&4$bI-lspVfXn+HKPPQ%|RF z2}g{=;XVIO&=34g^$Fv-_POfbmlK-J1z+Xhl3tnJYc|QK9;Kg#Moz_=0V{i4W9Hc? zjQubka|4*Xf{koZe3?)5%GnUewp< zQX`=K$r0Vqx|Gys>2+h9HuMp_Ys}LRZJ8q0RT_)*SzkZW=`cUsj5h8vu&AdV->@fn z&>kmKIDgDh?ov{_&CC_x&`RB^D{_!@*)ZwP(wY2klD;m@(T;H;54!Ztm}z=hqbN%K zBINo#5M7!U;naVBNVl~P{T9!2k_h8Zu&e722Y#Okjz}NgibS?l(&^zLXW2yv*HQ(G zm{eX4V*qxE2Nq(9=h(7@Nh`yWd|~h`C=wAIHSNQN(JGrpN2;6)Jn6b6%=7XFRed1< zWXYkzyrLNZ*DIy~C?GGG=^>U91hmt&P0zoi0b5rwCm0WU!InSB7Ux><*jF=R=4uqh jJX?5XcEf(el`FMQYSzQJH;A)of+M8I78cH5Sz&(x3ozMF diff --git a/scripts/verify-ci.mjs b/scripts/verify-ci.mjs index 96bc786..a6d505e 100644 --- a/scripts/verify-ci.mjs +++ b/scripts/verify-ci.mjs @@ -6,6 +6,7 @@ const packageJson = JSON.parse(await readFile('package.json', 'utf8')); const packageLock = JSON.parse(await readFile('package-lock.json', 'utf8')); const nodeVersion = (await readFile('.node-version', 'utf8')).trim(); const dependabot = await readFile('.github/dependabot.yml', 'utf8'); +const liveSmoke = await readFile('scripts/verify-live.mjs', 'utf8'); assert.equal(nodeVersion, '22.23.1', '.node-version must pin the approved Node 22 release'); assert.equal(packageJson.engines?.node, nodeVersion, 'package engines.node must match .node-version'); @@ -18,30 +19,77 @@ const globalPermissions = workflow.slice(0, workflow.indexOf('\njobs:')); assert.match(globalPermissions, /permissions:\s*\n\s+contents: read/, 'default permissions must be contents: read'); assert.doesNotMatch(globalPermissions, /pages: write|id-token: write/, 'deployment permissions must not be global'); -for (const job of ['build', 'browser', 'seam', 'quality_gate', 'publish_pages_artifact', 'deploy']) { +for (const job of ['build', 'browser', 'seam', 'seam_macos', 'quality_gate', 'publish_pages_artifact', 'deploy']) { assert.match(workflow, new RegExp(`^ ${job}:`, 'm'), `workflow job missing: ${job}`); } assert.match(workflow, /name: Quality gate/, 'aggregate check name must remain stable'); +assert.match( + workflow, + /DIST_ARTIFACT: site-dist-\$\{\{ github\.sha \}\}-\$\{\{ github\.run_attempt \}\}/, + 'full workflow reruns need a unique immutable artifact name', +); +assert.match( + workflow, + /build:[\s\S]*outputs:[\s\S]*dist-artifact-id: \$\{\{ steps\.upload-dist\.outputs\.artifact-id \}\}[\s\S]*id: upload-dist[\s\S]*name: \$\{\{ env\.DIST_ARTIFACT \}\}/, + 'build must expose the uploaded immutable artifact id', +); +assert.match( + workflow, + /name: Review dependency changes[\s\S]*if: github\.event_name == 'pull_request'[\s\S]*uses: actions\/dependency-review-action@a1d282b36b6f3519aa1f3fc636f609c47dddb294[\s\S]*fail-on-severity: moderate[\s\S]*fail-on-scopes: runtime, development, unknown/, + 'pull requests must reject newly introduced moderate-or-higher vulnerable dependencies in every scope', +); for (const command of ['verify:browser', 'verify:seam']) { assert.match(workflow, new RegExp(`npm run ${command}`), `QA lane missing ${command}`); } +assert.match( + workflow, + /seam_macos:[\s\S]*runs-on: macos-15[\s\S]*PW_CHANNEL=chrome[\s\S]*npm run verify:seam/, + 'the exact seam contract must run in hosted macOS Chrome as well as Linux Chromium', +); +assert.match( + workflow, + /quality_gate:[\s\S]*- seam_macos[\s\S]*MACOS_SEAM_RESULT:[\s\S]*test "\$MACOS_SEAM_RESULT" = success/, + 'the stable quality gate must require the macOS seam lane', +); assert.doesNotMatch( workflow, /npm run verify:performance/, 'hardware-sensitive full-circuit performance evidence must remain outside hosted CI', ); -assert.ok((workflow.match(/node-version-file: \.node-version/g) ?? []).length >= 3, 'every Node job must use .node-version'); +assert.ok((workflow.match(/node-version-file: \.node-version/g) ?? []).length >= 4, 'every Node job must use .node-version'); assert.match(workflow, /uses: actions\/upload-artifact@[^\s#]+[^\n]*[\s\S]*path: dist/, 'build must upload the single dist artifact'); assert.match(workflow, /name: Verify built static artifact[\s\S]*npm run verify:dist/, 'build must verify the exact dist before upload'); assert.match(workflow, /VERIFY_LAYOUT=1 URL='http:\/\/127\.0\.0\.1:4173\/' npm run verify:dist/, 'downloaded dist needs an HTTP and mobile target-size smoke gate'); -assert.ok((workflow.match(/uses: actions\/download-artifact@/g) ?? []).length >= 3, 'QA and publish jobs must consume the built dist'); -assert.match(workflow, /publish_pages_artifact:[\s\S]*needs: quality_gate[\s\S]*github\.event_name != 'pull_request'/, 'Pages artifact must wait for non-PR quality success'); +assert.ok((workflow.match(/uses: actions\/download-artifact@/g) ?? []).length >= 5, 'QA, publish, and live smoke jobs must consume the built dist'); +assert.equal( + (workflow.match(/artifact-ids: \$\{\{ needs\.build\.outputs\.dist-artifact-id \}\}/g) ?? []).length, + 5, + 'every dist consumer must select the exact build artifact id', +); +assert.doesNotMatch(workflow, /overwrite:\s*true/, 'immutable release evidence must never be overwritten'); +assert.match(workflow, /publish_pages_artifact:[\s\S]*needs:[\s\S]*- build[\s\S]*- quality_gate[\s\S]*github\.event_name != 'pull_request'/, 'Pages artifact must wait for build and non-PR quality success'); +assert.match(workflow, /deploy:[\s\S]*needs:[\s\S]*- build[\s\S]*- publish_pages_artifact/, 'post-deploy parity must retain access to the exact build artifact id'); assert.equal( (workflow.match(/if: github\.event_name != 'pull_request' && github\.ref == 'refs\/heads\/main'/g) ?? []).length, 2, 'both Pages artifact publication and deployment must be restricted to main', ); assert.match(workflow, /deploy:[\s\S]*pages: write[\s\S]*id-token: write/, 'only deploy receives Pages permissions'); +assert.match( + workflow, + /deploy:[\s\S]*permissions:\s*\n\s+contents: read\s*\n\s+pages: write\s*\n\s+id-token: write/, + 'deploy must retain only checkout read access plus Pages write and OIDC permissions', +); +assert.match( + workflow, + /name: Deploy GitHub Pages[\s\S]*name: Checkout smoke test[\s\S]*name: Download immutable dist for live comparison[\s\S]*name: Verify live Pages origin/, + 'a successful Pages deployment must be followed by exact live-origin artifact verification', +); +assert.match( + workflow, + /DEPLOYMENT_URL: \$\{\{ steps\.deployment\.outputs\.page_url \}\}[\s\S]*run: timeout 150s node scripts\/verify-live\.mjs/, + 'live-origin verification must use the deployment output and a bounded outer timeout', +); assert.doesNotMatch(workflow, /^\s*uses:\s+[^#\n]+@[vV]\d/m, 'workflow actions must use immutable commit SHAs'); for (const actionSha of [ '9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0', @@ -50,6 +98,7 @@ for (const actionSha of [ '3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c', 'fc324d3547104276b827a68afc52ff2a11cc49c9', 'cd2ce8fcbc39b97be8ca5fce6e763baed58fa128', + 'a1d282b36b6f3519aa1f3fc636f609c47dddb294', ]) { assert.ok(workflow.includes(`@${actionSha}`), `workflow is missing approved action SHA ${actionSha}`); } @@ -59,5 +108,26 @@ for (const ecosystem of ['npm', 'github-actions']) { } assert.ok((dependabot.match(/interval: ["']weekly["']/g) ?? []).length >= 2, 'Dependabot must run weekly for both ecosystems'); assert.match(dependabot, /groups:/, 'Dependabot updates must be grouped'); +assert.match( + dependabot, + /dependency-name: ["']vite["'][\s\S]*versions:[\s\S]*- ["']8\.x["']/, + 'Dependabot must not reintroduce the proven Vite 8 macOS seam regression', +); + +for (const route of [ + "fetchHtml('/')", + "fetchHtml('/about/')", + "fetchHtml('/beijing-loop/')", + "fetchBytes('/social-preview.png'", + "fetchBytes('/profile-preview.png'", + "fetchBytes('/resume.pdf'", + "'/poe2/guides/classes-explained.html'", +]) { + assert.ok(liveSmoke.includes(route), `live smoke is missing required proof target ${route}`); +} +assert.match(liveSmoke, /AbortSignal\.timeout\(requestTimeoutMs\)/, 'live requests need bounded timeouts'); +assert.match(liveSmoke, /sha256\(bytes\)/, 'approved binary assets need byte-level verification'); +assert.match(liveSmoke, /verifyDistParity\(\)/, 'live smoke must compare every deployed file with the immutable dist'); +assert.match(liveSmoke, /deployed bytes differ from immutable dist/, 'live byte drift must fail the deployment job'); -console.log('CI contract verified: Node pin, PR-safe Pages topology, parallel QA, and grouped Dependabot.'); +console.log('CI contract verified: Node pin, dependency review, parallel QA, exact deployment parity, and grouped Dependabot.'); diff --git a/scripts/verify-dist.mjs b/scripts/verify-dist.mjs index 2a8b880..4688d91 100644 --- a/scripts/verify-dist.mjs +++ b/scripts/verify-dist.mjs @@ -1,13 +1,12 @@ import assert from 'node:assert/strict'; -import { createHash } from 'node:crypto'; import { readFile } from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; +import { assertAccessibleResume } from './verify-resume.mjs'; const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const DIST = path.join(ROOT, 'dist'); const HTTP_ORIGIN = process.env.URL; -const APPROVED_RESUME_SHA256 = '3a4ceeebef174745fa8117dafee31d5741eb63f23891b60f47e1c94ad9eeff7e'; async function read(relativePath) { return readFile(path.join(DIST, relativePath), 'utf8'); @@ -19,12 +18,6 @@ function assertPng(buffer, name) { assert.equal(buffer.readUInt32BE(20), 630, `${name}: height must be 630px`); } -function assertResume(buffer, name) { - assert.equal(buffer.subarray(0, 5).toString('ascii'), '%PDF-', `${name}: invalid PDF signature`); - assert.equal(createHash('sha256').update(buffer).digest('hex'), APPROVED_RESUME_SHA256, `${name}: unapproved resume revision`); - assert.ok(!buffer.toString('latin1').toLowerCase().includes('yupeng-dev'), `${name}: stale GitHub identity`); -} - const about = await read('about/index.html'); assert.match(about, /]*rel=["']canonical["'][^>]*href=["']https:\/\/brickerp\.github\.io\/about\/["']/i, 'dist about canonical'); assert.match(about, /(?:property=["']og:image["'][^>]*content|content)=["']https:\/\/brickerp\.github\.io\/profile-preview\.png["']/i, 'dist about profile image'); @@ -44,7 +37,7 @@ assert.match(legacyBeijingLoop, /]*href=["']\/["']/i, 'legacy Beijing loo for (const image of ['social-preview.png', 'profile-preview.png']) { assertPng(await readFile(path.join(DIST, image)), `dist/${image}`); } -assertResume(await readFile(path.join(DIST, 'resume.pdf')), 'dist/resume.pdf'); +assertAccessibleResume(await readFile(path.join(DIST, 'resume.pdf')), 'dist/resume.pdf'); if (HTTP_ORIGIN) { const cases = [ @@ -69,7 +62,7 @@ if (HTTP_ORIGIN) { assert.match(response.headers.get('content-type') ?? '', definition.type, `${definition.pathname}: wrong content type`); const bytes = Buffer.from(await response.arrayBuffer()); if (definition.png) assertPng(bytes, definition.pathname); - if (definition.resume) assertResume(bytes, definition.pathname); + if (definition.resume) assertAccessibleResume(bytes, definition.pathname); if (definition.body) assert.match(bytes.toString('utf8'), definition.body, `${definition.pathname}: response body contract`); } } diff --git a/scripts/verify-live.mjs b/scripts/verify-live.mjs new file mode 100644 index 0000000..c21c9f3 --- /dev/null +++ b/scripts/verify-live.mjs @@ -0,0 +1,209 @@ +import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; +import { readFile, readdir } from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); +const EXPECTED_ORIGIN = 'https://brickerp.github.io'; +const expectedLiveOrigin = process.env.LIVE_ORIGIN ?? EXPECTED_ORIGIN; +const deploymentUrl = process.env.DEPLOYMENT_URL ?? process.argv[2]; +const distDirectory = path.resolve(ROOT, process.env.DIST_DIR ?? 'dist'); +const requestTimeoutMs = positiveInteger('REQUEST_TIMEOUT_MS', 8_000); +const retryDelayMs = positiveInteger('RETRY_DELAY_MS', 5_000); +const maxAttempts = positiveInteger('MAX_ATTEMPTS', 12); +const deadlineMs = positiveInteger('SMOKE_DEADLINE_MS', 120_000); + +assert.ok(deploymentUrl, 'DEPLOYMENT_URL or a deployment URL argument is required'); +const baseUrl = new URL(deploymentUrl); +assert.equal(baseUrl.origin, expectedLiveOrigin, `unexpected Pages origin: ${baseUrl.origin}`); +assert.equal(baseUrl.pathname, '/', `unexpected Pages base path: ${baseUrl.pathname}`); + +function positiveInteger(name, defaultValue) { + const value = Number(process.env[name] ?? defaultValue); + assert.ok(Number.isSafeInteger(value) && value > 0, `${name} must be a positive integer`); + return value; +} + +function escapeRegExp(value) { + return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +function attribute(tag, name) { + return tag.match(new RegExp(`\\b${name}=(['\"])(.*?)\\1`, 'i'))?.[2]; +} + +function meta(html, key, value) { + const expected = new RegExp(`^${escapeRegExp(value)}$`, 'i'); + for (const tag of html.match(/]*>/gi) ?? []) { + if (expected.test(attribute(tag, key) ?? '')) return attribute(tag, 'content'); + } + return undefined; +} + +function canonical(html) { + for (const tag of html.match(/]*>/gi) ?? []) { + if ((attribute(tag, 'rel') ?? '').split(/\s+/).includes('canonical')) return attribute(tag, 'href'); + } + return undefined; +} + +function normalizedRobots(html) { + return meta(html, 'name', 'robots')?.toLowerCase().replace(/\s/g, ''); +} + +function sha256(bytes) { + return createHash('sha256').update(bytes).digest('hex'); +} + +async function distFiles(directory = distDirectory, prefix = '') { + const entries = await readdir(directory, { withFileTypes: true }); + const files = []; + for (const entry of entries) { + const relative = path.posix.join(prefix, entry.name); + if (entry.isDirectory()) { + files.push(...await distFiles(path.join(directory, entry.name), relative)); + } else if (entry.isFile()) { + files.push(relative); + } + } + return files.sort(); +} + +function livePath(relative) { + if (relative === 'index.html') return '/'; + if (relative.endsWith('/index.html')) return `/${relative.slice(0, -'index.html'.length)}`; + return `/${relative}`; +} + +function expectedContentType(relative) { + if (relative.endsWith('.html')) return /^text\/html\b/i; + if (relative.endsWith('.css')) return /^text\/css\b/i; + if (relative.endsWith('.js')) return /^(?:text|application)\/(?:javascript|ecmascript)\b/i; + if (relative.endsWith('.png')) return /^image\/png\b/i; + if (relative.endsWith('.pdf')) return /^application\/pdf\b/i; + if (relative.endsWith('.txt')) return /^text\/plain\b/i; + if (relative.endsWith('.xml')) return /^(?:application|text)\/xml\b/i; + throw new Error(`unsupported deployed file type: ${relative}`); +} + +async function expectedHash(relativePath) { + return sha256(await readFile(path.join(ROOT, 'public', relativePath))); +} + +const approvedHashes = new Map(await Promise.all( + ['social-preview.png', 'profile-preview.png', 'resume.pdf'].map(async (file) => [file, await expectedHash(file)]), +)); + +async function fetchBytes(pathname, contentType) { + const response = await fetch(new URL(pathname, baseUrl), { + redirect: 'manual', + headers: { + 'cache-control': 'no-cache', + pragma: 'no-cache', + 'user-agent': 'BrickerP-post-deploy-smoke/1.0', + }, + signal: AbortSignal.timeout(requestTimeoutMs), + }); + assert.equal(response.status, 200, `${pathname}: expected HTTP 200, got ${response.status}`); + assert.match(response.headers.get('content-type') ?? '', contentType, `${pathname}: wrong content type`); + const bytes = Buffer.from(await response.arrayBuffer()); + assert.ok(bytes.length > 0, `${pathname}: empty response body`); + return bytes; +} + +async function fetchHtml(pathname) { + return (await fetchBytes(pathname, /^text\/html\b/i)).toString('utf8'); +} + +async function verifyDistParity() { + const files = await distFiles(); + assert.ok(files.length > 0, `immutable dist is empty: ${distDirectory}`); + await Promise.all(files.map(async (relative) => { + const expected = await readFile(path.join(distDirectory, relative)); + const pathname = livePath(relative); + const live = await fetchBytes(pathname, expectedContentType(relative)); + assert.equal(live.length, expected.length, `${pathname}: deployed size differs from immutable dist`); + assert.equal(sha256(live), sha256(expected), `${pathname}: deployed bytes differ from immutable dist`); + })); + return files; +} + +function assertPage(html, pathname, expectedCanonical, expectedRobots) { + assert.equal(canonical(html), expectedCanonical, `${pathname}: wrong canonical`); + assert.equal(normalizedRobots(html), expectedRobots, `${pathname}: wrong robots directive`); +} + +function criticalAssets(rootHtml) { + const references = []; + for (const tag of rootHtml.match(/<(?:script|link)\b[^>]*>/gi) ?? []) { + const isScript = tag.toLowerCase().startsWith(' asset.endsWith('.js')), '/: no critical JavaScript asset found'); + await Promise.all(assets.map((asset) => fetchBytes( + asset, + asset.endsWith('.css') ? /^text\/css\b/i : /^(?:text|application)\/(?:javascript|ecmascript)\b/i, + ))); + return { assets, deployedFiles }; +} + +const startedAt = Date.now(); +let lastError; +for (let attempt = 1; attempt <= maxAttempts && Date.now() - startedAt < deadlineMs; attempt += 1) { + try { + const { assets, deployedFiles } = await verifyAttempt(); + console.log(`Live Pages smoke passed on attempt ${attempt}: ${baseUrl.href}`); + console.log(`Exact immutable-dist parity: ${deployedFiles.length} files`); + console.log(`Critical assets: ${assets.join(', ')}`); + process.exit(0); + } catch (error) { + lastError = error; + const elapsedMs = Date.now() - startedAt; + console.error(`Attempt ${attempt}/${maxAttempts} failed after ${elapsedMs}ms: ${error.message}`); + if (attempt < maxAttempts && elapsedMs + retryDelayMs < deadlineMs) { + await new Promise((resolve) => setTimeout(resolve, retryDelayMs)); + } + } +} + +throw new Error(`Live Pages smoke failed within ${deadlineMs}ms`, { cause: lastError }); diff --git a/scripts/verify-resume.mjs b/scripts/verify-resume.mjs new file mode 100644 index 0000000..afd63bf --- /dev/null +++ b/scripts/verify-resume.mjs @@ -0,0 +1,49 @@ +import assert from 'node:assert/strict'; +import { createHash } from 'node:crypto'; + +export const APPROVED_RESUME_SHA256 = 'de68cbb3d943e7ed0533c0fc9c7bbad3385943257109f77bfcb805d8ff713524'; + +function countMatches(text, pattern) { + return text.match(pattern)?.length ?? 0; +} + +export function assertAccessibleResume(buffer, name) { + assert.equal(buffer.subarray(0, 5).toString('ascii'), '%PDF-', `${name}: invalid PDF signature`); + assert.equal(buffer.length, 50_918, `${name}: unexpected tagged artifact size`); + assert.equal(createHash('sha256').update(buffer).digest('hex'), APPROVED_RESUME_SHA256, `${name}: unapproved resume revision`); + + const pdf = buffer.toString('latin1'); + assert.match(pdf, /%%EOF\s*$/, `${name}: missing final PDF end marker`); + assert.equal(countMatches(pdf, /\/Type\s+\/Page\b/g), 2, `${name}: expected two pages`); + assert.ok(!pdf.toLowerCase().includes('yupeng-dev'), `${name}: stale GitHub identity`); + assert.match(pdf, /\/StructTreeRoot\b/, `${name}: missing tagged-PDF structure tree`); + assert.match(pdf, /\/MarkInfo\s*<<[\s\S]*?\/Marked\s+true[\s\S]*?>>/, `${name}: PDF is not marked as tagged`); + assert.match(pdf, /\/Lang\s*\(en\\055US\)/, `${name}: document language must be en-US`); + assert.match(pdf, /\/Title\s*\(Yupeng Lu \\055 AI Agent Engineer\)/, `${name}: accessible document title is missing`); + assert.match(pdf, /\/Author\s*\(Yupeng Lu\)/, `${name}: document author is missing`); + assert.match(pdf, /\/Metadata\s+\d+\s+0\s+R\b/, `${name}: XMP metadata stream is missing`); + assert.match(pdf, /\/ViewerPreferences\s*<<[\s\S]*?\/DisplayDocTitle\s+true[\s\S]*?>>/, `${name}: title display preference is missing`); + assert.equal(countMatches(pdf, /\/Tabs\s+\/S\b/g), 2, `${name}: both pages must use structural tab order`); + assert.equal(countMatches(pdf, /\/StructParents\s+[01]\b/g), 2, `${name}: both pages need structure-parent indices`); + assert.match(pdf, /\/ParentTreeNextKey\s+13\b/, `${name}: parent tree must cover pages and link annotations`); + assert.equal(countMatches(pdf, /\/S\s+\/Document\b/g), 1, `${name}: expected one document structure root`); + assert.equal(countMatches(pdf, /\/S\s+\/Sect\b/g), 10, `${name}: expected ten semantic sections`); + assert.equal(countMatches(pdf, /\/S\s+\/P\b/g), 12, `${name}: expected 12 paragraphs`); + assert.equal(countMatches(pdf, /\/S\s+\/H1\b/g), 1, `${name}: expected one H1`); + assert.equal(countMatches(pdf, /\/S\s+\/H2\b/g), 4, `${name}: expected four H2 headings`); + assert.equal(countMatches(pdf, /\/S\s+\/H3\b/g), 6, `${name}: expected six H3 headings`); + assert.equal(countMatches(pdf, /\/S\s+\/L\b/g), 5, `${name}: expected five semantic lists`); + assert.equal(countMatches(pdf, /\/S\s+\/LI\b/g), 22, `${name}: expected 22 list items`); + assert.equal(countMatches(pdf, /\/S\s+\/LBody\b/g), 22, `${name}: every list item needs a list body`); + assert.equal(countMatches(pdf, /\/S\s+\/Link\b/g), 11, `${name}: expected 11 tagged links`); + assert.equal(countMatches(pdf, /\/StructParent\s+\d+\b/g), 11, `${name}: every link annotation needs a structure parent`); + assert.equal(countMatches(pdf, /\/Type\s+\/OBJR\b/g), 11, `${name}: every tagged link needs an object reference`); + assert.equal(countMatches(pdf, /\/Contents\s*\([^\r\n]*\)/g), 11, `${name}: every link needs an accessible description`); + assert.ok(countMatches(pdf, /\/ToUnicode\s+\d+\s+0\s+R\b/g) >= 2, `${name}: fonts need Unicode maps`); + assert.match(pdf, /\/Outlines\s+\d+\s+0\s+R\b/, `${name}: section bookmarks are missing`); + assert.doesNotMatch( + pdf, + /\/(?:JavaScript|JS|OpenAction|AA|Launch|EmbeddedFile|AcroForm|Encrypt)\b/, + `${name}: active, embedded, form, or encrypted content is forbidden`, + ); +} diff --git a/scripts/verify-static.mjs b/scripts/verify-static.mjs index b87bb36..4c6b0d9 100644 --- a/scripts/verify-static.mjs +++ b/scripts/verify-static.mjs @@ -1,15 +1,14 @@ import assert from 'node:assert/strict'; -import { createHash } from 'node:crypto'; import { access, readFile, readdir } from 'node:fs/promises'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { documentFor, redirectPaths } from './generate-poe2-redirects.mjs'; import { assertAboutIsGenerated, escapeHtml, readPublicProfile } from './generate-about.mjs'; +import { assertAccessibleResume } from './verify-resume.mjs'; const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); const SITE_ORIGIN = 'https://brickerp.github.io'; const POE2_TARGET = `${SITE_ORIGIN}/poe2-build-lab/`; -const APPROVED_RESUME_SHA256 = '3a4ceeebef174745fa8117dafee31d5741eb63f23891b60f47e1c94ad9eeff7e'; async function text(relativePath) { return readFile(path.join(ROOT, relativePath), 'utf8'); @@ -246,8 +245,6 @@ for (const previewName of ['social-preview.png', 'profile-preview.png']) { } const resume = await readFile(path.join(ROOT, 'public', 'resume.pdf')); -assert.equal(resume.subarray(0, 5).toString('ascii'), '%PDF-', 'public/resume.pdf must be a PDF'); -assert.equal(createHash('sha256').update(resume).digest('hex'), APPROVED_RESUME_SHA256, 'public/resume.pdf must match the visually approved revision'); -assert.ok(!resume.toString('latin1').toLowerCase().includes('yupeng-dev'), 'public/resume.pdf must not retain the stale GitHub identity'); +assertAccessibleResume(resume, 'public/resume.pdf'); console.log(`Static integrity verified: root, about, sitemap, and ${redirectPaths.length} PoE2 redirects.`); From 6a36292b3e7001bbb0dd3857b8d11d4913510626 Mon Sep 17 00:00:00 2001 From: brickerP Date: Mon, 20 Jul 2026 02:50:58 +0800 Subject: [PATCH 2/2] chore: refresh protected analysis