Files
familysync/.planning/phases/12-initial-setup-wizard/12-VERIFICATION.md
T
Lucas BergerandClaude Opus 4.8 e821515d25
CI / changes (pull_request) Successful in 4s
CI / fast-checks (pull_request) Failing after 1m4s
CI / api (pull_request) Failing after 1m30s
CI / harness (pull_request) Failing after 1h2m7s
CI / security (pull_request) Failing after 13s
CI / gate (pull_request) Failing after 1s
docs(12): resolve VERIFICATION human-needed — wizard e2e satisfied via UAT re-verify
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-16 16:23:15 -04:00

14 KiB

phase, verified, status, score, overrides_applied, human_verification_resolved, re_verification, human_verification
phase verified status score overrides_applied human_verification_resolved re_verification human_verification
12-initial-setup-wizard 2026-06-16T20:20:00Z passed 9/9 must-haves verified 0 2026-06-16 — end-to-end wizard test against real Fastmail completed by operator (12-UAT.md Test 5: CalDAV PROPFIND validated, 'Setup complete' shown, /setup locks). UAT re-verification confirmed all 6 diagnosed gaps closed (12-UAT.md status: complete; 6 passed, 1 env-blocked-but-test-covered).
previous_status previous_score gaps_closed gaps_remaining regressions
gaps_found 8/9
VAPID validation wired into wizard UI: validateSetupVapid imported, vapid ValidationRow rendered, sequential DB → OIDC → VAPID chain enforced, bothPassed gates on all three
test expected why_human resolved
Complete the wizard end-to-end against a real Fastmail account Step 3 Credential entry with the operator's real Fastmail email + app password (CalDAV scope) produces 'Credential verified.' and then 'Setup complete' terminal screen with Sign in link to /; re-navigating to /setup shows 'Already Locked' screen Requires live Fastmail CalDAV PROPFIND against a real account and real app password; no mock can substitute for the live endpoint validation 2026-06-16 — operator completed the wizard end-to-end against their real Fastmail account (12-UAT.md Test 5). DB evidence: setup_complete=true, credential me@lucasberger.ca stored; 'Setup complete' terminal shown; /setup locks (Test 6).

Phase 12: Initial Setup Wizard — Re-Verification Report

Phase Goal: On first run (no admin/credentials configured), the operator is guided through a validated, step-by-step wizard to bootstrap the app — env presence, generated secrets to copy, DB/OIDC/VAPID/app-password validation — instead of hand-editing .env / docker-compose.yml; once complete, the setup endpoints lock.

Verified: 2026-06-16T20:20:00Z Status: passed (human verification resolved 2026-06-16 — see 12-UAT.md) Re-verification: Yes — after CR-01 gap closure (commit 0d53249); human end-to-end item satisfied via 12-UAT.md re-verification


Re-Verification Summary

The single BLOCKER from initial verification (CR-01: VAPID validation absent from wizard UI) is now CLOSED. Code evidence:

  • apps/pwa/src/routes/SetupPage.tsx line 30: validateSetupVapid imported
  • Line 445-448: validationRows state type is Pick<ValidationRowStatus, 'db' | 'oidc' | 'vapid'> with vapid: 'idle'
  • Lines 461-479: sequential chain DB → OIDC → VAPID enforced in configMutation.onSuccess; setBothPassed(true) only called after validateSetupVapid() resolves
  • Lines 665-673: <ValidationRow state={validationRows.vapid} ...> rendered in Step2Config JSX
  • Line 691: {bothPassed && configSaved && <ActionRow ...>} gates "Continue" on all three passing
  • PWA tests: 249/249 pass (commit 0d53249 GREEN run confirmed)
  • Both apps typecheck clean

Goal Achievement

Observable Truths

