import crypto from "crypto" import type { ESignAdapter, ESignTokens, SendParams } from "./types" // Dropbox Sign (formerly HelloSign). Per-landlord API-key auth — each landlord // pastes their own API key (no platform app credentials needed). Docs: // https://developers.hellosign.com/api/reference/ const BASE = "https://api.hellosign.com/v3" const TEST_MODE = process.env.DROPBOX_SIGN_TEST_MODE === "true" ? "1" : "0" function authFor(apiKey: string) { return "Basic " + Buffer.from(`${apiKey}:`).toString("base64") } /** Verify the `event_hash` (hex HMAC-SHA256 of event_time+event_type, key = API key). */ function verifyEventHash(apiKey: string, ev?: { event_type?: string; event_time?: string; event_hash?: string }): boolean { if (!apiKey || !ev?.event_type || !ev?.event_time || !ev?.event_hash) return false const expected = crypto.createHmac("sha256", apiKey).update(ev.event_time + ev.event_type).digest("hex") const a = Buffer.from(ev.event_hash) const b = Buffer.from(expected) return a.length === b.length && crypto.timingSafeEqual(a, b) } type DbxEvent = { event?: { event_type?: string; event_time?: string; event_hash?: string } signature_request?: { signature_request_id?: string } } function parseBody(body: string): DbxEvent | null { try { // Dropbox Sign posts multipart form-data with a `json` field (or raw JSON). const m = body.match(/name="json"\r?\n\r?\n([\s\S]*?)\r?\n--/) ?? body.match(/^(\{[\s\S]*\})\s*$/) return JSON.parse(m ? m[1] : body) } catch { return null } } export const dropboxSign: ESignAdapter = { id: "dropbox_sign", label: "Dropbox Sign", kind: "apikey", available: () => true, // landlord brings their own key; no operator setup required getAuthUrl(): string { throw new Error("Dropbox Sign connects with an API key, not OAuth") }, async exchangeCode(): Promise { throw new Error("Dropbox Sign connects with an API key, not OAuth") }, async refresh(): Promise { throw new Error("Dropbox Sign API keys don't expire") }, async connectApiKey(apiKey): Promise { const key = apiKey.trim() if (!key) throw new Error("Enter your Dropbox Sign API key") const res = await fetch(`${BASE}/account`, { headers: { Authorization: authFor(key) } }) if (res.status === 401 || res.status === 403) throw new Error("That API key was rejected by Dropbox Sign") if (!res.ok) throw new Error(`Dropbox Sign ${res.status}: could not validate the API key`) const j = (await res.json()) as { account?: { email_address?: string } } return { accessToken: key, refreshToken: null, expiresAt: null, accountId: null, baseUri: null, accountName: j.account?.email_address ?? "Dropbox Sign account", } }, async send(creds, p: SendParams) { const fd = new FormData() fd.append("subject", p.subject) fd.append("message", p.message) fd.append("test_mode", TEST_MODE) fd.append("signers[0][email_address]", p.signerEmail) fd.append("signers[0][name]", p.signerName) fd.append("file[0]", new Blob([new Uint8Array(p.document)], { type: "application/pdf" }), p.documentName) const res = await fetch(`${BASE}/signature_request/send`, { method: "POST", headers: { Authorization: authFor(creds.accessToken) }, body: fd }) if (!res.ok) throw new Error(`Dropbox Sign ${res.status}: ${(await res.text()).slice(0, 300)}`) const j = (await res.json()) as { signature_request?: { signature_request_id?: string } } const id = j.signature_request?.signature_request_id if (!id) throw new Error("Dropbox Sign did not return a signature_request_id") return { externalId: id } }, peekExternalId(body): string | null { return parseBody(body)?.signature_request?.signature_request_id ?? null }, async verifyAndGetStatus(creds, _externalId, body): Promise<"signed" | "declined" | "voided" | null> { const ev = parseBody(body) if (!ev || !verifyEventHash(creds.accessToken, ev.event)) return null switch (ev.event?.event_type) { case "signature_request_all_signed": return "signed" case "signature_request_declined": return "declined" case "signature_request_canceled": return "voided" default: return null } }, async getSignedDocument(creds, externalId): Promise { const res = await fetch(`${BASE}/signature_request/files/${externalId}?file_type=pdf`, { headers: { Authorization: authFor(creds.accessToken) }, }) if (!res.ok) return null return Buffer.from(await res.arrayBuffer()) }, }