--- phase: 03-event-write-back-pwa-install plan: '09' subsystem: api-events-router tags: [tdd, gap-closure, auth, schema, zod, oidc] dependency_graph: requires: [] provides: - canonical-event-schema-title-start-end - async-resolveUserId-with-upsertUser affects: - apps/api/src/routes/events.ts - apps/api/tests/routes/events.test.ts - plan-03-10 (outbox worker reads title/start/end from payload) tech_stack: added: [] patterns: - 'TDD RED→GREEN per task' - 'vi.hoisted() for configurable per-test auth mocks' - 'async resolveUserId with upsertUser for OIDC path' key_files: modified: - apps/api/src/routes/events.ts - apps/api/tests/routes/events.test.ts decisions: - 'D-CR01: Server adopts client field names title/start/end — one canonical name set end-to-end, no rename map' - 'D-CR06: resolveUserId async; dev-bypass path unchanged; OIDC path calls upsertUser(iss,sub,email)' metrics: duration_minutes: 6 completed_date: '2026-06-06' tasks_completed: 2 files_modified: 2 --- # Phase 03 Plan 09: Route Schema + OIDC Resolution Fix Summary Fix the events router's two blockers that made the write path dead on arrival: align the server zod schema to the PWA's `CreateEventPayload` shape (title/start/end), and implement real OIDC iss/sub → users.id resolution on all five write handlers via `upsertUser`. ## Tasks Completed | Task | Name | Commit | Files | | ------- | ------------------------------------------------------------- | ------- | ------------------------- | | 1 RED | Add contract tests for canonical title/start/end | 944693f | events.test.ts | | 1 GREEN | Rename eventFieldsSchema to title/start/end (CR-01) | 99cb169 | events.ts, events.test.ts | | 2 RED | Add OIDC path tests — resolveUserId must call upsertUser | 6d1d338 | events.test.ts | | 2 GREEN | Async resolveUserId with upsertUser on all 5 handlers (CR-06) | fac3a21 | events.ts | ## Verification - `cd apps/api && npx vitest run tests/routes/events.test.ts`: 19 tests pass - `npx tsc --noEmit` in apps/api: clean (no errors) - `grep -n 'summary\|dtstart\|dtend' eventFieldsSchema`: CLEAN (no old names) - `grep -c 'For now return 401' events.ts`: 0 stubs remain - `grep -c 'upsertUser' events.ts`: 3 (import + call in resolveUserId) ## Decisions Made - **D-CR01**: Server adopts client field names `title/start/end`. No internal rename map — one canonical name set end-to-end from PWA through events router to calendarOutbox payload to outbox worker (plan 03-10). - **D-CR06**: `resolveUserId` is now async. Dev-bypass path (`c.get('user')`) is unchanged. Production OIDC path calls `getAuth(c)` then `upsertUser(iss, sub, email)` to resolve DB user id. Returns null only when no session exists. ## Deviations from Plan ### Auto-fixed Issues None. The plan was executed exactly as written, including updating the three existing write tests that previously used the old field names (`summary/dtstart/dtend`) — this was the correct fix since those tests were testing against the wrong boundary (as the review noted). ### Test Infrastructure Deviation (Rule 3) The worktree has no `node_modules` — the pnpm workspace installs them in the main repo. Created a symlink `apps/api/node_modules → /home/luc/Projects/familysync/apps/api/node_modules` so vitest could run from within the worktree. This is a standard git-worktree-with-pnpm-workspace setup requirement. ## TDD Gate Compliance Both tasks followed RED→GREEN strictly: - Task 1: `test(03-09)` commit (944693f) → `feat(03-09)` commit (99cb169) - Task 2: `test(03-09)` commit (6d1d338) → `feat(03-09)` commit (fac3a21) ## Known Stubs None. All changes are functional code. ## Threat Flags No new network endpoints, auth paths, file access patterns, or schema changes introduced. The resolveUserId change closes a security gap (CR-06) by ensuring unauthenticated requests correctly 401 while authenticated OIDC sessions get through. ## Self-Check: PASSED - events.ts: FOUND - events.test.ts: FOUND - 03-09-SUMMARY.md: FOUND - 944693f (test RED task1): FOUND - 99cb169 (feat GREEN task1): FOUND - 6d1d338 (test RED task2): FOUND - fac3a21 (feat GREEN task2): FOUND