SPB Git

Repository Public

secrets.ts 3.0 KB 51 lines typescript

1// Secrets of the alerts layer: the VAPID key pair (generated at first start) and the webhook's shared secret.2// The API keeps its other secrets in its environment, written at deploy; these two are created at run time, so they live in a3// file owned by the API user — directory 0700, file 0600, the same permissions the signer uses for its own material. They are4// never stored in the database (which is backed up every night), never logged, never returned by an endpoint (the VAPID public5// key is public by design; the webhook secret is shown once, when it is generated).6import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, statSync, writeFileSync } from 'node:fs';7import { homedir } from 'node:os';8import { join } from 'node:path';9import { randomBytes } from 'node:crypto';10import { generateVapidKeys, type VapidKeys } from './webpush.ts';1112interface Secrets { vapid: VapidKeys; webhookSecret: string | null; createdAt: string }13export const alertsDataDir = (): string => process.env.SPW_DATA_DIR || join(homedir(), '.config', 'sp-wallet');14export const secretsPath = (): string => join(alertsDataDir(), 'alerts-secrets.json');1516let cached: { path: string; v: Secrets } | null = null;17function write(path: string, v: Secrets): void {18  mkdirSync(alertsDataDir(), { recursive: true, mode: 0o700 });19  const tmp = `${path}.${process.pid}.tmp`;20  writeFileSync(tmp, JSON.stringify(v, null, 2), { mode: 0o600 });21  chmodSync(tmp, 0o600);22  renameSync(tmp, path);23  cached = { path, v };24}25function load(): Secrets {26  const path = secretsPath();27  if (cached && cached.path === path) return cached.v;28  if (existsSync(path)) {29    if ((statSync(path).mode & 0o077) !== 0) { chmodSync(path, 0o600); console.warn('[alerts] the secrets file was readable by other users; its permissions were reset to 0600'); }30    const v = JSON.parse(readFileSync(path, 'utf8')) as Secrets;31    if (typeof v?.vapid?.publicKey !== 'string' || typeof v?.vapid?.privateKey !== 'string') throw new Error('alerts secrets file is malformed; move it aside to generate a new VAPID key (devices must subscribe again)');32    cached = { path, v: { vapid: v.vapid, webhookSecret: v.webhookSecret ?? null, createdAt: v.createdAt } };33    return cached.v;34  }35  const v: Secrets = { vapid: generateVapidKeys(), webhookSecret: null, createdAt: new Date().toISOString() };36  write(path, v);37  console.log(`[alerts] VAPID key pair generated (${path}, mode 0600)`);38  return v;39}4041export const vapidKeys = (): VapidKeys => load().vapid;42export const webhookSecret = (): string | null => load().webhookSecret;43/** a new random secret, returned once to the owner */44export function rotateWebhookSecret(): string {45  const s = load(), secret = randomBytes(32).toString('base64url');46  write(secretsPath(), { ...s, webhookSecret: secret });47  return secret;48}49/** contact given to the push services (RFC 8292 `sub`): the wallet's own origin unless SPW_VAPID_SUBJECT is set — no personal address by default */50export const vapidSubject = (origin: string): string => process.env.SPW_VAPID_SUBJECT || origin;51
Files main
Menu

Theme