---
phase: 08-schema-foundation-and-signing-page-safety
plan: "02"
type: execute
wave: 2
depends_on:
  - "08-01"
files_modified:
  - teressa-copeland-homes/src/app/api/sign/[token]/route.ts
  - teressa-copeland-homes/src/app/sign/[token]/_components/SigningPageClient.tsx
autonomous: false
requirements:
  - FIELD-01

must_haves:
  truths:
    - "GET /api/sign/[token] never returns agent-signature fields to the client — they are filtered server-side before the JSON response"
    - "SigningPageClient.handleFieldClick does not open the signature modal for non-client-signature field types"
    - "SigningPageClient.handleSubmit counts only client-signature fields in its completeness check — submit is not blocked by non-signable fields"
    - "SigningProgressBar receives the correct total (client-signature fields only, not all fields)"
    - "Existing v1.0 signing sessions are unaffected — all existing fields have no type property and read as client-signature"
    - "The POST /api/sign/[token] route is NOT modified — signature embedding pipeline is unchanged"
  artifacts:
    - path: "teressa-copeland-homes/src/app/api/sign/[token]/route.ts"
      provides: "Server-side agent-signature filter in GET response"
      contains: "isClientVisibleField"
    - path: "teressa-copeland-homes/src/app/sign/[token]/_components/SigningPageClient.tsx"
      provides: "Type-guarded handleFieldClick, type-filtered handleSubmit, correct SigningProgressBar total"
      contains: "getFieldType"
  key_links:
    - from: "teressa-copeland-homes/src/app/api/sign/[token]/route.ts (GET)"
      to: "client signing page JSON response"
      via: ".filter(isClientVisibleField) on doc.signatureFields"
      pattern: "isClientVisibleField"
    - from: "teressa-copeland-homes/src/app/sign/[token]/_components/SigningPageClient.tsx"
      to: "signature modal"
      via: "handleFieldClick type guard — getFieldType(field) !== 'client-signature' returns early"
      pattern: "getFieldType"
---

<objective>
Apply the server-side field filter in the signing GET route and add type-branching guards to SigningPageClient. This is the security-critical half of Phase 8 — the schema change in 08-01 defines the types, this plan enforces them at the boundaries.

Purpose: Without the server-side filter, agent-signature field coordinates are leaked to the client network response. Without the client-side guard, clicking a non-client-signature field overlay would open the signature modal (a broken UX that will only get worse as Phase 10 adds more field types). Both guards must ship together.

Output: Modified route.ts (GET filter), modified SigningPageClient.tsx (type guards), human verification checkpoint.
</objective>

<execution_context>
@/Users/ccopeland/.claude/get-shit-done/workflows/execute-plan.md
@/Users/ccopeland/.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/08-schema-foundation-and-signing-page-safety/08-RESEARCH.md
@.planning/phases/08-schema-foundation-and-signing-page-safety/08-01-SUMMARY.md

IMPORTANT: Read AGENTS.md in the Next.js app root before writing any code.
</context>

<interfaces>
<!-- From 08-01: helpers exported from schema.ts -->
```typescript
export function getFieldType(field: SignatureFieldData): SignatureFieldType {
  return field.type ?? 'client-signature';
}

export function isClientVisibleField(field: SignatureFieldData): boolean {
  return getFieldType(field) !== 'agent-signature';
}
```

<!-- Current unguarded line in GET /api/sign/[token] route.ts (line 88) -->
```typescript
// CURRENT — returns ALL fields including agent-signature
signatureFields: doc.signatureFields ?? [],

// TARGET — filters before response
signatureFields: (doc.signatureFields ?? []).filter(isClientVisibleField),
```

<!-- Current handleFieldClick in SigningPageClient.tsx (lines 85-92) — opens modal for ANY field -->
```typescript
const handleFieldClick = useCallback(
  (fieldId: string) => {
    if (signedFields.has(fieldId)) return;
    setActiveFieldId(fieldId);
    setModalOpen(true);
  },
  [signedFields]
);
```

<!-- Current handleSubmit count in SigningPageClient.tsx (line 129) — counts ALL fields -->
```typescript
if (signedFields.size < signatureFields.length || submitting) return;
```

<!-- Current SigningProgressBar total (line 328) — counts ALL fields -->
```typescript
<SigningProgressBar
  total={signatureFields.length}
  ...
```
</interfaces>

<tasks>

<task type="auto">
  <name>Task 1: Add isClientVisibleField filter to GET /api/sign/[token] route</name>
  <files>teressa-copeland-homes/src/app/api/sign/[token]/route.ts</files>
  <action>
Make two changes to the GET handler in `src/app/api/sign/[token]/route.ts`:

