Files
2026-06-18 22:21:38 -04:00

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
hono
drizzle
vitest
tdd
setup-wizard
oidc
vapid
pre-auth
guard
phase provides
12-01 setupGuard.ts stub, setup.ts stub router, Wave-0 RED test scaffolds, schema claimed column
apps/api/src/lib/setupGuard.ts — real isSetupLocked() per-call DB evaluation (SETUP-04/D-10)
apps/api/src/routes/setup.ts — setupRouter with all 7 pre-auth handlers
apps/api/src/index.ts — setupRouter mounted pre-auth before devAuthBypass
apps/api/src/auth/middleware.ts — oidcConfigFallbackMiddleware (env-OR-app_config, D-02/D-03)
apps/api/tests/routes/setup.test.ts — 17 integration tests all GREEN
12-03-pwa-setup-page (consumes /api/setup/* routes, esp. GET /status)
12-04-integration (full setup flow)
added patterns
isSetupLocked() per-call freshness pattern (D-10) — imported in every handler, no module-cache
guard-first handler pattern — isSetupLocked() is the FIRST await in every setup handler
noEchoHook anti-echo pattern (from admin.ts) — Zod error details never returned on credential routes
validateEncryptAndStoreCredential reuse (D-09) — no new crypto; shared helper for PROPFIND+encrypt+store
env-OR-app_config fallback middleware — reads DB per-request when env absent; injects into process.env
mysql2 $returningId() + re-select for local user insert (Pattern 4 from user.ts)
onDuplicateKeyUpdate upsert for app_config writes (Shared Pattern 1 from admin.ts)
created modified
apps/api/src/lib/setupGuard.ts
apps/api/src/routes/setup.ts
apps/api/src/index.ts
apps/api/src/auth/middleware.ts
apps/api/tests/routes/setup.test.ts
apps/api/tests/routes/push.test.ts
A2-CONFIRMED: @hono/oidc-auth reads OIDC_ISSUER/OIDC_CLIENT_ID/OIDC_AUTH_EXTERNAL_URL at per-request call time via env(c)→process.env — NOT at import time; fresh boot without OIDC env is safe (HTTP 500 only on protected /api/* requests)
D-02-FALLBACK: env-OR-app_config Recommendation (a) implemented: oidcConfigFallbackMiddleware reads from app_config when process.env absent, injects into process.env before oidcAuthMiddleware() per-request read
GUARD-ON-STATUS: GET /api/setup/status uses isSetupLocked() directly (covers effective-config branch too) — returns {setupComplete:true} when locked, {setupComplete:false} when not; aligns with must_haves.truths
LOCAL-USER-ROLLBACK: POST /api/setup/credential rolls back the local user insert if validateEncryptAndStoreCredential throws, preventing orphaned unclaimed user rows
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() in setupGuard.ts: reads app_config.setup_complete (check 1) and then checks member_credentials row + VAPID_PRIVATE_KEY/VAPID_PUBLIC_KEY env 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 in setup.test.ts into real integration tests (17 tests) — all GREEN after Task 2.
  • Implemented full setupRouter in setup.ts with all 7 routes:
    • GET /status — uses isSetupLocked() directly; returns {setupComplete: boolean}
    • POST /config — zod-validates https-URL issuer; upserts oidc_issuer, oidc_client_id, vapid_public_key, app_external_url
    • POST /validate/dbSELECT 1 connectivity check; 200/503
    • POST /validate/oidc — fetches discovery doc with 5s timeout; 200/400
    • POST /validate/vapidwebpush.setVapidDetails() structural check; env-only key read; 200/400
    • POST /credential — inserts local user first (Pitfall 5 FK), calls shared helper; noEchoHook; rollback on failure
    • POST /complete — upserts setup_complete='true'; 200 first call, 423 second (Pitfall 8/SETUP-04)
  • Mounted setupRouter in index.ts BEFORE devAuthBypass() (line 49 < line 54, T-12-09/Pitfall 1 acceptance-checked).
  • Implemented oidcConfigFallbackMiddleware in auth/middleware.ts: reads OIDC config from app_config when env absent, injects into process.env for downstream oidcAuthMiddleware() pickup. Mounted before OIDC guard when !devBypassActive.
  • Confirmed A2: @hono/oidc-auth reads env at per-request call time — boot is safe without OIDC env.
  • Fixed push.test.ts vi.doMock to include oidcConfigFallbackMiddleware stub (Rule 3 auto-fix).

Task Commits

  1. Task 1: isSetupLocked() real impl + RED-first setup tests4748d57 (test)
  2. Task 2: Setup router — all 7 routes + pre-auth mount20f91e4 (feat)
  3. Task 3: OIDC boot env-OR-app_config fallback + mount verification67a9d29 (feat)

Files Created/Modified

  • apps/api/src/lib/setupGuard.ts — real isSetupLocked(): setup_complete check + effective-config branch (D-10); no module-level cache
  • apps/api/src/routes/setup.tssetupRouter with 7 handlers; guard-first; noEchoHook; shared helper reuse; VAPID env-only
  • apps/api/src/index.tssetupRouter import + pre-auth mount; oidcConfigFallbackMiddleware import + mount before OIDC guard
  • apps/api/src/auth/middleware.tsoidcConfigFallbackMiddleware added (env-OR-app_config fallback); re-exports unchanged
  • apps/api/tests/routes/setup.test.ts — 17 real integration tests (all GREEN); full mock scaffolding
  • apps/api/tests/routes/push.test.tsvi.doMock updated to include oidcConfigFallbackMiddleware stub

Decisions Made

  • A2-CONFIRMED: @hono/oidc-auth reads OIDC env vars at per-request call time via env(c) → process.env (source: @hono/oidc-auth dist/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: oidcConfigFallbackMiddleware injects oidc_issuer / oidc_client_id / app_external_url from app_config into process.env when the env var is absent, before oidcAuthMiddleware() reads it per-request. Non-secret values only (D-01 env floor: OIDC_CLIENT_SECRET, OIDC_AUTH_SECRET stay 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/status calls isSetupLocked() to populate setupComplete. This makes the status response consistent with the guard state (covers the effective-config branch too) and satisfies the must_haves truth that /status returns {setupComplete:true} after setup is complete. The route never returns 423 — it always returns 200 with the boolean.

  • LOCAL-USER-ROLLBACK: POST /api/setup/credential deletes the inserted local user row if validateEncryptAndStoreCredential() throws, preventing orphaned claimed=false rows in the users table 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.ts uses vi.doMock('../../src/auth/middleware.js', ...) but the mock omitted the new oidcConfigFallbackMiddleware export. Vitest raises No "oidcConfigFallbackMiddleware" export is defined on the mock at 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 — FOUND
  • apps/api/src/routes/setup.ts — FOUND
  • apps/api/src/auth/middleware.ts — FOUND
  • apps/api/src/index.ts — FOUND
  • apps/api/tests/routes/setup.test.ts — FOUND

Commits exist:

  • 4748d57 — FOUND
  • 20f91e4 — FOUND
  • 67a9d29 — FOUND

Test suite: 394 passed | 5 todo | 0 failed TypeCheck: clean (0 errors)