# Vinterliste A small end-to-end-encrypted app for collecting *winter activities* — things to do when winter feels long. Activities can be: - **private** — encrypted client-side; the server only ever sees ciphertext; - **semi-public** — visible to everyone, but the creator is **not** shown; - **public** — visible to everyone, attributed to the creator. See [`SECURITY.md`](./SECURITY.md) for the cryptographic model. It's load-bearing — read it before changing anything in `shared/crypto.ts` or the auth flow. ## Stack - **Runtime:** [Bun](https://bun.sh) 1.3+. TypeScript everywhere. - **HTTP:** [Hono](https://hono.dev) on Bun. - **DB:** `bun:sqlite` (built-in), WAL mode. - **Server password hashing:** `Bun.password` (argon2id) — auth verifier only. - **Client crypto:** `libsodium-wrappers-sumo` (WASM — the SUMO build is needed because the standard `libsodium-wrappers` doesn't ship `crypto_pwhash`). Argon2id via `crypto_pwhash`; AEAD via XChaCha20-Poly1305-IETF. - **Frontend:** Svelte 5 + Vite. Private tag index in IndexedDB. - **Container:** single `oven/bun` image, one volume for the SQLite file. ## Layout ``` shared/ pure modules used by both server and frontend crypto.ts libsodium-backed key derivation, AEAD, wrap/unwrap, helpers types.ts wire-level types shared across the network boundary server/ Bun + Hono backend db.ts bun:sqlite, WAL, idempotent schema migration session.ts opaque, server-stored sessions (httpOnly cookie) auth.ts signup, challenge, login, logout, password change, recovery activities.ts CRUD with visibility rules tags.ts server-side (public/semi) tag store + autocomplete index.ts Hono app + static frontend in production frontend/ Vite + Svelte 5 SPA src/lib/crypto.ts re-exports shared/crypto for bundling src/lib/api.ts fetch wrapper for the JSON API src/lib/auth.ts signup/login/recovery orchestration src/lib/tagIndex.ts IndexedDB store for private tags src/lib/session.svelte.ts in-memory DEK + current user src/components/ Svelte 5 components (Login, Signup, Recovery, Home, …) tests/ Bun tests crypto.test.ts round-trip wrap/unwrap, AEAD, password change, recovery Containerfile single-image build for podman ``` ## Running locally You need Bun 1.3+ installed. ```bash bun install # 1. In one terminal — start the API on http://localhost:3000 bun run dev:server # 2. In another terminal — start the Vite dev server on http://localhost:5173 # (it proxies /api to :3000) bun run dev:frontend ``` The dev server writes the SQLite file to `data/vinterliste.db`. Set `VINTERLISTE_DB=/some/other/path` to override. ## Tests ```bash bun test ``` The crypto tests cover: - DEK wrap/unwrap via both the password and recovery paths; - AEAD encrypt/decrypt round-trip, plus tamper and wrong-key rejection; - password change preserves activity ciphertexts (DEK is the same); - recovery unlocks even after multiple password changes; - recovery-code normalisation handles dashes and casing; - the safe alphabet excludes visually ambiguous characters. ## Typecheck ```bash bun run typecheck ``` ## Production build ```bash bun run build:frontend # produces frontend/dist NODE_ENV=production bun run start ``` The server serves the SPA from `frontend/dist` in production. All non-`/api/*`, non-`/assets/*` requests fall through to `index.html` so client-side routing still works. ## Container (podman) The provided `Containerfile` builds a single image that serves API + frontend and persists the SQLite database in `/app/data` (one volume). ```bash BUILDAH_FORMAT=docker podman build \ --build-arg BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)" \ --build-arg GIT_REVISION="$(git describe --always --dirty 2>/dev/null || echo dev)" \ -t vinterliste:latest . # Create a named volume for the SQLite file podman volume create vinterliste-data podman run --replace --name vinterliste \ -p 3000:3000 \ -v vinterliste-data:/app/data:Z \ vinterliste:latest # Visit http://localhost:3000 ``` The container exposes `/api/health` for healthchecks and bakes the build date / git revision into both OCI labels and `/etc/build-info`. ## Manual verification After signing up an account, the spec asks you to inspect a `private` row directly in the DB and confirm only ciphertext is stored: ```bash sqlite3 data/vinterliste.db \ "SELECT id, visibility, title, loc_label, scheduled_at, length(ciphertext) AS ct_len, length(nonce) AS nc_len FROM activities WHERE visibility = 'private';" ``` You should see `title`, `loc_label`, and `scheduled_at` all `NULL`, and the `ciphertext` / `nonce` columns populated. ## Status / scope This is the scaffold from `winter-list-claude-code-prompt.md`. In scope: - repo structure, schema, single-image container - crypto module + tests - signup / login / password change / recovery - activity CRUD with strict visibility handling - tag autocomplete (server `tags` table + client IndexedDB) Explicitly **out of scope** for now: - sharing/permissions beyond the three visibility levels - comments, notifications, other social features - native/mobile apps - server-side full-text search over private data - email-confirmation step for recovery (lockout-DoS mitigation noted in `SECURITY.md`)