Seat tokens hashed on the ledger, inputs written as they arrive, codes checked against the disk

A seat line now carries the SHA-256 of its token, compared in constant
time; the token itself goes once to the browser that earned it. Ledgers
written before this are read once more by their raw token, and
deploy/hash-tokens.ts rewrites them in place. Each submitted move is its
own ledger line, so a half-written turn survives a restart. A room code
is never reissued over an evicted room's ledger. Only the host seats a
bot. The deploy gate compares against the gallery view, so no token
leaves the ledger for it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0141G6xqLeNRYEtviLWSB5Up
This commit is contained in:
Eric Wagoner
2026-09-23 12:07:24 -04:00
co-authored by Claude Fable 5.1
parent effe17c729
commit 966a36b4aa
6 changed files with 94 additions and 33 deletions
+3 -4
View File
@@ -13,7 +13,6 @@ import { createGame, type GameState, type TurnInput, type WizardId } from '../sr
interface Seat {
id: WizardId;
name: string;
token: string;
bot: boolean;
}
@@ -51,9 +50,9 @@ async function main(): Promise<void> {
continue;
}
let verdict = state ? `${turns} turns, ${state.over ? 'over' : 'in play'}` : 'not started';
const human = seats.find((s) => !s.bot);
if (host && state && human) {
const res = await fetch(`${host}/api/rooms/${code}?token=${encodeURIComponent(human.token)}`);
if (host && state) {
// The gallery's view carries every wizard's health and the outcome, which is all the comparison needs.
const res = await fetch(`${host}/api/rooms/${code}`);
if (!res.ok) {
verdict += `, server ${res.status}`;
} else {