@serve.zone/dcrouter 13.43.5 → 13.44.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 (70) hide show
  1. package/deno.json +1 -1
  2. package/dist_serve/bundle.js +491 -436
  3. package/dist_ts/00_commitinfo_data.js +1 -1
  4. package/dist_ts/classes.dcrouter.d.ts +16 -13
  5. package/dist_ts/classes.dcrouter.js +320 -82
  6. package/dist_ts/config/classes.route-config-manager.d.ts +1 -0
  7. package/dist_ts/config/classes.route-config-manager.js +4 -1
  8. package/dist_ts/db/documents/classes.email-server-settings.doc.d.ts +14 -0
  9. package/dist_ts/db/documents/classes.email-server-settings.doc.js +103 -0
  10. package/dist_ts/db/documents/classes.remote-ingress-hub-settings.doc.d.ts +3 -0
  11. package/dist_ts/db/documents/classes.remote-ingress-hub-settings.doc.js +20 -2
  12. package/dist_ts/db/documents/index.d.ts +1 -0
  13. package/dist_ts/db/documents/index.js +2 -1
  14. package/dist_ts/email/classes.email-domain.manager.d.ts +3 -1
  15. package/dist_ts/email/classes.email-domain.manager.js +6 -3
  16. package/dist_ts/email/classes.email-settings.manager.d.ts +25 -0
  17. package/dist_ts/email/classes.email-settings.manager.js +184 -0
  18. package/dist_ts/email/index.d.ts +1 -0
  19. package/dist_ts/email/index.js +2 -1
  20. package/dist_ts/opsserver/classes.opsserver.d.ts +1 -0
  21. package/dist_ts/opsserver/classes.opsserver.js +3 -1
  22. package/dist_ts/opsserver/handlers/config.handler.js +17 -14
  23. package/dist_ts/opsserver/handlers/email-settings.handler.d.ts +10 -0
  24. package/dist_ts/opsserver/handlers/email-settings.handler.js +57 -0
  25. package/dist_ts/opsserver/handlers/index.d.ts +1 -0
  26. package/dist_ts/opsserver/handlers/index.js +2 -1
  27. package/dist_ts/opsserver/handlers/remoteingress.handler.js +24 -6
  28. package/dist_ts/opsserver/handlers/workhoster.handler.js +2 -2
  29. package/dist_ts/remoteingress/classes.remoteingress-manager.d.ts +4 -6
  30. package/dist_ts/remoteingress/classes.remoteingress-manager.js +58 -19
  31. package/dist_ts_interfaces/data/email-settings.d.ts +39 -0
  32. package/dist_ts_interfaces/data/email-settings.js +2 -0
  33. package/dist_ts_interfaces/data/index.d.ts +1 -0
  34. package/dist_ts_interfaces/data/index.js +2 -1
  35. package/dist_ts_interfaces/data/remoteingress.d.ts +7 -0
  36. package/dist_ts_interfaces/requests/email-settings.d.ts +26 -0
  37. package/dist_ts_interfaces/requests/email-settings.js +2 -0
  38. package/dist_ts_interfaces/requests/index.d.ts +1 -0
  39. package/dist_ts_interfaces/requests/index.js +2 -1
  40. package/dist_ts_interfaces/requests/remoteingress.d.ts +4 -1
  41. package/dist_ts_migrations/index.d.ts +14 -1
  42. package/dist_ts_migrations/index.js +107 -2
  43. package/dist_ts_web/00_commitinfo_data.js +1 -1
  44. package/dist_ts_web/appstate.d.ts +6 -1
  45. package/dist_ts_web/appstate.js +23 -1
  46. package/dist_ts_web/elements/email/ops-view-email-domains.d.ts +4 -0
  47. package/dist_ts_web/elements/email/ops-view-email-domains.js +122 -1
  48. package/dist_ts_web/elements/network/ops-view-remoteingress.d.ts +2 -1
  49. package/dist_ts_web/elements/network/ops-view-remoteingress.js +57 -17
  50. package/package.json +1 -1
  51. package/ts/00_commitinfo_data.ts +1 -1
  52. package/ts/classes.dcrouter.ts +361 -100
  53. package/ts/config/classes.route-config-manager.ts +4 -0
  54. package/ts/db/documents/classes.email-server-settings.doc.ts +40 -0
  55. package/ts/db/documents/classes.remote-ingress-hub-settings.doc.ts +9 -0
  56. package/ts/db/documents/index.ts +1 -0
  57. package/ts/email/classes.email-domain.manager.ts +6 -2
  58. package/ts/email/classes.email-settings.manager.ts +221 -0
  59. package/ts/email/index.ts +1 -0
  60. package/ts/opsserver/classes.opsserver.ts +2 -0
  61. package/ts/opsserver/handlers/config.handler.ts +16 -13
  62. package/ts/opsserver/handlers/email-settings.handler.ts +72 -0
  63. package/ts/opsserver/handlers/index.ts +1 -0
  64. package/ts/opsserver/handlers/remoteingress.handler.ts +25 -5
  65. package/ts/opsserver/handlers/workhoster.handler.ts +1 -1
  66. package/ts/remoteingress/classes.remoteingress-manager.ts +65 -18
  67. package/ts_web/00_commitinfo_data.ts +1 -1
  68. package/ts_web/appstate.ts +33 -1
  69. package/ts_web/elements/email/ops-view-email-domains.ts +126 -0
  70. package/ts_web/elements/network/ops-view-remoteingress.ts +59 -17
