@push.rocks/smartproxy 18.1.0 → 18.2.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 (52) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/certificate/certificate-manager.d.ts +150 -0
  3. package/dist_ts/certificate/certificate-manager.js +505 -0
  4. package/dist_ts/certificate/events/simplified-events.d.ts +56 -0
  5. package/dist_ts/certificate/events/simplified-events.js +13 -0
  6. package/dist_ts/certificate/models/certificate-errors.d.ts +69 -0
  7. package/dist_ts/certificate/models/certificate-errors.js +141 -0
  8. package/dist_ts/certificate/models/certificate-strategy.d.ts +60 -0
  9. package/dist_ts/certificate/models/certificate-strategy.js +73 -0
  10. package/dist_ts/certificate/simplified-certificate-manager.d.ts +150 -0
  11. package/dist_ts/certificate/simplified-certificate-manager.js +501 -0
  12. package/dist_ts/http/index.d.ts +1 -9
  13. package/dist_ts/http/index.js +5 -11
  14. package/dist_ts/plugins.d.ts +3 -1
  15. package/dist_ts/plugins.js +4 -2
  16. package/dist_ts/proxies/network-proxy/network-proxy.js +3 -1
  17. package/dist_ts/proxies/network-proxy/simplified-certificate-bridge.d.ts +48 -0
  18. package/dist_ts/proxies/network-proxy/simplified-certificate-bridge.js +76 -0
  19. package/dist_ts/proxies/network-proxy/websocket-handler.js +21 -7
  20. package/dist_ts/proxies/smart-proxy/cert-store.d.ts +10 -0
  21. package/dist_ts/proxies/smart-proxy/cert-store.js +70 -0
  22. package/dist_ts/proxies/smart-proxy/certificate-manager.d.ts +116 -0
  23. package/dist_ts/proxies/smart-proxy/certificate-manager.js +401 -0
  24. package/dist_ts/proxies/smart-proxy/legacy-smart-proxy.d.ts +168 -0
  25. package/dist_ts/proxies/smart-proxy/legacy-smart-proxy.js +642 -0
  26. package/dist_ts/proxies/smart-proxy/models/route-types.d.ts +26 -0
  27. package/dist_ts/proxies/smart-proxy/models/route-types.js +1 -1
  28. package/dist_ts/proxies/smart-proxy/models/simplified-smartproxy-config.d.ts +65 -0
  29. package/dist_ts/proxies/smart-proxy/models/simplified-smartproxy-config.js +31 -0
  30. package/dist_ts/proxies/smart-proxy/models/smartproxy-options.d.ts +102 -0
  31. package/dist_ts/proxies/smart-proxy/models/smartproxy-options.js +73 -0
  32. package/dist_ts/proxies/smart-proxy/network-proxy-bridge.d.ts +10 -44
  33. package/dist_ts/proxies/smart-proxy/network-proxy-bridge.js +66 -202
  34. package/dist_ts/proxies/smart-proxy/route-connection-handler.d.ts +4 -0
  35. package/dist_ts/proxies/smart-proxy/route-connection-handler.js +62 -2
  36. package/dist_ts/proxies/smart-proxy/simplified-smart-proxy.d.ts +41 -0
  37. package/dist_ts/proxies/smart-proxy/simplified-smart-proxy.js +132 -0
  38. package/dist_ts/proxies/smart-proxy/smart-proxy.d.ts +18 -13
  39. package/dist_ts/proxies/smart-proxy/smart-proxy.js +79 -196
  40. package/package.json +5 -3
  41. package/readme.plan.md +1405 -617
  42. package/ts/00_commitinfo_data.ts +1 -1
  43. package/ts/http/index.ts +5 -12
  44. package/ts/plugins.ts +4 -1
  45. package/ts/proxies/network-proxy/network-proxy.ts +3 -0
  46. package/ts/proxies/network-proxy/websocket-handler.ts +18 -6
  47. package/ts/proxies/smart-proxy/cert-store.ts +86 -0
  48. package/ts/proxies/smart-proxy/certificate-manager.ts +506 -0
  49. package/ts/proxies/smart-proxy/models/route-types.ts +33 -3
  50. package/ts/proxies/smart-proxy/network-proxy-bridge.ts +86 -239
  51. package/ts/proxies/smart-proxy/route-connection-handler.ts +74 -1
  52. package/ts/proxies/smart-proxy/smart-proxy.ts +105 -222
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@push.rocks/smartproxy',
6
- version: '18.1.0',
6
+ version: '18.2.0',
7
7
  description: 'A powerful proxy package with unified route-based configuration for high traffic management. Features include SSL/TLS support, flexible routing patterns, WebSocket handling, advanced security options, and automatic ACME certificate management.'
8
8
  }
package/ts/http/index.ts CHANGED
@@ -5,19 +5,12 @@
5
5
  // Export types and models
6
6
  export * from './models/http-types.js';
7
7
 
8
- // Export submodules
9
- export * from './port80/index.js';
8
+ // Export submodules (remove port80 export)
10
9
  export * from './router/index.js';
11
10
  export * from './redirects/index.js';
11
+ // REMOVED: export * from './port80/index.js';
12
12
 
13
- // Import the components we need for the namespace
14
- import { Port80Handler } from './port80/port80-handler.js';
15
- import { ChallengeResponder } from './port80/challenge-responder.js';
16
-
17
- // Convenience namespace exports
13
+ // Convenience namespace exports (no more Port80)
18
14
  export const Http = {
19
- Port80: {
20
- Handler: Port80Handler,
21
- ChallengeResponder: ChallengeResponder
22
- }
23
- };
15
+ // Only router and redirect functionality remain
16
+ };
package/ts/plugins.ts CHANGED
@@ -21,7 +21,8 @@ import * as smartdelay from '@push.rocks/smartdelay';
21
21
  import * as smartpromise from '@push.rocks/smartpromise';
22
22
  import * as smartrequest from '@push.rocks/smartrequest';
23
23
  import * as smartstring from '@push.rocks/smartstring';
24
-
24
+ import * as smartfile from '@push.rocks/smartfile';
25
+ import * as smartcrypto from '@push.rocks/smartcrypto';
25
26
  import * as smartacme from '@push.rocks/smartacme';
26
27
  import * as smartacmePlugins from '@push.rocks/smartacme/dist_ts/smartacme.plugins.js';
27
28
  import * as smartacmeHandlers from '@push.rocks/smartacme/dist_ts/handlers/index.js';
@@ -33,6 +34,8 @@ export {
33
34
  smartrequest,
34
35
  smartpromise,
35
36
  smartstring,
37
+ smartfile,
38
+ smartcrypto,
36
39
  smartacme,
37
40
  smartacmePlugins,
38
41
  smartacmeHandlers,
@@ -500,6 +500,9 @@ export class NetworkProxy implements IMetricsTracker {
500
500
  this.logger.warn('Router has no recognized configuration method');
501
501
  }
502
502
 
503
+ // Update WebSocket handler with new routes
504
+ this.webSocketHandler.setRoutes(routes);
505
+
503
506
  this.logger.info(`Route configuration updated with ${routes.length} routes and ${legacyConfigs.length} proxy configs`);
504
507
  }
505
508
 
