Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
14 KiB
phase, verified, status, score, overrides_applied, re_verification, human_verification
| phase | verified | status | score | overrides_applied | re_verification | human_verification | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 12-initial-setup-wizard | 2026-06-15T15:35:00Z | human_needed | 9/9 must-haves verified | 0 |
|
|
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-15T15:35:00Z
Status: human_needed
Re-verification: Yes — after CR-01 gap closure (commit 0d53249)
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.tsxline 30:validateSetupVapidimported- Line 445-448:
validationRowsstate type isPick<ValidationRowStatus, 'db' | 'oidc' | 'vapid'>withvapid: 'idle' - Lines 461-479: sequential chain DB → OIDC → VAPID enforced in
configMutation.onSuccess;setBothPassed(true)only called aftervalidateSetupVapid()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
0d53249GREEN 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 |
Key Link Verification
| 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/status → isSetupLocked() → 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