# Phase 7: Audit Trail and Download - Research

**Researched:** 2026-03-21
**Domain:** Secure file download, agent-authenticated presigned URLs, dashboard status integration
**Confidence:** HIGH

<phase_requirements>
## Phase Requirements

| ID | Description | Research Support |
|----|-------------|-----------------|
| SIGN-07 | Agent can download the signed PDF from the dashboard | Agent-side presigned URL pattern; new API route + download button in DocumentsTable / document detail page |
| LEGAL-03 | Signed PDFs stored in private storage — never accessible via public or guessable URLs; agent downloads via authenticated presigned URLs only | uploads/ never under public/; agent JWT (Auth.js session) gates download route; path traversal guard already established |
</phase_requirements>

---

## Summary

Phase 7 is small but legally load-bearing. All the hard infrastructure is already in place from Phases 4 and 6: signed PDFs live in `uploads/` (never under `public/`), the path traversal guard pattern is established in multiple API routes, and the download token pattern was introduced in 06-05. What this phase adds is the **agent-facing** download surface — a new authenticated API route that lets the logged-in agent download any signed PDF, plus the UI hook (a download button) wired into the dashboard and/or document detail page.

LEGAL-03's "authenticated presigned URL" requirement in this local-filesystem context is best satisfied by: (1) a new API route `GET /api/documents/[id]/download` that requires an Auth.js session (agent-only), (2) a short-lived agent download token generated server-side when the agent clicks "Download", and (3) a streaming file response identical to the existing client download route. The client-facing download at `/api/sign/[token]/download` already satisfies the "never via guessable URL" requirement for the signing side; the agent side needs a parallel pattern that requires a valid agent session rather than a signing token.

SIGN-07's third success criterion — "Document status updates correctly to 'Signed' after a signing ceremony completes" — is **already implemented** in `POST /api/sign/[token]` (step 11: sets `status: 'Signed'`, `signedAt`, `signedFilePath`, `pdfHash`). The dashboard server component reads from the DB on each request (no client cache). What may be missing is the "Signed At" column in the dashboard table and the document detail page showing signed-state context. This should be verified at implementation time.

**Primary recommendation:** Build one new authenticated API route `GET /api/documents/[id]/download` (Auth.js session required), wire a "Download" button into the document detail page's PreparePanel/status section for Signed documents, and add `signedAt` to the DocumentsTable row type for display. No new libraries required.

---

## Standard Stack

### Core

| Library | Version | Purpose | Why Standard |
|---------|---------|---------|--------------|
| jose | already installed (via next-auth) | Agent download token JWTs | Same SIGNING_JWT_SECRET already used for signing/download tokens; consistent pattern |
| next-auth (Auth.js v5) | 5.0.0-beta.30 (pinned) | Agent session validation in API route | Already used in all /portal API routes |
| node:fs/promises | built-in | Read signed PDF from disk | Already used in /api/sign/[token]/download/route.ts |
| node:path | built-in | Absolute path construction + traversal guard | Already used in /api/documents/[id]/file/route.ts and /api/sign/[token]/download/route.ts |
| drizzle-orm | ^0.45.1 | DB query for document + signedFilePath | Already used everywhere |

### Supporting

| Library | Version | Purpose | When to Use |
|---------|---------|---------|-------------|
| @cantoo/pdf-lib | ^2.6.3 | Not needed in Phase 7 | Not applicable — no PDF manipulation, only download |
| signature_pad | ^5.1.3 | Not needed in Phase 7 | Not applicable |

No new packages need to be installed for Phase 7.

**Installation:**
```bash
# No new dependencies required
```

---

## Architecture Patterns

### Recommended Project Structure

Phase 7 adds/modifies these files:

```
src/
├── app/
│   ├── api/
│   │   └── documents/
│   │       └── [id]/
│   │           └── download/
│   │               └── route.ts          # NEW: agent-authenticated download
│   └── portal/
│       ├── _components/
│       │   └── DocumentsTable.tsx        # MODIFY: add signedAt column + download action for Signed rows
│       └── (protected)/
│           └── documents/
│               └── [docId]/
│                   ├── page.tsx          # MODIFY: pass signedFilePath/signedAt to panel
│                   └── _components/
│                       └── PreparePanel.tsx  # MODIFY: show Download button for Signed docs
```

### Pattern 1: Agent-Authenticated Presigned Download Token

