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>
This commit is contained in:
266
docs/AUDIT-FIX-WAVE-2026-05-18.md
Normal file
266
docs/AUDIT-FIX-WAVE-2026-05-18.md
Normal file
@@ -0,0 +1,266 @@
|
||||
# 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, ~1–2 weeks) or wait until Phases 1+2 can land
|
||||
together (also Documenso template push, ~3–4 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 2–7 (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.
|
||||
Reference in New Issue
Block a user