@serve.zone/dcrouter 13.40.3 → 13.41.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 (34) hide show
  1. package/deno.json +1 -1
  2. package/dist_serve/bundle.js +590 -546
  3. package/dist_ts/00_commitinfo_data.js +1 -1
  4. package/dist_ts/classes.dcrouter.d.ts +15 -0
  5. package/dist_ts/classes.dcrouter.js +156 -36
  6. package/dist_ts/db/documents/classes.remote-ingress-hub-settings.doc.d.ts +10 -0
  7. package/dist_ts/db/documents/classes.remote-ingress-hub-settings.doc.js +88 -0
  8. package/dist_ts/db/documents/index.d.ts +1 -0
  9. package/dist_ts/db/documents/index.js +2 -1
  10. package/dist_ts/opsserver/handlers/config.handler.js +2 -2
  11. package/dist_ts/opsserver/handlers/remoteingress.handler.js +71 -55
  12. package/dist_ts/remoteingress/classes.remoteingress-manager.d.ts +12 -2
  13. package/dist_ts/remoteingress/classes.remoteingress-manager.js +136 -3
  14. package/dist_ts/remoteingress/classes.tunnel-manager.d.ts +2 -0
  15. package/dist_ts/remoteingress/classes.tunnel-manager.js +45 -13
  16. package/dist_ts_interfaces/data/remoteingress.d.ts +5 -0
  17. package/dist_ts_interfaces/requests/remoteingress.d.ts +30 -1
  18. package/dist_ts_web/00_commitinfo_data.js +1 -1
  19. package/dist_ts_web/appstate.d.ts +4 -0
  20. package/dist_ts_web/appstate.js +30 -2
  21. package/dist_ts_web/elements/network/ops-view-remoteingress.d.ts +4 -0
  22. package/dist_ts_web/elements/network/ops-view-remoteingress.js +148 -1
  23. package/package.json +1 -1
  24. package/ts/00_commitinfo_data.ts +1 -1
  25. package/ts/classes.dcrouter.ts +178 -38
  26. package/ts/db/documents/classes.remote-ingress-hub-settings.doc.ts +29 -0
  27. package/ts/db/documents/index.ts +1 -0
  28. package/ts/opsserver/handlers/config.handler.ts +1 -1
  29. package/ts/opsserver/handlers/remoteingress.handler.ts +97 -74
  30. package/ts/remoteingress/classes.remoteingress-manager.ts +172 -3
  31. package/ts/remoteingress/classes.tunnel-manager.ts +41 -14
  32. package/ts_web/00_commitinfo_data.ts +1 -1
  33. package/ts_web/appstate.ts +41 -1
  34. package/ts_web/elements/network/ops-view-remoteingress.ts +164 -0
@@ -52,30 +52,21 @@ export class RemoteIngressHandler {
52
52
  scope: 'remote-ingress:write',
53
53
  requireAdminIdentity: true,
54
54
  });
55
- const manager = this.opsServerRef.dcRouterRef.remoteIngressManager;
56
- const tunnelManager = this.opsServerRef.dcRouterRef.tunnelManager;
57
-
58
- if (!manager) {
55
+ try {
56
+ const edge = await this.opsServerRef.dcRouterRef.mutateRemoteIngressEdges((manager) => manager.createEdge(
57
+ dataArg.name,
58
+ dataArg.listenPorts || [],
59
+ dataArg.tags,
60
+ dataArg.autoDerivePorts ?? true,
61
+ dataArg.performance,
62
+ ));
63
+ return { success: true, edge };
64
+ } catch (err: unknown) {
59
65
  return {
60
66
  success: false,
61
67
  edge: null as any,
62
68
  };
63
69
  }
64
-
65
- const edge = await manager.createEdge(
66
- dataArg.name,
67
- dataArg.listenPorts || [],
68
- dataArg.tags,
69
- dataArg.autoDerivePorts ?? true,
70
- dataArg.performance,
71
- );
72
-
73
- // Sync allowed edges with the hub
74
- if (tunnelManager) {
75
- await tunnelManager.syncAllowedEdges();
76
- }
77
-
78
- return { success: true, edge };
79
70
  },
80
71
  ),
81
72
  );
