// Avria Sign — send envelope (in-house e-signature) import { createClient } from "https://esm.sh/@supabase/supabase-js@2.45.0"; const corsHeaders = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type", }; interface Recipient { name: string; email: string } interface FieldPayload { recipient_index: number; field_type: "signature" | "date" | "name"; page_number: number; x_ratio: number; y_ratio: number; width_ratio: number; height_ratio: number; } function toSmtpSenderConfig(sender: any) { const port = Number(sender?.smtp_port ?? 587); const isImplicitSslPort = port === 465; const isStartTlsPort = port === 587; return { host: sender.smtp_host, port, username: sender.smtp_username, password: sender.smtp_password, use_ssl: isImplicitSslPort ? true : sender.use_ssl ?? false, use_tls: isImplicitSslPort ? false : sender.use_tls ?? isStartTlsPort, from: sender.sender_name ? `${sender.sender_name} <${sender.email_address}>` : sender.email_address, fromEmail: sender.email_address, fromName: sender.sender_name, envelopeFrom: sender.smtp_username || sender.email_address, signature_html: sender.signature_html || "", }; } 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 authHeader = req.headers.get("Authorization") || ""; const token = authHeader.replace("Bearer ", ""); const userClient = createClient(supabaseUrl, Deno.env.get("SUPABASE_ANON_KEY")!, { global: { headers: { Authorization: authHeader } }, }); const { data: userData } = await userClient.auth.getUser(token); if (!userData?.user) { return new Response(JSON.stringify({ error: "unauthorized" }), { status: 401, headers: { ...corsHeaders, "Content-Type": "application/json" } }); } const admin = createClient(supabaseUrl, serviceKey); const body = await req.json(); const { association_id, document_name, document_url, document_base64, file_extension = "pdf", recipients, email_subject, email_body, fields = [], }: { association_id?: string; document_name: string; document_url?: string; document_base64?: string; file_extension?: string; recipients: Recipient[]; email_subject?: string; email_body?: string; fields?: FieldPayload[]; } = body; if (!document_name || !recipients?.length) { return new Response(JSON.stringify({ error: "missing document_name or recipients" }), { status: 400, headers: { ...corsHeaders, "Content-Type": "application/json" } }); } // 1) Insert envelope const { data: env, error: envErr } = await admin.from("signature_envelopes").insert({ association_id: association_id || null, document_name, document_url: document_url || "", email_subject, email_body, status: "sent", created_by: userData.user.id, sent_at: new Date().toISOString(), }).select().single(); if (envErr) throw envErr; // 2) If raw upload, store let finalDocUrl = document_url || ""; if (document_base64) { const binary = Uint8Array.from(atob(document_base64), c => c.charCodeAt(0)); const path = `signature-envelopes/${env.id}/original.${file_extension}`; const { error: upErr } = await admin.storage.from("files").upload(path, binary, { contentType: file_extension === "pdf" ? "application/pdf" : "application/octet-stream", upsert: true, }); if (upErr) throw upErr; const { data: pub } = admin.storage.from("files").getPublicUrl(path); finalDocUrl = pub.publicUrl; await admin.from("signature_envelopes").update({ document_url: finalDocUrl }).eq("id", env.id); } // 3) Insert recipients const recipientRows = recipients.map((r, idx) => ({ envelope_id: env.id, name: r.name.trim(), email: r.email.trim().toLowerCase(), signing_order: idx + 1, status: "pending", })); const { data: insertedRecips, error: rErr } = await admin.from("signature_recipients").insert(recipientRows).select(); if (rErr) throw rErr; // 3b) Insert placed fields, mapping recipient_index -> inserted recipient id if (fields.length > 0 && insertedRecips) { const fieldRows = fields .filter(f => insertedRecips[f.recipient_index]) .map(f => ({ envelope_id: env.id, recipient_id: insertedRecips[f.recipient_index].id, field_type: f.field_type, page_number: f.page_number, x_ratio: f.x_ratio, y_ratio: f.y_ratio, width_ratio: f.width_ratio, height_ratio: f.height_ratio, })); if (fieldRows.length > 0) { const { error: fErr } = await admin.from("signature_fields").insert(fieldRows); if (fErr) console.warn("field insert error:", fErr); } } // 4) Audit await admin.from("signature_events").insert({ envelope_id: env.id, event_type: "sent", details: { recipient_count: recipients.length, document_name, field_count: fields.length }, }); // 5) Default sender const { data: defaultSender } = await admin .from("email_senders") .select("smtp_host, smtp_port, smtp_username, smtp_password, use_tls, use_ssl, email_address, sender_name, signature_html") .eq("is_active", true) .eq("is_default", true) .order("updated_at", { ascending: false }) .limit(1) .maybeSingle(); const smtpSender = defaultSender ? toSmtpSenderConfig(defaultSender) : null; // 6) Email + notifications const appOrigin = req.headers.get("origin") || "https://avria.cloud"; const subject = email_subject || `Please sign: ${document_name}`; for (const recip of insertedRecips || []) { const signUrl = `${appOrigin}/sign/${recip.signing_token}`; const html = `
Hi ${recip.name},
You have been requested to electronically sign the following document:
${document_name}
${email_body ? `${email_body}
` : ""}Or copy this link into your browser:
${signUrl}
Avria Sign — Secure Electronic Signatures