// 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-lint-ignore no-explicit-any async function deleteVaultSecret( admin: any, secretId: string, context: string, ): Promise { try { const { error } = await admin.rpc("vault_delete_secret", { secret_id: secretId, }); if (error) { console.warn(`vault_delete_secret ignored (${context}):`, error.message); } } catch (e) { console.warn( `vault_delete_secret ignored (${context}):`, e instanceof Error ? e.message : "unknown", ); } } 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") { return jsonResponse( { error: "Agente suspenso. Regularize sua assinatura antes de conectar o Telegram." }, 403, ); } // status='error' por falta de token anterior é recuperável — apenas seguimos. // 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, ); } const oldSecretId = agent.telegram_bot_token_vault_id as string | null; // 5) Cria secret novo no Vault antes de remover o antigo. Assim uma falha // de Vault não quebra um bot que já estava conectado. 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, ); } // 6) Atualiza agent_instances — reseta para 'provisioning' se estava em erro, // garante que o provision-agent re-execute do zero. const nextStatus = agent.status === "error" ? "provisioning" : agent.status; 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, status: nextStatus, updated_at: new Date().toISOString(), }) .eq("id", agent.id); if (updErr) { console.error("agent update error", updErr); await deleteVaultSecret( admin, secretId, "new secret after agent update failure", ); return jsonResponse({ error: "Falha ao salvar dados do bot." }, 500); } if (oldSecretId && oldSecretId !== secretId) { await deleteVaultSecret( admin, oldSecretId, "old telegram token after reconnect", ); } // 7) Auto-provisionamento: dispara provision-agent assincronamente para que // o container seja criado/atualizado no Railway. Fire-and-forget — o wizard // não bloqueia esperando o deploy completar. if (nextStatus === "provisioning" || agent.status === "active") { try { const provisionUrl = `${supabaseUrl}/functions/v1/provision-agent`; console.log(`auto-provision: disparando para agent ${agent.id}`); const provisionBody: Record = { agent_instance_id: agent.id, }; if (agent.status === "active") { provisionBody.mode = "telegram_reconnect"; } fetch(provisionUrl, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Bearer ${serviceKey}`, "X-Internal-Secret": Deno.env.get("INTERNAL_FUNCTION_SECRET") ?? "", }, body: JSON.stringify(provisionBody), }).catch((err) => console.error("auto-provision fetch error:", err)); } catch (err) { console.error("auto-provision trigger error:", err); } } 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, ); } });