/** * generate-secrets.mjs — FamilySync bootstrap secret generator (SETUP-03 / D-05). * * Generates all secrets required for a first-time FamilySync deployment: * - SESSION_SECRET (AES-256-GCM session signing key, 32 random bytes / 64 hex chars) * - APP_PASSWORD_ENCRYPTION_KEY (AES-256-GCM encryption key, 32 random bytes / 64 hex chars) * - VAPID_PUBLIC_KEY (EC P-256 public key, base64url, ~87 chars) * - VAPID_PRIVATE_KEY (EC P-256 private scalar, base64url, ~43 chars) * * Security contract (SC-3): * - Prints to stdout ONLY — never writes any file, never touches the DB, never calls any API. * - The operator is responsible for pasting the output into docker-compose.yml and keeping it safe. * - These values CANNOT be recovered if lost (VAPID key rotation invalidates push subscriptions). * * Usage: * node scripts/generate-secrets.mjs * # or via pnpm script: * pnpm generate-secrets */ // IN-03: use Node.js built-in crypto to generate VAPID keys — avoids importing // web-push via its private source tree (../apps/api/node_modules/web-push/src/index.js) // which breaks if web-push restructures internally or workspace hoisting moves the package. // createECDH('prime256v1') + getPublicKey()/getPrivateKey() produces the same // base64url-encoded keys as web-push.generateVAPIDKeys(). import { randomBytes, createECDH } from 'node:crypto'; const sessionSecret = randomBytes(32).toString('hex'); const encKey = randomBytes(32).toString('hex'); // Phase 19 (D-05): LOCAL_SESSION_SECRET signs the local-auth JWT session cookie. // Must be >= 32 chars. 32 random bytes encoded as base64 = 44 chars (safe, distinct from hex keys). const localSessionSecret = randomBytes(32).toString('base64'); // VAPID key generation (P-256 / prime256v1 — same curve as web-push) const ecdhCurve = createECDH('prime256v1'); ecdhCurve.generateKeys(); // Pad raw buffers to the expected lengths, matching web-push defensive padding // (https://github.com/web-push-libs/web-push/issues/295) let pubBuffer = ecdhCurve.getPublicKey(); let privBuffer = ecdhCurve.getPrivateKey(); if (privBuffer.length < 32) { privBuffer = Buffer.concat([Buffer.alloc(32 - privBuffer.length), privBuffer]); } if (pubBuffer.length < 65) { pubBuffer = Buffer.concat([Buffer.alloc(65 - pubBuffer.length), pubBuffer]); } const vapid = { publicKey: pubBuffer.toString('base64url'), privateKey: privBuffer.toString('base64url'), }; console.log(`# FamilySync Bootstrap Secrets — generated ${new Date().toISOString()} # Paste into your docker-compose.yml environment block under the 'api' service. # Keep this output safe — these values cannot be recovered if lost. # VAPID key rotation will invalidate all existing push subscriptions. SESSION_SECRET=${sessionSecret} APP_PASSWORD_ENCRYPTION_KEY=${encKey} VAPID_PUBLIC_KEY=${vapid.publicKey} VAPID_PRIVATE_KEY=${vapid.privateKey} # Phase 19 (D-05): Signs local-auth JWT session cookies. Required when not using DEV_AUTH_BYPASS. LOCAL_SESSION_SECRET=${localSessionSecret} `);