Skip to content

Feat/contracts 08 zod pinning barrel#155

Merged
adm01-debug merged 25 commits into
mainfrom
feat/contracts-08-zod-pinning-barrel
May 24, 2026
Merged

Feat/contracts 08 zod pinning barrel#155
adm01-debug merged 25 commits into
mainfrom
feat/contracts-08-zod-pinning-barrel

Conversation

@adm01-debug
Copy link
Copy Markdown
Owner

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

📋 Descrição

🎯 Tipo de mudança

  • 🚀 feat — nova funcionalidade
  • 🐛 fix — correção de bug
  • ♻️ refactor — refatoração (sem mudança de comportamento)
  • 🔧 chore — manutenção, deps, config
  • 📚 docs — documentação
  • ⚡ perf — performance
  • 🔒 security — segurança
  • 🚨 hotfix — correção urgente em produção
  • 💥 breaking change — quebra compatibilidade

🔗 Issues relacionadas

Closes #
Refs #

🌐 Sistemas afetados

  • Bitrix24 (CRM, SPAs, BizProc)
  • Supabase (DB, Edge Functions, RLS, migrations)
  • n8n (workflows)
  • Evolution API / WhatsApp
  • Bling (NFe, OAuth)
  • Cloudflare (Workers, Images, Tunnels)
  • Frontend (UI, dashboards)
  • CI / GitHub Actions
  • Outro: ____

🧪 Como testar

✅ Checklist pré-merge

Qualidade

  • Código segue style guide (ESLint passa)
  • npx tsc --noEmit passa sem erros
  • Testes passam (npm run test)
  • Adicionei testes para novas funcionalidades quando aplicável
  • CodeRabbit revisou o PR (ou justificativa para skip)

Segurança

  • Sem secrets, tokens ou credenciais hardcoded
  • Variáveis de ambiente novas documentadas
  • Sem console.log com payloads sensíveis (usar logger.*)
  • RLS revisado se houve mudança em tabelas
  • Edge functions: input validado com Zod

Documentação

  • Atualizei docs (README / CHANGELOG / docs/) se necessário
  • Memória atualizada (mem://) se a mudança afetar arquitetura/regras
  • Migrations com backup em _backup_*_YYYYMMDD se destrutivas

UI

  • Componentes usam tokens semânticos (sem cores hardcoded)
  • Screenshots / vídeo anexados (se mudança visual)

📸 Screenshots (se UI)

🔄 Plano de rollback

⚠️ Notas para o reviewer


Summary by cubic

Introduce a centralized contract validation package with pinned zod, unified errors, and versioning; migrate 13 Edge Functions with v1 default and opt‑in v2 via accept-version or ?v=. For webhook-inbound, add optional v1 gating via WEBHOOK_INBOUND_V1_COMPAT_ENABLED and issuer allowlist to encourage v2 adoption.

  • New Features

    • Added _shared/contracts with parseContract, standardized {code,message,fields} errors, and versioning headers.
    • Pinned zod via _shared/contracts/_zod.ts; zod-validate.ts now imports the central pin.
    • Migrated 13 functions to parseContract (includes webhook-inbound); v2 adds stricter schemas and idempotency keys where needed.
    • Docs and contract tests added; vitest config aliases esm.sh zod to npm zod.
  • Migration

    • No breaking changes: v1 remains default with Deprecation/Sunset headers.
    • To adopt v2, send accept-version: 2 or ?v=2 and update payloads to stricter schemas.
    • For webhook-inbound, v1 can be restricted when WEBHOOK_INBOUND_V1_COMPAT_ENABLED=true unless issuer is in WEBHOOK_INBOUND_V1_ALLOWLIST (falls back to v2).

Written for commit 7d2cfe5. Summary will update on new commits. Review in cubic

Summary by CodeRabbit

Release Notes

  • New Features

    • Novo pacote de validação de contratos para Edge Functions com suporte a versionamento de API.
    • Erros padronizados com estrutura consistente (código, mensagem, campos) em todas as respostas.
    • Negociação automática de versão via headers e query parameters com suporte a deprecação.
    • Headers de deprecação (RFC-style) indicando datas de sunset e URLs de migração.
  • Documentation

    • Guia de migração detalhado para converter Edge Functions legadas.
    • README documentando estrutura, uso e boas práticas de contratos.
  • Tests

    • Suite de testes para validação de contratos em múltiplos endpoints.

Review Change Stack

…rseContract (backward-compat v1; opt-in v2 via accept-version)
…t of 14 funcs + 5-step recipe + special cases)
Copilot AI review requested due to automatic review settings May 23, 2026 16:00
@vercel
Copy link
Copy Markdown

vercel Bot commented May 23, 2026

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

Project Deployment Actions Updated (UTC)
we-dream-big Error Error May 24, 2026 3:14pm

@supabase
Copy link
Copy Markdown

supabase Bot commented May 23, 2026

This pull request has been ignored for the connected project doufsxqlfjyuvxuezpln due to reaching the limit of concurrent preview branches.
Go to Project Integrations Settings ↗︎ if you wish to update this limit.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 23, 2026

Walkthrough

Cria um pacote _shared/contracts centralizado que padroniza validação, versionamento e formatação de erro para todas as Edge Functions. Migra ~20 handlers para usar parseContract, adicionando suporte a negociação de versão (header accept-version, query ?v=), deprecação com headers RFC 8594, e resposta de erro canônica com code, message e fields com notação de path. Inclui 16 schemas Zod versionados (v1 compatível + v2 strict com idempotência), testes abrangentes e documentação de migração.

Changes

Contract Infrastructure & Schemas

Layer / File(s) Summary
Core types, error responses, Zod centralization
supabase/functions/_shared/contracts/_zod.ts, supabase/functions/_shared/contracts/errors.ts
Define tipos canônicos (ContractErrorCode, FieldIssue, ContractError), builders de Response para HTTP 400/422/406, conversão de ZodError em lista plana de FieldIssue com notação dot/índice; centraliza Zod v3.23.8 em módulo _zod.ts.
Contract parsing, version resolution, test helpers
supabase/functions/_shared/contracts/parse.ts, supabase/functions/_shared/contracts/versioning.ts, tests/contracts/_helpers.ts
Implementa parseContract que resolve versão, lê body (com preread opcional), parseia JSON, valida com schema e retorna ParseResult tipado; resolveContractVersion negocia via header/query, monta headers RFC 8594 (Deprecation, Sunset, Link); helpers para testes agnósticos de runtime.
Schemas for simple endpoints (v1/v2 pairs)
supabase/functions/_shared/contracts/schemas/{bi-copilot,kit-ai-builder,send-transactional-email,market-intelligence-insights,ownership-*,force-global-logout,block-ip-temporarily,trends-insights,sync-external-db}.ts
Defini contratos Zod versionados: v1 compatível (campos opcionais) e v2 strict (.strict(), idempotência UUID, validações mais rigorosas). Cada endpoint exporta *V1, *V2 e *Schemas com metadados de deprecação.
Schemas for complex endpoints (discriminated unions)
supabase/functions/_shared/contracts/schemas/{product-webhook,webhook-dispatcher,webhook-inbound}.ts
Define product-webhook v2 com superRefine condicional (exige product em upsert, external_ids em delete, etc.); webhook-dispatcher v2 com union discriminada por mode (dispatch/replay/test); webhook-inbound v1 passthrough vs v2 envelope strict. Exporta tipos inferidos.
Barrel exports, Zod centralization
supabase/functions/_shared/contracts/index.ts, supabase/functions/_shared/zod-validate.ts, vitest.config.ts
Cria barrel que reexporta errors/versioning/parsing/zod; atualiza zod-validate.ts para importar z via ./contracts/_zod.ts; adiciona aliases Vitest para resolver zod de ESM URLs para npm.

Handler Migrations

Layer / File(s) Summary
Simple endpoint handlers (6)
supabase/functions/{bi-copilot,force-global-logout,kit-ai-builder,send-transactional-email,simulation-orchestrator,trends-insights}/index.ts
Substitui req.json() + validação manual por parseContract(req, *Schemas, { corsHeaders }); adiciona contractResponseHeaders para headers de deprecação; mescla headers de resposta; retorna contractResult.response em erro.
Complex webhook handlers (3)
supabase/functions/{product-webhook,webhook-dispatcher,webhook-inbound}/index.ts
product-webhook: muda para switch(data.action) com outcome de contadores, usa version para determinar source do log. webhook-dispatcher: normaliza v1/v2 para variáveis internas unificadas. webhook-inbound: preread body para HMAC, armazena contract_version, retorna JSON estruturado para erros.
Remaining handlers (7)
supabase/functions/{block-ip-temporarily,e2e-cleanup,ownership-audit,ownership-repair,sync-external-db,market-intelligence-insights,step-up-verify}/index.ts
Substitui body parsing por parseContract; mescla contractResponseHeaders em todas as respostas JSON; e2e-cleanup ajusta regras de presença; market-intelligence-insights propaga headers através de cache/erro/sucesso; step-up-verify usa estado de módulo.

Tests & Documentation

Layer / File(s) Summary
Contract tests, smoke testing setup
tests/contracts/{errors,versioning,migrated-endpoints,product-webhook,send-transactional-email,step-up-verify,webhooks}.test.ts, scripts/contract-testing.mjs
Testa error builders, version resolution, v1/v2 acceptance/rejection por endpoint; reescreve contract-testing.mjs para smoke tests HTTP reais (fetch + timeout + validação robusta de status/code/fields/headers).
Migration guide and architecture documentation
docs/contracts/{README,MIGRATION_GUIDE}.md
README descreve arquitetura, formato de erro, política de versionamento, como adicionar schemas; MIGRATION_GUIDE fornece passo a passo de migração com casos especiais (prereadBody, discriminated unions, backward compatibility v1/v2).

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

A PR implementa uma infraestrutura fundamental e ampla: novos tipos e builders de erro, orquestrador de parsing com estado complexo, 16 schemas com validações condicionais, migração de ~20 handlers com padrões repetitivos mas heterogêneos (alguns simples, outros com lógica de webhook/preread/discriminated unions). Requer atenção a: tipos Zod genéricos (ContractSchemas<V>), paths aninhados em FieldIssue, negociação de versão com prioridade header/query, normalização de payload v1/v2, e propagação consistente de headers de deprecação RFC 8594. Testes cobrem matriz adequada (valid/invalid JSON/validation/unsupported version/deprecation headers), mas lógica de webhook é densa. Sem security risks óbvios (Zod está central, parsing é defensivo, HMAC via timingSafeEqual), mas mudança de escopo de parsing em 20 funções exige validação em staging.

Possibly related issues

Possibly related PRs

✨ 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 feat/contracts-08-zod-pinning-barrel

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: bea6aae82b

