diff --git a/CHANGELOG.md b/CHANGELOG.md index 7bae99b..c5c0331 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Opt-in process metrics (`PAPERCUT_METRICS=1`) — `GET /api/metrics` counters only (`pastes_created`, `unlocks_ok`, `rate_limited`); disabled by default; no content/IP logging - Log canvas **wrap / no-wrap** toggle (dense horizontal scroll), sticky line-number gutter, preference in `localStorage` +### Fixed + +- Theme FOUC boot script is a static string (no `JSON.stringify` interpolation) to clear CodeQL `js/bad-code-sanitization` + ### Planned See [ROADMAP.md](./ROADMAP.md) (dependency updates, canvas tools, scale features, etc.). diff --git a/server/lib/theme.test.ts b/server/lib/theme.test.ts index 61f26be..7b253d4 100644 --- a/server/lib/theme.test.ts +++ b/server/lib/theme.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from "vitest"; -import { isTheme, THEME_STORAGE_KEY } from "./theme"; +import { isTheme, THEME_BOOT_SCRIPT, THEME_STORAGE_KEY } from "./theme"; describe("theme helpers", () => { it("accepts only dark and light", () => { @@ -10,7 +10,10 @@ describe("theme helpers", () => { expect(isTheme(null)).toBe(false); }); - it("uses a stable storage key", () => { + it("uses a stable storage key shared by the FOUC boot script", () => { expect(THEME_STORAGE_KEY).toBe("papercut-theme"); + // Boot script embeds the key as a static literal (no code construction). + expect(THEME_BOOT_SCRIPT).toContain(`localStorage.getItem("${THEME_STORAGE_KEY}")`); + expect(THEME_BOOT_SCRIPT).toContain('setAttribute("data-theme"'); }); }); diff --git a/server/lib/theme.ts b/server/lib/theme.ts index 7607af9..5b7b649 100644 --- a/server/lib/theme.ts +++ b/server/lib/theme.ts @@ -33,5 +33,10 @@ export function resolveInitialTheme(): Theme { return readStoredTheme() ?? "dark"; } -/** Inline script: set data-theme before paint (no FOUC). */ -export const THEME_BOOT_SCRIPT = `(function(){try{var k=${JSON.stringify(THEME_STORAGE_KEY)};var t=localStorage.getItem(k);if(t!=="light"&&t!=="dark")t="dark";document.documentElement.setAttribute("data-theme",t);}catch(e){document.documentElement.setAttribute("data-theme","dark");}})();`; +/** + * Inline script: set data-theme before paint (no FOUC). + * Fully static string (no interpolation) so CodeQL js/bad-code-sanitization stays clean. + * The localStorage key literal must stay equal to THEME_STORAGE_KEY. + */ +export const THEME_BOOT_SCRIPT = + '(function(){try{var t=localStorage.getItem("papercut-theme");if(t!=="light"&&t!=="dark")t="dark";document.documentElement.setAttribute("data-theme",t);}catch(e){document.documentElement.setAttribute("data-theme","dark");}})();';