Files
red/.planning/phases/15-multi-signer-backend/15-VERIFICATION.md

14 KiB

phase, verified, status, score, re_verification
phase verified status score re_verification
15-multi-signer-backend 2026-04-03T00:00:00Z passed 17/17 must-haves verified false

Phase 15: Multi-Signer Backend Verification Report

Phase Goal: The server correctly creates one signing token per signer, filters each signer's signing page to their own fields, and detects completion atomically — all signers receive their links at once, and when the last signer signs all parties get the final PDF. Verified: 2026-04-03 Status: passed Re-verification: No — initial verification


Goal Achievement

Observable Truths

# Truth Status Evidence
1 createSigningToken accepts optional signerEmail and writes it to DB VERIFIED token.ts:7 signature (documentId: string, signerEmail?: string), token.ts:22 signerEmail: signerEmail ?? null in DB insert
2 createSignerDownloadToken produces a 72h JWT with purpose signer-download VERIFIED token.ts:69-75setExpirationTime('72h'), purpose: 'signer-download'
3 verifySignerDownloadToken validates purpose claim and returns documentId VERIFIED token.ts:77-81 — checks payload['purpose'] !== 'signer-download', returns { documentId }
4 sendSignerCompletionEmail sends plain-text email with download link VERIFIED signing-mailer.tsx:69-86 — correct opts shape, subject "Signed copy ready: {name}", text with downloadUrl
5 GET /api/sign/download/[token] serves signed PDF for valid signer-download tokens VERIFIED download/[token]/route.ts:12-46 — public GET, verifies token, queries DB, returns PDF bytes
6 Download route rejects expired tokens, non-Signed documents, and path traversal VERIFIED route.ts:21-23 401 on invalid token; 30-32 404 on non-Signed/no-path; 35-37 403 on path traversal
7 When documents.signers is populated, one signing token is created per signer with that signer's email VERIFIED send/route.ts:31-50 — branches on doc.signers.length > 0, calls createSigningToken(doc.id, signer.email) per signer
8 All signing request emails are dispatched in parallel via Promise.all VERIFIED send/route.ts:34-50Promise.all(signers.map(async (signer) => { ... }))
9 When documents.signers is null/empty, existing single-signer behavior is preserved VERIFIED send/route.ts:51-67 — else branch: legacy assignedClientId ?? clientId path, createSigningToken(doc.id) without signerEmail
10 The signing URL uses APP_BASE_URL (not NEXT_PUBLIC_BASE_URL) VERIFIED send/route.ts:29 process.env.APP_BASE_URL ?? 'http://localhost:3000'; no NEXT_PUBLIC_BASE_URL reference in file
11 Document status is set to Sent after all emails dispatch successfully VERIFIED send/route.ts:70-75if (doc.status === 'Draft') updates to 'Sent' after both paths
12 Each signer gets an audit event email_sent with metadata.signerEmail VERIFIED send/route.ts:44-48logAuditEvent({ eventType: 'email_sent', metadata: { signerEmail: signer.email } })
13 GET handler returns only fields where field.signerEmail matches tokenRow.signerEmail VERIFIED sign/[token]/route.ts:90-98 — two-branch filter: tokenRow.signerEmail !== null gates field.signerEmail === tokenRow.signerEmail
14 GET handler returns all isClientVisibleField fields for legacy null-signer tokens VERIFIED sign/[token]/route.ts:97return true when tokenRow.signerEmail === null
15 POST handler scopes date field stamping and signable fields to this signer only VERIFIED sign/[token]/route.ts:199-204 date scope, 235-240 signable fields scope — both check signerEmail !== null before filtering by f.signerEmail === signerEmail
16 POST handler accumulates partial PDFs (JTI-keyed) and detects completion atomically VERIFIED route.ts:187 _partial_${payload.jti}.pdf; route.ts:303-315 UPDATE … WHERE completionTriggeredAt IS NULL RETURNING
17 Completion winner sends agent notification and all signer completion emails with download links VERIFIED route.ts:332-388 — fire-and-forget blocks gated behind won.length > 0; sendAgentNotificationEmail + sendSignerCompletionEmail to all signers; createSignerDownloadToken constructs download URL

Score: 17/17 truths verified


Required Artifacts

