---
phase: 06-signing-flow
verified: 2026-03-21T00:00:00Z
status: passed
score: 9/9 must-haves verified
re_verification: false
gaps: []
human_verification:
  - test: "Draw and submit a signature end-to-end"
    expected: "Signature canvas opens, drawing is captured, PDF is rendered with the signature image embedded at the correct position"
    why_human: "Canvas rendering and signature pad interaction cannot be verified programmatically"
  - test: "Open signing link in a browser without authentication"
    expected: "PDF renders inline via react-pdf in any browser without requiring login"
    why_human: "Browser rendering and unauthenticated access require a live environment check"
  - test: "Verify signed PDF has signature visually at correct coordinates"
    expected: "Signature image appears at the agent-placed field location in the downloaded copy"
    why_human: "PDF coordinate math (bottom-left origin flip) must be visually confirmed"
---

# Phase 06: Signing Flow Verification Report

**Phase Goal:** Client receives an email link, opens the prepared PDF in any browser, draws a signature, and the signed document is stored with a complete, legally defensible audit trail.
**Verified:** 2026-03-21
**Status:** PASSED
**Re-verification:** No — initial verification

Note: No `.planning/phases/06-signing-flow/` directory or PLAN/SUMMARY files were present in the repo. Verification was performed directly against the codebase using the requirement IDs and phase goal supplied in the prompt.

---

## Goal Achievement

### Observable Truths

| #  | Truth                                                                 | Status     | Evidence |
|----|-----------------------------------------------------------------------|------------|----------|
| 1  | Client receives a signing request email with a unique link            | VERIFIED   | `sendSigningRequestEmail` in `signing-mailer.tsx` sends via nodemailer with JWT link; called in `send/route.ts` after `createSigningToken` |
| 2  | Signing link opens the prepared PDF in any browser without login      | VERIFIED   | `/sign/[token]/page.tsx` is a public server component; `/api/sign/[token]/pdf/route.ts` serves file with JWT auth only, no session required |
| 3  | Client can draw a signature on the PDF                                | VERIFIED   | `SigningPageClient.tsx` renders `<SignatureModal>` on field click; modal captures dataURL; field overlays positioned over react-pdf `<Page>` |
| 4  | Signed document is stored on disk with embedded signature             | VERIFIED   | `embedSignatureInPdf` in `embed-signature.ts` uses `@cantoo/pdf-lib` to draw PNG onto PDF pages, atomically renames to `_signed.pdf`; `signedFilePath` stored in DB |
| 5  | Token is single-use and cannot be replayed                            | VERIFIED   | Atomic `UPDATE … WHERE used_at IS NULL RETURNING jti` in `sign/[token]/route.ts` POST; 0 rows = 409 Already Signed |
| 6  | SHA-256 hash of signed PDF is stored for legal integrity              | VERIFIED   | `embedSignatureInPdf` returns hex digest; stored as `pdfHash` in `documents` table; audit event `pdf_hash_computed` records hash + path |
| 7  | Complete audit trail is recorded for every signing event              | VERIFIED   | `logAuditEvent` persists `email_sent`, `link_opened`, `document_viewed`, `signature_submitted`, `pdf_hash_computed` to `audit_events` table with IP, user-agent, and timestamp |
| 8  | Agent is notified when a document is signed                           | VERIFIED   | `sendAgentNotificationEmail` called fire-and-forget in POST handler after successful embed |
| 9  | Client can download a copy of the signed PDF after signing            | VERIFIED   | `confirmed/page.tsx` generates 15-min download JWT via `createDownloadToken`; `/api/sign/[token]/download/route.ts` validates token and streams signed file |

**Score:** 9/9 truths verified

---

### Required Artifacts

