Skip to content

Recovery: Batches D.1 + D.2 + Fase 2 (Audit & Patch Cirúrgico)#143

Merged
adm01-debug merged 31 commits into
mainfrom
recovery/lovable-introspection
May 11, 2026
Merged

Recovery: Batches D.1 + D.2 + Fase 2 (Audit & Patch Cirúrgico)#143
adm01-debug merged 31 commits into
mainfrom
recovery/lovable-introspection

Conversation

@adm01-debug
Copy link
Copy Markdown
Owner

@adm01-debug adm01-debug commented May 11, 2026

🎯 Recovery: Restaura o banco Promo_Gifts via Audit & Patch Cirúrgico

Este PR consolida 3 fases de trabalho aplicadas em PROD entre 10 e 11 de Maio de 2026:

✅ Batch D.1 (P1 Features)     5/5 patches  →  13 tables + 10 RPCs
✅ Batch D.2 (P2 Infrastructure) 5/5 patches  →  15 tables + 16 RPCs
✅ Fase 2 (Secrets migration)   12 secrets   →  integration_credentials

Total: 28 tabelas novas + 26 RPCs novas + 1 enum + 7 colunas em admin_audit_log + 12 secrets migrados + 2 bugs corrigidos.


📦 O que está aplicado em PROD

Batch D.1 — P1 Features (já aplicado, este PR documenta)

Patch Escopo Tables RPCs
D.1.1 Storage Policies (6 buckets + 34 policies) 0 0
D.1.2 Optimization Queue 3 6
D.1.3 Collection Items (B2C) 3 2
D.1.4 Kit Collaboration 4 2
D.1.5 Dashboard Widgets 1 6

Batch D.2 — P2 Infrastructure (já aplicado, este PR documenta)

Patch Escopo Tables RPCs
D.2.1 Security & Audit 7 4
D.2.2 Outbound Webhooks 2 0
D.2.3 MCP API Keys 4 1
D.2.4 External Connections 1 5
D.2.5 Telemetry 3 6

Fase 2 — Migração de secrets (segurança)

  • 10 secrets Cloudflare + 2 XBZ → integration_credentials
  • Dual storage temporário (legacy preservado por 1-2 semanas)
  • Trigger tg_integration_credentials_derive ampliado (CLOUDFLARE_/XBZ_)

🗂️ Decisions documentadas (DECISIONS.md)

# Decisão Impacto
001 Pivot do plano de Recovery (v3 → v4) Mudança de estratégia
002 Frente A + B em paralelo Aceleração
003 Escopo: TUDO do Lovable Cobertura completa
004 Plano A' (B2B rename collections) Preserva 7 + 4433 rows B2B
005 Batch D.1 concluído 5/5 patches aplicados
006 Batch D.2 concluído 5/5 patches aplicados
007 Plano A'' (rename system_settings) Corrige bug de 18 dias
008 Fase 2 (migração de secrets) 12 secrets em local seguro

🐛 Bugs corrigidos durante o recovery

🚨 CRÍTICO — Bug de 18 dias em produção

A migration 20260423185624_*.sql tentou criar system_settings com schema novo via CREATE TABLE IF NOT EXISTS, mas a tabela já existia com schema antigo (setting_key/setting_value). O IF NOT EXISTS pulou silenciosamente.

Resultado: get_connection_failure_window_minutes apontava para coluna value que NÃO EXISTIA desde 23/Abr. Card FailureWindowCard em /admin/conexoes nunca chegou a funcionar.

Fix: Decision 007 — renomeia legacy + cria nova system_settings com schema Lovable.

🔧 Menor — Trigger sem prefixos CLOUDFLARE/XBZ

tg_integration_credentials_derive não auto-derivava provider para CLOUDFLARE_ e XBZ_. Ampliado na Fase 2.


📂 Arquivos adicionados

Patches (60 arquivos)

recovery/patches/
├── D1.1_storage_policies/      (5 arquivos: patch + backup + rollback + validate + smoke_test)
├── D1.2_optimization_queue/    (6 arquivos)
├── D1.3_collection_items/      (6 arquivos)
├── D1.4_kit_collaboration/     (6 arquivos)
├── D1.5_dashboard_widgets/     (6 arquivos)
├── D2.1_security_audit/        (6 arquivos)  ← NOVO (este PR)
├── D2.2_webhooks/              (6 arquivos)  ← NOVO (este PR)
├── D2.3_mcp_keys/              (6 arquivos)  ← NOVO (este PR)
├── D2.4_external_connections/  (6 arquivos)  ← NOVO (este PR)
└── D2.5_telemetry/             (6 arquivos)  ← NOVO (este PR)

Documentação (12 arquivos)

recovery/agent-db/
├── DECISIONS.md                ← 8 decisões com rastreabilidade
├── EXECUTION_LOG.md            ← histórico técnico das 3 fases
├── progress.md                 ← quadro consolidado
├── HANDOFF.md                  ← handoff para próxima sessão
├── ISSUES.md                   ← 4 fechadas + 5 abertas não-bloqueantes
├── BATCH_D2_COMPLETE.md        ← detalhamento técnico D.2
├── SECRETS_MIGRATION_FASE2.md  ← detalhamento técnico Fase 2
└── ... (outros docs históricos)

🛡️ Garantias de rollback

Todos os patches têm backup.sql + rollback.sql + validate.sql + smoke_test.md.

Backups críticos preservados em PROD

  • _backup_collections_b2b_20260511 (7 rows B2B)
  • _backup_collection_products_b2b_20260511 (4433 rows)
  • _backup_system_settings_legacy_20260511 (78 rows)
  • _backup_admin_audit_log_pre_d21 (criado durante D.2.1)
  • _backup_connection_test_history_pre_d24 (criado durante D.2.4)

Dual storage temporário

12 secrets ainda em system_settings_legacy (paralelo a integration_credentials) por 1-2 semanas, conforme ISSUES.md #6.


✅ Validação executada

  • 97% score auditoria D.1 (78/80 checks OK)
  • 19/19 tables com RLS habilitado
  • 11/11 RPCs executam corretamente
  • 42 indexes criados
  • 10 FKs com CASCADE corretos
  • 38 policies granulares ativas
  • Smoke tests documentados em cada patch

⏳ Pós-merge (não bloqueante)

Conforme ISSUES.md:


📊 Numbers summary

✅ 29+ commits na branch
✅ 5 batches aplicados (D.1.1 → D.2.5)
✅ 8 Decisions documentadas
✅ 28 tabelas novas
✅ 26 RPCs novas
✅ 12 secrets migrados
✅ 2 bugs corrigidos (1 crítico de 18 dias)
✅ 5 backups defensivos preservados
✅ 0 perda de dados

Sponsor: Joaquim (adm01@promobrindes.com.br)
Recovery executado: 10-11 de Maio de 2026

Summary by CodeRabbit

Release Notes

  • Documentação

    • Adicionado plano completo de recuperação e restauração de banco de dados com 20 fases de execução
    • Documentação detalhada de configuração de autenticação, hooks customizados e webhooks
    • Guias de diagnóstico e auditoria para Realtime, Edge Functions e segurança
  • Banco de Dados

    • Schema de recuperação incluindo tipos, políticas RLS, triggers e storage
    • Novas políticas de segurança em nível de linha (RLS) para proteção de dados
    • Funções de purga automática de tokens expirados
  • Exemplos de Código

    • Exemplo prático de integração com Realtime para mensagens em tempo real

Review Change Stack

- Script SQL read-only com 4 queries que extraem:
  1. Inventário + colunas + comentários (64 tabelas alvo)
  2. Constraints (PK/UNIQUE/FK/CHECK) + indexes
  3. Policies RLS completas
  4. Triggers + bodies de functions + enums customizados

- companies removida do escopo (vive em outro banco - bancodadosclientes)
- 100% read-only, validado sintaticamente no banco atual
- README com instruções passo-a-passo para o sponsor rodar no dashboard

Resultado: 4 JSONs que servem de base para construir a migration
final de Recovery das tabelas faltantes do Promo Gifts.
- Estado atual: aguardando dump do schema do Lovable
- Documenta todas as descobertas da Fase 1 (drift gigante repo vs banco atual)
- Lista próximos passos quando dump chegar
- Mantém princípio de zero modificação até aprovação
ACHADOS:
- 0% overlap entre repo (331 versions) e banco (204 aplicadas)
  → Repo é "artefato órfão", banco rodou via Dashboard
- Tabelas faltantes reais: 85 (não 64 como Fase 1 estimou)
  → 30 a mais são backend puro (auditoria, step-up, magic up social)
- 19 functions extraídas do block04 Lovable, prontas pra aplicação
- Storage backup do _other.sql incorporado

Nenhuma modificação em produção. Tudo read-only.
Aguardando: blocos 13-16 do Lovable + edge functions restantes (67)
Criado em recovery/agent-db/:
- CLAUDE.md          → system prompt do agente, regras de ouro, escopo
- PLANO_20_FASES.md  → roadmap completo de Recovery (21 fases: 0 + 1-20)
- HANDOFF.md         → estado do projeto, artefatos disponíveis, próximos passos
- PROTOCOLO.md       → como agente DB e gerente se comunicam
- progress.md        → checklist visual das 20 fases
- EXECUTION_LOG.md   → registro de execuções (vazio inicialmente)
- ISSUES.md          → registro de problemas (vazio)
- DECISIONS.md       → registro de decisões (vazio)

Estrutura: Sponsor → Gerente (Web) → Agente DB (Claude Code VPS)
Modelo: read-only por default, write só com aprovação,
        tudo no branch recovery/lovable-introspection.

Aguardando: aprovação do Sponsor pra acionar o Agente DB.
…vable

Blocos pequenos do dump pg_dump --schema-only do Lovable:
- block02_types.sql      → 6 enums (app_role, conversation_event_type, etc)
- block06_views.sql      → 1 view (v_full_scope_grants)
- block07_sequences.sql  → vazio (IDENTITY bound)
- block08_extensions.sql → 7 extensions (pgcrypto, uuid-ossp, pg_trgm, etc)
- block09_storage.sql    → 6 buckets + 34 storage policies
- block10_realtime.sql   → publication supabase_realtime (vazia no Lovable)
- block11_cron_jobs.sql  → 0 jobs no Lovable, só templates

Necessários para Fases 4, 5, 12 do Recovery.
Agente DB precisa desses arquivos pra executar o plano.
…lidadas)

Functions necessárias pelos triggers das 85 tabelas faltantes.
Origem: extraídas do block04_functions.sql do dump Lovable.
Idempotente (CREATE OR REPLACE).
Pronto pra Fase 6 do plano de Recovery.
Histórico de 366 migrations do repo Lovable + scripts de validação.
Lista completa truncada por tamanho - será capturada via Fase 1 (15_migrations.json).
…, secrets, edge auth audit)

5 blocos NOVOS recebidos do Lovable - complementam o dump original:

- block16_auth_hooks.md → Análise dos 6 hooks de auth customizados (verify-email, step-up-verify, etc)
  ACHADO: nenhum Supabase Auth Hook nativo - tudo via edge functions

- block17_database_webhooks.md → Sistema próprio de webhooks (outbound_webhooks + webhook-dispatcher)
  ACHADO: 4 triggers HTTP, 0 webhooks nativos do Dashboard

- block18_realtime_diagnostics.sql → Queries de diagnóstico do Realtime (publication, REPLICA IDENTITY, RLS)

- block19_secrets_inventory.md → Inventário de 11 secrets + 34 env vars únicas
  CRÍTICO: 14 secrets exigem provisionamento manual antes do deploy

- block20_edge_functions_auth_audit.md → Auditoria de verify_jwt das 85 edge functions
  CRÍTICO: 22 funções sem auth detectável - top 5 prioritárias: secure-upload, sync-quote-bitrix, dropbox-list, bi-copilot, external-db-inspect
  NOTA: total real é 85 edge functions (não 77 como tinha no contexto anterior)
…6-20

5 sub-fases adicionadas:
- 6.5 ⚠️  Refatorar dispatch_quote_webhook_event() (remove URL hardcoded)
- 11.5 ⚠️ Auditar 22 edge functions sem auth (top 5: secure-upload, sync-quote-bitrix, dropbox-list, bi-copilot, external-db-inspect)
- 15.5 ⚠️ Checklist de 14 secrets que exigem provisionamento manual
- 18.5 ⚠️ Provisionar 14 secrets no banco novo antes do merge prod
- 20.5 ⚠️ Setup CI gate check-edge-auth.mjs

Correções:
- Total de edge functions: 77 → 85 (corrigido pelo block20)
- vault.secrets: agora confirmado vazio
- Auth Hooks nativos: confirmado 0
- Database Webhooks nativos: confirmado 0

Estimativa: 9.5h trabalho efetivo total

Atualiza PLANO_20_FASES.md (239 linhas) + progress.md
…ntory granular)

block21_edge_shared_imports_audit.md:
  - Auditoria da pasta supabase/functions/_shared/ (26 arquivos)
  - TODAS as 85 functions importam pelo menos cors.ts
  - 7 functions com auth.ts realmente faltando (refinamento do block20)
  - Lista crítica REDUZIDA de 22 → 7 functions sem auth real:
    dropbox-list, external-db-inspect, bi-copilot, kit-ai-builder,
    comparison-ai-advisor, market-intelligence-insights, generate-mockup-nanobanana

block22_edge_secrets_inventory.md:
  - 33 secrets distintos, 81 functions com secrets
  - Granular por edge function (vs block19 que era genérico)
  - Checklist DEFINITIVO de 28 secrets (21 críticos + 7 opcionais E2E)
  - Risco máximo identificado: LOVABLE_API_KEY (17 functions dependem)
  - Risco médio: EXTERNAL_SUPABASE_* (7 functions)

IMPACTO NO PLANO:
  - Fase 15.5 → usar block22 §2 como checklist SSOT
  - Fase 18.5 → 21 secrets críticos pra provisionar (não 14 como antes)
  - Fase 15 → deve incluir pasta _shared/ completa (26 arquivos)
  - Fase 11.5 → lista de prioridade refinada para 7 functions críticas
…shell (16 arquivos)

NOVOS:
- block09b_storage_policies_full.sql (34 policies, 7 grupos, idempotente)
- block09b_storage_policies_full.md (matriz quem-pode-fazer-o-quê)
- migrations/20260511123935_*.sql (purge_expired_step_up_artifacts + cron */15min)
- _other.sql (vazio - só CREATE SCHEMA)
- examples/RealtimeMessagesExample.tsx
- docs/INDEX.md (índice mestre do Lovable)
- docs/block10b_replica_identity_benchmark.md
- docs/block10c_replica_identity_full_column_leak.md (warning de vazamento)
- docs/block14_auth_config.md (config Auth Dashboard + tools)
- block11_cron_status_report.sql (6 queries auditoria pg_cron)
- scripts/run_blocks.sh (runner fail-fast)
- scripts/run_all.sh (runner avançado com --resume, --from, etc)
- scripts/diff_validation.sh (comparador OK/MISSING/WARN/FAIL)
- scripts/gen_summary.sh (sumário markdown)