# Truth Status Evidence
1 Schema migration makes users.oidc_iss/oidc_sub nullable, adds users.claimed, and is applied to the dev DB VERIFIED 0002_lethal_millenium_guard.sql has ALTER TABLE MODIFY COLUMN making both nullable + ADD COLUMN claimed boolean NOT NULL; schema.ts confirms notNull() removed from both; _journal.json references the migration
2 Existing OIDC users are backfilled claimed=true VERIFIED Migration SQL: UPDATE users SET claimed = true WHERE oidc_iss IS NOT NULL; present
3 npm run generate-secrets prints SESSION_SECRET, APP_PASSWORD_ENCRYPTION_KEY, VAPID_PUBLIC_KEY, VAPID_PRIVATE_KEY for pasting into env — never to the DB VERIFIED Live run: all 4 values produced with correct shapes (64 hex / 64 hex / ~87 base64url / ~43 base64url); no writeFile/appendFile/fetch/db imports; root package.json "generate-secrets" script confirmed
4 GET /api/setup/status is reachable pre-auth and returns {setupComplete:false/true} VERIFIED setupRouter mounted at line 49 in index.ts, BEFORE devAuthBypass() at line 54; /status calls isSetupLocked() and returns { setupComplete: locked }; App.tsx gate confirmed by 249/249 PWA tests
5 Wizard collects non-secret config (oidc_issuer, oidc_client_id, vapid_public_key, app_external_url) into app_config via POST /api/setup/config VERIFIED setup.ts /config handler upserts all 4 keys via onDuplicateKeyUpdate; configSchema validates oidcIssuer as https-only URL
6 Each input validates before completing: DB connects, VAPID structurally valid (32/65-byte via setVapidDetails), OIDC discovery resolves, Fastmail app password reaches CalDAV PROPFIND VERIFIED DB (validateSetupDb), OIDC (validateSetupOidc), and VAPID (validateSetupVapid) now run sequentially in configMutation.onSuccess; bothPassed gates on all three; CalDAV PROPFIND runs via /credential; VAPID ValidationRow renders at line 665
7 A second call to any mutating setup endpoint after completion returns 423 (per-call guard) VERIFIED isSetupLocked() called as first statement in all 6 mutating handlers; no module-level cache; Pitfall-8 double-complete test in setup.test.ts; 249/249 PWA tests pass
8 POST /api/setup/complete promotes the local user to admin, sets app_config.setup_complete, after which the guard locks VERIFIED /credential inserts user with isAdmin:true + calls validateEncryptAndStoreCredential; /complete upserts setup_complete='true'; isSetupLocked() checks this flag on every call
9 First OIDC login after setup_complete claims the unclaimed local user (preserving is_admin, no email keying) VERIFIED user.ts: reads app_config.setup_complete, then queries WHERE isNull(users.oidcIss) AND eq(users.claimed, false) LIMIT 1; sets claimed:true; shouldBeAdmin gated on flagRow?.value !== 'true'; no email-keyed lookup

Score: 9/9 truths verified


Deferred Items

None.


Required Artifacts

Artifact Expected Status Details
apps/api/src/db/migrations/0002_lethal_millenium_guard.sql nullable oidc_iss/oidc_sub + claimed column + backfill UPDATE VERIFIED All three DDL changes confirmed; backfill present
scripts/generate-secrets.mjs Bootstrap secret generation helper VERIFIED Produces 4 correct-shape values; no side-effects
apps/api/src/lib/setupGuard.ts isSetupLocked() — real per-call DB evaluation VERIFIED Both branches (setup_complete flag + effective-config); no module-level cache
apps/api/src/routes/setup.ts 7-route setup router VERIFIED All 7 routes present; 6 mutating routes guard-first; noEchoHook on credential; VAPID private key from env only
apps/api/src/auth/user.ts upsertUser with first-login-claims branch VERIFIED setup_complete read; unclaimed query; no email keying; shouldBeAdmin gated
apps/pwa/src/routes/SetupPage.tsx Standalone multi-step wizard with DB, OIDC, and VAPID validation rows VERIFIED 1100+ lines; VAPID import at line 30; vapid ValidationRow at line 665; sequential chain lines 461-479; bothPassed gates Continue
apps/pwa/src/api/client.ts fetchSetupStatus, postSetupConfig, validateSetupDb/Oidc/Vapid, postSetupCredential, postSetupComplete VERIFIED All 6+ functions exported; SetupAlreadyLockedError class; 423 handled; validateSetupVapid at line 654
apps/pwa/src/App.tsx setup-status gate + /setup route + redirect VERIFIED setupQuery with staleTime:0; /setup route; SetupPage imported; setupComplete===false triggers Navigate; 249/249 PWA tests

From To Via Status Details
apps/api/src/index.ts apps/api/src/routes/setup.ts app.route('/api/setup', setupRouter) WIRED Line 49; before devAuthBypass() at line 54 — mount ordering verified
apps/api/src/routes/setup.ts apps/api/src/lib/setupGuard.ts isSetupLocked() first in every handler WIRED 10 grep hits; 6 with if-locked-return-423
apps/api/src/routes/setup.ts apps/api/src/broker/credentialSync.ts validateEncryptAndStoreCredential(localUserId, ...) WIRED Present in /credential handler
apps/pwa/src/App.tsx /api/setup/status fetchSetupStatus in setupQuery WIRED fetchSetupStatus imported from client.ts; staleTime:0; redirects on setupComplete===false
apps/pwa/src/routes/SetupPage.tsx /api/setup/config, /validate/db, /validate/oidc, /validate/vapid, /credential, /complete TanStack mutations WIRED All 6 routes called; validateSetupVapid called at line 469
apps/api/src/auth/middleware.ts app_config (oidc_issuer, oidc_client_id, app_external_url) oidcConfigFallbackMiddleware WIRED Reads 3 keys from app_config when env vars absent

Data-Flow Trace (Level 4)

Artifact Data Variable Source Produces Real Data Status
SetupPage.tsx / ValidationRow validationRows.db, validationRows.oidc, validationRows.vapid validateSetupDb(), validateSetupOidc(), validateSetupVapid() mutations Yes — live HTTP calls to backend routes FLOWING
App.tsx / setup gate setupQuery.data.setupComplete fetchSetupStatus()GET /api/setup/statusisSetupLocked() → DB Yes — real DB read on every load FLOWING

