14 KiB
phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 08-schema-foundation-and-signing-page-safety | 02 | execute | 2 |
|
|
false |
|
|
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.
<execution_context> @/Users/ccopeland/.claude/get-shit-done/workflows/execute-plan.md @/Users/ccopeland/.claude/get-shit-done/templates/summary.md </execution_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.mdIMPORTANT: 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'; }
<!-- 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),
const handleFieldClick = useCallback(
(fieldId: string) => {
if (signedFields.has(fieldId)) return;
setActiveFieldId(fieldId);
setModalOpen(true);
},
[signedFields]
);
if (signedFields.size < signatureFields.length || submitting) return;
<SigningProgressBar
total={signatureFields.length}
...
- Add
isClientVisibleFieldto the import from@/lib/db/schema(line 6 currently importssigningTokens, documents, clients):
// Current line 6:
import { signingTokens, documents, clients } from '@/lib/db/schema';
// Updated (add isClientVisibleField):
import { signingTokens, documents, clients, isClientVisibleField } from '@/lib/db/schema';
- On line 88 in the GET response (inside the
return NextResponse.json({...})block), replace the unguarded field assignment:
// 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:
import type { SignatureFieldData } from '@/lib/db/schema';
Replace with:
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):
const handleFieldClick = useCallback(
(fieldId: string) => {
if (signedFields.has(fieldId)) return;
setActiveFieldId(fieldId);
setModalOpen(true);
},
[signedFields]
);
Replace with:
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):
if (signedFields.size < signatureFields.length || submitting) return;
Replace with:
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:
<SigningProgressBar
total={signatureFields.length}
signed={signedFields.size}
onJumpToNext={handleJumpToNext}
onSubmit={handleSubmit}
submitting={submitting}
/>
Replace total={signatureFields.length} with:
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
handleJumpToNextcallback (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
fieldsByPagegrouping - 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
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:
grep -c "getFieldType\|isClientVisibleField\|SignatureFieldType" /Users/ccopeland/temp/red/teressa-copeland-homes/src/lib/db/schema.ts
Expected: 3 (at minimum)
Server filter applied:
grep "isClientVisibleField" /Users/ccopeland/temp/red/teressa-copeland-homes/src/app/api/sign/\[token\]/route.ts
Client guard applied:
grep "getFieldType" /Users/ccopeland/temp/red/teressa-copeland-homes/src/app/sign/\[token\]/_components/SigningPageClient.tsx
<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>