From a34ff1b17ba7627c6dd493c1068dbb400e0ea70d Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 19:57:36 +0000 Subject: [PATCH 1/2] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- supabase/config.toml | 12 + supabase/functions/_shared/telegram.ts | 47 ++++ .../configure-telegram-webhook/index.ts | 124 ++++++++++ .../functions/disconnect-telegram/index.ts | 111 +++++++++ supabase/functions/telegram-webhook/index.ts | 212 +++++++++++++++++ .../functions/validate-telegram-bot/index.ts | 221 ++++++++++++++++++ 6 files changed, 727 insertions(+) create mode 100644 supabase/functions/_shared/telegram.ts create mode 100644 supabase/functions/configure-telegram-webhook/index.ts create mode 100644 supabase/functions/disconnect-telegram/index.ts create mode 100644 supabase/functions/telegram-webhook/index.ts create mode 100644 supabase/functions/validate-telegram-bot/index.ts diff --git a/supabase/config.toml b/supabase/config.toml index a059644..dea520e 100644 --- a/supabase/config.toml +++ b/supabase/config.toml @@ -14,3 +14,15 @@ verify_jwt = true [functions.publish-skill-version] verify_jwt = true + +[functions.validate-telegram-bot] +verify_jwt = true + +[functions.configure-telegram-webhook] +verify_jwt = true + +[functions.disconnect-telegram] +verify_jwt = true + +[functions.telegram-webhook] +verify_jwt = false diff --git a/supabase/functions/_shared/telegram.ts b/supabase/functions/_shared/telegram.ts new file mode 100644 index 0000000..d440519 --- /dev/null +++ b/supabase/functions/_shared/telegram.ts @@ -0,0 +1,47 @@ +// Helpers compartilhados das Edge Functions Telegram + +const TELEGRAM_API = "https://api.telegram.org"; + +export type TelegramApiResult = { + ok: boolean; + status: number; + description?: string; + error_code?: number; + result?: T; + parameters?: { retry_after?: number }; +}; + +export async function telegramApi( + token: string, + method: string, + body?: Record, +): Promise> { + try { + const res = await fetch(`${TELEGRAM_API}/bot${token}/${method}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: body ? JSON.stringify(body) : undefined, + }); + const data = await res.json().catch(() => ({})); + return { + ok: !!data.ok, + status: res.status, + description: data.description, + error_code: data.error_code, + result: data.result, + parameters: data.parameters, + }; + } catch (err) { + return { + ok: false, + status: 0, + description: err instanceof Error ? err.message : String(err), + }; + } +} + +// Resposta padrão para o Telegram que NÃO deve gerar retry +// (tudo que vem do webhook deve responder 200, mesmo em falha interna) +export function telegramAck(): Response { + return new Response("ok", { status: 200 }); +} diff --git a/supabase/functions/configure-telegram-webhook/index.ts b/supabase/functions/configure-telegram-webhook/index.ts new file mode 100644 index 0000000..b173ba0 --- /dev/null +++ b/supabase/functions/configure-telegram-webhook/index.ts @@ -0,0 +1,124 @@ +// configure-telegram-webhook +// Gera secret aleatório, configura webhook no Telegram e marca telegram_webhook_configured=true. + +import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4"; +import { corsHeaders } from "../_shared/cors.ts"; +import { telegramApi } from "../_shared/telegram.ts"; + +function jsonResponse(body: unknown, status = 200): Response { + return new Response(JSON.stringify(body), { + status, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); +} + +function randomHex(bytes: number): string { + const buf = new Uint8Array(bytes); + crypto.getRandomValues(buf); + return Array.from(buf) + .map((b) => b.toString(16).padStart(2, "0")) + .join(""); +} + +async function getDecryptedSecret( + admin: ReturnType, + secretId: string, +): Promise { + const { data, error } = await admin + .rpc("vault_decrypt_secret", { secret_id: secretId }) + .single(); + if (!error && data) { + // deno-lint-ignore no-explicit-any + return (data as any).decrypted_secret ?? (data as unknown as string); + } + return null; +} + +Deno.serve(async (req) => { + if (req.method === "OPTIONS") { + return new Response(null, { headers: corsHeaders }); + } + + try { + const supabaseUrl = Deno.env.get("SUPABASE_URL")!; + const serviceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!; + const anonKey = Deno.env.get("SUPABASE_ANON_KEY")!; + + const authHeader = req.headers.get("Authorization") ?? ""; + const userClient = createClient(supabaseUrl, anonKey, { + global: { headers: { Authorization: authHeader } }, + }); + const { data: userData, error: userErr } = await userClient.auth.getUser(); + if (userErr || !userData.user) { + return jsonResponse({ error: "Não autenticado" }, 401); + } + const userId = userData.user.id; + + const admin = createClient(supabaseUrl, serviceKey); + + const { data: agent, error: agentErr } = await admin + .from("agent_instances") + .select("id, uuid_tenant, telegram_bot_token_vault_id, status") + .eq("user_id", userId) + .maybeSingle(); + + if (agentErr || !agent) { + return jsonResponse({ error: "Agente não encontrado." }, 404); + } + if (!agent.telegram_bot_token_vault_id) { + return jsonResponse( + { error: "Conecte um bot antes de configurar o webhook." }, + 400, + ); + } + + const token = await getDecryptedSecret(admin, agent.telegram_bot_token_vault_id); + if (!token) { + return jsonResponse( + { error: "Token corrompido. Desconecte e reconecte o bot." }, + 500, + ); + } + + const webhookSecret = randomHex(32); + const webhookUrl = + `${supabaseUrl}/functions/v1/telegram-webhook?token=${agent.uuid_tenant}`; + + const setRes = await telegramApi(token, "setWebhook", { + url: webhookUrl, + allowed_updates: ["message"], + drop_pending_updates: true, + secret_token: webhookSecret, + }); + + if (!setRes.ok) { + console.error("setWebhook failed", setRes); + return jsonResponse( + { error: "Falha ao configurar webhook no Telegram." }, + 500, + ); + } + + const { error: updErr } = await admin + .from("agent_instances") + .update({ + telegram_webhook_secret: webhookSecret, + telegram_webhook_configured: true, + updated_at: new Date().toISOString(), + }) + .eq("id", agent.id); + + if (updErr) { + console.error("agent update error", updErr); + return jsonResponse({ error: "Webhook configurado, mas falha ao salvar." }, 500); + } + + return jsonResponse({ success: true }); + } catch (err) { + console.error("configure-telegram-webhook fatal", err); + return jsonResponse( + { error: err instanceof Error ? err.message : "Erro inesperado" }, + 500, + ); + } +}); diff --git a/supabase/functions/disconnect-telegram/index.ts b/supabase/functions/disconnect-telegram/index.ts new file mode 100644 index 0000000..cac881e --- /dev/null +++ b/supabase/functions/disconnect-telegram/index.ts @@ -0,0 +1,111 @@ +// disconnect-telegram +// Remove webhook no Telegram, deleta secret do Vault e limpa colunas em agent_instances. +// Preserva telegram_messages_log (auditoria) e telegram_onboarding_completed. + +import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4"; +import { corsHeaders } from "../_shared/cors.ts"; +import { telegramApi } from "../_shared/telegram.ts"; + +function jsonResponse(body: unknown, status = 200): Response { + return new Response(JSON.stringify(body), { + status, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); +} + +async function getDecryptedSecret( + admin: ReturnType, + secretId: string, +): Promise { + const { data, error } = await admin + .rpc("vault_decrypt_secret", { secret_id: secretId }) + .single(); + if (!error && data) { + // deno-lint-ignore no-explicit-any + return (data as any).decrypted_secret ?? (data as unknown as string); + } + return null; +} + +Deno.serve(async (req) => { + if (req.method === "OPTIONS") { + return new Response(null, { headers: corsHeaders }); + } + + try { + const supabaseUrl = Deno.env.get("SUPABASE_URL")!; + const serviceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!; + const anonKey = Deno.env.get("SUPABASE_ANON_KEY")!; + + const authHeader = req.headers.get("Authorization") ?? ""; + const userClient = createClient(supabaseUrl, anonKey, { + global: { headers: { Authorization: authHeader } }, + }); + const { data: userData, error: userErr } = await userClient.auth.getUser(); + if (userErr || !userData.user) { + return jsonResponse({ error: "Não autenticado" }, 401); + } + const userId = userData.user.id; + + const admin = createClient(supabaseUrl, serviceKey); + + const { data: agent, error: agentErr } = await admin + .from("agent_instances") + .select("id, telegram_bot_token_vault_id") + .eq("user_id", userId) + .maybeSingle(); + + if (agentErr || !agent) { + return jsonResponse({ error: "Agente não encontrado." }, 404); + } + + // 1) Tenta deletar webhook no Telegram (silent em falha) + if (agent.telegram_bot_token_vault_id) { + const token = await getDecryptedSecret(admin, agent.telegram_bot_token_vault_id); + if (token) { + try { + await telegramApi(token, "deleteWebhook", { drop_pending_updates: true }); + } catch (e) { + console.warn("deleteWebhook ignorado:", e); + } + } + + // 2) Remove secret do Vault via RPC + try { + await admin.rpc("vault_delete_secret", { + secret_id: agent.telegram_bot_token_vault_id, + }); + } catch (e) { + console.warn("vault_delete_secret ignorado:", e); + } + } + + // 3) Limpa colunas no agent_instances + const { error: updErr } = await admin + .from("agent_instances") + .update({ + telegram_bot_token_vault_id: null, + telegram_bot_username: null, + telegram_webhook_configured: false, + telegram_webhook_secret: null, + telegram_first_message_received_at: null, + telegram_connected_at: null, + telegram_token_invalid: false, + updated_at: new Date().toISOString(), + }) + .eq("id", agent.id); + + if (updErr) { + console.error("agent update error", updErr); + return jsonResponse({ error: "Falha ao desconectar." }, 500); + } + + return jsonResponse({ success: true }); + } catch (err) { + console.error("disconnect-telegram fatal", err); + return jsonResponse( + { error: err instanceof Error ? err.message : "Erro inesperado" }, + 500, + ); + } +}); diff --git a/supabase/functions/telegram-webhook/index.ts b/supabase/functions/telegram-webhook/index.ts new file mode 100644 index 0000000..3a61b95 --- /dev/null +++ b/supabase/functions/telegram-webhook/index.ts @@ -0,0 +1,212 @@ +// telegram-webhook (PÚBLICA — verify_jwt = false) +// Recebe updates do Telegram, valida 3 camadas (uuid_tenant, secret, rate limit), +// registra mensagem em telegram_messages_log e responde com placeholder via sendMessage. +// +// TODO Fase 5: substituir resposta placeholder por proxy para container Hermes via SSH/API. + +import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4"; +import { telegramApi, telegramAck } from "../_shared/telegram.ts"; + +const RATE_LIMIT_MAX = 30; +const RATE_LIMIT_WINDOW_MS = 60_000; + +function firstName(fullName: string | null | undefined): string { + if (!fullName) return "Mika"; + const parts = fullName.trim().split(/\s+/); + return parts[0] || "Mika"; +} + +async function getDecryptedSecret( + admin: ReturnType, + secretId: string, +): Promise { + const { data, error } = await admin + .rpc("vault_decrypt_secret", { secret_id: secretId }) + .single(); + if (!error && data) { + // deno-lint-ignore no-explicit-any + return (data as any).decrypted_secret ?? (data as unknown as string); + } + return null; +} + +Deno.serve(async (req) => { + // Sempre responde 200 para o Telegram não retentar — ack cedo em qualquer falha. + try { + const supabaseUrl = Deno.env.get("SUPABASE_URL")!; + const serviceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!; + const admin = createClient(supabaseUrl, serviceKey); + + const url = new URL(req.url); + const uuidTenant = url.searchParams.get("token"); + if (!uuidTenant) return telegramAck(); + + const incomingSecret = req.headers.get("X-Telegram-Bot-Api-Secret-Token") ?? ""; + + // 1) Localiza agente + nome do usuário + const { data: agent, error: agentErr } = await admin + .from("agent_instances") + .select( + "id, user_id, status, telegram_webhook_secret, telegram_bot_token_vault_id, telegram_connected_at, profiles:profiles!agent_instances_user_id_fkey(full_name)", + ) + .eq("uuid_tenant", uuidTenant) + .maybeSingle(); + + if (agentErr || !agent) return telegramAck(); + if (agent.status === "suspended" || agent.status === "error") return telegramAck(); + + // 2) Valida secret_token (anti-spoofing) + if ( + !agent.telegram_webhook_secret || + incomingSecret !== agent.telegram_webhook_secret + ) { + console.warn("telegram-webhook: secret mismatch", { agent_id: agent.id }); + return new Response("unauthorized", { status: 401 }); + } + + // 3) Rate limit por agent_instance (30 req/min) + const now = new Date(); + const { data: bucket } = await admin + .from("telegram_rate_limit_bucket") + .select("*") + .eq("agent_instance_id", agent.id) + .maybeSingle(); + + if (!bucket) { + await admin.from("telegram_rate_limit_bucket").insert({ + agent_instance_id: agent.id, + request_count: 1, + window_start: now.toISOString(), + updated_at: now.toISOString(), + }); + } else { + const windowStart = new Date(bucket.window_start as string); + const elapsed = now.getTime() - windowStart.getTime(); + if (elapsed > RATE_LIMIT_WINDOW_MS) { + await admin + .from("telegram_rate_limit_bucket") + .update({ + request_count: 1, + window_start: now.toISOString(), + updated_at: now.toISOString(), + }) + .eq("agent_instance_id", agent.id); + } else { + const newCount = (bucket.request_count as number) + 1; + if (newCount > RATE_LIMIT_MAX) { + console.warn("telegram-webhook: rate limit hit", { agent_id: agent.id }); + return telegramAck(); // throttle silencioso + } + await admin + .from("telegram_rate_limit_bucket") + .update({ + request_count: newCount, + updated_at: now.toISOString(), + }) + .eq("agent_instance_id", agent.id); + } + } + + // 4) Parse do payload + const payload = await req.json().catch(() => null); + if (!payload || !payload.message) return telegramAck(); + + const message = payload.message; + const chatId = message.chat?.id as number | undefined; + if (!chatId) return telegramAck(); + + const fromId = message.from?.id as number | null | undefined; + const fromUsername = (message.from?.username as string | undefined) ?? null; + const text = (message.text as string | undefined) ?? null; + const entities = (message.entities as Array<{ type: string }> | undefined) ?? []; + + let messageType: "text" | "command" | "other" = "other"; + if (entities.some((e) => e.type === "bot_command")) messageType = "command"; + else if (text) messageType = "text"; + + // 5) Detecta primeira mensagem da sessão atual + let isFirstMessage = false; + if (agent.telegram_connected_at) { + const { count } = await admin + .from("telegram_messages_log") + .select("id", { count: "exact", head: true }) + .eq("agent_instance_id", agent.id) + .eq("direction", "incoming") + .gte("created_at", agent.telegram_connected_at as string); + if ((count ?? 0) === 0) { + isFirstMessage = true; + await admin + .from("agent_instances") + .update({ telegram_first_message_received_at: now.toISOString() }) + .eq("id", agent.id); + } + } + + // 6) Insert da mensagem incoming + await admin.from("telegram_messages_log").insert({ + agent_instance_id: agent.id, + user_id: agent.user_id, + telegram_chat_id: chatId, + telegram_user_id: fromId ?? null, + telegram_username: fromUsername, + direction: "incoming", + message_text: text, + message_type: messageType, + is_first_message: isFirstMessage, + raw_payload: payload, + }); + + // 7) Resposta placeholder via sendMessage + // TODO Fase 5: substituir resposta placeholder por proxy para container Hermes via SSH/API. + if (!agent.telegram_bot_token_vault_id) return telegramAck(); + const token = await getDecryptedSecret(admin, agent.telegram_bot_token_vault_id as string); + if (!token) return telegramAck(); + + // deno-lint-ignore no-explicit-any + const fullName = (agent as any).profiles?.full_name as string | null | undefined; + const replyText = + `Olá! Sou o Mika de ${firstName(fullName)}. Estou quase pronto para conversar com você de verdade — meu cérebro está sendo configurado pela DOMCO. Em breve vou responder de forma inteligente! Por enquanto, este é apenas um teste de conexão. ✨`; + + let send = await telegramApi(token, "sendMessage", { + chat_id: chatId, + text: replyText, + }); + + if (!send.ok && send.status === 401) { + await admin + .from("agent_instances") + .update({ telegram_token_invalid: true, updated_at: new Date().toISOString() }) + .eq("id", agent.id); + return telegramAck(); + } + + if (!send.ok && send.status === 429) { + const retryAfter = send.parameters?.retry_after ?? 1; + await new Promise((r) => setTimeout(r, Math.min(retryAfter, 5) * 1000)); + send = await telegramApi(token, "sendMessage", { + chat_id: chatId, + text: replyText, + }); + } + + if (send.ok) { + await admin.from("telegram_messages_log").insert({ + agent_instance_id: agent.id, + user_id: agent.user_id, + telegram_chat_id: chatId, + direction: "outgoing", + message_text: replyText, + message_type: "text", + is_first_message: false, + raw_payload: send.result ?? null, + }); + } else { + console.error("sendMessage failed", send); + } + + return telegramAck(); + } catch (err) { + console.error("telegram-webhook fatal", err); + return telegramAck(); + } +}); diff --git a/supabase/functions/validate-telegram-bot/index.ts b/supabase/functions/validate-telegram-bot/index.ts new file mode 100644 index 0000000..89f3446 --- /dev/null +++ b/supabase/functions/validate-telegram-bot/index.ts @@ -0,0 +1,221 @@ +// validate-telegram-bot +// Recebe { token } do usuário autenticado, valida no Telegram via getMe, +// garante unicidade do bot, salva o token no Vault e atualiza agent_instances. + +import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4"; +import { corsHeaders } from "../_shared/cors.ts"; +import { telegramApi } from "../_shared/telegram.ts"; + +interface GetMeResult { + id: number; + is_bot: boolean; + first_name: string; + username: string; +} + +function jsonResponse(body: unknown, status = 200): Response { + 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 }); + } + + try { + const supabaseUrl = Deno.env.get("SUPABASE_URL")!; + const serviceKey = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!; + const anonKey = Deno.env.get("SUPABASE_ANON_KEY")!; + + const authHeader = req.headers.get("Authorization") ?? ""; + const userClient = createClient(supabaseUrl, anonKey, { + global: { headers: { Authorization: authHeader } }, + }); + const { data: userData, error: userErr } = await userClient.auth.getUser(); + if (userErr || !userData.user) { + return jsonResponse({ error: "Não autenticado" }, 401); + } + const userId = userData.user.id; + + const body = await req.json().catch(() => ({})); + const token = (body?.token ?? "").toString().trim(); + if (!token || !/^\d+:[A-Za-z0-9_-]+$/.test(token)) { + return jsonResponse( + { error: "Token inválido. Verifique se copiou corretamente do BotFather." }, + 400, + ); + } + + // 1) Valida no Telegram + const me = await telegramApi(token, "getMe"); + if (!me.ok) { + if (me.status === 401 || me.status === 404) { + return jsonResponse( + { error: "Token inválido. Verifique se copiou corretamente do BotFather." }, + 400, + ); + } + if (me.status === 429) { + return jsonResponse( + { + error: + "O Telegram está limitando nossas requisições. Aguarde 1 minuto e tente novamente.", + }, + 429, + ); + } + return jsonResponse( + { error: me.description || "Falha ao validar token no Telegram." }, + 502, + ); + } + + const bot = me.result!; + const botUsername = bot.username; + const botName = bot.first_name; + const botId = bot.id; + + // 2) Service-role client p/ banco e vault + const admin = createClient(supabaseUrl, serviceKey); + + // 3) Garante que o usuário tem agent_instance + const { data: agent, error: agentErr } = await admin + .from("agent_instances") + .select("id, telegram_bot_token_vault_id, status") + .eq("user_id", userId) + .maybeSingle(); + + if (agentErr) { + console.error("agent lookup error", agentErr); + return jsonResponse({ error: "Falha ao localizar agente." }, 500); + } + if (!agent) { + return jsonResponse( + { error: "Seu agente ainda não está pronto. Aguarde o provisionamento." }, + 409, + ); + } + if (agent.status === "suspended" || agent.status === "error") { + return jsonResponse( + { error: "Agente suspenso. Regularize sua assinatura antes de conectar o Telegram." }, + 403, + ); + } + + // 4) Verifica unicidade do bot username (em outro agent_instance) + const { data: conflict } = await admin + .from("agent_instances") + .select("id") + .eq("telegram_bot_username", botUsername) + .neq("id", agent.id) + .maybeSingle(); + + if (conflict) { + return jsonResponse( + { + error: + "Este bot já está conectado a outra conta Mika. Crie um novo bot no BotFather.", + }, + 409, + ); + } + + // 5) Se já houver um vault_id antigo, remove (reconexão) + if (agent.telegram_bot_token_vault_id) { + await admin.rpc("exec_sql_void", {}).catch(() => {}); + // tenta remover diretamente; ignora falha + const { error: delErr } = await admin + .from("vault.secrets" as unknown as never) + .delete() + .eq("id", agent.telegram_bot_token_vault_id); + if (delErr) { + console.warn("Vault old secret delete (ignorável):", delErr.message); + } + } + + // 6) Cria secret no Vault via RPC SQL + const secretName = `telegram_bot_token_${userId}_${Math.floor(Date.now() / 1000)}`; + const { data: vaultData, error: vaultErr } = await admin + .rpc("vault_create_secret", { + secret_value: token, + secret_name: secretName, + secret_description: `Telegram bot token for user ${userId}`, + }) + .single(); + + let secretId: string | null = null; + if (!vaultErr && vaultData) { + // RPC pode retornar { secret_id } ou string + // deno-lint-ignore no-explicit-any + secretId = (vaultData as any).secret_id ?? (vaultData as unknown as string); + } else { + // Fallback: usa SQL direto via PostgREST (requer função vault.create_secret exposta). + // Caso não exista a RPC, criamos via PostgREST raw SQL exec. + const sqlRes = await fetch(`${supabaseUrl}/rest/v1/rpc/vault_create_secret`, { + method: "POST", + headers: { + apikey: serviceKey, + Authorization: `Bearer ${serviceKey}`, + "Content-Type": "application/json", + }, + body: JSON.stringify({ + secret_value: token, + secret_name: secretName, + secret_description: `Telegram bot token for user ${userId}`, + }), + }); + if (sqlRes.ok) { + const out = await sqlRes.json().catch(() => null); + secretId = + (Array.isArray(out) ? out[0]?.secret_id ?? out[0] : out?.secret_id ?? out) ?? null; + } else { + console.error("vault_create_secret RPC failed", await sqlRes.text()); + } + } + + if (!secretId) { + return jsonResponse( + { + error: + "Não foi possível salvar o token com segurança. Tente novamente em instantes.", + }, + 500, + ); + } + + // 7) Atualiza agent_instances + const { error: updErr } = await admin + .from("agent_instances") + .update({ + telegram_bot_token_vault_id: secretId, + telegram_bot_username: botUsername, + telegram_connected_at: new Date().toISOString(), + telegram_token_invalid: false, + telegram_webhook_configured: false, + telegram_first_message_received_at: null, + updated_at: new Date().toISOString(), + }) + .eq("id", agent.id); + + if (updErr) { + console.error("agent update error", updErr); + return jsonResponse({ error: "Falha ao salvar dados do bot." }, 500); + } + + return jsonResponse({ + valid: true, + bot_username: botUsername, + bot_name: botName, + bot_id: botId, + }); + } catch (err) { + console.error("validate-telegram-bot fatal", err); + return jsonResponse( + { error: err instanceof Error ? err.message : "Erro inesperado" }, + 500, + ); + } +}); From af4a6968a85dcd9a3ade697364639fec73da4fad Mon Sep 17 00:00:00 2001 From: "gpt-engineer-app[bot]" <159125892+gpt-engineer-app[bot]@users.noreply.github.com> Date: Fri, 17 Apr 2026 19:57:58 +0000 Subject: [PATCH 2/2] Changes Co-authored-by: domfelipe <53182096+domfelipe@users.noreply.github.com> --- src/integrations/supabase/types.ts | 17 ++++++ ...5_b341c835-f177-4c9f-a114-641c72aa6022.sql | 52 +++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 supabase/migrations/20260417195755_b341c835-f177-4c9f-a114-641c72aa6022.sql diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index 5766cc8..14c8b74 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -632,6 +632,23 @@ export type Database = { Args: { check_env?: string; user_uuid: string } Returns: boolean } + vault_create_secret: { + Args: { + secret_description?: string + secret_name: string + secret_value: string + } + Returns: { + secret_id: string + }[] + } + vault_decrypt_secret: { + Args: { secret_id: string } + Returns: { + decrypted_secret: string + }[] + } + vault_delete_secret: { Args: { secret_id: string }; Returns: undefined } } Enums: { [_ in never]: never diff --git a/supabase/migrations/20260417195755_b341c835-f177-4c9f-a114-641c72aa6022.sql b/supabase/migrations/20260417195755_b341c835-f177-4c9f-a114-641c72aa6022.sql new file mode 100644 index 0000000..bf62da8 --- /dev/null +++ b/supabase/migrations/20260417195755_b341c835-f177-4c9f-a114-641c72aa6022.sql @@ -0,0 +1,52 @@ +-- Wrappers para o Supabase Vault, executáveis apenas pelo service_role +-- (usados pelas Edge Functions Telegram para guardar tokens com criptografia). + +CREATE OR REPLACE FUNCTION public.vault_create_secret( + secret_value text, + secret_name text, + secret_description text DEFAULT '' +) +RETURNS TABLE(secret_id uuid) +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = public, vault +AS $$ +BEGIN + RETURN QUERY + SELECT vault.create_secret(secret_value, secret_name, secret_description); +END; +$$; + +REVOKE ALL ON FUNCTION public.vault_create_secret(text, text, text) FROM PUBLIC, anon, authenticated; +GRANT EXECUTE ON FUNCTION public.vault_create_secret(text, text, text) TO service_role; + +CREATE OR REPLACE FUNCTION public.vault_decrypt_secret(secret_id uuid) +RETURNS TABLE(decrypted_secret text) +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = public, vault +AS $$ +BEGIN + RETURN QUERY + SELECT ds.decrypted_secret + FROM vault.decrypted_secrets ds + WHERE ds.id = secret_id; +END; +$$; + +REVOKE ALL ON FUNCTION public.vault_decrypt_secret(uuid) FROM PUBLIC, anon, authenticated; +GRANT EXECUTE ON FUNCTION public.vault_decrypt_secret(uuid) TO service_role; + +CREATE OR REPLACE FUNCTION public.vault_delete_secret(secret_id uuid) +RETURNS void +LANGUAGE plpgsql +SECURITY DEFINER +SET search_path = public, vault +AS $$ +BEGIN + DELETE FROM vault.secrets WHERE id = secret_id; +END; +$$; + +REVOKE ALL ON FUNCTION public.vault_delete_secret(uuid) FROM PUBLIC, anon, authenticated; +GRANT EXECUTE ON FUNCTION public.vault_delete_secret(uuid) TO service_role; \ No newline at end of file