Files
pn-new-crm/src/server.ts

121 lines
4.3 KiB
TypeScript
Raw Normal View History

/**
* Custom server entry point.
*
* Boots Next.js, attaches Socket.io to the same HTTP server, and in
* development mode also registers BullMQ recurring jobs and starts the
* workers inline (so you don't need the separate crm-worker container).
*
* In production the workers run in the crm-worker container (Dockerfile.worker
* dist/worker.js) and this file only handles Next.js + Socket.io.
*/
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>
2026-05-05 19:52:58 +02:00
import { createServer, type Server as HttpServer } from 'node:http';
import next from 'next';
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>
2026-05-05 19:52:58 +02:00
import { initSocketServer, closeSocketServer } from '@/lib/socket/server';
import { logger } from '@/lib/logger';
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>
2026-05-05 19:52:58 +02:00
import { env } from '@/lib/env';
import { redis } from '@/lib/redis';
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>
2026-05-05 19:52:58 +02:00
const dev = env.NODE_ENV !== 'production';
async function gracefulShutdown(signal: string, httpServer: HttpServer): Promise<void> {
logger.info({ signal }, 'Shutdown signal received; closing connections');
fix(audit-verification): regressions found in post-Tier-6 review Two parallel reviews of the Tier 0–6 work surfaced one CRITICAL regression and a handful of remaining cross-tenant gaps that the original audit didn't enumerate. All fixed here: CRITICAL * document-reminders.processReminderQueue — the new bulk-fetch leftJoin to documentTemplates was scoped on `templateType` alone. Templates of the same type exist in every port; the cartesian explosion would have fired one Documenso reminder PER matching template-row per cron tick (a 5-port deploy = 5 reminders to the same signer per cycle). Added eq(documentTemplates.portId, portId) to the join. * All five remaining Documenso webhook handlers (RecipientSigned / Completed / Opened / Rejected / Cancelled) accept and require an optional portId now, with a shared resolveWebhookDocument() helper that refuses to mutate when the lookup is ambiguous across tenants without a resolved port. Tier 5's port-scoping was applied only to Expired; the route now forwards the matched portId to every handler. Tightens the WHERE clauses on subsequent UPDATEs to (id, portId) for defense-in-depth. HIGH * verifyDocumensoSecret rejects when `expected` is empty — timingSafeEqual(0-bytes, 0-bytes) was returning true, so a dev env with a blank DOCUMENSO_WEBHOOK_SECRET would accept a request whose X-Documenso-Secret header was also missing/empty. listDocumensoWebhookSecrets skips the env entry when blank. * /api/public/health — the website-intake-secret comparison was a string `===` (not constant-time). Switched to timingSafeEqual via Buffer.from(). MEDIUM * server.ts SIGTERM ordering — Socket.io closes BEFORE the HTTP drain so long-poll websockets stop holding the server open past the compose stop_grace_period. * /api/v1/me PATCH preferences merge — allow-list filter on the merged JSONB so legacy rows from the old .passthrough() era stop silently re-shipping their bloat to disk. Migration fixes (deploy-blocking) * 0041 referenced `port_role_overrides.permissions` (column is `permission_overrides`) — overrides are partial JSONB and don't need backfilling at all (deepMerge resolves edit from the base role). Removed the override UPDATEs entirely. * 0042 switched all FK + CHECK adds to NOT VALID + VALIDATE so the brief table-lock phase is decoupled from the row-scan validation, giving a cleaner abort-and-restart story if a constraint catches dirty production data. Added a pre-cleanup UPDATE for invoices.billing_entity_id = '' rows (backfills from clientName, falls back to the row id) so the new non-empty CHECK passes on a dirty table. Test status: 1175/1175 vitest, tsc clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 21:19:39 +02:00
// Order matters: close Socket.io first so it stops accepting new
// sockets and emits disconnect events while the HTTP server is still
// up to flush them. `httpServer.close` only stops new connections;
// it waits for keep-alive HTTP and long-poll websockets to drain on
// their own, so without an explicit io.close() upfront the polls hold
// the server past the compose stop_grace_period and the process gets
// SIGKILL'd mid-frame.
await closeSocketServer().catch((err) => logger.warn({ err }, 'closeSocketServer error'));
// Then drain the HTTP layer.
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>
2026-05-05 19:52:58 +02:00
await new Promise<void>((resolve) => {
httpServer.close((err) => {
if (err) logger.warn({ err }, 'httpServer.close emitted error');
resolve();
});
fix(audit-verification): regressions found in post-Tier-6 review Two parallel reviews of the Tier 0–6 work surfaced one CRITICAL regression and a handful of remaining cross-tenant gaps that the original audit didn't enumerate. All fixed here: CRITICAL * document-reminders.processReminderQueue — the new bulk-fetch leftJoin to documentTemplates was scoped on `templateType` alone. Templates of the same type exist in every port; the cartesian explosion would have fired one Documenso reminder PER matching template-row per cron tick (a 5-port deploy = 5 reminders to the same signer per cycle). Added eq(documentTemplates.portId, portId) to the join. * All five remaining Documenso webhook handlers (RecipientSigned / Completed / Opened / Rejected / Cancelled) accept and require an optional portId now, with a shared resolveWebhookDocument() helper that refuses to mutate when the lookup is ambiguous across tenants without a resolved port. Tier 5's port-scoping was applied only to Expired; the route now forwards the matched portId to every handler. Tightens the WHERE clauses on subsequent UPDATEs to (id, portId) for defense-in-depth. HIGH * verifyDocumensoSecret rejects when `expected` is empty — timingSafeEqual(0-bytes, 0-bytes) was returning true, so a dev env with a blank DOCUMENSO_WEBHOOK_SECRET would accept a request whose X-Documenso-Secret header was also missing/empty. listDocumensoWebhookSecrets skips the env entry when blank. * /api/public/health — the website-intake-secret comparison was a string `===` (not constant-time). Switched to timingSafeEqual via Buffer.from(). MEDIUM * server.ts SIGTERM ordering — Socket.io closes BEFORE the HTTP drain so long-poll websockets stop holding the server open past the compose stop_grace_period. * /api/v1/me PATCH preferences merge — allow-list filter on the merged JSONB so legacy rows from the old .passthrough() era stop silently re-shipping their bloat to disk. Migration fixes (deploy-blocking) * 0041 referenced `port_role_overrides.permissions` (column is `permission_overrides`) — overrides are partial JSONB and don't need backfilling at all (deepMerge resolves edit from the base role). Removed the override UPDATEs entirely. * 0042 switched all FK + CHECK adds to NOT VALID + VALIDATE so the brief table-lock phase is decoupled from the row-scan validation, giving a cleaner abort-and-restart story if a constraint catches dirty production data. Added a pre-cleanup UPDATE for invoices.billing_entity_id = '' rows (backfills from clientName, falls back to the row id) so the new non-empty CHECK passes on a dirty table. Test status: 1175/1175 vitest, tsc clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 21:19:39 +02:00
// Hard timeout — 25s leaves headroom under a 30s compose grace
// period before SIGKILL would arrive anyway.
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>
2026-05-05 19:52:58 +02:00
setTimeout(() => resolve(), 25_000).unref();
});
try {
redis.disconnect();
} catch (err) {
logger.warn({ err }, 'redis.disconnect error');
}
logger.info({ signal }, 'Shutdown complete');
}
async function main(): Promise<void> {
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>
2026-05-05 19:52:58 +02:00
const app = next({ dev, port: env.PORT });
const handle = app.getRequestHandler();
await app.prepare();
const httpServer = createServer((req, res) => {
handle(req, res);
});
// Attach Socket.io to the HTTP server (uses Redis adapter for pub/sub)
initSocketServer(httpServer);
logger.info('Socket.io initialized');
// In development, run BullMQ workers inline so a single `pnpm dev` is enough
if (dev) {
const { registerRecurringJobs } = await import('@/lib/queue/scheduler');
const { emailWorker } = await import('@/lib/queue/workers/email');
const { documentsWorker } = await import('@/lib/queue/workers/documents');
const { notificationsWorker } = await import('@/lib/queue/workers/notifications');
const { importWorker } = await import('@/lib/queue/workers/import');
const { exportWorker } = await import('@/lib/queue/workers/export');
const { aiWorker } = await import('@/lib/queue/workers/ai');
const { bulkWorker } = await import('@/lib/queue/workers/bulk');
const { maintenanceWorker } = await import('@/lib/queue/workers/maintenance');
const { reportsWorker } = await import('@/lib/queue/workers/reports');
const { webhooksWorker } = await import('@/lib/queue/workers/webhooks');
await registerRecurringJobs();
logger.info('BullMQ recurring jobs registered (dev mode)');
// Keep a reference so workers aren't GC'd
void [
emailWorker,
documentsWorker,
notificationsWorker,
importWorker,
exportWorker,
aiWorker,
bulkWorker,
maintenanceWorker,
reportsWorker,
webhooksWorker,
];
}
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>
2026-05-05 19:52:58 +02:00
httpServer.listen(env.PORT, () => {
feat(branding): multi-tenant brand naming + per-port email shell + auth UI continuity Removes the last hardcoded "Port Nimara" references so a tenant cloning the deploy with a fresh slug sees their own brand throughout. Browser + native chrome: - `generateMetadata` reads `branding_app_name` from the first port row so the browser tab title, apple-web-app title, and template literal reflect the tenant (fallback "CRM" until DB is seeded). - Mobile topbar derives the brand-mark initials from the port slug ("port-nimara" → "PN", "marina-alpha" → "MA") — no code edit on clone. - `documenso-payload` default redirect URL is `""` so Documenso falls back to its own post-sign page instead of routing every tenant's signers to portnimara.com; per-port `redirectUrl` setting still wins. - Server-startup log uses generic "CRM server listening". Email + auth shell: - New `auth-shell-branding.ts` resolves logo / background / appName once per request from `system_settings`; used by both the email shell and the auth-pages SSR layout. - `auth-branding-provider` wraps `/login`, `/reset-password`, `/set-password`, portal `/portal/*` so the branded shell hydrates with the same assets the inbox sees. - `me/email` change email uses the branded shell instead of inline HTML with "Port Nimara CRM" baked into copy. - Admin branding page adds an email-preview card (POSTs to `/api/v1/admin/branding/email-preview`) so an admin can spot-check their templates before going live. - `/api/public/files/[id]` exposes branding-category files anonymously so inbox images (no session cookie) can render; any other category still flows through authenticated `/api/v1/files/[id]/preview`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-20 15:54:10 +02:00
logger.info({ port: env.PORT, env: env.NODE_ENV }, 'CRM server listening');
});
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>
2026-05-05 19:52:58 +02:00
// Graceful stop on container restart / deploy. Without this, every
// `docker compose up -d` rolling restart drops in-flight uploads, EOI
// generation, Documenso requests, and Socket.io frames mid-statement.
// Match docker-compose `stop_grace_period: 30s` (or longer) so the
// 25s drain inside gracefulShutdown can complete before SIGKILL.
for (const sig of ['SIGTERM', 'SIGINT'] as const) {
process.once(sig, () => {
void gracefulShutdown(sig, httpServer).finally(() => process.exit(0));
});
}
}
main().catch((err) => {
logger.error(err, 'Failed to start server');
process.exit(1);
});