@push.rocks/smartproxy 7.1.2 → 10.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist_ts/00_commitinfo_data.js +2 -2
  2. package/dist_ts/classes.router.d.ts +9 -10
  3. package/dist_ts/classes.router.js +3 -5
  4. package/dist_ts/common/acmeFactory.d.ts +9 -0
  5. package/dist_ts/common/acmeFactory.js +20 -0
  6. package/dist_ts/common/eventUtils.d.ts +15 -0
  7. package/dist_ts/common/eventUtils.js +19 -0
  8. package/dist_ts/common/types.d.ts +82 -0
  9. package/dist_ts/common/types.js +17 -0
  10. package/dist_ts/networkproxy/classes.np.certificatemanager.js +23 -19
  11. package/dist_ts/networkproxy/classes.np.networkproxy.d.ts +1 -0
  12. package/dist_ts/networkproxy/classes.np.networkproxy.js +5 -1
  13. package/dist_ts/networkproxy/classes.np.types.d.ts +5 -10
  14. package/dist_ts/networkproxy/classes.np.types.js +1 -1
  15. package/dist_ts/plugins.d.ts +6 -3
  16. package/dist_ts/plugins.js +7 -4
  17. package/dist_ts/port80handler/classes.port80handler.d.ts +14 -111
  18. package/dist_ts/port80handler/classes.port80handler.js +94 -373
  19. package/dist_ts/smartproxy/classes.pp.certprovisioner.d.ts +54 -0
  20. package/dist_ts/smartproxy/classes.pp.certprovisioner.js +166 -0
  21. package/dist_ts/smartproxy/classes.pp.interfaces.d.ts +11 -33
  22. package/dist_ts/smartproxy/classes.pp.networkproxybridge.d.ts +5 -0
  23. package/dist_ts/smartproxy/classes.pp.networkproxybridge.js +21 -11
  24. package/dist_ts/smartproxy/classes.pp.portrangemanager.js +1 -11
  25. package/dist_ts/smartproxy/classes.smartproxy.d.ts +3 -5
  26. package/dist_ts/smartproxy/classes.smartproxy.js +94 -180
  27. package/package.json +12 -10
  28. package/readme.hints.md +64 -1
  29. package/readme.md +253 -408
  30. package/readme.plan.md +29 -0
  31. package/ts/00_commitinfo_data.ts +1 -1
  32. package/ts/classes.router.ts +13 -15
  33. package/ts/common/acmeFactory.ts +23 -0
  34. package/ts/common/eventUtils.ts +34 -0
  35. package/ts/common/types.ts +89 -0
  36. package/ts/networkproxy/classes.np.certificatemanager.ts +23 -19
  37. package/ts/networkproxy/classes.np.networkproxy.ts +4 -0
  38. package/ts/networkproxy/classes.np.types.ts +6 -10
  39. package/ts/plugins.ts +17 -4
  40. package/ts/port80handler/classes.port80handler.ts +108 -509
  41. package/ts/smartproxy/classes.pp.certprovisioner.ts +188 -0
  42. package/ts/smartproxy/classes.pp.interfaces.ts +13 -36
  43. package/ts/smartproxy/classes.pp.networkproxybridge.ts +22 -10
  44. package/ts/smartproxy/classes.pp.portrangemanager.ts +0 -10
  45. package/ts/smartproxy/classes.smartproxy.ts +103 -195
@@ -1,7 +1,30 @@
1
1
  import * as plugins from '../plugins.js';
2
2
  import { IncomingMessage, ServerResponse } from 'http';
3
- import * as fs from 'fs';
4
- import * as path from 'path';
3
+ import { Port80HandlerEvents } from '../common/types.js';
4
+ import type {
5
+ IForwardConfig,
6
+ IDomainOptions,
7
+ ICertificateData,
8
+ ICertificateFailure,
9
+ ICertificateExpiring,
10
+ IAcmeOptions
11
+ } from '../common/types.js';
12
+ // (fs and path I/O moved to CertProvisioner)
13
+ // ACME HTTP-01 challenge handler storing tokens in memory (diskless)
14
+ class DisklessHttp01Handler {
15
+ private storage: Map<string, string>;
16
+ constructor(storage: Map<string, string>) { this.storage = storage; }
17
+ public getSupportedTypes(): string[] { return ['http-01']; }
18
+ public async prepare(ch: any): Promise<void> {
19
+ this.storage.set(ch.token, ch.keyAuthorization);
20
+ }
21
+ public async verify(ch: any): Promise<void> {
22
+ return;
23
+ }
24
+ public async cleanup(ch: any): Promise<void> {
25
+ this.storage.delete(ch.token);
26
+ }
27
+ }
5
28
 
