From 1a1372e9ce9fb738c4ed50d9e921478270400aa2 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 23 May 2026 17:50:54 +0000 Subject: [PATCH 1/7] Work in progress --- src/routeTree.gen.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 0fe84b5..0bb5aca 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -670,3 +670,12 @@ const rootRouteChildren: RootRouteChildren = { export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) ._addFileTypes() + +import type { getRouter } from './router.tsx' +import type { createStart } from '@tanstack/react-start' +declare module '@tanstack/react-start' { + interface Register { + ssr: true + router: Awaited> + } +} From 4a331d6a699dfdbf62523112b6436ebc4672f50b Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 23 May 2026 17:52:46 +0000 Subject: [PATCH 2/7] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- supabase/functions/_shared/internal-auth.ts | 128 ++++++++++++++++++ .../bootstrap-internal-secret/index.ts | 96 +++++++++++++ 2 files changed, 224 insertions(+) create mode 100644 supabase/functions/_shared/internal-auth.ts create mode 100644 supabase/functions/bootstrap-internal-secret/index.ts diff --git a/supabase/functions/_shared/internal-auth.ts b/supabase/functions/_shared/internal-auth.ts new file mode 100644 index 0000000..ef22662 --- /dev/null +++ b/supabase/functions/_shared/internal-auth.ts @@ -0,0 +1,128 @@ +// internal-auth.ts +// Helper compartilhado para validar chamadas internas a edge functions. +// Permite que uma function aceite: +// - X-Internal-Secret válido (chamadas server-to-server, triggers pg_net, cron) +// - JWT de admin (painel admin via supabase.functions.invoke) +// - opcionalmente, JWT do dono do recurso (ownerUserId) +// +// Modo de operação: +// - Se INTERNAL_FUNCTION_SECRET estiver definido E o header X-Internal-Secret bater → OK +// - Senão, tenta validar o JWT do Authorization header +// - Retorna { ok, userId, isAdmin, isOwner, reason } + +import { createClient } from "https://esm.sh/@supabase/supabase-js@2.45.4"; + +export interface InternalAuthResult { + ok: boolean; + userId: string | null; + isAdmin: boolean; + isOwner: boolean; + viaSecret: boolean; + reason?: string; +} + +export interface AuthorizeInternalOptions { + /** Se fornecido, valida ownership contra esse user_id. */ + ownerUserId?: string | null; + /** Se true (default), permite admins. */ + allowAdmin?: boolean; + /** Se true (default), permite o dono. */ + allowOwner?: boolean; + /** Se true (default), permite via X-Internal-Secret. */ + allowSecret?: boolean; +} + +export async function authorizeInternalRequest( + req: Request, + opts: AuthorizeInternalOptions = {}, +): Promise { + const { + ownerUserId = null, + allowAdmin = true, + allowOwner = true, + allowSecret = true, + } = opts; + + // 1) X-Internal-Secret + if (allowSecret) { + const expected = Deno.env.get("INTERNAL_FUNCTION_SECRET") ?? ""; + const received = req.headers.get("x-internal-secret") ?? ""; + if (expected && received && constantTimeEq(expected, received)) { + return { + ok: true, + userId: null, + isAdmin: false, + isOwner: false, + viaSecret: true, + }; + } + } + + // 2) JWT + const authHeader = req.headers.get("authorization") ?? ""; + if (!authHeader.toLowerCase().startsWith("bearer ")) { + return { + ok: false, + userId: null, + isAdmin: false, + isOwner: false, + viaSecret: false, + reason: "missing bearer token", + }; + } + + const supabaseUrl = Deno.env.get("SUPABASE_URL")!; + const serviceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!; + const anonKey = Deno.env.get("SUPABASE_ANON_KEY") ?? Deno.env.get("SUPABASE_PUBLISHABLE_KEY") ?? ""; + + const userClient = createClient(supabaseUrl, anonKey, { + global: { headers: { Authorization: authHeader } }, + auth: { persistSession: false, autoRefreshToken: false }, + }); + const { data: userData, error: userErr } = await userClient.auth.getUser(); + if (userErr || !userData?.user) { + return { + ok: false, + userId: null, + isAdmin: false, + isOwner: false, + viaSecret: false, + reason: "invalid jwt", + }; + } + const userId = userData.user.id; + + let isAdmin = false; + if (allowAdmin) { + const admin = createClient(supabaseUrl, serviceKey, { + auth: { persistSession: false, autoRefreshToken: false }, + }); + const { data: roleOk } = await admin.rpc("has_role", { + _user_id: userId, + _role: "admin", + }); + isAdmin = roleOk === true; + } + + const isOwner = allowOwner && ownerUserId !== null && userId === ownerUserId; + + if (isAdmin || isOwner) { + return { ok: true, userId, isAdmin, isOwner, viaSecret: false }; + } + + return { + ok: false, + userId, + isAdmin, + isOwner, + viaSecret: false, + reason: "forbidden", + }; +} + +function constantTimeEq(a: string, b: string): boolean { + if (a.length !== b.length) return false; + let diff = 0; + for (let i = 0; i < a.length; i++) diff |= a.charCodeAt(i) ^ b.charCodeAt(i); + return diff === 0; +} diff --git a/supabase/functions/bootstrap-internal-secret/index.ts b/supabase/functions/bootstrap-internal-secret/index.ts new file mode 100644 index 0000000..c8e0c5b --- /dev/null +++ b/supabase/functions/bootstrap-internal-secret/index.ts @@ -0,0 +1,96 @@ +// bootstrap-internal-secret +// Função one-shot: lê INTERNAL_FUNCTION_SECRET de env e persiste em vault como +// 'internal_function_secret', para que triggers pg_net e cron job possam ler e +// enviar como X-Internal-Secret nas chamadas a outras edge functions. +// +// Requer JWT de admin. Idempotente — pode ser chamada várias vezes. + +import { createClient } from "https://esm.sh/@supabase/supabase-js@2.45.4"; +import { corsHeaders } from "../_shared/cors.ts"; + +const SUPABASE_URL = Deno.env.get("SUPABASE_URL")!; +const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!; +const SUPABASE_ANON_KEY = Deno.env.get("SUPABASE_ANON_KEY") ?? Deno.env.get("SUPABASE_PUBLISHABLE_KEY") ?? ""; + +function json(status: number, body: unknown) { + return new Response(JSON.stringify(body), { + status, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); +} + +Deno.serve(async (req) => { + if (req.method === "OPTIONS") return new Response(null, { headers: corsHeaders }); + + const secretValue = Deno.env.get("INTERNAL_FUNCTION_SECRET") ?? ""; + if (!secretValue) { + return json(500, { error: "INTERNAL_FUNCTION_SECRET not set in env" }); + } + + // Validar admin via JWT + const authHeader = req.headers.get("authorization") ?? ""; + if (!authHeader.toLowerCase().startsWith("bearer ")) { + return json(401, { error: "missing bearer token" }); + } + const userClient = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, { + global: { headers: { Authorization: authHeader } }, + auth: { persistSession: false, autoRefreshToken: false }, + }); + const { data: userData, error: userErr } = await userClient.auth.getUser(); + if (userErr || !userData?.user) return json(401, { error: "invalid jwt" }); + + const admin = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, { + auth: { persistSession: false, autoRefreshToken: false }, + }); + const { data: roleOk } = await admin.rpc("has_role", { + _user_id: userData.user.id, + _role: "admin", + }); + if (!roleOk) return json(403, { error: "admin role required" }); + + // Upsert no vault. Como vault.create_secret falha em nomes duplicados, + // tentamos atualizar existente primeiro via SQL direto. + try { + // 1) Tenta achar entry existente + const { data: existing } = await admin + .from("vault.decrypted_secrets" as never) + .select("id, decrypted_secret") + .eq("name", "internal_function_secret") + .maybeSingle() + .returns<{ id: string; decrypted_secret: string } | null>(); + + if (existing && existing.decrypted_secret === secretValue) { + return json(200, { ok: true, action: "already_synced" }); + } + + if (existing) { + // Atualiza o valor via RPC dedicada (não temos UPDATE direto em vault.secrets via PostgREST, + // então deletamos e recriamos). + await admin.rpc("vault_delete_secret", { secret_id: existing.id }); + } + + const { data: created, error: createErr } = await admin + .rpc("vault_create_secret", { + secret_value: secretValue, + secret_name: "internal_function_secret", + secret_description: "Shared secret for trigger pg_net → edge functions auth", + }) + .single(); + + if (createErr) { + console.error("vault_create_secret failed:", createErr); + return json(500, { error: "vault create failed", detail: createErr.message }); + } + + return json(200, { + ok: true, + action: existing ? "rotated" : "created", + // deno-lint-ignore no-explicit-any + secret_id: (created as any)?.secret_id ?? created, + }); + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + console.error("bootstrap-internal-secret fatal:", msg); + return json(500, { error: msg }); + } +}); From 85b6f0d46d2b71eacfcfa7b0e3362d25f23b3fbf Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 23 May 2026 17:53:07 +0000 Subject: [PATCH 3/7] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- src/routeTree.gen.ts | 9 --------- supabase/config.toml | 4 ++-- 2 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/routeTree.gen.ts b/src/routeTree.gen.ts index 0bb5aca..0fe84b5 100644 --- a/src/routeTree.gen.ts +++ b/src/routeTree.gen.ts @@ -670,12 +670,3 @@ const rootRouteChildren: RootRouteChildren = { export const routeTree = rootRouteImport ._addFileChildren(rootRouteChildren) ._addFileTypes() - -import type { getRouter } from './router.tsx' -import type { createStart } from '@tanstack/react-start' -declare module '@tanstack/react-start' { - interface Register { - ssr: true - router: Awaited> - } -} diff --git a/supabase/config.toml b/supabase/config.toml index 187b0d3..8e774b9 100644 --- a/supabase/config.toml +++ b/supabase/config.toml @@ -6,8 +6,8 @@ verify_jwt = false [functions.get-paddle-price] verify_jwt = false -[functions.generate-skill-markdown] -verify_jwt = false +# generate-skill-markdown: verify_jwt = true (default). Chamada do painel via +# supabase.functions.invoke já anexa o JWT do usuário autenticado. [functions.test-skill-dry-run] verify_jwt = false From b913f43dde5f69a5173915c368a7daf2ac34c2a2 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 23 May 2026 17:54:00 +0000 Subject: [PATCH 4/7] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- ...7_afd28c99-ee1f-41e6-9b38-a3bef45b59e9.sql | 213 ++++++++++++++++++ 1 file changed, 213 insertions(+) create mode 100644 supabase/migrations/20260523175357_afd28c99-ee1f-41e6-9b38-a3bef45b59e9.sql diff --git a/supabase/migrations/20260523175357_afd28c99-ee1f-41e6-9b38-a3bef45b59e9.sql b/supabase/migrations/20260523175357_afd28c99-ee1f-41e6-9b38-a3bef45b59e9.sql new file mode 100644 index 0000000..8c35763 --- /dev/null +++ b/supabase/migrations/20260523175357_afd28c99-ee1f-41e6-9b38-a3bef45b59e9.sql @@ -0,0 +1,213 @@ + +-- ========================================== +-- Correções de segurança — sem impacto em produção +-- ========================================== + +-- 1) RLS: policies service_role explícitas em tabelas backend-only +-- (silencia warnings do linter e documenta intenção) + +DROP POLICY IF EXISTS "service_role manages oauth_state_tokens" ON public.oauth_state_tokens; +CREATE POLICY "service_role manages oauth_state_tokens" + ON public.oauth_state_tokens + AS PERMISSIVE + FOR ALL + TO service_role + USING (true) + WITH CHECK (true); + +DROP POLICY IF EXISTS "service_role manages paddle_webhook_events" ON public.paddle_webhook_events; +CREATE POLICY "service_role manages paddle_webhook_events" + ON public.paddle_webhook_events + AS PERMISSIVE + FOR ALL + TO service_role + USING (true) + WITH CHECK (true); + +DROP POLICY IF EXISTS "service_role manages stripe_webhook_events" ON public.stripe_webhook_events; +CREATE POLICY "service_role manages stripe_webhook_events" + ON public.stripe_webhook_events + AS PERMISSIVE + FOR ALL + TO service_role + USING (true) + WITH CHECK (true); + +DROP POLICY IF EXISTS "service_role manages telegram_rate_limit_bucket" ON public.telegram_rate_limit_bucket; +CREATE POLICY "service_role manages telegram_rate_limit_bucket" + ON public.telegram_rate_limit_bucket + AS PERMISSIVE + FOR ALL + TO service_role + USING (true) + WITH CHECK (true); + +-- 2) REVOKE EXECUTE em SECURITY DEFINER internas (mantém só service_role/postgres) +-- Funções de uso EXCLUSIVAMENTE interno (triggers, vault, cleanup): +REVOKE EXECUTE ON FUNCTION public.update_updated_at_column() FROM anon, authenticated, public; +REVOKE EXECUTE ON FUNCTION public.handle_new_user() FROM anon, authenticated, public; +REVOKE EXECUTE ON FUNCTION public.vault_delete_secret(uuid) FROM anon, authenticated, public; +REVOKE EXECUTE ON FUNCTION public.vault_create_secret(text, text, text) FROM anon, authenticated, public; +REVOKE EXECUTE ON FUNCTION public.vault_decrypt_secret(uuid) FROM anon, authenticated, public; +REVOKE EXECUTE ON FUNCTION public.enforce_skill_limit() FROM anon, authenticated, public; +REVOKE EXECUTE ON FUNCTION public.cleanup_old_skill_versions() FROM anon, authenticated, public; +REVOKE EXECUTE ON FUNCTION public.cleanup_expired_oauth_states() FROM anon, authenticated, public; +REVOKE EXECUTE ON FUNCTION public.trigger_provision_agent() FROM anon, authenticated, public; +REVOKE EXECUTE ON FUNCTION public.enforce_job_limit() FROM anon, authenticated, public; +REVOKE EXECUTE ON FUNCTION public.trigger_suspend_or_resume_agent() FROM anon, authenticated, public; +REVOKE EXECUTE ON FUNCTION public.enforce_integration_limit() FROM anon, authenticated, public; + +-- Funções que precisam ser acessíveis a usuários autenticados (não a anon): +REVOKE EXECUTE ON FUNCTION public.has_role(uuid, app_role) FROM anon, public; +GRANT EXECUTE ON FUNCTION public.has_role(uuid, app_role) TO authenticated; + +REVOKE EXECUTE ON FUNCTION public.has_active_subscription(uuid, text) FROM anon, public; +GRANT EXECUTE ON FUNCTION public.has_active_subscription(uuid, text) TO authenticated; + +-- 3) Atualizar triggers para enviar X-Internal-Secret a partir do vault. +CREATE OR REPLACE FUNCTION public.trigger_provision_agent() + RETURNS trigger + LANGUAGE plpgsql + SECURITY DEFINER + SET search_path TO 'public', 'extensions' +AS $function$ +DECLARE + v_supabase_url text; + v_anon_key text; + v_internal_secret text; +BEGIN + IF NEW.status <> 'provisioning' THEN + RETURN NEW; + END IF; + + IF TG_OP = 'UPDATE' AND OLD.status = 'provisioning' THEN + RETURN NEW; + END IF; + + BEGIN + SELECT decrypted_secret INTO v_supabase_url + FROM vault.decrypted_secrets WHERE name = 'project_url' LIMIT 1; + SELECT decrypted_secret INTO v_anon_key + FROM vault.decrypted_secrets WHERE name = 'anon_key' LIMIT 1; + SELECT decrypted_secret INTO v_internal_secret + FROM vault.decrypted_secrets WHERE name = 'internal_function_secret' LIMIT 1; + EXCEPTION WHEN OTHERS THEN + v_supabase_url := NULL; + END; + + IF v_supabase_url IS NULL OR v_anon_key IS NULL THEN + RAISE LOG 'trigger_provision_agent: vault não configurado, pulando'; + RETURN NEW; + END IF; + + PERFORM net.http_post( + url := v_supabase_url || '/functions/v1/provision-agent', + headers := jsonb_build_object( + 'Content-Type', 'application/json', + 'Authorization', 'Bearer ' || v_anon_key, + 'X-Internal-Secret', COALESCE(v_internal_secret, '') + ), + body := jsonb_build_object('agent_instance_id', NEW.id) + ); + + RETURN NEW; +END; +$function$; + +CREATE OR REPLACE FUNCTION public.trigger_suspend_or_resume_agent() + RETURNS trigger + LANGUAGE plpgsql + SECURITY DEFINER + SET search_path TO 'public', 'extensions' +AS $function$ +DECLARE + v_supabase_url text; + v_anon_key text; + v_internal_secret text; + v_function text; + v_agent_id uuid; +BEGIN + IF TG_OP <> 'UPDATE' THEN + RETURN NEW; + END IF; + + IF NEW.status = OLD.status THEN + RETURN NEW; + END IF; + + IF NEW.status IN ('active', 'trialing') AND OLD.status NOT IN ('active', 'trialing') THEN + v_function := 'resume-agent'; + ELSIF NEW.status IN ('canceled', 'past_due', 'unpaid', 'paused') AND OLD.status IN ('active', 'trialing') THEN + v_function := 'suspend-agent'; + ELSE + RETURN NEW; + END IF; + + SELECT id INTO v_agent_id FROM public.agent_instances WHERE user_id = NEW.user_id LIMIT 1; + IF v_agent_id IS NULL THEN + RETURN NEW; + END IF; + + BEGIN + SELECT decrypted_secret INTO v_supabase_url + FROM vault.decrypted_secrets WHERE name = 'project_url' LIMIT 1; + SELECT decrypted_secret INTO v_anon_key + FROM vault.decrypted_secrets WHERE name = 'anon_key' LIMIT 1; + SELECT decrypted_secret INTO v_internal_secret + FROM vault.decrypted_secrets WHERE name = 'internal_function_secret' LIMIT 1; + EXCEPTION WHEN OTHERS THEN + v_supabase_url := NULL; + END; + + IF v_supabase_url IS NULL OR v_anon_key IS NULL THEN + RAISE LOG 'trigger_suspend_or_resume_agent: vault não configurado, pulando'; + RETURN NEW; + END IF; + + PERFORM net.http_post( + url := v_supabase_url || '/functions/v1/' || v_function, + headers := jsonb_build_object( + 'Content-Type', 'application/json', + 'Authorization', 'Bearer ' || v_anon_key, + 'X-Internal-Secret', COALESCE(v_internal_secret, '') + ), + body := jsonb_build_object('agent_instance_id', v_agent_id) + ); + + RETURN NEW; +END; +$function$; + +-- 4) Atualizar cron job keep-alive para enviar X-Internal-Secret +DO $$ +DECLARE + v_anon_key text; + v_internal_secret text; + v_command text; +BEGIN + SELECT decrypted_secret INTO v_anon_key + FROM vault.decrypted_secrets WHERE name = 'anon_key' LIMIT 1; + SELECT decrypted_secret INTO v_internal_secret + FROM vault.decrypted_secrets WHERE name = 'internal_function_secret' LIMIT 1; + + IF v_anon_key IS NULL OR v_internal_secret IS NULL THEN + RAISE LOG 'keep-alive cron: vault não populado, mantendo schedule atual'; + RETURN; + END IF; + + v_command := format($cmd$ + SELECT net.http_post( + url := 'https://smsarmgoirlcedmqvdgc.supabase.co/functions/v1/keep-alive-agents', + headers := jsonb_build_object( + 'Content-Type', 'application/json', + 'Authorization', 'Bearer %s', + 'X-Internal-Secret', '%s' + ), + body := '{}'::jsonb + ); + $cmd$, v_anon_key, v_internal_secret); + + PERFORM cron.unschedule('keep-alive-agents-every-4min'); + PERFORM cron.schedule('keep-alive-agents-every-4min', '*/4 * * * *', v_command); +END; +$$; From 70393534eba43f5d9b53c69a83c657c5346225ed Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 23 May 2026 17:54:36 +0000 Subject: [PATCH 5/7] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- supabase/functions/keep-alive-agents/index.ts | 8 ++++++++ supabase/functions/provision-agent/index.ts | 8 ++++++++ supabase/functions/resume-agent/index.ts | 11 ++++++++++- supabase/functions/suspend-agent/index.ts | 11 ++++++++++- 4 files changed, 36 insertions(+), 2 deletions(-) diff --git a/supabase/functions/keep-alive-agents/index.ts b/supabase/functions/keep-alive-agents/index.ts index 90aa1db..8541d8b 100644 --- a/supabase/functions/keep-alive-agents/index.ts +++ b/supabase/functions/keep-alive-agents/index.ts @@ -13,6 +13,7 @@ import { createClient } from "https://esm.sh/@supabase/supabase-js@2.45.4"; import { corsHeaders } from "../_shared/cors.ts"; +import { authorizeInternalRequest } from "../_shared/internal-auth.ts"; import { pullAgentCronjobsRuntimeState } from "../_shared/runtime-sync.ts"; const SUPABASE_URL = Deno.env.get("SUPABASE_URL")!; @@ -31,6 +32,13 @@ interface AgentRow { Deno.serve(async (req) => { if (req.method === "OPTIONS") return new Response(null, { headers: corsHeaders }); + // Auth: aceita X-Internal-Secret (pg_cron) OU JWT de admin. + const auth = await authorizeInternalRequest(req, { allowOwner: false }); + if (!auth.ok) { + console.warn(`keep-alive: auth rejected (${auth.reason})`); + return jsonResponse(401, { error: "unauthorized", reason: auth.reason }); + } + const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, { auth: { persistSession: false, autoRefreshToken: false }, }); diff --git a/supabase/functions/provision-agent/index.ts b/supabase/functions/provision-agent/index.ts index 8c92087..d41983b 100644 --- a/supabase/functions/provision-agent/index.ts +++ b/supabase/functions/provision-agent/index.ts @@ -5,6 +5,7 @@ import { createClient } from "https://esm.sh/@supabase/supabase-js@2.45.4"; import { corsHeaders } from "../_shared/cors.ts"; +import { authorizeInternalRequest } from "../_shared/internal-auth.ts"; import { HERMES_START_COMMAND, createRailwayService, @@ -60,6 +61,13 @@ async function notifyAdmin(message: string): Promise { Deno.serve(async (req) => { if (req.method === "OPTIONS") return new Response(null, { headers: corsHeaders }); + // Auth: aceita X-Internal-Secret (trigger pg_net / chamadas internas) OU JWT de admin. + const auth = await authorizeInternalRequest(req, { allowOwner: false }); + if (!auth.ok) { + console.warn(`[provision-agent] auth rejected: ${auth.reason}`); + return jsonResponse(401, { error: "unauthorized", reason: auth.reason }); + } + if (!RAILWAY_API_TOKEN) { return jsonResponse(500, { error: "RAILWAY_API_TOKEN not configured" }); } diff --git a/supabase/functions/resume-agent/index.ts b/supabase/functions/resume-agent/index.ts index 0af0053..017fd26 100644 --- a/supabase/functions/resume-agent/index.ts +++ b/supabase/functions/resume-agent/index.ts @@ -5,6 +5,7 @@ import { createClient } from "https://esm.sh/@supabase/supabase-js@2.45.4"; import { corsHeaders } from "../_shared/cors.ts"; +import { authorizeInternalRequest } from "../_shared/internal-auth.ts"; import { setHermesSuspended, getServiceContext } from "../_shared/railway.ts"; const SUPABASE_URL = Deno.env.get("SUPABASE_URL")!; @@ -39,12 +40,20 @@ Deno.serve(async (req) => { const { data: agent } = await supabase .from("agent_instances") - .select("id, status, railway_service_id, vps_pool_id") + .select("id, status, railway_service_id, vps_pool_id, user_id") .eq("id", body.agent_instance_id) .maybeSingle(); if (!agent) return jsonResponse(404, { error: "agent_instance not found" }); + // Auth: X-Internal-Secret (trigger), JWT de admin, OU JWT do dono do agente. + const auth = await authorizeInternalRequest(req, { ownerUserId: agent.user_id }); + if (!auth.ok) { + console.warn(`resume-agent: auth rejected (${auth.reason})`); + return jsonResponse(401, { error: "unauthorized", reason: auth.reason }); + } + + if (!agent.railway_service_id) { return jsonResponse(409, { error: "agent has no container — needs full provisioning instead", diff --git a/supabase/functions/suspend-agent/index.ts b/supabase/functions/suspend-agent/index.ts index 8bf9910..464b856 100644 --- a/supabase/functions/suspend-agent/index.ts +++ b/supabase/functions/suspend-agent/index.ts @@ -6,6 +6,7 @@ import { createClient } from "https://esm.sh/@supabase/supabase-js@2.45.4"; import { corsHeaders } from "../_shared/cors.ts"; +import { authorizeInternalRequest } from "../_shared/internal-auth.ts"; import { setHermesSuspended, getServiceContext } from "../_shared/railway.ts"; const SUPABASE_URL = Deno.env.get("SUPABASE_URL")!; @@ -40,11 +41,19 @@ Deno.serve(async (req) => { const { data: agent } = await supabase .from("agent_instances") - .select("id, status, railway_service_id, vps_pool_id") + .select("id, status, railway_service_id, vps_pool_id, user_id") .eq("id", body.agent_instance_id) .maybeSingle(); if (!agent) return jsonResponse(404, { error: "agent_instance not found" }); + + // Auth: X-Internal-Secret (trigger), JWT de admin, OU JWT do dono do agente. + const auth = await authorizeInternalRequest(req, { ownerUserId: agent.user_id }); + if (!auth.ok) { + console.warn(`suspend-agent: auth rejected (${auth.reason})`); + return jsonResponse(401, { error: "unauthorized", reason: auth.reason }); + } + if (agent.status === "suspended") { return jsonResponse(200, { ok: true, already_suspended: true }); } From 1674e4c2cf0615e5fb667864b31cf581a66592e3 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 23 May 2026 17:54:47 +0000 Subject: [PATCH 6/7] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- supabase/functions/validate-telegram-bot/index.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/supabase/functions/validate-telegram-bot/index.ts b/supabase/functions/validate-telegram-bot/index.ts index b4c02f4..b940bbe 100644 --- a/supabase/functions/validate-telegram-bot/index.ts +++ b/supabase/functions/validate-telegram-bot/index.ts @@ -221,6 +221,7 @@ Deno.serve(async (req) => { headers: { "Content-Type": "application/json", Authorization: `Bearer ${serviceKey}`, + "X-Internal-Secret": Deno.env.get("INTERNAL_FUNCTION_SECRET") ?? "", }, body: JSON.stringify({ agent_instance_id: agent.id }), }).catch((err) => console.error("auto-provision fetch error:", err)); From 262ff41baa89b8683a64a97660f928c9735ce2c9 Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Sat, 23 May 2026 17:55:11 +0000 Subject: [PATCH 7/7] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- .../configure-telegram-webhook/index.ts | 1 + .../functions/managed-bot-webhook/index.ts | 15 +++++++++ supabase/functions/railway-webhook/index.ts | 32 ++++++++++++++++++- 3 files changed, 47 insertions(+), 1 deletion(-) diff --git a/supabase/functions/configure-telegram-webhook/index.ts b/supabase/functions/configure-telegram-webhook/index.ts index 8848a8a..afe7d97 100644 --- a/supabase/functions/configure-telegram-webhook/index.ts +++ b/supabase/functions/configure-telegram-webhook/index.ts @@ -114,6 +114,7 @@ Deno.serve(async (req) => { headers: { "Content-Type": "application/json", Authorization: `Bearer ${serviceKey}`, + "X-Internal-Secret": Deno.env.get("INTERNAL_FUNCTION_SECRET") ?? "", }, body: JSON.stringify({ agent_instance_id: agent.id, diff --git a/supabase/functions/managed-bot-webhook/index.ts b/supabase/functions/managed-bot-webhook/index.ts index e08d082..9bdce4c 100644 --- a/supabase/functions/managed-bot-webhook/index.ts +++ b/supabase/functions/managed-bot-webhook/index.ts @@ -53,6 +53,20 @@ Deno.serve(async (req) => { return ack(); } + // Verificação de origem: se TELEGRAM_MANAGER_BOT_WEBHOOK_SECRET estiver + // configurado, exige o header X-Telegram-Bot-Api-Secret-Token correspondente. + // Caso contrário, opera em modo permissivo (comportamento anterior) + log. + const expectedSecret = Deno.env.get("TELEGRAM_MANAGER_BOT_WEBHOOK_SECRET") ?? ""; + if (expectedSecret) { + const incoming = req.headers.get("X-Telegram-Bot-Api-Secret-Token") ?? ""; + if (incoming !== expectedSecret) { + console.warn("managed-bot-webhook: invalid telegram secret token"); + return new Response("unauthorized", { status: 401 }); + } + } else { + console.warn("managed-bot-webhook: TELEGRAM_MANAGER_BOT_WEBHOOK_SECRET ausente — modo permissivo"); + } + if (!managerToken) { console.error("TELEGRAM_MANAGER_BOT_TOKEN ausente"); return ack(); @@ -165,6 +179,7 @@ Deno.serve(async (req) => { headers: { "Content-Type": "application/json", Authorization: `Bearer ${serviceKey}`, + "X-Internal-Secret": Deno.env.get("INTERNAL_FUNCTION_SECRET") ?? "", }, body: JSON.stringify({ agent_instance_id: agentInstance.id, diff --git a/supabase/functions/railway-webhook/index.ts b/supabase/functions/railway-webhook/index.ts index 4bd1af2..1cab649 100644 --- a/supabase/functions/railway-webhook/index.ts +++ b/supabase/functions/railway-webhook/index.ts @@ -48,9 +48,39 @@ Deno.serve(async (req) => { return jsonResponse(200, { ignored: true, reason: "method not allowed" }); } + const rawBody = await req.text(); + + // Verificação de assinatura: modo permissivo se RAILWAY_WEBHOOK_SECRET não estiver + // configurado (mantém comportamento atual). Quando configurado, exige HMAC SHA-256. + const railwaySecret = Deno.env.get("RAILWAY_WEBHOOK_SECRET") ?? ""; + if (railwaySecret) { + const sig = req.headers.get("X-Railway-Signature") ?? ""; + try { + const key = await crypto.subtle.importKey( + "raw", + new TextEncoder().encode(railwaySecret), + { name: "HMAC", hash: "SHA-256" }, + false, + ["sign"], + ); + const macBuf = await crypto.subtle.sign("HMAC", key, new TextEncoder().encode(rawBody)); + const macHex = Array.from(new Uint8Array(macBuf)).map((b) => b.toString(16).padStart(2, "0")).join(""); + const expected = sig.startsWith("sha256=") ? sig.slice(7) : sig; + if (expected !== macHex) { + console.warn("railway-webhook: invalid signature"); + return jsonResponse(401, { error: "invalid signature" }); + } + } catch (e) { + console.error("railway-webhook: signature verify failed:", String(e)); + return jsonResponse(401, { error: "signature verification failed" }); + } + } else { + console.warn("railway-webhook: RAILWAY_WEBHOOK_SECRET ausente — modo permissivo"); + } + let payload: Record; try { - payload = await req.json(); + payload = JSON.parse(rawBody); } catch { console.log("railway-webhook: invalid json body"); return jsonResponse(200, { ignored: true, reason: "invalid json" });