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.
This commit is contained in:
commit
47963c9225
39 changed files with 4007 additions and 0 deletions
161
README.md
Normal file
161
README.md
Normal file
|
|
@ -0,0 +1,161 @@
|
|||
# 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`)
|
||||
Loading…
Add table
Add a link
Reference in a new issue