SUBSTITUI:
- block11_cron_jobs.sql (versão antiga simples → expandida com vault setup + helper cron_invoke_edge)
- block16_auth_hooks.md (versão refletindo dump atual, sem auth-email-hook)
… dump

CONTEÚDO ATUALIZADO COM ACHADOS DESTA SESSÃO:

NÚMEROS REAIS DO DUMP (corrigidos):
- Tabelas: 85→136 (+60%)
- Indexes: ~150→234 (+56%)
- Foreign Keys: ~80→64 (-20%)
- Tabelas com RLS: ~80→129 (+61%)
- RLS Policies: ~150→317 (+111%)
- Functions: 19 críticas→163 total (+144)
- Edge Functions: 85→87 (+2)

ACHADOS-BOMBA:
- Project ref REAL do Lovable: jlpkghroyzkmseixtjxv (confirmado em config.toml)
- block14 vs block16: conflito sobre auth-email-hook (decisão Fase 19)
- block10c: WARNING REPLICA IDENTITY FULL + RLS = vazamento de colunas
- block11_cron expandido: vault setup + helper cron_invoke_edge
- 21 secrets críticos pra provisionar (não 14)
- 7 functions sem auth real (auditoria Fase 11.5)

NOVAS SUB-FASES:
- Fase 6.5: refatorar dispatch_quote_webhook_event (URL hardcoded)
- Fase 10.5: migration purge_step_up_artifacts
- Fase 11.5: auditoria edge functions sem auth
- Fase 15.5: provisionar 21 secrets
- Fase 18.5: BLOQUEADOR pré-merge prod (LOVABLE_API_KEY)
- Fase 20.5: CI gate check-edge-auth.mjs

ORDEM CANÔNICA OFICIAL (do README do Lovable):
08→02→01→04→05→07→06→03→09→09b→10→11

PENDÊNCIA ABERTA: restore de DADOS (user_roles, auth.users, mcp_keys)
progress.md:
- Reflete 25 fases (era 20)
- Mostra 14/22 JSONs criados na Fase 1 (60%)
- Tabela com status fase a fase + indicadores
- Achados consolidados desta sessão

HANDOFF.md (NOVO):
- Contexto rápido pra próxima sessão (30s)
- Comando completo de re-disparo do Agente DB no VPS
- 8 queries SQL prontas pra cada JSON faltante
- Regras de comportamento do Agente DB
- Plano de contingência (incêndio)
ACHADOS PRINCIPAIS:
- Destino é PROD ATIVO com evolução massiva pós-Lovable
- 195 tabelas (vs 136 dump) - 154 evoluções, 95 ausências
- 545 functions (vs 163 dump) - 521 evoluções, 137 ausências CRÍTICAS
- 458 policies (vs 317 dump)
- 850 indexes (vs 234 dump)

GAPS CRÍTICOS IDENTIFICADOS:
- Storage policies: 0 (vs 34 do dump) ⚠️
- MCP Keys system: 4 tabelas + 13 functions PERDIDAS
- Step-up Auth (MFA): 3 tabelas + 5 functions PERDIDAS
- Outbound webhooks: 4 tabelas + 4 functions PERDIDAS
- Security/Geofence: 5 tabelas + 11 functions PERDIDAS
- Quote/Order workflow: 13 functions PERDIDAS

EVOLUÇÕES POS-LOVABLE:
- ERP completo brindes (suppliers, products c/ 148 cols, tabelas preço gravação)
- Mockup AI pipeline
- Categoria + materiais com atributos
- Stock/inventory snapshots

PRÓXIMO PASSO: sponsor decide quais subsistemas resgatar
…ery from scratch

PIVOT ESTRATÉGICO COMPLETO:

ARQUIVOS NOVOS:
- PLANO_AUDIT_PATCH_V4.md (plano v4.0 oficial)

ARQUIVOS ATUALIZADOS:
- PLANO_20_FASES.md → marcado como OBSOLETO com warning bem visível
- progress.md → reflete Fase A concluída + Fase B próxima
- HANDOFF.md → instruções pra Fase B.1 (grep RPCs+tables no código)

NOVA ESTRATÉGIA (5 fases):
- Fase A: Auditoria de Gaps ✅ (concluída)
- Fase B: Investigação de Uso (grep no código React)
- Fase C: Decisão por Subsistema (sponsor classifica)
- Fase D: Patches Cirúrgicos (PRs pequenos, 1 por subsistema)
- Fase E: Documentação e Fechamento

PRINCÍPIOS NÃO-NEGOCIÁVEIS:
- Read-only por default
- Backup antes de cada patch
- 1 patch = 1 transaction
- Rollback documentado
- Smoke test obrigatório
- NUNCA aplicar dump por cima do prod

CRITÉRIO DE SAÍDA da Fase A: ✅ RELATORIO_GAPS.md gerado
PRÓXIMO PASSO: Fase B.1 - grep RPCs no código do app
ACHADO BOMBA:
- App está chamando 30 RPCs e 57 tables que NÃO EXISTEM no banco
- Provavelmente falhando silenciosamente em produção

ARTEFATOS GERADOS em recovery/audit/:
- rpcs_used_in_app.txt          (36 RPCs únicas chamadas no código)
- tables_used_in_app.txt        (116 tables únicas usadas no código)
- _missing_functions.txt        (137 functions faltantes do dump)
- _missing_tables.txt           (95 tables faltantes do dump)
- CRITICAL_missing_rpcs.txt     (30 = USADAS pelo app E FALTAM no banco) ⚠️
- CRITICAL_missing_tables.txt   (57 = USADAS pelo app E FALTAM no banco) ⚠️
- CRITICAL_rpcs_locations.txt   (linha+arquivo+snippet de cada RPC crítica)

METODOLOGIA:
- grep recursivo em src/ (1636 arquivos .ts/.tsx)
- cruzamento via comm -12 das listas sort -u
- localização extraída via grep com filename/lineno

PRÓXIMO PASSO: Fase B.2 - investigar se existem versões substitutas/renomeadas
no destino (renames pós-Lovable) antes de classificar como gap real.
VALIDAÇÃO SQL DIRETA NO BANCO DESTINO:
- 30/30 RPCs confirmadas como NÃO EXISTENTES (zero renames)
- 57 tables analisadas → 54 GAPS REAIS + 3 falsas alarmes

FALSAS ALARMES DESCOBERTAS:
- product_groups → VIEW alias de product_similarity_groups ✅
- product_group_members → VIEW alias de product_similarity_group_members ✅
- product_components → possível rename para product_kit_components 🟡

METODOLOGIA:
- pg_proc + pg_class para existência exata
- Busca fuzzy LIKE wildcards + prefixos (fn_, _v2, etc)
- pg_get_viewdef para validar aliases

DESCOBERTAS CHAVE:
- Time TI usou VIEWS como aliases de compat após refactor
- pg_stat_statements NÃO ajuda (queries falhadas geram 404 PostgREST
  ANTES de chegar no parser do Postgres)
- Só GlitchTip (frontend) ou postgres_logs (Supabase logs UI) confirma
  contagem real de chamadas falhando

PRÓXIMA AÇÃO:
- Aguardando token GlitchTip do sponsor (Frente A)
- Pode seguir direto pra Fase C (decisão por subsistema)

Triagem por prioridade:
🔴 P1 (impacto user): Dashboard widgets, Optimization Queue,
   Collection Items, Kit Collab (6 RPCs + 7 tables)
🟡 P2 (operacional): Security logs, Webhooks, MCP Keys, Connections
   config (9 RPCs + 9 tables)
🟢 P3 (descartar?): Magic Up, Expert chat, Voice commands,
   Role migration (1 RPC + 8 tables)
CITAÇÃO LITERAL:
"QUERO CRIAR TUDO QUE FOR NECESSARIO PARA O SISTEMA, MANTER TODAS
AS FUNCIONALIDADES QUE TINHA NO LOVABLE, APÓS O DEPLOY"

ESCOPO FINAL EXPANDIDO:
- Nenhuma feature é descartável (P3 inclusive)
- Resgatar todas 30 RPCs + 54 tables USADAS pelo app
- Resgatar também 107 RPCs + 41 tables NÃO-críticas
- Coexistir collection_items com collection_products
- Coexistir kit_* (4 tables) com custom_kits

ARQUIVOS:
- DECISIONS.md: Decisão 003 + 004 documentadas
- PLANO_AUDIT_PATCH_V4.md: bumped pra v4.1, Fase D expandida
  pra D.1 a D.5 cobrindo P1+P2+P3+não-críticas+validação

BLOQUEIO ATIVO:
Faltam 4 arquivos pesados do dump no VPS (não persistidos
entre sessões anteriores): block01_tables, block03_policies,
block04_functions, block12_edge_functions (~988 KB total)

PRÓXIMO PASSO: sponsor reupload pra iniciar Fase D.0
CONTEXTO:
Enquanto sponsor desbloqueia upload dos 4 arquivos pesados (block01/03/04/12),
preparei o D.1.1 COMPLETO pois usa block09 + block09b que JÁ TEMOS no repo.

ESTRUTURA recovery/patches/D1.1_storage_policies/:
- README.md      → plano + contexto + riscos
- backup.sql     → snapshot pré-patch (buckets + policies)
- patch.sql      → transação atômica: 6 buckets + 34 policies
                   (com pré-check de is_supervisor_or_above)
- validate.sql   → confirma 6 buckets + 34 policies criados
- rollback.sql   → DROP policies + DELETE buckets
- smoke_test.md  → checklist manual de 8 passos

DESCOBERTAS DA PREPARAÇÃO:
- 🚨 Destino tem APENAS 1 bucket (scripts), os 6 do dump não existem
- ✅ 5/6 funcs de role já existem (is_supervisor_or_above, is_admin, etc)
- 🟡 Risco médio: bucket quarantine usado pelo clamav (verificar infra)

STATUS: ⏳ AGUARDANDO REVIEW + APROVAÇÃO DO SPONSOR

Outras pastas criadas (vazias por ora):
- D1.2_optimization_queue/
- D1.3_collection_items/
- D1.4_kit_collaboration/
- D1.5_dashboard_widgets/

PRÓXIMO PASSO: sponsor revisa D.1.1 + manda os 4 arquivos pesados
UPLOADS RECEBIDOS via GitHub Web Upload (commit 8daf889):
✅ block01_tables_indexes_rls.sql (200 KB, 136 tables + 234 indexes)
✅ block03_policies.sql (91 KB, 317 RLS policies)
✅ block04_functions.sql (203 KB, 157 functions)
✅ block12_edge_functions_batch1-5.md (494 KB total)
✅ BONUS: full-public-schema.sql (526 KB, backup duplo)

LIMPEZA:
- Removidos 17 arquivos duplicados com sufixo "(1)", "(2)", "(3)"
- Mantidos apenas os canônicos sem sufixo

VALIDAÇÃO DAS 30 RPCs CRÍTICAS:
- 30/30 PRESENTES no block04_functions.sql ✅
- Confirmação dupla no full-public-schema.sql

VALIDAÇÃO D.1.2 (Optimization Queue):
- 1 table (optimization_queue) no block01
- 2 indexes
- 2 policies no block03
- 6 functions no block04 (5 críticas + 1 trigger)

BLOQUEIO DA FASE D.0 → RESOLVIDO
PRÓXIMO PASSO: aplicar D.1.1 (storage policies, já pronto)
                + preparar D.1.2 a D.1.5 em sequência
CONTEXTO:
Bloqueio dos 4 arquivos pesados foi RESOLVIDO via GitHub Web Upload (commit 8daf889).
Com block01/03/04 disponíveis, extraí TODOS os 4 patches restantes do P1.

CONTEÚDO:
- patches/BATCH_D1_README.md ← visão consolidada do batch + ordem de aplicação
- patches/D1.2_optimization_queue/ ← 7 arquivos (patch + 5 aux + stats)
- patches/D1.3_collection_items/   ← 7 arquivos
- patches/D1.4_kit_collaboration/  ← 7 arquivos
- patches/D1.5_dashboard_widgets/  ← 7 arquivos (inclui table user_comparisons descoberta como dep)

STATS GERAIS:
| Patch | Tables | Indexes | Policies | Alters | Functions | Tamanho |
|---|---:|---:|---:|---:|---:|---:|
| D.1.1 | 6 buckets | – | 34 | – | – | 12 KB |
| D.1.2 |  1 |  1 |  1 |  5 |  6 | 60 KB |
| D.1.3 |  2 |  5 |  5 |  8 |  2 | 143 KB |
| D.1.4 |  4 |  6 | 13 | 15 |  2 | 303 KB |
| D.1.5 |  1 |  3 |  5 |  3 |  6 | 143 KB |
| TOTAL | 14 | 15 | 58 | 31 | 18 | ~661 KB |

GARANTIAS:
- Cada patch é idempotente (CREATE IF NOT EXISTS, DROP IF EXISTS + CREATE)
- Cada patch é atômico (BEGIN/COMMIT)
- Cada pasta tem 6 arquivos: README, backup, patch, validate, rollback, smoke_test
- Smoke check sintático passou: BEGIN/COMMIT 1:1, $$ pairs OK

EXTRAÇÃO:
- Script Node.js automatizado em /tmp/extract_objects_v2.mjs
- Aux files em /tmp/generate_aux_files.mjs
- Sem intervenção manual nos SQL gerados

DEPENDÊNCIAS DESCOBERTAS:
- D.1.5 depende de collection_items (D.1.3) — documentado no BATCH_D1_README.md
- D.1.5 ganhou table user_comparisons na revisão (estava faltando)

PRÓXIMO PASSO:
Sponsor abre BATCH_D1_README.md e os 5 patches/D1.X_*/README.md
→ revisa em batch → manda GO → eu aplico SEQUENCIALMENTE com validate entre cada

Decisão 003 honrada: 100% Lovable, sem migrações, coexistência aceita.
PROBLEMA DESCOBERTO antes de aplicar D.1.2:
Os patches D.1.2-5 gerados pelo extract_objects_v2.mjs estavam CORROMPIDOS
porque o regex usava `[\s\S]+?` (ganancioso) ao invés de delimitar por blocos.

Sintoma: patch.sql do D.1.2 (60KB) tinha 50+ indexes e policies de OUTRAS
tabelas (admin_audit_log, ai_usage_logs, app_vitals, etc).

SOLUÇÃO (v3): parser baseado em blocos pg_dump
- pg_dump separa cada objeto com cabeçalho "-- Name: X; Type: Y; Schema: Z"
- Novo extrator parseia esses blocos e filtra pelo target real
- Resultado: patches MUITO menores e CORRETOS

