Files
red/.planning/phases/07-audit-trail-and-download/07-VERIFICATION.md
2026-03-21 10:50:40 -06:00

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
truth status reason artifacts missing
Signed PDFs are stored in a private local directory — a direct or guessable URL returns an access error, not the file partial 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.
path issue
teressa-copeland-homes/src/app/api/documents/[id]/file/route.ts Serves signedFilePath (signed PDF) via session auth only. No short-lived token. Contradicts LEGAL-03's 'presigned URLs only' requirement.
path issue
teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PdfViewer.tsx Renders <a href={`/api/documents/${docId}/file`} download> — a persistent session-based download link for the signed PDF, bypassing the presigned URL system.
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 status reason artifacts missing
LEGAL-03: agent downloads via authenticated presigned URLs only failed 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.
path issue
teressa-copeland-homes/src/app/api/documents/[id]/file/route.ts Line 25: serves signed PDF via session auth. Should be restricted to original filePath or gated behind a short-lived token.
Either restrict /file to serve only the original unsigned PDF (remove signedFilePath fallback), or require a presigned token on this route as well
test expected why_human
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) The viewer's own Download link goes through /api/documents/[id]/file — verify whether this also serves the signed PDF or only the original Programmatic analysis shows the route serves signedFilePath ?? filePath, but runtime behavior (whether the document in question has signedFilePath set) cannot be confirmed statically
test expected why_human
With a valid Auth.js session, visit /api/documents/[known-uuid]/file directly in the browser for a Signed document 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. This is the core conflict between the /file route and LEGAL-03. Requires a running app and a Signed document in the DB.
test expected why_human
Confirm that http://localhost:3000/uploads/ and http://localhost:3000/uploads/clients/ return 404 (not a directory listing) Both URLs return 404 Requires a running dev or prod server — cannot verify static file serving behavior programmatically

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().

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.
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:

  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 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)