ℹ️ 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 +111 to +114
if (!rawText || rawText.trim() === "") {
return {
ok: false,
response: missingBodyResponse({
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Allow empty bodies for backward-compatible v1 contracts

This helper rejects any request with an empty body before schema validation, which breaks several migrated v1 endpoints that previously accepted no body and applied defaults (for example ownership-audit/ownership-repair handlers and schemas documented as body-optional). A cron or manual call that sends no payload now returns 400 missing_body instead of running with defaults, so the migration is no longer backward compatible for existing callers that POST without a body.

Useful? React with 👍 / 👎.

// parseContract retorna 400 (json inválido/vazio), 422 (validação), 406 (versão).
const contractResult = await parseContract(req, WebhookDispatcherSchemas, { corsHeaders });
if (!contractResult.ok) return contractResult.response;
const { version: contractVersion, data: parsedData } = contractResult;
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 Preserve negotiated contract headers in dispatcher responses

After parseContract succeeds, this code discards responseHeaders and only keeps version/data, so success responses from webhook-dispatcher never include x-contract-version or deprecation headers. Clients requesting v1/v2 therefore cannot observe negotiated versioning metadata (especially deprecation signals), which defeats the new contract-version migration behavior for this endpoint.

Useful? React with 👍 / 👎.

corsHeaders,
prereadBody: rawBody,
});
if (!contractResult.ok) return contractResult.response;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Record inbound webhook attempts before contract rejection

This early return exits before the event/audit writes below, so empty or malformed webhook payloads are no longer inserted into inbound_webhook_events and no longer increment endpoint counters. Prior behavior still persisted these failed attempts (with invalid signature/error metadata), so this change introduces observability blind spots for abuse/debugging and violates the handler’s “records every event” contract whenever parseContract fails.

Useful? React with 👍 / 👎.

Comment on lines +82 to +87
const contractResult = await parseContract(req, WebhookInboundSchemas, {
corsHeaders,
prereadBody: rawBody,
});
if (!contractResult.ok) return contractResult.response;
const { version, data: payloadParsed, responseHeaders } = contractResult;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Verify webhook signature before returning contract errors

Contract validation now runs and can return 400/422 before HMAC verification, which lets unauthenticated callers probe schema/field-level errors on webhook-inbound instead of receiving a uniform signature failure. The previous flow validated signature first and only then processed payload semantics, so this change weakens the auth boundary and exposes internal contract details to requests without a valid signature.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Contributor

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.

Pull request overview

This PR introduces a shared, versioned contract-validation layer for Supabase Edge Functions, aiming to standardize request parsing, error responses, and contract version negotiation across multiple endpoints (with accompanying unit + smoke tests and documentation).

Changes:

  • Added _shared/contracts package (parseContract, version negotiation, canonical error format, and a Zod pinning entrypoint) plus per-endpoint versioned schemas.
  • Migrated several Edge Functions to use parseContract and to emit version/deprecation headers.
  • Added a Vitest-based contract test suite + updated contract-testing.mjs for real HTTP smoke tests; documented usage and migration steps.

Reviewed changes

Copilot reviewed 50 out of 50 changed files in this pull request and generated 25 comments.

Show a summary per file
File Description
vitest.config.ts Adds module aliasing so Vitest (Node) can resolve Deno-style Zod URL imports to npm zod.
tests/contracts/_helpers.ts Adds Request/Response helpers and a canonical contract-error assertion helper.
tests/contracts/errors.test.ts Tests canonical error response builders and Zod → field-issues conversion.
tests/contracts/versioning.test.ts Tests version negotiation priority and deprecated/unsupported version behaviors.
tests/contracts/webhooks.contract.test.ts Adds contract tests for webhook-inbound + webhook-dispatcher schemas.
tests/contracts/step-up-verify.contract.test.ts Adds contract tests for step-up-verify v1/v2 schemas.
tests/contracts/send-transactional-email.contract.test.ts Adds contract tests for send-transactional-email v1/v2 schemas.
tests/contracts/product-webhook.contract.test.ts Adds contract tests for product-webhook v1/v2 schemas and strictness/idempotency.
tests/contracts/migrated-endpoints.contract.test.ts Adds a consolidated smoke suite covering multiple migrated endpoints.
supabase/functions/webhook-inbound/index.ts Migrates webhook-inbound to parseContract, adds version/deprecation headers, and records contract_version.
supabase/functions/webhook-dispatcher/index.ts Migrates body parsing/validation to contract schemas and normalizes v1/v2 payloads.
supabase/functions/trends-insights/index.ts Migrates request validation to contracts and propagates version headers on success paths.
supabase/functions/sync-external-db/index.ts Migrates request validation to contracts and propagates version headers on success paths.
supabase/functions/step-up-verify/index.ts Migrates request validation to contracts and injects contract headers into JSON responses.
supabase/functions/simulation-orchestrator/index.ts Migrates request validation to contracts and includes version headers on success.
supabase/functions/send-transactional-email/index.ts Migrates request validation to contracts and includes version headers on success.
supabase/functions/product-webhook/index.ts Migrates request validation to contracts, introduces v2 strict schema, and emits version headers on success.
supabase/functions/ownership-repair/index.ts Migrates request validation to contracts and injects contract headers into responses.
supabase/functions/ownership-audit/index.ts Migrates request validation to contracts and injects contract headers into responses.
supabase/functions/market-intelligence-insights/index.ts Migrates request validation to contracts and propagates version headers on main response paths.
supabase/functions/kit-ai-builder/index.ts Migrates request validation to contracts and emits version headers on success.
supabase/functions/force-global-logout/index.ts Migrates request validation to contracts and injects contract headers into responses.
supabase/functions/e2e-cleanup/index.ts Migrates request validation to contracts and injects contract headers into responses/audit handling.
supabase/functions/block-ip-temporarily/index.ts Migrates request validation to contracts and injects contract headers into responses.
supabase/functions/bi-copilot/index.ts Migrates request validation to contracts and emits version headers on success.
supabase/functions/_shared/zod-validate.ts Switches to importing Zod via the contracts pinning entrypoint.
supabase/functions/_shared/contracts/index.ts Adds barrel exports for contracts package and re-exports pinned Zod.
supabase/functions/_shared/contracts/parse.ts Implements canonical parseContract (version resolve + JSON parse + Zod validate).
supabase/functions/_shared/contracts/errors.ts Implements canonical error types/builders (400/422/406) and Zod error flattening.
supabase/functions/_shared/contracts/versioning.ts Implements accept-version / ?v= negotiation + RFC 8594 deprecation headers.
supabase/functions/_shared/contracts/_zod.ts Introduces “single pin” Zod import location for contracts.
supabase/functions/_shared/contracts/schemas/webhook-inbound.ts Adds webhook-inbound versioned schemas (v1 passthrough, v2 strict envelope).
supabase/functions/_shared/contracts/schemas/webhook-dispatcher.ts Adds webhook-dispatcher versioned schemas (v2 discriminated union).
supabase/functions/_shared/contracts/schemas/trends-insights.ts Adds trends-insights versioned schemas.
supabase/functions/_shared/contracts/schemas/sync-external-db.ts Adds sync-external-db versioned schemas (v2 strict + since datetime).
supabase/functions/_shared/contracts/schemas/step-up-verify.ts Adds step-up-verify versioned schemas (v2 discriminated union).
supabase/functions/_shared/contracts/schemas/simulation-orchestrator.ts Adds simulation-orchestrator versioned schemas (v2 strict + idempotency).
supabase/functions/_shared/contracts/schemas/send-transactional-email.ts Adds send-transactional-email versioned schemas (v2 strict + idempotency).
supabase/functions/_shared/contracts/schemas/product-webhook.ts Adds product-webhook versioned schemas (v2 strict + idempotency + cross-field validation).
supabase/functions/_shared/contracts/schemas/ownership-repair.ts Adds ownership-repair versioned schemas.
supabase/functions/_shared/contracts/schemas/ownership-audit.ts Adds ownership-audit versioned schemas.
supabase/functions/_shared/contracts/schemas/market-intelligence-insights.ts Adds market-intelligence-insights versioned schemas (v2 UUID validations).
supabase/functions/_shared/contracts/schemas/kit-ai-builder.ts Adds kit-ai-builder versioned schemas (v2 idempotency).
supabase/functions/_shared/contracts/schemas/force-global-logout.ts Adds force-global-logout versioned schemas (v2 idempotency).
supabase/functions/_shared/contracts/schemas/e2e-cleanup.ts Adds e2e-cleanup versioned schemas (v2 strict + confirm + idempotency).
supabase/functions/_shared/contracts/schemas/block-ip-temporarily.ts Adds block-ip-temporarily versioned schemas (v2 stricter regex).
supabase/functions/_shared/contracts/schemas/bi-copilot.ts Adds bi-copilot versioned schemas (v2 strict + context required).
scripts/contract-testing.mjs Reworks smoke test runner to hit live functions and validate canonical errors/headers.
docs/contracts/README.md Documents the contracts package usage, error format, and testing approach.
docs/contracts/MIGRATION_GUIDE.md Provides migration steps/patterns for moving endpoints to the contracts package.
Comments suppressed due to low confidence (1)

supabase/functions/kit-ai-builder/index.ts:46

  • Depois que parseContract resolve a versão, responseHeaders deveriam ser anexados a todas as respostas (inclusive 500/402/429) para manter x-contract-version + Deprecation/Sunset consistentes. Aqui apenas a resposta 200 usa responseHeaders; os retornos de erro (ex.: LOVABLE_API_KEY ausente, falha do AI gateway, catch) ainda retornam só com corsHeaders.
    const contractResult = await parseContract(req, KitAiBuilderSchemas, {
      corsHeaders,
    });
    if (!contractResult.ok) return contractResult.response;
    const { data: body, responseHeaders } = contractResult;
    const prompt = body.prompt.trim();

    const LOVABLE_API_KEY = Deno.env.get('LOVABLE_API_KEY');
    if (!LOVABLE_API_KEY) {
      return new Response(
        JSON.stringify({ error: 'LOVABLE_API_KEY não configurado' }),
        { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
      );

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +2 to +10
* _zod.ts — Pinning ÚNICO de Zod para todo o projeto.
*
* Esta é a ÚNICA URL de Zod que pode existir em qualquer arquivo do projeto.
* Todos os demais módulos (incluindo `index.ts` deste pacote) devem importar
* `z` daqui via path relativo.
*
* Para subir/descer versão de Zod, edite somente este arquivo.
*
* Regra reforçada por ESLint (no-restricted-imports) em `eslint.config.js`.
Comment thread docs/contracts/README.md
Comment on lines +100 to +106
```ts
// supabase/functions/_shared/contracts/schemas/<endpoint>.ts
import { z } from "https://esm.sh/zod@3.23.8";

export const MyEndpointV1 = z.object({ /* ... */ });
export const MyEndpointV2 = z.object({ /* ... */ }).strict();

Comment on lines +47 to +55
```ts
import { z } from "https://esm.sh/zod@3.23.8";

export const SendEmailV1 = z.object({
event_type: z.enum(["quote_sent", "quote_approved", "quote_rejected", "order_created"]),
recipient_email: z.string().email().max(255),
recipient_name: z.string().max(150).optional(),
data: z.record(z.unknown()),
});
* v2 = strict envelope: `event`, `occurred_at`, `data` exigidos.
*/

import { z } from "https://esm.sh/zod@3.23.8";
* campos coerentes para cada modo via discriminated union.
*/

import { z } from "https://esm.sh/zod@3.23.8";
Comment on lines 18 to 31
// Module-scope CORS headers — atribuído per-request no handler.
let corsHeaders: Record<string, string> = {};
let contractResponseHeaders: Record<string, string> = {};

type RepairOrphansResult = {
report_id?: string;
totals?: Record<string, unknown>;
} & Record<string, unknown>;

Deno.serve(async (req) => {
corsHeaders = getCorsHeaders(req);
contractResponseHeaders = {};
if (req.method === "OPTIONS") return new Response(null, { headers: getCorsHeaders(req) });

Comment on lines 8 to 16
// Module-scope CORS headers — atribuído per-request no handler.
let corsHeaders: Record<string, string> = {};
let contractResponseHeaders: Record<string, string> = {};

function jsonRes(body: unknown, status = 200) {
return new Response(JSON.stringify(body), {
status,
headers: { ...corsHeaders, "Content-Type": "application/json" },
headers: { ...corsHeaders, ...contractResponseHeaders, "Content-Type": "application/json" },
});
Comment on lines 87 to 114
if (!aiResponse.ok) {
const errText = await aiResponse.text();
console.error("AI Gateway error:", aiResponse.status, errText);
if (aiResponse.status === 429) {
return new Response(
JSON.stringify({ error: "Muitas requisições. Tente em instantes." }),
{ status: 429, headers: { ...corsHeaders, "Content-Type": "application/json" } },
);
}
if (aiResponse.status === 402) {
return new Response(
JSON.stringify({ error: "Créditos esgotados — adicione fundos no Lovable AI." }),
{ status: 402, headers: { ...corsHeaders, "Content-Type": "application/json" } },
);
}
return new Response(JSON.stringify({ error: "Erro no provedor de IA." }), {
status: 502,
headers: { ...corsHeaders, "Content-Type": "application/json" },
});
}

const data = await aiResponse.json();
const answer = data?.choices?.[0]?.message?.content?.trim() ?? "Não consegui formular resposta.";

return new Response(JSON.stringify({ answer }), {
status: 200,
headers: { ...corsHeaders, "Content-Type": "application/json" },
headers: { ...corsHeaders, ...responseHeaders, "Content-Type": "application/json" },
});
Comment on lines 52 to +60
try {
const {
count = 100,
targetFunctions = ["external-db-bridge", "webhook-inbound", "product-webhook"],
mode = "resilience" // "resilience", "load", "fuzzing"
} = await req.json();
const contractResult = await parseContract(req, SimulationOrchestratorSchemas, {
corsHeaders,
});
if (!contractResult.ok) return contractResult.response;
const { data: parsedBody, responseHeaders } = contractResult;
const count = parsedBody.count ?? 100;
const targetFunctions = parsedBody.targetFunctions ?? ["external-db-bridge", "webhook-inbound", "product-webhook"];
const mode = parsedBody.mode ?? "resilience";
Comment on lines +45 to +48
const { version, data, responseHeaders } = result;
// headers anexados em TODAS as respostas de sucesso (versão + deprecation)
const okHeaders = { ...corsHeaders, ...responseHeaders, "Content-Type": "application/json" };

Copy link
Copy Markdown

@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: 17

Note

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

🟡 Minor comments (5)
supabase/functions/kit-ai-builder/index.ts-34-39 (1)

34-39: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Reuse os headers do contrato em todos os retornos após o parse.

Aqui responseHeaders só entra no 200. Se a IA responder 429/402 ou o handler cair no catch, o cliente deixa de receber os headers de versão/depreciação do contrato já negociado.

Also applies to: 134-136

🤖 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 `@supabase/functions/kit-ai-builder/index.ts` around lines 34 - 39, After
successfully parsing via parseContract (when contractResult.ok is true) ensure
responseHeaders from contractResult are merged into every HTTP response sent
thereafter (not just the 200 path): include responseHeaders in the
Response/return for error responses like 429/402 and in the catch/final error
handler; update all places that currently return without responseHeaders (e.g.,
the code immediately after "const { data: body, responseHeaders } =
contractResult" and the error handling at the section referenced around lines
134-136) to merge responseHeaders into the outgoing headers so contract
version/deprecation headers are always preserved.
supabase/functions/bi-copilot/index.ts-47-52 (1)

47-52: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Propague responseHeaders também nos erros pós-parse.

Depois que parseContract aceita a request, os retornos 429/402/502/500 continuam usando só corsHeaders. Isso faz os headers de versão/depreciação sumirem justamente nos cenários de erro.

💡 Ajuste sugerido
     const contractResult = await parseContract(req, BiCopilotSchemas, {
       corsHeaders,
     });
     if (!contractResult.ok) return contractResult.response;
     const { data: body, responseHeaders } = contractResult;
+    const baseHeaders = { ...corsHeaders, ...responseHeaders, "Content-Type": "application/json" };
...
         return new Response(
           JSON.stringify({ error: "Muitas requisições. Tente em instantes." }),
-          { status: 429, headers: { ...corsHeaders, "Content-Type": "application/json" } },
+          { status: 429, headers: baseHeaders },
         );
...
     return new Response(JSON.stringify({ answer }), {
       status: 200,
-      headers: { ...corsHeaders, ...responseHeaders, "Content-Type": "application/json" },
+      headers: baseHeaders,
     });

Also applies to: 111-113

🤖 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 `@supabase/functions/bi-copilot/index.ts` around lines 47 - 52, After
parseContract succeeds you extract responseHeaders but subsequent error
responses (e.g., the 429/402/502/500 paths after parsing) only use corsHeaders
and drop version/deprecation headers; update all places that construct error
responses after parseContract (refer to contractResult, responseHeaders,
corsHeaders and functions that return those error responses) to merge/spread
responseHeaders into the returned headers (e.g., { ...corsHeaders,
...responseHeaders }) so that responseHeaders are propagated on all error
returns (also apply the same change to the other identical block around the code
referenced at lines ~111-113).
supabase/functions/webhook-dispatcher/index.ts-56-60 (1)

56-60: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

parseContract está sem efeito nos headers de versão/depreciação.

Aqui você usa parseContract para validar/versionar, mas ignora responseHeaders. Resultado: as respostas pós-parse desse handler não carregam os headers de contrato negociado.

💡 Ajuste sugerido
     const contractResult = await parseContract(req, WebhookDispatcherSchemas, { corsHeaders });
     if (!contractResult.ok) return contractResult.response;
-    const { version: contractVersion, data: parsedData } = contractResult;
+    const {
+      version: contractVersion,
+      data: parsedData,
+      responseHeaders,
+    } = contractResult;
+    const baseHeaders = { ...corsHeaders, ...responseHeaders, "Content-Type": "application/json" };
🤖 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 `@supabase/functions/webhook-dispatcher/index.ts` around lines 56 - 60,
parseContract(...) returns responseHeaders for negotiated contract version but
the handler ignores them; update the webhook-dispatcher handler to capture
contractResult.responseHeaders (or responseHeaders) and merge those headers into
any subsequent responses sent from this handler (including early returns and the
final response), e.g., when using contractVersion/parsedData ensure to add the
returned headers to res (or to the Response object) so deprecation/version
headers are preserved for clients.
supabase/functions/_shared/contracts/versioning.ts-102-106 (1)

102-106: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Normalizar versão após trim() para aceitar valores com espaços.

Hoje, accept-version: v2 pode virar v2 (sem remover o v) e ser rejeitado indevidamente. O mesmo vale para ?v= v2.

Diff sugerido
+function normalizeVersionToken(raw: string): string {
+  return raw.trim().replace(/^v/i, "").split(".")[0].trim();
+}
+
 function readRequestedVersion(req: Request): string | null {
   // 1. Header `accept-version`
   const headerVal = req.headers.get("accept-version");
   if (headerVal) {
-    // aceita "2", "v2", "2.0"
-    return headerVal.replace(/^v/i, "").split(".")[0].trim();
+    const normalized = normalizeVersionToken(headerVal);
+    if (normalized) return normalized;
   }

   // 2. Query param `?v=`
   try {
     const url = new URL(req.url);
     const qv = url.searchParams.get("v");
-    if (qv) return qv.replace(/^v/i, "").split(".")[0].trim();
+    if (qv) {
+      const normalized = normalizeVersionToken(qv);
+      if (normalized) return normalized;
+    }
   } catch {

Also applies to: 111-113

🤖 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 `@supabase/functions/_shared/contracts/versioning.ts` around lines 102 - 106,
Trim the header/query raw value before stripping the optional "v" and extracting
the major version: for the accept-version handling (the headerVal obtained from
req.headers.get("accept-version")) call .trim() first, then apply
.replace(/^v/i, "") and .split(".")[0]; do the same normalization for the query
param handling that reads the "v" value (the logic around lines handling ?v) so
values like " v2 " or " v2.0 " are correctly normalized to "2".
supabase/functions/_shared/contracts/schemas/product-webhook.ts-58-59 (1)

58-59: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Evite z.any() no contrato público do product-webhook (linhas 58-59)

z.any() em variations e metadata faz o parseContract validar “qualquer coisa” nesses campos, e o handler upsertProducts repassa product.variations/product.metadata direto para productData (sem narrowing). Isso remove garantias de tipo e facilita uso inseguro do payload depois.

Sugestão de ajuste
-  variations: z.array(z.any()).max(200).optional(),
-  metadata: z.record(z.any()).optional(),
+  variations: z.array(z.unknown()).max(200).optional(),
+  metadata: z.record(z.unknown()).optional(),
🤖 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 `@supabase/functions/_shared/contracts/schemas/product-webhook.ts` around lines
58 - 59, The contract currently uses z.any() for variations and metadata which
removes type guarantees; replace z.array(z.any()).max(200).optional() and
z.record(z.any()).optional() with stricter schemas (e.g. define a
VariationSchema as z.object({...}) or at minimum use z.unknown() instead of
z.any(), and define MetadataSchema as z.record(z.unknown()) or a record of
strings) so the contract enforces shape, and then update the upsertProducts
handler to explicitly parse/narrow product.variations and product.metadata via
parseContract (or the new VariationSchema/MetadataSchema) before assigning them
to productData. Ensure you reference and change the symbols variations, metadata
in product-webhook schema and adjust upsertProducts parsing logic for
product.variations/product.metadata to use the new schemas.
🧹 Nitpick comments (1)
docs/contracts/MIGRATION_GUIDE.md (1)

125-125: ⚡ Quick win

Referência temporal "neste PR" em documentação evergreen.

A frase "migrado neste PR" torna a documentação datada. Guias de migração devem ser atemporais para serem úteis no futuro.

📝 Sugestão de correção
-Padrão usado em `webhook-inbound` migrado neste PR.
+Padrão usado em `webhook-inbound` (ver implementação de referência).

Ou simplesmente:

-Padrão usado em `webhook-inbound` migrado neste PR.
+Exemplo de uso: `webhook-inbound`.
🤖 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 `@docs/contracts/MIGRATION_GUIDE.md` at line 125, The phrase "Padrão usado em
`webhook-inbound` migrado neste PR" is time-bound; update the sentence to be
evergreen by removing "neste PR" and using a timeless phrasing such as "Padrão
usado em `webhook-inbound` migrado" or "Padrão usado em `webhook-inbound` —
migrado" (or "Padrão usado em `webhook-inbound` foi migrado"), ensuring the
reference to `webhook-inbound` remains for context.
🤖 Prompt for all review comments with 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.

Inline comments:
In `@docs/contracts/MIGRATION_GUIDE.md`:
- Around line 47-49: A linha que importa z diretamente from
"https://esm.sh/zod@3.23.8" deve ser substituída pelo barrel do projeto para
garantir a versão pinada e consistente; update the import of the symbol z (used
by SendEmailV1) to the project's central barrel `_zod.ts` (use the relative
import used in docs, e.g. "../_zod.ts") so the file imports `z` from the project
barrel instead of the external URL.

In `@docs/contracts/README.md`:
- Around line 100-103: O exemplo de importação de Zod usa a URL externa e pode
causar múltiplas versões; update o snippet em
supabase/functions/_shared/contracts/schemas/<endpoint>.ts para importar z a
partir do barrel central `_zod.ts` em vez de "https://esm.sh/..." (atualize a
linha de import para apontar para ../_zod.ts) e preserve o restante do exemplo
(por exemplo o symbol MyEndpointV1) — alternadamente, adicione uma nota no
README indicando que a importação direta é apenas ilustrativa e que a prática
correta é usar o barrel `_zod.ts`.

In `@scripts/contract-testing.mjs`:
- Around line 32-40: A variável ANON_KEY está usando SUPABASE_SERVICE_ROLE_KEY
como fallback (ANON_KEY = process.env.SUPABASE_ANON_KEY ||
process.env.SUPABASE_SERVICE_ROLE_KEY), o que permite executar os testes com
credenciais de service role; remove esse fallback so que ANON_KEY seja obtida
apenas de process.env.SUPABASE_ANON_KEY e atualize a validação/erro
correspondente (a mensagem em console.error e a checagem if (!ANON_KEY)) para
refletir que apenas SUPABASE_ANON_KEY é aceito.
- Around line 145-168: The scenarios for the "webhook-inbound" endpoint use a
fixed endpoint slug ("webhook-inbound?slug=contract-test-slug") and many
scenario expect.statusIn arrays include 404, which allows tests to pass by
hitting a missing route instead of validating payload/versioning; change tests
so they cannot succeed on 404: either ensure the slug exists (create/register
the "contract-test-slug" fixture before these scenarios) or remove 404 from the
expect.statusIn arrays for payload/versioning scenarios in the scenarios array
(e.g., the entries with description "empty body → 400 missing_body" and "v2
valid envelope...") so failures due to missing route are surfaced, and consider
making the endpoint slug unique per run or parameterized rather than hardcoded.

In `@supabase/functions/_shared/contracts/schemas/block-ip-temporarily.ts`:
- Around line 12-13: O regex IP_REGEX_V2 está demasiado permissivo (aceita
octetos >255 e IPv6 malformado); substitua-o por validação rigorosa: para IPv4
use um padrão que force cada octeto entre 0-255 (ex.: cada grupo
(25[0-5]|2[0-4]\d|1?\d{1,2})) e para IPv6 use uma expressão comprovada ou, ainda
melhor, remova o regex e implemente uma checagem de runtime reutilizável (por
exemplo uma função isValidIp(address) que usa uma biblioteca padrão como
ipaddr.js ou APIs confiáveis de isIP/isIPv4/isIPv6) e troque todas as
referências a IP_REGEX_V2 para chamarem isValidIp; atualize também as
ocorrências relacionadas nas linhas mencionadas (usos correspondentes nas
validações entre as linhas 25-27) para usar a nova função/padrão.

In `@supabase/functions/_shared/contracts/schemas/product-webhook.ts`:
- Line 14: Replace the direct esm.sh Zod import in product-webhook.ts with the
shared barrel export from supabase/functions/_shared/contracts/_zod.ts (use the
exported identifier from that module instead of importing
"https://esm.sh/zod@3.23.8"); then strengthen the webhook schema by replacing
z.any() usages in the fields variations and metadata with z.unknown() (and add a
comment or TODO to narrow/refine them later) or, preferably, define explicit Zod
sub-schemas for the expected variation and metadata shapes and use those (refer
to the schema object/constant names in this file to locate where to update
variations and metadata).

In `@supabase/functions/_shared/contracts/schemas/webhook-inbound.ts`:
- Around line 20-21: WebhookInboundV1 currently uses z.any(), allowing any
primitive and no structural validation; change WebhookInboundV1 to require an
object shape (e.g., use z.record(z.unknown()) or z.object({}).passthrough()) to
ensure the incoming payload is at least an object while preserving forward
compatibility, and keep defaultVersion: "1" unchanged; update the schema export
name WebhookInboundV1 accordingly and add a short comment referencing minimal
validation for webhooks (shared secret/HMAC checks remain separate).

In `@supabase/functions/_shared/contracts/versioning.ts`:
- Around line 77-85: Add a Vary header to avoid cross-version caching by setting
responseHeaders["Vary"] = "accept-version" alongside the existing headers in the
same block where responseHeaders is created (refer to the responseHeaders
variable and the surrounding logic that uses version and dep/toRfc1123); ensure
you add or append the Vary value so intermediate caches honor the Accept-Version
header and do not serve one versioned response to clients requesting another.

In `@supabase/functions/block-ip-temporarily/index.ts`:
- Around line 10-16: The module-scoped mutable object contractResponseHeaders is
shared across requests and read by jsonRes, causing cross-request header leaks;
make response headers request-local by removing contractResponseHeaders from
module scope and instead build per-request header objects and pass them into
jsonRes (or accept a headers parameter in jsonRes) so each invocation merges
corsHeaders with that request-specific headers and "Content-Type":
"application/json"; update all usages that reference contractResponseHeaders
(including the other occurrences around lines 27-28 and 55-60) to supply the
local headers rather than relying on the global variable.

In `@supabase/functions/e2e-cleanup/index.ts`:
- Around line 42-48: The module-scoped mutable variable contractResponseHeaders
causes cross-request header leakage; make contractResponseHeaders local inside
the request handler created for Deno.serve (or the function that builds
per-request context) and stop using the global. Change jsonResponse to accept an
explicit headers parameter (or pass the local contractResponseHeaders into its
existing extraHeaders) and update all call sites that rely on the global
(references to contractResponseHeaders, jsonResponse, and the Deno.serve
handler) so each request thread uses its own headers object; remove the
module-level declaration to prevent accidental shared state.

In `@supabase/functions/force-global-logout/index.ts`:
- Around line 10-16: O objeto module-scoped contractResponseHeaders está sendo
compartilhado entre requests e pode vazar headers entre execuções; em vez disso
crie headers por-request (por exemplo dentro do request handler) e passe-os para
jsonRes como parâmetro, ou torne jsonRes aceitar um segundo argumento headers
para mesclar com corsHeaders; atualize todas as chamadas de jsonRes (incluindo
onde contractResponseHeaders era setado/lia em jsonRes e nas áreas
correspondentes) para usar o headers local e remova/evite o estado global
contractResponseHeaders.

In `@supabase/functions/ownership-audit/index.ts`:
- Line 20: Os objetos mutáveis corsHeaders e contractResponseHeaders estão no
escopo do módulo e podem causar vazamento entre requisições concorrentes;
torne-os locais por requisição (declare como const dentro do handler função que
processa a request) e remova/referencie as variáveis de módulo. Atualize
chamadas ao helper json() (ou ao builder de response) para receber
explicitamente os headers locais como argumento em vez de depender de variáveis
do módulo, garantindo que cada request construa seu próprio conjunto de headers.

In `@supabase/functions/ownership-repair/index.ts`:
- Line 20: O uso de contractResponseHeaders e corsHeaders em escopo de módulo
causa condição de corrida; mova a criação e atribuição de headers para o escopo
local do handler (ou da função que trata a requisição), criando novos objetos
locais (por exemplo const corsHeaders = {...} e const contractResponseHeaders =
{...}) em vez de sobrescrever variáveis globais, e passe esses objetos locais
como argumento ao chamar json() ao montar a resposta; também evite mutações
posteriores nesses objetos (crie novos objetos em vez de modificar existentes).

In `@supabase/functions/product-webhook/index.ts`:
- Around line 140-145: No envio do error.message bruto na resposta 500: dentro
do bloco catch (onde está a variável errorMessage e o return new Response(..., {
status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" }})),
mantenha o console.error/error logging completo do erro para o servidor, mas
substitua o payload retornado por uma mensagem genérica (ex.:
"internal_server_error" ou "Internal server error") em vez de error.message e
remova qualquer detalhe interno; ou seja, preserve a chamada a
console.error(error) e altere o JSON retornado para algo genérico sem expor
SQL/constraints/tokens.

In `@supabase/functions/step-up-verify/index.ts`:
- Around line 15-17: The module-level mutable header variables corsHeaders and
contractResponseHeaders must be made request-scoped to avoid cross-request
leakage: remove the module-scope let/const declarations and instead create const
corsHeaders and const contractResponseHeaders inside the request handler (the
function that processes each request), then either pass those header objects
explicitly into the json() helper (update json() signature/usages to accept a
headers param) or move json() into the handler/closure so it closes over the
per-request headers; update all places that previously mutated the module-scope
vars (lines that assign to corsHeaders/contractResponseHeaders) to assign the
new local consts instead.

In `@supabase/functions/webhook-inbound/index.ts`:
- Around line 125-132: The update reads counters from memory and writes back
computed values causing lost updates; change the non-atomic update on
inbound_webhook_endpoints to an atomic SQL-level increment so the DB performs:
set last_received_at = now(), total_received = total_received + 1, and
total_invalid = total_invalid + (signatureValid ? 0 : 1) in a single statement
(use a Postgres UPDATE or RPC/SQL call instead of computing from
endpoint.total_received/total_invalid in memory), targeting the row by
endpoint.id so concurrent requests increment correctly.
- Around line 149-151: O response 500 está retornando a mensagem crua (variável
msg/err.message) para o cliente; substitua o payload retornado em new
Response(...) por uma mensagem genérica (ex.: code: "internal_error", message:
"Internal server error", fields: []) e mantenha detalhes do erro apenas em log
interno sanitizado (use o logger disponível — ex.: processLogger.error or
console.error — para gravar err.message/err.stack removendo/mascarando
tokens/credenciais). Assegure que o status continue 500 e os cabeçalhos
(corsHeaders, Content-Type) permaneçam inalterados.

---

Minor comments:
In `@supabase/functions/_shared/contracts/schemas/product-webhook.ts`:
- Around line 58-59: The contract currently uses z.any() for variations and
metadata which removes type guarantees; replace
z.array(z.any()).max(200).optional() and z.record(z.any()).optional() with
stricter schemas (e.g. define a VariationSchema as z.object({...}) or at minimum
use z.unknown() instead of z.any(), and define MetadataSchema as
z.record(z.unknown()) or a record of strings) so the contract enforces shape,
and then update the upsertProducts handler to explicitly parse/narrow
product.variations and product.metadata via parseContract (or the new
VariationSchema/MetadataSchema) before assigning them to productData. Ensure you
reference and change the symbols variations, metadata in product-webhook schema
and adjust upsertProducts parsing logic for product.variations/product.metadata
to use the new schemas.

In `@supabase/functions/_shared/contracts/versioning.ts`:
- Around line 102-106: Trim the header/query raw value before stripping the
optional "v" and extracting the major version: for the accept-version handling
(the headerVal obtained from req.headers.get("accept-version")) call .trim()
first, then apply .replace(/^v/i, "") and .split(".")[0]; do the same
normalization for the query param handling that reads the "v" value (the logic
around lines handling ?v) so values like " v2 " or " v2.0 " are correctly
normalized to "2".

In `@supabase/functions/bi-copilot/index.ts`:
- Around line 47-52: After parseContract succeeds you extract responseHeaders
but subsequent error responses (e.g., the 429/402/502/500 paths after parsing)
only use corsHeaders and drop version/deprecation headers; update all places
that construct error responses after parseContract (refer to contractResult,
responseHeaders, corsHeaders and functions that return those error responses) to
merge/spread responseHeaders into the returned headers (e.g., { ...corsHeaders,
...responseHeaders }) so that responseHeaders are propagated on all error
returns (also apply the same change to the other identical block around the code
referenced at lines ~111-113).

In `@supabase/functions/kit-ai-builder/index.ts`:
- Around line 34-39: After successfully parsing via parseContract (when
contractResult.ok is true) ensure responseHeaders from contractResult are merged
into every HTTP response sent thereafter (not just the 200 path): include
responseHeaders in the Response/return for error responses like 429/402 and in
the catch/final error handler; update all places that currently return without
responseHeaders (e.g., the code immediately after "const { data: body,
responseHeaders } = contractResult" and the error handling at the section
referenced around lines 134-136) to merge responseHeaders into the outgoing
headers so contract version/deprecation headers are always preserved.

In `@supabase/functions/webhook-dispatcher/index.ts`:
- Around line 56-60: parseContract(...) returns responseHeaders for negotiated
contract version but the handler ignores them; update the webhook-dispatcher
handler to capture contractResult.responseHeaders (or responseHeaders) and merge
those headers into any subsequent responses sent from this handler (including
early returns and the final response), e.g., when using
contractVersion/parsedData ensure to add the returned headers to res (or to the
Response object) so deprecation/version headers are preserved for clients.

---

Nitpick comments:
In `@docs/contracts/MIGRATION_GUIDE.md`:
- Line 125: The phrase "Padrão usado em `webhook-inbound` migrado neste PR" is
time-bound; update the sentence to be evergreen by removing "neste PR" and using
a timeless phrasing such as "Padrão usado em `webhook-inbound` migrado" or
"Padrão usado em `webhook-inbound` — migrado" (or "Padrão usado em
`webhook-inbound` foi migrado"), ensuring the reference to `webhook-inbound`
remains for context.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: df05b02b-e676-418a-9d53-efb7e2b20dd6

📥 Commits

Reviewing files that changed from the base of the PR and between 8e527e3 and bea6aae.

📒 Files selected for processing (50)
  • docs/contracts/MIGRATION_GUIDE.md
  • docs/contracts/README.md
  • scripts/contract-testing.mjs
  • supabase/functions/_shared/contracts/_zod.ts
  • supabase/functions/_shared/contracts/errors.ts
  • supabase/functions/_shared/contracts/index.ts
  • supabase/functions/_shared/contracts/parse.ts
  • supabase/functions/_shared/contracts/schemas/bi-copilot.ts
  • supabase/functions/_shared/contracts/schemas/block-ip-temporarily.ts
  • supabase/functions/_shared/contracts/schemas/e2e-cleanup.ts
  • supabase/functions/_shared/contracts/schemas/force-global-logout.ts
  • supabase/functions/_shared/contracts/schemas/kit-ai-builder.ts
  • supabase/functions/_shared/contracts/schemas/market-intelligence-insights.ts
  • supabase/functions/_shared/contracts/schemas/ownership-audit.ts
  • supabase/functions/_shared/contracts/schemas/ownership-repair.ts
  • supabase/functions/_shared/contracts/schemas/product-webhook.ts
  • supabase/functions/_shared/contracts/schemas/send-transactional-email.ts
  • supabase/functions/_shared/contracts/schemas/simulation-orchestrator.ts
  • supabase/functions/_shared/contracts/schemas/step-up-verify.ts
  • supabase/functions/_shared/contracts/schemas/sync-external-db.ts
  • supabase/functions/_shared/contracts/schemas/trends-insights.ts
  • supabase/functions/_shared/contracts/schemas/webhook-dispatcher.ts
  • supabase/functions/_shared/contracts/schemas/webhook-inbound.ts
  • supabase/functions/_shared/contracts/versioning.ts
  • supabase/functions/_shared/zod-validate.ts
  • supabase/functions/bi-copilot/index.ts
  • supabase/functions/block-ip-temporarily/index.ts
  • supabase/functions/e2e-cleanup/index.ts
  • supabase/functions/force-global-logout/index.ts
  • supabase/functions/kit-ai-builder/index.ts
  • supabase/functions/market-intelligence-insights/index.ts
  • supabase/functions/ownership-audit/index.ts
  • supabase/functions/ownership-repair/index.ts
  • supabase/functions/product-webhook/index.ts
  • supabase/functions/send-transactional-email/index.ts
  • supabase/functions/simulation-orchestrator/index.ts
  • supabase/functions/step-up-verify/index.ts
  • supabase/functions/sync-external-db/index.ts
  • supabase/functions/trends-insights/index.ts
  • supabase/functions/webhook-dispatcher/index.ts
  • supabase/functions/webhook-inbound/index.ts
  • tests/contracts/_helpers.ts
  • tests/contracts/errors.test.ts
  • tests/contracts/migrated-endpoints.contract.test.ts
  • tests/contracts/product-webhook.contract.test.ts
  • tests/contracts/send-transactional-email.contract.test.ts
  • tests/contracts/step-up-verify.contract.test.ts
  • tests/contracts/versioning.test.ts
  • tests/contracts/webhooks.contract.test.ts
  • vitest.config.ts

Comment on lines +47 to +49
```ts
import { z } from "https://esm.sh/zod@3.23.8";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Inconsistência na importação do Zod.

Mesmo problema do README: a linha 48 mostra importação direta do Zod, mas deveria usar o barrel central _shared/contracts/_zod.ts para manter a versão pinada e consistente em todo o projeto.

📝 Sugestão de correção
 ```ts
-import { z } from "https://esm.sh/zod@3.23.8";
+import { z } from "../_zod.ts";
 
 export const SendEmailV1 = z.object({
🤖 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 `@docs/contracts/MIGRATION_GUIDE.md` around lines 47 - 49, A linha que importa
z diretamente from "https://esm.sh/zod@3.23.8" deve ser substituída pelo barrel
do projeto para garantir a versão pinada e consistente; update the import of the
symbol z (used by SendEmailV1) to the project's central barrel `_zod.ts` (use
the relative import used in docs, e.g. "../_zod.ts") so the file imports `z`
from the project barrel instead of the external URL.

Comment thread docs/contracts/README.md
Comment on lines +100 to +103
```ts
// supabase/functions/_shared/contracts/schemas/<endpoint>.ts
import { z } from "https://esm.sh/zod@3.23.8";

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick win

Inconsistência na importação do Zod.

A linha 102 mostra importação direta do Zod via https://esm.sh/zod@3.23.8, mas o pacote contracts centraliza a versão do Zod em _shared/contracts/_zod.ts (conforme descrito no contexto do PR). Desenvolvedores que seguirem este exemplo podem acabar com importações inconsistentes e múltiplas versões do Zod no bundle.

Recomendação: atualizar o exemplo para importar do barrel central.

📝 Sugestão de correção
 ```ts
-// supabase/functions/_shared/contracts/schemas/<endpoint>.ts
-import { z } from "https://esm.sh/zod@3.23.8";
+// supabase/functions/_shared/contracts/schemas/<endpoint>.ts  
+import { z } from "../_zod.ts";
 
 export const MyEndpointV1 = z.object({ /* ... */ });

Ou mencionar explicitamente que o import direto é apenas ilustrativo e que na prática deve-se usar o barrel.

🤖 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 `@docs/contracts/README.md` around lines 100 - 103, O exemplo de importação de
Zod usa a URL externa e pode causar múltiplas versões; update o snippet em
supabase/functions/_shared/contracts/schemas/<endpoint>.ts para importar z a
partir do barrel central `_zod.ts` em vez de "https://esm.sh/..." (atualize a
linha de import para apontar para ../_zod.ts) e preserve o restante do exemplo
(por exemplo o symbol MyEndpointV1) — alternadamente, adicione uma nota no
README indicando que a importação direta é apenas ilustrativa e que a prática
correta é usar o barrel `_zod.ts`.

Comment on lines +32 to +40
const SUPABASE_URL = process.env.SUPABASE_URL || 'http://localhost:54321';
const ANON_KEY = process.env.SUPABASE_ANON_KEY || process.env.SUPABASE_SERVICE_ROLE_KEY;
const PRODUCT_WEBHOOK_SECRET = process.env.N8N_PRODUCT_WEBHOOK_SECRET || '';
const TIMEOUT_MS = Number(process.env.CONTRACT_TEST_TIMEOUT_MS || 10000);

if (!ANON_KEY) {
console.error('❌ SUPABASE_ANON_KEY (ou SERVICE_ROLE_KEY) é obrigatório.');
process.exit(2);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Remova o fallback para SUPABASE_SERVICE_ROLE_KEY no smoke test.

Na Line 33, usar chave de service role como fallback aumenta risco de execução acidental com privilégio alto. Para teste de contrato, mantenha apenas SUPABASE_ANON_KEY.

💡 Ajuste sugerido
-const ANON_KEY = process.env.SUPABASE_ANON_KEY || process.env.SUPABASE_SERVICE_ROLE_KEY;
+const ANON_KEY = process.env.SUPABASE_ANON_KEY;

 if (!ANON_KEY) {
-  console.error('❌ SUPABASE_ANON_KEY (ou SERVICE_ROLE_KEY) é obrigatório.');
+  console.error('❌ SUPABASE_ANON_KEY é obrigatório.');
   process.exit(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 `@scripts/contract-testing.mjs` around lines 32 - 40, A variável ANON_KEY está
usando SUPABASE_SERVICE_ROLE_KEY como fallback (ANON_KEY =
process.env.SUPABASE_ANON_KEY || process.env.SUPABASE_SERVICE_ROLE_KEY), o que
permite executar os testes com credenciais de service role; remove esse fallback
so que ANON_KEY seja obtida apenas de process.env.SUPABASE_ANON_KEY e atualize a
validação/erro correspondente (a mensagem em console.error e a checagem if
(!ANON_KEY)) para refletir que apenas SUPABASE_ANON_KEY é aceito.

Comment on lines +145 to +168
name: 'webhook-inbound',
endpoint: 'webhook-inbound?slug=contract-test-slug',
extraHeaders: {},
scenarios: [
{
description: "Valid select simulation",
payload: { operation: "select", table: "products", limit: 1 },
expectedStatus: 200,
validateResponse: (data) => Array.isArray(data.records || data.data?.records)
description: 'unknown slug → 404',
payload: { hello: 'world' },
expect: { statusIn: [404, 400] },
},
{
description: 'empty body → 400 missing_body',
rawBody: '',
expect: { statusIn: [400, 404] }, // 404 vem antes de body check
},
{
description: 'v2 valid envelope (will fail at HMAC, but contract passes)',
headers: { 'accept-version': '2' },
payload: {
event: 'contract.test',
occurred_at: new Date().toISOString(),
data: { ping: true },
},
expect: { statusIn: [200, 401, 404] },
},
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Os cenários de webhook-inbound aceitam 404 e podem passar sem validar contrato.

Na Line 146 o slug está fixo, e nas Lines 150-168 o statusIn aceita 404. Isso pode gerar falso positivo: o teste “passa” por rota inexistente, sem executar validação de payload/versionamento.

💡 Ajuste sugerido
+const WEBHOOK_INBOUND_TEST_SLUG = process.env.CONTRACT_TEST_WEBHOOK_INBOUND_SLUG;
+
+if (!WEBHOOK_INBOUND_TEST_SLUG) {
+  console.error('❌ CONTRACT_TEST_WEBHOOK_INBOUND_SLUG é obrigatório para testar webhook-inbound.');
+  process.exit(2);
+}

   {
     name: 'webhook-inbound',
-    endpoint: 'webhook-inbound?slug=contract-test-slug',
+    endpoint: `webhook-inbound?slug=${encodeURIComponent(WEBHOOK_INBOUND_TEST_SLUG)}`,
     extraHeaders: {},
     scenarios: [
       {
-        description: 'unknown slug → 404',
+        description: 'v1 passthrough com slug válido',
         payload: { hello: 'world' },
-        expect: { statusIn: [404, 400] },
+        expect: { statusIn: [200, 401] },
       },
       {
         description: 'empty body → 400 missing_body',
         rawBody: '',
-        expect: { statusIn: [400, 404] }, // 404 vem antes de body check
+        expect: { statusIn: [400, 401] },
       },
       {
         description: 'v2 valid envelope (will fail at HMAC, but contract passes)',
         headers: { 'accept-version': '2' },
@@
-        expect: { statusIn: [200, 401, 404] },
+        expect: { statusIn: [200, 401] },
       },
     ],
   },
🤖 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 `@scripts/contract-testing.mjs` around lines 145 - 168, The scenarios for the
"webhook-inbound" endpoint use a fixed endpoint slug
("webhook-inbound?slug=contract-test-slug") and many scenario expect.statusIn
arrays include 404, which allows tests to pass by hitting a missing route
instead of validating payload/versioning; change tests so they cannot succeed on
404: either ensure the slug exists (create/register the "contract-test-slug"
fixture before these scenarios) or remove 404 from the expect.statusIn arrays
for payload/versioning scenarios in the scenarios array (e.g., the entries with
description "empty body → 400 missing_body" and "v2 valid envelope...") so
failures due to missing route are surfaced, and consider making the endpoint
slug unique per run or parameterized rather than hardcoded.

Comment on lines +12 to +13
const IP_REGEX_V2 =
/^([0-9]{1,3}\.){3}[0-9]{1,3}(\/([0-9]|[1-2][0-9]|3[0-2]))?$|^([0-9a-fA-F:]+)(\/([0-9]|[1-9][0-9]|1[01][0-9]|12[0-8]))?$/;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

A validação “rigorosa” de IP em v2 está permissiva demais.

Em Line 13, o regex aceita casos inválidos como 999.999.999.999 e formas IPv6 malformadas (ex.: sequências de : inválidas). Isso enfraquece o contrato v2 e pode deixar payload inválido passar para camadas sensíveis.

As per coding guidelines, supabase/functions/**/*.ts: "Edge Functions Supabase em produção. Verificar com rigor ... Validação de payload em webhooks (shared secret, assinatura HMAC quando aplicável)".

Also applies to: 25-27

🤖 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 `@supabase/functions/_shared/contracts/schemas/block-ip-temporarily.ts` around
lines 12 - 13, O regex IP_REGEX_V2 está demasiado permissivo (aceita octetos
>255 e IPv6 malformado); substitua-o por validação rigorosa: para IPv4 use um
padrão que force cada octeto entre 0-255 (ex.: cada grupo
(25[0-5]|2[0-4]\d|1?\d{1,2})) e para IPv6 use uma expressão comprovada ou, ainda
melhor, remova o regex e implemente uma checagem de runtime reutilizável (por
exemplo uma função isValidIp(address) que usa uma biblioteca padrão como
ipaddr.js ou APIs confiáveis de isIP/isIPv4/isIPv6) e troque todas as
referências a IP_REGEX_V2 para chamarem isValidIp; atualize também as
ocorrências relacionadas nas linhas mencionadas (usos correspondentes nas
validações entre as linhas 25-27) para usar a nova função/padrão.


// Module-scope CORS headers — atribuído per-request no handler.
let corsHeaders: Record<string, string> = {};
let contractResponseHeaders: Record<string, string> = {};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n 'let (corsHeaders|contractResponseHeaders)' supabase/functions/ownership-repair/index.ts
rg -n 'corsHeaders = getCorsHeaders|contractResponseHeaders = \\{\\}|contractResponseHeaders = contractResult.responseHeaders|headers: \\{ \\.\\.\\.corsHeaders, \\.\\.\\.contractResponseHeaders' supabase/functions/ownership-repair/index.ts

Repository: adm01-debug/promo-gifts-v4

Length of output: 525


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="supabase/functions/ownership-repair/index.ts"
echo "== file =="
wc -l "$FILE"
echo
echo "== lines 1-160 =="
nl -ba "$FILE" | sed -n '1,160p'
echo
echo "== occurrences =="
rg -n "contractResponseHeaders" "$FILE" || true
rg -n "corsHeaders" "$FILE" || true

Repository: adm01-debug/promo-gifts-v4

Length of output: 209


🏁 Script executed:

#!/bin/bash
set -euo pipefail
FILE="supabase/functions/ownership-repair/index.ts"
echo "== file =="
wc -l "$FILE"
echo

echo "== lines 1-140 =="
cat -n "$FILE" | sed -n '1,140p'
echo

echo "== occurrences =="
rg -n "contractResponseHeaders" "$FILE" || true
rg -n "corsHeaders" "$FILE" || true

Repository: adm01-debug/promo-gifts-v4

Length of output: 5001


contractResponseHeaders (e corsHeaders) em escopo de módulo pode causar condição de corrida e misturar headers entre requisições concorrentes.

O handler sobrescreve corsHeaders/contractResponseHeaders (linhas 28-29, 53) e o json() monta os headers usando esses valores compartilhados (linhas 94-98). Em requests simultâneos, os valores podem ser trocados entre usuários. Mova os headers para escopo local da requisição e passe para json() (ou retorne/monte headers diretamente no handler).

🤖 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 `@supabase/functions/ownership-repair/index.ts` at line 20, O uso de
contractResponseHeaders e corsHeaders em escopo de módulo causa condição de
corrida; mova a criação e atribuição de headers para o escopo local do handler
(ou da função que trata a requisição), criando novos objetos locais (por exemplo
const corsHeaders = {...} e const contractResponseHeaders = {...}) em vez de
sobrescrever variáveis globais, e passe esses objetos locais como argumento ao
chamar json() ao montar a resposta; também evite mutações posteriores nesses
objetos (crie novos objetos em vez de modificar existentes).

Comment on lines +140 to +145
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error";
console.error("Product webhook error:", error);
return new Response(
JSON.stringify({ error: errorMessage }),
{ status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } }
JSON.stringify({ code: "internal_error", message: errorMessage, fields: [] }),
{ status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } },
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Não devolva error.message bruto no 500 do webhook.

Esse message pode carregar detalhe interno de banco/Supabase e acabar expondo constraint names ou SQL para o chamador. Logue o erro completo no servidor e responda uma mensagem genérica.

💡 Ajuste sugerido
   } catch (error) {
-    const errorMessage = error instanceof Error ? error.message : "Unknown error";
     console.error("Product webhook error:", error);
     return new Response(
-      JSON.stringify({ code: "internal_error", message: errorMessage, fields: [] }),
+      JSON.stringify({ code: "internal_error", message: "Internal server error", fields: [] }),
       { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } },
     );
   }

As per coding guidelines, "Tratamento de erro não pode vazar tokens, SQL ou credenciais no response".

📝 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
} catch (error) {
const errorMessage = error instanceof Error ? error.message : "Unknown error";
console.error("Product webhook error:", error);
return new Response(
JSON.stringify({ error: errorMessage }),
{ status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } }
JSON.stringify({ code: "internal_error", message: errorMessage, fields: [] }),
{ status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } },
} catch (error) {
console.error("Product webhook error:", error);
return new Response(
JSON.stringify({ code: "internal_error", message: "Internal server error", fields: [] }),
{ status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } },
);
}
🤖 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 `@supabase/functions/product-webhook/index.ts` around lines 140 - 145, No envio
do error.message bruto na resposta 500: dentro do bloco catch (onde está a
variável errorMessage e o return new Response(..., { status: 500, headers: {
...corsHeaders, "Content-Type": "application/json" }})), mantenha o
console.error/error logging completo do erro para o servidor, mas substitua o
payload retornado por uma mensagem genérica (ex.: "internal_server_error" ou
"Internal server error") em vez de error.message e remova qualquer detalhe
interno; ou seja, preserve a chamada a console.error(error) e altere o JSON
retornado para algo genérico sem expor SQL/constraints/tokens.

Comment on lines +15 to 17
// Module-scope contract response headers (Deprecation/Sunset) — setado após parseContract OK.
let contractResponseHeaders: Record<string, string> = {};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
rg -n 'let (corsHeaders|contractResponseHeaders)' supabase/functions/step-up-verify/index.ts
rg -n 'corsHeaders = getCorsHeaders|contractResponseHeaders = \\{\\}|contractResponseHeaders = contractResult.responseHeaders|headers: \\{ \\.\\.\\.corsHeaders, \\.\\.\\.contractResponseHeaders' supabase/functions/step-up-verify/index.ts

Repository: adm01-debug/promo-gifts-v4

Length of output: 525


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="supabase/functions/step-up-verify/index.ts"
echo "== file =="
wc -l "$FILE"
echo

echo "== lines 1-120 =="
nl -ba "$FILE" | sed -n '1,120p'
echo

echo "== lines 120-end =="
nl -ba "$FILE" | sed -n '121,260p'

Repository: adm01-debug/promo-gifts-v4

Length of output: 209


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="supabase/functions/step-up-verify/index.ts"

echo "== lines 1-140 =="
cat -n "$FILE" | sed -n '1,140p'

echo
echo "== lines 141-260 =="
cat -n "$FILE" | sed -n '141,260p'

echo
echo "== lines 261-380 =="
cat -n "$FILE" | sed -n '261,380p'

Repository: adm01-debug/promo-gifts-v4

Length of output: 15392


Evite estado global mutável para headers (pode misturar respostas entre requisições)

corsHeaders (linha 14) e contractResponseHeaders (linha 16) são variáveis no módulo e são sobrescritas a cada request (linhas 64-65 e 131). Como o helper json() (linhas 48-52) lê esses valores via escopo externo, requisições concorrentes podem intercalar awaits e fazer uma resposta sair com headers de outra. Faça corsHeaders/contractResponseHeaders serem request-scoped (const dentro do handler) e passe explicitamente para json() (ou mova json para dentro do handler/closure).

🤖 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 `@supabase/functions/step-up-verify/index.ts` around lines 15 - 17, The
module-level mutable header variables corsHeaders and contractResponseHeaders
must be made request-scoped to avoid cross-request leakage: remove the
module-scope let/const declarations and instead create const corsHeaders and
const contractResponseHeaders inside the request handler (the function that
processes each request), then either pass those header objects explicitly into
the json() helper (update json() signature/usages to accept a headers param) or
move json() into the handler/closure so it closes over the per-request headers;
update all places that previously mutated the module-scope vars (lines that
assign to corsHeaders/contractResponseHeaders) to assign the new local consts
instead.

Comment on lines +125 to +132
await supabase
.from("inbound_webhook_endpoints")
.update({
last_received_at: new Date().toISOString(),
total_received: (endpoint.total_received ?? 0) + 1,
total_invalid: (endpoint.total_invalid ?? 0) + (signatureValid ? 0 : 1),
})
.eq("id", endpoint.id);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Atualização de contadores não é atômica e pode perder eventos.

total_received/total_invalid é calculado com base no valor antigo lido em memória. Em requisições simultâneas, isso gera lost update. Troque por incremento atômico no banco (RPC/SQL com expressão) para garantir consistência.

🤖 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 `@supabase/functions/webhook-inbound/index.ts` around lines 125 - 132, The
update reads counters from memory and writes back computed values causing lost
updates; change the non-atomic update on inbound_webhook_endpoints to an atomic
SQL-level increment so the DB performs: set last_received_at = now(),
total_received = total_received + 1, and total_invalid = total_invalid +
(signatureValid ? 0 : 1) in a single statement (use a Postgres UPDATE or RPC/SQL
call instead of computing from endpoint.total_received/total_invalid in memory),
targeting the row by endpoint.id so concurrent requests increment correctly.

Comment on lines +149 to +151
return new Response(
JSON.stringify({ code: "internal_error", message: msg, fields: [] }),
{ status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } },
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Evite retornar err.message bruto no 500.

Esse retorno pode expor detalhes internos (ex.: SQL/credenciais) para o cliente. Responda com mensagem genérica e mantenha o detalhe apenas em log interno sanitizado.

🔧 Ajuste sugerido
-    const msg = err instanceof Error ? err.message : "Erro";
+    console.error("[webhook-inbound] internal_error", err);
     return new Response(
-      JSON.stringify({ code: "internal_error", message: msg, fields: [] }),
+      JSON.stringify({ code: "internal_error", message: "Erro interno", fields: [] }),
       { status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } },
     );

As per coding guidelines, "Tratamento de erro não pode vazar tokens, SQL ou credenciais no 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 `@supabase/functions/webhook-inbound/index.ts` around lines 149 - 151, O
response 500 está retornando a mensagem crua (variável msg/err.message) para o
cliente; substitua o payload retornado em new Response(...) por uma mensagem
genérica (ex.: code: "internal_error", message: "Internal server error", fields:
[]) e mantenha detalhes do erro apenas em log interno sanitizado (use o logger
disponível — ex.: processLogger.error or console.error — para gravar
err.message/err.stack removendo/mascarando tokens/credenciais). Assegure que o
status continue 500 e os cabeçalhos (corsHeaders, Content-Type) permaneçam
inalterados.

Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

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

21 issues found across 50 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="scripts/contract-testing.mjs">

<violation number="1" location="scripts/contract-testing.mjs:33">
P2: Falling back to `SUPABASE_SERVICE_ROLE_KEY` can hide real authorization failures in contract tests by running requests with elevated privileges.</violation>

<violation number="2" location="scripts/contract-testing.mjs:35">
P2: Validate `CONTRACT_TEST_TIMEOUT_MS` before using it, otherwise invalid env values can cause immediate aborts and false test failures.</violation>
</file>

<file name="supabase/functions/kit-ai-builder/index.ts">

<violation number="1" location="supabase/functions/kit-ai-builder/index.ts:39">
P2: Revalidate the prompt after `.trim()` (or trim inside the schema). Otherwise whitespace-only prompts can pass contract validation and still reach the AI call as empty input.</violation>
</file>

<file name="supabase/functions/_shared/contracts/schemas/block-ip-temporarily.ts">

<violation number="1" location="supabase/functions/_shared/contracts/schemas/block-ip-temporarily.ts:13">
P2: `IP_REGEX_V2` still accepts malformed IPv4 addresses (e.g. octets above 255), so V2 is not actually strict as intended.</violation>

<violation number="2" location="supabase/functions/_shared/contracts/schemas/block-ip-temporarily.ts:16">
P2: Normalize `ip` before regex validation (e.g. `.trim()`) to preserve v1 compatibility for inputs with surrounding whitespace.</violation>
</file>

<file name="supabase/functions/_shared/contracts/versioning.ts">

<violation number="1" location="supabase/functions/_shared/contracts/versioning.ts:77">
P2: Success responses drop the provided `corsHeaders`, so CORS handling is inconsistent with the error path and the function’s header contract.</violation>
</file>

<file name="supabase/functions/_shared/contracts/schemas/sync-external-db.ts">

<violation number="1" location="supabase/functions/_shared/contracts/schemas/sync-external-db.ts:19">
P1: `table` is only length-validated; it should be constrained to an explicit allowlist before being used in `.from(table)` with service-role clients.</violation>
</file>

<file name="supabase/functions/ownership-repair/index.ts">

<violation number="1" location="supabase/functions/ownership-repair/index.ts:20">
P2: `contractResponseHeaders` is request-specific data stored in module scope, which is unsafe under concurrent requests and can leak/mix headers across responses.</violation>
</file>

<file name="supabase/functions/e2e-cleanup/index.ts">

<violation number="1" location="supabase/functions/e2e-cleanup/index.ts:42">
P1: `contractResponseHeaders` is request-specific data stored in module-scope mutable state, which can race across concurrent requests and leak/mix response headers between users.</violation>

<violation number="2" location="supabase/functions/e2e-cleanup/index.ts:255">
P2: Do not map all HTTP 400 contract failures to `invalid_json`; `missing_body` is also 400 and is currently logged with the wrong audit reason.</violation>
</file>

<file name="supabase/functions/_shared/contracts/schemas/force-global-logout.ts">

<violation number="1" location="supabase/functions/_shared/contracts/schemas/force-global-logout.ts:29">
P2: `migrationUrl` uses an anchor that does not exist in the migration guide, so the deprecation link is effectively broken.</violation>
</file>

<file name="supabase/functions/sync-external-db/index.ts">

<violation number="1" location="supabase/functions/sync-external-db/index.ts:23">
P2: `responseHeaders` from `parseContract` are not propagated on the `catch` error response, so version/deprecation headers become inconsistent on failure paths.</violation>
</file>

<file name="supabase/functions/bi-copilot/index.ts">

<violation number="1" location="supabase/functions/bi-copilot/index.ts:113">
P2: Apply `responseHeaders` to all responses after `parseContract`, not only the 200 path, so deprecated-version headers are preserved on error responses too.</violation>
</file>

<file name="supabase/functions/block-ip-temporarily/index.ts">

<violation number="1" location="supabase/functions/block-ip-temporarily/index.ts:10">
P2: Avoid storing per-request response headers in module scope; this can leak contract/version headers across concurrent requests.</violation>

<violation number="2" location="supabase/functions/block-ip-temporarily/index.ts:55">
P2: This contract migration introduces a backward-incompatible change for `hours`: numeric strings that were previously accepted are now rejected by `z.number()`.</violation>
</file>

<file name="supabase/functions/ownership-audit/index.ts">

<violation number="1" location="supabase/functions/ownership-audit/index.ts:20">
P2: `contractResponseHeaders` is stored in shared module scope and then read by `json()`, which can mix headers between concurrent requests. Keep contract headers request-local instead of mutable global state.</violation>

<violation number="2" location="supabase/functions/ownership-audit/index.ts:42">
P1: This change makes an empty request body invalid, but the previous behavior accepted no body and defaulted to `triggered_by = "cron"`. That can break existing cron/manual invocations that send no JSON.</violation>
</file>

<file name="supabase/functions/_shared/contracts/schemas/product-webhook.ts">

<violation number="1" location="supabase/functions/_shared/contracts/schemas/product-webhook.ts:76">
P2: `updated_at` uses `datetime()` default validation, which rejects ISO datetimes with timezone offsets (e.g. `-03:00`). This can cause valid webhook payloads to fail in v2.</violation>

<violation number="2" location="supabase/functions/_shared/contracts/schemas/product-webhook.ts:79">
P2: `ProductV2` is only top-level strict; nested object fields are still non-strict, so extra nested keys can pass silently despite the v2 “no extra fields” contract.</violation>
</file>

<file name="docs/contracts/README.md">

<violation number="1" location="docs/contracts/README.md:102">
P2: The "Adicionando um novo schema" example incorrectly imports Zod directly from the esm.sh URL instead of from the centralized `_zod.ts` pinning file. This contradicts the project convention established in `_zod.ts`, bypasses the single-version pinning mechanism, and would trigger the ESLint `no-restricted-imports` rule if active.</violation>
</file>

<file name="docs/contracts/MIGRATION_GUIDE.md">

<violation number="1" location="docs/contracts/MIGRATION_GUIDE.md:48">
P3: Import `z` from the shared `_zod.ts` pin instead of a direct URL to keep version management centralized.</violation>
</file>

Reply with feedback, questions, or to request a fix.

Re-trigger cubic


export const SyncExternalDbV2 = z
.object({
table: z.string().min(1).max(63),
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.

P1: table is only length-validated; it should be constrained to an explicit allowlist before being used in .from(table) with service-role clients.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At supabase/functions/_shared/contracts/schemas/sync-external-db.ts, line 19:

<comment>`table` is only length-validated; it should be constrained to an explicit allowlist before being used in `.from(table)` with service-role clients.</comment>

<file context>
@@ -0,0 +1,37 @@
+
+export const SyncExternalDbV2 = z
+  .object({
+    table: z.string().min(1).max(63),
+    direction: DirectionEnum,
+    since: z.string().datetime({ offset: true }).optional(),
</file context>

};

const corsHeaders = buildPublicCorsHeaders({ extraAllowHeaders: ["x-e2e-cleanup-token"], allowMethods: "POST, OPTIONS" });
let contractResponseHeaders: Record<string, string> = {};
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.

P1: contractResponseHeaders is request-specific data stored in module-scope mutable state, which can race across concurrent requests and leak/mix response headers between users.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At supabase/functions/e2e-cleanup/index.ts, line 42:

<comment>`contractResponseHeaders` is request-specific data stored in module-scope mutable state, which can race across concurrent requests and leak/mix response headers between users.</comment>

<file context>
@@ -35,11 +39,12 @@ type E2ERateLimitRow = {
 };
 
 const corsHeaders = buildPublicCorsHeaders({ extraAllowHeaders: ["x-e2e-cleanup-token"], allowMethods: "POST, OPTIONS" });
+let contractResponseHeaders: Record<string, string> = {};
 
 function jsonResponse(body: unknown, status = 200, extraHeaders: Record<string, string> = {}) {
</file context>

Comment on lines +42 to +44
const contractResult = await parseContract(req, OwnershipAuditSchemas, {
corsHeaders,
});
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.

P1: This change makes an empty request body invalid, but the previous behavior accepted no body and defaulted to triggered_by = "cron". That can break existing cron/manual invocations that send no JSON.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At supabase/functions/ownership-audit/index.ts, line 42:

<comment>This change makes an empty request body invalid, but the previous behavior accepted no body and defaulted to `triggered_by = "cron"`. That can break existing cron/manual invocations that send no JSON.</comment>

<file context>
@@ -33,13 +39,12 @@ Deno.serve(async (req) => {
-    } catch {
-      // sem body — ok, usa default "cron"
-    }
+    const contractResult = await parseContract(req, OwnershipAuditSchemas, {
+      corsHeaders,
+    });
</file context>
Suggested change
const contractResult = await parseContract(req, OwnershipAuditSchemas, {
corsHeaders,
});
const rawBody = await req.text();
const contractResult = await parseContract(req, OwnershipAuditSchemas, {
corsHeaders,
prereadBody: rawBody.trim() === "" ? "{}" : rawBody,
});

const SUPABASE_URL = process.env.SUPABASE_URL || 'http://localhost:54321';
const ANON_KEY = process.env.SUPABASE_ANON_KEY || process.env.SUPABASE_SERVICE_ROLE_KEY;
const PRODUCT_WEBHOOK_SECRET = process.env.N8N_PRODUCT_WEBHOOK_SECRET || '';
const TIMEOUT_MS = Number(process.env.CONTRACT_TEST_TIMEOUT_MS || 10000);
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.

P2: Validate CONTRACT_TEST_TIMEOUT_MS before using it, otherwise invalid env values can cause immediate aborts and false test failures.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/contract-testing.mjs, line 35:

<comment>Validate `CONTRACT_TEST_TIMEOUT_MS` before using it, otherwise invalid env values can cause immediate aborts and false test failures.</comment>

<file context>
@@ -1,112 +1,303 @@
+const SUPABASE_URL = process.env.SUPABASE_URL || 'http://localhost:54321';
+const ANON_KEY = process.env.SUPABASE_ANON_KEY || process.env.SUPABASE_SERVICE_ROLE_KEY;
+const PRODUCT_WEBHOOK_SECRET = process.env.N8N_PRODUCT_WEBHOOK_SECRET || '';
+const TIMEOUT_MS = Number(process.env.CONTRACT_TEST_TIMEOUT_MS || 10000);
+
+if (!ANON_KEY) {
</file context>
Suggested change
const TIMEOUT_MS = Number(process.env.CONTRACT_TEST_TIMEOUT_MS || 10000);
const TIMEOUT_MS = Number.isFinite(Number(process.env.CONTRACT_TEST_TIMEOUT_MS)) && Number(process.env.CONTRACT_TEST_TIMEOUT_MS) > 0
? Number(process.env.CONTRACT_TEST_TIMEOUT_MS)
: 10000;

// ---------------------------------------------------------------------------

const SUPABASE_URL = process.env.SUPABASE_URL || 'http://localhost:54321';
const ANON_KEY = process.env.SUPABASE_ANON_KEY || process.env.SUPABASE_SERVICE_ROLE_KEY;
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.

P2: Falling back to SUPABASE_SERVICE_ROLE_KEY can hide real authorization failures in contract tests by running requests with elevated privileges.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At scripts/contract-testing.mjs, line 33:

<comment>Falling back to `SUPABASE_SERVICE_ROLE_KEY` can hide real authorization failures in contract tests by running requests with elevated privileges.</comment>

<file context>
@@ -1,112 +1,303 @@
+// ---------------------------------------------------------------------------
+
+const SUPABASE_URL = process.env.SUPABASE_URL || 'http://localhost:54321';
+const ANON_KEY = process.env.SUPABASE_ANON_KEY || process.env.SUPABASE_SERVICE_ROLE_KEY;
+const PRODUCT_WEBHOOK_SECRET = process.env.N8N_PRODUCT_WEBHOOK_SECRET || '';
+const TIMEOUT_MS = Number(process.env.CONTRACT_TEST_TIMEOUT_MS || 10000);
</file context>

const ProductV2 = ProductV1
.extend({
external_id: z.string().min(1).max(255), // agora OBRIGATÓRIO
updated_at: z.string().datetime().optional(),
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.

P2: updated_at uses datetime() default validation, which rejects ISO datetimes with timezone offsets (e.g. -03:00). This can cause valid webhook payloads to fail in v2.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At supabase/functions/_shared/contracts/schemas/product-webhook.ts, line 76:

<comment>`updated_at` uses `datetime()` default validation, which rejects ISO datetimes with timezone offsets (e.g. `-03:00`). This can cause valid webhook payloads to fail in v2.</comment>

<file context>
@@ -0,0 +1,141 @@
+const ProductV2 = ProductV1
+  .extend({
+    external_id: z.string().min(1).max(255), // agora OBRIGATÓRIO
+    updated_at: z.string().datetime().optional(),
+    currency: z.enum(["BRL", "USD", "EUR"]).default("BRL"),
+  })
</file context>
Suggested change
updated_at: z.string().datetime().optional(),
updated_at: z.string().datetime({ offset: true }).optional(),

Comment thread docs/contracts/README.md

```ts
// supabase/functions/_shared/contracts/schemas/<endpoint>.ts
import { z } from "https://esm.sh/zod@3.23.8";
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.

P2: The "Adicionando um novo schema" example incorrectly imports Zod directly from the esm.sh URL instead of from the centralized _zod.ts pinning file. This contradicts the project convention established in _zod.ts, bypasses the single-version pinning mechanism, and would trigger the ESLint no-restricted-imports rule if active.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docs/contracts/README.md, line 102:

<comment>The "Adicionando um novo schema" example incorrectly imports Zod directly from the esm.sh URL instead of from the centralized `_zod.ts` pinning file. This contradicts the project convention established in `_zod.ts`, bypasses the single-version pinning mechanism, and would trigger the ESLint `no-restricted-imports` rule if active.</comment>

<file context>
@@ -0,0 +1,156 @@
+
+```ts
+// supabase/functions/_shared/contracts/schemas/<endpoint>.ts
+import { z } from "https://esm.sh/zod@3.23.8";
+
+export const MyEndpointV1 = z.object({ /* ... */ });
</file context>
Suggested change
import { z } from "https://esm.sh/zod@3.23.8";
import { z } from "../_zod.ts";

dry_run: true,
status: "invalid",
reason: "invalid_json",
reason: contractResult.response.status === 400 ? "invalid_json" : "invalid_payload",
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.

P2: Do not map all HTTP 400 contract failures to invalid_json; missing_body is also 400 and is currently logged with the wrong audit reason.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At supabase/functions/e2e-cleanup/index.ts, line 255:

<comment>Do not map all HTTP 400 contract failures to `invalid_json`; `missing_body` is also 400 and is currently logged with the wrong audit reason.</comment>

<file context>
@@ -232,33 +240,31 @@ Deno.serve(async (req: Request) => {
       dry_run: true,
       status: "invalid",
-      reason: "invalid_json",
+      reason: contractResult.response.status === 400 ? "invalid_json" : "invalid_payload",
       ip,
       user_agent: userAgent,
</file context>

/^([0-9]{1,3}\.){3}[0-9]{1,3}(\/([0-9]|[1-2][0-9]|3[0-2]))?$|^([0-9a-fA-F:]+)(\/([0-9]|[1-9][0-9]|1[01][0-9]|12[0-8]))?$/;

export const BlockIpTemporarilyV1 = z.object({
ip: z.string().min(1).max(45).regex(IP_REGEX_V1, {
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.

P2: Normalize ip before regex validation (e.g. .trim()) to preserve v1 compatibility for inputs with surrounding whitespace.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At supabase/functions/_shared/contracts/schemas/block-ip-temporarily.ts, line 16:

<comment>Normalize `ip` before regex validation (e.g. `.trim()`) to preserve v1 compatibility for inputs with surrounding whitespace.</comment>

<file context>
@@ -0,0 +1,45 @@
+  /^([0-9]{1,3}\.){3}[0-9]{1,3}(\/([0-9]|[1-2][0-9]|3[0-2]))?$|^([0-9a-fA-F:]+)(\/([0-9]|[1-9][0-9]|1[01][0-9]|12[0-8]))?$/;
+
+export const BlockIpTemporarilyV1 = z.object({
+  ip: z.string().min(1).max(45).regex(IP_REGEX_V1, {
+    message: "IP inválido (use IPv4, IPv6 ou CIDR)",
+  }),
</file context>

Comment thread docs/contracts/MIGRATION_GUIDE.md Outdated
`supabase/functions/_shared/contracts/schemas/<endpoint>.ts`:

```ts
import { z } from "https://esm.sh/zod@3.23.8";
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.

P3: Import z from the shared _zod.ts pin instead of a direct URL to keep version management centralized.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At docs/contracts/MIGRATION_GUIDE.md, line 48:

<comment>Import `z` from the shared `_zod.ts` pin instead of a direct URL to keep version management centralized.</comment>

<file context>
@@ -0,0 +1,191 @@
+`supabase/functions/_shared/contracts/schemas/<endpoint>.ts`:
+
+```ts
+import { z } from "https://esm.sh/zod@3.23.8";
+
+export const SendEmailV1 = z.object({
</file context>

@adm01-debug adm01-debug merged commit 1b43e7a into main May 24, 2026
19 of 28 checks passed
@adm01-debug adm01-debug deleted the feat/contracts-08-zod-pinning-barrel branch May 24, 2026 15:26
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