import { and, eq } from "drizzle-orm" import { db } from "@/lib/db" import { esign_connections } from "@/lib/db/schema" import { encrypt, decrypt } from "@/lib/crypto" import { getAdapter } from "./registry" import type { ESignCredentials, ESignProvider, ESignTokens } from "./types" // Per-owner e-sign connection storage + credential resolution. Mirrors // lib/accounting/index.ts: tokens are AES-256-GCM encrypted at rest, decrypted // on demand, and DocuSign access tokens are transparently refreshed near expiry. /** Upsert an encrypted connection for (owner, provider). */ export async function saveEsignConnection(ownerId: string, provider: ESignProvider, tokens: ESignTokens) { const values = { user_id: ownerId, provider, access_token: encrypt(tokens.accessToken), refresh_token: tokens.refreshToken ? encrypt(tokens.refreshToken) : null, expires_at: tokens.expiresAt, account_id: tokens.accountId, base_uri: tokens.baseUri, account_name: tokens.accountName, status: "active" as const, last_error: null, } const existing = await db.query.esign_connections.findFirst({ where: and(eq(esign_connections.user_id, ownerId), eq(esign_connections.provider, provider)), columns: { id: true }, }) if (existing) { await db .update(esign_connections) .set({ ...values, updated_at: new Date().toISOString() }) .where(eq(esign_connections.id, existing.id)) } else { await db.insert(esign_connections).values(values) } } export async function getEsignConnection(ownerId: string, provider: ESignProvider) { return db.query.esign_connections.findFirst({ where: and(eq(esign_connections.user_id, ownerId), eq(esign_connections.provider, provider)), }) } /** Owner-facing list — never leaks tokens. */ export async function listEsignConnections(ownerId: string) { const rows = await db.query.esign_connections.findMany({ where: eq(esign_connections.user_id, ownerId) }) return rows.map((r) => ({ provider: r.provider, accountName: r.account_name, status: r.status, lastError: r.last_error, })) } export async function disconnectEsign(ownerId: string, provider: ESignProvider) { await db .delete(esign_connections) .where(and(eq(esign_connections.user_id, ownerId), eq(esign_connections.provider, provider))) } /** * Resolve ready-to-use credentials for a connected account, refreshing the * DocuSign access token first if it's near expiry. Returns null when the owner * hasn't connected this provider. */ export async function resolveEsignCreds(ownerId: string, provider: ESignProvider): Promise { const conn = await getEsignConnection(ownerId, provider) if (!conn || conn.status === "revoked") return null let accessToken = decrypt(conn.access_token) const refreshToken = conn.refresh_token ? decrypt(conn.refresh_token) : null let accountId = conn.account_id let baseUri = conn.base_uri const nearExpiry = conn.expires_at && new Date(conn.expires_at).getTime() - Date.now() < 5 * 60_000 if (nearExpiry && refreshToken) { const adapter = getAdapter(provider) if (adapter) { const next = await adapter.refresh(refreshToken) // Account id / base uri are stable across refresh — keep the stored ones. await saveEsignConnection(ownerId, provider, { ...next, accountId: conn.account_id, baseUri: conn.base_uri, accountName: conn.account_name, }) accessToken = next.accessToken accountId = conn.account_id baseUri = conn.base_uri } } return { provider, accessToken, refreshToken, accountId, baseUri } } /** Flag a connection as errored (e.g. after a failed send/refresh). */ export async function markEsignError(ownerId: string, provider: ESignProvider, message: string) { await db .update(esign_connections) .set({ status: "error", last_error: message.slice(0, 500), updated_at: new Date().toISOString() }) .where(and(eq(esign_connections.user_id, ownerId), eq(esign_connections.provider, provider))) }