NOVOS TAMANHOS DOS PATCHES:
- D.1.2: 60KB → 12KB (296 linhas, BEGIN/COMMIT 1:1, $$ 18)
- D.1.3: 143KB → 6KB (125 linhas, BEGIN/COMMIT 1:1, $$ 8)
- D.1.4: 303KB → 10KB (183 linhas, BEGIN/COMMIT 1:1, $$ 12)
- D.1.5: 143KB → 7KB (168 linhas, BEGIN/COMMIT 1:1, $$ 14)

DEPS DESCOBERTAS NO AUDIT:
- D.1.2 ganhou `optimization_queue_runs` (dep de complete_optimization)
- D.1.2 ganhou `connection_test_history` (dep de get_auto_test_job_status)

D.1.1 (storage policies) NÃO afetado — usa block09 direto sem extração.
Já foi aplicado com sucesso (commit anterior): 6 buckets + 34 policies.

PRÓXIMO PASSO: aplicar D.1.2 corrigido.
📊 RESUMO:
- D.1.1 Storage Policies → 6 buckets + 34 policies
- D.1.2 Optimization Queue → 3 tables + 6 funcs
- FASE 1 Plano A' RENAME atômico → B2B preservado (7+4433 rows)
- D.1.3 Collection Items v2 (Lovable) → 3 tables + 2 funcs
- D.1.4 Kit Collaboration → 4 tables + 2 funcs
- D.1.5 Dashboard Widgets → 1 table + 6 RPCs

TOTAL: 14 tables Lovable resgatadas + 16 RPCs P1 + ~60 policies + 6 buckets

🛡️ B2B PRESERVADO:
- b2b_collections: 7 rows (Natal 2026, Linha Térmica, etc)
- b2b_collection_products: 4433 rows vinculados
- FK + policies + PK renomeados consistentemente

💾 BACKUPS DISPONÍVEIS:
- _backup_collections_b2b_20260511
- _backup_collection_products_b2b_20260511
- _backup_collections_policies_b2b_20260511
- _backup_storage_buckets_20260511_d11
- _backup_functions_d12

⚙️ VALIDAÇÕES:
- 0 funcs sem search_path explícito
- 0 tables D.1 com RLS off
- Frontend volta a funcionar (types.ts já esperava Lovable)

