161 lines
5.3 KiB
Markdown
161 lines
5.3 KiB
Markdown
|
|
# 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`)
|