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).
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 |
|
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.
Key Link Verification
| 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)