Skip to content

feat(edges): createEdge template + auth hardening F1/F2#217

Merged
adm01-debug merged 1 commit into
mainfrom
feat/edge-auth-hardening
May 15, 2026
Merged

feat(edges): createEdge template + auth hardening F1/F2#217
adm01-debug merged 1 commit into
mainfrom
feat/edge-auth-hardening

Conversation

@adm01-debug
Copy link
Copy Markdown
Owner

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

O que muda

_shared/createEdge.ts — novo template unificado (130L)

Resolve 4 padrões de auth coexistindo em 83 funções. Novas edges devem usar este template. Migração das existentes é gradual.

Modos: jwt | cron | public

F1 — 6 edges expostas agora exigem JWT (agente ou acima)

  • bi-copilot — chamava Lovable AI sem auth
  • comparison-ai-advisor — chamava Lovable AI sem auth
  • kit-ai-builder — chamava Lovable AI sem auth
  • categories-api — usava service_role do banco externo sem auth
  • quote-sync — usava service_role sem auth
  • dropbox-list — usava token Dropbox do env sem auth

F2 — 13 crons protegidos com x-cron-secret

Usa authorizeCron() existente em _shared/dispatcher-auth.ts. Secret: env CRON_SECRET. Retrocompat: aceita chamada anônima com warning se CRON_SECRET não estiver configurado.

Funções: cleanup-notifications, cleanup-novelties, collections-watcher, comparison-price-watcher, connections-health-check, favorites-watcher, ownership-audit, process-queue, process-scheduled-reports, quote-followup-reminders, send-digest, send-notification, send-scheduled-reports

Ação necessária pós-merge

Registrar CRON_SECRET em Supabase Dashboard → Edge Functions → Secrets antes de ativar os crons em produção.

Impacto

  • 20 arquivos, +276L, 0 deletadas
  • Sem breaking change em frontend (edges autenticadas via JWT do usuário logado)
  • Sem breaking change em crons (retrocompat sem secret configurado)

Summary by cubic

Adds a shared createEdge template to unify auth across Edge Functions and hardens access: six exposed functions now require JWT, and 13 crons are protected with x-cron-secret.

  • New Features

    • Added _shared/createEdge.ts template with modes: jwt (role checks), cron (secret header), and public.
    • Enforced JWT (role: agente or higher) on: bi-copilot, comparison-ai-advisor, kit-ai-builder, categories-api, quote-sync, dropbox-list.
    • Secured crons with x-cron-secret using CRON_SECRET (backward compatible if unset): cleanup-notifications, cleanup-novelties, collections-watcher, comparison-price-watcher, connections-health-check, favorites-watcher, ownership-audit, process-queue, process-scheduled-reports, quote-followup-reminders, send-digest, send-notification, send-scheduled-reports.
  • Migration

    • Set CRON_SECRET in Supabase Dashboard → Edge Functions → Secrets before enabling crons in production.
    • Use the createEdge template for new functions; migrate existing ones gradually.

Written for commit af6774c. Summary will update on new commits.

- _shared/createEdge.ts: template unificado (130L)
- F1 — 6 edges expostas exigem JWT agente: bi-copilot, comparison-ai-advisor,
  kit-ai-builder, categories-api, quote-sync, dropbox-list
- F2 — 13 crons protegidos com x-cron-secret (CRON_SECRET env):
  cleanup-notifications, cleanup-novelties, collections-watcher,
  comparison-price-watcher, connections-health-check, favorites-watcher,
  ownership-audit, process-queue, process-scheduled-reports,
  quote-followup-reminders, send-digest, send-notification, send-scheduled-reports
Copilot AI review requested due to automatic review settings May 15, 2026 09:52
@vercel
Copy link
Copy Markdown

vercel Bot commented May 15, 2026

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

Project Deployment Actions Updated (UTC)
promo-gifts Building Building Preview, Comment May 15, 2026 9:52am

@adm01-debug adm01-debug merged commit 9ff7598 into main May 15, 2026
14 of 17 checks passed
@supabase
Copy link
Copy Markdown

supabase Bot commented May 15, 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 ↗︎.

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

ℹ️ 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 +10 to +11
const cronAuth = authorizeCron(req, { corsHeaders: {}, secretEnvName: "CRON_SECRET", headerName: "x-cron-secret" });
if (!cronAuth.ok) return cronAuth.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 Keep existing pg_cron callers authorized