**What:** Agent clicks "Download" — server generates a short-lived JWT (purpose: 'agent-download', 5-min TTL), returns the URL with the token as a query param, browser follows to stream the PDF.

**When to use:** Any time the agent needs to download a file that cannot be served statically.

**Why 5 minutes for agent vs 15 for client:** Both are arbitrary short-lived tokens. The success criterion says "5-minute TTL" explicitly. Match it exactly.

**Why a token rather than just requiring session in the streaming route:** Two patterns are valid here. The simpler pattern — just require the agent session in `GET /api/documents/[id]/download` directly — is cleaner for this local deployment. The "presigned URL" framing (token-as-query-param) matches cloud storage semantics and is what LEGAL-03 says. The existing Phase 6 download implementation uses the token-as-query-param pattern, so consistency favors reusing it.

**The cleanest implementation for this codebase:**

Option A (recommended — consistent with existing pattern):
1. Server component or server action generates a short-lived agent download token
2. Token passed to a `<a href="/api/documents/[id]/download?adt=[token]">` anchor
3. `GET /api/documents/[id]/download` verifies the token (purpose: 'agent-download') — NO session check needed at the streaming route itself
4. Streams the signed PDF

Option B (simpler, also valid):
1. `GET /api/documents/[id]/download` requires Auth.js session directly
2. No intermediate token; session cookie is the credential
3. Works fine but means the download button must trigger a fetch() (or form action), not a plain `<a href>`

**Recommendation: Option A** — matches the existing Phase 6 download pattern, satisfies "presigned URL" language in LEGAL-03, and lets the download button be a plain `<a href>` anchor (which correctly triggers browser PDF download dialog).

**Pattern A implementation sketch:**

```typescript
// In src/lib/signing/token.ts — add:
export async function createAgentDownloadToken(documentId: string): Promise<string> {
  return await new SignJWT({ documentId, purpose: 'agent-download' })
    .setProtectedHeader({ alg: 'HS256' })
    .setIssuedAt()
    .setExpirationTime('5m')
    .sign(getSecret());
}

export async function verifyAgentDownloadToken(token: string): Promise<{ documentId: string }> {
  const { payload } = await jwtVerify(token, getSecret());
  if (payload['purpose'] !== 'agent-download') throw new Error('Not an agent download token');
  return { documentId: payload['documentId'] as string };
}
```

```typescript
// In src/app/api/documents/[id]/download/route.ts:
import { auth } from '@/lib/auth';
import { verifyAgentDownloadToken } from '@/lib/signing/token';
import { db } from '@/lib/db';
import { documents } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';
import path from 'node:path';
import { readFile } from 'node:fs/promises';
import { NextRequest } from 'next/server';

const UPLOADS_DIR = path.join(process.cwd(), 'uploads');

// GET /api/documents/[id]/download?adt=[agentDownloadToken]
// Requires: valid agent-download JWT in adt query param
// Purpose: stream the signedFilePath PDF for authenticated agent download
export async function GET(req: NextRequest, { params }: { params: Promise<{ id: string }> }) {
  const { id } = await params;
  const url = new URL(req.url);
  const adt = url.searchParams.get('adt');

  if (!adt) return new Response(JSON.stringify({ error: 'Missing download token' }), { status: 401, headers: { 'Content-Type': 'application/json' } });

  let documentId: string;
  try {
    const verified = await verifyAgentDownloadToken(adt);
    documentId = verified.documentId;
  } catch {
    return new Response(JSON.stringify({ error: 'Download link expired or invalid' }), { status: 401, headers: { 'Content-Type': 'application/json' } });
  }

  // Ensure token's documentId matches route param (defense in depth)
  if (documentId !== id) {
    return new Response(JSON.stringify({ error: 'Forbidden' }), { status: 403, headers: { 'Content-Type': 'application/json' } });
  }

  const doc = await db.query.documents.findFirst({
    where: eq(documents.id, documentId),
    columns: { id: true, name: true, signedFilePath: true },
  });

  if (!doc || !doc.signedFilePath) {
    return new Response(JSON.stringify({ error: 'Signed PDF not found' }), { status: 404, headers: { 'Content-Type': 'application/json' } });
  }

  const absPath = path.join(UPLOADS_DIR, doc.signedFilePath);
  if (!absPath.startsWith(UPLOADS_DIR)) {
    return new Response(JSON.stringify({ error: 'Forbidden' }), { status: 403, headers: { 'Content-Type': 'application/json' } });
  }

  let fileBuffer: Buffer;
  try {
    fileBuffer = await readFile(absPath);
  } catch {
    return new Response(JSON.stringify({ error: 'File not found on disk' }), { status: 404, headers: { 'Content-Type': 'application/json' } });
  }

  const safeName = doc.name.replace(/[^a-zA-Z0-9-_ ]/g, '');
  return new Response(new Uint8Array(fileBuffer), {
    headers: {
      'Content-Type': 'application/pdf',
      'Content-Disposition': `attachment; filename="${safeName}_signed.pdf"`,
    },
  });
}
```