@@ -31,9 +31,10 @@ import { SecurityLogger, ContentScanner, IPReputationChecker, SecurityPolicyMana
31
31
  import { type IHttp3Config, augmentRoutesWithHttp3 } from './http3/index.js';
32
32
  import { DnsManager } from './dns/manager.dns.js';
33
33
  import { AcmeConfigManager } from './acme/manager.acme-config.js';
34
- import { EmailDomainManager, SmartMtaStorageManager, WorkAppMailManager, buildEmailDnsRecords } from './email/index.js';
34
+ import { EmailDomainManager, EmailSettingsManager, SmartMtaStorageManager, WorkAppMailManager, buildEmailDnsRecords } from './email/index.js';
35
35
  import type { IRoute } from '../ts_interfaces/data/route-management.js';
36
- import type { IDcRouterRouteConfig, IRemoteIngressHubSettings, IRemoteIngressPerformanceConfig } from '../ts_interfaces/data/remoteingress.js';
36
+ import type { IEmailPortConfig, IEmailServerSettings, IEmailServerSettingsSeed, TEmailServerSettingsUpdate } from '../ts_interfaces/data/email-settings.js';
37
+ import type { IDcRouterRouteConfig, IRemoteIngressHubSettings, IRemoteIngressPerformanceConfig, TRemoteIngressHubSettingsUpdate } from '../ts_interfaces/data/remoteingress.js';
37
38
  import type { ISecurityCompiledPolicy } from '../ts_interfaces/data/security-policy.js';
38
39
 
39
40
  export interface IDcRouterOptions {
@@ -57,14 +58,7 @@ export interface IDcRouterOptions {
57
58
  * Allows configuring specific ports for email handling
58
59
  * This overrides the default port mapping in the emailConfig
59
60
  */
60
- emailPortConfig?: {
61
- /** External to internal port mapping */
62
- portMapping?: Record<number, number>;
63
- /** Custom port configuration for specific ports */
64
- portSettings?: Record<number, any>;
65
- /** Path to store received emails */
66
- receivedEmailsPath?: string;
67
- };
61
+ emailPortConfig?: IEmailPortConfig;
68
62
 
69
63
  /** TLS/certificate configuration */
70
64
  tls?: {
@@ -282,6 +276,8 @@ export class DcRouter {
282
276
  public remoteIngressManager?: RemoteIngressManager;
283
277
  public tunnelManager?: TunnelManager;
284
278
  private remoteIngressHubLifecycleChain: Promise<void> = Promise.resolve();
279
+ private smartProxyLifecycleChain: Promise<void> = Promise.resolve();
280
+ private emailLifecycleChain: Promise<void> = Promise.resolve();
285
281
  private remoteIngressHubStopping = false;
286
282
  private remoteIngressHubGeneration = 0;
287
283
 
@@ -300,6 +296,7 @@ export class DcRouter {
300
296
 
301
297
  // ACME configuration (DB-backed singleton, replaces tls.contactEmail)
302
298
  public acmeConfigManager?: AcmeConfigManager;
299
+ public emailSettingsManager?: EmailSettingsManager;
303
300
  public emailDomainManager?: EmailDomainManager;
304
301
  public workAppMailManager: WorkAppMailManager;
305
302
  public securityPolicyManager?: SecurityPolicyManager;
@@ -341,7 +338,7 @@ export class DcRouter {
341
338
 
342
339
  // Seed routes assembled during setupSmartProxy, passed to RouteConfigManager for DB seeding
343
340
  private seedConfigRoutes: plugins.smartproxy.IRouteConfig[] = [];
344
- private seedEmailRoutes: plugins.smartproxy.IRouteConfig[] = [];
341
+ private seedEmailRoutes: IDcRouterRouteConfig[] = [];
345
342
  private seedDnsRoutes: plugins.smartproxy.IRouteConfig[] = [];
346
343
  // Live DoH routes used during SmartProxy bootstrap before RouteConfigManager re-applies stored routes.
347
344
  private runtimeDnsRoutes: plugins.smartproxy.IRouteConfig[] = [];
@@ -482,7 +479,7 @@ export class DcRouter {
482
479
  this.serviceManager.addService(
483
480
  new plugins.taskbuffer.Service('EmailDomainManager')
484
481
  .optional()
485
- .dependsOn('DcRouterDb')
482
+ .dependsOn('DcRouterDb', 'EmailSettingsManager')
486
483
  .withStart(async () => {
487
484
  this.emailDomainManager = new EmailDomainManager(this);
488
485
  await this.emailDomainManager.start();
@@ -496,6 +493,28 @@ export class DcRouter {
496
493
  );
497
494
  }
498
495
 
496
+ // EmailSettingsManager: optional, depends on DcRouterDb — owns the DB-backed
497
+ // singleton email server config and projects it into runtime options before
498
+ // SmartProxy and EmailDomainManager read email settings.
499
+ if (this.options.dbConfig?.enabled !== false) {
500
+ this.serviceManager.addService(
501
+ new plugins.taskbuffer.Service('EmailSettingsManager')
502
+ .optional()
503
+ .dependsOn('DcRouterDb')
504
+ .withStart(async () => {
505
+ this.emailSettingsManager = new EmailSettingsManager(this.options);
506
+ await this.emailSettingsManager.start();
507
+ })
508
+ .withStop(async () => {
509
+ if (this.emailSettingsManager) {
510
+ await this.emailSettingsManager.stop();
511
+ this.emailSettingsManager = undefined;
512
+ }
513
+ })
514
+ .withRetry({ maxRetries: 1, baseDelayMs: 500 }),
515
+ );
516
+ }
517
+
499
518
  // SecurityPolicyManager: optional, depends on DcRouterDb — owns IP intelligence
500
519
  // and compiles the global block policy for SmartProxy and remote ingress edges.
501
520
  if (this.options.dbConfig?.enabled !== false) {
@@ -519,13 +538,34 @@ export class DcRouter {
519
538
  );
520
539
  }
521
540
 
541
+ // RemoteIngressManager: optional, depends on DcRouterDb — owns DB-backed
542
+ // hub settings and edge registrations. It starts before SmartProxy so
543
+ // SmartProxy can use the DB-backed enabled flag for PROXY protocol setup.
544
+ if (this.options.dbConfig?.enabled !== false) {
545
+ this.serviceManager.addService(
546
+ new plugins.taskbuffer.Service('RemoteIngressManager')
547
+ .optional()
548
+ .dependsOn('DcRouterDb')
549
+ .withStart(async () => {
550
+ this.remoteIngressManager = new RemoteIngressManager();
551
+ await this.remoteIngressManager.initialize();
552
+ })
553
+ .withStop(async () => {
554
+ this.remoteIngressManager = undefined;
555
+ })
556
+ .withRetry({ maxRetries: 1, baseDelayMs: 500 }),
557
+ );
558
+ }
559
+
522
560
  // SmartProxy: critical, depends on DcRouterDb + DnsManager + AcmeConfigManager (if enabled)
523
561
  const smartProxyDeps: string[] = [];
524
562
  if (this.options.dbConfig?.enabled !== false) {
525
563
  smartProxyDeps.push('DcRouterDb');
526
564
  smartProxyDeps.push('DnsManager');
527
565
  smartProxyDeps.push('AcmeConfigManager');
566
+ smartProxyDeps.push('EmailSettingsManager');
528
567
  smartProxyDeps.push('SecurityPolicyManager');
568
+ smartProxyDeps.push('RemoteIngressManager');
529
569
  }
530
570
  this.serviceManager.addService(
531
571
  new plugins.taskbuffer.Service('SmartProxy')
@@ -535,11 +575,20 @@ export class DcRouter {
535
575
  await this.setupSmartProxy();
536
576
  })
537
577
  .withStop(async () => {
538
- if (this.smartProxy) {
539
- this.smartProxy.removeAllListeners();
540
- await this.smartProxy.stop();
541
- this.smartProxy = undefined;
542
- }
578
+ await this.queueSmartProxyLifecycleTask(async () => {
579
+ try {
580
+ if (this.smartProxy) {
581
+ const existingSmartProxy = this.smartProxy;
582
+ existingSmartProxy.removeAllListeners();
583
+ await existingSmartProxy.stop();
584
+ if (this.smartProxy === existingSmartProxy) {
585
+ this.smartProxy = undefined;
586
+ }
587
+ }
588
+ } finally {
589
+ await this.stopSmartAcme();
590
+ }
591
+ });
543
592
  })
544
593
  .withRetry({ maxRetries: 0 }),
545
594
  );
@@ -630,7 +679,7 @@ export class DcRouter {
630
679
  }
631
680
 
632
681
  // Email Server: optional, depends on SmartProxy
633
- if (this.options.emailConfig) {
682
+ if (this.options.dbConfig?.enabled !== false || this.options.emailConfig) {
634
683
  const emailServiceDeps = ['SmartProxy', 'MetricsManager'];
635
684
  if (this.options.dbConfig?.enabled !== false) {
636
685
  emailServiceDeps.push('EmailDomainManager');
@@ -640,14 +689,18 @@ export class DcRouter {
640
689
  .optional()
641
690
  .dependsOn(...emailServiceDeps)
642
691
  .withStart(async () => {
643
- await this.setupUnifiedEmailHandling();
692
+ await this.queueEmailLifecycleTask(async () => {
693
+ if (!this.options.emailConfig) {
694
+ logger.log('info', 'EmailServer: no email settings configured, skipping startup');
695
+ return;
696
+ }
697
+ await this.setupUnifiedEmailHandling();
698
+ });
644
699
  })
645
700
  .withStop(async () => {
646
- if (this.emailServer) {
647
- this.clearEmailEventSubscriptions();
648
- await this.emailServer.stop();
649
- this.emailServer = undefined;
650
- }
701
+ await this.queueEmailLifecycleTask(async () => {
702
+ await this.stopUnifiedEmailComponents();
703
+ });
651
704
  })
652
705
  .withRetry({ maxRetries: 3, baseDelayMs: 2000, maxDelayMs: 30_000 }),
653
706
  );
@@ -658,7 +711,7 @@ export class DcRouter {
658
711
  this.serviceManager.addService(
659
712
  new plugins.taskbuffer.Service('DnsServer')
660
713
  .optional()
661
- .dependsOn('SmartProxy', ...(this.options.emailConfig ? ['EmailServer'] : []))
714
+ .dependsOn('SmartProxy', ...((this.options.dbConfig?.enabled !== false || this.options.emailConfig) ? ['EmailServer'] : []))
662
715
  .withStart(async () => {
663
716
  await this.setupDnsWithSocketHandler();
664
717
  })
@@ -702,12 +755,14 @@ export class DcRouter {
702
755
  );
703
756
  }
704
757
 
705
- // Remote Ingress: optional, depends on SmartProxy
706
- if (this.options.remoteIngressConfig?.enabled) {
758
+ // Remote Ingress: optional, depends on SmartProxy and DB-backed settings.
759
+ // The service starts as a no-op when the DB setting is disabled, so the UI
760
+ // can still manage edge registrations and hub settings.
761
+ if (this.options.dbConfig?.enabled !== false) {
707
762
  this.serviceManager.addService(
708
763
  new plugins.taskbuffer.Service('RemoteIngress')
709
764
  .optional()
710
- .dependsOn('SmartProxy')
765
+ .dependsOn('SmartProxy', 'RemoteIngressManager')
711
766
  .withStart(async () => {
712
767
  await this.setupRemoteIngress();
713
768
  })
@@ -752,6 +807,42 @@ export class DcRouter {
752
807
  });
753
808
  }
754
809
 
810
+ private isRemoteIngressHubEnabled(): boolean {
811
+ return this.remoteIngressManager?.getHubSettings().enabled
812
+ ?? this.options.remoteIngressConfig?.enabled
813
+ ?? false;
814
+ }
815
+
816
+ private getRemoteIngressHubSettingsLegacySeed(): TRemoteIngressHubSettingsUpdate {
817
+ const remoteIngressConfig = this.options.remoteIngressConfig;
818
+ const seed: TRemoteIngressHubSettingsUpdate = {};
819
+ if (remoteIngressConfig?.enabled !== undefined) {
820
+ seed.enabled = remoteIngressConfig.enabled;
821
+ }
822
+ if (remoteIngressConfig?.tunnelPort !== undefined) {
823
+ seed.tunnelPort = remoteIngressConfig.tunnelPort;
824
+ }
825
+ if (remoteIngressConfig?.hubDomain !== undefined) {
826
+ seed.hubDomain = remoteIngressConfig.hubDomain;
827
+ }
828
+ if (remoteIngressConfig?.performance !== undefined) {
829
+ seed.performance = remoteIngressConfig.performance;
830
+ }
831
+ return seed;
832
+ }
833
+
834
+ private getEmailSettingsLegacySeed(): IEmailServerSettingsSeed {
835
+ const seed: IEmailServerSettingsSeed = {};
836
+ if (this.options.emailConfig) {
837
+ seed.enabled = true;
838
+ seed.emailConfig = JSON.parse(JSON.stringify(this.options.emailConfig));
839
+ }
840
+ if (this.options.emailPortConfig) {
841
+ seed.emailPortConfig = JSON.parse(JSON.stringify(this.options.emailPortConfig));
842
+ }
843
+ return seed;
844
+ }
845
+
755
846
  private startSmartAcmeInBackground(): void {
756
847
  if (!this.smartAcme) {
757
848
  this.smartAcmeReady = false;
@@ -965,10 +1056,11 @@ export class DcRouter {
965
1056
  }
966
1057
 
967
1058
  // Remote Ingress summary
968
- if (this.tunnelManager && this.options.remoteIngressConfig?.enabled) {
1059
+ const remoteIngressHubSettings = this.remoteIngressManager?.getHubSettings();
1060
+ if (this.tunnelManager && remoteIngressHubSettings?.enabled) {
969
1061
  const edgeCount = this.remoteIngressManager?.getAllEdges().length || 0;
970
1062
  const connectedCount = this.tunnelManager.getConnectedCount();
971
- logger.log('info', `Remote Ingress: tunnel port=${this.options.remoteIngressConfig.tunnelPort || 8443}, edges=${edgeCount} registered/${connectedCount} connected`);
1063
+ logger.log('info', `Remote Ingress: tunnel port=${remoteIngressHubSettings.tunnelPort}, edges=${edgeCount} registered/${connectedCount} connected`);
972
1064
  }
973
1065
 
974
1066
  // Database summary
@@ -1013,7 +1105,10 @@ export class DcRouter {
1013
1105
 
1014
1106
  // Run any pending data migrations before anything else reads from the DB.
1015
1107
  // This must complete before ConfigManagers loads profiles.
1016
- const migration = await createMigrationRunner(this.dcRouterDb.getDb(), commitinfo.version);
1108
+ const migration = await createMigrationRunner(this.dcRouterDb.getDb(), commitinfo.version, {
1109
+ remoteIngressHubSettings: this.getRemoteIngressHubSettingsLegacySeed(),
1110
+ emailServerSettings: this.getEmailSettingsLegacySeed(),
1111
+ });
1017
1112
  const migrationResult = await migration.run();
1018
1113
  if (migrationResult.stepsApplied.length > 0) {
1019
1114
  logger.log('info',
@@ -1043,8 +1138,16 @@ export class DcRouter {
1043
1138
 
1044
1139
  // Clean up any existing SmartProxy instance (e.g. from a retry)
1045
1140
  if (this.smartProxy) {
1046
- this.smartProxy.removeAllListeners();
1047
- this.smartProxy = undefined;
1141
+ const existingSmartProxy = this.smartProxy;
1142
+ try {
1143
+ existingSmartProxy.removeAllListeners();
1144
+ await existingSmartProxy.stop();
1145
+ if (this.smartProxy === existingSmartProxy) {
1146
+ this.smartProxy = undefined;
1147
+ }
1148
+ } finally {
1149
+ await this.stopSmartAcme();
1150
+ }
1048
1151
  }
1049
1152
 
1050
1153
  // Assemble serializable seed routes from constructor config — these will be seeded into DB
@@ -1279,7 +1382,7 @@ export class DcRouter {
1279
1382
 
1280
1383
  // When remoteIngress is enabled, the hub binary forwards tunneled connections
1281
1384
  // to SmartProxy with PROXY protocol v1 headers to preserve client IPs.
1282
- if (this.options.remoteIngressConfig?.enabled) {
1385
+ if (this.isRemoteIngressHubEnabled()) {
1283
1386
  smartProxyConfig.acceptProxyProtocol = true;
1284
1387
  if (!smartProxyConfig.proxyIPs) {
1285
1388
  smartProxyConfig.proxyIPs = [];
@@ -1303,16 +1406,17 @@ export class DcRouter {
1303
1406
  // Create SmartProxy instance
1304
1407
  logger.log('info', `Creating SmartProxy instance: routes=${smartProxyConfig.routes?.length}, acme=${smartProxyConfig.acme?.enabled}, certProvisionFunction=${!!smartProxyConfig.certProvisionFunction}`);
1305
1408
 
1306
- this.smartProxy = new plugins.smartproxy.SmartProxy(smartProxyConfig);
1409
+ const smartProxy = new plugins.smartproxy.SmartProxy(smartProxyConfig);
1410
+ this.smartProxy = smartProxy;
1307
1411
 
1308
1412
  // Set up event listeners
1309
- this.smartProxy.on('error', (err) => {
1413
+ smartProxy.on('error', (err) => {
1310
1414
  logger.log('error', `SmartProxy error: ${err.message}`, { stack: err.stack });
1311
1415
  });
1312
1416
 
1313
1417
  // Always listen for certificate events — emitted by both ACME and certProvisionFunction paths
1314
1418
  // Events are keyed by domain for domain-centric certificate tracking
1315
- this.smartProxy.on('certificate-issued', (event: plugins.smartproxy.ICertificateIssuedEvent) => {
1419
+ smartProxy.on('certificate-issued', (event: plugins.smartproxy.ICertificateIssuedEvent) => {
1316
1420
  logger.log('info', `Certificate issued for ${event.domain} via ${event.source}, expires ${event.expiryDate}`);
1317
1421
  const routeNames = this.findRouteNamesForDomain(event.domain);
1318
1422
  this.certificateStatusMap.set(event.domain, {
@@ -1326,7 +1430,7 @@ export class DcRouter {
1326
1430
  // Renewals come through 'certificate-issued' (with optional isRenewal? in the payload).
1327
1431
  // The vestigial 'certificate-renewed' event from common-types.ts is never emitted.
1328
1432
 
1329
- this.smartProxy.on('certificate-failed', (event: plugins.smartproxy.ICertificateFailedEvent) => {
1433
+ smartProxy.on('certificate-failed', (event: plugins.smartproxy.ICertificateFailedEvent) => {
1330
1434
  logger.log('error', `Certificate failed for ${event.domain} (${event.source}): ${event.error}`);
1331
1435
  const routeNames = this.findRouteNamesForDomain(event.domain);
1332
1436
  this.certificateStatusMap.set(event.domain, {
@@ -1337,7 +1441,23 @@ export class DcRouter {
1337
1441
 
1338
1442
  // Start SmartProxy
1339
1443
  logger.log('info', 'Starting SmartProxy...');
1340
- await this.smartProxy.start();
1444
+ try {
1445
+ await smartProxy.start();
1446
+ } catch (err) {
1447
+ smartProxy.removeAllListeners();
1448
+ if (this.smartProxy === smartProxy) {
1449
+ this.smartProxy = undefined;
1450
+ }
1451
+ await this.stopSmartAcme();
1452
+ if (this.certProvisionScheduler) {
1453
+ this.certProvisionScheduler.clear();
1454
+ this.certProvisionScheduler = undefined;
1455
+ }
1456
+ await smartProxy.stop().catch((stopErr) => {
1457
+ logger.log('warn', `Failed to clean up SmartProxy after startup failure: ${(stopErr as Error).message}`);
1458
+ });
1459
+ throw err;
1460
+ }
1341
1461
  logger.log('info', 'SmartProxy started successfully');
1342
1462
 
1343
1463
  // Populate certificateStatusMap for certs loaded from store at startup
@@ -1460,8 +1580,8 @@ export class DcRouter {
1460
1580
  /**
1461
1581
  * Generate SmartProxy routes for email configuration
1462
1582
  */
1463
- private generateEmailRoutes(emailConfig: IUnifiedEmailServerOptions): plugins.smartproxy.IRouteConfig[] {
1464
- const emailRoutes: plugins.smartproxy.IRouteConfig[] = [];
1583
+ private generateEmailRoutes(emailConfig: IUnifiedEmailServerOptions): IDcRouterRouteConfig[] {
1584
+ const emailRoutes: IDcRouterRouteConfig[] = [];
1465
1585
 
1466
1586
  // Create routes for each email port
1467
1587
  for (const port of emailConfig.ports) {
@@ -1535,13 +1655,17 @@ export class DcRouter {
1535
1655
  }
1536
1656
 
1537
1657
  // Create the route configuration
1538
- const routeConfig: plugins.smartproxy.IRouteConfig = {
1658
+ const routeConfig: IDcRouterRouteConfig = {
1539
1659
  name: routeName,
1540
1660
  match: {
1541
1661
  ports: [port]
1542
1662
  },
1543
1663
  action: action
1544
1664
  };
1665
+
1666
+ if (this.isRemoteIngressHubEnabled()) {
1667
+ routeConfig.remoteIngress = { enabled: true };
1668
+ }
1545
1669
 
1546
1670
  // Add the route to our list
1547
1671
  emailRoutes.push(routeConfig);
@@ -1768,19 +1892,33 @@ export class DcRouter {
1768
1892
  });
1769
1893
 
1770
1894
  // Create unified email server
1771
- this.emailServer = new UnifiedEmailServer(this, emailConfig);
1895
+ const emailServer = new UnifiedEmailServer(this, emailConfig);
1896
+ this.emailServer = emailServer;
1772
1897
  this.clearEmailEventSubscriptions();
1773
1898
 
1774
1899
  // Set up error handling
1775
- this.addEmailEventSubscription(this.emailServer, 'error', (err: Error) => {
1900
+ this.addEmailEventSubscription(emailServer, 'error', (err: Error) => {
1776
1901
  logger.log('error', `UnifiedEmailServer error: ${err.message}`);
1777
1902
  });
1778
1903
 
1779
1904
  // Start the server
1780
- await this.emailServer.start();
1905
+ try {
1906
+ await emailServer.start();
1907
+ } catch (error: unknown) {
1908
+ this.clearEmailEventSubscriptions();
1909
+ try {
1910
+ await emailServer.stop();
1911
+ } catch (stopError: unknown) {
1912
+ logger.log('warn', `Error cleaning up failed UnifiedEmailServer start: ${(stopError as Error).message}`);
1913
+ }
1914
+ if (this.emailServer === emailServer) {
1915
+ this.emailServer = undefined;
1916
+ }
1917
+ throw error;
1918
+ }
1781
1919
 
1782
1920
  // Wire delivery events to MetricsManager and logger using smartmta's public queue APIs.
1783
- if (this.metricsManager && this.emailServer) {
1921
+ if (this.metricsManager) {
1784
1922
  const getEnvelope = (item: { processingResult?: any; lastError?: string }) => {
1785
1923
  const emailLike = item?.processingResult;
1786
1924
  const from = emailLike?.from || emailLike?.email?.from || '';
@@ -1795,34 +1933,34 @@ export class DcRouter {
1795
1933
  };
1796
1934
  };
1797
1935
  const updateQueueSize = () => {
1798
- this.metricsManager!.updateQueueSize(this.emailServer!.getQueueStats().queueSize);
1936
+ this.metricsManager!.updateQueueSize(emailServer.getQueueStats().queueSize);
1799
1937
  };
1800
1938
 
1801
- this.addEmailEventSubscription(this.emailServer.deliveryQueue, 'itemEnqueued', (item: any) => {
1939
+ this.addEmailEventSubscription(emailServer.deliveryQueue, 'itemEnqueued', (item: any) => {
1802
1940
  const envelope = getEnvelope(item);
1803
1941
  this.metricsManager!.trackEmailReceived(envelope.from);
1804
1942
  updateQueueSize();
1805
1943
  logger.log('info', `Email queued: ${envelope.from} → ${envelope.recipients.join(', ') || 'unknown'}`, { zone: 'email' });
1806
1944
  });
1807
- this.addEmailEventSubscription(this.emailServer.deliveryQueue, 'itemDelivered', (item: any) => {
1945
+ this.addEmailEventSubscription(emailServer.deliveryQueue, 'itemDelivered', (item: any) => {
1808
1946
  const envelope = getEnvelope(item);
1809
1947
  this.metricsManager!.trackEmailSent(envelope.recipients[0]);
1810
1948
  updateQueueSize();
1811
1949
  logger.log('info', `Email delivered to ${envelope.recipients.join(', ') || 'unknown'}`, { zone: 'email' });
1812
1950
  });
1813
- this.addEmailEventSubscription(this.emailServer.deliveryQueue, 'itemFailed', (item: any) => {
1951
+ this.addEmailEventSubscription(emailServer.deliveryQueue, 'itemFailed', (item: any) => {
1814
1952
  const envelope = getEnvelope(item);
1815
1953
  this.metricsManager!.trackEmailFailed(envelope.recipients[0], item?.lastError);
1816
1954
  updateQueueSize();
1817
1955
  logger.log('warn', `Email delivery failed to ${envelope.recipients.join(', ') || 'unknown'}: ${item?.lastError || 'unknown error'}`, { zone: 'email' });
1818
1956
  });
1819
- this.addEmailEventSubscription(this.emailServer.deliveryQueue, 'itemDeferred', () => {
1957
+ this.addEmailEventSubscription(emailServer.deliveryQueue, 'itemDeferred', () => {
1820
1958
  updateQueueSize();
1821
1959
  });
1822
- this.addEmailEventSubscription(this.emailServer.deliveryQueue, 'itemRemoved', () => {
1960
+ this.addEmailEventSubscription(emailServer.deliveryQueue, 'itemRemoved', () => {
1823
1961
  updateQueueSize();
1824
1962
  });
1825
- this.addEmailEventSubscription(this.emailServer, 'bounceProcessed', () => {
1963
+ this.addEmailEventSubscription(emailServer, 'bounceProcessed', () => {
1826
1964
  this.metricsManager!.trackEmailBounced();
1827
1965
  logger.log('warn', 'Email bounce processed', { zone: 'email' });
1828
1966
  });
@@ -1837,16 +1975,57 @@ export class DcRouter {
1837
1975
  * @param config New email configuration
1838
1976
  */
1839
1977
  public async updateEmailConfig(config: IUnifiedEmailServerOptions): Promise<void> {
1840
- // Stop existing email components
1841
- await this.stopUnifiedEmailComponents();
1842
-
1843
- // Update configuration
1844
- this.options.emailConfig = config;
1845
-
1846
- // Start email handling with new configuration
1847
- await this.setupUnifiedEmailHandling();
1848
-
1849
- logger.log('info', 'Unified email configuration updated');
1978
+ await this.queueEmailLifecycleTask(async () => {
1979
+ // Stop existing email components
1980
+ await this.stopUnifiedEmailComponents();
1981
+
1982
+ // Update configuration
1983
+ this.options.emailConfig = config;
1984
+ this.emailDomainManager?.setBaseEmailDomains(config.domains as IEmailDomainConfig[] | undefined);
1985
+ await this.emailDomainManager?.syncManagedDomainsToRuntime();
1986
+
1987
+ // Start email handling with new configuration
1988
+ await this.setupUnifiedEmailHandling();
1989
+
1990
+ logger.log('info', 'Unified email configuration updated');
1991
+ });
1992
+ }
1993
+
1994
+ public async updateEmailServerSettings(
1995
+ settings: TEmailServerSettingsUpdate,
1996
+ updatedBy = 'system',
1997
+ ): Promise<IEmailServerSettings> {
1998
+ return await this.queueEmailLifecycleTask(async () => {
1999
+ if (!this.emailSettingsManager) {
2000
+ throw new Error('EmailSettingsManager is not initialized');
2001
+ }
2002
+
2003
+ const updatedSettings = await this.emailSettingsManager.updateSettings(settings, updatedBy);
2004
+ this.emailDomainManager?.setBaseEmailDomains(this.options.emailConfig?.domains as IEmailDomainConfig[] | undefined);
2005
+ await this.emailDomainManager?.syncManagedDomainsToRuntime();
2006
+ this.seedEmailRoutes = this.options.emailConfig
2007
+ ? this.generateEmailRoutes(this.options.emailConfig)
2008
+ : [];
2009
+
2010
+ if (this.routeConfigManager) {
2011
+ await this.routeConfigManager.initialize(
2012
+ this.seedConfigRoutes as import('../ts_interfaces/data/remoteingress.js').IDcRouterRouteConfig[],
2013
+ this.seedEmailRoutes as import('../ts_interfaces/data/remoteingress.js').IDcRouterRouteConfig[],
2014
+ this.seedDnsRoutes as import('../ts_interfaces/data/remoteingress.js').IDcRouterRouteConfig[],
2015
+ );
2016
+ }
2017
+
2018
+ if (this.options.emailConfig) {
2019
+ if (this.emailServer) {
2020
+ await this.stopUnifiedEmailComponents();
2021
+ }
2022
+ await this.setupUnifiedEmailHandling();
2023
+ } else if (this.emailServer) {
2024
+ await this.stopUnifiedEmailComponents();
2025
+ }
2026
+
2027
+ return updatedSettings;
2028
+ });
1850
2029
  }
1851
2030
 
1852
2031
  /**
@@ -2438,7 +2617,14 @@ export class DcRouter {
2438
2617
  * Set up Remote Ingress hub for edge tunnel connections
2439
2618
  */
2440
2619
  private async setupRemoteIngress(): Promise<void> {
2441
- if (!this.options.remoteIngressConfig?.enabled) {
2620
+ const remoteIngressManager = this.remoteIngressManager;
2621
+ if (!remoteIngressManager) {
2622
+ return;
2623
+ }
2624
+
2625
+ const hubSettings = remoteIngressManager.getHubSettings();
2626
+ if (!hubSettings.enabled) {
2627
+ logger.log('info', 'Remote Ingress hub is disabled in DB settings');
2442
2628
  return;
2443
2629
  }
2444
2630
 
@@ -2446,14 +2632,6 @@ export class DcRouter {
2446
2632
  this.remoteIngressHubStopping = false;
2447
2633
  const generation = ++this.remoteIngressHubGeneration;
2448
2634
 
2449
- // Initialize the edge registration manager
2450
- const remoteIngressManager = new RemoteIngressManager(this.options.remoteIngressConfig.performance);
2451
- this.remoteIngressManager = remoteIngressManager;
2452
- await remoteIngressManager.initialize();
2453
- if (!this.isRemoteIngressHubGenerationCurrent(generation, remoteIngressManager)) {
2454
- return;
2455
- }
2456
-
2457
2635
  const firewallConfig = await this.securityPolicyManager?.compileRemoteIngressFirewall();
2458
2636
  if (!this.isRemoteIngressHubGenerationCurrent(generation, remoteIngressManager)) {
2459
2637
  return;
@@ -2483,7 +2661,7 @@ export class DcRouter {
2483
2661
  }
2484
2662
 
2485
2663
  const edgeCount = remoteIngressManager.getAllEdges().length;
2486
- logger.log('info', `Remote Ingress hub started on port ${this.options.remoteIngressConfig.tunnelPort || 8443} with ${edgeCount} registered edge(s)`);
2664
+ logger.log('info', `Remote Ingress hub started on port ${hubSettings.tunnelPort} with ${edgeCount} registered edge(s)`);
2487
2665
  }
2488
2666
 
2489
2667
  private isRemoteIngressHubGenerationCurrent(generation: number, manager: RemoteIngressManager): boolean {
@@ -2498,17 +2676,30 @@ export class DcRouter {
2498
2676
  return run;
2499
2677
  }
2500
2678
 
2679
+ private queueSmartProxyLifecycleTask<T>(task: () => Promise<T>): Promise<T> {
2680
+ const run = this.smartProxyLifecycleChain.then(task);
2681
+ this.smartProxyLifecycleChain = run.then(() => undefined, () => undefined);
2682
+ return run;
2683
+ }
2684
+
2685
+ private queueEmailLifecycleTask<T>(task: () => Promise<T>): Promise<T> {
2686
+ const run = this.emailLifecycleChain.then(task);
2687
+ this.emailLifecycleChain = run.then(() => undefined, () => undefined);
2688
+ return run;
2689
+ }
2690
+
2501
2691
  private async stopRemoteIngress(): Promise<void> {
2502
2692
  this.remoteIngressHubStopping = true;
2503
2693
  this.remoteIngressHubGeneration++;
2504
2694
  await this.queueRemoteIngressHubTask(async () => {
2505
2695
  const currentTunnelManager = this.tunnelManager;
2506
- this.tunnelManager = undefined;
2507
2696
  if (currentTunnelManager) {
2508
2697
  await currentTunnelManager.stop();
2698
+ if (this.tunnelManager === currentTunnelManager) {
2699
+ this.tunnelManager = undefined;
2700
+ }
2509
2701
  }
2510
2702
  });
2511
- this.remoteIngressManager = undefined;
2512
2703
  }
2513
2704
 
2514
2705
  public async mutateRemoteIngressEdges<T>(
@@ -2544,35 +2735,96 @@ export class DcRouter {
2544
2735
  }
2545
2736
 
2546
2737
  public async updateRemoteIngressHubSettings(
2547
- updates: { performance?: IRemoteIngressPerformanceConfig },
2738
+ updates: TRemoteIngressHubSettingsUpdate,
2548
2739
  updatedBy: string,
2549
2740
  ): Promise<IRemoteIngressHubSettings> {
2550
- return await this.queueRemoteIngressHubTask(async () => {
2551
- if (this.remoteIngressHubStopping) {
2552
- throw new Error('RemoteIngress is stopping');
2553
- }
2554
- if (!this.remoteIngressManager) {
2555
- throw new Error('RemoteIngress is not configured');
2556
- }
2741
+ const manager = this.remoteIngressManager;
2742
+ if (!manager) {
2743
+ throw new Error('RemoteIngress is not configured');
2744
+ }
2745
+
2746
+ const previousSettings = manager.getHubSettings();
2747
+ const settings = await manager.updateHubSettings(updates, updatedBy);
2748
+ const enabledChanged = previousSettings.enabled !== settings.enabled;
2749
+
2750
+ if (!settings.enabled) {
2751
+ await this.queueRemoteIngressHubTask(async () => {
2752
+ await this.stopRemoteIngressTunnelHubLocked();
2753
+ });
2754
+ }
2557
2755
 
2558
- const settings = await this.remoteIngressManager.updateHubSettings(updates, updatedBy);
2559
- if (this.options.remoteIngressConfig?.enabled) {
2756
+ if (enabledChanged) {
2757
+ await this.restartSmartProxyForRemoteIngressSettings();
2758
+ }
2759
+
2760
+ if (settings.enabled) {
2761
+ await this.queueRemoteIngressHubTask(async () => {
2560
2762
  await this.restartRemoteIngressTunnelHubLocked();
2763
+ });
2764
+ }
2765
+
2766
+ return settings;
2767
+ }
2768
+
2769
+ private async restartSmartProxyForRemoteIngressSettings(): Promise<void> {
2770
+ await this.queueSmartProxyLifecycleTask(async () => {
2771
+ const restartSmartProxy = async () => {
2772
+ try {
2773
+ if (this.smartProxy) {
2774
+ const existingSmartProxy = this.smartProxy;
2775
+ existingSmartProxy.removeAllListeners();
2776
+ await existingSmartProxy.stop();
2777
+ if (this.smartProxy === existingSmartProxy) {
2778
+ this.smartProxy = undefined;
2779
+ }
2780
+ }
2781
+ } finally {
2782
+ await this.stopSmartAcme();
2783
+ }
2784
+ await this.setupSmartProxy();
2785
+ };
2786
+
2787
+ if (this.routeConfigManager) {
2788
+ await this.routeConfigManager.runExclusiveRouteUpdate(restartSmartProxy);
2789
+ } else {
2790
+ await restartSmartProxy();
2791
+ }
2792
+
2793
+ if (!this.routeConfigManager) {
2794
+ return;
2561
2795
  }
2562
- return settings;
2796
+ await this.routeConfigManager.initialize(
2797
+ this.seedConfigRoutes as IDcRouterRouteConfig[],
2798
+ this.seedEmailRoutes as IDcRouterRouteConfig[],
2799
+ this.seedDnsRoutes as IDcRouterRouteConfig[],
2800
+ );
2563
2801
  });
2564
2802
  }
2565
2803
 
2804
+ private async stopRemoteIngressTunnelHubLocked(): Promise<void> {
2805
+ this.remoteIngressHubGeneration++;
2806
+ const currentTunnelManager = this.tunnelManager;
2807
+ if (currentTunnelManager) {
2808
+ await currentTunnelManager.stop();
2809
+ if (this.tunnelManager === currentTunnelManager) {
2810
+ this.tunnelManager = undefined;
2811
+ }
2812
+ }
2813
+ }
2814
+
2566
2815
  private async restartRemoteIngressTunnelHubLocked(): Promise<void> {
2567
2816
  const generation = ++this.remoteIngressHubGeneration;
2568
- if (!this.remoteIngressManager || !this.options.remoteIngressConfig?.enabled || this.remoteIngressHubStopping) {
2817
+ const hubSettings = this.remoteIngressManager?.getHubSettings();
2818
+ if (!this.remoteIngressManager || !hubSettings?.enabled || this.remoteIngressHubStopping) {
2569
2819
  return;
2570
2820
  }
2571
2821
 
2572
2822
  const currentTunnelManager = this.tunnelManager;
2573
- this.tunnelManager = undefined;
2574
2823
  if (currentTunnelManager) {
2575
2824
  await currentTunnelManager.stop();
2825
+ if (this.tunnelManager === currentTunnelManager) {
2826
+ this.tunnelManager = undefined;
2827
+ }
2576
2828
  }
2577
2829
 
2578
2830
  if (this.remoteIngressHubStopping || generation !== this.remoteIngressHubGeneration) {
@@ -2582,19 +2834,25 @@ export class DcRouter {
2582
2834
  }
2583
2835
 
2584
2836
  private async startRemoteIngressTunnelHubLocked(generation: number): Promise<void> {
2585
- const riCfg = this.options.remoteIngressConfig;
2586
2837
  const manager = this.remoteIngressManager;
2587
- if (!riCfg?.enabled || !manager || this.remoteIngressHubStopping || generation !== this.remoteIngressHubGeneration) {
2838
+ const hubSettings = manager?.getHubSettings();
2839
+ if (!manager || !hubSettings?.enabled || this.remoteIngressHubStopping || generation !== this.remoteIngressHubGeneration) {
2588
2840
  return;
2589
2841
  }
2590
2842
 
2591
- const tlsConfig = await this.resolveRemoteIngressTlsConfig(riCfg);
2843
+ const firewallConfig = await this.securityPolicyManager?.compileRemoteIngressFirewall();
2844
+ if (this.remoteIngressHubStopping || generation !== this.remoteIngressHubGeneration || this.remoteIngressManager !== manager) {
2845
+ return;
2846
+ }
2847
+ manager.setFirewallConfig(firewallConfig);
2848
+
2849
+ const tlsConfig = await this.resolveRemoteIngressTlsConfig(hubSettings.hubDomain);
2592
2850
  if (this.remoteIngressHubStopping || generation !== this.remoteIngressHubGeneration || this.remoteIngressManager !== manager) {
2593
2851
  return;
2594
2852
  }
2595
2853
 
2596
2854
  const tunnelManager = new TunnelManager(manager, {
2597
- tunnelPort: riCfg.tunnelPort ?? 8443,
2855
+ tunnelPort: hubSettings.tunnelPort,
2598
2856
  targetHost: '127.0.0.1',
2599
2857
  tls: tlsConfig,
2600
2858
  performance: manager.getHubPerformanceConfig(),
@@ -2607,23 +2865,26 @@ export class DcRouter {
2607
2865
  }
2608
2866
 
2609
2867
  if (this.remoteIngressHubStopping || generation !== this.remoteIngressHubGeneration || this.remoteIngressManager !== manager) {
2610
- await tunnelManager.stop();
2868
+ await tunnelManager.stop().catch((err) => {
2869
+ logger.log('warn', `Failed to stop stale RemoteIngress tunnel hub: ${(err as Error).message}`);
2870
+ });
2611
2871
  return;
2612
2872
  }
2613
2873
  this.tunnelManager = tunnelManager;
2614
2874
  }
2615
2875
 
2616
2876
  private async resolveRemoteIngressTlsConfig(
2617
- riCfg: NonNullable<IDcRouterOptions['remoteIngressConfig']>,
2877
+ hubDomain?: string,
2618
2878
  ): Promise<{ certPem: string; keyPem: string } | undefined> {
2619
2879
  // Resolve TLS certs for tunnel: explicit paths > ACME for hubDomain > self-signed (Rust default)
2620
2880
  let tlsConfig: { certPem: string; keyPem: string } | undefined;
2621
2881
 
2622
2882
  // Priority 1: Explicit cert/key file paths
2623
- if (riCfg.tls?.certPath && riCfg.tls?.keyPath) {
2883
+ const explicitTls = this.options.remoteIngressConfig?.tls;
2884
+ if (explicitTls?.certPath && explicitTls?.keyPath) {
2624
2885
  try {
2625
- const certPem = plugins.fs.readFileSync(riCfg.tls.certPath, 'utf8');
2626
- const keyPem = plugins.fs.readFileSync(riCfg.tls.keyPath, 'utf8');
2886
+ const certPem = plugins.fs.readFileSync(explicitTls.certPath, 'utf8');
2887
+ const keyPem = plugins.fs.readFileSync(explicitTls.keyPath, 'utf8');
2627
2888
  tlsConfig = { certPem, keyPem };
2628
2889
  logger.log('info', 'Using explicit TLS cert/key for RemoteIngress tunnel');
2629
2890
  } catch (err: unknown) {
@@ -2632,12 +2893,12 @@ export class DcRouter {
2632
2893
  }
2633
2894
 
2634
2895
  // Priority 2: Existing cert from SmartProxy cert store for hubDomain
2635
- if (!tlsConfig && riCfg.hubDomain) {
2896
+ if (!tlsConfig && hubDomain) {
2636
2897
  try {
2637
- const stored = await ProxyCertDoc.findByDomain(riCfg.hubDomain);
2898
+ const stored = await ProxyCertDoc.findByDomain(hubDomain);
2638
2899
  if (stored?.publicKey && stored?.privateKey) {
2639
2900
  tlsConfig = { certPem: stored.publicKey, keyPem: stored.privateKey };
2640
- logger.log('info', `Using stored ACME cert for RemoteIngress tunnel TLS: ${riCfg.hubDomain}`);
2901
+ logger.log('info', `Using stored ACME cert for RemoteIngress tunnel TLS: ${hubDomain}`);
2641
2902
  }
2642
2903
  } catch { /* no stored cert, fall through */ }
2643
2904
  }