fix(audit-tier-1): timeouts, lifecycle, per-port Documenso, FK constraints
Closes the second wave of HIGH-priority audit findings: * fetchWithTimeout helper (new src/lib/fetch-with-timeout.ts) wraps Documenso, OCR, currency, Umami, IMAP, etc. — a hung upstream can no longer pin a worker concurrency slot indefinitely. OpenAI client passes timeout: 30_000. ImapFlow gets socket / greeting / connection timeouts. * SIGTERM / SIGINT handler in src/server.ts drains in-flight HTTP, closes Socket.io, and disconnects Redis before exit; compose stop_grace_period bumped to 30s. Adds closeSocketServer() helper. * env.ts gains zod-validated PORT and MULTI_NODE_DEPLOYMENT, and filesystem.ts now reads from env (a typo can no longer silently disable the multi-node guard). * Per-port Documenso template + recipient IDs land in system_settings with env fallback (PortDocumensoConfig now exposes eoiTemplateId, clientRecipientId, developerRecipientId, approvalRecipientId). document-templates.ts uses the per-port config and threads portId into documensoGenerateFromTemplate(). * Migration 0042 wires the eleven HIGH-tier missing FK constraints (documents/files/interests/reminders/berth_waiting_list/ form_submissions) plus polymorphic CHECK round 2 (yacht_ownership_history.owner_type, document_sends.document_kind), invoices.billing_entity_id NOT EMPTY, and clients.merged_into self-FK. Drizzle schema columns updated to .references(...) where possible so the misleading "FK wired in relations.ts" comments are gone. Test status: 1168/1168 vitest, tsc clean. Refs: docs/audit-comprehensive-2026-05-05.md HIGH §§5,6,7,8,9,10 + MED §§14,15,16,18. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { env } from '@/lib/env';
|
||||
import { logger } from '@/lib/logger';
|
||||
import { getPortDocumensoConfig, type DocumensoApiVersion } from '@/lib/services/port-config';
|
||||
import { fetchWithTimeout } from '@/lib/fetch-with-timeout';
|
||||
|
||||
interface DocumensoCreds {
|
||||
baseUrl: string;
|
||||
@@ -26,7 +27,7 @@ async function documensoFetch(
|
||||
portId?: string,
|
||||
): Promise<unknown> {
|
||||
const { baseUrl, apiKey } = await resolveCreds(portId);
|
||||
const res = await fetch(`${baseUrl}${path}`, {
|
||||
const res = await fetchWithTimeout(`${baseUrl}${path}`, {
|
||||
...options,
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
@@ -241,7 +242,7 @@ export async function sendReminder(
|
||||
|
||||
export async function downloadSignedPdf(docId: string, portId?: string): Promise<Buffer> {
|
||||
const { baseUrl, apiKey } = await resolveCreds(portId);
|
||||
const res = await fetch(`${baseUrl}/api/v1/documents/${docId}/download`, {
|
||||
const res = await fetchWithTimeout(`${baseUrl}/api/v1/documents/${docId}/download`, {
|
||||
headers: { Authorization: `Bearer ${apiKey}` },
|
||||
});
|
||||
|
||||
@@ -261,7 +262,7 @@ export async function checkDocumensoHealth(
|
||||
): Promise<{ ok: boolean; status?: number; error?: string }> {
|
||||
try {
|
||||
const { baseUrl, apiKey } = await resolveCreds(portId);
|
||||
const res = await fetch(`${baseUrl}/api/v1/health`, {
|
||||
const res = await fetchWithTimeout(`${baseUrl}/api/v1/health`, {
|
||||
headers: { Authorization: `Bearer ${apiKey}` },
|
||||
});
|
||||
return { ok: res.ok, status: res.status };
|
||||
@@ -355,7 +356,7 @@ export async function placeFields(
|
||||
// Note: v2 endpoint shape (envelopeId/recipientId types) must be
|
||||
// confirmed against a live Documenso 2.x instance - see PR11 realapi
|
||||
// suite. Spec risk register flags this drift as the top v2 risk.
|
||||
const res = await fetch(`${baseUrl}/api/v2/envelope/field/create-many`, {
|
||||
const res = await fetchWithTimeout(`${baseUrl}/api/v2/envelope/field/create-many`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
@@ -387,7 +388,7 @@ export async function placeFields(
|
||||
// 1000 ms; 4xx responses (validation errors) fail-fast.
|
||||
let lastError: { status: number; body: string } | null = null;
|
||||
for (let attempt = 0; attempt < 3; attempt += 1) {
|
||||
const res = await fetch(`${baseUrl}/api/v1/documents/${docId}/fields`, {
|
||||
const res = await fetchWithTimeout(`${baseUrl}/api/v1/documents/${docId}/fields`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${apiKey}`,
|
||||
@@ -470,7 +471,7 @@ export function computeDefaultSignatureLayout(
|
||||
export async function voidDocument(docId: string, portId?: string): Promise<void> {
|
||||
const { baseUrl, apiKey, apiVersion } = await resolveCreds(portId);
|
||||
const path = apiVersion === 'v2' ? `/api/v2/envelope/${docId}` : `/api/v1/documents/${docId}`;
|
||||
const res = await fetch(`${baseUrl}${path}`, {
|
||||
const res = await fetchWithTimeout(`${baseUrl}${path}`, {
|
||||
method: 'DELETE',
|
||||
headers: { Authorization: `Bearer ${apiKey}` },
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user