--- phase: 06-signing-flow plan: "05" type: execute wave: 4 depends_on: - "06-04" files_modified: - teressa-copeland-homes/src/app/sign/[token]/confirmed/page.tsx - teressa-copeland-homes/src/app/api/sign/[token]/download/route.ts - teressa-copeland-homes/src/app/sign/[token]/_components/SigningPageClient.tsx autonomous: true requirements: - SIGN-06 must_haves: truths: - "After POST /api/sign/[token] returns ok:true, the client is redirected to /sign/[token]/confirmed" - "Confirmation page shows: success checkmark, 'You've signed [Document Name]', timestamp of signing, and a 'Download your copy' button" - "The 'Download your copy' button downloads the signed PDF for the client via a short-lived download token (15-min TTL)" - "Revisiting an already-used /sign/[token] shows the 'Already Signed' state with signed date and a 'Download your copy' link" - "GET /api/sign/[token]/download validates a download JWT token (not the signing token) and streams the signedFilePath PDF" - "npm run build passes cleanly" artifacts: - path: "teressa-copeland-homes/src/app/sign/[token]/confirmed/page.tsx" provides: "Post-signing confirmation page with signed date + download button" - path: "teressa-copeland-homes/src/app/api/sign/[token]/download/route.ts" provides: "GET: validate download token, stream signedFilePath as PDF" key_links: - from: "confirmed/page.tsx" to: "/api/sign/[token]/download?dt=[downloadToken]" via: "download button href with short-lived token query param" - from: "download/route.ts" to: "documents.signedFilePath" via: "reads file from uploads/ and streams as application/pdf — never from public directory" - from: "SigningPageClient.tsx" to: "/sign/[token]/confirmed" via: "router.push after POST /api/sign/[token] returns ok:true" --- Build the post-signing confirmation screen and the client's ability to download a copy of their signed document via a short-lived token. Purpose: SIGN-06 — client must see confirmation after signing. The confirmation page is the final touch in the signing ceremony UX (locked decision: success checkmark, document name, timestamp, download button). Output: Confirmation page, already-signed download link, and /api/sign/[token]/download route for secure client PDF access. @/Users/ccopeland/.claude/get-shit-done/workflows/execute-plan.md @/Users/ccopeland/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/phases/06-signing-flow/06-CONTEXT.md @.planning/phases/06-signing-flow/06-RESEARCH.md @.planning/phases/06-signing-flow/06-04-SUMMARY.md From teressa-copeland-homes/src/lib/signing/token.ts: ```typescript export async function createSigningToken(documentId: string): Promise<{ token: string; jti: string; expiresAt: Date }> export async function verifySigningToken(token: string): Promise<{ documentId: string; jti: string; exp: number }> ``` Note: For the download token, use the same createSigningToken pattern but with a different purpose. The research recommends a second short-lived JWT with `purpose: 'download'` claim and 15-min TTL. Extend token.ts to add: ```typescript export async function createDownloadToken(documentId: string): Promise // SignJWT with { documentId, purpose: 'download' }, setExpirationTime('15m'), no DB record needed export async function verifyDownloadToken(token: string): Promise<{ documentId: string }> // jwtVerify + check payload.purpose === 'download' ``` From teressa-copeland-homes/src/lib/db/schema.ts: ```typescript // documents table — added in Plan 06-01: // signedFilePath: text — absolute path to signed PDF // signedAt: timestamp // pdfHash: text // // signingTokens table: // usedAt: timestamp — set when signing was completed ``` From Plan 06-04: POST /api/sign/[token] returns { ok: true } on success. SigningPageClient.tsx submits via fetch POST and receives this response — needs router.push to /sign/[token]/confirmed after ok:true. Task 1: Download token utilities + download API route teressa-copeland-homes/src/lib/signing/token.ts teressa-copeland-homes/src/app/api/sign/[token]/download/route.ts **Extend src/lib/signing/token.ts** — add two new exports for the client download token. Append to the end of the existing token.ts file: ```typescript // Short-lived download token for client copy download (15-min TTL, no DB record) // purpose: 'download' claim distinguishes from signing tokens export async function createDownloadToken(documentId: string): Promise { return await new SignJWT({ documentId, purpose: 'download' }) .setProtectedHeader({ alg: 'HS256' }) .setIssuedAt() .setExpirationTime('15m') .sign(getSecret()); } export async function verifyDownloadToken(token: string): Promise<{ documentId: string }> { const { payload } = await jwtVerify(token, getSecret()); if (payload['purpose'] !== 'download') throw new Error('Not a download token'); return { documentId: payload['documentId'] as string }; } ``` **Create src/app/api/sign/[token]/download/route.ts** — GET handler: The `[token]` in this path is the SIGNING token (used to identify the document via context). The actual download authorization uses a `dt` query parameter (the short-lived download token). Logic: 1. Resolve signing token from params, verify it (allows expired JWT here — signed docs should remain downloadable briefly after link expiry) - Actually: do NOT use the signing token for auth here. Instead, use ONLY the `dt` query param for authorization. 2. Get `dt` from URL search params: `const url = new URL(req.url); const dt = url.searchParams.get('dt')` 3. If no `dt`, return 401 4. Call `verifyDownloadToken(dt)` — if throws, return 401 "Download link expired or invalid" 5. Fetch document by `documentId` from the verified download token payload 6. Guard: if `doc.signedFilePath` is null, return 404 "Signed PDF not found" 7. Path traversal guard: ensure `doc.signedFilePath` starts with expected uploads/ prefix (same guard pattern used in Phase 4) 8. Read file: `const fileBuffer = await readFile(doc.signedFilePath)` 9. Return as streaming PDF response: ```typescript return new Response(fileBuffer, { headers: { 'Content-Type': 'application/pdf', 'Content-Disposition': `attachment; filename="${doc.name.replace(/[^a-zA-Z0-9-_ ]/g, '')}_signed.pdf"`, }, }); ``` No agent auth required — download token is the authorization mechanism. cd /Users/ccopeland/temp/red/teressa-copeland-homes && npm run build 2>&1 | grep -E "sign/\[token\]/download" | head -5 createDownloadToken and verifyDownloadToken exported from token.ts; download/route.ts exists; build passes Task 2: Confirmation page + redirect from signing client teressa-copeland-homes/src/app/sign/[token]/confirmed/page.tsx teressa-copeland-homes/src/app/sign/[token]/_components/SigningPageClient.tsx **Create src/app/sign/[token]/confirmed/page.tsx** — server component showing signing confirmation (locked UX decisions from CONTEXT.md): - Success checkmark - "You've signed [Document Name]" - Timestamp of signing - "Download your copy" button (generates a download token, passes as `dt` query param) - Clean thank-you only — no agent contact info on the page (locked decision) ```typescript import { verifySigningToken } from '@/lib/signing/token'; import { createDownloadToken } from '@/lib/signing/token'; import { db } from '@/lib/db'; import { signingTokens, documents } from '@/lib/db/schema'; import { eq } from 'drizzle-orm'; interface Props { params: Promise<{ token: string }>; } export default async function ConfirmedPage({ params }: Props) { const { token } = await params; // Verify signing token to get documentId (allow expired — confirmation page visited after signing) let documentId: string | null = null; let jti: string | null = null; try { const payload = await verifySigningToken(token); documentId = payload.documentId; jti = payload.jti; } catch { // Token expired is OK here — signing may have happened right before expiry // Try to look up by jti if token body is still parseable } if (!documentId) { return
Document not found.
; } const doc = await db.query.documents.findFirst({ where: eq(documents.id, documentId), }); // Get signed timestamp from signingTokens.usedAt let signedAt: Date | null = null; if (jti) { const tokenRow = await db.query.signingTokens.findFirst({ where: eq(signingTokens.jti, jti) }); signedAt = tokenRow?.usedAt ?? doc?.signedAt ?? null; } if (!doc || !doc.signedFilePath) { return (

