@serve.zone/dcrouter 13.43.5 → 13.44.1

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 (82) 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.cached.email.d.ts +2 -5
  9. package/dist_ts/db/documents/classes.cached.email.js +4 -23
  10. package/dist_ts/db/documents/classes.cached.ip.reputation.d.ts +2 -5
  11. package/dist_ts/db/documents/classes.cached.ip.reputation.js +4 -23
  12. package/dist_ts/db/documents/classes.email-server-settings.doc.d.ts +14 -0
  13. package/dist_ts/db/documents/classes.email-server-settings.doc.js +103 -0
  14. package/dist_ts/db/documents/classes.remote-ingress-hub-settings.doc.d.ts +3 -0
  15. package/dist_ts/db/documents/classes.remote-ingress-hub-settings.doc.js +20 -2
  16. package/dist_ts/db/documents/index.d.ts +1 -0
  17. package/dist_ts/db/documents/index.js +2 -1
  18. package/dist_ts/db/index.d.ts +0 -1
  19. package/dist_ts/db/index.js +1 -3
  20. package/dist_ts/email/classes.email-domain.manager.d.ts +3 -1
  21. package/dist_ts/email/classes.email-domain.manager.js +6 -3
  22. package/dist_ts/email/classes.email-settings.manager.d.ts +25 -0
  23. package/dist_ts/email/classes.email-settings.manager.js +184 -0
  24. package/dist_ts/email/index.d.ts +1 -0
  25. package/dist_ts/email/index.js +2 -1
  26. package/dist_ts/opsserver/classes.opsserver.d.ts +1 -0
  27. package/dist_ts/opsserver/classes.opsserver.js +3 -1
  28. package/dist_ts/opsserver/handlers/config.handler.js +17 -14
  29. package/dist_ts/opsserver/handlers/email-settings.handler.d.ts +10 -0
  30. package/dist_ts/opsserver/handlers/email-settings.handler.js +57 -0
  31. package/dist_ts/opsserver/handlers/index.d.ts +1 -0
  32. package/dist_ts/opsserver/handlers/index.js +2 -1
  33. package/dist_ts/opsserver/handlers/remoteingress.handler.js +24 -6
  34. package/dist_ts/opsserver/handlers/workhoster.handler.js +2 -2
  35. package/dist_ts/remoteingress/classes.remoteingress-manager.d.ts +4 -6
  36. package/dist_ts/remoteingress/classes.remoteingress-manager.js +58 -19
  37. package/dist_ts_interfaces/data/email-settings.d.ts +39 -0
  38. package/dist_ts_interfaces/data/email-settings.js +2 -0
  39. package/dist_ts_interfaces/data/index.d.ts +1 -0
  40. package/dist_ts_interfaces/data/index.js +2 -1
  41. package/dist_ts_interfaces/data/remoteingress.d.ts +7 -0
  42. package/dist_ts_interfaces/requests/email-settings.d.ts +26 -0
  43. package/dist_ts_interfaces/requests/email-settings.js +2 -0
  44. package/dist_ts_interfaces/requests/index.d.ts +1 -0
  45. package/dist_ts_interfaces/requests/index.js +2 -1
  46. package/dist_ts_interfaces/requests/remoteingress.d.ts +4 -1
  47. package/dist_ts_migrations/index.d.ts +14 -1
  48. package/dist_ts_migrations/index.js +107 -2
  49. package/dist_ts_web/00_commitinfo_data.js +1 -1
  50. package/dist_ts_web/appstate.d.ts +6 -1
  51. package/dist_ts_web/appstate.js +23 -1
  52. package/dist_ts_web/elements/email/ops-view-email-domains.d.ts +4 -0
  53. package/dist_ts_web/elements/email/ops-view-email-domains.js +122 -1
  54. package/dist_ts_web/elements/network/ops-view-remoteingress.d.ts +2 -1
  55. package/dist_ts_web/elements/network/ops-view-remoteingress.js +57 -17
  56. package/package.json +2 -2
  57. package/ts/00_commitinfo_data.ts +1 -1
  58. package/ts/classes.dcrouter.ts +361 -100
  59. package/ts/config/classes.route-config-manager.ts +4 -0
  60. package/ts/db/documents/classes.cached.email.ts +3 -12
  61. package/ts/db/documents/classes.cached.ip.reputation.ts +3 -12
  62. package/ts/db/documents/classes.email-server-settings.doc.ts +40 -0
  63. package/ts/db/documents/classes.remote-ingress-hub-settings.doc.ts +9 -0
  64. package/ts/db/documents/index.ts +1 -0
  65. package/ts/db/index.ts +0 -3
  66. package/ts/email/classes.email-domain.manager.ts +6 -2
  67. package/ts/email/classes.email-settings.manager.ts +221 -0
  68. package/ts/email/index.ts +1 -0
  69. package/ts/opsserver/classes.opsserver.ts +2 -0
  70. package/ts/opsserver/handlers/config.handler.ts +16 -13
  71. package/ts/opsserver/handlers/email-settings.handler.ts +72 -0
  72. package/ts/opsserver/handlers/index.ts +1 -0
  73. package/ts/opsserver/handlers/remoteingress.handler.ts +25 -5
  74. package/ts/opsserver/handlers/workhoster.handler.ts +1 -1
  75. package/ts/remoteingress/classes.remoteingress-manager.ts +65 -18
  76. package/ts_web/00_commitinfo_data.ts +1 -1
  77. package/ts_web/appstate.ts +33 -1
  78. package/ts_web/elements/email/ops-view-email-domains.ts +126 -0
  79. package/ts_web/elements/network/ops-view-remoteingress.ts +59 -17
  80. package/dist_ts/db/classes.cached.document.d.ts +0 -76
  81. package/dist_ts/db/classes.cached.document.js +0 -100
  82. package/ts/db/classes.cached.document.ts +0 -111
