Repository navigation
Description
O problema — o projeto já aprendeu essa lição, e aplicou em 1 dos 5 eventos
O canal coderacer:room:<CODE> é público: qualquer cliente com a anon key (que é pública,
está no bundle) entra nele e emite qualquer evento. O código sabe disso — a doc do
scheduleResync (useRoom.ts:333-338) diz, com todas as letras:
"O gatilho vem de um canal público: sem teto, um cliente hostil transformaria um flood de
mensagens (barato) num flood de GETs à API vezes o número de membros da sala."
E resolve — para um evento. Levantando os cinco handlers em origin/main@eee4b62:
| evento | handler | teto no receptor | efeito por mensagem |
|---|---|---|---|
room |
useRoom.ts:270-272 |
✅ RESYNC_MIN_INTERVAL_MS = 800 ms (:33, :339-346) |
releitura coalescida |
progress |
useRoom.ts:236-241 |
❌ | setProgress → invalida o memo de players (:422-448) → re-render de RoomView/Race/RaceTrack |
chat |
useRoom.ts:243-247 |
❌ | setChat(prev => [...prev.slice(-80), m]) — copia um array de 80 por mensagem |
ready |
useRoom.ts:250-254 |
❌ | setReadyMap → mesmo memo de players |
vote |
useRoom.ts:257-261 |
❌ | setVotesMap → activeVotes + tallyVotes (:456-463) |
PROGRESS_THROTTLE_MS (120 ms, :29, aplicado em :586) não protege nada disto: é o
throttle do emissor, no código do próprio cliente honesto. Um cliente hostil simplesmente
não roda esse if. Do lado de quem recebe não existe freio algum.
Por que isto é área sagrada (HANDBOOK §2)
O alvo é a corrida em andamento. Um participante da sala — ou qualquer um que saiba o código de
6 caracteres — emite progress em laço e cada mensagem recria o roster (players é um
useMemo sobre progress, com map + filter + sort, :422-448) e dispara o efeito de
maybeFinish (:524-526) em todos os clientes da sala ao mesmo tempo. O TypingCore é memo
com props estáveis e não re-renderiza (o Conselho verificou isso em 05/08), mas a reconciliação
de RoomView/Race/RaceTrack roda na mesma main thread da textarea — que é o que o §2 declara
intocável. É griefing de custo praticamente zero contra o fps de todos os adversários.
E não é só o caso hostil: chat sem teto significa que um jogador segurando Enter no chat já
degrada a corrida dos outros, sem nenhuma má-fé.
Isto não é coberto pela #104: aquela é
rate limit no POST /api/rooms (HTTP, server-side). Este tráfego não passa por rota nenhuma —
vai direto do cliente ao Realtime do Supabase e volta para os outros clientes. Nenhum limite de
servidor o alcança; a defesa tem de estar em quem recebe.
Acceptance criteria
- Cada um dos quatro handlers sem teto (
progress,chat,ready,vote) passa a ter um
limite por emissor (payload.id) — de modo que um id que exceda o orçamento seja
descartado sem que os outros jogadores sejam penalizados. Um teto global por sala não
serve: um griefer calaria a sala inteira. - O caminho honesto não regride: com todos os jogadores emitindo no ritmo do
PROGRESS_THROTTLE_MS(120 ms), nenhuma mensagem legítima é descartada — inclusive numa
sala cheia (ABSOLUTE_MAX_PLAYERS = 30,room.ts:282). - A mensagem final de um jogador nunca é descartada:
progresscomfinishedAt != null
(:585) decideplacee alimenta osresultsdofinish. Perdê-la por throttle
falsificaria a classificação — o remédio não pode virar um vetor novo. - A decisão é pura e testada em
src/lib/room.ts(no molde dereduceRoomSync), com o
handler só aplicando o veredito — o teste é o que prova a AC, e não dá para provar isto por
screenshot (ver abaixo). -
progresscontinua sem autoridade nenhuma: o servidor segue sendo a fronteira anti-cheat
(sanitizeResults). Esta issue trata de custo, não de confiança no conteúdo. -
pnpm typecheck+pnpm test+pnpm build+node scripts/validate-persistence.mjs
verdes.
Verificação — não tente por screenshot
A frota já registrou duas vezes que no Browser pane headless o requestAnimationFrame
congela: framer-motion trava e o visual mente. Prove pela lógica instrumentada — um teste
que alimente a função pura com uma rajada de N mensagens do mesmo id e afirme quantas passaram,
mais um caso com o tráfego honesto de 30 jogadores a 8,3 Hz afirmando que passaram todas.
Escopo e classificação
- §7.3 — normal (o arquivo é cliente; não toca
src/app/api/rooms/**, nem persistência, nem
o contrato anti-cheat). - Conflito de arquivo a declarar: a #111
também mexe emuseRoom.ts, mas em outra região (o efeito do relógio,:413-418). Se as duas
forem pegas juntas, atenção ao merge. - Fora de escopo: limitar o que o emissor honesto manda (já existe), qualquer mudança na
fronteira anti-cheat dofinish(Anti-cheat: finish sem coerência temporal — leaderboard forjável em 1 request e corrida alheia encerrável #34), e rate limit server-side (Segurança/Infra: POST /api/rooms sem rate limit — criar salas é grátis, ilimitado e a linha nunca é apagada (rooms/route.ts:42-80) #104).
Verificado em origin/main@eee4b62. Sem duplicata (--search por flood/broadcast/rate
limit/realtime e por throttle/progress/chat/grief: zero resultados).
Activity