@push.rocks/smartproxy 18.2.0 → 19.2.2

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 (63) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/common/eventUtils.d.ts +1 -2
  3. package/dist_ts/common/eventUtils.js +2 -1
  4. package/dist_ts/core/models/common-types.d.ts +1 -1
  5. package/dist_ts/core/models/common-types.js +1 -1
  6. package/dist_ts/core/utils/event-utils.d.ts +9 -9
  7. package/dist_ts/core/utils/event-utils.js +6 -14
  8. package/dist_ts/http/models/http-types.d.ts +13 -1
  9. package/dist_ts/http/models/http-types.js +1 -1
  10. package/dist_ts/index.d.ts +4 -6
  11. package/dist_ts/index.js +4 -10
  12. package/dist_ts/proxies/index.d.ts +3 -2
  13. package/dist_ts/proxies/index.js +4 -5
  14. package/dist_ts/proxies/network-proxy/certificate-manager.d.ts +31 -49
  15. package/dist_ts/proxies/network-proxy/certificate-manager.js +77 -374
  16. package/dist_ts/proxies/network-proxy/models/types.d.ts +12 -1
  17. package/dist_ts/proxies/network-proxy/models/types.js +1 -1
  18. package/dist_ts/proxies/network-proxy/network-proxy.d.ts +2 -7
  19. package/dist_ts/proxies/network-proxy/network-proxy.js +10 -19
  20. package/dist_ts/proxies/smart-proxy/certificate-manager.d.ts +6 -0
  21. package/dist_ts/proxies/smart-proxy/certificate-manager.js +24 -5
  22. package/dist_ts/proxies/smart-proxy/models/index.d.ts +1 -1
  23. package/dist_ts/proxies/smart-proxy/models/index.js +1 -5
  24. package/dist_ts/proxies/smart-proxy/models/interfaces.d.ts +30 -1
  25. package/dist_ts/proxies/smart-proxy/route-manager.d.ts +4 -0
  26. package/dist_ts/proxies/smart-proxy/route-manager.js +7 -1
  27. package/dist_ts/proxies/smart-proxy/smart-proxy.d.ts +4 -0
  28. package/dist_ts/proxies/smart-proxy/smart-proxy.js +112 -26
  29. package/package.json +1 -2
  30. package/readme.hints.md +31 -1
  31. package/readme.md +82 -6
  32. package/readme.plan.md +109 -1417
  33. package/ts/00_commitinfo_data.ts +1 -1
  34. package/ts/common/eventUtils.ts +2 -2
  35. package/ts/core/models/common-types.ts +1 -1
  36. package/ts/core/utils/event-utils.ts +12 -21
  37. package/ts/http/models/http-types.ts +8 -4
  38. package/ts/index.ts +11 -14
  39. package/ts/proxies/index.ts +7 -4
  40. package/ts/proxies/network-proxy/certificate-manager.ts +92 -417
  41. package/ts/proxies/network-proxy/models/types.ts +14 -2
  42. package/ts/proxies/network-proxy/network-proxy.ts +10 -19
  43. package/ts/proxies/smart-proxy/certificate-manager.ts +31 -4
  44. package/ts/proxies/smart-proxy/models/index.ts +2 -1
  45. package/ts/proxies/smart-proxy/models/interfaces.ts +31 -2
  46. package/ts/proxies/smart-proxy/models/route-types.ts +1 -1
  47. package/ts/proxies/smart-proxy/route-manager.ts +7 -0
  48. package/ts/proxies/smart-proxy/smart-proxy.ts +142 -25
  49. package/ts/certificate/acme/acme-factory.ts +0 -48
  50. package/ts/certificate/acme/challenge-handler.ts +0 -110
  51. package/ts/certificate/acme/index.ts +0 -3
  52. package/ts/certificate/events/certificate-events.ts +0 -36
  53. package/ts/certificate/index.ts +0 -75
  54. package/ts/certificate/models/certificate-types.ts +0 -109
  55. package/ts/certificate/providers/cert-provisioner.ts +0 -519
  56. package/ts/certificate/providers/index.ts +0 -3
  57. package/ts/certificate/storage/file-storage.ts +0 -234
  58. package/ts/certificate/storage/index.ts +0 -3
  59. package/ts/certificate/utils/certificate-helpers.ts +0 -50
  60. package/ts/http/port80/acme-interfaces.ts +0 -169
  61. package/ts/http/port80/challenge-responder.ts +0 -246
  62. package/ts/http/port80/index.ts +0 -13
  63. package/ts/http/port80/port80-handler.ts +0 -728
package/readme.plan.md CHANGED
@@ -1,1442 +1,134 @@
1
- # ACME/Certificate Simplification Plan for SmartProxy
2
-
3
- ## Command to reread CLAUDE.md
4
- `reread /home/philkunz/.claude/CLAUDE.md`
1
+ # SmartProxy ACME Simplification Plan
5
2
 
6
3
  ## Overview
