diff --git a/docs/deployment/self-hosting.md b/docs/deployment/self-hosting.md
index 2522bad6..3b0f2129 100644
--- a/docs/deployment/self-hosting.md
+++ b/docs/deployment/self-hosting.md
@@ -14,6 +14,8 @@ byteflow.tools is a static Next.js export for privacy-first browser-local develo
- Container wrapper: use a minimal static server only for files; do not add an API that processes tool payloads.
- Local automation prototype: `scripts/prototypes/byteflow-local-cli.mjs` can be evaluated on a workstation or CI runner for stdin/stdout transformations. Keep it local to the runner; do not expose it as a hosted payload-processing endpoint.
+The export uses extensionless public routes backed by `.html` files. Configure the host's clean-URL or HTML fallback support so `/en/json-formatter` resolves to `/en/json-formatter.html`. `npm start` provides a production-like local static server with that behavior after `npm run build`; a file server that only performs literal path lookup is not compatible.
+
## Privacy-Safe Analytics
Analytics should be disabled unless the deployment owner has reviewed the allowlisted taxonomy. If enabled, only aggregate event names and safe identifiers such as tool ID, category, language, size bucket, or result count are allowed. Raw search text, tool input, output, file names, file contents, tokens, logs, full URLs, request bodies, and response bodies are forbidden.
diff --git a/docs/security/next-postcss-advisory-runbook.md b/docs/security/next-postcss-advisory-runbook.md
index 511c27dd..87565286 100644
--- a/docs/security/next-postcss-advisory-runbook.md
+++ b/docs/security/next-postcss-advisory-runbook.md
@@ -1,19 +1,22 @@
# Next Bundled PostCSS Advisory Runbook
-This runbook records the remediation for issue #7: GHSA-qx2v-qp2m-jg93 through
-Next.js bundled `postcss`.
+This runbook records the remediation for issue #7 and the later PostCSS source-map
+advisories GHSA-6g55-p6wh-862q and GHSA-r28c-9q8g-f849 through Next.js bundled
+`postcss`.
## Stable Remediation
-Checked on 2026-07-16:
+Checked on 2026-07-26:
-- `npm view next version`: `16.2.10`
-- `npm view @next/bundle-analyzer version`: `16.2.10`
+- `npm view next version`: `16.2.12`
+- `npm view @next/bundle-analyzer version`: `16.2.12`
- Stable Next still declares vulnerable `postcss@8.4.31`.
- The project keeps Next, `@next/bundle-analyzer`, and `eslint-config-next` aligned at
- stable `16.2.10`.
+ stable `16.2.12`.
- A controlled npm override replaces only Next's internal PostCSS copy with
- `postcss@8.5.10`, the first patched version already used by Next 16.3 canaries.
+ `postcss@8.5.23`, which is newer than the fixed `8.5.18` floor for the current advisories.
+- A second controlled override raises Next's optional Sharp dependency to `0.35.3`,
+ above the `0.35.0` floor for the inherited libvips advisories.
This avoids the unsafe framework downgrade proposed by `npm audit fix --force`, does
not suppress the advisory, and keeps the application on a stable Next release.
@@ -22,20 +25,24 @@ After `npm ci`, verify the effective dependency and production audit:
```bash
node -p "require('./node_modules/next/package.json').version"
-node -p "require('./node_modules/next/node_modules/postcss/package.json').version"
+node -p "require('./node_modules/postcss/package.json').version"
+node -p "require('./node_modules/sharp/package.json').version"
npm audit --omit=dev --json
```
Expected results:
-- Next reports `16.2.10`.
-- Next's effective PostCSS reports `8.5.10`, satisfying `postcss >= 8.5.10`.
+- Next reports `16.2.12`.
+- Next's effective PostCSS reports `8.5.23`, satisfying `postcss >= 8.5.18`.
+- Sharp reports `0.35.3`.
- The production audit reports 0 moderate/high/critical vulnerabilities and no
`next -> postcss` advisory path.
## Upstream Evidence
- Advisory: https://github.com/advisories/GHSA-qx2v-qp2m-jg93
+- Advisory: https://github.com/advisories/GHSA-6g55-p6wh-862q
+- Advisory: https://github.com/advisories/GHSA-r28c-9q8g-f849
- Next PostCSS 8.5.10 bump: https://github.com/vercel/next.js/pull/93288
- Next applicability discussion: https://github.com/vercel/next.js/issues/93234
@@ -63,7 +70,7 @@ npm run test:e2e:pwa
- Do not merge Next canary into `main` without an explicit project decision.
- Do not suppress the advisory without upstream evidence.
- Do not loosen production audit gates.
-- Do not remove the override until stable Next resolves PostCSS to `8.5.10` or newer
+- Do not remove the override until stable Next resolves PostCSS to `8.5.18` or newer
without it.
## Closure Comment Template
diff --git a/package-lock.json b/package-lock.json
index 38b9a6e2..a7620015 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -18,15 +18,15 @@
"cronstrue": "^3.12.0",
"crypto-js": "^4.2.0",
"csso": "^5.0.5",
- "dompurify": "3.4.11",
+ "dompurify": "3.4.12",
"jq-wasm": "^1.1.0-jq-1.8.1",
- "js-beautify": "^1.15.4",
+ "js-beautify": "2.0.3",
"jsbarcode": "^3.12.3",
"jsqr": "^1.4.0",
"jwt-decode": "^4.0.0",
"lorem-ipsum": "^2.0.8",
"lucide-react": "^0.575.0",
- "next": "16.2.10",
+ "next": "16.2.12",
"pdf-lib": "^1.17.1",
"qrcode": "^1.5.4",
"radix-ui": "^1.4.3",
@@ -50,7 +50,7 @@
"yaml": "^2.8.2"
},
"devDependencies": {
- "@next/bundle-analyzer": "16.2.10",
+ "@next/bundle-analyzer": "16.2.12",
"@tailwindcss/postcss": "^4",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
@@ -69,7 +69,7 @@
"@vitest/coverage-v8": "^4.1.8",
"axe-core": "^4.11.1",
"eslint": "^9",
- "eslint-config-next": "16.2.10",
+ "eslint-config-next": "16.2.12",
"jsdom": "^28.1.0",
"playwright": "^1.58.2",
"serve-handler": "^6.1.7",
@@ -199,6 +199,7 @@
"integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@babel/code-frame": "^7.29.0",
"@babel/generator": "^7.29.0",
@@ -571,6 +572,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
},
@@ -619,6 +621,7 @@
}
],
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=20.19.0"
}
@@ -646,9 +649,9 @@
}
},
"node_modules/@emnapi/runtime": {
- "version": "1.8.1",
- "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.8.1.tgz",
- "integrity": "sha512-mehfKSMWjjNol8659Z8KxEMrdSJDDot5SXMq00dM8BN4o+CLNXQ0xH2V7EchNHV4RmbZLmmPdEaXZc5H2FXmDg==",
+ "version": "1.11.3",
+ "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz",
+ "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==",
"license": "MIT",
"optional": true,
"dependencies": {
@@ -1361,9 +1364,9 @@
}
},
"node_modules/@img/colour": {
- "version": "1.0.0",
- "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.0.0.tgz",
- "integrity": "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw==",
+ "version": "1.1.0",
+ "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz",
+ "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==",
"license": "MIT",
"optional": true,
"engines": {
@@ -1371,9 +1374,9 @@
}
},
"node_modules/@img/sharp-darwin-arm64": {
- "version": "0.34.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz",
- "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==",
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.3.tgz",
+ "integrity": "sha512-RMnFX7YQsMoh7lWfcM4NEHHymBX/rLuKNPVM84XE9ONPcaSCDgE7CHIHpSgPcO2xcRthgBy1HfNO319mwhIAkg==",
"cpu": [
"arm64"
],
@@ -1383,19 +1386,19 @@
"darwin"
],
"engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ "node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
- "@img/sharp-libvips-darwin-arm64": "1.2.4"
+ "@img/sharp-libvips-darwin-arm64": "1.3.2"
}
},
"node_modules/@img/sharp-darwin-x64": {
- "version": "0.34.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz",
- "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==",
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.3.tgz",
+ "integrity": "sha512-Xo+5uFBtLN0BKqieTxiFzFPQAUlBbbH5iBKyRX/z1JrbnYsHTfKJnUfL8+p2TPXr1pXqao4eeL4Rl144uDpK9w==",
"cpu": [
"x64"
],
@@ -1405,19 +1408,38 @@
"darwin"
],
"engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ "node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
- "@img/sharp-libvips-darwin-x64": "1.2.4"
+ "@img/sharp-libvips-darwin-x64": "1.3.2"
+ }
+ },
+ "node_modules/@img/sharp-freebsd-wasm32": {
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.3.tgz",
+ "integrity": "sha512-lUxcqWIj2wMQ9BrwNjngcr1gWUr5xgaGThBRqPPalIC2n67Cqj1uPh8NnA/ZhAg8hUbKl+kVHKwgUIwe6ZYPrg==",
+ "license": "Apache-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "dependencies": {
+ "@img/sharp-wasm32": "0.35.3"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-libvips-darwin-arm64": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz",
- "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==",
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.2.tgz",
+ "integrity": "sha512-9J6ypZFpQBj4YnePGoq/S38w6nz+vqg5WZLrLGY4YuSemdMq47GMLBPO42MzwdGwpg/agZ7xzZcFHa48xlywfg==",
"cpu": [
"arm64"
],
@@ -1431,9 +1453,9 @@
}
},
"node_modules/@img/sharp-libvips-darwin-x64": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz",
- "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==",
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.2.tgz",
+ "integrity": "sha512-m2pW1n6cns9VaubNwsZ+c3CRYjxNQWgJ5gPlnL1nbBcpkBvFm6SCFN5o0psFHI8w9n11NKhFkeEDns98tiqbEw==",
"cpu": [
"x64"
],
@@ -1447,9 +1469,9 @@
}
},
"node_modules/@img/sharp-libvips-linux-arm": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz",
- "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==",
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.2.tgz",
+ "integrity": "sha512-1eMLzy92I4J6rmi4mAT8yC3HxOtniyGELlzGbNMLLeqe052ahFQ0h6LFq+lh5DsDIdYViIDst08abvSbcEdLXQ==",
"cpu": [
"arm"
],
@@ -1463,9 +1485,9 @@
}
},
"node_modules/@img/sharp-libvips-linux-arm64": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz",
- "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==",
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.2.tgz",
+ "integrity": "sha512-dqVSFynCox4C/J8kT16V7SIFAns0IjgLwkvYT7p8LQVmJ5OS5b6tI9IGflxTeuBS//zXeFIUbwt5dwxyZ17cnA==",
"cpu": [
"arm64"
],
@@ -1479,9 +1501,9 @@
}
},
"node_modules/@img/sharp-libvips-linux-ppc64": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz",
- "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==",
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.2.tgz",
+ "integrity": "sha512-3z0NHDxD6n5I9gc05U1eW1AyRm+Gznzq3naMrthPNqE6oYykcogW0l/jfpJdjYnuNl8R7yI9pNbE1XiUeyq0Aw==",
"cpu": [
"ppc64"
],
@@ -1495,9 +1517,9 @@
}
},
"node_modules/@img/sharp-libvips-linux-riscv64": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz",
- "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==",
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.2.tgz",
+ "integrity": "sha512-bsb4rI+NldGOsXuej2r8OdSS8+zXDVaCWxyWrcv6kneTOlgAHtZABRzBBCwdsPiD90J4myNJuHpg6kA20ImW/w==",
"cpu": [
"riscv64"
],
@@ -1511,9 +1533,9 @@
}
},
"node_modules/@img/sharp-libvips-linux-s390x": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz",
- "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==",
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.2.tgz",
+ "integrity": "sha512-/ABshyj8gCpyIrNXnHn4LorDJ0HHm1VhXPBlxZ8zAtfVPAaSafXPGn+sUSIRiwaSBy0mmFjSjiXI5mkcwdChKQ==",
"cpu": [
"s390x"
],
@@ -1527,9 +1549,9 @@
}
},
"node_modules/@img/sharp-libvips-linux-x64": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz",
- "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==",
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.2.tgz",
+ "integrity": "sha512-ITPEtgffGJ0S6G9dRyw/366tJQqFRcHWPHhC+Stpg3Z8AEMrDrTr2lhdz4f/Y/HMbRh//7Z5mBzEpVdi62Oc3w==",
"cpu": [
"x64"
],
@@ -1543,9 +1565,9 @@
}
},
"node_modules/@img/sharp-libvips-linuxmusl-arm64": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz",
- "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==",
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.2.tgz",
+ "integrity": "sha512-zE9EdiUzUmg5mDT5a1rk5fYJ6GWPloTwWBYDS14naqHsL+EaMpDj1AWnpLgh3u0YCORv2Tt50wrcrpYqkP97Kw==",
"cpu": [
"arm64"
],
@@ -1559,9 +1581,9 @@
}
},
"node_modules/@img/sharp-libvips-linuxmusl-x64": {
- "version": "1.2.4",
- "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz",
- "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==",
+ "version": "1.3.2",
+ "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.2.tgz",
+ "integrity": "sha512-m0lrLiUt+lBYnCFr8qV/65yMR4E/c7/wf78I5eKTdkEakFAlZ9QlzEM3QIhhAwVeUhLAHLcCq7a7Vszq/oFNZQ==",
"cpu": [
"x64"
],
@@ -1575,9 +1597,9 @@
}
},
"node_modules/@img/sharp-linux-arm": {
- "version": "0.34.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz",
- "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==",
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.3.tgz",
+ "integrity": "sha512-affVWCTLooy8TSxbDx2qkzuDeaWLNVBA+P//FNBirHsXpP2fuBhk5AuboYUnrDnzoXes8GFjpTx0SBFOCRg+FA==",
"cpu": [
"arm"
],
@@ -1587,19 +1609,19 @@
"linux"
],
"engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ "node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
- "@img/sharp-libvips-linux-arm": "1.2.4"
+ "@img/sharp-libvips-linux-arm": "1.3.2"
}
},
"node_modules/@img/sharp-linux-arm64": {
- "version": "0.34.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz",
- "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==",
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.3.tgz",
+ "integrity": "sha512-QgKDspHPnrU+GQ55XPhGwyhC8acLVOOSyAvo1oVfFmrIXLkDNmGWzAfDZ4xK8oSA1qBQrALcHX0G5UZni/SuFQ==",
"cpu": [
"arm64"
],
@@ -1609,19 +1631,19 @@
"linux"
],
"engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ "node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
- "@img/sharp-libvips-linux-arm64": "1.2.4"
+ "@img/sharp-libvips-linux-arm64": "1.3.2"
}
},
"node_modules/@img/sharp-linux-ppc64": {
- "version": "0.34.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz",
- "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==",
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.3.tgz",
+ "integrity": "sha512-sMd8rDxmpLOwv/7N44klFjOD5DUO7FLdjiXDI0hoxYaf7Ar262dQIEkosE98bps+5HPLtp/EvNqeqQtOycP/IA==",
"cpu": [
"ppc64"
],
@@ -1631,19 +1653,19 @@
"linux"
],
"engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ "node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
- "@img/sharp-libvips-linux-ppc64": "1.2.4"
+ "@img/sharp-libvips-linux-ppc64": "1.3.2"
}
},
"node_modules/@img/sharp-linux-riscv64": {
- "version": "0.34.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz",
- "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==",
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.3.tgz",
+ "integrity": "sha512-0Eob78yjlYPfL5vMNWAW55l3R9Y6BQS/gOfe0ZcP9mEz9ohhKSt4im1hayiknXgf8AWrFqMvJcKIdmLmEe7yeQ==",
"cpu": [
"riscv64"
],
@@ -1653,19 +1675,19 @@
"linux"
],
"engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ "node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
- "@img/sharp-libvips-linux-riscv64": "1.2.4"
+ "@img/sharp-libvips-linux-riscv64": "1.3.2"
}
},
"node_modules/@img/sharp-linux-s390x": {
- "version": "0.34.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz",
- "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==",
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.3.tgz",
+ "integrity": "sha512-KgAxQ0DxpNOq1rG2t5cgTgShJFGSuU7XO45cqC+1NVOuZnP6tlgZRuSYOfNupGkHID0o3cJOsw4DVeJpMovcGw==",
"cpu": [
"s390x"
],
@@ -1675,19 +1697,19 @@
"linux"
],
"engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ "node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
- "@img/sharp-libvips-linux-s390x": "1.2.4"
+ "@img/sharp-libvips-linux-s390x": "1.3.2"
}
},
"node_modules/@img/sharp-linux-x64": {
- "version": "0.34.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz",
- "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==",
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.3.tgz",
+ "integrity": "sha512-8pqvxubL2PGdhlPy6GLqzDYMUjyRmKAwKHYKixpdJYBUK7PJ0C029XdsnpFIdgRZG68fZiGdHVWcKPvtiPB4cA==",
"cpu": [
"x64"
],
@@ -1697,19 +1719,19 @@
"linux"
],
"engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ "node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
- "@img/sharp-libvips-linux-x64": "1.2.4"
+ "@img/sharp-libvips-linux-x64": "1.3.2"
}
},
"node_modules/@img/sharp-linuxmusl-arm64": {
- "version": "0.34.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz",
- "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==",
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.3.tgz",
+ "integrity": "sha512-Vz0iQjzzcSX3HCbfwFfCSG/9SCIqyO0mH2sXyiHaAYfBk0cRsCWXRyQYX0ovCK/PAQBbTzQ0dsPQHh5MAFL59w==",
"cpu": [
"arm64"
],
@@ -1719,19 +1741,19 @@
"linux"
],
"engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ "node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
- "@img/sharp-libvips-linuxmusl-arm64": "1.2.4"
+ "@img/sharp-libvips-linuxmusl-arm64": "1.3.2"
}
},
"node_modules/@img/sharp-linuxmusl-x64": {
- "version": "0.34.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz",
- "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==",
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.3.tgz",
+ "integrity": "sha512-6O1NPKcDVj9QEdg7Hx549EX8U0rp6yXQERqru6yRN7fGBn32UvIRJUlWnk+8xDCiG76hXVBbX82NZ/ZKr0euIg==",
"cpu": [
"x64"
],
@@ -1741,38 +1763,54 @@
"linux"
],
"engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ "node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
- "@img/sharp-libvips-linuxmusl-x64": "1.2.4"
+ "@img/sharp-libvips-linuxmusl-x64": "1.3.2"
}
},
"node_modules/@img/sharp-wasm32": {
- "version": "0.34.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz",
- "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==",
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.3.tgz",
+ "integrity": "sha512-cZ0XkcYGpHZkqW6iCkqTcmUC0CD9DhD5d/qeZlZkfRBn6GnHniZXLUo5+9xw8Iv76YE6LQFN9YNBlKREcCG76w==",
+ "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
+ "optional": true,
+ "dependencies": {
+ "@emnapi/runtime": "^1.11.1"
+ },
+ "engines": {
+ "node": ">=20.9.0"
+ },
+ "funding": {
+ "url": "https://opencollective.com/libvips"
+ }
+ },
+ "node_modules/@img/sharp-webcontainers-wasm32": {
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.3.tgz",
+ "integrity": "sha512-2rnq7bX3NzeR2T4YWgz8qiG4h3TSdMe+vN1iQXpJleSJ3SM5zQ8Fy2SyyXAWlbxpEZ2Y+Z4u1BePgJEYbSy80Q==",
"cpu": [
"wasm32"
],
- "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT",
+ "license": "Apache-2.0",
"optional": true,
"dependencies": {
- "@emnapi/runtime": "^1.7.0"
+ "@img/sharp-wasm32": "0.35.3"
},
"engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ "node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-arm64": {
- "version": "0.34.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz",
- "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==",
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.3.tgz",
+ "integrity": "sha512-4bPwFdMbeC4JQ8L8LOyWp6nsHcboP5fxkp6iPOXz2Vg49R42TuMs2whkJ5OAP4/Ul035qOzy0AecOF9VOscn4w==",
"cpu": [
"arm64"
],
@@ -1782,16 +1820,16 @@
"win32"
],
"engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ "node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-ia32": {
- "version": "0.34.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz",
- "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==",
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.3.tgz",
+ "integrity": "sha512-r53mXsBN6lFUDiST764SvgwUdHAqM4rPAiDzAmf4fLoB6X/rkfyTrLCg6+g17wJJiCmB3JYgHuUldCWUIRFSXw==",
"cpu": [
"ia32"
],
@@ -1801,16 +1839,16 @@
"win32"
],
"engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ "node": "^20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
"node_modules/@img/sharp-win32-x64": {
- "version": "0.34.5",
- "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz",
- "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==",
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.3.tgz",
+ "integrity": "sha512-D4y1vNeZrIIJCN+uHaWVtH86B+aCrdMYYjicy9pXHvbGZeGYLLSd3wdVuC37FxVXlU1ARsk84eKWfWMXGYEqvA==",
"cpu": [
"x64"
],
@@ -1820,75 +1858,12 @@
"win32"
],
"engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ "node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
}
},
- "node_modules/@isaacs/cliui": {
- "version": "8.0.2",
- "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz",
- "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==",
- "license": "ISC",
- "dependencies": {
- "string-width": "^5.1.2",
- "string-width-cjs": "npm:string-width@^4.2.0",
- "strip-ansi": "^7.0.1",
- "strip-ansi-cjs": "npm:strip-ansi@^6.0.1",
- "wrap-ansi": "^8.1.0",
- "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0"
- },
- "engines": {
- "node": ">=12"
- }
- },
- "node_modules/@isaacs/cliui/node_modules/ansi-styles": {
- "version": "6.2.3",
- "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz",
- "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==",
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-styles?sponsor=1"
- }
- },
- "node_modules/@isaacs/cliui/node_modules/string-width": {
- "version": "5.1.2",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz",
- "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==",
- "license": "MIT",
- "dependencies": {
- "eastasianwidth": "^0.2.0",
- "emoji-regex": "^9.2.2",
- "strip-ansi": "^7.0.1"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/sponsors/sindresorhus"
- }
- },
- "node_modules/@isaacs/cliui/node_modules/wrap-ansi": {
- "version": "8.1.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz",
- "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==",
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^6.1.0",
- "string-width": "^5.0.1",
- "strip-ansi": "^7.0.1"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
- }
- },
"node_modules/@jridgewell/gen-mapping": {
"version": "0.3.13",
"resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
@@ -1988,9 +1963,9 @@
}
},
"node_modules/@next/bundle-analyzer": {
- "version": "16.2.10",
- "resolved": "https://registry.npmjs.org/@next/bundle-analyzer/-/bundle-analyzer-16.2.10.tgz",
- "integrity": "sha512-KcepWhb3IVniZgm00GSSCQDEUQqZXuXtuXRh8J6e3Un342TcQ77iK4DedeEkct+fcx7yFEDL2J6z4Jeho5JDAw==",
+ "version": "16.2.12",
+ "resolved": "https://registry.npmjs.org/@next/bundle-analyzer/-/bundle-analyzer-16.2.12.tgz",
+ "integrity": "sha512-0dYhmCYsTMFYUFaoEUx+VKw/oYP6b3XiGgq47EujXTE2UGgwVWAaur2tbko2pxfUka3WRZ9YWxa3PlSv3luWNQ==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1998,15 +1973,15 @@
}
},
"node_modules/@next/env": {
- "version": "16.2.10",
- "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.10.tgz",
- "integrity": "sha512-zLPxg9M0MEHmygpj5OuxjQ+vHMiy/K7cSp74G8ecYolmgUWw0RwN02tF56npup/+qaI8JB97hQgS/r2Hb6QwVA==",
+ "version": "16.2.12",
+ "resolved": "https://registry.npmjs.org/@next/env/-/env-16.2.12.tgz",
+ "integrity": "sha512-d0Z5Bc13Fa4nR8pFAKx2jay2yhJM16vlfHbTzYnUQAxlNb6B6lmn4hjt69lYNt4kRtyYP6gEM49lPRHNbIyneg==",
"license": "MIT"
},
"node_modules/@next/eslint-plugin-next": {
- "version": "16.2.10",
- "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.2.10.tgz",
- "integrity": "sha512-Gs8D2m21VnJeFo9qvYIIqJH94frWerWYu41BprU1pLtRVF7PCQNLiFZZ3fG+iPuj3K83Cwv/rt+msLOy8Qgu3Q==",
+ "version": "16.2.12",
+ "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-16.2.12.tgz",
+ "integrity": "sha512-uF2z/qAK2q7B5/6CpnFcBRX6jOq5iCO+Uqh1UkJhXljX1JwLarLYhhoJadO6dPb6moTprOKewMXheBcbIoSbug==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -2014,9 +1989,9 @@
}
},
"node_modules/@next/swc-darwin-arm64": {
- "version": "16.2.10",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.10.tgz",
- "integrity": "sha512-v9IdJCa0H0mbo+8z5zwUpOk1Vj7RjkcI5uNYf5Ws1y6szf/p3Mzl9hLaST8SCt6L9h8NGnruZcd2+o0NTNwDhA==",
+ "version": "16.2.12",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-16.2.12.tgz",
+ "integrity": "sha512-0W1R0teHWJrqKX0FH20IzzIWAOuGtBxPGuObrxy1lE8hQvCFj49KE8a3WUg0D7sq6rn6zkM4c7YGUnhudBS6oA==",
"cpu": [
"arm64"
],
@@ -2030,9 +2005,9 @@
}
},
"node_modules/@next/swc-darwin-x64": {
- "version": "16.2.10",
- "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.10.tgz",
- "integrity": "sha512-17IS0jJRViROGmA9uGdNR8VPJpfbnaVG7E9qhso5jDLkmyd0lSDORWxbcKINzcFqzZqGwGtMSnrFRxBpuUYjLQ==",
+ "version": "16.2.12",
+ "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-16.2.12.tgz",
+ "integrity": "sha512-Hy5Ls099+aFUmOLmIgPfLqNi6iCwhL3uQCssz5rWk+5Nkc6TUKCE83DY5BbNylfm3+mfwcSFnLRfrZDJhVxdtw==",
"cpu": [
"x64"
],
@@ -2046,9 +2021,9 @@
}
},
"node_modules/@next/swc-linux-arm64-gnu": {
- "version": "16.2.10",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.10.tgz",
- "integrity": "sha512-GRQRsRtuciNJvB54AvvuQTiq0oZtFwa1owQqtZD8wwnGpM2L39MV22kpI72YSXLKIyY40LC66EiLFv4PiicXxg==",
+ "version": "16.2.12",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-16.2.12.tgz",
+ "integrity": "sha512-+YqU2h1cQkHsGfvjAsrSmst8UIFBibBGm5x3Xgel8NLMiDQtNOM4sM2GOEMvG5YiOBNeN/Ykk8cQC2S0Xrqljg==",
"cpu": [
"arm64"
],
@@ -2062,9 +2037,9 @@
}
},
"node_modules/@next/swc-linux-arm64-musl": {
- "version": "16.2.10",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.10.tgz",
- "integrity": "sha512-zkN9MQYS7UQBro+FnISUq1itaQjXI9xqISzuQ+2bc921NcJ1x4yPCqrn77tVN6/dOOXaaWVX3k6/bR07pPwK+A==",
+ "version": "16.2.12",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-16.2.12.tgz",
+ "integrity": "sha512-0qjhiYBaKAqF63LA1ZWAAnKTzFUguAaZiRa5etMLGGPj/B6uEVjtIZldIzFEp3wHlB0koK6aTzqPtSdplTCjoA==",
"cpu": [
"arm64"
],
@@ -2078,9 +2053,9 @@
}
},
"node_modules/@next/swc-linux-x64-gnu": {
- "version": "16.2.10",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.10.tgz",
- "integrity": "sha512-iCVJnwvrPYECvA6WM/7+oo+OiTvedIKLxtCLAZP4xZR3nXa1zmzZyLPbYCmWvpd4CvMYF1EMTafd0ii3DygLvA==",
+ "version": "16.2.12",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-16.2.12.tgz",
+ "integrity": "sha512-7A3q26W+h7gnA15uqBToNuDqBEFZZcqh0mW2mn4AJh/G5pdg2RVE3n4slzLEliASZFG3NmsbEzng/x2Sh09mBg==",
"cpu": [
"x64"
],
@@ -2094,9 +2069,9 @@
}
},
"node_modules/@next/swc-linux-x64-musl": {
- "version": "16.2.10",
- "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.10.tgz",
- "integrity": "sha512-ov2g4H0dHY9bPoOU83m91hWT7Iq5qy13bUnyyshLU3HGR1Ownn0X9QpmDPc5iIUaahTp7f7LeGAhV4DSFtackw==",
+ "version": "16.2.12",
+ "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-16.2.12.tgz",
+ "integrity": "sha512-qSjL/uppm+cbh21s72Ss8gkiOhQ4dExWHNGOWy6eZV7STj5WsKehgxT61beSsOj+YYQuTplL376lOCdMQU5T8w==",
"cpu": [
"x64"
],
@@ -2110,9 +2085,9 @@
}
},
"node_modules/@next/swc-win32-arm64-msvc": {
- "version": "16.2.10",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.10.tgz",
- "integrity": "sha512-DwAnhLX76HQiFFQNgWlcK+JzlnD1rZ+UK/WY0ZMI/deXpvgnesjNYrqcfo1JzBuz4Kf7o3brIBL0glI1junatA==",
+ "version": "16.2.12",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-16.2.12.tgz",
+ "integrity": "sha512-X6hzsOUJac/e7AWSbn9gQ9nzHld1xWP5iyjHpYWvud8pufB679O1xg4JDyKr8Xd69Jvd+kM2Der6uftiZCmjYA==",
"cpu": [
"arm64"
],
@@ -2126,9 +2101,9 @@
}
},
"node_modules/@next/swc-win32-x64-msvc": {
- "version": "16.2.10",
- "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.10.tgz",
- "integrity": "sha512-0JXq3b85Jk9Jg4ntLUbXSPvoDw3gpZou7twuKdoFG2jOw635v7+IiXfTaa0TxVMyx78pUjnrVYwLgjKfX4e6/A==",
+ "version": "16.2.12",
+ "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-16.2.12.tgz",
+ "integrity": "sha512-F6fakeHuFTLOPt0bslQJdf+xtT+WIP9DVn/m4y1w1mRnVPyh3D/cNvzlRkxM444xfm+IvvYNSOrKiA2CDJ0Uxw==",
"cpu": [
"x64"
],
@@ -2213,16 +2188,6 @@
"pako": "^1.0.10"
}
},
- "node_modules/@pkgjs/parseargs": {
- "version": "0.11.0",
- "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz",
- "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==",
- "license": "MIT",
- "optional": true,
- "engines": {
- "node": ">=14"
- }
- },
"node_modules/@polka/url": {
"version": "1.0.0-next.29",
"resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz",
@@ -4474,7 +4439,6 @@
"integrity": "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"@babel/code-frame": "^7.10.4",
"@babel/runtime": "^7.12.5",
@@ -4495,7 +4459,6 @@
"integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==",
"dev": true,
"license": "Apache-2.0",
- "peer": true,
"dependencies": {
"dequal": "^2.0.3"
}
@@ -4571,8 +4534,7 @@
"resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz",
"integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==",
"dev": true,
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/@types/babel__core": {
"version": "7.20.5",
@@ -4772,6 +4734,7 @@
"resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
"integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"csstype": "^3.2.2"
}
@@ -4782,6 +4745,7 @@
"integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"devOptional": true,
"license": "MIT",
+ "peer": true,
"peerDependencies": {
"@types/react": "^19.2.0"
}
@@ -4865,6 +4829,7 @@
"integrity": "sha512-klQbnPAAiGYFyI02+znpBRLyjL4/BrBd0nyWkdC0s/6xFLkXYQ8OoRrSkqacS1ddVxf/LDyODIKbQ5TgKAf/Fg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@typescript-eslint/scope-manager": "8.56.1",
"@typescript-eslint/types": "8.56.1",
@@ -5019,16 +4984,16 @@
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": {
- "version": "5.0.6",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
- "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
+ "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
"dev": true,
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
- "node": "18 || 20 || >=22"
+ "node": "20 || >=22"
}
},
"node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": {
@@ -5417,6 +5382,7 @@
"integrity": "sha512-lt3kovsyHwYe00wq4D1ti0Z974fWj4NLp6siqiyEufUpyFwK9Yhi7rBhac9JL5aA0zoMrJqc4vYPZRUnI7l7nw==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@bcoe/v8-coverage": "^1.0.2",
"@vitest/utils": "4.1.8",
@@ -5555,20 +5521,12 @@
"url": "https://opencollective.com/vitest"
}
},
- "node_modules/abbrev": {
- "version": "2.0.0",
- "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz",
- "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==",
- "license": "ISC",
- "engines": {
- "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
- }
- },
"node_modules/acorn": {
"version": "8.16.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
"integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
"license": "MIT",
+ "peer": true,
"bin": {
"acorn": "bin/acorn"
},
@@ -5626,18 +5584,6 @@
"url": "https://github.com/sponsors/epoberezkin"
}
},
- "node_modules/ansi-regex": {
- "version": "6.2.2",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz",
- "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==",
- "license": "MIT",
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/ansi-regex?sponsor=1"
- }
- },
"node_modules/ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@@ -5937,6 +5883,7 @@
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
+ "dev": true,
"license": "MIT"
},
"node_modules/baseline-browser-mapping": {
@@ -5962,9 +5909,9 @@
}
},
"node_modules/brace-expansion": {
- "version": "1.1.15",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz",
- "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==",
+ "version": "1.1.16",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.16.tgz",
+ "integrity": "sha512-IDw48K2/2kRkg9LdJxurvq3lV3aBgq0REY89duEqFRthjlPdXHKMj7EnQOXVckxzgisinf3nHfrcE2FufFLXMw==",
"dev": true,
"license": "MIT",
"dependencies": {
@@ -6005,6 +5952,7 @@
}
],
"license": "MIT",
+ "peer": true,
"dependencies": {
"baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759",
@@ -6363,6 +6311,7 @@
"version": "7.0.6",
"resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
"integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"path-key": "^3.1.0",
@@ -6736,13 +6685,12 @@
"resolved": "https://registry.npmjs.org/dom-accessibility-api/-/dom-accessibility-api-0.5.16.tgz",
"integrity": "sha512-X7BJ2yElsnOJ30pZF4uIIDfBEVgF4XEBxL9Bxhy6dnrm5hkzqmsWHGTiHqRiITNhMyFLyAiWndIJP7Z1NTteDg==",
"dev": true,
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/dompurify": {
- "version": "3.4.11",
- "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.11.tgz",
- "integrity": "sha512-zhlUV12GsaRzMsf9q5M254YhA4+VuF0fG+QFqu6aYpoGlKtz+w8//jBcGVYBgQkR5GHjUomejY84AV+/uPbWdw==",
+ "version": "3.4.12",
+ "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.4.12.tgz",
+ "integrity": "sha512-zQvGet8Z2sWbQhCmfFz/T5QWH2oBmjnqK3qvOjaqaNLrLEF912WamU+ohnTp0TCep/MFVHpdJuCZEdFOdTnEFg==",
"license": "(MPL-2.0 OR Apache-2.0)",
"optionalDependencies": {
"@types/trusted-types": "^2.0.7"
@@ -6770,12 +6718,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/eastasianwidth": {
- "version": "0.2.0",
- "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz",
- "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
- "license": "MIT"
- },
"node_modules/editorconfig": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-3.0.2.tgz",
@@ -6804,15 +6746,15 @@
}
},
"node_modules/editorconfig/node_modules/brace-expansion": {
- "version": "5.0.6",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz",
- "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==",
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
+ "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
"license": "MIT",
"dependencies": {
"balanced-match": "^4.0.2"
},
"engines": {
- "node": "18 || 20 || >=22"
+ "node": "20 || >=22"
}
},
"node_modules/editorconfig/node_modules/minimatch": {
@@ -6853,6 +6795,7 @@
"version": "9.2.2",
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==",
+ "dev": true,
"license": "MIT"
},
"node_modules/enhanced-resolve": {
@@ -7136,6 +7079,7 @@
"integrity": "sha512-VmQ+sifHUbI/IcSopBCF/HO3YiHQx/AVd3UVyYL6weuwW+HvON9VYn5l6Zl1WZzPWXPNZrSQpxwkkZ/VuvJZzg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@eslint-community/eslint-utils": "^4.8.0",
"@eslint-community/regexpp": "^4.12.1",
@@ -7191,13 +7135,13 @@
}
},
"node_modules/eslint-config-next": {
- "version": "16.2.10",
- "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.2.10.tgz",
- "integrity": "sha512-HSybLOY0QKf39i4FWUqPN0xWiNDi6A6UqJmZtgDkS3zMqjXTqULvj/sueXx3cdCG0mVG+qH6k5/qdegklH1d1w==",
+ "version": "16.2.12",
+ "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-16.2.12.tgz",
+ "integrity": "sha512-iaaf4vvKo5h2LBdGt0JuRv7t0Ysqr9FMCiFxbptDg8LqOE//mIKR80DdpOnSVM7qjLH3jT8P0aFiwXxBEGZRXw==",
"dev": true,
"license": "MIT",
"dependencies": {
- "@next/eslint-plugin-next": "16.2.10",
+ "@next/eslint-plugin-next": "16.2.12",
"eslint-import-resolver-node": "^0.3.6",
"eslint-import-resolver-typescript": "^3.5.2",
"eslint-plugin-import": "^2.32.0",
@@ -7321,6 +7265,7 @@
"integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@rtsao/scc": "^1.1.0",
"array-includes": "^3.1.9",
@@ -7737,22 +7682,6 @@
"url": "https://github.com/sponsors/ljharb"
}
},
- "node_modules/foreground-child": {
- "version": "3.3.1",
- "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz",
- "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==",
- "license": "ISC",
- "dependencies": {
- "cross-spawn": "^7.0.6",
- "signal-exit": "^4.0.1"
- },
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -7918,21 +7847,17 @@
}
},
"node_modules/glob": {
- "version": "10.5.0",
- "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
- "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
- "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me",
- "license": "ISC",
+ "version": "13.0.6",
+ "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz",
+ "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==",
+ "license": "BlueOak-1.0.0",
"dependencies": {
- "foreground-child": "^3.1.0",
- "jackspeak": "^3.1.2",
- "minimatch": "^9.0.4",
- "minipass": "^7.1.2",
- "package-json-from-dist": "^1.0.0",
- "path-scurry": "^1.11.1"
+ "minimatch": "^10.2.2",
+ "minipass": "^7.1.3",
+ "path-scurry": "^2.0.2"
},
- "bin": {
- "glob": "dist/esm/bin.mjs"
+ "engines": {
+ "node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
@@ -7951,25 +7876,37 @@
"node": ">=10.13.0"
}
},
+ "node_modules/glob/node_modules/balanced-match": {
+ "version": "4.0.4",
+ "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz",
+ "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==",
+ "license": "MIT",
+ "engines": {
+ "node": "18 || 20 || >=22"
+ }
+ },
"node_modules/glob/node_modules/brace-expansion": {
- "version": "2.1.1",
- "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz",
- "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==",
+ "version": "5.0.8",
+ "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz",
+ "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==",
"license": "MIT",
"dependencies": {
- "balanced-match": "^1.0.0"
+ "balanced-match": "^4.0.2"
+ },
+ "engines": {
+ "node": "20 || >=22"
}
},
"node_modules/glob/node_modules/minimatch": {
- "version": "9.0.9",
- "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz",
- "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==",
- "license": "ISC",
+ "version": "10.2.5",
+ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz",
+ "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==",
+ "license": "BlueOak-1.0.0",
"dependencies": {
- "brace-expansion": "^2.0.2"
+ "brace-expansion": "^5.0.5"
},
"engines": {
- "node": ">=16 || 14 >=14.17"
+ "node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
@@ -9009,6 +8946,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
"integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "dev": true,
"license": "ISC"
},
"node_modules/istanbul-lib-coverage": {
@@ -9068,21 +9006,6 @@
"node": ">= 0.4"
}
},
- "node_modules/jackspeak": {
- "version": "3.4.3",
- "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz",
- "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==",
- "license": "BlueOak-1.0.0",
- "dependencies": {
- "@isaacs/cliui": "^8.0.2"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- },
- "optionalDependencies": {
- "@pkgjs/parseargs": "^0.11.0"
- }
- },
"node_modules/jiti": {
"version": "2.6.1",
"resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
@@ -9100,16 +9023,16 @@
"license": "MIT"
},
"node_modules/js-beautify": {
- "version": "1.15.4",
- "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-1.15.4.tgz",
- "integrity": "sha512-9/KXeZUKKJwqCXUdBxFJ3vPh467OCckSBmYDwSK/EtV090K+iMJ7zx2S3HLVDIWFQdqMIsZWbnaGiba18aWhaA==",
+ "version": "2.0.3",
+ "resolved": "https://registry.npmjs.org/js-beautify/-/js-beautify-2.0.3.tgz",
+ "integrity": "sha512-cyFbh3tkPhknnTD/0bLf0T0yy2ZIbqL05mttzbt4y1Zfr7NxqXQZ62dkBLKs3oHH/lpjmDRAnciJiSUyOy8XwQ==",
"license": "MIT",
"dependencies": {
"config-chain": "^1.1.13",
- "editorconfig": "^1.0.4",
- "glob": "^10.4.2",
- "js-cookie": "^3.0.5",
- "nopt": "^7.2.1"
+ "editorconfig": "^3.0.2",
+ "glob": "^13.0.6",
+ "js-cookie": "^3.0.8",
+ "nopt": "^10.0.1"
},
"bin": {
"css-beautify": "js/bin/css-beautify.js",
@@ -9120,6 +9043,30 @@
"node": ">=14"
}
},
+ "node_modules/js-beautify/node_modules/abbrev": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-4.0.0.tgz",
+ "integrity": "sha512-a1wflyaL0tHtJSmLSOVybYhy22vRih4eduhhrkcjgrWGnRfrZtovJ2FRjxuTtkkj47O/baf0R86QU5OuYpz8fA==",
+ "license": "ISC",
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
+ "node_modules/js-beautify/node_modules/nopt": {
+ "version": "9.0.0",
+ "resolved": "https://registry.npmjs.org/nopt/-/nopt-9.0.0.tgz",
+ "integrity": "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw==",
+ "license": "ISC",
+ "dependencies": {
+ "abbrev": "^4.0.0"
+ },
+ "bin": {
+ "nopt": "bin/nopt.js"
+ },
+ "engines": {
+ "node": "^20.17.0 || >=22.9.0"
+ }
+ },
"node_modules/js-cookie": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.8.tgz",
@@ -9700,7 +9647,6 @@
"integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"bin": {
"lz-string": "bin/bin.js"
}
@@ -9771,7 +9717,6 @@
"resolved": "https://registry.npmjs.org/marked/-/marked-14.0.0.tgz",
"integrity": "sha512-uIj4+faQ+MgHgwUW1l2PsPglZLOLOT1uErt06dAPtx2kjteLAkbsd/0FiYg/MGS+i7ZKLb7w2WClxHkzOOuryQ==",
"license": "MIT",
- "peer": true,
"bin": {
"marked": "bin/marked.js"
},
@@ -10741,9 +10686,9 @@
"license": "MIT"
},
"node_modules/nanoid": {
- "version": "3.3.12",
- "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz",
- "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==",
+ "version": "3.3.16",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz",
+ "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==",
"funding": [
{
"type": "github",
@@ -10810,12 +10755,12 @@
"license": "MIT"
},
"node_modules/next": {
- "version": "16.2.10",
- "resolved": "https://registry.npmjs.org/next/-/next-16.2.10.tgz",
- "integrity": "sha512-2som5AVXb3kE6Yjine3/mNbBayYF58eguBWIVVUdr1y/L426xyVEgYxgBG+1QC34P2x5E+tcDup6XkuOAX3dCA==",
+ "version": "16.2.12",
+ "resolved": "https://registry.npmjs.org/next/-/next-16.2.12.tgz",
+ "integrity": "sha512-iD59eYQWmbFcEbX7v/acG5DRym9iw1DdaPoD0WTA920naWsE25wShzJW4+UvAs8MK9EC2kBfIH6vtto1H1PHGw==",
"license": "MIT",
"dependencies": {
- "@next/env": "16.2.10",
+ "@next/env": "16.2.12",
"@swc/helpers": "0.5.15",
"baseline-browser-mapping": "^2.9.19",
"caniuse-lite": "^1.0.30001579",
@@ -10829,14 +10774,14 @@
"node": ">=20.9.0"
},
"optionalDependencies": {
- "@next/swc-darwin-arm64": "16.2.10",
- "@next/swc-darwin-x64": "16.2.10",
- "@next/swc-linux-arm64-gnu": "16.2.10",
- "@next/swc-linux-arm64-musl": "16.2.10",
- "@next/swc-linux-x64-gnu": "16.2.10",
- "@next/swc-linux-x64-musl": "16.2.10",
- "@next/swc-win32-arm64-msvc": "16.2.10",
- "@next/swc-win32-x64-msvc": "16.2.10",
+ "@next/swc-darwin-arm64": "16.2.12",
+ "@next/swc-darwin-x64": "16.2.12",
+ "@next/swc-linux-arm64-gnu": "16.2.12",
+ "@next/swc-linux-arm64-musl": "16.2.12",
+ "@next/swc-linux-x64-gnu": "16.2.12",
+ "@next/swc-linux-x64-musl": "16.2.12",
+ "@next/swc-win32-arm64-msvc": "16.2.12",
+ "@next/swc-win32-x64-msvc": "16.2.12",
"sharp": "^0.34.5"
},
"peerDependencies": {
@@ -10862,34 +10807,6 @@
}
}
},
- "node_modules/next/node_modules/postcss": {
- "version": "8.5.10",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.10.tgz",
- "integrity": "sha512-pMMHxBOZKFU6HgAZ4eyGnwXF/EvPGGqUr0MnZ5+99485wwW41kW91A4LOGxSHhgugZmSChL5AlElNdwlNgcnLQ==",
- "funding": [
- {
- "type": "opencollective",
- "url": "https://opencollective.com/postcss/"
- },
- {
- "type": "tidelift",
- "url": "https://tidelift.com/funding/github/npm/postcss"
- },
- {
- "type": "github",
- "url": "https://github.com/sponsors/ai"
- }
- ],
- "license": "MIT",
- "dependencies": {
- "nanoid": "^3.3.11",
- "picocolors": "^1.1.1",
- "source-map-js": "^1.2.1"
- },
- "engines": {
- "node": "^10 || ^12 || >=14"
- }
- },
"node_modules/node-exports-info": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz",
@@ -10916,21 +10833,6 @@
"dev": true,
"license": "MIT"
},
- "node_modules/nopt": {
- "version": "7.2.1",
- "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz",
- "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==",
- "license": "ISC",
- "dependencies": {
- "abbrev": "^2.0.0"
- },
- "bin": {
- "nopt": "bin/nopt.js"
- },
- "engines": {
- "node": "^14.17.0 || ^16.13.0 || >=18.0.0"
- }
- },
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -11152,12 +11054,6 @@
"node": ">=6"
}
},
- "node_modules/package-json-from-dist": {
- "version": "1.0.1",
- "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz",
- "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==",
- "license": "BlueOak-1.0.0"
- },
"node_modules/pako": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz",
@@ -11234,6 +11130,7 @@
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
"integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -11247,26 +11144,29 @@
"license": "MIT"
},
"node_modules/path-scurry": {
- "version": "1.11.1",
- "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz",
- "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==",
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz",
+ "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==",
"license": "BlueOak-1.0.0",
"dependencies": {
- "lru-cache": "^10.2.0",
- "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0"
+ "lru-cache": "^11.0.0",
+ "minipass": "^7.1.2"
},
"engines": {
- "node": ">=16 || 14 >=14.18"
+ "node": "18 || 20 || >=22"
},
"funding": {
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/path-scurry/node_modules/lru-cache": {
- "version": "10.4.3",
- "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
- "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
- "license": "ISC"
+ "version": "11.5.2",
+ "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.2.tgz",
+ "integrity": "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g==",
+ "license": "BlueOak-1.0.0",
+ "engines": {
+ "node": "20 || >=22"
+ }
},
"node_modules/pathe": {
"version": "2.0.3",
@@ -11379,10 +11279,9 @@
}
},
"node_modules/postcss": {
- "version": "8.5.15",
- "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz",
- "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==",
- "dev": true,
+ "version": "8.5.23",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz",
+ "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==",
"funding": [
{
"type": "opencollective",
@@ -11399,7 +11298,7 @@
],
"license": "MIT",
"dependencies": {
- "nanoid": "^3.3.12",
+ "nanoid": "^3.3.16",
"picocolors": "^1.1.1",
"source-map-js": "^1.2.1"
},
@@ -11423,7 +11322,6 @@
"integrity": "sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"dependencies": {
"ansi-regex": "^5.0.1",
"ansi-styles": "^5.0.0",
@@ -11439,7 +11337,6 @@
"integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=8"
}
@@ -11450,7 +11347,6 @@
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"dev": true,
"license": "MIT",
- "peer": true,
"engines": {
"node": ">=10"
},
@@ -11463,8 +11359,7 @@
"resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz",
"integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==",
"dev": true,
- "license": "MIT",
- "peer": true
+ "license": "MIT"
},
"node_modules/prop-types": {
"version": "15.8.1",
@@ -11788,6 +11683,7 @@
"resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
"integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==",
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=0.10.0"
}
@@ -11797,6 +11693,7 @@
"resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz",
"integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==",
"license": "MIT",
+ "peer": true,
"dependencies": {
"scheduler": "^0.27.0"
},
@@ -12458,54 +12355,59 @@
}
},
"node_modules/sharp": {
- "version": "0.34.5",
- "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz",
- "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==",
- "hasInstallScript": true,
+ "version": "0.35.3",
+ "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.3.tgz",
+ "integrity": "sha512-ej0zVHuZGHCiABXcNxeYhpRnPNPAcvbG8RMdBAhDAxLKkCRVSpK3Iyu7qbqw3JMzoj0REeM6f3tJLtVwl0023Q==",
"license": "Apache-2.0",
"optional": true,
"dependencies": {
- "@img/colour": "^1.0.0",
+ "@img/colour": "^1.1.0",
"detect-libc": "^2.1.2",
- "semver": "^7.7.3"
+ "semver": "^7.8.5"
},
"engines": {
- "node": "^18.17.0 || ^20.3.0 || >=21.0.0"
+ "node": ">=20.9.0"
},
"funding": {
"url": "https://opencollective.com/libvips"
},
"optionalDependencies": {
- "@img/sharp-darwin-arm64": "0.34.5",
- "@img/sharp-darwin-x64": "0.34.5",
- "@img/sharp-libvips-darwin-arm64": "1.2.4",
- "@img/sharp-libvips-darwin-x64": "1.2.4",
- "@img/sharp-libvips-linux-arm": "1.2.4",
- "@img/sharp-libvips-linux-arm64": "1.2.4",
- "@img/sharp-libvips-linux-ppc64": "1.2.4",
- "@img/sharp-libvips-linux-riscv64": "1.2.4",
- "@img/sharp-libvips-linux-s390x": "1.2.4",
- "@img/sharp-libvips-linux-x64": "1.2.4",
- "@img/sharp-libvips-linuxmusl-arm64": "1.2.4",
- "@img/sharp-libvips-linuxmusl-x64": "1.2.4",
- "@img/sharp-linux-arm": "0.34.5",
- "@img/sharp-linux-arm64": "0.34.5",
- "@img/sharp-linux-ppc64": "0.34.5",
- "@img/sharp-linux-riscv64": "0.34.5",
- "@img/sharp-linux-s390x": "0.34.5",
- "@img/sharp-linux-x64": "0.34.5",
- "@img/sharp-linuxmusl-arm64": "0.34.5",
- "@img/sharp-linuxmusl-x64": "0.34.5",
- "@img/sharp-wasm32": "0.34.5",
- "@img/sharp-win32-arm64": "0.34.5",
- "@img/sharp-win32-ia32": "0.34.5",
- "@img/sharp-win32-x64": "0.34.5"
+ "@img/sharp-darwin-arm64": "0.35.3",
+ "@img/sharp-darwin-x64": "0.35.3",
+ "@img/sharp-freebsd-wasm32": "0.35.3",
+ "@img/sharp-libvips-darwin-arm64": "1.3.2",
+ "@img/sharp-libvips-darwin-x64": "1.3.2",
+ "@img/sharp-libvips-linux-arm": "1.3.2",
+ "@img/sharp-libvips-linux-arm64": "1.3.2",
+ "@img/sharp-libvips-linux-ppc64": "1.3.2",
+ "@img/sharp-libvips-linux-riscv64": "1.3.2",
+ "@img/sharp-libvips-linux-s390x": "1.3.2",
+ "@img/sharp-libvips-linux-x64": "1.3.2",
+ "@img/sharp-libvips-linuxmusl-arm64": "1.3.2",
+ "@img/sharp-libvips-linuxmusl-x64": "1.3.2",
+ "@img/sharp-linux-arm": "0.35.3",
+ "@img/sharp-linux-arm64": "0.35.3",
+ "@img/sharp-linux-ppc64": "0.35.3",
+ "@img/sharp-linux-riscv64": "0.35.3",
+ "@img/sharp-linux-s390x": "0.35.3",
+ "@img/sharp-linux-x64": "0.35.3",
+ "@img/sharp-linuxmusl-arm64": "0.35.3",
+ "@img/sharp-linuxmusl-x64": "0.35.3",
+ "@img/sharp-webcontainers-wasm32": "0.35.3",
+ "@img/sharp-win32-arm64": "0.35.3",
+ "@img/sharp-win32-ia32": "0.35.3",
+ "@img/sharp-win32-x64": "0.35.3"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ }
}
},
"node_modules/sharp/node_modules/semver": {
- "version": "7.7.4",
- "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz",
- "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==",
+ "version": "7.8.5",
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
+ "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
"license": "ISC",
"optional": true,
"bin": {
@@ -12519,6 +12421,7 @@
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
"integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"shebang-regex": "^3.0.0"
@@ -12531,6 +12434,7 @@
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
"integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "dev": true,
"license": "MIT",
"engines": {
"node": ">=8"
@@ -12619,18 +12523,6 @@
"dev": true,
"license": "ISC"
},
- "node_modules/signal-exit": {
- "version": "4.1.0",
- "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz",
- "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==",
- "license": "ISC",
- "engines": {
- "node": ">=14"
- },
- "funding": {
- "url": "https://github.com/sponsors/isaacs"
- }
- },
"node_modules/sirv": {
"version": "2.0.4",
"resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz",
@@ -12760,48 +12652,6 @@
"node": ">= 0.4"
}
},
- "node_modules/string-width-cjs": {
- "name": "string-width",
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
- "license": "MIT",
- "dependencies": {
- "emoji-regex": "^8.0.0",
- "is-fullwidth-code-point": "^3.0.0",
- "strip-ansi": "^6.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/string-width-cjs/node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/string-width-cjs/node_modules/emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "license": "MIT"
- },
- "node_modules/string-width-cjs/node_modules/strip-ansi": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "license": "MIT",
- "dependencies": {
- "ansi-regex": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/string.prototype.includes": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz",
@@ -12929,43 +12779,6 @@
"url": "https://github.com/sponsors/wooorm"
}
},
- "node_modules/strip-ansi": {
- "version": "7.1.2",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz",
- "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==",
- "license": "MIT",
- "dependencies": {
- "ansi-regex": "^6.0.1"
- },
- "engines": {
- "node": ">=12"
- },
- "funding": {
- "url": "https://github.com/chalk/strip-ansi?sponsor=1"
- }
- },
- "node_modules/strip-ansi-cjs": {
- "name": "strip-ansi",
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "license": "MIT",
- "dependencies": {
- "ansi-regex": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/strip-ansi-cjs/node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/strip-bom": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz",
@@ -13090,7 +12903,8 @@
"version": "4.2.1",
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.2.1.tgz",
"integrity": "sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==",
- "license": "MIT"
+ "license": "MIT",
+ "peer": true
},
"node_modules/tailwindcss-animate": {
"version": "1.0.7",
@@ -13197,6 +13011,7 @@
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=12"
},
@@ -13464,6 +13279,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"dev": true,
"license": "Apache-2.0",
+ "peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -13879,6 +13695,7 @@
"integrity": "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"esbuild": "^0.27.0",
"fdir": "^6.5.0",
@@ -13972,6 +13789,7 @@
"integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==",
"dev": true,
"license": "MIT",
+ "peer": true,
"engines": {
"node": ">=12"
},
@@ -13985,6 +13803,7 @@
"integrity": "sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==",
"dev": true,
"license": "MIT",
+ "peer": true,
"dependencies": {
"@vitest/expect": "4.1.8",
"@vitest/mocker": "4.1.8",
@@ -14182,6 +14001,7 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
"integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "dev": true,
"license": "ISC",
"dependencies": {
"isexe": "^2.0.0"
@@ -14329,65 +14149,6 @@
"node": ">=8"
}
},
- "node_modules/wrap-ansi-cjs": {
- "name": "wrap-ansi",
- "version": "7.0.0",
- "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
- "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
- "license": "MIT",
- "dependencies": {
- "ansi-styles": "^4.0.0",
- "string-width": "^4.1.0",
- "strip-ansi": "^6.0.0"
- },
- "engines": {
- "node": ">=10"
- },
- "funding": {
- "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
- }
- },
- "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": {
- "version": "5.0.1",
- "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
- "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
- "license": "MIT",
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": {
- "version": "8.0.0",
- "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
- "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
- "license": "MIT"
- },
- "node_modules/wrap-ansi-cjs/node_modules/string-width": {
- "version": "4.2.3",
- "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
- "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
- "license": "MIT",
- "dependencies": {
- "emoji-regex": "^8.0.0",
- "is-fullwidth-code-point": "^3.0.0",
- "strip-ansi": "^6.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
- "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": {
- "version": "6.0.1",
- "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
- "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
- "license": "MIT",
- "dependencies": {
- "ansi-regex": "^5.0.1"
- },
- "engines": {
- "node": ">=8"
- }
- },
"node_modules/wrap-ansi/node_modules/ansi-regex": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
@@ -14501,6 +14262,7 @@
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
"integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
"license": "ISC",
+ "peer": true,
"bin": {
"yaml": "bin.mjs"
},
@@ -14530,6 +14292,7 @@
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
"dev": true,
"license": "MIT",
+ "peer": true,
"funding": {
"url": "https://github.com/sponsors/colinhacks"
}
diff --git a/package.json b/package.json
index 7a7bdb72..eddf1ee8 100644
--- a/package.json
+++ b/package.json
@@ -33,32 +33,32 @@
"npm": ">=10 <11"
},
"overrides": {
- "dompurify": "3.4.11",
+ "dompurify": "3.4.12",
"editorconfig": {
".": "3.0.2",
"minimatch": "10.2.4"
},
- "minimatch": "3.1.5",
+ "js-beautify": {
+ "nopt": "9.0.0"
+ },
"@typescript-eslint/typescript-estree": {
"minimatch": "10.2.4"
},
- "glob": {
- "minimatch": "9.0.9"
- },
+ "sharp": "0.35.3",
"undici": "7.28.0",
"ws": "7.5.11",
"next": {
- "postcss": "8.5.10"
+ "postcss": "8.5.23"
}
},
"scripts": {
"dev": "next dev",
- "analyze": "ANALYZE=true next build --webpack",
+ "analyze": "node scripts/build/run-bundle-analyzer.js",
"validate": "npm run check:sw-version && npm run check:sitemap-lastmod && npm run check:security-headers && npm run check:pwa-manifests && npm run check:runtime-scripts && npm run check:llms-txt && npm run check:bundle-boundaries && npm run check:performance-budget && npm run check:og-tool-images && npm run check:ia-stability && npm run check:route-width-inventory && npm run check:analytics-taxonomy && npm run check:registry-manifests && npm run check:tool-index && npm run check:legacy-routes && npm run check:client-tool-lookup && npm run check:privacy-copy && npm run check:i18n && npm run check:i18n-qa && npm run check:types",
"build:app": "next build",
"build:post": "npm run check:canonical && npm run check:hreflang && npm run check:legacy-routes && npm run check:metadata-localization && npm run check:jsonld-structured-data && npm run check:performance-budget:report && npm run check:rendered-i18n-copy && npm run check:related-tools && npm run check:content-template:legacy-paths && npm run check:content-template && npm run check:content-template:quality && npm run check:content-template:uniqueness && npm run check:faq-schema && npm run check:content-template:zh-cn && npm run check:content-template:quality:zh-cn && npm run check:faq-schema:zh-cn && npm run check:content-template:zh-tw && npm run check:content-template:quality:zh-tw && npm run check:faq-schema:zh-tw && npm run check:content-template:ja && npm run check:content-template:quality:ja && npm run check:faq-schema:ja && npm run check:content-template:ko && npm run check:content-template:quality:ko && npm run check:faq-schema:ko && npm run check:content-template:de && npm run check:content-template:quality:de && npm run check:faq-schema:de && npm run check:content-template:fr && npm run check:content-template:quality:fr && npm run check:faq-schema:fr && npm run postprocess:export-html-lang && npm run check:export-html-lang && npm run postprocess:export-robots-meta && npm run check:export-robots-meta && npm run check:sitemap-export && npm run build:sw",
"build": "npm run validate && npm run build:app && npm run build:post",
- "start": "next start",
+ "start": "node scripts/deployment/serve-export.js",
"lint": "eslint",
"test": "vitest run",
"test:coverage": "vitest run --coverage",
@@ -152,15 +152,15 @@
"cronstrue": "^3.12.0",
"crypto-js": "^4.2.0",
"csso": "^5.0.5",
- "dompurify": "3.4.11",
+ "dompurify": "3.4.12",
"jq-wasm": "^1.1.0-jq-1.8.1",
- "js-beautify": "^1.15.4",
+ "js-beautify": "2.0.3",
"jsbarcode": "^3.12.3",
"jsqr": "^1.4.0",
"jwt-decode": "^4.0.0",
"lorem-ipsum": "^2.0.8",
"lucide-react": "^0.575.0",
- "next": "16.2.10",
+ "next": "16.2.12",
"pdf-lib": "^1.17.1",
"qrcode": "^1.5.4",
"radix-ui": "^1.4.3",
@@ -184,7 +184,7 @@
"yaml": "^2.8.2"
},
"devDependencies": {
- "@next/bundle-analyzer": "16.2.10",
+ "@next/bundle-analyzer": "16.2.12",
"@tailwindcss/postcss": "^4",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
@@ -203,7 +203,7 @@
"@vitest/coverage-v8": "^4.1.8",
"axe-core": "^4.11.1",
"eslint": "^9",
- "eslint-config-next": "16.2.10",
+ "eslint-config-next": "16.2.12",
"jsdom": "^28.1.0",
"playwright": "^1.58.2",
"serve-handler": "^6.1.7",
diff --git a/public/sw.js b/public/sw.js
index 3049ee3f..4362a4aa 100644
--- a/public/sw.js
+++ b/public/sw.js
@@ -22,7 +22,7 @@ const ACTIVE_CACHE_NAMES = [
CACHE_META_NAME,
];
const OFFLINE_FALLBACK_URL = '/offline.html';
-const OFFLINE_FALLBACK_CANDIDATES = [OFFLINE_FALLBACK_URL, '/offline'];
+const OFFLINE_FALLBACK_CANDIDATES = [OFFLINE_FALLBACK_URL];
const MAX_RUNTIME_CACHE_ENTRIES = 120;
const MAX_RUNTIME_CACHE_AGE_MS = 7 * 24 * 60 * 60 * 1000;
const CACHE_WRITE_PAUSE_AFTER_CLEAR_MS = 5000;
@@ -30,7 +30,13 @@ const CACHE_META_URL_PREFIX = '/__byteflow-cache-meta__?url=';
let cacheWritesPausedUntil = 0;
const APP_SHELL_ASSETS = [
- ...OFFLINE_FALLBACK_CANDIDATES,
+ '/en',
+ '/zh-CN',
+ '/zh-TW',
+ '/ja',
+ '/ko',
+ '/de',
+ '/fr',
];
const MANIFEST_ICON_ASSETS = [
@@ -77,6 +83,7 @@ const SENSITIVE_QUERY_PARAMS = [
'signature',
'token',
];
+const CACHEABLE_QUERY_PARAMS = new Set(['source', 'utm_source']);
function hasSensitiveQuery(url) {
const sensitiveParams = new Set(SENSITIVE_QUERY_PARAMS);
@@ -86,9 +93,22 @@ function hasSensitiveQuery(url) {
return false;
}
+function hasUncacheableQuery(url) {
+ for (const param of url.searchParams.keys()) {
+ if (!CACHEABLE_QUERY_PARAMS.has(param.toLowerCase())) return true;
+ }
+ return false;
+}
+
+function cacheRequest(request) {
+ const url = new URL(typeof request === 'string' ? request : request.url, self.location.origin);
+ url.search = '';
+ url.hash = '';
+ return new Request(url.toString(), { method: 'GET' });
+}
+
function matchOfflineFallback() {
return caches.match(OFFLINE_FALLBACK_URL)
- .then((cached) => cached || caches.match('/offline'))
.then((cached) => cached || new Response(
'
Offline | byteflow.toolsYou are offline
Reconnect and refresh, or open a page you have visited before.
',
{ headers: { 'Content-Type': 'text/html; charset=utf-8' } },
@@ -96,7 +116,10 @@ function matchOfflineFallback() {
}
function cacheMetaRequest(cacheName, request) {
- return new Request(`${CACHE_META_URL_PREFIX}${encodeURIComponent(`${cacheName}:${request.url}`)}`);
+ const canonicalRequest = cacheRequest(request);
+ const metaUrl = new URL(CACHE_META_URL_PREFIX, self.location.origin);
+ metaUrl.searchParams.set('url', `${cacheName}:${canonicalRequest.url}`);
+ return new Request(metaUrl.toString());
}
function rememberCachedRequest(cacheName, request) {
@@ -153,12 +176,13 @@ function discardRuntimeCacheWrite(cacheName) {
function putRuntimeCache(request, response, cacheName) {
if (isCacheWritePaused()) return Promise.resolve();
const clone = response.clone();
+ const canonicalRequest = cacheRequest(request);
return caches.open(cacheName)
.then((cache) => {
if (isCacheWritePaused()) return discardRuntimeCacheWrite(cacheName);
- return cache.put(request, clone).then(() => {
+ return cache.put(canonicalRequest, clone).then(() => {
if (isCacheWritePaused()) return discardRuntimeCacheWrite(cacheName);
- return rememberCachedRequest(cacheName, request);
+ return rememberCachedRequest(cacheName, canonicalRequest);
});
})
.then(() => {
@@ -171,12 +195,59 @@ function isNetworkOnlyRequest(request, url) {
return (
url.origin !== self.location.origin ||
hasSensitiveQuery(url) ||
+ hasUncacheableQuery(url) ||
request.headers.get('x-byteflow-cache-mode') === 'network-only' ||
request.headers.get('x-byteflow-external-request') === '1' ||
url.pathname.startsWith('/api/')
);
}
+function extractAppShellAssets(html) {
+ const assets = new Set();
+ const attributePattern = /(?:src|href)=["']([^"']+)["']/g;
+ let match;
+ while ((match = attributePattern.exec(html)) !== null) {
+ const url = new URL(match[1], self.location.origin);
+ if (url.origin === self.location.origin && url.pathname.startsWith('/_next/static/')) {
+ assets.add(url.toString());
+ }
+ }
+ return [...assets];
+}
+
+async function precacheAppShellAsset(assetUrl) {
+ const request = cacheRequest(assetUrl);
+ const response = await fetch(request);
+ if (!response.ok) throw new Error(`Unable to precache app shell asset: ${request.url}`);
+ const url = new URL(request.url);
+ const cacheName = isToolChunkRequest(url) ? TOOL_CHUNK_CACHE_NAME : STATIC_ASSET_CACHE_NAME;
+ const cache = await caches.open(cacheName);
+ await cache.put(request, response);
+}
+
+async function precacheAppShellRoute(cache, route) {
+ const candidates = [route, `${route}.html`];
+ let response;
+ for (const candidate of candidates) {
+ const fetched = await fetch(candidate);
+ if (fetched.ok) {
+ response = fetched;
+ break;
+ }
+ }
+ if (!response) throw new Error(`Unable to precache app shell route: ${route}`);
+
+ await cache.put(cacheRequest(route), response.clone());
+ const assets = extractAppShellAssets(await response.text());
+ await Promise.all(assets.map(precacheAppShellAsset));
+}
+
+async function precacheAppShell() {
+ const cache = await caches.open(APP_SHELL_CACHE_NAME);
+ await cache.addAll(OFFLINE_FALLBACK_CANDIDATES);
+ await Promise.all(APP_SHELL_ASSETS.map((route) => precacheAppShellRoute(cache, route)));
+}
+
function isHtmlRequest(request, url) {
return url.pathname.endsWith('.html') || request.headers.get('accept')?.includes('text/html');
}
@@ -212,7 +283,7 @@ function deleteByteflowCaches() {
self.addEventListener('install', (event) => {
event.waitUntil(
Promise.all([
- caches.open(APP_SHELL_CACHE_NAME).then((cache) => cache.addAll(APP_SHELL_ASSETS)),
+ precacheAppShell(),
caches.open(MANIFEST_ICON_CACHE_NAME).then((cache) => cache.addAll(MANIFEST_ICON_ASSETS)),
])
);
@@ -277,7 +348,7 @@ self.addEventListener('fetch', (event) => {
return response;
})
.catch(() =>
- caches.match(event.request).then((cached) => cached || matchOfflineFallback())
+ caches.match(cacheRequest(event.request)).then((cached) => cached || matchOfflineFallback())
)
);
return;
@@ -286,7 +357,7 @@ self.addEventListener('fetch', (event) => {
// Cache-first for manifest, icons, fonts, images, and other static assets.
if (isManifestOrIconRequest(url) || CACHEABLE_STATIC_ASSET_PATTERN.test(url.pathname)) {
event.respondWith(
- caches.match(event.request).then((cached) => {
+ caches.match(cacheRequest(event.request)).then((cached) => {
if (cached) return cached;
return fetch(event.request).then((response) => {
if (response.ok) {
@@ -309,7 +380,7 @@ self.addEventListener('fetch', (event) => {
return response;
})
.catch(() =>
- caches.match(event.request).then((cached) => cached || matchOfflineFallback())
+ caches.match(cacheRequest(event.request)).then((cached) => cached || matchOfflineFallback())
)
);
});
diff --git a/scripts/build/run-bundle-analyzer.js b/scripts/build/run-bundle-analyzer.js
new file mode 100644
index 00000000..adb75572
--- /dev/null
+++ b/scripts/build/run-bundle-analyzer.js
@@ -0,0 +1,24 @@
+#!/usr/bin/env node
+
+import { spawn } from "node:child_process";
+import path from "node:path";
+
+const nextCli = path.resolve(process.cwd(), "node_modules/next/dist/bin/next");
+const child = spawn(process.execPath, [nextCli, "build", "--webpack"], {
+ env: { ...process.env, ANALYZE: "true" },
+ stdio: "inherit",
+});
+
+child.on("error", (error) => {
+ console.error(`[analyze] Unable to start Next.js: ${error.message}`);
+ process.exitCode = 1;
+});
+
+child.on("exit", (code, signal) => {
+ if (signal) {
+ console.error(`[analyze] Next.js exited after signal ${signal}.`);
+ process.exitCode = 1;
+ return;
+ }
+ process.exitCode = code ?? 1;
+});
diff --git a/scripts/deployment/serve-export.js b/scripts/deployment/serve-export.js
new file mode 100644
index 00000000..e2c8c43f
--- /dev/null
+++ b/scripts/deployment/serve-export.js
@@ -0,0 +1,38 @@
+#!/usr/bin/env node
+
+import { existsSync } from "node:fs";
+import { createServer } from "node:http";
+import path from "node:path";
+import serveHandler from "serve-handler";
+
+function readOption(name) {
+ const index = process.argv.indexOf(name);
+ return index >= 0 ? process.argv[index + 1] : undefined;
+}
+
+const outDir = path.resolve(process.cwd(), "out");
+if (!existsSync(outDir)) {
+ console.error(`[start] Missing static export at ${outDir}. Run npm run build first.`);
+ process.exit(1);
+}
+
+const port = Number(readOption("--port") ?? process.env.PORT ?? 3000);
+if (!Number.isInteger(port) || port < 1 || port > 65535) {
+ console.error(`[start] Invalid port: ${String(port)}`);
+ process.exit(1);
+}
+const host = readOption("--host") ?? process.env.HOST ?? "0.0.0.0";
+
+const server = createServer((request, response) => serveHandler(request, response, {
+ public: outDir,
+ cleanUrls: true,
+ etag: true,
+}));
+
+server.listen(port, host, () => {
+ console.log(`[start] Serving ${outDir} at http://${host}:${port}`);
+});
+
+for (const signal of ["SIGINT", "SIGTERM"]) {
+ process.on(signal, () => server.close(() => process.exit(0)));
+}
diff --git a/scripts/e2e/run-playwright-smoke.js b/scripts/e2e/run-playwright-smoke.js
index fa04d8ae..fff27ad0 100644
--- a/scripts/e2e/run-playwright-smoke.js
+++ b/scripts/e2e/run-playwright-smoke.js
@@ -2106,6 +2106,32 @@ async function assertPwaShellJourney(browser, baseUrl) {
throw new Error("Service worker is active but did not control the PWA smoke page.");
}
+ const installedAppShell = await page.evaluate(async () => {
+ const appShellName = (await caches.keys()).find((name) => name.startsWith("byteflow-app-shell-v"));
+ if (!appShellName) return { routes: [], staticAssets: 0 };
+ const appShell = await caches.open(appShellName);
+ const routes = (await appShell.keys()).map((request) => new URL(request.url).pathname);
+ const staticAssetNames = (await caches.keys()).filter((name) =>
+ name.startsWith("byteflow-static-assets-v") || name.startsWith("byteflow-tool-chunks-v"),
+ );
+ const staticAssets = (await Promise.all(staticAssetNames.map(async (name) => (await caches.open(name)).keys())))
+ .flat().length;
+ return { routes, staticAssets };
+ });
+ if (!installedAppShell.routes.includes("/en") || installedAppShell.staticAssets === 0) {
+ throw new Error(`PWA install did not precache the manifest start shell. Routes: ${installedAppShell.routes.join(", ")}; static assets: ${installedAppShell.staticAssets}`);
+ }
+
+ await context.setOffline(true);
+ contextOffline = true;
+ await page.goto(`${baseUrl}/en?utm_source=pwa`, { waitUntil: "domcontentloaded" });
+ await page.waitForSelector("main", { timeout: 15_000 });
+ if ((await page.title()).startsWith("Offline |")) {
+ throw new Error("Fresh PWA offline launch returned the fallback instead of the installed app shell.");
+ }
+ await context.setOffline(false);
+ contextOffline = false;
+
await page.goto(`${baseUrl}/en/json-formatter`, { waitUntil: "domcontentloaded" });
await page.waitForSelector("main", { timeout: 15_000 });
isControlled = await waitForController();
diff --git a/scripts/gates/performance-budgets.json b/scripts/gates/performance-budgets.json
index f62656b3..605ec420 100644
--- a/scripts/gates/performance-budgets.json
+++ b/scripts/gates/performance-budgets.json
@@ -40,6 +40,16 @@
"maxCssRawBytes": 220000,
"maxHtmlBytes": 335000
},
+ {
+ "route": "/ja/json-formatter",
+ "label": "JSON Formatter (Japanese)",
+ "maxInitialJsGzipBytes": 350000,
+ "maxInitialJsRawBytes": 1220000,
+ "maxInitialScriptFiles": 27,
+ "maxCssGzipBytes": 35000,
+ "maxCssRawBytes": 220000,
+ "maxHtmlBytes": 335000
+ },
{
"route": "/en/markdown-preview",
"label": "Markdown Preview",
diff --git a/src/app/[lang]/layout.tsx b/src/app/[lang]/layout.tsx
index fc7b6616..e929f26f 100644
--- a/src/app/[lang]/layout.tsx
+++ b/src/app/[lang]/layout.tsx
@@ -1,6 +1,6 @@
import type { Metadata } from "next";
import { notFound } from "next/navigation";
-import { LOCALES, isValidLocale } from "@/core/i18n/i18n";
+import { LOCALES, isValidLocale, type Locale } from "@/core/i18n/i18n";
import { AppLayout } from "@/components/layout/app-layout";
import { ServerFooter } from "@/components/layout/server-footer";
import { ServerNavbar } from "@/components/layout/server-navbar";
@@ -9,10 +9,13 @@ import { SiteJsonLd } from "@/core/seo/components/site-json-ld";
import { buildDefaultOgImageUrl, buildSiteKeywords, getOgLocale } from "@/core/seo/seo";
import { buildCanonicalUrl, buildLocalizedAlternates } from "@/core/seo/urls";
import { getEnglishToolSearchAliases, getTranslation } from "@/core/i18n/translations/catalog";
+import { encodeTranslationPayload } from "@/core/i18n/translation-payload";
import { ToolPrivacyFooterSlot } from "@/features/tool-shell/tool-privacy-footer-slot";
export const dynamicParams = false;
+const encodedLanguagePayloads = new Map();
+
export function generateStaticParams() {
return LOCALES.map((lang) => ({ lang }));
}
@@ -70,12 +73,16 @@ export default async function LangLayout({
const locale = lang;
const translations = getTranslation(locale);
const englishToolSearchAliases = locale === "en" ? undefined : getEnglishToolSearchAliases();
+ let encodedPayload = encodedLanguagePayloads.get(locale);
+ if (!encodedPayload) {
+ encodedPayload = encodeTranslationPayload({ translations, englishToolSearchAliases });
+ encodedLanguagePayloads.set(locale, encodedPayload);
+ }
return (
+
}
diff --git a/src/core/i18n/lang-provider.tsx b/src/core/i18n/lang-provider.tsx
index c7d280f0..8f34dc07 100644
--- a/src/core/i18n/lang-provider.tsx
+++ b/src/core/i18n/lang-provider.tsx
@@ -3,6 +3,7 @@
import * as React from "react"
import type { Locale } from "@/core/i18n/i18n"
import type { ToolSearchAliases, TranslationSchema } from "@/core/i18n/translations/catalog"
+import { decodeTranslationPayload } from "@/core/i18n/translation-payload"
export type TranslationType = Omit & {
tools: Record;
@@ -25,11 +26,13 @@ export function LangProvider({
lang,
translations,
englishToolSearchAliases,
+ encodedPayload,
}: {
children?: React.ReactNode
lang: Locale
- translations: TranslationSchema
+ translations?: TranslationSchema
englishToolSearchAliases?: ToolSearchAliases
+ encodedPayload?: string
}) {
React.useEffect(() => {
if (typeof document !== 'undefined') {
@@ -41,10 +44,20 @@ export function LangProvider({
}
}, [lang])
- const t = translations as TranslationType
+ const decoded = React.useMemo(() => encodedPayload
+ ? decodeTranslationPayload<{
+ translations: TranslationSchema
+ englishToolSearchAliases?: ToolSearchAliases
+ }>(encodedPayload)
+ : undefined, [encodedPayload])
+ const resolvedTranslations = translations ?? decoded?.translations
+ if (!resolvedTranslations) throw new Error("[i18n] LangProvider requires translations or an encoded payload")
+
+ const t = resolvedTranslations as TranslationType
+ const resolvedEnglishAliases = englishToolSearchAliases ?? decoded?.englishToolSearchAliases
const value = React.useMemo(
- () => ({ lang, t, englishToolSearchAliases }),
- [englishToolSearchAliases, lang, t],
+ () => ({ lang, t, englishToolSearchAliases: resolvedEnglishAliases }),
+ [lang, resolvedEnglishAliases, t],
)
return {children}
diff --git a/src/core/i18n/translation-payload.test.ts b/src/core/i18n/translation-payload.test.ts
new file mode 100644
index 00000000..3c21fa76
--- /dev/null
+++ b/src/core/i18n/translation-payload.test.ts
@@ -0,0 +1,16 @@
+import { describe, expect, it } from "vitest"
+import { getEnglishToolSearchAliases, getTranslation } from "@/core/i18n/translations/catalog"
+import { decodeTranslationPayload, encodeTranslationPayload } from "./translation-payload"
+
+describe("translation payload", () => {
+ it("round-trips localized dictionaries and search aliases", () => {
+ const value = {
+ translations: getTranslation("ja"),
+ englishToolSearchAliases: getEnglishToolSearchAliases(),
+ }
+ const payload = encodeTranslationPayload(value)
+
+ expect(decodeTranslationPayload(payload)).toEqual(value)
+ expect(payload.length).toBeLessThan(JSON.stringify(value).length)
+ })
+})
diff --git a/src/core/i18n/translation-payload.ts b/src/core/i18n/translation-payload.ts
new file mode 100644
index 00000000..2a732067
--- /dev/null
+++ b/src/core/i18n/translation-payload.ts
@@ -0,0 +1,77 @@
+function bytesToBase64(bytes: Uint8Array): string {
+ let binary = ""
+ const chunkSize = 0x8000
+ for (let index = 0; index < bytes.length; index += chunkSize) {
+ binary += String.fromCharCode(...bytes.subarray(index, index + chunkSize))
+ }
+ return btoa(binary)
+}
+
+function base64ToBytes(value: string): Uint8Array {
+ const binary = atob(value)
+ const bytes = new Uint8Array(binary.length)
+ for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index)
+ return bytes
+}
+
+function compressBytes(input: Uint8Array): Uint16Array {
+ if (input.length === 0) return new Uint16Array()
+ const dictionary = new Map()
+ const codes: number[] = []
+ let nextCode = 256
+ let phrase = String.fromCharCode(input[0])
+
+ for (let index = 1; index < input.length; index += 1) {
+ const character = String.fromCharCode(input[index])
+ const combined = phrase + character
+ if (dictionary.has(combined)) {
+ phrase = combined
+ continue
+ }
+ codes.push(phrase.length === 1 ? phrase.charCodeAt(0) : dictionary.get(phrase)!)
+ if (nextCode <= 0xffff) dictionary.set(combined, nextCode++)
+ phrase = character
+ }
+ codes.push(phrase.length === 1 ? phrase.charCodeAt(0) : dictionary.get(phrase)!)
+ return Uint16Array.from(codes)
+}
+
+function decompressBytes(codes: Uint16Array): Uint8Array {
+ if (codes.length === 0) return new Uint8Array()
+ const dictionary: string[] = []
+ let nextCode = 256
+ let phrase = String.fromCharCode(codes[0])
+ const chunks = [phrase]
+
+ for (let index = 1; index < codes.length; index += 1) {
+ const code = codes[index]
+ let entry = code < 256 ? String.fromCharCode(code) : dictionary[code]
+ if (entry === undefined && code === nextCode) entry = phrase + phrase[0]
+ if (entry === undefined) throw new Error("Invalid translation payload")
+ chunks.push(entry)
+ if (nextCode <= 0xffff) dictionary[nextCode++] = phrase + entry[0]
+ phrase = entry
+ }
+
+ const binary = chunks.join("")
+ const bytes = new Uint8Array(binary.length)
+ for (let index = 0; index < binary.length; index += 1) bytes[index] = binary.charCodeAt(index)
+ return bytes
+}
+
+export function encodeTranslationPayload(value: unknown): string {
+ const codes = compressBytes(new TextEncoder().encode(JSON.stringify(value)))
+ const packed = new Uint8Array(codes.length * 2)
+ const view = new DataView(packed.buffer)
+ codes.forEach((code, index) => view.setUint16(index * 2, code, false))
+ return bytesToBase64(packed)
+}
+
+export function decodeTranslationPayload(payload: string): T {
+ const bytes = base64ToBytes(payload)
+ if (bytes.byteLength % 2 !== 0) throw new Error("Invalid translation payload")
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
+ const codes = new Uint16Array(bytes.byteLength / 2)
+ codes.forEach((_, index) => { codes[index] = view.getUint16(index * 2, false) })
+ return JSON.parse(new TextDecoder().decode(decompressBytes(codes))) as T
+}
diff --git a/src/core/i18n/translations/de.json b/src/core/i18n/translations/de.json
index c68ca4ce..ce25465a 100644
--- a/src/core/i18n/translations/de.json
+++ b/src/core/i18n/translations/de.json
@@ -1412,7 +1412,7 @@
"parse_error": "Zertifikat konnte nicht geparst werden. Bitte ein gueltiges PEM-kodiertes X.509-Zertifikat pruefen.",
"validity_valid": "Zertifikat ist aktuell gueltig",
"validity_invalid": "Zertifikat ist abgelaufen oder noch nicht gueltig",
- "self_signed": "Selbstsigniert",
+ "self_issued": "Selbstausgestellt (Signatur nicht geprüft)",
"certificate_details": "Zertifikatsdetails",
"subject_label": "Subjekt",
"issuer_label": "Aussteller",
diff --git a/src/core/i18n/translations/en.json b/src/core/i18n/translations/en.json
index 2219b44a..d7bec509 100644
--- a/src/core/i18n/translations/en.json
+++ b/src/core/i18n/translations/en.json
@@ -1440,7 +1440,7 @@
"parse_error": "Failed to parse certificate. Ensure it's a valid PEM-encoded X.509 certificate.",
"validity_valid": "Certificate is currently valid",
"validity_invalid": "Certificate has expired or is not yet valid",
- "self_signed": "Self-Signed",
+ "self_issued": "Self-issued (signature not verified)",
"certificate_details": "Certificate Details",
"subject_label": "Subject",
"issuer_label": "Issuer",
diff --git a/src/core/i18n/translations/fr.json b/src/core/i18n/translations/fr.json
index 86a2b0f4..d8f71e4d 100644
--- a/src/core/i18n/translations/fr.json
+++ b/src/core/i18n/translations/fr.json
@@ -1412,7 +1412,7 @@
"parse_error": "Impossible d'analyser le certificat. Vérifiez que le certificat X.509 encodé en PEM est valide.",
"validity_valid": "Le certificat est actuellement valide",
"validity_invalid": "Le certificat a expiré ou n'est pas encore valide",
- "self_signed": "Auto-signé",
+ "self_issued": "Auto-émis (signature non vérifiée)",
"certificate_details": "Détails du certificat",
"subject_label": "Sujet",
"issuer_label": "Émetteur",
diff --git a/src/core/i18n/translations/ja.json b/src/core/i18n/translations/ja.json
index 257df54a..aa12a2f2 100644
--- a/src/core/i18n/translations/ja.json
+++ b/src/core/i18n/translations/ja.json
@@ -1412,7 +1412,7 @@
"parse_error": "証明書を解析できませんでした。有効な PEM 形式の X.509 証明書か確認してください。",
"validity_valid": "証明書は現在有効です",
"validity_invalid": "証明書は期限切れ、またはまだ有効ではありません",
- "self_signed": "自己署名",
+ "self_issued": "自己発行(署名未検証)",
"certificate_details": "証明書の詳細",
"subject_label": "サブジェクト",
"issuer_label": "発行者",
diff --git a/src/core/i18n/translations/ko.json b/src/core/i18n/translations/ko.json
index eb2d279c..e07cdf0a 100644
--- a/src/core/i18n/translations/ko.json
+++ b/src/core/i18n/translations/ko.json
@@ -1412,7 +1412,7 @@
"parse_error": "인증서를 파싱할 수 없습니다. 유효한 PEM 형식 X.509 인증서인지 확인하세요.",
"validity_valid": "인증서가 현재 유효합니다",
"validity_invalid": "인증서가 만료되었거나 아직 유효하지 않습니다",
- "self_signed": "자체 서명",
+ "self_issued": "자체 발급(서명 미검증)",
"certificate_details": "인증서 상세 정보",
"subject_label": "주체",
"issuer_label": "발급자",
diff --git a/src/core/i18n/translations/zh-CN.json b/src/core/i18n/translations/zh-CN.json
index a1c45b02..c91eea77 100644
--- a/src/core/i18n/translations/zh-CN.json
+++ b/src/core/i18n/translations/zh-CN.json
@@ -1412,7 +1412,7 @@
"parse_error": "证书解析失败。请确认这是有效的 PEM 编码 X.509 证书。",
"validity_valid": "证书当前有效",
"validity_invalid": "证书已过期或尚未生效",
- "self_signed": "自签名",
+ "self_issued": "自颁发(未验证签名)",
"certificate_details": "证书详情",
"subject_label": "主体",
"issuer_label": "签发者",
diff --git a/src/core/i18n/translations/zh-TW.json b/src/core/i18n/translations/zh-TW.json
index 01609af7..39418501 100644
--- a/src/core/i18n/translations/zh-TW.json
+++ b/src/core/i18n/translations/zh-TW.json
@@ -1412,7 +1412,7 @@
"parse_error": "憑證解析失敗。請確認這是有效的 PEM 編碼 X.509 憑證。",
"validity_valid": "憑證目前有效",
"validity_invalid": "憑證已過期或尚未生效",
- "self_signed": "自簽",
+ "self_issued": "自頒發(未驗證簽章)",
"certificate_details": "憑證詳細資訊",
"subject_label": "主體",
"issuer_label": "簽發者",
diff --git a/src/features/tools/certificate-decoder/logic.ts b/src/features/tools/certificate-decoder/logic.ts
new file mode 100644
index 00000000..e9035646
--- /dev/null
+++ b/src/features/tools/certificate-decoder/logic.ts
@@ -0,0 +1,6 @@
+export function calculateIntegerBitLength(bytes: Uint8Array): number {
+ const firstNonZero = bytes.findIndex((byte) => byte !== 0)
+ if (firstNonZero < 0) return 0
+ const firstByte = bytes[firstNonZero]
+ return (bytes.length - firstNonZero - 1) * 8 + Math.floor(Math.log2(firstByte)) + 1
+}
diff --git a/src/features/tools/certificate-decoder/page.test.ts b/src/features/tools/certificate-decoder/page.test.ts
new file mode 100644
index 00000000..2b42fbe6
--- /dev/null
+++ b/src/features/tools/certificate-decoder/page.test.ts
@@ -0,0 +1,16 @@
+import { describe, expect, it } from "vitest"
+import { calculateIntegerBitLength } from "./logic"
+
+describe("certificate key size calculation", () => {
+ it("calculates the modulus bit length without counting DER sign padding", () => {
+ const modulus = new Uint8Array(257)
+ modulus[1] = 0x80
+
+ expect(calculateIntegerBitLength(modulus)).toBe(2048)
+ })
+
+ it("supports non-byte-aligned integer sizes", () => {
+ expect(calculateIntegerBitLength(Uint8Array.from([0x01, 0xff]))).toBe(9)
+ expect(calculateIntegerBitLength(Uint8Array.from([0x00, 0x00]))).toBe(0)
+ })
+})
diff --git a/src/features/tools/certificate-decoder/page.tsx b/src/features/tools/certificate-decoder/page.tsx
index 178c2d04..b42e2985 100644
--- a/src/features/tools/certificate-decoder/page.tsx
+++ b/src/features/tools/certificate-decoder/page.tsx
@@ -8,6 +8,7 @@ import { RelatedTools } from "@/core/seo/components/related-tools"
import { ToolActionBar, type ToolAction } from "@/features/tool-shell/tool-action-bar"
import { SensitiveInputWarning } from "@/features/tool-shell/sensitive-input-warning"
import { ToolPageContainer } from "@/components/layout/page-container"
+import { calculateIntegerBitLength } from "./logic"
// ─── ASN.1 / PEM Parser (Pure JS, no dependencies) ──────────────────────────
@@ -155,7 +156,42 @@ interface CertInfo {
publicKeyBits: number
extensions: { name: string; value: string; critical: boolean }[]
isValid: boolean
- isSelfSigned: boolean
+ isSelfIssued: boolean
+}
+
+function calculatePublicKeyBits(spki: ASN1Node, algorithmOID: string): number {
+ const bitString = spki.children[1]?.value
+ if (!bitString || bitString.length < 2) return 0
+ const keyBytes = bitString.slice(1)
+
+ if (algorithmOID === "1.2.840.113549.1.1.1") {
+ try {
+ const rsaKey = parseASN1(keyBytes)
+ const modulus = rsaKey.children[0]?.value
+ if (modulus?.length) return calculateIntegerBitLength(modulus)
+ } catch {
+ // Fall through to the encoded key size for malformed or uncommon inputs.
+ }
+ }
+
+ if (algorithmOID === "1.2.840.10045.2.1") {
+ const curveNode = spki.children[0]?.children[1]
+ const curveOID = curveNode?.tag === 6 ? decodeOID(curveNode.value) : ""
+ const curveBits: Record = {
+ "1.2.840.10045.3.1.7": 256,
+ "1.3.132.0.34": 384,
+ "1.3.132.0.35": 521,
+ }
+ if (curveBits[curveOID]) return curveBits[curveOID]
+ }
+
+ const fixedAlgorithmBits: Record = {
+ "1.3.101.110": 255,
+ "1.3.101.111": 448,
+ "1.3.101.112": 256,
+ "1.3.101.113": 448,
+ }
+ return fixedAlgorithmBits[algorithmOID] ?? Math.max(0, keyBytes.length * 8 - bitString[0])
}
function parseDN(node: ASN1Node): string {
@@ -208,7 +244,7 @@ function parseCertificate(pem: string): CertInfo | null {
const spki = tbsCert.children[idx++]
const pkAlgOID = decodeOID(spki.children[0].children[0].value)
const publicKeyAlgorithm = OID_NAMES[pkAlgOID] || pkAlgOID
- const publicKeyBits = (spki.children[1].value.length - 1) * 8
+ const publicKeyBits = calculatePublicKeyBits(spki, pkAlgOID)
// Extensions (optional, context tag 3)
const extensions: { name: string; value: string; critical: boolean }[] = []
@@ -239,7 +275,7 @@ function parseCertificate(pem: string): CertInfo | null {
return {
subject, issuer, serialNumber, notBefore, notAfter,
signatureAlgorithm, publicKeyAlgorithm, publicKeyBits,
- extensions, isValid, isSelfSigned: subject === issuer,
+ extensions, isValid, isSelfIssued: subject === issuer,
}
} catch {
return null
@@ -366,9 +402,9 @@ export function CertificateDecoderPage() {
? text("validity_valid")
: text("validity_invalid")}
- {certInfo.isSelfSigned && (
+ {certInfo.isSelfIssued && (
- {text("self_signed")}
+ {text("self_issued")}
)}
diff --git a/src/features/tools/cron-visualizer/logic.ts b/src/features/tools/cron-visualizer/logic.ts
new file mode 100644
index 00000000..b4bf9331
--- /dev/null
+++ b/src/features/tools/cron-visualizer/logic.ts
@@ -0,0 +1,84 @@
+import { validateCronExpression } from "@/features/tools/crontab-generator/logic"
+
+export type VisualizerCron = {
+ minute: string
+ hour: string
+ dom: string
+ month: string
+ dow: string
+}
+
+export function parseVisualizerCron(expression: string): VisualizerCron | null {
+ const validation = validateCronExpression(expression, "Invalid cron expression.")
+ if (!validation.ok || validation.hasSeconds) return null
+
+ const [minute, hour, dom, month, dow] = validation.parts
+ return { minute, hour, dom, month, dow }
+}
+
+export function expandCronField(field: string, min: number, max: number): number[] {
+ const results = new Set()
+ for (const part of field.split(",")) {
+ const stepMatch = /^(\*|\d+|\d+-\d+)\/(\d+)$/.exec(part)
+ const step = stepMatch ? Number(stepMatch[2]) : 1
+ const range = stepMatch ? stepMatch[1] : part
+ if (!Number.isSafeInteger(step) || step <= 0) return []
+
+ if (range === "*") {
+ for (let value = min; value <= max; value += step) results.add(value)
+ continue
+ }
+
+ const rangeMatch = /^(\d+)-(\d+)$/.exec(range)
+ if (rangeMatch) {
+ const start = Number(rangeMatch[1])
+ const end = Number(rangeMatch[2])
+ if (start < min || end > max || start > end) return []
+ for (let value = start; value <= end; value += step) results.add(value)
+ continue
+ }
+
+ if (!/^\d+$/.test(range)) return []
+ const value = Number(range)
+ if (!Number.isSafeInteger(value) || value < min || value > max) return []
+ results.add(value)
+ }
+ return [...results].sort((a, b) => a - b)
+}
+
+export function getNextExecutions(expression: string, count: number): Date[] {
+ const cron = parseVisualizerCron(expression)
+ if (!cron) return []
+
+ const minutes = expandCronField(cron.minute, 0, 59)
+ const hours = expandCronField(cron.hour, 0, 23)
+ const doms = cron.dom === "*" ? null : expandCronField(cron.dom, 1, 31)
+ const months = cron.month === "*" ? null : expandCronField(cron.month, 1, 12)
+ const dows = cron.dow === "*" ? null : expandCronField(cron.dow, 0, 6)
+ if (minutes.length === 0 || hours.length === 0 || doms?.length === 0 || months?.length === 0 || dows?.length === 0) return []
+
+ const results: Date[] = []
+ const cursor = new Date()
+ cursor.setSeconds(0, 0)
+ cursor.setMinutes(cursor.getMinutes() + 1)
+
+ let iterations = 0
+ while (results.length < count && iterations < 525960) {
+ iterations++
+ const month = cursor.getMonth() + 1
+ const day = cursor.getDate()
+ const dayOfWeek = cursor.getDay()
+ const hour = cursor.getHours()
+ const minute = cursor.getMinutes()
+
+ if (months && !months.includes(month)) { cursor.setMinutes(cursor.getMinutes() + 1); continue }
+ if (doms && !doms.includes(day)) { cursor.setMinutes(cursor.getMinutes() + 1); continue }
+ if (dows && !dows.includes(dayOfWeek)) { cursor.setMinutes(cursor.getMinutes() + 1); continue }
+ if (!hours.includes(hour)) { cursor.setMinutes(cursor.getMinutes() + 1); continue }
+ if (!minutes.includes(minute)) { cursor.setMinutes(cursor.getMinutes() + 1); continue }
+
+ results.push(new Date(cursor))
+ cursor.setMinutes(cursor.getMinutes() + 1)
+ }
+ return results
+}
diff --git a/src/features/tools/cron-visualizer/page.tsx b/src/features/tools/cron-visualizer/page.tsx
index 17f4a9f3..45f9165b 100644
--- a/src/features/tools/cron-visualizer/page.tsx
+++ b/src/features/tools/cron-visualizer/page.tsx
@@ -8,71 +8,7 @@ import { useLang } from "@/core/i18n/lang-provider"
import { Input } from "@/components/ui/input"
import { safeClipboardWrite } from "@/core/clipboard/clipboard"
import { ToolPageContainer } from "@/components/layout/page-container"
-
-function parseCron(expr: string): { minute: string; hour: string; dom: string; month: string; dow: string } | null {
- const parts = expr.trim().split(/\s+/)
- if (parts.length !== 5) return null
- return { minute: parts[0], hour: parts[1], dom: parts[2], month: parts[3], dow: parts[4] }
-}
-
-function expandField(field: string, min: number, max: number): number[] {
- const results = new Set()
- for (const part of field.split(",")) {
- const stepMatch = part.match(/^(.+)\/(\d+)$/)
- const step = stepMatch ? parseInt(stepMatch[2]) : 1
- const range = stepMatch ? stepMatch[1] : part
-
- if (range === "*") {
- for (let i = min; i <= max; i += step) results.add(i)
- } else if (range.includes("-")) {
- const [a, b] = range.split("-").map(Number)
- for (let i = a; i <= b; i += step) results.add(i)
- } else {
- results.add(parseInt(range))
- }
- }
- return [...results].filter(n => n >= min && n <= max).sort((a, b) => a - b)
-}
-
-function getNextExecutions(expr: string, count: number): Date[] {
- const cron = parseCron(expr)
- if (!cron) return []
- try {
- const minutes = expandField(cron.minute, 0, 59)
- const hours = expandField(cron.hour, 0, 23)
- const doms = cron.dom === "*" ? null : expandField(cron.dom, 1, 31)
- const months = cron.month === "*" ? null : expandField(cron.month, 1, 12)
- const dows = cron.dow === "*" ? null : expandField(cron.dow, 0, 6)
-
- const results: Date[] = []
- const now = new Date()
- const cursor = new Date(now)
- cursor.setSeconds(0, 0)
- cursor.setMinutes(cursor.getMinutes() + 1)
-
- let iterations = 0
- while (results.length < count && iterations < 525960) {
- iterations++
- const m = cursor.getMonth() + 1
- const d = cursor.getDate()
- const dow = cursor.getDay()
- const h = cursor.getHours()
- const min = cursor.getMinutes()
-
- if (months && !months.includes(m)) { cursor.setMinutes(cursor.getMinutes() + 1); continue }
- if (doms && !doms.includes(d)) { cursor.setMinutes(cursor.getMinutes() + 1); continue }
- if (dows && !dows.includes(dow)) { cursor.setMinutes(cursor.getMinutes() + 1); continue }
- if (!hours.includes(h)) { cursor.setMinutes(cursor.getMinutes() + 1); continue }
- if (!minutes.includes(min)) { cursor.setMinutes(cursor.getMinutes() + 1); continue }
-
- results.push(new Date(cursor))
- cursor.setMinutes(cursor.getMinutes() + 1)
- }
- return results
- } catch {
- return []
- }
-}
+import { getNextExecutions, parseVisualizerCron } from "./logic"
export function CronVisualizerPage() {
const { t, lang } = useLang()
@@ -96,7 +32,7 @@ export function CronVisualizerPage() {
React.useEffect(() => {
if (!expr.trim()) { setExecutions([]); setError(null); return }
- const cron = parseCron(expr)
+ const cron = parseVisualizerCron(expr)
if (!cron) { setError(toolT.invalid_expression); setExecutions([]); return }
const next = getNextExecutions(expr, 20)
setExecutions(next)
@@ -116,7 +52,7 @@ export function CronVisualizerPage() {
toast.success(t.common.copied)
}
- const cron = parseCron(expr)
+ const cron = parseVisualizerCron(expr)
const fieldLabels = [
toolT.minute,
toolT.hour,
diff --git a/src/features/tools/crontab-generator/logic.ts b/src/features/tools/crontab-generator/logic.ts
index c82b3e7a..f3aaf3e5 100644
--- a/src/features/tools/crontab-generator/logic.ts
+++ b/src/features/tools/crontab-generator/logic.ts
@@ -40,13 +40,46 @@ function getFieldErrorLabel(field: CronFieldDefinition): string {
return field.key.replace("_", " ")
}
-function validateNumericTokens(field: string, definition: CronFieldDefinition): string | null {
- const numericTokens = field.match(/\d+/g) ?? []
- for (const token of numericTokens) {
- const value = Number(token)
- if (!Number.isInteger(value) || value < definition.min || value > definition.max) {
- return `Invalid cron ${getFieldErrorLabel(definition)} field. Expected ${definition.min}-${definition.max}, got ${token}.`
+function getOutOfRangeError(definition: CronFieldDefinition, token: string): string {
+ return `Invalid cron ${getFieldErrorLabel(definition)} field. Expected ${definition.min}-${definition.max}, got ${token}.`
+}
+
+function validateFieldValue(token: string, definition: CronFieldDefinition): string | null {
+ const value = Number(token)
+ if (!Number.isSafeInteger(value) || value < definition.min || value > definition.max) {
+ return getOutOfRangeError(definition, token)
+ }
+ return null
+}
+
+function validateCronField(field: string, definition: CronFieldDefinition, genericError: string): string | null {
+ for (const segment of field.split(",")) {
+ const match = /^(\*|\d+|\d+-\d+)(?:\/(\d+))?$/.exec(segment)
+ if (!match) return genericError
+
+ const [, base, stepToken] = match
+ if (stepToken !== undefined) {
+ const step = Number(stepToken)
+ if (!Number.isSafeInteger(step) || step <= 0) {
+ return `Invalid cron ${getFieldErrorLabel(definition)} field. Step must be a positive integer.`
+ }
}
+
+ if (base === "*") continue
+
+ const rangeMatch = /^(\d+)-(\d+)$/.exec(base)
+ if (!rangeMatch) {
+ const valueError = validateFieldValue(base, definition)
+ if (valueError) return valueError
+ continue
+ }
+
+ const [, startToken, endToken] = rangeMatch
+ const startError = validateFieldValue(startToken, definition)
+ if (startError) return startError
+ const endError = validateFieldValue(endToken, definition)
+ if (endError) return endError
+ if (Number(startToken) > Number(endToken)) return genericError
}
return null
}
@@ -59,7 +92,7 @@ export function validateCronExpression(expression: string, genericError: string)
const definitions = getCronFieldDefinitions(parts.length)
for (let index = 0; index < definitions.length; index += 1) {
- const fieldError = validateNumericTokens(parts[index] ?? "", definitions[index])
+ const fieldError = validateCronField(parts[index] ?? "", definitions[index], genericError)
if (fieldError) return { ok: false, error: fieldError }
}
diff --git a/src/features/tools/env-parser/utils.ts b/src/features/tools/env-parser/utils.ts
index f2d8f374..fa132b13 100644
--- a/src/features/tools/env-parser/utils.ts
+++ b/src/features/tools/env-parser/utils.ts
@@ -9,6 +9,24 @@ export type EnvVar = {
export type EnvExportFormat = "json" | "yaml" | "docker-args"
+const ENV_KEY_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/
+
+export function isValidEnvKey(key: string): boolean {
+ return ENV_KEY_PATTERN.test(key)
+}
+
+function getExportableEnvVars(vars: EnvVar[]): EnvVar[] {
+ return vars.filter((variable) =>
+ !variable.isComment &&
+ !variable.isEmpty &&
+ isValidEnvKey(variable.key),
+ )
+}
+
+function quotePosixShellValue(value: string): string {
+ return `'${value.replace(/'/g, `'"'"'`)}'`
+}
+
export function parseEnvFile(content: string): EnvVar[] {
return content.split("\n").map((raw, i) => {
const trimmed = raw.trim()
@@ -29,21 +47,21 @@ export function parseEnvFile(content: string): EnvVar[] {
export function envToJson(vars: EnvVar[]): string {
const obj: Record = {}
- vars.filter((variable) => !variable.isComment && !variable.isEmpty && variable.key).forEach((variable) => {
+ getExportableEnvVars(vars).forEach((variable) => {
obj[variable.key] = variable.value
})
return JSON.stringify(obj, null, 2)
}
export function envToYaml(vars: EnvVar[]): string {
- return vars.filter((variable) => !variable.isComment && !variable.isEmpty && variable.key)
- .map((variable) => `${variable.key}: "${variable.value.replace(/"/g, "\\\"")}"`)
+ return getExportableEnvVars(vars)
+ .map((variable) => `${variable.key}: ${JSON.stringify(variable.value)}`)
.join("\n")
}
export function envToDockerArgs(vars: EnvVar[]): string {
- return vars.filter((variable) => !variable.isComment && !variable.isEmpty && variable.key)
- .map((variable) => `-e ${variable.key}="${variable.value}"`)
+ return getExportableEnvVars(vars)
+ .map((variable) => `-e ${variable.key}=${quotePosixShellValue(variable.value)}`)
.join(" \\\n ")
}
diff --git a/src/features/tools/hash-generator/hash-task.ts b/src/features/tools/hash-generator/hash-task.ts
index 21b23a8d..12468f70 100644
--- a/src/features/tools/hash-generator/hash-task.ts
+++ b/src/features/tools/hash-generator/hash-task.ts
@@ -11,14 +11,18 @@ export async function runHashTask(input: HashTaskInput, options: HashTaskOptions
return runHashTaskSync(input)
}
+ const workerInput: HashTaskInput = input.mode === "file"
+ ? { mode: "file", bytes: input.bytes.slice() }
+ : input
+
try {
return await runWorkerTask(
() => new Worker(new URL("./hash-worker.ts", import.meta.url), { type: "module" }),
- input,
+ workerInput,
{
signal: options.signal,
timeoutMs: options.timeoutMs ?? 20_000,
- transfer: input.mode === "file" ? [input.bytes.buffer] : undefined,
+ transfer: workerInput.mode === "file" ? [workerInput.bytes.buffer] : undefined,
},
)
} catch (error) {
diff --git a/src/features/tools/json-to-typescript/utils.test.ts b/src/features/tools/json-to-typescript/utils.test.ts
new file mode 100644
index 00000000..3585f809
--- /dev/null
+++ b/src/features/tools/json-to-typescript/utils.test.ts
@@ -0,0 +1,37 @@
+import { describe, expect, it } from "vitest"
+import { jsonToTs } from "./utils"
+
+const options = { readonly: false, optional: false }
+
+describe("jsonToTs", () => {
+ it("merges every array element and marks missing fields optional", () => {
+ const output = jsonToTs([
+ { id: 1, active: true },
+ { id: "second", email: "user@example.com" },
+ ], "Root", options)
+
+ expect(output).toContain("id: number | string;")
+ expect(output).toContain("active?: boolean;")
+ expect(output).toContain("email?: string;")
+ })
+
+ it("does not reuse interfaces that only share field names", () => {
+ const output = jsonToTs({ a: { x: 1 }, b: { x: "text" } }, "Root", options)
+
+ expect(output).toContain("export interface RootA")
+ expect(output).toContain("x: number;")
+ expect(output).toContain("export interface RootB")
+ expect(output).toContain("x: string;")
+ expect(output).toContain("a: RootA;")
+ expect(output).toContain("b: RootB;")
+ })
+
+ it("infers unions for heterogeneous primitive arrays", () => {
+ expect(jsonToTs([1, "two", null], "Value", options)).toContain("export type Value = (number | string | null)[];")
+ })
+
+ it("quotes property names using valid TypeScript string literals", () => {
+ const output = jsonToTs({ 'quote"line\n': true }, "Root", options)
+ expect(output).toContain('"quote\\"line\\n": boolean;')
+ })
+})
diff --git a/src/features/tools/json-to-typescript/utils.ts b/src/features/tools/json-to-typescript/utils.ts
index 8bab3f42..7b0aa571 100644
--- a/src/features/tools/json-to-typescript/utils.ts
+++ b/src/features/tools/json-to-typescript/utils.ts
@@ -1,67 +1,172 @@
-export function jsonToTs(json: unknown, name: string, options: { readonly: boolean; optional: boolean }): string {
- const lines: string[] = []
- const seen = new Map()
+type PrimitiveType = {
+ kind: "primitive"
+ name: "string" | "number" | "boolean" | "null" | "unknown"
+}
- function capitalize(s: string): string {
- return s.charAt(0).toUpperCase() + s.slice(1)
- }
+type ArrayType = {
+ kind: "array"
+ item: InferredType
+}
+
+type ObjectProperty = {
+ name: string
+ type: InferredType
+ optional: boolean
+}
+
+type ObjectType = {
+ kind: "object"
+ properties: ObjectProperty[]
+}
- function sanitize(key: string): string {
- return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(key) ? key : `"${key}"`
+type UnionType = {
+ kind: "union"
+ types: InferredType[]
+}
+
+type InferredType = PrimitiveType | ArrayType | ObjectType | UnionType
+
+const primitive = (name: PrimitiveType["name"]): PrimitiveType => ({ kind: "primitive", name })
+
+function typeSignature(type: InferredType): string {
+ switch (type.kind) {
+ case "primitive":
+ return type.name
+ case "array":
+ return `array:${typeSignature(type.item)}`
+ case "object":
+ return `object:${type.properties
+ .map((property) => `${JSON.stringify(property.name)}${property.optional ? "?" : ""}:${typeSignature(property.type)}`)
+ .sort()
+ .join(",")}`
+ case "union":
+ return `union:${type.types.map(typeSignature).sort().join("|")}`
}
+}
- function inferType(value: unknown, typeName: string): string {
- if (value === null) return "null"
- if (Array.isArray(value)) {
- if (value.length === 0) return "unknown[]"
- const itemType = inferType(value[0], typeName + "Item")
- return `${itemType}[]`
+function mergeObjectTypes(left: ObjectType, right: ObjectType): ObjectType {
+ const rightByName = new Map(right.properties.map((property) => [property.name, property]))
+ const leftNames = new Set(left.properties.map((property) => property.name))
+ const properties = left.properties.map((property) => {
+ const other = rightByName.get(property.name)
+ if (!other) return { ...property, optional: true }
+ return {
+ name: property.name,
+ type: mergeTypes(property.type, other.type),
+ optional: property.optional || other.optional,
}
- switch (typeof value) {
- case "string": return "string"
- case "number": return "number"
- case "boolean": return "boolean"
- default:
- if (typeof value === "object" && value !== null) {
- const existing = seen.get(JSON.stringify(Object.keys(value).sort()))
- if (existing) return existing
- generateInterface(value as Record, typeName)
- return typeName
- }
- return "unknown"
+ })
+
+ for (const property of right.properties) {
+ if (!leftNames.has(property.name)) properties.push({ ...property, optional: true })
+ }
+ return { kind: "object", properties }
+}
+
+function mergeTypes(left: InferredType, right: InferredType): InferredType {
+ if (typeSignature(left) === typeSignature(right)) return left
+ if (left.kind === "object" && right.kind === "object") return mergeObjectTypes(left, right)
+
+ const candidates = [
+ ...(left.kind === "union" ? left.types : [left]),
+ ...(right.kind === "union" ? right.types : [right]),
+ ]
+ const unique = new Map()
+ candidates.forEach((candidate) => unique.set(typeSignature(candidate), candidate))
+ return { kind: "union", types: [...unique.values()] }
+}
+
+function inferType(value: unknown): InferredType {
+ if (value === null) return primitive("null")
+ if (Array.isArray(value)) {
+ const item = value.reduce(
+ (current, entry) => current ? mergeTypes(current, inferType(entry)) : inferType(entry),
+ null,
+ )
+ return { kind: "array", item: item ?? primitive("unknown") }
+ }
+ if (typeof value === "object") {
+ return {
+ kind: "object",
+ properties: Object.entries(value as Record).map(([name, entry]) => ({
+ name,
+ type: inferType(entry),
+ optional: false,
+ })),
}
}
+ if (typeof value === "string") return primitive("string")
+ if (typeof value === "number") return primitive("number")
+ if (typeof value === "boolean") return primitive("boolean")
+ return primitive("unknown")
+}
- function generateInterface(obj: Record, interfaceName: string) {
- const key = JSON.stringify(Object.keys(obj).sort())
- if (seen.has(key)) return
- seen.set(key, interfaceName)
+function sanitizePropertyName(name: string): string {
+ return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : JSON.stringify(name)
+}
- const prefix = options.readonly ? "readonly " : ""
- const suffix = options.optional ? "?" : ""
+function sanitizeTypeName(name: string): string {
+ const parts = name.trim().split(/[^\p{L}\p{N}_$]+/u).filter(Boolean)
+ const candidate = parts.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("") || "Root"
+ return /^\p{N}/u.test(candidate) ? `T${candidate}` : candidate
+}
+
+function childTypeName(parent: string, property: string): string {
+ return `${parent}${sanitizeTypeName(property)}`
+}
- const members = Object.entries(obj).map(([k, v]) => {
- const childTypeName = capitalize(interfaceName) + capitalize(k)
- const type = inferType(v, childTypeName)
- return ` ${prefix}${sanitize(k)}${suffix}: ${type};`
- })
+export function jsonToTs(json: unknown, name: string, options: { readonly: boolean; optional: boolean }): string {
+ const declarations: string[] = []
+ const usedNames = new Set()
+ const renderedObjects = new Map()
- lines.push(`export interface ${interfaceName} {\n${members.join("\n")}\n}\n`)
+ function reserveName(preferredName: string): string {
+ const base = sanitizeTypeName(preferredName)
+ let candidate = base
+ let suffix = 2
+ while (usedNames.has(candidate)) candidate = `${base}${suffix++}`
+ usedNames.add(candidate)
+ return candidate
}
- if (Array.isArray(json)) {
- if (json.length > 0 && typeof json[0] === "object" && json[0] !== null) {
- generateInterface(json[0] as Record, name)
- lines.push(`export type ${name}List = ${name}[];\n`)
- } else {
- const itemType = json.length > 0 ? inferType(json[0], name + "Item") : "unknown"
- lines.push(`export type ${name} = ${itemType}[];\n`)
+ function renderType(type: InferredType, preferredName: string): string {
+ switch (type.kind) {
+ case "primitive":
+ return type.name
+ case "array": {
+ const item = renderType(type.item, `${preferredName}Item`)
+ return type.item.kind === "union" ? `(${item})[]` : `${item}[]`
+ }
+ case "union":
+ return type.types.map((entry, index) => renderType(entry, `${preferredName}${index + 1}`)).join(" | ")
+ case "object": {
+ const existing = renderedObjects.get(type)
+ if (existing) return existing
+
+ const interfaceName = reserveName(preferredName)
+ renderedObjects.set(type, interfaceName)
+ const prefix = options.readonly ? "readonly " : ""
+ const members = type.properties.map((property) => {
+ const propertyType = renderType(property.type, childTypeName(interfaceName, property.name))
+ const suffix = options.optional || property.optional ? "?" : ""
+ return ` ${prefix}${sanitizePropertyName(property.name)}${suffix}: ${propertyType};`
+ })
+ declarations.push(`export interface ${interfaceName} {\n${members.join("\n")}\n}`)
+ return interfaceName
+ }
}
- } else if (typeof json === "object" && json !== null) {
- generateInterface(json as Record, name)
+ }
+
+ const rootName = sanitizeTypeName(name)
+ const inferred = inferType(json)
+ if (inferred.kind === "object") {
+ renderType(inferred, rootName)
+ } else if (inferred.kind === "array" && inferred.item.kind === "object") {
+ const itemType = renderType(inferred.item, rootName)
+ declarations.push(`export type ${rootName}List = ${itemType}[];`)
} else {
- lines.push(`export type ${name} = ${typeof json};\n`)
+ declarations.push(`export type ${rootName} = ${renderType(inferred, `${rootName}Item`)};`)
}
- return lines.join("\n")
+ return `${declarations.join("\n\n")}\n`
}
diff --git a/src/features/tools/messagepack-inspector/logic.test.ts b/src/features/tools/messagepack-inspector/logic.test.ts
index 0aa45bcf..5ed95c67 100644
--- a/src/features/tools/messagepack-inspector/logic.test.ts
+++ b/src/features/tools/messagepack-inspector/logic.test.ts
@@ -8,6 +8,29 @@ describe("messagepack-inspector logic", () => {
expect(report.summary).toContain("map")
})
+ it("preserves non-string map keys without collisions", () => {
+ const report = decodeMessagePack("8201a161a131a162", "hex")
+
+ expect(report.value).toEqual({
+ type: "map",
+ entries: [
+ { key: 1, value: "a" },
+ { key: "1", value: "b" },
+ ],
+ })
+ expect(report.json).toContain('"key": 1')
+ expect(report.json).toContain('"key": "1"')
+ })
+
+ it("treats __proto__ as data instead of mutating the result prototype", () => {
+ const report = decodeMessagePack("81a95f5f70726f746f5f5f81a17801", "hex")
+
+ expect(Object.getPrototypeOf(report.value)).toBeNull()
+ expect(Object.prototype.hasOwnProperty.call(report.value, "__proto__")).toBe(true)
+ expect((report.value as Record)["__proto__"]).toEqual({ x: 1 })
+ expect(report.json).toContain('"__proto__"')
+ })
+
it("decodes arrays and booleans", () => {
expect(decodeMessagePack("93c3c2c0", "hex").value).toEqual([true, false, null])
})
diff --git a/src/features/tools/messagepack-inspector/logic.ts b/src/features/tools/messagepack-inspector/logic.ts
index 685e700e..bce1e9a0 100644
--- a/src/features/tools/messagepack-inspector/logic.ts
+++ b/src/features/tools/messagepack-inspector/logic.ts
@@ -99,6 +99,11 @@ type MessagePackTimestampValue = {
hex: string
}
+type MessagePackMapValue = {
+ type: "map"
+ entries: Array<{ key: unknown; value: unknown }>
+}
+
function readUInt32FromBytes(bytes: Uint8Array, offset = 0): number {
return bytes[offset] * 2 ** 24 + (bytes[offset + 1] << 16) + (bytes[offset + 2] << 8) + bytes[offset + 3]
}
@@ -347,24 +352,24 @@ function decodeValue(reader: Reader): unknown {
}
}
-function decodeMap(reader: Reader, length: number): Record {
+function decodeMap(reader: Reader, length: number): Record | MessagePackMapValue {
if (length > Math.floor(reader.remaining() / 2)) throw new Error(`MessagePack map declares ${length} pair(s), but only ${reader.remaining()} byte(s) remain.`)
- const output: Record = {}
+ const entries: MessagePackMapValue["entries"] = []
for (let index = 0; index < length; index += 1) {
const key = decodeValue(reader)
- output[mapKeyToString(key)] = decodeValue(reader)
+ entries.push({ key, value: decodeValue(reader) })
}
- return output
-}
-function mapKeyToString(key: unknown): string {
- if (typeof key === "string") return key
- if (key === null || typeof key !== "object") return String(key)
- try {
- return JSON.stringify(key) ?? String(key)
- } catch {
- return String(key)
- }
+ const stringKeys = entries.map(({ key }) => typeof key === "string" ? key : null)
+ const canUseObject = stringKeys.every((key): key is string => key !== null)
+ && new Set(stringKeys).size === stringKeys.length
+ if (!canUseObject) return { type: "map", entries }
+
+ const output: Record = Object.create(null) as Record
+ entries.forEach(({ value }, index) => {
+ output[stringKeys[index]] = value
+ })
+ return output
}
function isRecord(value: unknown): value is Record {
@@ -374,6 +379,7 @@ function isRecord(value: unknown): value is Record {
function summarize(value: unknown): string {
if (Array.isArray(value)) return `Decoded MessagePack array with ${value.length} item(s).`
if (isRecord(value)) {
+ if (value.type === "map" && Array.isArray(value.entries)) return `Decoded MessagePack map with ${value.entries.length} key(s).`
if (value.type === "binary" && typeof value.length === "number") return `Decoded MessagePack binary with ${value.length} byte(s).`
if (value.type === "extension" && typeof value.extensionType === "number" && typeof value.length === "number") {
return `Decoded MessagePack extension type ${value.extensionType} with ${value.length} byte(s).`
diff --git a/src/features/tools/open-graph-meta-generator/manifest.ts b/src/features/tools/open-graph-meta-generator/manifest.ts
index e77d27a1..7624adb3 100644
--- a/src/features/tools/open-graph-meta-generator/manifest.ts
+++ b/src/features/tools/open-graph-meta-generator/manifest.ts
@@ -6,12 +6,17 @@ export const toolManifest = {
category: "generators",
relatedTools: ["tweet_generator", "tweet_to_image_converter", "instagram_post_generator", "instagram_story_generator"],
privacy: {
- executionMode: "browser-local",
- offlineCapable: true,
+ executionMode: "external-request",
+ offlineCapable: false,
sensitiveInput: false,
externalRequest: {
- required: false,
- endpointType: "none",
+ required: true,
+ endpointType: "user_provided_url",
+ domains: ["instagram.com", "i.ytimg.com", "vumbnail.com"],
+ purposeKey: "thumbnail_preview",
+ userDataSent: "user_provided_url",
+ disclosure: "Loads a supported public image URL you provide only after you confirm and choose Preview.",
+ consentRequired: true,
},
},
compliance: {
diff --git a/src/features/tools/open-graph-meta-generator/page.tsx b/src/features/tools/open-graph-meta-generator/page.tsx
index dfb5fd2f..5fac6026 100644
--- a/src/features/tools/open-graph-meta-generator/page.tsx
+++ b/src/features/tools/open-graph-meta-generator/page.tsx
@@ -1,22 +1,32 @@
"use client"
import * as React from "react"
-import { Copy, Download, Eraser, Globe2, TestTube2 } from "lucide-react"
+import { Copy, Download, Eraser, Globe2, ImageDown, TestTube2 } from "lucide-react"
import { useLang } from "@/core/i18n/lang-provider"
import { Input } from "@/components/ui/input"
import { Textarea } from "@/components/ui/textarea"
import { ToolActionBar, type ToolAction } from "@/features/tool-shell/tool-action-bar"
-import { copyTextWithToolFeedback, downloadedFileFeedback } from "@/features/tool-shell/tool-action-feedback"
+import {
+ copyTextWithToolFeedback,
+ downloadedFileFeedback,
+ notifyToolActionFailure,
+ notifyToolActionSuccess,
+} from "@/features/tool-shell/tool-action-feedback"
+import { ExternalRequestConfirmation } from "@/features/tool-shell/external-request-confirmation"
+import { isBrowserOffline, useBrowserOnlineStatus } from "@/features/tool-shell/external-request-offline"
import { TextOutputPanel } from "@/features/tool-shell/text-output-panel"
import { ToolPreviewArea } from "@/features/tool-shell/tool-preview-area"
import {
buildOpenGraphMetaTags,
buildOpenGraphSnippetDocument,
normalizeAbsoluteHttpUrl,
+ normalizeSupportedOpenGraphPreviewUrl,
type OpenGraphInput,
} from "@/features/tools/open-graph-meta-generator/utils"
import { ToolPageContainer } from "@/components/layout/page-container"
+const EXTERNAL_IMAGE_HOSTS = ["instagram.com", "i.ytimg.com", "vumbnail.com"] as const
+
export function OpenGraphMetaGeneratorPage() {
const { t, lang } = useLang()
const toolT = t.tools["open_graph_meta_generator"] as Record
@@ -33,22 +43,35 @@ export function OpenGraphMetaGeneratorPage() {
}), [lang, toolT.sample_description, toolT.sample_title])
const [form, setForm] = React.useState(sample)
+ const [approvedPreviewImage, setApprovedPreviewImage] = React.useState(null)
+ const [externalRequestConfirmed, setExternalRequestConfirmed] = React.useState(false)
+ const isOnline = useBrowserOnlineStatus()
React.useEffect(() => {
setForm(sample)
+ setApprovedPreviewImage(null)
+ setExternalRequestConfirmed(false)
}, [sample])
const tags = React.useMemo(() => buildOpenGraphMetaTags(form), [form])
const output = React.useMemo(() => buildOpenGraphSnippetDocument(tags), [tags])
const previewUrl = React.useMemo(() => normalizeAbsoluteHttpUrl(form.url), [form.url])
- const previewImage = React.useMemo(() => normalizeAbsoluteHttpUrl(form.image), [form.image])
+ const candidatePreviewImage = React.useMemo(() => normalizeSupportedOpenGraphPreviewUrl(form.image), [form.image])
const setField = (key: K, value: OpenGraphInput[K]) => {
+ if (key === "image") {
+ setApprovedPreviewImage(null)
+ setExternalRequestConfirmed(false)
+ }
setForm((prev) => ({ ...prev, [key]: value }))
}
- const handleSample = () => setForm(sample)
- const handleReset = () =>
+ const handleSample = () => {
+ setForm(sample)
+ setApprovedPreviewImage(null)
+ setExternalRequestConfirmed(false)
+ }
+ const handleReset = () => {
setForm({
title: "",
description: "",
@@ -59,6 +82,34 @@ export function OpenGraphMetaGeneratorPage() {
twitterCard: "summary",
twitterSite: "",
})
+ setApprovedPreviewImage(null)
+ setExternalRequestConfirmed(false)
+ }
+
+ const handlePreview = () => {
+ if (!candidatePreviewImage) return
+ if (!externalRequestConfirmed) {
+ return notifyToolActionFailure(t, {
+ kind: "share",
+ label: t.common.preview,
+ title: t.common.external_network_notice.confirm_required,
+ })
+ }
+ if (isBrowserOffline()) {
+ return notifyToolActionFailure(t, {
+ kind: "share",
+ label: t.common.preview,
+ title: t.common.external_network_notice.offline_required,
+ })
+ }
+
+ setApprovedPreviewImage(candidatePreviewImage)
+ return notifyToolActionSuccess(t, {
+ kind: "share",
+ label: t.common.preview,
+ title: t.common.action_status_success.replace("{action}", t.common.preview),
+ })
+ }
const handleCopy = async () => {
return copyTextWithToolFeedback(t, output, toolT.preview_meta_tags_label || t.common.output)
@@ -79,6 +130,18 @@ export function OpenGraphMetaGeneratorPage() {
const actions: ToolAction[] = [
{ id: "sample", label: t.common.sample, icon: TestTube2, onClick: handleSample },
{ id: "reset", label: t.common.reset, icon: Eraser, onClick: handleReset },
+ {
+ id: "preview",
+ label: t.common.preview,
+ icon: ImageDown,
+ onClick: handlePreview,
+ disabled: !candidatePreviewImage || !externalRequestConfirmed || !isOnline,
+ disabledReason: !candidatePreviewImage
+ ? t.common.action_disabled_input_required
+ : !externalRequestConfirmed
+ ? t.common.external_network_notice.confirm_required
+ : t.common.external_network_notice.offline_required,
+ },
{ id: "copy", label: t.common.copy, icon: Copy, onClick: handleCopy },
{ id: "download", label: t.common.download, icon: Download, onClick: handleDownload },
]
@@ -136,6 +199,16 @@ export function OpenGraphMetaGeneratorPage() {
+ {
+ setExternalRequestConfirmed(confirmed)
+ if (!confirmed) setApprovedPreviewImage(null)
+ }}
+ />
@@ -151,11 +224,12 @@ export function OpenGraphMetaGeneratorPage() {
allowBackgroundToggle={false}
allowFullscreen={true}
>
- {previewImage ? (
+ {approvedPreviewImage ? (
// eslint-disable-next-line @next/next/no-img-element

) : (
diff --git a/src/features/tools/open-graph-meta-generator/utils.ts b/src/features/tools/open-graph-meta-generator/utils.ts
index 340dc5a7..7454163c 100644
--- a/src/features/tools/open-graph-meta-generator/utils.ts
+++ b/src/features/tools/open-graph-meta-generator/utils.ts
@@ -9,6 +9,8 @@ export type OpenGraphInput = {
twitterSite: string
}
+const SUPPORTED_PREVIEW_HOSTS = new Set(["i.ytimg.com", "vumbnail.com"])
+
function collapseWhitespace(value: string): string {
return value.replace(/\s+/g, " ").trim()
}
@@ -34,14 +36,27 @@ export function normalizeAbsoluteHttpUrl(raw: string): string | null {
}
}
+export function normalizeSupportedOpenGraphPreviewUrl(raw: string): string | null {
+ const normalized = normalizeAbsoluteHttpUrl(raw)
+ if (!normalized) return null
+
+ const url = new URL(normalized)
+ if (url.protocol !== "https:" || url.username || url.password || url.port) return null
+
+ const hostname = url.hostname.toLowerCase()
+ const isInstagramHost = hostname === "instagram.com" || hostname.endsWith(".instagram.com")
+ return isInstagramHost || SUPPORTED_PREVIEW_HOSTS.has(hostname) ? url.toString() : null
+}
+
export function buildOpenGraphMetaTags(input: OpenGraphInput): string {
const title = escapeHtml(collapseWhitespace(input.title))
const description = escapeHtml(collapseWhitespace(input.description))
const siteName = escapeHtml(collapseWhitespace(input.siteName))
const url = normalizeAbsoluteHttpUrl(input.url) || ""
const image = normalizeAbsoluteHttpUrl(input.image) || ""
- const type = collapseWhitespace(input.type || "website")
+ const type = escapeHtml(collapseWhitespace(input.type || "website"))
const twitterSite = collapseWhitespace(input.twitterSite)
+ const twitterCard = input.twitterCard === "summary_large_image" ? "summary_large_image" : "summary"
const lines = [
`
`,
@@ -50,7 +65,7 @@ export function buildOpenGraphMetaTags(input: OpenGraphInput): string {
url ? `
` : "",
image ? `
` : "",
siteName ? `
` : "",
- `
`,
+ `
`,
`
`,
`
`,
image ? `
` : "",
diff --git a/src/features/tools/pipeline-builder/page.tsx b/src/features/tools/pipeline-builder/page.tsx
index 9b75eb84..832007cd 100644
--- a/src/features/tools/pipeline-builder/page.tsx
+++ b/src/features/tools/pipeline-builder/page.tsx
@@ -67,7 +67,7 @@ export function PipelineBuilderPage() {
const [importError, setImportError] = React.useState
(null)
const [actionAnnouncement, setActionAnnouncement] = React.useState("")
const [onboardingDismissed, setOnboardingDismissed] = React.useState(false)
-
+ const runRequestRef = React.useRef(0)
const selectedStep = recipe.steps.find((step) => step.id === selectedStepId) ?? recipe.steps[0] ?? null
const validation = React.useMemo(() => validateRecipe(recipe), [recipe])
const compatibilityHints = React.useMemo(() => getStepCompatibilityHints(recipe.steps), [recipe.steps])
@@ -105,11 +105,16 @@ export function PipelineBuilderPage() {
void refreshSavedRecipes()
}, [refreshSavedRecipes, storageAvailable])
+ const invalidateExecution = React.useCallback(() => {
+ runRequestRef.current += 1
+ setResult(null)
+ setIsRunning(false)
+ }, [])
const updateRecipe = React.useCallback((updater: (current: RecipeDocument) => RecipeDocument) => {
setRecipe((current) => updateRecipeTimestamp(updater(current)))
- setResult(null)
+ invalidateExecution()
setImportError(null)
- }, [])
+ }, [invalidateExecution])
const addStep = React.useCallback(() => {
const step = createStep(pendingToolKey)
@@ -186,10 +191,10 @@ export function PipelineBuilderPage() {
setRecipe(generated.recipe)
setInitialInput(generated.initialInput)
setSelectedStepId(generated.recipe.steps[0]?.id ?? null)
- setResult(null)
+ invalidateExecution()
setImportError(null)
toast.success(text("template_loaded"))
- }, [lang, text])
+ }, [invalidateExecution, lang, text])
React.useEffect(() => {
const params = new URLSearchParams(window.location.search)
@@ -229,9 +234,11 @@ export function PipelineBuilderPage() {
}, [recipe.steps, selectedStepId])
const runCurrentRecipe = React.useCallback(async () => {
+ const requestId = ++runRequestRef.current
setIsRunning(true)
try {
const execution = await runRecipe(recipe, initialInput)
+ if (requestId !== runRequestRef.current) return
setResult(execution)
if (execution.ok) {
toast.success(text("run_complete"))
@@ -239,7 +246,7 @@ export function PipelineBuilderPage() {
toast.error(text("run_failed"))
}
} finally {
- setIsRunning(false)
+ if (requestId === runRequestRef.current) setIsRunning(false)
}
}, [initialInput, recipe, text])
@@ -273,9 +280,9 @@ export function PipelineBuilderPage() {
}
setRecipe(loadResult.value.recipe)
setSelectedStepId(loadResult.value.recipe.steps[0]?.id ?? null)
- setResult(null)
+ invalidateExecution()
toast.success(text("recipe_loaded"))
- }, [selectedSavedId, storageAvailable, text])
+ }, [invalidateExecution, selectedSavedId, storageAvailable, text])
const deleteRecipe = React.useCallback(async () => {
if (!storageAvailable || !selectedSavedId) return
@@ -302,10 +309,10 @@ export function PipelineBuilderPage() {
}
setRecipe(imported.recipe)
setSelectedStepId(imported.recipe.steps[0]?.id ?? null)
- setResult(null)
+ invalidateExecution()
setImportError(null)
setActionAnnouncement(imported.announcement)
- }, [text])
+ }, [invalidateExecution, text])
const performShareRecipe = React.useCallback(async () => {
return sharePipelineRecipe(recipe, lang, t, text, setActionAnnouncement)
@@ -488,7 +495,7 @@ export function PipelineBuilderPage() {
value={initialInput}
onChange={(event) => {
setInitialInput(event.target.value)
- setResult(null)
+ invalidateExecution()
}}
placeholder={text("initial_input_placeholder")}
className="font-mono text-sm"
diff --git a/src/features/tools/qr-code-generator/browser-actions.ts b/src/features/tools/qr-code-generator/browser-actions.ts
index 13696552..31cacb8a 100644
--- a/src/features/tools/qr-code-generator/browser-actions.ts
+++ b/src/features/tools/qr-code-generator/browser-actions.ts
@@ -17,6 +17,16 @@ export async function loadToast() {
return toastPromise
}
+export function createQrRenderCanvas(): HTMLCanvasElement {
+ return document.createElement("canvas")
+}
+
+export function commitQrRenderCanvas(source: HTMLCanvasElement, target: HTMLCanvasElement): void {
+ target.width = source.width
+ target.height = source.height
+ target.getContext("2d")?.drawImage(source, 0, 0)
+}
+
export function drawRoundedRect(ctx: CanvasRenderingContext2D, x: number, y: number, width: number, height: number, radius: number) {
ctx.beginPath()
ctx.moveTo(x + radius, y)
diff --git a/src/features/tools/qr-code-generator/page.tsx b/src/features/tools/qr-code-generator/page.tsx
index d92d23df..e4b8c976 100644
--- a/src/features/tools/qr-code-generator/page.tsx
+++ b/src/features/tools/qr-code-generator/page.tsx
@@ -11,6 +11,8 @@ import { FILE_INPUT_POLICIES, validateFileAgainstPolicy } from "@/core/files/fil
import { parseSafeExternalUrl } from "@/core/security/external-url"
import {
buildQrSvg,
+ commitQrRenderCanvas,
+ createQrRenderCanvas,
decodeQrImageFile,
downloadCanvasPng,
downloadSvg as downloadSvgFile,
@@ -74,24 +76,27 @@ export function QrCodeGeneratorPage() {
const logoInputRef = React.useRef(null)
const decodeInputRef = React.useRef(null)
const decodeRequestRef = React.useRef(0)
+ const renderRequestRef = React.useRef(0)
const decodedUrl = React.useMemo(() => {
const parsed = parseSafeExternalUrl(decodedPayload, { requireHttps: false })
return parsed.ok ? parsed.url : null
}, [decodedPayload])
- const renderToCanvas = React.useCallback(async () => {
+ const renderToCanvas = React.useCallback(async (requestId: number) => {
if (!text.trim()) {
- setDataUrl("")
+ if (requestId === renderRequestRef.current) setDataUrl("")
return
}
- const canvas = canvasRef.current
- if (!canvas) return
+ const visibleCanvas = canvasRef.current
+ if (!visibleCanvas) return
+ const renderCanvas = createQrRenderCanvas()
const qrCode = await loadQRCode()
+ if (requestId !== renderRequestRef.current) return
- await qrCode.toCanvas(canvas, text, {
+ await qrCode.toCanvas(renderCanvas, text, {
width: size,
margin,
errorCorrectionLevel,
@@ -100,11 +105,13 @@ export function QrCodeGeneratorPage() {
light: bgColor,
},
})
+ if (requestId !== renderRequestRef.current) return
if (logoEnabled && logoDataUrl) {
- const ctx = canvas.getContext("2d")
+ const ctx = renderCanvas.getContext("2d")
if (ctx) {
const img = await loadImage(logoDataUrl)
+ if (requestId !== renderRequestRef.current) return
const logoSize = Math.floor((size * logoScale) / 100)
const x = (size - logoSize) / 2
const y = (size - logoSize) / 2
@@ -120,22 +127,24 @@ export function QrCodeGeneratorPage() {
}
}
- setDataUrl(canvas.toDataURL("image/png"))
+ if (requestId !== renderRequestRef.current) return
+ commitQrRenderCanvas(renderCanvas, visibleCanvas)
+ setDataUrl(renderCanvas.toDataURL("image/png"))
}, [bgColor, errorCorrectionLevel, fgColor, logoDataUrl, logoEnabled, logoScale, margin, size, text])
React.useEffect(() => {
- let cancelled = false
+ const requestId = ++renderRequestRef.current
void (async () => {
try {
- await renderToCanvas()
+ await renderToCanvas(requestId)
} catch {
- if (!cancelled) {
+ if (requestId === renderRequestRef.current) {
setDataUrl("")
}
}
})()
return () => {
- cancelled = true
+ if (requestId === renderRequestRef.current) renderRequestRef.current += 1
}
}, [renderToCanvas])
diff --git a/src/features/tools/svg-blob-generator/utils.ts b/src/features/tools/svg-blob-generator/utils.ts
index c472494d..454275b8 100644
--- a/src/features/tools/svg-blob-generator/utils.ts
+++ b/src/features/tools/svg-blob-generator/utils.ts
@@ -5,6 +5,18 @@ export type BlobConfig = {
seed: number
}
+const HEX_COLOR_PATTERN = /^#(?:[0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i
+const DEFAULT_FILL = "#22d3ee"
+const DEFAULT_STROKE = "#0f172a"
+
+export function normalizeSvgColor(value: string, fallback: string): string {
+ const normalized = value.trim()
+ if (HEX_COLOR_PATTERN.test(normalized)) return normalized
+
+ const normalizedFallback = fallback.trim()
+ return HEX_COLOR_PATTERN.test(normalizedFallback) ? normalizedFallback : "#000000"
+}
+
function clamp(value: number, min: number, max: number): number {
if (!Number.isFinite(value)) return min
return Math.max(min, Math.min(max, value))
@@ -54,9 +66,11 @@ export function buildBlobPath(config: BlobConfig): string {
export function buildBlobSvg(path: string, size: number, fill: string, stroke: string): string {
const safeSize = Math.round(clamp(size, 64, 1024))
+ const safeFill = normalizeSvgColor(fill, DEFAULT_FILL)
+ const safeStroke = normalizeSvgColor(stroke, DEFAULT_STROKE)
return [
`",
].join("\n")
}
diff --git a/src/features/tools/svg-pattern-generator/utils.ts b/src/features/tools/svg-pattern-generator/utils.ts
index 79be6104..9aa00f21 100644
--- a/src/features/tools/svg-pattern-generator/utils.ts
+++ b/src/features/tools/svg-pattern-generator/utils.ts
@@ -11,6 +11,18 @@ export type PatternConfig = {
height: number
}
+const HEX_COLOR_PATTERN = /^#(?:[0-9a-f]{3}|[0-9a-f]{4}|[0-9a-f]{6}|[0-9a-f]{8})$/i
+const DEFAULT_FOREGROUND = "#22d3ee"
+const DEFAULT_BACKGROUND = "#020617"
+
+export function normalizeSvgColor(value: string, fallback: string): string {
+ const normalized = value.trim()
+ if (HEX_COLOR_PATTERN.test(normalized)) return normalized
+
+ const normalizedFallback = fallback.trim()
+ return HEX_COLOR_PATTERN.test(normalizedFallback) ? normalizedFallback : "#000000"
+}
+
function clamp(value: number, min: number, max: number): number {
if (!Number.isFinite(value)) return min
return Math.max(min, Math.min(max, value))
@@ -22,20 +34,21 @@ export function buildPatternShape(config: PatternConfig): string {
const strokeWidth = clamp(config.strokeWidth, 0.5, 24)
const halfGap = gap / 2
const inner = Math.max(1, size - gap)
+ const foreground = normalizeSvgColor(config.foreground, DEFAULT_FOREGROUND)
if (config.kind === "grid") {
return [
- ``,
- ``,
+ ``,
+ ``,
].join("\n")
}
if (config.kind === "diagonal") {
- return ``
+ return ``
}
const radius = clamp(inner * 0.2, 1, size * 0.45)
- return ``
+ return ``
}
export function buildPatternSvg(config: PatternConfig): string {
@@ -43,12 +56,13 @@ export function buildPatternSvg(config: PatternConfig): string {
const height = clamp(Math.round(config.height), 64, 2048)
const size = clamp(Math.round(config.tileSize), 8, 256)
const shape = buildPatternShape(config)
+ const background = normalizeSvgColor(config.background, DEFAULT_BACKGROUND)
return [
`