From b4779bb49b694df6924ee0633b95fb05f818e383 Mon Sep 17 00:00:00 2001 From: alam00000 Date: Fri, 17 Apr 2026 23:40:24 +0530 Subject: [PATCH] feat: enhance sanitization --- .env.example | 4 +- .gitignore | 3 + Dockerfile | 1 + Dockerfile.nonroot | 1 + README.md | 8 +- docs/self-hosting/docker.md | 14 +- docs/self-hosting/index.md | 4 +- docs/tools/validate-signature-pdf.md | 25 + nginx.conf | 42 +- package-lock.json | 1515 ++++++++++--------- package.json | 9 +- public/sw.js | 49 +- scripts/generate-security-headers.mjs | 90 ++ src/js/logic/bookmark-pdf.ts | 22 +- src/js/logic/deskew-pdf-page.ts | 51 +- src/js/logic/form-creator.ts | 50 +- src/js/logic/form-filler-page.ts | 49 +- src/js/logic/pdf-layers-page.ts | 3 +- src/js/logic/pdf-multi-tool.ts | 9 + src/js/logic/remove-annotations-page.ts | 3 +- src/js/logic/remove-blank-pages-page.ts | 3 +- src/js/logic/validate-signature-pdf-page.ts | 596 ++++---- src/js/logic/validate-signature-pdf.ts | 566 ++++++- src/js/logic/word-to-pdf.ts | 26 +- src/js/main.ts | 14 +- src/js/sw-register.ts | 130 +- src/js/types/validate-signature-type.ts | 78 +- src/js/utils/helpers.ts | 32 +- src/js/utils/markdown-editor.ts | 15 +- src/js/utils/wasm-provider.ts | 80 +- src/pages/pdf-multi-tool.html | 6 +- src/pages/repair-pdf.html | 4 +- src/pages/wasm-settings.html | 8 +- src/tests/xss-replay.test.ts | 365 +++++ vite.config.ts | 68 +- 35 files changed, 2703 insertions(+), 1240 deletions(-) create mode 100644 scripts/generate-security-headers.mjs create mode 100644 src/tests/xss-replay.test.ts diff --git a/.env.example b/.env.example index 98bcda1..18835b9 100644 --- a/.env.example +++ b/.env.example @@ -9,8 +9,8 @@ VITE_CORS_PROXY_SECRET= # Pre-configured defaults enable advanced PDF features out of the box. # For air-gapped / offline deployments, point these to your internal server (e.g., /wasm/pymupdf/). VITE_WASM_PYMUPDF_URL=https://cdn.jsdelivr.net/npm/@bentopdf/pymupdf-wasm@0.11.16/ -VITE_WASM_GS_URL=https://cdn.jsdelivr.net/npm/@bentopdf/gs-wasm/assets/ -VITE_WASM_CPDF_URL=https://cdn.jsdelivr.net/npm/coherentpdf/dist/ +VITE_WASM_GS_URL=https://cdn.jsdelivr.net/npm/@bentopdf/gs-wasm@0.1.1/assets/ +VITE_WASM_CPDF_URL=https://cdn.jsdelivr.net/npm/coherentpdf@2.5.5/dist/ # OCR assets (optional) # Set all three together for self-hosted or air-gapped OCR. diff --git a/.gitignore b/.gitignore index a89bf35..1f6997f 100644 --- a/.gitignore +++ b/.gitignore @@ -33,6 +33,9 @@ coverage/ # Generated sitemap public/sitemap.xml +# Generated by scripts/generate-security-headers.mjs at build time +security-headers.conf + #backup .seo-backup libreoffice-wasm-package diff --git a/Dockerfile b/Dockerfile index d0e0f40..843ab34 100644 --- a/Dockerfile +++ b/Dockerfile @@ -89,6 +89,7 @@ USER nginx COPY --chown=nginx:nginx --from=builder /app/dist /usr/share/nginx/html${BASE_URL%/} COPY --chown=nginx:nginx nginx.conf /etc/nginx/nginx.conf +COPY --chown=nginx:nginx --from=builder /app/security-headers.conf /etc/nginx/security-headers.conf COPY --chown=nginx:nginx --chmod=755 nginx-ipv6.sh /docker-entrypoint.d/99-disable-ipv6.sh RUN mkdir -p /etc/nginx/tmp && chown -R nginx:nginx /etc/nginx/tmp diff --git a/Dockerfile.nonroot b/Dockerfile.nonroot index 22f0617..4fdf81f 100644 --- a/Dockerfile.nonroot +++ b/Dockerfile.nonroot @@ -80,6 +80,7 @@ RUN apk upgrade --no-cache && apk add --no-cache su-exec COPY --from=builder /app/dist /usr/share/nginx/html${BASE_URL%/} COPY nginx.conf /etc/nginx/nginx.conf +COPY --from=builder /app/security-headers.conf /etc/nginx/security-headers.conf COPY --chmod=755 entrypoint.sh /entrypoint.sh RUN mkdir -p /etc/nginx/tmp \ diff --git a/README.md b/README.md index 0038704..060d2d4 100644 --- a/README.md +++ b/README.md @@ -470,8 +470,8 @@ The default URLs are set in `.env.production`: ```bash VITE_WASM_PYMUPDF_URL=https://cdn.jsdelivr.net/npm/@bentopdf/pymupdf-wasm@0.11.16/ -VITE_WASM_GS_URL=https://cdn.jsdelivr.net/npm/@bentopdf/gs-wasm/assets/ -VITE_WASM_CPDF_URL=https://cdn.jsdelivr.net/npm/coherentpdf/dist/ +VITE_WASM_GS_URL=https://cdn.jsdelivr.net/npm/@bentopdf/gs-wasm@0.1.1/assets/ +VITE_WASM_CPDF_URL=https://cdn.jsdelivr.net/npm/coherentpdf@2.5.5/dist/ VITE_TESSERACT_WORKER_URL= VITE_TESSERACT_CORE_URL= VITE_TESSERACT_LANG_URL= @@ -1100,11 +1100,15 @@ For detailed release instructions, see [RELEASE.md](RELEASE.md). ``` 3. **Run the Development Server**: + ```bash npm run dev ``` + The application will be available at `http://localhost:5173`. + > The dev server binds to `localhost` only by default. To expose it on your LAN (e.g. for mobile device testing), set `VITE_DEV_HOST=0.0.0.0 npm run dev`. The built-in CORS proxy at `/cors-proxy?url=` restricts targets to a known host allowlist; to permit additional hosts in development, set `VITE_DEV_CORS_PROXY_EXTRA_HOSTS="host1.example.com,host2.example.com"`. + #### Option 2: Build and Run with Docker Compose 1. **Clone the Repository**: diff --git a/docs/self-hosting/docker.md b/docs/self-hosting/docker.md index 298ab84..d3807d6 100644 --- a/docs/self-hosting/docker.md +++ b/docs/self-hosting/docker.md @@ -95,8 +95,8 @@ docker run -d -p 3000:8080 bentopdf:custom | `SIMPLE_MODE` | Build without LibreOffice tools | `false` | | `BASE_URL` | Deploy to subdirectory | `/` | | `VITE_WASM_PYMUPDF_URL` | PyMuPDF WASM module URL | `https://cdn.jsdelivr.net/npm/@bentopdf/pymupdf-wasm@0.11.16/` | -| `VITE_WASM_GS_URL` | Ghostscript WASM module URL | `https://cdn.jsdelivr.net/npm/@bentopdf/gs-wasm/assets/` | -| `VITE_WASM_CPDF_URL` | CoherentPDF WASM module URL | `https://cdn.jsdelivr.net/npm/coherentpdf/dist/` | +| `VITE_WASM_GS_URL` | Ghostscript WASM module URL | `https://cdn.jsdelivr.net/npm/@bentopdf/gs-wasm@0.1.1/assets/` | +| `VITE_WASM_CPDF_URL` | CoherentPDF WASM module URL | `https://cdn.jsdelivr.net/npm/coherentpdf@2.5.5/dist/` | | `VITE_TESSERACT_WORKER_URL` | OCR worker script URL | _(empty; use Tesseract.js default CDN)_ | | `VITE_TESSERACT_CORE_URL` | OCR core runtime directory | _(empty; use Tesseract.js default CDN)_ | | `VITE_TESSERACT_LANG_URL` | OCR traineddata directory | _(empty; use Tesseract.js default CDN)_ | @@ -110,6 +110,16 @@ docker run -d -p 3000:8080 bentopdf:custom WASM module URLs are pre-configured with CDN defaults — all advanced features work out of the box. Override these for air-gapped or self-hosted deployments. +### Content-Security-Policy + +The nginx image ships with an enforcing `Content-Security-Policy` header. The CSP's `script-src`, `connect-src`, and `font-src` directives are **generated at build time** from the `VITE_*` URL variables above — whatever hosts you pass via `--build-arg` are automatically added to the policy. + +As a result: + +- If you override `VITE_CORS_PROXY_URL` or `VITE_WASM_*_URL` at build time, the CSP permits those origins automatically — no extra config needed. +- If you configure custom WASM URLs at _runtime_ via the in-app Advanced Settings page, those origins are **not** in the CSP and the browser will block fetches to them. Runtime configuration is intended for experimentation; for permanent custom URLs set the matching `VITE_*` build arg. +- Air-gapped deployments that override all three `VITE_WASM_*_URL` values also get the public `cdn.jsdelivr.net` removed from CSP (each default is replaced, not appended). Similarly, setting `VITE_CORS_PROXY_URL` replaces the public `bentopdf-cors-proxy.bentopdf.workers.dev` default. + For OCR, leave the `VITE_TESSERACT_*` variables empty to use the default online assets, or set all three together for self-hosted/offline OCR. Partial OCR overrides are rejected because the worker, core runtime, and traineddata directory must match. For fully offline searchable PDF output, also set `VITE_OCR_FONT_BASE_URL` so the OCR text-layer fonts are loaded from your internal server instead of the public Noto font URLs. `VITE_DEFAULT_LANGUAGE` sets the UI language for first-time visitors. Supported values: `en`, `ar`, `be`, `fr`, `de`, `es`, `zh`, `zh-TW`, `vi`, `tr`, `id`, `it`, `pt`, `nl`, `da`. Users can still switch languages — this only changes the default. diff --git a/docs/self-hosting/index.md b/docs/self-hosting/index.md index 707a1f5..ec9d1f6 100644 --- a/docs/self-hosting/index.md +++ b/docs/self-hosting/index.md @@ -203,8 +203,8 @@ These are set in `.env.production` and baked into the build: ```bash VITE_WASM_PYMUPDF_URL=https://cdn.jsdelivr.net/npm/@bentopdf/pymupdf-wasm@0.11.16/ -VITE_WASM_GS_URL=https://cdn.jsdelivr.net/npm/@bentopdf/gs-wasm/assets/ -VITE_WASM_CPDF_URL=https://cdn.jsdelivr.net/npm/coherentpdf/dist/ +VITE_WASM_GS_URL=https://cdn.jsdelivr.net/npm/@bentopdf/gs-wasm@0.1.1/assets/ +VITE_WASM_CPDF_URL=https://cdn.jsdelivr.net/npm/coherentpdf@2.5.5/dist/ VITE_TESSERACT_WORKER_URL= VITE_TESSERACT_CORE_URL= VITE_TESSERACT_LANG_URL= diff --git a/docs/tools/validate-signature-pdf.md b/docs/tools/validate-signature-pdf.md index 1a8b5ae..9610acc 100644 --- a/docs/tools/validate-signature-pdf.md +++ b/docs/tools/validate-signature-pdf.md @@ -26,6 +26,31 @@ The tool extracts PKCS#7 signature objects from the PDF, decodes the ASN.1 struc - **Self-signed detection**: Is the certificate its own issuer? - **Trust chain**: When a trusted certificate is provided, does the signer's certificate chain back to it? +### Cryptographic Verification + +- The tool reconstructs the bytes covered by the signature's `/ByteRange`, hashes them with the algorithm the signer declared, and compares the result against the `messageDigest` attribute inside the signature. +- It then re-serializes the signed attributes as a DER SET and verifies the signature against the signer certificate's public key. +- **A signature is reported as valid only when all of these pass.** If the PDF bytes were modified after signing, or the embedded signature does not match the signer's key, the status shows "Invalid — Cryptographic Verification Failed" with the specific reason. + +#### Supported Signature Algorithms + +| Algorithm | Verification path | +| ----------------------- | -------------------------------------------------------------------------------------- | +| RSA (PKCS#1 v1.5) | node-forge `publicKey.verify`, with Web Crypto fallback | +| RSA-PSS (RSASSA-PSS) | Web Crypto `verify({name: 'RSA-PSS', saltLength})` | +| ECDSA P-256/P-384/P-521 | Web Crypto `verify({name: 'ECDSA', hash})` after DER → IEEE P1363 signature conversion | + +If a signature uses an algorithm outside this list (for example Ed25519, SM2, or RSA with an unusual digest OID), the card shows **"Unverified — Unsupported Signature Algorithm"** in yellow, along with the specific OID or reason. This is a deliberate three-state distinction: + +- **Valid** — signature cryptographically verified against the signer's public key. +- **Invalid** — verification ran and produced a negative result (bytes changed, key mismatch). +- **Unverified** — the tool could not run verification for this algorithm. The certificate metadata is still shown, but you should treat the signature as "unknown" and verify it with Adobe Acrobat or `openssl cms -verify`. + +### Insecure Digest Algorithms + +- Signatures using **MD5 or SHA-1** are rejected as invalid and flagged with "Insecure Digest" status. Both algorithms have published collision attacks, so a signature over a SHA-1 or MD5 hash offers no integrity guarantee. +- SHA-224, SHA-256, SHA-384, and SHA-512 are all accepted. + ### Document Coverage - **Full coverage**: The signature covers the entire PDF file, meaning no bytes were added or changed after signing. diff --git a/nginx.conf b/nginx.conf index d22a0d9..e5d040c 100644 --- a/nginx.conf +++ b/nginx.conf @@ -12,7 +12,7 @@ http { default_type application/octet-stream; gzip_static on; - + gzip on; gzip_vary on; gzip_min_length 1024; @@ -27,27 +27,20 @@ http { index index.html; absolute_redirect off; + include /etc/nginx/security-headers.conf; + location ~ ^/(en|ar|be|da|de|es|fr|id|it|ko|nl|pt|ru|sv|tr|vi|zh|zh-TW)(/.*)?$ { try_files $uri $uri/ $uri.html /$1/index.html /index.html; expires 5m; - add_header Cache-Control "public, must-revalidate"; - add_header Cross-Origin-Embedder-Policy "require-corp" always; - add_header Cross-Origin-Opener-Policy "same-origin" always; } location ~ ^/(.+?)/(en|ar|be|da|de|es|fr|id|it|ko|nl|pt|ru|sv|tr|vi|zh|zh-TW)(/.*)?$ { try_files $uri $uri/ $uri.html /$1/$2/index.html /$1/index.html /index.html; expires 5m; - add_header Cache-Control "public, must-revalidate"; - add_header Cross-Origin-Embedder-Policy "require-corp" always; - add_header Cross-Origin-Opener-Policy "same-origin" always; } location ~* \.html$ { expires 1h; - add_header Cache-Control "public, must-revalidate"; - add_header Cross-Origin-Embedder-Policy "require-corp" always; - add_header Cross-Origin-Opener-Policy "same-origin" always; } location ~* /libreoffice-wasm/soffice\.wasm\.gz$ { @@ -55,9 +48,8 @@ http { types {} default_type application/wasm; add_header Content-Encoding gzip; add_header Vary "Accept-Encoding"; - add_header Cache-Control "public, immutable"; - add_header Cross-Origin-Embedder-Policy "require-corp" always; - add_header Cross-Origin-Opener-Policy "same-origin" always; + include /etc/nginx/security-headers.conf; + expires 1y; } location ~* /libreoffice-wasm/soffice\.data\.gz$ { @@ -65,38 +57,28 @@ http { types {} default_type application/octet-stream; add_header Content-Encoding gzip; add_header Vary "Accept-Encoding"; - add_header Cache-Control "public, immutable"; - add_header Cross-Origin-Embedder-Policy "require-corp" always; - add_header Cross-Origin-Opener-Policy "same-origin" always; + include /etc/nginx/security-headers.conf; + expires 1y; } location ~* \.(wasm|wasm\.gz|data|data\.gz)$ { expires 1y; - add_header Cache-Control "public, immutable"; - add_header Cross-Origin-Embedder-Policy "require-corp" always; - add_header Cross-Origin-Opener-Policy "same-origin" always; } location ~* \.(js|mjs|css|woff|woff2|ttf|eot|otf)$ { expires 1y; - add_header Cache-Control "public, immutable"; - add_header Cross-Origin-Embedder-Policy "require-corp" always; - add_header Cross-Origin-Opener-Policy "same-origin" always; } location ~* \.(png|jpg|jpeg|gif|ico|svg|webp|avif|mp4|webm)$ { expires 1y; - add_header Cache-Control "public, immutable"; } location ~* \.json$ { expires 1w; - add_header Cache-Control "public, must-revalidate"; } location ~* \.pdf$ { expires 1y; - add_header Cache-Control "public, immutable"; } error_page 404 /404.html; @@ -104,16 +86,6 @@ http { location / { try_files $uri $uri/ $uri.html =404; expires 5m; - add_header Cache-Control "public, must-revalidate"; - add_header Cross-Origin-Embedder-Policy "require-corp" always; - add_header Cross-Origin-Opener-Policy "same-origin" always; } - - add_header X-Frame-Options "SAMEORIGIN" always; - add_header X-Content-Type-Options "nosniff" always; - add_header X-XSS-Protection "1; mode=block" always; - add_header Cross-Origin-Opener-Policy "same-origin" always; - add_header Cross-Origin-Embedder-Policy "require-corp" always; - add_header Cross-Origin-Resource-Policy "cross-origin" always; } } diff --git a/package-lock.json b/package-lock.json index 82b70d2..f20a7b4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31,6 +31,7 @@ "bwip-js": "^4.8.0", "cropperjs": "^1.6.2", "diff": "^8.0.3", + "dompurify": "^3.4.0", "embedpdf-snippet": "file:vendor/embedpdf/embedpdf-snippet-2.9.1.tgz", "heic2any": "^0.0.4", "highlight.js": "^11.11.1", @@ -103,7 +104,7 @@ "vite": "^7.3.2", "vite-plugin-compression": "^0.5.1", "vite-plugin-handlebars": "^2.0.0", - "vite-plugin-node-polyfills": "^0.25.0", + "vite-plugin-node-polyfills": "^0.26.0", "vitepress": "^1.6.4", "vitest": "^4.0.18", "vue": "^3.5.29" @@ -1312,422 +1313,6 @@ "vue": ">=3.2.0" } }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", - "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", - "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", - "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", - "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", - "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", - "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", - "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", - "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", - "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", - "cpu": [ - "arm" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", - "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", - "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", - "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", - "cpu": [ - "loong64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", - "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", - "cpu": [ - "mips64el" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", - "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", - "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", - "cpu": [ - "riscv64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", - "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", - "cpu": [ - "s390x" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", - "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", - "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", - "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", - "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", - "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", - "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "openharmony" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", - "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", - "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", - "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", - "cpu": [ - "ia32" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", - "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", @@ -6700,47 +6285,6 @@ "node": ">= 0.4" } }, - "node_modules/esbuild": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", - "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.4", - "@esbuild/android-arm": "0.27.4", - "@esbuild/android-arm64": "0.27.4", - "@esbuild/android-x64": "0.27.4", - "@esbuild/darwin-arm64": "0.27.4", - "@esbuild/darwin-x64": "0.27.4", - "@esbuild/freebsd-arm64": "0.27.4", - "@esbuild/freebsd-x64": "0.27.4", - "@esbuild/linux-arm": "0.27.4", - "@esbuild/linux-arm64": "0.27.4", - "@esbuild/linux-ia32": "0.27.4", - "@esbuild/linux-loong64": "0.27.4", - "@esbuild/linux-mips64el": "0.27.4", - "@esbuild/linux-ppc64": "0.27.4", - "@esbuild/linux-riscv64": "0.27.4", - "@esbuild/linux-s390x": "0.27.4", - "@esbuild/linux-x64": "0.27.4", - "@esbuild/netbsd-arm64": "0.27.4", - "@esbuild/netbsd-x64": "0.27.4", - "@esbuild/openbsd-arm64": "0.27.4", - "@esbuild/openbsd-x64": "0.27.4", - "@esbuild/openharmony-arm64": "0.27.4", - "@esbuild/sunos-x64": "0.27.4", - "@esbuild/win32-arm64": "0.27.4", - "@esbuild/win32-ia32": "0.27.4", - "@esbuild/win32-x64": "0.27.4" - } - }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -7145,9 +6689,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.16.0.tgz", + "integrity": "sha512-y5rN/uOsadFT/JfYwhxRS5R7Qce+g3zG97+JrtFZlC9klX/W5hD7iiLzScI4nZqUS7DNUdhPgw4xI8W2LuXlUw==", "funding": [ { "type": "individual", @@ -12207,9 +11751,9 @@ } }, "node_modules/vite-plugin-handlebars/node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", "cpu": [ "ppc64" ], @@ -12220,13 +11764,13 @@ "aix" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vite-plugin-handlebars/node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", "cpu": [ "arm" ], @@ -12237,13 +11781,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vite-plugin-handlebars/node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", "cpu": [ "arm64" ], @@ -12254,13 +11798,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vite-plugin-handlebars/node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", "cpu": [ "x64" ], @@ -12271,13 +11815,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vite-plugin-handlebars/node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", "cpu": [ "arm64" ], @@ -12288,13 +11832,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vite-plugin-handlebars/node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", "cpu": [ "x64" ], @@ -12305,13 +11849,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vite-plugin-handlebars/node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", "cpu": [ "arm64" ], @@ -12322,13 +11866,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vite-plugin-handlebars/node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", "cpu": [ "x64" ], @@ -12339,13 +11883,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vite-plugin-handlebars/node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", "cpu": [ "arm" ], @@ -12356,13 +11900,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vite-plugin-handlebars/node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", "cpu": [ "arm64" ], @@ -12373,13 +11917,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vite-plugin-handlebars/node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", "cpu": [ "ia32" ], @@ -12390,13 +11934,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vite-plugin-handlebars/node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", "cpu": [ "loong64" ], @@ -12407,13 +11951,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vite-plugin-handlebars/node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", "cpu": [ "mips64el" ], @@ -12424,13 +11968,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vite-plugin-handlebars/node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", "cpu": [ "ppc64" ], @@ -12441,13 +11985,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vite-plugin-handlebars/node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", "cpu": [ "riscv64" ], @@ -12458,13 +12002,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vite-plugin-handlebars/node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", "cpu": [ "s390x" ], @@ -12475,13 +12019,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vite-plugin-handlebars/node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", "cpu": [ "x64" ], @@ -12492,13 +12036,30 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/vite-plugin-handlebars/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/vite-plugin-handlebars/node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", "cpu": [ "x64" ], @@ -12509,13 +12070,30 @@ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/vite-plugin-handlebars/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/vite-plugin-handlebars/node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", "cpu": [ "x64" ], @@ -12526,13 +12104,30 @@ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/vite-plugin-handlebars/node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" } }, "node_modules/vite-plugin-handlebars/node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", "cpu": [ "x64" ], @@ -12543,13 +12138,13 @@ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vite-plugin-handlebars/node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", "cpu": [ "arm64" ], @@ -12560,13 +12155,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vite-plugin-handlebars/node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", "cpu": [ "ia32" ], @@ -12577,13 +12172,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vite-plugin-handlebars/node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", "cpu": [ "x64" ], @@ -12594,13 +12189,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vite-plugin-handlebars/node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -12608,32 +12203,35 @@ "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" } }, "node_modules/vite-plugin-handlebars/node_modules/vite": { @@ -12697,9 +12295,9 @@ } }, "node_modules/vite-plugin-node-polyfills": { - "version": "0.25.0", - "resolved": "https://registry.npmjs.org/vite-plugin-node-polyfills/-/vite-plugin-node-polyfills-0.25.0.tgz", - "integrity": "sha512-rHZ324W3LhfGPxWwQb2N048TThB6nVvnipsqBUJEzh3R9xeK9KI3si+GMQxCuAcpPJBVf0LpDtJ+beYzB3/chg==", + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/vite-plugin-node-polyfills/-/vite-plugin-node-polyfills-0.26.0.tgz", + "integrity": "sha512-BAe5YzJf368XGev02hDvioidx4uVH8dqEJlG73bjQSxM26/AQnGcKFomq9n3vGq5yqpSHKN4h1XQNxx9l98mBg==", "dev": true, "license": "MIT", "dependencies": { @@ -12710,7 +12308,7 @@ "url": "https://github.com/sponsors/davidmyersdev" }, "peerDependencies": { - "vite": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + "vite": "^2.0.0 || ^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, "node_modules/vite-plugin-static-copy": { @@ -12735,6 +12333,463 @@ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/vite/node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/vite/node_modules/esbuild": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, "node_modules/vitepress": { "version": "1.6.4", "resolved": "https://registry.npmjs.org/vitepress/-/vitepress-1.6.4.tgz", @@ -12778,9 +12833,9 @@ } }, "node_modules/vitepress/node_modules/@esbuild/aix-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", - "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", "cpu": [ "ppc64" ], @@ -12791,13 +12846,13 @@ "aix" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vitepress/node_modules/@esbuild/android-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", - "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", "cpu": [ "arm" ], @@ -12808,13 +12863,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vitepress/node_modules/@esbuild/android-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", - "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", "cpu": [ "arm64" ], @@ -12825,13 +12880,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vitepress/node_modules/@esbuild/android-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", - "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", "cpu": [ "x64" ], @@ -12842,13 +12897,13 @@ "android" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vitepress/node_modules/@esbuild/darwin-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", - "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", "cpu": [ "arm64" ], @@ -12859,13 +12914,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vitepress/node_modules/@esbuild/darwin-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", - "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", "cpu": [ "x64" ], @@ -12876,13 +12931,13 @@ "darwin" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vitepress/node_modules/@esbuild/freebsd-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", - "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", "cpu": [ "arm64" ], @@ -12893,13 +12948,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vitepress/node_modules/@esbuild/freebsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", - "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", "cpu": [ "x64" ], @@ -12910,13 +12965,13 @@ "freebsd" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vitepress/node_modules/@esbuild/linux-arm": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", - "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", "cpu": [ "arm" ], @@ -12927,13 +12982,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vitepress/node_modules/@esbuild/linux-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", - "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", "cpu": [ "arm64" ], @@ -12944,13 +12999,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vitepress/node_modules/@esbuild/linux-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", - "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", "cpu": [ "ia32" ], @@ -12961,13 +13016,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vitepress/node_modules/@esbuild/linux-loong64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", - "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", "cpu": [ "loong64" ], @@ -12978,13 +13033,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vitepress/node_modules/@esbuild/linux-mips64el": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", - "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", "cpu": [ "mips64el" ], @@ -12995,13 +13050,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vitepress/node_modules/@esbuild/linux-ppc64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", - "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", "cpu": [ "ppc64" ], @@ -13012,13 +13067,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vitepress/node_modules/@esbuild/linux-riscv64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", - "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", "cpu": [ "riscv64" ], @@ -13029,13 +13084,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vitepress/node_modules/@esbuild/linux-s390x": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", - "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", "cpu": [ "s390x" ], @@ -13046,13 +13101,13 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vitepress/node_modules/@esbuild/linux-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", - "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", "cpu": [ "x64" ], @@ -13063,13 +13118,30 @@ "linux" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/vitepress/node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/vitepress/node_modules/@esbuild/netbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", - "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", "cpu": [ "x64" ], @@ -13080,13 +13152,30 @@ "netbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/vitepress/node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" } }, "node_modules/vitepress/node_modules/@esbuild/openbsd-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", - "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", "cpu": [ "x64" ], @@ -13097,13 +13186,30 @@ "openbsd" ], "engines": { - "node": ">=12" + "node": ">=18" + } + }, + "node_modules/vitepress/node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" } }, "node_modules/vitepress/node_modules/@esbuild/sunos-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", - "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", "cpu": [ "x64" ], @@ -13114,13 +13220,13 @@ "sunos" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vitepress/node_modules/@esbuild/win32-arm64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", - "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", "cpu": [ "arm64" ], @@ -13131,13 +13237,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vitepress/node_modules/@esbuild/win32-ia32": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", - "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", "cpu": [ "ia32" ], @@ -13148,13 +13254,13 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vitepress/node_modules/@esbuild/win32-x64": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", - "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", "cpu": [ "x64" ], @@ -13165,7 +13271,7 @@ "win32" ], "engines": { - "node": ">=12" + "node": ">=18" } }, "node_modules/vitepress/node_modules/@vitejs/plugin-vue": { @@ -13183,9 +13289,9 @@ } }, "node_modules/vitepress/node_modules/esbuild": { - "version": "0.21.5", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", - "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", "dev": true, "hasInstallScript": true, "license": "MIT", @@ -13193,32 +13299,35 @@ "esbuild": "bin/esbuild" }, "engines": { - "node": ">=12" + "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.21.5", - "@esbuild/android-arm": "0.21.5", - "@esbuild/android-arm64": "0.21.5", - "@esbuild/android-x64": "0.21.5", - "@esbuild/darwin-arm64": "0.21.5", - "@esbuild/darwin-x64": "0.21.5", - "@esbuild/freebsd-arm64": "0.21.5", - "@esbuild/freebsd-x64": "0.21.5", - "@esbuild/linux-arm": "0.21.5", - "@esbuild/linux-arm64": "0.21.5", - "@esbuild/linux-ia32": "0.21.5", - "@esbuild/linux-loong64": "0.21.5", - "@esbuild/linux-mips64el": "0.21.5", - "@esbuild/linux-ppc64": "0.21.5", - "@esbuild/linux-riscv64": "0.21.5", - "@esbuild/linux-s390x": "0.21.5", - "@esbuild/linux-x64": "0.21.5", - "@esbuild/netbsd-x64": "0.21.5", - "@esbuild/openbsd-x64": "0.21.5", - "@esbuild/sunos-x64": "0.21.5", - "@esbuild/win32-arm64": "0.21.5", - "@esbuild/win32-ia32": "0.21.5", - "@esbuild/win32-x64": "0.21.5" + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" } }, "node_modules/vitepress/node_modules/vite": { diff --git a/package.json b/package.json index 1c6d6e3..708e516 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "type": "module", "scripts": { "dev": "vite", - "build": "tsc && vite build && NODE_OPTIONS='--max-old-space-size=3072' node scripts/generate-i18n-pages.mjs && node scripts/generate-sitemap.mjs", + "build": "tsc && vite build && NODE_OPTIONS='--max-old-space-size=3072' node scripts/generate-i18n-pages.mjs && node scripts/generate-sitemap.mjs && node scripts/generate-security-headers.mjs", "build:with-docs": "npm run build && npm run docs:build && node scripts/include-docs-in-dist.js", "build:gzip": "COMPRESSION_MODE=g npm run build", "build:brotli": "COMPRESSION_MODE=b npm run build", @@ -58,7 +58,7 @@ "vite": "^7.3.2", "vite-plugin-compression": "^0.5.1", "vite-plugin-handlebars": "^2.0.0", - "vite-plugin-node-polyfills": "^0.25.0", + "vite-plugin-node-polyfills": "^0.26.0", "vitepress": "^1.6.4", "vitest": "^4.0.18", "vue": "^3.5.29" @@ -86,6 +86,7 @@ "bwip-js": "^4.8.0", "cropperjs": "^1.6.2", "diff": "^8.0.3", + "dompurify": "^3.4.0", "embedpdf-snippet": "file:vendor/embedpdf/embedpdf-snippet-2.9.1.tgz", "heic2any": "^0.0.4", "highlight.js": "^11.11.1", @@ -149,6 +150,8 @@ "flatted": "^3.4.2", "react": "^18.3.1", "react-dom": "^18.3.1", - "lodash-es": "^4.18.1" + "lodash-es": "^4.18.1", + "follow-redirects": "^1.15.11", + "esbuild": "^0.25.0" } } diff --git a/public/sw.js b/public/sw.js index 0ea3887..4f37700 100644 --- a/public/sw.js +++ b/public/sw.js @@ -5,9 +5,11 @@ * Version: 1.1.0 */ -const CACHE_VERSION = 'bentopdf-v10'; +const CACHE_VERSION = 'bentopdf-v11'; const CACHE_NAME = `${CACHE_VERSION}-static`; +const trustedCdnOrigins = new Set(['https://cdn.jsdelivr.net']); + const getBasePath = () => { const scope = self.registration?.scope || self.location.href; const url = new URL(scope); @@ -67,7 +69,7 @@ self.addEventListener('activate', (event) => { self.addEventListener('fetch', (event) => { const url = new URL(event.request.url); - const isCDN = url.hostname === 'cdn.jsdelivr.net'; + const isCDN = trustedCdnOrigins.has(url.origin); const isLocal = url.origin === location.origin; if (!isLocal && !isCDN) { @@ -220,11 +222,9 @@ async function removeDuplicateCache(cache, fileName, isCDN) { for (const req of requests) { const reqUrl = new URL(req.url); if (reqUrl.pathname.endsWith(fileName)) { - // If caching CDN version, remove local version (and vice versa) - const reqIsCDN = reqUrl.hostname === 'cdn.jsdelivr.net'; + const reqIsCDN = trustedCdnOrigins.has(reqUrl.origin); if (reqIsCDN !== isCDN) { await cache.delete(req); - // console.log(`[Dedup] Removed ${reqIsCDN ? 'CDN' : 'local'} version of:`, fileName); } } } @@ -280,13 +280,16 @@ function getLocalPathForCDNUrl(pathname) { * Determine if a URL should be cached * Handles both local and CDN URLs */ +const CACHEABLE_EXTENSIONS = + /\.(js|mjs|css|wasm|whl|zip|json|png|jpg|jpeg|gif|svg|woff|woff2|ttf|gz|br)$/; + function shouldCache(pathname, isCDN = false) { if (isCDN) { return ( pathname.includes('/@bentopdf/pymupdf-wasm') || pathname.includes('/@bentopdf/gs-wasm') || pathname.includes('/@matbee/libreoffice-converter') || - pathname.match(/\.(wasm|whl|zip|json|js|gz)$/) + CACHEABLE_EXTENSIONS.test(pathname) ); } @@ -294,9 +297,7 @@ function shouldCache(pathname, isCDN = false) { pathname.includes('/libreoffice-wasm/') || pathname.includes('/embedpdf/') || pathname.includes('/assets/') || - pathname.match( - /\.(js|mjs|css|wasm|whl|zip|json|png|jpg|jpeg|gif|svg|woff|woff2|ttf|gz|br)$/ - ) + CACHEABLE_EXTENSIONS.test(pathname) ); } @@ -327,16 +328,42 @@ async function cacheInBatches(cache, urls, batchSize = 5) { } self.addEventListener('message', (event) => { - if (event.data && event.data.type === 'SKIP_WAITING') { + if (!event.data) return; + + if (event.data.type === 'SKIP_WAITING') { self.skipWaiting(); + return; } - if (event.data && event.data.type === 'CLEAR_CACHE') { + if (event.data.type === 'CLEAR_CACHE') { event.waitUntil( caches.delete(CACHE_NAME).then(() => { console.log('[ServiceWorker] Cache cleared'); }) ); + return; + } + + if ( + event.data.type === 'SET_TRUSTED_CDN_HOSTS' && + Array.isArray(event.data.hosts) + ) { + for (const origin of event.data.hosts) { + if (typeof origin !== 'string') continue; + try { + const parsed = new URL(origin); + if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') { + continue; + } + trustedCdnOrigins.add(parsed.origin); + } catch (e) { + console.warn( + '[ServiceWorker] Ignoring malformed trusted-host origin:', + origin, + e + ); + } + } } }); diff --git a/scripts/generate-security-headers.mjs b/scripts/generate-security-headers.mjs new file mode 100644 index 0000000..2e30b8e --- /dev/null +++ b/scripts/generate-security-headers.mjs @@ -0,0 +1,90 @@ +#!/usr/bin/env node +import { writeFileSync } from 'node:fs'; +import { join, dirname } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +const repoRoot = join(dirname(fileURLToPath(import.meta.url)), '..'); + +function originOf(urlStr) { + if (!urlStr) return null; + try { + const u = new URL(urlStr); + if (u.protocol !== 'https:' && u.protocol !== 'http:') return null; + return `${u.protocol}//${u.host}`; + } catch { + return null; + } +} + +function uniq(values) { + return Array.from(new Set(values.filter(Boolean))); +} + +const DEFAULT_WASM_ORIGINS = { + pymupdf: 'https://cdn.jsdelivr.net', + gs: 'https://cdn.jsdelivr.net', + cpdf: 'https://cdn.jsdelivr.net', +}; +const DEFAULT_CORS_PROXY_ORIGIN = + 'https://bentopdf-cors-proxy.bentopdf.workers.dev'; + +const wasmOrigins = [ + originOf(process.env.VITE_WASM_PYMUPDF_URL) || DEFAULT_WASM_ORIGINS.pymupdf, + originOf(process.env.VITE_WASM_GS_URL) || DEFAULT_WASM_ORIGINS.gs, + originOf(process.env.VITE_WASM_CPDF_URL) || DEFAULT_WASM_ORIGINS.cpdf, +]; + +const tesseractOrigins = uniq([ + originOf(process.env.VITE_TESSERACT_WORKER_URL), + originOf(process.env.VITE_TESSERACT_CORE_URL), + originOf(process.env.VITE_TESSERACT_LANG_URL), +]); + +const corsProxyOrigin = + originOf(process.env.VITE_CORS_PROXY_URL) || DEFAULT_CORS_PROXY_ORIGIN; + +const ocrFontOrigin = originOf(process.env.VITE_OCR_FONT_BASE_URL); + +const scriptOrigins = uniq([...wasmOrigins, ...tesseractOrigins]); +const connectOrigins = uniq([ + ...wasmOrigins, + ...tesseractOrigins, + corsProxyOrigin, +]); +const fontOrigins = uniq([ocrFontOrigin].filter(Boolean)); + +const directives = [ + `default-src 'self'`, + `script-src 'self' 'wasm-unsafe-eval' ${scriptOrigins.join(' ')}`.trim(), + `worker-src 'self' blob:`, + `style-src 'self' 'unsafe-inline'`, + `img-src 'self' data: blob: https:`, + fontOrigins.length + ? `font-src 'self' data: ${fontOrigins.join(' ')}` + : `font-src 'self' data:`, + `connect-src 'self' ${connectOrigins.join(' ')}`.trim(), + `object-src 'none'`, + `base-uri 'self'`, + `frame-ancestors 'self'`, + `form-action 'self'`, + `upgrade-insecure-requests`, +]; + +const csp = directives.join('; '); + +const contents = `add_header Content-Security-Policy "${csp}" always; +add_header X-Frame-Options "SAMEORIGIN" always; +add_header X-Content-Type-Options "nosniff" always; +add_header X-XSS-Protection "1; mode=block" always; +add_header Referrer-Policy "strict-origin-when-cross-origin" always; +add_header Permissions-Policy "geolocation=(), camera=(), microphone=(), payment=(), usb=(), interest-cohort=()" always; +add_header Cross-Origin-Opener-Policy "same-origin" always; +add_header Cross-Origin-Embedder-Policy "require-corp" always; +add_header Cross-Origin-Resource-Policy "cross-origin" always; +`; + +const outPath = join(repoRoot, 'security-headers.conf'); +writeFileSync(outPath, contents); +console.log( + `[security-headers] wrote ${outPath} with ${scriptOrigins.length} script-src / ${connectOrigins.length} connect-src origin(s)` +); diff --git a/src/js/logic/bookmark-pdf.ts b/src/js/logic/bookmark-pdf.ts index e7bd208..28faa8b 100644 --- a/src/js/logic/bookmark-pdf.ts +++ b/src/js/logic/bookmark-pdf.ts @@ -232,22 +232,22 @@ function showInputModal( if (field.type === 'text') { return `
- + +placeholder="${escapeHTML(field.placeholder || '')}" />
`; } else if (field.type === 'select') { return `
- + 0 ? `maxlength="${field.combCells}"` : field.maxLength > 0 ? `maxlength="${field.maxLength}"` : ''} class="w-full bg-gray-600 border border-gray-500 text-white rounded px-2 py-1 text-sm focus:ring-indigo-500 focus:border-indigo-500"> + 0 ? `maxlength="${field.combCells}"` : field.maxLength > 0 ? `maxlength="${field.maxLength}"` : ''} class="w-full bg-gray-600 border border-gray-500 text-white rounded px-2 py-1 text-sm focus:ring-indigo-500 focus:border-indigo-500">
@@ -1151,11 +1151,11 @@ function showProperties(field: FormField): void { specificProps = `
- +
- +
@@ -1168,13 +1168,13 @@ function showProperties(field: FormField): void { specificProps = `
- +
@@ -1185,7 +1185,7 @@ function showProperties(field: FormField): void { specificProps = `
- +
@@ -1200,11 +1200,11 @@ function showProperties(field: FormField): void {
- +
- +
@@ -1215,7 +1215,7 @@ function showProperties(field: FormField): void { .filter((f) => f.id !== field.id) .map( (f) => - `` + `` ) .join('')} @@ -1281,7 +1281,7 @@ function showProperties(field: FormField): void {
- +
Example of current format: @@ -1298,7 +1298,7 @@ function showProperties(field: FormField): void { specificProps = `
- +
Clicking this field in the PDF will open a file picker to upload an image. @@ -1320,7 +1320,7 @@ function showProperties(field: FormField): void {
- +
`; @@ -1330,7 +1330,7 @@ function showProperties(field: FormField): void {
- +
${ @@ -1343,7 +1343,10 @@ function showProperties(field: FormField): void { +
@@ -1784,7 +1787,7 @@ function showProperties(field: FormField): void { field.options ?.map( (opt) => - `` + `` ) .join(''); @@ -2478,7 +2481,12 @@ downloadBtn.addEventListener('click', async () => { JS: field.jsScript, }); } else if (field.action === 'showHide' && field.targetFieldName) { - const target = field.targetFieldName; + const target = field.targetFieldName + .replace(/\\/g, '\\\\') + .replace(/"/g, '\\"') + .replace(/\r/g, '\\r') + .replace(/\n/g, '\\n') + .replace(/\0/g, '\\0'); let script: string; if (field.visibilityAction === 'show') { diff --git a/src/js/logic/form-filler-page.ts b/src/js/logic/form-filler-page.ts index 3ad9e5f..ae49b48 100644 --- a/src/js/logic/form-filler-page.ts +++ b/src/js/logic/form-filler-page.ts @@ -54,19 +54,42 @@ function updateFileDisplay() { ? `${(currentFile.size / 1024).toFixed(1)} KB` : `${(currentFile.size / 1024 / 1024).toFixed(2)} MB`; - displayArea.innerHTML = ` -
-
-
-

${currentFile.name}

-

${fileSize}

-
- -
-
- `; + displayArea.textContent = ''; + + const card = document.createElement('div'); + card.className = + 'bg-gray-700 p-3 rounded-lg border border-gray-600 hover:border-indigo-500 transition-colors'; + + const row = document.createElement('div'); + row.className = 'flex items-center justify-between'; + + const info = document.createElement('div'); + info.className = 'flex-1 min-w-0'; + + const nameP = document.createElement('p'); + nameP.className = 'truncate font-medium text-white'; + nameP.textContent = currentFile.name; + + const sizeP = document.createElement('p'); + sizeP.className = 'text-gray-400 text-sm'; + sizeP.textContent = fileSize; + + info.append(nameP, sizeP); + + const removeBtn = document.createElement('button'); + removeBtn.id = 'remove-file'; + removeBtn.className = + 'text-red-400 hover:text-red-300 p-2 flex-shrink-0 ml-2'; + removeBtn.title = 'Remove file'; + + const removeIcon = document.createElement('i'); + removeIcon.setAttribute('data-lucide', 'trash-2'); + removeIcon.className = 'w-4 h-4'; + removeBtn.appendChild(removeIcon); + + row.append(info, removeBtn); + card.appendChild(row); + displayArea.appendChild(card); createIcons({ icons }); diff --git a/src/js/logic/pdf-layers-page.ts b/src/js/logic/pdf-layers-page.ts index 80ed525..d37e40a 100644 --- a/src/js/logic/pdf-layers-page.ts +++ b/src/js/logic/pdf-layers-page.ts @@ -2,6 +2,7 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; import { t } from '../i18n/i18n'; import { downloadFile, + escapeHtml, readFileAsArrayBuffer, formatBytes, getPDFDocument, @@ -208,7 +209,7 @@ document.addEventListener('DOMContentLoaded', () => {
diff --git a/src/js/logic/pdf-multi-tool.ts b/src/js/logic/pdf-multi-tool.ts index 0e442da..69abdb1 100644 --- a/src/js/logic/pdf-multi-tool.ts +++ b/src/js/logic/pdf-multi-tool.ts @@ -193,6 +193,15 @@ function initializeTool() { document .getElementById('pdf-file-input') ?.addEventListener('change', handlePdfUpload); + document.getElementById('upload-area')?.addEventListener('click', () => { + document.getElementById('pdf-file-input')?.click(); + }); + document + .getElementById('pdf-file-input-select-btn') + ?.addEventListener('click', (e) => { + e.stopPropagation(); + document.getElementById('pdf-file-input')?.click(); + }); document .getElementById('insert-pdf-input') ?.addEventListener('change', handleInsertPdf); diff --git a/src/js/logic/remove-annotations-page.ts b/src/js/logic/remove-annotations-page.ts index 1fb199f..bea9245 100644 --- a/src/js/logic/remove-annotations-page.ts +++ b/src/js/logic/remove-annotations-page.ts @@ -2,6 +2,7 @@ import { PDFDocument, PDFName } from 'pdf-lib'; import { createIcons, icons } from 'lucide'; import { loadPdfWithPasswordPrompt } from '../utils/password-prompt.js'; import { loadPdfDocument } from '../utils/load-pdf-document.js'; +import { escapeHtml } from '../utils/helpers.js'; // State management const pageState: { pdfDoc: PDFDocument | null; file: File | null } = { @@ -70,7 +71,7 @@ function updateFileDisplay() {
-

${pageState.file.name}

+

${escapeHtml(pageState.file.name)}

${fileSize} • ${pageCount} page${pageCount !== 1 ? 's' : ''}

@@ -407,12 +452,16 @@ function createSignatureCard(result: SignatureValidationResult, index: number):
- ${result.signatureDate ? ` + ${ + result.signatureDate + ? `

Signed On

${formatDate(result.signatureDate)}

- ` : ''} + ` + : '' + }
@@ -425,19 +474,27 @@ function createSignatureCard(result: SignatureValidationResult, index: number):
- ${result.reason ? ` + ${ + result.reason + ? `

Reason

${escapeHtml(result.reason)}

- ` : ''} + ` + : '' + } - ${result.location ? ` + ${ + result.location + ? `

Location

${escapeHtml(result.location)}

- ` : ''} + ` + : '' + }
@@ -448,22 +505,23 @@ function createSignatureCard(result: SignatureValidationResult, index: number):

Digest Algorithm: ${escapeHtml(result.algorithms.digest)}

Signature Algorithm: ${escapeHtml(result.algorithms.signature)}

${result.errorMessage ? `

Error: ${escapeHtml(result.errorMessage)}

` : ''} + ${result.unsupportedAlgorithmReason ? `

${escapeHtml(result.unsupportedAlgorithmReason)}

` : ''}
`; - return card; + return card; } function escapeHtml(str: string): string { - const div = document.createElement('div'); - div.textContent = str; - return div.innerHTML; + const div = document.createElement('div'); + div.textContent = str; + return div.innerHTML; } if (document.readyState === 'loading') { - document.addEventListener('DOMContentLoaded', initializePage); + document.addEventListener('DOMContentLoaded', initializePage); } else { - initializePage(); + initializePage(); } diff --git a/src/js/logic/validate-signature-pdf.ts b/src/js/logic/validate-signature-pdf.ts index 697b78d..94d39e9 100644 --- a/src/js/logic/validate-signature-pdf.ts +++ b/src/js/logic/validate-signature-pdf.ts @@ -1,6 +1,11 @@ import forge from 'node-forge'; import { ExtractedSignature, SignatureValidationResult } from '@/types'; +const INSECURE_DIGEST_OIDS = new Set([ + '1.2.840.113549.2.5', + '1.3.14.3.2.26', +]); + export function extractSignatures(pdfBytes: Uint8Array): ExtractedSignature[] { const signatures: ExtractedSignature[] = []; const pdfString = new TextDecoder('latin1').decode(pdfBytes); @@ -70,11 +75,11 @@ export function extractSignatures(pdfBytes: Uint8Array): ExtractedSignature[] { return signatures; } -export function validateSignature( +export async function validateSignature( signature: ExtractedSignature, pdfBytes: Uint8Array, trustedCert?: forge.pki.Certificate -): SignatureValidationResult { +): Promise { const result: SignatureValidationResult = { signatureIndex: signature.index, isValid: false, @@ -104,7 +109,10 @@ export function validateSignature( asn1 ) as forge.pkcs7.PkcsSignedData & { rawCapture?: { - authenticatedAttributes?: Array<{ type: string; value: Date }>; + digestAlgorithm?: string; + authenticatedAttributes?: forge.asn1.Asn1[]; + signature?: string; + signatureAlgorithm?: forge.asn1.Asn1[]; }; }; @@ -161,21 +169,47 @@ export function validateSignature( } } + const signerInfoFields = extractSignerInfoFields(p7); + const digestOid = signerInfoFields?.digestOid; + result.algorithms = { - digest: getDigestAlgorithmName(signerCert.siginfo?.algorithmOid || ''), + digest: + (digestOid && getDigestAlgorithmName(digestOid)) || + getDigestAlgorithmName(signerCert.siginfo?.algorithmOid || ''), signature: getSignatureAlgorithmName(signerCert.signatureOid || ''), }; + if (digestOid && INSECURE_DIGEST_OIDS.has(digestOid)) { + result.usesInsecureDigest = true; + } + // Parse signing time if available in signature if (signature.signingTime) { result.signatureDate = new Date(signature.signingTime); } else { - // Try to extract from authenticated attributes try { - if (p7.rawCapture?.authenticatedAttributes) { - for (const attr of p7.rawCapture.authenticatedAttributes) { - if (attr.type === forge.pki.oids.signingTime) { - result.signatureDate = attr.value; + const attrs = p7.rawCapture?.authenticatedAttributes; + if (attrs) { + for (const attrNode of attrs) { + const attrChildren = attrNode.value; + if (!Array.isArray(attrChildren) || attrChildren.length < 2) + continue; + const oidNode = attrChildren[0]; + const setNode = attrChildren[1]; + if (!oidNode || oidNode.type !== forge.asn1.Type.OID) continue; + const oid = forge.asn1.derToOid(oidNode.value as string); + if (oid === forge.pki.oids.signingTime) { + const setValue = setNode?.value; + if (Array.isArray(setValue) && setValue[0]) { + const timeNode = setValue[0]; + const timeStr = timeNode.value as string; + if (typeof timeStr === 'string' && timeStr.length > 0) { + result.signatureDate = + timeNode.type === forge.asn1.Type.UTCTIME + ? forge.asn1.utcTimeToDate(timeStr) + : forge.asn1.generalizedTimeToDate(timeStr); + } + } break; } } @@ -190,7 +224,6 @@ export function validateSignature( if (signature.byteRange && signature.byteRange.length === 4) { const [, len1, start2, len2] = signature.byteRange; - const totalCovered = len1 + len2; const expectedEnd = start2 + len2; if (expectedEnd === pdfBytes.length) { @@ -200,7 +233,27 @@ export function validateSignature( } } - result.isValid = true; + const verification = await performCryptoVerification( + p7, + pdfBytes, + signature.byteRange, + signerCert, + signerInfoFields + ); + + result.cryptoVerified = verification.status === 'verified'; + result.cryptoVerificationStatus = verification.status; + if (verification.status === 'unsupported') { + result.unsupportedAlgorithmReason = verification.reason; + } else if (verification.status === 'failed') { + result.errorMessage = + verification.reason || 'Cryptographic verification failed'; + } + + result.isValid = + verification.status === 'verified' && + result.coverageStatus !== 'unknown' && + !result.usesInsecureDigest; } catch (e) { result.errorMessage = e instanceof Error ? e.message : 'Failed to parse signature'; @@ -214,7 +267,9 @@ export async function validatePdfSignatures( trustedCert?: forge.pki.Certificate ): Promise { const signatures = extractSignatures(pdfBytes); - return signatures.map((sig) => validateSignature(sig, pdfBytes, trustedCert)); + return Promise.all( + signatures.map((sig) => validateSignature(sig, pdfBytes, trustedCert)) + ); } export function countSignatures(pdfBytes: Uint8Array): number { @@ -262,3 +317,490 @@ function getSignatureAlgorithmName(oid: string): string { }; return signatureAlgorithms[oid] || oid || 'Unknown'; } + +interface SignerInfoFields { + digestOid: string; + authAttrs: forge.asn1.Asn1[] | null; + signatureBytes: string; +} + +function extractSignerInfoFields( + p7: forge.pkcs7.PkcsSignedData & { + rawCapture?: { + digestAlgorithm?: string; + authenticatedAttributes?: forge.asn1.Asn1[]; + signature?: string; + }; + } +): SignerInfoFields | null { + const rc = p7.rawCapture; + if (!rc) return null; + const digestAlgorithmBytes = rc.digestAlgorithm; + const signatureBytes = rc.signature; + if (typeof digestAlgorithmBytes !== 'string' || !signatureBytes) return null; + + return { + digestOid: forge.asn1.derToOid(digestAlgorithmBytes), + authAttrs: Array.isArray(rc.authenticatedAttributes) + ? rc.authenticatedAttributes + : null, + signatureBytes, + }; +} + +function createMd(digestOid: string): forge.md.MessageDigest | null { + switch (digestOid) { + case forge.pki.oids.sha256: + return forge.md.sha256.create(); + case forge.pki.oids.sha384: + return forge.md.sha384.create(); + case forge.pki.oids.sha512: + return forge.md.sha512.create(); + case forge.pki.oids.sha1: + return forge.md.sha1.create(); + case forge.pki.oids.md5: + return forge.md.md5.create(); + default: + return null; + } +} + +function uint8ToLatin1(bytes: Uint8Array): string { + let out = ''; + for (let i = 0; i < bytes.length; i++) { + out += String.fromCharCode(bytes[i]); + } + return out; +} + +type CryptoVerificationResult = + | { status: 'verified' } + | { status: 'failed'; reason: string } + | { status: 'unsupported'; reason: string }; + +interface SigScheme { + kind: 'rsa-pkcs1' | 'rsa-pss' | 'ecdsa' | 'rsa-raw'; + hashName: 'SHA-1' | 'SHA-256' | 'SHA-384' | 'SHA-512'; + pssSaltLength?: number; +} + +function latin1ToUint8(str: string): Uint8Array { + const out = new Uint8Array(str.length); + for (let i = 0; i < str.length; i++) out[i] = str.charCodeAt(i); + return out; +} + +function hashNameFromOid(oid: string): SigScheme['hashName'] | null { + switch (oid) { + case '1.3.14.3.2.26': + return 'SHA-1'; + case '2.16.840.1.101.3.4.2.1': + return 'SHA-256'; + case '2.16.840.1.101.3.4.2.2': + return 'SHA-384'; + case '2.16.840.1.101.3.4.2.3': + return 'SHA-512'; + default: + return null; + } +} + +function detectSigScheme( + signatureAlgorithmArr: forge.asn1.Asn1[] | undefined, + digestOid: string +): SigScheme | { unsupported: string } { + if (!signatureAlgorithmArr || signatureAlgorithmArr.length === 0) { + return { unsupported: 'Missing signatureAlgorithm' }; + } + const oidNode = signatureAlgorithmArr[0]; + if (!oidNode || oidNode.type !== forge.asn1.Type.OID) { + return { unsupported: 'Malformed signatureAlgorithm' }; + } + const oid = forge.asn1.derToOid(oidNode.value as string); + const implicitHash = hashNameFromOid(digestOid); + + switch (oid) { + case '1.2.840.113549.1.1.1': + return implicitHash + ? { kind: 'rsa-pkcs1', hashName: implicitHash } + : { unsupported: `Unsupported digest OID ${digestOid}` }; + case '1.2.840.113549.1.1.5': + return { kind: 'rsa-pkcs1', hashName: 'SHA-1' }; + case '1.2.840.113549.1.1.11': + return { kind: 'rsa-pkcs1', hashName: 'SHA-256' }; + case '1.2.840.113549.1.1.12': + return { kind: 'rsa-pkcs1', hashName: 'SHA-384' }; + case '1.2.840.113549.1.1.13': + return { kind: 'rsa-pkcs1', hashName: 'SHA-512' }; + case '1.2.840.113549.1.1.10': { + const params = parsePssParams(signatureAlgorithmArr[1]); + return { + kind: 'rsa-pss', + hashName: params.hashName, + pssSaltLength: params.saltLength, + }; + } + case '1.2.840.10045.4.1': + return { kind: 'ecdsa', hashName: 'SHA-1' }; + case '1.2.840.10045.4.3.2': + return { kind: 'ecdsa', hashName: 'SHA-256' }; + case '1.2.840.10045.4.3.3': + return { kind: 'ecdsa', hashName: 'SHA-384' }; + case '1.2.840.10045.4.3.4': + return { kind: 'ecdsa', hashName: 'SHA-512' }; + case '1.2.840.10045.2.1': + return implicitHash + ? { kind: 'ecdsa', hashName: implicitHash } + : { unsupported: `Unsupported digest OID ${digestOid}` }; + default: + return { unsupported: `Unsupported signature algorithm OID ${oid}` }; + } +} + +function parsePssParams(paramsNode: forge.asn1.Asn1 | undefined): { + hashName: SigScheme['hashName']; + saltLength: number; +} { + const fallback = { hashName: 'SHA-1' as const, saltLength: 20 }; + if (!paramsNode || !Array.isArray(paramsNode.value)) return fallback; + let hashName: SigScheme['hashName'] = 'SHA-1'; + let saltLength = 20; + for (const item of paramsNode.value) { + if (item.tagClass !== forge.asn1.Class.CONTEXT_SPECIFIC) continue; + if (item.type === 0 && Array.isArray(item.value) && item.value[0]) { + const algoIdSeq = item.value[0]; + if (Array.isArray(algoIdSeq.value) && algoIdSeq.value[0]) { + const hashOid = forge.asn1.derToOid(algoIdSeq.value[0].value as string); + const resolved = hashNameFromOid(hashOid); + if (resolved) hashName = resolved; + } + } else if (item.type === 2 && typeof item.value === 'string') { + let n = 0; + for (let i = 0; i < item.value.length; i++) { + n = (n << 8) | item.value.charCodeAt(i); + } + if (n > 0 && n < 1024) saltLength = n; + } + } + return { hashName, saltLength }; +} + +function extractSpkiDer( + p7: forge.pkcs7.PkcsSignedData & { + rawCapture?: { certificates?: forge.asn1.Asn1 }; + } +): Uint8Array | null { + try { + const certsNode = p7.rawCapture?.certificates; + if (!certsNode || !Array.isArray(certsNode.value) || !certsNode.value[0]) { + return null; + } + const certAsn1 = certsNode.value[0]; + if (!Array.isArray(certAsn1.value) || !certAsn1.value[0]) return null; + const tbs = certAsn1.value[0]; + if (!Array.isArray(tbs.value)) return null; + let startIdx = 0; + if ( + tbs.value[0] && + tbs.value[0].tagClass === forge.asn1.Class.CONTEXT_SPECIFIC + ) { + startIdx = 1; + } + const spkiAsn1 = tbs.value[startIdx + 5]; + if (!spkiAsn1) return null; + return latin1ToUint8(forge.asn1.toDer(spkiAsn1).getBytes()); + } catch { + return null; + } +} + +function curveFromSpki( + spkiDer: Uint8Array +): { name: 'P-256' | 'P-384' | 'P-521'; coordBytes: number } | null { + try { + const spki = forge.asn1.fromDer(uint8ToLatin1(spkiDer)); + if (!Array.isArray(spki.value) || !spki.value[0]) return null; + const algoId = spki.value[0]; + if (!Array.isArray(algoId.value) || !algoId.value[1]) return null; + const params = algoId.value[1]; + if (params.type !== forge.asn1.Type.OID) return null; + const oid = forge.asn1.derToOid(params.value as string); + if (oid === '1.2.840.10045.3.1.7') return { name: 'P-256', coordBytes: 32 }; + if (oid === '1.3.132.0.34') return { name: 'P-384', coordBytes: 48 }; + if (oid === '1.3.132.0.35') return { name: 'P-521', coordBytes: 66 }; + return null; + } catch { + return null; + } +} + +function ecdsaDerToP1363( + derSig: Uint8Array, + coordBytes: number +): Uint8Array | null { + try { + const parsed = forge.asn1.fromDer(uint8ToLatin1(derSig)); + if (!Array.isArray(parsed.value) || parsed.value.length !== 2) return null; + const r = latin1ToUint8(parsed.value[0].value as string); + const s = latin1ToUint8(parsed.value[1].value as string); + const rStripped = r[0] === 0 && r.length > 1 ? r.slice(1) : r; + const sStripped = s[0] === 0 && s.length > 1 ? s.slice(1) : s; + if (rStripped.length > coordBytes || sStripped.length > coordBytes) { + return null; + } + const out = new Uint8Array(coordBytes * 2); + out.set(rStripped, coordBytes - rStripped.length); + out.set(sStripped, coordBytes * 2 - sStripped.length); + return out; + } catch { + return null; + } +} + +async function verifyViaWebCrypto( + scheme: SigScheme, + spkiDer: Uint8Array, + signedBytes: Uint8Array, + signatureBytes: Uint8Array +): Promise { + const subtle = + typeof globalThis.crypto !== 'undefined' && globalThis.crypto.subtle + ? globalThis.crypto.subtle + : null; + if (!subtle) { + return { + status: 'unsupported', + reason: 'Web Crypto API not available in this context', + }; + } + + const spki = new Uint8Array(spkiDer); + const signed = new Uint8Array(signedBytes); + const sig = new Uint8Array(signatureBytes); + + try { + if (scheme.kind === 'rsa-pss') { + const key = await subtle.importKey( + 'spki', + spki, + { name: 'RSA-PSS', hash: scheme.hashName }, + false, + ['verify'] + ); + const ok = await subtle.verify( + { name: 'RSA-PSS', saltLength: scheme.pssSaltLength ?? 32 }, + key, + sig, + signed + ); + return ok + ? { status: 'verified' } + : { + status: 'failed', + reason: + 'RSA-PSS signature does not verify against signer public key', + }; + } + + if (scheme.kind === 'ecdsa') { + const curve = curveFromSpki(spki); + if (!curve) { + return { + status: 'unsupported', + reason: 'Unsupported ECDSA curve in signer certificate', + }; + } + const p1363 = ecdsaDerToP1363(sig, curve.coordBytes); + if (!p1363) { + return { + status: 'failed', + reason: 'Malformed ECDSA signature (could not parse r,s)', + }; + } + const key = await subtle.importKey( + 'spki', + spki, + { name: 'ECDSA', namedCurve: curve.name }, + false, + ['verify'] + ); + const ok = await subtle.verify( + { name: 'ECDSA', hash: scheme.hashName }, + key, + new Uint8Array(p1363), + signed + ); + return ok + ? { status: 'verified' } + : { + status: 'failed', + reason: 'ECDSA signature does not verify against signer public key', + }; + } + + if (scheme.kind === 'rsa-pkcs1') { + const key = await subtle.importKey( + 'spki', + spki, + { name: 'RSASSA-PKCS1-v1_5', hash: scheme.hashName }, + false, + ['verify'] + ); + const ok = await subtle.verify('RSASSA-PKCS1-v1_5', key, sig, signed); + return ok + ? { status: 'verified' } + : { + status: 'failed', + reason: + 'RSA-PKCS1 signature does not verify against signer public key', + }; + } + + return { + status: 'unsupported', + reason: `Signature scheme ${scheme.kind} not implemented`, + }; + } catch (e) { + return { + status: 'unsupported', + reason: + 'Web Crypto import/verify failed: ' + + (e instanceof Error ? e.message : String(e)), + }; + } +} + +async function performCryptoVerification( + p7: forge.pkcs7.PkcsSignedData & { + rawCapture?: { + signatureAlgorithm?: forge.asn1.Asn1[]; + certificates?: forge.asn1.Asn1; + }; + }, + pdfBytes: Uint8Array, + byteRange: number[], + signerCert: forge.pki.Certificate, + fields: SignerInfoFields | null +): Promise { + if (!fields) { + return { status: 'failed', reason: 'Could not parse signer info' }; + } + if (byteRange.length !== 4) { + return { status: 'failed', reason: 'Malformed ByteRange' }; + } + + const md = createMd(fields.digestOid); + if (!md) { + return { + status: 'unsupported', + reason: `Unsupported digest OID ${fields.digestOid}`, + }; + } + + const [start1, len1, start2, len2] = byteRange; + if ( + start1 < 0 || + len1 < 0 || + start2 < 0 || + len2 < 0 || + start1 + len1 > pdfBytes.length || + start2 + len2 > pdfBytes.length + ) { + return { status: 'failed', reason: 'ByteRange out of bounds' }; + } + + const signedContent = new Uint8Array(len1 + len2); + signedContent.set(pdfBytes.subarray(start1, start1 + len1), 0); + signedContent.set(pdfBytes.subarray(start2, start2 + len2), len1); + + md.update(uint8ToLatin1(signedContent)); + const contentHashBytes = md.digest().bytes(); + + const authAttrs = fields.authAttrs; + const signatureBytes = fields.signatureBytes; + if (!signatureBytes) { + return { status: 'failed', reason: 'Empty signature bytes' }; + } + + const scheme = detectSigScheme( + p7.rawCapture?.signatureAlgorithm, + fields.digestOid + ); + if ('unsupported' in scheme) { + return { status: 'unsupported', reason: scheme.unsupported }; + } + + let messageDigestAttrValue: string | null = null; + let signedBytesForVerify: Uint8Array; + + if (authAttrs) { + for (const attr of authAttrs) { + if (!attr.value || !Array.isArray(attr.value) || attr.value.length < 2) + continue; + const oidNode = attr.value[0]; + const setNode = attr.value[1]; + if (!oidNode || oidNode.type !== forge.asn1.Type.OID) continue; + const oid = forge.asn1.derToOid(oidNode.value as string); + if (oid === forge.pki.oids.messageDigest) { + if ( + setNode?.value && + Array.isArray(setNode.value) && + setNode.value[0] + ) { + messageDigestAttrValue = setNode.value[0].value as string; + } + break; + } + } + + if (messageDigestAttrValue === null) { + return { + status: 'failed', + reason: 'messageDigest attribute missing from authenticated attributes', + }; + } + if (messageDigestAttrValue !== contentHashBytes) { + return { + status: 'failed', + reason: + 'Content hash does not match messageDigest attribute — PDF was modified after signing', + }; + } + + const asSet = forge.asn1.create( + forge.asn1.Class.UNIVERSAL, + forge.asn1.Type.SET, + true, + authAttrs + ); + signedBytesForVerify = latin1ToUint8(forge.asn1.toDer(asSet).getBytes()); + } else { + signedBytesForVerify = signedContent; + } + + if (scheme.kind === 'rsa-pkcs1') { + try { + const publicKey = signerCert.publicKey as forge.pki.rsa.PublicKey; + const md2 = createMd(fields.digestOid)!; + md2.update(uint8ToLatin1(signedBytesForVerify)); + const ok = publicKey.verify(md2.digest().bytes(), signatureBytes); + if (ok) return { status: 'verified' }; + } catch { + // fall through to Web Crypto + } + } + + const spkiDer = extractSpkiDer(p7); + if (!spkiDer) { + return { + status: 'unsupported', + reason: 'Could not extract signer public key', + }; + } + return verifyViaWebCrypto( + scheme, + spkiDer, + signedBytesForVerify, + latin1ToUint8(signatureBytes) + ); +} diff --git a/src/js/logic/word-to-pdf.ts b/src/js/logic/word-to-pdf.ts index 7040814..2edb923 100644 --- a/src/js/logic/word-to-pdf.ts +++ b/src/js/logic/word-to-pdf.ts @@ -1,4 +1,5 @@ // NOTE: This is a work in progress and does not work correctly as of yet +import DOMPurify from 'dompurify'; import { showLoader, hideLoader, showAlert } from '../ui.js'; import { readFileAsArrayBuffer } from '../utils/helpers.js'; import { state } from '../state.js'; @@ -41,17 +42,20 @@ export async function wordToPdf() { const downloadBtn = document.getElementById('preview-download-btn'); const closeBtn = document.getElementById('preview-close-btn'); - const styledHtml = ` - - ${html} - `; - previewContent.innerHTML = styledHtml; + const STYLE_ID = 'word-to-pdf-preview-style'; + if (!document.getElementById(STYLE_ID)) { + const styleEl = document.createElement('style'); + styleEl.id = STYLE_ID; + styleEl.textContent = ` + #preview-content { font-family: 'Times New Roman', Times, serif; font-size: 12pt; line-height: 1.5; color: black; } + #preview-content table { border-collapse: collapse; width: 100%; } + #preview-content td, #preview-content th { border: 1px solid #dddddd; text-align: left; padding: 8px; } + #preview-content img { max-width: 100%; height: auto; } + #preview-content a { color: #0000ee; text-decoration: underline; } + `; + document.head.appendChild(styleEl); + } + previewContent.innerHTML = DOMPurify.sanitize(html); const marginDiv = document.createElement('div'); marginDiv.style.height = '100px'; diff --git a/src/js/main.ts b/src/js/main.ts index da8fc80..56664cb 100644 --- a/src/js/main.ts +++ b/src/js/main.ts @@ -5,7 +5,11 @@ import { createIcons, icons } from 'lucide'; import '@phosphor-icons/web/regular'; import * as pdfjsLib from 'pdfjs-dist'; import '../css/styles.css'; -import { formatShortcutDisplay, formatStars } from './utils/helpers.js'; +import { + escapeHtml, + formatShortcutDisplay, + formatStars, +} from './utils/helpers.js'; import { initI18n, applyTranslations, @@ -1051,8 +1055,8 @@ const init = async () => { await showWarningModal( t('settings.warnings.alreadyInUse'), - `${displayCombo} ${t('settings.warnings.assignedTo')}

` + - `"${translatedToolName}"

` + + `${escapeHtml(displayCombo)} ${t('settings.warnings.assignedTo')}

` + + `"${escapeHtml(translatedToolName)}"

` + t('settings.warnings.chooseDifferent'), false ); @@ -1071,8 +1075,8 @@ const init = async () => { const displayCombo = formatShortcutDisplay(combo, isMac); const shouldProceed = await showWarningModal( t('settings.warnings.reserved'), - `${displayCombo} ${t('settings.warnings.commonlyUsed')}

` + - `"${reservedWarning}"

` + + `${escapeHtml(displayCombo)} ${t('settings.warnings.commonlyUsed')}

` + + `"${escapeHtml(reservedWarning)}"

` + `${t('settings.warnings.unreliable')}

` + t('settings.warnings.useAnyway') ); diff --git a/src/js/sw-register.ts b/src/js/sw-register.ts index 1c21b7a..682490c 100644 --- a/src/js/sw-register.ts +++ b/src/js/sw-register.ts @@ -1,55 +1,111 @@ /** * Service Worker Registration * Registers the service worker to enable offline caching - * + * * Note: Service Worker is disabled in development mode to prevent * conflicts with Vite's HMR (Hot Module Replacement) */ // Skip service worker registration in development mode -const isDevelopment = window.location.hostname === 'localhost' || - window.location.hostname === '127.0.0.1' || - window.location.port !== ''; +const isDevelopment = + window.location.hostname === 'localhost' || + window.location.hostname === '127.0.0.1' || + window.location.port !== ''; + +function collectTrustedWasmHosts(): string[] { + const hosts = new Set(); + const candidates = [ + import.meta.env.VITE_WASM_PYMUPDF_URL, + import.meta.env.VITE_WASM_GS_URL, + import.meta.env.VITE_WASM_CPDF_URL, + import.meta.env.VITE_TESSERACT_WORKER_URL, + import.meta.env.VITE_TESSERACT_CORE_URL, + import.meta.env.VITE_TESSERACT_LANG_URL, + import.meta.env.VITE_OCR_FONT_BASE_URL, + ]; + for (const raw of candidates) { + if (!raw) continue; + try { + hosts.add(new URL(raw).origin); + } catch { + console.warn( + `[SW] Ignoring malformed VITE_* URL for SW trusted-hosts: ${raw}` + ); + } + } + return Array.from(hosts); +} + +function sendTrustedHostsToSw(target: ServiceWorker | null | undefined) { + if (!target) return; + const hosts = collectTrustedWasmHosts(); + if (hosts.length === 0) return; + target.postMessage({ type: 'SET_TRUSTED_CDN_HOSTS', hosts }); +} if (isDevelopment) { - console.log('[Dev Mode] Service Worker registration skipped in development'); - console.log('Service Worker will be active in production builds'); + console.log('[Dev Mode] Service Worker registration skipped in development'); + console.log('Service Worker will be active in production builds'); } else if ('serviceWorker' in navigator) { - window.addEventListener('load', () => { - const swPath = `${import.meta.env.BASE_URL}sw.js`; - console.log('[SW] Registering Service Worker at:', swPath); - navigator.serviceWorker - .register(swPath) - .then((registration) => { - console.log('[SW] Service Worker registered successfully:', registration.scope); + window.addEventListener('load', () => { + const swPath = `${import.meta.env.BASE_URL}sw.js`; + console.log('[SW] Registering Service Worker at:', swPath); + navigator.serviceWorker + .register(swPath) + .then((registration) => { + console.log( + '[SW] Service Worker registered successfully:', + registration.scope + ); - setInterval(() => { - registration.update(); - }, 24 * 60 * 60 * 1000); + sendTrustedHostsToSw( + registration.active || registration.waiting || registration.installing + ); - registration.addEventListener('updatefound', () => { - const newWorker = registration.installing; - if (newWorker) { - newWorker.addEventListener('statechange', () => { - if (newWorker.state === 'installed' && navigator.serviceWorker.controller) { - console.log('[SW] New version available! Reload to update.'); + setInterval( + () => { + registration.update(); + }, + 24 * 60 * 60 * 1000 + ); - if (confirm('A new version of BentoPDF is available. Reload to update?')) { - newWorker.postMessage({ type: 'SKIP_WAITING' }); - window.location.reload(); - } - } - }); - } - }); - }) - .catch((error) => { - console.error('[SW] Service Worker registration failed:', error); + registration.addEventListener('updatefound', () => { + const newWorker = registration.installing; + if (newWorker) { + newWorker.addEventListener('statechange', () => { + if (newWorker.state === 'activated') { + sendTrustedHostsToSw(newWorker); + } + if ( + newWorker.state === 'installed' && + navigator.serviceWorker.controller + ) { + console.log('[SW] New version available! Reload to update.'); + + if ( + confirm( + 'A new version of BentoPDF is available. Reload to update?' + ) + ) { + newWorker.postMessage({ type: 'SKIP_WAITING' }); + window.location.reload(); + } + } }); - - navigator.serviceWorker.addEventListener('controllerchange', () => { - console.log('[SW] New service worker activated, reloading...'); - window.location.reload(); + } }); + }) + .catch((error) => { + console.error('[SW] Service Worker registration failed:', error); + }); + + navigator.serviceWorker.ready.then((registration) => { + sendTrustedHostsToSw(registration.active); }); + + navigator.serviceWorker.addEventListener('controllerchange', () => { + console.log('[SW] New service worker activated, reloading...'); + window.location.reload(); + }); + }); } diff --git a/src/js/types/validate-signature-type.ts b/src/js/types/validate-signature-type.ts index 4633848..453f888 100644 --- a/src/js/types/validate-signature-type.ts +++ b/src/js/types/validate-signature-type.ts @@ -1,47 +1,51 @@ import forge from 'node-forge'; export interface SignatureValidationResult { - signatureIndex: number; - isValid: boolean; - signerName: string; - signerOrg?: string; - signerEmail?: string; - issuer: string; - issuerOrg?: string; - signatureDate?: Date; - validFrom: Date; - validTo: Date; - isExpired: boolean; - isSelfSigned: boolean; - isTrusted: boolean; - algorithms: { - digest: string; - signature: string; - }; - serialNumber: string; - reason?: string; - location?: string; - contactInfo?: string; - byteRange?: number[]; - coverageStatus: 'full' | 'partial' | 'unknown'; - errorMessage?: string; + signatureIndex: number; + isValid: boolean; + signerName: string; + signerOrg?: string; + signerEmail?: string; + issuer: string; + issuerOrg?: string; + signatureDate?: Date; + validFrom: Date; + validTo: Date; + isExpired: boolean; + isSelfSigned: boolean; + isTrusted: boolean; + algorithms: { + digest: string; + signature: string; + }; + serialNumber: string; + reason?: string; + location?: string; + contactInfo?: string; + byteRange?: number[]; + coverageStatus: 'full' | 'partial' | 'unknown'; + errorMessage?: string; + cryptoVerified?: boolean; + cryptoVerificationStatus?: 'verified' | 'failed' | 'unsupported'; + unsupportedAlgorithmReason?: string; + usesInsecureDigest?: boolean; } export interface ExtractedSignature { - index: number; - contents: Uint8Array; - byteRange: number[]; - reason?: string; - location?: string; - contactInfo?: string; - name?: string; - signingTime?: string; + index: number; + contents: Uint8Array; + byteRange: number[]; + reason?: string; + location?: string; + contactInfo?: string; + name?: string; + signingTime?: string; } export interface ValidateSignatureState { - pdfFile: File | null; - pdfBytes: Uint8Array | null; - results: SignatureValidationResult[]; - trustedCertFile: File | null; - trustedCert: forge.pki.Certificate | null; + pdfFile: File | null; + pdfBytes: Uint8Array | null; + results: SignatureValidationResult[]; + trustedCertFile: File | null; + trustedCert: forge.pki.Certificate | null; } diff --git a/src/js/utils/helpers.ts b/src/js/utils/helpers.ts index 5056ac7..ee57538 100644 --- a/src/js/utils/helpers.ts +++ b/src/js/utils/helpers.ts @@ -4,6 +4,7 @@ import { showLoader, hideLoader, showAlert } from '../ui.js'; import { createIcons } from 'lucide'; import { state, resetState } from '../state.js'; import * as pdfjsLib from 'pdfjs-dist'; +import DOMPurify from 'dompurify'; import type { DocumentInitParameters } from 'pdfjs-dist/types/src/display/api'; const STANDARD_SIZES = { @@ -319,19 +320,12 @@ export function uint8ArrayToBase64(bytes: Uint8Array): string { export function sanitizeEmailHtml(html: string): string { if (!html) return html; - let sanitized = html; + let sanitized = DOMPurify.sanitize(html, { + FORBID_TAGS: ['style', 'link', 'script', 'iframe', 'object', 'embed'], + FORBID_ATTR: ['style'], + ALLOW_DATA_ATTR: false, + }); - sanitized = sanitized.replace(/]*>[\s\S]*?<\/head>/gi, ''); - sanitized = sanitized.replace(/]*>[\s\S]*?<\/style>/gi, ''); - sanitized = sanitized.replace(/]*>[\s\S]*?<\/script>/gi, ''); - sanitized = sanitized.replace(/]*>/gi, ''); - sanitized = sanitized.replace(/\s+style=["'][^"']*["']/gi, ''); - sanitized = sanitized.replace(/\s+class=["'][^"']*["']/gi, ''); - sanitized = sanitized.replace(/\s+data-[a-z-]+=["'][^"']*["']/gi, ''); - sanitized = sanitized.replace( - /]*(?:width=["']1["'][^>]*height=["']1["']|height=["']1["'][^>]*width=["']1["'])[^>]*\/?>/gi, - '' - ); sanitized = sanitized.replace( /href=["']https?:\/\/[^"']*safelinks\.protection\.outlook\.com[^"']*url=([^&"']+)[^"']*["']/gi, (match, encodedUrl) => { @@ -343,10 +337,9 @@ export function sanitizeEmailHtml(html: string): string { } } ); - sanitized = sanitized.replace(/\s+originalsrc=["'][^"']*["']/gi, ''); sanitized = sanitized.replace( /href=["']([^"']{500,})["']/gi, - (match, url) => { + (_match, url: string) => { const baseUrl = url.split('?')[0]; if (baseUrl && baseUrl.length < 200) { return `href="${baseUrl}"`; @@ -354,15 +347,12 @@ export function sanitizeEmailHtml(html: string): string { return `href="${url.substring(0, 200)}"`; } ); - sanitized = sanitized.replace( - /\s+(cellpadding|cellspacing|bgcolor|border|valign|align|width|height|role|dir|id)=["'][^"']*["']/gi, + /]*(?:width=["']1["'][^>]*height=["']1["']|height=["']1["'][^>]*width=["']1["'])[^>]*\/?>/gi, '' ); sanitized = sanitized.replace(/<\/?table[^>]*>/gi, '
'); - sanitized = sanitized.replace(/<\/?tbody[^>]*>/gi, ''); - sanitized = sanitized.replace(/<\/?thead[^>]*>/gi, ''); - sanitized = sanitized.replace(/<\/?tfoot[^>]*>/gi, ''); + sanitized = sanitized.replace(/<\/?(tbody|thead|tfoot)[^>]*>/gi, ''); sanitized = sanitized.replace(/]*>/gi, '
'); sanitized = sanitized.replace(/<\/tr>/gi, '
'); sanitized = sanitized.replace(/]*>/gi, ' '); @@ -373,10 +363,6 @@ export function sanitizeEmailHtml(html: string): string { sanitized = sanitized.replace(/\s*<\/span>/gi, ''); sanitized = sanitized.replace(/(
)+/gi, '
'); sanitized = sanitized.replace(/(<\/div>)+/gi, '
'); - sanitized = sanitized.replace( - /]*href=["']\s*["'][^>]*>([^<]*)<\/a>/gi, - '$1' - ); const MAX_HTML_SIZE = 100000; if (sanitized.length > MAX_HTML_SIZE) { diff --git a/src/js/utils/markdown-editor.ts b/src/js/utils/markdown-editor.ts index 8fbbb25..6b892b5 100644 --- a/src/js/utils/markdown-editor.ts +++ b/src/js/utils/markdown-editor.ts @@ -1,4 +1,5 @@ import MarkdownIt from 'markdown-it'; +import DOMPurify from 'dompurify'; import hljs from 'highlight.js/lib/core'; import javascript from 'highlight.js/lib/languages/javascript'; import typescript from 'highlight.js/lib/languages/typescript'; @@ -297,7 +298,7 @@ export class MarkdownEditor { mermaid.initialize({ startOnLoad: false, theme: 'default', - securityLevel: 'loose', + securityLevel: 'strict', fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif', }); @@ -691,7 +692,9 @@ export class MarkdownEditor { const markdown = this.editor.value; const html = this.md.render(markdown); - this.preview.innerHTML = html; + this.preview.innerHTML = DOMPurify.sanitize(html, { + ADD_ATTR: ['target'], + }); this.renderMermaidDiagrams(); } @@ -714,7 +717,9 @@ export class MarkdownEditor { const wrapper = document.createElement('div'); wrapper.className = 'mermaid-diagram'; - wrapper.innerHTML = svg; + wrapper.innerHTML = DOMPurify.sanitize(svg, { + USE_PROFILES: { svg: true, svgFilters: true }, + }); pre.replaceWith(wrapper); } catch (error) { @@ -740,7 +745,9 @@ export class MarkdownEditor { } public getHtml(): string { - return this.md.render(this.getContent()); + return DOMPurify.sanitize(this.md.render(this.getContent()), { + ADD_ATTR: ['target'], + }); } private exportPdf(): void { diff --git a/src/js/utils/wasm-provider.ts b/src/js/utils/wasm-provider.ts index a6bb1c5..df10207 100644 --- a/src/js/utils/wasm-provider.ts +++ b/src/js/utils/wasm-provider.ts @@ -10,8 +10,8 @@ const STORAGE_KEY = 'bentopdf:wasm-providers'; const CDN_DEFAULTS: Record = { pymupdf: 'https://cdn.jsdelivr.net/npm/@bentopdf/pymupdf-wasm@0.11.16/', - ghostscript: 'https://cdn.jsdelivr.net/npm/@bentopdf/gs-wasm/assets/', - cpdf: 'https://cdn.jsdelivr.net/npm/coherentpdf/dist/', + ghostscript: 'https://cdn.jsdelivr.net/npm/@bentopdf/gs-wasm@0.1.1/assets/', + cpdf: 'https://cdn.jsdelivr.net/npm/coherentpdf@2.5.5/dist/', }; function envOrDefault(envVar: string | undefined, fallback: string): string { @@ -30,20 +30,77 @@ const ENV_DEFAULTS: Record = { cpdf: envOrDefault(import.meta.env.VITE_WASM_CPDF_URL, CDN_DEFAULTS.cpdf), }; +function hostnameOf(url: string): string | null { + try { + return new URL(url).hostname; + } catch { + return null; + } +} + +function collectBuiltinTrustedHosts(): Set { + const hosts = new Set(); + if (typeof location !== 'undefined' && location.hostname) { + hosts.add(location.hostname); + } + for (const url of Object.values(CDN_DEFAULTS)) { + const h = hostnameOf(url); + if (h) hosts.add(h); + } + for (const url of Object.values(ENV_DEFAULTS)) { + const h = hostnameOf(url); + if (h) hosts.add(h); + } + return hosts; +} + +const BUILTIN_TRUSTED_HOSTS = collectBuiltinTrustedHosts(); + class WasmProviderManager { private config: WasmProviderConfig; private validationCache: Map = new Map(); + private trustedHosts: Set = new Set(BUILTIN_TRUSTED_HOSTS); constructor() { this.config = this.loadConfig(); } + private isTrustedUrl(url: string): boolean { + const host = hostnameOf(url); + return !!host && this.trustedHosts.has(host); + } + private loadConfig(): WasmProviderConfig { try { const stored = localStorage.getItem(STORAGE_KEY); - if (stored) { - return JSON.parse(stored); + if (!stored) return {}; + const parsed = JSON.parse(stored) as WasmProviderConfig; + const safe: WasmProviderConfig = {}; + let dropped = false; + for (const key of ['pymupdf', 'ghostscript', 'cpdf'] as WasmPackage[]) { + const url = parsed[key]; + if (typeof url !== 'string') continue; + if (this.isTrustedUrl(url)) { + safe[key] = url; + } else { + dropped = true; + console.warn( + `[WasmProvider] Ignoring untrusted stored URL for ${key}: ${url}. ` + + 'Reconfigure via Advanced Settings to re-enable.' + ); + } } + if (dropped) { + try { + localStorage.setItem(STORAGE_KEY, JSON.stringify(safe)); + } catch (e) { + console.error( + '[WasmProvider] Failed to scrub untrusted config from localStorage:', + e + ); + } + } + return safe; } catch (e) { console.warn( '[WasmProvider] Failed to load config from localStorage:', @@ -66,11 +123,23 @@ class WasmProviderManager { } getUrl(packageName: WasmPackage): string | undefined { - return this.config[packageName] || this.getEnvDefault(packageName); + const stored = this.config[packageName]; + if (stored) { + if (this.isTrustedUrl(stored)) return stored; + console.warn( + `[WasmProvider] Refusing to use untrusted URL for ${packageName}; falling back to env default.` + ); + } + return this.getEnvDefault(packageName); } setUrl(packageName: WasmPackage, url: string): void { const normalizedUrl = url.endsWith('/') ? url : `${url}/`; + const host = hostnameOf(normalizedUrl); + if (!host) { + throw new Error('Invalid URL'); + } + this.trustedHosts.add(host); this.config[packageName] = normalizedUrl; this.validationCache.delete(packageName); this.saveConfig(); @@ -219,6 +288,7 @@ class WasmProviderManager { clearAll(): void { this.config = {}; this.validationCache.clear(); + this.trustedHosts = new Set(BUILTIN_TRUSTED_HOSTS); try { localStorage.removeItem(STORAGE_KEY); } catch (e) { diff --git a/src/pages/pdf-multi-tool.html b/src/pages/pdf-multi-tool.html index da35359..0928af2 100644 --- a/src/pages/pdf-multi-tool.html +++ b/src/pages/pdf-multi-tool.html @@ -375,7 +375,6 @@