@@ -89,21 +80,18 @@ export class RemoteIngressHandler {
89
80
  scope: 'remote-ingress:write',
90
81
  requireAdminIdentity: true,
91
82
  });
92
- const manager = this.opsServerRef.dcRouterRef.remoteIngressManager;
93
- const tunnelManager = this.opsServerRef.dcRouterRef.tunnelManager;
94
-
95
- if (!manager) {
96
- return { success: false, message: 'RemoteIngress not configured' };
97
- }
98
-
99
- const deleted = await manager.deleteEdge(dataArg.id);
100
- if (deleted && tunnelManager) {
101
- await tunnelManager.syncAllowedEdges();
102
- }
83
+ const deleted = await this.opsServerRef.dcRouterRef.mutateRemoteIngressEdges(
84
+ (manager) => manager.deleteEdge(dataArg.id),
85
+ ).catch((err: unknown) => {
86
+ if ((err as Error).message.includes('RemoteIngress')) {
87
+ return false;
88
+ }
89
+ throw err;
90
+ });
103
91
 
104
92
  return {
105
93
  success: deleted,
106
- message: deleted ? undefined : 'Edge not found',
94
+ message: deleted ? undefined : 'Edge not found or RemoteIngress not configured',
107
95
  };
108
96
  },
109
97
  ),
@@ -118,42 +106,42 @@ export class RemoteIngressHandler {
118
106
  scope: 'remote-ingress:write',
119
107
  requireAdminIdentity: true,
120
108
  });
121
- const manager = this.opsServerRef.dcRouterRef.remoteIngressManager;
122
- const tunnelManager = this.opsServerRef.dcRouterRef.tunnelManager;
123
-
124
- if (!manager) {
125
- return { success: false, edge: null as any };
126
- }
127
-
128
- const edge = await manager.updateEdge(dataArg.id, {
129
- name: dataArg.name,
130
- listenPorts: dataArg.listenPorts,
131
- autoDerivePorts: dataArg.autoDerivePorts,
132
- enabled: dataArg.enabled,
133
- performance: dataArg.performance,
134
- tags: dataArg.tags,
135
- });
136
-
137
- if (!edge) {
138
- return { success: false, edge: null as any };
139
- }
140
-
141
- // Sync allowed edges — ports, tags, or enabled may have changed
142
- if (tunnelManager) {
143
- await tunnelManager.syncAllowedEdges();
144
- }
145
-
146
- const breakdown = manager.getPortBreakdown(edge);
147
- return {
148
- success: true,
149
- edge: {
109
+ const result = await this.opsServerRef.dcRouterRef.mutateRemoteIngressEdges(async (manager) => {
110
+ const edge = await manager.updateEdge(dataArg.id, {
111
+ name: dataArg.name,
112
+ listenPorts: dataArg.listenPorts,
113
+ autoDerivePorts: dataArg.autoDerivePorts,
114
+ enabled: dataArg.enabled,
115
+ performance: dataArg.performance,
116
+ tags: dataArg.tags,
117
+ });
118
+
119
+ if (!edge) {
120
+ return null;
121
+ }
122
+
123
+ const breakdown = manager.getPortBreakdown(edge);
124
+ return {
150
125
  ...edge,
151
126
  secret: '********',
152
127
  effectiveListenPorts: manager.getEffectiveListenPorts(edge),
153
128
  effectiveListenPortsUdp: manager.getEffectiveListenPortsUdp(edge),
154
129
  manualPorts: breakdown.manual,
155
130
  derivedPorts: breakdown.derived,
156
- },
131
+ };
132
+ }).catch((err: unknown) => {
133
+ if ((err as Error).message.includes('RemoteIngress')) {
134
+ return null;
135
+ }
136
+ throw err;
137
+ });
138
+
139
+ if (!result) {
140
+ return { success: false, edge: null as any };
141
+ }
142
+ return {
143
+ success: true,
144
+ edge: result,
157
145
  };
158
146
  },
159
147
  ),
@@ -168,23 +156,18 @@ export class RemoteIngressHandler {
168
156
  scope: 'remote-ingress:write',
169
157
  requireAdminIdentity: true,
170
158
  });
