diff --git a/content/public/asset-recipes.json b/content/public/asset-recipes.json
new file mode 100644
index 0000000..0b76f60
--- /dev/null
+++ b/content/public/asset-recipes.json
@@ -0,0 +1,16 @@
+{
+ "schema": "portfolio-asset-recipes/v1",
+ "assets": [
+ { "id": "medication-mobile-logs", "kind": "image", "output": "project-medication-logs.webp", "maxWidth": 720, "rightsHolder": "Luca Pîslaru", "license": "All rights reserved", "attribution": "Luca Pîslaru" },
+ { "id": "medication-mobile-data-settings", "kind": "image", "output": "project-medication-data-settings.webp", "maxWidth": 720, "rightsHolder": "Luca Pîslaru", "license": "All rights reserved", "attribution": "Luca Pîslaru" },
+ { "id": "medication-mobile-notification-settings", "kind": "image", "output": "project-medication-notification-settings.webp", "maxWidth": 720, "rightsHolder": "Luca Pîslaru", "license": "All rights reserved", "attribution": "Luca Pîslaru" },
+ { "id": "footy-detection-frame", "kind": "image", "output": "project-footy-detection.webp", "maxWidth": 1600, "rightsHolder": "Third-party footage and Luca Pîslaru", "license": "Publication requires confirmation", "attribution": "Detection overlay by Luca Pîslaru" },
+ { "id": "opinion-polarization-chart", "kind": "image", "output": "project-opinion-polarization.webp", "maxWidth": 1000, "rightsHolder": "Luca Pîslaru", "license": "All rights reserved", "attribution": "Luca Pîslaru" },
+ { "id": "opinion-cascade-thresholds-chart", "kind": "image", "output": "project-opinion-cascade-thresholds.webp", "maxWidth": 1000, "rightsHolder": "Luca Pîslaru", "license": "All rights reserved", "attribution": "Luca Pîslaru" },
+ { "id": "opinion-average-degree-chart", "kind": "image", "output": "project-opinion-average-degree.webp", "maxWidth": 1000, "rightsHolder": "Luca Pîslaru", "license": "All rights reserved", "attribution": "Luca Pîslaru" },
+ { "id": "opinion-revolt-frequency-chart", "kind": "image", "output": "project-opinion-revolt-frequency.webp", "maxWidth": 1000, "rightsHolder": "Luca Pîslaru", "license": "All rights reserved", "attribution": "Luca Pîslaru" },
+ { "id": "opinion-final-distribution-chart", "kind": "image", "output": "project-opinion-final-distribution.webp", "maxWidth": 1000, "rightsHolder": "Luca Pîslaru", "license": "All rights reserved", "attribution": "Luca Pîslaru" },
+ { "id": "private-game-loop", "kind": "video", "output": "project-private-game-loop.webm", "maxWidth": 1280, "startSeconds": 3, "durationSeconds": 5, "rightsHolder": "Rights holder confirmation pending", "license": "Publication requires confirmation", "attribution": "Prototype contribution by Luca Pîslaru" },
+ { "id": "currency-converter-interface", "kind": "image", "output": "project-currency-converter.webp", "maxWidth": 1440, "rightsHolder": "Luca Pîslaru", "license": "All rights reserved", "attribution": "Luca Pîslaru" }
+ ]
+}
diff --git a/docs/asset-clearance-template.md b/docs/asset-clearance-template.md
new file mode 100644
index 0000000..bbe4f64
--- /dev/null
+++ b/docs/asset-clearance-template.md
@@ -0,0 +1,35 @@
+# Private asset clearance register template
+
+This template is for a private, untracked approval record. Do not place completed approvals, private source paths, original filenames, private product names, account details, or health data in the repository.
+
+## Required preparation evidence
+
+- Preparation date and reviewer.
+- Generic asset ID.
+- Candidate derivative filename and SHA-256 hash.
+- Rights holder and display permission confirmed.
+- Final derivative reviewed at readable size.
+- OCR, barcode/QR, byte, container, and metadata checks passed.
+- No browser chrome, account data, real health data, private branding, or identifying title frames.
+- Video gameplay range and every materially changing frame reviewed.
+- Health-related UI contains only fictional/demo information.
+
+## Approval file shape
+
+The private approval JSON must match `portfolio-asset-approvals/v1`. It is copied from the generated private `candidate-approval.json` only after visual review. Any later byte change invalidates approval because the SHA-256 hash changes.
+
+```json
+{
+ "schema": "portfolio-asset-approvals/v1",
+ "assets": [
+ {
+ "id": "generic-asset-id",
+ "files": [
+ { "fileName": "project-generic-output.webp", "sha256": "64-lowercase-hex-characters" }
+ ]
+ }
+ ]
+}
+```
+
+The preparation command stages candidates and creates a private contact sheet. It publishes nothing until this exact hash-bound register is supplied. The public manifest and prepared asset directory must remain empty while approval is pending.
diff --git a/package-lock.json b/package-lock.json
index 58ed816..fae08c8 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -19,9 +19,14 @@
"@vitejs/plugin-vue": "^6.0.8",
"@vue/server-renderer": "^3.5.42",
"@vue/test-utils": "^2.5.0",
+ "@zxing/library": "^0.21.3",
"eslint": "^10.9.1",
+ "file-type": "^22.0.2",
"happy-dom": "^20.12.0",
+ "lighthouse": "^13.4.1",
"prettier": "^3.9.6",
+ "sharp": "^0.35.4",
+ "tesseract.js": "^7.0.0",
"typescript": "^5.9.3",
"vite": "^8.2.2",
"vitest": "^4.1.11",
@@ -87,6 +92,28 @@
"node": ">=6.9.0"
}
},
+ "node_modules/@borewit/text-codec": {
+ "version": "0.2.2",
+ "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz",
+ "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Borewit"
+ }
+ },
+ "node_modules/@emnapi/runtime": {
+ "version": "1.11.3",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
+ "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "dependencies": {
+ "tslib": "^2.4.0"
+ }
+ },
"node_modules/@eslint-community/eslint-utils": {
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz",
@@ -194,6 +221,62 @@
"node": "^20.19.0 || ^22.13.0 || >=24"
}
},
+ "node_modules/@formatjs/ecma402-abstract": {
+ "version": "2.3.6",
+ "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.6.tgz",
+ "integrity": "sha512-HJnTFeRM2kVFVr5gr5kH1XP6K0JcJtE7Lzvtr3FS/so5f1kpsqqqxy5JF+FRaO6H2qmcMfAUIox7AJteieRtVw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@formatjs/fast-memoize": "2.2.7",
+ "@formatjs/intl-localematcher": "0.6.2",
+ "decimal.js": "^10.4.3",
+ "tslib": "^2.8.0"
+ }
+ },
+ "node_modules/@formatjs/fast-memoize": {
+ "version": "2.2.7",
+ "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.7.tgz",
+ "integrity": "sha512-Yabmi9nSvyOMrlSeGGWDiH7rf3a7sIwplbvo/dlz9WCIjzIQAfy1RMf4S0X3yG724n5Ghu2GmEl5NJIV6O9sZQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.8.0"
+ }
+ },
+ "node_modules/@formatjs/icu-messageformat-parser": {
+ "version": "2.11.4",
+ "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.4.tgz",
+ "integrity": "sha512-7kR78cRrPNB4fjGFZg3Rmj5aah8rQj9KPzuLsmcSn4ipLXQvC04keycTI1F7kJYDwIXtT2+7IDEto842CfZBtw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@formatjs/ecma402-abstract": "2.3.6",
+ "@formatjs/icu-skeleton-parser": "1.8.16",
+ "tslib": "^2.8.0"
+ }
+ },
+ "node_modules/@formatjs/icu-skeleton-parser": {
+ "version": "1.8.16",
+ "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.16.tgz",
+ "integrity": "sha512-H13E9Xl+PxBd8D5/6TVUluSpxGNvFSlN/b3coUp0e0JpuWXXnQDiavIpY3NnvSp4xhEMoXyyBvVfdFX8jglOHQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@formatjs/ecma402-abstract": "2.3.6",
+ "tslib": "^2.8.0"
+ }
+ },
+ "node_modules/@formatjs/intl-localematcher": {
+ "version": "0.6.2",
+ "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.6.2.tgz",
+ "integrity": "sha512-XOMO2Hupl0wdd172Y06h6kLpBz6Dv+J4okPLl4LPtzbr8f66WbIoy4ev98EBuZ6ZK4h5ydTN6XneT4QVpD7cdA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tslib": "^2.8.0"
+ }
+ },
"node_modules/@humanfs/core": {
"version": "0.19.2",
"resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz",
@@ -260,6 +343,533 @@
"url": "https://github.com/sponsors/nzakas"
}
},
+ "node_modules/@img/colour": {
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
+ "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@img/sharp-darwin-arm64": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz",
+ "integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-arm64": "1.3.3"
+ }
+ },
+ "node_modules/@img/sharp-darwin-x64": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz",
+ "integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-darwin-x64": "1.3.3"
+ }
+ },
+ "node_modules/@img/sharp-freebsd-wasm32": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz",
+ "integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "dependencies": {
+ "@img/sharp-wasm32": "0.35.4"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-darwin-arm64": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz",
+ "integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-darwin-x64": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz",
+ "integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-arm": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz",
+ "integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-arm64": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz",
+ "integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-ppc64": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz",
+ "integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-riscv64": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz",
+ "integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-s390x": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz",
+ "integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linux-x64": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz",
+ "integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linuxmusl-arm64": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz",
+ "integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-libvips-linuxmusl-x64": {
+ "version": "1.3.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz",
+ "integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-linux-arm": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz",
+ "integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm": "1.3.3"
+ }
+ },
+ "node_modules/@img/sharp-linux-arm64": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz",
+ "integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-arm64": "1.3.3"
+ }
+ },
+ "node_modules/@img/sharp-linux-ppc64": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz",
+ "integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-ppc64": "1.3.3"
+ }
+ },
+ "node_modules/@img/sharp-linux-riscv64": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz",
+ "integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==",
+ "cpu": [
+ "riscv64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-riscv64": "1.3.3"
+ }
+ },
+ "node_modules/@img/sharp-linux-s390x": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz",
+ "integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-s390x": "1.3.3"
+ }
+ },
+ "node_modules/@img/sharp-linux-x64": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz",
+ "integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linux-x64": "1.3.3"
+ }
+ },
+ "node_modules/@img/sharp-linuxmusl-arm64": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz",
+ "integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-arm64": "1.3.3"
+ }
+ },
+ "node_modules/@img/sharp-linuxmusl-x64": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz",
+ "integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-libvips-linuxmusl-x64": "1.3.3"
+ }
+ },
+ "node_modules/@img/sharp-wasm32": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz",
+ "integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==",
+ "dev": true,
+ "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/runtime": "^1.11.3"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-webcontainers-wasm32": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz",
+ "integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==",
+ "cpu": [
+ "wasm32"
+ ],
+ "dev": true,
+ "license": "Apache-2.0",
+ "optional": true,
+ "dependencies": {
+ "@img/sharp-wasm32": "0.35.4"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-arm64": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz",
+ "integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-ia32": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz",
+ "integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==",
+ "cpu": [
+ "ia32"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-win32-x64": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz",
+ "integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "Apache-2.0 AND LGPL-3.0-or-later",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz",
@@ -273,6 +883,127 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@opentelemetry/api": {
+ "version": "1.9.1",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz",
+ "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/@opentelemetry/api-logs": {
+ "version": "0.220.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.220.0.tgz",
+ "integrity": "sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ },
+ "engines": {
+ "node": ">=8.0.0"
+ }
+ },
+ "node_modules/@opentelemetry/core": {
+ "version": "2.11.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.11.0.tgz",
+ "integrity": "sha512-7YP44XH0tV6+Mb54x2YGf84i7yi+31MBZlE8JwvozkxyTvXbSp10X7cI7YE49ChJ3shMJoBmCJF3+1QFBJctGA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.0.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/instrumentation": {
+ "version": "0.220.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/instrumentation/-/instrumentation-0.220.0.tgz",
+ "integrity": "sha512-xQx3E2WxP1mDvKzxLxX+CTCtNLa560YJZ3087qYHerl2YmiKpv7AH+dAy7vmx+eVrZ5BwhfWUAVoKOoxCNHcpw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/api-logs": "0.220.0",
+ "import-in-the-middle": "^3.0.0",
+ "require-in-the-middle": "^8.0.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.3.0"
+ }
+ },
+ "node_modules/@opentelemetry/resources": {
+ "version": "2.11.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/resources/-/resources-2.11.0.tgz",
+ "integrity": "sha512-Ie7+8q8MDF4FAEQCKVMTx3ReUvxiIAgIiiW3c9JdmP8+HMcDy20puT+AHjexnExgnbvBxjQ9fjkFDWrikJ2jQA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.11.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.3.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-trace": {
+ "version": "2.11.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace/-/sdk-trace-2.11.0.tgz",
+ "integrity": "sha512-fFnTqGm8/G73GQVnxYi7LXa1ZVYEUvgL6XI1LpvV0bPC7WQ/ZGgKxCSl8FnlZBKto9JHHEFTO6s6CUpvvtwFrA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.11.0",
+ "@opentelemetry/resources": "2.11.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.3.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/sdk-trace-base": {
+ "version": "2.11.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/sdk-trace-base/-/sdk-trace-base-2.11.0.tgz",
+ "integrity": "sha512-H19x/TX/LZdqiYOjM7fqtSxwlplC5pgelavqbQdHbhdq0q/AI/TGkM2dfGuuynTXmJPeF2HoZVoPDu+TGoW78A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@opentelemetry/core": "2.11.0",
+ "@opentelemetry/resources": "2.11.0",
+ "@opentelemetry/sdk-trace": "2.11.0",
+ "@opentelemetry/semantic-conventions": "^1.29.0"
+ },
+ "engines": {
+ "node": "^18.19.0 || >=20.6.0"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": ">=1.3.0 <1.10.0"
+ }
+ },
+ "node_modules/@opentelemetry/semantic-conventions": {
+ "version": "1.43.0",
+ "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.43.0.tgz",
+ "integrity": "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=14"
+ }
+ },
"node_modules/@oxc-project/types": {
"version": "0.147.0",
"resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.147.0.tgz",
@@ -283,6 +1014,17 @@
"url": "https://github.com/sponsors/Boshen"
}
},
+ "node_modules/@paulirish/trace_engine": {
+ "version": "0.0.65",
+ "resolved": "https://registry.npmjs.org/@paulirish/trace_engine/-/trace_engine-0.0.65.tgz",
+ "integrity": "sha512-Qsm6F5C8xf6ZzQXbQc2+wcpe6sggfs/gvc/ytqSurdvYg3kyW0ECHCqE0CWBKZpqgjVfPNX9c7SCS3r2nEIRGg==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "legacy-javascript": "latest",
+ "third-party-web": "latest"
+ }
+ },
"node_modules/@playwright/test": {
"version": "1.62.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.62.1.tgz",
@@ -299,6 +1041,198 @@
"node": ">=20"
}
},
+ "node_modules/@puppeteer/browsers": {
+ "version": "3.2.1",
+ "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-3.2.1.tgz",
+ "integrity": "sha512-KDz+3qDRdBAlRlMjmKyj6dEs33YHTk/xRHEENSXq6TNnhgoU15ruSHtEBeVF6OZ9tBDY55Se4P0nFMNsipzU9A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "modern-tar": "^0.8.0",
+ "yargs": "^18.0.0"
+ },
+ "bin": {
+ "browsers": "lib/main-cli.js"
+ },
+ "engines": {
+ "node": ">=22.12.0"
+ },
+ "peerDependencies": {
+ "proxy-agent": ">=8.0.1",
+ "yauzl": "^2.10.0 || ^3.4.0"
+ },
+ "peerDependenciesMeta": {
+ "proxy-agent": {
+ "optional": true
+ },
+ "yauzl": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@puppeteer/browsers/node_modules/ansi-regex": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.3.0.tgz",
+ "integrity": "sha512-WpDfL7NO6j7tH88IDBNVdUJxDh9nmCteAVW9dsep846XdwF4naCBK+/tGLX3KJgcpgMRXCFlTM2hKGoK9FsdrQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-regex?sponsor=1"
+ }
+ },
+ "node_modules/@puppeteer/browsers/node_modules/ansi-styles": {
+ "version": "6.2.3",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
+ "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
+ "node_modules/@puppeteer/browsers/node_modules/cliui": {
+ "version": "9.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz",
+ "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^7.2.0",
+ "strip-ansi": "^7.1.0",
+ "wrap-ansi": "^9.0.0"
+ },
+ "engines": {
+ "node": ">=20"
+ }
+ },
+ "node_modules/@puppeteer/browsers/node_modules/cliui/node_modules/string-width": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
+ "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^10.3.0",
+ "get-east-asian-width": "^1.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@puppeteer/browsers/node_modules/emoji-regex": {
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz",
+ "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@puppeteer/browsers/node_modules/string-width": {
+ "version": "8.2.2",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-8.2.2.tgz",
+ "integrity": "sha512-GaPUh5gfdrYzqeVNZvUfT23vYYxXzKYidUcnMtJg/3rxRV63EFZy3k6xfKlmfeJD0176lnUV/Usr3XcwSvFzpg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "get-east-asian-width": "^1.5.0",
+ "strip-ansi": "^7.1.2"
+ },
+ "engines": {
+ "node": ">=20"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@puppeteer/browsers/node_modules/strip-ansi": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz",
+ "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^6.2.2"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/strip-ansi?sponsor=1"
+ }
+ },
+ "node_modules/@puppeteer/browsers/node_modules/wrap-ansi": {
+ "version": "9.0.2",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.2.tgz",
+ "integrity": "sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^6.2.1",
+ "string-width": "^7.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
+ "node_modules/@puppeteer/browsers/node_modules/wrap-ansi/node_modules/string-width": {
+ "version": "7.2.0",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz",
+ "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^10.3.0",
+ "get-east-asian-width": "^1.0.0",
+ "strip-ansi": "^7.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/@puppeteer/browsers/node_modules/yargs": {
+ "version": "18.1.0",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.1.0.tgz",
+ "integrity": "sha512-2rAgRKu54VsHkqI0/tYkmluGXHD4KW7yZoycuqDQ15QOTnc2VVfy0nN/1eMhnQLO00A+dwtK20xuCnc1YGeUyg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^9.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "string-width": "^8.2.1",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^22.0.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=23"
+ }
+ },
+ "node_modules/@puppeteer/browsers/node_modules/yargs-parser": {
+ "version": "22.0.0",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz",
+ "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "^20.19.0 || ^22.12.0 || >=23"
+ }
+ },
"node_modules/@rolldown/binding-android-arm-eabi": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.6.tgz",
@@ -561,6 +1495,123 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@sentry/conventions": {
+ "version": "0.16.0",
+ "resolved": "https://registry.npmjs.org/@sentry/conventions/-/conventions-0.16.0.tgz",
+ "integrity": "sha512-fO9PLmHdVURcSPUpWCItWAtgKiMwGdJHbovoSEyLplX5sxs2ugvI4CBPTrkkgqhObnZOD0CnWBKDzSVQYBKEyQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14"
+ }
+ },
+ "node_modules/@sentry/core": {
+ "version": "10.73.0",
+ "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.73.0.tgz",
+ "integrity": "sha512-FLO1UgH19RyasVpofu612WCOgb2nEH0dZy+R72d7p65XU9i0wxlMKm3+sgfwKmiSJp1Qhilaaxs4Jg6BbiM5HA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sentry/conventions": "^0.16.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@sentry/node": {
+ "version": "10.73.0",
+ "resolved": "https://registry.npmjs.org/@sentry/node/-/node-10.73.0.tgz",
+ "integrity": "sha512-jiMJ6GgXDw6UMGzJY+o0c8OoeA9OfHqZ/xEpHfDqy75hn+9CEkRkbNGCIdMvsc7wW/Se1Os394hHfTpU+YEskg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@opentelemetry/api": "^1.9.1",
+ "@opentelemetry/instrumentation": "^0.220.0",
+ "@opentelemetry/sdk-trace-base": "^2.9.0",
+ "@sentry/conventions": "^0.16.0",
+ "@sentry/core": "10.73.0",
+ "@sentry/node-core": "10.73.0",
+ "@sentry/opentelemetry": "10.73.0",
+ "@sentry/server-utils": "10.73.0",
+ "import-in-the-middle": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/@sentry/node-core": {
+ "version": "10.73.0",
+ "resolved": "https://registry.npmjs.org/@sentry/node-core/-/node-core-10.73.0.tgz",
+ "integrity": "sha512-GHAGUmZPmm6FKfxfv2maVVJ/A99YAmd7oFOuKMDmITI6O/Msl6JB3vPTEpopVAHLW0LYJQBOjddL9C7o+Jt44g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sentry/conventions": "^0.16.0",
+ "@sentry/core": "10.73.0",
+ "@sentry/opentelemetry": "10.73.0",
+ "import-in-the-middle": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.9.0",
+ "@opentelemetry/core": "^1.30.1 || ^2.1.0",
+ "@opentelemetry/exporter-trace-otlp-http": ">=0.57.0 <1",
+ "@opentelemetry/instrumentation": ">=0.57.1 <1",
+ "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0"
+ },
+ "peerDependenciesMeta": {
+ "@opentelemetry/api": {
+ "optional": true
+ },
+ "@opentelemetry/core": {
+ "optional": true
+ },
+ "@opentelemetry/exporter-trace-otlp-http": {
+ "optional": true
+ },
+ "@opentelemetry/instrumentation": {
+ "optional": true
+ },
+ "@opentelemetry/sdk-trace-base": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@sentry/opentelemetry": {
+ "version": "10.73.0",
+ "resolved": "https://registry.npmjs.org/@sentry/opentelemetry/-/opentelemetry-10.73.0.tgz",
+ "integrity": "sha512-fQouPQKsH0CQrw6oAn1k0Z2I+tgyochCovifr5qNS69i0OzjknLa03WJyiZ/IuzXc4AVa5jAKfOeE9slABz8Qw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sentry/conventions": "^0.16.0",
+ "@sentry/core": "10.73.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "peerDependencies": {
+ "@opentelemetry/api": "^1.9.0",
+ "@opentelemetry/core": "^1.30.1 || ^2.1.0",
+ "@opentelemetry/sdk-trace-base": "^1.30.1 || ^2.1.0"
+ }
+ },
+ "node_modules/@sentry/server-utils": {
+ "version": "10.73.0",
+ "resolved": "https://registry.npmjs.org/@sentry/server-utils/-/server-utils-10.73.0.tgz",
+ "integrity": "sha512-QskripdKFbM/+gipC6mpa2crLwL7+VbkX84IpHg2z9UlYQ1kNKd3aMT+Qk9NLRSw/zu1rSIAvlbWfx4D3rgNAA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@sentry/conventions": "^0.16.0",
+ "@sentry/core": "10.73.0"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/@standard-schema/spec": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
@@ -568,6 +1619,31 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/@tokenizer/inflate": {
+ "version": "0.4.1",
+ "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz",
+ "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.4.3",
+ "token-types": "^6.1.1"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Borewit"
+ }
+ },
+ "node_modules/@tokenizer/token": {
+ "version": "0.3.0",
+ "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz",
+ "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/@types/chai": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz",
@@ -938,6 +2014,30 @@
}
}
},
+ "node_modules/@zxing/library": {
+ "version": "0.21.3",
+ "resolved": "https://registry.npmjs.org/@zxing/library/-/library-0.21.3.tgz",
+ "integrity": "sha512-hZHqFe2JyH/ZxviJZosZjV+2s6EDSY0O24R+FQmlWZBZXP9IqMo7S3nb3+2LBWxodJQkSurdQGnqE7KXqrYgow==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ts-custom-error": "^3.2.1"
+ },
+ "engines": {
+ "node": ">= 10.4.0"
+ },
+ "optionalDependencies": {
+ "@zxing/text-encoding": "~0.9.0"
+ }
+ },
+ "node_modules/@zxing/text-encoding": {
+ "version": "0.9.0",
+ "resolved": "https://registry.npmjs.org/@zxing/text-encoding/-/text-encoding-0.9.0.tgz",
+ "integrity": "sha512-U/4aVJ2mxI0aDNI8Uq0wEhMgY+u4CNtEb0om3+y3+niDAsoTCOB33UF0sxpzqzdqXLqmvc+vZyAt4O8pPdfkwA==",
+ "dev": true,
+ "license": "(Unlicense OR Apache-2.0)",
+ "optional": true
+ },
"node_modules/abbrev": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-5.0.0.tgz",
@@ -995,6 +2095,42 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/ansi-colors": {
+ "version": "4.1.3",
+ "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz",
+ "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
+ "node_modules/ansi-regex": {
+ "version": "5.0.1",
+ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
+ "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/ansi-styles": {
+ "version": "4.3.0",
+ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
+ "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-convert": "^2.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/ansi-styles?sponsor=1"
+ }
+ },
"node_modules/assertion-error": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz",
@@ -1005,6 +2141,17 @@
"node": ">=12"
}
},
+ "node_modules/atomically": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/atomically/-/atomically-2.1.1.tgz",
+ "integrity": "sha512-P4w9o2dqARji6P7MHprklbfiArZAWvo07yW7qs3pdljb3BWr12FIB7W+p0zJiuiVsUpRO0iZn1kFFcpPegg0tQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "stubborn-fs": "^2.0.0",
+ "when-exit": "^2.1.4"
+ }
+ },
"node_modules/axe-core": {
"version": "4.13.0",
"resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.13.0.tgz",
@@ -1025,6 +2172,13 @@
"node": "18 || 20 || >=22"
}
},
+ "node_modules/bmp-js": {
+ "version": "0.1.0",
+ "resolved": "https://registry.npmjs.org/bmp-js/-/bmp-js-0.1.0.tgz",
+ "integrity": "sha512-vHdS19CnY3hwiNdkaqk93DvjVLfbEcI8mys4UjuWrlX1haDmroo8o4xCzh4wD6DGV6HxRCyauwhHRqMTfERtjw==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/brace-expansion": {
"version": "5.0.9",
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz",
@@ -1061,6 +2215,94 @@
"node": ">=18"
}
},
+ "node_modules/chrome-launcher": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-1.2.1.tgz",
+ "integrity": "sha512-qmFR5PLMzHyuNJHwOloHPAHhbaNglkfeV/xDtt5b7xiFFyU1I+AZZX0PYseMuhenJSSirgxELYIbswcoc+5H4A==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@types/node": "*",
+ "escape-string-regexp": "^4.0.0",
+ "is-wsl": "^2.2.0",
+ "lighthouse-logger": "^2.0.1"
+ },
+ "bin": {
+ "print-chrome-path": "bin/print-chrome-path.cjs"
+ },
+ "engines": {
+ "node": ">=12.13.0"
+ }
+ },
+ "node_modules/chromium-bidi": {
+ "version": "17.0.2",
+ "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-17.0.2.tgz",
+ "integrity": "sha512-5v9GQFhTktFvotn/OFNJBmKLKRAb6n9r0bVCwf7sHgWc3/JryK0bj1nn93L3pHFrfgcsu6Be6EWsDi+1XHTGDg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "mitt": "^3.0.1",
+ "zod": "^3.24.1"
+ },
+ "engines": {
+ "node": ">=20.19.0 <22.0.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "devtools-protocol": "*"
+ }
+ },
+ "node_modules/chromium-bidi/node_modules/zod": {
+ "version": "3.25.76",
+ "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz",
+ "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/colinhacks"
+ }
+ },
+ "node_modules/cjs-module-lexer": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-2.2.1.tgz",
+ "integrity": "sha512-Ca8swihM+/4yKecYHY52kgJd300hi2lADU/a1RxNTRe+RJ9jvqQlESpbz9DnG9mowez8qwXHB8qYdIUw9e+F5Q==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/cliui": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz",
+ "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==",
+ "dev": true,
+ "license": "ISC",
+ "dependencies": {
+ "string-width": "^4.2.0",
+ "strip-ansi": "^6.0.1",
+ "wrap-ansi": "^7.0.0"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/color-convert": {
+ "version": "2.0.1",
+ "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
+ "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "color-name": "~1.1.4"
+ },
+ "engines": {
+ "node": ">=7.0.0"
+ }
+ },
+ "node_modules/color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/commander": {
"version": "14.0.3",
"resolved": "https://registry.npmjs.org/commander/-/commander-14.0.3.tgz",
@@ -1082,6 +2324,25 @@
"proto-list": "~1.2.1"
}
},
+ "node_modules/configstore": {
+ "version": "7.1.0",
+ "resolved": "https://registry.npmjs.org/configstore/-/configstore-7.1.0.tgz",
+ "integrity": "sha512-N4oog6YJWbR9kGyXvS7jEykLDXIE2C0ILYqNBZBp9iwiJpoCBWYsuAdW6PPFn6w06jjnC+3JstVvWHO4cZqvRg==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "atomically": "^2.0.3",
+ "dot-prop": "^9.0.0",
+ "graceful-fs": "^4.2.11",
+ "xdg-basedir": "^5.1.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/convert-source-map": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
@@ -1104,6 +2365,13 @@
"node": ">= 8"
}
},
+ "node_modules/csp_evaluator": {
+ "version": "1.1.8",
+ "resolved": "https://registry.npmjs.org/csp_evaluator/-/csp_evaluator-1.1.8.tgz",
+ "integrity": "sha512-EwOnfYuNbTytvbMKsLixTrRgnjOa0WZCxGy8A9nnSYAicrdwn+T/epU/yjgymmOxlgKnvH+8wXt+7p/8ak5Feg==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
"node_modules/csstype": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
@@ -1128,6 +2396,13 @@
}
}
},
+ "node_modules/decimal.js": {
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
+ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/deep-is": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz",
@@ -1135,6 +2410,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/define-lazy-prop": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz",
+ "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/detect-libc": {
"version": "2.1.2",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
@@ -1145,6 +2430,29 @@
"node": ">=8"
}
},
+ "node_modules/devtools-protocol": {
+ "version": "0.0.1663043",
+ "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1663043.tgz",
+ "integrity": "sha512-33aOY3ZnBP1dgZsshgaL+/XlsQleiFZgyUaDtdZkEa1nbZhVY1MoDeWjk+wxg25fU924l1ZJfoGNmjjeA/5s1w==",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/dot-prop": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-9.0.0.tgz",
+ "integrity": "sha512-1gxPBJpI/pcjQhKgIU91II6Wkay+dLcN3M6rf2uwP8hRur3HtQXjVrdAK3sjC0piaEuxzMwjXChcETiJl47lAQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "type-fest": "^4.18.2"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/editorconfig": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-3.0.2.tgz",
@@ -1164,6 +2472,27 @@
"node": ">=20"
}
},
+ "node_modules/emoji-regex": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
+ "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/enquirer": {
+ "version": "2.4.1",
+ "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz",
+ "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-colors": "^4.1.1",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8.6"
+ }
+ },
"node_modules/entities": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz",
@@ -1183,6 +2512,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/escalade": {
+ "version": "3.2.0",
+ "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
+ "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6"
+ }
+ },
"node_modules/escape-string-regexp": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
@@ -1419,6 +2758,25 @@
"node": ">=16.0.0"
}
},
+ "node_modules/file-type": {
+ "version": "22.0.2",
+ "resolved": "https://registry.npmjs.org/file-type/-/file-type-22.0.2.tgz",
+ "integrity": "sha512-0H8TsCUGBLx+V5adH3EY52hTAcyLKbV1D4gq5cIOJ6DnQAHeV9Z2Hhuc5CoBX4YmvB2oL+JIC84z0qO7JsCoNw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@tokenizer/inflate": "^0.4.1",
+ "strtok3": "^10.3.5",
+ "token-types": "^6.1.2",
+ "uint8array-extras": "^1.5.0"
+ },
+ "engines": {
+ "node": ">=22"
+ },
+ "funding": {
+ "url": "https://github.com/sindresorhus/file-type?sponsor=1"
+ }
+ },
"node_modules/find-up": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
@@ -1472,6 +2830,29 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
+ "node_modules/get-caller-file": {
+ "version": "2.0.5",
+ "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
+ "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": "6.* || 8.* || >= 10.*"
+ }
+ },
+ "node_modules/get-east-asian-width": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz",
+ "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/glob": {
"version": "13.0.6",
"resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
@@ -1503,6 +2884,13 @@
"node": ">=10.13.0"
}
},
+ "node_modules/graceful-fs": {
+ "version": "4.2.11",
+ "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz",
+ "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
+ "dev": true,
+ "license": "ISC"
+ },
"node_modules/happy-dom": {
"version": "20.12.0",
"resolved": "https://registry.npmjs.org/happy-dom/-/happy-dom-20.12.0.tgz",
@@ -1522,6 +2910,44 @@
"node": ">=20.0.0"
}
},
+ "node_modules/http-link-header": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/http-link-header/-/http-link-header-1.1.4.tgz",
+ "integrity": "sha512-xT3GPW6/ZbGuw4UvwHqErSCEjNUlwbQJuZn9/q5U4WEKfp2kENVCAlousG1zLxHeaQ/ffOHUNpWamvkbBW0eNw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/idb-keyval": {
+ "version": "6.3.0",
+ "resolved": "https://registry.npmjs.org/idb-keyval/-/idb-keyval-6.3.0.tgz",
+ "integrity": "sha512-um+2dgAWmYsu615EXpWVwSmapJhON0G43t3Ka/EVaohzPQXSMqKEqeDK/oIW3Ow+BXaF2PvSc+oBTFp793A5Ow==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
"node_modules/ignore": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz",
@@ -1532,6 +2958,28 @@
"node": ">= 4"
}
},
+ "node_modules/image-ssim": {
+ "version": "0.2.0",
+ "resolved": "https://registry.npmjs.org/image-ssim/-/image-ssim-0.2.0.tgz",
+ "integrity": "sha512-W7+sO6/yhxy83L0G7xR8YAc5Z5QFtYEXXRV6EaE8tuYBZJnA3gVgp3q7X7muhLZVodeb9UfvjSbwt9VJwjIYAg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/import-in-the-middle": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/import-in-the-middle/-/import-in-the-middle-3.4.0.tgz",
+ "integrity": "sha512-Xfjwfarhe+LGmoaof+sexeNo3sGRysb5x56WrZLwHtmqT0OilSKtVWkv0lrCcMgSKyKP9oBogBAisHZvtJH0aw==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "cjs-module-lexer": "^2.2.0",
+ "es-module-lexer": "^2.2.0",
+ "module-details-from-path": "^1.0.4"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
"node_modules/imurmurhash": {
"version": "0.1.4",
"resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz",
@@ -1549,6 +2997,35 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/intl-messageformat": {
+ "version": "10.7.18",
+ "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.18.tgz",
+ "integrity": "sha512-m3Ofv/X/tV8Y3tHXLohcuVuhWKo7BBq62cqY15etqmLxg2DZ34AGGgQDeR+SCta2+zICb1NX83af0GJmbQ1++g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "dependencies": {
+ "@formatjs/ecma402-abstract": "2.3.6",
+ "@formatjs/fast-memoize": "2.2.7",
+ "@formatjs/icu-messageformat-parser": "2.11.4",
+ "tslib": "^2.8.0"
+ }
+ },
+ "node_modules/is-docker": {
+ "version": "2.2.1",
+ "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz",
+ "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "is-docker": "cli.js"
+ },
+ "engines": {
+ "node": ">=8"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/is-extglob": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
@@ -1559,6 +3036,16 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-fullwidth-code-point": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
+ "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/is-glob": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
@@ -1572,6 +3059,26 @@
"node": ">=0.10.0"
}
},
+ "node_modules/is-url": {
+ "version": "1.2.4",
+ "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz",
+ "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/is-wsl": {
+ "version": "2.2.0",
+ "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz",
+ "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "is-docker": "^2.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
"node_modules/isexe": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -1579,6 +3086,13 @@
"dev": true,
"license": "ISC"
},
+ "node_modules/jpeg-js": {
+ "version": "0.4.4",
+ "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz",
+ "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
"node_modules/js-beautify": {
"version": "2.0.3",
"resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-2.0.3.tgz",
@@ -1608,6 +3122,16 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/js-library-detector": {
+ "version": "6.7.0",
+ "resolved": "https://registry.npmjs.org/js-library-detector/-/js-library-detector-6.7.0.tgz",
+ "integrity": "sha512-c80Qupofp43y4cJ7+8TTDN/AsDwLi5oOm/plBrWI+iQt485vKXCco+yVmOwEgdo9VOdsYTuV0UlTeetVPTriXA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/json-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
@@ -1639,6 +3163,13 @@
"json-buffer": "3.0.1"
}
},
+ "node_modules/legacy-javascript": {
+ "version": "0.0.1",
+ "resolved": "https://registry.npmjs.org/legacy-javascript/-/legacy-javascript-0.0.1.tgz",
+ "integrity": "sha512-lPyntS4/aS7jpuvOlitZDFifBCb4W8L/3QU0PLbUTUj+zYah8rfVjYic88yG7ZKTxhS5h9iz7duT8oUXKszLhg==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
"node_modules/levn": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz",
@@ -1653,6 +3184,89 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/lighthouse": {
+ "version": "13.4.1",
+ "resolved": "https://registry.npmjs.org/lighthouse/-/lighthouse-13.4.1.tgz",
+ "integrity": "sha512-fDu8lt3QLK/lTqIxtp1HkzQNJ32rsFHhbadYOepcMZFLgA8oINhxutMbMv8XXnpTOvZ0TXCo4JCk1LDTWaRLnA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@paulirish/trace_engine": "0.0.65",
+ "@sentry/node": "^10.0.0",
+ "axe-core": "^4.12.1",
+ "chrome-launcher": "^1.2.1",
+ "configstore": "^7.0.0",
+ "csp_evaluator": "1.1.8",
+ "devtools-protocol": "0.0.1663043",
+ "enquirer": "^2.3.6",
+ "http-link-header": "^1.1.1",
+ "intl-messageformat": "^10.5.3",
+ "jpeg-js": "^0.4.4",
+ "js-library-detector": "^6.7.0",
+ "lighthouse-logger": "^2.0.2",
+ "lighthouse-stack-packs": "1.12.3",
+ "lodash-es": "^4.17.21",
+ "lookup-closest-locale": "6.2.0",
+ "open": "^8.4.0",
+ "puppeteer-core": "^25.3.0",
+ "robots-parser": "^3.0.1",
+ "speedline-core": "^1.4.3",
+ "third-party-web": "^0.29.2",
+ "tldts-icann": "^7.4.9",
+ "web-features": "^3.34.0",
+ "ws": "^7.0.0",
+ "yargs": "^17.3.1",
+ "yargs-parser": "^21.0.0"
+ },
+ "bin": {
+ "chrome-debug": "core/scripts/manual-chrome-launcher.js",
+ "lighthouse": "cli/index.js",
+ "smokehouse": "cli/test/smokehouse/frontends/smokehouse-bin.js"
+ },
+ "engines": {
+ "node": ">=22.19"
+ }
+ },
+ "node_modules/lighthouse-logger": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-2.0.2.tgz",
+ "integrity": "sha512-vWl2+u5jgOQuZR55Z1WM0XDdrJT6mzMP8zHUct7xTlWhuQs+eV0g+QL0RQdFjT54zVmbhLCP8vIVpy1wGn/gCg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "debug": "^4.4.1",
+ "marky": "^1.2.2"
+ }
+ },
+ "node_modules/lighthouse-stack-packs": {
+ "version": "1.12.3",
+ "resolved": "https://registry.npmjs.org/lighthouse-stack-packs/-/lighthouse-stack-packs-1.12.3.tgz",
+ "integrity": "sha512-d8IsOpE83kbANgnM+Tp8+x6HcMpX9o2ITBiUERssgzAIFdZCQzs/f4k6D0DLQTE59enml9mbAOU52Wu35exWtg==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/lighthouse/node_modules/ws": {
+ "version": "7.5.13",
+ "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.13.tgz",
+ "integrity": "sha512-rsKI6xDBFVf4r/x8XyChGK04QR/XHroxs/jUcoWvtEZM8TPU/X/uIY9B1CsSzYws9ZJb/6bbBu7dPhFW00CAoA==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=8.3.0"
+ },
+ "peerDependencies": {
+ "bufferutil": "^4.0.1",
+ "utf-8-validate": "^5.0.2"
+ },
+ "peerDependenciesMeta": {
+ "bufferutil": {
+ "optional": true
+ },
+ "utf-8-validate": {
+ "optional": true
+ }
+ }
+ },
"node_modules/lightningcss": {
"version": "1.33.0",
"resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
@@ -1930,6 +3544,20 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/lodash-es": {
+ "version": "4.18.1",
+ "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.18.1.tgz",
+ "integrity": "sha512-J8xewKD/Gk22OZbhpOVSwcs60zhd95ESDwezOFuA3/099925PdHJ7OFHNTGtajL3AlZkykD32HykiMo+BIBI8A==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/lookup-closest-locale": {
+ "version": "6.2.0",
+ "resolved": "https://registry.npmjs.org/lookup-closest-locale/-/lookup-closest-locale-6.2.0.tgz",
+ "integrity": "sha512-/c2kL+Vnp1jnV6K6RpDTHK3dgg0Tu2VVp+elEiJpjfS1UyY7AjOYHohRug6wT0OpoX2qFgNORndE9RqesfVxWQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/lru-cache": {
"version": "11.5.2",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
@@ -1949,6 +3577,13 @@
"@jridgewell/sourcemap-codec": "^1.5.5"
}
},
+ "node_modules/marky": {
+ "version": "1.3.0",
+ "resolved": "https://registry.npmjs.org/marky/-/marky-1.3.0.tgz",
+ "integrity": "sha512-ocnPZQLNpvbedwTy9kNrQEsknEfgvcLMvOtz3sFeWApDq1MXH1TqkCIx58xlpESsfwQOnuBO9beyQuNGzVvuhQ==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
"node_modules/minimatch": {
"version": "10.2.6",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.6.tgz",
@@ -1975,6 +3610,30 @@
"node": ">=16 || 14 >=14.17"
}
},
+ "node_modules/mitt": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz",
+ "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/modern-tar": {
+ "version": "0.8.4",
+ "resolved": "https://registry.npmjs.org/modern-tar/-/modern-tar-0.8.4.tgz",
+ "integrity": "sha512-gN54ddmyzEg10orwZ2u4OOv+bjpMWdIl5jIkodK97bMq8QBSL5c0D7YX0lT1Ooz+99S7+PvFbnxzdjgHo1r41g==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18.0.0"
+ }
+ },
+ "node_modules/module-details-from-path": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/module-details-from-path/-/module-details-from-path-1.0.4.tgz",
+ "integrity": "sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
@@ -2014,6 +3673,27 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/node-fetch": {
+ "version": "2.7.0",
+ "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
+ "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "whatwg-url": "^5.0.0"
+ },
+ "engines": {
+ "node": "4.x || >=6.0.0"
+ },
+ "peerDependencies": {
+ "encoding": "^0.1.0"
+ },
+ "peerDependenciesMeta": {
+ "encoding": {
+ "optional": true
+ }
+ }
+ },
"node_modules/nopt": {
"version": "10.0.1",
"resolved": "https://registry.npmjs.org/nopt/-/nopt-10.0.1.tgz",
@@ -2044,6 +3724,34 @@
"node": ">=12.20.0"
}
},
+ "node_modules/open": {
+ "version": "8.4.2",
+ "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz",
+ "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "define-lazy-prop": "^2.0.0",
+ "is-docker": "^2.1.1",
+ "is-wsl": "^2.2.0"
+ },
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/opencollective-postinstall": {
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/opencollective-postinstall/-/opencollective-postinstall-2.0.3.tgz",
+ "integrity": "sha512-8AV/sCtuzUeTo8gQK5qDZzARrulB3egtLzFgteqB2tcT4Mw7B8Kt7JcDHmltjz6FOAHsvTevk70gZEbhM4ZS9Q==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "opencollective-postinstall": "index.js"
+ }
+ },
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -2267,6 +3975,72 @@
"node": ">=6"
}
},
+ "node_modules/puppeteer-core": {
+ "version": "25.9.0",
+ "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-25.9.0.tgz",
+ "integrity": "sha512-U61rCwSMha62CA/Opy6tCx2Fx+ck7ouiKnbpEApzSoLYMoEu9F71nuFpHL55vmIt33/GYm6eKZVhH2ev0nAIeg==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@puppeteer/browsers": "3.2.1",
+ "chromium-bidi": "17.0.2",
+ "devtools-protocol": "0.0.1666840",
+ "typed-query-selector": "^2.12.2",
+ "webdriver-bidi-protocol": "0.4.2",
+ "ws": "^8.21.3"
+ },
+ "engines": {
+ "node": ">=22.12.0"
+ }
+ },
+ "node_modules/puppeteer-core/node_modules/devtools-protocol": {
+ "version": "0.0.1666840",
+ "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1666840.tgz",
+ "integrity": "sha512-gCcO42XCHKEs7Ag0S7aGYsnJ7hlgrO3qderYqeiY0Eqk+0GFfuvT13IA0hHreJTa2KCdDVyGMeOhdMNmrrTjVg==",
+ "dev": true,
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/regenerator-runtime": {
+ "version": "0.13.11",
+ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
+ "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/require-directory": {
+ "version": "2.1.1",
+ "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
+ "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/require-in-the-middle": {
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/require-in-the-middle/-/require-in-the-middle-8.0.1.tgz",
+ "integrity": "sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "debug": "^4.3.5",
+ "module-details-from-path": "^1.0.3"
+ },
+ "engines": {
+ "node": ">=9.3.0 || >=8.10.0 <9.0.0"
+ }
+ },
+ "node_modules/robots-parser": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/robots-parser/-/robots-parser-3.0.1.tgz",
+ "integrity": "sha512-s+pyvQeIKIZ0dx5iJiQk1tPLJAWln39+MI5jtM8wnyws+G5azk+dMnMX0qfbqNetKKNgcWWOdi0sfm+FbQbgdQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=10.0.0"
+ }
+ },
"node_modules/rolldown": {
"version": "1.2.6",
"resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.6.tgz",
@@ -2314,6 +4088,56 @@
"node": ">=10"
}
},
+ "node_modules/sharp": {
+ "version": "0.35.4",
+ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz",
+ "integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@img/colour": "^1.1.0",
+ "detect-libc": "^2.1.2",
+ "semver": "^7.8.5"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ },
+ "optionalDependencies": {
+ "@img/sharp-darwin-arm64": "0.35.4",
+ "@img/sharp-darwin-x64": "0.35.4",
+ "@img/sharp-freebsd-wasm32": "0.35.4",
+ "@img/sharp-libvips-darwin-arm64": "1.3.3",
+ "@img/sharp-libvips-darwin-x64": "1.3.3",
+ "@img/sharp-libvips-linux-arm": "1.3.3",
+ "@img/sharp-libvips-linux-arm64": "1.3.3",
+ "@img/sharp-libvips-linux-ppc64": "1.3.3",
+ "@img/sharp-libvips-linux-riscv64": "1.3.3",
+ "@img/sharp-libvips-linux-s390x": "1.3.3",
+ "@img/sharp-libvips-linux-x64": "1.3.3",
+ "@img/sharp-libvips-linuxmusl-arm64": "1.3.3",
+ "@img/sharp-libvips-linuxmusl-x64": "1.3.3",
+ "@img/sharp-linux-arm": "0.35.4",
+ "@img/sharp-linux-arm64": "0.35.4",
+ "@img/sharp-linux-ppc64": "0.35.4",
+ "@img/sharp-linux-riscv64": "0.35.4",
+ "@img/sharp-linux-s390x": "0.35.4",
+ "@img/sharp-linux-x64": "0.35.4",
+ "@img/sharp-linuxmusl-arm64": "0.35.4",
+ "@img/sharp-linuxmusl-x64": "0.35.4",
+ "@img/sharp-webcontainers-wasm32": "0.35.4",
+ "@img/sharp-win32-arm64": "0.35.4",
+ "@img/sharp-win32-ia32": "0.35.4",
+ "@img/sharp-win32-x64": "0.35.4"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
+ }
+ },
"node_modules/shebang-command": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -2353,6 +4177,21 @@
"node": ">=0.10.0"
}
},
+ "node_modules/speedline-core": {
+ "version": "1.4.3",
+ "resolved": "https://registry.npmjs.org/speedline-core/-/speedline-core-1.4.3.tgz",
+ "integrity": "sha512-DI7/OuAUD+GMpR6dmu8lliO2Wg5zfeh+/xsdyJZCzd8o5JgFUjCeLsBDuZjIQJdwXS3J0L/uZYrELKYqx+PXog==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*",
+ "image-ssim": "^0.2.0",
+ "jpeg-js": "^0.4.1"
+ },
+ "engines": {
+ "node": ">=8.0"
+ }
+ },
"node_modules/stackback": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz",
@@ -2367,6 +4206,101 @@
"dev": true,
"license": "MIT"
},
+ "node_modules/string-width": {
+ "version": "4.2.3",
+ "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
+ "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "emoji-regex": "^8.0.0",
+ "is-fullwidth-code-point": "^3.0.0",
+ "strip-ansi": "^6.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strip-ansi": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
+ "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-regex": "^5.0.1"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/strtok3": {
+ "version": "10.3.5",
+ "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz",
+ "integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@tokenizer/token": "^0.3.0"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Borewit"
+ }
+ },
+ "node_modules/stubborn-fs": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/stubborn-fs/-/stubborn-fs-2.0.0.tgz",
+ "integrity": "sha512-Y0AvSwDw8y+nlSNFXMm2g6L51rBGdAQT20J3YSOqxC53Lo3bjWRtr2BKcfYoAf352WYpsZSTURrA0tqhfgudPA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "stubborn-utils": "^1.0.1"
+ }
+ },
+ "node_modules/stubborn-utils": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/stubborn-utils/-/stubborn-utils-1.0.2.tgz",
+ "integrity": "sha512-zOh9jPYI+xrNOyisSelgym4tolKTJCQd5GBhK0+0xJvcYDcwlOoxF/rnFKQ2KRZknXSG9jWAp66fwP6AxN9STg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tesseract.js": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/tesseract.js/-/tesseract.js-7.0.0.tgz",
+ "integrity": "sha512-exPBkd+z+wM1BuMkx/Bjv43OeLBxhL5kKWsz/9JY+DXcXdiBjiAch0V49QR3oAJqCaL5qURE0vx9Eo+G5YE7mA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "bmp-js": "^0.1.0",
+ "idb-keyval": "^6.2.0",
+ "is-url": "^1.2.4",
+ "node-fetch": "^2.6.9",
+ "opencollective-postinstall": "^2.0.3",
+ "regenerator-runtime": "^0.13.3",
+ "tesseract.js-core": "^7.0.0",
+ "wasm-feature-detect": "^1.8.0",
+ "zlibjs": "^0.3.1"
+ }
+ },
+ "node_modules/tesseract.js-core": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/tesseract.js-core/-/tesseract.js-core-7.0.0.tgz",
+ "integrity": "sha512-WnNH518NzmbSq9zgTPeoF8c+xmilS8rFIl1YKbk/ptuuc7p6cLNELNuPAzcmsYw450ca6bLa8j3t0VAtq435Vw==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/third-party-web": {
+ "version": "0.29.2",
+ "resolved": "https://registry.npmjs.org/third-party-web/-/third-party-web-0.29.2.tgz",
+ "integrity": "sha512-fegtha91tq2DHphyoiBXVHjVi2YG9zFaRnboT9C28tO1en9Y3wJsfspuy40F+u5wl3hHVbw7cnd1b67kEGHb8g==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/three": {
"version": "0.185.1",
"resolved": "https://registry.npmjs.org/three/-/three-0.185.1.tgz",
@@ -2417,6 +4351,66 @@
"node": ">=14.0.0"
}
},
+ "node_modules/tldts-core": {
+ "version": "7.4.11",
+ "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-7.4.11.tgz",
+ "integrity": "sha512-CW3WN2rIIE/Of21mulhgnGOwoDyEFNygyIBOONSdyAuSATgMMUCpLeUlB+E8sAwA5xRV9hYPl+kyZ9citHCaKg==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/tldts-icann": {
+ "version": "7.4.11",
+ "resolved": "https://registry.npmjs.org/tldts-icann/-/tldts-icann-7.4.11.tgz",
+ "integrity": "sha512-1p+NDJ7FUYCliESmsQl9EW5Um8JIyFheiy6Y6ZoHho27d+TLAGFa0gMf1VOVy02vNY1aQo6xeKiaGj45+7P+PA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tldts-core": "^7.4.11"
+ }
+ },
+ "node_modules/token-types": {
+ "version": "6.1.2",
+ "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz",
+ "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@borewit/text-codec": "^0.2.1",
+ "@tokenizer/token": "^0.3.0",
+ "ieee754": "^1.2.1"
+ },
+ "engines": {
+ "node": ">=14.16"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/Borewit"
+ }
+ },
+ "node_modules/tr46": {
+ "version": "0.0.3",
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
+ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/ts-custom-error": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/ts-custom-error/-/ts-custom-error-3.3.1.tgz",
+ "integrity": "sha512-5OX1tzOjxWEgsr/YEUWSuPrQ00deKLh6D7OTWcvNHm12/7QPyRh8SYpyWvA4IZv8H/+GQWQEh/kwo95Q9OVW1A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=14.0.0"
+ }
+ },
+ "node_modules/tslib": {
+ "version": "2.8.1",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
+ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
+ "dev": true,
+ "license": "0BSD"
+ },
"node_modules/type-check": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -2430,6 +4424,26 @@
"node": ">= 0.8.0"
}
},
+ "node_modules/type-fest": {
+ "version": "4.41.0",
+ "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz",
+ "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==",
+ "dev": true,
+ "license": "(MIT OR CC0-1.0)",
+ "engines": {
+ "node": ">=16"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/typed-query-selector": {
+ "version": "2.12.2",
+ "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.2.tgz",
+ "integrity": "sha512-EOPFbyIub4ngnEdqi2yOcNeDLaX/0jcE1JoAXQDDMIthap7FoN795lc/SHfIq2d416VufXpM8z/lD+WRm2gfOQ==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/typescript": {
"version": "5.9.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
@@ -2444,6 +4458,19 @@
"node": ">=14.17"
}
},
+ "node_modules/uint8array-extras": {
+ "version": "1.5.0",
+ "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz",
+ "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=18"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
"node_modules/undici-types": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz",
@@ -2696,6 +4723,34 @@
"typescript": ">=5.0.0"
}
},
+ "node_modules/wasm-feature-detect": {
+ "version": "1.9.0",
+ "resolved": "https://registry.npmjs.org/wasm-feature-detect/-/wasm-feature-detect-1.9.0.tgz",
+ "integrity": "sha512-zonE+xlIIYtxPy++L24ow0hAD8CICb4+FgPyROd3buyXIqsJvUEDkBgfCCoXOd1Hu3DUr0GOfnPIdcGV+YpNaA==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/web-features": {
+ "version": "3.36.0",
+ "resolved": "https://registry.npmjs.org/web-features/-/web-features-3.36.0.tgz",
+ "integrity": "sha512-B5W9HbyXT76soRG7Fn3AELZkuDQpmpSxszSmJZLIPUHYxIGkufNz0vcoEhfqnUF1HAfaducWIBaaKuusq9Am2g==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/webdriver-bidi-protocol": {
+ "version": "0.4.2",
+ "resolved": "https://registry.npmjs.org/webdriver-bidi-protocol/-/webdriver-bidi-protocol-0.4.2.tgz",
+ "integrity": "sha512-VSV+fzfChirL3e7jay2yUC7B4HQCGtEWEg/MSSQbK+qWbqeGlRLlXTzPpYr3XGUvbpDHumWZBJxgesg4N7dbtA==",
+ "dev": true,
+ "license": "Apache-2.0"
+ },
+ "node_modules/webidl-conversions": {
+ "version": "3.0.1",
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
+ "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
+ "dev": true,
+ "license": "BSD-2-Clause"
+ },
"node_modules/whatwg-mimetype": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-3.0.0.tgz",
@@ -2706,6 +4761,24 @@
"node": ">=12"
}
},
+ "node_modules/whatwg-url": {
+ "version": "5.0.0",
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
+ "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "tr46": "~0.0.3",
+ "webidl-conversions": "^3.0.0"
+ }
+ },
+ "node_modules/when-exit": {
+ "version": "2.1.5",
+ "resolved": "https://registry.npmjs.org/when-exit/-/when-exit-2.1.5.tgz",
+ "integrity": "sha512-VGkKJ564kzt6Ms1dbgPP/yuIoQCrsFAnRbptpC5wOEsDaNsbCB2bnfnaA8i/vRs5tjUSEOtIuvl9/MyVsvQZCg==",
+ "dev": true,
+ "license": "MIT"
+ },
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -2749,6 +4822,24 @@
"node": ">=0.10.0"
}
},
+ "node_modules/wrap-ansi": {
+ "version": "7.0.0",
+ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
+ "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "ansi-styles": "^4.0.0",
+ "string-width": "^4.1.0",
+ "strip-ansi": "^6.0.0"
+ },
+ "engines": {
+ "node": ">=10"
+ },
+ "funding": {
+ "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
+ }
+ },
"node_modules/ws": {
"version": "8.21.3",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",
@@ -2771,6 +4862,58 @@
}
}
},
+ "node_modules/xdg-basedir": {
+ "version": "5.1.0",
+ "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-5.1.0.tgz",
+ "integrity": "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/sindresorhus"
+ }
+ },
+ "node_modules/y18n": {
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
+ "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=10"
+ }
+ },
+ "node_modules/yargs": {
+ "version": "17.7.3",
+ "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.3.tgz",
+ "integrity": "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "cliui": "^8.0.1",
+ "escalade": "^3.1.1",
+ "get-caller-file": "^2.0.5",
+ "require-directory": "^2.1.1",
+ "string-width": "^4.2.3",
+ "y18n": "^5.0.5",
+ "yargs-parser": "^21.1.1"
+ },
+ "engines": {
+ "node": ">=12"
+ }
+ },
+ "node_modules/yargs-parser": {
+ "version": "21.1.1",
+ "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
+ "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
+ "dev": true,
+ "license": "ISC",
+ "engines": {
+ "node": ">=12"
+ }
+ },
"node_modules/yocto-queue": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz",
@@ -2784,6 +4927,16 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/zlibjs": {
+ "version": "0.3.1",
+ "resolved": "https://registry.npmjs.org/zlibjs/-/zlibjs-0.3.1.tgz",
+ "integrity": "sha512-+J9RrgTKOmlxFSDHo0pI1xM6BLVUv+o0ZT9ANtCxGkjIVCCUdx9alUF8Gm+dGLKbkkkidWIHFDZHDMpfITt4+w==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ }
+ },
"node_modules/zod": {
"version": "4.5.4",
"resolved": "https://registry.npmjs.org/zod/-/zod-4.5.4.tgz",
diff --git a/package.json b/package.json
index 7bae8fe..0c9b70f 100644
--- a/package.json
+++ b/package.json
@@ -15,6 +15,8 @@
"build:ssr": "vite build --ssr src/entry-server.ts --outDir dist-ssr",
"prerender": "node scripts/prerender.mjs",
"verify:artifacts": "node scripts/verify-artifacts.mjs",
+ "prepare:assets": "node scripts/prepare-assets.mjs",
+ "verify:assets": "node scripts/prepare-assets.mjs --verify",
"prepare:preview-assets": "node scripts/materialize-empty-asset-set.mjs",
"build:preview": "npm run prepare:preview-assets && npm run typecheck && npm run test:unit && npm run build:client && npm run build:ssr && npm run prerender -- --config config/site.preview.json && npm run verify:artifacts -- --config config/site.preview.json"
},
@@ -32,9 +34,14 @@
"@vitejs/plugin-vue": "^6.0.8",
"@vue/server-renderer": "^3.5.42",
"@vue/test-utils": "^2.5.0",
+ "@zxing/library": "^0.21.3",
"eslint": "^10.9.1",
+ "file-type": "^22.0.2",
"happy-dom": "^20.12.0",
+ "lighthouse": "^13.4.1",
"prettier": "^3.9.6",
+ "sharp": "^0.35.4",
+ "tesseract.js": "^7.0.0",
"typescript": "^5.9.3",
"vite": "^8.2.2",
"vitest": "^4.1.11",
diff --git a/scripts/lib/asset-recipes.mjs b/scripts/lib/asset-recipes.mjs
new file mode 100644
index 0000000..b87da7c
--- /dev/null
+++ b/scripts/lib/asset-recipes.mjs
@@ -0,0 +1,91 @@
+import { readFile, realpath, stat } from 'node:fs/promises';
+import { basename, extname, isAbsolute, relative, resolve, sep } from 'node:path';
+
+const idPattern = /^[a-z][a-z0-9-]{0,63}$/u;
+const outputPattern = /^project-[a-z0-9][a-z0-9-]*\.(webp|png|webm)$/u;
+
+const fail = (rule) => { throw new Error(rule); };
+
+const exactKeys = (record, allowed, rule) => {
+ if (Object.keys(record).some((key) => !allowed.includes(key))) fail(rule);
+};
+
+export const readJson = async (path, rule = 'ASSET-JSON-001') => {
+ try {
+ return JSON.parse(await readFile(path, 'utf8'));
+ } catch {
+ fail(rule);
+ }
+};
+
+export const loadRecipes = async (path) => {
+ const input = await readJson(path, 'ASSET-RECIPE-001');
+ if (!input || typeof input !== 'object' || input.schema !== 'portfolio-asset-recipes/v1' || !Array.isArray(input.assets)) {
+ fail('ASSET-RECIPE-001');
+ }
+ exactKeys(input, ['schema', 'assets'], 'ASSET-RECIPE-001');
+ const seenIds = new Set();
+ const seenOutputs = new Set();
+ const assets = input.assets.map((asset) => {
+ if (!asset || typeof asset !== 'object') fail('ASSET-RECIPE-001');
+ exactKeys(asset, [
+ 'id', 'kind', 'output', 'maxWidth', 'startSeconds', 'durationSeconds',
+ 'rightsHolder', 'license', 'attribution',
+ ], 'ASSET-RECIPE-001');
+ if (!idPattern.test(asset.id) || !['image', 'video'].includes(asset.kind) || !outputPattern.test(asset.output)) {
+ fail('ASSET-RECIPE-001');
+ }
+ if (asset.kind === 'image' && !/\.(webp|png)$/u.test(asset.output)) fail('ASSET-RECIPE-001');
+ if (asset.kind === 'video' && !asset.output.endsWith('.webm')) fail('ASSET-RECIPE-001');
+ if (!Number.isInteger(asset.maxWidth) || asset.maxWidth < 64 || asset.maxWidth > 4096) fail('ASSET-RECIPE-001');
+ if (asset.kind === 'video' && (
+ typeof asset.startSeconds !== 'number' || asset.startSeconds < 0 ||
+ typeof asset.durationSeconds !== 'number' || asset.durationSeconds <= 0 || asset.durationSeconds > 30
+ )) fail('ASSET-RECIPE-001');
+ for (const field of ['rightsHolder', 'license', 'attribution']) {
+ if (typeof asset[field] !== 'string' || asset[field].trim().length === 0 || /[\u0000-\u001f\u007f]/u.test(asset[field])) {
+ fail('ASSET-RECIPE-001');
+ }
+ }
+ if (seenIds.has(asset.id) || seenOutputs.has(asset.output.toLowerCase())) fail('ASSET-RECIPE-002');
+ seenIds.add(asset.id);
+ seenOutputs.add(asset.output.toLowerCase());
+ return Object.freeze({ ...asset });
+ });
+ return Object.freeze(assets);
+};
+
+export const loadPrivateMapping = async (path, recipes) => {
+ const input = await readJson(path, 'ASSET-MAPPING-001');
+ if (!input || typeof input !== 'object' || input.schema !== 'portfolio-private-source-map/v1' ||
+ !input.sources || typeof input.sources !== 'object' || Array.isArray(input.sources)) fail('ASSET-MAPPING-001');
+ exactKeys(input, ['schema', 'sources'], 'ASSET-MAPPING-001');
+ const ids = recipes.map(({ id }) => id).sort();
+ if (JSON.stringify(Object.keys(input.sources).sort()) !== JSON.stringify(ids)) fail('ASSET-MAPPING-002');
+ for (const value of Object.values(input.sources)) {
+ if (typeof value !== 'string' || value.length === 0 || isAbsolute(value) || value.includes('\\') ||
+ value.split('/').some((part) => part === '' || part === '.' || part === '..')) fail('ASSET-MAPPING-001');
+ }
+ return input.sources;
+};
+
+export const resolvePrivateSource = async (sourceRoot, mappedPath) => {
+ const root = await realpath(sourceRoot);
+ const candidate = resolve(root, mappedPath);
+ const rel = relative(root, candidate);
+ if (!rel || rel.startsWith(`..${sep}`) || rel === '..' || isAbsolute(rel)) fail('ASSET-SOURCE-001');
+ let resolved;
+ try {
+ resolved = await realpath(candidate);
+ } catch {
+ fail('ASSET-SOURCE-001');
+ }
+ const resolvedRel = relative(root, resolved);
+ if (resolvedRel.startsWith(`..${sep}`) || resolvedRel === '..' || isAbsolute(resolvedRel)) fail('ASSET-SOURCE-001');
+ const info = await stat(resolved);
+ if (!info.isFile() || info.size <= 0 || info.size > 100 * 1024 * 1024) fail('ASSET-SOURCE-002');
+ return resolved;
+};
+
+export const publicFileNameFor = (recipe) => basename(recipe.output);
+export const publicExtensionFor = (recipe) => extname(recipe.output).slice(1);
diff --git a/scripts/lib/hash-file.mjs b/scripts/lib/hash-file.mjs
new file mode 100644
index 0000000..4ca68d2
--- /dev/null
+++ b/scripts/lib/hash-file.mjs
@@ -0,0 +1,10 @@
+import { createHash } from 'node:crypto';
+import { createReadStream } from 'node:fs';
+
+export const hashFile = async (path) => new Promise((resolve, reject) => {
+ const digest = createHash('sha256');
+ const stream = createReadStream(path);
+ stream.on('error', reject);
+ stream.on('data', (chunk) => digest.update(chunk));
+ stream.on('end', () => resolve(digest.digest('hex')));
+});
diff --git a/scripts/lib/privacy-scan.mjs b/scripts/lib/privacy-scan.mjs
new file mode 100644
index 0000000..5c46abd
--- /dev/null
+++ b/scripts/lib/privacy-scan.mjs
@@ -0,0 +1,118 @@
+import { access, readFile } from 'node:fs/promises';
+import { join } from 'node:path';
+import sharp from 'sharp';
+import { fileTypeFromFile } from 'file-type';
+import { createWorker, OEM } from 'tesseract.js';
+import {
+ BinaryBitmap,
+ HybridBinarizer,
+ MultiFormatReader,
+ NotFoundException,
+ RGBLuminanceSource,
+} from '@zxing/library';
+
+const normalize = (value) => value.normalize('NFKC').toLocaleLowerCase('en-US');
+const fail = (rule, location = '') => { throw new Error(location ? `${rule}:${location}` : rule); };
+
+export const loadDenylist = async (path) => {
+ let text;
+ try {
+ text = await readFile(path, 'utf8');
+ } catch {
+ fail('PRIVACY-DENYLIST-001');
+ }
+ const terms = [...new Set(text.split(/\r?\n/u).map((line) => line.trim()).filter(Boolean).map(normalize))];
+ if (terms.length === 0 || terms.some((term) => term.length < 3 || /[\u0000-\u001f\u007f]/u.test(term))) {
+ fail('PRIVACY-DENYLIST-001');
+ }
+ return terms;
+};
+
+export const scanText = (bytesOrText, denylist, rule = 'PRIVACY-BYTES-001') => {
+ const text = normalize(Buffer.isBuffer(bytesOrText) || bytesOrText instanceof Uint8Array
+ ? Buffer.from(bytesOrText).toString('utf8')
+ : String(bytesOrText));
+ if (denylist.some((term) => text.includes(normalize(term)))) fail(rule);
+};
+
+const scanBarcode = async (path) => {
+ let decoded;
+ try {
+ decoded = await sharp(path).removeAlpha().greyscale().raw().toBuffer({ resolveWithObject: true });
+ } catch {
+ fail('ASSET-DECODE-001');
+ }
+ const { data, info } = decoded;
+ const luminance = new RGBLuminanceSource(new Uint8ClampedArray(data), info.width, info.height);
+ const bitmap = new BinaryBitmap(new HybridBinarizer(luminance));
+ try {
+ new MultiFormatReader().decode(bitmap);
+ fail('PRIVACY-BARCODE-001');
+ } catch (error) {
+ if (error instanceof NotFoundException || error?.name === 'NotFoundException') return;
+ if (error instanceof Error && error.message === 'PRIVACY-BARCODE-001') throw error;
+ fail('PRIVACY-BARCODE-002');
+ }
+};
+
+const scanOcr = async (path, denylist) => {
+ const candidates = ['/usr/share/tessdata', '/usr/share/tesseract-ocr/5/tessdata'];
+ let langPath;
+ for (const candidate of candidates) {
+ try {
+ await access(join(candidate, 'eng.traineddata'));
+ langPath = candidate;
+ break;
+ } catch {}
+ }
+ if (!langPath) fail('PRIVACY-OCR-002');
+ let worker;
+ try {
+ worker = await createWorker('eng', OEM.LSTM_ONLY, { langPath, gzip: false, cacheMethod: 'none' });
+ } catch {
+ fail('PRIVACY-OCR-002');
+ }
+ let timer;
+ try {
+ const result = await Promise.race([
+ worker.recognize(path),
+ new Promise((_, reject) => { timer = setTimeout(() => reject(new Error('timeout')), 25_000); }),
+ ]);
+ scanText(result.data.text, denylist, 'PRIVACY-OCR-001');
+ } catch (error) {
+ if (error instanceof Error && error.message === 'PRIVACY-OCR-001') throw error;
+ fail('PRIVACY-OCR-002');
+ } finally {
+ if (timer !== undefined) clearTimeout(timer);
+ await worker.terminate().catch(() => {});
+ }
+};
+
+export const scanImage = async (path, denylist, options = {}) => {
+ let type;
+ try { type = await fileTypeFromFile(path); } catch { fail('PRIVACY-TYPE-001'); }
+ if (!type || !['image/png', 'image/webp', 'image/jpeg'].includes(type.mime)) fail('PRIVACY-TYPE-001');
+ let metadata;
+ try {
+ metadata = await sharp(path, { failOn: 'error', limitInputPixels: 50_000_000 }).metadata();
+ } catch {
+ fail('ASSET-DECODE-001');
+ }
+ if (metadata.exif || metadata.icc || metadata.iptc || metadata.xmp || metadata.orientation !== undefined) {
+ fail('PRIVACY-METADATA-001');
+ }
+ scanText(await readFile(path), denylist);
+ await scanBarcode(path);
+ if (options.runOcr !== false) await scanOcr(path, denylist);
+ return { width: metadata.width, height: metadata.height, mediaType: type.mime };
+};
+
+export const scanBinary = async (path, denylist) => {
+ let bytes;
+ try { bytes = await readFile(path); } catch { fail('PRIVACY-TYPE-001'); }
+ scanText(bytes, denylist);
+ let type;
+ try { type = await fileTypeFromFile(path); } catch { fail('PRIVACY-TYPE-001'); }
+ if (!type || !['video/webm', 'video/mp4'].includes(type.mime)) fail('PRIVACY-TYPE-001');
+ return { mediaType: type.mime };
+};
diff --git a/scripts/lib/video-frames.mjs b/scripts/lib/video-frames.mjs
new file mode 100644
index 0000000..88cf818
--- /dev/null
+++ b/scripts/lib/video-frames.mjs
@@ -0,0 +1,74 @@
+import { execFile } from 'node:child_process';
+import { mkdir, readFile } from 'node:fs/promises';
+import { join } from 'node:path';
+import { promisify } from 'node:util';
+
+const execFileAsync = promisify(execFile);
+const fail = (rule) => { throw new Error(rule); };
+
+export const runMediaCommand = async (command, args, rule, timeout = 60_000) => {
+ try {
+ return await execFileAsync(command, args, {
+ encoding: 'utf8', timeout, maxBuffer: 8 * 1024 * 1024, windowsHide: true,
+ });
+ } catch {
+ fail(rule);
+ }
+};
+
+export const probeVideo = async (path, options = {}) => {
+ const { stdout } = await runMediaCommand('ffprobe', [
+ '-v', 'error', '-show_entries', 'format=duration:stream=index,codec_type,width,height,duration',
+ '-of', 'json', path,
+ ], 'ASSET-VIDEO-PROBE-001', 20_000);
+ let probe;
+ try { probe = JSON.parse(stdout); } catch { fail('ASSET-VIDEO-PROBE-001'); }
+ const videoStreams = probe.streams?.filter((stream) => stream.codec_type === 'video') ?? [];
+ const audioStreams = probe.streams?.filter((stream) => stream.codec_type === 'audio') ?? [];
+ const unexpected = probe.streams?.filter((stream) => !['video', 'audio'].includes(stream.codec_type)) ?? [];
+ const duration = Number(probe.format?.duration ?? videoStreams[0]?.duration);
+ if (videoStreams.length !== 1 || unexpected.length > 0 || (options.allowAudio === false && audioStreams.length > 0) ||
+ !Number.isFinite(duration) || duration <= 0 || duration > 120) {
+ fail('ASSET-VIDEO-PROBE-001');
+ }
+ return { duration, width: Number(videoStreams[0].width), height: Number(videoStreams[0].height) };
+};
+
+export const encodeVideoDerivatives = async ({ source, webmPath, mp4Path, posterPath, startSeconds, durationSeconds, maxWidth }) => {
+ const common = [
+ '-hide_banner', '-loglevel', 'error', '-nostdin', '-y', '-threads', '1',
+ '-ss', String(startSeconds), '-i', source, '-t', String(durationSeconds), '-map', '0:v:0',
+ '-an', '-sn', '-dn', '-vf', `scale=w='min(${maxWidth},iw)':h=-2:flags=lanczos`,
+ '-map_metadata', '-1', '-map_chapters', '-1', '-pix_fmt', 'yuv420p',
+ ];
+ await runMediaCommand('ffmpeg', [
+ ...common, '-fflags', '+bitexact', '-flags:v', '+bitexact',
+ '-c:v', 'libvpx-vp9', '-deadline', 'good', '-cpu-used', '4', '-row-mt', '0',
+ '-b:v', '0', '-crf', '34', '-threads', '1', webmPath,
+ ], 'ASSET-VIDEO-ENCODE-001', 120_000);
+ await runMediaCommand('ffmpeg', [
+ ...common, '-fflags', '+bitexact', '-flags:v', '+bitexact',
+ '-c:v', 'libx264', '-preset', 'slow', '-crf', '25', '-threads', '1',
+ '-movflags', '+faststart', mp4Path,
+ ], 'ASSET-VIDEO-ENCODE-001', 120_000);
+ await runMediaCommand('ffmpeg', [
+ '-hide_banner', '-loglevel', 'error', '-nostdin', '-y', '-threads', '1',
+ '-ss', '0.05', '-i', webmPath, '-frames:v', '1', '-an', '-sn', '-dn', '-map_metadata', '-1', posterPath,
+ ], 'ASSET-VIDEO-POSTER-001', 30_000);
+};
+
+export const extractVideoFrames = async ({ source, durationSeconds, outputDir, prefix = 'frame' }) => {
+ await mkdir(outputDir, { recursive: true });
+ const points = [...new Set([0.05, durationSeconds / 2, Math.max(0.05, durationSeconds - 0.1)].map((value) => Math.min(durationSeconds, Math.max(0, value)).toFixed(3)))];
+ const paths = [];
+ for (const [index, point] of points.entries()) {
+ const path = join(outputDir, `${prefix}-${String(index + 1).padStart(2, '0')}.png`);
+ await runMediaCommand('ffmpeg', [
+ '-hide_banner', '-loglevel', 'error', '-nostdin', '-y', '-threads', '1',
+ '-ss', point, '-i', source, '-frames:v', '1', '-an', '-sn', '-dn', '-map_metadata', '-1', path,
+ ], 'ASSET-VIDEO-FRAME-001', 30_000);
+ if ((await readFile(path)).length === 0) fail('ASSET-VIDEO-FRAME-001');
+ paths.push(path);
+ }
+ return paths;
+};
diff --git a/scripts/prepare-assets.mjs b/scripts/prepare-assets.mjs
new file mode 100644
index 0000000..dce6a19
--- /dev/null
+++ b/scripts/prepare-assets.mjs
@@ -0,0 +1,265 @@
+#!/usr/bin/env node
+import { copyFile, mkdir, readFile, readdir, rm, stat, writeFile } from 'node:fs/promises';
+import { basename, dirname, extname, join, resolve } from 'node:path';
+import { fileURLToPath } from 'node:url';
+import sharp from 'sharp';
+import { fileTypeFromFile } from 'file-type';
+
+import { hashFile } from './lib/hash-file.mjs';
+import { loadPrivateMapping, loadRecipes, readJson, resolvePrivateSource } from './lib/asset-recipes.mjs';
+import { loadDenylist, scanBinary, scanImage } from './lib/privacy-scan.mjs';
+import { encodeVideoDerivatives, extractVideoFrames, probeVideo } from './lib/video-frames.mjs';
+
+const repositoryRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
+const fail = (rule) => { throw new Error(rule); };
+
+const assertSafeOutputRoot = (path, rule) => {
+ const resolved = resolve(path);
+ const permittedRepositoryOutput = join(repositoryRoot, 'inputs', 'prepared-assets');
+ if (resolved !== permittedRepositoryOutput && !resolved.startsWith('/tmp/')) fail(rule);
+ return resolved;
+};
+
+const resetDirectory = async (path, rule) => {
+ const safe = assertSafeOutputRoot(path, rule);
+ await rm(safe, { recursive: true, force: true });
+ await mkdir(safe, { recursive: true });
+ return safe;
+};
+
+const sorted = (items) => [...items].sort((left, right) => left.fileName.localeCompare(right.fileName, 'en-US'));
+
+const candidate = async ({ id, fileName, path, mediaType, width, height, durationMs, recipe }) => ({
+ id,
+ fileName,
+ sha256: await hashFile(path),
+ mediaType,
+ width,
+ height,
+ durationMs,
+ rightsHolder: recipe.rightsHolder,
+ license: recipe.license,
+ attribution: recipe.attribution,
+});
+
+const encodeImage = async (source, output, recipe) => {
+ let detected;
+ try { detected = await fileTypeFromFile(source); } catch { fail('ASSET-TYPE-001'); }
+ if (!detected || !['image/jpeg', 'image/png', 'image/webp'].includes(detected.mime)) fail('ASSET-TYPE-001');
+ try {
+ let pipeline = sharp(source, { failOn: 'error', limitInputPixels: 50_000_000, sequentialRead: true })
+ .rotate()
+ .resize({ width: recipe.maxWidth, withoutEnlargement: true, fit: 'inside', kernel: sharp.kernel.lanczos3 });
+ pipeline = recipe.output.endsWith('.png')
+ ? pipeline.png({ compressionLevel: 9, adaptiveFiltering: false, palette: false })
+ : pipeline.webp({ lossless: true, effort: 6, smartSubsample: false });
+ await pipeline.toFile(output);
+ } catch {
+ fail('ASSET-DECODE-001');
+ }
+ const details = await scanImage(output, [], { runOcr: false });
+ return details;
+};
+
+const prepareImage = async ({ source, recipe, stagingRoot, denylist }) => {
+ const path = join(stagingRoot, recipe.output);
+ const details = await encodeImage(source, path, recipe);
+ await scanImage(path, denylist, { runOcr: true });
+ return [await candidate({
+ id: recipe.id, fileName: recipe.output, path, mediaType: details.mediaType,
+ width: details.width, height: details.height, durationMs: null, recipe,
+ })];
+};
+
+const prepareVideo = async ({ source, recipe, stagingRoot, reportDir, denylist }) => {
+ const sourceProbe = await probeVideo(source);
+ if (recipe.startSeconds + recipe.durationSeconds > sourceProbe.duration + 0.05) fail('ASSET-VIDEO-RANGE-001');
+ const base = recipe.output.slice(0, -'.webm'.length);
+ const webmPath = join(stagingRoot, `${base}.webm`);
+ const mp4Path = join(stagingRoot, `${base}.mp4`);
+ const posterPath = join(stagingRoot, `${base}-poster.webp`);
+ await encodeVideoDerivatives({
+ source, webmPath, mp4Path, posterPath,
+ startSeconds: recipe.startSeconds, durationSeconds: recipe.durationSeconds, maxWidth: recipe.maxWidth,
+ });
+ const webmProbe = await probeVideo(webmPath, { allowAudio: false });
+ const mp4Probe = await probeVideo(mp4Path, { allowAudio: false });
+ const frames = await extractVideoFrames({
+ source: webmPath,
+ durationSeconds: Math.min(webmProbe.duration, recipe.durationSeconds),
+ outputDir: join(reportDir, 'video-frames'),
+ prefix: recipe.id,
+ });
+ await scanBinary(webmPath, denylist);
+ await scanBinary(mp4Path, denylist);
+ await scanImage(posterPath, denylist, { runOcr: true });
+ for (const frame of frames) await scanImage(frame, denylist, { runOcr: true });
+ const poster = await sharp(posterPath).metadata();
+ return [
+ await candidate({
+ id: recipe.id, fileName: `${base}.webm`, path: webmPath, mediaType: 'video/webm',
+ width: webmProbe.width, height: webmProbe.height, durationMs: Math.round(webmProbe.duration * 1000), recipe,
+ }),
+ await candidate({
+ id: `${recipe.id}-mp4`, fileName: `${base}.mp4`, path: mp4Path, mediaType: 'video/mp4',
+ width: mp4Probe.width, height: mp4Probe.height, durationMs: Math.round(mp4Probe.duration * 1000), recipe,
+ }),
+ await candidate({
+ id: `${recipe.id}-poster`, fileName: `${base}-poster.webp`, path: posterPath, mediaType: 'image/webp',
+ width: poster.width, height: poster.height, durationMs: null, recipe,
+ }),
+ ];
+};
+
+const createContactSheet = async (candidates, stagingRoot, reportDir) => {
+ const images = candidates.filter(({ mediaType }) => mediaType.startsWith('image/')).map((image) => ({
+ id: image.id,
+ path: join(stagingRoot, image.fileName),
+ }));
+ let frameNames = [];
+ try { frameNames = (await readdir(join(reportDir, 'video-frames'))).filter((name) => name.endsWith('.png')).sort(); } catch {}
+ images.push(...frameNames.map((name, index) => ({
+ id: `private-game-loop-frame-${String(index + 1).padStart(2, '0')}`,
+ path: join(reportDir, 'video-frames', name),
+ })));
+ if (images.length === 0) return;
+ const imageWidth = 1100;
+ const width = 1200;
+ const composites = [];
+ let y = 30;
+ for (const image of images) {
+ const thumb = await sharp(image.path)
+ .resize({ width: imageWidth, height: 1400, fit: 'inside', withoutEnlargement: false })
+ .png()
+ .toBuffer();
+ const metadata = await sharp(thumb).metadata();
+ const left = Math.round((width - (metadata.width ?? imageWidth)) / 2);
+ composites.push({ input: thumb, left, top: y });
+ const safeLabel = image.id.replace(/[^a-z0-9-]/gu, '');
+ const label = Buffer.from(``);
+ y += metadata.height ?? 0;
+ composites.push({ input: label, left: 50, top: y + 8 });
+ y += 86;
+ }
+ await sharp({ create: { width, height: y + 20, channels: 3, background: '#070910' } })
+ .composite(composites)
+ .webp({ lossless: true, effort: 6 })
+ .toFile(join(reportDir, 'contact-sheet.webp'));
+};
+
+const approvalProjection = (recipes, candidates) => ({
+ schema: 'portfolio-asset-approvals/v1',
+ assets: recipes.map(({ id }) => ({
+ id,
+ files: sorted(candidates.filter((item) => item.id === id || item.id.startsWith(`${id}-`)))
+ .map(({ fileName, sha256 }) => ({ fileName, sha256 })),
+ })).sort((left, right) => left.id.localeCompare(right.id, 'en-US')),
+});
+
+const approvalsMatch = async (path, expected) => {
+ const actual = await readJson(path, 'ASSET-APPROVAL-001');
+ if (JSON.stringify(actual) !== JSON.stringify(expected)) fail('ASSET-APPROVAL-002');
+};
+
+const publish = async ({ candidates, stagingRoot, publishRoot, manifestPath }) => {
+ if (candidates.some((item) => /pending|requires confirmation/iu.test(`${item.rightsHolder} ${item.license}`))) {
+ fail('ASSET-RIGHTS-001');
+ }
+ const safePublishRoot = await resetDirectory(publishRoot, 'ASSET-PUBLISH-001');
+ for (const item of candidates) await copyFile(join(stagingRoot, item.fileName), join(safePublishRoot, item.fileName));
+ await mkdir(dirname(manifestPath), { recursive: true });
+ await writeFile(manifestPath, `${JSON.stringify({ assets: candidates }, null, 2)}\n`, 'utf8');
+};
+
+export const prepareAssets = async (options) => {
+ const required = ['sourceRoot', 'mappingPath', 'denylistPath', 'recipesPath', 'stagingRoot', 'reportDir'];
+ if (!options || required.some((key) => typeof options[key] !== 'string')) fail('ASSET-ARGUMENT-001');
+ const recipes = await loadRecipes(options.recipesPath);
+ const mapping = await loadPrivateMapping(options.mappingPath, recipes);
+ const denylist = await loadDenylist(options.denylistPath);
+ const stagingRoot = await resetDirectory(options.stagingRoot, 'ASSET-STAGING-001');
+ const reportDir = await resetDirectory(options.reportDir, 'ASSET-REPORT-001');
+ const candidates = [];
+ for (const recipe of recipes) {
+ const source = await resolvePrivateSource(options.sourceRoot, mapping[recipe.id]);
+ const prepared = recipe.kind === 'image'
+ ? await prepareImage({ source, recipe, stagingRoot, denylist })
+ : await prepareVideo({ source, recipe, stagingRoot, reportDir, denylist });
+ candidates.push(...prepared);
+ }
+ const finalCandidates = sorted(candidates);
+ const approval = approvalProjection(recipes, finalCandidates);
+ await writeFile(join(reportDir, 'candidate-approval.json'), `${JSON.stringify(approval, null, 2)}\n`, 'utf8');
+ await writeFile(join(reportDir, 'scan-report.json'), `${JSON.stringify({
+ schema: 'portfolio-private-scan-report/v1', automatedChecks: 'passed', publication: 'pending-manual-approval',
+ assets: finalCandidates.map(({ id, fileName, sha256 }) => ({ id, fileName, sha256 })),
+ }, null, 2)}\n`, 'utf8');
+ await createContactSheet(finalCandidates, stagingRoot, reportDir);
+ let approvalStatus = 'pending';
+ if (options.approvalRegisterPath !== undefined) {
+ if (typeof options.publishRoot !== 'string' || typeof options.manifestPath !== 'string') fail('ASSET-ARGUMENT-001');
+ await approvalsMatch(options.approvalRegisterPath, approval);
+ await publish({ candidates: finalCandidates, stagingRoot, publishRoot: options.publishRoot, manifestPath: options.manifestPath });
+ approvalStatus = 'approved';
+ }
+ return { candidates: finalCandidates, approvalStatus, reportDir };
+};
+
+export const verifyPreparedAssets = async ({ publishRoot, manifestPath, denylistPath }) => {
+ if (![publishRoot, manifestPath, denylistPath].every((value) => typeof value === 'string')) fail('ASSET-ARGUMENT-001');
+ const manifest = await readJson(manifestPath, 'ASSET-MANIFEST-001');
+ if (!manifest || typeof manifest !== 'object' || !Array.isArray(manifest.assets)) fail('ASSET-MANIFEST-001');
+ const expected = manifest.assets.map(({ fileName }) => fileName).sort();
+ const actual = (await readdir(publishRoot, { withFileTypes: true }))
+ .map((entry) => {
+ if (!entry.isFile()) fail('ASSET-EXACT-001');
+ return entry.name;
+ }).sort();
+ if (JSON.stringify(actual) !== JSON.stringify(expected)) fail('ASSET-EXACT-001');
+ const denylist = await loadDenylist(denylistPath);
+ for (const item of manifest.assets) {
+ if (!item || typeof item.fileName !== 'string' || !/^project-[a-z0-9-]+\.(webp|png|webm|mp4)$/u.test(item.fileName)) fail('ASSET-MANIFEST-001');
+ const path = join(publishRoot, item.fileName);
+ if ((await stat(path)).nlink !== 1) fail('ASSET-EXACT-001');
+ if (await hashFile(path) !== item.sha256) fail('ASSET-HASH-001');
+ if (item.mediaType.startsWith('image/')) await scanImage(path, denylist, { runOcr: true });
+ else await scanBinary(path, denylist);
+ }
+ return true;
+};
+
+const parseArgs = (argv) => {
+ const values = {};
+ let verify = false;
+ for (let index = 0; index < argv.length; index += 1) {
+ const key = argv[index];
+ if (key === '--verify') { verify = true; continue; }
+ if (!key?.startsWith('--') || index + 1 >= argv.length) fail('ASSET-ARGUMENT-001');
+ values[key.slice(2)] = argv[index + 1];
+ index += 1;
+ }
+ return { verify, values };
+};
+
+const runCli = async () => {
+ const { verify, values } = parseArgs(process.argv.slice(2));
+ if (verify) {
+ await verifyPreparedAssets({ publishRoot: values['publish-root'], manifestPath: values.manifest, denylistPath: values.denylist });
+ return;
+ }
+ await prepareAssets({
+ sourceRoot: values['source-root'], mappingPath: values.mapping, denylistPath: values.denylist,
+ recipesPath: values.recipes, stagingRoot: values['staging-root'], reportDir: values['report-dir'],
+ approvalRegisterPath: values.approvals, publishRoot: values['publish-root'], manifestPath: values.manifest,
+ });
+};
+
+if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
+ runCli().catch((error) => {
+ const message = error instanceof Error && /^[A-Z]+(?:-[A-Z]+)*-\d{3}(?::[a-z0-9-]+)?$/u.test(error.message)
+ ? error.message
+ : 'ASSET-UNEXPECTED-001';
+ process.stderr.write(`${message}\n`);
+ process.exitCode = 1;
+ });
+}
diff --git a/tests/fixtures/assets/malformed.bin b/tests/fixtures/assets/malformed.bin
new file mode 100644
index 0000000..5db09ae
--- /dev/null
+++ b/tests/fixtures/assets/malformed.bin
@@ -0,0 +1 @@
+This is deliberately not a supported media container.
diff --git a/tests/integration/prepare-assets.test.ts b/tests/integration/prepare-assets.test.ts
new file mode 100644
index 0000000..f97a819
--- /dev/null
+++ b/tests/integration/prepare-assets.test.ts
@@ -0,0 +1,184 @@
+// @vitest-environment node
+import { mkdtemp, mkdir, readFile, readdir, stat, writeFile } from 'node:fs/promises';
+import { tmpdir } from 'node:os';
+import { join } from 'node:path';
+import { execFile } from 'node:child_process';
+import { promisify } from 'node:util';
+import sharp from 'sharp';
+import { BarcodeFormat, MultiFormatWriter } from '@zxing/library';
+import { describe, expect, it } from 'vitest';
+
+// @ts-expect-error The production script is native ESM and intentionally has no TypeScript runtime wrapper.
+import { hashFile } from '../../scripts/lib/hash-file.mjs';
+// @ts-expect-error The production script is native ESM and intentionally has no TypeScript runtime wrapper.
+import { scanImage, scanText } from '../../scripts/lib/privacy-scan.mjs';
+// @ts-expect-error The production script is native ESM and intentionally has no TypeScript runtime wrapper.
+import { prepareAssets, verifyPreparedAssets } from '../../scripts/prepare-assets.mjs';
+
+const execFileAsync = promisify(execFile);
+interface Candidate { id: string; fileName: string; sha256: string; }
+
+const createRoot = async (): Promise => mkdtemp(join(tmpdir(), 'portfolio-assets-test-'));
+
+const writeJson = async (path: string, value: unknown): Promise => {
+ await writeFile(path, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
+};
+
+const baseRecipe = (overrides: Record = {}) => ({
+ id: 'project-interface',
+ kind: 'image',
+ output: 'project-interface.webp',
+ maxWidth: 720,
+ rightsHolder: 'Portfolio owner',
+ license: 'All rights reserved',
+ attribution: 'Portfolio owner',
+ ...overrides,
+});
+
+const makePng = async (path: string, text = 'SAFE DEMO', includeMetadata = true): Promise => {
+ const svg = Buffer.from(``);
+ const pipeline = sharp(svg).png();
+ if (includeMetadata) await pipeline.withMetadata({ orientation: 6 }).toFile(path);
+ else await pipeline.toFile(path);
+};
+
+const makeQrPng = async (path: string): Promise => {
+ const matrix = new MultiFormatWriter().encode('https://private.invalid/account', BarcodeFormat.QR_CODE, 280, 280, new Map());
+ const pixels = Buffer.alloc(matrix.getWidth() * matrix.getHeight() * 3);
+ for (let y = 0; y < matrix.getHeight(); y += 1) {
+ for (let x = 0; x < matrix.getWidth(); x += 1) {
+ const value = matrix.get(x, y) ? 0 : 255;
+ const offset = (y * matrix.getWidth() + x) * 3;
+ pixels[offset] = value;
+ pixels[offset + 1] = value;
+ pixels[offset + 2] = value;
+ }
+ }
+ await sharp(pixels, { raw: { width: matrix.getWidth(), height: matrix.getHeight(), channels: 3 } }).png().toFile(path);
+};
+
+const makeVideo = async (path: string): Promise => {
+ await execFileAsync('ffmpeg', [
+ '-hide_banner', '-loglevel', 'error', '-nostdin', '-y',
+ '-f', 'lavfi', '-i', 'color=c=red:s=640x360:d=1:r=24',
+ '-f', 'lavfi', '-i', 'color=c=blue:s=640x360:d=2:r=24',
+ '-filter_complex', '[0:v][1:v]concat=n=2:v=1:a=0[outv]',
+ '-map', '[outv]', '-c:v', 'libx264', '-pix_fmt', 'yuv420p', '-threads', '1', path,
+ ], { timeout: 20_000 });
+};
+
+const makeInputs = async (root: string, recipe: Record, sourceName = 'source.png') => {
+ const sourceRoot = join(root, 'source');
+ const reportDir = join(root, 'private-report');
+ const stagingRoot = join(root, 'staging');
+ await mkdir(sourceRoot, { recursive: true });
+ const recipesPath = join(root, 'recipes.json');
+ const mappingPath = join(root, 'mapping.json');
+ const denylistPath = join(root, 'denylist.txt');
+ await writeJson(recipesPath, { schema: 'portfolio-asset-recipes/v1', assets: [recipe] });
+ await writeJson(mappingPath, { schema: 'portfolio-private-source-map/v1', sources: { [String(recipe.id)]: sourceName } });
+ await writeFile(denylistPath, 'SECRET CANARY\nprivate-source-name\n', 'utf8');
+ return { sourceRoot, reportDir, stagingRoot, recipesPath, mappingPath, denylistPath };
+};
+
+describe('deterministic asset preparation', () => {
+ it('re-encodes images without metadata under generated safe filenames, identically across runs', async () => {
+ const root = await createRoot();
+ const inputs = await makeInputs(root, baseRecipe());
+ await makePng(join(inputs.sourceRoot, 'source.png'));
+
+ const first = await prepareAssets(inputs);
+ const firstFile = first.candidates[0];
+ expect(firstFile?.fileName).toMatch(/^project-[a-z0-9-]+\.[a-z0-9]+$/);
+ expect(first.approvalStatus).toBe('pending');
+ expect(await stat(join(inputs.stagingRoot, firstFile!.fileName))).toBeDefined();
+ const firstHash = await hashFile(join(inputs.stagingRoot, firstFile!.fileName));
+ const metadata = await sharp(join(inputs.stagingRoot, firstFile!.fileName)).metadata();
+ expect(metadata.orientation).toBeUndefined();
+ expect(metadata.exif).toBeUndefined();
+ expect(metadata.icc).toBeUndefined();
+
+ const second = await prepareAssets({ ...inputs, stagingRoot: join(root, 'staging-two'), reportDir: join(root, 'report-two') });
+ expect(await hashFile(join(root, 'staging-two', second.candidates[0]!.fileName))).toBe(firstHash);
+ expect(await stat(join(inputs.reportDir, 'contact-sheet.webp'))).toBeDefined();
+ }, 30_000);
+
+ it('returns only opaque rule identifiers for OCR and byte denylist matches', async () => {
+ const root = await createRoot();
+ const secretCanary = 'SECRET CANARY';
+ const imagePath = join(root, 'secret.png');
+ await makePng(imagePath, secretCanary, false);
+
+ expect(() => scanText(Buffer.from(`prefix ${secretCanary} suffix`), [secretCanary])).toThrowError(/PRIVACY-BYTES-001/u);
+ try {
+ await scanImage(imagePath, [secretCanary], { runOcr: true });
+ throw new Error('expected privacy scan to fail');
+ } catch (error) {
+ const message = error instanceof Error ? error.message : String(error);
+ expect(message).toMatch(/PRIVACY-(OCR|BYTES)-001/u);
+ expect(message).not.toContain(secretCanary);
+ }
+ }, 30_000);
+
+ it('rejects QR/barcodes and malformed media closed', async () => {
+ const root = await createRoot();
+ const qrPath = join(root, 'qr.png');
+ await makeQrPng(qrPath);
+ await expect(scanImage(qrPath, [], { runOcr: false })).rejects.toThrow(/PRIVACY-BARCODE-001/u);
+
+ const inputs = await makeInputs(root, baseRecipe(), 'broken.png');
+ await writeFile(join(inputs.sourceRoot, 'broken.png'), 'not an image', 'utf8');
+ await expect(prepareAssets(inputs)).rejects.toThrow(/ASSET-(TYPE|DECODE)-001/u);
+ }, 20_000);
+
+ it('trims title frames, strips audio, samples video, and emits WebM, MP4, and a poster', async () => {
+ const root = await createRoot();
+ const recipe = baseRecipe({
+ id: 'private-game-loop', kind: 'video', output: 'project-private-game-loop.webm',
+ startSeconds: 1.1, durationSeconds: 1.5, maxWidth: 640,
+ });
+ const inputs = await makeInputs(root, recipe, 'clip.mp4');
+ await makeVideo(join(inputs.sourceRoot, 'clip.mp4'));
+
+ const result = await prepareAssets(inputs);
+ expect(result.candidates.map((candidate: Candidate) => candidate.fileName).sort()).toEqual([
+ 'project-private-game-loop-poster.webp',
+ 'project-private-game-loop.mp4',
+ 'project-private-game-loop.webm',
+ ]);
+ const poster = await sharp(join(inputs.stagingRoot, 'project-private-game-loop-poster.webp')).stats();
+ expect(poster.channels[2]!.mean).toBeGreaterThan(poster.channels[0]!.mean * 2);
+ const sampled = await readdir(join(inputs.reportDir, 'video-frames'));
+ expect(sampled.length).toBeGreaterThanOrEqual(3);
+
+ const second = await prepareAssets({
+ ...inputs,
+ stagingRoot: join(root, 'video-staging-two'),
+ reportDir: join(root, 'video-report-two'),
+ });
+ expect(second.candidates.map(({ fileName, sha256 }: Candidate) => ({ fileName, sha256 })))
+ .toEqual(result.candidates.map(({ fileName, sha256 }: Candidate) => ({ fileName, sha256 })));
+ }, 60_000);
+
+ it('publishes only when an approval register exactly matches every final hash and rejects extras', async () => {
+ const root = await createRoot();
+ const inputs = await makeInputs(root, baseRecipe());
+ await makePng(join(inputs.sourceRoot, 'source.png'));
+ const pending = await prepareAssets(inputs);
+ const approvalPath = join(root, 'approval.json');
+ await writeJson(approvalPath, {
+ schema: 'portfolio-asset-approvals/v1',
+ assets: [{ id: 'project-interface', files: pending.candidates.map(({ fileName, sha256 }: Candidate) => ({ fileName, sha256 })) }],
+ });
+ const publishRoot = join(root, 'published');
+ const manifestPath = join(root, 'asset-manifest.json');
+ const approved = await prepareAssets({ ...inputs, approvalRegisterPath: approvalPath, publishRoot, manifestPath });
+ expect(approved.approvalStatus).toBe('approved');
+ expect(JSON.parse(await readFile(manifestPath, 'utf8')).assets).toHaveLength(1);
+ await writeFile(join(publishRoot, 'project-unexpected.webp'), 'extra', 'utf8');
+ await expect(verifyPreparedAssets({ publishRoot, manifestPath, denylistPath: inputs.denylistPath })).rejects.toThrow(/ASSET-EXACT-001/u);
+ }, 30_000);
+});