@@ -1,5 +1,5 @@
1
1
  import * as plugins from '../plugins.js';
2
- import type { IDcRouterRouteConfig, IRemoteIngress, IRemoteIngressHubSettings, IRemoteIngressPerformanceConfig, TRemoteIngressPerformanceProfile } from '../../ts_interfaces/data/remoteingress.js';
2
+ import type { IDcRouterRouteConfig, IRemoteIngress, IRemoteIngressHubSettings, IRemoteIngressPerformanceConfig, TRemoteIngressHubSettingsUpdate, TRemoteIngressPerformanceProfile } from '../../ts_interfaces/data/remoteingress.js';
3
3
  import { RemoteIngressEdgeDoc, RemoteIngressHubSettingsDoc } from '../db/index.js';
4
4
 
5
5
  interface IRemoteIngressFirewallConfig {
@@ -30,6 +30,11 @@ const performanceIntegerMaxByField: Record<TPerformanceIntegerField, number> = {
30
30
  };
31
31
 
32
32
  const maxServerFirstPorts = 128;
33
+ const defaultTunnelPort = 8443;
34
+
35
+ function hasOwn(objectArg: object, keyArg: string): boolean {
36
+ return Object.prototype.hasOwnProperty.call(objectArg, keyArg);
37
+ }
33
38
 
34
39
  function extractPorts(portRange: plugins.smartproxy.IRouteConfig['match']['ports']): number[] {
35
40
  const ports = new Set<number>(plugins.smartproxy.expandPortRange(portRange) as number[]);
@@ -46,12 +51,13 @@ export class RemoteIngressManager {
46
51
  private routes: IDcRouterRouteConfig[] = [];
47
52
  private firewallConfig?: IRemoteIngressFirewallConfig;
48
53
  private hubSettings: IRemoteIngressHubSettings = {
54
+ enabled: false,
55
+ tunnelPort: defaultTunnelPort,
49
56
  updatedAt: 0,
50
57
  updatedBy: 'default',
51
58
  };
52
59
 
53
- constructor(private seedHubPerformance?: IRemoteIngressPerformanceConfig) {
54
- }
60
+ constructor() {}
55
61
 
56
62
  /**
57
63
  * Load all edge registrations from the database into memory.
@@ -86,21 +92,17 @@ export class RemoteIngressManager {
86
92
  private async initializeHubSettings(): Promise<void> {
87
93
  let doc = await RemoteIngressHubSettingsDoc.load();
88
94
  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
- }
95
+ doc = new RemoteIngressHubSettingsDoc();
96
+ doc.settingsId = 'remote-ingress-hub-settings';
97
+ doc.enabled = false;
98
+ doc.tunnelPort = defaultTunnelPort;
99
+ doc.hubDomain = '';
100
+ doc.updatedAt = Date.now();
101
+ doc.updatedBy = 'default';
102
+ await doc.save();
98
103
  }
99
104
 
100
- this.hubSettings = doc ? this.toHubSettings(doc) : {
101
- updatedAt: 0,
102
- updatedBy: 'default',
103
- };
105
+ this.hubSettings = this.toHubSettings(doc);
104
106
  }
105
107
 
106
108
  /**
@@ -131,16 +133,30 @@ export class RemoteIngressManager {
131
133
  }
132
134
 
133
135
  public async updateHubSettings(
134
- updates: { performance?: IRemoteIngressPerformanceConfig },
136
+ updates: TRemoteIngressHubSettingsUpdate,
135
137
  updatedBy: string,
136
138
  ): Promise<IRemoteIngressHubSettings> {
137
139
  let doc = await RemoteIngressHubSettingsDoc.load();
138
140
  if (!doc) {
139
141
  doc = new RemoteIngressHubSettingsDoc();
140
142
  doc.settingsId = 'remote-ingress-hub-settings';
143
+ doc.enabled = false;
144
+ doc.tunnelPort = defaultTunnelPort;
141
145
  }
142
146
 
143
- doc.performance = this.normalizePerformanceConfig(updates.performance);
147
+ const normalized = this.normalizeHubSettingsUpdate(updates);
148
+ if (hasOwn(normalized, 'enabled')) {
149
+ doc.enabled = normalized.enabled;
150
+ }
151
+ if (hasOwn(normalized, 'tunnelPort')) {
152
+ doc.tunnelPort = normalized.tunnelPort;
153
+ }
154
+ if (hasOwn(updates, 'hubDomain')) {
155
+ doc.hubDomain = normalized.hubDomain || '';
156
+ }
157
+ if (hasOwn(updates, 'performance')) {
158
+ doc.performance = normalized.performance || undefined;
159
+ }
144
160
  doc.updatedAt = Date.now();
145
161
  doc.updatedBy = updatedBy;
146
162
  await doc.save();
@@ -408,6 +424,34 @@ export class RemoteIngressManager {
408
424
  return result;
409
425
  }
410
426
 
427
+ private normalizeHubSettingsUpdate(
428
+ updates: TRemoteIngressHubSettingsUpdate,
429
+ ): TRemoteIngressHubSettingsUpdate {
430
+ const next: TRemoteIngressHubSettingsUpdate = {};
431
+
432
+ if (hasOwn(updates, 'enabled') && updates.enabled !== undefined) {
433
+ next.enabled = Boolean(updates.enabled);
434
+ }
435
+ if (hasOwn(updates, 'tunnelPort') && updates.tunnelPort !== undefined) {
436
+ const tunnelPort = Number(updates.tunnelPort);
437
+ if (!Number.isInteger(tunnelPort) || tunnelPort < 1 || tunnelPort > 65535) {
438
+ throw new Error('tunnelPort must be a valid TCP port');
439
+ }
440
+ next.tunnelPort = tunnelPort;
441
+ }
442
+ if (hasOwn(updates, 'hubDomain')) {
443
+ const hubDomain = `${updates.hubDomain || ''}`.trim();
444
+ next.hubDomain = hubDomain || undefined;
445
+ }
446
+ if (hasOwn(updates, 'performance')) {
447
+ next.performance = updates.performance === null
448
+ ? undefined
449
+ : this.normalizePerformanceConfig(updates.performance || undefined);
450
+ }
451
+
452
+ return next;
453
+ }
454
+
411
455
  private normalizePerformanceConfig(
412
456
  performance?: IRemoteIngressPerformanceConfig,
413
457
  ): IRemoteIngressPerformanceConfig | undefined {
@@ -488,6 +532,9 @@ export class RemoteIngressManager {
488
532
 
489
533
  private toHubSettings(doc: RemoteIngressHubSettingsDoc): IRemoteIngressHubSettings {
490
534
  return {
535
+ enabled: doc.enabled ?? false,
536
+ tunnelPort: doc.tunnelPort ?? defaultTunnelPort,
537
+ hubDomain: doc.hubDomain || undefined,
491
538
  performance: doc.performance,
492
539
  updatedAt: doc.updatedAt,
493
540
  updatedBy: doc.updatedBy,
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '13.43.5',
6
+ version: '13.44.1',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }
@@ -1230,7 +1230,10 @@ export const updateRemoteIngressAction = remoteIngressStatePart.createAction<{
1230
1230
  });
1231
1231
 
1232
1232
  export const updateRemoteIngressHubSettingsAction = remoteIngressStatePart.createAction<{
1233
- performance?: interfaces.data.IRemoteIngressPerformanceConfig;
1233
+ enabled?: boolean;
1234
+ tunnelPort?: number;
1235
+ hubDomain?: string | null;
1236
+ performance?: interfaces.data.IRemoteIngressPerformanceConfig | null;
1234
1237
  }>(async (statePartArg, dataArg, actionContext): Promise<IRemoteIngressState> => {
1235
1238
  const context = getActionContext();
1236
1239
  const currentState = statePartArg.getState()!;
@@ -1242,6 +1245,9 @@ export const updateRemoteIngressHubSettingsAction = remoteIngressStatePart.creat
1242
1245
 
1243
1246
  const response = await request.fire({
1244
1247
  identity: context.identity!,
1248
+ enabled: dataArg.enabled,
1249
+ tunnelPort: dataArg.tunnelPort,
1250
+ hubDomain: dataArg.hubDomain,
1245
1251
  performance: dataArg.performance,
1246
1252
  });
1247
1253
 
@@ -2956,6 +2962,7 @@ export const toggleApiTokenAction = routeManagementStatePart.createAction<{
2956
2962
 
2957
2963
  export interface IEmailDomainsState {
2958
2964
  domains: interfaces.data.IEmailDomain[];
2965
+ settings: interfaces.data.IEmailServerSettings | null;
2959
2966
  isLoading: boolean;
2960
2967
  lastUpdated: number;
2961
2968
  }
@@ -2964,6 +2971,7 @@ export const emailDomainsStatePart = await appState.getStatePart<IEmailDomainsSt
2964
2971
  'emailDomains',
2965
2972
  {
2966
2973
  domains: [],
2974
+ settings: null,
2967
2975
  isLoading: false,
2968
2976
  lastUpdated: 0,
2969
2977
  },
@@ -2980,10 +2988,15 @@ export const fetchEmailDomainsAction = emailDomainsStatePart.createAction(
2980
2988
  const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
2981
2989
  interfaces.requests.IReq_GetEmailDomains
2982
2990
  >('/typedrequest', 'getEmailDomains');
2991
+ const settingsRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
2992
+ interfaces.requests.IReq_GetEmailServerSettings
2993
+ >('/typedrequest', 'getEmailServerSettings');
2983
2994
  const response = await request.fire({ identity: context.identity });
2995
+ const settingsResponse = await settingsRequest.fire({ identity: context.identity });
2984
2996
  return {
2985
2997
  ...currentState,
2986
2998
  domains: response.domains,
2999
+ settings: settingsResponse.settings,
2987
3000
  isLoading: false,
2988
3001
  lastUpdated: Date.now(),
2989
3002
  };
@@ -3014,6 +3027,25 @@ export const createEmailDomainAction = emailDomainsStatePart.createAction<{
3014
3027
  }
3015
3028
  });
3016
3029
 
3030
+ export const updateEmailServerSettingsAction = emailDomainsStatePart.createAction<
3031
+ interfaces.data.TEmailServerSettingsUpdate
3032
+ >(async (statePartArg, settings, actionContext) => {
3033
+ const context = getActionContext();
3034
+ const currentState = statePartArg.getState()!;
3035
+ try {
3036
+ const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
3037
+ interfaces.requests.IReq_UpdateEmailServerSettings
3038
+ >('/typedrequest', 'updateEmailServerSettings');
3039
+ const response = await request.fire({ identity: context.identity!, settings });
3040
+ if (!response.success) {
3041
+ return currentState;
3042
+ }
3043
+ return await actionContext!.dispatch(fetchEmailDomainsAction, null);
3044
+ } catch {
3045
+ return currentState;
3046
+ }
3047
+ });
3048
+
3017
3049
  export const deleteEmailDomainAction = emailDomainsStatePart.createAction<string>(
3018
3050
  async (statePartArg, id, actionContext) => {
3019
3051
  const context = getActionContext();
@@ -101,6 +101,7 @@ export class OpsViewEmailDomains extends DeesElement {
101
101
 
102
102
  public render(): TemplateResult {
103
103
  const domains = this.emailDomainsState.domains;
104
+ const settings = this.emailDomainsState.settings;
104
105
  const validCount = domains.filter(
105
106
  (d) =>
106
107
  d.dnsStatus.mx === 'valid' &&
@@ -127,6 +128,22 @@ export class OpsViewEmailDomains extends DeesElement {
127
128
  icon: 'lucide:Check',
128
129
  color: '#22c55e',
129
130
  },
131
+ {
132
+ id: 'server',
133
+ title: 'Server',
134
+ value: settings?.enabled ? 'enabled' : 'disabled',
135
+ type: 'text',
136
+ icon: 'lucide:mail-check',
137
+ color: settings?.enabled ? '#22c55e' : '#6b7280',
138
+ },
139
+ {
140
+ id: 'ports',
141
+ title: 'SMTP Ports',
142
+ value: settings?.ports?.join(', ') || 'none',
143
+ type: 'text',
144
+ icon: 'lucide:plug',
145
+ color: '#0ea5e9',
146
+ },
130
147
  {
131
148
  id: 'issues',
132
149
  title: 'Issues',
@@ -163,6 +180,13 @@ export class OpsViewEmailDomains extends DeesElement {
163
180
  );
164
181
  },
165
182
  },
183
+ {
184
+ name: 'Settings',
185
+ iconName: 'lucide:settings',
186
+ action: async () => {
187
+ await this.showSettingsDialog();
188
+ },
189
+ },
166
190
  ]}
167
191
  ></dees-statsgrid>
168
192
 
@@ -258,6 +282,108 @@ export class OpsViewEmailDomains extends DeesElement {
258
282
  return html`<span class="sourceBadge">${label}</span>`;
259
283
  }
260
284
 
285
+ private parsePortList(value: string): number[] {
286
+ return value
287
+ .split(',')
288
+ .map((part) => Number.parseInt(part.trim(), 10))
289
+ .filter((port) => Number.isInteger(port));
290
+ }
291
+
292
+ private parsePortMapping(value: string): Record<number, number> | null {
293
+ const trimmed = value.trim();
294
+ if (!trimmed) return null;
295
+ const mapping: Record<number, number> = {};
296
+ for (const pair of trimmed.split(',')) {
297
+ const [externalPort, internalPort] = pair
298
+ .split(':')
299
+ .map((part) => Number.parseInt(part.trim(), 10));
300
+ if (Number.isInteger(externalPort) && Number.isInteger(internalPort)) {
301
+ mapping[externalPort] = internalPort;
302
+ }
303
+ }
304
+ return Object.keys(mapping).length > 0 ? mapping : null;
305
+ }
306
+
307
+ private formatPortMapping(mapping: Record<number, number> | null | undefined): string {
308
+ if (!mapping) return '';
309
+ return Object.entries(mapping)
310
+ .map(([externalPort, internalPort]) => `${externalPort}:${internalPort}`)
311
+ .join(', ');
312
+ }
313
+
314
+ private async showSettingsDialog() {
315
+ const { DeesModal, DeesToast } = await import('@design.estate/dees-catalog');
316
+ const settings = this.emailDomainsState.settings;
317
+
318
+ DeesModal.createAndShow({
319
+ heading: 'Email Server Settings',
320
+ content: html`
321
+ <dees-form>
322
+ <dees-input-checkbox
323
+ .key=${'enabled'}
324
+ .label=${'Enable email server'}
325
+ .value=${settings?.enabled ?? false}
326
+ ></dees-input-checkbox>
327
+ <dees-input-text
328
+ .key=${'hostname'}
329
+ .label=${'SMTP hostname'}
330
+ .description=${'Public hostname used in SMTP banners and DNS records'}
331
+ .value=${settings?.hostname || ''}
332
+ ></dees-input-text>
333
+ <dees-input-text
334
+ .key=${'ports'}
335
+ .label=${'Public ports'}
336
+ .description=${'Comma-separated SMTP ingress ports, e.g. 25, 587, 465'}
337
+ .value=${settings?.ports?.join(', ') || '25, 587, 465'}
338
+ ></dees-input-text>
339
+ <dees-input-text
340
+ .key=${'portMapping'}
341
+ .label=${'Port mapping'}
342
+ .description=${'Optional external:internal pairs, e.g. 25:10025, 587:10587'}
343
+ .value=${this.formatPortMapping(settings?.portMapping)}
344
+ ></dees-input-text>
345
+ <dees-input-text
346
+ .key=${'maxMessageSize'}
347
+ .label=${'Max message size'}
348
+ .description=${'Bytes; leave empty for smartmta default'}
349
+ .value=${settings?.maxMessageSize ? String(settings.maxMessageSize) : ''}
350
+ ></dees-input-text>
351
+ <dees-input-text
352
+ .key=${'receivedEmailsPath'}
353
+ .label=${'Received emails path'}
354
+ .description=${'Optional storage path for received email artifacts'}
355
+ .value=${settings?.receivedEmailsPath || ''}
356
+ ></dees-input-text>
357
+ </dees-form>
358
+ `,
359
+ menuOptions: [
360
+ { name: 'Cancel', action: async (m: any) => m.destroy() },
361
+ {
362
+ name: 'Save',
363
+ action: async (m: any) => {
364
+ const form = m.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
365
+ if (!form) return;
366
+ const data = await form.collectFormData();
367
+ const maxMessageSizeRaw = String(data.maxMessageSize || '').trim();
368
+ await appstate.emailDomainsStatePart.dispatchAction(
369
+ appstate.updateEmailServerSettingsAction,
370
+ {
371
+ enabled: Boolean(data.enabled),
372
+ hostname: String(data.hostname || '').trim() || null,
373
+ ports: this.parsePortList(String(data.ports || '')),
374
+ portMapping: this.parsePortMapping(String(data.portMapping || '')),
375
+ maxMessageSize: maxMessageSizeRaw ? Number.parseInt(maxMessageSizeRaw, 10) : null,
376
+ receivedEmailsPath: String(data.receivedEmailsPath || '').trim() || null,
377
+ },
378
+ );
379
+ DeesToast.show({ message: 'Email settings saved', type: 'success', duration: 2500 });
380
+ m.destroy();
381
+ },
382
+ },
383
+ ],
384
+ });
385
+ }
386
+
261
387
  private async showCreateDialog() {
262
388
  const { DeesModal } = await import('@design.estate/dees-catalog');
263
389
  const domainOptions = this.domainsState.domains.map((d) => ({
@@ -620,16 +620,34 @@ export class OpsViewRemoteIngress extends DeesElement {
620
620
 
621
621
  private async showHubSettingsDialog(): Promise<void> {
622
622
  const { DeesModal, DeesToast } = await import('@design.estate/dees-catalog');
623
- const performance = this.riState.hubSettings?.performance || {};
623
+ const hubSettings = this.riState.hubSettings;
624
+ const performance = hubSettings?.performance || {};
624
625
  const selectedProfile = performanceProfileOptions.find((option) => option.key === (performance.profile || '')) || performanceProfileOptions[0];
625
- const updatedAt = this.riState.hubSettings?.updatedAt
626
- ? new Date(this.riState.hubSettings.updatedAt).toLocaleString()
626
+ const updatedAt = hubSettings?.updatedAt
627
+ ? new Date(hubSettings.updatedAt).toLocaleString()
627
628
  : 'not persisted yet';
628
629
 
629
630
  await DeesModal.createAndShow({
630
631
  heading: 'RemoteIngress Hub Settings',
631
632
  content: html`
632
633
  <dees-form>
634
+ <dees-input-checkbox
635
+ .key=${'enabled'}
636
+ .label=${'Enable RemoteIngress Hub'}
637
+ .value=${hubSettings?.enabled ?? false}
638
+ ></dees-input-checkbox>
639
+ <dees-input-text
640
+ .key=${'tunnelPort'}
641
+ .label=${'Tunnel Port'}
642
+ .description=${'TCP/UDP port edges connect to on the hub.'}
643
+ .value=${(hubSettings?.tunnelPort || 8443).toString()}
644
+ ></dees-input-text>
645
+ <dees-input-text
646
+ .key=${'hubDomain'}
647
+ .label=${'Hub Domain / Address'}
648
+ .description=${'Public host or IP embedded in edge connection tokens.'}
649
+ .value=${hubSettings?.hubDomain || ''}
650
+ ></dees-input-text>
633
651
  <dees-input-dropdown
634
652
  .key=${'profile'}
635
653
  .label=${'Performance Profile'}
@@ -662,8 +680,8 @@ export class OpsViewRemoteIngress extends DeesElement {
662
680
  ></dees-input-text>
663
681
  </dees-form>
664
682
  <p class="settingsNote">
665
- Saving restarts the RemoteIngress hub so connected edges reconnect and pick up the new defaults.
666
- Last updated: ${updatedAt} by ${this.riState.hubSettings?.updatedBy || 'default'}.
683
+ Saving applies DB-backed hub settings. Enabling or disabling the hub restarts SmartProxy so tunneled traffic and route metadata stay consistent.
684
+ Last updated: ${updatedAt} by ${hubSettings?.updatedBy || 'default'}.
667
685
  </p>
668
686
  `,
669
687
  menuOptions: [
@@ -679,9 +697,11 @@ export class OpsViewRemoteIngress extends DeesElement {
679
697
  const form = modalArg.shadowRoot?.querySelector('.content')?.querySelector('dees-form');
680
698
  if (!form) return;
681
699
  const formData = await form.collectFormData();
682
- let performanceSettings: interfaces.data.IRemoteIngressPerformanceConfig | undefined;
700
+ let performanceSettings: interfaces.data.IRemoteIngressPerformanceConfig | null;
701
+ let tunnelPort: number;
683
702
  try {
684
- performanceSettings = this.collectHubPerformanceSettings(formData);
703
+ tunnelPort = this.parseRequiredPort(formData.tunnelPort, 'Tunnel Port');
704
+ performanceSettings = this.collectHubPerformanceSettings(formData, performance);
685
705
  } catch (err: unknown) {
686
706
  DeesToast.show({ message: (err as Error).message, type: 'error', duration: 4000 });
687
707
  return;
@@ -689,7 +709,12 @@ export class OpsViewRemoteIngress extends DeesElement {
689
709
 
690
710
  const nextState = await appstate.remoteIngressStatePart.dispatchAction(
691
711
  appstate.updateRemoteIngressHubSettingsAction,
692
- { performance: performanceSettings },
712
+ {
713
+ enabled: formData.enabled !== false,
714
+ tunnelPort,
715
+ hubDomain: `${formData.hubDomain || ''}`.trim() || null,
716
+ performance: performanceSettings,
717
+ },
693
718
  );
694
719
  if (nextState.error) {
695
720
  DeesToast.show({ message: nextState.error, type: 'error', duration: 4000 });
@@ -703,29 +728,37 @@ export class OpsViewRemoteIngress extends DeesElement {
703
728
  });
704
729
  }
705
730
 
706
- private collectHubPerformanceSettings(formData: Record<string, any>): interfaces.data.IRemoteIngressPerformanceConfig | undefined {
707
- const next: interfaces.data.IRemoteIngressPerformanceConfig = {};
731
+ private collectHubPerformanceSettings(
732
+ formData: Record<string, any>,
733
+ currentPerformance: interfaces.data.IRemoteIngressPerformanceConfig,
734
+ ): interfaces.data.IRemoteIngressPerformanceConfig | null {
735
+ const next: interfaces.data.IRemoteIngressPerformanceConfig = { ...currentPerformance };
708
736
  const profile = getDropdownKey(formData.profile) as interfaces.data.TRemoteIngressPerformanceProfile | '';
709
737
  if (profile) {
710
738
  next.profile = profile;
739
+ } else {
740
+ delete next.profile;
711
741
  }
712
742
 
713
- this.assignPositiveIntegerSetting(next, 'maxStreamsPerEdge', formData.maxStreamsPerEdge, 'Max Connections / Edge');
714
- this.assignPositiveIntegerSetting(next, 'clientWriteTimeoutMs', formData.clientWriteTimeoutMs, 'Client Write Timeout');
715
- this.assignPositiveIntegerSetting(next, 'firstDataConnectTimeoutMs', formData.firstDataConnectTimeoutMs, 'First Data Timeout');
743
+ this.assignOptionalPositiveIntegerSetting(next, 'maxStreamsPerEdge', formData.maxStreamsPerEdge, 'Max Connections / Edge');
744
+ this.assignOptionalPositiveIntegerSetting(next, 'clientWriteTimeoutMs', formData.clientWriteTimeoutMs, 'Client Write Timeout');
745
+ this.assignOptionalPositiveIntegerSetting(next, 'firstDataConnectTimeoutMs', formData.firstDataConnectTimeoutMs, 'First Data Timeout');
716
746
 
717
- const serverFirstPorts = this.parsePortList(formData.serverFirstPorts, 'Server-first Ports');
718
- if (serverFirstPorts.length > 0) {
747
+ const serverFirstPortsText = `${formData.serverFirstPorts || ''}`.trim();
748
+ if (serverFirstPortsText) {
749
+ const serverFirstPorts = this.parsePortList(serverFirstPortsText, 'Server-first Ports');
719
750
  if (serverFirstPorts.includes(443)) {
720
751
  throw new Error('Port 443 is client-first TLS and must not be listed as server-first');
721
752
  }
722
753
  next.serverFirstPorts = serverFirstPorts;
754
+ } else {
755
+ delete next.serverFirstPorts;
723
756
  }
724
757
 
725
- return Object.keys(next).length > 0 ? next : undefined;
758
+ return Object.keys(next).length > 0 ? next : null;
726
759
  }
727
760
 
728
- private assignPositiveIntegerSetting(
761
+ private assignOptionalPositiveIntegerSetting(
729
762
  target: interfaces.data.IRemoteIngressPerformanceConfig,
730
763
  key: 'maxStreamsPerEdge' | 'clientWriteTimeoutMs' | 'firstDataConnectTimeoutMs',
731
764
  value: any,
@@ -733,6 +766,7 @@ export class OpsViewRemoteIngress extends DeesElement {
733
766
  ): void {
734
767
  const text = `${value || ''}`.trim();
735
768
  if (!text) {
769
+ delete target[key];
736
770
  return;
737
771
  }
738
772
  const parsed = Number.parseInt(text, 10);
@@ -755,4 +789,12 @@ export class OpsViewRemoteIngress extends DeesElement {
755
789
  }
756
790
  return [...new Set(ports)].sort((a, b) => a - b);
757
791
  }
792
+
793
+ private parseRequiredPort(value: any, label: string): number {
794
+ const port = Number.parseInt(`${value || ''}`.trim(), 10);
795
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
796
+ throw new Error(`${label} must be a valid port number`);
797
+ }
798
+ return port;
799
+ }
758
800
  }
@@ -1,76 +0,0 @@
1
- import * as plugins from '../plugins.js';
2
- /**
3
- * Base class for all cached documents with TTL support
4
- *
5
- * Extends smartdata's SmartDataDbDoc to add:
6
- * - Automatic timestamps (createdAt, lastAccessedAt)
7
- * - TTL/expiration support (expiresAt)
8
- * - Helper methods for TTL management
9
- *
10
- * NOTE: Subclasses MUST add @svDb() decorators to createdAt, expiresAt, and lastAccessedAt
11
- * since decorators on abstract classes don't propagate correctly.
12
- */
13
- export declare abstract class CachedDocument<T extends CachedDocument<T>> extends plugins.smartdata.SmartDataDbDoc<T, T> {
14
- /**
15
- * Timestamp when the document was created
16
- * NOTE: Subclasses must add @svDb() decorator
17
- */
18
- createdAt: Date;
19
- /**
20
- * Timestamp when the document expires and should be cleaned up
21
- * NOTE: Subclasses must add @svDb() decorator
22
- */
23
- expiresAt: Date;
24
- /**
25
- * Timestamp of last access (for LRU-style eviction if needed)
26
- * NOTE: Subclasses must add @svDb() decorator
27
- */
28
- lastAccessedAt: Date;
29
- /**
30
- * Set the TTL (time to live) for this document
31
- * @param ttlMs Time to live in milliseconds
32
- */
33
- setTTL(ttlMs: number): void;
34
- /**
35
- * Set TTL using days
36
- * @param days Number of days until expiration
37
- */
38
- setTTLDays(days: number): void;
39
- /**
40
- * Set TTL using hours
41
- * @param hours Number of hours until expiration
42
- */
43
- setTTLHours(hours: number): void;
44
- /**
45
- * Check if this document has expired
46
- */
47
- isExpired(): boolean;
48
- /**
49
- * Update the lastAccessedAt timestamp
50
- */
51
- touch(): void;
52
- /**
53
- * Get remaining TTL in milliseconds
54
- * Returns 0 if expired, -1 if no expiration set
55
- */
56
- getRemainingTTL(): number;
57
- /**
58
- * Extend the TTL by the specified milliseconds from now
59
- * @param ttlMs Additional time to live in milliseconds
60
- */
61
- extendTTL(ttlMs: number): void;
62
- /**
63
- * Set the document to never expire (100 years in the future)
64
- */
65
- setNeverExpires(): void;
66
- }
67
- /**
68
- * TTL constants in milliseconds
69
- */
70
- export declare const TTL: {
71
- readonly HOURS_1: number;
72
- readonly HOURS_24: number;
73
- readonly DAYS_7: number;
74
- readonly DAYS_30: number;
75
- readonly DAYS_90: number;
76
- };