Add owner-only room backup snapshots and audited exports

This commit is contained in:
Quintine committed 2026-09-28 20:12:57 +10:00
1 parent 92564f8de5
commit 53a1ba77a1
19 files changed
+5613 -4

No files matched your search

+35
View File
@@ -0,0 +1,35 @@
# Private room backups
The owner-only backup page at `/admin/backups` captures, lists and downloads immutable snapshots of an existing room. This is a **capture-only** capability: importing, validating, restoring or resuming from a backup is not implemented. A downloaded file must not be treated as a safe restore input or as a replacement for protected database/volume backups. Room play, seats, revocations and game versions remain unchanged during capture.
## Access and operation
Sign in as a current owner and select an exact eight-character room code. The UI reads the live room version separately, then submits `POST /api/admin/backups` with exactly `{roomCode, operationId, expectedVersion, reason}`. The operation ID is a fresh lowercase UUID; the reason is a single-line operational explanation (1–300 characters). Capture is fenced to the live room version **and** an exact re-read of all room-owned data within the insertion transaction. Concurrent game or seat changes, including tables that do not bump the game version, reject the capture. No partial snapshot is saved. A retry of the same operation ID, owner, room, version and reason returns the original immutable receipt, even after later room changes. A different request/owner using that ID is rejected. An identical room/version/payload snapshot cannot be stored twice under different IDs.
Only successful captures appear in the dedicated backup ledger. Its receipt retains owner ID, room code, reason, timestamp, room version, digest and size; it survives room/account deletion. Each successful download separately adds an append-only receipt with export ID, backup ID, owner ID, room code and time, committed before private bytes leave the database. Failed attempts are not recorded. Logs and public metadata never include the private payload or credential hashes.
The append-only download-audit ledger has a global **32,768-record prototype
cap**. The limit is checked in the same transaction as each audit insert.
At capacity, further downloads fail closed with HTTP 507; existing backups
and rooms are unchanged, and no unlogged private export is permitted.
There is no automatic pruning. Operators must plan a protected database
capacity/retention migration with preserved audit provenance before export
availability can resume; deleting audit records merely to bypass this limit
would defeat the security boundary.
The dedicated backup receipts are not yet projected in the separate in-progress
unified administrator action-history page. Owner-only metadata lists also omit
actor and reason. Operators/viewers therefore cannot inspect backup events in
that page yet; the durable receipt tables retain capture and export provenance.
`GET /api/admin/backups?room=CODE` returns at most 100 newest metadata entries `{id,roomCode,roomVersion,createdAt,sizeBytes,digest}`, never game state, reason or credential hashes. `GET /api/admin/backups/[id]/download` returns the full JSON as an attachment. Both reads repeat live owner authorization in the database query; disabled, demoted, expired or revoked sessions lose access immediately. Neither a participant seat cookie nor an operator/viewer session grants backup access. Routes send `Cache-Control: no-store`, `Vary: Cookie` and `X-Content-Type-Options: nosniff`. Mutating requests additionally require the configured exact origin and matching administrator identity header.
## Format and privacy
The downloaded UTF-8 JSON has `format: 1`, `room: {code,state,version,updated_at}`, and `tables` keyed by their SQLite table names. `room.state` is the exact serialized game-state **string** as stored, not a sanitized player view. Each table contains an array of rows with original snake-case column names and values, including revoked seat records. Captured tables: `seats`, `room_controls`, `room_removals`, `room_closures`, `room_archives`, `seat_recovery_keys`, `seat_recovery_receipts`, `seat_handover_offers`, `seat_handover_claim_receipts`, `seat_ai_delegations`, `room_entry_receipts`, `seat_discussion_controls`, `room_messages`. Snapshot data is taken from one room only. Site administrator keys/sessions, unrelated rooms and admin action receipts are excluded. SHA-256 `digest` covers the exact UTF-8 payload bytes and `sizeBytes` is their byte count.
**Treat downloaded JSON as highly sensitive.** It can contain secret cards and plans, private discussion, participant names, seat token hashes, recovery/handover hashes and revoked credentials. Store/download only to a restricted location; never paste it into tickets, logs, shared browser storage or public test reports. The database backup payload is private; the list response is deliberately metadata only. There is no automatic expiration or deletion policy in this capture slice.
Capture refuses a serialized payload over **1,800,000 UTF-8 bytes** with HTTP 413; the estimate is conservative and evaluated before constructing a D1 cell against its 2,000,000-byte string/row limit. No backup is silently truncated. This **prototype limit** means a long-lived room with extensive discussion may become uncapturable: larger rooms require a future chunked snapshot format with atomic manifest, bounded chunk reads and validated assembly. Such rooms are **not backed up** by this feature; use a protected database/volume backup instead. The ledger additionally limits each room to 32 snapshots and 16 MiB total, and all rooms to 64 MiB total (HTTP 507 when full). There is no automatic expiry or pruning; quota management/deletion is not implemented. List queries return only the newest 100 receipts per room; older receipts remain downloadable by exact ID. Future restore/import work must independently validate game schema, custody, ownership and credential revocation before any write, preserve a current recovery checkpoint, and must not silently reinstate a revoked seat. None of those restoration behaviors exists here.
The row limit follows the [Cloudflare D1 storage limits](https://developers.cloudflare.com/d1/platform/limits/).
+6 -1
View File
@@ -14,7 +14,7 @@ recovery and persistence systems where suitable.
| Create and configure | [Create a lobby](ADMIN_ROOM_CREATION.md) with an explicitly owned new host seat, Basic/Advanced preview and initial AI configuration; invite humans and use ordinary lobby controls/voluntary host-seat handover. [Neutral lobby configuration](ADMIN_LOBBY_CONFIGURATION.md) adds rules/modules, AI seats and host assignment to an existing human without private access. Reserved human seats remain. | Prototyped, partial |
| Lifecycle and removal | [Pause/resume and joining locks](ADMIN_ROOM_CONTROLS.md), [administrative close/reopen](ADMIN_ROOM_CLOSURE.md) and [recoverable removal/restoration](ADMIN_ROOM_REMOVAL.md) have controls, durable audit, exact retries and player/AI enforcement. [Archive/unarchive](ADMIN_ROOM_ARCHIVE.md) has separate directory filters and exact retries. Permanent deletion and bulk actions remain missing. | Prototyped, partial |
| Participant support | [Participant AI](ADMIN_PARTICIPANT_AI.md) enables an existing difficulty on an eligible human seat in a paused game, retaining access/takeback and private custody. [Discussion controls](ADMIN_DISCUSSION.md) mute/unmute new sends while retaining history and gameplay. Participant removal, access revocation, assisted recovery and further replacement remain. Existing lobby controls can reassign the host. | Prototyped, partial |
| Saved-game operations | Create/list/download backups, validate imports, restore a selected checkpoint safely, and diagnose or resume interrupted automatic work through authoritative game actions. | Missing |
| Saved-game operations | [Owner-only room backups](ADMIN_BACKUPS.md) capture/list/download a size-limited, version-fenced room snapshot without changing play or access; import validation, safe checkpoint restore and interrupted-work diagnosis/resume remain missing. | Prototyped, partial |
| Operations and audit | Show server/build/storage health, room/player counts and actionable errors; provide maintenance controls and a searchable record of administrator actions. | Missing |
Track each area through Missing, Prototyped, Integrated, Verified and Polished.
@@ -59,6 +59,11 @@ the administrator receives no private seat. Resume remains a separate action.
[Discussion controls](ADMIN_DISCUSSION.md) restrict new messages from a human seat,
including after recovery/handover, without exposing private history.
[Private room backups](ADMIN_BACKUPS.md) let an owner capture the current saved
state and related room-owned records without pausing or editing the room. The
metadata directory omits private contents; downloaded JSON contains secret
game state and hashed seat/recovery credentials. Import/restore are not available.
The directory uses an explicit field allowlist, independent of any player view.
It never returns hands, Traitors, predictions, plans, private messages, recovery
records or raw saved state. Searches inspect only codes and public player names.
+7
View File
@@ -32,6 +32,13 @@ enter TrueNAS Apps because the host desktop was OS-locked; it made no NAS change
backup or post-rollout claim. Production remains at the last verified `c9a3625`
image until the Apps session is unlocked and this exact image is deployed.
An owner-only [room-backup prototype](ADMIN_BACKUPS.md) now captures, lists and
downloads immutable private snapshots with live authorization, unchanged room
versions and append-only capture/export receipts. The per-room size/storage
limits, missing import/restore and absent unified action-history entries keep
the saved-game operations gate open. Local isolated capture/export and a
signed-out browser denial passed; this is not deployed evidence.
The bounded [Fremen Nexus Cunning](NEXUS_FREMEN_RULES.md#connected-native-cunning-path)
prototype connects an initially empty worm's public offer, one spent private
card, a Karama response, a typed remote ride after the Nexus, human controls
+38
View File
@@ -1,5 +1,43 @@
# Dune implementation status
## 28 September 2026 — owner-only room backup capture
The new `/admin/backups` page lets an authenticated owner capture, list and
download an immutable private room snapshot. It includes the saved game and
related seat/recovery, lifecycle and discussion records without changing room
version or granting access. Exact operation replays, live database authorization,
content/version fencing and byte quotas protect competing writes and storage.
An append-only owner/room/time receipt records each successful private export
before its payload is returned. These are capture-only files: there is no import,
validated restore, server health display or unified action-history entry yet.
Twelve isolated in-memory D1 backup cases pass, including exact replay, revoked
access, unversioned concurrent changes, highly escaped state rejection, the
32,768-record download-audit capacity boundary and duplicate snapshots. A throwaway genuine game created in an isolated store was
captured, listed and downloaded once; its room version remained zero and its
export receipt count was one. The signed-out local browser showed the owner
gate. A separate intercepted local UI view showed an older snapshot available
to download after its live room had disappeared; that view used mocked API
responses, not a real room restoration. The HTTP suite passed 53/53, including
a new anonymous, forged-admin and real room-host backup denial case; the build
included both new backup routes and the page. The stable full offline run
passed 5,662/5,662 cases with zero failures.
`npm run check` remains blocked by the unrelated concurrent Fremen Nexus draft's
missing `Player.fremenNexusMovementBlockedTurn` type field. The 1,800,000-byte
per-snapshot cap, 16 MiB per-room and 64 MiB global quotas, bounded export
ledger without archival, absent chunked backups, and unavailable live NAS Apps
session keep saved-game and deployed acceptance open. No production game or credential was modified.
## 28 September 2026 — rules-reference duplicate links
The Ecaz faction topic no longer repeats its Duke Vidal related link, and the
Homeworld implementation checklist lists each Tupile intelligence and Terror
entry receipt source once. The local `/rules?topic=faction-ecaz#faction-ecaz`
page rendered its opened Ecaz guide and one Duke link after the change; its
fresh browser console had no React duplicate-key errors. Before the change,
the same page reported duplicate keys for all three entries. This is a
reference-display correction, not new game-rule acceptance.
## 28 September 2026 — source-stable complete-game integration samples
`tools/faction-games.ts` completed five genuine Basic Tleilaxu/Leader Skills