| Artifact | Expected | Status | Details |
|----------|----------|--------|---------|
| `src/lib/signing/token.ts` | JWT create/verify for signing + download tokens | VERIFIED | `createSigningToken` (72h, DB-stored jti), `verifySigningToken`, `createDownloadToken` (15m, no DB), `verifyDownloadToken` with purpose check |
| `src/lib/signing/audit.ts` | Persist audit events to DB | VERIFIED | Inserts to `auditEvents` table with documentId, eventType, ipAddress, userAgent, metadata; timestamp is server-side `defaultNow()` |
| `src/lib/signing/embed-signature.ts` | Embed signature PNG into PDF, return SHA-256 hash | VERIFIED | Uses `@cantoo/pdf-lib`; atomic tmp-then-rename write; SHA-256 computed from disk after rename |
| `src/lib/signing/signing-mailer.tsx` | Send signing request and agent notification emails | VERIFIED | Two exported functions: `sendSigningRequestEmail` (react-email rendered) and `sendAgentNotificationEmail` (plain text) |
| `src/app/api/documents/[id]/send/route.ts` | Agent-triggered POST to send signing email | VERIFIED | Auth-guarded, creates token, sends email, logs `email_sent` audit event, updates status to Sent |
| `src/app/api/sign/[token]/route.ts` | Public GET (validate/audit) and POST (atomic sign) | VERIFIED | GET logs `link_opened` + `document_viewed`, returns doc data; POST atomically claims token, embeds PDF, logs audit chain, updates documents row |
| `src/app/api/sign/[token]/pdf/route.ts` | Serve prepared PDF bytes authenticated by JWT | VERIFIED | JWT-only auth (no session), path traversal guard, serves with `Content-Disposition: inline` |
| `src/app/api/sign/[token]/download/route.ts` | Serve signed PDF via short-lived download token | VERIFIED | Validates `purpose: download` claim, path traversal guard, streams as attachment |
| `src/app/sign/[token]/page.tsx` | Public signing page — server-side token validation | VERIFIED | Validates JWT and one-time-use before rendering any UI; error pages for expired/used/invalid |
| `src/app/sign/[token]/_components/SigningPageClient.tsx` | Client PDF viewer with field overlays and submission | VERIFIED | `react-pdf` renders pages; field overlays computed with coordinate flip (PDF bottom-left to screen top-left); POST to `/api/sign/[token]` with dataURLs |
| `src/app/sign/[token]/confirmed/page.tsx` | Confirmation page with download link | VERIFIED | Shows document name, signed timestamp, download button with 15-min JWT link |
| `src/lib/db/schema.ts` (signingTokens) | DB table for one-time-use token tracking | VERIFIED | `jti` PK, `documentId` FK, `expiresAt`, `usedAt` |
| `src/lib/db/schema.ts` (auditEvents) | DB table for full audit trail | VERIFIED | 6 event types enumerated, IP, user-agent, metadata (JSONB), server-side timestamp |
| `src/lib/db/schema.ts` (documents columns) | `signedFilePath`, `pdfHash`, `signedAt`, `status` | VERIFIED | All four columns present; status enum includes `Signed` |

---

### Key Link Verification

| From | To | Via | Status | Details |
|------|----|-----|--------|---------|
| `send/route.ts` | `createSigningToken` | import + call | WIRED | Token created, URL built, email sent in sequence |
| `send/route.ts` | `sendSigningRequestEmail` | import + call | WIRED | Called with token URL and client email resolved from DB |
| `send/route.ts` | `logAuditEvent('email_sent')` | import + call | WIRED | Logged after email send succeeds |
| `sign/[token]/route.ts POST` | `embedSignatureInPdf` | import + await | WIRED | After atomic token claim; result (pdfHash) used in subsequent DB update |
| `sign/[token]/route.ts POST` | `logAuditEvent` (x3) | import + calls | WIRED | `signature_submitted`, `pdf_hash_computed` with hash metadata |
| `sign/[token]/route.ts POST` | `documents` update | drizzle `.update()` | WIRED | `status`, `signedAt`, `signedFilePath`, `pdfHash` all set |
| `SigningPageClient.tsx` | `/api/sign/${token}` POST | `fetch` in `handleSubmit` | WIRED | Sends `{ signatures: [...] }`, handles 200 and 409 |
| `SigningPageClient.tsx` | `/api/sign/${token}/pdf` | `react-pdf Document file=` | WIRED | PDF bytes fetched and rendered via react-pdf |
| `confirmed/page.tsx` | `createDownloadToken` | import + await | WIRED | 15-min token generated and embedded in download URL |
| `download/route.ts` | `verifyDownloadToken` | import + call | WIRED | Purpose claim validated; documentId extracted |

---

### Requirements Coverage

