Absence is not evidence: the seat wallet trusts only verdicts
The games-list reply pruned any wallet seat the server did not vouch for — so one poll against a restarting (or wrong) server silently deleted live credentials, the second seat-burning path this session's server-squatting exposed. myGames now returns explicit verdicts: a seat is voided only when its room EXISTS and refused that exact token. Absent rooms earn no verdict and absent seats survive; the wallet caps by age at fifty instead of by trust. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_015RCWSTnb1KYTPyL4GmhGnF
This commit is contained in:
co-authored by
Claude Fable 5
parent
72148dd7c2
commit
3d2bf17585
@@ -732,18 +732,26 @@ wss.on("connection", (socket) => {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case "myGames": {
|
case "myGames": {
|
||||||
// {seats: [{roomId, name, token}]} -> summaries for valid seats.
|
// {seats: [{roomId, name, token}]} -> summaries for valid seats,
|
||||||
|
// plus explicit verdicts on seats PROVEN dead: the room exists
|
||||||
|
// and refused this exact token. A room this server simply does
|
||||||
|
// not know earns no verdict — absence is not evidence (a
|
||||||
|
// restarting or wrong server knows nothing about anything).
|
||||||
const seats = Array.isArray(msg.seats) ? msg.seats.slice(0, MAX_MYGAMES_SEATS) : [];
|
const seats = Array.isArray(msg.seats) ? msg.seats.slice(0, MAX_MYGAMES_SEATS) : [];
|
||||||
const games = [];
|
const games = [];
|
||||||
|
const voided: string[] = [];
|
||||||
for (const seat of seats) {
|
for (const seat of seats) {
|
||||||
if (typeof seat !== "object" || seat === null) continue;
|
if (typeof seat !== "object" || seat === null) continue;
|
||||||
const room = getRoom(String(seat.roomId ?? ""));
|
const room = getRoom(String(seat.roomId ?? ""));
|
||||||
if (!room) continue;
|
if (!room) continue;
|
||||||
const name = String(seat.name ?? "");
|
const name = String(seat.name ?? "");
|
||||||
if (!seatTokenValid(room, name, typeof seat.token === "string" ? seat.token : null)) continue;
|
if (!seatTokenValid(room, name, typeof seat.token === "string" ? seat.token : null)) {
|
||||||
|
voided.push(`${room.id}:${name}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
games.push(summarize(room, name));
|
games.push(summarize(room, name));
|
||||||
}
|
}
|
||||||
send(socket, { type: "games", games });
|
send(socket, { type: "games", games, voided });
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
|
|||||||
@@ -491,18 +491,18 @@ class Net {
|
|||||||
if (g.yourTurn && !was) this.notifyTurn(g);
|
if (g.yourTurn && !was) this.notifyTurn(g);
|
||||||
this.lastYourTurn.set(key, g.yourTurn);
|
this.lastYourTurn.set(key, g.yourTurn);
|
||||||
}
|
}
|
||||||
// A seat the server answered for but did not list is gone — the
|
// Prune ONLY seats the server proved dead: the room exists and
|
||||||
// room was deleted (or the seat's token voided). Quietly drop it
|
// refused this exact token. A seat merely ABSENT from the reply
|
||||||
// from the ledger rather than showing "unreachable" forever.
|
// stays — a restarting server, a stale restore, or the wrong
|
||||||
// (The server checks at most MAX_MYGAMES_SEATS = 50 per ask;
|
// backend all answer with ignorance, and treating ignorance as
|
||||||
// never prune blind past that cap.)
|
// deletion once cost a live player his seat mid-game. The
|
||||||
if (this.seats.length <= 50) {
|
// wallet is capped by age instead of by trust.
|
||||||
const listed = new Set((msg.games as GameSummary[]).map((g) => `${g.roomId}:${g.name}`));
|
const voided = new Set((msg.voided as string[] | undefined) ?? []);
|
||||||
const kept = this.seats.filter((s) => listed.has(`${s.roomId}:${s.name}`));
|
let kept = this.seats.filter((s) => !voided.has(`${s.roomId}:${s.name}`));
|
||||||
if (kept.length !== this.seats.length) {
|
if (kept.length > 50) kept = kept.slice(kept.length - 50);
|
||||||
this.seats = kept;
|
if (kept.length !== this.seats.length) {
|
||||||
saveSeats(this.seats);
|
this.seats = kept;
|
||||||
}
|
saveSeats(this.seats);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user