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>
Final audit polish — closes the remaining LOW + MED items the previous
tiers didn't reach:
* Validation hardening: me.preferences uses .strict() + 8KB cap
instead of unbounded .passthrough(); files.uploadFile gains
magic-byte verification (jpeg/png/gif/webp/pdf/doc/xlsx); OCR scan
endpoint enforces 10MB cap + magic-byte check on receipt images;
port logoUrl + me.avatarUrl reject javascript:/data: schemes via
a shared httpUrl refinement.
* Permission gates: document-sends/{brochure,berth-pdf} now require
email.send (was withAuth-only); document-sends/{preview,list} on
email.view; ai/email-draft on email.send; documents/[id]/send
uses send_for_signing (was create); expenses/export/parent-company
flips from hard isSuperAdmin to expenses.export for parity;
admin/users/options gated on reminders.assign_others (was withAuth).
* Envelope hygiene: auth/set-password switches the third {message}
variant to errorResponse + {data: {email}}; ai/email-draft wraps
jobId in {data: {jobId}}.
* UI polish: reports-list.handleDownload surfaces failures via
toastError (was console-only).
* Ops/infra: pin pnpm@10.33.2 across all three Dockerfiles +
packageManager field in package.json; Dockerfile.worker re-orders
user creation BEFORE pnpm install so node_modules / .cache dirs
are worker-owned (fixes tesseract.js + sharp EACCES at first PDF
parse); add Redis-ping HEALTHCHECK to the worker container.
* Public health endpoint: returns full env+appUrl payload only when
the caller presents X-Intake-Secret, otherwise a minimal {status}
so generic uptime monitors still work but anonymous internet
doesn't get deployment fingerprints.
* Per-port Documenso webhook secret: new system_settings key
+ listDocumensoWebhookSecrets() helper. The webhook receiver
iterates every configured per-port secret with timing-safe
comparison + falls back to env, then forwards the resolved portId
into handleDocumentExpired so two ports sharing a documensoId
cannot cross-mutate.
Deferred (handled in dedicated follow-up PRs):
* Tier 5.1 — direct service tests for portal-auth / users /
email-accounts / document-sends / sales-email-config. MED, large
test-writing scope.
* The {ok: true} → {data: null} envelope migration across
alerts/expenses/admin-ocr-settings/storage routes. Mechanical but
needs coordinated client + test updates.
* CSP-nonce migration (drop unsafe-inline) — needs middleware-level
nonce generation that the Next 15 router has to thread through.
* Idempotency-Key header on Documenso createDocument. Requires
schema column on documents to persist the key; deferred so it
doesn't bundle a migration into this commit.
* The 16 better-auth user_id FKs — separate dedicated migration
with care (some columns are NOT NULL today and cascade decisions
matter).
* PermissionGate / Skeleton / EmptyState wraps across 5 admin lists
(auditor-H §§36–37) and the residential-clients filter bar.
Test status: 1175/1175 vitest, tsc clean.
Refs: docs/audit-comprehensive-2026-05-05.md MED §§28,29,30 + LOW §§32–43
+ HIGH §9 (Documenso secrets follow-up).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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>
Composer validator now takes senderType (system|user) and an
attachments[] array, and the service dispatches across two paths:
the system path uses lib/email/index.ts with port-config noreply
identity and logs signed_doc_emailed when an attachment matches a
document's signed PDF; the user path stays on the existing personal-
account flow but is gated by the new email.allowPersonalAccountSends
toggle and the attachment fileIds are persisted on email_messages.
sendEmail in lib/email accepts attachments and resolves them from
MinIO with cross-port enforcement.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds DOCUMENSO_API_VERSION env (default v1) plus per-port override.
Introduces placeFields, placeDefaultSignatureFields, and voidDocument
that hide v1 (per-field POST, pixel coords) vs v2 (bulk POST, percent +
fieldMeta) differences. cancelDocument now voids in Documenso first and
treats transient void failures as recoverable so the CRM stays the
system of record. 16 unit specs cover dispatch, layout math, idempotent
404, and v1 pixel conversion.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>