/* |-------------------------------------------------------------------------- | Agência PSI — Edge Function: evolution-webhook-provision |-------------------------------------------------------------------------- | Configura automaticamente o webhook MESSAGES_UPSERT numa instância da | Evolution API pra apontar pra edge function `evolution-whatsapp-inbound`. | | Body JSON: | { | "tenant_id": "", | "api_url": "http://localhost:8080", | "api_key": "", | "instance_name": "", | "public_url": "https://seu-projeto.supabase.co" (opcional) | } | | Usa SUPABASE_URL como fallback do public_url. |-------------------------------------------------------------------------- */ import { createClient } from 'https://esm.sh/@supabase/supabase-js@2' const corsHeaders = { 'Access-Control-Allow-Origin': '*', 'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type', 'Access-Control-Allow-Methods': 'POST, OPTIONS', } function json(body: unknown, status = 200) { return new Response(JSON.stringify(body), { status, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }) } // Edge function roda em container — localhost aponta pra ele mesmo, não pro host. // Substitui localhost/127.0.0.1 por host.docker.internal pra alcançar o host. function rewriteForContainer(apiUrl: string): string { try { const u = new URL(apiUrl) if (u.hostname === 'localhost' || u.hostname === '127.0.0.1') { u.hostname = 'host.docker.internal' return u.toString().replace(/\/+$/, '') } return apiUrl.replace(/\/+$/, '') } catch { return apiUrl } } Deno.serve(async (req: Request) => { if (req.method === 'OPTIONS') return new Response('ok', { headers: corsHeaders }) if (req.method !== 'POST') return json({ ok: false, error: 'method' }, 405) try { const body = await req.json().catch(() => null) if (!body || typeof body !== 'object') return json({ ok: false, error: 'body invalido' }, 400) const { tenant_id, api_url, api_key, instance_name, public_url } = body as Record if (!tenant_id || !api_url || !api_key || !instance_name) { return json({ ok: false, error: 'faltam campos obrigatorios (tenant_id, api_url, api_key, instance_name)' }, 400) } // Verifica caller: precisa ser member do tenant ou saas_admin const auth = req.headers.get('Authorization') ?? '' const supaAuthed = createClient( Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_ANON_KEY')!, { global: { headers: { Authorization: auth } } } ) const { data: authData, error: authErr } = await supaAuthed.auth.getUser() if (authErr || !authData?.user) return json({ ok: false, error: 'auth' }, 401) const supaSvc = createClient( Deno.env.get('SUPABASE_URL')!, Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')! ) const userId = authData.user.id // check membro ativo ou saas_admin const { data: isAdmin } = await supaSvc.rpc('is_saas_admin') if (!isAdmin) { const { data: membership } = await supaSvc .from('tenant_members') .select('id') .eq('tenant_id', tenant_id) .eq('user_id', userId) .eq('status', 'active') .maybeSingle() if (!membership) return json({ ok: false, error: 'forbidden' }, 403) } // Monta a URL publica do webhook const basePublic = public_url || Deno.env.get('SUPABASE_URL') if (!basePublic) return json({ ok: false, error: 'public_url nao definido' }, 400) const webhookUrl = `${basePublic.replace(/\/+$/, '')}/functions/v1/evolution-whatsapp-inbound?tenant_id=${encodeURIComponent(tenant_id)}` // Configura via Evolution API (rewrite localhost → host.docker.internal pra container) const evoEndpoint = `${rewriteForContainer(api_url)}/webhook/set/${encodeURIComponent(instance_name)}` // Evolution v2 payload (wrapped em "webhook") const evoBodyV2 = { webhook: { enabled: true, url: webhookUrl, byEvents: false, base64: false, events: ['MESSAGES_UPSERT', 'MESSAGES_UPDATE'] } } // Evolution v1 payload (flat) const evoBodyV1 = { enabled: true, url: webhookUrl, webhook_by_events: false, events: ['MESSAGES_UPSERT', 'MESSAGES_UPDATE'] } async function tryPost(bodyObj: Record) { const r = await fetch(evoEndpoint, { method: 'POST', headers: { 'Content-Type': 'application/json', 'apikey': api_key }, body: JSON.stringify(bodyObj) }) const t = await r.text() let j: unknown = t try { j = JSON.parse(t) } catch { /* keep text */ } return { resp: r, json: j } } // Tenta v2 primeiro (user tem Evolution v2 pela resposta do state) let attempt = await tryPost(evoBodyV2) let payloadFormatUsed = 'v2' if (!attempt.resp.ok) { // Fallback v1 const v1Attempt = await tryPost(evoBodyV1) if (v1Attempt.resp.ok) { attempt = v1Attempt payloadFormatUsed = 'v1' } else { // Retorna o erro da segunda tentativa return json({ ok: false, error: `Evolution retornou ${attempt.resp.status} (v2) e ${v1Attempt.resp.status} (v1)`, evolution_response_v2: attempt.json, evolution_response_v1: v1Attempt.json, webhook_url: webhookUrl, evolution_endpoint: evoEndpoint }, 502) } } const respJson = attempt.json return json({ ok: true, webhook_url: webhookUrl, payload_format: payloadFormatUsed, evolution_response: respJson }) } catch (err) { return json({ ok: false, error: String(err) }, 500) } })