### Pattern 2: Generating the Agent Download URL (Server Component)

The document detail page (`/portal/(protected)/documents/[docId]/page.tsx`) is already a server component with full DB access. For Signed documents, it can:

1. Check `doc.signedFilePath !== null`
2. Call `createAgentDownloadToken(doc.id)` (server-side, no round-trip)
3. Pass the resulting `downloadUrl` as a prop to PreparePanel (or a new SignedDocumentPanel)

```typescript
// In documents/[docId]/page.tsx — for Signed docs:
import { createAgentDownloadToken } from '@/lib/signing/token';

// After fetching doc:
const agentDownloadUrl = doc.signedFilePath
  ? `/api/documents/${docId}/download?adt=${await createAgentDownloadToken(docId)}`
  : null;

// Pass to PreparePanel:
<PreparePanel
  docId={docId}
  defaultEmail={docClient?.email ?? ''}
  clientName={docClient?.name ?? ''}
  currentStatus={doc.status}
  agentDownloadUrl={agentDownloadUrl}
  signedAt={doc.signedAt ?? null}
/>
```

### Pattern 3: Dashboard Table Enhancement

The dashboard currently selects: `id, name, status, sentAt, clientName, clientId`. Adding `signedAt` from the documents table requires only adding it to the select and the DocumentRow type:

```typescript
// In dashboard/page.tsx select:
signedAt: documents.signedAt,

// In DocumentsTable.tsx DocumentRow type:
signedAt: Date | null;

// In table row rendering: show signedAt when status === 'Signed'
```

The "Download" action for signed documents in the table is a design choice: either (a) clicking the document name navigates to the detail page (which shows the Download button), or (b) the table row itself shows a Download icon link. Option (a) requires zero new columns; option (b) requires an additional server component layer since download token generation is server-only.

**Recommendation: Option (a) for the table** — document name links to detail page, Download button lives on detail page. This avoids turning DocumentsTable into a server component or adding complex client-to-server fetching just for a download button.

### Pattern 4: Status Display Verification

The third success criterion — "Document status updates correctly to 'Signed' after a signing ceremony completes" — is already wired in `POST /api/sign/[token]`. Verification should confirm:

1. `documents.status` = 'Signed' in DB after signing
2. Dashboard shows "Signed" StatusBadge (it reads from DB on each render — no stale cache)
3. Document detail page shows "Signed" in the status display (`doc.status` read in server component)

If there is any discrepancy, the likely cause is the dashboard query not re-fetching after signing. Since the dashboard is a server component with no client-side caching, this should work correctly. The verification plan should include a manual browser check: complete a signing flow, navigate to dashboard, confirm "Signed" badge.

### Anti-Patterns to Avoid

- **Serving signed PDFs via a static public URL:** uploads/ is never under public/ (established in Phase 4). Never move signed PDFs to public/. Never add a rewrite rule that exposes uploads/ statically.
- **Token purpose confusion:** The existing `verifyDownloadToken` checks `purpose === 'download'` (client token). The new `verifyAgentDownloadToken` must check `purpose === 'agent-download'`. Do not reuse the same purpose string across client and agent flows.
- **Generating agent download tokens client-side:** The token must be generated by a server component or API route — never in a client component (that would expose SIGNING_JWT_SECRET to the browser).
- **Route ID mismatch bypass:** The `adt` token contains `documentId`. The streaming route should verify that `documentId === params.id` — prevents a valid token for document A being used to download document B.
- **Missing path traversal guard:** Every file read from uploads/ must check `absPath.startsWith(UPLOADS_DIR)`. This guard is established in Phase 4 and repeated in Phase 6. Phase 7 must repeat it.

---

## Don't Hand-Roll