ATUALIZADO:
- DECISIONS.md: Decision 004 (Plano A') + Decision 005 (D.1 done)
- BATCH_D1_README.md: status ✅ em todos + lições aprendidas

PENDÊNCIAS:
- FK connection_test_history.connection_id (após D.2 criar external_connections)
- Cron pg_cron para cleanup_expired_collection_trash (investigar block11)
- Smoke test manual do sponsor antes de D.2
- Regenerar types.ts (Supabase CLI)
- GlitchTip Auth Token (Frente A)
═══════════════════════════════════════════════════════════════════
  RESUMO EXECUTIVO
═══════════════════════════════════════════════════════════════════

✅ D.1.1 — Storage Policies (6 buckets + 34 policies storage.objects)
✅ D.1.2 — Optimization Queue (3 tables + 5 policies + 6 funcs)
✅ D.1.3 — Collection Items v2 LOVABLE (3 tables + 7 policies + 2 funcs)
   ⚠️  Aplicado APÓS Decision 004 (Plano A' — renomeação B2B)
✅ D.1.4 — Kit Collaboration (4 tables + 13 policies + 2 funcs)
✅ D.1.5 — Dashboard Widgets (1 table + 5 policies + 6 funcs P1)

MÉTRICAS:
- 13/13 tables Lovable resgatadas
- 16/16 RPCs P1 críticas ativas
- 30 policies em public.* + 34 em storage.objects
- 0 dados perdidos (B2B preservado com prefixo b2b_)

═══════════════════════════════════════════════════════════════════
  DECISION 004 — Plano A' (Coexistência B2B + Lovable)
═══════════════════════════════════════════════════════════════════

COLISÃO DESCOBERTA mid-execução:
- destino: collections schema B2B (slug, organization_id, 7 rows
  "Natal 2026", "Linha Térmica", etc + 4433 collection_products)
- Lovable: collections schema B2C (user_id, share_token)
- frontend useCollections.ts faz .eq("user_id") → HARD BROKEN

SIMULAÇÃO: 100 cenários × 4 opções
- A' (renomear B2B + criar Lovable): score 87/100 ✅ VENCEDOR
- B (renomear destino atual):  8/100
- C (skip):                   15/100
- D (pausa+audita):            0/100

APROVADO PELO SPONSOR. APLICADO:
- RENAME collections → b2b_collections (7 rows preservados)
- RENAME collection_products → b2b_collection_products
  + RENAME COLUMN collection_id → b2b_collection_id (4433 preservados)
- RENAME PK, FK, 4 policies com prefixo b2b_
- Backup completo em _backup_*_b2b_20260511
- CREATE collections schema Lovable
- CREATE collection_items + collection_items_trash
- Frontend volta a funcionar sem mudar 1 linha React

═══════════════════════════════════════════════════════════════════
  BUGS ENCONTRADOS E CORRIGIDOS NO PROCESSO
═══════════════════════════════════════════════════════════════════

1. Extrator regex ganancioso v2 → v3 (parser pg_dump por blocos)
2. Colisão collections B2B vs Lovable → Decision 004
3. Order-of-creation D.1.4 → reordenar TABLES → FUNCS → POLICIES

═══════════════════════════════════════════════════════════════════
  PENDÊNCIAS PARA D.2
═══════════════════════════════════════════════════════════════════

- Adicionar FK connection_test_history.connection_id (D.2 cria
  external_connections — FK falhou silenciosamente, esperado)
- Regenerar types.ts (npx supabase gen types)
- Smoke test no app
- Advisors security + performance
- Cron pg_cron para cleanup_expired_collection_trash()

Estado: SAFE — todos backups intactos, rollback documentado.
D.2.4 External Connections (CRÍTICO - fechou FK D.1):
- table external_connections + 4 indexes + 1 policy
- 5 RPCs: get/set_connection_failure_window_minutes, get/set_connections_auto_test_interval, sync_external_connections_from_credentials
- FK retroativa: connection_test_history.connection_id → external_connections.id ✅

D.2.1 Security & Audit (7 tables + 4 RPCs):
- access_security_settings, audit_logs, auth_login_attempts, geo_allowed_countries
- hardening_health_snapshots, rls_denial_log, step_up_audit_log
- log_access_denied, log_rls_denial, log_user_logout, check_hardening_status
- enum step_up_action criado
- admin_audit_log expandido: +7 cols (status, source, started_at, finished_at, duration_ms, request_id, payload_summary)

D.2.2 Outbound Webhooks: outbound_webhooks + webhook_deliveries

D.2.3 MCP Keys: mcp_api_keys (FORCE RLS) + mcp_key_auto_revocations + mcp_full_grantors + mcp_access_violations + can_grant_mcp_full

D.2.5 Telemetry: app_vitals + query_telemetry + webhook_delivery_metrics + 6 RPCs

Decision 007 (Plano A''): system_settings renomeado para system_settings_legacy (78 rows preservados), criada nova system_settings com schema Lovable (key/value).

Smoke test: 19/19 tables OK, 11/11 funcs executam, 3 funcs admin-protected funcionando.
…credentials

CONTEXTO: Auditoria do Decision 007 (system_settings_legacy) revelou que 12 secrets
estavam em local incorreto: tabela com 1 policy ALL genérica, sem criptografia,
misturada com configs e backups históricos.

AÇÃO: Migrados para public.integration_credentials (RLS + 4 policies granulares
+ acesso restrito a is_admin_or_above + trigger auto-derive).

12 secrets migrados:
- 10 Cloudflare (ACCOUNT_HASH, ACCOUNT_ID, API_TOKEN, IMAGES_URL, STREAM_SUBDOMAIN, VARIANT_*)
- 2 XBZ (CDN_BASE_URL, IMAGE_SOURCE)

Melhorias incrementais:
- Trigger tg_integration_credentials_derive: adiciona reconhecimento de prefixos
  CLOUDFLARE_ e XBZ_ (futuras inserções terão provider auto-derivado)
- Dual storage temporário (legacy preservado por 1-2 semanas para rollback)

Auditoria de uso confirmou: zero código atual (Edge Functions OU Frontend) consome
esses tokens — eram cadastrados em antecipação a features ainda não implementadas.
Agora estão no local correto, prontos para uso quando a integração Cloudflare for
desenvolvida.

Sem breaking changes. Rollback disponível via:
DELETE FROM integration_credentials WHERE notes LIKE '%Decision 007%';

Documentação completa: recovery/agent-db/SECRETS_MIGRATION_FASE2.md
Caminho B — Fase 1 de 3: atualização da documentação interna.

Arquivos atualizados:
- DECISIONS.md: adicionadas Decisions 006 (D.2), 007 (rename system_settings A''), 008 (Fase 2 secrets)
- EXECUTION_LOG.md: reescrito com status real (D.1+D.2+Fase 2 todos aplicados em PROD)
- progress.md: quadro consolidado com 28 tables + 26 RPCs + 2 bugs corrigidos
- HANDOFF.md: atualizado para próxima sessão (estado atual + próxima ação = PR)
- ISSUES.md: 4 issues fechadas + 5 abertas não-bloqueantes catalogadas

Próximos passos:
- Fase 2 (Caminho B): criar 20 arquivos auxiliares D.2 (backup/rollback/validate/smoke_test)
- Fase 3 (Caminho B): abrir PR recovery/lovable-introspection → main
…om D.1)

Caminho B — Fase 2 de 3: cria backup.sql + rollback.sql + validate.sql + smoke_test.md
para cada um dos 5 patches D.2. Agora D.1 e D.2 têm a mesma estrutura completa.

Arquivos criados (4 por patch × 5 patches = 20):

D.2.1 Security & Audit:
- backup.sql (snapshot admin_audit_log antes do ALTER)
- rollback.sql (DROP 4 RPCs + 7 tables + enum + reverter ALTER de admin_audit_log)
- validate.sql (7 checks: tables, RLS, RPCs, enum, alters, policies, indexes)
- smoke_test.md (7 testes manuais incluindo enum + RLS)

D.2.2 Outbound Webhooks:
- backup.sql (tabelas são novas, verificação preventiva)
- rollback.sql (DROP webhook_deliveries + outbound_webhooks com CASCADE)
- validate.sql (5 checks: tables, RLS, policies, FK, indexes)
- smoke_test.md (3 testes incluindo FK CASCADE)

D.2.3 MCP API Keys:
- backup.sql (verificação preventiva)
- rollback.sql (DROP can_grant_mcp_full + 4 tables com CASCADE)
- validate.sql (6 checks: tables, FORCE RLS crítico, RLS, RPC, policies, indexes)
- smoke_test.md (4 testes incluindo verificação de FORCE RLS)

D.2.4 External Connections (fecha bug 18 dias):
- backup.sql (snapshot connection_test_history antes da FK CASCADE)
- rollback.sql (DROP FK retroativa + 5 RPCs + tabela)
- validate.sql (7 checks: table, RLS, RPCs, FK retroativa, indexes, policy, RPC bug-fixed)
- smoke_test.md (4 testes incluindo verificação explícita do bug histórico fixado)

D.2.5 Telemetry:
- backup.sql (verificação preventiva)
- rollback.sql (DROP 6 RPCs + 3 tables base)
- validate.sql (5 checks: tables, RLS, RPCs + execução de get_app_health_summary)
- smoke_test.md (5 testes incluindo cenários admin-protected)

Próximo passo (Fase 3): abrir PR recovery/lovable-introspection → main
Copilot AI review requested due to automatic review settings May 11, 2026 23:20
@vercel
Copy link
Copy Markdown

vercel Bot commented May 11, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
promo-gifts Ready Ready Preview, Comment May 11, 2026 11:21pm

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot wasn't able to review this pull request because it exceeds the maximum number of lines (20,000). Try reducing the number of changed lines and requesting a review from Copilot again.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented May 11, 2026

Walkthrough

Cria extensa base de documentação e scripts de recuperação Supabase, adiciona enums/policies/triggers/views/storage/realtime/cron, exemplos TSX/Edge e auditorias. Inclui novo RPC SECURITY DEFINER para expurgar artefatos step-up e agenda via pg_cron a cada 15 minutos.

Changes

Export/Recovery + Purge Job

Layer / File(s) Summary
Recuperação completa + RPC de expurgo
recovery/**/*, RECOVERY_PLAN.md, recovery/20260511123935_*.sql
Documentação e blocos SQL de export/aplicação (tipos, policies, triggers, views, storage, realtime, cron), auditorias e exemplos. Novo public.purge_expired_step_up_artifacts() com privilégios restritos e job pg_cron idempotente para limpeza periódica de step_up_tokens e step_up_challenges com logging best-effort.

Sequence Diagram(s)

(sem diagramas adicionais)

Estimated code review effort

🎯 5 (Critical) | ⏱️ ~120 minutes

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch recovery/lovable-introspection

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: cfcdb6c752

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +40 to +42
CREATE POLICY "Acesso de inserção para usuários autenticados em personaliza"
ON storage.objects FOR INSERT TO authenticated
WITH CHECK (bucket_id = 'personalization-images');
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Recreate storage policies idempotently

When this storage patch is run on a database that already has any of these policy names, the first CREATE POLICY raises a duplicate-object error and rolls back the whole transaction. The patch README/workflow says policy-name collisions are mitigated with DROP POLICY IF EXISTS, but the script never drops the policies before recreating them, so reruns or partially restored environments cannot apply D1.1.

Useful? React with 👍 / 👎.

CREATE INDEX IF NOT EXISTS idx_kit_collab_user ON public.kit_collaborators USING btree (user_id);

-- ─────────── POLICIES: public.kit_collaborators (4) ───────────
CREATE POLICY "Owner can invite collaborators" ON public.kit_collaborators FOR INSERT WITH CHECK (public.is_kit_owner(kit_id, auth.uid()));
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Define kit helper functions before policies

If this patch is applied to an environment where public.is_kit_owner/public.is_kit_collaborator are not already present, it fails at the first policy creation because those helper functions are only created later in the same file. Since the patch itself claims to create these functions, a clean recovery/bootstrap of the kit collaboration objects rolls back before reaching the function definitions.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

🟠 Major comments (14)
recovery/analysis/85_TABELAS_FALTANTES_REAIS.md-1-10 (1)

1-10: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Inconsistência com RECOVERY_PLAN.md.

Este documento afirma 85 tabelas faltantes (linha 10), enquanto RECOVERY_PLAN.md afirma 65 tabelas faltantes (linhas 1, 15). Uma diferença de 20 tabelas impacta diretamente o planejamento, cronograma e validação da recuperação.

Recomendo consolidar os números antes de aplicar qualquer batch em PROD.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@recovery/analysis/85_TABELAS_FALTANTES_REAIS.md` around lines 1 - 10, The
numbers for missing tables are inconsistent between the analysis doc and the
recovery plan: update and reconcile the counts by re-running the canonical
dump/counts and then update both "85_TABELAS_FALTANTES_REAIS.md" (the table
under "Faltam recuperar") and "RECOVERY_PLAN.md" to the same authoritative
value; reference the source of truth you used (e.g., the dump/recount script or
query), add a one-line note in both documents stating which script/query
produced the count and its timestamp, and if you changed the count from 85 to 65
(or vice versa) also update any downstream totals/estimates that depend on that
number so planning numbers remain consistent.
recovery/agent-db/SECRETS_MIGRATION_FASE2.md-86-89 (1)

86-89: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Revertir com filtro por secret_name explícito, não por notes LIKE

Na linha 88, usar DELETE ... WHERE notes LIKE '%Decision 007%' é riscado em produção: o campo notes é livre para documentação e admin, qualquer nova credencial documentada com "Decision 007" será deletada no rollback. Filtrar pelos 12 secret_name concretos (CLOUDFLARE_* e XBZ_*) torna a operação determinística e segura.

Diff
-DELETE FROM public.integration_credentials WHERE notes LIKE '%Decision 007%';
+DELETE FROM public.integration_credentials
+WHERE secret_name IN (
+  'CLOUDFLARE_ACCOUNT_HASH',
+  'CLOUDFLARE_ACCOUNT_ID',
+  'CLOUDFLARE_API_TOKEN',
+  'CLOUDFLARE_IMAGES_URL',
+  'CLOUDFLARE_STREAM_SUBDOMAIN',
+  'CLOUDFLARE_VARIANT_LARGE',
+  'CLOUDFLARE_VARIANT_MEDIUM',
+  'CLOUDFLARE_VARIANT_PUBLIC',
+  'CLOUDFLARE_VARIANT_SMALL',
+  'CLOUDFLARE_VARIANT_THUMBNAIL',
+  'XBZ_CDN_BASE_URL',
+  'XBZ_IMAGE_SOURCE'
+);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@recovery/agent-db/SECRETS_MIGRATION_FASE2.md` around lines 86 - 89, The
DELETE rollback uses a fragile notes LIKE filter; change it to delete only the
known 12 secrets by filtering on the secret_name column in the
integration_credentials table (explicit WHERE secret_name IN (...)) using the
concrete CLOUDFLARE_* and XBZ_* secret_name values so the operation is
deterministic and won’t accidentally remove other rows; ensure the list contains
exactly the 12 original secret_name entries and nothing else.
recovery/block15_migrations_history.md-420-421 (1)

420-421: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Cast direto de version para timestamp derruba a query com versões legadas

A query em linha 420 falha com ERROR: invalid input syntax for type timestamp porque o inventário contém versões em múltiplos formatos (001_, 20250103_01_, 20250103_complete_schema, com e sem UUIDs). Apenas versões com exatamente 14 dígitos são parseáveis. Isso quebra o bloco inteiro em execução.

Use validação antes do cast:

Diff sugerido
-  max(version)::timestamp AS ultima_em
+  max(
+    CASE
+      WHEN version ~ '^[0-9]{14}$'
+      THEN to_timestamp(version, 'YYYYMMDDHH24MISS')
+      ELSE NULL
+    END
+  ) AS ultima_em
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@recovery/block15_migrations_history.md` around lines 420 - 421, The query is
failing because it casts heterogeneous version strings directly to timestamp;
update the SELECT to only convert versions that match exactly 14 digits and
ignore others: in the query that selects max(...) AS ultima_em from
supabase_migrations.schema_migrations, replace the direct cast of version to
timestamp with a guarded expression that checks version ~ '^\d{14}$' (or
equivalent regex) and then converts that 14-digit string to a timestamp using
to_timestamp with the 'YYYYMMDDHH24MISS' format; non-matching versions should
yield NULL so they don't break the aggregate.
recovery/block12_edge_functions_batch2.md-310-324 (1)

310-324: 🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

Validação de CPF/CNPJ não verifica dígitos verificadores.

A função aceita qualquer sequência de 11 ou 14 dígitos sem validar os dígitos verificadores do CPF/CNPJ. Isso permite assinaturas com documentos falsos como "111.111.111-11" ou "000.000.000-00".

Para assinaturas com valor jurídico, adicione validação de dígitos verificadores ou ao menos adicione um aviso no metadata da assinatura indicando que a validação algorítmica não foi feita.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@recovery/block12_edge_functions_batch2.md` around lines 310 - 324, The
current approval branch only checks length of signer_document digits (docDigits)
and allows invalid CPFs/CNPJs; update the validation in the block guarded by if
(response === "approved") that reads signer_document/signer_name to perform
proper CPF and CNPJ checksum validation (implement the standard modulus-11
routines for CPF and the CNPJ two-check-digit algorithm) and reject inputs that
fail the checks, returning the same 400 Response pattern, or if you cannot
enforce checksum validation right now, add a metadata flag to the created
signature object (e.g., signatureMetadata.validationPerformed = false and/or
validationResult = "not_checked") and include a warning message in the response
body indicating algorithmic validation was not performed; modify the docDigits
length check location (the code that constructs docDigits and the error
responses) to incorporate the new checksum logic or metadata instead of only
length-checking.
recovery/agent-db/ISSUES.md-53-59 (1)

53-59: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Issue #7 (migration idempotente) merece atenção antes do merge.

O problema descrito é real: se supabase db reset for executado, a migration 20260423185624 vai cair no mesmo bug de CREATE TABLE IF NOT EXISTS com schema divergente.

Sugestão: adicione a migration corretiva 20260511_fix_system_settings_schema.sql neste PR em vez de deixar para depois. É uma linha adicional de código mas previne repetição do bug crítico de 18 dias.

Posso gerar a migration idempotente se quiser.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@recovery/agent-db/ISSUES.md` around lines 53 - 59, Add an idempotent fix
migration and a warning comment: update the original migration file (migration
`20260423185624`) to include a clear alert comment about the non-idempotent
CREATE TABLE behavior, and add a new migration named
`20260511_fix_system_settings_schema.sql` that performs a safe rename+recreate
sequence for the system_settings table (detecting existing schema, renaming the
old table if necessary, creating the correct table, copying data, and dropping
the old table) so the operations are idempotent on fresh clones and after
`supabase db reset`; ensure the new migration logic references the existing
table name `system_settings` and uses transactional/conditional SQL to avoid
failures on repeated runs.
recovery/block05_triggers.sql-640-650 (1)

640-650: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Triggers conflitantes mantendo limites diferentes em recently_viewed_products.

Ambos os triggers disparam em INSERT, mas com lógicas que entram em conflito:

  • trigger_limit_recently_viewed (INSERT/UPDATE) mantém até 100 items por usuário
  • trigger_limit_recently_viewed_products (INSERT) mantém até 50 items por usuário

Na prática, cada INSERT executa ambas as funções na mesma transação. A segunda função sempre sobrescreve a primeira, mantendo apenas 50 items e tornando a primeira inútil. Além disso, a ordem de execução entre triggers com mesmo timing (AFTER INSERT) não é garantida.

Decida qual limite é desejado (100 ou 50) e mantenha apenas um trigger/função.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@recovery/block05_triggers.sql` around lines 640 - 650, There are two
conflicting triggers for recently_viewed_products—trigger_limit_recently_viewed
(AFTER INSERT OR UPDATE -> public.limit_recently_viewed_items()) and
trigger_limit_recently_viewed_products (AFTER INSERT ->
public.limit_recently_viewed_products())—that both run on INSERT and enforce
different limits (100 vs 50); decide which limit is correct, keep only one
trigger+function implementing that limit (either consolidate logic into
public.limit_recently_viewed_items() or
public.limit_recently_viewed_products()), drop the other CREATE TRIGGER line
(and remove/rename the unused function if present), and ensure the remaining
trigger has the appropriate timing (INSERT or INSERT OR UPDATE) and comment
updated in the SQL file.
recovery/block11_cron_jobs.sql-22-43 (1)

22-43: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

⚠️ Fortaleça o aviso anti-hardcode de secrets nos templates.

Embora exista um aviso na linha 24 para não commitar a anon_key, o template ainda mostra o padrão <ANON_KEY> diretamente no código comentado (linhas 37-38). Risco: alguém pode substituir o placeholder por uma chave real e acidentalmente commitar.

🔐 Sugestão: reforce o aviso e mostre APENAS o padrão vault
--- EXEMPLO 1: invocar edge function a cada minuto ----
+-- ⚠️  ATENÇÃO: NUNCA hardcode secrets no código!
+-- Passo 1: crie os secrets no Vault primeiro:
+--   SELECT vault.create_secret('https://<project>.supabase.co/...', 'edge_url_webhook_alerts');
+--   SELECT vault.create_secret('<sua_anon_key>', 'edge_anon_key');
+--
+-- Passo 2: use os secrets via vault.decrypted_secrets:
 SELECT cron.schedule(
   'webhook-alerts-monitor',
   '* * * * *',
   $$
-  SELECT net.http_post(
-    url     := 'https://<PROJECT_REF>.supabase.co/functions/v1/webhook-alerts-monitor',
-    headers := jsonb_build_object(
-      'Content-Type', 'application/json',
-      'apikey',       '<ANON_KEY>',
-      'Authorization','Bearer <ANON_KEY>'
-    ),
-    body    := jsonb_build_object('triggered_at', now())
-  );
+  SELECT public.cron_invoke_edge(
+    'webhook-alerts-monitor',
+    jsonb_build_object('triggered_at', now())
+  );  -- helper que lê do Vault internamente
   $$
 );
+--
+-- OU, se preferir net.http_post direto (avançado):
+-- SELECT net.http_post(
+--   url     := (SELECT decrypted_secret FROM vault.decrypted_secrets WHERE name = 'edge_url_webhook_alerts'),
+--   headers := jsonb_build_object(
+--     'Content-Type', 'application/json',
+--     'apikey',       (SELECT decrypted_secret FROM vault.decrypted_secrets WHERE name = 'edge_anon_key'),
+--     'Authorization','Bearer ' || (SELECT decrypted_secret FROM vault.decrypted_secrets WHERE name = 'edge_anon_key')
+--   ),
+--   body := ...
+-- );
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@recovery/block11_cron_jobs.sql` around lines 22 - 43, Atualize o template
para eliminar o placeholder literal "<ANON_KEY>" nas chamadas de net.http_post e
em todo o bloco de exemplo e substitua-o por instruções de uso do Vault
(referenciando vault.create_secret) e/ou por um placeholder claramente marcado
como VAULT_SECRET (por exemplo, "VAULT_EDGE_ANON_KEY"); também fortaleça a
mensagem de aviso no topo para obrigar o uso de vault.create_secret antes de
executar cron.schedule e adicione uma linha explicita orientando a recuperar o
segredo do Vault em tempo de execução em vez de embuti‑lo no código.
recovery/agent-db/DECISIONS.md-244-248 (1)

244-248: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Comando de reversão está correto, mas adicione validação.

O comando DELETE FROM public.integration_credentials WHERE notes LIKE '%Decision 007%' funcionará para rollback. Porém, adicione validação pré-delete para evitar acidentes:

🛡️ Comando de reversão mais seguro
-- 1) Verificar quantos rows serão afetados
SELECT COUNT(*) as rows_to_delete 
FROM public.integration_credentials 
WHERE notes LIKE '%Decision 007%';
-- Esperar: 12

-- 2) Se count = 12, então executar delete
BEGIN;
DELETE FROM public.integration_credentials WHERE notes LIKE '%Decision 007%';
-- Os 12 originais continuam intactos em system_settings_legacy
COMMIT;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@recovery/agent-db/DECISIONS.md` around lines 244 - 248, Atualize o rollback
para incluir validação pré-delete e execução em transação: antes do comando
DELETE FROM public.integration_credentials WHERE notes LIKE '%Decision 007%',
execute um SELECT COUNT(*) FROM public.integration_credentials WHERE notes LIKE
'%Decision 007%' e verifique que o resultado é 12; só então executar o DELETE
dentro de um BEGIN ... COMMIT; se o count for diferente, não execute o DELETE
(ou faça ROLLBACK) e registre/lançe um alerta para investigação.
recovery/block09_storage.sql-8-18 (1)

8-18: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Considere restringir allowed_mime_types para segurança.

Todos os buckets têm allowed_mime_types = NULL, permitindo qualquer tipo de arquivo. Isso pode ser vetor de ataque (upload de executáveis, scripts maliciosos, etc).

Sugestão: Defina mime types específicos por bucket:

  • personalization-images: ARRAY['image/png','image/jpeg','image/webp']
  • product-videos: ARRAY['video/mp4','video/webm']
  • supplier-logos: ARRAY['image/png','image/jpeg','image/svg+xml']
  • component-media: idem images
  • mockup-art-files: images + PDFs se necessário
  • quarantine: manter NULL (proposital para files suspeitos)
🔒 SQL com mime types restritos
INSERT INTO storage.buckets (id, name, public, file_size_limit, allowed_mime_types) VALUES
  ('component-media', 'component-media', false, 5242880, 
   ARRAY['image/png','image/jpeg','image/webp','image/gif']),
  ('personalization-images', 'personalization-images', false, 5242880,
   ARRAY['image/png','image/jpeg','image/webp']),
  ('product-videos', 'product-videos', false, 104857600,
   ARRAY['video/mp4','video/webm','video/quicktime']),
  ('supplier-logos', 'supplier-logos', false, 2097152,
   ARRAY['image/png','image/jpeg','image/svg+xml']),
  ('mockup-art-files', 'mockup-art-files', false, 5242880,
   ARRAY['image/png','image/jpeg','image/webp','application/pdf']),
  ('quarantine', 'quarantine', false, 5242880, NULL)  -- intencional
ON CONFLICT (id) DO UPDATE ...
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@recovery/block09_storage.sql` around lines 8 - 18, The storage.buckets rows
currently set allowed_mime_types = NULL (allowing any upload); update the
INSERT/ON CONFLICT to provide explicit MIME restrictions per bucket (set
allowed_mime_types to ARRAY[...] for the buckets listed) — e.g., for
'personalization-images' use ARRAY['image/png','image/jpeg','image/webp'], for
'product-videos' use ARRAY['video/mp4','video/webm'], for 'supplier-logos' use
ARRAY['image/png','image/jpeg','image/svg+xml'], for 'component-media' and
'mockup-art-files' include appropriate image types (and application/pdf for
mockup if needed), and keep 'quarantine' as NULL; apply the same
EXCLUDED.allowed_mime_types in the ON CONFLICT DO UPDATE so updates preserve
these arrays.
recovery/block20_edge_functions_auth_audit.md-166-167 (1)

166-167: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

MAJOR: dropbox-list acessa token sem validação.

dropbox-listDROPBOX_ACCESS_TOKEN (secret) sem validar quem está chamando. Atacante com acesso à anon key pode listar arquivos Dropbox da empresa.

Fix:

const auth = await authorize(req, { requireRole: 'supervisor' });
if (!auth.ok) return auth.response;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@recovery/block20_edge_functions_auth_audit.md` around lines 166 - 167, The
dropbox-list edge function reads DROPBOX_ACCESS_TOKEN without authenticating
callers; add an authorization guard by calling authorize(req, { requireRole:
'supervisor' }) at the start of the dropbox-list handler (and similarly for
elevenlabs-scribe-token if applicable) and immediately return auth.response when
auth.ok is false, ensuring no secret is accessed unless authorize succeeds.
recovery/diff_validation.sh-39-54 (1)

39-54: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Edge case: DSN com múltiplos = pode quebrar parsing.

Linha 40-41 usa ${arg%%=*} e ${arg#*=} para split em nome=DSN. Se o DSN contiver = (ex: prod="postgresql://...?options=value"), o parsing falhará.

Exemplo problemático:

./diff_validation.sh file.sql \
  prod="postgres://host/db?sslmode=require"
  # dsn será "postgres://host/db?sslmode" (truncado)

Fix sugerido:

🔧 Parsing robusto
-  name="${arg%%=*}"
-  dsn="${arg#*=}"
+  # Split apenas no primeiro =
+  IFS='=' read -r name dsn <<< "$arg"
   if [[ -z "$name" || -z "$dsn" || "$name" == "$arg" ]]; then
     echo "erro: argumento inválido '$arg' (esperado nome=DSN)" >&2; exit 2
   fi
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@recovery/diff_validation.sh` around lines 39 - 54, The current split using
${arg%%=*} and ${arg#*=} can truncate a DSN that contains '='; replace that
parsing with a robust split using IFS='=' read -r name dsn <<< "$arg" so the
second variable receives the remainder (including additional '=' characters),
then keep the existing validation (check that name and dsn are non-empty) and
continue to use ENV_NAMES+=("$name"), out="$OUT_DIR/${name}.tsv" and psql "$dsn"
-f "$SQL_FILE" as before; ensure you preserve quoting when passing $dsn to psql.
recovery/diff_validation.sh-49-53 (1)

49-53: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Possível command injection via DSN malicioso.

Linha 49 passa "$dsn" diretamente para psql. Se um DSN malicioso contiver caracteres especiais (ex: prod="$(rm -rf /)"), pode executar comandos arbitrários.

Risco: Baixo em uso normal (DSNs vêm de admin), mas alto se script aceita DSNs de origem não-confiável.

Mitigação sugerida:

🛡️ Validação de DSN
+  # Validar formato básico do DSN (deve começar com postgres:// ou postgresql://)
+  if [[ ! "$dsn" =~ ^postgres(ql)?:// ]]; then
+    echo "erro: DSN inválido em '$name' (deve começar com postgres://)" >&2
+    exit 2
+  fi
   ENV_NAMES+=("$name")

Alternativamente, documentar que DSNs devem vir de fonte confiável apenas.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@recovery/diff_validation.sh` around lines 49 - 53, The psql invocation uses
the user-supplied variable dsn directly which can allow malicious connection
strings; add a validation step (e.g., a validate_dsn function) before the psql
call to allow only a safe subset of characters and formats (use a conservative
POSIX regex permitting letters, digits, `@/`:._- and basic key=value pairs, or
explicitly require a known prefix like "postgres://" or "host="), and if
validation fails print an error and exit; reference the variable dsn and the
psql invocation in the script (the block that runs psql "$dsn" -X -A -t -F $'\t'
...) and replace it so the script calls validate_dsn "$dsn" first.
recovery/block16_auth_hooks_validation.sql-29-29 (1)

29-29: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Nome incorreto de tabela: mcp_keys deve ser mcp_api_keys

O script valida a existência de mcp_keys (linhas 29, 62, 208-212, 264, 319, 406), mas o código das Edge Functions (batch3.md) referencia consistentemente mcp_api_keys (linhas 908, 1254, 1520, 1747).

Impacto: todas as validações de RLS/indexes/triggers para essa tabela retornarão MISSING/FAIL incorretamente.

Fix:

   VALUES
     ('login_attempts'),
     ('user_known_devices'),
     ('admin_audit_log'),
     ('step_up_challenges'),
     ('step_up_tokens'),
     ('user_roles'),
-    ('mcp_keys'),
+    ('mcp_api_keys'),
     ('mcp_key_auto_revocations')

Aplicar o mesmo fix em todas as 6 ocorrências (linhas 29, 62, 208-212, 264, 319, 406).

Also applies to: 62-62, 208-212, 264-264, 319-319, 406-406

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@recovery/block16_auth_hooks_validation.sql` at line 29, The script wrongly
references the table name 'mcp_keys' instead of the correct 'mcp_api_keys' in
multiple validation checks; update every occurrence of 'mcp_keys' in
recovery/block16_auth_hooks_validation.sql (the six places currently validating
that table) to 'mcp_api_keys' so RLS/index/trigger validations match the Edge
Functions code; ensure you replace all six instances (the ones noted in the
review) so validations no longer report MISSING/FAIL.
recovery/block12_edge_functions_batch4.md-1845-1851 (1)

1845-1851: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Validação de input fraca em bi-copilot (sem Zod)

O body é cast diretamente para RequestBody sem validação Zod (linha 1845):

const body = (await req.json()) as RequestBody;
if (!body.question || body.question.length > 500) {
  return new Response(JSON.stringify({ error: "Pergunta inválida." }), { status: 400 });
}

Problemas:

  1. Sem validação de context: atacante pode passar context: "string" em vez de Record<string, any>, causando erro no JSON.stringify (linha 1865) ou comportamento inesperado.
  2. Sem validação de history: atacante pode passar history: [{ role: "hacker", content: "..." }] com roles inválidos, causando erro no AI provider.
  3. JSON.stringify sem try/catch: se context tiver referências circulares, JSON.stringify lança TypeError: Converting circular structure to JSON (linha 1865), quebrando o handler sem tratamento.

Fix: adicionar validação Zod:

import { z } from "https://deno.land/x/zod@v3.23.8/mod.ts";

const RequestBodySchema = z.object({
  question: z.string().min(1).max(500),
  context: z.record(z.unknown()),
  history: z.array(z.object({
    role: z.enum(["user", "assistant"]),
    content: z.string(),
  })).optional(),
});

const parsed = RequestBodySchema.safeParse(await req.json());
if (!parsed.success) {
  return new Response(
    JSON.stringify({ error: "Validação falhou", details: parsed.error.flatten().fieldErrors }),
    { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } }
  );
}
const body = parsed.data;

Also applies to: 1865-1865

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@recovery/block12_edge_functions_batch4.md` around lines 1845 - 1851, The
handler currently casts req.json() to RequestBody and only checks body.question,
which allows invalid context/history shapes and can blow up on circular
structures; define a Zod schema (e.g., RequestBodySchema) that requires
question: string().min(1).max(500), context: record(z.unknown()), and optional
history: array(object({ role: z.enum(["user","assistant"]), content: z.string()
})), then replace the cast with RequestBodySchema.safeParse(await req.json())
and return a 400 with parsed.error.flatten() details when validation fails;
after validation, assign parsed.data to body and wrap any
JSON.stringify(body.context) or response serialization in try/catch to handle
circular references and return a 400/500 with a clear error instead of letting
the handler throw; keep existing corsHeaders and Content-Type in error
responses.
🟡 Minor comments (5)
recovery/block19_secrets_inventory.md-30-78 (1)

30-78: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Contagens declaradas estão inconsistentes com a lista

No bloco de Line 30-78, os totais por subtópico não fecham com os itens listados (ex.: 2.3 marcado como 8, mas há mais entradas; 2.5 marcado como 6, mas lista mais itens). Isso pode induzir provisionamento incompleto.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@recovery/block19_secrets_inventory.md` around lines 30 - 78, The counts in
the headings (e.g., "2) Env vars ... (34 únicas)" and subsection totals like
"2.3 Integrações de terceiros (8)" and "2.5 E2E / testes (6)") do not match the
actual listed env vars; recount each subsection (2.1, 2.2, 2.3, 2.4, 2.5, 2.6)
and update the numeric totals to match the items shown (or remove/adjust items
so the counts are accurate), and ensure the overall total ("34 únicas") equals
the sum of the subsections; reference subsection titles like "2.3 Integrações de
terceiros" and specific env names (e.g., BITRIX24_WEBHOOK_URL,
DROPBOX_ACCESS_TOKEN, E2E_CLEANUP_TOKEN) while making the fixes so readers and
provisioning scripts see correct counts.
recovery/block19_secrets_inventory.md-92-97 (1)

92-97: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Linha da tabela em formato inválido compromete leitura do checklist

Em Line 96, a linha do “❌ Usado no código...” está como célula única (quebra de colunas), o que prejudica renderização e revisão operacional.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@recovery/block19_secrets_inventory.md` around lines 92 - 97, The markdown
table row starting with "❌ **Usado no código mas NÃO presente em
`fetch_secrets`**" is currently a single-cell line breaking the table; fix it by
splitting it into two pipe-separated columns so the first column is the category
marker and the second contains the long list of secrets, e.g. replace that line
with a row like "| ❌ **Usado no código mas NÃO presente em `fetch_secrets`** |
`BITRIX24_WEBHOOK_URL`, `BI_SHARE_SECRET`, `DROPBOX_ACCESS_TOKEN`,
`ELEVENLABS_API_KEY`, `GITHUB_PAT`, `N8N_PRODUCT_WEBHOOK_SECRET`,
`N8N_QUOTE_WEBHOOK_URL`, `QUOTE_SYNC_API_KEY`, `RESEND_API_KEY`,
`SALESPRO_WEBHOOK_URL`, `VIRUSTOTAL_API_KEY`, `E2E_*`, `TEST_*`,
`ALLOW_HTTP_FETCH`, `LOG_*` |" ensuring the pipes align with the header and
separator rows in block19_secrets_inventory.md so the table renders correctly.
recovery/block10b_replica_identity_benchmark.md-30-32 (1)

30-32: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

⚠️ Policy de teste aberta (USING (true)) — garanta remoção após benchmark.

A policy "bench read all" (linha 31) expõe todas as linhas de realtime_bench para qualquer usuário autenticado. Embora o comentário na linha 29 alerte para remoção, há risco de esquecimento se o benchmark for interrompido ou o cleanup não for executado.

🛡️ Sugestão: adicione validação pós-cleanup

Adicione ao final da seção de cleanup (após linha 184):

-- Validação: confirme que a tabela e policy foram removidas
DO $$
BEGIN
  IF EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'public' AND tablename = 'realtime_bench') THEN
    RAISE EXCEPTION 'ATENÇÃO: tabela realtime_bench ainda existe! Cleanup incompleto.';
  END IF;
END $$;

-- Confirme que não há policies órfãs
SELECT schemaname, tablename, policyname 
FROM pg_policies 
WHERE policyname = 'bench read all';
-- Deve retornar 0 linhas

Ou, alternativamente, documente um tempo de vida máximo (TTL) para a tabela:

-- Adicione um TTL automático (requer pg_cron)
SELECT cron.schedule(
  'cleanup-realtime-bench',
  '*/30 * * * *',  -- a cada 30 min
  $$
  DROP TABLE IF EXISTS public.realtime_bench CASCADE;
  SELECT cron.unschedule('cleanup-realtime-bench');
  $$
);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@recovery/block10b_replica_identity_benchmark.md` around lines 30 - 32, The
temporary permissive policy "bench read all" on table realtime_bench is
dangerous if left behind; after the cleanup section (after the existing cleanup
block) add a post-cleanup validation that checks for existence of
public.realtime_bench and for any pg_policies rows with policyname = 'bench read
all' and fail/raise if found, and/or install a TTL/scheduled job to drop
realtime_bench automatically (e.g., via cron) so the policy/table cannot
persist; reference the policy name "bench read all" and table "realtime_bench"
when adding these checks and the scheduled cleanup.
recovery/agent-db/DECISIONS.md-24-48 (1)

24-48: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Decisão 004 está duplicada — consolidar para clareza.

A Decisão 004 (rename B2B collections) aparece duas vezes:

  • Linhas 24-48: versão resumida
  • Linhas 116-152: versão expandida

Ambas documentam o mesmo evento. Sugiro manter apenas a versão expandida (linhas 116-152) e remover a duplicação, ou adicionar nota de referência cruzada.

🧹 Proposta de consolidação

Opção 1: Remover linhas 24-48, manter 116-152.

Opção 2: Manter ambas, mas adicionar cross-reference:

 ## ✅ Decisão 004 — Plano A' para colisão de `collections` (atualizada 2026-05-11 20:30)
+> **Nota:** Versão expandida desta decisão está documentada abaixo (linhas 116-152).
 **Contexto:** Mid-batch durante D.1.3, descoberta colisão SEMÂNTICA crítica:

Also applies to: 116-152

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@recovery/agent-db/DECISIONS.md` around lines 24 - 48, Remove the duplicate
"## ✅ Decisão 004 — Plano A' para colisão de `collections`" summary block and
keep the expanded version (the fuller "Decisão 004" section) as the single
source of truth; ensure any internal cross-references or links pointing to the
removed summary now point to the remaining expanded "Decisão 004" heading and
update the document's table of contents or indices if present to avoid stale
references.
recovery/agent-db/DECISIONS.md-73-79 (1)

73-79: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Atualizar status da pendência #1 — já foi resolvida.

A FK connection_test_history.connection_id foi criada na Decisão 006 (Batch D.2, linha 171). Sugestão:

-### `#1` — FK pendente (resolverá em D.2)
+### `#1` — FK pendente ✅ RESOLVIDA em D.2
 `connection_test_history.connection_id` sem FK pra `external_connections`. Após D.2:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@recovery/agent-db/DECISIONS.md` around lines 73 - 79, Atualize a entrada “#1
— FK pendente” em DECISIONS.md para marcar como resolvida: note que a FK
connection_test_history.connection_id => external_connections(id) foi criada na
Decisão 006 (Batch D.2, linha 171) com o constraint name
connection_test_history_connection_id_fkey; remova/altere o texto que diz que a
FK está pendente e adicione uma referência à Decisão 006 indicando onde a
alteração foi aplicada.
🧹 Nitpick comments (11)
recovery/block06_views.sql (1)

6-28: View criada corretamente com security_invoker.

O uso de security_invoker='on' (linha 6) garante que RLS das tabelas base seja aplicado.

Observação: os casts diretos nas linhas 14 e 16 (::uuid, ::timestamptz) falharão se metadata contiver dados inválidos. Para maior robustez, considere wrapping com CASE ou funções de validação, mas para uma view de audit/recovery o comportamento fail-fast pode ser intencional.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@recovery/block06_views.sql` around lines 6 - 28, The view v_full_scope_grants
currently casts JSON metadata fields directly ((sual.metadata ->>
'key_id')::uuid and ((sual.metadata ->> 'expires_at'))::timestamp with time
zone), which will error if metadata contains invalid values; modify the SELECT
to validate before casting (e.g., check the text with a regex or EXISTS
condition and only cast when it matches, otherwise return NULL) for the key_id
and key_expires_at expressions so the view is robust to malformed metadata while
keeping the same column names (key_id, key_expires_at) and preserving the
security_invoker behavior of v_full_scope_grants.
recovery/RealtimeMessagesExample.tsx (1)

131-154: Handlers CRUD apropriados para código de exemplo.

O error handling via console.error é suficiente para um exemplo demonstrativo. Em código de produção, recomenda-se feedback visual de erro ao usuário (toast, alert, etc.).

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@recovery/RealtimeMessagesExample.tsx` around lines 131 - 154, The CRUD
handlers (handleInsert, handleUpdate, handleDelete) currently only log errors to
console.error which is acceptable for a demo; update these functions to provide
user-visible error feedback in production by replacing or augmenting
console.error with a UI notification (e.g., toast/alert/modal) or at minimum add
a TODO comment referencing a notification helper so callers can see where to
hook in user-facing error handling.
recovery/block12_edge_functions_batch2.md (2)

69-76: ⚡ Quick win

Rate limit pode ser generoso demais para endpoint de aprovação.

30 requisições/minuto permite tentar 1800 tokens diferentes por hora antes do bloqueio. Para um endpoint que valida tokens de aprovação de orçamento, considere reduzir para 10-15 req/min para dificultar ataques de força bruta.

🔒 Sugestão de endurecimento
     const protection = await runBotProtection(req, {
       endpoint: 'quote-public-view',
-      maxRequests: 30,
+      maxRequests: 10,
       windowSeconds: 60,
       blockSeconds: 3600,
       allowSearchBots: false,
     }, corsHeaders);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@recovery/block12_edge_functions_batch2.md` around lines 69 - 76, The rate
limit for the approval endpoint configured via runBotProtection (endpoint:
'quote-public-view') is too permissive; reduce maxRequests from 30 to a lower
value (e.g., 10–15) while keeping windowSeconds at 60 and preserving/blocking
behavior via blockSeconds (3600) to increase brute-force resistance; update the
call-site that sets { maxRequests: 30, windowSeconds: 60, blockSeconds: 3600,
allowSearchBots: false } so maxRequests is lowered and ensure any related tests
or docs referencing this limit are adjusted accordingly.

257-285: 💤 Low value

Whitelist de dados públicos bem implementada, mas client_id pode vazar info.

A exclusão explícita de negotiation_markup_percent, real_subtotal e real_discount_percent está correta para proteger dados de pricing interno. Porém, client_id (linha 262) exposto permite enumerar IDs de clientes via força bruta de tokens, o que pode ser usado para inferir volume de negócios.

Se client_id não for necessário no frontend público, remova. Se for, considere adicionar ao log de acesso para auditoria.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@recovery/block12_edge_functions_batch2.md` around lines 257 - 285, The
publicQuote object is leaking internal identifiers via client_id; remove
client_id from the whitelist (delete the client_id: quote.client_id line in the
publicQuote construction) unless the frontend truly needs it—if it must remain,
replace direct exposure with a safe reference (e.g., a hashed/opaque id) and add
an access audit in the code path that returns publicQuote (log the access with
request/user context). Update usages that expect publicQuote.client_id to use
the new safe reference or server-side lookups instead.
recovery/block10b_replica_identity_benchmark.md (1)

92-157: ⚡ Quick win

Benchmark SQL bem estruturado, mas atenção ao ambiente de execução.

O script usa \gset (linhas 100-101, 133-134), que é sintaxe específica do psql. O comentário nas linhas 159-161 documenta isso, mas seria útil fornecer a versão alternativa completa para SQL Editor (Lovable Cloud) logo após cada rodada, não apenas a nota final.

📝 Sugestão: adicione versão SQL Editor inline

Após a rodada A (linha 125), adicione:

-- Alternativa para SQL Editor (sem \gset):
-- 1. Rode antes da carga:
--    SELECT wal_records, wal_bytes, now() FROM pg_stat_wal;
-- 2. Anote os valores
-- 3. Rode a carga (INSERT/UPDATE/DELETE)
-- 4. Rode novamente:
--    SELECT wal_records, wal_bytes, now() FROM pg_stat_wal;
-- 5. Subtraia manualmente

Isso reduz fricção para quem não tem acesso a psql.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@recovery/block10b_replica_identity_benchmark.md` around lines 92 - 157, The
script relies on psql-only `\gset` usage in the pg_stat_wal snapshots (the
SELECTs that set :rec_a0/:bytes_a0/:t_a0 and :rec_b0/:bytes_b0/:t_b0 around the
"RODADA A" and "RODADA B" blocks); add an inline SQL-Editor-friendly alternative
immediately after each psql snapshot SELECT that shows the equivalent manual
steps: a plain SELECT of wal_records, wal_bytes, now() to run before the
workload, instructions to record those values, re-run the same SELECT after the
workload, and guidance to compute the deltas manually (so users of SQL editors
like Lovable Cloud can follow without `\gset`).
recovery/block09_storage.sql (1)

24-62: ⚡ Quick win

Policies duplicadas/redundantes para personalization-images.

Detectadas 4 policies redundantes:

  1. Linhas 24-26 e 36-38: ambas são INSERT para authenticated no mesmo bucket
  2. Linhas 28-30 e 32-34: ambas são SELECT para authenticated no mesmo bucket

Impacto: Não quebra funcionalidade (Postgres aceita múltiplas policies do mesmo tipo com OR), mas dificulta auditoria e manutenção.

Sugestão: Consolidar em uma policy por operação:

🧹 Limpeza de policies duplicadas
-- Remover linhas 24-30 (versões PT-BR duplicadas)
-- Manter apenas linhas 32-62 (versões EN mais descritivas)

-- OU consolidar em policies únicas mais claras:
CREATE POLICY "personalization_images_select_authenticated"
  ON storage.objects FOR SELECT TO authenticated
  USING (bucket_id = 'personalization-images');

CREATE POLICY "personalization_images_insert_authenticated"
  ON storage.objects FOR INSERT TO authenticated
  WITH CHECK (bucket_id = 'personalization-images');
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@recovery/block09_storage.sql` around lines 24 - 62, There are duplicate
storage policies for the personalization-images bucket (e.g., "Acesso de
inserção para usuários autenticados em personaliza" and "Authenticated users can
upload personalization images" for INSERT, and "Acesso de leitura para usuários
autenticados em personalizatio" and "Authenticated users can view
personalization images" for SELECT); remove the redundant entries and keep a
single clear policy per operation (one INSERT policy and one SELECT policy) —
either retain the existing English descriptive policies ("Authenticated users
can upload personalization images" and "Authenticated users can view
personalization images") or replace them with consolidated names like
personalization_images_insert_authenticated and
personalization_images_select_authenticated so that INSERT uses WITH CHECK
(bucket_id = 'personalization-images') and SELECT uses USING (bucket_id =
'personalization-images').
recovery/block22_edge_secrets_inventory.md (1)

101-1061: ⚡ Quick win

Mapeamento detalhado útil, mas considere padronização futura.

O mapeamento por função está completo. Observação: SUPABASE_SERVICE_ROLE_KEY tem 11+ aliases diferentes (SERVICE_KEY, serviceRoleKey, localService, etc). Isso não é um bug bloqueador agora, mas dificulta grep/refactoring futuro.

Sugestão: após o recovery, considere criar um helper centralizado getSupabaseServiceKey() em _shared/ para padronizar.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@recovery/block22_edge_secrets_inventory.md` around lines 101 - 1061, Summary:
SUPABASE_SERVICE_ROLE_KEY is referenced with many different local aliases across
edge functions, hindering discoverability and refactoring; create a centralized
accessor to standardize usage. Add a new helper function getSupabaseServiceKey()
in _shared/ (e.g. in _shared/credentials.ts or a new _shared/supabase.ts) that
reads Deno.env.get("SUPABASE_SERVICE_ROLE_KEY") with any fallback logic you
need, export it, and replace direct Deno.env.get(...) calls and local aliases
like SERVICE_KEY, serviceRoleKey, serviceKey, localService, supabaseServiceKey,
SERVICE, etc. in the listed files to call getSupabaseServiceKey(); update places
that construct createClient(...) to use the helper, run tests/linters, and do a
repo-wide grep to ensure no remaining aliases are used.
recovery/block09b_storage_policies_full.sql (1)

41-68: ⚡ Quick win

Policies duplicadas dificultam manutenção

O bucket mockup-art-files tem 7 policies, sendo várias duplicadas com lógica similar mas roles diferentes (authenticated vs public):

  • DELETE: linhas 42-43 + 46-47
  • INSERT: linhas 50-51 + 54-55
  • SELECT: linhas 58-59 + 62-63
  • UPDATE: linha 66-67 (sem par)

Como policies do mesmo comando são combinadas com OR no Postgres, a policy public (mais permissiva) já cobre usuários authenticated na maioria dos casos, tornando as policies authenticated redundantes exceto pela verificação extra de is_supervisor_or_above na linha 59.

♻️ Sugestão de consolidação

Considere manter apenas uma policy por operação, usando role authenticated e incluindo a lógica de supervisor:

-- DELETE consolidado
DROP POLICY IF EXISTS "Users can delete their own art files" ON storage.objects;
DROP POLICY IF EXISTS "Users delete own art files in storage" ON storage.objects;
CREATE POLICY "Users can delete own mockup art files" ON storage.objects 
FOR DELETE TO authenticated
USING ((bucket_id = 'mockup-art-files') 
   AND ((storage.foldername(name))[1] = (auth.uid())::text));

-- INSERT consolidado  
DROP POLICY IF EXISTS "Users can upload their own art files" ON storage.objects;
DROP POLICY IF EXISTS "Users upload own art files to storage" ON storage.objects;
CREATE POLICY "Users can upload own mockup art files" ON storage.objects
FOR INSERT TO authenticated
WITH CHECK ((bucket_id = 'mockup-art-files') 
   AND ((storage.foldername(name))[1] = (auth.uid())::text));

-- SELECT consolidado (mantendo supervisor check)
DROP POLICY IF EXISTS "Users can view their own or shared art files" ON storage.objects;
DROP POLICY IF EXISTS "Users view own art files in storage" ON storage.objects;
CREATE POLICY "Users can view own or shared mockup art files" ON storage.objects
FOR SELECT TO authenticated
USING ((bucket_id = 'mockup-art-files') 
   AND (((storage.foldername(name))[1] = (auth.uid())::text) 
     OR is_supervisor_or_above(auth.uid())));

-- UPDATE consolidado
CREATE POLICY "Users can update own mockup art files" ON storage.objects
FOR UPDATE TO authenticated
USING ((bucket_id = 'mockup-art-files') 
   AND ((storage.foldername(name))[1] = (auth.uid())::text));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@recovery/block09b_storage_policies_full.sql` around lines 41 - 68, There are
duplicate row-level policies on storage.objects (e.g., "Users can delete their
own art files"/"Users delete own art files in storage", insert/select pairs, and
an update without a pair) causing redundancy; consolidate to a single policy per
operation by removing the redundant TO public policies and DROPs and create one
policy per operation on storage.objects using TO authenticated, reusing
storage.foldername(name)[1] = auth.uid()::text for ownership checks and
preserving the is_supervisor_or_above(auth.uid()) check in the SELECT policy;
update policy names (e.g., "Users can delete own mockup art files", "Users can
upload own mockup art files", "Users can view own or shared mockup art files",
"Users can update own mockup art files") and ensure DELETE/INSERT use USING/WITH
CHECK as appropriate and SELECT keeps the supervisor OR clause.
recovery/block12_edge_functions_batch4.md (1)

2118-2118: ⚡ Quick win

Fetch sem timeout em webhook dispatch (risco de hang)

Os fetches para webhook endpoints (linhas 2030, 2118) não definem timeout:

const res = await fetch(hook.url, { method: "POST", headers, body: bodyJson });

Se o endpoint alvo trava ou demora indefinidamente, o request do dispatcher fica pendurado, consumindo recursos e potencialmente bloqueando outros dispatches na fila.

Sugestão: adicionar timeout usando AbortSignal:

const controller = new AbortController();
const timeoutId = setTimeout(() => controller.abort(), 30_000); // 30s timeout

try {
  const res = await fetch(hook.url, {
    method: "POST",
    headers,
    body: bodyJson,
    signal: controller.signal,
  });
  clearTimeout(timeoutId);
  // ... resto do código
} catch (err) {
  clearTimeout(timeoutId);
  if (err.name === 'AbortError') {
    // Timeout atingido
    await supabase.from("webhook_deliveries").insert({
      webhook_id: hook.id, event, payload: payload ?? null, payload_hash: phash,
      status_code: null, response_body_truncated: "Timeout após 30s",
      attempt, success: false, error_message: "Request timeout",
    });
  } else {
    // ... erro de rede normal
  }
}

Also applies to: 2030-2030

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@recovery/block12_edge_functions_batch4.md` at line 2118, The fetch to
hook.url currently has no timeout and can hang; wrap the fetch call with an
AbortController, start a 30s timeout (setTimeout -> controller.abort()), pass
controller.signal into fetch, clear the timeout on success, and in the catch
handle AbortError separately: record a failed delivery into the
webhook_deliveries insert (use webhook_id: hook.id, event, payload,
payload_hash: phash, attempt, success: false, status_code null,
response_body_truncated "Timeout após 30s", error_message "Request timeout"),
otherwise handle normal network errors as before; update both occurrences around
the fetch lines (including the one at line with hook.url and the one at 2030)
and ensure timeoutId is cleared in all paths.
recovery/block12_edge_functions_batch5.md (1)

188-188: ⚡ Quick win

Validação fraca de URL scheme em inputs de imagem (potencial SSRF)

Os schemas Zod validam imageUrl, productImageUrl e logoUrl apenas como z.string().url() ou z.string().min(10), sem restringir o scheme para http/https:

  • magic-up-score (linha 188): imageUrl: z.string().min(10) — não valida se é URL
  • generate-ad-image (linha 533): productImageUrl: z.string().url(), logoUrl: z.string().url().optional()

Se o AI provider (Lovable AI Gateway) faz fetch server-side dessas URLs, um atacante pode passar:

  • file:///etc/passwd (leitura de arquivo local)
  • http://169.254.169.254/latest/meta-data/ (SSRF para metadata AWS)
  • http://internal-service/admin (acesso a serviços internos)

Sugestão: restringir schemes permitidos:

const HttpUrlSchema = z.string().url().refine(
  (url) => url.startsWith('http://') || url.startsWith('https://'),
  { message: 'URL deve usar http ou https' }
);

// Aplicar em:
imageUrl: HttpUrlSchema,
productImageUrl: HttpUrlSchema,
logoUrl: HttpUrlSchema.optional(),

Nota: se o AI provider já filtra schemes no backend, o risco é menor. Mas validação client-side adiciona camada extra de defesa.

Also applies to: 533-533

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@recovery/block12_edge_functions_batch5.md` at line 188, Define a strict HTTP
URL Zod schema (e.g., HttpUrlSchema using z.string().url().refine to allow only
"http://" or "https://") and replace the weak validators: change
magic-up-score's imageUrl (currently z.string().min(10)) to HttpUrlSchema, and
in generate-ad-image replace productImageUrl: z.string().url() and logoUrl:
z.string().url().optional() with productImageUrl: HttpUrlSchema and logoUrl:
HttpUrlSchema.optional(); ensure the new schema is exported/available where
those schemas are defined so server-side fetches cannot accept non-http(s)
schemes.
recovery/block12_edge_functions_batch3.md (1)

1768-1777: ⚡ Quick win

Step-up não exigido para update de metadata de chave FULL (inconsistência com revoke)

O update de name ou description de uma chave MCP com scope FULL não exige step-up (linha 1768-1777 apenas exige para escalating). Comparando com:

  • revoke (linha 1492-1517): exige step-up para QUALQUER chave
  • issue (linha 847-876): exige step-up para QUALQUER chave
  • rotate (linha 1186-1213): exige step-up para QUALQUER chave

Um dev com sessão comprometida pode alterar o name/description de chaves FULL existentes sem confirmação recente (senha+OTP). Isso reduz a fricção para metadata, mas cria inconsistência de segurança.

Sugestão: se o design é intencional (metadata menos sensível), documente explicitamente. Caso contrário, considere exigir step-up para qualquer mutation de chave FULL:

-    if (escalating) {
+    const isFullKey = willBeFull; // true se chave atual ou futura é FULL
+    if (isFullKey && Object.keys(patch).length > 0) {
       if (!step_up_token) {
-        await auditFailure("denied", { reason: "step_up_required" }, key_id);
+        await auditFailure("denied", { reason: "step_up_required", scope: "full" }, key_id);
         return jsonResponse(
-          { error: "step_up_required", message: "Confirme sua identidade (senha + código por e-mail) antes de escalar a chave para escopo total." },
+          { error: "step_up_required", message: "Confirme sua identidade (senha + código por e-mail) antes de modificar uma chave FULL." },
           403,
           requestId,
         );
       }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@recovery/block12_edge_functions_batch3.md` around lines 1768 - 1777, The
metadata update path currently only enforces step-up when "escalating" is true,
allowing updates to name/description on FULL-scoped keys without recent
password+OTP; change the logic handling key metadata updates to require step-up
for any key with scope FULL by checking the key's scope (e.g., key.scope ===
"FULL") in the same place you check "escalating" and "step_up_token"; if no
step_up_token is present for a FULL key, call auditFailure("denied", { reason:
"step_up_required" }, key_id) and return the same jsonResponse( { error:
"step_up_required", ... }, 403, requestId ) flow used elsewhere so metadata
updates follow the same protection as revoke/issue/rotate.

