15 KiB
phase, verified, status, score, re_verification, gaps, human_verification
| phase | verified | status | score | re_verification | gaps | human_verification | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 07-audit-trail-and-download | 2026-03-21T00:00:00Z | gaps_found | 8/10 must-haves verified | false |
|
|
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
Goal Achievement
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. |
Score: 8/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 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). |
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 | 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(). |
Key Link Verification
Plan 07-01 Key Links
| 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. |
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 | Line 25: signedAt in select. Line 54: rows={filteredRows} passed to DocumentsTable. |
Requirements Coverage
| 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. |
Both requirement IDs from all three 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 | <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. |
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. |
Human Verification Required
1. PdfViewer Download Button Behavior for Signed Documents
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.
2. /api/documents/[id]/file Direct Access for Signed Documents
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.
3. Guessable URL Access for Unauthenticated Users
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.
4. /uploads/ Directory Direct Access
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.
Gaps Summary
Root Cause
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.
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:
- Is accessible to any logged-in agent for any document UUID they can guess or find
- Does not use a short-lived token
- Is not "presigned" in any meaningful sense
- 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.
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.
Verified: 2026-03-21 Verifier: Claude (gsd-verifier)