Closes the silence after the first signing invitation. Three real
improvements on top of the existing webhook plumbing, all aligned with
the Documenso v1.32 + v2 webhook payload shape (verified against the
official OpenAPI spec + Context7 docs):
1. Cascading "your turn" emails — when DOCUMENT_SIGNED / DOCUMENT_
RECIPIENT_COMPLETED / RECIPIENT_SIGNED fires for a recipient,
handleRecipientSigned now resolves the next pending signer in
signing order and sends them the branded sendSigningInvitation()
email with the embedded-host-wrapped URL. Stamps invitedAt so a
duplicate webhook retry doesn't re-send.
2. On-completion PDF distribution — handleDocumentCompleted now re-
reads the just-committed signedFileId, resolves all signers, and
fires sendSigningCompleted() to every recipient with the signed
PDF attached. resolveAttachments in lib/email already pulls bytes
through getStorageBackend() so this works under both the s3/minio
and filesystem backends without changes. Failures fall through to
logger.error rather than throwing — the document is already marked
completed and the admin can re-trigger manually.
3. Token-based recipient matching — Documenso v1 + v2 webhook recipients
carry a `token` field (per the OpenAPI spec); same token appears in
the document-create response. Captured at send time into the existing
document_signers.signing_token column (already in schema from Phase 1)
and used by handleRecipientSigned + handleDocumentOpened before
falling back to email match. Robust against the case where one email
serves multiple roles on a contract — which is the documented gap in
the legacy nocodb-based handler.
Supporting changes:
- New helper module lib/services/documenso-signers.ts with
extractSigningToken() (URL-tail fallback), DOC_TYPE_LABEL map, and
nextPendingSigner() picker. 11 unit tests cover the token-regex,
the helper picks the lowest pending signing-order, and rejects
declined/signed correctly.
- documenso-client normalizeDocument now reads `token` from both
`recipients[]` and the legacy capital-R `Recipient[]` array Documenso
v1.32 sometimes ships in webhooks.
- documents.service signer-update at send time prefers the explicit
token field, falling back to extractSigningToken(signingUrl) for any
v2 deployment whose distribute response omits it.
Out of scope for Phase 2 (per the build plan):
- Custom-doc upload-to-Documenso path (Phase 3)
- Recipient + field-placement UI (Phase 4)
- DNS-rebinding hardening + circuit-breaker (deferred-refactor list)
- Auto-reminder cron — manual "Send reminder" button + auto-reminder
toggle remain manual until Phase 6 polish
Tests: 1315/1315 vitest ✅ + 11 new tests for documenso-signers ✅;
tsc clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>