Comment thread recovery/_other.sql
-- Name: public; Type: SCHEMA; Schema: -; Owner: -
--

CREATE SCHEMA public;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Falta proteção IF NOT EXISTS — script vai falhar se o schema já existir.

O comando CREATE SCHEMA public; vai quebrar em ambientes onde o schema public já existe (que é o padrão em PostgreSQL/Supabase). Para execução idempotente (exigência do recovery plan), use CREATE SCHEMA IF NOT EXISTS public;.

🛡️ Fix proposto
-CREATE SCHEMA public;
+CREATE SCHEMA IF NOT EXISTS public;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
CREATE SCHEMA public;
CREATE SCHEMA IF NOT EXISTS public;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@recovery/_other.sql` at line 5, O comando CREATE SCHEMA public; falha se o
schema já existir — torne o script idempotente alterando a instrução CREATE
SCHEMA public para usar a proteção IF NOT EXISTS (ou seja, use a variante que
verifica existência antes de criar) para que a execução do recovery/_other.sql
não quebre em ambientes onde o schema public já existe.

-- Name: file_scan_logs Apenas administradores podem visualizar logs de scan; Type: POLICY; Schema: public; Owner: -
--

CREATE POLICY "Apenas administradores podem visualizar logs de scan" ON public.file_scan_logs FOR SELECT TO authenticated USING (((auth.jwt() ->> 'email'::text) ~~ '%admin%'::text));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
# Localiza todas as policies com checagem por email substring
rg -n -C2 "auth\\.jwt\\(\\).*email|LIKE '%admin%'" recovery/block03_policies.sql

Repository: adm01-debug/Promo_Gifts

Length of output: 276


🏁 Script executed:

# 1. Verificar se existe função is_admin
rg -n "is_admin|CREATE FUNCTION.*admin" recovery/

Repository: adm01-debug/Promo_Gifts

Length of output: 48626


🏁 Script executed:

# 2. Verificar todas as policies com padrões de email inseguros
rg -n "email.*~~|email.*LIKE|auth\.jwt\(\).*email" recovery/

Repository: adm01-debug/Promo_Gifts

Length of output: 1218


🏁 Script executed:

# 3. Verificar estrutura da tabela auth e constraints de email
rg -n "CREATE TABLE.*auth\|ALTER TABLE.*auth\|email" recovery/ | head -30

Repository: adm01-debug/Promo_Gifts

Length of output: 49


🏁 Script executed:

# 4. Procurar por funções de autorização existentes
rg -n "CREATE FUNCTION|RETURNS.*boolean" recovery/block03_policies.sql

Repository: adm01-debug/Promo_Gifts

Length of output: 49


Autorização por email LIKE '%admin%' em múltiplas policies é bypass de privilégio crítico

A policy em linha 476 usa substring de email para controle ('%admin%'), permitindo que qualquer conta com esse padrão no email acesse file_scan_logs. Problema ainda maior: esse padrão vulnerável está replicado em outras policies (block09_storage.sql, block09b_storage_policies_full.sql).

Qualquer usuário com emails como usuario+admin@domain.com, admin-test@domain.com ou similares consegue ler logs sensíveis.

Substituir por public.is_admin(auth.uid()) que já existe na base:

Solução proposta
-- Linha 476 (e políticas equivalentes)
CREATE POLICY "Apenas administradores podem visualizar logs de scan"
  ON public.file_scan_logs
  FOR SELECT TO authenticated
  USING (public.is_admin(auth.uid()));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@recovery/block03_policies.sql` at line 476, Replace the insecure
