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.
Auth — classificação de logout 401 e bridge de beacon
Banner SessionExpiredBanner com código de referência #EP0120-EM0003. Após um 401 que força logout, o login modal abre por cima da página atual com o banner "Sua sessão expirou" e um código mono-spaced copiável. O email vem prefilled com o último login conhecido. Em rotas protegidas (/user/*, /vip/*) a saída acontece via SPA navigation, sem full-page reload. 15 novos error codes (EM0001–EM0014) com matchers derivados do BFF Laravel real (wrong_ha, wrong_hf1, wrrong_hf2 typo preservado, wrong_token, ip_changed, device_changed, location_changed, liveness_required, deprecated_version, user_blocked, security_error).
Beacon estruturado /api/logs/auth-logout agora pousa de fato no Cloudflare Observability. O navigator.sendBeacon() só aceita content-types CORS-safelisted (application/x-www-form-urlencoded, multipart/form-data, text/plain) — passar Blob({ type: "application/json" }) fazia Chrome/Firefox/Safari rejeitarem silenciosamente, perdendo 100% dos payloads em prod. Fix em duas pontas: client troca pra text/plain;charset=UTF-8 e captura o boolean retornado pelo sendBeacon (caindo pra fetch({ keepalive: true }) quando false); server promove o log principal de log.warn → log.error (sobrevive aos filtros default do CF) e adiciona log.warn estruturado em todos os early-returns.
Refresh-token retry opcional + recheck-spa.createApiClient({ enableRefreshRetry: true, refreshTokenFn }) agora coalesce um refresh único antes do logout — wired nos 5 *.client.ts services com proxy /api/auth/refresh e timeout de 5s. RestrictedModeAlert ganha botão "Verificar meus limites" pra usuários em spa_exclusion, backed pelo novo /api/auth/recheck-spa com cookie rate-limit server-side de 30s (paridade com AlertOnlyWithdraw.verifyLimits do legado Vue). Interceptor global window.fetch 401 agora whitelista same-origin /api/* + origem do BFF — 401s de terceiros (WordPress, Connect, ad tech) não disparam mais logout.
@cactus-agents/accounts 2.2.0 para de mandar ?check_spa_again=1 em todo /auth/user-profile (era um SIGAP lookup síncrono em todo SSR loader + revalidation + profile sync). Agora é opt-in via { checkSpaAgain: true }. Bumps complementares: @cactus-agents/api-client 0.13.1 (registries de erro + refresh retry) e @cactus-agents/i18n 0.86.2 (15 chaves novas em auth:session_expired_banner.* / auth:session_error.* + 5 em user:protection.recheck_spa_* nos 4 locales).
Envelope {success:false} do BFF agora vira erro de verdade. O proxy de confirmReset, sendEmail, sendSms e validateCode tratava qualquer 2xx como sucesso, então a UI renderizava "Senha atualizada com sucesso" mesmo quando o BFF rejeitava semanticamente (ex: KYC com kyc_id ausente/inválido no fluxo de recuperação por KYC). Agora o proxy detecta o envelope de falha e converte para HTTP 400 + mensagem de erro, preserva o cookie recovery_session em falha de confirmReset (usuário pode retentar sem refazer o KYC), e pula a persistência do cookie de código em falha de validateCode.
Typo source: "recovery_password" → "password_recovery". O fluxo de recovery por KYC mandava source=recovery_password para /bff/users/kyc/recovery — ordem invertida vs todas as brands do legado Nuxt (source=password_recovery). O BFF aceitava o KYC mas nunca associava o correlation_id ao reset, fazendo o /auth/passwords/reset/confirm subsequente falhar com o envelope genérico. Adicionado também o gate em saveKycId pra omitir kyc_id quando vindo de password_recovery_attempt_limit (paridade com ResetForm.vue legado). Typo introduzido em bb2859bc0 (Mar/25/2026, bootstrap do novo fluxo) e nunca pego porque RecoverKycStep não tinha testes.
Step options nunca mais auto-avança. O useEffect que disparava setStep(options[0].step) quando havia só um método ativo fazia o botão "Voltar" parecer quebrado: Betpontobet (só KYC) voltava pra KYC ao cancelar; Vera (só email) voltava pro código ao apertar "Voltar". Removido o auto-advance — usuário sempre vê a tela de opções e clica, mesmo com um único método. 5 testes novos cobrindo as 4 permutações de método ativo. Comentário do prefixo t2: do Turnstile atualizado: o hardcode é intencional (plataforma tem um único sitekey always-show provisionado), não TODO.
Cache topGames reaproveitado para o fetch de high-payers. O refactor 6867beda renomeou o resource de topGames para highPayers mas o nome novo nunca foi adicionado ao defaults.yml do front-ops. Resultado: todo SSR de home/casino fazia bypass total do cache (sem TTL, SWR ou single-flight coalescing) e batia direto no BFF /bff/games/game-high-payers-dl, sobrecarregando upstream e o database de stats. Voltar a usar topGames (que já tem 1h TTL + KV snapshot + post-deploy purge declarados no defaults) restaura o cache imediatamente sem mudança coordenada no front-ops.
AppsFlyer TWA bridge restaurada para a 7k. Após a virada SSG → SSR, eventos AppsFlyer pararam de chegar pra 7k (Vera e Cassino sobreviveram por terem mais tráfego de mobile browser; 7k é praticamente 100% TWA). O legado useAppsFlyerS2S tinha dois paths: bridge nativa via window.twa.getPostMessageService() quando APK >= postMessageMinVersion (path dominante em prod) e HTTP S2S como fallback. A primeira porta do hook só implementou o HTTP path — e ainda com gate invertido. Solução: dispatcher dual-path com AppsFlyerConfig.postMessageMinVersion?: string novo. TWA + APK no threshold + bridge presente → postMessage; tudo o resto → HTTP S2S. Thresholds legados respeitados: 7k 3.0.0, Cassino 4.0.20, Vera 5.0.0.
Cassino.bet.br — Pendo habilitado com tracking manual. Override de brand novo ativando o Pendo (token + accountId: "cassino-bet-br", paridade com o runtime config do legado front-web-cassino-bet-br). Hook usePendo estendido pra emitir manualmente os eventos que o agente no-code não consegue observar: app_loaded (uma vez por document load — primeira visita, F5, navegação externa), login (transição isAuthenticated false → true) e logout (true → false). Re-identifica o visitante nas transições via pendo.identify() (fallback para initialize), espelhando stores/auth.ts do legado Nuxt. visitor.id é o user id da plataforma Cactus (AuthUser.id do @cactus-agents/accounts).
Refactor de config por brand:brand.yml agora declara só identidade (repo: <owner>/<name>, singular — cada brand é dona de um repo) e environments/<env>/deploy.yml vira a source of truth para worker_name, cf_account, default_branch, language, origin_domain, worker_url e cf_zone. repos.yml removido das duas brands. Contrato do reusable workflow inalterado (environment + ref), então callers em outras branches continuam funcionando.
Referência EP/EM de códigos de erro adicionada ao docs-internal. Nova doc docs-internal/endpoints/error-codes.md lista todos os EP (endpoint) e EM (error kind) que o front emite ao classificar 401/403/429/5xx forçando logout — superfície compartilhada com o time de back-end pra triagem de tickets "fui deslogado com #EP0120-EM0003". 13 tabelas de EP por domínio (Auth, User, Wallet, Payments, KYC, Games, Sports, Brand, internal /api/*, Unknown) extraídas verbatim de packages/api-client/src/errors/endpoint-codes.ts; 5 tabelas de EM por status (401 com 13 matchers específicos, 403, 409/429, 5xx, transport, unknown) com cross-reference pro BFF Laravel (User.php, CheckBlocked.php, CheckUserLoggedIn.php). Inclui playbook de triagem com 2 exemplos end-to-end, checklist pra adicionar códigos novos, e schema do beacon estruturado que app/routes/api/logs/auth-logout.ts escreve no CF Workers Observability.