docs(phase-7): complete phase execution and verification

This commit is contained in:
Chandler Copeland
2026-03-21 11:02:37 -06:00
parent d910288e42
commit 5f2507a69e
3 changed files with 103 additions and 100 deletions

View File

@@ -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 | | 4. PDF Ingest | 4/4 | Complete | 2026-03-20 |
| 5. PDF Fill and Field Mapping | 3/4 | In Progress| | | 5. PDF Fill and Field Mapping | 3/4 | In Progress| |
| 6. Signing Flow | 6/6 | Complete | 2026-03-21 | | 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 |

View File

@@ -3,7 +3,7 @@ gsd_state_version: 1.0
milestone: v1.0 milestone: v1.0
milestone_name: milestone milestone_name: milestone
status: unknown status: unknown
last_updated: "2026-03-21T16:56:31.521Z" last_updated: "2026-03-21T17:02:31.470Z"
progress: progress:
total_phases: 7 total_phases: 7
completed_phases: 7 completed_phases: 7

View File

@@ -1,48 +1,49 @@
--- ---
phase: 07-audit-trail-and-download phase: 07-audit-trail-and-download
verified: 2026-03-21T00:00:00Z verified: 2026-03-21T17:00:00Z
status: gaps_found status: human_needed
score: 8/10 must-haves verified score: 10/10 must-haves verified
re_verification: false re_verification: true
gaps: previous_status: gaps_found
- truth: "Signed PDFs are stored in a private local directory — a direct or guessable URL returns an access error, not the file" previous_score: 8/10
status: partial gaps_closed:
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." - "GET /api/documents/[id]/file NEVER serves the signed PDF — signedFilePath fallback removed, doc.filePath only"
artifacts: - "LEGAL-03: sole signed PDF download path is now /api/documents/[id]/download?adt=[token] — /file route confirmed clean"
- path: "teressa-copeland-homes/src/app/api/documents/[id]/file/route.ts" - "PdfViewer Download anchor absent when docStatus is 'Signed' — {docStatus !== 'Signed' && ...} conditional confirmed at line 60"
issue: "Serves signedFilePath (signed PDF) via session auth only. No short-lived token. Contradicts LEGAL-03's 'presigned URLs only' requirement." gaps_remaining: []
- path: "teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewer.tsx" regressions: []
issue: "Renders <a href={`/api/documents/${docId}/file`} download> — 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"
human_verification: 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)" - test: "Navigate to a Signed document detail page (http://localhost:3000/portal/documents/[id]) and inspect the PDF viewer toolbar"
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" expected: "The toolbar Download anchor is absent for Signed documents. Only the PreparePanel 'Download Signed PDF' button should be visible."
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" 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-uuid]/file directly in the browser for a Signed document" - test: "With a valid Auth.js session, visit /api/documents/[known-signed-uuid]/file directly in the browser"
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." 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: "This is the core conflict between the /file route and LEGAL-03. Requires a running app and a Signed document in the DB." 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: "Confirm that http://localhost:3000/uploads/ and http://localhost:3000/uploads/clients/ return 404 (not a directory listing)" - test: "Visit http://localhost:3000/uploads/ and http://localhost:3000/uploads/clients/ in a browser"
expected: "Both URLs return 404" expected: "Both URLs return 404 — no directory listing or file contents."
why_human: "Requires a running dev or prod server — cannot verify static file serving behavior programmatically" 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 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 **Phase Goal:** Agent can download any signed PDF securely, and signed documents are never accessible via guessable public URLs
**Verified:** 2026-03-21 **Verified:** 2026-03-21
**Status:** gaps_found **Status:** human_needed
**Re-verification:** No — initial verification **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 ### 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 | | # | 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 | | 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 | | 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 | | 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 | Lines 61-67: `absPath.startsWith(UPLOADS_DIR)` guard before readFile | | 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 | Lines 53-58: `if (!doc \|\| !doc.signedFilePath)` returns 404 | | 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 lines 34-73: `if (currentStatus === 'Signed')` branch renders anchor | | 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 `<a href={agentDownloadUrl}>` at line 53-57. |
| 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 | | 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 lines 43-45 (header) and 71-79 (cell); dashboard query includes signedAt | | 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 | 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 | | 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: agent downloads via authenticated presigned URLs only | LEGAL-03 | FAILED | Two download paths exist for signed PDFs. See gap detail. | | 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' && (<a href=...download>)}` — 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 | | 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/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. Exports GET. Full security surface: 401/403/404 paths all implemented. Streaming via new Uint8Array(fileBuffer). | | `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 ### 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]/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/(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/_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 | Line 25: signedAt: documents.signedAt in db.select(). | | `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 `<a>`. 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 | | 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) | `token.ts` | `import { verifyAgentDownloadToken }` | WIRED | Line 2: import present. 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) | `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 ### 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` | `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}`. | | `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 `<a>` 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 | | 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). | | 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 | 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. | | 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 ## Anti-Patterns Found
| File | Line | Pattern | Severity | Impact | No blockers or warnings remaining. Previous anti-patterns were resolved by Plan 07-04:
|------|------|---------|----------|--------|
| `src/app/portal/(protected)/documents/[docId]/_components/PdfViewer.tsx` | 61 | `<a href={/api/documents/${docId}/file} download>` | 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. | | File | Previous Pattern | Resolution |
| `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. | |------|-----------------|------------|
| `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 `<a href=.../file download>` for all statuses | Wrapped in `{docStatus !== 'Signed' && ...}` at line 60 |
--- ---
## Human Verification Required ## 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. ### 1. PdfViewer Toolbar Download Anchor Absent for Signed Documents
**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.
### 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`. ### 2. /file Route Serves Original PDF Only for Signed Documents
**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.
### 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). ### 3. Guessable /uploads/ URL Returns 404
**Expected:** 401 returned.
**Why human:** Confirms the presigned route properly rejects unauthenticated access.
### 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. ### 4. End-to-End Presigned Download Flow
**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. **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 `<Document file=...>` 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 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.
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 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. 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.
### 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.
--- ---