6 Commits
Author SHA1 Message Date
Eric WagonerandClaude Fable 5 1cf5d0a68d The determinism harness moves into deploy/ for real
deploy/verify-ledgers.sh fetches every production ledger and strictly
replays it against the local engine (any refusal or crash fails; a
lobby that never dealt is trivially clean). Run from the repo root
before deploying engine changes — the check both of tonight's replay
breaks would have caught.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138A8CjeQRpvzKxuMfz1Bqc
2026-08-19 22:06:56 -04:00
Eric WagonerandClaude Fable 5 3dcdf50498 Nightly ledger backup to DO Spaces (deploy/wizwar-backup.sh)
Installed on the droplet at /usr/local/bin/wizwar-backup.sh, cron 07:17
UTC daily: mirrors /var/lib/wizwar to spaces:kestrel-wizwar-backups/
wizwar/current and keeps 90 days of dated snapshots. Ledgers are
append-only, so no service stop is needed. The script no-ops with a
log line until Spaces credentials land in /root/.config/rclone/
rclone.conf.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0138A8CjeQRpvzKxuMfz1Bqc
2026-08-17 23:15:28 -04:00
Eric WagonerandClaude Fable 5 695307daa8 Credibility pass: one voice, no scars
A three-reviewer sweep for tells of piecemeal machine generation,
every finding verified against the code before touching it. No
behavior changes; the full suite passes unchanged (plus two
strengthened pins).

Engine: removed four void-silenced fossils (a parseEdgeKey call
voided where it stood, stoneEffect's ignored cardId parameter, the
actualTarget remnant in doCast, a voided loop variable in shadow
upkeep); fixed the initialize-then-overwrite narration in
spawnCreature; replaced a filter(() => false) no-op; waterwall now
rides waveFromEdge instead of carrying its own verbatim copy (and the
single-caller washBack wrapper went with it); blind wall-bumps and
LOS blockers each collapsed to one implementation; the wand-id list
and the "permanent" duration sentinel became named constants; the
ambush number local no longer shadows the imported numberValue
function; assorted reviewer-aimed phrasings rewritten as the
constraints they guard.

Server/deploy: the protocol header now documents all eleven message
types; dropped an eslint pragma with no eslint, a test script with no
tests, and an rsync exclude anchored at a path that never existed
(the real data/ dir now excluded); the Caddy vhost has one source of
truth; stale "pending DNS" note removed — the record resolves.

Web: ~90 lines of CSS swallowed verbatim into a mobile media query
deduplicated; the reduced-motion guard on the board now actually
stops the marked-cell pulse; one shared color module replaces two
drifted palettes; an orphaned doc comment rejoined its function.

Tests: the ten-times-pasted helper block became test/helpers.ts;
wave-numbered files renamed for the behaviors they pin; deliberation
comments and void-ed corpses of unwritten assertions deleted; silent
seed-dependent early-returns now fail loudly; one assertion that
compared a value to itself now pins the home-translation it meant to;
the stored-log single-number command form gained the explicit
compatibility test it deserved.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 11:56:55 -04:00
Eric WagonerandClaude Fable 5 f0a264147f Security hardening for public playtesting
The server code learns to distrust strangers: a 64KB WebSocket payload
cap (the ws default is 100MB — an easy OOM on a 1GB droplet), a
per-connection token-bucket rate limit, caps on concurrent sockets,
total rooms, rooms per connection, pending transfer codes, and seats
per myGames query. Player names are stripped of control characters
and bounded at 24 chars, room codes at 8, serialized commands at
16KB before they touch the append-only log. Catch-up replays — a
full game rebuild per request — get a 3-second cooldown. Unexpected
exceptions now log server-side and send strangers a bare "internal
error" instead of the exception text.

One real bug found by the sweep: myGames compared the client's raw
seat token against the stored hash, so the lobby ledger silently
matched nothing since tokens were hashed at rest — and the comparison
wasn't timing-safe either. It now goes through the same timingSafeEqual
path as every other seat check, via a new exported seatTokenValid.

The droplet tightens too: the game server binds loopback (HOST env)
so port 8787 no longer answers the internet — it was reachable
directly, plaintext, bypassing Caddy — and ufw now allows only ssh,
80, and 443. The systemd unit gains a sandbox (ProtectSystem=strict,
ProtectHome, NoNewPrivileges, PrivateTmp, MemoryMax=700M so a runaway
process is killed and restarted before it takes the box down) and
execs tsx directly instead of through npx. Caddy adds HSTS, nosniff,
frame-denial, and no-referrer headers; setup-droplet.sh records all
of it for future rebuilds.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 10:08:23 -04:00
Eric WagonerandClaude Fable 5 0b6ac25bc3 Prepare wizwar.kestrelsnest.social: Caddy vhost and DNS notes
Caddy on the droplet now answers for both the sslip.io hostname and
wizwar.kestrelsnest.social, fetching the latter's certificate
automatically the moment DNS resolves. The domain's authoritative
nameservers are Hover's, so the required A record must be added there;
a matching record was also placed in the (currently inert)
DigitalOcean zone for future-proofing. README updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 09:38:04 -04:00
Eric WagonerandClaude Fable 5 23bdd009a0 Deploy to a DigitalOcean droplet: wizwar is on the internet
Production shape: one Node process serves the built client and the
websocket on a single port (SPA fallback, same-origin wss in the
client), with Caddy terminating auto-TLS in front. The droplet
(nyc3, $6/mo) runs it under systemd as an unprivileged user with room
files on the persistent disk at /var/lib/wizwar/rooms — deploys and
reboots cannot eat a game. deploy/ carries the one-time droplet setup
script, the systemd unit, the Caddyfile, an everyday deploy script
(build locally, rsync, install, restart), and a README. Live at
https://wizwar.104.236.96.198.sslip.io via sslip.io, so TLS needed no
DNS setup at all. Verified over the real internet: room created,
second player joined, expansion game started, hands dealt, room file
persisted. Fixed en route: rsync's unanchored "data" exclude was
stripping the engine's card database, and tsx must ship (it is the
runtime).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-16 00:54:15 -04:00