From 7a49fb958bd922f13e652eea8ed397aa5318bd5e Mon Sep 17 00:00:00 2001 From: adm01-debug Date: Mon, 25 May 2026 15:04:51 -0300 Subject: [PATCH] chore(contracts): centralize Zod pinning via _shared/contracts barrel (closes #52) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Substitui 21 imports diretos de Zod (mix de esm.sh@3.22.4, esm.sh@3.23.8, deno.land/x@3.22.4 e deno.land/x@3.23.8) pelo barrel canônico: import { z } from "../_shared/contracts/index.ts"; O barrel já re-exportava `z` do pin canônico `https://esm.sh/zod@3.23.8` desde o #45. Este PR fecha o ciclo migrando os 21 call sites restantes e adiciona guardrail anti-regressão. ## Mudanças - 21 arquivos `supabase/functions//index.ts` migrados (1 import cada) - `scripts/check-zod-pinning.mjs` — script que falha com exit 1 se achar qualquer import direto de zod fora de `_shared/contracts/` - `package.json` — novo script `check:zod-pinning` - `.github/workflows/ci.yml` — step "Zod pinning gate" adicionado ao pipeline (roda após o ESLint baseline gate) ## Benefícios - Bundle size das Edge Functions diminui (Zod 3.23 single-version vs 4 versões diferentes coexistindo) - Migração futura para Zod 4 vira PR único no barrel, não 21 PRs - Comportamento previsível (mesma versão = mesmas semânticas) ## Validação - `node scripts/check-zod-pinning.mjs` → "✓ Pinning de Zod centralizado" - `grep -rn 'from.*zod' supabase/functions/ | grep -v _shared/contracts/` → 0 hits - Schemas internos (`_shared/contracts/schemas/*.ts`) continuam importando direto do esm.sh — esse é o ponto de centralização ## Sobre #48 (referenciado como dependência neste issue) Auditoria pós-fato: as 3 funções P2 (`force-global-logout`, `e2e-cleanup`, `block-ip-temporarily`) já usam `parseContract` com schemas v1+v2. Será fechado em paralelo com nota documental. Refs: closes #52 --- .github/workflows/ci.yml | 3 ++ package.json | 1 + scripts/check-zod-pinning.mjs | 46 +++++++++++++++++++ .../functions/commemorative-dates/index.ts | 2 +- .../functions/comparison-ai-advisor/index.ts | 2 +- supabase/functions/connection-tester/index.ts | 2 +- supabase/functions/crm-db-bridge/index.ts | 2 +- supabase/functions/dropbox-list/index.ts | 2 +- supabase/functions/elevenlabs-tts/index.ts | 2 +- .../functions/external-db-bridge/index.ts | 2 +- .../functions/external-db-inspect/index.ts | 2 +- .../functions/full-op-diagnostics/index.ts | 2 +- .../functions/generate-ad-image/index.test.ts | 3 +- supabase/functions/generate-ad-image/index.ts | 2 +- supabase/functions/magic-up-score/index.ts | 3 +- supabase/functions/mcp-keys-issue/index.ts | 2 +- supabase/functions/mcp-keys-revoke/index.ts | 2 +- supabase/functions/mcp-keys-rotate/index.ts | 2 +- supabase/functions/mcp-keys-update/index.ts | 2 +- supabase/functions/quote-sync/index.ts | 2 +- supabase/functions/rate-limit-check/index.ts | 3 +- supabase/functions/secrets-manager/index.ts | 2 +- supabase/functions/verify-email/index.ts | 3 +- supabase/functions/voice-agent/index.ts | 2 +- 24 files changed, 71 insertions(+), 25 deletions(-) create mode 100755 scripts/check-zod-pinning.mjs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1c61650e9..032cce369 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -127,6 +127,9 @@ jobs: - name: ESLint baseline gate (bloqueia apenas regressões novas) run: npm run lint:baseline + - name: Zod pinning gate (issue #52 — barrel centralization) + run: npm run check:zod-pinning + # hooks já cobertos pelo job dedicado hooks-tests (timeout-minutes: 10). # test:strict-ref e test:coverage omitidos aqui — rodam em ref-warning-suite # e integration-tests respectivamente, evitando tripla execução da suite. diff --git a/package.json b/package.json index cc8fee3b0..f2490abfe 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "lint:check": "eslint src --max-warnings=500", "lint:baseline": "node scripts/check-eslint-baseline.mjs", "lint:baseline:update": "node scripts/eslint-baseline-generate.mjs", + "check:zod-pinning": "node scripts/check-zod-pinning.mjs", "typecheck": "node scripts/check-tsc-baseline.mjs", "qa:lint": "eslint src --max-warnings=500", "qa:typecheck": "tsc -p tsconfig.app.json --noEmit", diff --git a/scripts/check-zod-pinning.mjs b/scripts/check-zod-pinning.mjs new file mode 100755 index 000000000..0995773bf --- /dev/null +++ b/scripts/check-zod-pinning.mjs @@ -0,0 +1,46 @@ +#!/usr/bin/env node +/** + * Guardrail anti-regressão #52 — pinning centralizado de Zod. + * + * Falha (exit 1) se encontrar imports diretos de Zod fora de + * `supabase/functions/_shared/contracts/`. Edge Functions devem importar + * `z` exclusivamente via barrel: + * + * import { z } from "../_shared/contracts/index.ts"; + * + * Ref: https://github.com/adm01-debug/promo-gifts-v4/issues/52 + */ +import { execSync } from 'node:child_process'; +import { exit } from 'node:process'; + +const ALLOWED_ROOT = 'supabase/functions/_shared/contracts/'; + +// grep com -P (pcre) não disponível em todo lugar; usa -E e filtramos depois +let output = ''; +try { + output = execSync( + 'grep -rn "from[[:space:]]*[\\"\\\\\']https://.*zod" supabase/functions/ 2>/dev/null || true', + { encoding: 'utf8' }, + ); +} catch (err) { + // grep retorna exit 1 quando não acha nada — tratamos como sucesso + output = ''; +} + +const violations = output + .split('\n') + .filter(Boolean) + .filter(line => !line.startsWith(ALLOWED_ROOT)); + +if (violations.length === 0) { + console.log('✓ Pinning de Zod centralizado (0 imports diretos fora de _shared/contracts/)'); + exit(0); +} + +console.error('❌ Imports diretos de Zod encontrados fora de _shared/contracts/:'); +console.error(' (Zod deve ser importado via barrel: import { z } from "../_shared/contracts/index.ts")\n'); +for (const v of violations) { + console.error(` ${v}`); +} +console.error('\n Veja https://github.com/adm01-debug/promo-gifts-v4/issues/52 para contexto.'); +exit(1); diff --git a/supabase/functions/commemorative-dates/index.ts b/supabase/functions/commemorative-dates/index.ts index 4a52eb388..5a398e8f0 100644 --- a/supabase/functions/commemorative-dates/index.ts +++ b/supabase/functions/commemorative-dates/index.ts @@ -1,7 +1,7 @@ import { getCorsHeaders } from '../_shared/cors.ts'; import { safeErrorResponse } from '../_shared/error-response.ts'; import { createClient } from "https://esm.sh/@supabase/supabase-js@2.49.4"; -import { z } from "https://esm.sh/zod@3.23.8"; +import { z } from "../_shared/contracts/index.ts"; import { parseBodyWithSchema } from "../_shared/zod-validate.ts"; const ActionSchema = z.object({ diff --git a/supabase/functions/comparison-ai-advisor/index.ts b/supabase/functions/comparison-ai-advisor/index.ts index 98618dc77..e9d51e80e 100644 --- a/supabase/functions/comparison-ai-advisor/index.ts +++ b/supabase/functions/comparison-ai-advisor/index.ts @@ -3,7 +3,7 @@ import { authenticateRequest, requireRole, authErrorResponse } from '../_shared/ // Comparison AI Advisor — Lovable AI Gateway // Recebe lista slim de produtos e retorna 3-5 bullets + bestFor highVolume/fastDelivery/premium. -import { z } from 'https://deno.land/x/zod@v3.22.4/mod.ts'; +import { z } from "../_shared/contracts/index.ts"; import { safeErrorFields } from '../_shared/log-safety.ts'; // Fallback CORS headers — sobrescritos per-request via getCorsHeaders(req). diff --git a/supabase/functions/connection-tester/index.ts b/supabase/functions/connection-tester/index.ts index e44cf9646..7ffb81343 100644 --- a/supabase/functions/connection-tester/index.ts +++ b/supabase/functions/connection-tester/index.ts @@ -4,7 +4,7 @@ import { safeErrorResponse } from "../_shared/error-response.ts"; // Reads credentials from `integration_credentials` (DB-first) with env fallback. // Core ping/persistence logic lives in `_shared/connection-test-runner.ts`. import { createClient } from "https://esm.sh/@supabase/supabase-js@2.49.4"; -import { z } from "https://deno.land/x/zod@v3.23.8/mod.ts"; +import { z } from "../_shared/contracts/index.ts"; import { runConnectionTest } from "../_shared/connection-test-runner.ts"; const BodySchema = z.object({ diff --git a/supabase/functions/crm-db-bridge/index.ts b/supabase/functions/crm-db-bridge/index.ts index a41c9238e..a33bebcda 100644 --- a/supabase/functions/crm-db-bridge/index.ts +++ b/supabase/functions/crm-db-bridge/index.ts @@ -1,6 +1,6 @@ import { getCorsHeaders, handleCorsPreflightIfNeeded } from '../_shared/cors.ts'; import { createClient, SupabaseClient } from "https://esm.sh/@supabase/supabase-js@2.49.4"; -import { z } from "https://esm.sh/zod@3.23.8"; +import { z } from "../_shared/contracts/index.ts"; import { runBotProtection } from '../_shared/bot-protection.ts'; import { getBreaker, circuitOpenResponse, getAllBreakerStatuses } from '../_shared/circuit-breaker.ts'; import { AsyncLocalStorage } from "node:async_hooks"; diff --git a/supabase/functions/dropbox-list/index.ts b/supabase/functions/dropbox-list/index.ts index e595bd8ba..d2546ca7f 100644 --- a/supabase/functions/dropbox-list/index.ts +++ b/supabase/functions/dropbox-list/index.ts @@ -1,6 +1,6 @@ import { getCorsHeaders } from '../_shared/cors.ts'; import { authenticateRequest, requireRole, authErrorResponse } from '../_shared/auth.ts'; -import { z } from "https://esm.sh/zod@3.23.8"; +import { z } from "../_shared/contracts/index.ts"; import { fetchWithBreaker, CircuitOpenError, circuitOpenResponse } from '../_shared/external-fetch.ts'; const BodySchema = z.object({ diff --git a/supabase/functions/elevenlabs-tts/index.ts b/supabase/functions/elevenlabs-tts/index.ts index bd5f30c55..a9c4af8f0 100644 --- a/supabase/functions/elevenlabs-tts/index.ts +++ b/supabase/functions/elevenlabs-tts/index.ts @@ -1,6 +1,6 @@ import { getCorsHeaders } from '../_shared/cors.ts'; import { authenticateRequest, authErrorResponse } from '../_shared/auth.ts'; -import { z } from 'https://deno.land/x/zod@v3.22.4/mod.ts'; +import { z } from "../_shared/contracts/index.ts"; import { runBotProtection } from '../_shared/bot-protection.ts'; import { fetchWithBreaker, CircuitOpenError, circuitOpenResponse } from '../_shared/external-fetch.ts'; diff --git a/supabase/functions/external-db-bridge/index.ts b/supabase/functions/external-db-bridge/index.ts index d8b5dc4e2..bcc3eb745 100644 --- a/supabase/functions/external-db-bridge/index.ts +++ b/supabase/functions/external-db-bridge/index.ts @@ -6,7 +6,7 @@ import { type ServiceClient, castSupabaseClient, } from "../_shared/supabase-client-adapter.ts"; -import { z } from "https://deno.land/x/zod@v3.23.8/mod.ts"; +import { z } from "../_shared/contracts/index.ts"; import { buildPublicCorsHeaders, getCorsHeaders, handleCorsPreflightIfNeeded } from "../_shared/cors.ts"; import { type Operation, diff --git a/supabase/functions/external-db-inspect/index.ts b/supabase/functions/external-db-inspect/index.ts index 2bb313f9e..4af674ec4 100644 --- a/supabase/functions/external-db-inspect/index.ts +++ b/supabase/functions/external-db-inspect/index.ts @@ -1,6 +1,6 @@ import { getCorsHeaders } from '../_shared/cors.ts'; import { createClient } from "https://esm.sh/@supabase/supabase-js@2.49.4"; -import { z } from "https://esm.sh/zod@3.23.8"; +import { z } from "../_shared/contracts/index.ts"; import { runBotProtection } from '../_shared/bot-protection.ts'; const BodySchema = z.object({ diff --git a/supabase/functions/full-op-diagnostics/index.ts b/supabase/functions/full-op-diagnostics/index.ts index 7e8158e41..bcc45c401 100644 --- a/supabase/functions/full-op-diagnostics/index.ts +++ b/supabase/functions/full-op-diagnostics/index.ts @@ -15,7 +15,7 @@ // token como usado — preserva a possibilidade de execução real depois. // ---------------------------------------------------------------------------- import { createClient } from "https://esm.sh/@supabase/supabase-js@2.45.0"; -import { z } from "https://esm.sh/zod@3.23.8"; +import { z } from "../_shared/contracts/index.ts"; import { getCorsHeaders, handleCorsPreflightIfNeeded } from "../_shared/cors.ts"; type CheckStatus = "pass" | "fail" | "skipped" | "error"; diff --git a/supabase/functions/generate-ad-image/index.test.ts b/supabase/functions/generate-ad-image/index.test.ts index f82f2f3ca..a6e54d3e0 100644 --- a/supabase/functions/generate-ad-image/index.test.ts +++ b/supabase/functions/generate-ad-image/index.test.ts @@ -1,7 +1,6 @@ import "https://deno.land/std@0.224.0/dotenv/load.ts"; import { assertEquals } from "https://deno.land/std@0.224.0/assert/mod.ts"; -import { z } from "https://deno.land/x/zod@v3.23.8/mod.ts"; - +import { z } from "../_shared/contracts/index.ts"; // Mirror the schema from the function const BodySchema = z.object({ productImageUrl: z.string().url(), diff --git a/supabase/functions/generate-ad-image/index.ts b/supabase/functions/generate-ad-image/index.ts index 268332a39..d4bc01987 100644 --- a/supabase/functions/generate-ad-image/index.ts +++ b/supabase/functions/generate-ad-image/index.ts @@ -1,7 +1,7 @@ import { getCorsHeaders, handleCorsPreflightIfNeeded } from '../_shared/cors.ts'; import { authenticateRequest, authErrorResponse } from '../_shared/auth.ts'; import { callAiWithTracking, QuotaExceededError } from '../_shared/ai-usage.ts'; -import { z } from "https://deno.land/x/zod@v3.23.8/mod.ts"; +import { z } from "../_shared/contracts/index.ts"; import { runBotProtection } from '../_shared/bot-protection.ts'; const BodySchema = z.object({ diff --git a/supabase/functions/magic-up-score/index.ts b/supabase/functions/magic-up-score/index.ts index e04cf4b00..b0f9b736f 100644 --- a/supabase/functions/magic-up-score/index.ts +++ b/supabase/functions/magic-up-score/index.ts @@ -2,8 +2,7 @@ import { getCorsHeaders } from '../_shared/cors.ts'; import { authenticateRequest, authErrorResponse } from '../_shared/auth.ts'; import { callAiWithTracking, QuotaExceededError } from '../_shared/ai-usage.ts'; import { runBotProtection } from '../_shared/bot-protection.ts'; -import { z } from "https://deno.land/x/zod@v3.23.8/mod.ts"; - +import { z } from "../_shared/contracts/index.ts"; const CriterionSchema = z.object({ id: z.string().min(1), label: z.string().min(1), diff --git a/supabase/functions/mcp-keys-issue/index.ts b/supabase/functions/mcp-keys-issue/index.ts index efef61b00..71ba2046d 100644 --- a/supabase/functions/mcp-keys-issue/index.ts +++ b/supabase/functions/mcp-keys-issue/index.ts @@ -16,7 +16,7 @@ import { getCorsHeaders } from "../_shared/cors.ts"; */ import { createClient } from "https://esm.sh/@supabase/supabase-js@2.95.0"; -import { z } from "https://esm.sh/zod@3.23.8"; +import { z } from "../_shared/contracts/index.ts"; import { KNOWN_SCOPES, FULL_SCOPE, diff --git a/supabase/functions/mcp-keys-revoke/index.ts b/supabase/functions/mcp-keys-revoke/index.ts index 58e5016d8..ed888df48 100644 --- a/supabase/functions/mcp-keys-revoke/index.ts +++ b/supabase/functions/mcp-keys-revoke/index.ts @@ -6,7 +6,7 @@ import { getCorsHeaders } from "../_shared/cors.ts"; * payload_summary antes do trigger DB. */ import { createClient } from "https://esm.sh/@supabase/supabase-js@2.95.0"; -import { z } from "https://esm.sh/zod@3.23.8"; +import { z } from "../_shared/contracts/index.ts"; import { getOrCreateRequestId, REQUEST_ID_HEADER } from "../_shared/request-id.ts"; import { writeAuditEntry, summarizePayload, extractRequestMeta } from "../_shared/audit-log.ts"; import { recordMcpViolation, mapViolationReason } from "../_shared/mcp-violations.ts"; diff --git a/supabase/functions/mcp-keys-rotate/index.ts b/supabase/functions/mcp-keys-rotate/index.ts index 3c7d4a548..e955eceeb 100644 --- a/supabase/functions/mcp-keys-rotate/index.ts +++ b/supabase/functions/mcp-keys-rotate/index.ts @@ -8,7 +8,7 @@ import { getCorsHeaders } from "../_shared/cors.ts"; */ import { createClient } from "https://esm.sh/@supabase/supabase-js@2.95.0"; -import { z } from "https://esm.sh/zod@3.23.8"; +import { z } from "../_shared/contracts/index.ts"; import { FULL_SCOPE_CONFIRMATION, FULL_SCOPE_MIN_JUSTIFICATION, diff --git a/supabase/functions/mcp-keys-update/index.ts b/supabase/functions/mcp-keys-update/index.ts index db98b1bc5..1edc1aa24 100644 --- a/supabase/functions/mcp-keys-update/index.ts +++ b/supabase/functions/mcp-keys-update/index.ts @@ -6,7 +6,7 @@ import { getCorsHeaders } from "../_shared/cors.ts"; * Toda mudança é auditada com request_id, payload_summary, duração e status. */ import { createClient } from "https://esm.sh/@supabase/supabase-js@2.95.0"; -import { z } from "https://esm.sh/zod@3.23.8"; +import { z } from "../_shared/contracts/index.ts"; import { KNOWN_SCOPES, FULL_SCOPE_CONFIRMATION, diff --git a/supabase/functions/quote-sync/index.ts b/supabase/functions/quote-sync/index.ts index 0a175622c..b5053aee6 100644 --- a/supabase/functions/quote-sync/index.ts +++ b/supabase/functions/quote-sync/index.ts @@ -2,7 +2,7 @@ import { getCorsHeaders } from "../_shared/cors.ts"; import { authenticateRequest, requireRole, authErrorResponse } from "../_shared/auth.ts"; /// import { createClient } from "npm:@supabase/supabase-js@2.49.1"; -import { z } from "https://esm.sh/zod@3.23.8"; +import { z } from "../_shared/contracts/index.ts"; import { parseBodyWithSchema } from "../_shared/zod-validate.ts"; import { resolveCredential } from "../_shared/credentials.ts"; diff --git a/supabase/functions/rate-limit-check/index.ts b/supabase/functions/rate-limit-check/index.ts index cc55696a6..59f58acee 100644 --- a/supabase/functions/rate-limit-check/index.ts +++ b/supabase/functions/rate-limit-check/index.ts @@ -1,8 +1,7 @@ import { getCorsHeaders, handleCorsPreflightIfNeeded } from '../_shared/cors.ts'; import { safeErrorResponse } from '../_shared/error-response.ts'; import { logSecurityEvent } from '../_shared/security.ts'; -import { z } from "https://deno.land/x/zod@v3.23.8/mod.ts"; - +import { z } from "../_shared/contracts/index.ts"; const BodySchema = z.object({ endpoint: z.enum(['login', 'api', 'ai', 'approval']).default('api'), }).partial(); diff --git a/supabase/functions/secrets-manager/index.ts b/supabase/functions/secrets-manager/index.ts index 8169cc6a7..60735fc5f 100644 --- a/supabase/functions/secrets-manager/index.ts +++ b/supabase/functions/secrets-manager/index.ts @@ -2,7 +2,7 @@ import { getCorsHeaders } from "../_shared/cors.ts"; // Admin-only secrets manager for the Conexões hub. // Persists values in `integration_credentials` and never returns plaintext to the client. import { createClient } from "https://esm.sh/@supabase/supabase-js@2.49.4"; -import { z } from "https://deno.land/x/zod@v3.23.8/mod.ts"; +import { z } from "../_shared/contracts/index.ts"; import { invalidateCredentialCache, getCredentialCacheMetrics, diff --git a/supabase/functions/verify-email/index.ts b/supabase/functions/verify-email/index.ts index 16df19d96..f57574e2e 100644 --- a/supabase/functions/verify-email/index.ts +++ b/supabase/functions/verify-email/index.ts @@ -1,7 +1,6 @@ import { getCorsHeaders, handleCorsPreflightIfNeeded } from '../_shared/cors.ts'; import { createClient } from "https://esm.sh/@supabase/supabase-js@2.49.4"; -import { z } from "https://deno.land/x/zod@v3.23.8/mod.ts"; - +import { z } from "../_shared/contracts/index.ts"; const BodySchema = z.object({ token: z.string().min(1, "Token não fornecido"), }); diff --git a/supabase/functions/voice-agent/index.ts b/supabase/functions/voice-agent/index.ts index 05bbb0916..68900ca20 100644 --- a/supabase/functions/voice-agent/index.ts +++ b/supabase/functions/voice-agent/index.ts @@ -1,6 +1,6 @@ import { getCorsHeaders } from '../_shared/cors.ts'; import { authenticateRequest, authErrorResponse } from '../_shared/auth.ts'; -import { z } from 'https://deno.land/x/zod@v3.22.4/mod.ts'; +import { z } from "../_shared/contracts/index.ts"; import { callAiWithTracking, QuotaExceededError } from '../_shared/ai-usage.ts'; import { SYSTEM_PROMPT, VOICE_COMMAND_TOOL, TOOL_CHOICE } from './systemPrompt.ts'; import { parseAiResponse } from './parseAiResponse.ts';