diff --git a/docs/P0_IMPLEMENTATION_GUIDE.md b/docs/P0_IMPLEMENTATION_GUIDE.md new file mode 100644 index 000000000..7499bb7e2 --- /dev/null +++ b/docs/P0_IMPLEMENTATION_GUIDE.md @@ -0,0 +1,480 @@ +# Guia de Implementação P0 — ZAPP WEB + +Este guia traz as alterações exatas a serem aplicadas nos arquivos críticos para fechar os bloqueadores de produção. + +--- + +## 1. `supabase/config.toml` + +Substituir os trechos abaixo para exigir JWT nas funções sensíveis. + +```toml +[functions.ai-conversation-summary] +verify_jwt = true + +[functions.ai-conversation-analysis] +verify_jwt = true + +[functions.ai-suggest-reply] +verify_jwt = true + +[functions.ai-transcribe-audio] +verify_jwt = true + +[functions.sentiment-alert] +verify_jwt = true + +[functions.evolution-api] +verify_jwt = true + +[functions.bitrix-api] +verify_jwt = true + +[functions.evolution-sync] +verify_jwt = true + +[functions.ai-enhance-message] +verify_jwt = true + +[functions.classify-audio-meme] +verify_jwt = true + +[functions.classify-emoji] +verify_jwt = true + +[functions.classify-sticker] +verify_jwt = true + +[functions.elevenlabs-tts] +verify_jwt = true + +[functions.elevenlabs-scribe-token] +verify_jwt = true + +[functions.elevenlabs-tts-stream] +verify_jwt = true + +[functions.elevenlabs-dialogue] +verify_jwt = true + +[functions.elevenlabs-voice-design] +verify_jwt = true + +[functions.elevenlabs-sts] +verify_jwt = true + +[functions.elevenlabs-sfx] +verify_jwt = true +``` + +Manter sem JWT apenas webhooks públicos reais ou jobs controlados externamente. + +--- + +## 2. `supabase/functions/whatsapp-webhook/index.ts` + +### Adicionar utilitário antes do `serve` + +```ts +async function verifyWhatsAppSignature( + rawBody: string, + signatureHeader: string | null, + appSecret: string, +): Promise { + if (!signatureHeader?.startsWith('sha256=')) return false; + + const encoder = new TextEncoder(); + const key = await crypto.subtle.importKey( + 'raw', + encoder.encode(appSecret), + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['sign'], + ); + + const digest = await crypto.subtle.sign('HMAC', key, encoder.encode(rawBody)); + const expected = Array.from(new Uint8Array(digest)) + .map((b) => b.toString(16).padStart(2, '0')) + .join(''); + + const provided = signatureHeader.replace('sha256=', ''); + if (provided.length !== expected.length) return false; + + let mismatch = 0; + for (let i = 0; i < provided.length; i++) { + mismatch |= provided.charCodeAt(i) ^ expected.charCodeAt(i); + } + return mismatch === 0; +} +``` + +### No bloco `POST`, trocar o parse direto por: + +```ts +const rawBody = await req.text(); +const appSecret = Deno.env.get('WHATSAPP_APP_SECRET'); + +if (appSecret) { + const valid = await verifyWhatsAppSignature( + rawBody, + req.headers.get('x-hub-signature-256'), + appSecret, + ); + + if (!valid) { + log.warn('Invalid WhatsApp webhook signature'); + return new Response('Forbidden', { status: 403, headers: getCorsHeaders(req) }); + } +} else { + log.warn('WHATSAPP_APP_SECRET not configured; POST signature verification skipped'); +} + +const rawPayload = JSON.parse(rawBody); +const parsed = WhatsAppWebhookSchema.safeParse(rawPayload); +``` + +--- + +## 3. `supabase/functions/evolution-webhook/index.ts` + +### Ajustar imports +Trocar: +```ts +import { Logger } from "../_shared/validation.ts"; +``` +por: +```ts +import { Logger, getCorsHeaders } from "../_shared/validation.ts"; +``` + +### Remover `corsHeaders` local com `*` +Não usar mais o objeto local de CORS. Sempre usar `getCorsHeaders(req)`. + +### Adicionar autenticação do emissor +Adicionar antes do `serve`: + +```ts +function isAuthorizedEvolutionWebhook(req: Request, payload: WebhookPayload): boolean { + const expected = Deno.env.get('EVOLUTION_WEBHOOK_SECRET') || Deno.env.get('EVOLUTION_API_KEY'); + if (!expected) return false; + + const headerKey = req.headers.get('x-evolution-apikey') || req.headers.get('apikey'); + return headerKey === expected || payload.apikey === expected; +} +``` + +### No início do handler +Trocar por: + +```ts +serve(async (req) => { + if (req.method === 'OPTIONS') { + return new Response(null, { headers: getCorsHeaders(req) }); + } + + if (req.method !== 'POST') { + return new Response('Method not allowed', { status: 405, headers: getCorsHeaders(req) }); + } + + try { + const supabaseUrl = Deno.env.get('SUPABASE_URL')!; + const supabaseServiceKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!; + const supabase = createClient(supabaseUrl, supabaseServiceKey); + + const payload: WebhookPayload = await req.json(); + + if (!isAuthorizedEvolutionWebhook(req, payload)) { + return new Response(JSON.stringify({ error: 'Unauthorized webhook sender' }), { + status: 403, + headers: { ...getCorsHeaders(req), 'Content-Type': 'application/json' }, + }); + } +``` + +### Todas as respostas JSON devem usar `getCorsHeaders(req)` +Exemplo: + +```ts +return new Response(JSON.stringify({ success: true }), { + status: 200, + headers: { ...getCorsHeaders(req), 'Content-Type': 'application/json' }, +}); +``` + +--- + +## 4. `supabase/functions/evolution-api/index.ts` + +### Ajustar imports +Trocar: +```ts +import { Logger } from "../_shared/validation.ts"; +``` +por: +```ts +import { Logger, getCorsHeaders } from "../_shared/validation.ts"; +``` + +### Remover `corsHeaders` local com `*` +Não usar mais `Access-Control-Allow-Origin: '*'`. + +### Substituir preflight +```ts +if (req.method === 'OPTIONS') { + return new Response('ok', { headers: getCorsHeaders(req) }); +} +``` + +### Adicionar controle de ações administrativas +Adicionar próximo da resolução de `action`: + +```ts +const adminOnlyActions = new Set([ + 'create-instance', + 'delete-instance', + 'disconnect', + 'restart-instance', + 'set-webhook', + 'set-chatwoot', + 'set-typebot', + 'set-openai', + 'set-dify', + 'set-flowise', + 'set-evolution-bot', + 'set-rabbitmq', + 'set-sqs', + 'set-proxy', + 'set-evoai', + 'set-n8n', + 'set-kafka', + 'set-nats', + 'set-pusher', +]); +``` + +### Validar usuário autenticado e papel +Adicionar antes do `try` principal de roteamento: + +```ts +const authHeader = req.headers.get('authorization'); +if (!authHeader) { + return new Response(JSON.stringify({ error: 'Missing authorization' }), { + status: 401, + headers: { ...getCorsHeaders(req), 'Content-Type': 'application/json' }, + }); +} + +const anonKey = Deno.env.get('SUPABASE_ANON_KEY'); +if (!anonKey) { + return new Response(JSON.stringify({ error: 'SUPABASE_ANON_KEY not configured' }), { + status: 500, + headers: { ...getCorsHeaders(req), 'Content-Type': 'application/json' }, + }); +} + +const authClient = createClient(supabaseUrl, anonKey, { + global: { headers: { Authorization: authHeader } }, +}); + +const { data: authData } = await authClient.auth.getUser(); +const userId = authData.user?.id; +if (!userId) { + return new Response(JSON.stringify({ error: 'Unauthorized' }), { + status: 401, + headers: { ...getCorsHeaders(req), 'Content-Type': 'application/json' }, + }); +} + +if (adminOnlyActions.has(String(action))) { + const { data: profile } = await supabase + .from('profiles') + .select('role') + .eq('user_id', userId) + .maybeSingle(); + + if (!profile || !['admin', 'supervisor'].includes(String(profile.role))) { + return new Response(JSON.stringify({ error: 'Forbidden' }), { + status: 403, + headers: { ...getCorsHeaders(req), 'Content-Type': 'application/json' }, + }); + } +} +``` + +### Todas as respostas devem usar `getCorsHeaders(req)` +Aplicar em todo o arquivo. + +--- + +## 5. `src/hooks/useRealtimeMessages.ts` + +### Objetivo +Eliminar fallback perigoso de conexão errada em `sendMessage`. + +### Remover este comportamento +Não buscar mais “a última conexão conectada” quando a conexão do contato estiver desconectada. + +### Substituir pela lógica abaixo +```ts +const { data: contact } = await supabase + .from('contacts') + .select('phone, whatsapp_connection_id') + .eq('id', contactId) + .single(); + +if (!contact?.whatsapp_connection_id) { + await supabase.from('messages').update({ status: 'failed' }).eq('id', data.id); + throw new Error('Contato sem conexão WhatsApp vinculada'); +} + +const { data: linkedConnection } = await supabase + .from('whatsapp_connections') + .select('id, instance_id, status') + .eq('id', contact.whatsapp_connection_id) + .single(); + +if (!linkedConnection?.instance_id || linkedConnection.status !== 'connected') { + await supabase + .from('messages') + .update({ status: 'failed', whatsapp_connection_id: contact.whatsapp_connection_id }) + .eq('id', data.id); + + throw new Error('A conexão vinculada ao contato não está ativa'); +} + +const resolvedConnectionId = linkedConnection.id; +const connection = { + instance_id: linkedConnection.instance_id, + status: linkedConnection.status, +}; +``` + +--- + +## 6. `src/hooks/useMessages.ts` + +### Objetivo +Evitar corrida em troca rápida de contato. + +### Adicionar ref +```ts +const requestIdRef = useRef(0); +``` + +### No começo de `fetchMessages` +```ts +const requestId = ++requestIdRef.current; +``` + +### Antes de qualquer `setMessages`, `setError` e `setLoading` +Usar guard: + +```ts +if (!mountedRef.current || requestId !== requestIdRef.current) return; +``` + +### Exemplo de bloco final correto +```ts +if (mountedRef.current && requestId === requestIdRef.current) { + setMessages(mappedMessages); +} +``` + +```ts +if (mountedRef.current && requestId === requestIdRef.current) { + setError(err instanceof Error ? err.message : 'Failed to fetch messages'); +} +``` + +```ts +if (mountedRef.current && requestId === requestIdRef.current) { + setLoading(false); +} +``` + +--- + +## 7. `src/components/inbox/ConversationTasksPanel.tsx` + +### Objetivo +Bloquear criação sem ownership e tratar erro corretamente. + +### Padrão recomendado +```ts +if (!profileId) { + toast.error('Perfil ainda não carregado. Tente novamente em alguns segundos.'); + return; +} + +try { + setAdding(true); + const { error } = await supabase.from('conversation_tasks').insert({ + contact_id: contactId, + title: newTitle.trim(), + priority, + status: 'pending', + created_by: profileId, + assigned_to: profileId, + }); + + if (error) throw error; + setNewTitle(''); + await loadTasks(); +} catch (err) { + toast.error('Erro ao salvar tarefa.'); +} finally { + setAdding(false); +} +``` + +### Inputs e botão +Desabilitar quando `!profileId || adding`. + +--- + +## 8. `src/components/inbox/RemindersPanel.tsx` + +Aplicar o mesmo padrão de robustez do painel de tarefas. + +### Padrão recomendado +```ts +if (!profileId) { + toast.error('Perfil ainda não carregado. Tente novamente em alguns segundos.'); + return; +} + +try { + setSaving(true); + // insert/update reminder + await loadReminders(); +} catch (err) { + toast.error('Erro ao salvar lembrete.'); +} finally { + setSaving(false); +} +``` + +--- + +## 9. `package.json` + +### Adicionar scripts de teste +```json +{ + "scripts": { + "test": "vitest run", + "test:watch": "vitest", + "test:ui": "vitest --ui" + } +} +``` + +--- + +## 10. Evidência final necessária +Depois das alterações: +1. rodar lint; +2. rodar build; +3. validar webhook legítimo e webhook inválido; +4. validar envio com conexão correta e erro com conexão errada; +5. validar troca rápida de conversa sem sobrescrita stale. diff --git a/docs/PROD_HARDENING_TEST_PLAN.md b/docs/PROD_HARDENING_TEST_PLAN.md new file mode 100644 index 000000000..a0af0b651 --- /dev/null +++ b/docs/PROD_HARDENING_TEST_PLAN.md @@ -0,0 +1,298 @@ +# Plano de Testes e Validação Operacional — ZAPP WEB + +## Objetivo +Este documento organiza a rodada de validação pré-produção do ZAPP WEB com foco em: +- segurança do perímetro externo; +- integridade do fluxo de atendimento; +- estabilidade do inbox em alto volume; +- confiabilidade de webhooks e integrações; +- regressão funcional dos módulos operacionais; +- critérios claros de go-live. + +--- + +## Critérios de Bloqueio (P0) + +### P0-1 — Edge Functions sensíveis expostas sem JWT +**Sinal encontrado** +Funções sensíveis estão configuradas com `verify_jwt = false`, incluindo `evolution-api`, `bitrix-api`, `ai-suggest-reply`, `ai-conversation-summary`, `ai-conversation-analysis`, `ai-transcribe-audio`, `evolution-sync` e outras. + +**Risco operacional** +- abuso de integrações pagas; +- uso indevido de service role por rotas públicas; +- execução não autenticada de ações administrativas. + +**Critério para aprovar** +- todas as funções sensíveis devem exigir JWT; +- apenas webhooks públicos reais permanecem sem JWT. + +### P0-2 — Webhook Evolution sem autenticação do emissor +**Sinal encontrado** +`evolution-webhook` processa eventos que alteram banco, mas não valida segredo/assinatura do emissor. + +**Risco operacional** +- eventos falsos injetados no banco; +- corrupção de mensagens, contatos, tags e chamadas. + +**Critério para aprovar** +- rejeitar POST sem segredo válido; +- registrar auditoria de rejeição. + +### P0-3 — WhatsApp Cloud webhook sem validação de assinatura no POST +**Sinal encontrado** +`whatsapp-webhook` valida challenge de inscrição, mas não valida `X-Hub-Signature-256`. + +**Risco operacional** +- POST forjado com eventos falsos. + +**Critério para aprovar** +- verificar HMAC SHA-256 com `WHATSAPP_APP_SECRET` antes de processar. + +### P0-4 — Risco de envio por conexão errada +**Sinal encontrado** +`sendMessage` cai para a última conexão conectada quando a conexão do contato não está ativa. + +**Risco operacional** +- mensagem enviada pelo número errado; +- dano comercial e reputacional. + +**Critério para aprovar** +- envio só pode ocorrer pela conexão do contato ou por fallback explicitamente permitido por regra de negócio; +- fallback genérico deve ser removido. + +--- + +## Critérios Importantes (P1) + +### P1-1 — Inbox global com limites rígidos +- `SEEDED_CONTACT_LIMIT = 500` +- `RECENT_MESSAGES_LIMIT = 1000` + +**Risco** +Perda de cobertura do inbox em alto volume. + +**Aprovação** +- paginação progressiva ou fetch incremental por janela de atividade; +- métricas de volume e fallback seguro. + +### P1-2 — Corrida em troca rápida de conversa +`useMessages` não cancela fetch anterior nem usa request token. + +**Risco** +Mensagens do contato anterior sobrescrevendo o atual. + +**Aprovação** +- guard por request id/abort; +- ignorar resposta stale. + +### P1-3 — Ausência de suíte de regressão operacional +`vitest` existe no projeto, mas não há script de teste nem suíte detectada. + +**Aprovação** +- script `test`; +- smoke tests unitários e de hooks críticos; +- checklist manual de produção. + +### P1-4 — Divergência entre inventário e realidade do projeto +O inventário funcional está defasado em relação ao número/configuração atual das functions e módulos. + +**Aprovação** +- alinhar documentação técnica e superfície real do projeto. + +--- + +## Matriz de Testes por Módulo + +## 1. Autenticação e Perfis +### Cenários +1. Login com credenciais válidas. +2. Login com senha inválida. +3. Sessão restaurada após refresh. +4. Usuário autenticado sem linha em `profiles`. +5. Logout e limpeza de estado. +6. RefreshProfile após alteração de perfil. + +### Resultado esperado +- sessão consistente; +- `profile` carregado ou erro claramente tratado; +- nenhum estado fantasma após logout. + +### Severidade +P2 + +--- + +## 2. Inbox / Lista de Conversas +### Cenários +1. Carregar inbox com 10, 100, 500, 2.000 contatos. +2. Ordenação por última mensagem. +3. Contagem de não lidas coerente. +4. Contato sem mensagens ainda visível. +5. Realtime de nova mensagem reposicionando conversa para o topo. +6. Contato com mensagens fora do recorte inicial. + +### Resultado esperado +- lista não perde cobertura em alto volume; +- conversa certa sobe para o topo; +- contadores permanecem corretos. + +### Severidade +P1 + +--- + +## 3. Mensagens / Detalhe da Conversa +### Cenários +1. Abrir contato com histórico pequeno. +2. Abrir contato com histórico longo (10k+ mensagens). +3. Trocar rapidamente A → B → C. +4. Receber mensagem enquanto conversa está aberta. +5. Atualização de status `sending → sent → delivered → read`. +6. Mensagem deletada no provedor refletida no banco/UI. +7. Mensagem com mídia e transcrição. + +### Resultado esperado +- nenhuma troca de contexto incorreta; +- realtime coerente; +- status nunca regride; +- histórico completo carregado. + +### Severidade +P1 + +--- + +## 4. Envio de Mensagens +### Cenários +1. Enviar texto simples. +2. Enviar imagem com legenda. +3. Enviar documento. +4. Enviar áudio. +5. Enviar localização. +6. Enviar sem conexão ativa. +7. Enviar quando contato possui conexão vinculada inativa. +8. Operação com múltiplas instâncias conectadas. + +### Resultado esperado +- envio usa a conexão correta; +- falhas deixam mensagem como `failed` com trilha clara; +- nenhum fallback silencioso para outra instância. + +### Severidade +P0 + +--- + +## 5. Tarefas Contextuais e Lembretes +### Cenários +1. Criar tarefa no detalhe do contato. +2. Criar lembrete no detalhe do contato. +3. Concluir tarefa. +4. Excluir tarefa. +5. Operador sem `profileId` ainda carregado tenta criar tarefa. +6. Realtime atualiza painel lateral após alteração. + +### Resultado esperado +- UX não permite criação sem ownership definido; +- erros aparecem ao usuário; +- estado do painel é consistente. + +### Severidade +P2/P1 + +--- + +## 6. Webhook Evolution +### Cenários +1. POST legítimo de `messages.upsert`. +2. POST sem segredo válido. +3. POST de `connection.update`. +4. POST de `messages.update` fora de ordem. +5. POST de `messages.delete` para mensagem inexistente. +6. POST com sticker, áudio, vídeo e documento. +7. POST com taxa alta para testar rate limit por instância. + +### Resultado esperado +- emissor inválido é rejeitado; +- eventos legítimos processam normalmente; +- status não degradam; +- placeholders são reconciliados. + +### Severidade +P0 + +--- + +## 7. WhatsApp Cloud Webhook +### Cenários +1. GET de challenge válido. +2. GET de challenge inválido. +3. POST com assinatura correta. +4. POST com assinatura inválida. +5. Status update para mensagem existente. +6. Status update para mensagem inexistente. + +### Resultado esperado +- GET e POST validados corretamente; +- POST inválido recebe 403; +- eventos válidos atualizam status. + +### Severidade +P0 + +--- + +## 8. Edge Functions de IA +### Cenários +1. Gerar sugestão de resposta com JWT válido. +2. Tentar chamar função sem JWT. +3. Exceder rate limit. +4. KB vazia. +5. Resposta do gateway sem JSON válido. +6. Transcrição de áudio válida. +7. Transcrição de áudio inválida/corrompida. + +### Resultado esperado +- funções exigem autenticação; +- fallback amigável em falhas de IA; +- rate limit funciona. + +### Severidade +P0/P1 + +--- + +## 9. Bitrix +### Cenários +1. Listar leads autenticado. +2. Criar lead autenticado. +3. Chamada sem JWT. +4. Erro de Bitrix retorna mensagem amigável. +5. Sync de contatos com duplicidade por telefone. + +### Resultado esperado +- ações administrativas só com autenticação; +- duplicidade tratada; +- erros externos controlados. + +### Severidade +P0/P1 + +--- + +## 10. Critérios de Go-Live +O sistema só pode entrar em produção quando: +1. todos os P0 estiverem corrigidos; +2. todos os cenários P0 tiverem evidência de teste aprovado; +3. houver suíte mínima de regressão para auth, inbox, sendMessage e webhooks; +4. documentação técnica estiver alinhada com a superfície atual do projeto; +5. fallback de conexão errada estiver eliminado. + +--- + +## Evidências esperadas +- PR com correções P0/P1; +- checklist assinado de smoke tests; +- logs de rejeição de webhooks inválidos; +- prints/registro de cenários aprovados; +- validação final em ambiente staging. diff --git a/docs/PROD_PATCHES_P0_P1.md b/docs/PROD_PATCHES_P0_P1.md new file mode 100644 index 000000000..a139b2cd1 --- /dev/null +++ b/docs/PROD_PATCHES_P0_P1.md @@ -0,0 +1,463 @@ +# Patches Técnicos Recomendados — P0 e P1 + +Este documento consolida as alterações prioritárias para eliminar os bloqueadores identificados na auditoria pré-produção. + +--- + +## Patch 1 — Endurecer `supabase/config.toml` + +### Objetivo +Fechar JWT em todas as Edge Functions sensíveis. + +### Alteração recomendada +```toml +project_id = "allrjhkpuscmgbsnmjlv" + +[functions.ai-conversation-summary] +verify_jwt = true + +[functions.ai-conversation-analysis] +verify_jwt = true + +[functions.ai-suggest-reply] +verify_jwt = true + +[functions.ai-transcribe-audio] +verify_jwt = true + +[functions.get-mapbox-token] +verify_jwt = false + +[functions.whatsapp-webhook] +verify_jwt = false + +[functions.sentiment-alert] +verify_jwt = true + +[functions.evolution-api] +verify_jwt = true + +[functions.evolution-webhook] +verify_jwt = false + +[functions.bitrix-api] +verify_jwt = true + +[functions.elevenlabs-tts] +verify_jwt = true + +[functions.elevenlabs-scribe-token] +verify_jwt = true + +[functions.send-rate-limit-alert] +verify_jwt = false + +[functions.cleanup-rate-limit-logs] +verify_jwt = false + +[functions.evolution-sync] +verify_jwt = true + +[functions.ai-enhance-message] +verify_jwt = true + +[functions.classify-audio-meme] +verify_jwt = true + +[functions.classify-emoji] +verify_jwt = true + +[functions.classify-sticker] +verify_jwt = true + +[functions.elevenlabs-tts-stream] +verify_jwt = true + +[functions.elevenlabs-webhook] +verify_jwt = false + +[functions.elevenlabs-dialogue] +verify_jwt = true + +[functions.elevenlabs-voice-design] +verify_jwt = true + +[functions.elevenlabs-sts] +verify_jwt = true + +[functions.elevenlabs-sfx] +verify_jwt = true +``` + +### Observação +Webhooks externos reais podem permanecer sem JWT, desde que tenham validação própria de assinatura/segredo. + +--- + +## Patch 2 — Validar assinatura no `whatsapp-webhook` + +### Objetivo +Rejeitar POST forjado. + +### Bloco utilitário sugerido +```ts +async function verifyWhatsAppSignature( + rawBody: string, + signatureHeader: string | null, + appSecret: string, +): Promise { + if (!signatureHeader?.startsWith('sha256=')) return false; + + const encoder = new TextEncoder(); + const key = await crypto.subtle.importKey( + 'raw', + encoder.encode(appSecret), + { name: 'HMAC', hash: 'SHA-256' }, + false, + ['sign'], + ); + + const digest = await crypto.subtle.sign('HMAC', key, encoder.encode(rawBody)); + const expected = Array.from(new Uint8Array(digest)) + .map((b) => b.toString(16).padStart(2, '0')) + .join(''); + + const provided = signatureHeader.replace('sha256=', ''); + if (provided.length !== expected.length) return false; + + let mismatch = 0; + for (let i = 0; i < provided.length; i++) { + mismatch |= provided.charCodeAt(i) ^ expected.charCodeAt(i); + } + return mismatch === 0; +} +``` + +### Uso no POST +```ts +const rawBody = await req.text(); +const appSecret = Deno.env.get('WHATSAPP_APP_SECRET'); + +if (appSecret) { + const valid = await verifyWhatsAppSignature( + rawBody, + req.headers.get('x-hub-signature-256'), + appSecret, + ); + + if (!valid) { + log.warn('Invalid WhatsApp webhook signature'); + return new Response('Forbidden', { status: 403, headers: getCorsHeaders(req) }); + } +} + +const rawPayload = JSON.parse(rawBody); +``` + +--- + +## Patch 3 — Autenticar o emissor no `evolution-webhook` + +### Objetivo +Impedir que POSTs externos alterem o banco sem segredo válido. + +### Bloco sugerido +```ts +import { getCorsHeaders } from "../_shared/validation.ts"; + +function isAuthorizedEvolutionWebhook(req: Request, payload: WebhookPayload): boolean { + const expected = Deno.env.get('EVOLUTION_WEBHOOK_SECRET') || Deno.env.get('EVOLUTION_API_KEY'); + if (!expected) return false; + + const headerKey = req.headers.get('x-evolution-apikey') || req.headers.get('apikey'); + return headerKey === expected || payload.apikey === expected; +} +``` + +### Uso no handler +```ts +if (req.method === 'OPTIONS') { + return new Response(null, { headers: getCorsHeaders(req) }); +} + +if (req.method !== 'POST') { + return new Response('Method not allowed', { status: 405, headers: getCorsHeaders(req) }); +} + +const payload: WebhookPayload = await req.json(); + +if (!isAuthorizedEvolutionWebhook(req, payload)) { + return new Response(JSON.stringify({ error: 'Unauthorized webhook sender' }), { + status: 403, + headers: { ...getCorsHeaders(req), 'Content-Type': 'application/json' }, + }); +} +``` + +### Ajuste adicional +Substituir `corsHeaders` local por `getCorsHeaders(req)`. + +--- + +## Patch 4 — Endurecer `evolution-api` + +### Objetivo +Bloquear função administrativa para chamadores sem JWT/permissão. + +### Medidas mínimas +1. trocar `corsHeaders` local por `getCorsHeaders(req)`; +2. manter `verify_jwt = true`; +3. validar o usuário e, para ações administrativas, exigir perfil `admin` ou `supervisor`. + +### Exemplo de agrupamento de ações sensíveis +```ts +const adminOnlyActions = new Set([ + 'create-instance', + 'delete-instance', + 'disconnect', + 'restart-instance', + 'set-webhook', + 'set-chatwoot', + 'set-typebot', + 'set-openai', + 'set-dify', + 'set-flowise', + 'set-evolution-bot', + 'set-rabbitmq', + 'set-sqs', + 'set-proxy', + 'set-evoai', + 'set-n8n', + 'set-kafka', + 'set-nats', + 'set-pusher', +]); +``` + +### Validação sugerida +```ts +const authHeader = req.headers.get('authorization'); +if (!authHeader) { + return new Response(JSON.stringify({ error: 'Missing authorization' }), { + status: 401, + headers: { ...getCorsHeaders(req), 'Content-Type': 'application/json' }, + }); +} + +const anonClient = createClient(supabaseUrl, Deno.env.get('SUPABASE_ANON_KEY')!, { + global: { headers: { Authorization: authHeader } }, +}); + +const { data: authData } = await anonClient.auth.getUser(); +const userId = authData.user?.id; +if (!userId) { + return new Response(JSON.stringify({ error: 'Unauthorized' }), { + status: 401, + headers: { ...getCorsHeaders(req), 'Content-Type': 'application/json' }, + }); +} +``` + +Depois disso, resolver o perfil e bloquear `adminOnlyActions` para usuários sem papel apropriado. + +--- + +## Patch 5 — Eliminar fallback perigoso em `sendMessage` + +### Objetivo +Impedir envio pela conexão errada. + +### Problema atual +Quando a conexão vinculada ao contato não está conectada, o código busca “a última conexão conectada” do sistema. + +### Alteração recomendada +Remover completamente o fallback genérico. + +### Comportamento desejado +- se o contato não tem `whatsapp_connection_id`, abortar com erro explícito; +- se a conexão vinculada está desconectada, abortar com erro explícito; +- nunca escolher automaticamente outra instância. + +### Exemplo de regra +```ts +if (!contact?.whatsapp_connection_id) { + throw new Error('Contato sem conexão WhatsApp vinculada'); +} + +const { data: linkedConnection } = await supabase + .from('whatsapp_connections') + .select('id, instance_id, status') + .eq('id', contact.whatsapp_connection_id) + .single(); + +if (!linkedConnection?.instance_id || linkedConnection.status !== 'connected') { + throw new Error('A conexão vinculada ao contato não está ativa'); +} +``` + +--- + +## Patch 6 — Corrigir corrida em `useMessages` + +### Objetivo +Evitar que resposta de fetch antigo sobrescreva o contato atual. + +### Implementação sugerida +Adicionar `requestIdRef`. + +```ts +const requestIdRef = useRef(0); + +const fetchMessages = useCallback(async () => { + const requestId = ++requestIdRef.current; + + if (!contactId || !mountedRef.current) { + if (mountedRef.current) { + setMessages([]); + setLoading(false); + } + return; + } + + try { + setLoading(true); + setError(null); + + // ... fetch paginado ... + + if (!mountedRef.current || requestId !== requestIdRef.current) return; + setMessages(mappedMessages); + } catch (err) { + if (!mountedRef.current || requestId !== requestIdRef.current) return; + setError(err instanceof Error ? err.message : 'Failed to fetch messages'); + } finally { + if (mountedRef.current && requestId === requestIdRef.current) { + setLoading(false); + } + } +}, [contactId]); +``` + +--- + +## Patch 7 — Reduzir risco de truncamento no inbox global + +### Objetivo +Evitar perda de cobertura com volume alto. + +### Ajuste recomendado +Substituir limites rígidos por estratégia incremental. + +### Opção mínima +- manter 500/1000 como primeira carga visual; +- adicionar banner/telemetria quando houver truncamento; +- buscar contactos faltantes por janela de atividade; +- suportar paginação adicional por scroll ou filtro. + +### Telemetria sugerida +```ts +if ((seededContacts?.length ?? 0) === SEEDED_CONTACT_LIMIT) { + log.warn('Inbox contacts may be truncated', { limit: SEEDED_CONTACT_LIMIT }); +} + +if ((recentMessages?.length ?? 0) === RECENT_MESSAGES_LIMIT) { + log.warn('Recent messages may be truncated', { limit: RECENT_MESSAGES_LIMIT }); +} +``` + +--- + +## Patch 8 — Robustecer painéis de Tarefas e Lembretes + +### Objetivo +Evitar criação sem ownership e melhorar tratamento de erro. + +### Exemplo para `ConversationTasksPanel` +```ts +if (!profileId) { + toast.error('Perfil ainda não carregado. Tente novamente em alguns segundos.'); + return; +} + +try { + setSaving(true); + const { error } = await supabase.from('conversation_tasks').insert({ + contact_id: contactId, + created_by: profileId, + assigned_to: profileId, + title: newTitle.trim(), + priority, + status: 'pending', + }); + + if (error) throw error; + await loadTasks(); +} catch (err) { + toast.error('Erro ao salvar tarefa.'); +} finally { + setSaving(false); +} +``` + +Aplicar lógica equivalente em `RemindersPanel`. + +--- + +## Patch 9 — Suíte mínima de regressão + +### Objetivo +Criar base de proteção para deploys. + +### Ajustes recomendados +Adicionar scripts no `package.json`: +```json +{ + "scripts": { + "test": "vitest run", + "test:watch": "vitest", + "test:ui": "vitest --ui" + } +} +``` + +### Casos mínimos +1. `useMessages` ignora resposta stale. +2. `useRealtimeMessages` não duplica mensagem. +3. `sendMessage` falha quando conexão vinculada está inativa. +4. `ConversationTasksPanel` não cria sem `profileId`. +5. `whatsapp-webhook` rejeita assinatura inválida. + +--- + +## Patch 10 — Atualizar documentação de superfície + +### Objetivo +Eliminar divergência entre inventário e sistema real. + +### Ações +- alinhar número real de functions no inventário; +- alinhar lista de módulos/rotas/views; +- adicionar tabela de exposição externa: função, finalidade, auth, segredo. + +--- + +## Ordem de implementação recomendada +1. Patch 1 — config JWT. +2. Patch 2 — assinatura WhatsApp. +3. Patch 3 — autenticação Evolution webhook. +4. Patch 4 — hardening evolution-api. +5. Patch 5 — remover fallback de conexão errada. +6. Patch 6 — corrida em `useMessages`. +7. Patch 7 — truncamento inbox. +8. Patch 8 — painéis contextuais. +9. Patch 9 — testes automatizados. +10. Patch 10 — documentação. + +--- + +## Critério de aceite técnico +A rodada P0/P1 só é considerada concluída quando: +- as alterações estiverem em PR aprovado; +- houver evidência de teste manual e automatizado para cada patch crítico; +- staging validar envio, recebimento, webhook legítimo e rejeição de webhook inválido; +- nenhuma mensagem puder sair por conexão não vinculada ao contato.