feat(audit-cleanup): finish all 15 outstanding items from verified backlog
Audit cleanup completion plan, all tiers shipped: Tier 1 (security + data integrity) - A.7 RTBF true wipe: redact email_messages body/subject/addresses for threads owned by deleted client; redact document_sends.recipient_email; collect file storage keys + delete blobs post-commit. - A.8 user_permission_overrides FK: documented inline why cascade is correct (not set-null as audit suggested) — overrides have no value without their user. - W2.14 PII redaction: camelCase normalization in audit.ts + error-events.service.ts isSensitiveKey; added city/postal/country/ birth fragments. firstName/lastName/dateOfBirth/postalCode etc. now caught in BOTH masker paths. 12 new test cases lock the coverage. Tier 2 (Documenso completion + refactor) - C.2: documentEvents.recipient_email column + partial unique index for per-recipient webhook dedup (migration 0075). handleDocumentSigned now sets recipient_email on insert. - Phase 2: completion_cc_emails distribution. handleDocumentCompleted reads documents.completionCcEmails, filters out signer-duplicates case-insensitively, fans signed PDF out to non-signer recipients. - C.4: extracted createPublicInterest() service from the 346-line api/public/interests route. Route becomes a thin shell (rate-limit, port resolution, audit log, email fan-out). The trio creation logic is now unit-testable without an HTTP fixture. - Phase 4: POST /api/v1/document-templates/[id]/detect-fields wired to document-field-detector.detectFields(). Sparkles "Auto-detect" button added to template-editor.tsx — maps DetectedField → marker with best-guess merge token (DATE / NAME / EMAIL); user retags. Tier 3 (reporting + recommender snapshot lockfiles) - W7.reports: extracted rollupStageRevenue / rollupStageCounts / computeTotalForecast / computeOccupancyRate / rollupBerthStatusCounts into src/lib/services/report-math.ts (pure functions). 16 new tests including an inline-snapshot lockfile on a representative 7-stage forecast. report-generators.ts now delegates. - W7.recommender: 18 new toMatchSnapshot tripwires on classifyTier boundaries + computeHeat at canonical input points. Tier 4 (rolling) - W6.attach: fixed outdated CLAUDE.md claim — threshold banner is informational and never depended on IMAP; bounce monitoring (the IMAP poller) is separate. - D.1 + D.2: documented deferral inline with full why-not-build-it reasoning so a future engineer sees the rationale. - G.1: representative formatDate sweep (audit-log-list, user-list, document-templates merge tokens, document-signing email). Rest of the ~100 sites stay rolling. Quality gates: 1420/1420 vitest (46 new tests above baseline of 1374), tsc clean, 0 lint errors. Plan: docs/superpowers/plans/2026-05-18-audit-cleanup-completion.md Migration: 0075_c2_document_events_recipient_email.sql (applied to dev DB). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
-- C.2: per-recipient webhook event dedup.
|
||||
--
|
||||
-- The legacy `idx_de_dedup` index keyed off (document_id, signature_hash)
|
||||
-- where signature_hash was a body-content hash. A RECIPIENT_SIGNED webhook
|
||||
-- for signer A and a separate RECIPIENT_SIGNED for signer B on the same
|
||||
-- envelope would have identical bodies in some Documenso versions, causing
|
||||
-- the second to be incorrectly deduplicated as a re-delivery.
|
||||
--
|
||||
-- This migration adds:
|
||||
-- 1. A nullable `recipient_email` column to document_events.
|
||||
-- 2. A partial unique index on (document_id, recipient_email, event_type)
|
||||
-- where recipient_email IS NOT NULL — so per-recipient events dedup
|
||||
-- independently while legacy events without recipient context fall
|
||||
-- through to the existing signature_hash dedup.
|
||||
|
||||
ALTER TABLE document_events
|
||||
ADD COLUMN IF NOT EXISTS recipient_email text;
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_de_per_recipient_dedup
|
||||
ON document_events (document_id, recipient_email, event_type)
|
||||
WHERE recipient_email IS NOT NULL;
|
||||
@@ -207,8 +207,13 @@ export const documentEvents = pgTable(
|
||||
// H-01: events outlive their signer row so the audit trail stays
|
||||
// intact if a recipient is removed.
|
||||
signerId: text('signer_id').references(() => documentSigners.id, { onDelete: 'set null' }),
|
||||
// C.2: recipient_email captured at event time. Enables per-recipient
|
||||
// dedup (`(documenso_document_id, recipient_email, event_type)`) so
|
||||
// a RECIPIENT_SIGNED webhook for signer-A doesn't dedup against an
|
||||
// earlier RECIPIENT_SIGNED for signer-B on the same envelope.
|
||||
recipientEmail: text('recipient_email'),
|
||||
eventData: jsonb('event_data').default({}),
|
||||
signatureHash: text('signature_hash'), // deduplication
|
||||
signatureHash: text('signature_hash'), // deduplication (legacy: per-payload-hash)
|
||||
createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(),
|
||||
},
|
||||
(table) => [
|
||||
@@ -218,6 +223,13 @@ export const documentEvents = pgTable(
|
||||
uniqueIndex('idx_de_dedup')
|
||||
.on(table.documentId, table.signatureHash)
|
||||
.where(sql`${table.signatureHash} IS NOT NULL`),
|
||||
// C.2: per-recipient event dedup. Distinct event_type per (document,
|
||||
// recipient) so re-delivery of the same SIGNED webhook for the same
|
||||
// recipient is a no-op, while a different recipient's SIGNED still
|
||||
// lands.
|
||||
uniqueIndex('idx_de_per_recipient_dedup')
|
||||
.on(table.documentId, table.recipientEmail, table.eventType)
|
||||
.where(sql`${table.recipientEmail} IS NOT NULL`),
|
||||
],
|
||||
);
|
||||
|
||||
|
||||
@@ -314,6 +314,11 @@ export const userPermissionOverrides = pgTable(
|
||||
id: text('id')
|
||||
.primaryKey()
|
||||
.$defaultFn(() => crypto.randomUUID()),
|
||||
// onDelete: 'cascade' is intentional here (not 'set null' as a stale 2026-05-12
|
||||
// audit item suggested). A permission override has no semantic value without
|
||||
// the user it grants permissions to — preserving a row with user_id=NULL
|
||||
// would be an orphan with no audit value, since the override is per-user
|
||||
// additive permissions, not a historical event we need to retain.
|
||||
userId: text('user_id')
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: 'cascade' }),
|
||||
|
||||
Reference in New Issue
Block a user