1. Add `isClientVisibleField` to the import from `@/lib/db/schema` (line 6 currently imports `signingTokens, documents, clients`):

```typescript
// Current line 6:
import { signingTokens, documents, clients } from '@/lib/db/schema';

// Updated (add isClientVisibleField):
import { signingTokens, documents, clients, isClientVisibleField } from '@/lib/db/schema';
```

2. On line 88 in the GET response (inside the `return NextResponse.json({...})` block), replace the unguarded field assignment:

```typescript
// REMOVE:
signatureFields: doc.signatureFields ?? [],

// ADD:
signatureFields: (doc.signatureFields ?? []).filter(isClientVisibleField),
```

That is the ONLY change to route.ts. Do NOT modify:
- The POST handler at all — the signing submission pipeline is correct and reads signatureFields from the DB directly (not from client), so it must remain untouched
- The token verification logic
- The audit logging logic
- Any other field in the GET response

Why server-side: If the filter were only in SigningPageClient, a caller could hit GET /api/sign/[token] directly (via curl, browser DevTools, etc.) and see agent-signature field coordinates and IDs. The server-side filter is the primary protection.
  </action>
  <verify>
    <automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -20 && grep -n "isClientVisibleField" src/app/api/sign/\[token\]/route.ts</automated>
  </verify>
  <done>
    - isClientVisibleField is imported from @/lib/db/schema in route.ts
    - Line 88 uses .filter(isClientVisibleField) on signatureFields
    - TypeScript compilation passes (no new errors)
    - POST handler is untouched (verify by diffing — only the import line and line 88 changed)
  </done>
</task>

<task type="auto">
  <name>Task 2: Add type-branching guards to SigningPageClient.tsx</name>
  <files>teressa-copeland-homes/src/app/sign/[token]/_components/SigningPageClient.tsx</files>
  <action>
Make three targeted changes to `SigningPageClient.tsx`. All changes require importing `getFieldType` first.

**Change 1 — Add import.**

The current import on line 10 is:
```typescript
import type { SignatureFieldData } from '@/lib/db/schema';
```

Replace with:
```typescript
import type { SignatureFieldData } from '@/lib/db/schema';
import { getFieldType } from '@/lib/db/schema';
```

(Keep the `import type` separate — it is a type-only import. The `getFieldType` function import is a value import and must be on its own non-type import line.)

**Change 2 — Guard handleFieldClick.**

The current handleFieldClick (lines 85-92):
```typescript
const handleFieldClick = useCallback(
  (fieldId: string) => {
    if (signedFields.has(fieldId)) return;
    setActiveFieldId(fieldId);
    setModalOpen(true);
  },
  [signedFields]
);
```

Replace with:
```typescript
const handleFieldClick = useCallback(
  (fieldId: string) => {
    const field = signatureFields.find((f) => f.id === fieldId);
    if (!field) return;
    // Defense-in-depth: primary protection is the server filter in GET /api/sign/[token]
    // Only client-signature fields open the modal; Phase 10 will expand this for initials
    if (getFieldType(field) !== 'client-signature') return;
    if (signedFields.has(fieldId)) return;
    setActiveFieldId(fieldId);
    setModalOpen(true);
  },
  [signatureFields, signedFields]
);
```

Note: `signatureFields` is now in the dependency array. This is correct — the callback uses `signatureFields.find`.

**Change 3 — Fix handleSubmit count and SigningProgressBar total.**

In `handleSubmit`, find the guard (currently line 129):
```typescript
if (signedFields.size < signatureFields.length || submitting) return;
```

Replace with:
```typescript
const clientSigFields = signatureFields.filter(
  (f) => getFieldType(f) === 'client-signature'
);
if (signedFields.size < clientSigFields.length || submitting) return;
```

In the JSX near line 328, find the SigningProgressBar:
```typescript
<SigningProgressBar
  total={signatureFields.length}
  signed={signedFields.size}
  onJumpToNext={handleJumpToNext}
  onSubmit={handleSubmit}
  submitting={submitting}
/>
```

Replace `total={signatureFields.length}` with:
```typescript
total={signatureFields.filter((f) => getFieldType(f) === 'client-signature').length}
```

Why these guards are needed even though the server already filters: The GET route filter is the primary protection. These client-side guards are defense-in-depth. They also prepare the component for Phase 10, which will add `initials` handling — at that point `handleFieldClick` will be expanded with an `else if (getFieldType(field) === 'initials')` branch. Establishing the type-switch pattern now makes Phase 10 a clean extension rather than a surgical patch.