7
- Simplify the ACME/Certificate system by consolidating components, removing unnecessary abstraction layers, and integrating directly into SmartProxy's route-based architecture.
8
-
9
- ## Core Principles
10
- 1. **No backward compatibility** - Clean break from legacy implementations
11
- 2. **No migration helpers** - Users must update to new configuration format
12
- 3. **Remove all legacy code** - Delete deprecated methods and interfaces
13
- 4. **Forward-only approach** - Focus on simplicity over compatibility
14
- 5. **No complexity for edge cases** - Only support the clean, new way
15
-
16
- ## Key Discoveries from Implementation Analysis
17
-
18
- 1. **SmartProxy already supports static routes** - The 'static' type exists in TRouteActionType
19
- 2. **Path-based routing works perfectly** - The route matching system handles paths with glob patterns
20
- 3. **Dynamic route updates are safe** - SmartProxy's updateRoutes() method handles changes gracefully
21
- 4. **Priority-based routing exists** - Routes are sorted by priority, ensuring ACME routes match first
22
- 5. **No separate HTTP server needed** - ACME challenges can be regular SmartProxy routes
23
-
24
- ## Current State Analysis
25
-
26
- ### Files to be Removed/Replaced
27
- ```
28
- ts/certificate/ (ENTIRE DIRECTORY TO BE REMOVED)
29
- ├── acme/
30
- │ ├── acme-factory.ts (28 lines)
31
- │ ├── challenge-handler.ts (227 lines)
32
- │ └── index.ts (2 lines)
33
- ├── events/
34
- │ └── certificate-events.ts (75 lines)
35
- ├── models/
36
- │ └── certificate-types.ts (168 lines)
37
- ├── providers/
38
- │ ├── cert-provisioner.ts (547 lines)
39
- │ └── index.ts (2 lines)
40
- ├── storage/
41
- │ ├── file-storage.ts (134 lines)
42
- │ └── index.ts (2 lines)
43
- ├── utils/
44
- │ └── certificate-helpers.ts (166 lines)
45
- └── index.ts (75 lines)
46
-
47
- ts/http/port80/ (ENTIRE SUBDIRECTORY TO BE REMOVED)
48
- ├── acme-interfaces.ts
49
- ├── challenge-responder.ts
50
- ├── port80-handler.ts
51
- └── index.ts
4
+ This plan addresses the certificate acquisition confusion in SmartProxy v19.0.0 and proposes simplifications to make ACME configuration more intuitive.
5
+
6
+ ## Current Issues
7
+ 1. ACME configuration placement is confusing (route-level vs top-level)
8
+ 2. SmartAcme initialization logic is complex and error-prone
9
+ 3. Documentation doesn't clearly explain the correct configuration format
10
+ 4. Error messages like "SmartAcme not initialized" are not helpful
11
+
12
+ ## Proposed Simplifications
13
+
14
+ ### 1. Support Both Configuration Styles
15
+ - [x] Reread CLAUDE.md before starting implementation
16
+ - [x] Accept ACME config at both top-level and route-level
17
+ - [x] Use top-level ACME config as defaults for all routes
18
+ - [x] Allow route-level ACME config to override top-level defaults
19
+ - [x] Make email field required when any route uses `certificate: 'auto'`
20
+
21
+ ### 2. Improve SmartAcme Initialization
22
+ - [x] Initialize SmartAcme when top-level ACME config exists with email
23
+ - [x] Initialize SmartAcme when any route has `certificate: 'auto'`
24
+ - [x] Provide clear error messages when initialization fails
25
+ - [x] Add debug logging for ACME initialization steps
26
+
27
+ ### 3. Simplify Certificate Configuration
28
+ - [x] Create helper method to validate ACME configuration
29
+ - [x] Auto-detect when port 80 is needed for challenges
30
+ - [x] Provide sensible defaults for ACME settings
31
+ - [x] Add configuration examples in documentation
32
+
33
+ ### 4. Update Documentation
34
+ - [x] Create clear examples for common ACME scenarios
35
+ - [x] Document the configuration hierarchy (top-level vs route-level)
36
+ - [x] Add troubleshooting guide for common certificate issues
37
+ - [x] Include migration guide from v18 to v19
38
+
39
+ ### 5. Add Configuration Helpers
40
+ - [x] Create `SmartProxyConfig.fromSimple()` helper for basic setups (part of validation)
41
+ - [x] Add validation for common misconfigurations
42
+ - [x] Provide warning messages for deprecated patterns
43
+ - [x] Include auto-correction suggestions
44
+
45
+ ## Implementation Steps
46
+
47
+ ### Phase 1: Configuration Support ✅
48
+ 1. ✅ Update ISmartProxyOptions interface to clarify ACME placement
49
+ 2. ✅ Modify SmartProxy constructor to handle top-level ACME config
50
+ 3. ✅ Update SmartCertManager to accept global ACME defaults
51
+ 4. ✅ Add configuration validation and helpful error messages
52
+
53
+ ### Phase 2: Testing ✅
54
+ 1. ✅ Add tests for both configuration styles
55
+ 2. ✅ Test ACME initialization with various configurations
56
+ 3. ✅ Verify certificate acquisition works in all scenarios
57
+ 4. ✅ Test error handling and messaging
58
+
59
+ ### Phase 3: Documentation ✅
60
+ 1. ✅ Update main README with clear ACME examples
61
+ 2. ✅ Create dedicated certificate-management.md guide
62
+ 3. ✅ Add migration guide for v18 to v19 users
63
+ 4. ✅ Include troubleshooting section
64
+
65
+ ## Example Simplified Configuration
52
66
 
53
- ts/http/ (KEEP OTHER SUBDIRECTORIES)
54
- ├── index.ts (UPDATE to remove port80 exports)
55
- ├── models/ (KEEP)
56
- ├── redirects/ (KEEP)
57
- ├── router/ (KEEP)
58
- └── utils/ (KEEP)
59
-
60
- ts/proxies/smart-proxy/
61
- └── network-proxy-bridge.ts (267 lines - to be simplified)
62
- ```
63
-
64
- ### Current Dependencies
65
- - @push.rocks/smartacme (ACME client)
66
- - @push.rocks/smartfile (file operations)
67
- - @push.rocks/smartcrypto (certificate operations)
68
- - @push.rocks/smartexpress (HTTP server for challenges)
69
-
70
- ## Detailed Implementation Plan
71
-
72
- ### Phase 1: Create SmartCertManager
73
-
74
- #### 1.1 Create certificate-manager.ts
75
67
  ```typescript
76
- // ts/proxies/smart-proxy/certificate-manager.ts
77
- import * as plugins from '../../plugins.js';
78
- import { NetworkProxy } from '../network-proxy/index.js';
79
- import type { IRouteConfig, IRouteTls } from './models/route-types.js';
80
- import { CertStore } from './cert-store.js';
81
- import { AcmeClient } from './acme-client.js';
82
-
83
- export interface ICertStatus {
84
- domain: string;
85
- status: 'valid' | 'pending' | 'expired' | 'error';
86
- expiryDate?: Date;
87
- issueDate?: Date;
88
- source: 'static' | 'acme';
89
- error?: string;
90
- }
91
-
92
- export interface ICertificateData {
93
- cert: string;
94
- key: string;
95
- ca?: string;
96
- expiryDate: Date;
97
- issueDate: Date;
98
- }
99
-
100
- export class SmartCertManager {
101
- private certStore: CertStore;
102
- private smartAcme: plugins.smartacme.SmartAcme | null = null;
103
- private networkProxy: NetworkProxy | null = null;
104
- private renewalTimer: NodeJS.Timer | null = null;
105
- private pendingChallenges: Map<string, string> = new Map();
106
-
107
- // Track certificate status by route name
108
- private certStatus: Map<string, ICertStatus> = new Map();
109
-
110
- // Callback to update SmartProxy routes for challenges
111
- private updateRoutesCallback?: (routes: IRouteConfig[]) => Promise<void>;
112
-
113
- constructor(
114
- private routes: IRouteConfig[],
115
- private certDir: string = './certs',
116
- private acmeOptions?: {
117
- email?: string;
118
- useProduction?: boolean;
119
- port?: number;
120
- }
121
- ) {
122
- this.certStore = new CertStore(certDir);
123
- }
124
-
125
- public setNetworkProxy(networkProxy: NetworkProxy): void {
126
- this.networkProxy = networkProxy;
127
- }
128
-
129
- /**
130
- * Set callback for updating routes (used for challenge routes)
131
- */
132
- public setUpdateRoutesCallback(callback: (routes: IRouteConfig[]) => Promise<void>): void {
133
- this.updateRoutesCallback = callback;
134
- }
135
-
136
- /**
137
- * Initialize certificate manager and provision certificates for all routes
138
- */
139
- public async initialize(): Promise<void> {
140
- // Create certificate directory if it doesn't exist
141
- await this.certStore.initialize();
142
-
143
- // Initialize SmartAcme if we have any ACME routes
144
- const hasAcmeRoutes = this.routes.some(r =>
145
- r.action.tls?.certificate === 'auto'
146
- );
147
-
148
- if (hasAcmeRoutes && this.acmeOptions?.email) {
149
- // Create SmartAcme instance with our challenge handler
150
- this.smartAcme = new plugins.smartacme.SmartAcme({
151
- accountEmail: this.acmeOptions.email,
152
- environment: this.acmeOptions.useProduction ? 'production' : 'staging',
153
- certManager: new InMemoryCertManager(), // Simple in-memory cert manager
154
- challengeHandlers: [{
155
- type: 'http-01',
156
- setChallenge: async (domain: string, token: string, keyAuth: string) => {
157
- await this.handleChallenge(token, keyAuth);
158
- },
159
- removeChallenge: async (domain: string, token: string) => {
160
- await this.cleanupChallenge(token);
161
- }
162
- }]
163
- });
164
-
165
- await this.smartAcme.start();
166
- }
167
-
168
- // Provision certificates for all routes
169
- await this.provisionAllCertificates();
170
-
171
- // Start renewal timer
172
- this.startRenewalTimer();
173
- }
174
-
175
- /**
176
- * Provision certificates for all routes that need them
177
- */
178
- private async provisionAllCertificates(): Promise<void> {
179
- const certRoutes = this.routes.filter(r =>
180
- r.action.tls?.mode === 'terminate' ||
181
- r.action.tls?.mode === 'terminate-and-reencrypt'
182
- );
183
-
184
- for (const route of certRoutes) {
185
- try {
186
- await this.provisionCertificate(route);
187
- } catch (error) {
188
- console.error(`Failed to provision certificate for route ${route.name}: ${error}`);
189
- }
190
- }
191
- }
192
-
193
- /**
194
- * Provision certificate for a single route
195
- */
196
- public async provisionCertificate(route: IRouteConfig): Promise<void> {
197
- const tls = route.action.tls;
198
- if (!tls || (tls.mode !== 'terminate' && tls.mode !== 'terminate-and-reencrypt')) {
199
- return;
200
- }
201
-
202
- const domains = this.extractDomainsFromRoute(route);
203
- if (domains.length === 0) {
204
- console.warn(`Route ${route.name} has TLS termination but no domains`);
205
- return;
206
- }
207
-
208
- const primaryDomain = domains[0];
209
-
210
- if (tls.certificate === 'auto') {
211
- // ACME certificate
212
- await this.provisionAcmeCertificate(route, domains);
213
- } else if (typeof tls.certificate === 'object') {
214
- // Static certificate
215
- await this.provisionStaticCertificate(route, primaryDomain, tls.certificate);
216
- }
217
- }
218
-
219
- /**
220
- * Provision ACME certificate
221
- */
222
- private async provisionAcmeCertificate(
223
- route: IRouteConfig,
224
- domains: string[]
225
- ): Promise<void> {
226
- if (!this.smartAcme) {
227
- throw new Error('SmartAcme not initialized');
228
- }
229
-
230
- const primaryDomain = domains[0];
231
- const routeName = route.name || primaryDomain;
232
-
233
- // Check if we already have a valid certificate
234
- const existingCert = await this.certStore.getCertificate(routeName);
235
- if (existingCert && this.isCertificateValid(existingCert)) {
236
- console.log(`Using existing valid certificate for ${primaryDomain}`);
237
- await this.applyCertificate(primaryDomain, existingCert);
238
- this.updateCertStatus(routeName, 'valid', 'acme', existingCert);
239
- return;
240
- }
241
-
242
- console.log(`Requesting ACME certificate for ${domains.join(', ')}`);
243
- this.updateCertStatus(routeName, 'pending', 'acme');
244
-
245
- try {
246
- // Use smartacme to get certificate
247
- const cert = await this.smartAcme.getCertificateForDomain(primaryDomain, {
248
- altNames: domains.slice(1)
249
- });
250
-
251
- // smartacme returns a Cert object with these properties
252
- const certData: ICertificateData = {
253
- cert: cert.cert,
254
- key: cert.privateKey,
255
- ca: cert.fullChain || cert.cert, // Use fullChain if available
256
- expiryDate: new Date(cert.validTo),
257
- issueDate: new Date(cert.validFrom)
258
- };
259
-
260
- await this.certStore.saveCertificate(routeName, certData);
261
- await this.applyCertificate(primaryDomain, certData);
262
- this.updateCertStatus(routeName, 'valid', 'acme', certData);
263
-
264
- console.log(`Successfully provisioned ACME certificate for ${primaryDomain}`);
265
- } catch (error) {
266
- console.error(`Failed to provision ACME certificate for ${primaryDomain}: ${error}`);
267
- this.updateCertStatus(routeName, 'error', 'acme', undefined, error.message);
268
- throw error;
269
- }
270
- }
271
-
272
- /**
273
- * Provision static certificate
274
- */
275
- private async provisionStaticCertificate(
276
- route: IRouteConfig,
277
- domain: string,
278
- certConfig: { key: string; cert: string; keyFile?: string; certFile?: string }
279
- ): Promise<void> {
280
- const routeName = route.name || domain;
281
-
282
- try {
283
- let key: string = certConfig.key;
284
- let cert: string = certConfig.cert;
285
-
286
- // Load from files if paths are provided
287
- if (certConfig.keyFile) {
288
- key = await plugins.smartfile.fs.readFileAsString(certConfig.keyFile);
289
- }
290
- if (certConfig.certFile) {
291
- cert = await plugins.smartfile.fs.readFileAsString(certConfig.certFile);
292
- }
293
-
294
- // Parse certificate to get dates
295
- const certInfo = await plugins.smartcrypto.cert.parseCert(cert);
296
-
297
- const certData: ICertificateData = {
298
- cert,
299
- key,
300
- expiryDate: certInfo.validTo,
301
- issueDate: certInfo.validFrom
302
- };
303
-
304
- // Save to store for consistency
305
- await this.certStore.saveCertificate(routeName, certData);
306
- await this.applyCertificate(domain, certData);
307
- this.updateCertStatus(routeName, 'valid', 'static', certData);
308
-
309
- console.log(`Successfully loaded static certificate for ${domain}`);
310
- } catch (error) {
311
- console.error(`Failed to provision static certificate for ${domain}: ${error}`);
312
- this.updateCertStatus(routeName, 'error', 'static', undefined, error.message);
313
- throw error;
314
- }
315
- }
316
-
317
- /**
318
- * Apply certificate to NetworkProxy
319
- */
320
- private async applyCertificate(domain: string, certData: ICertificateData): Promise<void> {
321
- if (!this.networkProxy) {
322
- console.warn('NetworkProxy not set, cannot apply certificate');
323
- return;
324
- }
325
-
326
- // Apply certificate to NetworkProxy
327
- this.networkProxy.updateCertificate(domain, certData.cert, certData.key);
328
-
329
- // Also apply for wildcard if it's a subdomain
330
- if (domain.includes('.') && !domain.startsWith('*.')) {
331
- const parts = domain.split('.');
332
- if (parts.length >= 2) {
333
- const wildcardDomain = `*.${parts.slice(-2).join('.')}`;
334
- this.networkProxy.updateCertificate(wildcardDomain, certData.cert, certData.key);
335
- }
336
- }
337
- }
338
-
339
- /**
340
- * Extract domains from route configuration
341
- */
342
- private extractDomainsFromRoute(route: IRouteConfig): string[] {
343
- if (!route.match.domains) {
344
- return [];
345
- }
346
-
347
- const domains = Array.isArray(route.match.domains)
348
- ? route.match.domains
349
- : [route.match.domains];
350
-
351
- // Filter out wildcards and patterns
352
- return domains.filter(d =>
353
- !d.includes('*') &&
354
- !d.includes('{') &&
355
- d.includes('.')
356
- );
357
- }
358
-
359
- /**
360
- * Check if certificate is valid
361
- */
362
- private isCertificateValid(cert: ICertificateData): boolean {
363
- const now = new Date();
364
- const expiryThreshold = new Date(now.getTime() + 30 * 24 * 60 * 60 * 1000); // 30 days
365
-
366
- return cert.expiryDate > expiryThreshold;
367
- }
368
-
369
- /**
370
- * Create ACME challenge route
371
- * NOTE: SmartProxy already handles path-based routing and priority
372
- */
373
- private createChallengeRoute(): IRouteConfig {
374
- return {
375
- name: 'acme-challenge',
376
- priority: 1000, // High priority to ensure it's checked first
377
- match: {
378
- ports: 80,
379
- path: '/.well-known/acme-challenge/*'
380
- },
68
+ // Simplified configuration with top-level ACME
69
+ const proxy = new SmartProxy({
70
+ // Global ACME settings (applies to all routes with certificate: 'auto')
71
+ acme: {
72
+ email: 'ssl@example.com',
73
+ useProduction: false,
74
+ port: 80 // Automatically listened on when needed
75
+ },
76
+
77
+ routes: [
78
+ {
79
+ name: 'secure-site',
80
+ match: { domains: 'example.com', ports: 443 },
381
81
  action: {
382
- type: 'static',
383
- handler: async (context) => {
384
- const token = context.path?.split('/').pop();
385
- const keyAuth = token ? this.pendingChallenges.get(token) : undefined;
386
-
387
- if (keyAuth) {
388
- return {
389
- status: 200,
390
- headers: { 'Content-Type': 'text/plain' },
391
- body: keyAuth
392
- };
393
- } else {
394
- return {
395
- status: 404,
396
- body: 'Not found'
397
- };
398
- }
399
- }
400
- }
401
- };
402
- }
403
-
404
- /**
405
- * Add challenge route to SmartProxy
406
- */
407
- private async addChallengeRoute(): Promise<void> {
408
- if (!this.updateRoutesCallback) {
409
- throw new Error('No route update callback set');
410
- }
411
-
412
- const challengeRoute = this.createChallengeRoute();
413
- const updatedRoutes = [...this.routes, challengeRoute];
414
-
415
- await this.updateRoutesCallback(updatedRoutes);
416
- }
417
-
418
- /**
419
- * Remove challenge route from SmartProxy
420
- */
421
- private async removeChallengeRoute(): Promise<void> {
422
- if (!this.updateRoutesCallback) {
423
- return;
424
- }
425
-
426
- const filteredRoutes = this.routes.filter(r => r.name !== 'acme-challenge');
427
- await this.updateRoutesCallback(filteredRoutes);
428
- }
429
-
430
- /**
431
- * Start renewal timer
432
- */
433
- private startRenewalTimer(): void {
434
- // Check for renewals every 12 hours
435
- this.renewalTimer = setInterval(() => {
436
- this.checkAndRenewCertificates();
437
- }, 12 * 60 * 60 * 1000);
438
-
439
- // Also do an immediate check
440
- this.checkAndRenewCertificates();
441
- }
442
-
443
- /**
444
- * Check and renew certificates that are expiring
445
- */
446
- private async checkAndRenewCertificates(): Promise<void> {
447
- for (const route of this.routes) {
448
- if (route.action.tls?.certificate === 'auto') {
449
- const routeName = route.name || this.extractDomainsFromRoute(route)[0];
450
- const cert = await this.certStore.getCertificate(routeName);
451
-
452
- if (cert && !this.isCertificateValid(cert)) {
453
- console.log(`Certificate for ${routeName} needs renewal`);
454
- try {
455
- await this.provisionCertificate(route);
456
- } catch (error) {
457
- console.error(`Failed to renew certificate for ${routeName}: ${error}`);
458
- }
459
- }
460
- }
461
- }
462
- }
463
-
464
- /**
465
- * Update certificate status
466
- */
467
- private updateCertStatus(
468
- routeName: string,
469
- status: ICertStatus['status'],
470
- source: ICertStatus['source'],
471
- certData?: ICertificateData,
472
- error?: string
473
- ): void {
474
- this.certStatus.set(routeName, {
475
- domain: routeName,
476
- status,
477
- source,
478
- expiryDate: certData?.expiryDate,
479
- issueDate: certData?.issueDate,
480
- error
481
- });
482
- }
483
-
484
- /**
485
- * Get certificate status for a route
486
- */
487
- public getCertificateStatus(routeName: string): ICertStatus | undefined {
488
- return this.certStatus.get(routeName);
489
- }
490
-
491
- /**
492
- * Force renewal of a certificate
493
- */
494
- public async renewCertificate(routeName: string): Promise<void> {
495
- const route = this.routes.find(r => r.name === routeName);
496
- if (!route) {
497
- throw new Error(`Route ${routeName} not found`);
498
- }
499
-
500
- // Remove existing certificate to force renewal
501
- await this.certStore.deleteCertificate(routeName);
502
- await this.provisionCertificate(route);
503
- }
504
-
505
- /**
506
- * Handle ACME challenge
507
- */
508
- private async handleChallenge(token: string, keyAuth: string): Promise<void> {
509
- this.pendingChallenges.set(token, keyAuth);
510
-
511
- // Add challenge route if it's the first challenge
512
- if (this.pendingChallenges.size === 1) {
513
- await this.addChallengeRoute();
514
- }
515
- }
516
-
517
- /**
518
- * Cleanup ACME challenge
519
- */
520
- private async cleanupChallenge(token: string): Promise<void> {
521
- this.pendingChallenges.delete(token);
522
-
523
- // Remove challenge route if no more challenges
524
- if (this.pendingChallenges.size === 0) {
525
- await this.removeChallengeRoute();
526
- }
527
- }
528
-
529
- /**
530
- * Stop certificate manager
531
- */
532
- public async stop(): Promise<void> {
533
- if (this.renewalTimer) {
534
- clearInterval(this.renewalTimer);
535
- this.renewalTimer = null;
536
- }
537
-
538
- if (this.smartAcme) {
539
- await this.smartAcme.stop();
540
- }
541
-
542
- // Remove any active challenge routes
543
- if (this.pendingChallenges.size > 0) {
544
- this.pendingChallenges.clear();
545
- await this.removeChallengeRoute();
546
- }
547
- }
548
-
549
- /**
550
- * Get ACME options (for recreating after route updates)
551
- */
552
- public getAcmeOptions(): { email?: string; useProduction?: boolean; port?: number } | undefined {
553
- return this.acmeOptions;
554
- }
555
- }
556
-
557
- /**
558
- * Simple in-memory certificate manager for SmartAcme
559
- * We only use this to satisfy SmartAcme's interface - actual storage is handled by CertStore
560
- */
561
- class InMemoryCertManager implements plugins.smartacme.CertManager {
562
- private store = new Map<string, any>();
563
-
564
- public async getCert(domain: string): Promise<any> {
565
- // SmartAcme uses this to check for existing certs
566
- // We return null to force it to always request new certs
567
- return null;
568
- }
569
-
570
- public async setCert(domain: string, certificate: any): Promise<void> {
571
- // SmartAcme calls this after getting a cert
572
- // We ignore it since we handle storage ourselves
573
- }
574
-
575
- public async removeCert(domain: string): Promise<void> {
576
- // Not needed for our use case
577
- }
578
- }
579
- ```
580
-
581
- #### 1.2 Create cert-store.ts
582
- ```typescript
583
- // ts/proxies/smart-proxy/cert-store.ts
584
- import * as plugins from '../../plugins.js';
585
- import type { ICertificateData } from './certificate-manager.js';
586
-
587
- export class CertStore {
588
- constructor(private certDir: string) {}
589
-
590
- public async initialize(): Promise<void> {
591
- await plugins.smartfile.fs.ensureDirectory(this.certDir);
592
- }
593
-
594
- public async getCertificate(routeName: string): Promise<ICertificateData | null> {
595
- const certPath = this.getCertPath(routeName);
596
- const metaPath = `${certPath}/meta.json`;
597
-
598
- if (!await plugins.smartfile.fs.fileExists(metaPath)) {
599
- return null;
600
- }
601
-
602
- try {
603
- const meta = await plugins.smartfile.fs.readJson(metaPath);
604
- const cert = await plugins.smartfile.fs.readFileAsString(`${certPath}/cert.pem`);
605
- const key = await plugins.smartfile.fs.readFileAsString(`${certPath}/key.pem`);
606
-
607
- let ca: string | undefined;
608
- const caPath = `${certPath}/ca.pem`;
609
- if (await plugins.smartfile.fs.fileExists(caPath)) {
610
- ca = await plugins.smartfile.fs.readFileAsString(caPath);
611
- }
612
-
613
- return {
614
- cert,
615
- key,
616
- ca,
617
- expiryDate: new Date(meta.expiryDate),
618
- issueDate: new Date(meta.issueDate)
619
- };
620
- } catch (error) {
621
- console.error(`Failed to load certificate for ${routeName}: ${error}`);
622
- return null;
623
- }
624
- }
625
-
626
- public async saveCertificate(
627
- routeName: string,
628
- certData: ICertificateData
629
- ): Promise<void> {
630
- const certPath = this.getCertPath(routeName);
631
- await plugins.smartfile.fs.ensureDirectory(certPath);
632
-
633
- // Save certificate files
634
- await plugins.smartfile.fs.writeFileAsString(
635
- `${certPath}/cert.pem`,
636
- certData.cert
637
- );
638
- await plugins.smartfile.fs.writeFileAsString(
639
- `${certPath}/key.pem`,
640
- certData.key
641
- );
642
-
643
- if (certData.ca) {
644
- await plugins.smartfile.fs.writeFileAsString(
645
- `${certPath}/ca.pem`,
646
- certData.ca
647
- );
648
- }
649
-
650
- // Save metadata
651
- const meta = {
652
- expiryDate: certData.expiryDate.toISOString(),
653
- issueDate: certData.issueDate.toISOString(),
654
- savedAt: new Date().toISOString()
655
- };
656
-
657
- await plugins.smartfile.fs.writeJson(`${certPath}/meta.json`, meta);
658
- }
659
-
660
- public async deleteCertificate(routeName: string): Promise<void> {
661
- const certPath = this.getCertPath(routeName);
662
- if (await plugins.smartfile.fs.fileExists(certPath)) {
663
- await plugins.smartfile.fs.removeDirectory(certPath);
664
- }
665
- }
666
-
667
- private getCertPath(routeName: string): string {
668
- // Sanitize route name for filesystem
669
- const safeName = routeName.replace(/[^a-zA-Z0-9-_]/g, '_');
670
- return `${this.certDir}/${safeName}`;
671
- }
672
- }
673
- ```
674
-
675
-
676
- ### Phase 2: Update Route Types and Handler
677
-
678
- #### 2.1 Update route-types.ts
679
- ```typescript
680
- // Add to ts/proxies/smart-proxy/models/route-types.ts
681
-
682
- /**
683
- * ACME configuration for automatic certificate provisioning
684
- */
685
- export interface IRouteAcme {
686
- email: string; // Contact email for ACME account
687
- useProduction?: boolean; // Use production ACME servers (default: false)
688
- challengePort?: number; // Port for HTTP-01 challenges (default: 80)
689
- renewBeforeDays?: number; // Days before expiry to renew (default: 30)
690
- }
691
-
692
- /**
693
- * Static route handler response
694
- */
695
- export interface IStaticResponse {
696
- status: number;
697
- headers?: Record<string, string>;
698
- body: string | Buffer;
699
- }
700
-
701
- /**
702
- * Update IRouteAction to support static handlers
703
- * NOTE: The 'static' type already exists in TRouteActionType
704
- */
705
- export interface IRouteAction {
706
- type: TRouteActionType;
707
- target?: IRouteTarget;
708
- security?: IRouteSecurity;
709
- options?: IRouteOptions;
710
- tls?: IRouteTls;
711
- redirect?: IRouteRedirect;
712
- handler?: (context: IRouteContext) => Promise<IStaticResponse>; // For static routes
713
- }
714
-
715
- /**
716
- * Extend IRouteConfig to ensure challenge routes have higher priority
717
- */
718
- export interface IRouteConfig {
719
- name?: string;
720
- match: IRouteMatch;
721
- action: IRouteAction;
722
- priority?: number; // Already exists - ACME routes should use high priority
723
- }
724
-
725
- /**
726
- * Extended TLS configuration for route actions
727
- */
728
- export interface IRouteTls {
729
- mode: TTlsMode;
730
- certificate?: 'auto' | { // Auto = use ACME
731
- key: string; // PEM-encoded private key
732
- cert: string; // PEM-encoded certificate
733
- ca?: string; // PEM-encoded CA chain
734
- keyFile?: string; // Path to key file (overrides key)
735
- certFile?: string; // Path to cert file (overrides cert)
736
- };
737
- acme?: IRouteAcme; // ACME options when certificate is 'auto'
738
- versions?: string[]; // Allowed TLS versions (e.g., ['TLSv1.2', 'TLSv1.3'])
739
- ciphers?: string; // OpenSSL cipher string
740
- honorCipherOrder?: boolean; // Use server's cipher preferences
741
- sessionTimeout?: number; // TLS session timeout in seconds
742
- }
743
- ```
744
-
745
- #### 2.2 Add Static Route Handler
746
- ```typescript
747
- // Add to ts/proxies/smart-proxy/route-connection-handler.ts
748
-
749
- /**
750
- * Handle the route based on its action type
751
- */
752
- switch (route.action.type) {
753
- case 'forward':
754
- return this.handleForwardAction(socket, record, route, initialChunk);
755
-
756
- case 'redirect':
757
- return this.handleRedirectAction(socket, record, route);
758
-
759
- case 'block':
760
- return this.handleBlockAction(socket, record, route);
761
-
762
- case 'static':
763
- return this.handleStaticAction(socket, record, route);
764
-
765
- default:
766
- console.log(`[${connectionId}] Unknown action type: ${(route.action as any).type}`);
767
- socket.end();
768
- this.connectionManager.cleanupConnection(record, 'unknown_action');
769
- }
770
-
771
- /**
772
- * Handle a static action for a route
773
- */
774
- private async handleStaticAction(
775
- socket: plugins.net.Socket,
776
- record: IConnectionRecord,
777
- route: IRouteConfig
778
- ): Promise<void> {
779
- const connectionId = record.id;
780
-
781
- if (!route.action.handler) {
782
- console.error(`[${connectionId}] Static route '${route.name}' has no handler`);
783
- socket.end();
784
- this.connectionManager.cleanupConnection(record, 'no_handler');
785
- return;
786
- }
787
-
788
- try {
789
- // Build route context
790
- const context: IRouteContext = {
791
- port: record.localPort,
792
- domain: record.lockedDomain,
793
- clientIp: record.remoteIP,
794
- serverIp: socket.localAddress!,
795
- path: record.path, // Will need to be extracted from HTTP request
796
- isTls: record.isTLS,
797
- tlsVersion: record.tlsVersion,
798
- routeName: route.name,
799
- routeId: route.name,
800
- timestamp: Date.now(),
801
- connectionId
802
- };
803
-
804
- // Call the handler
805
- const response = await route.action.handler(context);
806
-
807
- // Send HTTP response
808
- const headers = response.headers || {};
809
- headers['Content-Length'] = Buffer.byteLength(response.body).toString();
810
-
811
- let httpResponse = `HTTP/1.1 ${response.status} ${getStatusText(response.status)}\r\n`;
812
- for (const [key, value] of Object.entries(headers)) {
813
- httpResponse += `${key}: ${value}\r\n`;
814
- }
815
- httpResponse += '\r\n';
816
-
817
- socket.write(httpResponse);
818
- socket.write(response.body);
819
- socket.end();
820
-
821
- this.connectionManager.cleanupConnection(record, 'completed');
822
- } catch (error) {
823
- console.error(`[${connectionId}] Error in static handler: ${error}`);
824
- socket.end();
825
- this.connectionManager.cleanupConnection(record, 'handler_error');
826
- }
827
- }
828
-
829
- // Helper function for status text
830
- function getStatusText(status: number): string {
831
- const statusTexts: Record<number, string> = {
832
- 200: 'OK',
833
- 404: 'Not Found',
834
- 500: 'Internal Server Error'
835
- };
836
- return statusTexts[status] || 'Unknown';
837
- }
838
- ```
839
-
840
- ### Phase 3: SmartProxy Integration
841
-
842
- #### 3.1 Update SmartProxy class
843
- ```typescript
844
- // Changes to ts/proxies/smart-proxy/smart-proxy.ts
845
-
846
- import { SmartCertManager } from './certificate-manager.js';
847
- // Remove ALL certificate/ACME related imports:
848
- // - CertProvisioner
849
- // - Port80Handler
850
- // - buildPort80Handler
851
- // - createPort80HandlerOptions
852
-
853
- export class SmartProxy extends plugins.EventEmitter {
854
- // Replace certProvisioner and port80Handler with just:
855
- private certManager: SmartCertManager | null = null;
856
-
857
- constructor(settingsArg: ISmartProxyOptions) {
858
- super();
859
-
860
- // ... existing initialization ...
861
-
862
- // No need for ACME settings in ISmartProxyOptions anymore
863
- // Certificate configuration is now in route definitions
864
- }
865
-
866
- /**
867
- * Initialize certificate manager
868
- */
869
- private async initializeCertificateManager(): Promise<void> {
870
- // Extract global ACME options if any routes use auto certificates
871
- const autoRoutes = this.settings.routes.filter(r =>
872
- r.action.tls?.certificate === 'auto'
873
- );
874
-
875
- if (autoRoutes.length === 0 && !this.hasStaticCertRoutes()) {
876
- console.log('No routes require certificate management');
877
- return;
878
- }
879
-
880
- // Use the first auto route's ACME config as defaults
881
- const defaultAcme = autoRoutes[0]?.action.tls?.acme;
882
-
883
- this.certManager = new SmartCertManager(
884
- this.settings.routes,
885
- './certs', // Certificate directory
886
- defaultAcme ? {
887
- email: defaultAcme.email,
888
- useProduction: defaultAcme.useProduction,
889
- port: defaultAcme.challengePort || 80
890
- } : undefined
891
- );
892
-
893
- // Connect with NetworkProxy
894
- if (this.networkProxyBridge.getNetworkProxy()) {
895
- this.certManager.setNetworkProxy(this.networkProxyBridge.getNetworkProxy());
896
- }
897
-
898
- // Set route update callback for ACME challenges
899
- this.certManager.setUpdateRoutesCallback(async (routes) => {
900
- await this.updateRoutes(routes);
901
- });
902
-
903
- await this.certManager.initialize();
904
- }
905
-
906
- /**
907
- * Check if we have routes with static certificates
908
- */
909
- private hasStaticCertRoutes(): boolean {
910
- return this.settings.routes.some(r =>
911
- r.action.tls?.certificate &&
912
- r.action.tls.certificate !== 'auto'
913
- );
914
- }
915
-
916
- public async start() {
917
- if (this.isShuttingDown) {
918
- console.log("Cannot start SmartProxy while it's shutting down");
919
- return;
920
- }
921
-
922
- // Initialize certificate manager before starting servers
923
- await this.initializeCertificateManager();
924
-
925
- // Initialize and start NetworkProxy if needed
926
- if (this.settings.useNetworkProxy && this.settings.useNetworkProxy.length > 0) {
927
- await this.networkProxyBridge.initialize();
928
-
929
- // Connect NetworkProxy with certificate manager
930
- if (this.certManager) {
931
- this.certManager.setNetworkProxy(this.networkProxyBridge.getNetworkProxy());
932
- }
933
-
934
- await this.networkProxyBridge.start();
935
- }
936
-
937
- // ... rest of start method ...
938
- }
939
-
940
- public async stop() {
941
- console.log('SmartProxy shutting down...');
942
- this.isShuttingDown = true;
943
- this.portManager.setShuttingDown(true);
944
-
945
- // Stop certificate manager
946
- if (this.certManager) {
947
- await this.certManager.stop();
948
- console.log('Certificate manager stopped');
949
- }
950
-
951
- // ... rest of stop method ...
952
- }
953
-
954
- /**
955
- * Update routes with new configuration
956
- */
957
- public async updateRoutes(newRoutes: IRouteConfig[]): Promise<void> {
958
- console.log(`Updating routes (${newRoutes.length} routes)`);
959
-
960
- // Update certificate manager with new routes
961
- if (this.certManager) {
962
- await this.certManager.stop();
963
-
964
- this.certManager = new SmartCertManager(
965
- newRoutes,
966
- './certs',
967
- this.certManager.getAcmeOptions()
968
- );
969
-
970
- if (this.networkProxyBridge.getNetworkProxy()) {
971
- this.certManager.setNetworkProxy(this.networkProxyBridge.getNetworkProxy());
972
- }
973
-
974
- await this.certManager.initialize();
975
- }
976
-
977
- // ... rest of updateRoutes method ...
978
- }
979
-
980
- /**
981
- * Manually provision a certificate for a route
982
- */
983
- public async provisionCertificate(routeName: string): Promise<void> {
984
- if (!this.certManager) {
985
- throw new Error('Certificate manager not initialized');
986
- }
987
-
988
- const route = this.settings.routes.find(r => r.name === routeName);
989
- if (!route) {
990
- throw new Error(`Route ${routeName} not found`);
991
- }
992
-
993
- await this.certManager.provisionCertificate(route);
994
- }
995
-
996
- /**
997
- * Force renewal of a certificate
998
- */
999
- public async renewCertificate(routeName: string): Promise<void> {
1000
- if (!this.certManager) {
1001
- throw new Error('Certificate manager not initialized');
1002
- }
1003
-
1004
- await this.certManager.renewCertificate(routeName);
1005
- }
1006
-
1007
- /**
1008
- * Get certificate status for a route
1009
- */
1010
- public getCertificateStatus(routeName: string): ICertStatus | undefined {
1011
- if (!this.certManager) {
1012
- return undefined;
1013
- }
1014
-
1015
- return this.certManager.getCertificateStatus(routeName);
1016
- }
1017
- }
1018
- ```
1019
-
1020
- #### 3.2 Simplify NetworkProxyBridge
1021
- ```typescript
1022
- // Simplified ts/proxies/smart-proxy/network-proxy-bridge.ts
1023
-
1024
- import * as plugins from '../../plugins.js';
1025
- import { NetworkProxy } from '../network-proxy/index.js';
1026
- import type { IConnectionRecord, ISmartProxyOptions } from './models/interfaces.js';
1027
- import type { IRouteConfig } from './models/route-types.js';
1028
-
1029
- export class NetworkProxyBridge {
1030
- private networkProxy: NetworkProxy | null = null;
1031
-
1032
- constructor(private settings: ISmartProxyOptions) {}
1033
-
1034
- /**
1035
- * Get the NetworkProxy instance
1036
- */
1037
- public getNetworkProxy(): NetworkProxy | null {
1038
- return this.networkProxy;
1039
- }
1040
-
1041
- /**
1042
- * Initialize NetworkProxy instance
1043
- */
1044
- public async initialize(): Promise<void> {
1045
- if (!this.networkProxy && this.settings.useNetworkProxy && this.settings.useNetworkProxy.length > 0) {
1046
- const networkProxyOptions: any = {
1047
- port: this.settings.networkProxyPort!,
1048
- portProxyIntegration: true,
1049
- logLevel: this.settings.enableDetailedLogging ? 'debug' : 'info'
1050
- };
1051
-
1052
- this.networkProxy = new NetworkProxy(networkProxyOptions);
1053
- console.log(`Initialized NetworkProxy on port ${this.settings.networkProxyPort}`);
1054
-
1055
- // Apply route configurations to NetworkProxy
1056
- await this.syncRoutesToNetworkProxy(this.settings.routes || []);
1057
- }
1058
- }
1059
-
1060
- /**
1061
- * Sync routes to NetworkProxy
1062
- */
1063
- private async syncRoutesToNetworkProxy(routes: IRouteConfig[]): Promise<void> {
1064
- if (!this.networkProxy) return;
1065
-
1066
- // Convert routes to NetworkProxy format
1067
- const networkProxyConfigs = routes
1068
- .filter(route =>
1069
- this.settings.useNetworkProxy?.includes(route.match.domains?.[0]) ||
1070
- this.settings.useNetworkProxy?.includes('*')
1071
- )
1072
- .map(route => this.routeToNetworkProxyConfig(route));
1073
-
1074
- // Apply configurations to NetworkProxy
1075
- await this.networkProxy.updateProxyConfigs(networkProxyConfigs);
1076
- }
1077
-
1078
- /**
1079
- * Convert route to NetworkProxy configuration
1080
- */
1081
- private routeToNetworkProxyConfig(route: IRouteConfig): any {
1082
- // Convert route to NetworkProxy domain config format
1083
- return {
1084
- domain: route.match.domains?.[0] || '*',
1085
- target: route.action.target,
1086
- tls: route.action.tls,
1087
- security: route.action.security
1088
- };
1089
- }
1090
-
1091
- /**
1092
- * Check if connection should use NetworkProxy
1093
- */
1094
- public shouldUseNetworkProxy(connection: IConnectionRecord, routeMatch: any): boolean {
1095
- // Only use NetworkProxy for TLS termination
1096
- return (
1097
- routeMatch.route.action.tls?.mode === 'terminate' ||
1098
- routeMatch.route.action.tls?.mode === 'terminate-and-reencrypt'
1099
- ) && this.networkProxy !== null;
1100
- }
1101
-
1102
- /**
1103
- * Pipe connection to NetworkProxy
1104
- */
1105
- public async pipeToNetworkProxy(socket: plugins.net.Socket): Promise<void> {
1106
- if (!this.networkProxy) {
1107
- throw new Error('NetworkProxy not initialized');
1108
- }
1109
-
1110
- const proxySocket = new plugins.net.Socket();
1111
-
1112
- await new Promise<void>((resolve, reject) => {
1113
- proxySocket.connect(this.settings.networkProxyPort!, 'localhost', () => {
1114
- console.log(`Connected to NetworkProxy for termination`);
1115
- resolve();
1116
- });
1117
-
1118
- proxySocket.on('error', reject);
1119
- });
1120
-
1121
- // Pipe the sockets together
1122
- socket.pipe(proxySocket);
1123
- proxySocket.pipe(socket);
1124
-
1125
- // Handle cleanup
1126
- const cleanup = () => {
1127
- socket.unpipe(proxySocket);
1128
- proxySocket.unpipe(socket);
1129
- proxySocket.destroy();
1130
- };
1131
-
1132
- socket.on('end', cleanup);
1133
- socket.on('error', cleanup);
1134
- proxySocket.on('end', cleanup);
1135
- proxySocket.on('error', cleanup);
1136
- }
1137
-
1138
- /**
1139
- * Start NetworkProxy
1140
- */
1141
- public async start(): Promise<void> {
1142
- if (this.networkProxy) {
1143
- await this.networkProxy.start();
1144
- }
1145
- }
1146
-
1147
- /**
1148
- * Stop NetworkProxy
1149
- */
1150
- public async stop(): Promise<void> {
1151
- if (this.networkProxy) {
1152
- await this.networkProxy.stop();
1153
- this.networkProxy = null;
1154
- }
1155
- }
1156
- }
1157
- ```
1158
-
1159
- ### Phase 4: Configuration Examples (No Migration)
1160
-
1161
- #### 4.1 New Configuration Format ONLY
1162
- ```typescript
1163
- // Update test files to use new structure
1164
- // test/test.certificate-provisioning.ts
1165
-
1166
- import { SmartProxy } from '../ts/proxies/smart-proxy/index.js';
1167
- import { expect, tap } from '@push.rocks/tapbundle';
1168
-
1169
- const testProxy = new SmartProxy({
1170
- routes: [{
1171
- name: 'test-route',
1172
- match: { ports: 443, domains: 'test.example.com' },
1173
- action: {
1174
- type: 'forward',
1175
- target: { host: 'localhost', port: 8080 },
1176
- tls: {
1177
- mode: 'terminate',
1178
- certificate: 'auto',
1179
- acme: {
1180
- email: 'test@example.com',
1181
- useProduction: false
82
+ type: 'forward',
83
+ target: { host: 'localhost', port: 8080 },
84
+ tls: {
85
+ mode: 'terminate',
86
+ certificate: 'auto' // Uses global ACME settings
1182
87
  }
1183
88
  }
1184
89
  }
1185
- }]
90
+ ]
1186
91
  });
1187
92
 
1188
- tap.test('should provision certificate automatically', async () => {
1189
- await testProxy.start();
1190
-
1191
- // Wait for certificate provisioning
1192
- await new Promise(resolve => setTimeout(resolve, 5000));
1193
-
1194
- const status = testProxy.getCertificateStatus('test-route');
1195
- expect(status).toBeDefined();
1196
- expect(status.status).toEqual('valid');
1197
- expect(status.source).toEqual('acme');
1198
-
1199
- await testProxy.stop();
1200
- });
1201
-
1202
- tap.test('should handle static certificates', async () => {
1203
- const proxy = new SmartProxy({
1204
- routes: [{
1205
- name: 'static-route',
1206
- match: { ports: 443, domains: 'static.example.com' },
93
+ // Or with route-specific ACME override
94
+ const proxy = new SmartProxy({
95
+ routes: [
96
+ {
97
+ name: 'special-site',
98
+ match: { domains: 'special.com', ports: 443 },
1207
99
  action: {
1208
100
  type: 'forward',
1209
101
  target: { host: 'localhost', port: 8080 },
1210
102
  tls: {
1211
103
  mode: 'terminate',
1212
- certificate: {
1213
- certFile: './test/fixtures/cert.pem',
1214
- keyFile: './test/fixtures/key.pem'
104
+ certificate: 'auto',
105
+ acme: { // Route-specific override
106
+ email: 'special@example.com',
107
+ useProduction: true
1215
108
  }
1216
109
  }
1217
110
  }
1218
- }]
1219
- });
1220
-
1221
- await proxy.start();
1222
-
1223
- const status = proxy.getCertificateStatus('static-route');
1224
- expect(status).toBeDefined();
1225
- expect(status.status).toEqual('valid');
1226
- expect(status.source).toEqual('static');
1227
-
1228
- await proxy.stop();
1229
- });
1230
- ```
1231
-
1232
- ### Phase 5: Documentation Update
1233
-
1234
- #### 5.1 Update README.md sections
1235
- ```markdown
1236
- ## Certificate Management
1237
-
1238
- SmartProxy includes built-in certificate management with automatic ACME (Let's Encrypt) support.
1239
-
1240
- ### Automatic Certificates (ACME)
1241
-
1242
- ```typescript
1243
- const proxy = new SmartProxy({
1244
- routes: [{
1245
- name: 'secure-site',
1246
- match: {
1247
- ports: 443,
1248
- domains: ['example.com', 'www.example.com']
1249
- },
1250
- action: {
1251
- type: 'forward',
1252
- target: { host: 'backend', port: 8080 },
1253
- tls: {
1254
- mode: 'terminate',
1255
- certificate: 'auto',
1256
- acme: {
1257
- email: 'admin@example.com',
1258
- useProduction: true,
1259
- renewBeforeDays: 30
1260
- }
1261
- }
1262
- }
1263
- }]
1264
- });
1265
- ```
1266
-
1267
- ### Static Certificates
1268
-
1269
- ```typescript
1270
- const proxy = new SmartProxy({
1271
- routes: [{
1272
- name: 'static-cert',
1273
- match: { ports: 443, domains: 'secure.example.com' },
1274
- action: {
1275
- type: 'forward',
1276
- target: { host: 'backend', port: 8080 },
1277
- tls: {
1278
- mode: 'terminate',
1279
- certificate: {
1280
- certFile: './certs/secure.pem',
1281
- keyFile: './certs/secure.key'
1282
- }
1283
- }
1284
111
  }
1285
- }]
112
+ ]
1286
113
  });
