From 2881ecf1e2be58430e4b59551cb163a892d13d8b Mon Sep 17 00:00:00 2001 From: martin veillette Date: Tue, 11 Aug 2026 08:15:06 -0400 Subject: [PATCH] chore: retrofit template hardening from SceneryStackTemplate Ports portable pieces of OpenPhysics/SceneryStackTemplate@ab309a1: Dependabot override ignores, tilde override pins, security headers, PWA/share metadata, and CLAUDE override docs. --- .github/dependabot.yml | 5 ++++ CLAUDE.md | 16 ++++++++++++ README.md | 2 ++ index.html | 22 ++++++++++++++++ package-lock.json | 6 ++--- package.json | 6 ++--- public/screenshots/narrow.png | Bin 0 -> 28237 bytes public/screenshots/wide.png | Bin 0 -> 25611 bytes scripts/generate-icons.ts | 33 +++++++++++++++++++++--- vite.config.ts | 46 +++++++++++++++++++++++++++++++--- 10 files changed, 124 insertions(+), 12 deletions(-) create mode 100644 public/screenshots/narrow.png create mode 100644 public/screenshots/wide.png diff --git a/.github/dependabot.yml b/.github/dependabot.yml index c67ba08..350b6e7 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -18,6 +18,11 @@ updates: ignore: - dependency-name: "@types/node" update-types: ["version-update:semver-major"] + # Forced in package.json overrides (see CLAUDE.md → package.json overrides). + # Dependabot PRs would fight the pin / reopen known-accepted risk. + - dependency-name: "lodash" + - dependency-name: "three" + - dependency-name: "brace-expansion" groups: development-dependencies: dependency-type: "development" diff --git a/CLAUDE.md b/CLAUDE.md index 305eb4c..be18adb 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -227,6 +227,20 @@ A11y strings live under `a11y.plateTectonics` in each locale JSON, exposed via - **Canvas painting instead of Scenery nodes** for the map and cross-sections, for the performance reason above. The interactive controls are all standard sun components. + +### `package.json` overrides + +JSON cannot carry comments, so the rationale for forced transitive pins lives here. Prefer +**tilde (`~`) or exact** versions — caret (`^`) lets minors drift under what is meant to be a +hard pin. Dependabot ignores these three names (see `.github/dependabot.yml`) so it does not +open PRs that fight the overrides. Revisit when SceneryStack drops or re-pins them upstream. + +| Override | Pin | Why | +|---|---|---| +| `lodash` | `~4.18.1` | SceneryStack declares `~4.17.12`. Bump clears Dependabot/npm advisories patched in 4.18.x (e.g. GHSA-r5fr-rjxr-66jc, GHSA-f23m-r3pf-42rh). | +| `three` | `~0.125.2` | SceneryStack declares `^0.104.0`. Floor is 0.125.0 for GHSA-fq6p-x6j3-cmmq (ReDoS). Staying on the 0.125 line avoids a larger API jump; **0.125.x still has open CVEs** (e.g. XSS GHSA-7vvq-7r29-5vg3, fixed only in ≥0.137.0). Remove this override if/when SceneryStack stops depending on `three` or pins a patched line itself. LightPropagation keeps a higher `three` pin — do not force 0.125 there. | +| `brace-expansion` | `~5.0.9` | Transitive via `vite-plugin-pwa` / Workbox. Clears npm audit (originally GHSA-mh99-v99m-4gvg; keep ≥5.0.9 for GHSA-rgw5-rvv9-x895). | + ## Testing | Path | Purpose | @@ -258,6 +272,8 @@ Unit tests live only under root `tests/`, mirroring `src/`. npm run lint && npm run check && npm run build && npm test ``` +`npm run release` intentionally skips `npm test` in some sims — append `&& npm test` before the version bump so a release cannot ship a failing suite. + | Command | Description | |---|---| | `npm start` / `npm run dev` | Vite dev server | diff --git a/README.md b/README.md index 66f03bc..e4b4579 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Plate Tectonics +[![CI](https://github.com/OpenPhysics/PlateTectonics/actions/workflows/ci.yml/badge.svg)](https://github.com/OpenPhysics/PlateTectonics/actions/workflows/ci.yml) + An interactive map of the Earth's tectonic plates, built with [SceneryStack](https://scenerystack.org/) — and drawn from real data throughout. diff --git a/index.html b/index.html index caf258c..c48c4d0 100644 --- a/index.html +++ b/index.html @@ -12,6 +12,28 @@ + + + + + + + + + + + + + diff --git a/package-lock.json b/package-lock.json index b8b251a..026bbbc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4618,9 +4618,9 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.8.tgz", - "integrity": "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg==", + "version": "5.0.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", "dev": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 628fd46..465d68c 100644 --- a/package.json +++ b/package.json @@ -60,8 +60,8 @@ "node": ">=24" }, "overrides": { - "lodash": "^4.18.0", - "three": "^0.125.0", - "brace-expansion": "^5.0.8" + "lodash": "~4.18.1", + "three": "~0.125.2", + "brace-expansion": "~5.0.9" } } diff --git a/public/screenshots/narrow.png b/public/screenshots/narrow.png new file mode 100644 index 0000000000000000000000000000000000000000..1f68dc98c44e21b3ba1f666a2e54b085b75f6141 GIT binary patch literal 28237 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 literal 0 HcmV?d00001 diff --git a/scripts/generate-icons.ts b/scripts/generate-icons.ts index 25cb167..b624819 100644 --- a/scripts/generate-icons.ts +++ b/scripts/generate-icons.ts @@ -1,11 +1,14 @@ /** * generate-icons.ts * - * Rasterizes public/icons/icon.svg into the PNG icons and favicon.ico used by the PWA. - * Run with: npm run icons + * Rasterizes public/icons/icon.svg into the PNG icons, favicon.ico, and placeholder + * PWA install screenshots used by the manifest. Run with: npm run icons + * + * Replace public/screenshots/{wide,narrow}.png with real sim shots before shipping + * (e.g. Baton/scripts/generate-screenshots.sh → copy into public/screenshots/). */ -import { readFileSync, writeFileSync } from "node:fs"; +import { mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import pngToIco from "png-to-ico"; @@ -15,6 +18,9 @@ const here = dirname(fileURLToPath(import.meta.url)); const publicDir = resolve(here, "..", "public"); const svg = readFileSync(resolve(publicDir, "icons", "icon.svg")); +/** Theme background matching `theme_color` / icon.svg fill (`#12182b`). */ +const THEME_BG = { r: 18, g: 24, b: 43, alpha: 1 }; + const density = 512; const pngTargets = [ @@ -31,3 +37,24 @@ const icoBuffers = await Promise.all( [16, 32, 48, 64].map((size) => sharp(svg, { density }).resize(size, size).png().toBuffer()), ); writeFileSync(resolve(publicDir, "favicon.ico"), await pngToIco(icoBuffers)); + +/** Branded placeholder screenshots for the Web App Manifest `screenshots` member. */ +async function writeScreenshot(width: number, height: number, file: string): Promise { + const iconSize = Math.round(Math.min(width, height) * 0.4); + const icon = await sharp(svg, { density }).resize(iconSize, iconSize).png().toBuffer(); + await sharp({ + create: { + width, + height, + channels: 4, + background: THEME_BG, + }, + }) + .composite([{ input: icon, gravity: "center" }]) + .png() + .toFile(resolve(publicDir, file)); +} + +mkdirSync(resolve(publicDir, "screenshots"), { recursive: true }); +await writeScreenshot(1280, 720, "screenshots/wide.png"); +await writeScreenshot(720, 1280, "screenshots/narrow.png"); diff --git a/vite.config.ts b/vite.config.ts index 5937a0f..79b40d3 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -6,6 +6,7 @@ import { VitePWA } from "vite-plugin-pwa"; * Security headers required for: * - COOP/COEP: SharedArrayBuffer support * - CSP: restrict resource loading to same-origin + known blob/data exceptions + * - Referrer / Permissions: tighten default browser leakage * - X-Content-Type-Options: prevent MIME sniffing * - X-Frame-Options: prevent clickjacking (belt-and-suspenders alongside frame-ancestors) */ @@ -14,9 +15,17 @@ const securityHeaders: Record = { "Cross-Origin-Embedder-Policy": "require-corp", "Content-Security-Policy": [ "default-src 'self'", + // TODO(scenerystack): drop 'unsafe-eval' when SceneryStack no longer needs + + // Function/eval for query-parameter parsing — reopen a CSP audit then. + // 'unsafe-eval' is required for SceneryStack query parameter parsing "script-src 'self' 'unsafe-eval'", "worker-src blob: 'self'", + // TODO(scenerystack): drop 'unsafe-inline' when SceneryStack stops setting + + // element.style / cssText for theming (same CSP revisit as unsafe-eval). + // Inline styles are set via element.style / cssText throughout the UI layer "style-src 'self' 'unsafe-inline'", // data: for icons @@ -29,10 +38,18 @@ const securityHeaders: Record = { "base-uri 'self'", "frame-ancestors 'none'", ].join("; "), + "Referrer-Policy": "strict-origin-when-cross-origin", + "Permissions-Policy": "camera=(), microphone=(), geolocation=()", "X-Content-Type-Options": "nosniff", "X-Frame-Options": "DENY", }; +/** Single-file mode: inline every imported asset as base64 (effectively unlimited). */ +const INLINE_LIMIT_BYTES = 100 * 1024 * 1024; + +/** Workbox precache ceiling — SceneryStack bundles exceed the default 2 MB limit. */ +const WORKBOX_MAX_FILE_BYTES = 12 * 1024 * 1024; + /** Escape a string for literal use inside a `RegExp`. */ function escapeRegExp(value: string): string { return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); @@ -130,7 +147,7 @@ export default defineConfig(({ mode }) => { chunkSizeWarningLimit: 5000, ...(single && { // Inline every imported asset as a base64 data URI instead of emitting files. - assetsInlineLimit: 100_000_000, + assetsInlineLimit: INLINE_LIMIT_BYTES, // Emit one CSS file (no per-chunk split) so there is a single tag to inline. cssCodeSplit: false, // Skip copying public/ (favicon, icons) — nothing external should remain. @@ -154,16 +171,20 @@ export default defineConfig(({ mode }) => { registerType: "autoUpdate", includeAssets: ["favicon.ico", "icons/apple-touch-icon.png"], manifest: { + id: "plate-tectonics", name: "Plate Tectonics", // biome-ignore lint/style/useNamingConvention: Web App Manifest spec requires snake_case keys short_name: "PlateTectonics", description: "Explore Earth's tectonic plates: boundaries, motion, earthquakes and volcanoes.", + categories: ["education", "science"], // biome-ignore lint/style/useNamingConvention: Web App Manifest spec requires snake_case keys theme_color: "#12182b", // biome-ignore lint/style/useNamingConvention: Web App Manifest spec requires snake_case keys background_color: "#000000", display: "standalone", - orientation: "landscape", + // biome-ignore lint/style/useNamingConvention: Web App Manifest spec requires snake_case keys + display_override: ["window-controls-overlay", "standalone"], + // No `orientation` — leave free so portrait-friendly sims are not forced landscape. icons: [ { src: "icons/icon-192.png", @@ -182,9 +203,28 @@ export default defineConfig(({ mode }) => { purpose: "maskable", }, ], + // Placeholder shots from `npm run icons`; replace with real sim screenshots before shipping. + screenshots: [ + { + src: "screenshots/wide.png", + sizes: "1280x720", + type: "image/png", + // biome-ignore lint/style/useNamingConvention: Web App Manifest spec requires snake_case keys + form_factor: "wide", + label: "Plate Tectonics", + }, + { + src: "screenshots/narrow.png", + sizes: "720x1280", + type: "image/png", + // biome-ignore lint/style/useNamingConvention: Web App Manifest spec requires snake_case keys + form_factor: "narrow", + label: "Plate Tectonics", + }, + ], }, workbox: { - maximumFileSizeToCacheInBytes: 12 * 1024 * 1024, + maximumFileSizeToCacheInBytes: WORKBOX_MAX_FILE_BYTES, globPatterns: ["**/*.{js,css,html,svg,png,woff2}"], }, }),