@push.rocks/smartproxy 18.0.2 → 18.2.0

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