Documento SSR auth-agnóstico — useAuthGate e o expurgo de dado de usuário dos loaders
Janela desta entrada — cobre 20/07 a 26/07 (171 commits em main-7k, a semana mais movimentada do trimestre). Foi também a semana em que main-fix e main-7k foram cruzadas nos dois sentidos (PR #1770 e PR #1773), então muitos assuntos aparecem duas vezes no histórico com SHAs diferentes — cada item abaixo cita um SHA só.
O documento SSR deixa de saber quem é o usuário (PR #1808, branch feat/user-data-out-of-ssr) — 6b1f32a90 remove auth e smarticoHash do loader de routes/_layout, e cb1b41c26 introduz o gate canônico app/hooks/useAuthGate.ts, client-side e sem snapshot SSR: o SSR sempre pinta convidado, o cookie is_authenticated serve como sinal otimista pós-mount e a store reativa assume depois da hidratação. Regra que nasce daqui: visibilidade e comportamento de UI nunca devem ser decididos com useRouteLoaderData("routes/_layout"), porque aquele é um snapshot SSR que não atualiza quando o login/logout acontece por modal.
Loaders per-user desmontados um por um — d006a45fc (wallet, fetch 100% client-side), 62fd7b064 (rewards e income-report), 7de40d656 (página de favoritos), f17975eff (favoritos e recentes da home), 5683cdb3c (a página de jogo passa a serializar só a contagem pública de voteState; o userVote vira client-side), e23934b23 (bootstrap do álbum via /api/album/bootstrap). 28366ffca é a reescrita larga do caminho de dado de usuário no SSR.
Guardrail em teste, não em revisão — 282070eca cria o leak-canary: loaders públicos não podem serializar dado de usuário, e o teste falha se voltarem a fazê-lo. e06e384a0 fecha um furo do próprio guardrail (o canary passou a resolver todos os campos-promise do _layout, não só os síncronos). f8d6acf53 testa o useAuthGate contra a store real e f4d4eb059 deduplica React e Zustand no vitest.config — a resolução source-direct estava trazendo uma segunda cópia de React pela via de alias.
Reconciliação do Smartico — 7c5659fba mantém o smarticoHash correto em login e logout dentro da sessão SPA agora que o doc é auth-agnóstico, e 338c824b0 adiciona guard de corrida no fetch dele (além de corrigir comentários que descreviam o gate SSR antigo).
Semana 25–31/05 — Governança: CODEOWNERS em todo repo + gate de autorização de deploy
CODEOWNERS chega a todos os repositórios de front — 045206ec2 no front-web-base, e93a938 no front-cactus-core, 296deb8 no front-ops, 333c4f3 no front-tests-e2e, da59ea2 no front-service-api e dfd3af6 no front-service-api-dev-proxy. A regra default é * apontando pro time de codeowners de front — qualquer arquivo do repositório passa a exigir review de quem está no time, e adicionar codeowner virou uma mudança de membro de team no GitHub em vez de edição de arquivo.
.github/ endurecido com owners explícitos — 848c853a6. A ownership default segue no time, mas .github/ (workflows e o próprio CODEOWNERS) passa a exigir aprovação nominal de dois owners específicos. A justificativa está no commit: arquivos de governança não deveriam ser mergeáveis só porque qualquer membro do time aprovou — uma aprovação futura maliciosa ou distraída bastaria pra neutralizar a proteção. A sintaxe é gitignore-like com last-match-wins: a regra mais específica abaixo substitui a default acima, não faz união.
Gate de autorização para workflow_dispatch de deploy — front-ops 5dbdcb5 adiciona o campo opcional deploy_protection.required_team no deploy.yml de cada ambiente. Quando presente, deploys manuais via workflow_dispatch naquele ambiente ficam restritos a membros do time nomeado; push (auto-deploy) nunca é gateado por esse campo e continua disparando exatamente como antes. A implementação consulta GET /orgs/.../teams/.../memberships/<actor> com um token que passou a carregar read:org, e falha rápido com erro linkando o time. Aplicado aos 13 ambientes de produção do web-base — todos sem prefixo stage-/dev-/performance-, menos os dois ambientes internos.
Gate movido pro job check — 3d645e8 (mais o gêmeo ee5751e) reposiciona a verificação: o gate é uma pré-condição, como a checagem de elegibilidade de auto-deploy que já existia, então pertence ao job que valida inputs e resolve config. O ganho concreto é a forma da falha na UI — vira check ❌ → deploy skipped em vez de check ✅ → deploy ❌, que sugeria falsamente que o deploy tinha sido tentado. Sem mudança de comportamento: mesmo schema de campo, mesmas checagens, mesmas mensagens de erro.
Substituição integral do módulo pela v2 — a079bcfab troca app/components/album/** pela versão v2 do drop-in: subdiretório share/ novo (15 arquivos) com pipeline de compartilhamento social por figurinha, URL canônica, card de OG safe pra Satori e helpers de UTM; três componentes novos (CheckinSuccessModal, MissionTypeChoiceModal, PackSummaryPhase); StickerCard e StickerRevealStack extraídos como reusáveis; check-in consolidado em CheckInState com dois tipos de claim (diário e de conclusão); e a novidade estrutural — trilhas duplas (casinoMissions / sportbookMissions mais missionType, um gate bloqueante de escolha no primeiro acesso). O hook público passa a ser useAlbumProgress; useProgressSync, CheckInTier e AlbumBootstrap.checkInDays saem do contrato.
Handshake de auth próprio, server-side — 7644499ec. O BFF do álbum exige fluxo de auth distinto do JWT do cookie HttpOnly do host: POST {STICKER_API_URL}/auth/token com { encryptedUserId, brandId, campaign } devolvendo { accessToken, tokenType, expiresAtUtc }, e Authorization: Bearer em todas as demais chamadas. A versão anterior passava o JWT do cookie direto, o que não funcionava contra o BFF real. loadAlbumBootstrap agora faz o handshake antes do Promise.all de endpoints e retorna null quando ele falha — feature degradada em silêncio em vez de rota quebrada. 459b1b0d5 adiciona cache de token cross-request e fd8690a9b projeta o apiCampaign no servidor antes de mandar pro client.
Auditoria de porte com 53 achados — 3a2a0eab6 compara a v2 portada contra o harness do sticker-book, o Swagger oficial do BFF e os contratos consumidos pelo módulo: 19 críticos, 31 médios, 3 baixos. Os commits seguintes endereçam os 19 críticos — entre eles ff17d4230 (sincronizar state.checkIn a partir de data.checkIn após refetch), d78a718a9 (blindar loadAlbumPageData contra throws inesperados), def9b3f41 (gatear o polling de 60s em document.visibilityState), 9066c2284 (alinhar os handlers de rota ao Swagger) e 819b5dd03 (adapters de mutação, retry de 401, tipos de API compartilhados).
Kill-switch por marca e gating de acesso — 93b3d1312 cria albumConfig.enabled fail-closed: o código fica disponível pra todas as marcas mas só executa pra quem declara enabled: true explicitamente, com guard no loader de /vip/album que redireciona pra home antes de qualquer chamada ao BFF. Só o 7k liga. 72bf89007 adiciona modo preview, splash, gating de feature flag, entrada de sidebar e endurecimento de cache; b7157c1e6 esconde a entrada pra usuário deslogado; a13218eae faz o deslogado fechar pelo X e pelo backdrop em vez de abrir login.
Missões vindas do Smartico e do BFF, mescladas por id — dc868194f passa a carregar "Suas Missões" da seção ALBUM_FIGURINHAS do Smartico, 07672c7ae cai em todas as missões quando a seção não existe, 0d0fed93e mescla Smartico e BFF por id e 8dca06afd remove /users/me/missions (deprecado — o Smartico é autoritativo).
Renderização de flip-book e polimento visual — b64d23165 (capa centralizada, tag de faixa de páginas, backgroundImageUrl), 7b84ca712 (continuidade do flip: fatia da lombada, imagem de curl, lombada antecipando o BACK), 746803a4c (tap-to-flip nas bordas), bac022447 (thresholds assimétricos de commit FORWARD/BACK), 253ed4f2e (watermark central nos cards travados), b0a8d3c51 / bc9f28802 (dimensões, centralização e escalas proporcionais no mobile). d392f4072 fecha a semana com a LP de compartilhamento, download de composição e preview anônimo. Testes em c26c5af85, ad332e995 e 9340a8888 cobrem handshake, orquestração, os seis endpoints /api/album/* e os gates de degradação do loader.
Chaves album.* de i18n foram adicionadas como overrides do host em vez de irem pro core (e251c0d09), e 8d78a79d7 troca nomes de placeholder por versões neutras na config base.
Segurança — remoção total de debug do bundle de produção
Toggle ?EnableDebug=1 removido em prod. O parâmetro de URL e a chave sessionStorage["debug_enabled"] foram completamente eliminados — não existe mais runtime gate que possa ser ligado por query param, cookie ou storage. O motivador foi um leak no /api/games/start: o endpoint passava debug:true ao ApiClient e ecoava _debug.requestInfo na response, expondo o header cf-worker-key que o Worker usa pra autenticar com o BFF — qualquer um clicando em "Play" em qualquer brand de prod conseguia ler do DevTools. Endpoint corrigido com 6 testes de regressão que travam o contrato (debug:true proibido, _debug ausente em sucesso e erro).
Painéis DevApiDebug/DevApiExplorer agora gated por import.meta.env.DEV. Novo wrapper DevPanel com React.lazy condicional — em prod, o dynamic import('./DevApiDebug') vira inalcançável e o Vite remove o chunk inteiro do build. Consumers (home, favorites, game detail, wallet, history) trocaram <DevApiDebug> por <DevPanel> envolto em {import.meta.env.DEV && (...)}, garantindo que loader _debug data nunca serializa no SSR stream de prod.
Rotas /api/dev/*, /dev/ftd-cashback, /debug e /debug/analytics só existem em dev build. O registro em app/router/routes.ts é gated por process.env.NODE_ENV !== 'production' — em prod build esses arquivos são tree-shaken por completo (server e client). Eliminados também app/utils/debug.server.ts (com isDebugRequest/?EnableDebug) e a doc docs/enable-debug/overview.md.
CI guard pnpm bundle:guard falha se strings proibidas vazarem para o bundle de prod. Novo script scripts/check-prod-bundle.mjs faz grep no build/ após pnpm build e detecta DevApiDebug, DevApiExplorer, activateDebugFromUrl, isDebugActive, EnableDebug, debug_enabled, validation_debug, /api/dev/, /dev/ftd-cashback, debug/analytics e cf-worker-key (no client bundle apenas). Wired via pnpm build:check (= build + bundle:guard). Adicionar nova string secret-adjacent exige atualizar FORBIDDEN_STRINGS no script.
Bumps no core que sustentam a remoção:@cactus-agents/api-client 0.12.0 (redação regex-based de headers *-key/*-token/*-secret/cf-*/x-api-* no requestInfo em modo debug) e @cactus-agents/utils 1.0.0 major (remove os exports activateDebugFromUrl e isDebugActive — não existe mais API pública pra ligar o toggle em runtime). Defense-in-depth: console.* em ftd-cashback.client.ts, smartico-checkin.client.ts e useOnFirstScrollIntoView.ts agora também passam por import.meta.env.DEV, então mesmo se um caller acidentalmente passar debug:true em prod o output some no build.
Pós-registro & Auth — fluxo regulatório e dedup de bursts
Novo hook useAfterRegisterFlow (app/hooks/useAfterRegisterFlow.ts) orquestra o pós-registro: abre o modal de depósito uma vez, logo após o cadastro, bypassando o throttle de 12h do useAutoDepositModal. Replica o handleOpenRegisterStrategy → openModal('deposit') do handleSuccessRegister legado em Vue. Suporta o ramo regulatório (espera o LimitsStep do ValidationBlockerOverlay resolver antes de abrir o depósito) e suprime quando há ?deposit=<campaign> na URL, conta restrita ou featuresConfig.autoDepositAfterRegister === false.
Store app/store/onboarding.ts (Zustand, não persistida) carrega o sinal justRegistered + lastPrudentialLimitActive entre o sucesso do RegisterModal e o próximo render do DefaultLayout. Reset automático no logout.
Forward de user_prudential_limit_active no payload de signup (/bff/register-simplified, /auth/register): sinaliza ao BFF qual botão regulatório o usuário clicou ("Quero definir meus limites agora" vs "Continuar sem limites"). Nova feature flag autoDepositAfterRegister propagada nos 13 overrides de brand.
Dedup cross-request de /auth/user-profile em app/services/auth.server.ts: cache token-scoped com TTL de 10s + cap de 100 entradas (chave = SHA-256 hex do token, evita expor token cru em dumps de memória). Resolve o burst pós-login onde POST /api/auth/login, useAuthProfileSync → GET /api/auth/profile e revalidator.revalidate() → _layout loader pegavam três Requests distintos com o mesmo token — o WeakMap<Request> antigo não conseguia deduplicar.
/api/auth/profile agora consome getAuthForRequest em vez de chamar createAuthFromClient direto, herdando automaticamente o cache acima. PRs touch-up em 20+ componentes (UserSummaryHeader, ValidationContextGate, GameIframe, payments, protection, validation steps) que liam useProfile() ad-hoc.
Semana 13–21/04 — auth, user e wallet viram @cactus-agents/accounts (breaking)
Os três pacotes de conta foram fundidos em um só, no dia 17/04.eac3812 cria packages/accounts movendo para lá o código de packages/auth, packages/user e packages/wallet (serviços de auth, recuperação, usuário e wallet; restrição de conta; histórico de login; jogo responsável; refresh de token; tipos de auth/user/wallet), e 452450eapaga os três pacotes no mesmo dia — 16 arquivos, 2.623 linhas removidas. Não existe mais @cactus-agents/auth, @cactus-agents/user nem @cactus-agents/wallet.
Migração para quem consome: troque os três imports por @cactus-agents/accounts (serviços e tipos) e @cactus-agents/accounts/react (hooks). No base isso foi feito em 88f85f275, com o pacote entrando via fde5ef596 / 4db2b4ed1 e a versão subindo de ^0.2.0 (a3396a973) para ^0.14.1 ao fim da semana. O package.json do base saiu de auth ^0.6.0 + user ^0.6.0 + wallet ^0.4.1 para uma única dependência accounts.
Retratação: as entradas de março que anunciaram @cactus-agents/wallet e @cactus-agents/user como pacotes SDK publicados descrevem um estado que deixou de existir aqui. Leia-as como histórico — os nomes não resolvem mais no registry.
O subpath /react é o que muda o dia a dia do base.212615a expõe store Zustand no nível do módulo e os hooks de saldo já formatados por locale/moeda (useRealBalance, useBonusBalance, useCashbackBalance); 3fad9c8 completa a superfície de wallet (useWallet, useRollover, useWalletFlags, useTransactions, useTransferBonus, useTransferCashback, useRefreshWallet, useFormatMoney, useAccountsInit), tornando removível a fachada useWallet que o base mantinha à mão; dd73c98 adiciona os hooks de auth (useCurrentUser, useLogin, useRegister, useLogout, useRefreshProfile, useValidateDocument, useRecovery); da07b42 adiciona useProfile com updateAddress, updatePhone, updateProfile, storeDocument, updateMarketing e lookupPostalCode; 0afc73a adiciona useWithdrawReceipt.
Os adapters HTTP default vêm embutidos (a492634): como todos os forks do base herdam as rotas de proxy /api/wallet/refresh|transactions|action, /api/auth/*, /api/user/* e /api/address/* com o envelope { ok, data, error }, a lib passou a falar esse protocolo por padrão — o consumidor só chama useAccountsInit({ locale, currency }). 2816079 desacopla initAccounts do client concreto (recebe refetchWallet em vez de walletService), 0a89fe9 alinha os adapters de auth ao envelope do base, e 494c42c documenta por que o /react usa fetch cru em vez do ApiClient.
useTransactions devolve transação pré-formatada (b5ae424): novo FormattedTransaction = Transaction & { amountFormatted: string }, com o amount cru preservado para a lógica de sinal. Consumidor não importa formatador nem recebe um por prop.
Do lado do base, a consequência foi apagar código:551fa1470 remove os fetches manuais, a76c112c1 migra as cinco seções de edição de perfil (AddressSection, PhoneSection, DocumentSection, InfosSection, MarketingSection) para useProfile — adicionando apenas a rota /api/user/update-marketing, porque as demais reaproveitam proxies existentes — e deleta app/store/user.ts. d58617465 conclui a migração de auth, f161de1ab corrige o import de useAccountsStore na PixSection e 886e81f58 fecha os erros de tipo em TransactionCard e BonusWalletCard.
Rollover ficou plano.6c2b9c8 promove os campos de firstTransaction do rollover para o nível de cima e 9ca9cf5 adiciona lookupByPostalCode e maxBonusWithdrawAmount no topo; 10c9d48, 2b5f002 e e0ca444 simplificam a transformação de wallet em cima disso. e2f2f61b6 ajusta o BonusWalletCard para o novo shape.
Semana 06–12/04 — Onboarding de depósito: banner de bônus, auto-abertura e prefill
FirstDepositBonusBanner entra no DepositModal (94fe5c7b7), visível enquanto o formulário de depósito está ativo, para expor a oferta de primeiro depósito no exato momento da decisão. 62ff8e4a1 ajusta a margem e renomeia o maxLabel não consumido para _maxLabel, mantendo o Biome quieto sem apagar o campo.
O modal de depósito volta a abrir sozinho para quem tem saldo baixo (bf390c85e), replicando o useOpenModalAfterTime do front Vue/Nuxt legado: abre após login ou registro quando o saldo é ≤ R$ 0,99, pula contas restritas (autoexclusão, somente-saque), respeita cooldown de 12 h gravado em localStorage, espera 1 s para que modais de validação e bloqueio tenham prioridade, e limpa a chave no logout.
?value= na URL pré-preenche o valor do depósito (c34c93d93), fechando o ciclo de campanhas que mandam o usuário direto para um valor sugerido. O namespace de i18n de pagamentos ganha as mensagens de bônus de primeiro depósito nos três idiomas (core f31c309).
Novos componentes de Stories implementados — StoriesCircles, StoriesModal, StoriesOnlyButton e StoriesWithModal para exibição interativa de banners
Autoplay adicionado ao HomeBannerCarousel com remoção do skeleton loading
Controle de visibilidade por linha (visibility control) adicionado ao sistema de home rows com classes responsivas
Correções de estabilidade no StoriesModal: safeClose para fechamento seguro com overlay invisível, reset de progresso e timing corretos, ajuste de padding e role de acessibilidade
Widgets configuráveis: HomeBannerCarousel, TournamentsSection e demais widgets agora aceitam title, icon e i18nKey como props para maior flexibilidade