import { logger } from '../logger.js'; import { AcmeConfigDoc } from '../db/documents/index.js'; import type { IAcmeConfig } from '../../ts_interfaces/data/acme-config.js'; /** * AcmeConfigManager — owns the singleton ACME configuration in the DB. * * Lifecycle: * - `start()` — loads the DB-backed singleton configuration. * - `getConfig()` — returns the in-memory cached `IAcmeConfig` (or null) * - `updateConfig(args, updatedBy)` — upserts and refreshes the cache * * Reload semantics: updates take effect on the next dcrouter restart because * `SmartAcme` is instantiated once in `setupSmartProxy()`. `renewThresholdDays` * applies immediately to the next renewal check. See * `ts_web/elements/domains/ops-view-certificates.ts` for the UI warning. */ export class AcmeConfigManager { private cached: IAcmeConfig | null = null; public async start(): Promise { logger.log('info', 'AcmeConfigManager: starting'); const doc = await AcmeConfigDoc.load(); if (!doc) { logger.log('info', 'AcmeConfigManager: no AcmeConfig in DB — ACME disabled until configured via Domains > Certificates > Settings.'); } this.cached = doc ? this.toPlain(doc) : null; if (this.cached) { logger.log( 'info', `AcmeConfigManager: loaded ACME config (accountEmail=${this.cached.accountEmail}, enabled=${this.cached.enabled}, useProduction=${this.cached.useProduction})`, ); } } public async stop(): Promise { this.cached = null; } /** * Returns the current ACME config, or null if not configured. * In-memory — does not hit the DB. */ public getConfig(): IAcmeConfig | null { return this.cached; } /** * True if there is an enabled ACME config. Used by `setupSmartProxy()` to * decide whether to instantiate SmartAcme. */ public hasEnabledConfig(): boolean { return this.cached !== null && this.cached.enabled; } /** * Upsert the ACME config. All fields are optional; missing fields are * preserved from the existing row (or defaulted if there is no row yet). */ public async updateConfig( args: Partial>, updatedBy: string, ): Promise { let doc = await AcmeConfigDoc.load(); const now = Date.now(); if (!doc) { doc = new AcmeConfigDoc(); doc.configId = 'acme-config'; doc.accountEmail = args.accountEmail ?? ''; doc.enabled = args.enabled ?? true; doc.useProduction = args.useProduction ?? true; doc.autoRenew = args.autoRenew ?? true; doc.renewThresholdDays = args.renewThresholdDays ?? 30; } else { if (args.accountEmail !== undefined) doc.accountEmail = args.accountEmail; if (args.enabled !== undefined) doc.enabled = args.enabled; if (args.useProduction !== undefined) doc.useProduction = args.useProduction; if (args.autoRenew !== undefined) doc.autoRenew = args.autoRenew; if (args.renewThresholdDays !== undefined) doc.renewThresholdDays = args.renewThresholdDays; } doc.updatedAt = now; doc.updatedBy = updatedBy; await doc.save(); this.cached = this.toPlain(doc); return this.cached; } // ========================================================================== // Internal helpers // ========================================================================== private toPlain(doc: AcmeConfigDoc): IAcmeConfig { return { accountEmail: doc.accountEmail, enabled: doc.enabled, useProduction: doc.useProduction, autoRenew: doc.autoRenew, renewThresholdDays: doc.renewThresholdDays, updatedAt: doc.updatedAt, updatedBy: doc.updatedBy, }; } }