| 
							
							
							
						 |  |  | @@ -1,9 +1,58 @@ | 
		
	
		
			
				|  |  |  |  | import * as plugins from './plugins.js'; | 
		
	
		
			
				|  |  |  |  | import { IncomingMessage, ServerResponse } from 'http'; | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  | /** | 
		
	
		
			
				|  |  |  |  |  * Represents a domain certificate with various status information | 
		
	
		
			
				|  |  |  |  |  * Custom error classes for better error handling | 
		
	
		
			
				|  |  |  |  |  */ | 
		
	
		
			
				|  |  |  |  | export class Port80HandlerError extends Error { | 
		
	
		
			
				|  |  |  |  |   constructor(message: string) { | 
		
	
		
			
				|  |  |  |  |     super(message); | 
		
	
		
			
				|  |  |  |  |     this.name = 'Port80HandlerError'; | 
		
	
		
			
				|  |  |  |  |   } | 
		
	
		
			
				|  |  |  |  | } | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  | export class CertificateError extends Port80HandlerError { | 
		
	
		
			
				|  |  |  |  |   constructor( | 
		
	
		
			
				|  |  |  |  |     message: string, | 
		
	
		
			
				|  |  |  |  |     public readonly domain: string, | 
		
	
		
			
				|  |  |  |  |     public readonly isRenewal: boolean = false | 
		
	
		
			
				|  |  |  |  |   ) { | 
		
	
		
			
				|  |  |  |  |     super(`${message} for domain ${domain}${isRenewal ? ' (renewal)' : ''}`); | 
		
	
		
			
				|  |  |  |  |     this.name = 'CertificateError'; | 
		
	
		
			
				|  |  |  |  |   } | 
		
	
		
			
				|  |  |  |  | } | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  | export class ServerError extends Port80HandlerError { | 
		
	
		
			
				|  |  |  |  |   constructor(message: string, public readonly code?: string) { | 
		
	
		
			
				|  |  |  |  |     super(message); | 
		
	
		
			
				|  |  |  |  |     this.name = 'ServerError'; | 
		
	
		
			
				|  |  |  |  |   } | 
		
	
		
			
				|  |  |  |  | } | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  | /** | 
		
	
		
			
				|  |  |  |  |  * Domain forwarding configuration | 
		
	
		
			
				|  |  |  |  |  */ | 
		
	
		
			
				|  |  |  |  | export interface IForwardConfig { | 
		
	
		
			
				|  |  |  |  |   ip: string; | 
		
	
		
			
				|  |  |  |  |   port: number; | 
		
	
		
			
				|  |  |  |  | } | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  | /** | 
		
	
		
			
				|  |  |  |  |  * Domain configuration options | 
		
	
		
			
				|  |  |  |  |  */ | 
		
	
		
			
				|  |  |  |  | export interface IDomainOptions { | 
		
	
		
			
				|  |  |  |  |   domainName: string; | 
		
	
		
			
				|  |  |  |  |   sslRedirect: boolean;   // if true redirects the request to port 443 | 
		
	
		
			
				|  |  |  |  |   acmeMaintenance: boolean; // tries to always have a valid cert for this domain | 
		
	
		
			
				|  |  |  |  |   forward?: IForwardConfig; // forwards all http requests to that target | 
		
	
		
			
				|  |  |  |  |   acmeForward?: IForwardConfig; // forwards letsencrypt requests to this config | 
		
	
		
			
				|  |  |  |  | } | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  | /** | 
		
	
		
			
				|  |  |  |  |  * Represents a domain configuration with certificate status information | 
		
	
		
			
				|  |  |  |  |  */ | 
		
	
		
			
				|  |  |  |  | interface IDomainCertificate { | 
		
	
		
			
				|  |  |  |  |   options: IDomainOptions; | 
		
	
		
			
				|  |  |  |  |   certObtained: boolean; | 
		
	
		
			
				|  |  |  |  |   obtainingInProgress: boolean; | 
		
	
		
			
				|  |  |  |  |   certificate?: string; | 
		
	
	
		
			
				
					
					|  |  |  | @@ -15,9 +64,9 @@ interface IDomainCertificate { | 
		
	
		
			
				|  |  |  |  | } | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  | /** | 
		
	
		
			
				|  |  |  |  |  * Configuration options for the ACME Certificate Manager | 
		
	
		
			
				|  |  |  |  |  * Configuration options for the Port80Handler | 
		
	
		
			
				|  |  |  |  |  */ | 
		
	
		
			
				|  |  |  |  | interface IAcmeCertManagerOptions { | 
		
	
		
			
				|  |  |  |  | interface IPort80HandlerOptions { | 
		
	
		
			
				|  |  |  |  |   port?: number; | 
		
	
		
			
				|  |  |  |  |   contactEmail?: string; | 
		
	
		
			
				|  |  |  |  |   useProduction?: boolean; | 
		
	
	
		
			
				
					
					|  |  |  | @@ -29,7 +78,7 @@ interface IAcmeCertManagerOptions { | 
		
	
		
			
				|  |  |  |  | /** | 
		
	
		
			
				|  |  |  |  |  * Certificate data that can be emitted via events or set from outside | 
		
	
		
			
				|  |  |  |  |  */ | 
		
	
		
			
				|  |  |  |  | interface ICertificateData { | 
		
	
		
			
				|  |  |  |  | export interface ICertificateData { | 
		
	
		
			
				|  |  |  |  |   domain: string; | 
		
	
		
			
				|  |  |  |  |   certificate: string; | 
		
	
		
			
				|  |  |  |  |   privateKey: string; | 
		
	
	
		
			
				
					
					|  |  |  | @@ -37,34 +86,53 @@ interface ICertificateData { | 
		
	
		
			
				|  |  |  |  | } | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  | /** | 
		
	
		
			
				|  |  |  |  |  * Events emitted by the ACME Certificate Manager | 
		
	
		
			
				|  |  |  |  |  * Events emitted by the Port80Handler | 
		
	
		
			
				|  |  |  |  |  */ | 
		
	
		
			
				|  |  |  |  | export enum CertManagerEvents { | 
		
	
		
			
				|  |  |  |  | export enum Port80HandlerEvents { | 
		
	
		
			
				|  |  |  |  |   CERTIFICATE_ISSUED = 'certificate-issued', | 
		
	
		
			
				|  |  |  |  |   CERTIFICATE_RENEWED = 'certificate-renewed', | 
		
	
		
			
				|  |  |  |  |   CERTIFICATE_FAILED = 'certificate-failed', | 
		
	
		
			
				|  |  |  |  |   CERTIFICATE_EXPIRING = 'certificate-expiring', | 
		
	
		
			
				|  |  |  |  |   MANAGER_STARTED = 'manager-started', | 
		
	
		
			
				|  |  |  |  |   MANAGER_STOPPED = 'manager-stopped', | 
		
	
		
			
				|  |  |  |  |   REQUEST_FORWARDED = 'request-forwarded', | 
		
	
		
			
				|  |  |  |  | } | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  | /** | 
		
	
		
			
				|  |  |  |  |  * Improved ACME Certificate Manager with event emission and external certificate management | 
		
	
		
			
				|  |  |  |  |  * Certificate failure payload type | 
		
	
		
			
				|  |  |  |  |  */ | 
		
	
		
			
				|  |  |  |  | export class AcmeCertManager extends plugins.EventEmitter { | 
		
	
		
			
				|  |  |  |  | export interface ICertificateFailure { | 
		
	
		
			
				|  |  |  |  |   domain: string; | 
		
	
		
			
				|  |  |  |  |   error: string; | 
		
	
		
			
				|  |  |  |  |   isRenewal: boolean; | 
		
	
		
			
				|  |  |  |  | } | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  | /** | 
		
	
		
			
				|  |  |  |  |  * Certificate expiry payload type | 
		
	
		
			
				|  |  |  |  |  */ | 
		
	
		
			
				|  |  |  |  | export interface ICertificateExpiring { | 
		
	
		
			
				|  |  |  |  |   domain: string; | 
		
	
		
			
				|  |  |  |  |   expiryDate: Date; | 
		
	
		
			
				|  |  |  |  |   daysRemaining: number; | 
		
	
		
			
				|  |  |  |  | } | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  | /** | 
		
	
		
			
				|  |  |  |  |  * Port80Handler with ACME certificate management and request forwarding capabilities | 
		
	
		
			
				|  |  |  |  |  */ | 
		
	
		
			
				|  |  |  |  | export class Port80Handler extends plugins.EventEmitter { | 
		
	
		
			
				|  |  |  |  |   private domainCertificates: Map<string, IDomainCertificate>; | 
		
	
		
			
				|  |  |  |  |   private server: plugins.http.Server | null = null; | 
		
	
		
			
				|  |  |  |  |   private acmeClient: plugins.acme.Client | null = null; | 
		
	
		
			
				|  |  |  |  |   private accountKey: string | null = null; | 
		
	
		
			
				|  |  |  |  |   private renewalTimer: NodeJS.Timeout | null = null; | 
		
	
		
			
				|  |  |  |  |   private isShuttingDown: boolean = false; | 
		
	
		
			
				|  |  |  |  |   private options: Required<IAcmeCertManagerOptions>; | 
		
	
		
			
				|  |  |  |  |   private options: Required<IPort80HandlerOptions>; | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |   /** | 
		
	
		
			
				|  |  |  |  |    * Creates a new ACME Certificate Manager | 
		
	
		
			
				|  |  |  |  |    * Creates a new Port80Handler | 
		
	
		
			
				|  |  |  |  |    * @param options Configuration options | 
		
	
		
			
				|  |  |  |  |    */ | 
		
	
		
			
				|  |  |  |  |   constructor(options: IAcmeCertManagerOptions = {}) { | 
		
	
		
			
				|  |  |  |  |   constructor(options: IPort80HandlerOptions = {}) { | 
		
	
		
			
				|  |  |  |  |     super(); | 
		
	
		
			
				|  |  |  |  |     this.domainCertificates = new Map<string, IDomainCertificate>(); | 
		
	
		
			
				|  |  |  |  |      | 
		
	
	
		
			
				
					
					|  |  |  | @@ -73,7 +141,7 @@ export class AcmeCertManager extends plugins.EventEmitter { | 
		
	
		
			
				|  |  |  |  |       port: options.port ?? 80, | 
		
	
		
			
				|  |  |  |  |       contactEmail: options.contactEmail ?? 'admin@example.com', | 
		
	
		
			
				|  |  |  |  |       useProduction: options.useProduction ?? false, // Safer default: staging | 
		
	
		
			
				|  |  |  |  |       renewThresholdDays: options.renewThresholdDays ?? 30, | 
		
	
		
			
				|  |  |  |  |       renewThresholdDays: options.renewThresholdDays ?? 10, // Changed to 10 days as per requirements | 
		
	
		
			
				|  |  |  |  |       httpsRedirectPort: options.httpsRedirectPort ?? 443, | 
		
	
		
			
				|  |  |  |  |       renewCheckIntervalHours: options.renewCheckIntervalHours ?? 24, | 
		
	
		
			
				|  |  |  |  |     }; | 
		
	
	
		
			
				
					
					|  |  |  | @@ -84,11 +152,11 @@ export class AcmeCertManager extends plugins.EventEmitter { | 
		
	
		
			
				|  |  |  |  |    */ | 
		
	
		
			
				|  |  |  |  |   public async start(): Promise<void> { | 
		
	
		
			
				|  |  |  |  |     if (this.server) { | 
		
	
		
			
				|  |  |  |  |       throw new Error('Server is already running'); | 
		
	
		
			
				|  |  |  |  |       throw new ServerError('Server is already running'); | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     if (this.isShuttingDown) { | 
		
	
		
			
				|  |  |  |  |       throw new Error('Server is shutting down'); | 
		
	
		
			
				|  |  |  |  |       throw new ServerError('Server is shutting down'); | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |     return new Promise((resolve, reject) => { | 
		
	
	
		
			
				
					
					|  |  |  | @@ -97,22 +165,33 @@ export class AcmeCertManager extends plugins.EventEmitter { | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         this.server.on('error', (error: NodeJS.ErrnoException) => { | 
		
	
		
			
				|  |  |  |  |           if (error.code === 'EACCES') { | 
		
	
		
			
				|  |  |  |  |             reject(new Error(`Permission denied to bind to port ${this.options.port}. Try running with elevated privileges or use a port > 1024.`)); | 
		
	
		
			
				|  |  |  |  |             reject(new ServerError(`Permission denied to bind to port ${this.options.port}. Try running with elevated privileges or use a port > 1024.`, error.code)); | 
		
	
		
			
				|  |  |  |  |           } else if (error.code === 'EADDRINUSE') { | 
		
	
		
			
				|  |  |  |  |             reject(new Error(`Port ${this.options.port} is already in use.`)); | 
		
	
		
			
				|  |  |  |  |             reject(new ServerError(`Port ${this.options.port} is already in use.`, error.code)); | 
		
	
		
			
				|  |  |  |  |           } else { | 
		
	
		
			
				|  |  |  |  |             reject(error); | 
		
	
		
			
				|  |  |  |  |             reject(new ServerError(error.message, error.code)); | 
		
	
		
			
				|  |  |  |  |           } | 
		
	
		
			
				|  |  |  |  |         }); | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         this.server.listen(this.options.port, () => { | 
		
	
		
			
				|  |  |  |  |           console.log(`AcmeCertManager is listening on port ${this.options.port}`); | 
		
	
		
			
				|  |  |  |  |           console.log(`Port80Handler is listening on port ${this.options.port}`); | 
		
	
		
			
				|  |  |  |  |           this.startRenewalTimer(); | 
		
	
		
			
				|  |  |  |  |           this.emit(CertManagerEvents.MANAGER_STARTED, this.options.port); | 
		
	
		
			
				|  |  |  |  |           this.emit(Port80HandlerEvents.MANAGER_STARTED, this.options.port); | 
		
	
		
			
				|  |  |  |  |            | 
		
	
		
			
				|  |  |  |  |           // Start certificate process for domains with acmeMaintenance enabled | 
		
	
		
			
				|  |  |  |  |           for (const [domain, domainInfo] of this.domainCertificates.entries()) { | 
		
	
		
			
				|  |  |  |  |             if (domainInfo.options.acmeMaintenance && !domainInfo.certObtained && !domainInfo.obtainingInProgress) { | 
		
	
		
			
				|  |  |  |  |               this.obtainCertificate(domain).catch(err => { | 
		
	
		
			
				|  |  |  |  |                 console.error(`Error obtaining initial certificate for ${domain}:`, err); | 
		
	
		
			
				|  |  |  |  |               }); | 
		
	
		
			
				|  |  |  |  |             } | 
		
	
		
			
				|  |  |  |  |           } | 
		
	
		
			
				|  |  |  |  |            | 
		
	
		
			
				|  |  |  |  |           resolve(); | 
		
	
		
			
				|  |  |  |  |         }); | 
		
	
		
			
				|  |  |  |  |       } catch (error) { | 
		
	
		
			
				|  |  |  |  |         reject(error); | 
		
	
		
			
				|  |  |  |  |         const message = error instanceof Error ? error.message : 'Unknown error starting server'; | 
		
	
		
			
				|  |  |  |  |         reject(new ServerError(message)); | 
		
	
		
			
				|  |  |  |  |       } | 
		
	
		
			
				|  |  |  |  |     }); | 
		
	
		
			
				|  |  |  |  |   } | 
		
	
	
		
			
				
					
					|  |  |  | @@ -138,7 +217,7 @@ export class AcmeCertManager extends plugins.EventEmitter { | 
		
	
		
			
				|  |  |  |  |         this.server.close(() => { | 
		
	
		
			
				|  |  |  |  |           this.server = null; | 
		
	
		
			
				|  |  |  |  |           this.isShuttingDown = false; | 
		
	
		
			
				|  |  |  |  |           this.emit(CertManagerEvents.MANAGER_STOPPED); | 
		
	
		
			
				|  |  |  |  |           this.emit(Port80HandlerEvents.MANAGER_STOPPED); | 
		
	
		
			
				|  |  |  |  |           resolve(); | 
		
	
		
			
				|  |  |  |  |         }); | 
		
	
		
			
				|  |  |  |  |       } else { | 
		
	
	
		
			
				
					
					|  |  |  | @@ -149,13 +228,41 @@ export class AcmeCertManager extends plugins.EventEmitter { | 
		
	
		
			
				|  |  |  |  |   } | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |   /** | 
		
	
		
			
				|  |  |  |  |    * Adds a domain to be managed for certificates | 
		
	
		
			
				|  |  |  |  |    * @param domain The domain to add | 
		
	
		
			
				|  |  |  |  |    * Adds a domain with configuration options | 
		
	
		
			
				|  |  |  |  |    * @param options Domain configuration options | 
		
	
		
			
				|  |  |  |  |    */ | 
		
	
		
			
				|  |  |  |  |   public addDomain(domain: string): void { | 
		
	
		
			
				|  |  |  |  |     if (!this.domainCertificates.has(domain)) { | 
		
	
		
			
				|  |  |  |  |       this.domainCertificates.set(domain, { certObtained: false, obtainingInProgress: false }); | 
		
	
		
			
				|  |  |  |  |       console.log(`Domain added: ${domain}`); | 
		
	
		
			
				|  |  |  |  |   public addDomain(options: IDomainOptions): void { | 
		
	
		
			
				|  |  |  |  |     if (!options.domainName || typeof options.domainName !== 'string') { | 
		
	
		
			
				|  |  |  |  |       throw new Port80HandlerError('Invalid domain name'); | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     const domainName = options.domainName; | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     if (!this.domainCertificates.has(domainName)) { | 
		
	
		
			
				|  |  |  |  |       this.domainCertificates.set(domainName, { | 
		
	
		
			
				|  |  |  |  |         options, | 
		
	
		
			
				|  |  |  |  |         certObtained: false, | 
		
	
		
			
				|  |  |  |  |         obtainingInProgress: false | 
		
	
		
			
				|  |  |  |  |       }); | 
		
	
		
			
				|  |  |  |  |        | 
		
	
		
			
				|  |  |  |  |       console.log(`Domain added: ${domainName} with configuration:`, { | 
		
	
		
			
				|  |  |  |  |         sslRedirect: options.sslRedirect, | 
		
	
		
			
				|  |  |  |  |         acmeMaintenance: options.acmeMaintenance, | 
		
	
		
			
				|  |  |  |  |         hasForward: !!options.forward, | 
		
	
		
			
				|  |  |  |  |         hasAcmeForward: !!options.acmeForward | 
		
	
		
			
				|  |  |  |  |       }); | 
		
	
		
			
				|  |  |  |  |        | 
		
	
		
			
				|  |  |  |  |       // If acmeMaintenance is enabled, start certificate process immediately | 
		
	
		
			
				|  |  |  |  |       if (options.acmeMaintenance && this.server) { | 
		
	
		
			
				|  |  |  |  |         this.obtainCertificate(domainName).catch(err => { | 
		
	
		
			
				|  |  |  |  |           console.error(`Error obtaining initial certificate for ${domainName}:`, err); | 
		
	
		
			
				|  |  |  |  |         }); | 
		
	
		
			
				|  |  |  |  |       } | 
		
	
		
			
				|  |  |  |  |     } else { | 
		
	
		
			
				|  |  |  |  |       // Update existing domain with new options | 
		
	
		
			
				|  |  |  |  |       const existing = this.domainCertificates.get(domainName)!; | 
		
	
		
			
				|  |  |  |  |       existing.options = options; | 
		
	
		
			
				|  |  |  |  |       console.log(`Domain ${domainName} configuration updated`); | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  |   } | 
		
	
		
			
				|  |  |  |  |  | 
		
	
	
		
			
				
					
					|  |  |  | @@ -177,10 +284,25 @@ export class AcmeCertManager extends plugins.EventEmitter { | 
		
	
		
			
				|  |  |  |  |    * @param expiryDate Optional expiry date | 
		
	
		
			
				|  |  |  |  |    */ | 
		
	
		
			
				|  |  |  |  |   public setCertificate(domain: string, certificate: string, privateKey: string, expiryDate?: Date): void { | 
		
	
		
			
				|  |  |  |  |     if (!domain || !certificate || !privateKey) { | 
		
	
		
			
				|  |  |  |  |       throw new Port80HandlerError('Domain, certificate and privateKey are required'); | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     let domainInfo = this.domainCertificates.get(domain); | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     if (!domainInfo) { | 
		
	
		
			
				|  |  |  |  |       domainInfo = { certObtained: false, obtainingInProgress: false }; | 
		
	
		
			
				|  |  |  |  |       // Create default domain options if not already configured | 
		
	
		
			
				|  |  |  |  |       const defaultOptions: IDomainOptions = { | 
		
	
		
			
				|  |  |  |  |         domainName: domain, | 
		
	
		
			
				|  |  |  |  |         sslRedirect: true, | 
		
	
		
			
				|  |  |  |  |         acmeMaintenance: true | 
		
	
		
			
				|  |  |  |  |       }; | 
		
	
		
			
				|  |  |  |  |        | 
		
	
		
			
				|  |  |  |  |       domainInfo = {  | 
		
	
		
			
				|  |  |  |  |         options: defaultOptions,  | 
		
	
		
			
				|  |  |  |  |         certObtained: false,  | 
		
	
		
			
				|  |  |  |  |         obtainingInProgress: false  | 
		
	
		
			
				|  |  |  |  |       }; | 
		
	
		
			
				|  |  |  |  |       this.domainCertificates.set(domain, domainInfo); | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  |      | 
		
	
	
		
			
				
					
					|  |  |  | @@ -192,27 +314,18 @@ export class AcmeCertManager extends plugins.EventEmitter { | 
		
	
		
			
				|  |  |  |  |     if (expiryDate) { | 
		
	
		
			
				|  |  |  |  |       domainInfo.expiryDate = expiryDate; | 
		
	
		
			
				|  |  |  |  |     } else { | 
		
	
		
			
				|  |  |  |  |       // Try to extract expiry date from certificate | 
		
	
		
			
				|  |  |  |  |       try { | 
		
	
		
			
				|  |  |  |  |         // This is a simplistic approach - in a real implementation, use a proper | 
		
	
		
			
				|  |  |  |  |         // certificate parsing library like node-forge or x509 | 
		
	
		
			
				|  |  |  |  |         const matches = certificate.match(/Not After\s*:\s*(.*?)(?:\n|$)/i); | 
		
	
		
			
				|  |  |  |  |         if (matches && matches[1]) { | 
		
	
		
			
				|  |  |  |  |           domainInfo.expiryDate = new Date(matches[1]); | 
		
	
		
			
				|  |  |  |  |         } | 
		
	
		
			
				|  |  |  |  |       } catch (error) { | 
		
	
		
			
				|  |  |  |  |         console.warn(`Failed to extract expiry date from certificate for ${domain}`); | 
		
	
		
			
				|  |  |  |  |       } | 
		
	
		
			
				|  |  |  |  |       // Extract expiry date from certificate | 
		
	
		
			
				|  |  |  |  |       domainInfo.expiryDate = this.extractExpiryDateFromCertificate(certificate, domain); | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     console.log(`Certificate set for ${domain}`); | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     // Emit certificate event | 
		
	
		
			
				|  |  |  |  |     this.emitCertificateEvent(CertManagerEvents.CERTIFICATE_ISSUED, { | 
		
	
		
			
				|  |  |  |  |     this.emitCertificateEvent(Port80HandlerEvents.CERTIFICATE_ISSUED, { | 
		
	
		
			
				|  |  |  |  |       domain, | 
		
	
		
			
				|  |  |  |  |       certificate, | 
		
	
		
			
				|  |  |  |  |       privateKey, | 
		
	
		
			
				|  |  |  |  |       expiryDate: domainInfo.expiryDate || new Date(Date.now() + 90 * 24 * 60 * 60 * 1000) // 90 days default | 
		
	
		
			
				|  |  |  |  |       expiryDate: domainInfo.expiryDate || this.getDefaultExpiryDate() | 
		
	
		
			
				|  |  |  |  |     }); | 
		
	
		
			
				|  |  |  |  |   } | 
		
	
		
			
				|  |  |  |  |    | 
		
	
	
		
			
				
					
					|  |  |  | @@ -231,7 +344,7 @@ export class AcmeCertManager extends plugins.EventEmitter { | 
		
	
		
			
				|  |  |  |  |       domain, | 
		
	
		
			
				|  |  |  |  |       certificate: domainInfo.certificate, | 
		
	
		
			
				|  |  |  |  |       privateKey: domainInfo.privateKey, | 
		
	
		
			
				|  |  |  |  |       expiryDate: domainInfo.expiryDate || new Date(Date.now() + 90 * 24 * 60 * 60 * 1000) // 90 days default | 
		
	
		
			
				|  |  |  |  |       expiryDate: domainInfo.expiryDate || this.getDefaultExpiryDate() | 
		
	
		
			
				|  |  |  |  |     }; | 
		
	
		
			
				|  |  |  |  |   } | 
		
	
		
			
				|  |  |  |  |  | 
		
	
	
		
			
				
					
					|  |  |  | @@ -244,23 +357,28 @@ export class AcmeCertManager extends plugins.EventEmitter { | 
		
	
		
			
				|  |  |  |  |       return this.acmeClient; | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     // Generate a new account key | 
		
	
		
			
				|  |  |  |  |     this.accountKey = (await plugins.acme.forge.createPrivateKey()).toString(); | 
		
	
		
			
				|  |  |  |  |     try { | 
		
	
		
			
				|  |  |  |  |       // Generate a new account key | 
		
	
		
			
				|  |  |  |  |       this.accountKey = (await plugins.acme.forge.createPrivateKey()).toString(); | 
		
	
		
			
				|  |  |  |  |        | 
		
	
		
			
				|  |  |  |  |     this.acmeClient = new plugins.acme.Client({ | 
		
	
		
			
				|  |  |  |  |       directoryUrl: this.options.useProduction  | 
		
	
		
			
				|  |  |  |  |         ? plugins.acme.directory.letsencrypt.production  | 
		
	
		
			
				|  |  |  |  |         : plugins.acme.directory.letsencrypt.staging, | 
		
	
		
			
				|  |  |  |  |       accountKey: this.accountKey, | 
		
	
		
			
				|  |  |  |  |     }); | 
		
	
		
			
				|  |  |  |  |       this.acmeClient = new plugins.acme.Client({ | 
		
	
		
			
				|  |  |  |  |         directoryUrl: this.options.useProduction  | 
		
	
		
			
				|  |  |  |  |           ? plugins.acme.directory.letsencrypt.production  | 
		
	
		
			
				|  |  |  |  |           : plugins.acme.directory.letsencrypt.staging, | 
		
	
		
			
				|  |  |  |  |         accountKey: this.accountKey, | 
		
	
		
			
				|  |  |  |  |       }); | 
		
	
		
			
				|  |  |  |  |        | 
		
	
		
			
				|  |  |  |  |     // Create a new account | 
		
	
		
			
				|  |  |  |  |     await this.acmeClient.createAccount({ | 
		
	
		
			
				|  |  |  |  |       termsOfServiceAgreed: true, | 
		
	
		
			
				|  |  |  |  |       contact: [`mailto:${this.options.contactEmail}`], | 
		
	
		
			
				|  |  |  |  |     }); | 
		
	
		
			
				|  |  |  |  |       // Create a new account | 
		
	
		
			
				|  |  |  |  |       await this.acmeClient.createAccount({ | 
		
	
		
			
				|  |  |  |  |         termsOfServiceAgreed: true, | 
		
	
		
			
				|  |  |  |  |         contact: [`mailto:${this.options.contactEmail}`], | 
		
	
		
			
				|  |  |  |  |       }); | 
		
	
		
			
				|  |  |  |  |        | 
		
	
		
			
				|  |  |  |  |     return this.acmeClient; | 
		
	
		
			
				|  |  |  |  |       return this.acmeClient; | 
		
	
		
			
				|  |  |  |  |     } catch (error) { | 
		
	
		
			
				|  |  |  |  |       const message = error instanceof Error ? error.message : 'Unknown error initializing ACME client'; | 
		
	
		
			
				|  |  |  |  |       throw new Port80HandlerError(`Failed to initialize ACME client: ${message}`); | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  |   } | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |   /** | 
		
	
	
		
			
				
					
					|  |  |  | @@ -279,12 +397,7 @@ export class AcmeCertManager extends plugins.EventEmitter { | 
		
	
		
			
				|  |  |  |  |     // Extract domain (ignoring any port in the Host header) | 
		
	
		
			
				|  |  |  |  |     const domain = hostHeader.split(':')[0]; | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |     // If the request is for an ACME HTTP-01 challenge, handle it | 
		
	
		
			
				|  |  |  |  |     if (req.url && req.url.startsWith('/.well-known/acme-challenge/')) { | 
		
	
		
			
				|  |  |  |  |       this.handleAcmeChallenge(req, res, domain); | 
		
	
		
			
				|  |  |  |  |       return; | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |     // Check if domain is configured | 
		
	
		
			
				|  |  |  |  |     if (!this.domainCertificates.has(domain)) { | 
		
	
		
			
				|  |  |  |  |       res.statusCode = 404; | 
		
	
		
			
				|  |  |  |  |       res.end('Domain not configured'); | 
		
	
	
		
			
				
					
					|  |  |  | @@ -292,9 +405,28 @@ export class AcmeCertManager extends plugins.EventEmitter { | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |     const domainInfo = this.domainCertificates.get(domain)!; | 
		
	
		
			
				|  |  |  |  |     const options = domainInfo.options; | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |     // If certificate exists, redirect to HTTPS | 
		
	
		
			
				|  |  |  |  |     if (domainInfo.certObtained) { | 
		
	
		
			
				|  |  |  |  |     // If the request is for an ACME HTTP-01 challenge, handle it | 
		
	
		
			
				|  |  |  |  |     if (req.url && req.url.startsWith('/.well-known/acme-challenge/')) { | 
		
	
		
			
				|  |  |  |  |       // Check if we should forward ACME requests | 
		
	
		
			
				|  |  |  |  |       if (options.acmeForward) { | 
		
	
		
			
				|  |  |  |  |         this.forwardRequest(req, res, options.acmeForward, 'ACME challenge'); | 
		
	
		
			
				|  |  |  |  |         return; | 
		
	
		
			
				|  |  |  |  |       } | 
		
	
		
			
				|  |  |  |  |        | 
		
	
		
			
				|  |  |  |  |       this.handleAcmeChallenge(req, res, domain); | 
		
	
		
			
				|  |  |  |  |       return; | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |     // Check if we should forward non-ACME requests | 
		
	
		
			
				|  |  |  |  |     if (options.forward) { | 
		
	
		
			
				|  |  |  |  |       this.forwardRequest(req, res, options.forward, 'HTTP'); | 
		
	
		
			
				|  |  |  |  |       return; | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |     // If certificate exists and sslRedirect is enabled, redirect to HTTPS | 
		
	
		
			
				|  |  |  |  |     if (domainInfo.certObtained && options.sslRedirect) { | 
		
	
		
			
				|  |  |  |  |       const httpsPort = this.options.httpsRedirectPort; | 
		
	
		
			
				|  |  |  |  |       const portSuffix = httpsPort === 443 ? '' : `:${httpsPort}`; | 
		
	
		
			
				|  |  |  |  |       const redirectUrl = `https://${domain}${portSuffix}${req.url || '/'}`; | 
		
	
	
		
			
				
					
					|  |  |  | @@ -302,17 +434,93 @@ export class AcmeCertManager extends plugins.EventEmitter { | 
		
	
		
			
				|  |  |  |  |       res.statusCode = 301; | 
		
	
		
			
				|  |  |  |  |       res.setHeader('Location', redirectUrl); | 
		
	
		
			
				|  |  |  |  |       res.end(`Redirecting to ${redirectUrl}`); | 
		
	
		
			
				|  |  |  |  |     } else { | 
		
	
		
			
				|  |  |  |  |       return; | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     // Handle case where certificate maintenance is enabled but not yet obtained | 
		
	
		
			
				|  |  |  |  |     if (options.acmeMaintenance && !domainInfo.certObtained) { | 
		
	
		
			
				|  |  |  |  |       // Trigger certificate issuance if not already running | 
		
	
		
			
				|  |  |  |  |       if (!domainInfo.obtainingInProgress) { | 
		
	
		
			
				|  |  |  |  |         this.obtainCertificate(domain).catch(err => { | 
		
	
		
			
				|  |  |  |  |           this.emit(CertManagerEvents.CERTIFICATE_FAILED, { domain, error: err.message }); | 
		
	
		
			
				|  |  |  |  |           const errorMessage = err instanceof Error ? err.message : 'Unknown error'; | 
		
	
		
			
				|  |  |  |  |           this.emit(Port80HandlerEvents.CERTIFICATE_FAILED, { | 
		
	
		
			
				|  |  |  |  |             domain, | 
		
	
		
			
				|  |  |  |  |             error: errorMessage, | 
		
	
		
			
				|  |  |  |  |             isRenewal: false | 
		
	
		
			
				|  |  |  |  |           }); | 
		
	
		
			
				|  |  |  |  |           console.error(`Error obtaining certificate for ${domain}:`, err); | 
		
	
		
			
				|  |  |  |  |         }); | 
		
	
		
			
				|  |  |  |  |       } | 
		
	
		
			
				|  |  |  |  |        | 
		
	
		
			
				|  |  |  |  |       res.statusCode = 503; | 
		
	
		
			
				|  |  |  |  |       res.end('Certificate issuance in progress, please try again later.'); | 
		
	
		
			
				|  |  |  |  |       return; | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     // Default response for unhandled request | 
		
	
		
			
				|  |  |  |  |     res.statusCode = 404; | 
		
	
		
			
				|  |  |  |  |     res.end('No handlers configured for this request'); | 
		
	
		
			
				|  |  |  |  |   } | 
		
	
		
			
				|  |  |  |  |    | 
		
	
		
			
				|  |  |  |  |   /** | 
		
	
		
			
				|  |  |  |  |    * Forwards an HTTP request to the specified target | 
		
	
		
			
				|  |  |  |  |    * @param req The original request | 
		
	
		
			
				|  |  |  |  |    * @param res The response object | 
		
	
		
			
				|  |  |  |  |    * @param target The forwarding target (IP and port) | 
		
	
		
			
				|  |  |  |  |    * @param requestType Type of request for logging | 
		
	
		
			
				|  |  |  |  |    */ | 
		
	
		
			
				|  |  |  |  |   private forwardRequest( | 
		
	
		
			
				|  |  |  |  |     req: plugins.http.IncomingMessage,  | 
		
	
		
			
				|  |  |  |  |     res: plugins.http.ServerResponse, | 
		
	
		
			
				|  |  |  |  |     target: IForwardConfig, | 
		
	
		
			
				|  |  |  |  |     requestType: string | 
		
	
		
			
				|  |  |  |  |   ): void { | 
		
	
		
			
				|  |  |  |  |     const options = { | 
		
	
		
			
				|  |  |  |  |       hostname: target.ip, | 
		
	
		
			
				|  |  |  |  |       port: target.port, | 
		
	
		
			
				|  |  |  |  |       path: req.url, | 
		
	
		
			
				|  |  |  |  |       method: req.method, | 
		
	
		
			
				|  |  |  |  |       headers: { ...req.headers } | 
		
	
		
			
				|  |  |  |  |     }; | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     const domain = req.headers.host?.split(':')[0] || 'unknown'; | 
		
	
		
			
				|  |  |  |  |     console.log(`Forwarding ${requestType} request for ${domain} to ${target.ip}:${target.port}`); | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     const proxyReq = plugins.http.request(options, (proxyRes) => { | 
		
	
		
			
				|  |  |  |  |       // Copy status code | 
		
	
		
			
				|  |  |  |  |       res.statusCode = proxyRes.statusCode || 500; | 
		
	
		
			
				|  |  |  |  |        | 
		
	
		
			
				|  |  |  |  |       // Copy headers | 
		
	
		
			
				|  |  |  |  |       for (const [key, value] of Object.entries(proxyRes.headers)) { | 
		
	
		
			
				|  |  |  |  |         if (value) res.setHeader(key, value); | 
		
	
		
			
				|  |  |  |  |       } | 
		
	
		
			
				|  |  |  |  |        | 
		
	
		
			
				|  |  |  |  |       // Pipe response data | 
		
	
		
			
				|  |  |  |  |       proxyRes.pipe(res); | 
		
	
		
			
				|  |  |  |  |        | 
		
	
		
			
				|  |  |  |  |       this.emit(Port80HandlerEvents.REQUEST_FORWARDED, { | 
		
	
		
			
				|  |  |  |  |         domain, | 
		
	
		
			
				|  |  |  |  |         requestType, | 
		
	
		
			
				|  |  |  |  |         target: `${target.ip}:${target.port}`, | 
		
	
		
			
				|  |  |  |  |         statusCode: proxyRes.statusCode | 
		
	
		
			
				|  |  |  |  |       }); | 
		
	
		
			
				|  |  |  |  |     }); | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     proxyReq.on('error', (error) => { | 
		
	
		
			
				|  |  |  |  |       console.error(`Error forwarding request to ${target.ip}:${target.port}:`, error); | 
		
	
		
			
				|  |  |  |  |       if (!res.headersSent) { | 
		
	
		
			
				|  |  |  |  |         res.statusCode = 502; | 
		
	
		
			
				|  |  |  |  |         res.end(`Proxy error: ${error.message}`); | 
		
	
		
			
				|  |  |  |  |       } else { | 
		
	
		
			
				|  |  |  |  |         res.end(); | 
		
	
		
			
				|  |  |  |  |       } | 
		
	
		
			
				|  |  |  |  |     }); | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     // Pipe original request to proxy request | 
		
	
		
			
				|  |  |  |  |     if (req.readable) { | 
		
	
		
			
				|  |  |  |  |       req.pipe(proxyReq); | 
		
	
		
			
				|  |  |  |  |     } else { | 
		
	
		
			
				|  |  |  |  |       proxyReq.end(); | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  |   } | 
		
	
		
			
				|  |  |  |  |  | 
		
	
	
		
			
				
					
					|  |  |  | @@ -354,7 +562,13 @@ export class AcmeCertManager extends plugins.EventEmitter { | 
		
	
		
			
				|  |  |  |  |     // Get the domain info | 
		
	
		
			
				|  |  |  |  |     const domainInfo = this.domainCertificates.get(domain); | 
		
	
		
			
				|  |  |  |  |     if (!domainInfo) { | 
		
	
		
			
				|  |  |  |  |       throw new Error(`Domain not found: ${domain}`); | 
		
	
		
			
				|  |  |  |  |       throw new CertificateError('Domain not found', domain, isRenewal); | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     // Verify that acmeMaintenance is enabled | 
		
	
		
			
				|  |  |  |  |     if (!domainInfo.options.acmeMaintenance) { | 
		
	
		
			
				|  |  |  |  |       console.log(`Skipping certificate issuance for ${domain} - acmeMaintenance is disabled`); | 
		
	
		
			
				|  |  |  |  |       return; | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     // Prevent concurrent certificate issuance | 
		
	
	
		
			
				
					
					|  |  |  | @@ -377,40 +591,8 @@ export class AcmeCertManager extends plugins.EventEmitter { | 
		
	
		
			
				|  |  |  |  |       // Get the authorizations for the order | 
		
	
		
			
				|  |  |  |  |       const authorizations = await client.getAuthorizations(order); | 
		
	
		
			
				|  |  |  |  |        | 
		
	
		
			
				|  |  |  |  |       for (const authz of authorizations) { | 
		
	
		
			
				|  |  |  |  |         const challenge = authz.challenges.find(ch => ch.type === 'http-01'); | 
		
	
		
			
				|  |  |  |  |         if (!challenge) { | 
		
	
		
			
				|  |  |  |  |           throw new Error('HTTP-01 challenge not found'); | 
		
	
		
			
				|  |  |  |  |         } | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         // Get the key authorization for the challenge | 
		
	
		
			
				|  |  |  |  |         const keyAuthorization = await client.getChallengeKeyAuthorization(challenge); | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         // Store the challenge data | 
		
	
		
			
				|  |  |  |  |         domainInfo.challengeToken = challenge.token; | 
		
	
		
			
				|  |  |  |  |         domainInfo.challengeKeyAuthorization = keyAuthorization; | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |         // ACME client type definition workaround - use compatible approach | 
		
	
		
			
				|  |  |  |  |         // First check if challenge verification is needed | 
		
	
		
			
				|  |  |  |  |         const authzUrl = authz.url; | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         try { | 
		
	
		
			
				|  |  |  |  |           // Check if authzUrl exists and perform verification | 
		
	
		
			
				|  |  |  |  |           if (authzUrl) { | 
		
	
		
			
				|  |  |  |  |             await client.verifyChallenge(authz, challenge); | 
		
	
		
			
				|  |  |  |  |           } | 
		
	
		
			
				|  |  |  |  |            | 
		
	
		
			
				|  |  |  |  |           // Complete the challenge | 
		
	
		
			
				|  |  |  |  |           await client.completeChallenge(challenge); | 
		
	
		
			
				|  |  |  |  |            | 
		
	
		
			
				|  |  |  |  |           // Wait for validation | 
		
	
		
			
				|  |  |  |  |           await client.waitForValidStatus(challenge); | 
		
	
		
			
				|  |  |  |  |           console.log(`HTTP-01 challenge completed for ${domain}`); | 
		
	
		
			
				|  |  |  |  |         } catch (error) { | 
		
	
		
			
				|  |  |  |  |           console.error(`Challenge error for ${domain}:`, error); | 
		
	
		
			
				|  |  |  |  |           throw error; | 
		
	
		
			
				|  |  |  |  |         } | 
		
	
		
			
				|  |  |  |  |       } | 
		
	
		
			
				|  |  |  |  |       // Process each authorization | 
		
	
		
			
				|  |  |  |  |       await this.processAuthorizations(client, domain, authorizations); | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |       // Generate a CSR and private key | 
		
	
		
			
				|  |  |  |  |       const [csrBuffer, privateKeyBuffer] = await plugins.acme.forge.createCsr({ | 
		
	
	
		
			
				
					
					|  |  |  | @@ -436,28 +618,20 @@ export class AcmeCertManager extends plugins.EventEmitter { | 
		
	
		
			
				|  |  |  |  |       delete domainInfo.challengeKeyAuthorization; | 
		
	
		
			
				|  |  |  |  |        | 
		
	
		
			
				|  |  |  |  |       // Extract expiry date from certificate | 
		
	
		
			
				|  |  |  |  |       try { | 
		
	
		
			
				|  |  |  |  |         const matches = certificate.match(/Not After\s*:\s*(.*?)(?:\n|$)/i); | 
		
	
		
			
				|  |  |  |  |         if (matches && matches[1]) { | 
		
	
		
			
				|  |  |  |  |           domainInfo.expiryDate = new Date(matches[1]); | 
		
	
		
			
				|  |  |  |  |           console.log(`Certificate for ${domain} will expire on ${domainInfo.expiryDate.toISOString()}`); | 
		
	
		
			
				|  |  |  |  |         } | 
		
	
		
			
				|  |  |  |  |       } catch (error) { | 
		
	
		
			
				|  |  |  |  |         console.warn(`Failed to extract expiry date from certificate for ${domain}`); | 
		
	
		
			
				|  |  |  |  |       } | 
		
	
		
			
				|  |  |  |  |       domainInfo.expiryDate = this.extractExpiryDateFromCertificate(certificate, domain); | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |       console.log(`Certificate ${isRenewal ? 'renewed' : 'obtained'} for ${domain}`); | 
		
	
		
			
				|  |  |  |  |        | 
		
	
		
			
				|  |  |  |  |       // Emit the appropriate event | 
		
	
		
			
				|  |  |  |  |       const eventType = isRenewal  | 
		
	
		
			
				|  |  |  |  |         ? CertManagerEvents.CERTIFICATE_RENEWED  | 
		
	
		
			
				|  |  |  |  |         : CertManagerEvents.CERTIFICATE_ISSUED; | 
		
	
		
			
				|  |  |  |  |         ? Port80HandlerEvents.CERTIFICATE_RENEWED  | 
		
	
		
			
				|  |  |  |  |         : Port80HandlerEvents.CERTIFICATE_ISSUED; | 
		
	
		
			
				|  |  |  |  |        | 
		
	
		
			
				|  |  |  |  |       this.emitCertificateEvent(eventType, { | 
		
	
		
			
				|  |  |  |  |         domain, | 
		
	
		
			
				|  |  |  |  |         certificate, | 
		
	
		
			
				|  |  |  |  |         privateKey, | 
		
	
		
			
				|  |  |  |  |         expiryDate: domainInfo.expiryDate || new Date(Date.now() + 90 * 24 * 60 * 60 * 1000) // 90 days default | 
		
	
		
			
				|  |  |  |  |         expiryDate: domainInfo.expiryDate || this.getDefaultExpiryDate() | 
		
	
		
			
				|  |  |  |  |       }); | 
		
	
		
			
				|  |  |  |  |        | 
		
	
		
			
				|  |  |  |  |     } catch (error: any) { | 
		
	
	
		
			
				
					
					|  |  |  | @@ -473,17 +647,76 @@ export class AcmeCertManager extends plugins.EventEmitter { | 
		
	
		
			
				|  |  |  |  |       } | 
		
	
		
			
				|  |  |  |  |        | 
		
	
		
			
				|  |  |  |  |       // Emit failure event | 
		
	
		
			
				|  |  |  |  |       this.emit(CertManagerEvents.CERTIFICATE_FAILED, { | 
		
	
		
			
				|  |  |  |  |       this.emit(Port80HandlerEvents.CERTIFICATE_FAILED, { | 
		
	
		
			
				|  |  |  |  |         domain, | 
		
	
		
			
				|  |  |  |  |         error: error.message || 'Unknown error', | 
		
	
		
			
				|  |  |  |  |         isRenewal | 
		
	
		
			
				|  |  |  |  |       }); | 
		
	
		
			
				|  |  |  |  |       } as ICertificateFailure); | 
		
	
		
			
				|  |  |  |  |        | 
		
	
		
			
				|  |  |  |  |       throw new CertificateError( | 
		
	
		
			
				|  |  |  |  |         error.message || 'Certificate issuance failed', | 
		
	
		
			
				|  |  |  |  |         domain, | 
		
	
		
			
				|  |  |  |  |         isRenewal | 
		
	
		
			
				|  |  |  |  |       ); | 
		
	
		
			
				|  |  |  |  |     } finally { | 
		
	
		
			
				|  |  |  |  |       // Reset flag whether successful or not | 
		
	
		
			
				|  |  |  |  |       domainInfo.obtainingInProgress = false; | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  |   } | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |   /** | 
		
	
		
			
				|  |  |  |  |    * Process ACME authorizations by verifying and completing challenges | 
		
	
		
			
				|  |  |  |  |    * @param client ACME client  | 
		
	
		
			
				|  |  |  |  |    * @param domain Domain name | 
		
	
		
			
				|  |  |  |  |    * @param authorizations Authorizations to process | 
		
	
		
			
				|  |  |  |  |    */ | 
		
	
		
			
				|  |  |  |  |   private async processAuthorizations( | 
		
	
		
			
				|  |  |  |  |     client: plugins.acme.Client, | 
		
	
		
			
				|  |  |  |  |     domain: string, | 
		
	
		
			
				|  |  |  |  |     authorizations: plugins.acme.Authorization[] | 
		
	
		
			
				|  |  |  |  |   ): Promise<void> { | 
		
	
		
			
				|  |  |  |  |     const domainInfo = this.domainCertificates.get(domain); | 
		
	
		
			
				|  |  |  |  |     if (!domainInfo) { | 
		
	
		
			
				|  |  |  |  |       throw new CertificateError('Domain not found during authorization', domain); | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     for (const authz of authorizations) { | 
		
	
		
			
				|  |  |  |  |       const challenge = authz.challenges.find(ch => ch.type === 'http-01'); | 
		
	
		
			
				|  |  |  |  |       if (!challenge) { | 
		
	
		
			
				|  |  |  |  |         throw new CertificateError('HTTP-01 challenge not found', domain); | 
		
	
		
			
				|  |  |  |  |       } | 
		
	
		
			
				|  |  |  |  |        | 
		
	
		
			
				|  |  |  |  |       // Get the key authorization for the challenge | 
		
	
		
			
				|  |  |  |  |       const keyAuthorization = await client.getChallengeKeyAuthorization(challenge); | 
		
	
		
			
				|  |  |  |  |        | 
		
	
		
			
				|  |  |  |  |       // Store the challenge data | 
		
	
		
			
				|  |  |  |  |       domainInfo.challengeToken = challenge.token; | 
		
	
		
			
				|  |  |  |  |       domainInfo.challengeKeyAuthorization = keyAuthorization; | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |       // ACME client type definition workaround - use compatible approach | 
		
	
		
			
				|  |  |  |  |       // First check if challenge verification is needed | 
		
	
		
			
				|  |  |  |  |       const authzUrl = authz.url; | 
		
	
		
			
				|  |  |  |  |        | 
		
	
		
			
				|  |  |  |  |       try { | 
		
	
		
			
				|  |  |  |  |         // Check if authzUrl exists and perform verification | 
		
	
		
			
				|  |  |  |  |         if (authzUrl) { | 
		
	
		
			
				|  |  |  |  |           await client.verifyChallenge(authz, challenge); | 
		
	
		
			
				|  |  |  |  |         } | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         // Complete the challenge | 
		
	
		
			
				|  |  |  |  |         await client.completeChallenge(challenge); | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         // Wait for validation | 
		
	
		
			
				|  |  |  |  |         await client.waitForValidStatus(challenge); | 
		
	
		
			
				|  |  |  |  |         console.log(`HTTP-01 challenge completed for ${domain}`); | 
		
	
		
			
				|  |  |  |  |       } catch (error) { | 
		
	
		
			
				|  |  |  |  |         const errorMessage = error instanceof Error ? error.message : 'Unknown challenge error'; | 
		
	
		
			
				|  |  |  |  |         console.error(`Challenge error for ${domain}:`, error); | 
		
	
		
			
				|  |  |  |  |         throw new CertificateError(`Challenge verification failed: ${errorMessage}`, domain); | 
		
	
		
			
				|  |  |  |  |       } | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  |   } | 
		
	
		
			
				|  |  |  |  |  | 
		
	
		
			
				|  |  |  |  |   /** | 
		
	
		
			
				|  |  |  |  |    * Starts the certificate renewal timer | 
		
	
		
			
				|  |  |  |  |    */ | 
		
	
	
		
			
				
					
					|  |  |  | @@ -519,6 +752,11 @@ export class AcmeCertManager extends plugins.EventEmitter { | 
		
	
		
			
				|  |  |  |  |     const renewThresholdMs = this.options.renewThresholdDays * 24 * 60 * 60 * 1000; | 
		
	
		
			
				|  |  |  |  |      | 
		
	
		
			
				|  |  |  |  |     for (const [domain, domainInfo] of this.domainCertificates.entries()) { | 
		
	
		
			
				|  |  |  |  |       // Skip domains with acmeMaintenance disabled | 
		
	
		
			
				|  |  |  |  |       if (!domainInfo.options.acmeMaintenance) { | 
		
	
		
			
				|  |  |  |  |         continue; | 
		
	
		
			
				|  |  |  |  |       } | 
		
	
		
			
				|  |  |  |  |        | 
		
	
		
			
				|  |  |  |  |       // Skip domains without certificates or already in renewal | 
		
	
		
			
				|  |  |  |  |       if (!domainInfo.certObtained || domainInfo.obtainingInProgress) { | 
		
	
		
			
				|  |  |  |  |         continue; | 
		
	
	
		
			
				
					
					|  |  |  | @@ -534,26 +772,67 @@ export class AcmeCertManager extends plugins.EventEmitter { | 
		
	
		
			
				|  |  |  |  |       // Check if certificate is near expiry | 
		
	
		
			
				|  |  |  |  |       if (timeUntilExpiry <= renewThresholdMs) { | 
		
	
		
			
				|  |  |  |  |         console.log(`Certificate for ${domain} expires soon, renewing...`); | 
		
	
		
			
				|  |  |  |  |         this.emit(CertManagerEvents.CERTIFICATE_EXPIRING, { | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         const daysRemaining = Math.ceil(timeUntilExpiry / (24 * 60 * 60 * 1000)); | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         this.emit(Port80HandlerEvents.CERTIFICATE_EXPIRING, { | 
		
	
		
			
				|  |  |  |  |           domain, | 
		
	
		
			
				|  |  |  |  |           expiryDate: domainInfo.expiryDate, | 
		
	
		
			
				|  |  |  |  |           daysRemaining: Math.ceil(timeUntilExpiry / (24 * 60 * 60 * 1000)) | 
		
	
		
			
				|  |  |  |  |         }); | 
		
	
		
			
				|  |  |  |  |           daysRemaining | 
		
	
		
			
				|  |  |  |  |         } as ICertificateExpiring); | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         // Start renewal process | 
		
	
		
			
				|  |  |  |  |         this.obtainCertificate(domain, true).catch(err => { | 
		
	
		
			
				|  |  |  |  |           console.error(`Error renewing certificate for ${domain}:`, err); | 
		
	
		
			
				|  |  |  |  |           const errorMessage = err instanceof Error ? err.message : 'Unknown error'; | 
		
	
		
			
				|  |  |  |  |           console.error(`Error renewing certificate for ${domain}:`, errorMessage); | 
		
	
		
			
				|  |  |  |  |         }); | 
		
	
		
			
				|  |  |  |  |       } | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  |   } | 
		
	
		
			
				|  |  |  |  |    | 
		
	
		
			
				|  |  |  |  |   /** | 
		
	
		
			
				|  |  |  |  |    * Extract expiry date from certificate using a more robust approach | 
		
	
		
			
				|  |  |  |  |    * @param certificate Certificate PEM string | 
		
	
		
			
				|  |  |  |  |    * @param domain Domain for logging | 
		
	
		
			
				|  |  |  |  |    * @returns Extracted expiry date or default | 
		
	
		
			
				|  |  |  |  |    */ | 
		
	
		
			
				|  |  |  |  |   private extractExpiryDateFromCertificate(certificate: string, domain: string): Date { | 
		
	
		
			
				|  |  |  |  |     try { | 
		
	
		
			
				|  |  |  |  |       // This is still using regex, but in a real implementation you would use | 
		
	
		
			
				|  |  |  |  |       // a library like node-forge or x509 to properly parse the certificate | 
		
	
		
			
				|  |  |  |  |       const matches = certificate.match(/Not After\s*:\s*(.*?)(?:\n|$)/i); | 
		
	
		
			
				|  |  |  |  |       if (matches && matches[1]) { | 
		
	
		
			
				|  |  |  |  |         const expiryDate = new Date(matches[1]); | 
		
	
		
			
				|  |  |  |  |          | 
		
	
		
			
				|  |  |  |  |         // Validate that we got a valid date | 
		
	
		
			
				|  |  |  |  |         if (!isNaN(expiryDate.getTime())) { | 
		
	
		
			
				|  |  |  |  |           console.log(`Certificate for ${domain} will expire on ${expiryDate.toISOString()}`); | 
		
	
		
			
				|  |  |  |  |           return expiryDate; | 
		
	
		
			
				|  |  |  |  |         } | 
		
	
		
			
				|  |  |  |  |       } | 
		
	
		
			
				|  |  |  |  |        | 
		
	
		
			
				|  |  |  |  |       console.warn(`Could not extract valid expiry date from certificate for ${domain}, using default`); | 
		
	
		
			
				|  |  |  |  |       return this.getDefaultExpiryDate(); | 
		
	
		
			
				|  |  |  |  |     } catch (error) { | 
		
	
		
			
				|  |  |  |  |       console.warn(`Failed to extract expiry date from certificate for ${domain}, using default`); | 
		
	
		
			
				|  |  |  |  |       return this.getDefaultExpiryDate(); | 
		
	
		
			
				|  |  |  |  |     } | 
		
	
		
			
				|  |  |  |  |   } | 
		
	
		
			
				|  |  |  |  |    | 
		
	
		
			
				|  |  |  |  |   /** | 
		
	
		
			
				|  |  |  |  |    * Get a default expiry date (90 days from now) | 
		
	
		
			
				|  |  |  |  |    * @returns Default expiry date | 
		
	
		
			
				|  |  |  |  |    */ | 
		
	
		
			
				|  |  |  |  |   private getDefaultExpiryDate(): Date { | 
		
	
		
			
				|  |  |  |  |     return new Date(Date.now() + 90 * 24 * 60 * 60 * 1000); // 90 days default | 
		
	
		
			
				|  |  |  |  |   } | 
		
	
		
			
				|  |  |  |  |    | 
		
	
		
			
				|  |  |  |  |   /** | 
		
	
		
			
				|  |  |  |  |    * Emits a certificate event with the certificate data | 
		
	
		
			
				|  |  |  |  |    * @param eventType The event type to emit | 
		
	
		
			
				|  |  |  |  |    * @param data The certificate data | 
		
	
		
			
				|  |  |  |  |    */ | 
		
	
		
			
				|  |  |  |  |   private emitCertificateEvent(eventType: CertManagerEvents, data: ICertificateData): void { | 
		
	
		
			
				|  |  |  |  |   private emitCertificateEvent(eventType: Port80HandlerEvents, data: ICertificateData): void { | 
		
	
		
			
				|  |  |  |  |     this.emit(eventType, data); | 
		
	
		
			
				|  |  |  |  |   } | 
		
	
		
			
				|  |  |  |  | } |