| Problem | Don't Build | Use Instead | Why |
|---------|-------------|-------------|-----|
| Short-lived agent download tokens | Custom HMAC or encrypted URL | `jose` SignJWT (same pattern as existing token.ts) | Already in use; consistent; jose handles expiry, alg, claims automatically |
| File streaming | Manual chunked read | `readFile` + `new Response(new Uint8Array(...))` | Already proven in `/api/sign/[token]/download/route.ts` — no streaming complexity needed for typical PDF sizes (< 10 MB) |
| Access control on download route | Custom session parsing | `auth()` from `@/lib/auth` or token verification | Already established pattern; do not re-implement JWT parsing |

**Key insight:** Phase 7 is almost entirely assembly of existing patterns. The client download route (`/api/sign/[token]/download`) is a complete reference implementation — copy and adapt it with the agent-auth pattern instead of the signing-token pattern.

---

## Common Pitfalls

### Pitfall 1: Token Generated in Client Component

**What goes wrong:** Developer tries to generate the agent download token in PreparePanel (a `'use client'` component) — this fails because `createAgentDownloadToken` imports `jose` and uses `process.env.SIGNING_JWT_SECRET`, which cannot run in the browser.

**Why it happens:** PreparePanel already handles other state so it's tempting to add the download logic there.

**How to avoid:** Generate the token in the server component (`documents/[docId]/page.tsx`) and pass the pre-generated URL as a prop to PreparePanel. PreparePanel only renders an `<a href>` anchor — it does not call any token function.

**Warning signs:** TypeScript error about server-only module in client context; or runtime error "SIGNING_JWT_SECRET is undefined" in the browser.

### Pitfall 2: PreparePanel Props Interface Not Updated

**What goes wrong:** The page.tsx passes `agentDownloadUrl` and `signedAt` to PreparePanel, but PreparePanel's TypeScript interface (`PreparePanelProps`) doesn't declare them — TypeScript error at build time.

**How to avoid:** Add `agentDownloadUrl?: string | null` and `signedAt?: Date | null` to the PreparePanelProps interface before wiring the prop.

### Pitfall 3: DocumentsTable Row Type Missing signedAt

**What goes wrong:** Dashboard query selects `signedAt` but `DocumentRow` type in DocumentsTable.tsx only has `{ id, name, clientName, status, sentAt, clientId }` — TypeScript error on the table component.

**How to avoid:** Update the `DocumentRow` type in DocumentsTable.tsx and the select object in dashboard/page.tsx together.

### Pitfall 4: signedAt Display Timezone Confusion

**What goes wrong:** `doc.signedAt` from Drizzle is a JS `Date` object (UTC). If displayed with `toLocaleDateString` without explicit timezone, it may show one day off for agents in UTC-offset timezones.

**How to avoid:** Use `toLocaleString('en-US', { timeZone: 'America/Denver', ... })` — Teressa is a Utah agent (Mountain Time). Or display both date and time so any offset ambiguity is visible.

### Pitfall 5: UPLOADS_DIR Path Traversal Guard Omitted

**What goes wrong:** If `signedFilePath` in the DB ever contains `../../../etc/passwd` (unlikely but defensive coding requires it), the file read would escape uploads/. Route returns sensitive content.

**Why it happens:** Developer copies the route from Phase 6 but forgets the guard.

**How to avoid:** Copy the guard verbatim from `/api/sign/[token]/download/route.ts`:
```typescript
const absPath = path.join(UPLOADS_DIR, doc.signedFilePath);
if (!absPath.startsWith(UPLOADS_DIR)) {
  return new Response(JSON.stringify({ error: 'Forbidden' }), { status: 403, ... });
}
```

### Pitfall 6: Expired Signing Token on Confirmed Page

**What goes wrong:** Phase 6's confirmed page calls `verifySigningToken(token)` — if the 72-hour signing token has expired by the time the agent views the confirmed page, the function throws and `documentId` is null.

**Status:** This is already handled in the existing `confirmed/page.tsx` with a try/catch that allows expired tokens. No action needed for Phase 7 — note this for awareness only.

### Pitfall 7: Download Button Appears for Unsigned Documents

**What goes wrong:** PreparePanel shows the download button even when `doc.signedFilePath` is null (status is "Sent" or "Viewed"). Clicking it would get a 404.

**How to avoid:** Guard: only show the download button when `currentStatus === 'Signed' && agentDownloadUrl !== null`.

---

## Code Examples

Verified patterns from existing codebase:

### Existing Client Download Route (Reference Implementation)