email-substring authorization in the CREATE POLICY named "Apenas administradores
podem visualizar logs de scan" on table file_scan_logs (and the equivalent
policies in block09_storage.sql and block09b_storage_policies_full.sql) so it
calls the existing admin-check function instead of matching email text;
specifically update the USING clause to invoke public.is_admin(auth.uid())
rather than ((auth.jwt() ->> 'email') ~~ '%admin%') to prevent privilege bypass.

Comment on lines +73 to +90
-- Name: orders set_order_number; Type: TRIGGER; Schema: public; Owner: -
--

CREATE TRIGGER set_order_number BEFORE INSERT ON public.orders FOR EACH ROW EXECUTE FUNCTION public.generate_order_number();


--
-- Name: integration_credentials sync_external_connections_on_credential_change; Type: TRIGGER; Schema: public; Owner: -
--

CREATE TRIGGER sync_external_connections_on_credential_change AFTER INSERT OR DELETE OR UPDATE ON public.integration_credentials FOR EACH ROW EXECUTE FUNCTION public.trg_sync_external_connections();


--
-- Name: orders tr_generate_order_number; Type: TRIGGER; Schema: public; Owner: -
--

CREATE TRIGGER tr_generate_order_number BEFORE INSERT ON public.orders FOR EACH ROW EXECUTE FUNCTION public.generate_order_number_v5();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Verificar se existem outros triggers duplicados no schema
rg -n "CREATE TRIGGER.*BEFORE INSERT.*orders" recovery/

