import * as plugins from '../../plugins.js'; // Importing required components import { ConnectionManager } from './connection-manager.js'; import { SecurityManager } from './security-manager.js'; import { DomainConfigManager } from './domain-config-manager.js'; import { TlsManager } from './tls-manager.js'; import { NetworkProxyBridge } from './network-proxy-bridge.js'; import { TimeoutManager } from './timeout-manager.js'; import { PortRangeManager } from './port-range-manager.js'; import { RouteManager } from './route-manager.js'; import { RouteConnectionHandler } from './route-connection-handler.js'; // External dependencies import { Port80Handler } from '../../http/port80/port80-handler.js'; import { CertProvisioner } from '../../certificate/providers/cert-provisioner.js'; import type { ICertificateData } from '../../certificate/models/certificate-types.js'; import { buildPort80Handler } from '../../certificate/acme/acme-factory.js'; import { createPort80HandlerOptions } from '../../common/port80-adapter.js'; // Import types and utilities import type { ISmartProxyOptions, IRoutedSmartProxyOptions, IDomainConfig } from './models/interfaces.js'; import { isRoutedOptions, isLegacyOptions } from './models/interfaces.js'; import type { IRouteConfig } from './models/route-types.js'; /** * SmartProxy - Unified route-based API */ export class SmartProxy extends plugins.EventEmitter { private netServers: plugins.net.Server[] = []; private connectionLogger: NodeJS.Timeout | null = null; private isShuttingDown: boolean = false; // Component managers private connectionManager: ConnectionManager; private securityManager: SecurityManager; private domainConfigManager: DomainConfigManager; private tlsManager: TlsManager; private networkProxyBridge: NetworkProxyBridge; private timeoutManager: TimeoutManager; private portRangeManager: PortRangeManager; private routeManager: RouteManager; private routeConnectionHandler: RouteConnectionHandler; // Port80Handler for ACME certificate management private port80Handler: Port80Handler | null = null; // CertProvisioner for unified certificate workflows private certProvisioner?: CertProvisioner; /** * Constructor that supports both legacy and route-based configuration */ constructor(settingsArg: ISmartProxyOptions) { super(); // Set reasonable defaults for all settings this.settings = { ...settingsArg, initialDataTimeout: settingsArg.initialDataTimeout || 120000, socketTimeout: settingsArg.socketTimeout || 3600000, inactivityCheckInterval: settingsArg.inactivityCheckInterval || 60000, maxConnectionLifetime: settingsArg.maxConnectionLifetime || 86400000, inactivityTimeout: settingsArg.inactivityTimeout || 14400000, gracefulShutdownTimeout: settingsArg.gracefulShutdownTimeout || 30000, noDelay: settingsArg.noDelay !== undefined ? settingsArg.noDelay : true, keepAlive: settingsArg.keepAlive !== undefined ? settingsArg.keepAlive : true, keepAliveInitialDelay: settingsArg.keepAliveInitialDelay || 10000, maxPendingDataSize: settingsArg.maxPendingDataSize || 10 * 1024 * 1024, disableInactivityCheck: settingsArg.disableInactivityCheck || false, enableKeepAliveProbes: settingsArg.enableKeepAliveProbes !== undefined ? settingsArg.enableKeepAliveProbes : true, enableDetailedLogging: settingsArg.enableDetailedLogging || false, enableTlsDebugLogging: settingsArg.enableTlsDebugLogging || false, enableRandomizedTimeouts: settingsArg.enableRandomizedTimeouts || false, allowSessionTicket: settingsArg.allowSessionTicket !== undefined ? settingsArg.allowSessionTicket : true, maxConnectionsPerIP: settingsArg.maxConnectionsPerIP || 100, connectionRateLimitPerMinute: settingsArg.connectionRateLimitPerMinute || 300, keepAliveTreatment: settingsArg.keepAliveTreatment || 'extended', keepAliveInactivityMultiplier: settingsArg.keepAliveInactivityMultiplier || 6, extendedKeepAliveLifetime: settingsArg.extendedKeepAliveLifetime || 7 * 24 * 60 * 60 * 1000, networkProxyPort: settingsArg.networkProxyPort || 8443, }; // Set default ACME options if not provided this.settings.acme = this.settings.acme || {}; if (Object.keys(this.settings.acme).length === 0) { this.settings.acme = { enabled: false, port: 80, accountEmail: 'admin@example.com', useProduction: false, renewThresholdDays: 30, autoRenew: true, certificateStore: './certs', skipConfiguredCerts: false, httpsRedirectPort: this.settings.fromPort || 443, renewCheckIntervalHours: 24, domainForwards: [] }; } // Initialize component managers this.timeoutManager = new TimeoutManager(this.settings); this.securityManager = new SecurityManager(this.settings); this.connectionManager = new ConnectionManager( this.settings, this.securityManager, this.timeoutManager ); // Create the new route manager first this.routeManager = new RouteManager(this.settings); // Create domain config manager and port range manager this.domainConfigManager = new DomainConfigManager(this.settings); // Share the route manager with the domain config manager if (typeof this.domainConfigManager.setRouteManager === 'function') { this.domainConfigManager.setRouteManager(this.routeManager); } this.portRangeManager = new PortRangeManager(this.settings); // Create other required components this.tlsManager = new TlsManager(this.settings); this.networkProxyBridge = new NetworkProxyBridge(this.settings); // Initialize connection handler with route support this.routeConnectionHandler = new RouteConnectionHandler( this.settings, this.connectionManager, this.securityManager, this.domainConfigManager, this.tlsManager, this.networkProxyBridge, this.timeoutManager, this.routeManager ); } /** * The settings for the SmartProxy */ public settings: ISmartProxyOptions; /** * Initialize the Port80Handler for ACME certificate management */ private async initializePort80Handler(): Promise { const config = this.settings.acme!; if (!config.enabled) { console.log('ACME is disabled in configuration'); return; } try { // Build and start the Port80Handler this.port80Handler = buildPort80Handler({ ...config, httpsRedirectPort: config.httpsRedirectPort || (isLegacyOptions(this.settings) ? this.settings.fromPort : 443) }); // Share Port80Handler with NetworkProxyBridge before start this.networkProxyBridge.setPort80Handler(this.port80Handler); await this.port80Handler.start(); console.log(`Port80Handler started on port ${config.port}`); } catch (err) { console.log(`Error initializing Port80Handler: ${err}`); } } /** * Start the proxy server with support for both configuration types */ public async start() { // Don't start if already shutting down if (this.isShuttingDown) { console.log("Cannot start SmartProxy while it's shutting down"); return; } // Initialize domain config based on configuration type if (isLegacyOptions(this.settings)) { // Initialize domain config manager with the legacy domain configs this.domainConfigManager.updateDomainConfigs(this.settings.domainConfigs || []); } else if (isRoutedOptions(this.settings)) { // For pure route-based configuration, the domain config is already initialized // in the constructor, but we might need to regenerate it if (typeof this.domainConfigManager.generateDomainConfigsFromRoutes === 'function') { this.domainConfigManager.generateDomainConfigsFromRoutes(); } } // Initialize Port80Handler if enabled await this.initializePort80Handler(); // Initialize CertProvisioner for unified certificate workflows if (this.port80Handler) { const acme = this.settings.acme!; // Setup domain forwards based on configuration type const domainForwards = acme.domainForwards?.map(f => { if (isLegacyOptions(this.settings)) { // If using legacy mode, check if domain config exists const domainConfig = this.settings.domainConfigs.find( dc => dc.domains.some(d => d === f.domain) ); if (domainConfig?.forwarding) { return { domain: f.domain, forwardConfig: f.forwardConfig, acmeForwardConfig: f.acmeForwardConfig, sslRedirect: f.sslRedirect || domainConfig.forwarding.http?.redirectToHttps || false }; } } else { // In route mode, look for matching route const route = this.routeManager.findMatchingRoute({ port: 443, domain: f.domain, clientIp: '127.0.0.1' // Dummy IP for finding routes })?.route; if (route && route.action.type === 'forward' && route.action.tls) { // If we found a matching route with TLS settings return { domain: f.domain, forwardConfig: f.forwardConfig, acmeForwardConfig: f.acmeForwardConfig, sslRedirect: f.sslRedirect || false }; } } // Otherwise use the existing configuration return { domain: f.domain, forwardConfig: f.forwardConfig, acmeForwardConfig: f.acmeForwardConfig, sslRedirect: f.sslRedirect || false }; }) || []; // Create CertProvisioner with appropriate parameters if (isLegacyOptions(this.settings)) { this.certProvisioner = new CertProvisioner( this.settings.domainConfigs, this.port80Handler, this.networkProxyBridge, this.settings.certProvisionFunction, acme.renewThresholdDays!, acme.renewCheckIntervalHours!, acme.autoRenew!, domainForwards ); } else { // For route-based configuration, we need to adapt the interface // Convert routes to domain configs for CertProvisioner const domainConfigs: IDomainConfig[] = this.extractDomainConfigsFromRoutes( (this.settings as IRoutedSmartProxyOptions).routes ); this.certProvisioner = new CertProvisioner( domainConfigs, this.port80Handler, this.networkProxyBridge, this.settings.certProvisionFunction, acme.renewThresholdDays!, acme.renewCheckIntervalHours!, acme.autoRenew!, domainForwards ); } // Register certificate event handler this.certProvisioner.on('certificate', (certData) => { this.emit('certificate', { domain: certData.domain, publicKey: certData.certificate, privateKey: certData.privateKey, expiryDate: certData.expiryDate, source: certData.source, isRenewal: certData.isRenewal }); }); await this.certProvisioner.start(); console.log('CertProvisioner started'); } // Initialize and start NetworkProxy if needed if (this.settings.useNetworkProxy && this.settings.useNetworkProxy.length > 0) { await this.networkProxyBridge.initialize(); await this.networkProxyBridge.start(); } // Validate the route configuration const configWarnings = this.routeManager.validateConfiguration(); if (configWarnings.length > 0) { console.log("Route configuration warnings:"); for (const warning of configWarnings) { console.log(` - ${warning}`); } } // Get listening ports from RouteManager const listeningPorts = this.routeManager.getListeningPorts(); // Create servers for each port for (const port of listeningPorts) { const server = plugins.net.createServer((socket) => { // Check if shutting down if (this.isShuttingDown) { socket.end(); socket.destroy(); return; } // Delegate to route connection handler this.routeConnectionHandler.handleConnection(socket); }).on('error', (err: Error) => { console.log(`Server Error on port ${port}: ${err.message}`); }); server.listen(port, () => { const isNetworkProxyPort = this.settings.useNetworkProxy?.includes(port); console.log( `SmartProxy -> OK: Now listening on port ${port}${ isLegacyOptions(this.settings) && this.settings.sniEnabled && !isNetworkProxyPort ? ' (SNI passthrough enabled)' : '' }${isNetworkProxyPort ? ' (NetworkProxy forwarding enabled)' : ''}` ); }); this.netServers.push(server); } // Set up periodic connection logging and inactivity checks this.connectionLogger = setInterval(() => { // Immediately return if shutting down if (this.isShuttingDown) return; // Perform inactivity check this.connectionManager.performInactivityCheck(); // Log connection statistics const now = Date.now(); let maxIncoming = 0; let maxOutgoing = 0; let tlsConnections = 0; let nonTlsConnections = 0; let completedTlsHandshakes = 0; let pendingTlsHandshakes = 0; let keepAliveConnections = 0; let networkProxyConnections = 0; // Get connection records for analysis const connectionRecords = this.connectionManager.getConnections(); // Analyze active connections for (const record of connectionRecords.values()) { // Track connection stats if (record.isTLS) { tlsConnections++; if (record.tlsHandshakeComplete) { completedTlsHandshakes++; } else { pendingTlsHandshakes++; } } else { nonTlsConnections++; } if (record.hasKeepAlive) { keepAliveConnections++; } if (record.usingNetworkProxy) { networkProxyConnections++; } maxIncoming = Math.max(maxIncoming, now - record.incomingStartTime); if (record.outgoingStartTime) { maxOutgoing = Math.max(maxOutgoing, now - record.outgoingStartTime); } } // Get termination stats const terminationStats = this.connectionManager.getTerminationStats(); // Log detailed stats console.log( `Active connections: ${connectionRecords.size}. ` + `Types: TLS=${tlsConnections} (Completed=${completedTlsHandshakes}, Pending=${pendingTlsHandshakes}), ` + `Non-TLS=${nonTlsConnections}, KeepAlive=${keepAliveConnections}, NetworkProxy=${networkProxyConnections}. ` + `Longest running: IN=${plugins.prettyMs(maxIncoming)}, OUT=${plugins.prettyMs(maxOutgoing)}. ` + `Termination stats: ${JSON.stringify({ IN: terminationStats.incoming, OUT: terminationStats.outgoing, })}` ); }, this.settings.inactivityCheckInterval || 60000); // Make sure the interval doesn't keep the process alive if (this.connectionLogger.unref) { this.connectionLogger.unref(); } } /** * Extract domain configurations from routes for certificate provisioning */ private extractDomainConfigsFromRoutes(routes: IRouteConfig[]): IDomainConfig[] { const domainConfigs: IDomainConfig[] = []; for (const route of routes) { // Skip routes without domain specs if (!route.match.domains) continue; // Skip non-forward routes if (route.action.type !== 'forward') continue; // Only process routes that need TLS termination (those with certificates) if (!route.action.tls || route.action.tls.mode === 'passthrough' || !route.action.target) continue; const domains = Array.isArray(route.match.domains) ? route.match.domains : [route.match.domains]; // Determine forwarding type based on TLS mode const forwardingType = route.action.tls.mode === 'terminate' ? 'https-terminate-to-http' : 'https-terminate-to-https'; // Create a forwarding config const forwarding = { type: forwardingType as any, target: { host: Array.isArray(route.action.target.host) ? route.action.target.host[0] : route.action.target.host, port: route.action.target.port }, // Add TLS settings https: { customCert: route.action.tls.certificate !== 'auto' ? route.action.tls.certificate : undefined }, // Add security settings if present security: route.action.security, // Add advanced settings if present advanced: route.action.advanced }; domainConfigs.push({ domains, forwarding }); } return domainConfigs; } /** * Stop the proxy server */ public async stop() { console.log('SmartProxy shutting down...'); this.isShuttingDown = true; // Stop CertProvisioner if active if (this.certProvisioner) { await this.certProvisioner.stop(); console.log('CertProvisioner stopped'); } // Stop the Port80Handler if running if (this.port80Handler) { try { await this.port80Handler.stop(); console.log('Port80Handler stopped'); this.port80Handler = null; } catch (err) { console.log(`Error stopping Port80Handler: ${err}`); } } // Stop accepting new connections const closeServerPromises: Promise[] = this.netServers.map( (server) => new Promise((resolve) => { if (!server.listening) { resolve(); return; } server.close((err) => { if (err) { console.log(`Error closing server: ${err.message}`); } resolve(); }); }) ); // Stop the connection logger if (this.connectionLogger) { clearInterval(this.connectionLogger); this.connectionLogger = null; } // Wait for servers to close await Promise.all(closeServerPromises); console.log('All servers closed. Cleaning up active connections...'); // Clean up all active connections this.connectionManager.clearConnections(); // Stop NetworkProxy await this.networkProxyBridge.stop(); // Clear all servers this.netServers = []; console.log('SmartProxy shutdown complete.'); } /** * Updates the domain configurations for the proxy (legacy support) */ public async updateDomainConfigs(newDomainConfigs: IDomainConfig[]): Promise { console.log(`Updating domain configurations (${newDomainConfigs.length} configs)`); // Update domain configs in DomainConfigManager (legacy) this.domainConfigManager.updateDomainConfigs(newDomainConfigs); // Also update the RouteManager with these domain configs this.routeManager.updateFromDomainConfigs(newDomainConfigs); // If NetworkProxy is initialized, resync the configurations if (this.networkProxyBridge.getNetworkProxy()) { await this.networkProxyBridge.syncDomainConfigsToNetworkProxy(); } // If Port80Handler is running, provision certificates based on forwarding type if (this.port80Handler && this.settings.acme?.enabled) { for (const domainConfig of newDomainConfigs) { // Skip certificate provisioning for http-only or passthrough configs that don't need certs const forwardingType = this.domainConfigManager.getForwardingType(domainConfig); const needsCertificate = forwardingType === 'https-terminate-to-http' || forwardingType === 'https-terminate-to-https'; // Skip certificate provisioning if ACME is explicitly disabled for this domain const acmeDisabled = domainConfig.forwarding.acme?.enabled === false; if (!needsCertificate || acmeDisabled) { if (this.settings.enableDetailedLogging) { console.log(`Skipping certificate provisioning for ${domainConfig.domains.join(', ')} (${forwardingType})`); } continue; } for (const domain of domainConfig.domains) { const isWildcard = domain.includes('*'); let provision: string | plugins.tsclass.network.ICert = 'http01'; // Check for ACME forwarding configuration in the domain const forwardAcmeChallenges = domainConfig.forwarding.acme?.forwardChallenges; if (this.settings.certProvisionFunction) { try { provision = await this.settings.certProvisionFunction(domain); } catch (err) { console.log(`certProvider error for ${domain}: ${err}`); } } else if (isWildcard) { console.warn(`Skipping wildcard domain without certProvisionFunction: ${domain}`); continue; } if (provision === 'http01') { if (isWildcard) { console.warn(`Skipping HTTP-01 for wildcard domain: ${domain}`); continue; } // Create Port80Handler options from the forwarding configuration const port80Config = createPort80HandlerOptions(domain, domainConfig.forwarding); this.port80Handler.addDomain(port80Config); console.log(`Registered domain ${domain} with Port80Handler for HTTP-01`); } else { // Static certificate (e.g., DNS-01 provisioned) supports wildcards const certObj = provision as plugins.tsclass.network.ICert; const certData: ICertificateData = { domain: certObj.domainName, certificate: certObj.publicKey, privateKey: certObj.privateKey, expiryDate: new Date(certObj.validUntil) }; this.networkProxyBridge.applyExternalCertificate(certData); console.log(`Applied static certificate for ${domain} from certProvider`); } } } console.log('Provisioned certificates for new domains'); } } /** * Update routes with new configuration (new API) */ public async updateRoutes(newRoutes: IRouteConfig[]): Promise { console.log(`Updating routes (${newRoutes.length} routes)`); // Update routes in RouteManager this.routeManager.updateRoutes(newRoutes); // If NetworkProxy is initialized, resync the configurations if (this.networkProxyBridge.getNetworkProxy()) { // Create equivalent domain configs for NetworkProxy const domainConfigs = this.extractDomainConfigsFromRoutes(newRoutes); // Update domain configs in DomainConfigManager for sync this.domainConfigManager.updateDomainConfigs(domainConfigs); // Sync with NetworkProxy await this.networkProxyBridge.syncDomainConfigsToNetworkProxy(); } // If Port80Handler is running, provision certificates based on routes if (this.port80Handler && this.settings.acme?.enabled) { for (const route of newRoutes) { // Skip routes without domains if (!route.match.domains) continue; // Skip non-forward routes if (route.action.type !== 'forward') continue; // Skip routes without TLS termination if (!route.action.tls || route.action.tls.mode === 'passthrough' || !route.action.target) continue; // Skip certificate provisioning if certificate is not auto if (route.action.tls.certificate !== 'auto') continue; const domains = Array.isArray(route.match.domains) ? route.match.domains : [route.match.domains]; for (const domain of domains) { const isWildcard = domain.includes('*'); let provision: string | plugins.tsclass.network.ICert = 'http01'; if (this.settings.certProvisionFunction) { try { provision = await this.settings.certProvisionFunction(domain); } catch (err) { console.log(`certProvider error for ${domain}: ${err}`); } } else if (isWildcard) { console.warn(`Skipping wildcard domain without certProvisionFunction: ${domain}`); continue; } if (provision === 'http01') { if (isWildcard) { console.warn(`Skipping HTTP-01 for wildcard domain: ${domain}`); continue; } // Register domain with Port80Handler this.port80Handler.addDomain({ domainName: domain, sslRedirect: true, acmeMaintenance: true }); console.log(`Registered domain ${domain} with Port80Handler for HTTP-01`); } else { // Handle static certificate (e.g., DNS-01 provisioned) const certObj = provision as plugins.tsclass.network.ICert; const certData: ICertificateData = { domain: certObj.domainName, certificate: certObj.publicKey, privateKey: certObj.privateKey, expiryDate: new Date(certObj.validUntil) }; this.networkProxyBridge.applyExternalCertificate(certData); console.log(`Applied static certificate for ${domain} from certProvider`); } } } console.log('Provisioned certificates for new routes'); } } /** * Request a certificate for a specific domain */ public async requestCertificate(domain: string): Promise { // Validate domain format if (!this.isValidDomain(domain)) { console.log(`Invalid domain format: ${domain}`); return false; } // Use Port80Handler if available if (this.port80Handler) { try { // Check if we already have a certificate const cert = this.port80Handler.getCertificate(domain); if (cert) { console.log(`Certificate already exists for ${domain}, valid until ${cert.expiryDate.toISOString()}`); return true; } // Register domain for certificate issuance this.port80Handler.addDomain({ domainName: domain, sslRedirect: true, acmeMaintenance: true }); console.log(`Domain ${domain} registered for certificate issuance`); return true; } catch (err) { console.log(`Error registering domain with Port80Handler: ${err}`); return false; } } // Fall back to NetworkProxyBridge return this.networkProxyBridge.requestCertificate(domain); } /** * Validates if a domain name is valid for certificate issuance */ private isValidDomain(domain: string): boolean { // Very basic domain validation if (!domain || domain.length === 0) { return false; } // Check for wildcard domains (they can't get ACME certs) if (domain.includes('*')) { console.log(`Wildcard domains like "${domain}" are not supported for ACME certificates`); return false; } // Check if domain has at least one dot and no invalid characters const validDomainRegex = /^[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$/; if (!validDomainRegex.test(domain)) { console.log(`Domain "${domain}" has invalid format`); return false; } return true; } /** * Get statistics about current connections */ public getStatistics(): any { const connectionRecords = this.connectionManager.getConnections(); const terminationStats = this.connectionManager.getTerminationStats(); let tlsConnections = 0; let nonTlsConnections = 0; let keepAliveConnections = 0; let networkProxyConnections = 0; // Analyze active connections for (const record of connectionRecords.values()) { if (record.isTLS) tlsConnections++; else nonTlsConnections++; if (record.hasKeepAlive) keepAliveConnections++; if (record.usingNetworkProxy) networkProxyConnections++; } return { activeConnections: connectionRecords.size, tlsConnections, nonTlsConnections, keepAliveConnections, networkProxyConnections, terminationStats, acmeEnabled: !!this.port80Handler, port80HandlerPort: this.port80Handler ? this.settings.acme?.port : null, routes: this.routeManager.getListeningPorts().length }; } /** * Get a list of eligible domains for ACME certificates */ public getEligibleDomainsForCertificates(): string[] { const domains: string[] = []; // Get domains from routes const routes = isRoutedOptions(this.settings) ? this.settings.routes : []; for (const route of routes) { if (!route.match.domains) continue; // Skip routes without TLS termination or auto certificates if (route.action.type !== 'forward' || !route.action.tls || route.action.tls.mode === 'passthrough' || route.action.tls.certificate !== 'auto') continue; const routeDomains = Array.isArray(route.match.domains) ? route.match.domains : [route.match.domains]; // Skip domains that can't be used with ACME const eligibleDomains = routeDomains.filter(domain => !domain.includes('*') && this.isValidDomain(domain) ); domains.push(...eligibleDomains); } // For legacy mode, also get domains from domain configs if (isLegacyOptions(this.settings)) { for (const config of this.settings.domainConfigs) { // Skip domains that can't be used with ACME const eligibleDomains = config.domains.filter(domain => !domain.includes('*') && this.isValidDomain(domain) ); domains.push(...eligibleDomains); } } return domains; } /** * Get status of certificates managed by Port80Handler */ public getCertificateStatus(): any { if (!this.port80Handler) { return { enabled: false, message: 'Port80Handler is not enabled' }; } // Get eligible domains const eligibleDomains = this.getEligibleDomainsForCertificates(); const certificateStatus: Record = {}; // Check each domain for (const domain of eligibleDomains) { const cert = this.port80Handler.getCertificate(domain); if (cert) { const now = new Date(); const expiryDate = cert.expiryDate; const daysRemaining = Math.floor((expiryDate.getTime() - now.getTime()) / (24 * 60 * 60 * 1000)); certificateStatus[domain] = { status: 'valid', expiryDate: expiryDate.toISOString(), daysRemaining, renewalNeeded: daysRemaining <= (this.settings.acme?.renewThresholdDays ?? 0) }; } else { certificateStatus[domain] = { status: 'missing', message: 'No certificate found' }; } } const acme = this.settings.acme!; return { enabled: true, port: acme.port!, useProduction: acme.useProduction!, autoRenew: acme.autoRenew!, certificates: certificateStatus }; } }