Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
9.6 KiB
phase, plan, subsystem, tags, requires, provides, affects, tech-stack, key-files, key-decisions, duration, completed
| phase | plan | subsystem | tags | requires | provides | affects | tech-stack | key-files | key-decisions | duration | completed | ||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 12-initial-setup-wizard | 02 | api, auth, testing |
|
|
|
|
|
|
|
15min | 2026-06-15 |
Phase 12 Plan 02: Setup Routes Summary
Real isSetupLocked() 423 guard + all 7 pre-auth /api/setup/ routes + OIDC env-OR-app_config fallback; 394 tests green including Pitfall 8 regression*
Performance
- Duration: 15 min
- Started: 2026-06-15T17:48:42Z
- Completed: 2026-06-15T18:03:21Z
- Tasks: 3
- Files modified: 6
Accomplishments
- Implemented real
isSetupLocked()insetupGuard.ts: readsapp_config.setup_complete(check 1) and then checksmember_credentialsrow +VAPID_PRIVATE_KEY/VAPID_PUBLIC_KEYenv for effective-config branch (D-10 check 2). Re-queries DB fresh every call — no module-level cache. - Converted all 20 Wave-0
it.todo()scaffolds insetup.test.tsinto real integration tests (17 tests) — all GREEN after Task 2. - Implemented full
setupRouterinsetup.tswith all 7 routes:GET /status— usesisSetupLocked()directly; returns{setupComplete: boolean}POST /config— zod-validates https-URL issuer; upsertsoidc_issuer,oidc_client_id,vapid_public_key,app_external_urlPOST /validate/db—SELECT 1connectivity check; 200/503POST /validate/oidc— fetches discovery doc with 5s timeout; 200/400POST /validate/vapid—webpush.setVapidDetails()structural check; env-only key read; 200/400POST /credential— inserts local user first (Pitfall 5 FK), calls shared helper; noEchoHook; rollback on failurePOST /complete— upsertssetup_complete='true'; 200 first call, 423 second (Pitfall 8/SETUP-04)
- Mounted
setupRouterinindex.tsBEFOREdevAuthBypass()(line 49 < line 54, T-12-09/Pitfall 1 acceptance-checked). - Implemented
oidcConfigFallbackMiddlewareinauth/middleware.ts: reads OIDC config fromapp_configwhen env absent, injects intoprocess.envfor downstreamoidcAuthMiddleware()pickup. Mounted before OIDC guard when!devBypassActive. - Confirmed A2:
@hono/oidc-authreads env at per-request call time — boot is safe without OIDC env. - Fixed
push.test.tsvi.doMockto includeoidcConfigFallbackMiddlewarestub (Rule 3 auto-fix).
Task Commits
- Task 1: isSetupLocked() real impl + RED-first setup tests —
4748d57(test) - Task 2: Setup router — all 7 routes + pre-auth mount —
20f91e4(feat) - Task 3: OIDC boot env-OR-app_config fallback + mount verification —
67a9d29(feat)
Files Created/Modified
apps/api/src/lib/setupGuard.ts— realisSetupLocked():setup_completecheck + effective-config branch (D-10); no module-level cacheapps/api/src/routes/setup.ts—setupRouterwith 7 handlers; guard-first; noEchoHook; shared helper reuse; VAPID env-onlyapps/api/src/index.ts—setupRouterimport + pre-auth mount;oidcConfigFallbackMiddlewareimport + mount before OIDC guardapps/api/src/auth/middleware.ts—oidcConfigFallbackMiddlewareadded (env-OR-app_config fallback); re-exports unchangedapps/api/tests/routes/setup.test.ts— 17 real integration tests (all GREEN); full mock scaffoldingapps/api/tests/routes/push.test.ts—vi.doMockupdated to includeoidcConfigFallbackMiddlewarestub
Decisions Made
-
A2-CONFIRMED:
@hono/oidc-authreads OIDC env vars at per-request call time viaenv(c) → process.env(source:@hono/oidc-authdist/index.js line 30). NOT at import time. A fresh unconfigured instance boots without crashing; HTTP 500 only occurs on OIDC-protected/api/*requests when env is absent — acceptable since/api/setup/*is pre-auth and is the only pre-setup surface. Recommendation (a) implemented. -
D-02-FALLBACK:
oidcConfigFallbackMiddlewareinjectsoidc_issuer/oidc_client_id/app_external_urlfromapp_configintoprocess.envwhen the env var is absent, beforeoidcAuthMiddleware()reads it per-request. Non-secret values only (D-01 env floor:OIDC_CLIENT_SECRET,OIDC_AUTH_SECRETstay in env always). Options (b) and (c) (defer mount, lazy-per-request) not needed — option (a) is simpler and correct per A2 confirmation. -
GUARD-ON-STATUS:
GET /api/setup/statuscallsisSetupLocked()to populatesetupComplete. This makes the status response consistent with the guard state (covers the effective-config branch too) and satisfies the must_haves truth that/statusreturns{setupComplete:true}after setup is complete. The route never returns 423 — it always returns 200 with the boolean. -
LOCAL-USER-ROLLBACK:
POST /api/setup/credentialdeletes the inserted local user row ifvalidateEncryptAndStoreCredential()throws, preventing orphanedclaimed=falserows in theuserstable that would permanently increment color slot usage and confuse the first-login-claims query.
Deviations from Plan
Auto-fixed Issues
1. [Rule 3 - Blocking] push.test.ts vi.doMock missing oidcConfigFallbackMiddleware
- Found during: Task 3 test run
- Issue:
push.test.tsusesvi.doMock('../../src/auth/middleware.js', ...)but the mock omitted the newoidcConfigFallbackMiddlewareexport. Vitest raisesNo "oidcConfigFallbackMiddleware" export is defined on the mockat runtime. - Fix: Added
oidcConfigFallbackMiddleware: async (_c, next) => next()to the doMock factory. - Files modified:
apps/api/tests/routes/push.test.ts - Commit:
67a9d29(Task 3)
Total deviations: 1 auto-fixed (Rule 3 blocking — test mock missing new export) Impact on plan: Zero scope creep. Fix was mechanical and localized to a test file.
Threat Surface Scan
No new threat surface beyond what is explicitly modeled in the plan's <threat_model>. All mitigations verified:
| Threat | Mitigation | Verified |
|---|---|---|
| T-12-04: Setup endpoint replay after completion | isSetupLocked() first in every handler; 423; re-queried per call |
All 7 handlers call isSetupLocked() — source-grep ≥7 passed |
| T-12-05: App password echoed in 400 | noEchoHook; no console.log of password or valid('json') |
grep returns 0 echo/log hits |
| T-12-06: VAPID_PRIVATE_KEY in DB or response | /validate/vapid reads ONLY from process.env; never from app_config; never returned |
grep confirms env-only read |
| T-12-08: OIDC issuer SSRF via /config | Zod .refine(v => v.startsWith('https://')) rejects non-https URLs |
Test returns 400 when oidcIssuer is not an https URL passes |
| T-12-09: /api/setup/* caught by OIDC guard | Mounted at line 49, devAuthBypass() at line 54 — ordering verified |
awk mount-order acceptance gate passes |
Self-Check: PASSED
Files exist:
apps/api/src/lib/setupGuard.ts— FOUNDapps/api/src/routes/setup.ts— FOUNDapps/api/src/auth/middleware.ts— FOUNDapps/api/src/index.ts— FOUNDapps/api/tests/routes/setup.test.ts— FOUND
Commits exist:
4748d57— FOUND20f91e4— FOUND67a9d29— FOUND
Test suite: 394 passed | 5 todo | 0 failed TypeCheck: clean (0 errors)