Varredura de "afirmações de estado": toda frase que afirma como o mundo ESTÁ e
que portanto pode ter envelhecido. 393 afirmações medidas contra a fonte em 11
grupos de documento, cada uma com o comando que a responde. 166 confirmadas, 227
com problema, 4 vereditos VERDADEIRA derrubados por um passe adversarial.
E a primeira coisa que a varredura derrubou foi uma frase que EU escrevi hoje.
## O pior achado, e é meu
O runbook dizia "**Desde a 1.3.0**, o agente completa o pin sozinho — em até 5
minutos". Falso. O `completar_pin_ausente` entrou em `81b3bd5d` (2026-08-14),
POSTERIOR à v1.3.0 (2026-08-13), e a v1.3.0 continua sendo a tag mais recente:
$ git show v1.3.0:hostgator-setup-kit/_common.sh | grep -c completar_pin_ausente
0
Nenhuma instalação existente tem esse comportamento. E o `update.sh` faz
`git checkout "$TARGET_TAG"` — o kit que roda na VPS é o da TAG, não o da `main`.
Quem atendesse um cliente lendo aquela linha diria "espere cinco minutos que se
resolve", e nada aconteceria, para sempre.
O erro é exatamente o que esta sessão passou o dia caçando: **provei presença na
main e afirmei comportamento na versão publicada.** A mesma frase estava no
CHANGELOG, e pior — dentro da seção `## [1.3.0]`, não em `[Não lançado]`. As duas
corrigidas, e a entrada foi para onde pertence.
## O segundo: o aviso que declarava não-provado o que o documento prova
O topo do runbook dizia "ainda não coberto: app contra Supabase real, sessão de
WhatsApp pareada". O §P4 do MESMO arquivo, 340 linhas abaixo, documenta as duas
coisas na produção. Faltava uma palavra — "pelo ENSAIO" — e sem ela o primeiro
parágrafo que qualquer leitor vê mandava refazer trabalho já feito.
## Três documentos mandavam o leitor agir errado
- **`triagem/TRIAGEM.md`**: "Obrigatórios no merge: verify, build-and-size,
invariants" — três de cinco, faltando `e2e` e `imagens-ok`. É o arquivo que
DEFINE a triagem, e o CLAUDE.md registra que medir contra a régua errada é o
modo de falha número um dela. Agora traz o comando, não a lista.
- **`docs/deploy-hostgator/README.md`**: mandava o comprador leigo instalar um
autenticador e esperar um QR de MFA "no primeiro login". Com o MFA opcional, o
`bootstrap-owner.ts` grava `mfa_required: false` e essa tela não aparece — o
leigo concluiria que a instalação falhou. É o guia P0 de primeira impressão.
- **`ARCHITECTURE.md`**: "MFA TOTP forçado pra admin/super-admin", a regra antiga.
O CLAUDE.md já tinha a nova; a porta de entrada linkada pelo README, não.
## A régua de RAM: duas parcelas medidas, uma herdada
"~150 MB por número de WhatsApp" aparece em SETE documentos que se citam entre si
e **nunca foi medido neste projeto** — vem da síntese do curso WAHA (2026-05). Fui
medir na produção: o contêiner `waha` inteiro em **304,5 MiB com uma sessão
pareada**, contra `mem_limit` de 1280. Um ponto não decompõe baseline e sessão.
A parcela agora está marcada como herdada, com o comando para quem quiser medir.
**O tier recomendado não muda** — a régua dos 4 GB é a soma da stack em operação,
não o WAHA isolado, e os materiais comerciais não foram tocados.
## Segurança: corrigi o fato, não rebaixei o risco
O `docs/threat-model.md` afirma que não há limite de tentativa em login, signup e
aceite de convite. `lib/auth/rate-limit.ts` existe desde 13/08 e cobre os quatro
pontos, com limites nomeados. **Não rebaixei o T1**: isso pede reauditoria com
teste contra instância viva, que o próprio `confidence` do documento diz nunca ter
havido. Corrigi os fatos e marquei a reauditoria como devida.
Uma sub-afirmação sobrevive à letra e morre no espírito, e ficou registrada: a
sonda do doc (`grep lockout|failed_attempts`) devolve ZERO ainda hoje — mas
`rate-limit.ts:158` conta falha de login POR CONTA. A sonda é cega para a defesa
que existe.
## O gate novo, e ele nasceu vermelho pelo motivo certo
`documentacao-aponta-para-o-que-existe.test.ts` vigia a classe inteira nos 36
documentos de AUTORIDADE: link relativo morto, path em crase que não existe, e
frase de pendência que sobreviveu à pendência.
Escopo deliberado: `docs/stories/`, `docs/handoffs/` e `docs/specs/` ficam de fora
— são planejamento, e incluí-los faria o gate nascer com 384 violações e ser
desligado na primeira semana. Nos 36 de autoridade havia UMA, e ela é honesta (o
runbook cita o script e declara que ele não existe): congelada com justificativa.
Ele nasceu vermelho e estava certo — pegou sozinho duas notas de pendência que a
varredura tinha achado e eu ainda não corrigira, em CONTRIBUTING.md e na doutrina.
Quatro sabotagens, cada uma derrubando exatamente 1 dos 4 — inclusive a do escopo
vazio, porque um gate que varre zero arquivo passa verde. Sabotei DEPOIS de deixar
o controle verde: na primeira tentativa o controle já falhava, e as sabotagens não
provavam nada. E `git add -N` antes, porque `git checkout` não restaura arquivo
que o git ainda não conhece — a sabotagem do escopo ficou aplicada e só apareceu
no run seguinte.
## O que NÃO entra aqui
227 achados; apliquei os de gravidade alta cuja consequência é alguém agir errado.
Os de gravidade média e baixa — sobretudo contagens que envelheceram em
`docs/current-state.md` e `docs/harness-audit.md` — ficam listados no relatório,
não corrigidos. Aplicar 227 correções de texto num commit seria trocar prosa velha
por prosa não verificada.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01RKm2XcTcfgi1vdMcDYSRWa
7.4 KiB
Architecture — DeskcommCRM
Visão de 1 página. Profundidade vive em
docs/specs/edocs/stories/epics/MASTER.md. Mapa de toda a documentação:docs/index.md. Estado real de implementação (o que está pronto vs. incompleto):docs/current-state.md.
Camadas
- App (Next.js 16 App Router): UI + Route Handlers no mesmo repo. Server Components por default, Client onde precisa de estado. Middleware de borda em
proxy.ts(Next 16 renomeoumiddleware.ts→proxy.ts). - DB (Supabase Postgres): RLS em toda tabela tenant-aware via
fn_user_org_ids(). Migrations versionadas emsupabase/migrations/. - Auth (Supabase Auth +
@supabase/ssr): cookie SameSite=Strict. SempregetUser()no server, nuncagetSession(). MFA TOTP é opcional e ligado por quem administra — duas políticas independentes que somam (platform_admins.mfa_requiredeorganizations.settings.security.mfa_required), ambas com padrão não exigir; regra pura emlib/auth/politica-mfa.ts. Esta linha dizia "forçado pra admin/super-admin", que era a regra antiga: como oinstall.shcria o dono como platform admin, toda instalação self-host recebia um bloqueador de tela cheia logo após o onboarding. Cadastrar e provar são coisas diferentes — quem TEM fator prova na sessão sempre, independente da política. - Realtime (Supabase Realtime):
postgres_changespara inbox/kanban;broadcastpara sinais leves. - Storage (Supabase Storage): bucket
whatsapp-mediaprivado, URLs assinadas. - WhatsApp (WAHA Plus / engine NOWEB): HMAC-SHA512 webhooks; throttle anti-banimento; STOP detection.
- Filas (event sourcing leve):
event_logtable + workers via cron. Trigger Postgres NUNCA faz HTTP. - Rate limit (Upstash Redis): contador de janela fixa (
INCR+EXPIRE) emlib/ai/dispatcher/rate-limit.ts, com fallback in-memory quando Redis falta. ⚠️ Aplicado hoje em apenas 2 pontos (webhook de captação e dispatcher de IA) — o surface público de auth está sem. Verdocs/threat-model.md§T1. - AI (Vercel AI Gateway): Anthropic primário, OpenAI backup pra embeddings.
- Observability (Sentry):
beforeSendscrubs PII (CPF/email/phone) e headers sensíveis.
Multi-tenancy
organization_id uuid not null em toda tabela tenant-aware. RLS via helper. Service role bypassa RLS — handlers admin DEVEM filtrar organization_id manualmente, resolvido de fonte confiável (cookie/JWT/webhook secret/path token), nunca do body.
Detalhes: docs/specs/01-spec-platform-base.md.
API REST /api/v1/
- JSON snake_case. UUID v4. ISO-8601 UTC. Dinheiro
_cents+currency. - Wrappers
ok()/fail()emlib/api/wrappers.ts. - Auth dual: cookie session (frontend) ou
Authorization: Bearer tok_...(server-to-server). X-Request-Idem toda response, injetado emproxy.tse correlacionado com o audit log.Idempotency-Keyé o contrato pretendido para POSTs de criação; implementado hoje em 1 rota (lgpd/requests/[id]/approve). Verdocs/current-state.md§4.- Detalhes:
docs/specs/01-spec-platform-base.md§API.
Fluxo de uma requisição
Rota autenticada de tenant (/api/v1/*, 166 handlers):
request → proxy.ts (X-Request-Id, x-pathname; isPublicPath? → bypass;
senão valida sessão Supabase via cookie sb-deskcomm-auth)
→ route handler:
1. Zod valida o input externo
2. guard: requireRole() | requirePlatformAdmin() | secret/HMAC
3. resolveActiveOrg() → organization_id de fonte confiável (nunca do body)
4. query (RLS pelo client de sessão, ou filtro manual de org com service role)
5. audit() fire-and-forget se houve mutação
6. ok(data, meta) | fail(code, message, status)
Superfícies não-cookie: /api/v1/cron/* (Bearer INTERNAL_CRON_SECRET, fail-closed),
/api/internal/* (x-internal-secret), /api/mcp (Bearer tok_... contra api_tokens),
/api/v1/webhooks/* (HMAC + path token). Inventário completo em
docs/threat-model.md §1.
Turno do agente de IA: inbound WhatsApp → HMAC + idempotência → event_log →
worker → runAgentTurn (RAG + tools MCP) → guardrails before-send → adapter WAHA →
handoff humano se gatilho. Diagrama: docs/architecture/agent-turn.html.
Event log + workers
Triggers Postgres emitem linhas em event_log. Workers (cron / Realtime listener) consomem e disparam side effects. Idempotência via unique (organization_id, external_id) + captura code === '23505'.
Workers vivem em workers/ (ai-response, ai-sentiment, rag-indexer, media-persist,
media-derive, lgpd-export, lgpd-redact, storage-cleanup, agent-worker), drenados
pelos 10 endpoints em app/api/v1/cron/. Contrato: docs/specs/07-spec-events-workers.md.
Integrações externas
| Serviço | Uso | Onde | Falta ⇒ |
|---|---|---|---|
| Supabase | Postgres + Auth + Realtime + Storage | lib/supabase/{browser,server,admin}.ts |
app não sobe (obrigatório sempre) |
| WAHA Plus (NOWEB) | WhatsApp: envio, recebimento, sessões multi-número | lib/waha/ |
canal indisponível; obrigatório em produção |
| Upstash Redis | rate limit + debounce de RAG | lib/ai/dispatcher/rate-limit.ts, lib/ai/rag/debounce.ts |
degrada para memória com warn |
| Vercel AI Gateway | LLM + embeddings (@ai-sdk/anthropic|openai|google) |
lib/ai/ |
agente não responde |
| Nuvemshop | e-commerce: pedidos, produtos, webhooks LGPD | lib/nuvemshop/ |
opcional (NUVEMSHOP_ENABLED) |
| Sentry | erros + performance, beforeSend higieniza PII |
sentry.*.config.ts, instrumentation*.ts |
opcional |
| Resend | e-mail transacional (convite de time) | lib/email/ |
opcional — o convite cai em copy-to-clipboard |
| MCP | CRM exposto como tools para agentes | app/api/mcp/, lib/mcp/ |
— |
Hardening
- Error boundaries em
app/error.tsx,app/app/error.tsx,app/(public)/error.tsx,app/global-error.tsx(Sentry capture + eventId visível). - Páginas customizadas 404/403/500/503 com copy PT-BR canônica.
- Loading skeletons em rotas P0.
- E2E Playwright + axe-core.
- Detalhes:
docs/stories/epics/EPIC-12-hardening.md.
Onde olhar a fundo
docs/prd/— PRDs (visão, escopo MVP, KPIs, plataforma base, customer 360, WhatsApp, pipeline, IA-RAG, Nuvemshop).docs/specs/— specs técnicas com schema SQL e payloads.docs/business-rules/— regras de negócio fora do código.docs/stories/epics/MASTER.md— plano de execução por epic/wave.CLAUDE.md— convenções não-negociáveis (multi-tenancy, idempotência, RBAC, LGPD, WAHA, anti-patterns).AGENTS.md— contrato portável para agentes de código (qualquer ferramenta).docs/index.md— índice de toda a documentação.docs/harness-audit.md— maturidade do harness e lacunas de verificação.docs/threat-model.md— superfície de ataque do self-host.