Files
familysync/.planning/phases/01-foundation-broker-spike/01-VERIFICATION.md
T
Lucas Berger 5bed2228b7 test(01): verification (human_needed, 11/13) + persist Gate 2 human-UAT
Code verified against codebase, 24 tests pass, CAL-01 + CAL-08 proven live.
AUTH-01/02/03 live login + SSE-over-Pangolin smoke test require operator infra —
tracked as 01-HUMAN-UAT.md (status: partial).
2026-06-04 11:44:52 -04:00

18 KiB

phase, verified, status, score, overrides_applied, human_verification
phase verified status score overrides_applied human_verification
01-foundation-broker-spike 2026-06-04T11:45:00Z human_needed 10/13 must-haves verified (3 human-pending) 0
test expected why_human
AUTH-01: Open https://familysync.<domain> from an external network, confirm redirect to Authelia authorize endpoint, log in, land on shell with name + color — no Fastmail credential prompt Browser redirects to Authelia, login succeeds, PWA shell renders member name and color swatch (MemberBadge component) Requires live Authelia + Pangolin infrastructure not available in this environment; oidcAuthMiddleware only validates at runtime against a real OIDC issuer
test expected why_human
AUTH-02: Close browser completely after step above, reopen the public URL, confirm no re-login required Session cookie persists; /api/me still returns 200 without re-authenticating; access-token refresh rotation has kept the session alive Session persistence is enforced by @hono/oidc-auth refresh-token rotation at runtime; cannot verify without real Authelia token endpoint
test expected why_human
AUTH-03 (cross-member): Log in as the second member (wife) over the same public URL, confirm she receives a visually distinct color from Lucas palette[1] (#E8734A warm coral) assigned; both members' MemberBadge components show different colors; identity row for second member present in users table Requires a second live Authelia account and real browser session; unit tests verify color assignment logic but not end-to-end identity creation for both members
test expected why_human
SSE smoke test (D-08): From external network with a valid session cookie, run 'curl -N https://familysync.<domain>/api/sse/heartbeat' for 5+ minutes, confirm heartbeat events keep arriving Server-sent events arrive every 10 seconds without proxy timeout; stream stays alive through the Pangolin/Newt tunnel for Phase 4 transport decision Pangolin idle-timeout behavior is network-infrastructure-dependent; only observable over the real tunnel

Phase 01: Foundation + Broker Spike — Verification Report

Phase Goal: The app stack is running, both members can authenticate through Authelia OIDC, and the CalDAV broker can read Fastmail calendars — with a confirmed go/no-go decision on personal-calendar cross-account sharing. Verified: 2026-06-04T11:45:00Z Status: human_needed Re-verification: No — initial verification


Goal Achievement

Observable Truths

# Truth Status Evidence
1 Docker Compose stack starts MariaDB healthy and API serving VERIFIED docker-compose.yml has mariadb:11 with healthcheck; api depends_on service_healthy; 01-01-SUMMARY confirms /health returned {"ok":true,"db":"up"} live
2 GET /health returns 200 with real DB round-trip VERIFIED apps/api/src/routes/health.ts executes db.execute(sql'SELECT 1') before returning {ok:true,db:"up"}; health.test.ts 2/2 green
3 Drizzle schema pushed to live MariaDB (4 tables) VERIFIED 01-01-SUMMARY records drizzle-kit push clearing Task 3 checkpoint; all 4 tables listed in SHOW TABLES output
4 React PWA fetches /health and renders result VERIFIED apps/pwa/src/App.tsx uses useQuery({queryKey:['health'],queryFn:fetchHealth}) and renders "stack: up/down"
5 OIDC middleware guards /api/* and redirects unauthenticated requests (AUTH-01 code path) VERIFIED apps/api/src/index.ts line 24: app.use('/api/*', oidcAuthMiddleware()); /callback registered before guard; /health before guard
6 upsertUser keyed on oidc_iss + oidc_sub with stable color assignment (AUTH-03) VERIFIED apps/api/src/auth/user.ts SELECT by and(eq(users.oidcIss,...),eq(users.oidcSub,...)), never email; COLOR_PALETTE 6 entries; user.test.ts 6/6 green
7 GET /api/me returns authenticated member identity + color VERIFIED apps/api/src/routes/me.ts calls getAuth → upsertUser → returns {user:{id,displayName,color}}; mounted behind oidcAuthMiddleware
8 AES-256-GCM app-password encryption: lossless roundtrip, unique IVs, tamper detection VERIFIED apps/api/src/broker/crypto.ts uses node:crypto aes-256-gcm, 96-bit random IV; crypto.test.ts 5/5 green (roundtrip, IV uniqueness, tamper-throws x2, payload shape)
9 CalDAV broker reads Fastmail calendars and caches VEVENTs with D-13 all-day DATE handling VERIFIED CAL-08-DECISION.md records live REPORT against Fastmail fetching 503 real events; sync.test.ts 6/6 green confirming timed→dtstart_utc, all-day→dtstart_date, onDuplicateKeyUpdate idempotency
10 Broker poller skips DB writes when ctag unchanged VERIFIED apps/api/src/broker/poller.ts lines 59-61: skip when currentCtag !== null && currentCtag === knownCtag; poller.test.ts 5/5 green
11 CAL-08 go/no-go decision recorded VERIFIED CAL-08-DECISION.md present, committed (0b074cd), contains "Decision: GO" — per-member app-password model proven live; 503 events cached; cross-account ACL concern resolved as moot
12 Both members authenticate live through Authelia over Pangolin (AUTH-01/02) HUMAN-PENDING Code wired (oidcAuthMiddleware, processOAuthCallback, OIDC env vars); live verification requires real Authelia + Pangolin infrastructure
13 Both members have distinct stable colors confirmed in real browser (AUTH-03 cross-member) HUMAN-PENDING upsertUser unit-tested for color assignment; live cross-member test requires two real OIDC sessions

Score: 11/13 truths verified (2 human-pending, counted as HUMAN-PENDING not FAILED; see requirements section for SSE smoke test)


Required Artifacts

Artifact Expected Status Details
apps/api/src/db/schema.ts 4 mysqlTable defs, D-13 dtstart split, D-10 iss+sub unique key VERIFIED All 4 tables; unique('uniq_oidc_identity').on(oidcIss,oidcSub); separate dtstart_utc (timestamp) + dtstart_date (date) + allDay boolean
apps/api/src/db/client.ts drizzle(mysql2 pool) db singleton export VERIFIED Exports db = drizzle({client:pool,schema,mode:'default'}); connectionLimit 10
apps/api/src/routes/health.ts GET / with real DB round-trip VERIFIED SELECT 1 via db.execute; returns 200 or 503; unauthenticated
docker-compose.yml api + mariadb:11 (healthcheck) + redis VERIFIED mariadb healthcheck using healthcheck.sh --connect; api depends_on service_healthy; redis present
apps/api/src/auth/user.ts upsertUser + COLOR_PALETTE VERIFIED Exports both; palette 6 entries; SELECT-first idempotent pattern
apps/api/src/auth/middleware.ts oidcAuthMiddleware re-export VERIFIED Re-exports oidcAuthMiddleware, processOAuthCallback, getAuth from @hono/oidc-auth
apps/api/src/routes/me.ts GET /api/me → {user:{id,displayName,color}} VERIFIED getAuth → upsertUser → returns user shape; exports meRouter
apps/api/src/broker/crypto.ts encryptPassword/decryptPassword (AES-256-GCM) VERIFIED node:crypto aes-256-gcm, 96-bit IV, JSON {iv,authTag,ciphertext} payload
apps/api/src/broker/client.ts createFastmailClient → tsdav DAVClient VERIFIED caldav.fastmail.com, Basic auth, caldav account type
apps/api/src/broker/sync.ts syncCalendar: REPORT → ical.js → upsert VERIFIED ical.js ICAL.Time.isDate routing, onDuplicateKeyUpdate on calendarId+uid
apps/api/src/broker/poller.ts startBrokerPoller (node-cron */5) + ctag detection VERIFIED schedule('*/5 * * * *'); ctag skip logic; per-credential try/catch; exports runPoll for tests
apps/api/src/routes/events.ts GET /api/events reads DB cache only VERIFIED db.select().from(calendarEvents); no tsdav import; exports eventsRouter
apps/api/src/routes/sse.ts GET /api/sse/heartbeat (streamSSE) VERIFIED streamSSE every 10s; exports sseRouter; mounted behind oidcAuthMiddleware
apps/api/src/broker/spike.ts CAL-08 spike script VERIFIED createFastmailClient → fetchCalendars → print URLs; never logs password
.planning/phases/01-foundation-broker-spike/CAL-08-DECISION.md Decision: GO/NO-GO recorded VERIFIED "Decision: GO"; 503 events cached; ctag/syncToken findings recorded
apps/pwa/src/App.tsx Renders MemberBadge + EventProof VERIFIED useQuery(['me']) → MemberBadge; EventProof component rendered
apps/pwa/src/components/EventProof.tsx Fetches /api/events, renders first event VERIFIED useQuery(['events'],fetchEvents); renders event SUMMARY + date or empty state
apps/api/tests/auth/user.test.ts 6 passing tests VERIFIED 6/6 green (palette[0], palette[1], idempotent, iss+sub identity, full row shape, COLOR_PALETTE validity)
apps/api/tests/broker/crypto.test.ts 5 passing tests VERIFIED 5/5 green
apps/api/tests/broker/sync.test.ts 6 passing tests VERIFIED 6/6 green
apps/api/tests/broker/poller.test.ts 5 passing tests VERIFIED 5/5 green
apps/api/tests/health.test.ts 2 passing tests VERIFIED 2/2 green

