From 5f2507a69e2a07d8ac6e0a345a4b17c0d4a6fabf Mon Sep 17 00:00:00 2001 From: Chandler Copeland Date: Sat, 21 Mar 2026 11:02:37 -0600 Subject: [PATCH] docs(phase-7): complete phase execution and verification --- .planning/ROADMAP.md | 2 +- .planning/STATE.md | 2 +- .../07-VERIFICATION.md | 199 +++++++++--------- 3 files changed, 103 insertions(+), 100 deletions(-) diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 2c0a83a..dc0a277 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -157,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 | 4/4 | Complete | 2026-03-21 | +| 7. Audit Trail and Download | 4/4 | Complete | 2026-03-21 | diff --git a/.planning/STATE.md b/.planning/STATE.md index b11be92..cc3e9fc 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -3,7 +3,7 @@ gsd_state_version: 1.0 milestone: v1.0 milestone_name: milestone status: unknown -last_updated: "2026-03-21T16:56:31.521Z" +last_updated: "2026-03-21T17:02:31.470Z" progress: total_phases: 7 completed_phases: 7 diff --git a/.planning/phases/07-audit-trail-and-download/07-VERIFICATION.md b/.planning/phases/07-audit-trail-and-download/07-VERIFICATION.md index b58e093..427d2c0 100644 --- a/.planning/phases/07-audit-trail-and-download/07-VERIFICATION.md +++ b/.planning/phases/07-audit-trail-and-download/07-VERIFICATION.md @@ -1,48 +1,49 @@ --- phase: 07-audit-trail-and-download -verified: 2026-03-21T00:00:00Z -status: gaps_found -score: 8/10 must-haves verified -re_verification: false -gaps: - - truth: "Signed PDFs are stored in a private local directory — a direct or guessable URL returns an access error, not the file" - status: partial - reason: "GET /api/documents/[id]/download (presigned, 5-min TTL) is implemented and correct. However, a separate route GET /api/documents/[id]/file serves the signed PDF using only an Auth.js session cookie — no short-lived token. PdfViewer.tsx renders a persistent Download anchor pointing to this session-authenticated route. LEGAL-03 states 'agent downloads via authenticated presigned URLs only,' but the /file route allows indefinite download for any logged-in session, bypassing the presigned token system." - artifacts: - - path: "teressa-copeland-homes/src/app/api/documents/[id]/file/route.ts" - issue: "Serves signedFilePath (signed PDF) via session auth only. No short-lived token. Contradicts LEGAL-03's 'presigned URLs only' requirement." - - path: "teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewer.tsx" - issue: "Renders — a persistent session-based download link for the signed PDF, bypassing the presigned URL system." - missing: - - "Determine whether the /file route was intentional (PDF viewer needs to load the signed PDF for display) or an oversight" - - "If /file must serve the signed PDF for viewing, ensure the Download button in PdfViewer is removed or replaced with the presigned URL anchor from PreparePanel" - - "If /file must not serve signed PDFs at all, restrict it to serve only the unsigned original (filePath) and block signedFilePath serving" - - truth: "LEGAL-03: agent downloads via authenticated presigned URLs only" - status: failed - reason: "Two download paths exist for signed PDFs: (1) the new /api/documents/[id]/download?adt=[token] presigned route, which is correct; (2) the pre-existing /api/documents/[id]/file route, which serves signedFilePath ?? filePath behind a session check. Path (2) is not a presigned URL and violates the 'only' clause of LEGAL-03." - artifacts: - - path: "teressa-copeland-homes/src/app/api/documents/[id]/file/route.ts" - issue: "Line 25: serves signed PDF via session auth. Should be restricted to original filePath or gated behind a short-lived token." - missing: - - "Either restrict /file to serve only the original unsigned PDF (remove signedFilePath fallback), or require a presigned token on this route as well" +verified: 2026-03-21T17:00:00Z +status: human_needed +score: 10/10 must-haves verified +re_verification: true +previous_status: gaps_found +previous_score: 8/10 +gaps_closed: + - "GET /api/documents/[id]/file NEVER serves the signed PDF — signedFilePath fallback removed, doc.filePath only" + - "LEGAL-03: sole signed PDF download path is now /api/documents/[id]/download?adt=[token] — /file route confirmed clean" + - "PdfViewer Download anchor absent when docStatus is 'Signed' — {docStatus !== 'Signed' && ...} conditional confirmed at line 60" +gaps_remaining: [] +regressions: [] human_verification: - - test: "Navigate to a Signed document in the portal (http://localhost:3000/portal/documents/[id]) and click the Download button in the PDF viewer toolbar (top left of the viewer, not the PreparePanel button)" - expected: "The viewer's own Download link goes through /api/documents/[id]/file — verify whether this also serves the signed PDF or only the original" - why_human: "Programmatic analysis shows the route serves signedFilePath ?? filePath, but runtime behavior (whether the document in question has signedFilePath set) cannot be confirmed statically" - - test: "With a valid Auth.js session, visit /api/documents/[known-uuid]/file directly in the browser for a Signed document" - expected: "If LEGAL-03 is fully satisfied, this should NOT serve the signed PDF — should redirect to the presigned download or return 403. If it serves the signed PDF, LEGAL-03 is violated." - why_human: "This is the core conflict between the /file route and LEGAL-03. Requires a running app and a Signed document in the DB." - - test: "Confirm that http://localhost:3000/uploads/ and http://localhost:3000/uploads/clients/ return 404 (not a directory listing)" - expected: "Both URLs return 404" - why_human: "Requires a running dev or prod server — cannot verify static file serving behavior programmatically" + - test: "Navigate to a Signed document detail page (http://localhost:3000/portal/documents/[id]) and inspect the PDF viewer toolbar" + expected: "The toolbar Download anchor is absent for Signed documents. Only the PreparePanel 'Download Signed PDF' button should be visible." + why_human: "The {docStatus !== 'Signed' && ...} conditional is confirmed in code, but runtime rendering with an actual Signed document cannot be verified statically." + - test: "With a valid Auth.js session, visit /api/documents/[known-signed-uuid]/file directly in the browser" + expected: "The response is the original unsigned PDF — NOT the signed PDF. The signed PDF is available only via /api/documents/[id]/download?adt=[token]." + why_human: "The signedFilePath reference is confirmed absent from /file route, but runtime behavior (correct original file serving) requires a running app and a Signed document in the DB." + - test: "Visit http://localhost:3000/uploads/ and http://localhost:3000/uploads/clients/ in a browser" + expected: "Both URLs return 404 — no directory listing or file contents." + why_human: "Requires a running dev or prod server — static file serving behavior cannot be verified programmatically." + - test: "Click 'Download Signed PDF' in PreparePanel for a Signed document" + expected: "Browser PDF download dialog appears; downloaded file is a PDF containing the drawn signature." + why_human: "End-to-end flow requires a running app and a Signed document with signedFilePath populated." --- # Phase 7: Audit Trail and Download — Verification Report **Phase Goal:** Agent can download any signed PDF securely, and signed documents are never accessible via guessable public URLs **Verified:** 2026-03-21 -**Status:** gaps_found -**Re-verification:** No — initial verification +**Status:** human_needed +**Re-verification:** Yes — after gap closure (Plan 07-04) + +--- + +## Re-verification Summary + +| Item | Previous Status | Current Status | +|------|----------------|----------------| +| Gap 1: `/file` route serving signedFilePath | PARTIAL | CLOSED | +| Gap 2: LEGAL-03 two download paths | FAILED | CLOSED | + +Both gaps from the initial verification were closed by Plan 07-04. All 10 observable truths now pass automated verification. Remaining items require human testing with a running application. --- @@ -50,22 +51,20 @@ human_verification: ### Observable Truths -The must-haves from the three PLANs are consolidated below. Plans 01 and 02 each defined specific truths; Plan 03 defined human-verification-only truths. - | # | Truth | Source Plan | Status | Evidence | |----|-------|-------------|--------|----------| -| 1 | GET /api/documents/[id]/download?adt=[token] streams the signed PDF when the agent-download JWT is valid | 07-01 | VERIFIED | Route exists, verifyAgentDownloadToken called, readFile + Response streaming confirmed | -| 2 | A missing or expired adt token returns 401 — no file served | 07-01 | VERIFIED | Lines 22-27 (missing adt) and 33-38 (catch block) both return 401 | -| 3 | An adt token for document A cannot download document B (documentId vs id mismatch returns 403) | 07-01 | VERIFIED | Lines 41-46: `if (documentId !== id)` returns 403 | -| 4 | A signedFilePath containing path traversal characters returns 403 | 07-01 | VERIFIED | Lines 61-67: `absPath.startsWith(UPLOADS_DIR)` guard before readFile | -| 5 | A document with no signedFilePath (unsigned) returns 404 | 07-01 | VERIFIED | Lines 53-58: `if (!doc \|\| !doc.signedFilePath)` returns 404 | -| 6 | Agent sees a Download button on the document detail page when document status is Signed | 07-02 | VERIFIED | PreparePanel lines 34-73: `if (currentStatus === 'Signed')` branch renders anchor | -| 7 | Download button is absent when document status is Draft, Sent, or Viewed | 07-02 | VERIFIED | Signed branch returns early at line 74; non-Draft returns at line 76-82; Draft renders prepare form | -| 8 | Dashboard table shows a Date Signed column populated for Signed documents | 07-02 | VERIFIED | DocumentsTable.tsx lines 43-45 (header) and 71-79 (cell); dashboard query includes signedAt | -| 9 | Signed PDFs are stored in a private local directory — a direct or guessable URL returns an access error, not the file | 07-03 | PARTIAL | Uploads/ directory not publicly served (no Next.js public/ mapping). BUT: /api/documents/[id]/file serves signedFilePath via session auth only (no presigned token) — violates LEGAL-03 "only" clause | -| 10 | LEGAL-03: agent downloads via authenticated presigned URLs only | LEGAL-03 | FAILED | Two download paths exist for signed PDFs. See gap detail. | +| 1 | GET /api/documents/[id]/download?adt=[token] streams the signed PDF when the agent-download JWT is valid | 07-01 | VERIFIED | Route exists at `src/app/api/documents/[id]/download/route.ts`. verifyAgentDownloadToken called, readFile + Response streaming confirmed. | +| 2 | A missing or expired adt token returns 401 — no file served | 07-01 | VERIFIED | Lines 22-27 (missing adt) and 33-38 (catch block) both return 401. | +| 3 | An adt token for document A cannot download document B (documentId vs id mismatch returns 403) | 07-01 | VERIFIED | `if (documentId !== id)` returns 403 at line 41-46. | +| 4 | A signedFilePath containing path traversal characters returns 403 | 07-01 | VERIFIED | `absPath.startsWith(UPLOADS_DIR)` guard before readFile at lines 61-67. | +| 5 | A document with no signedFilePath (unsigned) returns 404 | 07-01 | VERIFIED | `if (!doc || !doc.signedFilePath)` returns 404 at lines 53-58. | +| 6 | Agent sees a Download button on the document detail page when document status is Signed | 07-02 | VERIFIED | PreparePanel: `if (currentStatus === 'Signed')` branch renders `` at line 53-57. | +| 7 | Download button is absent when document status is Draft, Sent, or Viewed | 07-02 | VERIFIED | Signed branch returns early; non-Draft branch at line 76-82 renders read-only message; Draft renders prepare form. | +| 8 | Dashboard table shows a Date Signed column populated for Signed documents | 07-02 | VERIFIED | DocumentsTable.tsx: signedAt in type, th header, td cell. Dashboard query includes signedAt. | +| 9 | GET /api/documents/[id]/file NEVER serves the signed PDF — always reads doc.filePath only | 07-04 | VERIFIED | `/file/route.ts` line 25: `const relativePath = doc.filePath`. Zero grep matches for `signedFilePath` in that file. Comment updated to reference LEGAL-03. | +| 10 | LEGAL-03: sole download path for signed PDFs is /api/documents/[id]/download?adt=[token] (presigned, 5-min TTL) | LEGAL-03 | VERIFIED | Two-part evidence: (1) `/file` route no longer references signedFilePath — confirmed by grep returning no matches. (2) PdfViewer.tsx line 60: `{docStatus !== 'Signed' && ()}` — toolbar Download anchor hidden for Signed documents. Only download path remaining is PreparePanel presigned URL. | -**Score:** 8/10 truths verified +**Score:** 10/10 truths verified --- @@ -75,8 +74,8 @@ The must-haves from the three PLANs are consolidated below. Plans 01 and 02 each | Artifact | Expected | Status | Details | |----------|----------|--------|---------| -| `teressa-copeland-homes/src/lib/signing/token.ts` | createAgentDownloadToken and verifyAgentDownloadToken exports | VERIFIED | Both functions present at lines 52-64. TTL is 5m. purpose:'agent-download'. Existing 4 exports untouched. | -| `teressa-copeland-homes/src/app/api/documents/[id]/download/route.ts` | GET handler streaming signed PDF for authenticated agent download | VERIFIED | 87-line file. Exports GET. Full security surface: 401/403/404 paths all implemented. Streaming via new Uint8Array(fileBuffer). | +| `teressa-copeland-homes/src/lib/signing/token.ts` | createAgentDownloadToken and verifyAgentDownloadToken exports | VERIFIED | Both functions present at lines 52-64. TTL 5m, purpose:'agent-download'. Existing 4 exports untouched. | +| `teressa-copeland-homes/src/app/api/documents/[id]/download/route.ts` | GET handler streaming signed PDF for authenticated agent download | VERIFIED | 87-line file. 401/403/404 security paths all implemented. Streaming via new Uint8Array(fileBuffer). | ### Plan 07-02 Artifacts @@ -84,8 +83,15 @@ The must-haves from the three PLANs are consolidated below. Plans 01 and 02 each |----------|----------|--------|---------| | `teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/page.tsx` | Server component generating agentDownloadUrl and passing to PreparePanel | VERIFIED | Line 9 imports createAgentDownloadToken. Lines 35-37 generate URL. Lines 66-67 pass props to PreparePanel. | | `teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx` | Download button rendered only when currentStatus === Signed and agentDownloadUrl is non-null | VERIFIED | Lines 6-13 interface. Line 34: Signed branch. Line 53: agentDownloadUrl conditional anchor. Sent/Viewed/Draft branches all distinct. | -| `teressa-copeland-homes/src/app/portal/_components/DocumentsTable.tsx` | DocumentRow type with signedAt field + Date Signed column | VERIFIED | Line 10: signedAt: Date \| null in type. Lines 43-45: th header. Lines 71-79: td cell. | -| `teressa-copeland-homes/src/app/portal/(protected)/dashboard/page.tsx` | Select includes signedAt from documents table | VERIFIED | Line 25: signedAt: documents.signedAt in db.select(). | +| `teressa-copeland-homes/src/app/portal/_components/DocumentsTable.tsx` | DocumentRow type with signedAt field + Date Signed column | VERIFIED | signedAt: Date | null in type. th header and td cell present. | +| `teressa-copeland-homes/src/app/portal/(protected)/dashboard/page.tsx` | Select includes signedAt from documents table | VERIFIED | signedAt: documents.signedAt in db.select(). | + +### Plan 07-04 Artifacts (Gap Closure) + +| Artifact | Expected | Status | Details | +|----------|----------|--------|---------| +| `teressa-copeland-homes/src/app/api/documents/[id]/file/route.ts` | Restricted /file route — doc.filePath only, no signedFilePath reference | VERIFIED | Line 25: `const relativePath = doc.filePath`. Grep for `signedFilePath` returns zero matches. Comment reads "Serve the original unsigned PDF only — see LEGAL-03". | +| `teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewer.tsx` | Download anchor absent when docStatus is 'Signed' | VERIFIED | Line 60: `{docStatus !== 'Signed' && (` wrapping the Download ``. Document file prop (`/api/documents/${docId}/file`) unchanged at line 74 — viewer still loads original for display. | --- @@ -95,8 +101,8 @@ The must-haves from the three PLANs are consolidated below. Plans 01 and 02 each | From | To | Via | Status | Details | |------|----|-----|--------|---------| -| `route.ts` (download) | `token.ts` | `import { verifyAgentDownloadToken }` | WIRED | Line 2 of route.ts: `import { verifyAgentDownloadToken } from '@/lib/signing/token'`. Used at line 31. | -| `route.ts` (download) | `uploads/` directory | `readFile + path.join(UPLOADS_DIR) + startsWith guard` | WIRED | UPLOADS_DIR defined at line 9. absPath.startsWith(UPLOADS_DIR) guard at line 62. readFile at line 71. | +| `route.ts` (download) | `token.ts` | `import { verifyAgentDownloadToken }` | WIRED | Line 2: import present. Used at line 31. | +| `route.ts` (download) | `uploads/` directory | `readFile + path.join(UPLOADS_DIR, signedFilePath) + startsWith guard` | WIRED | UPLOADS_DIR at line 9. startsWith guard at line 62. readFile at line 71. | ### Plan 07-02 Key Links @@ -104,7 +110,13 @@ The must-haves from the three PLANs are consolidated below. Plans 01 and 02 each |------|----|-----|--------|---------| | `documents/[docId]/page.tsx` | `token.ts` | `import { createAgentDownloadToken }` | WIRED | Line 9: import present. Called at line 36. | | `documents/[docId]/page.tsx` | `PreparePanel` | `agentDownloadUrl prop` | WIRED | Line 66: `agentDownloadUrl={agentDownloadUrl}`. Line 67: `signedAt={doc.signedAt ?? null}`. | -| `dashboard/page.tsx` | `DocumentsTable` | `rows prop including signedAt field` | WIRED | Line 25: signedAt in select. Line 54: rows={filteredRows} passed to DocumentsTable. | +| `dashboard/page.tsx` | `DocumentsTable` | `rows prop including signedAt field` | WIRED | signedAt in select at line 25. rows={filteredRows} passed to DocumentsTable. | + +### Plan 07-04 Key Links (Gap Closure) + +| From | To | Via | Status | Details | +|------|----|-----|--------|---------| +| `PdfViewer.tsx` | `/api/documents/[id]/file` | `Document file prop (viewing only — not downloadable for Signed)` | WIRED (viewing) / GATED (download) | Document component still uses /file for display (line 74). Download `` pointing to /file is wrapped in `{docStatus !== 'Signed' && ...}` at line 60. | --- @@ -112,74 +124,65 @@ The must-haves from the three PLANs are consolidated below. Plans 01 and 02 each | Requirement | Source Plans | Description | Status | Evidence | |-------------|-------------|-------------|--------|----------| -| SIGN-07 | 07-01, 07-02, 07-03 | Agent can download the signed PDF from the dashboard | SATISFIED | Presigned download route implemented (07-01). PreparePanel Download button wired (07-02). Human-verified (07-03). | -| LEGAL-03 | 07-01, 07-02, 07-03 | Signed PDFs never accessible via public or guessable URLs; agent downloads via authenticated presigned URLs only | PARTIAL | Presigned URL system correctly implemented. BUT: /api/documents/[id]/file serves signedFilePath via session auth — not a presigned URL. Violates "only" clause. | +| SIGN-07 | 07-01, 07-02, 07-03, 07-04 | Agent can download the signed PDF from the dashboard | SATISFIED | Presigned download route (07-01), PreparePanel Download button (07-02), human checkpoint (07-03). REQUIREMENTS.md shows `[x]` at line 51. | +| LEGAL-03 | 07-01, 07-02, 07-03, 07-04 | Signed PDFs never accessible via public or guessable URLs; agent downloads via authenticated presigned URLs only | SATISFIED | Presigned URL system (07-01/02). /file route restricted to filePath only — signedFilePath reference removed (07-04). PdfViewer Download anchor hidden for Signed (07-04). REQUIREMENTS.md shows `[x]` at line 57. Both requirements marked Complete in requirements table (lines 137-138). | -Both requirement IDs from all three PLANs are accounted for. No orphaned requirements found in REQUIREMENTS.md for Phase 7. +Both requirement IDs from all four PLANs are accounted for. No orphaned requirements found in REQUIREMENTS.md for Phase 7. --- ## Anti-Patterns Found -| File | Line | Pattern | Severity | Impact | -|------|------|---------|----------|--------| -| `src/app/portal/(protected)/documents/[docId]/_components/PdfViewer.tsx` | 61 | `` | WARNING | Renders a persistent session-based download link for the signed PDF alongside the presigned PreparePanel button. Two download paths presented to the agent for Signed documents. | -| `src/app/api/documents/[id]/file/route.ts` | 25 | `const relativePath = doc.signedFilePath ?? doc.filePath` | WARNING | Serves the signed PDF via session auth only. LEGAL-03 requires "presigned URLs only" — this route is a session-authenticated persistent URL, not a presigned one. | +No blockers or warnings remaining. Previous anti-patterns were resolved by Plan 07-04: + +| File | Previous Pattern | Resolution | +|------|-----------------|------------| +| `src/app/api/documents/[id]/file/route.ts` | `doc.signedFilePath ?? doc.filePath` served signed PDF via session auth | Removed — line 25 now reads `doc.filePath` only | +| `src/app/portal/(protected)/documents/[docId]/_components/PdfViewer.tsx` | Persistent `` for all statuses | Wrapped in `{docStatus !== 'Signed' && ...}` at line 60 | --- ## Human Verification Required -### 1. PdfViewer Download Button Behavior for Signed Documents +All automated checks pass. The following items require a running application and a Signed document in the database to confirm end-to-end behavior. -**Test:** Navigate to a Signed document's detail page. Observe both the PreparePanel ("Download Signed PDF" anchor) and the PDF viewer toolbar ("Download" anchor). Click the viewer toolbar Download. -**Expected:** If LEGAL-03 is strictly satisfied, the toolbar Download should NOT serve the signed PDF. It should either serve the unsigned original or be absent for Signed documents. -**Why human:** The /file route serves `signedFilePath ?? filePath` — when signedFilePath is set, it serves the signed PDF. Determining whether this is acceptable requires a policy decision and runtime verification. +### 1. PdfViewer Toolbar — Download Anchor Absent for Signed Documents -### 2. /api/documents/[id]/file Direct Access for Signed Documents +**Test:** Log in to the portal. Navigate to a Signed document detail page (`/portal/documents/[id]`). Examine the PDF viewer toolbar (top of viewer). +**Expected:** No "Download" anchor appears in the toolbar for Signed documents. The only download option visible is the PreparePanel "Download Signed PDF" button in the right sidebar. +**Why human:** The conditional `{docStatus !== 'Signed' && (...)}` is confirmed in source at line 60, but runtime rendering with an actual Signed document cannot be verified statically. -**Test:** With a valid agent session, navigate directly to `http://localhost:3000/api/documents/[known-signed-doc-uuid]/file`. -**Expected:** Should either return the original unsigned PDF only (if signedFilePath is excluded), or require a presigned token. -**Why human:** This is the root of the LEGAL-03 gap — whether this route serves the signed PDF at runtime needs human confirmation against the running app. +### 2. /file Route Serves Original PDF Only for Signed Documents -### 3. Guessable URL Access for Unauthenticated Users +**Test:** With a valid agent Auth.js session, navigate directly to `http://localhost:3000/api/documents/[known-signed-doc-uuid]/file` in the browser. +**Expected:** The response is the original unsigned PDF (before signing), NOT the signed PDF. The signed PDF is exclusively available via `/api/documents/[id]/download?adt=[token]`. +**Why human:** The `signedFilePath` reference is confirmed absent from the route, but runtime behavior (serving the correct original file) requires a running app with a Signed document in the DB. -**Test:** In a fresh browser (no session), visit `http://localhost:3000/api/documents/[known-uuid]/download` (no adt param). -**Expected:** 401 returned. -**Why human:** Confirms the presigned route properly rejects unauthenticated access. +### 3. Guessable /uploads/ URL Returns 404 -### 4. /uploads/ Directory Direct Access +**Test:** In a browser, visit `http://localhost:3000/uploads/` and `http://localhost:3000/uploads/clients/`. +**Expected:** Both URLs return 404 — no directory listing, no file contents accessible. +**Why human:** Requires a running dev or prod server — Next.js static file serving behavior cannot be verified programmatically. -**Test:** Visit `http://localhost:3000/uploads/` and `http://localhost:3000/uploads/clients/` in the browser. -**Expected:** 404 — no directory listing or file contents. -**Why human:** Requires a running dev/prod server to verify Next.js does not serve the uploads/ directory statically. +### 4. End-to-End Presigned Download Flow + +**Test:** From the dashboard, click a Signed document, then click "Download Signed PDF" in the PreparePanel. +**Expected:** Browser PDF download dialog appears. The downloaded file is a valid PDF containing the drawn signature (not the blank original). +**Why human:** End-to-end flow requires a running application, a complete signing ceremony in the DB, and a signedFilePath-populated document. --- -## Gaps Summary +## Summary -### Root Cause +The two gaps from the initial verification were both closed by Plan 07-04 (committed 2026-03-21, commits 6775cc7 and cac5d5b): -The phase correctly implemented the presigned download system (Plans 01 and 02): a 5-minute JWT-gated route at `/api/documents/[id]/download`, token generation in the server component, and a Download button in PreparePanel that uses the presigned URL. +1. `/api/documents/[id]/file/route.ts` — Line 25 changed from `doc.signedFilePath ?? doc.filePath` to `doc.filePath`. The signedFilePath fallback that created a second, non-presigned download path for signed PDFs has been removed. Grep confirms zero occurrences of `signedFilePath` in this file. -However, a pre-existing route `/api/documents/[id]/file` — used by PdfViewer to load the PDF for in-browser viewing — serves the signed PDF using only an Auth.js session check (line 25: `doc.signedFilePath ?? doc.filePath`). This route: +2. `PdfViewer.tsx` — Line 60 wraps the toolbar Download anchor in `{docStatus !== 'Signed' && (...)}`. The Download link pointing to `/file` is now absent when viewing a Signed document. The `` prop for in-browser display is unchanged (still uses `/file` to load the original PDF). -1. Is accessible to any logged-in agent for any document UUID they can guess or find -2. Does not use a short-lived token -3. Is not "presigned" in any meaningful sense -4. PdfViewer exposes a persistent "Download" anchor pointing to this route +LEGAL-03 is now satisfied: the only download path for a signed PDF is `GET /api/documents/[id]/download?adt=[token]` — a presigned, 5-minute JWT-gated route created in Plan 07-01. REQUIREMENTS.md confirms both SIGN-07 and LEGAL-03 are marked `[x]` complete. -LEGAL-03 states "agent downloads via authenticated presigned URLs only." The /file route violates this unless a policy decision has been made that "presigned" applies only to the explicit download action (PreparePanel button) and not to the PDF viewer's internal file loading. - -### Resolution Options - -**Option A (Minimal):** Remove the `?? doc.filePath` signed-PDF fallback from /file — restrict it to serve only `doc.filePath` (the unsigned original). The PDF viewer then always shows the original for viewing; signed PDF download is exclusively through the presigned route. Remove the "Download" anchor from PdfViewer.tsx for Signed documents. - -**Option B (Strict):** Gate the /file route behind a short-lived token as well, making all file serving presigned. Higher implementation cost. - -**Option C (Policy exception):** Document that the /file route serves the PDF for in-browser viewing and is session-protected (not public/guessable to unauthenticated users). If the team accepts that session-authenticated viewing does not violate LEGAL-03, mark as accepted deviation. UUIDs are not guessable, and Auth.js session is required. - -The gap closure plan should resolve which option is taken and implement accordingly. +The four remaining human verification items are observational (runtime behavior, rendering confirmation, and file serving confirmation) — they do not represent code gaps, only testing checkpoints that require a running application. ---