@push.rocks/smartproxy 16.0.2 → 16.0.3

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 (115) hide show
  1. package/dist_ts/00_commitinfo_data.js +1 -1
  2. package/dist_ts/core/models/index.d.ts +2 -0
  3. package/dist_ts/core/models/index.js +3 -1
  4. package/dist_ts/core/models/route-context.d.ts +62 -0
  5. package/dist_ts/core/models/route-context.js +43 -0
  6. package/dist_ts/core/models/socket-augmentation.d.ts +12 -0
  7. package/dist_ts/core/models/socket-augmentation.js +18 -0
  8. package/dist_ts/core/utils/event-system.d.ts +200 -0
  9. package/dist_ts/core/utils/event-system.js +224 -0
  10. package/dist_ts/core/utils/index.d.ts +7 -0
  11. package/dist_ts/core/utils/index.js +8 -1
  12. package/dist_ts/core/utils/route-manager.d.ts +118 -0
  13. package/dist_ts/core/utils/route-manager.js +383 -0
  14. package/dist_ts/core/utils/route-utils.d.ts +94 -0
  15. package/dist_ts/core/utils/route-utils.js +264 -0
  16. package/dist_ts/core/utils/security-utils.d.ts +111 -0
  17. package/dist_ts/core/utils/security-utils.js +212 -0
  18. package/dist_ts/core/utils/shared-security-manager.d.ts +110 -0
  19. package/dist_ts/core/utils/shared-security-manager.js +252 -0
  20. package/dist_ts/core/utils/template-utils.d.ts +37 -0
  21. package/dist_ts/core/utils/template-utils.js +104 -0
  22. package/dist_ts/core/utils/websocket-utils.d.ts +23 -0
  23. package/dist_ts/core/utils/websocket-utils.js +86 -0
  24. package/dist_ts/http/router/index.d.ts +5 -1
  25. package/dist_ts/http/router/index.js +4 -2
  26. package/dist_ts/http/router/route-router.d.ts +108 -0
  27. package/dist_ts/http/router/route-router.js +393 -0
  28. package/dist_ts/index.d.ts +8 -2
  29. package/dist_ts/index.js +10 -3
  30. package/dist_ts/proxies/index.d.ts +7 -2
  31. package/dist_ts/proxies/index.js +10 -4
  32. package/dist_ts/proxies/network-proxy/certificate-manager.d.ts +21 -0
  33. package/dist_ts/proxies/network-proxy/certificate-manager.js +92 -1
  34. package/dist_ts/proxies/network-proxy/context-creator.d.ts +34 -0
  35. package/dist_ts/proxies/network-proxy/context-creator.js +108 -0
  36. package/dist_ts/proxies/network-proxy/function-cache.d.ts +90 -0
  37. package/dist_ts/proxies/network-proxy/function-cache.js +198 -0
  38. package/dist_ts/proxies/network-proxy/http-request-handler.d.ts +40 -0
  39. package/dist_ts/proxies/network-proxy/http-request-handler.js +256 -0
  40. package/dist_ts/proxies/network-proxy/http2-request-handler.d.ts +24 -0
  41. package/dist_ts/proxies/network-proxy/http2-request-handler.js +201 -0
  42. package/dist_ts/proxies/network-proxy/models/types.d.ts +73 -1
  43. package/dist_ts/proxies/network-proxy/models/types.js +242 -1
  44. package/dist_ts/proxies/network-proxy/network-proxy.d.ts +23 -20
  45. package/dist_ts/proxies/network-proxy/network-proxy.js +147 -60
  46. package/dist_ts/proxies/network-proxy/request-handler.d.ts +38 -5
  47. package/dist_ts/proxies/network-proxy/request-handler.js +584 -198
  48. package/dist_ts/proxies/network-proxy/security-manager.d.ts +65 -0
  49. package/dist_ts/proxies/network-proxy/security-manager.js +255 -0
  50. package/dist_ts/proxies/network-proxy/websocket-handler.d.ts +13 -2
  51. package/dist_ts/proxies/network-proxy/websocket-handler.js +238 -20
  52. package/dist_ts/proxies/smart-proxy/index.d.ts +1 -1
  53. package/dist_ts/proxies/smart-proxy/index.js +3 -3
  54. package/dist_ts/proxies/smart-proxy/models/interfaces.d.ts +3 -5
  55. package/dist_ts/proxies/smart-proxy/models/route-types.d.ts +56 -3
  56. package/dist_ts/proxies/smart-proxy/network-proxy-bridge.d.ts +4 -57
  57. package/dist_ts/proxies/smart-proxy/network-proxy-bridge.js +19 -228
  58. package/dist_ts/proxies/smart-proxy/port-manager.d.ts +81 -0
  59. package/dist_ts/proxies/smart-proxy/port-manager.js +166 -0
  60. package/dist_ts/proxies/smart-proxy/route-connection-handler.d.ts +5 -0
  61. package/dist_ts/proxies/smart-proxy/route-connection-handler.js +131 -15
  62. package/dist_ts/proxies/smart-proxy/route-helpers/index.d.ts +3 -1
  63. package/dist_ts/proxies/smart-proxy/route-helpers/index.js +5 -3
  64. package/dist_ts/proxies/smart-proxy/route-helpers.d.ts +5 -178
  65. package/dist_ts/proxies/smart-proxy/route-helpers.js +8 -296
  66. package/dist_ts/proxies/smart-proxy/route-manager.d.ts +11 -2
  67. package/dist_ts/proxies/smart-proxy/route-manager.js +79 -10
  68. package/dist_ts/proxies/smart-proxy/smart-proxy.d.ts +29 -2
  69. package/dist_ts/proxies/smart-proxy/smart-proxy.js +48 -43
  70. package/dist_ts/proxies/smart-proxy/utils/route-helpers.d.ts +67 -1
  71. package/dist_ts/proxies/smart-proxy/utils/route-helpers.js +120 -1
  72. package/dist_ts/proxies/smart-proxy/utils/route-validators.d.ts +3 -3
  73. package/dist_ts/proxies/smart-proxy/utils/route-validators.js +27 -5
  74. package/package.json +1 -1
  75. package/readme.md +102 -14
  76. package/readme.plan.md +103 -168
  77. package/ts/00_commitinfo_data.ts +1 -1
  78. package/ts/core/models/index.ts +2 -0
  79. package/ts/core/models/route-context.ts +113 -0
  80. package/ts/core/models/socket-augmentation.ts +33 -0
  81. package/ts/core/utils/event-system.ts +376 -0
  82. package/ts/core/utils/index.ts +7 -0
  83. package/ts/core/utils/route-manager.ts +489 -0
  84. package/ts/core/utils/route-utils.ts +312 -0
  85. package/ts/core/utils/security-utils.ts +309 -0
  86. package/ts/core/utils/shared-security-manager.ts +333 -0
  87. package/ts/core/utils/template-utils.ts +124 -0
  88. package/ts/core/utils/websocket-utils.ts +81 -0
  89. package/ts/http/router/index.ts +8 -1
  90. package/ts/http/router/route-router.ts +482 -0
  91. package/ts/index.ts +14 -2
  92. package/ts/proxies/index.ts +12 -3
  93. package/ts/proxies/network-proxy/certificate-manager.ts +114 -10
  94. package/ts/proxies/network-proxy/context-creator.ts +145 -0
  95. package/ts/proxies/network-proxy/function-cache.ts +259 -0
  96. package/ts/proxies/network-proxy/http-request-handler.ts +330 -0
  97. package/ts/proxies/network-proxy/http2-request-handler.ts +255 -0
  98. package/ts/proxies/network-proxy/models/types.ts +312 -1
  99. package/ts/proxies/network-proxy/network-proxy.ts +195 -86
  100. package/ts/proxies/network-proxy/request-handler.ts +698 -246
  101. package/ts/proxies/network-proxy/security-manager.ts +298 -0
  102. package/ts/proxies/network-proxy/websocket-handler.ts +276 -33
  103. package/ts/proxies/smart-proxy/index.ts +2 -12
  104. package/ts/proxies/smart-proxy/models/interfaces.ts +7 -4
  105. package/ts/proxies/smart-proxy/models/route-types.ts +78 -10
  106. package/ts/proxies/smart-proxy/network-proxy-bridge.ts +20 -257
  107. package/ts/proxies/smart-proxy/port-manager.ts +195 -0
  108. package/ts/proxies/smart-proxy/route-connection-handler.ts +156 -21
  109. package/ts/proxies/smart-proxy/route-manager.ts +98 -14
  110. package/ts/proxies/smart-proxy/smart-proxy.ts +56 -55
  111. package/ts/proxies/smart-proxy/utils/route-helpers.ts +167 -1
  112. package/ts/proxies/smart-proxy/utils/route-validators.ts +24 -5
  113. package/ts/proxies/smart-proxy/domain-config-manager.ts.bak +0 -441
  114. package/ts/proxies/smart-proxy/route-helpers/index.ts +0 -9
  115. package/ts/proxies/smart-proxy/route-helpers.ts +0 -498