6
29
  /**
7
30
  * Custom error classes for better error handling
@@ -31,24 +54,6 @@ export class ServerError extends Port80HandlerError {
31
54
  }
32
55
  }
33
56
 
34
- /**
35
- * Domain forwarding configuration
36
- */
37
- export interface IForwardConfig {
38
- ip: string;
39
- port: number;
40
- }
41
-
42
- /**
43
- * Domain configuration options
44
- */
45
- export interface IDomainOptions {
46
- domainName: string;
47
- sslRedirect: boolean; // if true redirects the request to port 443
48
- acmeMaintenance: boolean; // tries to always have a valid cert for this domain
49
- forward?: IForwardConfig; // forwards all http requests to that target
50
- acmeForward?: IForwardConfig; // forwards letsencrypt requests to this config
51
- }
52
57
 
53
58
  /**
54
59
  * Represents a domain configuration with certificate status information
@@ -59,8 +64,6 @@ interface IDomainCertificate {
59
64
  obtainingInProgress: boolean;
60
65
  certificate?: string;
61
66
  privateKey?: string;
62
- challengeToken?: string;
63
- challengeKeyAuthorization?: string;
64
67
  expiryDate?: Date;
65
68
  lastRenewalAttempt?: Date;
66
69
  }
@@ -68,59 +71,8 @@ interface IDomainCertificate {
68
71
  /**
69
72
  * Configuration options for the Port80Handler
70
73
  */
71
- interface IPort80HandlerOptions {
72
- port?: number;
73
- contactEmail?: string;
74
- useProduction?: boolean;
75
- renewThresholdDays?: number;
76
- httpsRedirectPort?: number;
77
- renewCheckIntervalHours?: number;
78
- enabled?: boolean; // Whether ACME is enabled at all
79
- autoRenew?: boolean; // Whether to automatically renew certificates
80
- certificateStore?: string; // Directory to store certificates
81
- skipConfiguredCerts?: boolean; // Skip domains that already have certificates
82
- }
74
+ // Port80Handler options moved to common types
83
75
 
84
- /**
85
- * Certificate data that can be emitted via events or set from outside
86
- */
87
- export interface ICertificateData {
88
- domain: string;
89
- certificate: string;
90
- privateKey: string;
91
- expiryDate: Date;
92
- }
93
-
94
- /**
95
- * Events emitted by the Port80Handler
96
- */
97
- export enum Port80HandlerEvents {
98
- CERTIFICATE_ISSUED = 'certificate-issued',
99
- CERTIFICATE_RENEWED = 'certificate-renewed',
100
- CERTIFICATE_FAILED = 'certificate-failed',
101
- CERTIFICATE_EXPIRING = 'certificate-expiring',
102
- MANAGER_STARTED = 'manager-started',
103
- MANAGER_STOPPED = 'manager-stopped',
104
- REQUEST_FORWARDED = 'request-forwarded',
105
- }
106
-
107
- /**
108
- * Certificate failure payload type
109
- */
110
- export interface ICertificateFailure {
111
- domain: string;
112
- error: string;
113
- isRenewal: boolean;
114
- }
115
-
116
- /**
117
- * Certificate expiry payload type
118
- */
119
- export interface ICertificateExpiring {
120
- domain: string;
121
- expiryDate: Date;
122
- daysRemaining: number;
123
- }
124
76
 
125
77
  /**
126
78
  * Port80Handler with ACME certificate management and request forwarding capabilities
@@ -128,18 +80,21 @@ export interface ICertificateExpiring {
128
80
  */
129
81
  export class Port80Handler extends plugins.EventEmitter {
130
82
  private domainCertificates: Map<string, IDomainCertificate>;
83
+ // In-memory storage for ACME HTTP-01 challenge tokens
84
+ private acmeHttp01Storage: Map<string, string> = new Map();
85
+ // SmartAcme instance for certificate management
86
+ private smartAcme: plugins.smartacme.SmartAcme | null = null;
131
87
  private server: plugins.http.Server | null = null;
132
- private acmeClient: plugins.acme.Client | null = null;
133
- private accountKey: string | null = null;
134
- private renewalTimer: NodeJS.Timeout | null = null;
88
+ // Renewal scheduling is handled externally by SmartProxy
89
+ // (Removed internal renewal timer)
135
90
  private isShuttingDown: boolean = false;
136
- private options: Required<IPort80HandlerOptions>;
91
+ private options: Required<IAcmeOptions>;
137
92
 
138
93
  /**
139
94
  * Creates a new Port80Handler
140
95
  * @param options Configuration options
141
96
  */
142
- constructor(options: IPort80HandlerOptions = {}) {
97
+ constructor(options: IAcmeOptions = {}) {
143
98
  super();
144
99
  this.domainCertificates = new Map<string, IDomainCertificate>();
145
100
 
@@ -148,13 +103,14 @@ export class Port80Handler extends plugins.EventEmitter {
148
103
  port: options.port ?? 80,
149
104
  contactEmail: options.contactEmail ?? 'admin@example.com',
150
105
  useProduction: options.useProduction ?? false, // Safer default: staging
151
- renewThresholdDays: options.renewThresholdDays ?? 10, // Changed to 10 days as per requirements
152
106
  httpsRedirectPort: options.httpsRedirectPort ?? 443,
153
- renewCheckIntervalHours: options.renewCheckIntervalHours ?? 24,
154
107
  enabled: options.enabled ?? true, // Enable by default
155
- autoRenew: options.autoRenew ?? true, // Auto-renew by default
156
- certificateStore: options.certificateStore ?? './certs', // Default store location
157
- skipConfiguredCerts: options.skipConfiguredCerts ?? false
108
+ certificateStore: options.certificateStore ?? './certs',
109
+ skipConfiguredCerts: options.skipConfiguredCerts ?? false,
110
+ renewThresholdDays: options.renewThresholdDays ?? 30,
111
+ renewCheckIntervalHours: options.renewCheckIntervalHours ?? 24,
112
+ autoRenew: options.autoRenew ?? true,
113
+ domainForwards: options.domainForwards ?? []
158
114
  };
159
115
  }
160
116
 
@@ -175,13 +131,20 @@ export class Port80Handler extends plugins.EventEmitter {
175
131
  console.log('Port80Handler is disabled, skipping start');
176
132
  return;
177
133
  }
134
+ // Initialize SmartAcme for ACME challenge management (diskless HTTP handler)
135
+ if (this.options.enabled) {
136
+ this.smartAcme = new plugins.smartacme.SmartAcme({
137
+ accountEmail: this.options.contactEmail,
138
+ certManager: new plugins.smartacme.MemoryCertManager(),
139
+ environment: this.options.useProduction ? 'production' : 'integration',
140
+ challengeHandlers: [ new DisklessHttp01Handler(this.acmeHttp01Storage) ],
141
+ challengePriority: ['http-01'],
142
+ });
143
+ await this.smartAcme.start();
144
+ }
178
145
 
179
146
  return new Promise((resolve, reject) => {
180
147
  try {
181
- // Load certificates from store if enabled
182
- if (this.options.certificateStore) {
183
- this.loadCertificatesFromStore();
184
- }
185
148
 
186
149
  this.server = plugins.http.createServer((req, res) => this.handleRequest(req, res));
187
150
 
@@ -197,7 +160,6 @@ export class Port80Handler extends plugins.EventEmitter {
197
160
 
198
161
  this.server.listen(this.options.port, () => {
199
162
  console.log(`Port80Handler is listening on port ${this.options.port}`);
200
- this.startRenewalTimer();
201
163
  this.emit(Port80HandlerEvents.MANAGER_STARTED, this.options.port);
202
164
 
203
165
  // Start certificate process for domains with acmeMaintenance enabled
@@ -234,11 +196,6 @@ export class Port80Handler extends plugins.EventEmitter {
234
196
 
235
197
  this.isShuttingDown = true;
236
198
 
237
- // Stop the renewal timer
238
- if (this.renewalTimer) {
239
- clearInterval(this.renewalTimer);
240
- this.renewalTimer = null;
241
- }
242
199
 
243
200
  return new Promise<void>((resolve) => {
244
201
  if (this.server) {
@@ -353,10 +310,7 @@ export class Port80Handler extends plugins.EventEmitter {
353
310
 
354
311
  console.log(`Certificate set for ${domain}`);
355
312
 
356
- // Save certificate to store if enabled
357
- if (this.options.certificateStore) {
358
- this.saveCertificateToStore(domain, certificate, privateKey);
359
- }
313
+ // (Persistence of certificates moved to CertProvisioner)
360
314
 
361
315
  // Emit certificate event
362
316
  this.emitCertificateEvent(Port80HandlerEvents.CERTIFICATE_ISSUED, {
@@ -391,134 +345,7 @@ export class Port80Handler extends plugins.EventEmitter {
391
345
  };
392
346
  }
393
347
 
394
- /**
395
- * Saves a certificate to the filesystem store
396
- * @param domain The domain for the certificate
397
- * @param certificate The certificate (PEM format)
398
- * @param privateKey The private key (PEM format)
399
- * @private
400
- */
401
- private saveCertificateToStore(domain: string, certificate: string, privateKey: string): void {
402
- // Skip if certificate store is not enabled
403
- if (!this.options.certificateStore) return;
404
-
405
- try {
406
- const storePath = this.options.certificateStore;
407
-
408
- // Ensure the directory exists
409
- if (!fs.existsSync(storePath)) {
410
- fs.mkdirSync(storePath, { recursive: true });
411
- console.log(`Created certificate store directory: ${storePath}`);
412
- }
413
-
414
- const certPath = path.join(storePath, `${domain}.cert.pem`);
415
- const keyPath = path.join(storePath, `${domain}.key.pem`);
416
-
417
- // Write certificate and private key files
418
- fs.writeFileSync(certPath, certificate);
419
- fs.writeFileSync(keyPath, privateKey);
420
-
421
- // Set secure permissions for private key
422
- try {
423
- fs.chmodSync(keyPath, 0o600);
424
- } catch (err) {
425
- console.log(`Warning: Could not set secure permissions on ${keyPath}`);
426
- }
427
-
428
- console.log(`Saved certificate for ${domain} to ${certPath}`);
429
- } catch (err) {
430
- console.error(`Error saving certificate for ${domain}:`, err);
431
- }
432
- }
433
348
 
434
- /**
435
- * Loads certificates from the certificate store
436
- * @private
437
- */
438
- private loadCertificatesFromStore(): void {
439
- if (!this.options.certificateStore) return;
440
-
441
- try {
442
- const storePath = this.options.certificateStore;
443
-
444
- // Ensure the directory exists
445
- if (!fs.existsSync(storePath)) {
446
- fs.mkdirSync(storePath, { recursive: true });
447
- console.log(`Created certificate store directory: ${storePath}`);
448
- return;
449
- }
450
-
451
- // Get list of certificate files
452
- const files = fs.readdirSync(storePath);
453
- const certFiles = files.filter(file => file.endsWith('.cert.pem'));
454
-
455
- // Load each certificate
456
- for (const certFile of certFiles) {
457
- const domain = certFile.replace('.cert.pem', '');
458
- const keyFile = `${domain}.key.pem`;
459
-
460
- // Skip if key file doesn't exist
461
- if (!files.includes(keyFile)) {
462
- console.log(`Warning: Found certificate for ${domain} but no key file`);
463
- continue;
464
- }
465
-
466
- // Skip if we should skip configured certs
467
- if (this.options.skipConfiguredCerts) {
468
- const domainInfo = this.domainCertificates.get(domain);
469
- if (domainInfo && domainInfo.certObtained) {
470
- console.log(`Skipping already configured certificate for ${domain}`);
471
- continue;
472
- }
473
- }
474
-
475
- // Load certificate and key
476
- try {
477
- const certificate = fs.readFileSync(path.join(storePath, certFile), 'utf8');
478
- const privateKey = fs.readFileSync(path.join(storePath, keyFile), 'utf8');
479
-
480
- // Extract expiry date
481
- let expiryDate: Date | undefined;
482
- try {
483
- const matches = certificate.match(/Not After\s*:\s*(.*?)(?:\n|$)/i);
484
- if (matches && matches[1]) {
485
- expiryDate = new Date(matches[1]);
486
- }
487
- } catch (err) {
488
- console.log(`Warning: Could not extract expiry date from certificate for ${domain}`);
489
- }
490
-
491
- // Check if domain is already registered
492
- let domainInfo = this.domainCertificates.get(domain);
493
- if (!domainInfo) {
494
- // Register domain if not already registered
495
- domainInfo = {
496
- options: {
497
- domainName: domain,
498
- sslRedirect: true,
499
- acmeMaintenance: true
500
- },
501
- certObtained: false,
502
- obtainingInProgress: false
503
- };
504
- this.domainCertificates.set(domain, domainInfo);
505
- }
506
-
507
- // Set certificate
508
- domainInfo.certificate = certificate;
509
- domainInfo.privateKey = privateKey;
510
- domainInfo.certObtained = true;
511
- domainInfo.expiryDate = expiryDate;
512
-
513
- console.log(`Loaded certificate for ${domain} from store, valid until ${expiryDate?.toISOString() || 'unknown'}`);
514
- } catch (err) {
515
- console.error(`Error loading certificate for ${domain}:`, err);
516
- }
517
- }
518
- } catch (err) {
519
- console.error('Error loading certificates from store:', err);
520
- }
521
- }
522
349
 
523
350
  /**
524
351
  * Check if a domain is a glob pattern
@@ -579,38 +406,6 @@ export class Port80Handler extends plugins.EventEmitter {
579
406
  }
580
407
  }
581
408
 
582
- /**
583
- * Lazy initialization of the ACME client
584
- * @returns An ACME client instance
585
- */
586
- private async getAcmeClient(): Promise<plugins.acme.Client> {
587
- if (this.acmeClient) {
588
- return this.acmeClient;
589
- }
590
-
591
- try {
592
- // Generate a new account key
593
- this.accountKey = (await plugins.acme.forge.createPrivateKey()).toString();
594
-
595
- this.acmeClient = new plugins.acme.Client({
596
- directoryUrl: this.options.useProduction
597
- ? plugins.acme.directory.letsencrypt.production
598
- : plugins.acme.directory.letsencrypt.staging,
599
- accountKey: this.accountKey,
600
- });
601
-
602
- // Create a new account
603
- await this.acmeClient.createAccount({
604
- termsOfServiceAgreed: true,
605
- contact: [`mailto:${this.options.contactEmail}`],
606
- });
607
-
608
- return this.acmeClient;
609
- } catch (error) {
610
- const message = error instanceof Error ? error.message : 'Unknown error initializing ACME client';
611
- throw new Port80HandlerError(`Failed to initialize ACME client: ${message}`);
612
- }
613
- }
614
409
 
615
410
  /**
616
411
  * Handles incoming HTTP requests
@@ -640,19 +435,32 @@ export class Port80Handler extends plugins.EventEmitter {
640
435
  const { domainInfo, pattern } = domainMatch;
641
436
  const options = domainInfo.options;
642
437
 
643
- // If the request is for an ACME HTTP-01 challenge, handle it
644
- if (req.url && req.url.startsWith('/.well-known/acme-challenge/') && (options.acmeMaintenance || options.acmeForward)) {
645
- // Check if we should forward ACME requests
438
+ // Handle ACME HTTP-01 challenge requests or forwarding
439
+ if (req.url && req.url.startsWith('/.well-known/acme-challenge/')) {
440
+ // Forward ACME requests if configured
646
441
  if (options.acmeForward) {
647
442
  this.forwardRequest(req, res, options.acmeForward, 'ACME challenge');
648
443
  return;
649
444
  }
650
-
651
- // Only handle ACME challenges for non-glob patterns
652
- if (!this.isGlobPattern(pattern)) {
653
- this.handleAcmeChallenge(req, res, domain);
445
+ // If not managing ACME for this domain, return 404
446
+ if (!options.acmeMaintenance) {
447
+ res.statusCode = 404;
448
+ res.end('Not found');
654
449
  return;
655
450
  }
451
+ // Serve challenge response from in-memory storage
452
+ const token = req.url.split('/').pop() || '';
453
+ const keyAuth = this.acmeHttp01Storage.get(token);
454
+ if (keyAuth) {
455
+ res.statusCode = 200;
456
+ res.setHeader('Content-Type', 'text/plain');
457
+ res.end(keyAuth);
458
+ console.log(`Served ACME challenge response for ${domain}`);
459
+ } else {
460
+ res.statusCode = 404;
461
+ res.end('Challenge token not found');
462
+ }
463
+ return;
656
464
  }
657
465
 
658
466
  // Check if we should forward non-ACME requests
@@ -762,292 +570,71 @@ export class Port80Handler extends plugins.EventEmitter {
762
570
  }
763
571
  }
764
572
 
765
- /**
766
- * Serves the ACME HTTP-01 challenge response
767
- * @param req The HTTP request
768
- * @param res The HTTP response
769
- * @param domain The domain for the challenge
770
- */
771
- private handleAcmeChallenge(req: plugins.http.IncomingMessage, res: plugins.http.ServerResponse, domain: string): void {
772
- const domainInfo = this.domainCertificates.get(domain);
773
- if (!domainInfo) {
774
- res.statusCode = 404;
775
- res.end('Domain not configured');
776
- return;
777
- }
778
-
779
- // The token is the last part of the URL
780
- const urlParts = req.url?.split('/');
781
- const token = urlParts ? urlParts[urlParts.length - 1] : '';
782
-
783
- if (domainInfo.challengeToken === token && domainInfo.challengeKeyAuthorization) {
784
- res.statusCode = 200;
785
- res.setHeader('Content-Type', 'text/plain');
786
- res.end(domainInfo.challengeKeyAuthorization);
787
- console.log(`Served ACME challenge response for ${domain}`);
788
- } else {
789
- res.statusCode = 404;
790
- res.end('Challenge token not found');
791
- }
792
- }
793
573
 
794
574
  /**
795
575
  * Obtains a certificate for a domain using ACME HTTP-01 challenge
796
576
  * @param domain The domain to obtain a certificate for
797
577
  * @param isRenewal Whether this is a renewal attempt
798
578
  */
579
+ /**
580
+ * Obtains a certificate for a domain using SmartAcme HTTP-01 challenges
581
+ * @param domain The domain to obtain a certificate for
582
+ * @param isRenewal Whether this is a renewal attempt
583
+ */
799
584
  private async obtainCertificate(domain: string, isRenewal: boolean = false): Promise<void> {
800
- // Don't allow certificate issuance for glob patterns
801
585
  if (this.isGlobPattern(domain)) {
802
586
  throw new CertificateError('Cannot obtain certificates for glob pattern domains', domain, isRenewal);
803
587
  }
804
-
805
- // Get the domain info
806
- const domainInfo = this.domainCertificates.get(domain);
807
- if (!domainInfo) {
808
- throw new CertificateError('Domain not found', domain, isRenewal);
809
- }
810
-
811
- // Verify that acmeMaintenance is enabled
588
+ const domainInfo = this.domainCertificates.get(domain)!;
812
589
  if (!domainInfo.options.acmeMaintenance) {
813
590
  console.log(`Skipping certificate issuance for ${domain} - acmeMaintenance is disabled`);
814
591
  return;
815
592
  }
816
-
817
- // Prevent concurrent certificate issuance
818
593
  if (domainInfo.obtainingInProgress) {
819
594
  console.log(`Certificate issuance already in progress for ${domain}`);
820
595
  return;
821
596
  }
822
-
597
+ if (!this.smartAcme) {
598
+ throw new Port80HandlerError('SmartAcme is not initialized');
599
+ }
823
600
  domainInfo.obtainingInProgress = true;
824
601
  domainInfo.lastRenewalAttempt = new Date();
825
-
826
602
  try {
827
- const client = await this.getAcmeClient();
828
-
829
- // Create a new order for the domain
830
- const order = await client.createOrder({
831
- identifiers: [{ type: 'dns', value: domain }],
832
- });
833
-
834
- // Get the authorizations for the order
835
- const authorizations = await client.getAuthorizations(order);
836
-
837
- // Process each authorization
838
- await this.processAuthorizations(client, domain, authorizations);
839
-
840
- // Generate a CSR and private key
841
- const [csrBuffer, privateKeyBuffer] = await plugins.acme.forge.createCsr({
842
- commonName: domain,
843
- });
844
-
845
- const csr = csrBuffer.toString();
846
- const privateKey = privateKeyBuffer.toString();
847
-
848
- // Finalize the order with our CSR
849
- await client.finalizeOrder(order, csr);
850
-
851
- // Get the certificate with the full chain
852
- const certificate = await client.getCertificate(order);
853
-
854
- // Store the certificate and key
603
+ // Request certificate via SmartAcme
604
+ const certObj = await this.smartAcme.getCertificateForDomain(domain);
605
+ const certificate = certObj.publicKey;
606
+ const privateKey = certObj.privateKey;
607
+ const expiryDate = new Date(certObj.validUntil);
855
608
  domainInfo.certificate = certificate;
856
609
  domainInfo.privateKey = privateKey;
857
610
  domainInfo.certObtained = true;
858
-
859
- // Clear challenge data
860
- delete domainInfo.challengeToken;
861
- delete domainInfo.challengeKeyAuthorization;
862
-
863
- // Extract expiry date from certificate
864
- domainInfo.expiryDate = this.extractExpiryDateFromCertificate(certificate, domain);
611
+ domainInfo.expiryDate = expiryDate;
865
612
 
866
613
  console.log(`Certificate ${isRenewal ? 'renewed' : 'obtained'} for ${domain}`);
867
-
868
- // Save the certificate to the store if enabled
869
- if (this.options.certificateStore) {
870
- this.saveCertificateToStore(domain, certificate, privateKey);
871
- }
872
-
873
- // Emit the appropriate event
874
- const eventType = isRenewal
875
- ? Port80HandlerEvents.CERTIFICATE_RENEWED
614
+ // Persistence moved to CertProvisioner
615
+ const eventType = isRenewal
616
+ ? Port80HandlerEvents.CERTIFICATE_RENEWED
876
617
  : Port80HandlerEvents.CERTIFICATE_ISSUED;
877
-
878
618
  this.emitCertificateEvent(eventType, {
879
619
  domain,
880
620
  certificate,
881
621
  privateKey,
882
- expiryDate: domainInfo.expiryDate || this.getDefaultExpiryDate()
622
+ expiryDate: expiryDate || this.getDefaultExpiryDate()
883
623
  });
884
-
885
624
  } catch (error: any) {
886
- // Check for rate limit errors
887
- if (error.message && (
888
- error.message.includes('rateLimited') ||
889
- error.message.includes('too many certificates') ||
890
- error.message.includes('rate limit')
891
- )) {
892
- console.error(`Rate limit reached for ${domain}. Waiting before retry.`);
893
- } else {
894
- console.error(`Error during certificate issuance for ${domain}:`, error);
895
- }
896
-
897
- // Emit failure event
625
+ const errorMsg = error?.message || 'Unknown error';
626
+ console.error(`Error during certificate issuance for ${domain}:`, error);
898
627
  this.emit(Port80HandlerEvents.CERTIFICATE_FAILED, {
899
628
  domain,
900
- error: error.message || 'Unknown error',
629
+ error: errorMsg,
901
630
  isRenewal
902
631
  } as ICertificateFailure);
903
-
904
- throw new CertificateError(
905
- error.message || 'Certificate issuance failed',
906
- domain,
907
- isRenewal
908
- );
632
+ throw new CertificateError(errorMsg, domain, isRenewal);
909
633
  } finally {
910
- // Reset flag whether successful or not
911
634
  domainInfo.obtainingInProgress = false;
912
635
  }
913
636
  }
914
637
 
915
- /**
916
- * Process ACME authorizations by verifying and completing challenges
917
- * @param client ACME client
918
- * @param domain Domain name
919
- * @param authorizations Authorizations to process
920
- */
921
- private async processAuthorizations(
922
- client: plugins.acme.Client,
923
- domain: string,
924
- authorizations: plugins.acme.Authorization[]
925
- ): Promise<void> {
926
- const domainInfo = this.domainCertificates.get(domain);
927
- if (!domainInfo) {
928
- throw new CertificateError('Domain not found during authorization', domain);
929
- }
930
-
931
- for (const authz of authorizations) {
932
- const challenge = authz.challenges.find(ch => ch.type === 'http-01');
933
- if (!challenge) {
934
- throw new CertificateError('HTTP-01 challenge not found', domain);
935
- }
936
-
937
- // Get the key authorization for the challenge
938
- const keyAuthorization = await client.getChallengeKeyAuthorization(challenge);
939
-
940
- // Store the challenge data
941
- domainInfo.challengeToken = challenge.token;
942
- domainInfo.challengeKeyAuthorization = keyAuthorization;
943
-
944
- // ACME client type definition workaround - use compatible approach
945
- // First check if challenge verification is needed
946
- const authzUrl = authz.url;
947
-
948
- try {
949
- // Check if authzUrl exists and perform verification
950
- if (authzUrl) {
951
- await client.verifyChallenge(authz, challenge);
952
- }
953
-
954
- // Complete the challenge
955
- await client.completeChallenge(challenge);
956
-
957
- // Wait for validation
958
- await client.waitForValidStatus(challenge);
959
- console.log(`HTTP-01 challenge completed for ${domain}`);
960
- } catch (error) {
961
- const errorMessage = error instanceof Error ? error.message : 'Unknown challenge error';
962
- console.error(`Challenge error for ${domain}:`, error);
963
- throw new CertificateError(`Challenge verification failed: ${errorMessage}`, domain);
964
- }
965
- }
966
- }
967
-
968
- /**
969
- * Starts the certificate renewal timer
970
- */
971
- private startRenewalTimer(): void {
972
- if (this.renewalTimer) {
973
- clearInterval(this.renewalTimer);
974
- }
975
-
976
- // Convert hours to milliseconds
977
- const checkInterval = this.options.renewCheckIntervalHours * 60 * 60 * 1000;
978
-
979
- this.renewalTimer = setInterval(() => this.checkForRenewals(), checkInterval);
980
-
981
- // Prevent the timer from keeping the process alive
982
- if (this.renewalTimer.unref) {
983
- this.renewalTimer.unref();
984
- }
985
-
986
- console.log(`Certificate renewal check scheduled every ${this.options.renewCheckIntervalHours} hours`);
987
- }
988
-
989
- /**
990
- * Checks for certificates that need renewal
991
- */
992
- private checkForRenewals(): void {
993
- if (this.isShuttingDown) {
994
- return;
995
- }
996
-
997
- // Skip renewal if auto-renewal is disabled
998
- if (this.options.autoRenew === false) {
999
- console.log('Auto-renewal is disabled, skipping certificate renewal check');
1000
- return;
1001
- }
1002
-
1003
- console.log('Checking for certificates that need renewal...');
1004
-
1005
- const now = new Date();
1006
- const renewThresholdMs = this.options.renewThresholdDays * 24 * 60 * 60 * 1000;
1007
-
1008
- for (const [domain, domainInfo] of this.domainCertificates.entries()) {
1009
- // Skip glob patterns
1010
- if (this.isGlobPattern(domain)) {
1011
- continue;
1012
- }
1013
-
1014
- // Skip domains with acmeMaintenance disabled
1015
- if (!domainInfo.options.acmeMaintenance) {
1016
- continue;
1017
- }
1018
-
1019
- // Skip domains without certificates or already in renewal
1020
- if (!domainInfo.certObtained || domainInfo.obtainingInProgress) {
1021
- continue;
1022
- }
1023
-
1024
- // Skip domains without expiry dates
1025
- if (!domainInfo.expiryDate) {
1026
- continue;
1027
- }
1028
-
1029
- const timeUntilExpiry = domainInfo.expiryDate.getTime() - now.getTime();
1030
-
1031
- // Check if certificate is near expiry
1032
- if (timeUntilExpiry <= renewThresholdMs) {
1033
- console.log(`Certificate for ${domain} expires soon, renewing...`);
1034
-
1035
- const daysRemaining = Math.ceil(timeUntilExpiry / (24 * 60 * 60 * 1000));
1036
-
1037
- this.emit(Port80HandlerEvents.CERTIFICATE_EXPIRING, {
1038
- domain,
1039
- expiryDate: domainInfo.expiryDate,
1040
- daysRemaining
1041
- } as ICertificateExpiring);
1042
-
1043
- // Start renewal process
1044
- this.obtainCertificate(domain, true).catch(err => {
1045
- const errorMessage = err instanceof Error ? err.message : 'Unknown error';
1046
- console.error(`Error renewing certificate for ${domain}:`, errorMessage);
1047
- });
1048
- }
1049
- }
1050
- }
1051
638
 
1052
639
  /**
1053
640
  * Extract expiry date from certificate using a more robust approach
@@ -1173,7 +760,19 @@ export class Port80Handler extends plugins.EventEmitter {
1173
760
  * Gets configuration details
1174
761
  * @returns Current configuration
1175
762
  */
1176
- public getConfig(): Required<IPort80HandlerOptions> {
763
+ public getConfig(): Required<IAcmeOptions> {
1177
764
  return { ...this.options };
1178
765
  }
766
+
767
+ /**
768
+ * Request a certificate renewal for a specific domain.
769
+ * @param domain The domain to renew.
770
+ */
771
+ public async renewCertificate(domain: string): Promise<void> {
772
+ if (!this.domainCertificates.has(domain)) {
773
+ throw new Port80HandlerError(`Domain not managed: ${domain}`);
774
+ }
775
+ // Trigger renewal via ACME
776
+ await this.obtainCertificate(domain, true);
777
+ }
1179
778
  }