Repository: adm01-debug/Promo_Gifts

Length of output: 1443


🏁 Script executed:

#!/bin/bash
# Procurar por DROP TRIGGER para esses triggers
rg -n "DROP TRIGGER.*(set_order_number|tr_generate_order_number)" recovery/

# Procurar por comentários ou documentação sobre qual usar
rg -B5 -A5 "generate_order_number_v5" recovery/block05_triggers.sql | head -30

# Verificar se há mais triggers BEFORE INSERT na tabela orders além dos já identificados
rg -n "CREATE TRIGGER.*BEFORE INSERT.*public.orders" recovery/block05_triggers.sql

Repository: adm01-debug/Promo_Gifts

Length of output: 946


🌐 Web query:

PostgreSQL multiple BEFORE INSERT triggers same table execution order

💡 Result:

In PostgreSQL, if you define multiple triggers of the same timing and event (e.g., multiple row-level BEFORE INSERT triggers) on the same table, they fire in alphabetical order by trigger name [1]. For BEFORE triggers, each trigger can modify the row; the (possibly modified) NEW row value returned by one BEFORE trigger becomes the input to the next BEFORE trigger [1]. If any BEFORE (or INSTEAD OF) trigger returns NULL for a row, that row’s operation is abandoned and subsequent triggers are not fired for that row [1]. So to control order for multiple BEFORE INSERT triggers on the same table, name them so the desired order corresponds to alphabetical order (e.g., prefix with 01_, 02_, etc.) [1][2].

Citations:


🏁 Script executed:

#!/bin/bash
# Procurar por referências a ambas funções no schema
rg -n "generate_order_number" recovery/block04_functions.sql | head -20

# Procurar se há migrations ou commits que indicam qual é a versão "ativa"
git log --oneline -n 50 --all -- recovery/ 2>/dev/null | grep -i "order_number" || echo "Git history not available"

# Verificar se há comentários no arquivo sobre qual trigger usar
sed -n '70,95p' recovery/block05_triggers.sql

Repository: adm01-debug/Promo_Gifts

Length of output: 1407


🏁 Script executed:

#!/bin/bash
# Ler as funções para confirmar que ambas modificam NEW.order_number
sed -n '2640,2665p' recovery/block04_functions.sql
echo "=== FUNÇÃO V5 ==="
sed -n '2694,2720p' recovery/block04_functions.sql

Repository: adm01-debug/Promo_Gifts

Length of output: 1526


Remover trigger set_order_number — está sobrescrevendo o número gerado.

Dois triggers BEFORE INSERT disparam em ordem alfabética na tabela orders:

  1. set_order_numbergenerate_order_number() (executa primeiro)
  2. tr_generate_order_numbergenerate_order_number_v5() (executa depois e sobrescreve)

PostgreSQL executa triggers BEFORE INSERT em ordem alfabética pelo nome. O segundo trigger sobrescreve o NEW.order_number gerado pelo primeiro. Além disso, as funções geram formatos diferentes:

  • v1: 'PED-' || YYYY || '-' || número
  • v5: 'PED-' || YY || '-' || número

O resultado final é sempre do v5 (trigger tr_generate_order_number), deixando o primeiro trigger inútil e o código confuso.

Delete o trigger set_order_number da linha 76. A versão _v5 é a implementação ativa correta.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@recovery/block05_triggers.sql` around lines 73 - 90, Remova a criação do
trigger redundante "set_order_number" que executa a função
public.generate_order_number() para evitar sobrescrita pelo trigger
"tr_generate_order_number" (que executa public.generate_order_number_v5());
localize e delete a declaração CREATE TRIGGER set_order_number BEFORE INSERT ON
public.orders FOR EACH ROW EXECUTE FUNCTION public.generate_order_number();
deixando apenas o CREATE TRIGGER tr_generate_order_number ... que usa a versão
v5.

Comment on lines +173 to +177
CREATE POLICY "Admins podem visualizar quarentena"
ON storage.objects FOR SELECT TO authenticated
USING (bucket_id = 'quarantine'
AND ((auth.jwt() ->> 'email') LIKE '%admin%'
OR ((auth.jwt() -> 'app_metadata') ->> 'role') = 'admin'));
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

Auth frágil em quarantine — requer is_supervisor_or_above.

Linha 176-177 usa (auth.jwt() ->> 'email') LIKE '%admin%' para autorizar acesso a quarantine. Isso é extremamente frágil:

  • Qualquer email contendo "admin" (ex: badmin@attacker.com) passa
  • Bypass trivial se usuário conseguir manipular email

Fix obrigatório:

🔒 Policy segura usando RBAC
 CREATE POLICY "Admins podem visualizar quarentena"
   ON storage.objects FOR SELECT TO authenticated
   USING (bucket_id = 'quarantine'
-         AND ((auth.jwt() ->> 'email') LIKE '%admin%'
-              OR ((auth.jwt() -> 'app_metadata') ->> 'role') = 'admin'));
+         AND public.is_supervisor_or_above(auth.uid()));

Isso usa a função RBAC já disponível (pré-requisito linha 4) e evita bypass por email spoofing.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@recovery/block09_storage.sql` around lines 173 - 177, The SELECT policy
"Admins podem visualizar quarentena" on storage.objects uses a fragile email
LIKE check via (auth.jwt() ->> 'email') LIKE '%admin%'; replace that condition
with the RBAC check (call the available is_supervisor_or_above() function or
check ((auth.jwt() -> 'app_metadata') ->> 'role') against the RBAC-approved
roles) so only users with supervisor-or-higher privileges can access bucket_id =
'quarantine'; update the USING clause to remove the email LIKE predicate and
rely on the RBAC function/role check (keep bucket_id = 'quarantine' and the
policy name intact).

Comment on lines +137 to +140
DROP POLICY IF EXISTS "Admins podem visualizar quarentena" ON storage.objects;
CREATE POLICY "Admins podem visualizar quarentena" ON storage.objects FOR SELECT TO authenticated
USING (((bucket_id = 'quarantine'::text) AND (((auth.jwt() ->> 'email'::text) ~~ '%admin%'::text) OR (((auth.jwt() -> 'app_metadata'::text) ->> 'role'::text) = 'admin'::text))));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

⚠️ CRÍTICO: Regex de email extremamente permissiva permite bypass de autenticação

A condição (auth.jwt() ->> 'email'::text) ~~ '%admin%'::text aceita qualquer email contendo a substring "admin", incluindo:

  • hacker+admin@evil.com
  • badmin@attacker.org
  • administrator.fake@gmail.com

Como está usando OR, basta passar na primeira condição para ter acesso total à quarentena.