When CRON_SECRET is configured as the post-merge action requires, this new authorizeCron check only accepts the x-cron-secret header. I checked supabase/cron/cron-config.sql, and the existing scheduled callers for process-queue, send-digest, and cleanup-notifications still send only Authorization: Bearer <service_role> plus Content-Type, so those jobs will start receiving 401s and stop processing queues/digests/cleanup unless the cron definitions are updated or service-role bearer is still accepted.

Useful? React with 👍 / 👎.

Comment on lines +15 to +19
try {
const authCtx = await authenticateRequest(req);
requireRole(authCtx, "agente");
} catch (authErr) {
return authErrorResponse(authErr, corsHeaders);
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 Move CORS preflight before JWT auth

For browser calls to categories-api (for example src/hooks/useProductsByCategory.ts uses supabase.functions.invoke), the browser sends an unauthenticated OPTIONS preflight before the real request. Because this JWT check runs before the OPTIONS branch below, the preflight gets a 401 instead of a 2xx CORS response, so the browser blocks the actual authenticated category request.

Useful? React with 👍 / 👎.

@@ -1,5 +1,6 @@
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.49.4";
import { buildPublicCorsHeaders } from "../_shared/cors.ts";
import { authorizeCron } from "../_shared/dispatcher-auth.ts";
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 Call authorizeCron for scheduled reports

This only imports authorizeCron; the handler never invokes it before processing due reports. In environments where this Edge Function is deployed without Supabase JWT verification like the other cron/service functions, an unauthenticated request can still trigger the scheduled report batch and send due emails with the service-role client, so send-scheduled-reports remains exposed while the commit claims it is protected with x-cron-secret.

Useful? React with 👍 / 👎.

Comment on lines +15 to +18
const authCtx = await authenticateRequest(req);
requireRole(authCtx, "agente");
} catch (authErr) {
return authErrorResponse(authErr, corsHeaders);
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 Move Dropbox preflight before JWT auth

For browser calls to dropbox-list (documented in src/hooks/useDropboxFiles.ts), the unauthenticated OPTIONS preflight reaches this JWT guard before the handler's OPTIONS response below. That makes the preflight return 401, so the browser blocks the real authenticated Dropbox list/check request even when the user has a valid session.

Useful? React with 👍 / 👎.

Comment on lines +64 to +65
const authCtx = await authenticateRequest(req);
requireRole(authCtx, "agente");
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 Allow supervisors through seller guards

The new guards are commented as allowing “agente ou acima”, but requireRole(authCtx, "agente") only accepts a literal agente role (besides dev); supervisor/legacy admin only pass when the required role is supervisor or admin. A supervisor who does not also have an agente row will now get 403 on these newly protected frontend flows, including comparison AI, even though the intended hierarchy says supervisors are above agents.

Useful? React with 👍 / 👎.

Comment on lines +22 to +27
const cronAuth = authorizeCron(req, {
corsHeaders: {},
secretEnvName: "CRON_SECRET",
headerName: "x-cron-secret",
});
if (!cronAuth.ok) return cronAuth.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.

P2 Badge Preserve manual admin ownership audits

Once CRON_SECRET is configured, this guard only accepts x-cron-secret, but the admin page still invokes ownership-audit from the browser with a normal Supabase JWT (src/pages/admin/OwnershipAuditAdminPage.tsx). That means the “run now” action for admins/devs will start returning 401 even though this function is documented as cron-or-manual and the underlying RPC checks admin permissions.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Hardening pass for Supabase Edge Functions: introduces a unified createEdge template and applies two concrete authorization tiers — JWT-protected ("agente or above") on six previously unauthenticated public edges (F1) and x-cron-secret (timing-safe) on thirteen cron edges (F2). The template is meant to consolidate four divergent auth patterns currently living across 83 functions; existing edges keep their hand-rolled handlers and will migrate gradually.

Changes:

  • Adds _shared/createEdge.ts with jwt / cron / public modes and a jsonResponse helper.
  • Adds JWT auth (authenticateRequest + requireRole(_, "agente")) to six public edges that previously hit Lovable AI / service-role / Dropbox tokens with no auth.
  • Adds authorizeCron(req, { secretEnvName: "CRON_SECRET" }) to thirteen cron edges with retrocompat (anonymous accepted with warning when secret is unset).

Reviewed changes

Copilot reviewed 20 out of 20 changed files in this pull request and generated 17 comments.

Show a summary per file
File Description
supabase/functions/_shared/createEdge.ts New unified template; documents hmac mode that isn't implemented and forces wildcard CORS for public.
supabase/functions/bi-copilot/index.ts Adds JWT + role check after OPTIONS (correct order).
supabase/functions/comparison-ai-advisor/index.ts Adds JWT + role check; reuses module-scope mutable corsHeaders.
supabase/functions/kit-ai-builder/index.ts Adds JWT + role check after OPTIONS.
supabase/functions/categories-api/index.ts Adds JWT + role check before OPTIONS — breaks CORS preflight.
supabase/functions/dropbox-list/index.ts Same OPTIONS-after-auth ordering bug as categories-api.
supabase/functions/quote-sync/index.ts Adds JWT + role check after OPTIONS.
supabase/functions/cleanup-notifications/index.ts Adds cron secret; introduces a duplicate OPTIONS short-circuit without CORS.
supabase/functions/cleanup-novelties/index.ts Same duplicate OPTIONS issue.
supabase/functions/process-queue/index.ts Same duplicate OPTIONS issue.
supabase/functions/process-scheduled-reports/index.ts Same duplicate OPTIONS issue.
supabase/functions/send-digest/index.ts Same duplicate OPTIONS issue.
supabase/functions/send-notification/index.ts Same duplicate OPTIONS issue.
supabase/functions/send-scheduled-reports/index.ts Imports authorizeCron but never calls it — function remains unprotected.
supabase/functions/quote-followup-reminders/index.ts Adds cron secret with empty CORS headers for error path.
supabase/functions/ownership-audit/index.ts Adds cron secret but passes corsHeaders: {} despite having per-request CORS.
supabase/functions/favorites-watcher/index.ts Adds cron secret with empty CORS headers for error path.
supabase/functions/collections-watcher/index.ts Same as favorites-watcher.
supabase/functions/comparison-price-watcher/index.ts Same as favorites-watcher.
supabase/functions/connections-health-check/index.ts Same as favorites-watcher.

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

Comment on lines 11 to 23
Deno.serve(async (req) => {
const corsHeaders = getCorsHeaders(req);
// Auth: exige vendedor autenticado (agente ou acima)
try {
const authCtx = await authenticateRequest(req);
requireRole(authCtx, "agente");
} catch (authErr) {
return authErrorResponse(authErr, corsHeaders);
}

if (req.method === "OPTIONS") {
return new Response(null, { headers: corsHeaders });
}
Comment on lines 12 to 24
Deno.serve(async (req) => {
const corsHeaders = getCorsHeaders(req);
// Auth: exige vendedor autenticado (agente ou acima)
try {
const authCtx = await authenticateRequest(req);
requireRole(authCtx, "agente");
} catch (authErr) {
return authErrorResponse(authErr, corsHeaders);
}

if (req.method === 'OPTIONS') {
return new Response(null, { headers: corsHeaders });
}
// Auth: exige vendedor autenticado (agente ou acima)
try {
const authCtx = await authenticateRequest(req);
requireRole(authCtx, "agente");
Comment on lines +8 to 15
// Cron: exige x-cron-secret para evitar chamadas diretas não autorizadas
if (req.method === "OPTIONS") return new Response(null, { status: 204 });
const cronAuth = authorizeCron(req, { corsHeaders: {}, secretEnvName: "CRON_SECRET", headerName: "x-cron-secret" });
if (!cronAuth.ok) return cronAuth.response;

if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders });
}
Comment on lines +9 to 16
// Cron: exige x-cron-secret para evitar chamadas diretas não autorizadas
if (req.method === "OPTIONS") return new Response(null, { status: 204 });
const cronAuth = authorizeCron(req, { corsHeaders: {}, secretEnvName: "CRON_SECRET", headerName: "x-cron-secret" });
if (!cronAuth.ok) return cronAuth.response;

const corsHeaders = getCorsHeaders(req);
if (req.method === "OPTIONS") {
return new Response(null, { headers: corsHeaders });
Comment on lines +42 to +86
export type EdgeRole = "agente" | "supervisor" | "dev";

export type EdgeConfig =
| { auth: "jwt"; role?: EdgeRole }
| { auth: "cron"; secretEnv: string; headerName?: string }
| { auth: "public" };

export interface EdgeContext {
/** Presente apenas no modo 'jwt'. */
user?: Pick<AuthResult, "userId" | "userRole" | "userRoles" | "localServiceClient">;
corsHeaders: Record<string, string>;
}

export type EdgeHandler = (
req: Request,
ctx: EdgeContext,
) => Promise<Response>;

// ---------------------------------------------------------------------------
// Factory
// ---------------------------------------------------------------------------

export function createEdge(
config: EdgeConfig,
handler: EdgeHandler,
): (req: Request) => Promise<Response> {
return async (req: Request): Promise<Response> => {
// CORS headers — modo public usa buildPublicCorsHeaders
const corsHeaders =
config.auth === "public"
? buildPublicCorsHeaders()
: getCorsHeaders(req);

// Preflight OPTIONS — responde sempre
if (req.method === "OPTIONS") {
return new Response(null, { headers: corsHeaders, status: 204 });
}

try {
// ── Modo jwt ──────────────────────────────────────────────────────────
if (config.auth === "jwt") {
const auth = await authenticateRequest(req);
if (config.role) requireRole(auth, config.role);
return await handler(req, { user: auth, corsHeaders });
}
* Modos suportados:
* jwt → JWT obrigatório + verificação de role (usa _shared/auth.ts)
* cron → x-cron-secret timing-safe (usa _shared/dispatcher-auth.ts)
* hmac → HMAC de payload (usar diretamente dispatcher-auth.ts)
Comment on lines +64 to +115
export function createEdge(
config: EdgeConfig,
handler: EdgeHandler,
): (req: Request) => Promise<Response> {
return async (req: Request): Promise<Response> => {
// CORS headers — modo public usa buildPublicCorsHeaders
const corsHeaders =
config.auth === "public"
? buildPublicCorsHeaders()
: getCorsHeaders(req);

// Preflight OPTIONS — responde sempre
if (req.method === "OPTIONS") {
return new Response(null, { headers: corsHeaders, status: 204 });
}

try {
// ── Modo jwt ──────────────────────────────────────────────────────────
if (config.auth === "jwt") {
const auth = await authenticateRequest(req);
if (config.role) requireRole(auth, config.role);
return await handler(req, { user: auth, corsHeaders });
}

// ── Modo cron ─────────────────────────────────────────────────────────
if (config.auth === "cron") {
const result = authorizeCron(req, {
corsHeaders,
secretEnvName: config.secretEnv,
headerName: config.headerName ?? "x-cron-secret",
});
if (!result.ok) return result.response;
return await handler(req, { corsHeaders });
}

// ── Modo public ───────────────────────────────────────────────────────
return await handler(req, { corsHeaders });

} catch (err) {
// Erros lançados por authenticateRequest / requireRole (status + message)
if ((err as any)?.status) {
return authErrorResponse(err, corsHeaders);
}
// Erros inesperados
console.error("[createEdge] unhandled error:", err);
return new Response(
JSON.stringify({ error: "internal_error" }),
{ status: 500, headers: { ...corsHeaders, "Content-Type": "application/json" } },
);
}
};
}
Comment on lines +69 to +73
// CORS headers — modo public usa buildPublicCorsHeaders
const corsHeaders =
config.auth === "public"
? buildPublicCorsHeaders()
: getCorsHeaders(req);
@@ -1,5 +1,6 @@
import { createClient } from "https://esm.sh/@supabase/supabase-js@2.49.4";
import { buildPublicCorsHeaders } from "../_shared/cors.ts";
import { authorizeCron } from "../_shared/dispatcher-auth.ts";
adm01-debug pushed a commit that referenced this pull request May 15, 2026
…icts

Conflito em 3 arquivos entre PR #216 e commits #217/#218/#214 no main:

- vitest.config.ts: mantém comentário explicativo do PR sobre exclusão
  de tests/e2e/** (não estava em main), restante idêntico.
- src/hooks/__tests__/useCatalogState.unit.test.tsx: usa versão do main
  (bloco TODO detalhado no topo do arquivo é superior ao comentário
  redundante antes do describe.skip que estava no PR).
- tests/components/SimulationPriceSourceBadge.test.tsx: resolvido
  automaticamente (mudança idêntica nas duas branches).

https://claude.ai/code/session_017f7HQxDreJKyNDJM3R7qf6
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