Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 44 additions & 1 deletion supabase/config.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,49 @@ verify_jwt = false
[functions.e2e-cleanup]
verify_jwt = false

# Cron functions — autenticadas via x-cron-secret (vault-based, ver _shared/dispatcher-auth.ts)
# Não usam JWT do Supabase Auth porque pg_cron não tem usuário/sessão.
# Segurança: header x-cron-secret comparado em tempo constante com vault.CRON_SECRET.

[functions.cleanup-notifications]
verify_jwt = false

[functions.cleanup-novelties]
verify_jwt = false

[functions.collections-watcher]
verify_jwt = false

[functions.comparison-price-watcher]
verify_jwt = false

[functions.connections-health-check]
verify_jwt = false

[functions.favorites-watcher]
verify_jwt = false

[functions.ownership-audit]
verify_jwt = false

[functions.process-queue]
verify_jwt = false
Comment on lines +55 to +56
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 Pass x-cron-secret from existing pg_cron jobs

Disabling JWT verification for process-queue makes the function depend on the in-function x-cron-secret guard, but the existing pg_cron job in supabase/cron/cron-config.sql still posts only Authorization and Content-Type headers to /functions/v1/process-queue (checked lines 18-22). Once CRON_SECRET is actually available through vault/env, that scheduled queue processor will receive no x-cron-secret and return 401 every minute; the schedule needs to include the same vault-backed secret header before relying on this auth mode.

Useful? React with 👍 / 👎.


[functions.process-scheduled-reports]
verify_jwt = false

[functions.quote-followup-reminders]
verify_jwt = false

[functions.send-digest]
verify_jwt = false
Comment on lines +64 to +65
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 Add the cron secret to the remaining scheduled calls

The same auth switch also affects send-digest and cleanup-notifications, but their existing schedules in supabase/cron/cron-config.sql still send only the service-role bearer plus Content-Type headers (checked lines 37-40 and 56-59). After CRON_SECRET is configured, both jobs will start getting 401s from the new x-cron-secret guard and stop running, so those schedules need to pass the vault-backed cron header before JWT verification is disabled for these functions.

Useful? React with 👍 / 👎.


[functions.send-notification]
verify_jwt = false

[functions.send-scheduled-reports]
verify_jwt = false

[auth]
enable_signup = false
enable_anonymous_sign_ins = false
enable_anonymous_sign_ins = false
95 changes: 58 additions & 37 deletions supabase/functions/_shared/dispatcher-auth.ts
Original file line number Diff line number Diff line change
@@ -1,21 +1,19 @@
// supabase/functions/_shared/dispatcher-auth.ts
// --------------------------------------------------------------
// Autorização para `webhook-dispatcher` e `connections-auto-test`.
// Autorização para `webhook-dispatcher`, `connections-auto-test` e crons.
//
// Duas edges chamadas por contextos diferentes — autoriza por modos:
//
// `webhook-dispatcher` (3 chamadores):
// Modo A — `x-dispatcher-secret: <SECRET>` (triggers DB, RPCs, cron)
// Modos:
// Modo A — `x-dispatcher-secret: <SECRET>` (webhook-dispatcher: triggers DB/RPC)
// Modo B — `Authorization: Bearer <user JWT>` + role admin|supervisor|dev (frontend)
// Modo C — `x-cron-secret: <SECRET>` (cron jobs — agora lê do vault)
//
// `connections-auto-test` (1 chamador):
// Modo C — `x-cron-secret: <SECRET>` (cron job)
//
// Compatibilidade: se a env do secret NÃO estiver setada, log warning e
// aceita (modo retrocompat). Permite rollback seguro e dev sem config.
// Vault-based SoT (15/mai/2026):
// `authorizeCron` agora lê o secret esperado do vault PostgreSQL via
// RPC `get_edge_function_secret(_name)` quando service_role está disponível.
// Fallback para `Deno.env.get()` se vault indisponível (retrocompat).
// Cache em memória por cold-start: 1 RPC por instância.
//
// Segurança: comparação de secret em tempo constante (anti timing attack).
// Logs estruturados (JSON single-line) sem nunca expor o valor do secret.
// Segurança: comparação em tempo constante. Logs estruturados sem expor secret.

import { createClient, SupabaseClient } from "https://esm.sh/@supabase/supabase-js@2.49.4";

Expand All @@ -27,7 +25,7 @@ export type AppRole = "dev" | "supervisor" | "agente";
const ROLE_RANK: Record<AppRole, number> = { agente: 1, supervisor: 2, dev: 3 };

