Deploy on DigitalOcean App Platform (GitHub-source build) + consolidate audit-fixes

Deploy config:
- .do/app.yaml: build the Dockerfile directly from GitHub (deploy_on_push) instead
  of a pre-built DOCR image; NEXT_PUBLIC_* set RUN_AND_BUILD_TIME with the
  propertymanagement.network domain so they bake into the client bundle; add
  custom domains block (apex + www); wire Sentry DSN (server + browser).

Included pending work from the audit-fixes branch:
- AI provider abstraction (OpenAI/Anthropic, admin-selectable; Anthropic default)
- Per-landlord e-signature (DocuSign OAuth + Dropbox Sign) + migration 0010
- Outbound webhooks / Zapier integration
- PayPal removal (Stripe-only billing)
- Storage hardening (fail-loud when Spaces unconfigured), security fixes

Verified: full production Docker build (same build-args as DO) passes clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Leon Serfaty
2026-07-03 04:45:24 -04:00
co-authored by Claude Opus 4.8
parent 917a06ee85
commit 5495b94924
86 changed files with 7647 additions and 1182 deletions
+73 -30
View File
@@ -1,42 +1,48 @@
import { and, desc, eq } from "drizzle-orm"
import { db } from "@/lib/db"
import { leases, signature_requests } from "@/lib/db/schema"
import { readFile } from "@/lib/storage"
import { docusign } from "./docusign"
import { dropboxSign } from "./dropbox-sign"
import type { ESignAdapter, ESignProvider } from "./types"
import { readFile, saveBuffer } from "@/lib/storage"
import { getAdapter } from "./registry"
import { resolveEsignCreds, markEsignError } from "./credentials"
import type { ESignProvider } from "./types"
export type { ESignProvider } from "./types"
export { getAdapter, listEsignAdapters } from "./registry"
export {
resolveEsignCreds,
listEsignConnections,
getEsignConnection,
saveEsignConnection,
disconnectEsign,
} from "./credentials"
const ADAPTERS: Record<ESignProvider, ESignAdapter> = { docusign, dropbox_sign: dropboxSign }
const FILES_PREFIX = "/api/files/"
export function getAdapter(id: string): ESignAdapter | null {
return id === "docusign" || id === "dropbox_sign" ? ADAPTERS[id] : null
}
export function listAdapters() {
return (Object.keys(ADAPTERS) as ESignProvider[]).map((id) => ({ id, label: ADAPTERS[id].label, configured: ADAPTERS[id].configured() }))
}
export function anyEsignConfigured(): boolean {
return listAdapters().some((a) => a.configured)
/** The webhook URL a provider should call back — used for DocuSign envelope-level Connect. */
function webhookUrl(provider: ESignProvider): string {
const base = (process.env.NEXT_PUBLIC_APP_URL ?? "http://localhost:3000").replace(/\/+$/, "")
return `${base}/api/esign/${provider}/webhook`
}
/** Read the lease's stored document. Only /api/files keys are allowed (no SSRF). */
async function getDocumentBytes(documentUrl: string): Promise<{ bytes: Buffer; name: string }> {
const prefix = "/api/files/"
if (documentUrl.startsWith(prefix)) {
const key = documentUrl.slice(prefix.length)
return { bytes: await readFile(key), name: key.split("/").pop() ?? "lease.pdf" }
if (!documentUrl.startsWith(FILES_PREFIX)) {
throw new Error("Lease document must be an uploaded file")
}
const res = await fetch(documentUrl)
if (!res.ok) throw new Error("Could not fetch the lease document")
return { bytes: Buffer.from(await res.arrayBuffer()), name: documentUrl.split("/").pop()?.split("?")[0] ?? "lease.pdf" }
const key = documentUrl.slice(FILES_PREFIX.length)
return { bytes: await readFile(key), name: key.split("/").pop() ?? "lease.pdf" }
}
/**
* Send a lease for signature through the owner's OWN connected account.
* Requires the provider to be connected (per-user OAuth / API key).
*/
export async function sendLeaseForSignature(ownerId: string, leaseId: string, provider: ESignProvider) {
const adapter = getAdapter(provider)
if (!adapter) throw new Error("Unknown provider")
if (!adapter.configured()) throw new Error(`${adapter.label} is not configured`)
const creds = await resolveEsignCreds(ownerId, provider)
if (!creds) throw new Error(`Connect your ${adapter.label} account in Settings → Integrations first`)
const lease = await db.query.leases.findFirst({
where: and(eq(leases.id, leaseId), eq(leases.user_id, ownerId)),
@@ -51,13 +57,14 @@ export async function sendLeaseForSignature(ownerId: string, leaseId: string, pr
const { bytes, name } = await getDocumentBytes(lease.document_url)
try {
const { externalId } = await adapter.send({
const { externalId } = await adapter.send(creds, {
document: bytes,
documentName: name,
signerEmail: email,
signerName,
subject: "Please sign your lease agreement",
message: "Your landlord has sent your lease agreement for electronic signature.",
webhookUrl: webhookUrl(provider),
})
const [row] = await db
.insert(signature_requests)
@@ -66,6 +73,7 @@ export async function sendLeaseForSignature(ownerId: string, leaseId: string, pr
return row
} catch (e) {
const msg = (e as Error).message.slice(0, 500)
await markEsignError(ownerId, provider, msg)
await db
.insert(signature_requests)
.values({ user_id: ownerId, lease_id: leaseId, provider, status: "error", signer_email: email, signer_name: signerName, document_name: name, last_error: msg })
@@ -80,18 +88,53 @@ export async function listRequestsForLease(ownerId: string, leaseId: string) {
})
}
/** Update a request's status from an inbound provider webhook. */
/**
* Process an inbound provider webhook. The body is UNTRUSTED: we use it only to
* find which signature request (and therefore which owner + credentials) it
* concerns, then authenticate the event via the adapter (DocuSign pull-verify /
* Dropbox HMAC) before updating status and archiving the signed document.
*/
export async function handleEsignWebhook(provider: string, body: string, headers: Headers) {
const adapter = getAdapter(provider)
if (!adapter) return
const result = adapter.parseWebhook(body, headers)
if (!result) return
const externalId = adapter.peekExternalId(body)
if (!externalId) return
const reqRow = await db.query.signature_requests.findFirst({
where: eq(signature_requests.external_id, externalId),
columns: { id: true, user_id: true, status: true },
})
if (!reqRow) return
const creds = await resolveEsignCreds(reqRow.user_id, provider as ESignProvider)
if (!creds) return
const status = await adapter.verifyAndGetStatus(creds, externalId, body, headers)
if (!status) return
await db
.update(signature_requests)
.set({
status: result.status,
completed_at: result.status === "signed" ? new Date().toISOString() : null,
status,
completed_at: status === "signed" ? new Date().toISOString() : null,
updated_at: new Date().toISOString(),
})
.where(eq(signature_requests.external_id, result.externalId))
.where(eq(signature_requests.id, reqRow.id))
// Archive the executed document so the landlord can download the signed copy.
if (status === "signed") {
try {
const bytes = await adapter.getSignedDocument(creds, externalId)
if (bytes && bytes.length) {
const { key } = await saveBuffer(bytes, { userId: reqRow.user_id, scope: "esign", ext: "pdf" })
await db
.update(signature_requests)
.set({ signed_document_url: `${FILES_PREFIX}${key}` })
.where(eq(signature_requests.id, reqRow.id))
}
} catch {
// Best-effort — status is already recorded.
}
}
}