Files
red/.planning/phases/07-audit-trail-and-download/07-VERIFICATION.md
2026-03-21 11:02:37 -06:00

191 lines
14 KiB
Markdown

---
phase: 07-audit-trail-and-download
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 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:** 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.
---
## Goal Achievement
### Observable 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 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 `<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; 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' && (<a href=...download>)}` — toolbar Download anchor hidden for Signed documents. Only download path remaining is PreparePanel presigned URL. |
**Score:** 10/10 truths verified
---
## Required Artifacts
### Plan 07-01 Artifacts
| 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 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
| Artifact | Expected | Status | Details |
|----------|----------|--------|---------|
| `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 | 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 `<a>`. Document file prop (`/api/documents/${docId}/file`) unchanged at line 74 — viewer still loads original for display. |
---
## Key Link Verification
### Plan 07-01 Key Links
| From | To | Via | Status | Details |
|------|----|-----|--------|---------|
| `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
| From | To | Via | Status | Details |
|------|----|-----|--------|---------|
| `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 | 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. |
---
## Requirements Coverage
| Requirement | Source Plans | Description | Status | Evidence |
|-------------|-------------|-------------|--------|----------|
| 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 four PLANs are accounted for. No orphaned requirements found in REQUIREMENTS.md for Phase 7.
---
## Anti-Patterns Found
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 `<a href=.../file download>` for all statuses | Wrapped in `{docStatus !== 'Signed' && ...}` at line 60 |
---
## Human Verification Required
All automated checks pass. The following items require a running application and a Signed document in the database to confirm end-to-end behavior.
### 1. PdfViewer Toolbar — Download Anchor Absent 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.
### 2. /file Route Serves Original PDF Only for Signed Documents
**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.
### 3. Guessable /uploads/ URL Returns 404
**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.
### 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.
---
## Summary
The two gaps from the initial verification were both closed by Plan 07-04 (committed 2026-03-21, commits 6775cc7 and cac5d5b):
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.
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).
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.
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.
---
_Verified: 2026-03-21_
_Verifier: Claude (gsd-verifier)_