diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md index d20fdf8..115460d 100644 --- a/.planning/ROADMAP.md +++ b/.planning/ROADMAP.md @@ -267,7 +267,7 @@ Plans: **Wave 4 — Gap closure** *(UAT 12-UAT.md gaps 1-6; 06+07 parallel, 05 blocked on 06)* -- [ ] 12-06-PLAN.md — Backend: validate/vapid asserts wizard key == env VAPID_PUBLIC_KEY (gap 2) + status exposes non-secret DB name (gap 3) (SETUP-02) +- [x] 12-06-PLAN.md — Backend: validate/vapid asserts wizard key == env VAPID_PUBLIC_KEY (gap 2) + status exposes non-secret DB name (gap 3) (SETUP-02) - [ ] 12-07-PLAN.md — App.tsx: reverse-gate /setup post-completion (gap 5) + reconcile ['me'] so calendar banner clears after wizard (gap 6) (SETUP-01/04) - [ ] 12-05-PLAN.md — SetupPage: drop DB-vs-env aside (gap 1) + read-only DB-name field (gap 3) + persist fields across Back (gap 4) (SETUP-01) — depends on 12-06 @@ -430,7 +430,7 @@ At ≤767px (`window.matchMedia('(max-width: 767px)')` in `apps/pwa/src/App.tsx` | 9. Faster Write-Back | v1.1 | 2/2 | Complete | 2026-06-12 | | 10. Admin Role & Settings | v1.1 | 4/4 | Complete | 2026-06-13 | | 11. Per-Event Reminders | v1.1 | 5/5 | Complete | 2026-06-14 | -| 12. Initial Setup Wizard | v1.1 | 4/4 | Complete | 2026-06-15 | +| 12. Initial Setup Wizard | v1.1 | 5/7 | In Progress| | | 13. Real Lint Gate (ESLint) | v1.1 | 3/3 | Complete | 2026-06-12 | | 14. Desktop E2E Coverage | v1.1 | 1/1 | Complete | 2026-06-12 | | 15. Doc-Only CI Skip + MD Lint | v1.1 | 3/3 | Complete | 2026-06-12 | diff --git a/.planning/STATE.md b/.planning/STATE.md index 0bbdd78..337c0e5 100644 --- a/.planning/STATE.md +++ b/.planning/STATE.md @@ -2,16 +2,16 @@ gsd_state_version: 1.0 milestone: v1.1 milestone_name: Operability & Polish -status: verifying -stopped_at: Phase 12 Plan 04 Tasks 1-3 complete — paused at Task 4 human-verify checkpoint (Docker rebuild + fresh-DB e2e verification needed) -last_updated: "2026-06-15T19:16:35.449Z" -last_activity: 2026-06-15 +status: executing +stopped_at: Completed 12-06-PLAN.md (UAT gaps 2+3 closed) +last_updated: "2026-06-16T01:15:09.647Z" +last_activity: 2026-06-16 progress: total_phases: 24 - completed_phases: 11 - total_plans: 40 - completed_plans: 40 - percent: 46 + completed_phases: 10 + total_plans: 43 + completed_plans: 41 + percent: 42 --- # Project State @@ -26,9 +26,9 @@ See: .planning/PROJECT.md (updated 2026-06-10) ## Current Position Phase: 12 (initial-setup-wizard) — EXECUTING -Plan: 4 of 4 -Status: Phase complete — ready for verification -Last activity: 2026-06-15 +Plan: 2 of 7 +Status: Ready to execute +Last activity: 2026-06-16 ### ✅ Resolved Checkpoint — Phase 15 Plan 15-03 Task 2 (human-action) @@ -114,6 +114,7 @@ _Updated after each plan completion_ | Phase 12 P01 | 8 | 4 tasks | 10 files | | Phase 12 P02 | 15 | 3 tasks | 6 files | | Phase 12 P03 | 8 | 1 tasks | 2 files | +| Phase 12 P06 | 8 | 2 tasks tasks | 3 files files | ## Accumulated Context @@ -196,6 +197,8 @@ Recent decisions affecting current work: - [Phase ?]: D-NULL-FALLBACK: occurrence.reminderLeadMinutes===null mapped to None; occurrence cannot distinguish absolute/multi-VALARM from no-reminder; rely on server-side preserve (absent payload) - [Phase ?]: D-05/18-03: three all-day broker sites now route through getHouseholdTimezone(db) - [Phase ?]: D-12-03-EMAIL-GREP (2026-06-15, 12-03): claims.email in deriveDisplayName is display-name only; claim branch has zero email refs; D-10 upheld +- [Phase 12-06]: D-12-06-VAPID-EQ: validate/vapid compares submitted PUBLIC key (app_config.vapid_public_key) to process.env.VAPID_PUBLIC_KEY; mismatched/absent 400s. Private key stays env-only, never compared/returned (T-12-06). +- [Phase 12-06]: D-12-06-DBNAME: GET /api/setup/status returns non-secret dbName from process.env.DB_NAME only; no DB_HOST/DB_USER/DB_PASSWORD in any response. ### Roadmap Evolution @@ -264,8 +267,8 @@ Recent decisions affecting current work: ## Session Continuity -Last session: 2026-06-15T19:16:35.431Z -Stopped at: Phase 12 Plan 04 Tasks 1-3 complete — paused at Task 4 human-verify checkpoint (Docker rebuild + fresh-DB e2e verification needed) +Last session: 2026-06-16T01:15:09.630Z +Stopped at: Completed 12-06-PLAN.md (UAT gaps 2+3 closed) Resume file: None ## Operator Next Steps diff --git a/.planning/phases/12-initial-setup-wizard/12-06-SUMMARY.md b/.planning/phases/12-initial-setup-wizard/12-06-SUMMARY.md new file mode 100644 index 0000000..c64b0d9 --- /dev/null +++ b/.planning/phases/12-initial-setup-wizard/12-06-SUMMARY.md @@ -0,0 +1,81 @@ +--- +phase: 12-initial-setup-wizard +plan: 06 +subsystem: setup-wizard-backend +tags: [setup, vapid, security, uat-gap-closure] +requires: + - app_config.vapid_public_key (written by POST /api/setup/config) + - process.env.VAPID_PUBLIC_KEY / VAPID_PRIVATE_KEY (Docker env) + - process.env.DB_NAME (Docker env) +provides: + - "POST /api/setup/validate/vapid rejects a submitted public key that does not match the env VAPID_PUBLIC_KEY" + - "GET /api/setup/status returns { setupComplete, dbName } with the non-secret DB name" + - "SetupStatusResponse.dbName typed field for the PWA (Plan 05) read-only referent" +affects: + - apps/pwa setup wizard (Plan 05 consumes dbName + the now-strict VAPID row) +tech-stack: + added: [] + patterns: + - "validate/vapid equality check uses the same app_config select idiom as validate/oidc" + - "non-secret env surfacing: only DB_NAME exposed, never DB_HOST/DB_USER/DB_PASSWORD" +key-files: + created: [] + modified: + - apps/api/src/routes/setup.ts + - apps/api/tests/routes/setup.test.ts + - apps/pwa/src/api/client.ts +decisions: + - "D-12-06-VAPID-EQ: validate/vapid compares the operator-submitted PUBLIC key (app_config.vapid_public_key) to process.env.VAPID_PUBLIC_KEY; the private key is never compared or echoed (T-12-06 preserved)." + - "D-12-06-DBNAME: only process.env.DB_NAME (?? null) is surfaced in GET /status; DB_HOST/DB_USER/DB_PASSWORD are never added to any response (grep-verified)." +metrics: + duration_minutes: 8 + completed: 2026-06-16 +--- + +# Phase 12 Plan 06: Setup-Route Gap Closure (VAPID equality + DB name) Summary + +Closed UAT gaps 2 and 3 on the backend setup-route surface: `POST /api/setup/validate/vapid` now rejects a wrong/typoed wizard-entered VAPID public key by asserting it equals the env `VAPID_PUBLIC_KEY`, and `GET /api/setup/status` now returns the non-secret `dbName` so the DB-connection row has an on-screen referent. + +## What Was Built + +### Task 1 — validate/vapid asserts submitted key matches env public key (gap 2, TDD) +Before the structural `webpush.setVapidDetails()` check, the handler now reads `app_config.vapid_public_key` (the operator-submitted key) and returns 400 unless it exactly equals `process.env.VAPID_PUBLIC_KEY`. Previously a clearly-invalid key like `BH123` still went green because only the env pair was validated — push would silently break in production (SETUP-02). The equality compares PUBLIC keys only; `VAPID_PRIVATE_KEY` remains read solely from `process.env` and is never compared or returned (T-12-06). + +- RED commit `e9d07b3`: mismatch → 400 (no private-key leak), absent row → 400, happy path seeds matching row. +- GREEN commit `e46e80a`: equality assertion implemented. + +### Task 2 — Expose non-secret DB name via GET /api/setup/status (gap 3 backend) +`GET /api/setup/status` now returns `{ setupComplete, dbName }` where `dbName = process.env.DB_NAME ?? null` (the var read by `apps/api/src/db/client.ts`). Only the database NAME is surfaced — never DB_HOST/DB_USER/DB_PASSWORD. `SetupStatusResponse` in the PWA client gained `dbName?: string | null` so Plan 05 can render a typed read-only field. + +- Commit `fbd3b77`. + +## Verification + +- `cd apps/api && set -a; source ../../.env; set +a; DB_HOST=127.0.0.1 pnpm test -- setup` → **407 passed (29 files)**. +- `grep -nE "VAPID_PRIVATE_KEY" apps/api/src/routes/setup.ts` → only the env-only structural-check lines + doc comments; never compared against app_config or returned. +- `grep -nE "DB_PASSWORD|DB_HOST|DB_USER" apps/api/src/routes/setup.ts | grep -i "status\|c.json"` → **no matches** (no secret/topology in status response). +- `cd apps/pwa && pnpm typecheck` → clean (tsc + e2e tsconfig). + +## Deviations from Plan + +None — plan executed exactly as written. The pre-existing "invalid/truncated VAPID key" test (env keys invalid, no app_config row) still asserts 400/`ok:false` and stays GREEN; with the new equality check it now 400s on the absent-row branch rather than the structural branch, which is the intended stricter behavior. + +## TDD Gate Compliance + +Task 1 followed RED→GREEN: failing test commit `e9d07b3` (`test(12-06): ...`) precedes implementation commit `e46e80a` (`feat(12-06): ...`). No REFACTOR step needed. Task 2 is a non-behavioral env-surfacing change with an accompanying assertion added in the same commit. + +## Threat Surface + +| Threat ID | Disposition | Outcome | +|-----------|-------------|---------| +| T-12-06 (VAPID_PRIVATE_KEY disclosure) | mitigate | Preserved — private key env-only; equality uses PUBLIC keys; test asserts no private key in mismatch body. | +| T-12-3DB (DB secret/topology disclosure) | mitigate | Only DB_NAME surfaced; grep confirms no DB_HOST/DB_USER/DB_PASSWORD in status response. | +| T-12-04 (setup-route replay) | mitigate | `isSetupLocked()` remains the first await in every handler (unchanged). | + +No new security-relevant surface introduced beyond the planned `threat_model`. + +## Known Stubs + +None. + +## Self-Check: PASSED