import { randomBytes } from 'node:crypto' import fs from 'node:fs' import path from 'node:path' import { config, ensureDirExists } from './config' import { logger } from './logger' function getGeneratedFilePath(): string { return path.join(config.dataDir, '.auto-generated') } interface PersistedValues { AUTH_SECRET?: string API_KEY?: string } function readPersisted(): PersistedValues { try { if (!fs.existsSync(getGeneratedFilePath())) return {} const raw = fs.readFileSync(getGeneratedFilePath(), 'utf8') const values: PersistedValues = {} for (const line of raw.split('\n')) { const trimmed = line.trim() if (!trimmed || trimmed.startsWith('#')) continue const eqIdx = trimmed.indexOf('=') if (eqIdx < 0) continue const key = trimmed.slice(0, eqIdx).trim() const value = trimmed.slice(eqIdx + 1).trim() if (key === 'AUTH_SECRET' || key === 'API_KEY') { values[key] = value } } return values } catch { return {} } } function writePersisted(values: PersistedValues): void { try { ensureDirExists(config.dataDir) const lines = [ '# Auto-generated values. Overridden by env vars when set.', ] if (values.AUTH_SECRET) lines.push(`AUTH_SECRET=${values.AUTH_SECRET}`) if (values.API_KEY) lines.push(`API_KEY=${values.API_KEY}`) fs.writeFileSync(getGeneratedFilePath(), lines.join('\n') + '\n', { mode: 0o600 }) } catch (err) { logger.warn({ err }, 'Failed to persist auto-generated values') } } function generate(): string { return randomBytes(32).toString('hex') } // Known placeholder values from .env.example that should be replaced const PLACEHOLDER_AUTH_SECRETS = new Set([ 'random-secret-for-legacy-cookies', ]) const PLACEHOLDER_API_KEYS = new Set([ 'generate-a-random-key', ]) /** * Ensure AUTH_SECRET and API_KEY are available. * Priority: env var > persisted file > auto-generate + persist. * Sets process.env so downstream code picks them up. */ export function ensureAutoGeneratedCredentials(): void { if (process.env.NEXT_PHASE === 'phase-production-build') return const persisted = readPersisted() let dirty = false // AUTH_SECRET const currentAuthSecret = (process.env.AUTH_SECRET || '').trim() if (!currentAuthSecret || PLACEHOLDER_AUTH_SECRETS.has(currentAuthSecret)) { if (persisted.AUTH_SECRET) { process.env.AUTH_SECRET = persisted.AUTH_SECRET } else { const val = generate() process.env.AUTH_SECRET = val persisted.AUTH_SECRET = val dirty = true logger.info('Auto-generated AUTH_SECRET (persisted to .data/.auto-generated)') } } // API_KEY const currentApiKey = (process.env.API_KEY || '').trim() if (!currentApiKey || PLACEHOLDER_API_KEYS.has(currentApiKey)) { if (persisted.API_KEY) { process.env.API_KEY = persisted.API_KEY } else { const val = generate() process.env.API_KEY = val persisted.API_KEY = val dirty = true logger.info('Auto-generated API_KEY (persisted to .data/.auto-generated)') } } if (dirty) { writePersisted(persisted) } }