@@ -419,9 +419,15 @@ export class WebSocketHandler {
419
419
  wsIncoming.on('close', (code, reason) => {
420
420
  this.logger.debug(`WebSocket client connection closed: ${code} ${reason}`);
421
421
  if (wsOutgoing.readyState === wsOutgoing.OPEN) {
422
- const validCode = code || 1000;
423
- const reasonString = toBuffer(reason).toString();
424
- wsOutgoing.close(validCode, reasonString);
422
+ // Ensure code is a valid WebSocket close code number
423
+ const validCode = typeof code === 'number' && code >= 1000 && code <= 4999 ? code : 1000;
424
+ try {
425
+ const reasonString = reason ? toBuffer(reason).toString() : '';
426
+ wsOutgoing.close(validCode, reasonString);
427
+ } catch (err) {
428
+ this.logger.error('Error closing wsOutgoing:', err);
429
+ wsOutgoing.close(validCode);
430
+ }
425
431
  }
426
432
 
427
433
  // Clean up timers
@@ -432,9 +438,15 @@ export class WebSocketHandler {
432
438
  wsOutgoing.on('close', (code, reason) => {
433
439
  this.logger.debug(`WebSocket target connection closed: ${code} ${reason}`);
434
440
  if (wsIncoming.readyState === wsIncoming.OPEN) {
435
- const validCode = code || 1000;
436
- const reasonString = toBuffer(reason).toString();
437
- wsIncoming.close(validCode, reasonString);
441
+ // Ensure code is a valid WebSocket close code number
442
+ const validCode = typeof code === 'number' && code >= 1000 && code <= 4999 ? code : 1000;
443
+ try {
444
+ const reasonString = reason ? toBuffer(reason).toString() : '';
445
+ wsIncoming.close(validCode, reasonString);
446
+ } catch (err) {
447
+ this.logger.error('Error closing wsIncoming:', err);
448
+ wsIncoming.close(validCode);
449
+ }
438
450
  }
439
451
 
440
452
  // Clean up timers
@@ -0,0 +1,86 @@
1
+ import * as plugins from '../../plugins.js';
2
+ import type { ICertificateData } from './certificate-manager.js';
3
+
4
+ export class CertStore {
5
+ constructor(private certDir: string) {}
6
+
7
+ public async initialize(): Promise<void> {
8
+ await plugins.smartfile.fs.ensureDirSync(this.certDir);
9
+ }
10
+
11
+ public async getCertificate(routeName: string): Promise<ICertificateData | null> {
12
+ const certPath = this.getCertPath(routeName);
13
+ const metaPath = `${certPath}/meta.json`;
14
+
15
+ if (!await plugins.smartfile.fs.fileExistsSync(metaPath)) {
16
+ return null;
17
+ }
18
+
19
+ try {
20
+ const metaFile = await plugins.smartfile.SmartFile.fromFilePath(metaPath);
21
+ const meta = JSON.parse(metaFile.contents.toString());
22
+
23
+ const certFile = await plugins.smartfile.SmartFile.fromFilePath(`${certPath}/cert.pem`);
24
+ const cert = certFile.contents.toString();
25
+
26
+ const keyFile = await plugins.smartfile.SmartFile.fromFilePath(`${certPath}/key.pem`);
27
+ const key = keyFile.contents.toString();
28
+
29
+ let ca: string | undefined;
30
+ const caPath = `${certPath}/ca.pem`;
31
+ if (await plugins.smartfile.fs.fileExistsSync(caPath)) {
32
+ const caFile = await plugins.smartfile.SmartFile.fromFilePath(caPath);
33
+ ca = caFile.contents.toString();
34
+ }
35
+
36
+ return {
37
+ cert,
38
+ key,
39
+ ca,
40
+ expiryDate: new Date(meta.expiryDate),
41
+ issueDate: new Date(meta.issueDate)
42
+ };
43
+ } catch (error) {
44
+ console.error(`Failed to load certificate for ${routeName}: ${error}`);
45
+ return null;
46
+ }
47
+ }
48
+
49
+ public async saveCertificate(
50
+ routeName: string,
51
+ certData: ICertificateData
52
+ ): Promise<void> {
53
+ const certPath = this.getCertPath(routeName);
54
+ await plugins.smartfile.fs.ensureDirSync(certPath);
55
+
56
+ // Save certificate files
57
+ await plugins.smartfile.memory.toFs(certData.cert, `${certPath}/cert.pem`);
58
+ await plugins.smartfile.memory.toFs(certData.key, `${certPath}/key.pem`);
59
+
60
+ if (certData.ca) {
61
+ await plugins.smartfile.memory.toFs(certData.ca, `${certPath}/ca.pem`);
62
+ }
63
+
64
+ // Save metadata
65
+ const meta = {
66
+ expiryDate: certData.expiryDate.toISOString(),
67
+ issueDate: certData.issueDate.toISOString(),
68
+ savedAt: new Date().toISOString()
69
+ };
70
+
71
+ await plugins.smartfile.memory.toFs(JSON.stringify(meta, null, 2), `${certPath}/meta.json`);
72
+ }
73
+
74
+ public async deleteCertificate(routeName: string): Promise<void> {
75
+ const certPath = this.getCertPath(routeName);
76
+ if (await plugins.smartfile.fs.fileExistsSync(certPath)) {
77
+ await plugins.smartfile.fs.removeManySync([certPath]);
78
+ }
79
+ }
80
+
81
+ private getCertPath(routeName: string): string {
82
+ // Sanitize route name for filesystem
83
+ const safeName = routeName.replace(/[^a-zA-Z0-9-_]/g, '_');
84
+ return `${this.certDir}/${safeName}`;
85
+ }
86
+ }
@@ -0,0 +1,506 @@
1
+ import * as plugins from '../../plugins.js';
2
+ import { NetworkProxy } from '../network-proxy/index.js';
3
+ import type { IRouteConfig, IRouteTls } from './models/route-types.js';
4
+ import { CertStore } from './cert-store.js';
5
+
6
+ export interface ICertStatus {
7
+ domain: string;
8
+ status: 'valid' | 'pending' | 'expired' | 'error';
9
+ expiryDate?: Date;
10
+ issueDate?: Date;
11
+ source: 'static' | 'acme';
12
+ error?: string;
13
+ }
14
+
15
+ export interface ICertificateData {
16
+ cert: string;
17
+ key: string;
18
+ ca?: string;
19
+ expiryDate: Date;
20
+ issueDate: Date;
21
+ }
22
+
23
+ export class SmartCertManager {
24
+ private certStore: CertStore;
25
+ private smartAcme: plugins.smartacme.SmartAcme | null = null;
26
+ private networkProxy: NetworkProxy | null = null;
27
+ private renewalTimer: NodeJS.Timeout | null = null;
28
+ private pendingChallenges: Map<string, string> = new Map();
29
+ private challengeRoute: IRouteConfig | null = null;
30
+
31
+ // Track certificate status by route name
32
+ private certStatus: Map<string, ICertStatus> = new Map();
33
+
34
+ // Callback to update SmartProxy routes for challenges
35
+ private updateRoutesCallback?: (routes: IRouteConfig[]) => Promise<void>;
36
+
37
+ constructor(
38
+ private routes: IRouteConfig[],
39
+ private certDir: string = './certs',
40
+ private acmeOptions?: {
41
+ email?: string;
42
+ useProduction?: boolean;
43
+ port?: number;
44
+ }
45
+ ) {
46
+ this.certStore = new CertStore(certDir);
47
+ }
48
+
49
+ public setNetworkProxy(networkProxy: NetworkProxy): void {
50
+ this.networkProxy = networkProxy;
51
+ }
52
+
53
+ /**
54
+ * Set callback for updating routes (used for challenge routes)
55
+ */
56
+ public setUpdateRoutesCallback(callback: (routes: IRouteConfig[]) => Promise<void>): void {
57
+ this.updateRoutesCallback = callback;
58
+ }
59
+
60
+ /**
61
+ * Initialize certificate manager and provision certificates for all routes
62
+ */
63
+ public async initialize(): Promise<void> {
64
+ // Create certificate directory if it doesn't exist
65
+ await this.certStore.initialize();
66
+
67
+ // Initialize SmartAcme if we have any ACME routes
68
+ const hasAcmeRoutes = this.routes.some(r =>
69
+ r.action.tls?.certificate === 'auto'
70
+ );
71
+
72
+ if (hasAcmeRoutes && this.acmeOptions?.email) {
73
+ // Create HTTP-01 challenge handler
74
+ const http01Handler = new plugins.smartacme.handlers.Http01MemoryHandler();
75
+
76
+ // Set up challenge handler integration with our routing
77
+ this.setupChallengeHandler(http01Handler);
78
+
79
+ // Create SmartAcme instance with built-in MemoryCertManager and HTTP-01 handler
80
+ this.smartAcme = new plugins.smartacme.SmartAcme({
81
+ accountEmail: this.acmeOptions.email,
82
+ environment: this.acmeOptions.useProduction ? 'production' : 'integration',
83
+ certManager: new plugins.smartacme.certmanagers.MemoryCertManager(),
84
+ challengeHandlers: [http01Handler]
85
+ });
86
+
87
+ await this.smartAcme.start();
88
+ }
89
+
90
+ // Provision certificates for all routes
91
+ await this.provisionAllCertificates();
92
+
93
+ // Start renewal timer
94
+ this.startRenewalTimer();
95
+ }
96
+
97
+ /**
98
+ * Provision certificates for all routes that need them
99
+ */
100
+ private async provisionAllCertificates(): Promise<void> {
101
+ const certRoutes = this.routes.filter(r =>
102
+ r.action.tls?.mode === 'terminate' ||
103
+ r.action.tls?.mode === 'terminate-and-reencrypt'
104
+ );
105
+
106
+ for (const route of certRoutes) {
107
+ try {
108
+ await this.provisionCertificate(route);
109
+ } catch (error) {
110
+ console.error(`Failed to provision certificate for route ${route.name}: ${error}`);
111
+ }
112
+ }
113
+ }
114
+
115
+ /**
116
+ * Provision certificate for a single route
117
+ */
118
+ public async provisionCertificate(route: IRouteConfig): Promise<void> {
119
+ const tls = route.action.tls;
120
+ if (!tls || (tls.mode !== 'terminate' && tls.mode !== 'terminate-and-reencrypt')) {
121
+ return;
122
+ }
123
+
124
+ const domains = this.extractDomainsFromRoute(route);
125
+ if (domains.length === 0) {
126
+ console.warn(`Route ${route.name} has TLS termination but no domains`);
127
+ return;
128
+ }
129
+
130
+ const primaryDomain = domains[0];
131
+
132
+ if (tls.certificate === 'auto') {
133
+ // ACME certificate
134
+ await this.provisionAcmeCertificate(route, domains);
135
+ } else if (typeof tls.certificate === 'object') {
136
+ // Static certificate
137
+ await this.provisionStaticCertificate(route, primaryDomain, tls.certificate);
138
+ }
139
+ }
140
+
141
+ /**
142
+ * Provision ACME certificate
143
+ */
144
+ private async provisionAcmeCertificate(
145
+ route: IRouteConfig,
146
+ domains: string[]
147
+ ): Promise<void> {
148
+ if (!this.smartAcme) {
149
+ throw new Error('SmartAcme not initialized');
150
+ }
151
+
152
+ const primaryDomain = domains[0];
153
+ const routeName = route.name || primaryDomain;
154
+
155
+ // Check if we already have a valid certificate
156
+ const existingCert = await this.certStore.getCertificate(routeName);
157
+ if (existingCert && this.isCertificateValid(existingCert)) {
158
+ console.log(`Using existing valid certificate for ${primaryDomain}`);
159
+ await this.applyCertificate(primaryDomain, existingCert);
160
+ this.updateCertStatus(routeName, 'valid', 'acme', existingCert);
161
+ return;
162
+ }
163
+
164
+ console.log(`Requesting ACME certificate for ${domains.join(', ')}`);
165
+ this.updateCertStatus(routeName, 'pending', 'acme');
166
+
167
+ try {
168
+ // Add challenge route before requesting certificate
169
+ await this.addChallengeRoute();
170
+
171
+ try {
172
+ // Use smartacme to get certificate
173
+ const cert = await this.smartAcme.getCertificateForDomain(primaryDomain);
174
+
175
+ // SmartAcme's Cert object has these properties:
176
+ // - publicKey: The certificate PEM string
177
+ // - privateKey: The private key PEM string
178
+ // - csr: Certificate signing request
179
+ // - validUntil: Timestamp in milliseconds
180
+ // - domainName: The domain name
181
+ const certData: ICertificateData = {
182
+ cert: cert.publicKey,
183
+ key: cert.privateKey,
184
+ ca: cert.publicKey, // Use same as cert for now
185
+ expiryDate: new Date(cert.validUntil),
186
+ issueDate: new Date(cert.created)
187
+ };
188
+
189
+ await this.certStore.saveCertificate(routeName, certData);
190
+ await this.applyCertificate(primaryDomain, certData);
191
+ this.updateCertStatus(routeName, 'valid', 'acme', certData);
192
+
193
+ console.log(`Successfully provisioned ACME certificate for ${primaryDomain}`);
194
+ } catch (error) {
195
+ console.error(`Failed to provision ACME certificate for ${primaryDomain}: ${error}`);
196
+ this.updateCertStatus(routeName, 'error', 'acme', undefined, error.message);
197
+ throw error;
198
+ } finally {
199
+ // Always remove challenge route after provisioning
200
+ await this.removeChallengeRoute();
201
+ }
202
+ } catch (error) {
203
+ // Handle outer try-catch from adding challenge route
204
+ console.error(`Failed to setup ACME challenge for ${primaryDomain}: ${error}`);
205
+ this.updateCertStatus(routeName, 'error', 'acme', undefined, error.message);
206
+ throw error;
207
+ }
208
+ }
209
+
210
+ /**
211
+ * Provision static certificate
212
+ */
213
+ private async provisionStaticCertificate(
214
+ route: IRouteConfig,
215
+ domain: string,
216
+ certConfig: { key: string; cert: string; keyFile?: string; certFile?: string }
217
+ ): Promise<void> {
218
+ const routeName = route.name || domain;
219
+
220
+ try {
221
+ let key: string = certConfig.key;
222
+ let cert: string = certConfig.cert;
223
+
224
+ // Load from files if paths are provided
225
+ if (certConfig.keyFile) {
226
+ const keyFile = await plugins.smartfile.SmartFile.fromFilePath(certConfig.keyFile);
227
+ key = keyFile.contents.toString();
228
+ }
229
+ if (certConfig.certFile) {
230
+ const certFile = await plugins.smartfile.SmartFile.fromFilePath(certConfig.certFile);
231
+ cert = certFile.contents.toString();
232
+ }
233
+
234
+ // Parse certificate to get dates
235
+ // Parse certificate to get dates - for now just use defaults
236
+ // TODO: Implement actual certificate parsing if needed
237
+ const certInfo = { validTo: new Date(Date.now() + 90 * 24 * 60 * 60 * 1000), validFrom: new Date() };
238
+
239
+ const certData: ICertificateData = {
240
+ cert,
241
+ key,
242
+ expiryDate: certInfo.validTo,
243
+ issueDate: certInfo.validFrom
244
+ };
245
+
246
+ // Save to store for consistency
247
+ await this.certStore.saveCertificate(routeName, certData);
248
+ await this.applyCertificate(domain, certData);
249
+ this.updateCertStatus(routeName, 'valid', 'static', certData);
250
+
251
+ console.log(`Successfully loaded static certificate for ${domain}`);
252
+ } catch (error) {
253
+ console.error(`Failed to provision static certificate for ${domain}: ${error}`);
254
+ this.updateCertStatus(routeName, 'error', 'static', undefined, error.message);
255
+ throw error;
256
+ }
257
+ }
258
+
259
+ /**
260
+ * Apply certificate to NetworkProxy
261
+ */
262
+ private async applyCertificate(domain: string, certData: ICertificateData): Promise<void> {
263
+ if (!this.networkProxy) {
264
+ console.warn('NetworkProxy not set, cannot apply certificate');
265
+ return;
266
+ }
267
+
268
+ // Apply certificate to NetworkProxy
269
+ this.networkProxy.updateCertificate(domain, certData.cert, certData.key);
270
+
271
+ // Also apply for wildcard if it's a subdomain
272
+ if (domain.includes('.') && !domain.startsWith('*.')) {
273
+ const parts = domain.split('.');
274
+ if (parts.length >= 2) {
275
+ const wildcardDomain = `*.${parts.slice(-2).join('.')}`;
276
+ this.networkProxy.updateCertificate(wildcardDomain, certData.cert, certData.key);
277
+ }
278
+ }
279
+ }
280
+
281
+ /**
282
+ * Extract domains from route configuration
283
+ */
284
+ private extractDomainsFromRoute(route: IRouteConfig): string[] {
285
+ if (!route.match.domains) {
286
+ return [];
287
+ }
288
+
289
+ const domains = Array.isArray(route.match.domains)
290
+ ? route.match.domains
291
+ : [route.match.domains];
292
+
293
+ // Filter out wildcards and patterns
294
+ return domains.filter(d =>
295
+ !d.includes('*') &&
296
+ !d.includes('{') &&
297
+ d.includes('.')
298
+ );
299
+ }
300
+
301
+ /**
302
+ * Check if certificate is valid
303
+ */
304
+ private isCertificateValid(cert: ICertificateData): boolean {
305
+ const now = new Date();
306
+ const expiryThreshold = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000); // 30 days
307
+
308
+ return cert.expiryDate > expiryThreshold;
309
+ }
310
+
311
+
312
+ /**
313
+ * Add challenge route to SmartProxy
314
+ */
315
+ private async addChallengeRoute(): Promise<void> {
316
+ if (!this.updateRoutesCallback) {
317
+ throw new Error('No route update callback set');
318
+ }
319
+
320
+ if (!this.challengeRoute) {
321
+ throw new Error('Challenge route not initialized');
322
+ }
323
+ const challengeRoute = this.challengeRoute;
324
+
325
+ const updatedRoutes = [...this.routes, challengeRoute];
326
+ await this.updateRoutesCallback(updatedRoutes);
327
+ }
328
+
329
+ /**
330
+ * Remove challenge route from SmartProxy
331
+ */
332
+ private async removeChallengeRoute(): Promise<void> {
333
+ if (!this.updateRoutesCallback) {
334
+ return;
335
+ }
336
+
337
+ const filteredRoutes = this.routes.filter(r => r.name !== 'acme-challenge');
338
+ await this.updateRoutesCallback(filteredRoutes);
339
+ }
340
+
341
+ /**
342
+ * Start renewal timer
343
+ */
344
+ private startRenewalTimer(): void {
345
+ // Check for renewals every 12 hours
346
+ this.renewalTimer = setInterval(() => {
347
+ this.checkAndRenewCertificates();
348
+ }, 12 * 60 * 60 * 1000);
349
+
350
+ // Also do an immediate check
351
+ this.checkAndRenewCertificates();
352
+ }
353
+
354
+ /**
355
+ * Check and renew certificates that are expiring
356
+ */
357
+ private async checkAndRenewCertificates(): Promise<void> {
358
+ for (const route of this.routes) {
359
+ if (route.action.tls?.certificate === 'auto') {
360
+ const routeName = route.name || this.extractDomainsFromRoute(route)[0];
361
+ const cert = await this.certStore.getCertificate(routeName);
362
+
363
+ if (cert && !this.isCertificateValid(cert)) {
364
+ console.log(`Certificate for ${routeName} needs renewal`);
365
+ try {
366
+ await this.provisionCertificate(route);
367
+ } catch (error) {
368
+ console.error(`Failed to renew certificate for ${routeName}: ${error}`);
369
+ }
370
+ }
371
+ }
372
+ }
373
+ }
374
+
375
+ /**
376
+ * Update certificate status
377
+ */
378
+ private updateCertStatus(
379
+ routeName: string,
380
+ status: ICertStatus['status'],
381
+ source: ICertStatus['source'],
382
+ certData?: ICertificateData,
383
+ error?: string
384
+ ): void {
385
+ this.certStatus.set(routeName, {
386
+ domain: routeName,
387
+ status,
388
+ source,
389
+ expiryDate: certData?.expiryDate,
390
+ issueDate: certData?.issueDate,
391
+ error
392
+ });
393
+ }
394
+
395
+ /**
396
+ * Get certificate status for a route
397
+ */
398
+ public getCertificateStatus(routeName: string): ICertStatus | undefined {
399
+ return this.certStatus.get(routeName);
400
+ }
401
+
402
+ /**
403
+ * Force renewal of a certificate
404
+ */
405
+ public async renewCertificate(routeName: string): Promise<void> {
406
+ const route = this.routes.find(r => r.name === routeName);
407
+ if (!route) {
408
+ throw new Error(`Route ${routeName} not found`);
409
+ }
410
+
411
+ // Remove existing certificate to force renewal
412
+ await this.certStore.deleteCertificate(routeName);
413
+ await this.provisionCertificate(route);
414
+ }
415
+
416
+ /**
417
+ * Setup challenge handler integration with SmartProxy routing
418
+ */
419
+ private setupChallengeHandler(http01Handler: plugins.smartacme.handlers.Http01MemoryHandler): void {
420
+ // Create a challenge route that delegates to SmartAcme's HTTP-01 handler
421
+ const challengeRoute: IRouteConfig = {
422
+ name: 'acme-challenge',
423
+ priority: 1000, // High priority
424
+ match: {
425
+ ports: 80,
426
+ path: '/.well-known/acme-challenge/*'
427
+ },
428
+ action: {
429
+ type: 'static',
430
+ handler: async (context) => {
431
+ // Extract the token from the path
432
+ const token = context.path?.split('/').pop();
433
+ if (!token) {
434
+ return { status: 404, body: 'Not found' };
435
+ }
436
+
437
+ // Create mock request/response objects for SmartAcme
438
+ const mockReq = {
439
+ url: context.path,
440
+ method: 'GET',
441
+ headers: context.headers || {}
442
+ };
443
+
444
+ let responseData: any = null;
445
+ const mockRes = {
446
+ statusCode: 200,
447
+ setHeader: (name: string, value: string) => {},
448
+ end: (data: any) => {
449
+ responseData = data;
450
+ }
451
+ };
452
+
453
+ // Use SmartAcme's handler
454
+ const handled = await new Promise<boolean>((resolve) => {
455
+ http01Handler.handleRequest(mockReq as any, mockRes as any, () => {
456
+ resolve(false);
457
+ });
458
+ // Give it a moment to process
459
+ setTimeout(() => resolve(true), 100);
460
+ });
461
+
462
+ if (handled && responseData) {
463
+ return {
464
+ status: mockRes.statusCode,
465
+ headers: { 'Content-Type': 'text/plain' },
466
+ body: responseData
467
+ };
468
+ } else {
469
+ return { status: 404, body: 'Not found' };
470
+ }
471
+ }
472
+ }
473
+ };
474
+
475
+ // Store the challenge route to add it when needed
476
+ this.challengeRoute = challengeRoute;
477
+ }
478
+
479
+ /**
480
+ * Stop certificate manager
481
+ */
482
+ public async stop(): Promise<void> {
483
+ if (this.renewalTimer) {
484
+ clearInterval(this.renewalTimer);
485
+ this.renewalTimer = null;
486
+ }
487
+
488
+ if (this.smartAcme) {
489
+ await this.smartAcme.stop();
490
+ }
491
+
492
+ // Remove any active challenge routes
493
+ if (this.pendingChallenges.size > 0) {
494
+ this.pendingChallenges.clear();
495
+ await this.removeChallengeRoute();
496
+ }
497
+ }
498
+
499
+ /**
500
+ * Get ACME options (for recreating after route updates)
501
+ */
502
+ public getAcmeOptions(): { email?: string; useProduction?: boolean; port?: number } | undefined {
503
+ return this.acmeOptions;
504
+ }
505
+ }
506
+