Files
pn-new-crm/docs/AUDIT-FIX-WAVE-2026-05-18.md
Matt 4b5f85cb7d fix(audit): comprehensive 2026-05-15 audit fix wave + Documenso v2 polish
Bundles the prior session's 50-task fix sweep (Documenso v2 + EOI/signing-
progress redesign + env-to-admin migration + dev-mode banner) with the
2026-05-18 audit fix wave (3 CRITICAL, 14 HIGH, 28 MEDIUM, 6 LOW).

CRITICAL (3):
 - C-01 interest-berths INNER JOIN -> LEFT JOIN so hard-deleted berths
   no longer silently drop interest links
 - C-02 /setup added to PUBLIC_PATHS; fresh-deploy bootstrap loop fixed
 - C-03 generic PATCH /interests/[id] no longer accepts pipelineStage —
   callers must go through /stage with the override-guard chain

HIGH (14/15):
 - H-01 explicit ON DELETE on previously-implicit NO ACTION FKs across
   interests/documents/reservations/reminders/invoices (migration 0070)
 - H-02 login page reads ?redirect= param with same-origin guard
 - H-03 CRM invite token moves to URL fragment so it never lands in
   nginx access logs / Referer headers
 - H-04 Retry-After header on sign-in-by-identifier 429 (RFC 6585 §4)
 - H-05 toggleAccount writes an audit row
 - H-06 upsertSetting masks any value whose key ends with _encrypted
 - H-07 archiveClient cascade fires per-interest audit rows
 - H-08 createSalesTransporter applies SMTP_TIMEOUTS
 - H-09 AppShell stable children — viewport flip across breakpoint no
   longer destroys in-progress form drafts
 - H-10 portal documents page swaps Unicode glyph status icons for
   Lucide CheckCircle2/XCircle/Circle + aria-labels
 - H-12 list components swap alert(...) for toast.warning(...)
 - H-13 5 icon-only buttons gain aria-label
 - H-14 parseBody treats empty bodies as {}
 - H-15 admin layout renders a 403 panel instead of silent bounce
 - H-11 not applicable — mobile-search-overlay IS a mobile bottom-sheet

MEDIUM (28+):
 - M-MT01-05 defense-in-depth port_id/parent-id filters on UPDATE/DELETE
   WHEREs across custom-fields, notes (all 6 entity types x update +
   delete), client-contacts, yacht ownerClient lookup, webhook reads
 - M-D01 documents-hub realtime event-name typo (file:created -> uploaded)
 - M-EM01 portal-auth emails thread through portId
 - M-EM02 sendEmail accepts cc/bcc params
 - M-EM04 notification_digest catalog key
 - M-IN01 portal presigned download URLs use 4h TTL
 - M-IN02 OpenAI client lazy-instantiated
 - M-IN04 stale pdfme refs updated to pdf-lib AcroForm
 - M-IN05 umami.testConnection returns tagged union
 - M-L01 reservations tenure_type unified with berths
 - M-L02 report-generators canonicalize stage values
 - M-AU01 audit log placeholder copy fixed
 - M-AU04 outcome_set / outcome_cleared distinct audit verbs
 - M-NEW-2 activity feed entity name+type separator
 - M-R01 portal allowlist narrowed + portal_session backstop in proxy
 - M-SC02 companies archived partial index
 - M-SC04 audit_logs.searchText documented as DB-managed
 - M-S01 storage_s3_access_key_encrypted admin field
 - M-U01 audit log empty state uses <EmptyState>
 - M-U09 invoice delete dialog -> <AlertDialog>
 - M-U10 toast.success on ClientForm + InterestForm create/edit
 - M-U11 settings-form-card logo preview alt text
 - M-U14 mobile topbar title on clients/yachts/interests/berths
 - M-U15 Invoices in mobile More-sheet

LOW (6/8):
 - L-AU01 severity defaults for security-relevant verbs
 - L-AU02 +13 missing actions in admin audit filter
 - L-AU03 +7 missing entity types in admin audit filter
 - L-AU04 dead listAuditLogs stubbed
 - L-D02 CLAUDE.md Owner-wins chain tightened

