From 9fe793630487c7dc82a2c812ed6ce61326972a64 Mon Sep 17 00:00:00 2001 From: Chandler Copeland Date: Sat, 21 Mar 2026 10:30:05 -0600 Subject: [PATCH] docs(07-audit-trail-and-download): create phase 7 plan 3 plans in 3 sequential waves: agent download token + API route (01), UI wiring for download button + signedAt column (02), human verification checkpoint (03). Covers SIGN-07 and LEGAL-03. Co-Authored-By: Claude Sonnet 4.6 --- .planning/ROADMAP.md | 9 +- .../07-audit-trail-and-download/07-01-PLAN.md | 284 +++++++++++++ .../07-audit-trail-and-download/07-02-PLAN.md | 396 ++++++++++++++++++ .../07-audit-trail-and-download/07-03-PLAN.md | 108 +++++ 4 files changed, 794 insertions(+), 3 deletions(-) create mode 100644 .planning/phases/07-audit-trail-and-download/07-01-PLAN.md create mode 100644 .planning/phases/07-audit-trail-and-download/07-02-PLAN.md create mode 100644 .planning/phases/07-audit-trail-and-download/07-03-PLAN.md diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index f0a8f43..8d04d78 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -137,9 +137,12 @@ Plans: 1. Agent can download the signed PDF from the dashboard via an authenticated presigned URL (5-minute TTL) 2. Signed PDFs are stored in a private local directory (not publicly accessible) — a direct or guessable URL returns an access error, not the file 3. Document status in the dashboard updates correctly to "Signed" after a signing ceremony completes -**Plans**: TBD +**Plans**: 3 plans -Plans: none yet +Plans: +- [ ] 07-01-PLAN.md — Agent download token utilities (createAgentDownloadToken/verifyAgentDownloadToken in token.ts) + GET /api/documents/[id]/download route with 5-min presigned JWT and path traversal guard +- [ ] 07-02-PLAN.md — PreparePanel Signed-state panel with Download button, document detail page server-side token generation, DocumentsTable Date Signed column, dashboard signedAt select +- [ ] 07-03-PLAN.md — Full Phase 7 human verification checkpoint (SIGN-07 + LEGAL-03) ## Progress @@ -154,4 +157,4 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7 | 4. PDF Ingest | 4/4 | Complete | 2026-03-20 | | 5. PDF Fill and Field Mapping | 3/4 | In Progress| | | 6. Signing Flow | 6/6 | Complete | 2026-03-21 | -| 7. Audit Trail and Download | 0/? | Not started | - | +| 7. Audit Trail and Download | 0/3 | Not started | - | diff --git a/.planning/phases/07-audit-trail-and-download/07-01-PLAN.md b/.planning/phases/07-audit-trail-and-download/07-01-PLAN.md new file mode 100644 index 0000000..480c80e --- /dev/null +++ b/.planning/phases/07-audit-trail-and-download/07-01-PLAN.md @@ -0,0 +1,284 @@ +--- +phase: 07-audit-trail-and-download +plan: "01" +type: execute +wave: 1 +depends_on: [] +files_modified: + - teressa-copeland-homes/src/lib/signing/token.ts + - teressa-copeland-homes/src/app/api/documents/[id]/download/route.ts +autonomous: true +requirements: + - SIGN-07 + - LEGAL-03 + +must_haves: + truths: + - "GET /api/documents/[id]/download?adt=[token] streams the signed PDF when the agent-download JWT is valid" + - "A missing or expired adt token returns 401 — no file served" + - "An adt token for document A cannot download document B (route ID vs token documentId mismatch returns 403)" + - "A signedFilePath containing path traversal characters returns 403" + - "A document with no signedFilePath (unsigned) returns 404" + artifacts: + - path: "teressa-copeland-homes/src/lib/signing/token.ts" + provides: "createAgentDownloadToken and verifyAgentDownloadToken exports" + contains: "purpose: 'agent-download'" + - path: "teressa-copeland-homes/src/app/api/documents/[id]/download/route.ts" + provides: "GET handler streaming signed PDF for authenticated agent download" + exports: ["GET"] + key_links: + - from: "teressa-copeland-homes/src/app/api/documents/[id]/download/route.ts" + to: "teressa-copeland-homes/src/lib/signing/token.ts" + via: "verifyAgentDownloadToken import" + pattern: "verifyAgentDownloadToken" + - from: "route.ts download handler" + to: "uploads/ directory on disk" + via: "readFile + path.join(UPLOADS_DIR, signedFilePath) + startsWith guard" + pattern: "absPath.startsWith\\(UPLOADS_DIR\\)" +--- + + +Add agent-authenticated PDF download: extend token.ts with agent-download JWT utilities and create GET /api/documents/[id]/download route that streams signed PDFs behind a 5-min presigned token. + +Purpose: Satisfy LEGAL-03 (signed PDFs never accessible via guessable public URLs; agent downloads via authenticated presigned URLs only) and provide the API surface SIGN-07 requires. + +Output: Two files — updated token.ts with agent download token functions, new download API route. + + + +@/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/STATE.md +@.planning/phases/07-audit-trail-and-download/07-RESEARCH.md + + + + + +```typescript +import { SignJWT, jwtVerify } from 'jose'; +import { db } from '@/lib/db'; +import { signingTokens } from '@/lib/db/schema'; + +const getSecret = () => new TextEncoder().encode(process.env.SIGNING_JWT_SECRET!); + +// Existing exports (keep all of these): +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 }> + +// Client download token — purpose: 'download', 15-min TTL, no DB record +export async function createDownloadToken(documentId: string): Promise +export async function verifyDownloadToken(token: string): Promise<{ documentId: string }> + +// Phase 7 adds agent download token — purpose: 'agent-download', 5-min TTL, no DB record +// ADD: createAgentDownloadToken and verifyAgentDownloadToken +``` + + + + +```typescript +// Pattern: query param token (dt=) → verify → path traversal guard → readFile → Response +// Agent route uses same pattern with adt= query param and purpose: 'agent-download' +const UPLOADS_DIR = path.join(process.cwd(), 'uploads'); + +export async function GET(req: NextRequest) { + const url = new URL(req.url); + const dt = url.searchParams.get('dt'); + // ... verifyDownloadToken(dt) → doc.signedFilePath → absPath traversal guard → readFile → Response + const absPath = path.join(UPLOADS_DIR, doc.signedFilePath); + if (!absPath.startsWith(UPLOADS_DIR)) return 403; + // new Response(new Uint8Array(fileBuffer), { 'Content-Disposition': 'attachment; filename=...' }) +} +``` + + + + +```typescript +status: documentStatusEnum("status").notNull().default("Draft"), +signedFilePath: text("signed_file_path"), // null until signed +pdfHash: text("pdf_hash"), +signedAt: timestamp("signed_at"), // null until signed +``` + + + + + + + Task 1: Add agent download token functions to token.ts + teressa-copeland-homes/src/lib/signing/token.ts + +Append two new exported functions to the end of the existing token.ts. Do NOT modify or remove any existing functions. + +Add: + +```typescript +// Agent download token — purpose: 'agent-download', 5-min TTL, no DB record +// Generated server-side only (server component or API route). Never in a client component. +export async function createAgentDownloadToken(documentId: string): Promise { + return await new SignJWT({ documentId, purpose: 'agent-download' }) + .setProtectedHeader({ alg: 'HS256' }) + .setIssuedAt() + .setExpirationTime('5m') + .sign(getSecret()); +} + +export async function verifyAgentDownloadToken(token: string): Promise<{ documentId: string }> { + const { payload } = await jwtVerify(token, getSecret()); + if (payload['purpose'] !== 'agent-download') throw new Error('Not an agent download token'); + return { documentId: payload['documentId'] as string }; +} +``` + +This is consistent with the existing createDownloadToken/verifyDownloadToken pattern (purpose: 'download', 15m TTL) — same signing secret, different purpose string, shorter TTL per success criterion. + + + cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -20 + + token.ts exports createAgentDownloadToken and verifyAgentDownloadToken; tsc --noEmit passes; existing exports are untouched + + + + Task 2: Create GET /api/documents/[id]/download route + teressa-copeland-homes/src/app/api/documents/[id]/download/route.ts + +Create the directory and file `teressa-copeland-homes/src/app/api/documents/[id]/download/route.ts`. + +This is the agent-facing download endpoint. It mirrors the existing `/api/sign/[token]/download/route.ts` pattern exactly, substituting `verifyAgentDownloadToken` for `verifyDownloadToken` and adding a document ID cross-check. + +Implementation: + +```typescript +import { NextRequest } from 'next/server'; +import { verifyAgentDownloadToken } from '@/lib/signing/token'; +import { db } from '@/lib/db'; +import { documents } from '@/lib/db/schema'; +import { eq } from 'drizzle-orm'; +import path from 'node:path'; +import { readFile } from 'node:fs/promises'; + +const UPLOADS_DIR = path.join(process.cwd(), 'uploads'); + +// GET /api/documents/[id]/download?adt=[agentDownloadToken] +// Requires: valid agent-download JWT in adt query param (generated server-side in document detail page) +// No Auth.js session check at this route — the short-lived JWT IS the credential (same as client download pattern) +export async function GET( + req: NextRequest, + { params }: { params: Promise<{ id: string }> } +) { + const { id } = await params; + const url = new URL(req.url); + const adt = url.searchParams.get('adt'); + + if (!adt) { + return new Response(JSON.stringify({ error: 'Missing download token' }), { + status: 401, + headers: { 'Content-Type': 'application/json' }, + }); + } + + let documentId: string; + try { + const verified = await verifyAgentDownloadToken(adt); + documentId = verified.documentId; + } catch { + return new Response(JSON.stringify({ error: 'Download link expired or invalid' }), { + status: 401, + headers: { 'Content-Type': 'application/json' }, + }); + } + + // Defense in depth: token documentId must match route [id] param + if (documentId !== id) { + return new Response(JSON.stringify({ error: 'Forbidden' }), { + status: 403, + headers: { 'Content-Type': 'application/json' }, + }); + } + + const doc = await db.query.documents.findFirst({ + where: eq(documents.id, documentId), + columns: { id: true, name: true, signedFilePath: true }, + }); + + if (!doc || !doc.signedFilePath) { + return new Response(JSON.stringify({ error: 'Signed PDF not found' }), { + status: 404, + headers: { 'Content-Type': 'application/json' }, + }); + } + + // Path traversal guard — required on every file read from uploads/ + const absPath = path.join(UPLOADS_DIR, doc.signedFilePath); + if (!absPath.startsWith(UPLOADS_DIR)) { + return new Response(JSON.stringify({ error: 'Forbidden' }), { + status: 403, + headers: { 'Content-Type': 'application/json' }, + }); + } + + let fileBuffer: Buffer; + try { + fileBuffer = await readFile(absPath); + } catch { + return new Response(JSON.stringify({ error: 'File not found on disk' }), { + status: 404, + headers: { 'Content-Type': 'application/json' }, + }); + } + + const safeName = doc.name.replace(/[^a-zA-Z0-9-_ ]/g, ''); + return new Response(new Uint8Array(fileBuffer), { + headers: { + 'Content-Type': 'application/pdf', + 'Content-Disposition': `attachment; filename="${safeName}_signed.pdf"`, + }, + }); +} +``` + +Key invariants: +- `new Uint8Array(fileBuffer)` not `fileBuffer` directly — required for Next.js 16 TypeScript strict mode (Buffer is not assignable to BodyInit; established in Phase 6) +- `absPath.startsWith(UPLOADS_DIR)` traversal guard before every readFile — never skip +- Token `documentId` === route `id` cross-check — prevents token for doc A downloading doc B + + + cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -20 && npm run build 2>&1 | tail -10 + + + Route file exists at correct path; tsc --noEmit passes; npm run build passes; + GET /api/documents/[id]/download without adt returns 401; + GET /api/documents/[id]/download with expired/invalid token returns 401; + GET /api/documents/[id]/download with valid token for a non-existent docId returns 404 + + + + + + +1. `npx tsc --noEmit` passes with no errors +2. `npm run build` completes successfully +3. `src/lib/signing/token.ts` exports: createSigningToken, verifySigningToken, createDownloadToken, verifyDownloadToken, createAgentDownloadToken, verifyAgentDownloadToken (6 total) +4. Route file exists at `src/app/api/documents/[id]/download/route.ts` +5. Route contains `absPath.startsWith(UPLOADS_DIR)` guard +6. Route verifies `documentId !== id` mismatch returns 403 + + + +- createAgentDownloadToken creates a JWT with purpose:'agent-download' and 5-min expiry +- verifyAgentDownloadToken throws if purpose is not 'agent-download' +- GET /api/documents/[id]/download streams signed PDF when adt JWT is valid +- Route returns 401 for missing/expired token, 403 for ID mismatch, 403 for path traversal, 404 for unsigned document +- No new npm packages required; no existing exports modified + + + +After completion, create `.planning/phases/07-audit-trail-and-download/07-01-SUMMARY.md` + diff --git a/.planning/phases/07-audit-trail-and-download/07-02-PLAN.md b/.planning/phases/07-audit-trail-and-download/07-02-PLAN.md new file mode 100644 index 0000000..8380a68 --- /dev/null +++ b/.planning/phases/07-audit-trail-and-download/07-02-PLAN.md @@ -0,0 +1,396 @@ +--- +phase: 07-audit-trail-and-download +plan: "02" +type: execute +wave: 2 +depends_on: + - "07-01" +files_modified: + - teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/page.tsx + - teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx + - teressa-copeland-homes/src/app/portal/_components/DocumentsTable.tsx + - teressa-copeland-homes/src/app/portal/(protected)/dashboard/page.tsx +autonomous: true +requirements: + - SIGN-07 + - LEGAL-03 + +must_haves: + truths: + - "Agent sees a Download button on the document detail page when document status is Signed" + - "Clicking the Download button triggers browser PDF download dialog (no login prompt, no 404)" + - "Download button is absent when document status is Draft, Sent, or Viewed" + - "Dashboard table shows a Date Signed column populated for Signed documents" + - "Dashboard StatusBadge shows Signed for documents that have completed signing" + artifacts: + - path: "teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/page.tsx" + provides: "Server component that generates agentDownloadUrl and passes it to PreparePanel" + contains: "createAgentDownloadToken" + - path: "teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx" + provides: "Download button rendered only when currentStatus === Signed and agentDownloadUrl is non-null" + contains: "agentDownloadUrl" + - path: "teressa-copeland-homes/src/app/portal/_components/DocumentsTable.tsx" + provides: "DocumentRow type with signedAt field + Date Signed column in table" + contains: "signedAt" + - path: "teressa-copeland-homes/src/app/portal/(protected)/dashboard/page.tsx" + provides: "Select includes signedAt from documents table" + contains: "signedAt: documents.signedAt" + key_links: + - from: "teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/page.tsx" + to: "teressa-copeland-homes/src/lib/signing/token.ts" + via: "import { createAgentDownloadToken } from '@/lib/signing/token'" + pattern: "createAgentDownloadToken" + - from: "teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/page.tsx" + to: "PreparePanel" + via: "agentDownloadUrl prop" + pattern: "agentDownloadUrl" + - from: "teressa-copeland-homes/src/app/portal/(protected)/dashboard/page.tsx" + to: "DocumentsTable" + via: "rows prop including signedAt field" + pattern: "signedAt" +--- + + +Wire the agent-facing download UI: generate a presigned download URL in the document detail server component and pass it to PreparePanel for Signed documents; add signedAt to the dashboard table. + +Purpose: Complete SIGN-07 (agent can download signed PDF) by surfacing the API from Plan 01 in the portal UI. Satisfies LEGAL-03 by ensuring the only download path is the presigned token route — no direct file URLs anywhere in the UI. + +Output: Four modified files — document detail page (token generation), PreparePanel (Download button), DocumentsTable (signedAt type + column), dashboard page (signedAt select). + + + +@/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/STATE.md +@.planning/phases/07-audit-trail-and-download/07-RESEARCH.md +@.planning/phases/07-audit-trail-and-download/07-01-SUMMARY.md + + + + + +```typescript +// Current: fetches doc + docClient, renders PdfViewerWrapper + PreparePanel +// Phase 7 change: generate agentDownloadUrl server-side for Signed docs and pass as prop + +export default async function DocumentPage({ params }: { params: Promise<{ docId: string }> }) { + // ...existing auth, db fetch... + // ADD after fetching doc: + const agentDownloadUrl = doc.signedFilePath + ? `/api/documents/${docId}/download?adt=${await createAgentDownloadToken(docId)}` + : null; + + // MODIFY PreparePanel call to pass new props: + +} +``` + + + + +```typescript +// Current interface: +interface PreparePanelProps { + docId: string; + defaultEmail: string; + clientName: string; + currentStatus: string; +} + +// Phase 7 change: extend interface, add Download button section for Signed status +// CRITICAL: Token generation must NOT happen in PreparePanel — PreparePanel is 'use client' +// PreparePanel only renders an anchor — it does not call createAgentDownloadToken + +// Current non-Draft status return (replace with status-aware rendering): +if (currentStatus !== 'Draft') { + return ( +
+ Document status is {currentStatus} — preparation is only available for Draft documents. +
+ ); +} +// Phase 7: When currentStatus === 'Signed' and agentDownloadUrl !== null, show download section instead +// When currentStatus === 'Sent' or 'Viewed', keep the read-only message (no download button) +``` + + + + +```typescript +type DocumentRow = { + id: string; + name: string; + clientName: string | null; + status: "Draft" | "Sent" | "Viewed" | "Signed"; + sentAt: Date | null; + clientId: string; + // ADD: signedAt: Date | null; +}; +``` + + + + +```typescript +const allRows = await db + .select({ + id: documents.id, + name: documents.name, + status: documents.status, + sentAt: documents.sentAt, + clientName: clients.name, + clientId: documents.clientId, + // ADD: signedAt: documents.signedAt, + }) + .from(documents) + // ... +``` + + +```typescript +status: documentStatusEnum("status").notNull().default("Draft"), +signedFilePath: text("signed_file_path"), // null until signed +signedAt: timestamp("signed_at"), // null until signed +``` + + +```typescript +// From teressa-copeland-homes/src/lib/signing/token.ts (after Plan 01): +export async function createAgentDownloadToken(documentId: string): Promise +// Returns a JWT with purpose:'agent-download', 5-min TTL +// Used in server component only — import forbidden in 'use client' files +``` +
+
+ + + + + Task 1: Update PreparePanel props interface and add Download button for Signed status + teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx + +Two changes to PreparePanel.tsx: + +**1. Extend PreparePanelProps interface:** +```typescript +interface PreparePanelProps { + docId: string; + defaultEmail: string; + clientName: string; + currentStatus: string; + agentDownloadUrl?: string | null; // ADD + signedAt?: Date | null; // ADD +} +``` + +**2. Replace the non-Draft early return block with status-aware rendering.** Currently the function returns a generic message for any non-Draft status. Replace with: + +```typescript +// For Signed status: show download section with optional date +if (currentStatus === 'Signed') { + return ( +
+

+ Document Signed +

+ {signedAt && ( +

+ Signed on{' '} + {new Date(signedAt).toLocaleString('en-US', { + timeZone: 'America/Denver', + month: 'short', + day: 'numeric', + year: 'numeric', + hour: 'numeric', + minute: '2-digit', + })} +

+ )} + {agentDownloadUrl ? ( +
+ Download Signed PDF + + ) : ( +

Signed PDF not available.

+ )} +
+ ); +} + +// For Sent/Viewed: keep existing read-only message +if (currentStatus !== 'Draft') { + return ( +
+ Document status is {currentStatus} — preparation is only available for Draft documents. +
+ ); +} +``` + +Important: The Download button is a plain `` anchor — no fetch(), no onClick handler. The browser follows the link directly, which triggers the Content-Disposition: attachment response from the API route. + +Do not destructure `agentDownloadUrl` or `signedAt` from props in the function signature until you have updated the interface. Update interface first. + + + cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -20 + + PreparePanel accepts agentDownloadUrl and signedAt props; TypeScript passes; Signed status renders download section; Sent/Viewed status renders read-only message; Draft status renders full prepare form unchanged + + + + Task 2: Wire document detail page, update dashboard table for signedAt + + teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/page.tsx + teressa-copeland-homes/src/app/portal/_components/DocumentsTable.tsx + teressa-copeland-homes/src/app/portal/(protected)/dashboard/page.tsx + + +**File 1: documents/[docId]/page.tsx** + +Add import for createAgentDownloadToken at the top: +```typescript +import { createAgentDownloadToken } from '@/lib/signing/token'; +``` + +After the existing `const [doc, docClient] = await Promise.all([...])` block, add: +```typescript +// Generate agent download URL server-side for Signed documents +// Must be done here (server component) — PreparePanel is 'use client' and cannot call createAgentDownloadToken +const agentDownloadUrl = doc.signedFilePath + ? `/api/documents/${docId}/download?adt=${await createAgentDownloadToken(docId)}` + : null; +``` + +Pass the two new props to PreparePanel: +```tsx + +``` + +Note: `doc.signedAt` is available on the document object — it's a column in the documents table (timestamp("signed_at")). + +--- + +**File 2: DocumentsTable.tsx** + +Add `signedAt: Date | null` to the DocumentRow type: +```typescript +type DocumentRow = { + id: string; + name: string; + clientName: string | null; + status: "Draft" | "Sent" | "Viewed" | "Signed"; + sentAt: Date | null; + signedAt: Date | null; // ADD + clientId: string; +}; +``` + +Add a "Date Signed" column header after the "Date Sent" ``: +```tsx + + Date Signed + +``` + +Add a "Date Signed" `` in the row map after the "Date Sent" cell: +```tsx + + {row.signedAt + ? new Date(row.signedAt).toLocaleDateString("en-US", { + timeZone: "America/Denver", + month: "short", + day: "numeric", + year: "numeric", + }) + : "—"} + +``` + +--- + +**File 3: dashboard/page.tsx** + +Add `signedAt: documents.signedAt` to the select object: +```typescript +const allRows = await db + .select({ + id: documents.id, + name: documents.name, + status: documents.status, + sentAt: documents.sentAt, + signedAt: documents.signedAt, // ADD + clientName: clients.name, + clientId: documents.clientId, + }) + .from(documents) + .leftJoin(clients, eq(documents.clientId, clients.id)) + .orderBy(desc(documents.createdAt)); +``` + +The `allRows` inferred type will now include `signedAt: Date | null`, which matches the updated DocumentRow type in DocumentsTable.tsx. + + + cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -20 && npm run build 2>&1 | tail -10 + + + tsc --noEmit passes with no errors; + npm run build passes cleanly; + DocumentsTable accepts rows with signedAt field; + Dashboard query selects signedAt; + Document detail page imports and calls createAgentDownloadToken for signed docs; + PreparePanel receives agentDownloadUrl and signedAt props + + + + + + +1. `npx tsc --noEmit` passes — no type errors across all four modified files +2. `npm run build` completes successfully +3. PreparePanel renders three distinct states: Draft (prepare form), Sent/Viewed (read-only message), Signed (green panel with download link) +4. agentDownloadUrl is generated in the server component (page.tsx), not in PreparePanel +5. DocumentsTable has Date Signed column +6. Dashboard query includes signedAt in select + + + +- Agent can navigate to a Signed document detail page and see a green "Document Signed" panel with signed timestamp and "Download Signed PDF" anchor link +- Download button is absent for Draft/Sent/Viewed documents +- Dashboard table shows "Date Signed" column with date for Signed documents, "—" for others +- Build passes with no TypeScript errors + + + +After completion, create `.planning/phases/07-audit-trail-and-download/07-02-SUMMARY.md` + diff --git a/.planning/phases/07-audit-trail-and-download/07-03-PLAN.md b/.planning/phases/07-audit-trail-and-download/07-03-PLAN.md new file mode 100644 index 0000000..689a9f3 --- /dev/null +++ b/.planning/phases/07-audit-trail-and-download/07-03-PLAN.md @@ -0,0 +1,108 @@ +--- +phase: 07-audit-trail-and-download +plan: "03" +type: execute +wave: 3 +depends_on: + - "07-02" +files_modified: [] +autonomous: false +requirements: + - SIGN-07 + - LEGAL-03 + +must_haves: + truths: + - "Agent downloads the signed PDF from the document detail page and receives the actual file" + - "Dashboard shows Signed status badge for the signed document" + - "Dashboard shows a non-empty Date Signed value for the signed document" + - "Accessing uploads/ directory or file directly via guessable URL returns an error, not the file" + artifacts: [] + key_links: [] +--- + + +Human verification checkpoint: confirm that the agent-facing download, Signed status badge, private storage guard, and download-only-for-Signed rule all pass the Phase 7 success criteria. + +Purpose: Satisfy the observable success criteria for SIGN-07 and LEGAL-03 through direct browser verification. + +Output: Human confirmation that all Phase 7 success criteria are met (or issue report for gap closure). + + + +@/Users/ccopeland/.claude/get-shit-done/workflows/execute-plan.md +@/Users/ccopeland/.claude/get-shit-done/templates/summary.md + + + +@.planning/ROADMAP.md +@.planning/phases/07-audit-trail-and-download/07-02-SUMMARY.md + + + + + + Task 1: Full Phase 7 browser verification + Start the dev server and walk through the Phase 7 verification checklist below. No code changes needed — this task is observation only. + none + Human approval of all 4 verification criteria below. + Agent confirms: download button works, Signed badge shows, Date Signed populates, guessable URLs return 404. + +Plan 01: Agent-authenticated download API at GET /api/documents/[id]/download — 5-min presigned JWT (adt query param), path traversal guard, streams signedFilePath PDF. + +Plan 02: +- Document detail page: generates agentDownloadUrl server-side for Signed docs, passes to PreparePanel +- PreparePanel: green "Document Signed" panel with signed timestamp + "Download Signed PDF" anchor for Signed status; unchanged prepare form for Draft; read-only message for Sent/Viewed +- DocumentsTable: Date Signed column added to table +- Dashboard page: signedAt included in DB select + + +Start the dev server: `cd teressa-copeland-homes && npm run dev` + +Prerequisite: You need a document with status "Signed" and signedFilePath populated in the DB. Use an existing signed document from Phase 6 testing, or run through a quick signing ceremony now. + +**Criterion 1 — Agent download (SIGN-07):** +1. Log in to the portal at http://localhost:3000/portal/dashboard +2. Find a document with status "Signed" — confirm the "Date Signed" column shows a formatted date (not "—") +3. Click the document name to navigate to the document detail page (/portal/documents/[id]) +4. Confirm the right sidebar shows a green panel labeled "Document Signed" with the signed date/time and a "Download Signed PDF" button +5. Click "Download Signed PDF" — confirm browser PDF download dialog appears and the file saves successfully +6. Open the downloaded file — confirm it is a PDF containing the drawn signature + +**Criterion 2 — Status badge (SIGN-07 success criterion 3):** +7. Return to http://localhost:3000/portal/dashboard +8. Confirm the signed document's Status column shows "Signed" badge (green/teal styling) + +**Criterion 3 — Private storage (LEGAL-03):** +9. In the browser address bar, visit: http://localhost:3000/uploads/ +10. Confirm it returns 404 — NOT a directory listing or file contents +11. Also try: http://localhost:3000/uploads/clients/ — confirm 404 + +**Criterion 4 — Download button absent for non-Signed documents:** +12. Navigate to any document with status "Draft", "Sent", or "Viewed" +13. Confirm the right sidebar does NOT show a "Download Signed PDF" button (Draft shows prepare form; Sent/Viewed shows read-only status message) + + +Type "approved" if all 4 criteria pass. +Or describe which criterion failed and what you observed — Claude will diagnose and create a gap closure plan. + + + + + + +All three Phase 7 roadmap success criteria verified by human: +1. Agent can download the signed PDF from the document detail page via authenticated presigned URL (5-minute TTL) +2. Signed PDFs are stored in a private local directory — a direct or guessable URL returns 404, not the file +3. Document status in the dashboard updates correctly to "Signed" after a signing ceremony completes + + + +- Human types "approved" after verifying all 4 browser checks +- Phase 7 is marked complete in ROADMAP.md and STATE.md +- REQUIREMENTS.md checkboxes for SIGN-07 and LEGAL-03 updated to [x] + + + +After completion, create `.planning/phases/07-audit-trail-and-download/07-03-SUMMARY.md` +