vinterliste/README.md

161 lines
5.3 KiB
Markdown
Raw Normal View History

Scaffold Vinterliste — end-to-end encrypted winter activity list Foundation for an E2E-encrypted activity list per winter-list-claude-code-prompt.md. Server (Bun + Hono): - bun:sqlite with WAL and the spec's schema (idempotent migration) - opaque server-stored sessions, httpOnly cookie - signup / challenge / login / logout / me / password / recovery-challenge / recovery-complete - activity CRUD with strict visibility rules: private uses ciphertext+nonce, semi never serializes owner_id, public attributes the owner - tag store with normalisation + autocomplete (semi/public only) Frontend (Svelte 5 + Vite): - libsodium-wrappers-sumo for client crypto (Argon2id + XChaCha20-Poly1305). SUMO is required because the standard build omits crypto_pwhash. - IndexedDB-backed private tag index (never leaves the browser) - in-memory DEK (no localStorage); page reload re-prompts for password - signup shows the recovery code once; tag input merges server + private sources with clear labelling - Bokmål UI Crypto module (shared/crypto.ts): - pure, runs in both Bun and the browser via a runtime-conditional loader that papers over libsodium-wrappers-sumo's broken ESM entry (createRequire on server, Vite alias in the browser) - DEK wrap/unwrap, AEAD payload encryption, recovery code generation with a visually-unambiguous alphabet Verification: - 22 crypto round-trip tests (wrap/unwrap, AEAD tamper rejection, password change preserves ciphertexts, recovery still works after rotation) - typecheck passes for server and frontend - Vite production build succeeds; libsodium SUMO chunk is ~315 KB gzipped Single-image Containerfile for podman: builds frontend in a builder stage, runs Bun in a slim runtime; one volume for the SQLite file; BUILD_DATE / GIT_REVISION baked into OCI labels and /etc/build-info. Known limitation deferred for this commit: the recovery endpoint has no server-side proof of the recovery code (anyone who knows an email can lock out the legitimate user, though they can't read any data). Closed in the next commit.
2026-05-25 12:27:14 +02:00
# 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`)