Consolidate audit-fixes branch: webhooks, integrations, and deploy hardening
Batch commit of the pending working tree on security/audit-fixes-2026-07. Major areas: - Outbound webhooks / Zapier: schema + signed delivery with retries, public v1 API (REST-hook subscribe/unsubscribe), settings UI, cron drain. - Deploy hardening: email via SMTP2GO (Resend fully removed), verified DB TLS (DATABASE_SSL=require + DATABASE_CA), storage fails loud in production when Spaces is unconfigured instead of silently using ephemeral disk. - Integrations & features (concurrent work): accounting (QuickBooks/Xero), e-signature (DocuSign/Dropbox Sign), PayPal, geocoding/maps, onboarding, expanded legal pages. - DB migrations 0006–0009. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
969d5d4c8a
commit
c9968531e4
@@ -0,0 +1,65 @@
|
||||
import type { ESignAdapter, SendParams, WebhookResult } from "./types"
|
||||
|
||||
// DocuSign eSignature REST API. Uses a pre-obtained access token (via JWT grant
|
||||
// or OAuth) — set DOCUSIGN_ACCESS_TOKEN / DOCUSIGN_ACCOUNT_ID / DOCUSIGN_BASE_URI.
|
||||
// Docs: https://developers.docusign.com/docs/esign-rest-api/reference/envelopes/envelopes/create/
|
||||
const ACCESS_TOKEN = process.env.DOCUSIGN_ACCESS_TOKEN ?? ""
|
||||
const ACCOUNT_ID = process.env.DOCUSIGN_ACCOUNT_ID ?? ""
|
||||
const BASE_URI = (process.env.DOCUSIGN_BASE_URI ?? "https://demo.docusign.net").replace(/\/+$/, "")
|
||||
|
||||
function extOf(name: string): string {
|
||||
const e = name.split(".").pop()?.toLowerCase()
|
||||
return e && /^(pdf|docx?|png|jpe?g)$/.test(e) ? e : "pdf"
|
||||
}
|
||||
|
||||
export const docusign: ESignAdapter = {
|
||||
id: "docusign",
|
||||
label: "DocuSign",
|
||||
configured: () => Boolean(ACCESS_TOKEN && ACCOUNT_ID),
|
||||
|
||||
async send({ document, documentName, signerEmail, signerName, subject }: SendParams) {
|
||||
const envelope = {
|
||||
emailSubject: subject,
|
||||
status: "sent",
|
||||
documents: [{ documentBase64: document.toString("base64"), name: documentName, fileExtension: extOf(documentName), documentId: "1" }],
|
||||
recipients: {
|
||||
signers: [
|
||||
{
|
||||
email: signerEmail,
|
||||
name: signerName,
|
||||
recipientId: "1",
|
||||
routingOrder: "1",
|
||||
// Default sign placement (bottom of page 1). Use a template/anchor
|
||||
// string for precise field placement in production.
|
||||
tabs: { signHereTabs: [{ documentId: "1", pageNumber: "1", xPosition: "100", yPosition: "650" }] },
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
const res = await fetch(`${BASE_URI}/restapi/v2.1/accounts/${ACCOUNT_ID}/envelopes`, {
|
||||
method: "POST",
|
||||
headers: { Authorization: `Bearer ${ACCESS_TOKEN}`, "Content-Type": "application/json" },
|
||||
body: JSON.stringify(envelope),
|
||||
})
|
||||
if (!res.ok) throw new Error(`DocuSign ${res.status}: ${(await res.text()).slice(0, 300)}`)
|
||||
const j = (await res.json()) as { envelopeId?: string }
|
||||
if (!j.envelopeId) throw new Error("DocuSign did not return an envelopeId")
|
||||
return { externalId: j.envelopeId }
|
||||
},
|
||||
|
||||
parseWebhook(body): WebhookResult | null {
|
||||
// DocuSign Connect (JSON format) payload.
|
||||
try {
|
||||
const j = JSON.parse(body) as { event?: string; data?: { envelopeId?: string; envelopeSummary?: { status?: string } } }
|
||||
const externalId = j.data?.envelopeId
|
||||
const status = (j.data?.envelopeSummary?.status ?? j.event ?? "").toLowerCase()
|
||||
if (!externalId) return null
|
||||
if (status.includes("completed") || status.includes("signed")) return { externalId, status: "signed" }
|
||||
if (status.includes("declined")) return { externalId, status: "declined" }
|
||||
if (status.includes("voided")) return { externalId, status: "voided" }
|
||||
return null
|
||||
} catch {
|
||||
return null
|
||||
}
|
||||
},
|
||||
}
|
||||
Reference in New Issue
Block a user