import type Database from 'better-sqlite3'; import { ConfigSchema, type Config } from './index.js'; type StoredConfig = Omit & { openrouter: Omit; web: Omit; pushNotifications: Omit; }; /** * Persists application-owned configuration in SQLite. Secrets and the * database location remain environment-owned bootstrap settings and are * deliberately never written to the settings record. */ export class ConfigStore { constructor(private readonly db: Database.Database) {} load(environment: NodeJS.ProcessEnv = process.env): Config | null { const row = this.db.prepare('SELECT config_json FROM app_config WHERE id = 1').get() as | { config_json: string } | undefined; if (!row) return null; let stored: unknown; try { stored = JSON.parse(row.config_json); } catch (error) { throw new Error(`Stored application configuration is not valid JSON: ${String(error)}`); } return validateConfig(withBootstrapSettings(stored, environment)); } save(config: Config): void { const stored = toStoredConfig(config); this.db.prepare(` INSERT INTO app_config (id, config_json, updated_at) VALUES (1, ?, ?) ON CONFLICT(id) DO UPDATE SET config_json = excluded.config_json, updated_at = excluded.updated_at `).run(JSON.stringify(stored), Date.now()); } } function toStoredConfig(config: Config): StoredConfig { const { database: _database, ...applicationConfig } = config; const { apiKey: _apiKey, ...openrouter } = applicationConfig.openrouter; const { authToken: _authToken, agentReadToken: _agentReadToken, agentControlToken: _agentControlToken, ...web } = applicationConfig.web; const { authToken: _ntfyToken, ...pushNotifications } = applicationConfig.pushNotifications; return { ...applicationConfig, openrouter, web, pushNotifications }; } function withBootstrapSettings(stored: unknown, environment: NodeJS.ProcessEnv): unknown { if (!stored || typeof stored !== 'object' || Array.isArray(stored)) return stored; const record = stored as Record; const openrouter = objectValue(record.openrouter); const web = objectValue(record.web); const pushNotifications = objectValue(record.pushNotifications); return { ...record, pushNotifications: { ...pushNotifications, authToken: optionalSecret(environment.NTFY_AUTH_TOKEN), }, database: { path: environment.DATABASE_PATH ?? './data/reddit-bot.db' }, openrouter: { ...openrouter, apiKey: environment.OPENROUTER_API_KEY }, web: { ...web, authToken: environment.WEB_DASHBOARD_TOKEN, agentReadToken: optionalSecret(environment.REDDIT_BOT_AGENT_READ_TOKEN), agentControlToken: optionalSecret(environment.REDDIT_BOT_AGENT_CONTROL_TOKEN), }, }; } function objectValue(value: unknown): Record { return value && typeof value === 'object' && !Array.isArray(value) ? value as Record : {}; } function optionalSecret(value: string | undefined): string | undefined { return value?.trim() ? value : undefined; } function validateConfig(raw: unknown): Config { const result = ConfigSchema.safeParse(raw); if (!result.success) { const errors = result.error.issues .map((issue) => ` - ${String(issue.path.join('.'))}: ${issue.message}`) .join('\n'); throw new Error(`Invalid stored configuration:\n${errors}`); } if (result.data.web.enabled && !result.data.web.authToken) { throw new Error('WEB_DASHBOARD_TOKEN environment variable is required when the dashboard is enabled'); } return result.data; }