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 WagonerandClaude Fable 5.1 committed 2026-09-23 12:07:24 -04:00
1 parent effe17c729
commit 966a36b4aa
6 files changed
+94 -33

No files matched your search

+5 -5
View File
@@ -3,7 +3,7 @@
//
// POST /api/rooms {name, size?} create a duel and take seat A
// POST /api/rooms/:id/join {name} take the next seat
// POST /api/rooms/:id/bot {token} seat a bot (any seated player may, before the duel begins)
// POST /api/rooms/:id/bot {token} the host seats a bot, before the duel begins
// POST /api/rooms/:id/begin {token} the host begins the duel with the wizards seated so far
// POST /api/rooms/:id/unseat {token, seat} the host sends a bot away before the duel begins
// GET /api/rooms/:id?token= the view for that seat; without a token, the gallery's view
@@ -183,8 +183,8 @@ async function handle(req: IncomingMessage, res: ServerResponse): Promise<void>
if (parts.length === 2 && req.method === 'POST') {
if (!doors.allow(clientOf(req))) throw new RoomError('Too many duels opened from here just now; try again later.', 429);
const body = await readBody(req);
const { room, seat } = rooms.create(String(body.name ?? ''), Number(body.size ?? 2));
return send(res, 201, { seat: seat.id, token: seat.token, view: rooms.view(room, seat.id) });
const { room, seat, token } = rooms.create(String(body.name ?? ''), Number(body.size ?? 2));
return send(res, 201, { seat: seat.id, token, view: rooms.view(room, seat.id) });
}
const room = rooms.get(parts[2] ?? '');
@@ -198,8 +198,8 @@ async function handle(req: IncomingMessage, res: ServerResponse): Promise<void>
const body = await readBody(req);
if (action === 'join') {
if (!doors.allow(clientOf(req))) throw new RoomError('Too many seats taken from here just now; try again later.', 429);
const seat = rooms.join(room, String(body.name ?? ''));
return send(res, 200, { seat: seat.id, token: seat.token, view: rooms.view(room, seat.id) });
const { seat, token: minted } = rooms.join(room, String(body.name ?? ''));
return send(res, 200, { seat: seat.id, token: minted, view: rooms.view(room, seat.id) });
}
if (action === 'bot') {
rooms.addBot(room, typeof body.token === 'string' ? body.token : undefined);