```typescript
// Source: teressa-copeland-homes/src/app/api/sign/[token]/download/route.ts
// This is the complete working pattern — agent route mirrors this exactly

import { NextRequest } from 'next/server';
import { verifyDownloadToken } from '@/lib/signing/token';
import { db } from '@/lib/db';
import { documents } from '@/lib/db/schema';
import { eq } from 'drizzle-orm';
import path from 'node:path';
import { readFile } from 'node:fs/promises';

const UPLOADS_DIR = path.join(process.cwd(), 'uploads');

export async function GET(req: NextRequest) {
  const url = new URL(req.url);
  const dt = url.searchParams.get('dt');
  if (!dt) return new Response(JSON.stringify({ error: 'Missing download token' }), { status: 401, headers: { 'Content-Type': 'application/json' } });

  let documentId: string;
  try {
    const verified = await verifyDownloadToken(dt);
    documentId = verified.documentId;
  } catch {
    return new Response(JSON.stringify({ error: 'Download link expired or invalid' }), { status: 401, headers: { 'Content-Type': 'application/json' } });
  }

  const doc = await db.query.documents.findFirst({
    where: eq(documents.id, documentId),
    columns: { id: true, name: true, signedFilePath: true },
  });

  if (!doc || !doc.signedFilePath) return new Response(JSON.stringify({ error: 'Signed PDF not found' }), { status: 404, headers: { 'Content-Type': 'application/json' } });

  const absPath = path.join(UPLOADS_DIR, doc.signedFilePath);
  if (!absPath.startsWith(UPLOADS_DIR)) return new Response(JSON.stringify({ error: 'Forbidden' }), { status: 403, headers: { 'Content-Type': 'application/json' } });

  let fileBuffer: Buffer;
  try {
    fileBuffer = await readFile(absPath);
  } catch {
    return new Response(JSON.stringify({ error: 'File not found on disk' }), { status: 404, headers: { 'Content-Type': 'application/json' } });
  }

  const safeName = doc.name.replace(/[^a-zA-Z0-9-_ ]/g, '');
  return new Response(new Uint8Array(fileBuffer), {
    headers: {
      'Content-Type': 'application/pdf',
      'Content-Disposition': `attachment; filename="${safeName}_signed.pdf"`,
    },
  });
}
```

### Existing Token Pattern (Reference for Adding Agent Token)

```typescript
// Source: teressa-copeland-homes/src/lib/signing/token.ts
// createDownloadToken and verifyDownloadToken already follow this pattern
// Agent token adds purpose: 'agent-download' to distinguish

export async function createDownloadToken(documentId: string): Promise<string> {
  return await new SignJWT({ documentId, purpose: 'download' })
    .setProtectedHeader({ alg: 'HS256' })
    .setIssuedAt()
    .setExpirationTime('15m')
    .sign(getSecret());
}

export async function verifyDownloadToken(token: string): Promise<{ documentId: string }> {
  const { payload } = await jwtVerify(token, getSecret());
  if (payload['purpose'] !== 'download') throw new Error('Not a download token');
  return { documentId: payload['documentId'] as string };
}
```

### Existing Audit Event Logging (for reference — LEGAL-01 already complete)

```typescript
// Source: teressa-copeland-homes/src/lib/signing/audit.ts
// auditEventTypeEnum values: document_prepared | email_sent | link_opened | document_viewed | signature_submitted | pdf_hash_computed
// All 6 events are already logged in Phase 6. No new audit events needed for Phase 7.
await logAuditEvent({
  documentId: opts.documentId,
  eventType: 'signature_submitted',
  ipAddress: ip,
  userAgent: ua,
});
```

### Existing DocumentsTable Row Type (for reference — needs signedAt added)

```typescript
// Source: teressa-copeland-homes/src/app/portal/_components/DocumentsTable.tsx
// Current type (missing signedAt):
type DocumentRow = {
  id: string;
  name: string;
  clientName: string | null;
  status: "Draft" | "Sent" | "Viewed" | "Signed";
  sentAt: Date | null;
  clientId: string;
};
// Add: signedAt: Date | null;
```

---

## State of the Art

| Old Approach | Current Approach | When Changed | Impact |
|--------------|------------------|--------------|--------|
| Serving files via public/ static folder | files in uploads/ served via API route with auth gate | Phase 4 | Files are never guessable by URL |
| Cloud presigned URLs (S3, GCS) | Local filesystem + JWT-gated API route | Architecture decision (home Docker, no cloud) | No S3 SDK; pattern is JWT-as-presigned-URL |
| Separate download service | In-process Next.js API route | Entire project | No infrastructure to add |

