fix(audit): documenso — M2 (reservation EOI-milestone pollution), L11 (v2 numericId GET fallback), L12 (API URL normalize/validate), L13 (event dedup)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -845,34 +845,50 @@ export async function sendForSigning(documentId: string, portId: string, meta: A
|
||||
|
||||
// Update interest if linked
|
||||
if (interest) {
|
||||
// Always link the Documenso envelope to the interest, regardless of
|
||||
// document type. The EOI-specific milestone columns + eoi_sent rule are
|
||||
// gated below so a reservation_agreement send doesn't pollute EOI funnel
|
||||
// data (dateEoiSent / eoiStatus / eoiDocStatus / eoi stage advance).
|
||||
await db
|
||||
.update(interests)
|
||||
.set({
|
||||
documensoId: documensoDoc.id,
|
||||
dateEoiSent: new Date(),
|
||||
eoiStatus: 'waiting_for_signatures',
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(interests.id, interest.id));
|
||||
|
||||
// Trigger berth rules
|
||||
void evaluateRule('eoi_sent', interest.id, portId, meta);
|
||||
// EOI documents only: stamp EOI milestones, fire the eoi_sent berth
|
||||
// rule, and advance to the `eoi` stage. A reservation_agreement is not
|
||||
// an EOI and must not touch these columns.
|
||||
if (doc.documentType === 'eoi') {
|
||||
await db
|
||||
.update(interests)
|
||||
.set({
|
||||
dateEoiSent: new Date(),
|
||||
eoiStatus: 'waiting_for_signatures',
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(interests.id, interest.id));
|
||||
|
||||
// Advance pipeline stage to eoi (no-op if already further along).
|
||||
// Doc sub-status is set by the webhook receiver when Documenso confirms;
|
||||
// we stamp eoiDocStatus optimistically here so the UI shows "sent".
|
||||
void advanceStageIfBehindGated(
|
||||
interest.id,
|
||||
portId,
|
||||
'eoi',
|
||||
meta,
|
||||
'EOI sent for signing',
|
||||
'eoi_sent',
|
||||
);
|
||||
await db
|
||||
.update(interests)
|
||||
.set({ eoiDocStatus: 'sent', updatedAt: new Date() })
|
||||
.where(eq(interests.id, interest.id));
|
||||
// Trigger berth rules
|
||||
void evaluateRule('eoi_sent', interest.id, portId, meta);
|
||||
|
||||
// Advance pipeline stage to eoi (no-op if already further along).
|
||||
// Doc sub-status is set by the webhook receiver when Documenso confirms;
|
||||
// we stamp eoiDocStatus optimistically here so the UI shows "sent".
|
||||
void advanceStageIfBehindGated(
|
||||
interest.id,
|
||||
portId,
|
||||
'eoi',
|
||||
meta,
|
||||
'EOI sent for signing',
|
||||
'eoi_sent',
|
||||
);
|
||||
await db
|
||||
.update(interests)
|
||||
.set({ eoiDocStatus: 'sent', updatedAt: new Date() })
|
||||
.where(eq(interests.id, interest.id));
|
||||
}
|
||||
|
||||
// Reservation agreements drive the reservation stage; the contract
|
||||
// pathway uses its own send call and stamps contractDocStatus.
|
||||
@@ -1752,11 +1768,21 @@ export async function handleDocumentCompleted(eventData: { documentId: string; p
|
||||
);
|
||||
}
|
||||
|
||||
await db.insert(documentEvents).values({
|
||||
documentId: doc.id,
|
||||
eventType: 'completed',
|
||||
eventData: { documensoId: eventData.documentId },
|
||||
});
|
||||
// Idempotent insert: DOCUMENT_COMPLETED is retried by Documenso on
|
||||
// receiver 5xx and re-reconciled by the poll worker. There is exactly one
|
||||
// logical `completed` event per document, so use a deterministic
|
||||
// per-document hash as the dedup key — the `idx_de_dedup` partial unique
|
||||
// index on (documentId, signatureHash) + onConflictDoNothing collapses
|
||||
// retries into a single timeline row instead of accumulating duplicates.
|
||||
await db
|
||||
.insert(documentEvents)
|
||||
.values({
|
||||
documentId: doc.id,
|
||||
eventType: 'completed',
|
||||
signatureHash: `completed:${doc.id}`,
|
||||
eventData: { documensoId: eventData.documentId },
|
||||
})
|
||||
.onConflictDoNothing();
|
||||
|
||||
emitToRoom(`port:${doc.portId}`, 'document:completed', { documentId: doc.id });
|
||||
|
||||
@@ -1915,6 +1941,11 @@ export async function handleDocumentOpened(eventData: {
|
||||
documentId: doc.id,
|
||||
eventType: 'viewed',
|
||||
signerId: signer?.id ?? null,
|
||||
// recipient_email is the key for idx_de_per_recipient_dedup (partial
|
||||
// unique on (documentId, recipientEmail, eventType) WHERE
|
||||
// recipientEmail IS NOT NULL). Without it the index never engages and
|
||||
// v2 multi-delivery RECIPIENT_VIEWED opens can't dedup.
|
||||
recipientEmail: eventData.recipientEmail ?? null,
|
||||
signatureHash: eventData.signatureHash ?? null,
|
||||
eventData: { recipientEmail: eventData.recipientEmail },
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user