Artifact total: 22/22 present and substantive.


From To Via Status Details
apps/api/src/routes/health.ts apps/api/src/db/client.ts db.execute() WIRED import { db } from '../db/client.js'; execute called in route handler
apps/pwa/src/App.tsx /health fetch('/health') in fetchHealth WIRED fetchHealth calls fetch('/health') inside useQuery
apps/api/src/routes/me.ts apps/api/src/auth/user.ts upsertUser(iss, sub, email) WIRED import { upsertUser } + called in handler body
apps/api/src/index.ts @hono/oidc-auth oidcAuthMiddleware on /api/* WIRED app.use('/api/*', oidcAuthMiddleware()) line 24
apps/pwa/src/App.tsx /api/me React Query via fetchMe WIRED import { fetchMe } + useQuery({queryKey:['me'],queryFn:fetchMe})
apps/api/src/broker/poller.ts apps/api/src/broker/crypto.ts decryptPassword WIRED import { decryptPassword } + called before createFastmailClient
apps/api/src/broker/sync.ts apps/api/src/db/client.ts calendarEvents upsert WIRED import { db } + db.insert(calendarEvents)...onDuplicateKeyUpdate()
apps/api/src/routes/events.ts apps/api/src/db/client.ts cache read WIRED import { db } + db.select().from(calendarEvents)
apps/api/src/index.ts apps/api/src/broker/poller.ts startBrokerPoller() on boot WIRED import { startBrokerPoller } + called unconditionally at module level
apps/api/src/index.ts apps/api/src/routes/events.ts app.route('/api/events', eventsRouter) WIRED Line 28 in index.ts
apps/pwa/src/components/EventProof.tsx /api/events React Query via fetchEvents WIRED import { fetchEvents } + useQuery({queryKey:['events'],queryFn:fetchEvents})

All 11 key links WIRED.


Data-Flow Trace (Level 4)

Artifact Data Variable Source Produces Real Data Status
apps/pwa/src/App.tsx (MemberBadge) meQuery.data.user /api/me → upsertUser → MariaDB users table Yes — upsertUser does SELECT then INSERT; backed by real DB FLOWING
apps/pwa/src/App.tsx (health indicator) healthQuery.data /health → db.execute SELECT 1 Yes — real DB round-trip FLOWING
apps/pwa/src/components/EventProof.tsx events[0] /api/events → db.select().from(calendarEvents) Yes — live Fastmail REPORT cached 503 events into MariaDB (CAL-08-DECISION.md); dev DB populated FLOWING

Behavioral Spot-Checks

Behavior Command Result Status
All 24 unit tests pass cd apps/api && pnpm vitest run 24/24 passed, 5 test files PASS
TypeScript compiles clean (api) cd apps/api && pnpm exec tsc --noEmit Exit 0, no errors PASS
tsdav imported only under broker module grep -rn "import.*tsdav" apps/api/src/ broker/sync.ts, broker/client.ts only PASS
events route imports no tsdav or broker client grep -rn "createFastmailClient|tsdav" apps/api/src/routes/ No matches PASS
oidcAuthMiddleware mounted on /api/* grep -n "oidcAuthMiddleware" apps/api/src/index.ts Line 24: app.use('/api/*', oidcAuthMiddleware()) PASS

Probe Execution

No scripts/*/tests/probe-*.sh files declared or found. Task 3 of Plan 01 and Tasks 2+3 of Plan 04 are checkpoint:human-action gates that require live infrastructure. These are routed to Human Verification.