**What "presigned URL" means in this local context:**
In cloud storage (S3), a presigned URL contains a time-limited HMAC signature in the URL query string — no auth cookie needed to follow the URL. The project replicates this pattern with a JWT in the `adt` query parameter. The HMAC secret is `SIGNING_JWT_SECRET`. This is functionally equivalent for a single-server local deployment.

---

## What Phase 7 Is NOT

To keep the planner focused: Phase 7 does NOT need to:

- Implement a new audit event type — LEGAL-01 (all 6 audit events) is already complete from Phase 6
- Change how signed PDFs are stored (path structure, location) — already established
- Add a PDF hash viewer or audit log viewer for the agent — not in requirements
- Add a full audit trail UI — not in SIGN-07 or LEGAL-03
- Add client download from the agent portal — client download is already implemented on the confirmation page
- Change the `auditEvents` schema — no new columns needed

---

## Open Questions

1. **Should the Download button be on the dashboard table row or only on the document detail page?**
   - What we know: Both locations are technically possible. Table requires server-side token generation per row (complex). Detail page is already a server component with DB access.
   - What's unclear: Whether the agent would find it more convenient to download without navigating to the detail page.
   - Recommendation: Put Download button on the document detail page only (PreparePanel context for Signed status). The dashboard table documents link to the detail page — one extra click is acceptable and avoids complexity.

2. **Should `signedAt` be shown in the DocumentsTable alongside sentAt?**
   - What we know: The dashboard currently shows "Date Sent" column with `sentAt`. Success criterion 3 says "status updates correctly to Signed" — this is about the StatusBadge, not a separate column.
   - What's unclear: Whether a "Date Signed" column is desired.
   - Recommendation: Add `signedAt` to the DocumentRow type and show it in a new "Date Signed" column only when `status === 'Signed'`. Minimal change — adds clarity without clutter. If planner prefers not to add the column, the StatusBadge alone satisfies criterion 3.

3. **What happens if signedFilePath exists in DB but file is missing from disk?**
   - What we know: The download route already handles this with a 404 (`readFile` catch). This is an operational issue, not a Phase 7 code issue.
   - Recommendation: No code change needed; document it in verification steps.

---

## Sources

### Primary (HIGH confidence)
- Direct code inspection of `teressa-copeland-homes/src/app/api/sign/[token]/download/route.ts` — complete working client download pattern
- Direct code inspection of `teressa-copeland-homes/src/lib/signing/token.ts` — all token functions including existing download token pattern
- Direct code inspection of `teressa-copeland-homes/src/lib/db/schema.ts` — documents table columns including `signedFilePath`, `signedAt`, `pdfHash`, `status`
- Direct code inspection of `teressa-copeland-homes/src/app/api/sign/[token]/route.ts` — confirms `status: 'Signed'` already set in POST handler (criterion 3 already met at DB level)
- Direct code inspection of `teressa-copeland-homes/src/app/portal/_components/DocumentsTable.tsx` — current row type and rendering
- Direct code inspection of `teressa-copeland-homes/src/app/portal/(protected)/dashboard/page.tsx` — server component, reads from DB on each request
- Direct code inspection of `teressa-copeland-homes/src/app/portal/(protected)/documents/[docId]/_components/PreparePanel.tsx` — PreparePanel is `'use client'`; token generation must happen in server parent

### Secondary (MEDIUM confidence)
- .planning/REQUIREMENTS.md — SIGN-07 and LEGAL-03 requirement text
- .planning/STATE.md decisions — confirmed: `uploads/` at project root, relative paths in DB, download token pattern from 06-05
- .planning/phases/06-signing-flow/06-05-PLAN.md — confirmed scope boundary: Phase 05 built client download; Phase 7 adds agent download
- .planning/ROADMAP.md — Phase 7 success criteria (3 criteria)

---

## Metadata

**Confidence breakdown:**
- Standard stack: HIGH — no new libraries; all patterns directly verified in existing code
- Architecture: HIGH — agent download route mirrors existing client download route exactly; pattern verified in source
- Pitfalls: HIGH — derived directly from source code inspection of PreparePanel ('use client' constraint), DocumentsTable type, existing guard patterns

**Research date:** 2026-03-21
**Valid until:** 2026-04-20 (stable — no fast-moving dependencies; all patterns are from the project's own codebase)