171
- const manager = this.opsServerRef.dcRouterRef.remoteIngressManager;
172
- const tunnelManager = this.opsServerRef.dcRouterRef.tunnelManager;
173
-
174
- if (!manager) {
175
- return { success: false, secret: '' };
176
- }
177
-
178
- const secret = await manager.regenerateSecret(dataArg.id);
159
+ const secret = await this.opsServerRef.dcRouterRef.mutateRemoteIngressEdges(
160
+ (manager) => manager.regenerateSecret(dataArg.id),
161
+ ).catch((err: unknown) => {
162
+ if ((err as Error).message.includes('RemoteIngress')) {
163
+ return null;
164
+ }
165
+ throw err;
166
+ });
179
167
  if (!secret) {
180
168
  return { success: false, secret: '' };
181
169
  }
182
170
 
183
- // Sync allowed edges since secret changed
184
- if (tunnelManager) {
185
- await tunnelManager.syncAllowedEdges();
186
- }
187
-
188
171
  return { success: true, secret };
189
172
  },
190
173
  ),
@@ -205,6 +188,46 @@ export class RemoteIngressHandler {
205
188
  ),
206
189
  );
207
190
 
191
+ // Get hub-level settings (read)
192
+ viewRouter.addTypedHandler(
193
+ new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRemoteIngressHubSettings>(
194
+ 'getRemoteIngressHubSettings',
195
+ async (dataArg, toolsArg) => {
196
+ await requireOpsAuth(this.opsServerRef, dataArg, { scope: 'remote-ingress:read' });
197
+ const manager = this.opsServerRef.dcRouterRef.remoteIngressManager;
198
+ return {
199
+ settings: manager?.getHubSettings() || {
200
+ updatedAt: 0,
201
+ updatedBy: 'default',
202
+ },
203
+ };
204
+ },
205
+ ),
206
+ );
207
+
208
+ // Update hub-level settings (write)
209
+ adminRouter.addTypedHandler(
210
+ new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_UpdateRemoteIngressHubSettings>(
211
+ 'updateRemoteIngressHubSettings',
212
+ async (dataArg, toolsArg) => {
213
+ const auth = await requireOpsAuth(this.opsServerRef, dataArg, {
214
+ scope: 'remote-ingress:write',
215
+ requireAdminIdentity: true,
216
+ });
217
+
218
+ try {
219
+ const settings = await this.opsServerRef.dcRouterRef.updateRemoteIngressHubSettings(
220
+ { performance: dataArg.performance },
221
+ auth.userId,
222
+ );
223
+ return { success: true, settings };
224
+ } catch (err: unknown) {
225
+ return { success: false, message: (err as Error).message };
226
+ }
227
+ },
228
+ ),
229
+ );
230
+
208
231
  // Get a connection token for an edge (write — exposes secret)
