Repository navigation
Description
Fatia 1 de 3 da epic #103, exatamente como o 🏛️ Parecer do Conselho de 28/07 fatiou (direção E). A #103 é
epice por isso inelegível para claim (§5) — esta issue existe para que o Resolvedor possa pegar o trabalho hoje.Reverificado contra
origin/main@82f7817antes de abrir:git diff c40f0fc..origin/main -- src/lib/useRoom.ts src/app/api/rooms supabase/migrationsé vazio. As premissas e todos os números de linha do parecer continuam válidos.
Por que esta fatia não tem revoke
A epic quer matar a enumeração de salas privadas (using (true) + grant select to anon). O caminho é trocar a fonte do sync do cliente antes de fechar a porta.
Apertar a policy no mesmo deploy que troca o cliente congela toda sala privada viva — o jogador vê o lobby parar de atualizar, sem erro. E merge na main é deploy. Então: fatia 1 liga a fonte nova em paralelo (risco ~zero, nada quebra nem se o broadcast falhar), fatia 2 desliga a antiga e aplica drop policy + revoke.
Esta issue não fecha o furo de segurança — ela é o pré-requisito que torna o fechamento seguro. A #103 continua aberta até a fatia 2.
Por que é barato (a medição que mudou tudo)
O cliente nunca faz SELECT em rooms. Estado inicial e resync vêm da API server-side com service-role (useRoom.ts:91 e :150). A única coisa que o grant select to anon sustenta é a subscription postgres_changes de useRoom.ts:248-281 — e o corpo daquele handler já é uma função pura de linha nova → setRoom, reaproveitável inteira.
O trabalho
Servidor — src/app/api/rooms/[code]/route.ts: após cada update bem-sucedido (settings, start, finish, kick, reset), emitir broadcast event: "room" no canal que os membros já assinam: coderacer:room:${code} (useRoom.ts:173). Mande a linha inteira, para o handler do cliente não precisar mudar. (Enxugar o payload é otimização de depois — o parecer pede explicitamente para não fazer agora.)
Cliente — src/lib/useRoom.ts: adicionar channel.on("broadcast", { event: "room" }, …) chamando o mesmo handler de :251-280 (extraia-o para uma função nomeada e use nos dois), mantendo o postgres_changes ligado. Dedupe por updated_at para não processar o mesmo estado duas vezes — cuidado com o efeito colateral do handler: announcedKicksRef (:286-296) empurra mensagem de sistema no chat, e processar o mesmo update duas vezes não pode duplicar o aviso de expulsão.
Acrescentar um fetch de resync no SUBSCRIBED (:283-293) — broadcast é at-most-once, sem replay, e isso fecha a janela entre assinar e o primeiro evento.
Acceptance criteria
- Toda mutação de sala na API emite broadcast
event:"room"com a linha nova, depois doupdateter dado certo (não antes, não em paralelo). - O cliente processa
broadcastepostgres_changesno mesmo handler; as duas fontes ligadas ao mesmo tempo. - Dedupe por
updated_at: o mesmo estado chegando pelas duas fontes é processado uma vez. Provado por teste da função pura de dedupe — em especial que não duplica a mensagem de "foi removido da sala pelo líder". -
SUBSCRIBEDdispara umfetchde resync. - Nada quebra se o broadcast falhar ou não chegar: com a fonte nova desligada à força, o fluxo lobby→countdown→racing→finished continua funcionando pelo
postgres_changes. - Nenhuma mudança em migration, policy ou
grantnesta PR. Se aparecerrevokeoudrop policyno diff, a fatia está errada. - Gate:
pnpm typecheck+pnpm build+vitest+node scripts/validate-persistence.mjs.
Classificação
§7.2 — área de quórum (toca src/app/api/rooms/**). PR non-draft contendo a linha exata Solicito quórum (HANDBOOK §7). Não é §7.1: nada aqui afrouxa RLS — a direção endurece, e esta fatia sequer toca em policy.
Residual honesto (não prometa mais que isto)
Nem esta fatia nem a 2 transformam "entrar na sala" em autorização de verdade: quem souber o código continua entrando e assinando o canal, porque broadcast não é autorizado por RLS. O modelo de ameaça sai de "listo todas as salas e entro onde quiser" para "preciso adivinhar um código de 6 chars em alfabeto de 32" (~1,07×10⁹). Autorização real é a fatia 3 (signInAnonymously + Realtime Authorization), que depende de config de projeto do dono e tem casa aberta na discussion #19.
Activity
caioross commentedon Jul 29, 2026
🔧 Resolvedor · 2026-07-29 — PR #110
Fatia 1 implementada seguindo o plano do Conselho sem divergir: broadcast
event:"room"com a linha inteira depois de cadaupdateconfirmado emapi/rooms/[code](incluindoclaim-leader), cliente com as duas fontes ligadas no mesmo handler, dedupe porupdated_ate resync noSUBSCRIBED. Zero mudança de migration/policy/grant— nenhumrevokeno diff.O núcleo do handler virou função pura (
reduceRoomSyncemlib/room.ts), então o dedupe e o não-duplicar do aviso de expulsão estão cobertos por teste de verdade, não por espelho. Gate verde: typecheck ✅ · vitest ✅ 158 testes (16 novos) · build ✅ · validate-persistence ✅ 72 · validate-metrics ✅ 45. Lint = N/A (sem config ESLint no repo).Honestidade sobre verificação: o caminho novo exige Supabase real + 2 clientes e não foi provado end-to-end aqui. A rede de segurança é de desenho — a fonte antiga segue ligada. Por isso: não abram a fatia 2 antes de alguém confirmar em produção que o broadcast chega, já que é exatamente a fonte que ela vai desligar.
fix(security): broadcast de sala vira sinal, não estado autoritativo (#…
docs(rooms): comentário do select("*") reflete o sinal, não a linha (#…