Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
1111254
fix(security): CSP functional truth — wasm-unsafe-eval + frame-src bl…
qnbs Jul 29, 2026
d2d61f3
docs(ci): CSP gate-governance table + post-mortem (F-04 follow-through)
qnbs Jul 29, 2026
186fe22
fix(security): desktop API-key PBKDF2 + doc truth-up (F-05, F-06, F-07)
qnbs Jul 29, 2026
6b36119
fix(security): connect-src completeness — LanguageTool port + HF host…
qnbs Jul 29, 2026
aa47e27
fix(security): self-host DuckDB-WASM instead of unversioned CDN (F-09)
qnbs Jul 29, 2026
cb0d7f9
fix(docs): unify canonical Vercel URL + add drift gate (F-10)
qnbs Jul 29, 2026
ad6424b
chore(ci): dedupe Storybook build, fix rebrand header, fix broken tes…
qnbs Jul 29, 2026
c6c5a7e
docs: record worker-generation duplication as tracked debt (F-14)
qnbs Jul 29, 2026
611ffd4
docs(ci): non-blocking-gate exit criteria table (F-13, part 1/2)
qnbs Jul 29, 2026
576bb7e
fix(ci): update stale check-doc-metrics.d.mts declarations (CI-caught…
qnbs Jul 29, 2026
2d05da1
fix(security): case-insensitive inline-script detection (CodeQL-caugh…
qnbs Jul 29, 2026
bdc13bf
fix(security): tolerate malformed-but-valid closing script tags (Code…
qnbs Jul 29, 2026
a506739
chore: review-loop refinements from CodeRabbit/CodeAnt (WS-8/WS-9/WS-…
qnbs Jul 29, 2026
9945c4e
chore(test): raise coverage ratchet to CI-measured values (F-13, part…
qnbs Jul 29, 2026
91db07f
chore(release): bump version to 1.24.2
qnbs Jul 29, 2026
c11f9a7
docs: WS-11 re-audit close-out + post-mortem (all 14 findings)
qnbs Jul 29, 2026
d4771fb
fix: hostname-boundary regex + comment format (CodeRabbit) + missing …
qnbs Jul 29, 2026
c6e33f5
docs: doc-truth + markdownlint fixes from CodeRabbit's second review …
qnbs Jul 29, 2026
8fc28b4
fix(a11y): use pre-vetted nav-active tokens for Writer VC/ProForge to…
qnbs Jul 29, 2026
73c9c6c
docs: mark the a11y color-contrast finding as fixed in TODO.md
qnbs Jul 29, 2026
6cb9f2d
fix(a11y): apply nav-active contrast fix to desktop ProForge/Focus-Mo…
qnbs Jul 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 16 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# ============================================================
# StoryCraft Studio – CI/CD Pipeline
# WorldScript Studio – CI/CD Pipeline
# security → quality (lint, i18n, tsgo, vitest+coverage) → build (+chunk budget, rollup analyze)
# ├→ e2e | storybook (parallel) ; lighthouse after build
# deploy (main): needs build + e2e → GitHub Pages
Expand Down Expand Up @@ -130,9 +130,6 @@ jobs:
- name: Typecheck (tsgo)
run: npx tsgo --project tsconfig.tsgo.json --noEmit --checkers 4

- name: Build Storybook
run: pnpm run build-storybook

# QNBS-v3: Vitest mit --retry=2 für stabile Runs + NODE_OPTIONS für Node 24 Kompatibilität.
# --no-experimental-webstorage deaktiviert native Node 24+ Web Storage API (verhindert localStorage.clear() Fehler).
# Best Practice 2026: Retry-Logik fängt transiente Fehler ab, die bei CI-Runner-Überlastung auftreten können.
Expand Down Expand Up @@ -452,20 +449,28 @@ jobs:
run: pnpm exec playwright install --with-deps chromium

- name: Serve Storybook + Run test-runner (cloud-optimised)
# QNBS-v3: || true — test failures produce artifacts and annotation but do NOT block the job;
# flaky a11y stories should not cascade into a full pipeline block on every PR.
# Remove || true once baseline is stable and no flaky a11y stories remain.
# QNBS-v3 (F-12, 2026-07-29): the command below was invoking three flags this
# @storybook/test-runner@^0.24.4 CLI does not support (`--max-workers` — wrong case, should
# be `--maxWorkers`; `--retries` and `--screenshot-on-failure` don't exist in this version
# 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.
# 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*
run: |
npx http-server storybook-static -p 6006 -s &
npx wait-on http://localhost:6006 --timeout 90000
pnpm exec test-storybook --url http://localhost:6006 \
--max-workers=2 \
--retries=3 \
--junit \
--screenshot-on-failure || true
--maxWorkers=2 \
--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
Expand Down
5 changes: 5 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,11 @@ graphify-out/cost.json
public/locales/*/*.json
!public/locales/*/bundle.json

# Self-hosted DuckDB-WASM runtime assets (F-09) — ~75 MB, copied from the pinned
# @duckdb/duckdb-wasm npm dependency by scripts/copy-duckdb-assets.mjs (predev/prebuild).
# Never commit: these are build artifacts of an existing dependency, not source.
public/duckdb/

# Claude Code local config and Lighthouse CI results
.claude/
.lighthouseci/
Expand Down
74 changes: 72 additions & 2 deletions AUDIT.md

Large diffs are not rendered by default.

60 changes: 60 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,66 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

<!-- QNBS-v3 (CodeRabbit): kept under [Unreleased], not a dated [1.24.2] heading — PR #284 is open/CI-green but not yet merged, tagged, or released; package.json/tauri.conf.json are pre-bumped to 1.24.2 as prep for that tag, matching this repo's existing version-bump-before-tag convention, but the CHANGELOG entry itself must not claim a release date until the tag actually exists. -->

> 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),
Expand Down
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ Pre-commit hook runs Biome check via `simple-git-hooks` + `lint-staged` on stage
Conventional Commits format: `feat:`, `fix:`, `docs:`, `refactor:`, `test:`, `chore:`.
## Architecture

WorldScript Studio is an offline-first PWA — a React 19 SPA with Google Gemini AI, IndexedDB persistence, and optional Tauri desktop packaging. No backend; API keys are entered in the UI and encrypted at rest.
WorldScript Studio is an offline-first PWA — a React 19 SPA with Google Gemini AI, IndexedDB persistence, and optional Tauri desktop packaging. No backend; API keys are entered in the UI and encrypted at rest via one of four platform-specific mechanisms (no single blanket guarantee) — see README § [Encryption — which mechanism protects what](README.md#-encryption--which-mechanism-protects-what).

**Live:** `https://worldscript-studio.vercel.app/` (Vercel, primary) · GitHub Pages: `https://qnbs.github.io/WorldScript-Studio/` · Cloudflare Pages: `wrangler.toml` · Vercel: `vercel.json`.

Expand Down
33 changes: 23 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@
<img src="https://img.shields.io/badge/TypeScript-7.x_(tsgo)-3178C6?logo=typescript&logoColor=white" alt="TypeScript 7 (tsgo)">
<img src="https://img.shields.io/badge/AI-Gemini_%7C_OpenAI_%7C_OpenRouter_%7C_Ollama_%7C_WebLLM-4285F4?logo=google" alt="Gemini · OpenAI · OpenRouter · Ollama · WebLLM">
<img src="https://img.shields.io/badge/Local_AI-WebGPU_%7C_ONNX_%7C_Transformers.js-8B5CF6" alt="WebGPU · ONNX · Transformers.js">
<img src="https://img.shields.io/badge/Version-v1.24.1-6366F1" alt="v1.24.1">
<img src="https://img.shields.io/badge/Version-v1.24.2-6366F1" alt="v1.24.2">
<img src="https://img.shields.io/badge/Storage-IndexedDB_v8-F59E0B" alt="IndexedDB v8">
<img src="https://img.shields.io/badge/PWA-v3.0-5BB974?logo=pwa" alt="PWA v3.0">
<img src="https://img.shields.io/badge/i18n-19_locales-2849_keys-0EA5E9" alt="i18n 19 locales — 2849 keys">
<img src="https://img.shields.io/badge/Tests-5807%2B_%2F_528_files-22C55E" alt="5807+ tests / 528 files">
<img src="https://img.shields.io/badge/Tests-5807%2B_%2F_529_files-22C55E" alt="5807+ tests / 529 files">
<img src="https://img.shields.io/codecov/c/github/qnbs/WorldScript-Studio?logo=codecov&label=Coverage" alt="Codecov Coverage">
<img src="https://img.shields.io/badge/License-MIT-22C55E" alt="License MIT">
<img src="https://img.shields.io/github/actions/workflow/status/qnbs/WorldScript-Studio/.github/workflows/ci.yml?branch=main&logo=github" alt="CI Status">
Expand Down Expand Up @@ -302,8 +302,7 @@ All project data, snapshots, and settings stored in IndexedDB can be encrypted a

- **AES-256-GCM** with a PBKDF2-derived key (600 000 iterations, SHA-256, 32-byte random salt).
- Gated behind `featureFlags.enableIdbAtRestEncryption` (on by default since v1.23; the passphrase unlock UX is complete — Settings → Privacy).
- Web build: passphrase-entry unlock screen on cold start (session-scoped in-memory key).
- Tauri build: transparent OS-keychain protection via `tauri-plugin-stronghold` (no user friction).
- Same passphrase-entry unlock screen (`IdbUnlockModal`) on cold start, session-scoped in-memory key, on **both** the web build and the Tauri desktop build — Tauri's WebView uses the same IndexedDB-backed storage path, not an OS keychain. (No `tauri-plugin-stronghold` or equivalent OS-keychain integration ships today — see the API-key encryption note below for the desktop-specific mechanism that does exist.)
- GDPR-compliant: encrypted blobs are unreadable without the passphrase, even from the browser profile directory.

### 🔐 Encrypted Library Backup
Expand All @@ -315,6 +314,20 @@ One-click encrypted export of your entire project library from **Settings → Da
- No plaintext project data ever leaves your device unencrypted.
- Import on any device using the same passphrase to restore your full library.

### 🔑 Encryption — which mechanism protects what

There is no single blanket "encrypted at rest" guarantee — four independent mechanisms protect
different data, with different key material:

| Data | Mechanism | Where |
|------|-----------|-------|
| **Browser BYOK API key** | Random, non-extractable AES-256-GCM key generated via `crypto.subtle.generateKey()` — no passphrase, nothing to derive | `services/storage/idbKeyStore.ts` |
| **Browser IDB-at-rest data** _(opt-in, B-1)_ | User passphrase → PBKDF2 (600 000 iterations, SHA-256, random 32-byte salt) → AES-256-GCM, non-extractable key | `services/storage/storageEncryptionService.ts` |
| **Desktop (Tauri) BYOK API key** | Install-scoped secret material → PBKDF2 (600 000 iterations, SHA-256, random 32-byte salt) → AES-256-GCM, non-extractable key | `services/fs/fsCore.ts`, `services/fs/settingsFsStore.ts` |
| **Library backup vault** | User passphrase → PBKDF2 (600 000 iterations, SHA-256) → AES-256-GCM | `services/libraryBackupService.ts` |

See [`docs/SECURITY-THREAT-MODEL.md`](docs/SECURITY-THREAT-MODEL.md) for the full threat-model mapping.

### 💾 Robust Offline-First Data Management

- **Auto-save** to IndexedDB on every change (debounced, non-blocking)
Expand Down Expand Up @@ -453,12 +466,12 @@ The Settings → AI panel shows a live GPU status badge with adapter details and
| **AI Facade** | `packages/ai-core` workspace package | Unified local inference interface; sanitizeForPrompt truncation |
| **Storage** | Dual IndexedDB v8 (`StateDB` + `DataDB`) | Split state/asset persistence; LZ-String compression + AES-256-GCM |
| **Collaboration** | Yjs + `packages/collab-transport` (y-webrtc vendor fork) | P2P CRDT editing; RTCDataChannel E2E AES-256-GCM; PBKDF2 600 000 iter |
| **Encryption** | Web Crypto API (AES-256-GCM + PBKDF2) | API-key encryption at rest; IDB at-rest encryption; library backup vault |
| **Encryption** | Web Crypto API (AES-256-GCM; PBKDF2 where a passphrase is involved) | 4 independent mechanisms — see [Encryption — which mechanism protects what](#-encryption--which-mechanism-protects-what) |
| **PDF Export** | jsPDF | Client-side, configurable PDF document generation |
| **Document Export** | docx + jszip | Word-compatible `.docx` generation (lazy-loaded) |
| **PWA** | Service Worker + Web App Manifest v3 | Offline support, installability, Workbox chunking |
| **i18n** | Custom React Context (`I18nContext.tsx`) | 2849 keys × 19 locales (de/en/es/fr/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu Beta); EN fallback; `localStorage` persistence |
| **Testing** | Vitest 4.x (5807+ tests / 528 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) |
| **Testing** | Vitest 4.x (5807+ tests / 529 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) |
| **Code Quality** | Biome (lint + format) + TypeScript 7 (tsgo) strict | `--error-on-warnings` in CI; zero `any` policy |
| **Visualization** | Force-directed graph | Interactive character relationship network |
| **Desktop** | Tauri v2 | Cross-platform installer; auto-updater via `latest.json` |
Expand Down Expand Up @@ -495,7 +508,7 @@ WorldScript-Studio/
│ ├── sw.js # PWA Service Worker
│ └── manifest.json # PWA Web App Manifest v3
├── tests/
│ ├── unit/ # Vitest unit tests (5807+ tests, 528 files) — count spans tests/, components/, packages/*/tests/, not just this folder
│ ├── unit/ # Vitest unit tests (5807+ tests, 529 files) — count spans tests/, components/, packages/*/tests/, not just this folder
│ │ ├── ai/ # aiSmallModules, aiCoreFallbackPaths
│ │ └── settings/ # WebLlmPanel, AiSections
│ └── e2e/ # Playwright specs + helpers.ts
Expand Down Expand Up @@ -523,11 +536,11 @@ WorldScript Studio supports local-only AI (no API key) as well as BYOK cloud pro

1. **Get your key** — e.g. at [Google AI Studio](https://aistudio.google.com/app/apikey) (free tier available)
2. **Open Settings** → AI Provider → select your provider
3. **Enter your API key** — encrypted with AES-256-GCM and stored only in your browser's IndexedDB; never transmitted except to the provider you select
3. **Enter your API key** — encrypted with AES-256-GCM (web build: stored in your browser's IndexedDB; desktop build: stored on disk, see the encryption breakdown below); never transmitted except to the provider you select

**Security best practices:**
- ✅ Your key never leaves your device in plaintext
- ✅ Encrypted at rest via the Web Crypto API
- ✅ Encrypted at rest via the Web Crypto API — see [Encryption — which mechanism protects what](#-encryption--which-mechanism-protects-what) for the exact mechanism per platform
- 🔒 **Recommended:** Restrict your Gemini key to `*.github.io` in Google AI Studio

#### Option B: Ollama (local server)
Expand Down Expand Up @@ -654,7 +667,7 @@ The main pipeline is [`.github/workflows/ci.yml`](.github/workflows/ci.yml). Opt
| `scorecard` | weekly + `main` push | OpenSSF Scorecard — SARIF uploaded to GitHub Code Scanning |

**Current test metrics (2026-07-28):**
- **5807+ unit tests** across **528 test files** — all passing
- **5807+ unit tests** across **529 test files** — all passing
- Coverage thresholds: lines ≥ 74 · branches ≥ 60 · functions ≥ 67 · statements ≥ 72 — enforced in CI (see Codecov badge for live metrics)
- i18n: **2849 keys × 19 locales** (en/de/fr/es/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu Beta)

Expand Down
Loading
Loading