---
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
