From 331efa82cc2c66f8679b73cdc85fd988f8ce3832 Mon Sep 17 00:00:00 2001 From: martin veillette Date: Tue, 11 Aug 2026 02:29:40 -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 -> 40701 bytes public/screenshots/wide.png | Bin 0 -> 38413 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 855cda7..064818a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -51,6 +51,20 @@ frequency + play state) via the `screenSummaryContent` super-option, and orders - **Root constants:** `src/DopplerEffectConstants.ts` (sim-wide); no separate nested primary constants module. - **Domain clock:** `timeSpeedProperty` (including reverse) and simulation-state history drive the clock instead of composing fleet-standard `TimeModel` (`src/common/TimeModel.ts` is present for shared reference only). + +### `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: @@ -78,6 +92,8 @@ 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. + ## Development notes - Microphone node listens at an arbitrary point (can differ from observer icon). Motion trails and projector mode supported via preferences. diff --git a/README.md b/README.md index 8836709..8b45d18 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,7 @@ # Doppler Effect Simulation +[![CI](https://github.com/OpenPhysics/DopplerEffect/actions/workflows/ci.yml/badge.svg)](https://github.com/OpenPhysics/DopplerEffect/actions/workflows/ci.yml) + An interactive simulation of the Doppler Effect built with [SceneryStack](https://scenerystack.org/). Explore how observed sound frequency shifts when source and observer move relative to each other. diff --git a/index.html b/index.html index 9af08c7..a225fc5 100644 --- a/index.html +++ b/index.html @@ -12,6 +12,28 @@ + + + + + + + + + + + + + diff --git a/package-lock.json b/package-lock.json index 5b47c33..83c07d2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4701,9 +4701,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 a01551d..701d500 100644 --- a/package.json +++ b/package.json @@ -62,8 +62,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..c8c8f7300984918c9c5c485a195c4142669c02f7 GIT binary patch literal 40701 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π6}2%d={y8`vS>LL#k?$4 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 literal 0 HcmV?d00001 diff --git a/public/screenshots/wide.png b/public/screenshots/wide.png new file mode 100644 index 0000000000000000000000000000000000000000..730cd965c39fd8b9c15b44c1866e80c29c072833 GIT binary patch literal 38413 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 literal 0 HcmV?d00001 diff --git a/scripts/generate-icons.ts b/scripts/generate-icons.ts index 25cb167..c342638 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 (`#2575ba`). */ +const THEME_BG = { r: 37, g: 117, b: 186, 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 ea87239..1520193 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: "doppler-effect", name: "Doppler Effect", // biome-ignore lint/style/useNamingConvention: Web App Manifest spec requires snake_case keys short_name: "Doppler", description: "An interactive simulation of the Doppler Effect using SceneryStack.", + categories: ["education", "science"], // biome-ignore lint/style/useNamingConvention: Web App Manifest spec requires snake_case keys theme_color: "#2575ba", // 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: "Doppler Effect", + }, + { + 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: "Doppler Effect", + }, + ], }, workbox: { - maximumFileSizeToCacheInBytes: 12 * 1024 * 1024, + maximumFileSizeToCacheInBytes: WORKBOX_MAX_FILE_BYTES, globPatterns: ["**/*.{js,css,html,svg,png,woff2}"], }, }),