🔒 Correção recomendada
 DROP POLICY IF EXISTS "Admins podem visualizar quarentena" ON storage.objects;
 CREATE POLICY "Admins podem visualizar quarentena" ON storage.objects FOR SELECT TO authenticated
-  USING (((bucket_id = 'quarantine'::text) AND (((auth.jwt() ->> 'email'::text) ~~ '%admin%'::text) OR (((auth.jwt() -> 'app_metadata'::text) ->> 'role'::text) = 'admin'::text))));
+  USING (((bucket_id = 'quarantine'::text) AND (((auth.jwt() -> 'app_metadata'::text) ->> 'role'::text) = 'admin'::text)));

Ou, se precisar validar email, use lista exata:

USING ((bucket_id = 'quarantine'::text) 
  AND ((auth.jwt() ->> 'email') = ANY(ARRAY['admin@promogifts.com.br', 'suporte@promogifts.com.br'])
    OR ((auth.jwt() -> 'app_metadata' ->> 'role') = 'admin')))
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
DROP POLICY IF EXISTS "Admins podem visualizar quarentena" ON storage.objects;
CREATE POLICY "Admins podem visualizar quarentena" ON storage.objects FOR SELECT TO authenticated
USING (((bucket_id = 'quarantine'::text) AND (((auth.jwt() ->> 'email'::text) ~~ '%admin%'::text) OR (((auth.jwt() -> 'app_metadata'::text) ->> 'role'::text) = 'admin'::text))));
DROP POLICY IF EXISTS "Admins podem visualizar quarentena" ON storage.objects;
CREATE POLICY "Admins podem visualizar quarentena" ON storage.objects FOR SELECT TO authenticated
USING (((bucket_id = 'quarantine'::text) AND (((auth.jwt() -> 'app_metadata'::text) ->> 'role'::text) = 'admin'::text)));
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@recovery/block09b_storage_policies_full.sql` around lines 137 - 140, The
policy "Admins podem visualizar quarentena" on storage.objects uses a permissive
pattern match (auth.jwt() ->> 'email' ~~ '%admin%') that grants access to any
email containing "admin"; replace that predicate with a strict check against
exact allowed emails or a membership test (e.g., compare auth.jwt() ->> 'email'
for equality or = ANY(ARRAY[...])) and keep the role check ((auth.jwt() ->
'app_metadata' ->> 'role') = 'admin') as the fallback; update the USING clause
in the CREATE POLICY for "Admins podem visualizar quarentena" to require
bucket_id = 'quarantine' AND (exact-email-membership OR role-check) so only
explicit admin addresses or role=admin are authorized.

});

// We resolve the auth context from a per-request module-level holder.
let currentCtx: AuthCtx | null = null;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | 🏗️ Heavy lift

Race condition crítica em currentCtx (module-level variable compartilhada)

currentCtx é uma variável module-level compartilhada entre todas as requisições concorrentes. Em Deno.serve, múltiplas requests rodam no mesmo isolate:

  1. Request A chega, seta currentCtx = auth.ctx (linha 583)
  2. Request B chega imediatamente, sobrescreve currentCtx com seu próprio contexto
  3. Request A executa um tool handler que lê currentCtx (linha 314) — MAS agora tem o contexto de B
  4. Tool de A é executado com scopes/keyId de B → violação de autorização

Impacto: um usuário com chave limitada pode ter seus tools executados com scopes de outra chave ativa simultaneamente (potencialmente FULL access).

Fix: passar ctx como parâmetro explícito no closure do handler em vez de ler de variável global:

 function defineTool<I>(
   name: string,
   guard: ToolGuard,
   description: string,
   inputSchema: Record<string, unknown>,
   handler: ToolHandler<I>,
 ) {
   mcpServer.tool(name, {
     description: `${description} [scope: ${guard.scope} | mode: ${guard.mode}]`,
     inputSchema,
-    handler: async (input: I): Promise<ToolResult> => {
-      const ctx = currentCtx;
+    handler: async (input: I, runtimeCtx: AuthCtx | null): Promise<ToolResult> => {
+      const ctx = runtimeCtx;
       const startedAt = Date.now();
       const payloadSummary = summarizePayload(input);
       try {
         const authed = authorizeTool(ctx, name, guard);

E no bind:

 app.all("/*", async (c) => {
   const auth = await authenticate(c.req.raw);
   // ... validações ...
   const ctx = auth.ctx;
-  currentCtx = ctx;
   try {
-    const res = await httpHandler(c.req.raw);
+    const res = await httpHandler(c.req.raw, ctx); // passar ctx
     // ...
-  } finally {
-    currentCtx = null;
   }
 });

Also applies to: 583-583, 314-314

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@recovery/block12_edge_functions_batch3.md` at line 361, currentCtx is a
module-level AuthCtx shared across requests causing a race; stop reading/writing
currentCtx and instead thread the request-specific AuthCtx through closures and
bind calls: remove usage of the global currentCtx, change any code that sets
currentCtx = auth.ctx to pass auth.ctx as an explicit parameter into the tool
handler factory/closure (e.g., when creating/binding handlers for tools) and
update the bind/invoke sites so each handler captures its own ctx variable
(AuthCtx) rather than accessing currentCtx; ensure all places that previously
read currentCtx (tool handler code paths) accept ctx as a param or capture it in
their closure so request A cannot read request B's context.

Comment on lines +42 to +46
1. Usuário clica em link recebido (template default Supabase).
2. Frontend extrai `token` (= `user.id`) e chama `POST /verify-email`.
3. Edge usa `auth.admin.getUserById(token)` para validar.
4. Marca `email_confirm: true` via `auth.admin.updateUserById`.
5. Retorna `{ success: true }` ou `400 token inválido/expirado`.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
# Verifica implementação da edge function verify-email
fd -i "verify-email" supabase/functions
rg -n -C3 "getUserById|updateUserById|token|email_confirm|confirm" supabase/functions --iglob "*verify-email*"

Repository: adm01-debug/Promo_Gifts

Length of output: 100


🏁 Script executed:

find supabase/functions/verify-email -type f -name "*.ts" -o -name "*.js" -o -name "*.tsx" -o -name "*.jsx"

Repository: adm01-debug/Promo_Gifts

Length of output: 108


🏁 Script executed:

cat -n recovery/block16_auth_hooks.md | head -60

Repository: adm01-debug/Promo_Gifts

Length of output: 3150


🏁 Script executed:

fd -type f "verify-email" supabase/functions --exec cat {}

Repository: adm01-debug/Promo_Gifts

Length of output: 238


🏁 Script executed:

cat -n supabase/functions/verify-email/index.ts

Repository: adm01-debug/Promo_Gifts

Length of output: 3213


🏁 Script executed:

cat -n recovery/block16_auth_hooks.md | sed -n '40,80p'

Repository: adm01-debug/Promo_Gifts

Length of output: 2006


Token previsível e sem expiração em verify-email permite bypass de confirmação

A função verify-email usa user.id (UUID) como token sem expiração ou validação de unicidade. Isso permite que qualquer pessoa com acesso ao UUID do usuário confirme indevida e permanentemente o email, mesmo fora da janela do link original. Implemente token assinado/criptografado com TTL curto (15-30min), use token de uso único em base de dados, ou migre para fluxo nativo de confirmação da Supabase Auth.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@recovery/block16_auth_hooks.md` around lines 42 - 46, A atual rota
verify-email aceita o user.id como token sem expiração nem unicidade, permitindo
confirmação indevida; change verify-email to validate a short-lived, single-use
token instead of raw UUID: generate a signed/crypted token with TTL (15–30min)
when sending the email and store a one-time token record in the DB (or use
Supabase Auth’s native confirmation flow), then in verify-email verify the
signature and TTL (or lookup and consume the one-time token record) before
calling auth.admin.updateUserById; remove acceptance of raw user.id in
auth.admin.getUserById and ensure tokens are invalidated after use.

Comment on lines +63 to +69
- Edge chama RPC `start_step_up_challenge()` que devolve
`{ challenge_id, otp_plain, expires_at }` e dispara email com OTP
(via `send-notification` ou template inline).
2. `step="verify_password"` → confere senha do usuário (re-login).
3. `step="verify_otp"` → valida OTP e emite **step-up token** (de
curta duração) consumido pelos endpoints sensíveis via
header `X-Step-Up-Token`.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/usr/bin/env bash
# Confirma se otp_plain está no retorno da edge/RPC
rg -n -C3 "start_step_up_challenge|otp_plain|verify_otp|step-up" supabase/functions supabase/migrations recovery

Repository: adm01-debug/Promo_Gifts

Length of output: 50381


🏁 Script executed:

#!/bin/bash
# Verificar o que é retornado ao cliente HTTP na edge function step-up-verify
rg -n -A 10 "step=\"request\"" supabase/functions/step-up-verify/index.ts | head -60

Repository: adm01-debug/Promo_Gifts

Length of output: 49


🏁 Script executed:

#!/bin/bash
# Verificar resposta HTTP ao cliente quando step="request"
cat supabase/functions/step-up-verify/index.ts | head -250 | tail -100

Repository: adm01-debug/Promo_Gifts

Length of output: 3237


🏁 Script executed:

#!/bin/bash
# Confirmar se otp_plain é usado em logs ou outras respostas
rg -n "otp_plain|otp:" supabase/functions/step-up-verify/index.ts

Repository: adm01-debug/Promo_Gifts

Length of output: 392


RPC start_step_up_challenge() não deve retornar otp_plain em plaintext

O contrato { challenge_id, otp_plain, expires_at } (linha 104 da migration 20260425203103) expõe o OTP em plaintext na camada backend. Embora o HTTP response ao cliente omita o otp_plain (retorna apenas challenge_id, expires_at), a RPC retorna plaintext para a edge function, criando surface de vazamento: logs, monitoramento, memory dumps, insider risk.

Refatoração necessária:

  • RPC deve retornar apenas { challenge_id, hash_otp, expires_at }
  • RPC gera OTP, envia por email internamente, retorna apenas hash para comparação
  • Ou: edge function chama segundo RPC send_otp_and_get_hash() que trata dispatch + retorna hash
  • Eliminar linhas 195, 209 que lidam com plaintext no edge function
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@recovery/block16_auth_hooks.md` around lines 63 - 69, O RPC
start_step_up_challenge() não deve retornar otp_plain — altere sua assinatura
para retornar { challenge_id, hash_otp, expires_at } e faça com que o RPC gere o
OTP, envie o email internamente e só exponha o hash; alternativamente,
implementem um segundo RPC send_otp_and_get_hash() que cuide do dispatch e
retorne o hash. Atualize o edge function que chama start_step_up_challenge()
para não readicionar ou logar otp_plain (remova qualquer uso/variável/registro
de otp_plain) e passe apenas challenge_id/expires_at to the client; ajuste a
validação em verify_otp() para comparar hash_otp em vez de plaintext. Ensure no
otp_plain flows into logs, monitoring or memory paths.

Comment on lines +182 to +183
| secure-upload | upload de arquivos | **Adicionar `authorize`** ⚠️ alto risco |
| sync-quote-bitrix | sync CRM | **Adicionar `authorize`** ⚠️ |
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

CRÍTICO: secure-upload e sync-quote-bitrix SEM autenticação.

Linha 182: secure-upload permite upload de arquivos SEM nenhuma autenticação detectável. Isso é vetor de ataque direto (upload de malware, exhaustion de storage, etc).

Linha 183: sync-quote-bitrix faz sync de CRM também sem auth. Atacante pode injetar dados falsos no CRM.

Ação obrigatória antes de produção:

🚨 Adicionar auth imediatamente
// supabase/functions/secure-upload/index.ts
import { authorize } from "../_shared/edge-authz.ts";

const auth = await authorize(req, {
  requireRole: 'authenticated',  // ou 'supervisor' se só admins podem upload
  enforceServerSide: true,
});
if (!auth.ok) return auth.response;

Mesmo padrão para sync-quote-bitrix.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@recovery/block20_edge_functions_auth_audit.md` around lines 182 - 183, As
correção: ambos endpoints estão sem autenticação; injetar a verificação de
autorização chamando a função authorize do shared edge-authz em cada handler
(por exemplo no arquivo supabase/functions/secure-upload/index.ts e no handler
de sync-quote-bitrix) e exigir papel/escopo adequado (por exemplo requireRole:
'authenticated' ou 'supervisor') com enforceServerSide: true; atribua o
resultado a uma variável (ex. auth), cheque auth.ok e retorne auth.response
quando false para bloquear requisições não autorizadas antes de processar
uploads ou syncs.

@adm01-debug adm01-debug merged commit ac35c06 into main May 11, 2026
14 of 15 checks passed
adm01-debug added a commit that referenced this pull request May 11, 2026
Detectado via Supabase Security Advisor após o merge do PR #143:
a tabela auth_login_attempts (criada no D.2.1) ficou com RLS habilitado
mas sem policies, tornando-a inacessível para todos os roles exceto
service_role (que bypassa RLS).

Root cause: o extrator regex do dump Lovable não capturou as policies
dessa tabela específica no patch.sql do D.2.1.

Fix aplicado em PROD:
- Admins podem SELECT (auditoria)
- Service role pode INSERT (Edge Functions de auth)

Padrão idêntico ao usado em audit_logs (mesma natureza).

Documentação: recovery/agent-db/POST_MERGE_FIX_AUTH_LOGIN_ATTEMPTS.md
ISSUES.md: Issue #10 fechada
adm01-debug added a commit that referenced this pull request May 12, 2026
Recovery completo dos batches D.3-D.5 após merge do PR #143:

- 85/85 funções extraídas do block04_functions.sql aplicadas em PROD
- Agrupadas em 11 batches funcionais:
  * D.4.1 Step-Up MFA (8 funcs)
  * D.4.2 Quote Advanced (15 funcs)
  * D.4.3 Ownership Audit (1 func)
  * D.4.5 Reactions+Trash (3 funcs)
  * D.4.6 Security/Auth Rate Limit (17 funcs)
  * D.4.7 MCP Advanced (10 funcs)
  * D.2.2 Webhooks Extra (4 funcs, URL adaptada)
  * D.2.4 External Conn Extra (3 funcs)
  * D.3.4 Role Migration (1 func)
  * D.3.5 Analytics/UX (3 funcs, types uuid)
  * D.5 Misc Cross-cutting (19 funcs)

3 tables auxiliares criadas (deps):
- e2e_cleanup_rate_limit
- security_settings
- organization_members

Adaptações vs Lovable:
- URLs hardcoded substituídas (nmojwp -> doufsx)
- maintain_webhook_metrics sem partition logic
- Types text -> uuid em get_bundle_suggestions, get_client_seasonality,
  get_industry_seasonality, fn_create_quote_v3

Validação: 85/85 funcoes aplicadas com SECURITY DEFINER + search_path.

Refs: Decision 009, BATCH_D3_D5_COMPLETE.md, EXECUTION_LOG.md
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants