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
created
modified
apps/api/src/routes/setup.ts
apps/api/tests/routes/setup.test.ts
apps/pwa/src/api/client.ts
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).
duration_minutes
completed
8
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.
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).
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.