Do NOT change:
- The `handleJumpToNext` callback (it finds the next unsigned field from signatureFields — this is fine because agent-signature fields are filtered by the GET route before the component receives them)
- The `fieldsByPage` grouping
- The PDF render logic
- The signature modal integration
- Any styling or overlay rendering
  </action>
  <verify>
    <automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -20 && grep -n "getFieldType" src/app/sign/\[token\]/_components/SigningPageClient.tsx</automated>
  </verify>
  <done>
    - getFieldType imported from @/lib/db/schema in SigningPageClient.tsx
    - handleFieldClick returns early for any field where getFieldType(field) !== 'client-signature'
    - signatureFields is in the handleFieldClick dependency array
    - handleSubmit counts only client-signature fields for completeness check
    - SigningProgressBar receives total = client-signature count (not all fields)
    - TypeScript compilation passes
  </done>
</task>

<task type="checkpoint:human-verify" gate="blocking">
  <name>Task 3: Human verification of Phase 8 safety gate</name>
  <what-built>
    Complete Phase 8 safety gate:
    - 08-01: SignatureFieldData type discriminant, getFieldType(), isClientVisibleField() in schema.ts + Drizzle migration 0006
    - 08-02: Server-side agent-signature filter in GET /api/sign/[token], type-branching guards in SigningPageClient.tsx
  </what-built>
  <how-to-verify>
    1. Start the dev server if not running: cd /Users/ccopeland/temp/red/teressa-copeland-homes && npm run dev

    2. TypeScript check — must pass clean:
       npx tsc --noEmit

    3. Verify the schema exports:
       grep -n "SignatureFieldType\|getFieldType\|isClientVisibleField" /Users/ccopeland/temp/red/teressa-copeland-homes/src/lib/db/schema.ts
       Expected: all three present with correct signatures

    4. Verify the GET route filter:
       grep -n "isClientVisibleField" /Users/ccopeland/temp/red/teressa-copeland-homes/src/app/api/sign/\[token\]/route.ts
       Expected: import line + filter applied to signatureFields in GET response

    5. Verify SigningPageClient guards:
       grep -n "getFieldType" /Users/ccopeland/temp/red/teressa-copeland-homes/src/app/sign/\[token\]/_components/SigningPageClient.tsx
       Expected: 3+ lines (import, handleFieldClick guard, handleSubmit filter, progressbar total)

    6. Verify POST handler is untouched (no isClientVisibleField/getFieldType references in POST section):
       The POST handler begins at line 98 of route.ts. Confirm no changes below that line.

    7. Verify the migration file exists:
       ls /Users/ccopeland/temp/red/teressa-copeland-homes/drizzle/0006_*.sql

    8. Manual backward-compat smoke test (if DB is running):
       - Log in to the portal at http://localhost:3000/portal
       - Open an existing signed or prepared document
       - If a signing token exists, visit the /sign/[token] URL — the page should load and display fields normally
       - No visual change expected (all existing fields have no type property and default to client-signature)
  </how-to-verify>
  <resume-signal>Type "approved" if all checks pass, or describe any issues found</resume-signal>
  <action>Human reviews and approves all automated checks completed in Tasks 1 and 2. No code changes required — this is a verification-only step.</action>
  <verify><automated>cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -5</automated></verify>
  <done>Human has approved Phase 8 safety gate: TypeScript compiles clean, server filter confirmed, client guards confirmed, backward-compat verified</done>
</task>

</tasks>

<verification>
TypeScript compilation passes:
```bash
cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit
```

All three schema exports present:
```bash
grep -c "getFieldType\|isClientVisibleField\|SignatureFieldType" /Users/ccopeland/temp/red/teressa-copeland-homes/src/lib/db/schema.ts
```
Expected: 3 (at minimum)

Server filter applied:
```bash
grep "isClientVisibleField" /Users/ccopeland/temp/red/teressa-copeland-homes/src/app/api/sign/\[token\]/route.ts
```

Client guard applied:
```bash
grep "getFieldType" /Users/ccopeland/temp/red/teressa-copeland-homes/src/app/sign/\[token\]/_components/SigningPageClient.tsx
```
</verification>

<success_criteria>
- GET /api/sign/[token] response only contains client-visible field types (agent-signature excluded)
- SigningPageClient does not open signature modal for non-client-signature field types
- handleSubmit completeness check counts only client-signature fields
- SigningProgressBar total reflects client-signature field count only
- TypeScript compilation passes across the entire project
- All existing v1.0 signing behavior is unchanged (no type = client-signature fallback)
- Phase 8 ships as an atomic unit: schema foundation + signing page safety active simultaneously
</success_criteria>

<output>
After completion, create `.planning/phases/08-schema-foundation-and-signing-page-safety/08-02-SUMMARY.md`
</output>
