From 7e49fdd61d510511a4d68416a86750238764ea65 Mon Sep 17 00:00:00 2001 From: martin veillette Date: Tue, 11 Aug 2026 08:16:31 -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 -> 26297 bytes public/screenshots/wide.png | Bin 0 -> 23703 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 3c3f723..ee0ed00 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -58,6 +58,20 @@ via `StringManager.getA11yStrings()`. - **Domain clock:** `BaseModel` / Chladni playback state machines own play/pause and stepping instead of composing fleet-standard `TimeModel` (`src/common/TimeModel.ts` is present for shared reference only). - **Vitest environment:** `jsdom` (not the fleet-default `happy-dom`) — canvas/DOM-heavy model tests need it; `tests/setup.ts` + `execArgv: ["--expose-gc"]` are otherwise fleet-standard. + +### `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 Fleet-standard Vitest layout (`jsdom`, `tests/setup.ts`, `execArgv: ["--expose-gc"]`): @@ -89,6 +103,8 @@ runs the suite when a `test` script is present. 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 7f7eda3..8d1d82d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Resonance +[![CI](https://github.com/OpenPhysics/Resonance/actions/workflows/ci.yml/badge.svg)](https://github.com/OpenPhysics/Resonance/actions/workflows/ci.yml) + An interactive simulation of resonance in driven oscillating systems, built with [SceneryStack](https://scenerystack.org/). Visualize driven mass-spring systems and Chladni plate vibration patterns. diff --git a/index.html b/index.html index 3b0b514..1d8803a 100644 --- a/index.html +++ b/index.html @@ -12,6 +12,28 @@ + + + + + + + + + + + + + diff --git a/package-lock.json b/package-lock.json index 2b92060..3d0d370 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4774,9 +4774,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 3e64ca1..2ace57b 100644 --- a/package.json +++ b/package.json @@ -66,9 +66,9 @@ "node": ">=24" }, "overrides": { - "lodash": "^4.18.0", - "three": "^0.125.0", + "lodash": "~4.18.1", + "three": "~0.125.2", "minimatch": "^3.1.3", - "brace-expansion": "^5.0.8" + "brace-expansion": "~5.0.9" } } diff --git a/public/screenshots/narrow.png b/public/screenshots/narrow.png new file mode 100644 index 0000000000000000000000000000000000000000..6649780b9a3c1f004d88253331faf5bb8b549e06 GIT binary patch literal 26297 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 c$2{1&?zDa_jt>47jmwHyO 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 Q8i%Z3>%J!UH{`+p1 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 50882a8..bbaa992 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,17 +171,21 @@ export default defineConfig(({ mode }) => { registerType: "autoUpdate", includeAssets: ["favicon.ico", "icons/apple-touch-icon.png"], manifest: { + id: "resonance", name: "Resonance", // biome-ignore lint/style/useNamingConvention: Web App Manifest spec requires snake_case keys short_name: "Resonance", description: "Interactive simulation of resonance in driven oscillating systems. Visualizes driven mass-spring systems and Chladni plate patterns.", + categories: ["education", "science"], // biome-ignore lint/style/useNamingConvention: Web App Manifest spec requires snake_case keys theme_color: "#1a1a2e", // 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", @@ -183,9 +204,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: "Resonance", + }, + { + 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: "Resonance", + }, + ], }, workbox: { - maximumFileSizeToCacheInBytes: 12 * 1024 * 1024, + maximumFileSizeToCacheInBytes: WORKBOX_MAX_FILE_BYTES, globPatterns: ["**/*.{js,css,html,svg,png,woff2}"], }, }),