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