From 1111254648af173015784dfa248435599746d764 Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Wed, 29 Jul 2026 05:12:27 +0200 Subject: [PATCH 01/21] =?UTF-8?q?fix(security):=20CSP=20functional=20truth?= =?UTF-8?q?=20=E2=80=94=20wasm-unsafe-eval=20+=20frame-src=20blob:=20(F-01?= =?UTF-8?q?..F-04)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit script-src on all 5 deployment surfaces (index.html, vercel.json, public/_headers, nginx.conf x3, tauri.conf.json) has been 'self' without 'wasm-unsafe-eval' since faad8f0 (2026-05-27), silently blocking WebAssembly.instantiate in every Chromium browser in production. The advertised local-inference stack (WebLLM, ONNX Runtime Web, Transformers.js, DuckDB-WASM, Whisper-STT, Kokoro-TTS) never functioned. The same commit added an unhashed inline +
diff --git a/index.tsx b/index.tsx index a4e14015..b340b597 100644 --- a/index.tsx +++ b/index.tsx @@ -49,6 +49,14 @@ import '@fontsource/noto-sans/greek-700.css'; import './index.css'; import './register-sw'; +// QNBS-v3: Disable Aurora on low-end hardware (≤4 logical CPUs) to prevent GPU/battery drain; +// prefers-reduced-data handled in CSS. Moved out of an inline bodies in index.html that have no `src` attribute and non-empty content. */ +function inlineScriptContents(html: string): string[] { + const results: string[] = []; + for (const m of html.matchAll(/]*)>([\s\S]*?)<\/script>/g)) { + const attrs = m[1] ?? ''; + const body = m[2] ?? ''; + if (!/\bsrc\s*=/.test(attrs) && body.trim().length > 0) { + results.push(body); + } + } + return results; +} + +const namedCsps: readonly [name: string, csp: string][] = [ + ['index.html (meta)', webCsp()], + ['vercel.json (header)', extractVercelHeaderValue(vercelJson, 'Content-Security-Policy')], + ['public/_headers (Cloudflare)', extractHeadersFileValue(headersFile, 'Content-Security-Policy')], + ['src-tauri/tauri.conf.json (Tauri WebView)', tauriCsp()], + ...nginxCsps().map((csp, i) => [`nginx.conf (block ${i + 1})`, csp] as [string, string]), +]; + +describe('CSP correctness — Layer B (functional directives, not cross-surface consistency)', () => { + it('nginx.conf has exactly 3 Content-Security-Policy add_header blocks', () => { + expect(nginxCsps()).toHaveLength(3); + }); + + it('covers all 5 deployment surfaces (4 header/meta files + 3 nginx blocks = 7 CSP strings)', () => { + expect(namedCsps).toHaveLength(7); + }); + + it.each(namedCsps)( + "%s: script-src contains 'wasm-unsafe-eval' (WebLLM/ONNX Runtime Web/Transformers.js/DuckDB-WASM/Whisper-STT/Kokoro-TTS)", + (_name, csp) => { + const tokens = directiveTokens(csp, 'script-src'); + expect(tokens, 'script-src directive must exist').toBeDefined(); + expect(tokens).toContain("'wasm-unsafe-eval'"); + }, + ); + + it.each(namedCsps)("%s: script-src does NOT contain 'unsafe-eval'", (_name, csp) => { + const tokens = directiveTokens(csp, 'script-src') ?? []; + expect(tokens).not.toContain("'unsafe-eval'"); + }); + + it.each(namedCsps)("%s: script-src does NOT contain 'unsafe-inline'", (_name, csp) => { + const tokens = directiveTokens(csp, 'script-src') ?? []; + expect(tokens).not.toContain("'unsafe-inline'"); + }); + + it.each(namedCsps)( + '%s: frame-src (or child-src) allows blob: (Binder/ManuscriptResearchSplit PDF-preview iframes)', + (_name, csp) => { + const tokens = directiveTokens(csp, 'frame-src') ?? directiveTokens(csp, 'child-src'); + expect(tokens, 'frame-src or child-src directive must exist').toBeDefined(); + expect(tokens).toContain('blob:'); + }, + ); + + it.each(namedCsps)( + '%s: worker-src allows blob: (regression guard — DuckDB/inference workers)', + (_name, csp) => { + expect(directiveTokens(csp, 'worker-src')).toContain('blob:'); + }, + ); + + it.each(namedCsps)('%s: img-src allows blob: (regression guard)', (_name, csp) => { + expect(directiveTokens(csp, 'img-src')).toContain('blob:'); + }); + + it.each(namedCsps)("%s: object-src is 'none'", (_name, csp) => { + expect(directiveTokens(csp, 'object-src')).toEqual(["'none'"]); + }); + + it.each(namedCsps)("%s: base-uri is 'self'", (_name, csp) => { + expect(directiveTokens(csp, 'base-uri')).toEqual(["'self'"]); + }); + + it.each(namedCsps)( + "%s: frame-ancestors is 'none' (inert in the meta tag, effective as a header)", + (_name, csp) => { + expect(directiveTokens(csp, 'frame-ancestors')).toEqual(["'none'"]); + }, + ); +}); + +describe('CSP correctness — no ungehashtes (unhashed) inline bodies in index.html that have no `src` attribute and non-empty content. */ +/** + * All inline bodies in index.html that have no `src` attribute and non-empty + * content. Case-insensitive (`i` flag) — HTML tag names are case-insensitive per spec, and an + * uppercase `'; + expect(inlineScriptContents(fakeHtml)).toEqual(['doSomething();']); + }); + + it('does not flag an uppercase '; + expect(inlineScriptContents(fakeHtml)).toEqual([]); + }); }); From bdc13bf93375a49d87d60cae4eacd963e8a0ae2d Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Wed, 29 Jul 2026 09:37:58 +0200 Subject: [PATCH 12/21] fix(security): tolerate malformed-but-valid closing script tags (CodeQL follow-up) Further CodeQL "Bad HTML filtering regexp" refinement on inlineScriptContents(): browsers still parse a malformed closing tag like or as a valid , so a strict `<\/script>` literal would let such a script slip past this detector undetected -- the same failure class as the case-sensitivity gap fixed in the previous commit. The closing-tag alternative `(?:\s[^>]*)?` now tolerates trailing whitespace/attributes. `\b` after the opening ``. Two new regression tests: a malformed closing tag is still detected, and an unrelated tag is not mistaken for a script open tag. Co-Authored-By: Claude Sonnet 5 --- tests/unit/cspCorrectness.test.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/tests/unit/cspCorrectness.test.ts b/tests/unit/cspCorrectness.test.ts index ba2fac8d..ac693c0d 100644 --- a/tests/unit/cspCorrectness.test.ts +++ b/tests/unit/cspCorrectness.test.ts @@ -74,10 +74,15 @@ function directiveTokens(csp: string, directive: string): string[] | undefined { * content. Case-insensitive (`i` flag) — HTML tag names are case-insensitive per spec, and an * uppercase `` + * or `` — browsers still parse these as a valid `` (CodeQL's own "Bad HTML + * filtering regexp" example), so a strict `<\/script>` literal would let such a script slip past + * this detector undetected, same failure class as the case-sensitivity gap above. `\b` after the + * opening ``. */ function inlineScriptContents(html: string): string[] { const results: string[] = []; - for (const m of html.matchAll(/]*)>([\s\S]*?)<\/script>/gi)) { + for (const m of html.matchAll(/]*)>([\s\S]*?)<\/script(?:\s[^>]*)?>/gi)) { const attrs = m[1] ?? ''; const body = m[2] ?? ''; if (!/\bsrc\s*=/i.test(attrs) && body.trim().length > 0) { @@ -218,4 +223,16 @@ describe('CSP correctness — no ungehashtes (unhashed) inline '; expect(inlineScriptContents(fakeHtml)).toEqual([]); }); + + // QNBS-v3: regression guard for CodeQL's "Bad HTML filtering regexp" (js/bad-tag-filter) — + // a malformed-but-browser-valid closing tag must still be caught, not silently skipped. + it('detects an inline script whose closing tag has trailing whitespace/attributes', () => { + const fakeHtml = ''; + expect(inlineScriptContents(fakeHtml)).toEqual(['doSomething();']); + }); + + it('does not mistake an unrelated tag name for a script open tag', () => { + const fakeHtml = 'not a script'; + expect(inlineScriptContents(fakeHtml)).toEqual([]); + }); }); From a50673964413ba694c0cfddf4b8c1303814b52a8 Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Wed, 29 Jul 2026 10:05:25 +0200 Subject: [PATCH 13/21] chore: review-loop refinements from CodeRabbit/CodeAnt (WS-8/WS-9/WS-10 follow-up) Several substantive corrections applied and verified during the PR review loop on #284, all re-tested (lint, typecheck, affected vitest suites) before accepting: - .github/workflows/ci.yml: the Storybook test-runner's `|| true` swallowed the step's real exit code, so it always showed green in the Actions UI even while genuinely failing -- literally the same visibility gap that let the F-12 flag-name bug go unnoticed for as long as it did. Moved to step-level `continue-on-error: true`, which still surfaces the true pass/fail state without blocking the job. - scripts/check-doc-metrics.mjs: the F-10 URL-drift gate excluded locales/it/help.json ("out of scope, generated content") -- but that's the actual source file where the real F-10 drift happened, and it references the host with no `https://` scheme inside a tag, which the scheme-required regex would never have matched anyway. Now scans locales/it/help.json too, with an optional-scheme pattern and matching normalization. Two new regression tests cover the scheme-less case. - scripts/copy-duckdb-assets.mjs: the idempotency check only compared file size, which a same-size dependency bump could pass while leaving stale content in public/duckdb/. Added a SHA-256 content hash check (only computed once sizes already match, avoiding wasted hashing on clearly-different files). - tests/unit/cspCorrectness.test.ts: additional CodeQL rule-ID cross-reference comment. - vite.config.ts, workers/duckdbWorker.ts, workers/v2/duckdb.worker.ts, constants/brand.ts, docs/CI.md, docs/adr/0014, WS-RUN-LOG: comment condensing / precision wording, no functional change. Co-Authored-By: Claude Sonnet 5 --- .github/workflows/ci.yml | 13 +++++++---- constants/brand.ts | 10 +++----- docs/CI.md | 4 ++-- .../adr/0014-worker-generation-duplication.md | 13 ++++++++--- docs/audit/WS-RUN-LOG-2026-07-29.md | 6 ++--- scripts/check-doc-metrics.mjs | 23 +++++++++++++------ scripts/copy-duckdb-assets.mjs | 17 +++++++++++--- tests/unit/checkDocMetrics.test.ts | 15 ++++++++++++ tests/unit/cspCorrectness.test.ts | 1 + vite.config.ts | 5 +--- workers/duckdbWorker.ts | 9 +------- workers/v2/duckdb.worker.ts | 3 +-- 12 files changed, 75 insertions(+), 44 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 628723b0..d0d9a3ca 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -455,10 +455,13 @@ jobs: # at all). Every prior CI run failed on the FIRST unknown option before any story ever ran, # printed --help text, and `|| true` silently turned that into a green step — this gate has # produced zero real signal since it was added. Fixed to only pass supported flags. - # || true stays for now: this is the first run where test-storybook actually executes real - # stories, so there is no track record yet to judge stability against. Re-evaluate after - # ~10 real (non-help-text) runs on main — if none fail on genuine a11y/story assertions, - # remove || true and make this blocking. + # QNBS-v3 (CodeRabbit, 2026-07-29): `|| true` swallowed the exit code so this step showed + # green even when it truly failed — literally how the bug above stayed invisible. Moved the + # non-blocking behavior to step-level `continue-on-error: true`, which still shows the step + # as failed (visible signal for the F-13 "~10 real runs" re-evaluation) without failing the + # job. Re-evaluate after ~10 real (non-help-text) runs on main — if none fail on genuine + # a11y/story assertions, drop `continue-on-error` and make this blocking. + continue-on-error: true env: CI: true DEBUG: pw:browser* @@ -467,7 +470,7 @@ jobs: npx wait-on http://localhost:6006 --timeout 90000 pnpm exec test-storybook --url http://localhost:6006 \ --maxWorkers=2 \ - --junit || true + --junit # QNBS-v3: always() — upload even on failure; artifacts are the primary debug tool on low-end # local hardware where storybook cannot run. Use the Storybook Debug workflow for diff --git a/constants/brand.ts b/constants/brand.ts index 97ade842..3b038bc8 100644 --- a/constants/brand.ts +++ b/constants/brand.ts @@ -10,11 +10,7 @@ export const APP_NAME = 'WorldScript Studio'; /** Lowercase, filesystem-safe slug for generated artefacts (download filenames, key prefixes). */ export const APP_FILE_SLUG = 'worldscript'; -/** - * QNBS-v3 (F-10, 2026-07-29): the canonical, live Vercel deployment URL — single source of truth, - * defined once to prevent re-drift. A stale `worldscript-studio-indol.vercel.app` URL (a dead - * preview-deployment domain, confirmed 404) had drifted into the in-app link and the Italian - * locale; both now reference this constant / are kept in sync with it. Verify liveness before - * changing: this is the only URL that actually resolves (200) among the domains found in the repo. - */ +// QNBS-v3 (F-10): single source of truth for the canonical, live Vercel URL — a stale +// `worldscript-studio-indol.vercel.app` (dead preview domain, confirmed 404) had drifted into the +// in-app link and the Italian locale; see docs/audit/WS-RUN-LOG-2026-07-29.md for the full trace. export const PRODUCTION_URL = 'https://worldscript-studio.vercel.app/'; diff --git a/docs/CI.md b/docs/CI.md index 5dbbfcdb..7856726c 100644 --- a/docs/CI.md +++ b/docs/CI.md @@ -88,7 +88,7 @@ Mutation testing (Stryker) is **not** in this graph — it runs only via manual | `build` | `quality` | Production `pnpm run build`, **`bundle:budget`**, **`analyze`** (upload `bundle-analysis.html`), **`pnpm run smoke:prod`** (headless-Chromium prod-build + CSP-runtime gate — see below), `dist` artifact; on `main` (non-PR): Pages artifact + **SLSA build provenance attestation**. No `if:` on the job itself — `smoke:prod` runs on every PR, not just `main` pushes. | | `e2e` | `quality` | Playwright **Chromium** + **Mobile Chrome** (Pixel 5) — `CI=true`, 2× retries, 50 min timeout; browser cache via `actions/cache@v5`. Firefox optional locally. `PLAYWRIGHT_SKIP_VRT=true` (VRT is its own job). | | `lighthouse` | `build` | LHCI (mobile): **accessibility error gate** `minScore: 0.95`; **CLS error** ≤ 0.1; performance/SEO warn. Desktop run: `continue-on-error: true` until baselines stabilise. Timeout 25 min. | -| `storybook` | `quality` | Cloud-first — Storybook build + test-runner only run in CI (not locally); Playwright browser cache `v5`; `--max-workers=2 --retries=3 --screenshot-on-failure`; artifacts uploaded always. Debug: manual `storybook-debug.yml` workflow. | +| `storybook` | `quality` | Cloud-first — Storybook build + test-runner only run in CI (not locally); Playwright browser cache `v5`; `--maxWorkers=2 --junit` (non-blocking, `continue-on-error: true` — see [exit criteria](#non-blocking-gates--exit-criteria-f-13)); artifacts uploaded always. Debug: manual `storybook-debug.yml` workflow. | | `vrt` | `build` | Visual regression against production `dist`; `toHaveScreenshot()` with committed PNG baselines (4 views × Chromium); artifacts uploaded always | | `deploy` | `build`, `e2e` | **Only** `main` push (not PR): `deploy-pages` | @@ -143,7 +143,7 @@ in `ci.yml` is listed here with why it isn't blocking today and what has to be t | Gate | Why not blocking today | Exit criterion | |------|------------------------|-----------------| -| Storybook `test-storybook` (`storybook` job, `\|\| true`) | Fixed 2026-07-29 (F-12): the invocation was calling flags this test-runner version doesn't support (`--max-workers`/`--retries`/`--screenshot-on-failure`), so it failed on argument parsing before running a single story, on every prior run. This is the first run where it will actually execute real stories/a11y checks — no track record exists yet. | Re-evaluate after **~10 real (non-argument-error) runs on `main`**; remove `\|\| true` and make blocking if none fail on a genuine story/a11y assertion. | +| Storybook `test-storybook` (`storybook` job, `continue-on-error: true`) | Fixed 2026-07-29 (F-12): the invocation was calling flags this test-runner version doesn't support (`--max-workers`/`--retries`/`--screenshot-on-failure`), so it failed on argument parsing before running a single story, on every prior run. This is the first run where it will actually execute real stories/a11y checks — no track record exists yet. Moved from `\|\| true` to step-level `continue-on-error: true` the same day (CodeRabbit-caught): `\|\| true` swallowed the exit code so the step showed green even while genuinely failing — the exact mechanism that let the F-12 bug go unnoticed. | Re-evaluate after **~10 real (non-argument-error) runs on `main`**; drop `continue-on-error` and make blocking if none fail on a genuine story/a11y assertion. | | `e2e-deep` (feature-flag matrix job, `continue-on-error: true`) | Deliberately informational by design — parametrizes across the full `testConfigurations` flag matrix (`tests/e2e/config/test-matrix.ts`) specifically to surface flag-interaction regressions that the required `e2e` gate's default-flag-state run cannot see; failures here are diagnostic signal, not necessarily a merge-blocking defect in the default configuration. | Promote a **specific flag combination** to blocking (add it to the required `e2e` spec instead) once it has been stable for **3 consecutive weeks of `main` runs** — do not flip the entire matrix job blocking at once, since that reintroduces the flakiness-cascade risk `e2e-deep` was created to avoid. | | Lighthouse **Desktop** step (`lighthouse` job, `continue-on-error: true`) — note: accessibility (`minScore: 0.95`) and CLS (`≤ 0.1`) stay **error**-level gates even on this step; only the broader desktop performance/SEO scores are non-blocking | Desktop performance baselines haven't been formally re-verified as stable since the last CI-runner change | Re-run `pnpm exec lhci autorun --config=.lighthouserc.desktop.cjs` locally or via `storybook-debug.yml`-style manual dispatch across **5 consecutive `main` runs**; if performance/SEO scores stay within the existing `warn` thresholds each time, remove `continue-on-error` for the step. | | Coverage ratchet (`scripts/check-coverage-ratchet.mjs`, `continue-on-error: true`) | **Deliberately, permanently advisory** — by design (see `vitest.config.ts`'s ratchet-history comment), it exists to *suggest* the next threshold bump, not to gate a merge on hitting one. | None — this is the one gate above intentionally without an exit criterion; its purpose is met by staying advisory. Reviewed here for completeness so it isn't mistaken for forgotten debt. | diff --git a/docs/adr/0014-worker-generation-duplication.md b/docs/adr/0014-worker-generation-duplication.md index 7b87fde7..bd08a051 100644 --- a/docs/adr/0014-worker-generation-duplication.md +++ b/docs/adr/0014-worker-generation-duplication.md @@ -42,9 +42,16 @@ intended long-term target, without ever stating that v1 is deprecated or schedul (`duckdbClient.ts`, `localNlpService.ts`, `localEmbeddingService.ts`) onto WorkerBus v2 touches live, load-bearing code paths (analytics dual-write, local NLP/embedding inference used across RAG, cross-project search, and ProForge's memory bank) and is a properly-scoped migration effort in its -own right — not a byproduct of a CSP/crypto/doc-truth hardening sprint. Per this sprint's own hard -rules, a workstream that turns out to be >3× its estimated scope is a STOP-AND-ASK, not something -to absorb silently. +own right — not a byproduct of a CSP/crypto/doc-truth hardening sprint. + +This sprint's estimated scope was fixing CSP directives, desktop crypto, and doc-truth drift across +a bounded set of already-identified files (F-01..F-10). Migrating the 3 v1 call sites onto WorkerBus +v2 additionally requires re-verifying analytics dual-write, RAG/cross-project search, and ProForge's +memory bank — a different, materially larger surface than anything this sprint scoped, not merely a +longer version of it. That is this ADR's own STOP-AND-ASK decision (a sprint-local heuristic adopted +here, not a citation to an external governing policy): a workstream discovered mid-sprint to require +touching unrelated, independently load-bearing subsystems is deferred and documented rather than +absorbed silently. ## Consequences diff --git a/docs/audit/WS-RUN-LOG-2026-07-29.md b/docs/audit/WS-RUN-LOG-2026-07-29.md index 268825d6..2abc200a 100644 --- a/docs/audit/WS-RUN-LOG-2026-07-29.md +++ b/docs/audit/WS-RUN-LOG-2026-07-29.md @@ -1,7 +1,7 @@ # WS Run Log — 2026-07-29 CSP/Crypto/Truth Sprint -Tracks execution of `PROMPT-WSS-v1.24.x` (plan: `fix/csp-truth-hardening-v1.24.2`). -Full plan: `/home/pc/.claude/plans/prompt-wss-v1-24-x-csp-joyful-walrus.md`. +Tracks execution of `PROMPT-WSS-v1.24.x` (plan ref: `prompt-wss-v1-24-x-csp-joyful-walrus`, +branch `fix/csp-truth-hardening-v1.24.2`). ## Baseline (pre-flight, before any code change) @@ -84,7 +84,7 @@ low-end-hardware rule) rather than run redundantly here. Test Files 4 passed (4) Tests 103 passed (103) - $ pnpm run lint / pnpm run typecheck + $ pnpm run lint && pnpm run typecheck (both clean, 0 errors) ``` - **Unexpected finding surfaced by Layer C (not in the original audit):** after the WASM fix, a diff --git a/scripts/check-doc-metrics.mjs b/scripts/check-doc-metrics.mjs index 4a385838..9ef3ba95 100644 --- a/scripts/check-doc-metrics.mjs +++ b/scripts/check-doc-metrics.mjs @@ -89,7 +89,9 @@ export function getActualKeyCount() { // QNBS-v3 (F-10): the sole source of truth for the canonical production URL — see constants/brand.ts. const PRODUCTION_URL_ASSIGNMENT = /export const PRODUCTION_URL = '([^']+)';/; -const VERCEL_URL_PATTERN = /https?:\/\/worldscript-studio[a-z0-9-]*\.vercel\.app\/?/gi; +// QNBS-v3 (CodeRabbit): scheme optional — locales/it/help.json references the host inside +// tags with no `https://` prefix, and a scheme-required pattern would silently miss drift there. +const VERCEL_URL_PATTERN = /(?:https?:\/\/)?worldscript-studio[a-z0-9-]*\.vercel\.app\/?/gi; /** Read the canonical production URL from constants/brand.ts (the single source of truth). */ export function getCanonicalProductionUrl() { @@ -108,7 +110,13 @@ export function scanForUrlDrift(content, filePath, canonicalUrl) { const findings = []; const scanned = stripHistoricalSections(content); const lines = scanned.split('\n'); - const normalize = (u) => u.replace(/\/$/, '').toLowerCase(); + // QNBS-v3 (CodeRabbit): strip the scheme too, so a scheme-less match still normalizes to the + // same key as the (always-schemed) canonical URL instead of always comparing unequal. + const normalize = (u) => + u + .replace(/^https?:\/\//i, '') + .replace(/\/$/, '') + .toLowerCase(); lines.forEach((line, i) => { for (const m of line.matchAll(VERCEL_URL_PATTERN)) { const found = m[0]; @@ -187,11 +195,12 @@ export function scanForDrift(content, filePath, { localeCount, keyCount, latestV } // QNBS-v3: exits 1 on any finding — unlike check-coverage-ratchet.mjs this gate is blocking, since a doc claiming a wrong locale/key/release count is actively misleading, not just an opportunity. -// QNBS-v3 (F-10): README.md and CLAUDE.md are the two docs that reference the live production -// URL in prose; the in-app link (components/settings/GeneralSections.tsx) reads the constant -// directly, so it can't drift, and the Italian locale is generated content out of scope here (its -// source, locales/it/help.json, doesn't go through this doc-metrics gate). -const URL_CHECK_FILES = ['README.md', 'CLAUDE.md']; +// QNBS-v3 (F-10, CodeRabbit follow-up): README.md and CLAUDE.md reference the live production URL +// in prose; the in-app link (components/settings/GeneralSections.tsx) reads the constant directly +// so it can't drift. locales/it/help.json IS included — it's exactly where the F-10 stale-URL +// drift actually happened, so excluding it would leave the one file with a real incident history +// uncovered. +const URL_CHECK_FILES = ['README.md', 'CLAUDE.md', 'locales/it/help.json']; function main() { const localeCount = getActualLocaleCount(); diff --git a/scripts/copy-duckdb-assets.mjs b/scripts/copy-duckdb-assets.mjs index 23279bb1..a330d391 100644 --- a/scripts/copy-duckdb-assets.mjs +++ b/scripts/copy-duckdb-assets.mjs @@ -6,13 +6,21 @@ * unpinned `cdn.jsdelivr.net/npm/@duckdb/duckdb-wasm/dist/...` URL (floating "latest", decoupled * from the locked `@duckdb/duckdb-wasm@^1.32.0` version, and already blocked by `worker-src 'self' * blob:'` CSP with no jsdelivr allowance — this was already-dead code, not just a supply-chain risk). - * Run via predev/prebuild — idempotent, only copies when missing or the source changed size. + * Run via predev/prebuild — idempotent, only copies when missing or the source content changed. * `public/duckdb/` is gitignored: these are build artifacts of the pinned dependency, not source. */ -import { copyFileSync, existsSync, mkdirSync, statSync } from 'node:fs'; +import { createHash } from 'node:crypto'; +import { copyFileSync, existsSync, mkdirSync, readFileSync, statSync } from 'node:fs'; import { dirname, join } from 'node:path'; import { fileURLToPath } from 'node:url'; +// QNBS-v3 (CodeRabbit): equal byte length doesn't prove equal content — a dependency bump that +// happens to keep the same asset size would leave a stale WASM/worker file in public/duckdb/ with +// this check alone, so content is hashed once sizes already match. +function fileHash(path) { + return createHash('sha256').update(readFileSync(path)).digest('hex'); +} + const root = join(dirname(fileURLToPath(import.meta.url)), '..'); const srcDir = join(root, 'node_modules', '@duckdb', 'duckdb-wasm', 'dist'); const destDir = join(root, 'public', 'duckdb'); @@ -42,7 +50,10 @@ for (const name of ASSETS) { process.exitCode = 1; continue; } - const alreadyCurrent = existsSync(dest) && statSync(dest).size === statSync(src).size; + const alreadyCurrent = + existsSync(dest) && + statSync(dest).size === statSync(src).size && + fileHash(dest) === fileHash(src); if (!alreadyCurrent) { copyFileSync(src, dest); copied += 1; diff --git a/tests/unit/checkDocMetrics.test.ts b/tests/unit/checkDocMetrics.test.ts index bbc5cf59..87a6e939 100644 --- a/tests/unit/checkDocMetrics.test.ts +++ b/tests/unit/checkDocMetrics.test.ts @@ -143,4 +143,19 @@ describe('scanForUrlDrift', () => { const findings = scanForUrlDrift(content, 'FAKE.md', canonical); expect(findings).toHaveLength(0); }); + + // QNBS-v3 (CodeRabbit): locales/it/help.json references the host with no `https://` prefix + // (inside a tag) — the scheme must be optional or this exact drift shape goes undetected. + it('flags a scheme-less stale Vercel hostname', () => { + const content = 'URL di produzione: worldscript-studio-indol.vercel.app'; + const findings = scanForUrlDrift(content, 'FAKE.json', canonical); + expect(findings).toHaveLength(1); + expect(findings[0]).toContain('worldscript-studio-indol.vercel.app'); + }); + + it('does not flag the canonical hostname without a scheme', () => { + const content = 'URL di produzione: worldscript-studio.vercel.app'; + const findings = scanForUrlDrift(content, 'FAKE.json', canonical); + expect(findings).toHaveLength(0); + }); }); diff --git a/tests/unit/cspCorrectness.test.ts b/tests/unit/cspCorrectness.test.ts index ac693c0d..0df529c5 100644 --- a/tests/unit/cspCorrectness.test.ts +++ b/tests/unit/cspCorrectness.test.ts @@ -82,6 +82,7 @@ function directiveTokens(csp: string, directive: string): string[] | undefined { */ function inlineScriptContents(html: string): string[] { const results: string[] = []; + // QNBS-v3 (CodeQL js/bad-tag-filter): tolerate a malformed-but-browser-valid closing tag like `` so it can't evade this detector. for (const m of html.matchAll(/]*)>([\s\S]*?)<\/script(?:\s[^>]*)?>/gi)) { const attrs = m[1] ?? ''; const body = m[2] ?? ''; diff --git a/vite.config.ts b/vite.config.ts index f8eba5db..5d24ed26 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -69,10 +69,7 @@ export default defineConfig({ '**/vendor-transformers*', '**/vendor-voice-wasm*', '**/*.wasm', - // QNBS-v3 (F-09): self-hosted DuckDB-WASM runtime assets (scripts/copy-duckdb-assets.mjs) - // — the *.worker.js files here match the **/*.{js,...} allowlist above (unlike the - // *.wasm binaries, already excluded by both globIgnores and the extension allowlist); - // this is feature-flag-gated, lazily loaded content, same reasoning as vendor-duckdb*. + // QNBS-v3 (F-09): self-hosted duckdb-wasm *.worker.js (scripts/copy-duckdb-assets.mjs) matches the **/*.js allowlist above unlike the already-excluded *.wasm — exclude explicitly, same feature-flag-gated reasoning as vendor-duckdb*. '**/duckdb/**', ], }, diff --git a/workers/duckdbWorker.ts b/workers/duckdbWorker.ts index 70394fdc..30899b36 100644 --- a/workers/duckdbWorker.ts +++ b/workers/duckdbWorker.ts @@ -54,14 +54,7 @@ async function isOPFSSupported(): Promise { } } -// QNBS-v3 (F-09): self-hosted, same-origin DuckDB-WASM assets — copied from the pinned -// @duckdb/duckdb-wasm npm dependency by scripts/copy-duckdb-assets.mjs (predev/prebuild) into -// public/duckdb/. Previously loaded from an unversioned third-party CDN, which was (a) a -// floating "latest" dependency decoupled from the locked npm version, (b) a supply-chain trust -// dependency in an app that markets "offline-first" and "no data is sent to a server", and (c) -// already unreachable under worker-src 'self' blob:' CSP (no CDN allowance) — dead code, not -// just a risk. Absolute path (not relative) so resolution is unambiguous whether constructed -// from the main thread or this nested worker context. +// QNBS-v3 (F-09): self-hosted, same-origin DuckDB-WASM assets (scripts/copy-duckdb-assets.mjs) replacing an unversioned CDN that was already dead under worker-src 'self' blob:' CSP; absolute path so resolution is unambiguous from either the main thread or this nested worker context. const DUCKDB_ASSET_BASE = `${import.meta.env.BASE_URL}duckdb/`; const SELF_HOSTED_BUNDLES = { mvp: { diff --git a/workers/v2/duckdb.worker.ts b/workers/v2/duckdb.worker.ts index 37559319..228e384e 100644 --- a/workers/v2/duckdb.worker.ts +++ b/workers/v2/duckdb.worker.ts @@ -29,8 +29,7 @@ async function isOPFSSupported(): Promise { } } -// QNBS-v3 (F-09): self-hosted, same-origin DuckDB-WASM assets — see workers/duckdbWorker.ts for -// the full rationale (unpinned CDN URL, supply-chain trust, already CSP-dead code). +// QNBS-v3 (F-09): self-hosted, same-origin DuckDB-WASM assets — see workers/duckdbWorker.ts for the full rationale (unpinned CDN URL, supply-chain trust, already CSP-dead code). const DUCKDB_ASSET_BASE = `${import.meta.env.BASE_URL}duckdb/`; const SELF_HOSTED_BUNDLES = { mvp: { From 9945c4e937b4168cadf339ed486378adfaa5db23 Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Wed, 29 Jul 2026 10:36:11 +0200 Subject: [PATCH 14/21] chore(test): raise coverage ratchet to CI-measured values (F-13, part 2/2) Completes WS-9: this machine can't run the full --coverage suite locally (confirmed by two killed zero-output attempts), so the threshold bump was deferred until CI reported real numbers on this PR's own commits (per this repo's CI-cloud-first policy). quality job (Node 22 and 24, identical): 79.02% statements / 66.74% branches / 73.78% functions / 80.86% lines. vitest.config.ts thresholds: L74/F67/B60/S72 -> L79/F72/B65/S77 -- capped at +5 per metric (the documented quarterly cap) rather than the full ~1pt-below-measured jump, since a straight 1pt margin would have pushed statements +6. First ratchet of Q3 2026 (prior history entries are all Q2), so the cap isn't stacking within a quarter. Co-Authored-By: Claude Sonnet 5 --- docs/audit/WS-RUN-LOG-2026-07-29.md | 66 +++++++++++++++++++++++++++++ vitest.config.ts | 13 ++++-- 2 files changed, 75 insertions(+), 4 deletions(-) diff --git a/docs/audit/WS-RUN-LOG-2026-07-29.md b/docs/audit/WS-RUN-LOG-2026-07-29.md index 2abc200a..31892cab 100644 --- a/docs/audit/WS-RUN-LOG-2026-07-29.md +++ b/docs/audit/WS-RUN-LOG-2026-07-29.md @@ -430,3 +430,69 @@ low-end-hardware rule) rather than run redundantly here. ``` `pnpm run test:run`/`typecheck` not re-run — zero code files touched, confirmed via `git status`. - **Deviations (🟡):** none. + +--- + +### WS-9 — Coverage ratchet raise +- **Status:** ✅ done (in two parts, across the PR review loop — see below) +- **Finding IDs:** F-13 +- **Part 1 (docs, committed before push):** `docs/CI.md` gained the 4-gate non-blocking-gate table + with exit criteria (Storybook test-runner, `e2e-deep`, Lighthouse desktop, coverage ratchet — + the last one intentionally has none, documented as such). +- **Part 2 (thresholds, blocked on real numbers until CI ran):** this machine cannot run + `pnpm run test:coverage` locally — confirmed by **two separate killed attempts, both zero + output**, consistent with the ~3.7 GB RAM / <500 MB-free constraint already documented for WS-1's + full `test:run` attempt. Per this repo's own CI-cloud-first policy, pushed the branch (PR #284) + and read the real numbers from the `quality` job's Vitest output instead of guessing: + ``` + Node 22: All files | 79.02 % Stmts | 66.74 % Branch | 73.78 % Funcs | 80.86 % Lines + Node 24: All files | 79.02 % Stmts | 66.74 % Branch | 73.78 % Funcs | 80.86 % Lines (identical) + ``` + `vitest.config.ts` thresholds raised `L74/F67/B60/S72` → `L79/F72/B65/S77` — capped at **+5 per + metric** (the documented quarterly cap) rather than the full ~1pt-below-measured jump, since a + straight 1pt margin would have pushed statements +6. This is the first ratchet of Q3 2026 (all + prior history entries are Q2), so the +5 cap isn't stacking within the same quarter. History + comment in `vitest.config.ts` updated in place, same convention as prior entries. +- **Real bugs found and fixed via the PR's CI/review loop while waiting on these numbers** (both + documented in detail under their originating workstream's entry above, cross-referenced here + since they surfaced during WS-9's wait): + - **CI-caught typecheck failure** (documented under WS-6): a stale hand-maintained + `scripts/check-doc-metrics.d.mts` shadowed the `.mjs` source and hadn't been updated with the + two new F-10 function signatures. The "typecheck → clean" claim in WS-6's own entry had been + wrong — trusted a background-task notification summary instead of its actual output. Fixed; + lesson applied for the rest of this sprint (always read the real output). + - **CodeQL-caught high-severity finding** (documented under WS-1): `cspCorrectness.test.ts`'s + `inlineScriptContents()` regex was case-sensitive and used a strict closing-tag literal, so an + uppercase `` could slip past the + exact detector meant to catch the F-01/F-02 defect class. Fixed in two follow-up commits, both + with new regression tests. + - **Review-loop refinements** (CodeRabbit/CodeAnt, all verified before accepting): the F-10 URL + drift gate had excluded `locales/it/help.json` — the actual file where the real incident + happened — reasoning it was "generated" (it isn't; `public/locales/it/bundle.json` is the + generated one). Now scanned, with the URL regex's scheme made optional (the Italian source + references the bare hostname inside a `` tag, no `https://` prefix). The Storybook + `|| true` fix from WS-8 was itself swallowing the step's real exit code — moved to step-level + `continue-on-error: true`, which still surfaces genuine failure in the Actions UI without + blocking the job (the exact visibility gap that let F-12's flag-name bug hide for so long). + `copy-duckdb-assets.mjs`'s idempotency check gained a SHA-256 content hash alongside the + file-size check (same-size dependency bump could otherwise leave stale content). +- **Unrelated, pre-existing E2E finding (documented, not fixed — correctly out of scope):** + `tests/e2e/a11y.spec.ts` — "writer version control panel has no serious axe violations" fails + deterministically (same 4.47-vs-4.5 `color-contrast` ratio on both `chromium` and `Mobile + Chrome`, both CI attempts) on `--sc-accent` (`#92400e`) over its own `/20`-opacity background + (`#e0cab0`). Confirmed unrelated to this PR: `git diff --name-only main...HEAD` touches no CSS, + Tailwind config, Writer/version-control component, or color token, and `package.json`'s only + diff is the WS-5 `copy-duckdb-assets.mjs` script wiring (no dependency bump). `e2e` is **not** in + the branch's required status checks (`gh api .../branches/main/protection` → + `required_status_checks.contexts` = Security Audit, Build, Quality Gate ×2 only), so this does + not block merge. Left untouched — fixing an unrelated design-token contrast issue is out of scope + for a CSP/crypto/doc-truth security sprint; flagged here for a maintainer to file separately. +- **DoD evidence:** + ``` + $ grep -q "Ausstiegskriterium\|exit criterion" docs/CI.md → OK + $ pnpm exec biome check vitest.config.ts → clean + CI quality job (both Node versions) → pass, with the new thresholds in effect for this exact commit + ``` +- **Deviations (🟡):** thresholds set to +5-per-metric cap rather than the literal "~1pt below + measured" (which would have been +6 on statements) — documented above and in the + `vitest.config.ts` comment itself. diff --git a/vitest.config.ts b/vitest.config.ts index 16910b3e..6ed20842 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -72,14 +72,19 @@ export default defineConfig({ // → L74/F66/B60/S72 (2026-06-03: ratchet to ~1pt below CI-measured 75.15/67.84/61.23/73.14 // after Phase 2.3/2.4 tests; margin absorbs Node 22/24 variance). // → L74/F67/B60/S72 (2026-06-06: incremental ratchet to ~1pt below CI-measured values). + // → L79/F72/B65/S77 (2026-07-29, F-13: WS-9 of the CSP/crypto/doc-truth sprint — + // CI-measured L80.86/F73.78/B66.74/S79.02, identical on Node 22 and 24; capped at +5 pts + // per metric per the quarterly cap below rather than the full ~1pt-below-measured jump, + // since the straight ~1pt margin would have exceeded +5 on statements. First ratchet of + // Q3 2026 — the prior entries above are all Q2, so this is not a same-quarter stack-up). // P1 target: L85/B75/F80 — next ratchet after verification. // RATCHET RULE: After 3 consecutive green CI runs on both Node versions, increase each threshold // by 1 point (max 5 points per quarter). Document the new baseline in this comment. thresholds: { - lines: 74, - functions: 67, - branches: 60, - statements: 72, + lines: 79, + functions: 72, + branches: 65, + statements: 77, perFile: false, }, }, From 91db07f883a0f26835414ddd49eff2b5da5663ac Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Wed, 29 Jul 2026 10:41:54 +0200 Subject: [PATCH 15/21] chore(release): bump version to 1.24.2 package.json bumped; src-tauri/tauri.conf.json, src-tauri/Cargo.toml, and public/sw.js's APP_VERSION synced via the existing sync-tauri-version.mjs / sync-sw-version.mjs scripts rather than hand-edited. README version badge updated (sync-readme-metrics.mjs doesn't track it). New CHANGELOG.md [1.24.2] entry lists every finding (F-01..F-14) from this sprint's audit. Tag creation and GitHub Release publishing are maintainer actions, not performed here. Co-Authored-By: Claude Sonnet 5 --- CHANGELOG.md | 60 +++++++++++++++++++++++++++++ README.md | 2 +- docs/audit/WS-RUN-LOG-2026-07-29.md | 28 ++++++++++++++ package.json | 2 +- public/sw.js | 2 +- src-tauri/Cargo.toml | 2 +- src-tauri/tauri.conf.json | 2 +- 7 files changed, 93 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f86ef4c7..3de38a08 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,66 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [1.24.2] — 2026-07-29 + +> CSP functional-truth, desktop crypto, and documentation-truth hardening sprint +> (`PROMPT-WSS-v1.24.x`). Headline fix: `script-src` shipped without `'wasm-unsafe-eval'` for two +> months (2026-05-27 → 2026-07-29), silently blocking `WebAssembly.instantiate` in every deployed +> Chromium browser — the entire advertised local-inference stack (WebLLM, ONNX Runtime Web, +> Transformers.js, DuckDB-WASM, Whisper-STT, Kokoro-TTS) never functioned in production. No gate +> caught it because the only CSP tests that existed checked cross-surface consistency, never +> functional correctness. See [`docs/adr/0013-csp-wasm-and-blob-frames.md`](docs/adr/0013-csp-wasm-and-blob-frames.md) +> and [`docs/audit/WS-RUN-LOG-2026-07-29.md`](docs/audit/WS-RUN-LOG-2026-07-29.md) for the full record. + +### Security + +- **F-01/F-02 — CSP functional truth.** `'wasm-unsafe-eval'` (never the broader `'unsafe-eval'`) + and `frame-src 'self' blob:` added to all 5 deployment surfaces (`index.html`, `vercel.json`, + `public/_headers`, `nginx.conf` ×3, `src-tauri/tauri.conf.json`). The unhashed inline + `aurora-disabled` script that shipped alongside the broken CSP moved into `index.tsx` as a + same-origin module. New 3-layer test architecture (`tests/unit/cspCorrectness.test.ts` + a + hardened `scripts/smoke-prod-build.mjs` with real CSP-violation and WASM-instantiate probes) + so this class of defect can't silently recur. +- **F-05/F-06 — desktop API-key encryption.** `services/fs/fsCore.ts`'s key derivation used a + single unsalted SHA-256 digest of publicly-derivable material — fixed to PBKDF2 (600,000 + iterations) + a random 32-byte salt, matching the existing `storageEncryptionService.ts` + pattern. Pre-existing key files are discarded (not migrated) with a one-time notification + prompting re-entry. +- **F-08 — Tauri/web `connect-src` completeness.** Added LanguageTool's default self-hosted port + (missing on **all 5** surfaces, not just Tauri) and the Hugging Face hosts WebLLM/Transformers.js + actually resolve models from, including the Xet CDN bridge (`us.aws.cdn.hf.co`) that real model + weight downloads redirect to — traced empirically, not assumed. +- **F-09 — DuckDB-WASM self-hosted.** Replaced an unversioned, unreachable (already CSP-dead) + third-party CDN with assets copied from the pinned `@duckdb/duckdb-wasm` npm dependency at build + time (`scripts/copy-duckdb-assets.mjs`), never committed to git. + +### Fixed + +- **F-07 — doc truth-up.** README/CLAUDE.md no longer make a blanket "encrypted at rest" claim; + a fabricated `tauri-plugin-stronghold` reference (no trace of that dependency anywhere in the + repo) removed and replaced with the actual mechanism per platform. +- **F-10 — canonical production URL unified.** A dead preview-deployment domain + (`worldscript-studio-indol.vercel.app`, confirmed 404) had drifted into the in-app link and the + Italian locale; both now reference a single `PRODUCTION_URL` constant. New drift gate in + `scripts/check-doc-metrics.mjs`. +- **F-12 — CI Storybook deduplication + a genuine broken test-runner invocation.** Storybook was + built 3× per CI run; deduped to 1×. While evaluating whether the Storybook test-runner's + `|| true` could be made blocking, found the invocation was calling flags + (`--max-workers`/`--retries`/`--screenshot-on-failure`) the installed CLI version doesn't + support — every prior run failed on argument parsing before executing a single story, silently + turned green by `|| true`. Fixed the invocation and moved the non-blocking behavior to + step-level `continue-on-error: true`, which still surfaces genuine failures in the Actions UI. +- **F-13 — coverage ratchet raised** to CI-measured values (L79/F72/B65/S77, from L74/F67/B60/S72). + +### Documentation + +- **F-04 — CSP gate governance.** `docs/CI.md` gains a "which layer catches which failure class" + table and a post-mortem on why cross-surface-consistency tests alone were never sufficient. +- **F-14 — worker-generation duplication documented** (`docs/adr/0014-worker-generation-duplication.md`). + Both a v1 and a WorkerBus-v2 generation of the DuckDB and local-inference workers are live via + real call chains; consolidating them is a properly-scoped migration effort of its own, tracked + but out of scope for this sprint. + ## [1.24.1] — 2026-07-28 > Local-AI reliability (desktop Ollama/LM Studio/vLLM discovery, misleading browser status badge), diff --git a/README.md b/README.md index dbb36b0f..0e0d34b9 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,7 @@ TypeScript 7 (tsgo) Gemini · OpenAI · OpenRouter · Ollama · WebLLM WebGPU · ONNX · Transformers.js - v1.24.1 + v1.24.2 IndexedDB v8 PWA v3.0 i18n 19 locales — 2849 keys diff --git a/docs/audit/WS-RUN-LOG-2026-07-29.md b/docs/audit/WS-RUN-LOG-2026-07-29.md index 31892cab..7f44619e 100644 --- a/docs/audit/WS-RUN-LOG-2026-07-29.md +++ b/docs/audit/WS-RUN-LOG-2026-07-29.md @@ -496,3 +496,31 @@ low-end-hardware rule) rather than run redundantly here. - **Deviations (🟡):** thresholds set to +5-per-metric cap rather than the literal "~1pt below measured" (which would have been +6 on statements) — documented above and in the `vitest.config.ts` comment itself. + +--- + +### WS-7 — Version bump to 1.24.2 +- **Status:** ✅ done (version-bump commit only, per hard rule — tagging/publishing a release is + a maintainer action, STOP-AND-ASK, not performed) +- **Finding IDs:** none directly — release housekeeping, last in Wave 2 per the execution order +- **Fix:** `package.json` bumped to `1.24.2`; ran `node scripts/sync-tauri-version.mjs` and + `node scripts/sync-sw-version.mjs` rather than hand-editing (per `CLAUDE.md`'s documented + auto-sync convention) — these updated `src-tauri/tauri.conf.json`, `src-tauri/Cargo.toml`, and + `public/sw.js`'s `APP_VERSION` in place. `node scripts/sync-readme-metrics.mjs` reported + "already in sync" (it doesn't track the version badge specifically) — the `README.md` version + badge (`v1.24.1` → `v1.24.2`) updated by hand. New `CHANGELOG.md` `[1.24.2]` entry lists every + finding ID (F-01…F-14) with its resolution, organized by Security/Fixed/Documentation per + Keep-a-Changelog convention, matching this project's existing entry style. +- **Scope note:** `ROADMAP.md`/`AUDIT.md`/`TODO.md`'s remaining `1.24.1` references are WS-11's + job (re-audit close-out), not this workstream — confirmed against the plan's own division of + labor before leaving them untouched here. +- **DoD evidence:** + ``` + $ node -p "require('./package.json').version" → 1.24.2 + $ python3 -c "...tauri.conf.json...['version']" → 1.24.2 + $ grep -q "v1.24.2" README.md CHANGELOG.md → both found + $ node scripts/check-doc-metrics.mjs → OK + $ pnpm exec biome check package.json → clean + $ pnpm exec vitest run tests/unit/swLocaleStrategy.test.ts → 5/5 passed (only test referencing APP_VERSION conceptually; no hardcoded version-string assertion, confirmed no breakage) + ``` +- **Deviations (🟡):** none. diff --git a/package.json b/package.json index 95b08ebf..03f2d366 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "worldscript-studio", "private": true, - "version": "1.24.1", + "version": "1.24.2", "description": "WorldScript Studio — offline-first, AI-powered creative writing application.", "author": "QNBS", "keywords": [ diff --git a/public/sw.js b/public/sw.js index 46ea50a5..9b10602c 100644 --- a/public/sw.js +++ b/public/sw.js @@ -5,7 +5,7 @@ // offline fallback, push notifications, share target // ============================================================ -const APP_VERSION = '1.24.1'; +const APP_VERSION = '1.24.2'; const CACHE_STATIC = `worldscript-static-v${APP_VERSION}`; const CACHE_DYNAMIC = `worldscript-dynamic-v${APP_VERSION}`; const CACHE_IMAGES = `worldscript-images-v${APP_VERSION}`; diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index f1e8b962..5e51dd7d 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "worldscript-studio" -version = "1.24.1" +version = "1.24.2" description = "AI-powered creative writing and world-building application" authors = ["QNBS"] license = "MIT" diff --git a/src-tauri/tauri.conf.json b/src-tauri/tauri.conf.json index 0efeac94..c8d438db 100644 --- a/src-tauri/tauri.conf.json +++ b/src-tauri/tauri.conf.json @@ -1,7 +1,7 @@ { "$schema": "../node_modules/@tauri-apps/cli/config.schema.json", "productName": "WorldScript Studio", - "version": "1.24.1", + "version": "1.24.2", "identifier": "com.worldscript.studio", "build": { "frontendDist": "../dist", From c11f9a704c2b9663b23edce7ac8ed78061778611 Mon Sep 17 00:00:00 2001 From: qnbs <155236708+qnbs@users.noreply.github.com> Date: Wed, 29 Jul 2026 11:14:28 +0200 Subject: [PATCH 16/21] docs: WS-11 re-audit close-out + post-mortem (all 14 findings) Full re-run of every gate from WS-0's baseline plus the heavy stack on the final commit state: i18n, doc-metrics, suppressions, token-audit, lint, typecheck, parity:check, build + bundle:budget, smoke-prod-build -- all green. AUDIT.md gains an "Audit 2026-07-29 -- CSP / Crypto / Truth" section: full finding table (F-01..F-14, severity, resolution) plus a post-mortem answering the sprint's real question -- would a faad8f0-shaped commit fail today? Yes, in 3 independent places. The post-mortem also names two further instances of the identical "gate looks green but checks the wrong property" pattern that surfaced during this same sprint's own execution (Storybook's || true; the F-10 drift gate's own initial exclusion of the file with real incident history) -- three occurrences in one sprint, documented as one finding rather than three unrelated bugs. TODO.md gains a v1.24.2 block; ROADMAP.md synced. All three explicitly state PR #284 is open/CI-green/not-yet-merged rather than claiming release -- caught in my own first draft and corrected before committing, per this sprint's own doc-truth discipline. Co-Authored-By: Claude Sonnet 5 --- AUDIT.md | 74 ++++++++++++++++++++++++++++- ROADMAP.md | 22 +++++++++ TODO.md | 31 ++++++++++++ docs/audit/WS-RUN-LOG-2026-07-29.md | 53 +++++++++++++++++++++ 4 files changed, 178 insertions(+), 2 deletions(-) diff --git a/AUDIT.md b/AUDIT.md index bf879eac..166ce2bc 100644 --- a/AUDIT.md +++ b/AUDIT.md @@ -1,8 +1,8 @@ # WorldScript Studio — Codebase Audit Report -**Date:** 2026-04-17 (baseline); **follow-up chain:** … → 2026-05-28 (v1.19.0 — Security/Voice/RTL/Logger B-1..B-8) → **2026-05-30 (B-1 passphrase UX + CI unblock)** → **2026-05-31 (i18n audit + settings features + CI stabilization)** → **2026-05-31 (Edge-AI Perfection Cycle — Phases 0-7 complete)** → **2026-06-01 (Post-crash session: CI stabilisation + 14 CodeAnt AI fixes + E2E hardening)** → **2026-06-02 (Perf Phase 2.3 — pipeline-LRU unification + PR #69 CodeAnt fixes)** → **2026-06-03 (WorkerBus v2 Phase 3 — Rust TaskSupervisor + Tauri-build unblock)** → **2026-06-06 (Phase 3 i18n Expansion — ja/zh/pt/el + Intl APIs)** → **2026-06-09 (v1.21 Deep Audit Correction — Whisper WASM download UI + 3 CodeAnt fixes + CloudSync LWW)** → **2026-06-09 (feat/deep-audit-v1.21 — CSP hardening, zh locale ≤5% EN, coverage Batches A/B/C, VoiceActivityCoordinator B-2 bridge)** → **2026-06-11 (Ultimate Copilot v2 Phase 2+3 — markdown, sidebar, Apply-to-chapter, InlineAnnotation, ProForge chip; PR #110+#111)** → **2026-06-11 (v1.22.0 release — OpenRouter Cloud 5 provider, AI Execution Modes hybrid/cloud/local/eco, AiModeIndicator, SW cache-invalidation fix)** → **2026-06-13/14 (v1.23 perfection batch — OpenRouter + AI-Execution-Mode settings sections localized/modernized, i18n single-brace interpolation bug-class fix + `i18nPlaceholders` regression guard, bundle split + budget tightening PR #130)** → **2026-06-16 (v1.23.0 release — rebrand StoryCraft → WorldScript Studio, local-first data foundation ADR-0008, Tauri blank-screen + asset-URL fixes, AI error taxonomy + retry hardening, command-palette & local-AI settings localization, WorldScript W monogram icons)** → **2026-06-16 (post-release documentation perfection pass — corpus sync, metric reconciliation, history archival, dependabot hardening)** → **2026-06-17 (Language expansion — +6 locales fi/sv/hu/is/eu/fa (RTL); PR #174 merged; `LanguageSelector` exonym localization via `portal.language.names.*`; portal chrome 100 % for the 6 new langs; README/AUDIT/CHANGELOG docs sync)** → **2026-07-28 (v1.24.1 — Local-AI reliability fixes #266, Dependabot backlog triage, Issue #60 audit closeout, security/build hardening)** +**Date:** 2026-04-17 (baseline); **follow-up chain:** … → 2026-05-28 (v1.19.0 — Security/Voice/RTL/Logger B-1..B-8) → **2026-05-30 (B-1 passphrase UX + CI unblock)** → **2026-05-31 (i18n audit + settings features + CI stabilization)** → **2026-05-31 (Edge-AI Perfection Cycle — Phases 0-7 complete)** → **2026-06-01 (Post-crash session: CI stabilisation + 14 CodeAnt AI fixes + E2E hardening)** → **2026-06-02 (Perf Phase 2.3 — pipeline-LRU unification + PR #69 CodeAnt fixes)** → **2026-06-03 (WorkerBus v2 Phase 3 — Rust TaskSupervisor + Tauri-build unblock)** → **2026-06-06 (Phase 3 i18n Expansion — ja/zh/pt/el + Intl APIs)** → **2026-06-09 (v1.21 Deep Audit Correction — Whisper WASM download UI + 3 CodeAnt fixes + CloudSync LWW)** → **2026-06-09 (feat/deep-audit-v1.21 — CSP hardening, zh locale ≤5% EN, coverage Batches A/B/C, VoiceActivityCoordinator B-2 bridge)** → **2026-06-11 (Ultimate Copilot v2 Phase 2+3 — markdown, sidebar, Apply-to-chapter, InlineAnnotation, ProForge chip; PR #110+#111)** → **2026-06-11 (v1.22.0 release — OpenRouter Cloud 5 provider, AI Execution Modes hybrid/cloud/local/eco, AiModeIndicator, SW cache-invalidation fix)** → **2026-06-13/14 (v1.23 perfection batch — OpenRouter + AI-Execution-Mode settings sections localized/modernized, i18n single-brace interpolation bug-class fix + `i18nPlaceholders` regression guard, bundle split + budget tightening PR #130)** → **2026-06-16 (v1.23.0 release — rebrand StoryCraft → WorldScript Studio, local-first data foundation ADR-0008, Tauri blank-screen + asset-URL fixes, AI error taxonomy + retry hardening, command-palette & local-AI settings localization, WorldScript W monogram icons)** → **2026-06-16 (post-release documentation perfection pass — corpus sync, metric reconciliation, history archival, dependabot hardening)** → **2026-06-17 (Language expansion — +6 locales fi/sv/hu/is/eu/fa (RTL); PR #174 merged; `LanguageSelector` exonym localization via `portal.language.names.*`; portal chrome 100 % for the 6 new langs; README/AUDIT/CHANGELOG docs sync)** → **2026-07-28 (v1.24.1 — Local-AI reliability fixes #266, Dependabot backlog triage, Issue #60 audit closeout, security/build hardening)** → **2026-07-29 (v1.24.2, PR #284 — CSP functional-truth, desktop crypto, and doc-truth hardening; the local-inference stack had been silently non-functional in production for two months; PR open/CI green, not yet merged)** **Scope:** Full application, repository configuration, CI/CD, documentation, release validation -**Current version:** **v1.24.1** — 2026-07-28 (Local-AI reliability — desktop Ollama/LM Studio/vLLM discovery + browser status-badge fixes; AI heuristic-fallback foundation + Outline/Character/World/Plot-Board generators; real self-hosted LanguageTool integration; 20-PR Dependabot backlog triage; Issue #60 vendor-fork audit closeout; `persist-credentials`/CWE-209 security hardening; **2844 keys × 19 locales**) +**Current version:** **v1.24.2** — 2026-07-29, **PR #284 open, CI green, not yet merged/tagged/released** (tag + GitHub Release publish are maintainer actions per this sprint's own hard rules) — CSP/crypto/doc-truth hardening: `'wasm-unsafe-eval'` restored on all 5 deployment surfaces after a 2-month functional-truth gap; desktop API-key encryption moved from unsalted SHA-256 to PBKDF2 600k + salt; DuckDB-WASM self-hosted; canonical Vercel URL unified; CI Storybook dedup + a genuinely broken test-runner invocation fixed; coverage ratchet raised to CI-measured values; **2849 keys × 19 locales**. Last actually-released version remains **v1.24.1** (2026-07-28) until this PR merges. **Quality gate (2026-06-17 — language expansion +6 locales):** lint ✅ · typecheck ✅ · i18n:check ✅ (**2716 keys × 17 locales** — fi/sv/hu/is/eu + fa RTL) · placeholder guard ✅ (17 bundles) · targeted unit tests ✅ (LanguageSelector 9 · I18nContext 59 · i18nPlaceholders 33). `LanguageSelector` exonym labels localized via `portal.language.names.*` (native endonym stays hardcoded by design). **Bulk translation completed** for all 10 Beta locales (glossary v2.0, ~44 anchor terms/locale; placeholder-masked, checkpointed): post-run coverage fi 91 % · sv 90 % · hu 91 % · is 92 % · eu 92 % · fa 93 % · ja 99 % · zh 100 % · pt 98 % · el 97 % (Beta MT; human native review tracked). Two bulk-script bugs fixed: (1) `glossaryTranslate` partial-match left ~1,300 strings partially English → now exact-match only; (2) `--all` mangled `help.json` rich HTML → `help.json` excluded from `--all` (`ALL_SKIP`) and kept English fallback for the 6 new langs (tag-dense markup isn't MT-safe; human-review task). New `docs/TRANSLATION-GUIDE.md` + `I18N-GLOSSARY.md` v2.0. @@ -12,6 +12,76 @@ **Quality gate (2026-06-21 — feature-flag catalog + grouped Settings + ProForge opt-in):** lint ✅ · typecheck ✅ (tsgo) · i18n:check ✅ (**2793 keys × 17 locales**) · parity:check ✅ (0 drifts) · suppressions ratchet ✅ (52, no new) · targeted unit tests ✅ (127: slice 89, catalog 7, flagDependencies 5, FeatureFlagsSection 15, FeatureFlagsAndOverview 11). `enableProForge` default flipped to opt-in (now **17 on / 6 off**); `featureCatalog.ts` reconciled to all 23 flags with `defaultOn` **derived** from the slice (drift now structurally impossible — guarded by `tests/unit/featureCatalog.test.ts`). Shipped as a standalone PR off `main`. +## Audit 2026-07-29 — CSP / Crypto / Truth (v1.24.2) + +**Quality gate (2026-07-29):** lint ✅ · typecheck ✅ (tsgo) · i18n:check ✅ (**2849 keys × 19 +locales**) · suppressions ratchet ✅ (52, no new) · token-audit ✅ (160, no new) · parity:check ✅ +(0 drifts) · `pnpm run build` + `bundle:budget` ✅ (154 JS chunks ≤ 6200 KB) · +`scripts/smoke-prod-build.mjs` ✅ (0 real CSP violations, 1 documented known-benign exclusion, +`wasm: ok`) · CI `quality` job (Node 22 + 24) ✅ — coverage L80.86/F73.78/B66.74/S79.02, ratchet +raised to L79/F72/B65/S77. Full execution record: +[`docs/audit/WS-RUN-LOG-2026-07-29.md`](docs/audit/WS-RUN-LOG-2026-07-29.md). Branch +`fix/csp-truth-hardening-v1.24.2`, PR #284. + +Source audit (`PROMPT-WSS-v1.24.x`) empirically re-verified against current disk state before any +change landed — all 14 findings CONFIRMED, no baseline drift, base SHA `f5f9c1ba`. + +### Finding table + +| ID | Severity | Finding | Resolution | +|---|---|---|---| +| F-01 | 🔴 P0 | `script-src 'self'` missing `'wasm-unsafe-eval'` on all 5 CSP surfaces — `WebAssembly.instantiate` blocked in every deployed Chromium browser since 2026-05-27 (`faad8f0`); the entire advertised local-inference stack never functioned in production | Fixed on all 5 surfaces; [ADR-0013](docs/adr/0013-csp-wasm-and-blob-frames.md) | +| F-02 | 🔴 P0 | Unhashed inline `` closing tag in cspCorrectness.test.ts — the existing `(?:\s[^>]*)?` tolerance already covered this case, but there was no test asserting it directly (only the `foo="bar"` attribute variant was tested). Co-Authored-By: Claude Sonnet 5 --- components/writing/WriterViewUI.tsx | 7 +++++-- tests/unit/cspCorrectness.test.ts | 5 +++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/components/writing/WriterViewUI.tsx b/components/writing/WriterViewUI.tsx index 2301d733..a0a1dbed 100644 --- a/components/writing/WriterViewUI.tsx +++ b/components/writing/WriterViewUI.tsx @@ -112,9 +112,12 @@ const WriterViewUI: FC = () => { aria-label={ isProForgeActive ? t('proforge.toggle.deactivate') : t('proforge.toggle.activate') } + // QNBS-v3: desktop counterpart of the mobile ProForge/VC-panel contrast fix below — + // bg-accent/20 + text-ring-focus (a semi-transparent accent) was the same sub-4.5:1 + // sepia failure, just via a different token pairing; use the vetted nav-active pair. className={`text-xs px-2 py-1 rounded border transition-colors ${ isProForgeActive - ? 'bg-[var(--sc-accent)]/20 border-[var(--sc-ring-focus)]/40 text-[var(--sc-ring-focus)]' + ? 'bg-[var(--nav-background-active)] border-[var(--sc-accent)]/40 text-[var(--nav-text-active)]' : 'border-[var(--sc-border-subtle)] text-[var(--sc-text-muted)] hover:text-[var(--sc-text-primary)] hover:bg-[var(--sc-surface-raised)]' }`} > @@ -145,7 +148,7 @@ const WriterViewUI: FC = () => { type="button" onClick={() => setFocusMode((f) => !f)} title={focusMode ? t('writer.focusMode.exit') : t('writer.focusMode.enter')} - className={`text-xs px-2 py-1 rounded border transition-colors ${focusMode ? 'bg-[var(--sc-accent)]/20 border-[var(--sc-ring-focus)]/40 text-[var(--sc-ring-focus)]' : 'border-[var(--sc-border-subtle)] text-[var(--sc-text-muted)] hover:text-[var(--sc-text-primary)] hover:bg-[var(--sc-surface-raised)]'}`} + className={`text-xs px-2 py-1 rounded border transition-colors ${focusMode ? 'bg-[var(--nav-background-active)] border-[var(--sc-accent)]/40 text-[var(--nav-text-active)]' : 'border-[var(--sc-border-subtle)] text-[var(--sc-text-muted)] hover:text-[var(--sc-text-primary)] hover:bg-[var(--sc-surface-raised)]'}`} > {focusMode ? `⊠ ${t('writer.focusMode.exitLabel')}` diff --git a/tests/unit/cspCorrectness.test.ts b/tests/unit/cspCorrectness.test.ts index 0df529c5..2879d369 100644 --- a/tests/unit/cspCorrectness.test.ts +++ b/tests/unit/cspCorrectness.test.ts @@ -232,6 +232,11 @@ describe('CSP correctness — no ungehashtes (unhashed) inline '; + expect(inlineScriptContents(fakeHtml)).toEqual(['doSomething();']); + }); + it('does not mistake an unrelated tag name for a script open tag', () => { const fakeHtml = 'not a script'; expect(inlineScriptContents(fakeHtml)).toEqual([]);