Artifact Expected Status Details
teressa-copeland-homes/src/lib/signing/token.ts Extended createSigningToken + signer-download token pair VERIFIED All 6 exported functions present: createSigningToken, verifySigningToken, createDownloadToken, verifyDownloadToken, createAgentDownloadToken, verifyAgentDownloadToken, createSignerDownloadToken, verifySignerDownloadToken
teressa-copeland-homes/src/lib/signing/signing-mailer.tsx Signer completion email VERIFIED 3 email functions: sendSigningRequestEmail, sendAgentNotificationEmail, sendSignerCompletionEmail
teressa-copeland-homes/src/app/api/sign/download/[token]/route.ts Public signer download route VERIFIED File exists, exports GET, no auth import, correct guards
teressa-copeland-homes/src/app/api/documents/[id]/send/route.ts Multi-signer send route with legacy fallback VERIFIED Multi-signer branch + legacy branch, DocumentSigner typed, APP_BASE_URL
teressa-copeland-homes/src/app/api/sign/[token]/route.ts Signer-aware GET + POST handlers with accumulate PDF and atomic completion VERIFIED Signer-aware field filter in GET; accumulate PDF, scoped date/sig fields, atomic completionTriggeredAt guard, completion notifications in POST

From To Via Status Details
sign/download/[token]/route.ts src/lib/signing/token.ts verifySignerDownloadToken import WIRED route.ts:2 imports verifySignerDownloadToken, called at route.ts:20
sign/download/[token]/route.ts documents table db.query.documents.findFirst WIRED route.ts:25-28, guards status !== 'Signed' at route.ts:30
send/route.ts token.ts createSigningToken(doc.id, signer.email) WIRED send/route.ts:6 import, 36 call with signerEmail
send/route.ts signing-mailer.tsx Promise.all with sendSigningRequestEmail WIRED send/route.ts:7 import, 34-50 Promise.all loop
sign/[token]/route.ts GET documents.signatureFields filter by tokenRow.signerEmail WIRED route.ts:46-48 fetches tokenRow, 90-98 filters on tokenRow.signerEmail
sign/[token]/route.ts POST documents.completionTriggeredAt UPDATE WHERE IS NULL RETURNING atomic guard WIRED route.ts:303-315isNull(documents.completionTriggeredAt) guard with .returning()
sign/[token]/route.ts POST signing-mailer.tsx sendSignerCompletionEmail + sendAgentNotificationEmail WIRED route.ts:12 imports both, called in winner block at 344, 373
sign/[token]/route.ts POST token.ts createSignerDownloadToken WIRED route.ts:3 import, 368 call in winner block

Data-Flow Trace (Level 4)

Artifact Data Variable Source Produces Real Data Status
send/route.ts doc.signers db.query.documents.findFirst Yes — JSONB from DB FLOWING
sign/[token]/route.ts GET tokenRow.signerEmail db.query.signingTokens.findFirst Yes — DB row FLOWING
sign/[token]/route.ts POST freshDoc.signedFilePath Re-fetch from DB at step 7 Yes — DB row FLOWING
sign/[token]/route.ts POST remaining[0].cnt SQL count(*) on signingTokens WHERE usedAt IS NULL Yes — live DB count FLOWING
sign/[token]/route.ts POST won UPDATE … RETURNING Yes — returns row or empty array FLOWING

Behavioral Spot-Checks

Behavior Command Result Status
token.ts exports all 8 functions grep -c "^export async function" token.ts 8 PASS
signer-download purpose used in create and verify grep "signer-download" token.ts 3 lines (comment + create + verify) PASS
No auth import in download route (public) grep "import.*auth" download/[token]/route.ts 0 matches PASS
status: 'Signed' set exactly once in sign route grep -c "status: 'Signed'" sign/[token]/route.ts 1 PASS
APP_BASE_URL used; no NEXT_PUBLIC_BASE_URL in send route grep NEXT_PUBLIC_BASE_URL send/route.ts 0 matches PASS
completionTriggeredAt atomic guard present grep "isNull(documents.completionTriggeredAt)" Found at line 308 PASS
Signer completion emails fire only in winner block Pattern gated behind won.length === 0 check Lines 357-388 inside if (won.length === 0) is NOT it — see note PASS (see note)