Behavioral Spot-Checks

Behavior Command Result Status
generate-secrets produces 4 correct values Carried from initial verification All 4 patterns matched PASS
/api/setup mounted before /api/* OIDC guard Carried from initial verification setup at line 49, devAuthBypass at line 54 PASS
validateSetupVapid imported in SetupPage.tsx grep -n "validateSetupVapid" apps/pwa/src/routes/SetupPage.tsx Line 30: import; line 469: call site PASS
VAPID sequential chain: vapid only runs after OIDC passes Lines 466-479 in SetupPage.tsx Nested try-catch confirms DB → OIDC → VAPID ordering PASS
bothPassed only set after all three pass Line 471: setBothPassed(true) inside innermost try after validateSetupVapid() Confirmed PASS
PWA typecheck clean pnpm --filter @familysync/pwa typecheck Exit 0, no errors PASS
API typecheck clean pnpm --filter @familysync/api typecheck Exit 0, no errors PASS
PWA tests 249/249 pass pnpm --filter @familysync/pwa test --run 249 passed (21 test files) PASS
API tests pnpm --filter @familysync/api test --run ER_ACCESS_DENIED — MariaDB service not running in dev host shell; not a code defect SKIP (env)

Probe Execution

Step 7c: No probe-*.sh scripts declared or present for Phase 12.


Requirements Coverage

Requirement Plans Description Status Evidence
SETUP-01 02, 03, 04 Guided wizard bootstrap on first run SATISFIED /status pre-auth gate; App.tsx redirect; SetupPage renders standalone; first-login-claims in user.ts; all 4 wizard steps functional
SETUP-02 02, 04 Wizard validates each input before completing SATISFIED DB + OIDC + VAPID + CalDAV all validated; VAPID ValidationRow rendered; bothPassed gates on all three; gap CR-01 closed in commit 0d53249
SETUP-03 01 Secrets generated for copy-paste; never persisted SATISFIED generate-secrets.mjs prints 4 values to stdout only; no file/DB writes; VAPID_PRIVATE_KEY read from env only in /validate/vapid
SETUP-04 01, 02 Setup endpoints lock after completion; guard per-call SATISFIED isSetupLocked() first in all 6 mutating handlers; no module-level cache; effective-config branch; Pitfall-8 double-complete test present

Anti-Patterns Found

File Line Pattern Severity Impact
None found No TBD/FIXME/XXX markers in SetupPage.tsx

Code-review warnings from 12-REVIEW.md (WR-01..WR-05, IN-01..IN-04) are advisory and do not block the phase goal. They are carried as follow-ups:

Finding File Severity Goal Impact
WR-01: orphaned user row on re-select failure setup.ts Warning Edge-case only; does not affect primary flow
WR-02: concurrent /credential creates duplicate unclaimed rows setup.ts Warning Low-probability race; self-hosted 2-person app
WR-03: misleading test mock hides missing 2nd /credential 423 coverage setup.test.ts Warning Test coverage gap for effective-config branch
WR-04: appExternalUrl accepts http:// setup.ts Warning Operator can set non-HTTPS redirect_uri; Authelia will reject at OIDC login
WR-05: oidcConfigFallbackMiddleware permanently mutates process.env middleware.ts Warning No update path without container restart; affects test isolation

Human Verification Required

1. End-to-End Wizard Completion with Real Fastmail Credentials

Test: Bring up a fresh instance (no setup_complete, no member_credentials). Navigate to the app root, confirm redirect to /setup. Complete all wizard steps: Welcome → Instance Config (with real OIDC/VAPID values from generate-secrets, real Authelia issuer/client-id) → Credential (with a real Fastmail account email + CalDAV app password). Confirm "Setup complete" terminal screen appears with Sign in link. Re-navigate to /setup and confirm "Already Locked" screen.

Expected: Step 2 runs DB → OIDC → VAPID validations sequentially and all three show success before "Continue" appears. Step 3 posts to /api/setup/credential (CalDAV PROPFIND succeeds against Fastmail), then /api/setup/complete returns 200, wizard shows terminal screen. /setup after that shows AlreadyLocked.

Why human: Requires a live Fastmail CalDAV PROPFIND against a real account with a real app password scoped to Calendars/CalDAV. Authelia OIDC discovery requires the Authelia instance to be reachable from the API container. No mock can substitute for either live endpoint.


Gaps Summary

No code gaps remain. The single BLOCKER (CR-01, SETUP-02 VAPID validation absent from wizard UI) was resolved in commit 0d53249. All 9 must-have truths are VERIFIED in code. The only remaining item is the human end-to-end test against live Fastmail credentials.


Verified: 2026-06-15T15:35:00Z Verifier: Claude (gsd-verifier) — re-verification after CR-01 gap closure