Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
15 changes: 9 additions & 6 deletions supabase/functions/proxy-health/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,13 @@
// Ideal para chamar via cron a cada 5 minutos com evaluate=1.

import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.49.1'
import { getCorsHeaders } from '../_shared/validation.ts'

const corsHeaders = {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
function getJsonCorsHeaders(req?: Request) {
return {
...getCorsHeaders(req),

Copilot AI Apr 27, 2026

Copy link

Choose a reason for hiding this comment

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

getCorsHeaders(req) includes Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS. Because this handler doesn’t enforce GET-only, this change makes cross-origin non-GET requests from allowed origins pass CORS preflight (previously the endpoint didn’t advertise allowed methods). To avoid widening the callable surface, either add an explicit method guard (405 for non-GET/OPTIONS) and/or override Access-Control-Allow-Methods here to GET, OPTIONS to match the endpoint contract.

Suggested change
...getCorsHeaders(req),
...getCorsHeaders(req),
'Access-Control-Allow-Methods': 'GET, OPTIONS',

Copilot uses AI. Check for mistakes.
'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
}
}

// Limiares de alerta — ajustáveis via env vars.
Expand Down Expand Up @@ -168,7 +171,7 @@ export function evaluateAlerts(m: ComputedMetrics): AlertCandidate[] {

Deno.serve(async (req) => {
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: corsHeaders })
return new Response('ok', { headers: getJsonCorsHeaders(req) })
}

const url = new URL(req.url)
Expand All @@ -192,7 +195,7 @@ Deno.serve(async (req) => {

if (error) {
return new Response(JSON.stringify({ error: error.message }), {
status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' },
status: 500, headers: { ...getJsonCorsHeaders(req), 'Content-Type': 'application/json' },
})
}

Expand Down Expand Up @@ -259,6 +262,6 @@ Deno.serve(async (req) => {
alert_candidates: candidates,
fired_alerts: firedAlerts,
}, null, 2), {
headers: { ...corsHeaders, 'Content-Type': 'application/json' },
headers: { ...getJsonCorsHeaders(req), 'Content-Type': 'application/json' },
})
})
27 changes: 15 additions & 12 deletions supabase/functions/proxy-metrics/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,20 @@
// Output: text/plain; version=0.0.4 (Prometheus exposition format).

import { createClient } from 'https://esm.sh/@supabase/supabase-js@2.45.0'
import { getCorsHeaders } from '../_shared/validation.ts'

const SUPABASE_URL = Deno.env.get('SUPABASE_URL')!
const SERVICE_ROLE_KEY = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
const SCRAPE_TOKEN = Deno.env.get('PROXY_METRICS_TOKEN') ?? ''

const PROM_HEADERS = {
'Content-Type': 'text/plain; version=0.0.4; charset=utf-8',
'Cache-Control': 'no-store',
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers': 'authorization, content-type',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
function getPromHeaders(req?: Request) {
return {
...getCorsHeaders(req),
'Content-Type': 'text/plain; version=0.0.4; charset=utf-8',
'Cache-Control': 'no-store',
'Access-Control-Allow-Headers': 'authorization, content-type',
'Access-Control-Allow-Methods': 'GET, OPTIONS',
}
}

type MetricRow = {
Expand Down Expand Up @@ -195,23 +198,23 @@ function buildExposition(rows: MetricRow[], windowKey: WindowKey, generatedAtMs:

Deno.serve(async (req) => {
if (req.method === 'OPTIONS') {
return new Response('ok', { headers: PROM_HEADERS })
return new Response('ok', { headers: getPromHeaders(req) })
}
if (req.method !== 'GET') {
return new Response('Method not allowed\n', { status: 405, headers: PROM_HEADERS })
return new Response('Method not allowed\n', { status: 405, headers: getPromHeaders(req) })
}

// Auth — fail-closed if no token configured.
if (!SCRAPE_TOKEN) {
return new Response(
'# proxy-metrics: PROXY_METRICS_TOKEN secret is not configured.\n',
{ status: 503, headers: PROM_HEADERS },
{ status: 503, headers: getPromHeaders(req) },
)
}
const auth = req.headers.get('Authorization') ?? ''
const provided = auth.startsWith('Bearer ') ? auth.slice(7) : auth
if (provided !== SCRAPE_TOKEN) {
return new Response('# unauthorized\n', { status: 401, headers: PROM_HEADERS })
return new Response('# unauthorized\n', { status: 401, headers: getPromHeaders(req) })
}

const url = new URL(req.url)
Expand All @@ -236,7 +239,7 @@ Deno.serve(async (req) => {
if (error) {
return new Response(
`# error fetching proxy_metrics: ${error.message.replace(/\n/g, ' ')}\n`,
{ status: 500, headers: PROM_HEADERS },
{ status: 500, headers: getPromHeaders(req) },
)
}
if (!data || data.length === 0) break
Expand All @@ -245,5 +248,5 @@ Deno.serve(async (req) => {
}

const body = buildExposition(rows, windowKey, Date.now())
return new Response(body, { status: 200, headers: PROM_HEADERS })
return new Response(body, { status: 200, headers: getPromHeaders(req) })
})
Loading