feat(backup): encrypted on-site DB backup engine + local target
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
This commit is contained in:
+13
@@ -1921,3 +1921,16 @@ Keys are NEVER inside the backup they unlock — recovery = backup file + both e
|
||||
Updated: [[open-questions]] #5 (design SETTLED) + #10 note (backup includes snapshot BLOBs by default, future
|
||||
exclude toggle); [[disk-os-hardening]] deploy env runbook (EVENT_SIGNING_KEY-not-sealed rationale +
|
||||
`park_buzi_backup_key`); index catalog + concept count 45→46. Design only — NOT yet built.
|
||||
|
||||
## [2026-06-29] feat | Backup engine + local target (first slice of backup-recovery)
|
||||
Built the durability engine designed in [[backup-recovery]]. `apps/server/src/backup.ts`: consistent
|
||||
online copy via better-sqlite3 `.backup()` (NOT a raw file copy of a live WAL DB) → AES-256-GCM with a
|
||||
scrypt-derived key from BACKUP_KEY, self-describing header (magic|ver|salt|iv|…|tag), zero new deps;
|
||||
plaintext intermediate kept in scratch + wiped in finally; keep-last-N + dailies retention. Tested:
|
||||
round-trip → byte-identical queryable DB, GCM tamper/wrong-key fails, short-key rejected, scratch always
|
||||
cleaned. `backup-service.ts` (env config, single in-flight guard, last-success/error) + `routes/backup.ts`
|
||||
(GET /api/backup/status backup:read, POST /api/backup/run backup:create, 409 when unconfigured; NO restore
|
||||
route — out-of-band by design). New `backup` permission resource in @parking/shared. server.ts: unref'd
|
||||
daily timer, no-op until configured, NOT run at startup. `openRawDb()` added to @parking/db/testing.
|
||||
SMB/NFS work as mount paths; SFTP + admin UI + restore runbook deferred. build/lint/test green (212 server
|
||||
tests, 25 files). Updated [[open-questions]] #5 (first slice BUILT). NOT yet committed beyond this branch.
|
||||
|
||||
Reference in New Issue
Block a user