Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
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
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
<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-2942_keys-0EA5E9" alt="i18n 19 locales — 2942 keys">
<img src="https://img.shields.io/badge/Tests-7567%2B_%2F_603_files-22C55E" alt="7567+ tests / 603 files">
<img src="https://img.shields.io/badge/Tests-7588%2B_%2F_603_files-22C55E" alt="7588+ tests / 603 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 @@ -511,7 +511,7 @@ The Settings → AI panel shows a live GPU status badge with adapter details and
| **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`) | 2942 keys × 19 locales (de/en/es/fr/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu/ru/ko Beta); EN fallback; `localStorage` persistence |
| **Testing** | Vitest 4.x (7567+ tests / 603 files) + Playwright E2E | Unit/integration + cross-browser E2E; Stryker mutation (manual workflow) |
| **Testing** | Vitest 4.x (7588+ tests / 603 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 @@ -549,7 +549,7 @@ WorldScript-Studio/
│ ├── sw.js # PWA Service Worker
│ └── manifest.json # PWA Web App Manifest v3
├── tests/
│ ├── unit/ # Vitest unit tests (7567+ tests, 603 files) — count spans tests/, components/, packages/*/tests/, not just this folder
│ ├── unit/ # Vitest unit tests (7588+ tests, 603 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 @@ -714,7 +714,7 @@ The main pipeline is [`.github/workflows/ci.yml`](.github/workflows/ci.yml). Opt
Raw bundle-budget ceilings (KB per uncompressed asset): entry **2500 KB**, vendor **6200 KB**, other JavaScript **2500 KB**, and WASM **30000 KB**.

**Current test metrics (2026-09-07, source-synchronized; CI remains authoritative for pass/fail):**
- **7567+ unit tests** across **603 test files** — CI is authoritative for pass/fail
- **7588+ unit tests** across **603 test files** — CI is authoritative for pass/fail
- Coverage thresholds: lines ≥ 80 · branches ≥ 66 · functions ≥ 72 · statements ≥ 78 — enforced in CI (see Codecov badge for live metrics)
- i18n: **2942 keys × 19 locales** (en/de/fr/es/it + ar/he/fa RTL Beta + ja/zh/pt/el/fi/sv/hu/is/eu/ru/ko Beta)

Expand Down
15 changes: 10 additions & 5 deletions docs/DEPENDABOT-TRIAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -81,11 +81,16 @@ rule; it applies to dependency PRs exactly as it does to feature PRs.

## OSV ignore-expiry review

The 2026-08-20 consolidation review found **19** entries in `src-tauri/osv-scanner.toml`, all
currently sharing the `2026-11-30T00:00:00Z` review deadline. This is a synchronized review cliff,
not evidence that the risks were extended or resolved. The entries remain grouped by their real
reason: legacy GTK3/WebKit bindings, build-time `proc-macro-error`/`paste`, archived Unicode data
crates, and the transitive `extract-zip` advisory.
The 2026-08-20 consolidation review found 19 entries in `src-tauri/osv-scanner.toml`, all
currently sharing the `2026-11-30T00:00:00Z` review deadline. **2026-09-08 update: two more npm
entries were added under the same deadline** — a second, distinct `extract-zip` 2.0.1 advisory
(`GHSA-7pqw-9j4j-h8q3`, not an alias of the original) and `adm-zip` 0.6.0
(`GHSA-vwc7-r8mq-g2x9`) — bringing the total to **21**. This is a synchronized review cliff, not
evidence that the risks were extended or resolved; **`src-tauri/osv-scanner.toml` is the dynamic
source of truth for the exact count and entries** — treat this list as a cluster summary, not a
substitute for reading the file. The entries remain grouped by their real reason: legacy GTK3/WebKit
bindings, build-time `proc-macro-error`/`paste`, archived Unicode data crates, both `extract-zip`
advisories, and `adm-zip`.

No ignore deadline was extended in this pass. `extract-zip` remains a transitive Playwright browser
download dependency with no patched release and no production-runtime footprint; it must still be
Expand Down
2 changes: 1 addition & 1 deletion docs/IDB-ENCRYPTION.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,7 +165,7 @@ Every protected store writer runs inside `withProtectedWriteAdmission()` (shared

On the Tauri desktop build, primary project, settings, snapshot, image, Codex, RAG, and binder-asset data is persisted by the filesystem-backed store (`services/fs/*Store.ts`), not IndexedDB. That store writes plaintext (LZ-string compressed only, no encryption) regardless of `enableIdbAtRestEncryption`. Enabling the setting on desktop still shows `IdbUnlockModal`/`PassphraseModal` (the passphrase sentinel lives in the WebView's own IndexedDB, which persists on desktop too), but that unlock flow gates nothing on the filesystem side today — only the UI, not the actual manuscript files under `$APPDATA`, is shared with the web build. Character and world image reads use `storageService`, so they now follow the same selected backend as image uploads; this removes the prior desktop filesystem/IndexedDB split-persistence availability bug. See `README.md`'s "Encryption — which mechanism protects what" table for the authoritative per-mechanism breakdown. Extending real at-rest protection to the desktop filesystem store is a tracked, open gap — not yet implemented.

**API keys (resolved 2026-08-14):** all provider API keys, including Gemini, now route through `storageService` directly to the IndexedDB key store (`services/storage/idbKeyStore.ts`, random non-extractable AES-GCM key) on every platform, desktop included. The Tauri filesystem adapter's `saveApiKey`/`getApiKey` (`services/fs/settingsFsStore.ts`) is now a defense-in-depth backstop rather than the active path: `saveApiKey` throws if ever called, and `getApiKey` silently removes any pre-existing legacy key file — whether from the pre-2026-07-29 unsalted-SHA-256 scheme or the since-hardened but still filesystem-reconstructible PBKDF2 scheme — and returns `null`. A user-facing "API Key Reset Required" notification fires only if that removal itself fails (a permissions/IO error surfacing through the catch path); the common case (file found and removed cleanly) is silent, since a re-prompt for a never-populated key is indistinguishable from normal first-use. `encryptText`/`decryptText` in `fsCore.ts` are no longer called anywhere in the codebase for API keys and remain only as shared crypto plumbing pending [PR #356](https://github.com/qnbs/WorldScript-Studio/pull/356)'s project-data encryption work. This also resolves the earlier Gemini split-persistence bug tracked in [#358](https://github.com/qnbs/WorldScript-Studio/issues/358): `components/ApiKeySection.tsx` and `services/geminiService.ts` now both route through `storageService`.
**API keys (resolved 2026-08-14):** all provider API keys, including Gemini, now route through `storageService` directly to the IndexedDB key store (`services/storage/idbKeyStore.ts`, random non-extractable AES-GCM key) on every platform, desktop included. The Tauri filesystem adapter's `saveApiKey`/`getApiKey` (`services/fs/settingsFsStore.ts`) is now a defense-in-depth backstop rather than the active path: `saveApiKey` throws if ever called, and `getApiKey` silently removes any pre-existing legacy key file — whether from the pre-2026-07-29 unsalted-SHA-256 scheme or the since-hardened but still filesystem-reconstructible PBKDF2 scheme — and returns `null`. A user-facing "API Key Reset Required" notification fires only when a failure occurs after the key-file path is resolved — during the existence check or the removal attempt itself, e.g. a transient permissions/IO error — **and** the catch block's own follow-up removal then succeeds; earlier failures (acquiring the platform APIs or resolving the app-data path) never reach the cleanup/notification logic at all, and if the follow-up removal also fails, the notification is likewise suppressed and only a warning is logged. The common case — no legacy file exists, or the initial removal succeeds outright — returns silently, since a re-prompt for a never-populated key is indistinguishable from normal first-use. `encryptText`/`decryptText` in `fsCore.ts` are no longer called anywhere in the codebase for API keys and remain only as shared crypto plumbing pending R-15's project-data encryption work (`docs/native/CORE-MIGRATION-LEDGER.md` row 10 — check the ledger directly for its current readiness marker rather than trusting a copied status here — additionally gated behind row 9, the project state-shape compatibility adapter, converging first). This also resolves the earlier Gemini split-persistence bug tracked in [#358](https://github.com/qnbs/WorldScript-Studio/issues/358): `components/ApiKeySection.tsx` and `services/geminiService.ts` now both route through `storageService`.

The repository does **not** currently use `tauri-plugin-stronghold`, an OS keychain, or a transparent desktop-only passphrase store.

Expand Down
4 changes: 2 additions & 2 deletions docs/SECURITY-THREAT-MODEL.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,8 @@ This document provides a formal STRIDE threat analysis for WorldScript Studio, m
| Threat | Mitigation | Code Location |
|--------|------------|-------------|
| API key leakage via logs | StructuredLogger sanitization; never log keys | `services/logger.ts:sanitizeLogContext()` |
| Desktop API key exposure via local filesystem read | **Resolved 2026-08-14 (was "not resolved" through 2026-08-13 — see history).** Filesystem API-key persistence is disabled: `storageService`'s key methods route directly to the IndexedDB key store (random non-extractable AES-GCM key) on every platform, desktop included — the filesystem adapter's own `saveApiKey` is a defense-in-depth backstop that throws if ever called directly. The prior PBKDF2-from-reconstructible-material derivation (`deriveFileSystemCryptoKey()`) is no longer used for API keys; it remains in `fsCore.ts` as shared crypto plumbing for other filesystem-encrypted data (see [PR #356](https://github.com/qnbs/WorldScript-Studio/pull/356)). Legacy filesystem key files are removed on a best-effort basis (each failure is logged, not retried indefinitely) and re-entry is required if cleanup or decryption fails. **Gemini split-persistence bug also resolved:** `components/ApiKeySection.tsx` and `services/geminiService.ts` now both route through `storageService`, closing [#358](https://github.com/qnbs/WorldScript-Studio/issues/358) (previously ApiKeySection read/wrote the Gemini key via `dbService` directly while geminiService read it via `storageService`, so a key saved on desktop was invisible to the code that used it). | `services/storage/idbKeyStore.ts`, `services/storageService.ts`, `services/fs/settingsFsStore.ts` |
| Desktop project/settings/snapshot/Codex/RAG/image/binder-asset data disclosure via local file-read access | **Not resolved on current `main`.** Tauri filesystem records, including binder `.bin` payloads, are plaintext (some text records are compressed only), so an attacker who can read the app-data directory can disclose them. The separate tampering threat requires local file-write access and is modeled under Tampering above. Enabling the current browser/IndexedDB setting does not protect these files. [PR #356](https://github.com/qnbs/WorldScript-Studio/pull/356) is the active remediation; do not broaden this claim until its combined-state runtime proof completes. | `services/fs/*Store.ts` |
| Desktop API key exposure via local filesystem read | **Resolved 2026-08-14 (was "not resolved" through 2026-08-13 — see history).** Filesystem API-key persistence is disabled: `storageService`'s key methods route directly to the IndexedDB key store (random non-extractable AES-GCM key) on every platform, desktop included — the filesystem adapter's own `saveApiKey` is a defense-in-depth backstop that throws if ever called directly. The prior PBKDF2-from-reconstructible-material derivation (`deriveFileSystemCryptoKey()`) is no longer used for API keys; it remains in `fsCore.ts` as shared crypto plumbing for other filesystem-encrypted data (see [PR #356](https://github.com/qnbs/WorldScript-Studio/pull/356), closed 2026-08-18 as superseded). Legacy filesystem key files are removed on a best-effort basis (each failure is logged, not retried indefinitely) and re-entry is required if cleanup or decryption fails. **Gemini split-persistence bug also resolved:** `components/ApiKeySection.tsx` and `services/geminiService.ts` now both route through `storageService`, closing [#358](https://github.com/qnbs/WorldScript-Studio/issues/358) (previously ApiKeySection read/wrote the Gemini key via `dbService` directly while geminiService read it via `storageService`, so a key saved on desktop was invisible to the code that used it). | `services/storage/idbKeyStore.ts`, `services/storageService.ts`, `services/fs/settingsFsStore.ts` |
| Desktop project/settings/snapshot/Codex/RAG/image/binder-asset data disclosure via local file-read access | **Not resolved on current `main`.** Tauri filesystem records, including binder `.bin` payloads, are plaintext (some text records are compressed only), so an attacker who can read the app-data directory can disclose them. The separate tampering threat requires local file-write access and is modeled under Tampering above. Enabling the current browser/IndexedDB setting does not protect these files. R-15 (`docs/native/CORE-MIGRATION-LEDGER.md` row 10) is the design-complete remediation — implementation readiness is tracked live by row 10's own readiness marker, additionally gated behind row 9 (the project state-shape compatibility adapter) converging first; check the ledger directly rather than trusting a copied status here, and do not broaden this claim until implementation actually starts. | `services/fs/*Store.ts` |
| Manuscript data in IndexedDB | AES-256-GCM at-rest encryption | `services/storage/storageEncryptionService.ts` |
| Voice audio to cloud | Web Speech API consent gate | `components/voice/VoicePrivacyConsentModal.tsx` |
| DuckDB analytics unencrypted (SEC-6) | **Bounded by design, with one prose column now encrypted:** most persisted fields are local metadata only (titles, loglines, character names, word counts, embeddings) and **nothing leaves the device**. The one column that genuinely holds literal manuscript prose, `codex_mentions.excerpt`, is now cell-level encrypted (AES-256-GCM via `services/duckdb/duckdbEncryption.ts`, reusing the IDB at-rest encryption key) whenever `enableIdbAtRestEncryption` is active: `duckdbCodexWrite()` writes ciphertext into `excerpt_enc BLOB` and nulls the plaintext `excerpt` column; `services/duckdb/codexExcerptEncryptionMigration.ts` backfills any pre-existing plaintext rows once encryption is unlocked. Gated by `enableDuckDbAnalytics` **and** the Settings → Privacy "Analytics" opt-out (`isAnalyticsPersistenceAllowed` in `app/listenerMiddleware.ts`); turning the toggle off stops all DuckDB writes + inference telemetry. Full OPFS file-level encryption remains **infeasible** — DuckDB-WASM owns the OPFS file handle directly, so there is no app-level interception point; the other metadata columns stay intentionally plaintext (bounded-exposure design). | `app/listenerMiddleware.ts:isAnalyticsPersistenceAllowed`, `services/duckdb/duckdbAnalytics.ts:duckdbCodexWrite()`, `services/duckdb/duckdbEncryption.ts`, `services/duckdb/codexExcerptEncryptionMigration.ts` |
Expand Down
Loading
Loading