@@ -6,7 +6,7 @@ import { SecurityManager } from './security-manager.js';
6
6
  import { TlsManager } from './tls-manager.js';
7
7
  import { NetworkProxyBridge } from './network-proxy-bridge.js';
8
8
  import { TimeoutManager } from './timeout-manager.js';
9
- // import { PortRangeManager } from './port-range-manager.js';
9
+ import { PortManager } from './port-manager.js';
10
10
  import { RouteManager } from './route-manager.js';
11
11
  import { RouteConnectionHandler } from './route-connection-handler.js';
12
12
 
@@ -39,7 +39,8 @@ import type { IRouteConfig } from './models/route-types.js';
39
39
  * - Advanced options (timeout, headers, etc.)
40
40
  */
41
41
  export class SmartProxy extends plugins.EventEmitter {
42
- private netServers: plugins.net.Server[] = [];
42
+ // Port manager handles dynamic listener management
43
+ private portManager: PortManager;
43
44
  private connectionLogger: NodeJS.Timeout | null = null;
44
45
  private isShuttingDown: boolean = false;
45
46
 
@@ -49,8 +50,7 @@ export class SmartProxy extends plugins.EventEmitter {
49
50
  private tlsManager: TlsManager;
50
51
  private networkProxyBridge: NetworkProxyBridge;
51
52
  private timeoutManager: TimeoutManager;
52
- // private portRangeManager: PortRangeManager;
53
- private routeManager: RouteManager;
53
+ public routeManager: RouteManager; // Made public for route management
54
54
  private routeConnectionHandler: RouteConnectionHandler;
55
55
 
56
56
  // Port80Handler for ACME certificate management
@@ -151,8 +151,6 @@ export class SmartProxy extends plugins.EventEmitter {
151
151
  // Create the route manager
152
152
  this.routeManager = new RouteManager(this.settings);
153
153
 
154
- // Create port range manager
155
- // this.portRangeManager = new PortRangeManager(this.settings);
156
154
 
157
155
  // Create other required components
158
156
  this.tlsManager = new TlsManager(this.settings);
@@ -168,6 +166,9 @@ export class SmartProxy extends plugins.EventEmitter {
168
166
  this.timeoutManager,
169
167
  this.routeManager
170
168
  );
169
+
170
+ // Initialize port manager
171
+ this.portManager = new PortManager(this.settings, this.routeConnectionHandler);
171
172
  }
172
173
 
173
174
  /**
@@ -271,33 +272,8 @@ export class SmartProxy extends plugins.EventEmitter {
271
272
  // Get listening ports from RouteManager
272
273
  const listeningPorts = this.routeManager.getListeningPorts();
273
274
 
274
- // Create servers for each port
275
- for (const port of listeningPorts) {
276
- const server = plugins.net.createServer((socket) => {
277
- // Check if shutting down
278
- if (this.isShuttingDown) {
279
- socket.end();
280
- socket.destroy();
281
- return;
282
- }
283
-
284
- // Delegate to route connection handler
285
- this.routeConnectionHandler.handleConnection(socket);
286
- }).on('error', (err: Error) => {
287
- console.log(`Server Error on port ${port}: ${err.message}`);
288
- });
289
-
290
- server.listen(port, () => {
291
- const isNetworkProxyPort = this.settings.useNetworkProxy?.includes(port);
292
- console.log(
293
- `SmartProxy -> OK: Now listening on port ${port}${
294
- isNetworkProxyPort ? ' (NetworkProxy forwarding enabled)' : ''
295
- }`
296
- );
297
- });
298
-
299
- this.netServers.push(server);
300
- }
275
+ // Start port listeners using the PortManager
276
+ await this.portManager.addPorts(listeningPorts);
301
277
 
302
278
  // Set up periodic connection logging and inactivity checks
303
279
  this.connectionLogger = setInterval(() => {
@@ -383,6 +359,7 @@ export class SmartProxy extends plugins.EventEmitter {
383
359
  public async stop() {
384
360
  console.log('SmartProxy shutting down...');
385
361
  this.isShuttingDown = true;
362
+ this.portManager.setShuttingDown(true);
386
363
 
387
364
  // Stop CertProvisioner if active
388
365
  if (this.certProvisioner) {
@@ -401,31 +378,14 @@ export class SmartProxy extends plugins.EventEmitter {
401
378
  }
402
379
  }
403
380
 
404
- // Stop accepting new connections
405
- const closeServerPromises: Promise<void>[] = this.netServers.map(
406
- (server) =>
407
- new Promise<void>((resolve) => {
408
- if (!server.listening) {
409
- resolve();
410
- return;
411
- }
412
- server.close((err) => {
413
- if (err) {
414
- console.log(`Error closing server: ${err.message}`);
415
- }
416
- resolve();
417
- });
418
- })
419
- );
420
-
421
381
  // Stop the connection logger
422
382
  if (this.connectionLogger) {
423
383
  clearInterval(this.connectionLogger);
424
384
  this.connectionLogger = null;
425
385
  }
426
386
 
427
- // Wait for servers to close
428
- await Promise.all(closeServerPromises);
387
+ // Stop all port listeners
388
+ await this.portManager.closeAll();
429
389
  console.log('All servers closed. Cleaning up active connections...');
430
390
 
431
391
  // Clean up all active connections
@@ -434,8 +394,6 @@ export class SmartProxy extends plugins.EventEmitter {
434
394
  // Stop NetworkProxy
435
395
  await this.networkProxyBridge.stop();
436
396
 
437
- // Clear all servers
438
- this.netServers = [];
439
397
 
440
398
  console.log('SmartProxy shutdown complete.');
441
399
  }
@@ -479,6 +437,12 @@ export class SmartProxy extends plugins.EventEmitter {
479
437
  // Update routes in RouteManager
480
438
  this.routeManager.updateRoutes(newRoutes);
481
439
 
440
+ // Get the new set of required ports
441
+ const requiredPorts = this.routeManager.getListeningPorts();
442
+
443
+ // Update port listeners to match the new configuration
444
+ await this.portManager.updatePorts(requiredPorts);
445
+
482
446
  // If NetworkProxy is initialized, resync the configurations
483
447
  if (this.networkProxyBridge.getNetworkProxy()) {
484
448
  await this.networkProxyBridge.syncRoutesToNetworkProxy(newRoutes);
@@ -609,6 +573,41 @@ export class SmartProxy extends plugins.EventEmitter {
609
573
  return true;
610
574
  }
611
575
 
576
+ /**
577
+ * Add a new listening port without changing the route configuration
578
+ *
579
+ * This allows you to add a port listener without updating routes.
580
+ * Useful for preparing to listen on a port before adding routes for it.
581
+ *
582
+ * @param port The port to start listening on
583
+ * @returns Promise that resolves when the port is listening
584
+ */
585
+ public async addListeningPort(port: number): Promise<void> {
586
+ return this.portManager.addPort(port);
587
+ }
588
+
589
+ /**
590
+ * Stop listening on a specific port without changing the route configuration
591
+ *
592
+ * This allows you to stop a port listener without updating routes.
593
+ * Useful for temporary maintenance or port changes.
594
+ *
595
+ * @param port The port to stop listening on
596
+ * @returns Promise that resolves when the port is closed
597
+ */
598
+ public async removeListeningPort(port: number): Promise<void> {
599
+ return this.portManager.removePort(port);
600
+ }
601
+
602
+ /**
603
+ * Get a list of all ports currently being listened on
604
+ *
605
+ * @returns Array of port numbers
606
+ */
607
+ public getListeningPorts(): number[] {
608
+ return this.portManager.getListeningPorts();
609
+ }
610
+
612
611
  /**
613
612
  * Get statistics about current connections
614
613
  */
@@ -638,7 +637,9 @@ export class SmartProxy extends plugins.EventEmitter {
638
637
  terminationStats,
639
638
  acmeEnabled: !!this.port80Handler,
640
639
  port80HandlerPort: this.port80Handler ? this.settings.acme?.port : null,
641
- routes: this.routeManager.getListeningPorts().length
640
+ routes: this.routeManager.getListeningPorts().length,
641
+ listeningPorts: this.portManager.getListeningPorts(),
642
+ activePorts: this.portManager.getListeningPorts().length
642
643
  };
643
644
  }
644
645
 
@@ -14,9 +14,11 @@
14
14
  * - Static file server routes (createStaticFileRoute)
15
15
  * - API routes (createApiRoute)
16
16
  * - WebSocket routes (createWebSocketRoute)
17
+ * - Port mapping routes (createPortMappingRoute, createOffsetPortMappingRoute)
18
+ * - Dynamic routing (createDynamicRoute, createSmartLoadBalancer)
17
19
  */
18
20
 
19
- import type { IRouteConfig, IRouteMatch, IRouteAction, IRouteTarget, TPortRange } from '../models/route-types.js';
21
+ import type { IRouteConfig, IRouteMatch, IRouteAction, IRouteTarget, TPortRange, IRouteContext } from '../models/route-types.js';
20
22
 
21
23
  /**
22
24
  * Create an HTTP-only route configuration
@@ -452,4 +454,168 @@ export function createWebSocketRoute(
452
454
  priority: options.priority || 100, // Higher priority for WebSocket routes
453
455
  ...options
454
456
  };
457
+ }
458
+
459
+ /**
460
+ * Create a helper function that applies a port offset
461
+ * @param offset The offset to apply to the matched port
462
+ * @returns A function that adds the offset to the matched port
463
+ */
464
+ export function createPortOffset(offset: number): (context: IRouteContext) => number {
465
+ return (context: IRouteContext) => context.port + offset;
466
+ }
467
+
468
+ /**
469
+ * Create a port mapping route with context-based port function
470
+ * @param options Port mapping route options
471
+ * @returns Route configuration object
472
+ */
473
+ export function createPortMappingRoute(options: {
474
+ sourcePortRange: TPortRange;
475
+ targetHost: string | string[] | ((context: IRouteContext) => string | string[]);
476
+ portMapper: (context: IRouteContext) => number;
477
+ name?: string;
478
+ domains?: string | string[];
479
+ priority?: number;
480
+ [key: string]: any;
481
+ }): IRouteConfig {
482
+ // Create route match
483
+ const match: IRouteMatch = {
484
+ ports: options.sourcePortRange,
485
+ domains: options.domains
486
+ };
487
+
488
+ // Create route action
489
+ const action: IRouteAction = {
490
+ type: 'forward',
491
+ target: {
492
+ host: options.targetHost,
493
+ port: options.portMapper
494
+ }
495
+ };
496
+
497
+ // Create the route config
498
+ return {
499
+ match,
500
+ action,
501
+ name: options.name || `Port Mapping Route for ${options.domains || 'all domains'}`,
502
+ priority: options.priority,
503
+ ...options
504
+ };
505
+ }
506
+
507
+ /**
508
+ * Create a simple offset port mapping route
509
+ * @param options Offset port mapping route options
510
+ * @returns Route configuration object
511
+ */
512
+ export function createOffsetPortMappingRoute(options: {
513
+ ports: TPortRange;
514
+ targetHost: string | string[];
515
+ offset: number;
516
+ name?: string;
517
+ domains?: string | string[];
518
+ priority?: number;
519
+ [key: string]: any;
520
+ }): IRouteConfig {
521
+ return createPortMappingRoute({
522
+ sourcePortRange: options.ports,
523
+ targetHost: options.targetHost,
524
+ portMapper: (context) => context.port + options.offset,
525
+ name: options.name || `Offset Mapping (${options.offset > 0 ? '+' : ''}${options.offset}) for ${options.domains || 'all domains'}`,
526
+ domains: options.domains,
527
+ priority: options.priority,
528
+ ...options
529
+ });
530
+ }
531
+
532
+ /**
533
+ * Create a dynamic route with context-based host and port mapping
534
+ * @param options Dynamic route options
535
+ * @returns Route configuration object
536
+ */
537
+ export function createDynamicRoute(options: {
538
+ ports: TPortRange;
539
+ targetHost: (context: IRouteContext) => string | string[];
540
+ portMapper: (context: IRouteContext) => number;
541
+ name?: string;
542
+ domains?: string | string[];
543
+ path?: string;
544
+ clientIp?: string[];
545
+ priority?: number;
546
+ [key: string]: any;
547
+ }): IRouteConfig {
548
+ // Create route match
549
+ const match: IRouteMatch = {
550
+ ports: options.ports,
551
+ domains: options.domains,
552
+ path: options.path,
553
+ clientIp: options.clientIp
554
+ };
555
+
556
+ // Create route action
557
+ const action: IRouteAction = {
558
+ type: 'forward',
559
+ target: {
560
+ host: options.targetHost,
561
+ port: options.portMapper
562
+ }
563
+ };
564
+
565
+ // Create the route config
566
+ return {
567
+ match,
568
+ action,
569
+ name: options.name || `Dynamic Route for ${options.domains || 'all domains'}`,
570
+ priority: options.priority,
571
+ ...options
572
+ };
573
+ }
574
+
575
+ /**
576
+ * Create a smart load balancer with dynamic domain-based backend selection
577
+ * @param options Smart load balancer options
578
+ * @returns Route configuration object
579
+ */
580
+ export function createSmartLoadBalancer(options: {
581
+ ports: TPortRange;
582
+ domainTargets: Record<string, string | string[]>;
583
+ portMapper: (context: IRouteContext) => number;
584
+ name?: string;
585
+ defaultTarget?: string | string[];
586
+ priority?: number;
587
+ [key: string]: any;
588
+ }): IRouteConfig {
589
+ // Extract all domain keys to create the match criteria
590
+ const domains = Object.keys(options.domainTargets);
591
+
592
+ // Create the smart host selector function
593
+ const hostSelector = (context: IRouteContext) => {
594
+ const domain = context.domain || '';
595
+ return options.domainTargets[domain] || options.defaultTarget || 'localhost';
596
+ };
597
+
598
+ // Create route match
599
+ const match: IRouteMatch = {
600
+ ports: options.ports,
601
+ domains
602
+ };
603
+
604
+ // Create route action
605
+ const action: IRouteAction = {
606
+ type: 'forward',
607
+ target: {
608
+ host: hostSelector,
609
+ port: options.portMapper
610
+ }
611
+ };
612
+
613
+ // Create the route config
614
+ return {
615
+ match,
616
+ action,
617
+ name: options.name || `Smart Load Balancer for ${domains.join(', ')}`,
618
+ priority: options.priority,
619
+ ...options
620
+ };
455
621
  }
@@ -9,14 +9,24 @@ import type { IRouteConfig, IRouteMatch, IRouteAction, TPortRange } from '../mod
9
9
 
10
10
  /**
11
11
  * Validates a port range or port number
12
- * @param port Port number or port range
12
+ * @param port Port number, port range, or port function
13
13
  * @returns True if valid, false otherwise
14
14
  */
15
- export function isValidPort(port: TPortRange): boolean {
15
+ export function isValidPort(port: any): boolean {
16
16
  if (typeof port === 'number') {
17
17
  return port > 0 && port < 65536; // Valid port range is 1-65535
18
18
  } else if (Array.isArray(port)) {
19
- return port.every(p => typeof p === 'number' && p > 0 && p < 65536);
19
+ return port.every(p =>
20
+ (typeof p === 'number' && p > 0 && p < 65536) ||
21
+ (typeof p === 'object' && 'from' in p && 'to' in p &&
22
+ p.from > 0 && p.from < 65536 && p.to > 0 && p.to < 65536)
23
+ );
24
+ } else if (typeof port === 'function') {
25
+ // For function-based ports, we can't validate the result at config time
26
+ // so we just check that it's a function
27
+ return true;
28
+ } else if (typeof port === 'object' && 'from' in port && 'to' in port) {
29
+ return port.from > 0 && port.from < 65536 && port.to > 0 && port.to < 65536;
20
30
  }
21
31
  return false;
22
32
  }
@@ -100,11 +110,20 @@ export function validateRouteAction(action: IRouteAction): { valid: boolean; err
100
110
  // Validate target host
101
111
  if (!action.target.host) {
102
112
  errors.push('Target host is required');
113
+ } else if (typeof action.target.host !== 'string' &&
114
+ !Array.isArray(action.target.host) &&
115
+ typeof action.target.host !== 'function') {
116
+ errors.push('Target host must be a string, array of strings, or function');
103
117
  }
104
118
 
105
119
  // Validate target port
106
- if (!action.target.port || !isValidPort(action.target.port)) {
107
- errors.push('Valid target port is required');
120
+ if (action.target.port === undefined) {
121
+ errors.push('Target port is required');
122
+ } else if (typeof action.target.port !== 'number' &&
123
+ typeof action.target.port !== 'function') {
124
+ errors.push('Target port must be a number or a function');
125
+ } else if (typeof action.target.port === 'number' && !isValidPort(action.target.port)) {
126
+ errors.push('Target port must be between 1 and 65535');
108
127
  }
109
128
  }
110
129