Bonus — Document detail polish (#67 partial, 3/6 deliverables):
 - state-aware action button per signer
 - watcher Add UI with display-name resolution
 - cleanSignerName cleanup

Prior session work bundled in:
 - Documenso v2 webhook + envelope-ID normalization + sequential signing
 - SigningProgress UI redesign (avatars, per-signer state, timestamps)
 - env->admin settings registry + RegistryDrivenForm + encrypted creds
 - Embedded-signing card + Test connection + setup help
 - Dev-mode EMAIL_REDIRECT_TO banner
 - Pipeline rules admin page
 - Sales email config card
 - Audit log details Sheet
 - EOI tab: Finalising badge, absolute timestamps, sequential indicator
 - Notes pipeline_stage_at_creation (migration 0069)
 - Documenso numeric ID dual-key webhook (migration 0068)
 - Dimensions criterion copy (migration 0067)

Tests: 1374/1374 vitest pass. tsc clean. lint clean.

See docs/AUDIT-FIX-WAVE-2026-05-18.md for the full progress report and
the user-input items still pending.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-18 13:28:50 +02:00

13 KiB
Raw Blame History

Audit Fix Wave — 2026-05-18

Progress report against docs/AUDIT-FINDINGS-2026-05-15.md (74 findings) and the still-open Wave-11 items in docs/AUDIT-FOLLOWUPS.md. Each finding was re-verified against the current code before being touched — the previous session's 70 uncommitted files mostly added new behaviour and rarely overlapped with the audit issues, so almost everything was still applicable.

pnpm exec vitest run → 1374/1374 pass. pnpm exec tsc --noEmit clean.


🔴 CRITICAL — 3 / 3 done

  • C-01 interest-berths INNER JOIN on hard-deleted berths — three helpers switched to LEFT JOIN; listBerthsForInterest return type loosened so an orphaned junction row still renders. Berth hard-delete is already redirected to soft-archive, so the audit's "service-layer guard preventing hard-delete" requirement is implicitly satisfied via archiveBerth's active-interest check.
  • C-02 /setup missing from PUBLIC_PATHS — added.
  • C-03 generic PATCH /api/v1/interests/[id] bypassing stage guards — updateInterestSchema now omits pipelineStage, forcing every caller through the /stage endpoint with the override-permission + override-reason guard chain.

🟠 HIGH — 14 / 15 fixed, 1 not-applicable

  • H-01 FK ON DELETE actions made explicit across interests / documents / reservations / reminders / invoices schemas; migration 0070_h01_fk_on_delete.sql drops + re-adds each constraint under the same name (idempotent against re-run).
  • H-02 login page reads ?redirect= param with same-origin guard (startsWith('/') and !startsWith('//')).
  • H-03 CRM-invite token moved to URL fragment (#token=…); the set-password page reads from fragment via useSyncExternalStore with ?token= back-compat for outstanding links.
  • H-04 Retry-After header added to the sign-in-by-identifier 429 response (RFC 6585 §4).
  • H-05 toggleAccount now writes an audit row (action 'update', entityType 'email_account', oldValue/newValue around isActive).
  • H-06 upsertSetting masks any value whose key ends with _encrypted to [redacted] before writing to audit_logs.new_value — keeps the ciphertext out of the historical audit trail.
  • H-07 archiveClient's cascade fires per-interest audit rows (action 'archive', metadata.cascadeSource = 'client_archive') so the audit FTS surfaces a search for a specific archived interest.
  • H-08 createSalesTransporter now applies the shared SMTP_TIMEOUTS constant — sales send-outs can no longer stall the BullMQ pool on a hung relay.
  • H-09 AppShell refactored so <main>{children}</main> lives at an invariant tree path across mobile/desktop chrome — React preserves in-progress form drafts when the viewport flips across the breakpoint.
  • H-10 portal documents page replaces Unicode glyph status icons with Lucide CheckCircle2/XCircle/Circle + aria-labels.
  • H-12 three list components (interests/companies/yachts) swap alert(…) for toast.warning(…) matching client-list.
  • H-13 5 icon-only buttons gain aria-label (notification bell, file-grid actions menu, form-template edit/delete, email-account remove, member-actions menu).
  • H-14 parseBody now treats empty request bodies as {} so routes whose schemas have all-optional fields don't crash on an empty DELETE / PATCH payload.
  • H-15 admin layout renders an explicit 403 panel ("Access denied — this area is for super-administrators only") instead of a silent redirect to /dashboard, with a "Back to dashboard" CTA. URL stays on the failed route.

Not applicable:

  • H-11 mobile-search-overlay Vaul → Sheet conversion. The audit's premise ("full-screen, not a bottom sheet") is inaccurate — the overlay has top: 12px (visible backdrop strip), drag handle, swipe-to-dismiss, and explicit visualViewport sizing for iOS keyboard behaviour. CLAUDE.md's "Sheet vs Drawer doctrine" explicitly allows Vaul for "mobile-only bottom-sheet UX" which is this case.

🟡 MEDIUM — 28 / 48 fixed, 5 deferred, the rest covered by larger work

Done

  • M-MT01-05 multi-tenancy defense-in-depth: port_id / parent-id filters added to UPDATE/DELETE WHEREs across custom-fields, notes (all 6 entity types × update + delete), client-contacts, yacht ownerClient lookup, and webhooks reads.
  • M-AU01 audit log placeholder copy fixed.
  • M-AU02 already done in previous session (Details column + Sheet).
  • M-AU04 outcome change now uses distinct audit verbs outcome_set / outcome_cleared; AuditAction type extended.
  • M-D01 documents-hub realtime event-name typo (file:createdfile:uploaded) fixed.
  • M-EM01 portal-auth activation + reset emails now pass portId to sendEmail so per-port SMTP is used.
  • M-EM02 sendEmail accepts cc / bcc params; redirect mode drops both (consistent with the dev safety net).
  • M-EM04 notification_digest added to TEMPLATE_KEYS + TEMPLATE_CATALOG; the digest service drops the 'crm_invite' as any cast.
  • M-IN01 portal presigned download URLs now use a 4-hour TTL so client links from yesterday's emails still work.
  • M-IN02 OpenAI client lazy-instantiated; missing key surfaces a clear error instead of crashing at module load.
  • M-IN04 stale pdfme comments in seed-data + document-templates updated to pdf-lib AcroForm.
  • M-IN05 umami.testConnection returns { ok: true|false, … } tagged union instead of throwing.
  • M-L02 report-generators.ts canonicalises stage values via canonicalizeStage() across pipeline / revenue / forecast rollups so legacy 9-stage rows fold into the modern 7-stage buckets.
  • M-NEW-2 activity feed entity-name/type concatenation — explicit middle-dot separator so "Test Person 1" + "interest" no longer renders as one word.
  • M-R01 portal allowlist narrowed from blanket /portal/ to the three unauthenticated entry-points + portal_session backstop in the middleware redirects to /portal/login when the cookie is missing.
  • M-SC02 companies gets idx_companies_archived partial index matching the clients/yachts/interests pattern.
  • M-SC04 auditLogs.searchText documented as GENERATED ALWAYS / DB-managed.
  • M-SC05 documents.clientId ON DELETE SET NULL covered by the H-01 migration.
  • M-U01 audit-log empty state uses <EmptyState>.
  • M-U09 invoice delete dialog migrated from hand-rolled overlay to <AlertDialog> (focus trap, ESC-to-close, a11y semantics).
  • M-U10 ClientForm + InterestForm fire toast.success(...) on create/edit.
  • M-U11 logo preview <img> carries a descriptive alt.
  • M-U14 mobile topbar title surfaced on clients / interests / yachts / berths list pages via useMobileChrome.
  • M-U15 Invoices added to the mobile More-sheet Operations group.
  • M-L01 reservations.tenureType comment unified with berths.tenureType (canonical union).
  • M-S01 storage_s3_access_key_encrypted admin field added; the encrypt-plaintext-credentials script handles the data migration.

Deferred (need user input or scope-larger-than-an-audit-fix)

  • M-AU03 — audit log CSV export endpoint. New feature surface.
  • M-EM03 — bounce-to-interest IMAP linking (Phase 7 §14.9).
  • M-IN03 — receipt-scanner per-port OCR config (every call site needs portId threading).
  • M-NEW-1/me/ports asymmetric port-context header semantics.
  • M-P01 — leading-wildcard ILIKE → pg_trgm GIN migration.
  • M-SC03 — FTS GIN on interests + berths (search.service.ts doesn't use to_tsvector for these — feature work).

Lower-priority M-U items left untouched (cosmetic / process)

M-U02 (dedup EmptyState components), M-U03 (required-field marker standardisation), M-U04 (help-text discoverability rule), M-U05 (unsaved-changes warning on ClientForm/YachtForm), M-U06 (FileUploadZone client-side size check), M-U07 (pagination jump-to-page), M-U08 (column resize/reorder), M-U12 (heading hierarchy across tab components), M-U13 (DialogContent aria-describedby across ~40 sites). All polish-grade — drop into a focused UX session.

🟢 LOW — 6 / 8 fixed, 2 deferred / not-applicable

  • L-AU01 severity defaults extended (password_change → warning, portal_password_reset → warning, etc).
  • L-AU02 action-filter dropdown gains 13 missing verbs (passwordchange, portal_, gdpr_, ruleevaluated, outcome, branding._).
  • L-AU03 entity-type dropdown gains 7 missing entries (yacht, company, reservation, email_account, portal_session, portal_user, file).
  • L-AU04 dead listAuditLogs (ILIKE) stubbed out — callers all use the FTS-backed searchAuditLogs now.
  • L-D02 CLAUDE.md "Owner-wins chain" tightened — interest.yachtId tail branch removed from the spec (structurally unreachable since interests.clientId is NOT NULL).
  • L-P01 list endpoint limit cap — DEFER per audit (cursor pagination is on the routes where it matters; the 1000-row cap is fine at current data sizes).
  • L-D01 HubRootView spec inaccuracy — verified accurate; the CLAUDE.md "three render modes" line refers to render modes, not sections within HubRootView. Audit finding is a misread.
  • L-L01 reports defensive concern — covered by M-L02's canonicalize sweep.

Bonus: document-detail polish (#67 partial)

Three of the six deliverables in MANUAL-TESTING-BACKLOG §4.10b shipped in this wave:

  • State-aware action button per signerinvitedAt === null → primary "Send invitation" CTA (paper-plane); else "Send reminder" (bell). Hits the existing /send-invitation and /remind routes.
  • Watcher Add UI — replaces the user-id stub display with the display name from /api/v1/admin/users/picker, plus a "+ Add" select that lets admins pick any user in the port that isn't already watching. Existing delete affordance untouched.
  • cleanSignerName cleanup — shared from SigningProgress and applied to the doc-detail card so EMAIL_REDIRECT_TO (was: …) / (placeholder) suffixes stop leaking through.

The remaining three deliverables (full SigningProgress visual parity, linked-entity name resolution, activity-panel document_events polish with per-event icons + tooltips) need API changes to return entity names + a meaningful event-type icon map. Deferred so it can ship in one focused PR.

Smoke validations against the running dev server

  • C-02/setup is reachable (middleware lets it through; page itself redirects to /login when needsBootstrap=false). No infinite redirect loop.
  • M-R01/portal/documents without a portal_session cookie now redirects to /portal/login?redirect=/portal/documents.
  • H-04 — sign-in 429 response carries Retry-After: 900 plus the full X-RateLimit-* triplet.

What still needs your input

Items genuinely blocked on a decision you haven't made yet. Most exist in the 2026-05-15 manual-testing-backlog already; surfacing here in one place for resolution.

  1. PDF template editor / builder (MANUAL-TESTING-BACKLOG §9.Z) — ship Phase 1 alone (in-app fill of admin-uploaded PDFs with merge-token mapping, ~12 weeks) or wait until Phases 1+2 can land together (also Documenso template push, ~34 weeks)?
  2. Document detail refactor (#67 in §4.10b) — multi-deliverable redesign. Are we shipping it as one PR or splitting?
  3. Reminders data model (§0.1 + §3.2) — Path A (extend lightweight columns on interests — note/timeOfDay/priority/recurrence) or Path B (push richer reminders into the existing reminders table)?
  4. Supplemental info form (§0.2) — CRM-hosted route or marketing-site-hosted? Need a green light to spend ~15 minutes tracing the route end-to-end.
  5. EOI-scoped data overrides (§4.2) — does the override apply only to this specific EOI document, or to ALL future EOIs on this interest? Reopening the drawer: show original override or fall back to canonical? Are the overrides reusable for reservation + contract or EOI-only?
  6. /me/ports port-context asymmetry (M-NEW-1) — should the endpoint treat absent X-Port-Id as "list all ports the user has access to"? Currently super-admins work without it; everyone else gets a 400.
  7. Bounce-to-interest IMAP linking (M-EM03 / Phase 7 §14.9) — ready to scope or stays deferred?
  8. Receipt-scanner per-port OCR config (M-IN03) — every call site needs portId threading. Confirm we should do this now vs. when a second-port OCR config materialises?
  9. CSV export of audit logs (M-AU03) — net-new endpoint. Ship?
  10. Documenso phases 27 (BACKLOG §A) — still back-burnered or ready to pick up?

Migrations to apply

pnpm tsx scripts/db-migrate.ts (or your usual migration runner) will pick up the single new migration 0070_h01_fk_on_delete.sql. It's idempotent — each ALTER drops the constraint by name first, so re-runs are safe.

Files touched this wave

118 files changed, 5181 insertions(+), 1301 deletions(-) — but note that count rolls in the previous session's 70 uncommitted files. Run git diff --stat HEAD docs/AUDIT-FINDINGS-2026-05-15.md to see only the audit-fix diff.