#!/usr/bin/env tsx /** * Rotate the VAPID keypair used by Web Push notifications. * * Behavior: * 1. Read the current key from `data/secrets/vapid.json` (or wherever * `notifications.push.vapid_current_path` points, default * `data/secrets/vapid.json`). * 2. Move it to the history directory (default `data/secrets/vapid-history/`). * 3. Generate a fresh keypair and write it as the new current. * 4. Existing subscriptions in the DB are NOT modified — they retain the * old `vapid_key_id` and will still receive pushes (we read from history * based on that id). To force re-subscribe, delete the history file * manually after a grace period; users will see 401 on next push and * can re-subscribe from Settings. * * Usage: * npm run vapid-rotate */ import { join } from 'path'; import { loadConfig } from '../src/config.js'; import { VapidKeyStore } from '../src/vapid-store.js'; async function main(): Promise { const config = await loadConfig(); const pushCfg = config.notifications?.push; const subject = pushCfg?.vapidSubject ?? 'https://maestro.example.com/'; const currentPath = join(process.cwd(), 'data/secrets/vapid.json'); const historyDir = pushCfg?.vapidHistoryDir ?? join(process.cwd(), 'data/secrets/vapid-history'); const store = new VapidKeyStore(currentPath, historyDir); const newKey = store.rotate(subject); console.log('VAPID rotation complete.'); console.log(` new keyId: ${newKey.keyId}`); console.log(` public key: ${newKey.publicKey}`); console.log(` history dir: ${historyDir}`); console.log(''); console.log('Existing subscriptions remain valid via the history key.'); console.log('To force re-subscribe, remove the history file after the grace period'); console.log('and notify users via Settings → Notifications.'); } main().catch(err => { console.error('VAPID rotation failed:', err); process.exit(1); });