| Requirement | Description | Status | Evidence |
|-------------|-------------|--------|----------|
| SIGN-01 | Agent sends signing request email to client | SATISFIED | `POST /api/documents/[id]/send` creates JWT token and calls `sendSigningRequestEmail` |
| SIGN-02 | Client opens PDF in browser via email link | SATISFIED | `/sign/[token]/page.tsx` public server page; `/api/sign/[token]/pdf` serves bytes with JWT auth, no session |
| SIGN-03 | Client draws signature on document fields | SATISFIED | `SigningPageClient.tsx` renders `<SignatureModal>` on field click, captures canvas dataURL per field |
| SIGN-04 | Signed PDF is stored with embedded signature | SATISFIED | `embedSignatureInPdf` writes `_signed.pdf`; `signedFilePath` persisted to documents table |
| SIGN-05 | Token is single-use (replay prevention) | SATISFIED | Atomic `UPDATE … WHERE used_at IS NULL` returns 0 rows on replay; 409 returned without re-embedding |
| SIGN-06 | Client can download signed copy after signing | SATISFIED | `confirmed/page.tsx` + `download/route.ts` with 15-min scoped download token |
| LEGAL-01 | Audit trail: email sent, link opened, document viewed, signature submitted | SATISFIED | All four event types logged via `logAuditEvent` at correct points in `send/route.ts` and `sign/[token]/route.ts` |
| LEGAL-02 | SHA-256 hash of signed PDF stored | SATISFIED | `embedSignatureInPdf` returns SHA-256 hex; stored in `documents.pdfHash`; logged in `pdf_hash_computed` audit event with metadata |
| LEGAL-04 | IP address and user-agent captured in audit events | SATISFIED | `x-forwarded-for` / `x-real-ip` and `user-agent` headers extracted in both GET and POST handlers of `sign/[token]/route.ts` and passed to every `logAuditEvent` call |

**All 9 requirement IDs: SATISFIED.**

Note: LEGAL-03 was not listed in the phase requirement IDs supplied. If it exists in REQUIREMENTS.md it is not claimed by this phase and should be checked against another phase.

---

### Anti-Patterns Found

| File | Line | Pattern | Severity | Impact |
|------|------|---------|----------|--------|
| `src/app/api/sign/[token]/route.ts` | 229-246 | Agent notification email uses a second `db.query` to re-fetch document name, but passes `freshDoc?.name` for `clientName` (which is the document name, not client name) | Warning | Agent email body says "Client: {document name}" — cosmetic bug, not a legal or security issue |

No blockers found. No TODO/FIXME/placeholder patterns. No empty return stubs. All handlers return real data from DB queries.

---

### Human Verification Required

**1. Signature Canvas Rendering**
Test: Open `/sign/[token]` in a browser with a valid token and click a signature field.
Expected: Signature modal opens with a drawable canvas; drawing captures correctly and preview shows on the field overlay after confirmation.
Why human: Canvas API and signature_pad behavior cannot be statically verified.

**2. PDF Renders Inline Without Login**
Test: Copy the signing URL from a sent email and open it in an incognito/private browser window.
Expected: The prepared PDF renders page-by-page via react-pdf without any authentication prompt.
Why human: Public route behavior with JWT-only auth requires a live browser test.

**3. Signature Appears at Correct Position in Downloaded PDF**
Test: Complete a signing flow and download the signed copy.
Expected: Signature image appears at the exact location where the agent placed the field during the prepare step, not offset or clipped.
Why human: The coordinate transform (PDF bottom-left to screen top-left, applied in `getFieldOverlayStyle` and then inverted in `embedSignatureInPdf`) requires visual confirmation.

---

### Gaps Summary

No gaps. All 9 observable truths are verified as substantive, wired implementations. The signing flow is fully connected from email dispatch through token validation, PDF rendering, signature capture, atomic one-time-use enforcement, PDF embedding, SHA-256 hashing, audit trail persistence, and client download.

One cosmetic warning: the agent notification email passes document name as the `clientName` parameter (line 237 of `sign/[token]/route.ts`). This produces a misleading subject or body but does not affect correctness of the signing flow or legal defensibility of the audit trail.

---

_Verified: 2026-03-21_
_Verifier: Claude (gsd-verifier)_
