// Avria Sign — stamp signed PDF (using placed fields when available) and append validation certificate import { createClient } from "https://esm.sh/@supabase/supabase-js@2.45.0"; import { PDFDocument, StandardFonts, rgb } from "https://esm.sh/pdf-lib@1.17.1"; const corsHeaders = { "Access-Control-Allow-Origin": "*", "Access-Control-Allow-Headers": "authorization, x-client-info, apikey, content-type", }; 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 }); const admin = createClient(Deno.env.get("SUPABASE_URL")!, Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!); try { const { envelope_id } = await req.json(); if (!envelope_id) return json({ error: "missing envelope_id" }, 400); const { data: env } = await admin .from("signature_envelopes").select("*").eq("id", envelope_id).maybeSingle(); if (!env) return json({ error: "envelope not found" }, 404); const { data: recipients } = await admin .from("signature_recipients").select("*").eq("envelope_id", envelope_id).order("signing_order"); const { data: allFields } = await admin .from("signature_fields").select("*").eq("envelope_id", envelope_id); const pdfRes = await fetch(env.document_url); if (!pdfRes.ok) throw new Error("Could not fetch original document"); const pdfBytes = new Uint8Array(await pdfRes.arrayBuffer()); let pdfDoc: PDFDocument; try { pdfDoc = await PDFDocument.load(pdfBytes, { ignoreEncryption: true }); } catch { pdfDoc = await PDFDocument.create(); const p = pdfDoc.addPage(); p.drawText("Original document could not be embedded.", { x: 50, y: 700, size: 12 }); } const helv = await pdfDoc.embedFont(StandardFonts.Helvetica); const helvBold = await pdfDoc.embedFont(StandardFonts.HelveticaBold); const pages = pdfDoc.getPages(); // Cache embedded signature images per recipient const sigImageCache = new Map(); for (const r of recipients || []) { if (!r.signature_data_url) continue; try { const isPng = r.signature_data_url.includes("image/png"); const base64 = r.signature_data_url.split(",")[1] || ""; const sigBytes = Uint8Array.from(atob(base64), c => c.charCodeAt(0)); const img = isPng ? await pdfDoc.embedPng(sigBytes) : await pdfDoc.embedJpg(sigBytes); sigImageCache.set(r.id, img); } catch (e) { console.warn("sig embed failed:", r.id, e); } } const fieldsByRecipient = new Map(); (allFields || []).forEach(f => { const arr = fieldsByRecipient.get(f.recipient_id) || []; arr.push(f); fieldsByRecipient.set(f.recipient_id, arr); }); const hasPlacedFields = (allFields || []).length > 0; if (hasPlacedFields) { // Stamp at each placed field location for (const r of recipients || []) { const fields = fieldsByRecipient.get(r.id) || []; const sigImg = sigImageCache.get(r.id); const signedDate = r.signed_at ? new Date(r.signed_at) : new Date(); const dateStr = signedDate.toLocaleDateString("en-US", { timeZone: "America/New_York" }); for (const f of fields) { const pageIdx = Math.min((f.page_number || 1) - 1, pages.length - 1); const page = pages[pageIdx]; const { width: pw, height: ph } = page.getSize(); // UI uses top-left origin; pdf-lib uses bottom-left const x = f.x_ratio * pw; const y = ph - (f.y_ratio + f.height_ratio) * ph; const w = f.width_ratio * pw; const h = f.height_ratio * ph; if (f.field_type === "signature" && sigImg) { // Fit signature into box preserving aspect ratio const ratio = Math.min(w / sigImg.width, h / sigImg.height); const drawW = sigImg.width * ratio; const drawH = sigImg.height * ratio; page.drawImage(sigImg, { x: x + (w - drawW) / 2, y: y + (h - drawH) / 2, width: drawW, height: drawH, }); } else if (f.field_type === "date") { page.drawText(dateStr, { x: x + 4, y: y + h / 2 - 4, size: Math.min(11, h * 0.7), font: helv, color: rgb(0, 0, 0) }); } else if (f.field_type === "name") { page.drawText(r.name, { x: x + 4, y: y + h / 2 - 4, size: Math.min(11, h * 0.7), font: helvBold, color: rgb(0, 0, 0) }); } } } } else { // Fallback: stamp on bottom of last page const lastPage = pages[pages.length - 1]; let stampY = 80; for (const r of recipients || []) { const sigImg = sigImageCache.get(r.id); if (!sigImg) continue; const sigDims = sigImg.scale(120 / sigImg.width); lastPage.drawImage(sigImg, { x: 50, y: stampY, width: sigDims.width, height: sigDims.height }); lastPage.drawText(`${r.name}`, { x: 180, y: stampY + 25, size: 9, font: helvBold, color: rgb(0, 0, 0) }); lastPage.drawText(`Signed: ${new Date(r.signed_at).toLocaleString("en-US", { timeZone: "America/New_York" })} EST`, { x: 180, y: stampY + 12, size: 8, font: helv, color: rgb(0.3, 0.3, 0.3), }); lastPage.drawText(`ID: ${r.id.slice(0, 8).toUpperCase()}`, { x: 180, y: stampY, size: 7, font: helv, color: rgb(0.5, 0.5, 0.5), }); stampY += 60; if (stampY > 250) break; } } // Append Certificate of Validation page const cert = pdfDoc.addPage(); const { width, height } = cert.getSize(); cert.drawRectangle({ x: 30, y: 30, width: width - 60, height: height - 60, borderColor: rgb(0, 0, 0), borderWidth: 2 }); cert.drawRectangle({ x: 40, y: 40, width: width - 80, height: height - 80, borderColor: rgb(0, 0, 0), borderWidth: 0.5 }); cert.drawText("CERTIFICATE OF VALIDATION", { x: width / 2 - 165, y: height - 110, size: 22, font: helvBold }); cert.drawText("Cryptographic proof of document signing and integrity.", { x: width / 2 - 175, y: height - 135, size: 10, font: helv, color: rgb(0.4, 0.4, 0.4), }); let y = height - 200; const drawRow = (label: string, value: string) => { cert.drawText(label, { x: 70, y, size: 11, font: helvBold }); cert.drawText(value, { x: 230, y, size: 11, font: helv }); cert.drawLine({ start: { x: 70, y: y - 5 }, end: { x: width - 70, y: y - 5 }, thickness: 0.3, color: rgb(0.7, 0.7, 0.7) }); y -= 30; }; drawRow("Document Name:", env.document_name); drawRow("Envelope ID:", env.id); drawRow("Issued By:", "Avria Community Management"); drawRow("Completed:", new Date(env.completed_at || Date.now()).toLocaleString("en-US", { timeZone: "America/New_York" }) + " EST"); drawRow("Status:", "COMPLETED"); y -= 10; cert.drawText("SIGNERS", { x: 70, y, size: 12, font: helvBold }); y -= 20; for (const r of recipients || []) { cert.drawText(`• ${r.name} <${r.email}>`, { x: 80, y, size: 10, font: helv }); y -= 14; if (r.signed_at) { cert.drawText( ` Signed ${new Date(r.signed_at).toLocaleString("en-US", { timeZone: "America/New_York" })} EST` + (r.signed_ip ? ` from ${r.signed_ip}` : "") + ` (${r.signature_method || "draw"})`, { x: 80, y, size: 8, font: helv, color: rgb(0.4, 0.4, 0.4) } ); y -= 12; } cert.drawText(` Signature ID: ${r.id}`, { x: 80, y, size: 7, font: helv, color: rgb(0.5, 0.5, 0.5) }); y -= 16; } cert.drawCircle({ x: width - 110, y: 110, size: 45, borderColor: rgb(0.2, 0.4, 0.7), borderWidth: 3, opacity: 0.6 }); cert.drawText("OFFICIAL", { x: width - 130, y: 110, size: 9, font: helvBold, color: rgb(0.2, 0.4, 0.7) }); cert.drawText("SEAL", { x: width - 122, y: 98, size: 9, font: helvBold, color: rgb(0.2, 0.4, 0.7) }); const finalBytes = await pdfDoc.save(); const path = `signature-envelopes/${envelope_id}/signed.pdf`; const { error: upErr } = await admin.storage.from("files").upload(path, finalBytes, { contentType: "application/pdf", upsert: true, }); if (upErr) throw upErr; const { data: pub } = admin.storage.from("files").getPublicUrl(path); await admin.from("signature_envelopes").update({ signed_document_url: pub.publicUrl }).eq("id", envelope_id); // Notify all signers + creator const { data: envFull } = await admin.from("signature_envelopes").select("created_by, document_name").eq("id", envelope_id).single(); const recipientEmails = (recipients || []).map(r => r.email); let creatorEmail: string | null = null; if (envFull?.created_by) { const { data: creator } = await admin.auth.admin.getUserById(envFull.created_by); creatorEmail = creator?.user?.email || null; } const allEmails = Array.from(new Set([...recipientEmails, ...(creatorEmail ? [creatorEmail] : [])])); 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; for (const email of allEmails) { try { if (smtpSender) { const html = `

Document Signed

The document ${envFull?.document_name || "Document"} has been signed by all parties.

Download Signed Copy


Avria Sign — Secure Electronic Signatures

`; await admin.functions.invoke("send-smtp-email", { body: { sender: smtpSender, recipient: email, subject: `Signed: ${envFull?.document_name || "Document"}`, html, body: html, fallback_user_id: envFull?.created_by || null, feature_type: "signature_completed", }, }); } } catch (e) { console.warn("Completion email failed for", email, e); } } return json({ ok: true, signed_document_url: pub.publicUrl }); } catch (err: any) { console.error("avria-sign-stamp error:", err); return json({ error: err.message || String(err) }, 500); } }); function json(payload: unknown, status = 200) { return new Response(JSON.stringify(payload), { status, headers: { ...corsHeaders, "Content-Type": "application/json" }, }); }