export type DispatcherAuthMode = "secret" | "user_jwt" | "legacy_no_auth";
export type CronAuthMode = "secret" | "legacy_no_auth";
export type CronAuthMode = "secret" | "secret_vault" | "legacy_no_auth";

export interface DispatcherAuthOk {
ok: true;
Expand All @@ -48,9 +46,7 @@ export interface CronAuthOk {
export type CronAuthResult = CronAuthOk | DispatcherAuthErr;

/**
* Comparação em tempo constante (constant-time) para evitar timing attacks
* onde o atacante deduz o secret medindo quanto tempo o servidor leva pra
* retornar 401. Sempre processa todos os caracteres antes de retornar.
* Comparação em tempo constante (constant-time) para evitar timing attacks.
*/
export function constantTimeEqual(a: string, b: string): boolean {
if (typeof a !== "string" || typeof b !== "string") return false;
Expand All @@ -63,8 +59,6 @@ export function constantTimeEqual(a: string, b: string): boolean {
}

function logAuthEvent(payload: Record<string, unknown>): void {
// JSON estruturado pra buscar em logs depois.
// NUNCA logar o valor do secret nem o header completo.
try {
console.log(JSON.stringify({ evt: "dispatcher_auth", ts: new Date().toISOString(), ...payload }));
} catch {
Expand All @@ -79,12 +73,38 @@ function jsonResponse(body: unknown, status: number, cors: Record<string, string
});
}

// --------------------------------------------------------------
// Vault secret fetcher — single source of truth para cron secrets.
// Cache em memória durante o cold-start para evitar 1 RPC por request.
// --------------------------------------------------------------

const _vaultCache = new Map<string, Promise<string>>();

async function getVaultSecret(name: string): Promise<string> {
if (_vaultCache.has(name)) return _vaultCache.get(name)!;
const promise = (async () => {
if (!SUPABASE_URL || !SERVICE_KEY) return "";
try {
const client = createClient(SUPABASE_URL, SERVICE_KEY, {
auth: { persistSession: false, autoRefreshToken: false },
});
const { data, error } = await client.rpc("get_edge_function_secret", { _name: name });
if (error || !data) return "";
return data as string;
Comment on lines +83 to +93
} catch {
return "";
}
})();
_vaultCache.set(name, promise);
return promise;
}
Comment on lines +83 to +100
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Não cacheie resultado vazio do Vault como definitivo.

Se a primeira RPC falhar/retornar vazio, esse "" fica preso no _vaultCache durante todo o cold-start. Em funções com verify_jwt = false, isso pode prolongar fallback/legacy_no_auth além do necessário.

💡 Ajuste sugerido
 async function getVaultSecret(name: string): Promise<string> {
   if (_vaultCache.has(name)) return _vaultCache.get(name)!;
   const promise = (async () => {
@@
     } catch {
       return "";
     }
   })();
   _vaultCache.set(name, promise);
+  promise.then((value) => {
+    if (!value) _vaultCache.delete(name);
+  }).catch(() => {
+    _vaultCache.delete(name);
+  });
   return promise;
 }
🤖 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/dispatcher-auth.ts` around lines 83 - 100, The
getVaultSecret function currently caches an empty string on RPC failure which
makes that failure sticky; change the caching logic around _vaultCache so an
empty result or an error is not stored permanently: call createClient and invoke
the get_edge_function_secret RPC as before, but after the promise resolves
remove the cache entry if the returned value is "" (or throw/catch produced no
value), otherwise keep/store the non-empty secret; alternatively only insert
into _vaultCache when the resolved value is non-empty and ensure any caught
errors also avoid caching by deleting _vaultCache entry for the given name.
Ensure you update references to getVaultSecret, _vaultCache, createClient, and
get_edge_function_secret accordingly.


// ============================================================================
// webhook-dispatcher: Modo A (secret) ou Modo B (user JWT)
// ============================================================================

export interface AuthorizeDispatcherOptions {
/** Se true, exige Modo B (user JWT). Modo A retorna 403. Usado em `test_mode` e `replay_delivery_id`. */
/** Se true, exige Modo B (user JWT). Modo A retorna 403. */
requireUserContext?: boolean;
/** Role mínimo no Modo B. Default: 'supervisor'. */
minRole?: AppRole;
Expand All @@ -104,7 +124,7 @@ export async function authorizeDispatcher(
const providedSecret = req.headers.get("x-dispatcher-secret") ?? "";
const authHeader = req.headers.get("Authorization") ?? req.headers.get("authorization") ?? "";

// ───────── Modo A: x-dispatcher-secret ─────────
// Modo A: x-dispatcher-secret
if (providedSecret && expectedSecret) {
if (!constantTimeEqual(providedSecret, expectedSecret)) {
logAuthEvent({ outcome: "denied", reason: "bad_secret", mode_attempted: "secret" });
Expand All @@ -131,26 +151,20 @@ export async function authorizeDispatcher(
};
}

// ───────── Modo B: Bearer user JWT ─────────
// Modo B: Bearer user JWT
if (authHeader.toLowerCase().startsWith("bearer ")) {
const token = authHeader.slice(7).trim();
if (!token) {
logAuthEvent({ outcome: "denied", reason: "empty_bearer" });
return { ok: false, response: jsonResponse({ error: "missing_token" }, 401, corsHeaders) };
}

// Evita falso-positivo: Bearer com SERVICE_ROLE_KEY não é "user".
// Mas é caller legítimo do servidor → permite como Modo A.
if (SERVICE_KEY && constantTimeEqual(token, SERVICE_KEY)) {
if (requireUserContext) {
logAuthEvent({ outcome: "denied", reason: "service_role_not_allowed_in_user_only_mode" });
return {
ok: false,
response: jsonResponse(
{ error: "user_context_required" },
403,
corsHeaders,
),
response: jsonResponse({ error: "user_context_required" }, 403, corsHeaders),
};
}
logAuthEvent({ outcome: "allowed", mode: "secret", via: "service_role_bearer" });
Expand Down Expand Up @@ -222,7 +236,7 @@ export async function authorizeDispatcher(
};
}

// ───────── Retrocompat: nenhum env setado → aceita anônimo com warning ─────────
// Retrocompat: nenhum env setado → aceita anônimo com warning
if (!expectedSecret) {
logAuthEvent({
outcome: "allowed",
Expand Down Expand Up @@ -251,23 +265,30 @@ export async function authorizeDispatcher(
}

// ============================================================================
// connections-auto-test: Modo C (cron secret)
// crons: Modo C (cron secret) — agora vault-based com fallback env
// ============================================================================

export function authorizeCron(
export async function authorizeCron(
req: Request,
opts: { corsHeaders: Record<string, string>; secretEnvName: string; headerName: string },
): CronAuthResult {
): Promise<CronAuthResult> {
const { corsHeaders, secretEnvName, headerName } = opts;
const expectedSecret = Deno.env.get(secretEnvName) ?? "";

// Vault first (single source of truth), fallback env (retrocompat)
let expectedSecret = await getVaultSecret(secretEnvName);
const viaVault = !!expectedSecret;
if (!expectedSecret) {
expectedSecret = Deno.env.get(secretEnvName) ?? "";
Comment on lines +278 to +281
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 CRON_SECRET through the vault helper

When these cron functions pass secretEnvName: "CRON_SECRET", this vault lookup always fails because the existing public.get_edge_function_secret helper only permits WEBHOOK_DISPATCHER_SECRET and CONNECTIONS_AUTO_TEST_SECRET (and the vault setup migration only creates those two). In a deployment that follows this change’s new vault-based source of truth and removes the edge env fallback, expectedSecret stays empty and the code falls through to legacy_no_auth, so the newly verify_jwt=false cron endpoints accept unauthenticated requests instead of requiring the cron secret. The migration/setup needs to add CRON_SECRET to the vault and helper whitelist before relying on this path.

Useful? React with 👍 / 👎.

}

const providedSecret = req.headers.get(headerName) ?? "";

if (!expectedSecret) {
logAuthEvent({
outcome: "allowed",
mode: "legacy_no_auth",
env: secretEnvName,
warning: `${secretEnvName} nao configurado — aceitando chamada anonima. Configure secret para hardening.`,
warning: `${secretEnvName} nao configurado em vault nem env — aceitando chamada anonima. Configure para hardening.`,
});
return { ok: true, mode: "legacy_no_auth" };
}
Expand All @@ -285,10 +306,10 @@ export function authorizeCron(
}

if (!constantTimeEqual(providedSecret, expectedSecret)) {
logAuthEvent({ outcome: "denied", reason: "bad_cron_secret", env: secretEnvName });
logAuthEvent({ outcome: "denied", reason: "bad_cron_secret", env: secretEnvName, via_vault: viaVault });
return { ok: false, response: jsonResponse({ error: "unauthorized" }, 401, corsHeaders) };
}

logAuthEvent({ outcome: "allowed", mode: "secret", env: secretEnvName });
return { ok: true, mode: "secret" };
logAuthEvent({ outcome: "allowed", mode: viaVault ? "secret_vault" : "secret", env: secretEnvName });
return { ok: true, mode: viaVault ? "secret_vault" : "secret" };
}
2 changes: 1 addition & 1 deletion supabase/functions/cleanup-notifications/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const corsHeaders = buildPublicCorsHeaders();
Deno.serve(async (req) => {
// 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" });
const cronAuth = await authorizeCron(req, { corsHeaders: {}, secretEnvName: "CRON_SECRET", headerName: "x-cron-secret" });
if (!cronAuth.ok) return cronAuth.response;

if (req.method === 'OPTIONS') {
Expand Down
2 changes: 1 addition & 1 deletion supabase/functions/cleanup-novelties/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { createClient } from "https://esm.sh/@supabase/supabase-js@2.49.4";
Deno.serve(async (req) => {
// 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" });
const cronAuth = await authorizeCron(req, { corsHeaders: {}, secretEnvName: "CRON_SECRET", headerName: "x-cron-secret" });
if (!cronAuth.ok) return cronAuth.response;

const corsHeaders = getCorsHeaders(req);
Expand Down
2 changes: 1 addition & 1 deletion supabase/functions/collections-watcher/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Deno.serve(async (req) => {
if (req.method === "OPTIONS") return new Response("ok", { headers: corsHeaders });

// Cron: exige x-cron-secret para evitar chamadas diretas não autorizadas
const cronAuth = authorizeCron(req, {
const cronAuth = await authorizeCron(req, {
corsHeaders: {},
secretEnvName: "CRON_SECRET",
headerName: "x-cron-secret",
Expand Down
2 changes: 1 addition & 1 deletion supabase/functions/comparison-price-watcher/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Deno.serve(async (req) => {
if (req.method === "OPTIONS") return new Response(null, { headers: getCorsHeaders(req) });

// Cron: exige x-cron-secret para evitar chamadas diretas não autorizadas
const cronAuth = authorizeCron(req, {
const cronAuth = await authorizeCron(req, {
corsHeaders: {},
secretEnvName: "CRON_SECRET",
headerName: "x-cron-secret",
Expand Down
2 changes: 1 addition & 1 deletion supabase/functions/connections-auto-test/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,7 @@ Deno.serve(async (req) => {
if (req.method === "OPTIONS") return new Response(null, { headers: getCorsHeaders(req) });

// Hardening Onda 1: valida x-cron-secret
const auth = authorizeCron(req, {
const auth = await authorizeCron(req, {
corsHeaders,
secretEnvName: "CONNECTIONS_AUTO_TEST_SECRET",
headerName: "x-cron-secret",
Comment on lines +130 to 133
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

connections-auto-test ficou fora do SoT de CRON_SECRET.

Aqui a autenticação usa CONNECTIONS_AUTO_TEST_SECRET, divergindo da estratégia declarada de secret único para crons. Se esse nome não estiver provisionado em vault/env, o fluxo pode cair em legacy_no_auth enquanto verify_jwt está desativado.

💡 Ajuste sugerido
   const auth = await authorizeCron(req, {
     corsHeaders,
-    secretEnvName: "CONNECTIONS_AUTO_TEST_SECRET",
+    secretEnvName: "CRON_SECRET",
     headerName: "x-cron-secret",
   });
📝 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
const auth = await authorizeCron(req, {
corsHeaders,
secretEnvName: "CONNECTIONS_AUTO_TEST_SECRET",
headerName: "x-cron-secret",
const auth = await authorizeCron(req, {
corsHeaders,
secretEnvName: "CRON_SECRET",
headerName: "x-cron-secret",
🤖 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/connections-auto-test/index.ts` around lines 130 - 133,
The cron handler uses a specific env name "CONNECTIONS_AUTO_TEST_SECRET" instead
of the single source-of-truth cron secret, so update the authorizeCron call to
use the canonical secret name (e.g., the shared CRON_SECRET constant or env key
used across crons) by replacing secretEnvName: "CONNECTIONS_AUTO_TEST_SECRET"
with the central secret identifier; adjust any references near the authorizeCron
invocation (function authorizeCron, parameter secretEnvName) to point to the
shared CRON secret so the endpoint uses the same vault/env key as other crons
and avoids falling back to legacy_no_auth.

Expand Down
2 changes: 1 addition & 1 deletion supabase/functions/connections-health-check/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ Deno.serve(async (req) => {
if (req.method === "OPTIONS") return new Response(null, { headers: getCorsHeaders(req) });

// Cron: exige x-cron-secret para evitar chamadas diretas não autorizadas
const cronAuth = authorizeCron(req, {
const cronAuth = await authorizeCron(req, {
corsHeaders: {},
secretEnvName: "CRON_SECRET",
headerName: "x-cron-secret",
Expand Down
2 changes: 1 addition & 1 deletion supabase/functions/favorites-watcher/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ Deno.serve(async (req) => {
if (req.method === "OPTIONS") return new Response("ok", { headers: corsHeaders });

// Cron: exige x-cron-secret para evitar chamadas diretas não autorizadas
const cronAuth = authorizeCron(req, {
const cronAuth = await authorizeCron(req, {
corsHeaders: {},
secretEnvName: "CRON_SECRET",
headerName: "x-cron-secret",
Expand Down
2 changes: 1 addition & 1 deletion supabase/functions/ownership-audit/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ Deno.serve(async (req) => {
if (req.method === "OPTIONS") return new Response(null, { headers: getCorsHeaders(req) });

// Cron: exige x-cron-secret para evitar chamadas diretas não autorizadas
const cronAuth = authorizeCron(req, {
const cronAuth = await authorizeCron(req, {
corsHeaders: {},
secretEnvName: "CRON_SECRET",
headerName: "x-cron-secret",
Expand Down
2 changes: 1 addition & 1 deletion supabase/functions/process-queue/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const corsHeaders = buildPublicCorsHeaders();
Deno.serve(async (req) => {
// 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" });
const cronAuth = await authorizeCron(req, { corsHeaders: {}, secretEnvName: "CRON_SECRET", headerName: "x-cron-secret" });
if (!cronAuth.ok) return cronAuth.response;

if (req.method === 'OPTIONS') {
Expand Down
2 changes: 1 addition & 1 deletion supabase/functions/process-scheduled-reports/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const corsHeaders = buildPublicCorsHeaders();
Deno.serve(async (req) => {
// 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" });
const cronAuth = await authorizeCron(req, { corsHeaders: {}, secretEnvName: "CRON_SECRET", headerName: "x-cron-secret" });
if (!cronAuth.ok) return cronAuth.response;

if (req.method === "OPTIONS") {
Expand Down
2 changes: 1 addition & 1 deletion supabase/functions/quote-followup-reminders/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Deno.serve(async (req) => {
if (req.method === "OPTIONS") return new Response(null, { headers: corsHeaders });

// Cron: exige x-cron-secret para evitar chamadas diretas não autorizadas
const cronAuth = authorizeCron(req, {
const cronAuth = await authorizeCron(req, {
corsHeaders: {},
secretEnvName: "CRON_SECRET",
headerName: "x-cron-secret",
Expand Down
2 changes: 1 addition & 1 deletion supabase/functions/send-digest/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const corsHeaders = buildPublicCorsHeaders();
Deno.serve(async (req) => {
// 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" });
const cronAuth = await authorizeCron(req, { corsHeaders: {}, secretEnvName: "CRON_SECRET", headerName: "x-cron-secret" });
if (!cronAuth.ok) return cronAuth.response;

if (req.method === 'OPTIONS') {
Expand Down
2 changes: 1 addition & 1 deletion supabase/functions/send-notification/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ function jsonRes(corsHeaders: Record<string, string>, body: unknown, status = 20
Deno.serve(async (req) => {
// 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" });
const cronAuth = await authorizeCron(req, { corsHeaders: {}, secretEnvName: "CRON_SECRET", headerName: "x-cron-secret" });
if (!cronAuth.ok) return cronAuth.response;

const corsHeaders = getCorsHeaders(req);
Expand Down
8 changes: 8 additions & 0 deletions supabase/functions/send-scheduled-reports/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@ Deno.serve(async (req: Request) => {
return new Response("ok", { headers: corsHeaders });
}

// Cron: exige x-cron-secret para evitar chamadas diretas não autorizadas
const cronAuth = await authorizeCron(req, {
corsHeaders,
secretEnvName: "CRON_SECRET",
headerName: "x-cron-secret",
});
if (!cronAuth.ok) return cronAuth.response;

try {
const supabaseUrl = Deno.env.get("SUPABASE_URL")!;
const serviceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!;
Expand Down
Loading
Loading