Requirements Coverage

Requirement Source Plan Description Status Evidence
AUTH-01 01-02 User can log in through Authelia (OIDC SSO) HUMAN-PENDING Code wired: oidcAuthMiddleware on /api/*, processOAuthCallback on /callback; live Authelia test pending
AUTH-02 01-02 User stays logged in across sessions HUMAN-PENDING @hono/oidc-auth refresh-token rotation documented in middleware.ts; live session persistence test pending
AUTH-03 01-02 Stable iss+sub identity + consistent per-member color PARTIALLY VERIFIED Unit-tested fully (6 tests); live cross-member color check pending human verification
CAL-01 01-01, 01-03 Broker reads shared Fastmail calendar via CalDAV, caches locally (ctag polling) VERIFIED CAL-08-DECISION.md: 503 real events cached live via REPORT; poller ctag-detection unit tested
CAL-08 01-04 Personal calendar spike go/no-go VERIFIED CAL-08-DECISION.md: Decision GO; per-member app-password model proven; no fallback needed

Anti-Patterns Found

File Pattern Severity Impact
None found

No TBD/FIXME/XXX markers in source files. No return null or return [] stubs in non-test production code. No hardcoded empty values flowing to rendering. No console.log of credentials or encryption key in broker code.

The one notable "pending" marker is in CAL-08-DECISION.md history — the initial template had Decision: [PENDING] but was overwritten by commit 0b074cd with the live GO decision. Current state is fully filled.


Human Verification Required

1. AUTH-01 — Authelia OIDC Login (Member 1: Lucas)

Test: From an external network (not the Docker host), open https://familysync.<domain>. Confirm the browser is redirected to Authelia's authorize endpoint. Log in with Lucas's Authelia credentials. Confirm landing on the PWA shell showing MemberBadge (name + color swatch). Expected: Successful redirect to Authelia, login completes, PWA renders MemberBadge with Lucas's display name and assigned hex color (#4A90D9 if first user). No separate FamilySync login, no Fastmail credential prompt. Why human: oidcAuthMiddleware contacts the real OIDC issuer at runtime; OIDC_ISSUER, OIDC_CLIENT_ID, OIDC_CLIENT_SECRET, OIDC_REDIRECT_URI, OIDC_AUTH_EXTERNAL_URL must all be set and Authelia must have the client registered (YAML in 01-02-SUMMARY.md).


2. AUTH-02 — Session Persistence Across Browser Restart

Test: After AUTH-01 passes, close the browser completely (not just the tab). Reopen https://familysync.<domain>. Confirm no Authelia login prompt appears — the PWA loads directly to the authenticated shell. Expected: Session cookie survives browser restart; @hono/oidc-auth refresh-token rotation silently renews the access token; /api/me returns 200 without re-authentication. Why human: Session cookie persistence and refresh-token rotation require live interaction with Authelia's token endpoint over time.


3. AUTH-03 — Cross-Member Distinct Color (Member 2: Wife)

Test: Log in as the second member (wife) at https://familysync.<domain>. Confirm she lands on the PWA shell with a MemberBadge showing a different color from Lucas. Expected: palette[1] (#E8734A warm coral) assigned to wife's account; both members' shells display visually distinct color swatches; wife's users row is present in MariaDB with a different color value. Why human: Requires a second live Authelia account and a real second browser session to trigger upsertUser for the second member.


4. SSE Smoke Test — Pangolin Pass-Through (D-08)

Test: From an external network with a valid authenticated session cookie, run: curl -N -b "session=<cookie>" https://familysync.<domain>/api/sse/heartbeat. Keep the connection open for 5+ minutes. Expected: Heartbeat events ({"ts":"...","id":0}, {"ts":"...","id":1}, ...) arrive every 10 seconds without the stream being cut. Result is PASS (SSE viable for Phase 4 real-time list sync) or FAIL (Pangolin idle-timeout needs configuration). Why human: Pangolin/Newt proxy idle-timeout behavior is network-infrastructure-dependent and can only be observed over the real tunnel. The sseRouter code is verified; tunnel compatibility is the open question.


Gaps Summary

No gaps (no must-haves are FAILED or MISSING). All code artifacts are present, substantive, and wired. All 24 unit tests pass. TypeScript is clean. The CAL-08 GO decision is recorded from a live run.

The 4 human verification items are classified as HUMAN-PENDING (code implemented, live operator confirmation required), not as failures. This is the expected terminal state for a phase whose Plans 01-04 included checkpoint:human-action tasks requiring real Authelia + Pangolin infrastructure that is not available in the dev environment.

The 01-04 SUMMARY explicitly notes "Code-complete. Live verification (CAL-08 spike + Authelia login + Pangolin SSE smoke) pending human action." CAL-08 was subsequently cleared (commit 0b074cd). AUTH-01/02/03 live tests and SSE smoke remain pending.


Verified: 2026-06-04T11:45:00Z Verifier: Claude (gsd-verifier)