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 f35c02d..2be99f6 100644 Binary files a/public/resume.pdf and b/public/resume.pdf differ 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.`);