diff --git a/packages/server/src/games.ts b/packages/server/src/games.ts index db06877..a6f1a0e 100644 --- a/packages/server/src/games.ts +++ b/packages/server/src/games.ts @@ -93,10 +93,11 @@ function makeSlot(token: PlayerToken, now: number) { } function makeBotSlot(now: number) { - // Synthetic slot: occupies the player's color but never connects. - // Token is a 24-char placeholder; never matches a real client. + // Synthetic slot: occupies the player's color but never connects. The token + // is randomized (same shape as a real client token) so a third party can't + // hijack the bot's color by guessing a fixed placeholder. return { - token: 'bot' + 'x'.repeat(21), + token: newPlayerToken(), socket: null, joinedAt: now, rateBucket: { tokens: RATE_LIMIT.capacity, last: now }, diff --git a/packages/server/src/ws.ts b/packages/server/src/ws.ts index fdcfa3a..9ef0dcb 100644 --- a/packages/server/src/ws.ts +++ b/packages/server/src/ws.ts @@ -32,20 +32,12 @@ async function pokeBot(game: Game): Promise { } } -function broadcastSinceLast( - game: Game, - extra?: { touchedPieceFor?: Color; touchedPiece?: import('@blind-chess/shared').Square }, -): void { +function broadcastSinceLast(game: Game): void { for (const c of ['w', 'b'] as const) { const lastIdx = game.lastBroadcastIdx[c]; const all = game.announcements; const slice = all.slice(lastIdx).filter((a) => a.audience === 'both' || a.audience === c); - sendUpdateTo( - game, - c, - slice, - extra?.touchedPieceFor === c ? { touchedPiece: extra.touchedPiece } : undefined, - ); + sendUpdateTo(game, c, slice); game.lastBroadcastIdx[c] = all.length; } } @@ -249,7 +241,7 @@ function onClose(ctx: SocketCtx): void { if (game.status === 'active') { game.disconnectAt[color] = Date.now(); // Schedule grace timer. - setTimeout(() => maybeAbandon(game, color), GRACE_MS + 100); + setTimeout(() => { void maybeAbandon(game, color); }, GRACE_MS + 100); } notifyPeer(game, color, false, Date.now() + GRACE_MS); }