209
232
  adminRouter.addTypedHandler(
210
233
  new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetRemoteIngressConnectionToken>(
@@ -1,11 +1,36 @@
1
1
  import * as plugins from '../plugins.js';
2
- import type { IRemoteIngress, IRemoteIngressPerformanceConfig, IDcRouterRouteConfig } from '../../ts_interfaces/data/remoteingress.js';
3
- import { RemoteIngressEdgeDoc } from '../db/index.js';
2
+ import type { IDcRouterRouteConfig, IRemoteIngress, IRemoteIngressHubSettings, IRemoteIngressPerformanceConfig, TRemoteIngressPerformanceProfile } from '../../ts_interfaces/data/remoteingress.js';
3
+ import { RemoteIngressEdgeDoc, RemoteIngressHubSettingsDoc } from '../db/index.js';
4
4
 
5
5
  interface IRemoteIngressFirewallConfig {
6
6
  blockedIps?: string[];
7
7
  }
8
8
 
9
+ type TPerformanceIntegerField =
10
+ | 'maxStreamsPerEdge'
11
+ | 'totalWindowBudgetBytes'
12
+ | 'minStreamWindowBytes'
13
+ | 'maxStreamWindowBytes'
14
+ | 'sustainedStreamWindowBytes'
15
+ | 'quicDatagramReceiveBufferBytes'
16
+ | 'streamFramePayloadBytes'
17
+ | 'firstDataConnectTimeoutMs'
18
+ | 'clientWriteTimeoutMs';
19
+
20
+ const performanceIntegerMaxByField: Record<TPerformanceIntegerField, number> = {
21
+ maxStreamsPerEdge: 100_000,
22
+ totalWindowBudgetBytes: 1_073_741_824,
23
+ minStreamWindowBytes: 16_777_216,
24
+ maxStreamWindowBytes: 134_217_728,
25
+ sustainedStreamWindowBytes: 134_217_728,
26
+ quicDatagramReceiveBufferBytes: 67_108_864,
27
+ streamFramePayloadBytes: 16_777_216,
28
+ firstDataConnectTimeoutMs: 3_600_000,
29
+ clientWriteTimeoutMs: 3_600_000,
30
+ };
31
+
32
+ const maxServerFirstPorts = 128;
33
+
9
34
  function extractPorts(portRange: plugins.smartproxy.IRouteConfig['match']['ports']): number[] {
10
35
  const ports = new Set<number>(plugins.smartproxy.expandPortRange(portRange) as number[]);
11
36
  return [...ports].sort((a, b) => a - b);
@@ -20,8 +45,12 @@ export class RemoteIngressManager {
20
45
  private edges: Map<string, IRemoteIngress> = new Map();
21
46
  private routes: IDcRouterRouteConfig[] = [];
22
47
  private firewallConfig?: IRemoteIngressFirewallConfig;
48
+ private hubSettings: IRemoteIngressHubSettings = {
49
+ updatedAt: 0,
50
+ updatedBy: 'default',
51
+ };
23
52
 
24
- constructor() {
53
+ constructor(private seedHubPerformance?: IRemoteIngressPerformanceConfig) {
25
54
  }
26
55
 
27
56
  /**
@@ -50,6 +79,28 @@ export class RemoteIngressManager {
50
79
  };
51
80
  this.edges.set(edge.id, edge);
52
81
  }
82
+
83
+ await this.initializeHubSettings();
84
+ }
85
+
86
+ private async initializeHubSettings(): Promise<void> {
87
+ let doc = await RemoteIngressHubSettingsDoc.load();
88
+ if (!doc) {
89
+ const seedPerformance = this.normalizePerformanceConfig(this.seedHubPerformance);
90
+ if (seedPerformance) {
91
+ doc = new RemoteIngressHubSettingsDoc();
92
+ doc.settingsId = 'remote-ingress-hub-settings';
93
+ doc.performance = seedPerformance;
94
+ doc.updatedAt = Date.now();
95
+ doc.updatedBy = 'seed';
96
+ await doc.save();
97
+ }
98
+ }
99
+
100
+ this.hubSettings = doc ? this.toHubSettings(doc) : {
101
+ updatedAt: 0,
102
+ updatedBy: 'default',
103
+ };
53
104
  }
54
105
 
55
106
  /**
@@ -66,6 +117,38 @@ export class RemoteIngressManager {
66
117
  this.firewallConfig = firewallConfig;
67
118
  }
68
119
 
120
+ public getHubSettings(): IRemoteIngressHubSettings {
121
+ return {
122
+ ...this.hubSettings,
123
+ performance: this.hubSettings.performance ? { ...this.hubSettings.performance } : undefined,
124
+ };
125
+ }
126
+
127
+ public getHubPerformanceConfig(): IRemoteIngressPerformanceConfig | undefined {
128
+ return this.hubSettings.performance && Object.keys(this.hubSettings.performance).length > 0
129
+ ? { ...this.hubSettings.performance }
130
+ : undefined;
131
+ }
132
+
133
+ public async updateHubSettings(
134
+ updates: { performance?: IRemoteIngressPerformanceConfig },
135
+ updatedBy: string,
136
+ ): Promise<IRemoteIngressHubSettings> {
137
+ let doc = await RemoteIngressHubSettingsDoc.load();
138
+ if (!doc) {
139
+ doc = new RemoteIngressHubSettingsDoc();
140
+ doc.settingsId = 'remote-ingress-hub-settings';
141
+ }
142
+
143
+ doc.performance = this.normalizePerformanceConfig(updates.performance);
144
+ doc.updatedAt = Date.now();
145
+ doc.updatedBy = updatedBy;
146
+ await doc.save();
147
+
148
+ this.hubSettings = this.toHubSettings(doc);
149
+ return this.getHubSettings();
150
+ }
151
+
69
152
  /**
70
153
  * Derive listen ports for an edge from routes tagged with remoteIngress.enabled.
71
154
  * When a route specifies edgeFilter, only edges whose id or tags match get that route's ports.
@@ -324,4 +407,90 @@ export class RemoteIngressManager {
324
407
  }
325
408
  return result;
326
409
  }
410
+
411
+ private normalizePerformanceConfig(
412
+ performance?: IRemoteIngressPerformanceConfig,
413
+ ): IRemoteIngressPerformanceConfig | undefined {
414
+ if (!performance) {
415
+ return undefined;
416
+ }
417
+
418
+ const next: IRemoteIngressPerformanceConfig = {};
419
+ const validProfiles: TRemoteIngressPerformanceProfile[] = ['balanced', 'throughput', 'highConcurrency'];
420
+ if (performance.profile !== undefined) {
421
+ if (!validProfiles.includes(performance.profile)) {
422
+ throw new Error('Invalid RemoteIngress performance profile');
423
+ }
424
+ next.profile = performance.profile;
425
+ }
426
+
427
+ const assignPositiveInteger = (field: TPerformanceIntegerField) => {
428
+ const value = performance[field];
429
+ if (value === undefined) {
430
+ return;
431
+ }
432
+ const maxValue = performanceIntegerMaxByField[field];
433
+ if (!Number.isSafeInteger(value) || value < 1 || value > maxValue) {
434
+ throw new Error(`${field} must be a positive safe integer no greater than ${maxValue}`);
435
+ }
436
+ (next as Record<string, number>)[field] = value;
437
+ };
438
+
439
+ assignPositiveInteger('maxStreamsPerEdge');
440
+ assignPositiveInteger('totalWindowBudgetBytes');
441
+ assignPositiveInteger('minStreamWindowBytes');
442
+ assignPositiveInteger('maxStreamWindowBytes');
443
+ assignPositiveInteger('sustainedStreamWindowBytes');
444
+ assignPositiveInteger('quicDatagramReceiveBufferBytes');
445
+ assignPositiveInteger('streamFramePayloadBytes');
446
+ assignPositiveInteger('firstDataConnectTimeoutMs');
447
+ assignPositiveInteger('clientWriteTimeoutMs');
448
+
449
+ if (
450
+ next.minStreamWindowBytes !== undefined
451
+ && next.maxStreamWindowBytes !== undefined
452
+ && next.minStreamWindowBytes > next.maxStreamWindowBytes
453
+ ) {
454
+ throw new Error('minStreamWindowBytes must not exceed maxStreamWindowBytes');
455
+ }
456
+ if (
457
+ next.sustainedStreamWindowBytes !== undefined
458
+ && next.maxStreamWindowBytes !== undefined
459
+ && next.sustainedStreamWindowBytes > next.maxStreamWindowBytes
460
+ ) {
461
+ throw new Error('sustainedStreamWindowBytes must not exceed maxStreamWindowBytes');
462
+ }
463
+
464
+ const configuredServerFirstPorts = performance.serverFirstPorts;
465
+ if (configuredServerFirstPorts !== undefined) {
466
+ if (!Array.isArray(configuredServerFirstPorts)) {
467
+ throw new Error('serverFirstPorts must contain valid port numbers');
468
+ }
469
+ if (configuredServerFirstPorts.length > maxServerFirstPorts) {
470
+ throw new Error(`serverFirstPorts must contain at most ${maxServerFirstPorts} ports`);
471
+ }
472
+ const serverFirstPorts = [...new Set(configuredServerFirstPorts.map((port) => Number(port)))].sort((a, b) => a - b);
473
+ for (const port of serverFirstPorts) {
474
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
475
+ throw new Error('serverFirstPorts must contain valid port numbers');
476
+ }
477
+ if (port === 443) {
478
+ throw new Error('Port 443 is client-first TLS and must not be listed as server-first');
479
+ }
480
+ }
481
+ if (serverFirstPorts.length > 0) {
482
+ next.serverFirstPorts = serverFirstPorts;
483
+ }
484
+ }
485
+
486
+ return Object.keys(next).length > 0 ? next : undefined;
487
+ }
488
+
489
+ private toHubSettings(doc: RemoteIngressHubSettingsDoc): IRemoteIngressHubSettings {
490
+ return {
491
+ performance: doc.performance,
492
+ updatedAt: doc.updatedAt,
493
+ updatedBy: doc.updatedBy,
494
+ };
495
+ }
327
496
  }
@@ -22,6 +22,8 @@ export class TunnelManager {
22
22
  private edgeStatuses: Map<string, IRemoteIngressStatus> = new Map();
23
23
  private reconcileInterval: ReturnType<typeof setInterval> | null = null;
24
24
  private syncChain: Promise<void> = Promise.resolve();
25
+ private reconcileChain: Promise<void> = Promise.resolve();
26
+ private stopped = true;
25
27
 
26
28
  constructor(manager: RemoteIngressManager, config: ITunnelManagerConfig = {}) {
27
29
  this.manager = manager;
@@ -64,30 +66,51 @@ export class TunnelManager {
64
66
  * Start the tunnel hub and load allowed edges.
65
67
  */
66
68
  public async start(): Promise<void> {
67
- await this.hub.start({
68
- tunnelPort: this.config.tunnelPort ?? 8443,
69
- targetHost: this.config.targetHost ?? '127.0.0.1',
70
- tls: this.config.tls,
71
- ...(this.config.performance ? { performance: this.config.performance } : {}),
72
- } as any);
73
-
74
- // Send allowed edges to the hub
75
- await this.syncAllowedEdges();
76
-
77
- // Periodically reconcile with authoritative Rust hub status
78
- this.reconcileInterval = setInterval(() => {
79
- this.reconcile().catch(() => {});
80
- }, 15_000);
69
+ this.stopped = false;
70
+ try {
71
+ await this.hub.start({
72
+ tunnelPort: this.config.tunnelPort ?? 8443,
73
+ targetHost: this.config.targetHost ?? '127.0.0.1',
74
+ tls: this.config.tls,
75
+ ...(this.config.performance ? { performance: this.config.performance } : {}),
76
+ } as any);
77
+
78
+ if (this.stopped) return;
79
+
80
+ // Send allowed edges to the hub
81
+ await this.syncAllowedEdges();
82
+
83
+ if (this.stopped) return;
84
+
85
+ // Periodically reconcile with authoritative Rust hub status
86
+ this.reconcileInterval = setInterval(() => {
87
+ this.reconcileChain = this.reconcileChain
88
+ .catch(() => {})
89
+ .then(() => this.reconcile());
90
+ this.reconcileChain.catch(() => {});
91
+ }, 15_000);
92
+ } catch (err) {
93
+ await this.stop();
94
+ throw err;
95
+ }
81
96
  }
82
97
 
83
98
  /**
84
99
  * Stop the tunnel hub.
85
100
  */
86
101
  public async stop(): Promise<void> {
102
+ if (this.stopped) {
103
+ return;
104
+ }
105
+ this.stopped = true;
87
106
  if (this.reconcileInterval) {
88
107
  clearInterval(this.reconcileInterval);
89
108
  this.reconcileInterval = null;
90
109
  }
110
+ await Promise.all([
111
+ this.syncChain.catch(() => {}),
112
+ this.reconcileChain.catch(() => {}),
113
+ ]);
91
114
  // Remove event listeners before stopping to prevent leaks
92
115
  this.hub.removeAllListeners();
93
116
  await this.hub.stop();
@@ -99,7 +122,9 @@ export class TunnelManager {
99
122
  * Overwrites event-derived activeTunnels with the real activeStreams count.
100
123
  */
101
124
  private async reconcile(): Promise<void> {
125
+ if (this.stopped) return;
102
126
  const hubStatus = await this.hub.getStatus();
127
+ if (this.stopped) return;
103
128
  if (!hubStatus || !hubStatus.connectedEdges) return;
104
129
 
105
130
  const rustEdgeIds = new Set<string>();
@@ -144,7 +169,9 @@ export class TunnelManager {
144
169
  */
145
170
  public async syncAllowedEdges(): Promise<void> {
146
171
  const run = this.syncChain.catch(() => {}).then(async () => {
172
+ if (this.stopped) return;
147
173
  const edges = this.manager.getAllowedEdges();
174
+ if (this.stopped) return;
148
175
  await this.hub.updateAllowedEdges(edges as any);
149
176
  });
150
177
  this.syncChain = run;
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '13.40.3',
6
+ version: '13.41.0',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }
@@ -260,6 +260,7 @@ export const acmeConfigStatePart = await appState.getStatePart<IAcmeConfigState>
260
260
  export interface IRemoteIngressState {
261
261
  edges: interfaces.data.IRemoteIngress[];
262
262
  statuses: interfaces.data.IRemoteIngressStatus[];
263
+ hubSettings: interfaces.data.IRemoteIngressHubSettings | null;
263
264
  selectedEdgeId: string | null;
264
265
  newEdgeId: string | null;
265
266
  isLoading: boolean;
@@ -272,6 +273,7 @@ export const remoteIngressStatePart = await appState.getStatePart<IRemoteIngress
272
273
  {
273
274
  edges: [],
274
275
  statuses: [],
276
+ hubSettings: null,
275
277
  selectedEdgeId: null,
276
278
  newEdgeId: null,
277
279
  isLoading: false,
@@ -1094,15 +1096,21 @@ export const fetchRemoteIngressAction = remoteIngressStatePart.createAction(asyn
1094
1096
  interfaces.requests.IReq_GetRemoteIngressStatus
1095
1097
  >('/typedrequest', 'getRemoteIngressStatus');
1096
1098
 
1097
- const [edgesResponse, statusResponse] = await Promise.all([
1099
+ const hubSettingsRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
1100
+ interfaces.requests.IReq_GetRemoteIngressHubSettings
1101
+ >('/typedrequest', 'getRemoteIngressHubSettings');
1102
+
1103
+ const [edgesResponse, statusResponse, hubSettingsResponse] = await Promise.all([
1098
1104
  edgesRequest.fire({ identity: context.identity }),
1099
1105
  statusRequest.fire({ identity: context.identity }),
1106
+ hubSettingsRequest.fire({ identity: context.identity }),
1100
1107
  ]);
1101
1108
 
1102
1109
  return {
1103
1110
  ...currentState,
1104
1111
  edges: edgesResponse.edges,
1105
1112
  statuses: statusResponse.statuses,
1113
+ hubSettings: hubSettingsResponse.settings,
1106
1114
  isLoading: false,
1107
1115
  error: null,
1108
1116
  lastUpdated: Date.now(),
@@ -1219,6 +1227,38 @@ export const updateRemoteIngressAction = remoteIngressStatePart.createAction<{
1219
1227
  }
1220
1228
  });
1221
1229
 
1230
+ export const updateRemoteIngressHubSettingsAction = remoteIngressStatePart.createAction<{
1231
+ performance?: interfaces.data.IRemoteIngressPerformanceConfig;
1232
+ }>(async (statePartArg, dataArg, actionContext): Promise<IRemoteIngressState> => {
1233
+ const context = getActionContext();
1234
+ const currentState = statePartArg.getState()!;
1235
+
1236
+ try {
1237
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
1238
+ interfaces.requests.IReq_UpdateRemoteIngressHubSettings
1239
+ >('/typedrequest', 'updateRemoteIngressHubSettings');
1240
+
1241
+ const response = await request.fire({
1242
+ identity: context.identity!,
1243
+ performance: dataArg.performance,
1244
+ });
1245
+
1246
+ if (!response.success) {
1247
+ return {
1248
+ ...currentState,
1249
+ error: response.message || 'Failed to update RemoteIngress hub settings',
1250
+ };
1251
+ }
1252
+
1253
+ return await actionContext!.dispatch(fetchRemoteIngressAction, null);
1254
+ } catch (error: unknown) {
1255
+ return {
1256
+ ...currentState,
1257
+ error: error instanceof Error ? error.message : 'Failed to update RemoteIngress hub settings',
1258
+ };
1259
+ }
1260
+ });
1261
+
1222
1262
  export const regenerateRemoteIngressSecretAction = remoteIngressStatePart.createAction<string>(
1223
1263
  async (statePartArg, edgeId): Promise<IRemoteIngressState> => {
1224
1264
  const context = getActionContext();