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:
2026-06-02 12:59:07 +02:00
parent 37ffb2c3b4
commit 4084029962
4 changed files with 156 additions and 36 deletions

View File

@@ -459,6 +459,31 @@ export async function generateDocumentFromTemplate(
portId,
).then(normalizeDocument);
// Resolve an authoritative numeric pk. `/template/use` returns the
// `envelope_xxx` string under `id`/`envelopeId` but does NOT reliably
// surface the internal numeric pk, so `created.numericId` is frequently
// null. DOCUMENT_COMPLETED (and other v2 webhooks) carry ONLY that
// numeric pk as `payload.id`, and `resolveWebhookDocument` matches it
// against `documents.documenso_numeric_id`. Persisting null there means
// the webhook resolves against neither column and the completion is
// dropped (signed PDF never downloads, stage never advances, no
// completion email/tenancy) until the poll worker reconciles by
// envelope id. Re-fetch the full envelope (GET /api/v2/envelope/{id})
// when the numeric pk is missing so we persist a non-null value.
let numericId = created.numericId;
if (!numericId) {
try {
const fetched = await getDocument(created.id, portId);
if (fetched.numericId) numericId = fetched.numericId;
} catch (err) {
logger.warn(
{ docId: created.id, err: err instanceof Error ? err.message : err },
'Documenso envelope re-fetch for numericId failed - documenso_numeric_id will be null; ' +
'completion webhooks rely on the poll-worker reconciliation until then.',
);
}
}
const desiredTitle =
typeof v2Payload.title === 'string' && v2Payload.title.length > 0 ? v2Payload.title : null;
// `/template/use` silently drops the `meta` field on the request body -
@@ -571,11 +596,12 @@ export async function generateDocumentFromTemplate(
const normalized = normalizeDocument({
envelopeId: distributed.id ?? created.id,
// Distribute doesn't return the numeric id, so we synthesize it
// from the original /template/use response by passing the numeric
// id as Documenso's `id` field - normalizeDocument picks it up
// as numericId. Without this, the row would lose its numeric id
// on distribute and webhooks couldn't resolve back to it.
id: created.numericId,
// from the authoritative numericId resolved above (created response
// or envelope re-fetch) by passing it as Documenso's `id` field -
// normalizeDocument picks it up as numericId. Without this, the row
// would lose its numeric id on distribute and webhooks couldn't
// resolve back to it.
id: numericId,
status: 'PENDING',
recipients: distributed.recipients,
});
@@ -585,7 +611,9 @@ export async function generateDocumentFromTemplate(
{ docId: created.id, err: err instanceof Error ? err.message : err },
'Documenso envelope distribute failed - signingUrl will be null. Send-invitation will fail until the envelope is distributed.',
);
return created;
// Preserve the authoritative numericId resolved above so the persisted
// documenso_numeric_id is non-null even when distribute fails.
return { ...created, numericId };
}
}