Note on winner block: The sendSignerCompletionEmail and sendAgentNotificationEmail calls appear after line 315 (the if (won.length === 0) return early exit). Any code reached past that guard is winner-only. Code correctly gated.


Requirements Coverage

Requirement Source Plan Description Status Evidence
MSIGN-05 15-02 Document recipients list built automatically from unique signer emails on placed fields SATISFIED Send route reads doc.signers (JSONB written from field signerEmails in Phase 14); loops over each — no separate manual entry needed
MSIGN-06 15-02 All signers receive their unique signing links simultaneously when agent sends SATISFIED Promise.all in send route dispatches all signing emails concurrently
MSIGN-07 15-03 Each signer's signing page shows only their own assigned fields SATISFIED GET handler filters signatureFields by tokenRow.signerEmail
MSIGN-10 15-01, 15-03 When all signers complete, agent receives a notification email SATISFIED sendAgentNotificationEmail fires in completion winner block only
MSIGN-11 15-01, 15-03 When all signers complete, all parties receive the final merged PDF via email link SATISFIED sendSignerCompletionEmail fires for each doc.signers entry with createSignerDownloadToken URL; download route serves signed PDF

Orphaned requirements check: REQUIREMENTS.md traceability table maps exactly MSIGN-05, MSIGN-06, MSIGN-07, MSIGN-10, MSIGN-11 to Phase 15 — matches plan frontmatter declarations exactly. No orphaned requirements.

Out-of-scope for Phase 15 (correctly deferred to Phase 16): MSIGN-01, MSIGN-02, MSIGN-03, MSIGN-04, MSIGN-08 (Phase 14), MSIGN-09.


Anti-Patterns Found

File Line Pattern Severity Impact
sign/[token]/route.ts 263 console.error Info Debug logging — non-blocking, appropriate error handler
send/route.ts 79 console.error Info Error boundary logging — appropriate
sign/[token]/route.ts 329 process.env.APP_BASE_URL ?? 'http://localhost:3000' Info Fallback to localhost acceptable for dev; production env must set APP_BASE_URL

No blockers or warnings. All console.error calls are in error handlers, not substituting for real implementation. No stubs, no placeholder returns, no hardcoded empty arrays passed to rendering.


Human Verification Required

1. Multi-signer full ceremony end-to-end

Test: Create a document with two signers in documents.signers. Call POST /api/documents/[id]/send. Open both signing links in separate browser sessions. Have signer A sign, then signer B sign. Expected: Agent receives one notification email. Both signer A and signer B receive a completion email with a download link. Downloading the PDF returns a document with both signers' signatures embedded. Why human: Requires live SMTP delivery, real browser signing sessions, and PDF visual inspection.

2. Atomic completion race condition

Test: Simulate two concurrent POST submissions to /api/sign/[token] for the last two signers arriving within milliseconds of each other. Expected: Exactly one winner sets completionTriggeredAt and fires notifications; the other returns { ok: true } without double-sending emails. Why human: Requires concurrent load-testing tooling (e.g., k6 or two concurrent requests); cannot be verified with grep or static analysis.

3. Accumulate PDF correctness

Test: Have signer A sign fields on page 1; have signer B sign fields on page 2. Inspect the final signed PDF. Expected: Final PDF contains signer A's signature on page 1 AND signer B's signature on page 2 — accumulated correctly via the JTI-keyed partial path chain. Why human: PDF content inspection requires visual review of the output file.


Gaps Summary

No gaps found. All 17 must-haves are verified at all four levels (exists, substantive, wired, data-flowing). All 5 required requirement IDs (MSIGN-05, MSIGN-06, MSIGN-07, MSIGN-10, MSIGN-11) have clear implementation evidence in the codebase. The phase goal is achieved:

  • One signing token per signer is created with that signer's email stored in the DB
  • Each signer's GET returns only their own fields (filtered by tokenRow.signerEmail)
  • Completion is detected atomically via the completionTriggeredAt UPDATE-WHERE-IS-NULL-RETURNING race guard
  • All signers receive links simultaneously via Promise.all
  • When the last signer signs, the completion winner sends agent notification + signer download links

Three items are flagged for human verification (live email delivery, concurrent race, PDF accumulation correctness) but no automated check can substitute for them.


Verified: 2026-04-03 Verifier: Claude (gsd-verifier)