diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index 77d9549..03a4287 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -176,7 +176,7 @@ Plans: 2. `/api/sign/[token]` route filters `signatureFields` to client-visible types only — `agent-signature` fields are never returned to the signing page 3. `SigningPageClient.tsx` branches on field type and does not attempt to open the signature modal for non-client-signature types 4. Drizzle migration runs cleanly against the existing database with no data loss on existing documents -**Plans**: TBD +**Plans**: 2 plans Plans: - [ ] 08-01-PLAN.md — Discriminated union type extension in schema.ts, Drizzle migration, backward-compat fallback in all field-reading paths diff --git a/.planning/phases/08-schema-foundation-and-signing-page-safety/08-01-PLAN.md b/.planning/phases/08-schema-foundation-and-signing-page-safety/08-01-PLAN.md new file mode 100644 index 0000000..905b122 --- /dev/null +++ b/.planning/phases/08-schema-foundation-and-signing-page-safety/08-01-PLAN.md @@ -0,0 +1,218 @@ +--- +phase: 08-schema-foundation-and-signing-page-safety +plan: "01" +type: execute +wave: 1 +depends_on: [] +files_modified: + - teressa-copeland-homes/src/lib/db/schema.ts + - teressa-copeland-homes/drizzle/0006_type_discriminant.sql +autonomous: true +requirements: + - FIELD-01 + +must_haves: + truths: + - "SignatureFieldData interface has an optional 'type' field accepting all six SignatureFieldType literals" + - "getFieldType() helper exported from schema.ts returns field.type ?? 'client-signature' — never returns undefined" + - "isClientVisibleField() pure function exported from schema.ts returns false for agent-signature, true for all others" + - "Drizzle meta snapshot is in sync with schema.ts (npm run db:generate runs clean)" + - "Existing documents with no 'type' on their JSONB fields continue to work (backward-compat)" + artifacts: + - path: "teressa-copeland-homes/src/lib/db/schema.ts" + provides: "SignatureFieldType union, extended SignatureFieldData interface, getFieldType helper, isClientVisibleField predicate" + contains: "SignatureFieldType" + - path: "teressa-copeland-homes/drizzle/0006_type_discriminant.sql" + provides: "Drizzle migration snapshot sync (may be empty SQL — that is correct)" + key_links: + - from: "teressa-copeland-homes/src/lib/db/schema.ts" + to: "teressa-copeland-homes/src/app/api/sign/[token]/route.ts" + via: "isClientVisibleField import (added in plan 08-02)" + pattern: "isClientVisibleField" + - from: "teressa-copeland-homes/src/lib/db/schema.ts" + to: "teressa-copeland-homes/src/app/sign/[token]/_components/SigningPageClient.tsx" + via: "getFieldType import (added in plan 08-02)" + pattern: "getFieldType" +--- + + +Extend the SignatureFieldData TypeScript interface with a type discriminant and export two pure helper functions that all field-reading code will use. Run the Drizzle migration to keep the meta snapshot current. + +Purpose: Every phase 9-13 feature that places a new field type on a document depends on this discriminant. Without it, the signing page treats every field as a required client-signature, and an agent-signature field would surface as an unsigned required field to the client. This plan establishes the single source of truth for field type resolution. + +Output: Extended schema.ts with SignatureFieldType, updated SignatureFieldData interface, getFieldType() and isClientVisibleField() helpers, and migration file 0006. + + + +@/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 + +IMPORTANT: Read AGENTS.md in the Next.js app root before writing any code. The app uses a version of Next.js that may differ from training data. + + + + +```typescript +export interface SignatureFieldData { + id: string; + page: number; // 1-indexed + x: number; // PDF user space, bottom-left origin, points + y: number; // PDF user space, bottom-left origin, points + width: number; // PDF points (default: 144 — 2 inches) + height: number; // PDF points (default: 36 — 0.5 inches) +} + +// Current JSONB column annotation (documents table): +signatureFields: jsonb("signature_fields").$type(), +``` + + + + + + Task 1: Extend SignatureFieldData in schema.ts with type discriminant and helper exports + teressa-copeland-homes/src/lib/db/schema.ts + +Add the following to schema.ts BEFORE the `SignatureFieldData` interface declaration (i.e., above line 4): + +```typescript +export type SignatureFieldType = + | 'client-signature' + | 'initials' + | 'text' + | 'checkbox' + | 'date' + | 'agent-signature'; +``` + +Then update the existing `SignatureFieldData` interface by adding one optional field after `height`: + +```typescript + type?: SignatureFieldType; // Optional — v1.0 documents have no type; fallback = 'client-signature' +``` + +Then add two exported helper functions immediately after the `SignatureFieldData` interface (before the `users` table declaration): + +```typescript +/** + * Safe field type reader — always returns a SignatureFieldType, never undefined. + * v1.0 documents have no `type` on their JSONB fields; this coalesces to 'client-signature'. + * ALWAYS use this instead of reading field.type directly. + */ +export function getFieldType(field: SignatureFieldData): SignatureFieldType { + return field.type ?? 'client-signature'; +} + +/** + * Returns true for field types that should be visible in the client signing session. + * agent-signature fields are embedded during document preparation and must never + * surface to the client as required unsigned fields. + */ +export function isClientVisibleField(field: SignatureFieldData): boolean { + return getFieldType(field) !== 'agent-signature'; +} +``` + +Do NOT change the `.$type()` annotation on the `signatureFields` column — it already annotates the JSONB column with the correct type and will automatically reflect the updated interface. + +Do NOT touch any other tables, enums, or imports. The rest of schema.ts is unchanged. + +Why type is optional: all v1.0 documents in the database have JSONB field objects with no `type` property. Making it required would break TypeScript on every FieldPlacer call site that constructs a SignatureFieldData without a type. The `getFieldType()` fallback handles the coercion centrally. + + + cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit 2>&1 | head -30 + + + - schema.ts exports SignatureFieldType, getFieldType, and isClientVisibleField + - TypeScript compilation passes with no errors related to schema.ts + - getFieldType({ id: 'x', page: 1, x: 0, y: 0, width: 144, height: 36 }) returns 'client-signature' (no type property = backward compat) + - getFieldType({ ..., type: 'agent-signature' }) returns 'agent-signature' + - isClientVisibleField({ ..., type: 'agent-signature' }) returns false + - isClientVisibleField({ ..., type: 'client-signature' }) returns true + - isClientVisibleField({ id: 'x', page: 1, x: 0, y: 0, width: 144, height: 36 }) returns true (no type = backward compat) + + + + + Task 2: Run Drizzle migration to sync meta snapshot + teressa-copeland-homes/drizzle/0006_type_discriminant.sql + +Run the migration generator from the app directory: + +```bash +cd /Users/ccopeland/temp/red/teressa-copeland-homes && npm run db:generate +``` + +This produces `drizzle/0006_*.sql`. The SQL content will be empty or contain only a comment — that is CORRECT and expected. The `.$type()` annotation on the JSONB column is a TypeScript-only change and generates no DDL. + +After generation, rename the file to have a predictable name if the generated name is not `0006_type_discriminant.sql`: + +```bash +cd /Users/ccopeland/temp/red/teressa-copeland-homes && ls drizzle/0006_*.sql +``` + +If the file was generated with a random name (e.g., `0006_milky_black_cat.sql`), rename it: +```bash +mv drizzle/0006_*.sql drizzle/0006_type_discriminant.sql +``` + +Then apply the migration to the local database: + +```bash +cd /Users/ccopeland/temp/red/teressa-copeland-homes && npm run db:migrate +``` + +If `db:migrate` fails because the database is not running, note the failure in the summary but DO NOT block plan completion — the TypeScript changes are the deliverable. The migration file itself is what must exist and be committed. + +Why run even an empty migration: Drizzle stores a JSON snapshot of the TypeScript schema in `drizzle/meta/`. Skipping `db:generate` after a schema.ts change causes the next `db:generate` run (Phase 9 or later) to produce an incorrect diff that re-adds or re-creates things that already exist. The snapshot must stay in sync. + + + ls /Users/ccopeland/temp/red/teressa-copeland-homes/drizzle/0006_*.sql 2>/dev/null && echo "Migration file exists" || echo "MISSING" + + + - drizzle/0006_type_discriminant.sql (or 0006_*.sql) exists + - File content is empty SQL or a comment — no DDL statements (correct) + - drizzle/meta/ directory has been updated by db:generate + - npm run db:migrate ran without error (or was noted as pending if DB unavailable) + + + + + + +Run TypeScript compilation to confirm no type errors introduced: + +```bash +cd /Users/ccopeland/temp/red/teressa-copeland-homes && npx tsc --noEmit +``` + +Confirm all three exports are present: + +```bash +grep -n "SignatureFieldType\|getFieldType\|isClientVisibleField" /Users/ccopeland/temp/red/teressa-copeland-homes/src/lib/db/schema.ts +``` + +Confirm migration file exists: + +```bash +ls /Users/ccopeland/temp/red/teressa-copeland-homes/drizzle/0006_*.sql +``` + + + +- schema.ts exports: SignatureFieldType (union type), updated SignatureFieldData (with optional type field), getFieldType() (backward-compat fallback), isClientVisibleField() (pure predicate) +- TypeScript compilation passes: npx tsc --noEmit exits 0 +- drizzle/0006_*.sql exists and was generated by db:generate (not hand-written) +- All existing v1.0 SignatureFieldData objects (no type property) continue to resolve as 'client-signature' via getFieldType() + + + +After completion, create `.planning/phases/08-schema-foundation-and-signing-page-safety/08-01-SUMMARY.md` + diff --git a/.planning/phases/08-schema-foundation-and-signing-page-safety/08-02-PLAN.md b/.planning/phases/08-schema-foundation-and-signing-page-safety/08-02-PLAN.md new file mode 100644 index 0000000..3ebc4e5 --- /dev/null +++ b/.planning/phases/08-schema-foundation-and-signing-page-safety/08-02-PLAN.md @@ -0,0 +1,344 @@ +--- +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` +