Companion to PLAN.md. Charnik is a standalone Tauri desktop app (no HTTP server). That removes the whole network attack surface; the real risks are untrusted content (CSV/JSON, shared homebrew, bundle/content-pack imports) and file-system scope. Security tasks are woven across roadmap phases, not one late step.
- No network server → no LAN/remote/auth surface. Single local user, single app instance. (This is why the server-era controls — localhost bind, LAN token — are gone.)
- Content is untrusted input — it may come from strangers (shared packs, imports).
- Assumption: personal desktop use; not multi-tenant. Concurrency clobber is largely moot (one instance) but writes are still atomic + mtime-guarded.
- No HTTP server. Tauri loads the SPA from a bundled protocol in the webview; the frontend talks to the OS only through scoped Tauri commands/plugins. No open ports.
- Tauri capabilities / least privilege. Grant only the plugins we need (
fs,dialog,path) and scopefstodataDir/roots — no broad filesystem access. Path traversal is blocked by the fs scope and validated in theStorageinterface. - All IO via the
Storageinterface. One audited seam; no scattered raw fs; nothing above it imports Tauri. The node/in-memory test impl exercises the same validation. - Effects are data, never code. The effects engine is a fixed-vocabulary
interpreter, not
eval/a DSL (incl. user-entered custom effects; free text is inert display). Malicious content can't execute — worst case it's flagged in content-health. Expressiveness comes in three layers, never by putting code in a CSV cell:- L1 declarative bounded vocab (data:
kind/target/op/value/when/scope) — ~95%. - L2 safe value-expressions (
1d4,prof*2,ceil(level/2)): OUR dice+arithmetic parser, non-Turing-complete, whitelisted variables, noeval, no host access. - L3 plugins (long tail): first-party/signed handlers registered on the engine seam
are trusted/easy; community plugins run in a QuickJS-in-WASM sandbox (DECIDED) —
quickjs-emscripten, a narrow host API that only takes effect-context and returns{value, trace}contributions, hard time/memory limits, no DOM / no Tauriinvoke/ no fs / no network. Never raw dynamic-import, never an unsandboxed Worker-with-bridge. Every layer keeps the{value, trace}contract so contributions stay explainable.
- L1 declarative bounded vocab (data:
- Webview hardening. Strict Tauri CSP; no remote content loading; no
eval/inline script; external links open in the OS browser, not the app webview. - Image upload hardening. Allowlist types (png/jpg/webp); size cap; re-encode (strip EXIF / prevent polyglots); store only inside the character folder (in scope).
- Bundle / content-pack import = data only. Parsed, validated (zod) against the schema, surfaced via collision/health UI before use; never executed, never silent overwrite.
- Parsing safety. Vetted parsers (
papaparse,JSON.parse); row/cell/file size caps to avoid memory blowups; malformed rows → health view, not a crash. - Minimal Rust surface. Prefer official audited plugins; keep custom Tauri commands few and narrow (each is native attack surface).
- No secrets, no telemetry. FOSS; nothing phones home.
- Atomic writes (temp→rename) + rotating backups (corruption robustness).
- Dependency hygiene. Minimal deps; pin versions; periodic
npm audit+cargo audit(Rust crates from Tauri).
- P1: Tauri capabilities + fs-scope config; the audited
Storageinterface (+ node/in-memory impl) with path validation; strict CSP. - P3/P4: effects-as-data interpreter (no code execution); zod validation.
- P7: atomic writes/backups; image re-encode + scope on photo save; bundle-import validation.
- P12: content-pack export/import validation (treat imported packs as untrusted).
- Pre-1.0: security pass +
npm audit/cargo audit; README notes.
Multi-tenant accounts, permissions, encryption at rest, sandboxing beyond Tauri's model. Revisit only if the project grows beyond personal desktop use.