Phase 12: Initial Setup Wizard #22

Merged
luckberg merged 76 commits from gsd/phase-12-initial-setup-wizard into main 2026-06-16 19:10:33 -04:00
Showing only changes of commit e46e80a15c - Show all commits
+28 -4
View File
@@ -193,10 +193,17 @@ setupRouter.post('/validate/oidc', async (c) => {
// ---------------------------------------------------------------------------
// POST /api/setup/validate/vapid
//
// Validates VAPID key pair structure by calling webpush.setVapidDetails().
// Reads BOTH keys ONLY from process.env — NEVER from app_config (T-12-06 / D-01 / SC-3).
// VAPID_PRIVATE_KEY is never returned in any response.
// Returns 200 { ok: true } on structural validity, 400 { ok: false } on failure.
// Validates the operator-entered VAPID public key (app_config.vapid_public_key)
// against the configured key pair:
// 1. Equality (gap 2): the submitted public key MUST equal process.env.VAPID_PUBLIC_KEY.
// A wrong/typoed key (e.g. "BH123") now fails the row and gates Continue — push
// would silently break in production otherwise (SETUP-02).
// 2. Structural: webpush.setVapidDetails() validates the env pair's byte structure.
//
// VAPID_PRIVATE_KEY is read ONLY from process.env — NEVER from app_config or returned
// (T-12-06 / D-01 / SC-3). The equality check compares the submitted PUBLIC key to the
// env PUBLIC key only — the private key is never compared or echoed.
// Returns 200 { ok: true } on success, 400 { ok: false } on any failure.
// ---------------------------------------------------------------------------
setupRouter.post('/validate/vapid', async (c) => {
@@ -210,6 +217,23 @@ setupRouter.post('/validate/vapid', async (c) => {
return c.json({ ok: false, error: 'VAPID_PRIVATE_KEY and VAPID_PUBLIC_KEY env vars must be set' }, 400);
}
// Gap 2: assert the operator-submitted public key matches the env public key BEFORE
// the structural check. Read the submitted key from app_config (same idiom as validate/oidc).
const [submittedRow] = await db
.select({ value: appConfig.value })
.from(appConfig)
.where(eq(appConfig.key, 'vapid_public_key'))
.limit(1);
const submittedPublicKey = submittedRow?.value;
if (!submittedPublicKey || submittedPublicKey !== publicKey) {
return c.json({
ok: false,
error:
'VAPID public key does not match the configured key pair. Paste the exact VAPID_PUBLIC_KEY printed by `npm run generate-secrets`.',
}, 400);
}
try {
// setVapidDetails runs validatePrivateKey (32-byte check) and validatePublicKey (65-byte check)
// internally — this is the library's own structural validation (Pattern 7).