Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
82 lines
4.9 KiB
Markdown
82 lines
4.9 KiB
Markdown
---
|
|
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
|