- schema.ts: export localCredentials = mysqlTable('local_credentials', {...})
- user_id FK->users(cascade), username, password_hash, createdAt, updatedAt
- UNIQUE(user_id), UNIQUE(username), INDEX(user_id)
- 0003_warm_deathstrike.sql: purely additive CREATE TABLE (no ALTER/DROP/TRUNCATE on existing tables)
- Applied to dev DB: pnpm --filter @familysync/api db:migrate exits 0
- test/setup.ts: add localCredentials to afterEach delete cleanup (FK-safe ordering)
- generate-secrets.mjs: emit LOCAL_SESSION_SECRET (base64 32-byte, >=32 chars, D-05)
- .dockerignore: add apps/api/scripts/ exclusion (D-15/IMG-02) — entire break-glass dir excluded
64 lines
3.0 KiB
JavaScript
64 lines
3.0 KiB
JavaScript
/**
|
|
* 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}
|
|
`);
|