--- 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" --- 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. @/Users/ccopeland/.claude/get-shit-done/workflows/execute-plan.md @/Users/ccopeland/.claude/get-shit-done/templates/summary.md @.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. ```typescript export function getFieldType(field: SignatureFieldData): SignatureFieldType { return field.type ?? 'client-signature'; } export function isClientVisibleField(field: SignatureFieldData): boolean { return getFieldType(field) !== 'agent-signature'; } ``` ```typescript // CURRENT — returns ALL fields including agent-signature signatureFields: doc.signatureFields ?? [], // TARGET — filters before response signatureFields: (doc.signatureFields ?? []).filter(isClientVisibleField), ``` ```typescript const handleFieldClick = useCallback( (fieldId: string) => { if (signedFields.has(fieldId)) return; setActiveFieldId(fieldId); setModalOpen(true); }, [signedFields] ); ``` ```typescript if (signedFields.size < signatureFields.length || submitting) return; ``` ```typescript Task 1: Add isClientVisibleField filter to GET /api/sign/[token] route teressa-copeland-homes/src/app/api/sign/[token]/route.ts 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. 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 - 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) Task 2: Add type-branching guards to SigningPageClient.tsx teressa-copeland-homes/src/app/sign/[token]/_components/SigningPageClient.tsx 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 ``` 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 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 - 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 Task 3: Human verification of Phase 8 safety gate 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 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) Type "approved" if all checks pass, or describe any issues found Human reviews and approves all automated checks completed in Tasks 1 and 2. No code changes required — this is a verification-only step. cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -5 Human has approved Phase 8 safety gate: TypeScript compiles clean, server filter confirmed, client guards confirmed, backward-compat verified 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 ``` - 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 After completion, create `.planning/phases/08-schema-foundation-and-signing-page-safety/08-02-SUMMARY.md`