Document not yet available

Please check back shortly.

); } // Generate 15-minute download token for client copy const downloadToken = await createDownloadToken(doc.id); const downloadUrl = `/api/sign/${token}/download?dt=${downloadToken}`; const formattedDate = signedAt ? signedAt.toLocaleString('en-US', { month: 'long', day: 'numeric', year: 'numeric', hour: 'numeric', minute: '2-digit' }) : 'Just now'; return (
{/* Success checkmark */}

You've signed

{doc.name}

Signed on {formattedDate}

Download your copy

Download link valid for 15 minutes.

); } ``` **Update SigningPageClient.tsx** — add navigation to confirmed page after successful submission. Find the `handleSubmit` function (or where the POST response is handled). After receiving `{ ok: true }`: ```typescript import { useRouter } from 'next/navigation'; // ... const router = useRouter(); // After POST returns ok:true: router.push(`/sign/${token}/confirmed`); ``` The `token` prop is already passed from page.tsx to SigningPageClient. Use it for the redirect URL.
cd /Users/ccopeland/temp/red/teressa-copeland-homes && npm run build 2>&1 | grep -E "confirmed|error|Error" | grep -v "^$" | head -10 confirmed/page.tsx exists; SigningPageClient.tsx uses router.push to confirmed after successful submit; download route streams PDF; npm run build passes
- Build passes: `npm run build` exits 0 - Confirmation page: `ls teressa-copeland-homes/src/app/sign/*/confirmed/page.tsx` - Download route: `ls teressa-copeland-homes/src/app/api/sign/*/download/route.ts` - Download token functions: `grep -n "createDownloadToken\|verifyDownloadToken" teressa-copeland-homes/src/lib/signing/token.ts` - Client redirect: `grep -n "router.push\|confirmed" teressa-copeland-homes/src/app/sign/*/\_components/SigningPageClient.tsx` Post-signing flow complete when: confirmed page renders success checkmark + document name + signed timestamp + download button, download route streams the signedFilePath PDF using a 15-min token, SigningPageClient navigates to confirmed after successful POST, and npm run build passes. After completion, create `.planning/phases/06-signing-flow/06-05-SUMMARY.md`