1287
114
  ```
1288
115
 
1289
- ### Certificate Management API
1290
-
1291
- ```typescript
1292
- // Get certificate status
1293
- const status = proxy.getCertificateStatus('route-name');
1294
- console.log(status);
1295
- // {
1296
- // domain: 'example.com',
1297
- // status: 'valid',
1298
- // source: 'acme',
1299
- // expiryDate: Date,
1300
- // issueDate: Date
1301
- // }
1302
-
1303
- // Manually provision certificate
1304
- await proxy.provisionCertificate('route-name');
1305
-
1306
- // Force certificate renewal
1307
- await proxy.renewCertificate('route-name');
1308
- ```
1309
-
1310
- ### Certificate Storage
1311
-
1312
- Certificates are stored in the `./certs` directory by default:
1313
-
1314
- ```
1315
- ./certs/
1316
- ├── route-name/
1317
- │ ├── cert.pem
1318
- │ ├── key.pem
1319
- │ ├── ca.pem (if available)
1320
- │ └── meta.json
1321
- ```
1322
- ```
1323
-
1324
- ### Phase 5: Update HTTP Module
1325
-
1326
- #### 5.1 Update http/index.ts
1327
- ```typescript
1328
- // ts/http/index.ts
1329
- /**
1330
- * HTTP functionality module
1331
- */
1332
-
1333
- // Export types and models
1334
- export * from './models/http-types.js';
1335
-
1336
- // Export submodules (remove port80 export)
1337
- export * from './router/index.js';
1338
- export * from './redirects/index.js';
1339
- // REMOVED: export * from './port80/index.js';
1340
-
1341
- // Convenience namespace exports (no more Port80)
1342
- export const Http = {
1343
- // Only router and redirect functionality remain
1344
- };
1345
- ```
1346
-
1347
- ### Phase 6: Cleanup Tasks
1348
-
1349
- #### 6.1 File Deletion Script
1350
- ```bash
1351
- #!/bin/bash
1352
- # cleanup-certificates.sh
1353
-
1354
- # Remove old certificate module
1355
- rm -rf ts/certificate/
1356
-
1357
- # Remove entire port80 subdirectory
1358
- rm -rf ts/http/port80/
1359
-
1360
- # Remove old imports from index files
1361
- sed -i '/certificate\//d' ts/index.ts
1362
- sed -i '/port80\//d' ts/http/index.ts
1363
-
1364
- # Update plugins.ts to remove unused dependencies (if not used elsewhere)
1365
- # sed -i '/smartexpress/d' ts/plugins.ts
1366
- ```
1367
-
1368
- #### 6.2 Key Simplifications Achieved
1369
-
1370
- 1. **No custom ACME wrapper** - Direct use of @push.rocks/smartacme
1371
- 2. **No separate HTTP server** - ACME challenges are regular routes
1372
- 3. **Built-in path routing** - SmartProxy already handles path-based matching
1373
- 4. **Built-in priorities** - Routes are already sorted by priority
1374
- 5. **Safe updates** - Route updates are already thread-safe
1375
- 6. **Minimal new code** - Mostly configuration and integration
1376
-
1377
- The simplification leverages SmartProxy's existing capabilities rather than reinventing them.
1378
-
1379
- #### 6.2 Update Package.json
1380
- ```json
1381
- {
1382
- "dependencies": {
1383
- // Remove if no longer needed elsewhere:
1384
- // "@push.rocks/smartexpress": "x.x.x"
1385
- }
1386
- }
1387
- ```
1388
-
1389
- ## Implementation Sequence
1390
-
1391
- 1. **Day 1: Core Implementation**
1392
- - Create SmartCertManager class
1393
- - Create CertStore and AcmeClient
1394
- - Update route types
1395
-
1396
- 2. **Day 2: Integration**
1397
- - Update SmartProxy to use SmartCertManager
1398
- - Simplify NetworkProxyBridge
1399
- - Remove old certificate system
1400
-
1401
- 3. **Day 3: Testing**
1402
- - Create new tests using new format only
1403
- - No migration testing needed
1404
- - Test all new functionality
1405
-
1406
- 4. **Day 4: Documentation & Cleanup**
1407
- - Update all documentation
1408
- - Clean up old files
1409
- - Final testing and validation
1410
-
1411
- ## Risk Mitigation
1412
-
1413
- 1. **Static Route Handler**
1414
- - Already exists in the type system
1415
- - Just needs implementation in route-connection-handler.ts
1416
- - Low risk as it follows existing patterns
1417
-
1418
- 2. **Route Updates During Operation**
1419
- - SmartProxy's updateRoutes() is already thread-safe
1420
- - Sequential processing prevents race conditions
1421
- - Challenge routes are added/removed atomically
1422
-
1423
- 3. **Port 80 Conflicts**
1424
- - Priority-based routing ensures ACME routes match first
1425
- - Path-based matching (`/.well-known/acme-challenge/*`) is specific
1426
- - Other routes on port 80 won't interfere
116
+ ## Success Criteria
117
+ 1. ✅ Users can configure ACME at top-level for all routes
118
+ 2. ✅ Clear error messages guide users to correct configuration
119
+ 3. Certificate acquisition works with minimal configuration
120
+ 4. Documentation clearly explains all configuration options
121
+ 5. ✅ Migration from v18 to v19 is straightforward
1427
122
 
1428
- 4. **Error Recovery**
1429
- - SmartAcme initialization failures are handled gracefully
1430
- - Null checks prevent crashes if ACME isn't available
1431
- - Routes continue to work without certificates
123
+ ## Timeline
124
+ - Phase 1: 2-3 days
125
+ - Phase 2: 1-2 days
126
+ - Phase 3: 1 day
1432
127
 
1433
- 5. **Testing Strategy**
1434
- - Test concurrent ACME challenges
1435
- - Test route priority conflicts
1436
- - Test certificate renewal during high traffic
1437
- - Test the new configuration format only
128
+ Total estimated time: 5-6 days
1438
129
 
1439
- 6. **No Migration Path**
1440
- - Breaking change is intentional
1441
- - Old configurations must be manually updated
1442
- - No compatibility shims or helpers provided
130
+ ## Notes
131
+ - Maintain backward compatibility with existing route-level ACME config
132
+ - Consider adding a configuration wizard for interactive setup
133
+ - Explore integration with popular DNS providers for DNS-01 challenges
134
+ - Add metrics/monitoring for certificate renewal status