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

267 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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:created`
`file: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
(password*change, portal*\_, gdpr\__, rule*evaluated, 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 signer** — `invitedAt === 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.