feat(contracts): contracts/02+03+04 — migrate 13 edge functions to parseContract#69
Conversation
…builder, bi-copilot
…, simulation-orchestrator, sync-external-db
…logout, e2e-cleanup, block-ip-temporarily
…r, sync-external-db, trends-insights
…ara parseContract
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Important Review skippedAuto reviews are disabled on base/target branches other than the default branch. 🗂️ Base branches to auto review (1)
Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Path: .coderabbit.yaml Review profile: CHILL Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
|
This pull request has been ignored for the connected project Preview Branches by Supabase. |
There was a problem hiding this comment.
10 issues found across 29 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="supabase/functions/trends-insights/index.ts">
<violation number="1" location="supabase/functions/trends-insights/index.ts:52">
P1: This change makes empty request bodies fail, but this endpoint previously accepted no body and defaulted `days` to 30. It introduces a compatibility regression for existing callers that omit payloads.</violation>
</file>
<file name="supabase/functions/e2e-cleanup/index.ts">
<violation number="1" location="supabase/functions/e2e-cleanup/index.ts:42">
P2: Request-specific contract headers are stored in module-scope mutable state, which can leak/cross-contaminate headers between requests.</violation>
</file>
<file name="supabase/functions/ownership-repair/index.ts">
<violation number="1" location="supabase/functions/ownership-repair/index.ts:20">
P2: Avoid storing `contractResponseHeaders` in module scope; shared mutable state can leak headers across concurrent requests.</violation>
</file>
<file name="supabase/functions/bi-copilot/index.ts">
<violation number="1" location="supabase/functions/bi-copilot/index.ts:113">
P2: `responseHeaders` are only applied on the success response. After parsing succeeds, error responses (429/402/502/500) should also include contract headers to keep version/deprecation signaling consistent.</violation>
</file>
<file name="supabase/functions/block-ip-temporarily/index.ts">
<violation number="1" location="supabase/functions/block-ip-temporarily/index.ts:10">
P2: `contractResponseHeaders` is stored in module scope and mutated per request, which can leak/overwrite headers across concurrent requests.</violation>
<violation number="2" location="supabase/functions/block-ip-temporarily/index.ts:55">
P2: `hours` input became stricter: numeric strings that previously worked are now rejected by `z.number()`, causing a compatibility break for existing clients.</violation>
</file>
<file name="supabase/functions/ownership-audit/index.ts">
<violation number="1" location="supabase/functions/ownership-audit/index.ts:42">
P1: This migration changed ownership-audit from accepting empty request bodies to returning 422. Empty-body invocations previously defaulted `triggered_by` to `"cron"`, so scheduled calls without payload can now fail.</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:12">
P2: The V2 IP regex is not actually strict: it accepts invalid IPv4/IPv6 values, so malformed addresses can pass contract validation.</violation>
<violation number="2" location="supabase/functions/_shared/contracts/schemas/block-ip-temporarily.ts:29">
P2: V2 is missing `idempotency_key` even though this endpoint performs writes; retries can create duplicate side effects and this is inconsistent with other mutating V2 contracts.</violation>
</file>
<file name="supabase/functions/_shared/contracts/schemas/step-up-verify.ts">
<violation number="1" location="supabase/functions/_shared/contracts/schemas/step-up-verify.ts:32">
P2: `step-up-verify` V2 is strict but has no `idempotency_key`, so retried side-effect requests cannot be deduplicated and may execute twice.</violation>
</file>
Reply with feedback, questions, or to request a fix.
Re-trigger cubic
| const contractResult = await parseContract(req, TrendsInsightsSchemas, { | ||
| corsHeaders, | ||
| }); |
There was a problem hiding this comment.
P1: This change makes empty request bodies fail, but this endpoint previously accepted no body and defaulted days to 30. It introduces a compatibility regression for existing callers that omit payloads.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At supabase/functions/trends-insights/index.ts, line 52:
<comment>This change makes empty request bodies fail, but this endpoint previously accepted no body and defaulted `days` to 30. It introduces a compatibility regression for existing callers that omit payloads.</comment>
<file context>
@@ -45,8 +49,12 @@ Deno.serve(async (req) => {
- const body = (await req.json().catch(() => ({}))) as Body;
- const days = Math.max(1, Math.min(body.days ?? 30, 365));
+ const contractResult = await parseContract(req, TrendsInsightsSchemas, {
+ corsHeaders,
+ });
</file context>
| const contractResult = await parseContract(req, TrendsInsightsSchemas, { | |
| corsHeaders, | |
| }); | |
| const rawBody = await req.text().catch(() => ""); | |
| const contractResult = await parseContract(req, TrendsInsightsSchemas, { | |
| corsHeaders, | |
| prereadBody: rawBody.trim() === "" ? "{}" : rawBody, | |
| }); |
| const contractResult = await parseContract(req, OwnershipAuditSchemas, { | ||
| corsHeaders, | ||
| }); | ||
| if (!contractResult.ok) return contractResult.response; | ||
| contractResponseHeaders = contractResult.responseHeaders; | ||
| const triggeredBy = contractResult.data.triggered_by ?? "cron"; |
There was a problem hiding this comment.
P1: This migration changed ownership-audit from accepting empty request bodies to returning 422. Empty-body invocations previously defaulted triggered_by to "cron", so scheduled calls without payload can now fail.
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 migration changed ownership-audit from accepting empty request bodies to returning 422. Empty-body invocations previously defaulted `triggered_by` to `"cron"`, so scheduled calls without payload can now fail.</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>
| const contractResult = await parseContract(req, OwnershipAuditSchemas, { | |
| corsHeaders, | |
| }); | |
| if (!contractResult.ok) return contractResult.response; | |
| contractResponseHeaders = contractResult.responseHeaders; | |
| const triggeredBy = contractResult.data.triggered_by ?? "cron"; | |
| const rawBody = await req.text(); | |
| const contractResult = await parseContract(req, OwnershipAuditSchemas, { | |
| corsHeaders, | |
| prereadBody: rawBody.trim() === "" ? "{}" : rawBody, | |
| }); | |
| if (!contractResult.ok) return contractResult.response; | |
| contractResponseHeaders = contractResult.responseHeaders; | |
| const triggeredBy = contractResult.data.triggered_by ?? "cron"; |
| }; | ||
|
|
||
| const corsHeaders = buildPublicCorsHeaders({ extraAllowHeaders: ["x-e2e-cleanup-token"], allowMethods: "POST, OPTIONS" }); | ||
| let contractResponseHeaders: Record<string, string> = {}; |
There was a problem hiding this comment.
P2: Request-specific contract headers are stored in module-scope mutable state, which can leak/cross-contaminate headers between requests.
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>Request-specific contract headers are stored in module-scope mutable state, which can leak/cross-contaminate headers between requests.</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>
|
|
||
| // Module-scope CORS headers — atribuído per-request no handler. | ||
| let corsHeaders: Record<string, string> = {}; | ||
| let contractResponseHeaders: Record<string, string> = {}; |
There was a problem hiding this comment.
P2: Avoid storing contractResponseHeaders in module scope; shared mutable state can leak headers across concurrent requests.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At supabase/functions/ownership-repair/index.ts, line 20:
<comment>Avoid storing `contractResponseHeaders` in module scope; shared mutable state can leak headers across concurrent requests.</comment>
<file context>
@@ -10,9 +10,14 @@ import { getCorsHeaders } from "../_shared/cors.ts";
// Module-scope CORS headers — atribuído per-request no handler.
let corsHeaders: Record<string, string> = {};
+let contractResponseHeaders: Record<string, string> = {};
type RepairOrphansResult = {
</file context>
| return new Response(JSON.stringify({ answer }), { | ||
| status: 200, | ||
| headers: { ...corsHeaders, "Content-Type": "application/json" }, | ||
| headers: { ...corsHeaders, ...responseHeaders, "Content-Type": "application/json" }, |
There was a problem hiding this comment.
P2: responseHeaders are only applied on the success response. After parsing succeeds, error responses (429/402/502/500) should also include contract headers to keep version/deprecation signaling consistent.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At supabase/functions/bi-copilot/index.ts, line 113:
<comment>`responseHeaders` are only applied on the success response. After parsing succeeds, error responses (429/402/502/500) should also include contract headers to keep version/deprecation signaling consistent.</comment>
<file context>
@@ -108,7 +110,7 @@ ${JSON.stringify(body.context, null, 2)}`;
return new Response(JSON.stringify({ answer }), {
status: 200,
- headers: { ...corsHeaders, "Content-Type": "application/json" },
+ headers: { ...corsHeaders, ...responseHeaders, "Content-Type": "application/json" },
});
} catch (e) {
</file context>
| if (!ip || !IP_REGEX.test(ip) || ip.length > 45) { | ||
| return jsonRes({ error: "IP inválido (use IPv4, IPv6 ou CIDR)" }, 400); | ||
| } | ||
| const contractResult = await parseContract(req, BlockIpTemporarilySchemas, { |
There was a problem hiding this comment.
P2: hours input became stricter: numeric strings that previously worked are now rejected by z.number(), causing a compatibility break for existing clients.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At supabase/functions/block-ip-temporarily/index.ts, line 55:
<comment>`hours` input became stricter: numeric strings that previously worked are now rejected by `z.number()`, causing a compatibility break for existing clients.</comment>
<file context>
@@ -46,16 +52,16 @@ Deno.serve(async (req) => {
- if (!ip || !IP_REGEX.test(ip) || ip.length > 45) {
- return jsonRes({ error: "IP inválido (use IPv4, IPv6 ou CIDR)" }, 400);
- }
+ const contractResult = await parseContract(req, BlockIpTemporarilySchemas, {
+ corsHeaders,
+ });
</file context>
|
|
||
| // Module-scope CORS headers — atribuído per-request no handler. | ||
| let corsHeaders: Record<string, string> = {}; | ||
| let contractResponseHeaders: Record<string, string> = {}; |
There was a problem hiding this comment.
P2: contractResponseHeaders is stored in module scope and mutated per request, which can leak/overwrite headers across concurrent requests.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At supabase/functions/block-ip-temporarily/index.ts, line 10:
<comment>`contractResponseHeaders` is stored in module scope and mutated per request, which can leak/overwrite headers across concurrent requests.</comment>
<file context>
@@ -1,13 +1,18 @@
// 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) {
</file context>
| message: "IP inválido (use IPv4, IPv6 ou CIDR)", | ||
| }), | ||
| reason: z.string().min(1).max(500), | ||
| hours: z.number().int().min(1).max(720), |
There was a problem hiding this comment.
P2: V2 is missing idempotency_key even though this endpoint performs writes; retries can create duplicate side effects and this is inconsistent with other mutating V2 contracts.
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 29:
<comment>V2 is missing `idempotency_key` even though this endpoint performs writes; retries can create duplicate side effects and this is inconsistent with other mutating V2 contracts.</comment>
<file context>
@@ -0,0 +1,45 @@
+ message: "IP inválido (use IPv4, IPv6 ou CIDR)",
+ }),
+ reason: z.string().min(1).max(500),
+ hours: z.number().int().min(1).max(720),
+ })
+ .strict();
</file context>
| // V1 = mesmo regex permissivo do handler atual em produção (compat exato) | ||
| const IP_REGEX_V1 = /^[0-9a-fA-F:.\/]{3,45}$/; | ||
| // V2 = validação rigorosa IPv4/IPv6/CIDR | ||
| const IP_REGEX_V2 = |
There was a problem hiding this comment.
P2: The V2 IP regex is not actually strict: it accepts invalid IPv4/IPv6 values, so malformed addresses can pass contract validation.
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 12:
<comment>The V2 IP regex is not actually strict: it accepts invalid IPv4/IPv6 values, so malformed addresses can pass contract validation.</comment>
<file context>
@@ -0,0 +1,45 @@
+// V1 = mesmo regex permissivo do handler atual em produção (compat exato)
+const IP_REGEX_V1 = /^[0-9a-fA-F:.\/]{3,45}$/;
+// V2 = validação rigorosa IPv4/IPv6/CIDR
+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]))?$/;
+
</file context>
| cancel_reason: z.string().max(500).optional(), | ||
| }); | ||
|
|
||
| const RequestStepV2 = z |
There was a problem hiding this comment.
P2: step-up-verify V2 is strict but has no idempotency_key, so retried side-effect requests cannot be deduplicated and may execute twice.
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/step-up-verify.ts, line 32:
<comment>`step-up-verify` V2 is strict but has no `idempotency_key`, so retried side-effect requests cannot be deduplicated and may execute twice.</comment>
<file context>
@@ -0,0 +1,84 @@
+ cancel_reason: z.string().max(500).optional(),
+});
+
+const RequestStepV2 = z
+ .object({
+ step: z.literal("request"),
</file context>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 3aa25f1f36
ℹ️ 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".
| const contractResult = await parseContract(req, OwnershipAuditSchemas, { | ||
| corsHeaders, | ||
| }); | ||
| if (!contractResult.ok) return contractResult.response; |
There was a problem hiding this comment.
Preserve no-body cron calls for ownership audit
This change makes ownership-audit reject empty POST bodies: parseContract returns missing_body (400) when req.text() is empty, so the handler now exits before applying the previous default triggeredBy = "cron". Before this commit, the function explicitly tolerated missing JSON (catch { /* sem body */ }), which is a common cron invocation pattern; with this change those calls fail and the daily audit job can stop running unless every caller sends {}.
Useful? React with 👍 / 👎.
| const contractResult = await parseContract(req, OwnershipRepairSchemas, { | ||
| corsHeaders, | ||
| }); | ||
| if (!contractResult.ok) return contractResult.response; |
There was a problem hiding this comment.
Keep ownership-repair default behavior on empty body
Switching to parseContract also removes the old fallback for empty/missing JSON in ownership-repair. Previously the handler defaulted to {} and proceeded with dry_run=true/default triggered_by; now empty-body requests fail at validation with 400 before reaching those defaults. Any existing caller that relied on the documented optional body behavior will regress even when using v1.
Useful? React with 👍 / 👎.
| const contractResult = await parseContract(req, MarketIntelligenceInsightsSchemas, { | ||
| corsHeaders, | ||
| }); | ||
| if (!contractResult.ok) return contractResult.response; |
There was a problem hiding this comment.
Keep empty-body defaults for market insights v1
This migration changes market-intelligence-insights from tolerant parsing (req.json().catch(() => ({}))) to hard failure on empty body via parseContract, so requests that previously relied on default filters/days now return missing_body 400 before business logic runs. Because v1 schema/documentation treat all fields as optional and defaultable, rejecting no-body calls is a behavioral regression for existing clients that POST without {}.
Useful? React with 👍 / 👎.
| const contractResult = await parseContract(req, TrendsInsightsSchemas, { | ||
| corsHeaders, | ||
| }); | ||
| if (!contractResult.ok) return contractResult.response; |
There was a problem hiding this comment.
Preserve no-body fallback in trends insights v1
The new contract parse path rejects empty request bodies, but this endpoint previously accepted them and computed days from defaults (req.json().catch(() => ({}))). As a result, callers that omit the body (a valid v1 usage before this commit) now get missing_body 400 instead of a normal insights response, which is an unintended compatibility break.
Useful? React with 👍 / 👎.
| import { z } from "https://esm.sh/zod@3.23.8"; | ||
|
|
||
| export const MarketIntelligenceInsightsV1 = z.object({ | ||
| days: z.number().int().min(1).max(365).optional(), |
There was a problem hiding this comment.
Keep v1 days coercion/clamping in market insights
The new v1 contract enforces days as a strict bounded integer, but the previous handler coerced/clamped days (Math.max(1, Math.min(body.days ?? 30, 365))), so values like "30", 0, or 400 used to be normalized and processed successfully. After this commit, those inputs fail validation with 422 before reaching the existing normalization path, which breaks backward compatibility for clients relying on v1 leniency.
Useful? React with 👍 / 👎.
| import { z } from "https://esm.sh/zod@3.23.8"; | ||
|
|
||
| export const TrendsInsightsV1 = z.object({ | ||
| days: z.number().int().min(1).max(365).optional(), |
There was a problem hiding this comment.
Keep v1 days coercion/clamping in trends insights
The v1 schema now rejects out-of-range or string days (z.number().int().min(1).max(365)), but the pre-migration endpoint intentionally clamped/coerced days via arithmetic before querying. This means formerly tolerated inputs (e.g., "7", 0, 999) now return 422 instead of being normalized, creating a compatibility regression in the default v1 contract.
Useful? React with 👍 / 👎.
| message: "IP inválido (use IPv4, IPv6 ou CIDR)", | ||
| }), | ||
| reason: z.string().max(500).optional(), | ||
| hours: z.number().int().min(1).max(720).optional(), |
There was a problem hiding this comment.
Preserve v1 numeric-string support for block duration
In block-ip-temporarily, v1 now requires hours to be a typed number, while the previous handler deliberately coerced with Number(body.hours) and clamped to [1,720]. Requests sending hours as a numeric string (common in loosely typed clients) previously succeeded but now fail contract validation, which is a backward-incompatible change in the v1 path.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Pull request overview
This PR continues the contracts migration introduced in _shared/contracts, moving 13 Supabase Edge Functions to parseContract-based request parsing with v1/v2 Zod schemas, deprecation/version headers, and a larger contract test suite.
Changes:
- Migrates 13 Edge Functions from manual
req.json()parsing toparseContract(req, Schemas, { corsHeaders })and propagates contractresponseHeaderson success paths. - Adds 13 new contract schema modules under
supabase/functions/_shared/contracts/schemas/(v1 compat + v2 strict/idempotency/discriminated unions where applicable). - Adds/expands contract tests under
tests/contracts/(endpoint-specific + consolidated suite).
Reviewed changes
Copilot reviewed 29 out of 29 changed files in this pull request and generated 30 comments.
Show a summary per file
| File | Description |
|---|---|
| tests/contracts/step-up-verify.contract.test.ts | Adds contract tests covering v1 compat + v2 discriminated-union behavior. |
| tests/contracts/send-transactional-email.contract.test.ts | Adds contract tests for v1/v2, error codes, and version negotiation. |
| tests/contracts/migrated-endpoints.contract.test.ts | Consolidated smoke-style contract tests for the other migrated endpoints. |
| supabase/functions/trends-insights/index.ts | Switches request parsing to parseContract and propagates response headers on most responses. |
| supabase/functions/sync-external-db/index.ts | Uses parseContract for request validation and adds contract headers to responses. |
| supabase/functions/step-up-verify/index.ts | Uses parseContract and threads contract headers into the JSON helper. |
| supabase/functions/simulation-orchestrator/index.ts | Uses parseContract and adds contract headers to the success response. |
| supabase/functions/send-transactional-email/index.ts | Uses parseContract and adds contract headers to the success response. |
| supabase/functions/ownership-repair/index.ts | Uses parseContract and threads contract headers into the JSON helper. |
| supabase/functions/ownership-audit/index.ts | Uses parseContract and threads contract headers into the JSON helper. |
| supabase/functions/market-intelligence-insights/index.ts | Uses parseContract and propagates contract headers on most responses. |
| supabase/functions/kit-ai-builder/index.ts | Uses parseContract and propagates contract headers on the success response. |
| supabase/functions/force-global-logout/index.ts | Uses parseContract and threads contract headers into the JSON helper. |
| supabase/functions/e2e-cleanup/index.ts | Uses parseContract and threads contract headers into the JSON helper. |
| supabase/functions/block-ip-temporarily/index.ts | Uses parseContract and threads contract headers into the JSON helper. |
| supabase/functions/bi-copilot/index.ts | Uses parseContract and propagates contract headers on the success response. |
| supabase/functions/_shared/contracts/schemas/trends-insights.ts | Adds v1/v2 contract schemas + deprecation metadata for trends-insights. |
| supabase/functions/_shared/contracts/schemas/sync-external-db.ts | Adds v1/v2 contract schemas + deprecation metadata for sync-external-db. |
| supabase/functions/_shared/contracts/schemas/step-up-verify.ts | Adds v1/v2 contract schemas with v2 discriminated union. |
| supabase/functions/_shared/contracts/schemas/simulation-orchestrator.ts | Adds v1/v2 contract schemas including v2 idempotency requirements. |
| supabase/functions/_shared/contracts/schemas/send-transactional-email.ts | Adds v1/v2 contract schemas including v2 idempotency requirements. |
| supabase/functions/_shared/contracts/schemas/ownership-repair.ts | Adds v1/v2 contract schemas including v2 idempotency requirements. |
| supabase/functions/_shared/contracts/schemas/ownership-audit.ts | Adds v1/v2 contract schemas (v2 requires triggered_by). |
| supabase/functions/_shared/contracts/schemas/market-intelligence-insights.ts | Adds v1/v2 contract schemas (v2 strict + UUID validation). |
| supabase/functions/_shared/contracts/schemas/kit-ai-builder.ts | Adds v1/v2 contract schemas including v2 idempotency requirements. |
| supabase/functions/_shared/contracts/schemas/force-global-logout.ts | Adds v1/v2 contract schemas including v2 idempotency requirements. |
| supabase/functions/_shared/contracts/schemas/e2e-cleanup.ts | Adds v1/v2 contract schemas including v2 confirm + refine rule. |
| supabase/functions/_shared/contracts/schemas/block-ip-temporarily.ts | Adds v1/v2 contract schemas including stricter IP validation in v2. |
| supabase/functions/_shared/contracts/schemas/bi-copilot.ts | Adds v1/v2 contract schemas (v2 strict + required context). |
Comments suppressed due to low confidence (1)
supabase/functions/trends-insights/index.ts:205
- Após um
parseContractbem-sucedido, ocatchainda retorna headers apenas comcorsHeaders(semresponseHeaders). Isso faz respostas de erro perderemx-contract-versione, em v1,Deprecation/Sunset/Link. Sugestão: declararresponseHeadersfora do try e sempre mesclar em todas as respostas depois do parse (inclusive no catch).
headers: { ...corsHeaders, ...responseHeaders, "Content-Type": "application/json" },
});
} catch (e) {
console.error("trends-insights error:", e);
return new Response(JSON.stringify({ error: e instanceof Error ? e.message : "Unknown error" }), {
status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" },
});
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| const contractResult = await parseContract(req, TrendsInsightsSchemas, { | ||
| corsHeaders, | ||
| }); | ||
| if (!contractResult.ok) return contractResult.response; | ||
| const { data: body, responseHeaders } = contractResult; | ||
| const days = body.days ?? 30; | ||
|
|
| const contractResult = await parseContract(req, KitAiBuilderSchemas, { | ||
| corsHeaders, | ||
| }); | ||
| if (!contractResult.ok) return contractResult.response; | ||
| const { data: body, responseHeaders } = contractResult; | ||
| const prompt = body.prompt.trim(); |
| .min(6, { message: "prompt inválido (6–2000 chars)" }) | ||
| .max(2000, { message: "prompt inválido (6–2000 chars)" }), | ||
| }); | ||
|
|
||
| export const KitAiBuilderV2 = z | ||
| .object({ | ||
| prompt: z.string().min(6).max(2000), |
| const contractResult = await parseContract(req, BiCopilotSchemas, { | ||
| corsHeaders, | ||
| }); | ||
| if (!contractResult.ok) return contractResult.response; | ||
| const { data: body, responseHeaders } = contractResult; | ||
|
|
| // Return email content (actual sending happens when email domain is configured) | ||
| return new Response( | ||
| JSON.stringify({ | ||
| success: true, | ||
| message: "Email queued successfully", | ||
| preview: { subject, recipient: body.recipient_email, event_type: body.event_type }, | ||
| }), | ||
| { headers: { ...corsHeaders, "Content-Type": "application/json" } } | ||
| { headers: { ...corsHeaders, ...responseHeaders, "Content-Type": "application/json" } } | ||
| ); |
| version: "1", | ||
| sunset: "2026-10-31", | ||
| migrationUrl: | ||
| "https://github.com/adm01-debug/promo-gifts-v4/blob/main/docs/contracts/MIGRATION_GUIDE.md#market-intelligence-insights", |
| version: "1", | ||
| sunset: "2026-11-30", | ||
| migrationUrl: | ||
| "https://github.com/adm01-debug/promo-gifts-v4/blob/main/docs/contracts/MIGRATION_GUIDE.md#simulation-orchestrator", |
| version: "1", | ||
| sunset: "2026-10-31", | ||
| migrationUrl: | ||
| "https://github.com/adm01-debug/promo-gifts-v4/blob/main/docs/contracts/MIGRATION_GUIDE.md#step-up-verify", |
| version: "1", | ||
| sunset: "2026-11-30", | ||
| migrationUrl: | ||
| "https://github.com/adm01-debug/promo-gifts-v4/blob/main/docs/contracts/MIGRATION_GUIDE.md#sync-external-db", |
| export const MarketIntelligenceInsightsV1 = z.object({ | ||
| days: z.number().int().min(1).max(365).optional(), | ||
| categoryId: z.string().max(100).nullable().optional(), | ||
| supplierId: z.string().max(100).nullable().optional(), | ||
| productId: z.string().max(100).nullable().optional(), | ||
| categoryName: z.string().max(255).nullable().optional(), | ||
| supplierName: z.string().max(255).nullable().optional(), | ||
| productName: z.string().max(255).nullable().optional(), | ||
| forceRefresh: z.boolean().optional(), | ||
| }); |
b2d8da6
into
feat/contracts-validation-zod-422-versioning
Resumo
Migra 13 edge functions para o pacote
_shared/contractsentregue em #45.Cada função ganha:
.strict()+idempotency_keyonde houver side effects)parseContract(req, XSchemas, { corsHeaders })substituireq.json()manualresponseHeaders(Deprecation/Sunset/X-Contract-Version)Endpoints migrados
P0 — sunset v1: 2026-10-31 (#46)
send-transactional-emailkit-ai-builderbi-copilotmarket-intelligence-insightsstep-up-verifyP1 — sunset v1: 2026-11-30 (#47)
ownership-audittriggered_byobrigatórioownership-repairdry_runobrigatório +idempotency_keysimulation-orchestratortargetFunctionsmin 1 +idempotency_keysync-external-dbsincevalida ISO 8601trends-insights.strict()P2 — sunset v1: 2026-12-31 (#48)
force-global-logoutidempotency_keye2e-cleanupconfirm: true+idempotency_key+.refine(sellerScope/sellerId)block-ip-temporarilyCobertura de testes
tests/contracts/:send-transactional-email.contract.test.ts(10 testes: 6 v1 + 3 v2 + 1 versão inválida)step-up-verify.contract.test.ts(10 testes cobrindo discriminated union v2)migrated-endpoints.contract.test.ts(29 testes consolidados para os outros 11)Compatibilidade
Schemas V1 são byte-for-byte equivalentes aos shapes atuais em produção. Nenhum consumidor existente quebra — o que mudou é apenas:
{code, message, fields[], version}Deprecation: true+Sunset: <date>aparecem nas respostas v1Comunicação aos consumidores n8n sobre essa mudança 400→422 está rastreada em #54 (ação pendente do humano antes do merge).
Dependências
_shared/contracts)Próximos passos pós-merge
Summary by cubic
Migrated 13 edge functions to contract-based validation using
parseContractand versioned Zod schemas. Adds v1 (compat) and v2 (strict) contracts, standardized errors/headers, and 49 contract tests.Refactors
supabase/functions/_shared/contracts/schemas/*.req.json()withparseContract(...)in all 13 handlers.Deprecation,Sunset, andX-Contract-Versionheaders from handlers.step-up-verifyv2 uses a discriminated union per step for stricter auth flows.Migration
{ code, message, fields, version }.Accept-Version: 2. v1 responds withDeprecation: trueandSunset(P0: 2026-10-31, P1: 2026-11-30, P2: 2026-12-31).idempotency_keyon endpoints with side effects (send-transactional-email,kit-ai-builder,bi-copilot,simulation-orchestrator,ownership-repair,e2e-cleanup,force-global-logout).since), stricter IP/CIDR, required fields where missing in v1.Written for commit 3aa25f1. Summary will update on new commits. Review in cubic