The SQLite DB is the signed append-only ledger, so a disk failure / stolen or
destroyed PC means total revenue-history loss (open-question #5). This is the first
slice of the backup-recovery design: the engine + a local/mounted target + a daily
timer + a manual route.
Engine (apps/server/src/backup.ts):
- Consistent online copy of the live WAL DB via better-sqlite3's native .backup()
(not a raw file copy, which can capture a torn WAL) — the restored copy is a
byte-identical, queryable DB.
- AES-256-GCM with a scrypt-derived key from BACKUP_KEY; self-describing header
(magic|version|salt|iv|...|authTag) so a restore tool needs only the key + file.
Zero new dependencies (Node crypto).
- The plaintext intermediate is kept in scratch (not the removable/network target)
and wiped in a finally, success or fail.
- Retention: keep-last-N + one-per-day within N days.
Wiring:
- BackupService (env config, single in-flight guard, last-success/last-error).
- routes/backup.ts: GET /api/backup/status (backup:read), POST /api/backup/run
(backup:create), 409 when unconfigured. No restore route — restore is an
out-of-band runbook action on a fresh appliance, not a console call.
- New permission resource in @parking/shared.
- server.ts: an unref'd daily timer, a no-op until BACKUP_TARGET_DIR + BACKUP_KEY
are set, deliberately not run at startup (a just-power-cut booth shouldn't write
to a possibly-unmounted disk).
- openRawDb() added to @parking/db/testing (open a file without migrating, for
restore-verification tests).
BACKUP_KEY is deliberately SEPARATE from EVENT_SIGNING_KEY (independent rotation;
backups travel, the signing key shouldn't). SMB/NFS work as mount paths; SFTP +
admin UI + restore runbook are deferred slices. Tests: round-trip byte-identical,
GCM tamper/wrong-key fail, short-key rejected, scratch cleaned, route auth/RBAC +
409. build/lint/test green (212 server tests). Wiki + open-question #5 updated.
Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V
Foundation for testing every service. Adds @parking/db/testing — createTestDb()
spins a fresh in-memory SQLite and applies the real Drizzle migrations, so server
tests run against the production schema with zero live-DB risk.
Wires Vitest into apps/server (test script + config; test signing keys via env)
and adds the first Phase-1 suites against the anti-fraud core:
- signer.test.ts (10): sign/verify round-trip, tamper + forgery rejection,
malformed-signature guard, determinism, keyId rotation (buildVerifier).
- event-log.test.ts (12): monotonic index, prevHash linkage, payload-in-signature,
append serialization, and verifyChain() catching every tamper class — edited
payload, deleted row (index gap), broken prevHash, unknown keyId — plus
canonicalize byte-stability.
Also stops *.test.ts leaking into shipped dist/ (tsconfig exclude in server +
shared; shared had been emitting compiled tests all along).
server 22/22, shared 87/87 green.
Claude-Session: https://claude.ai/code/session_01Xcm6ikLgGoCxxHrxtjkk5V