@serve.zone/dcrouter 17.2.3 → 17.3.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 (98) hide show
  1. package/deno.json +1 -1
  2. package/dist_serve/bundle.js +2219 -1887
  3. package/dist_ts/00_commitinfo_data.js +1 -1
  4. package/dist_ts/classes.dcrouter.d.ts +4 -1
  5. package/dist_ts/classes.dcrouter.js +42 -2
  6. package/dist_ts/config/classes.api-token-manager.d.ts +20 -2
  7. package/dist_ts/config/classes.api-token-manager.js +201 -6
  8. package/dist_ts/config/classes.gateway-client-manager.d.ts +36 -2
  9. package/dist_ts/config/classes.gateway-client-manager.js +391 -27
  10. package/dist_ts/db/documents/classes.api-token.doc.d.ts +2 -1
  11. package/dist_ts/db/documents/classes.api-token.doc.js +8 -2
  12. package/dist_ts/db/documents/classes.gateway-client.doc.d.ts +25 -0
  13. package/dist_ts/db/documents/classes.gateway-client.doc.js +73 -2
  14. package/dist_ts/db/documents/classes.ops-config-event.doc.d.ts +55 -0
  15. package/dist_ts/db/documents/classes.ops-config-event.doc.js +296 -0
  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/dns/classes.gateway-route-dns-reconciler.d.ts +52 -0
  19. package/dist_ts/dns/classes.gateway-route-dns-reconciler.js +609 -0
  20. package/dist_ts/dns/index.d.ts +1 -0
  21. package/dist_ts/dns/index.js +2 -1
  22. package/dist_ts/dns/manager.dns.d.ts +19 -0
  23. package/dist_ts/dns/manager.dns.js +25 -1
  24. package/dist_ts/monitoring/classes.ops-event-manager.d.ts +32 -0
  25. package/dist_ts/monitoring/classes.ops-event-manager.js +71 -0
  26. package/dist_ts/monitoring/index.d.ts +1 -0
  27. package/dist_ts/monitoring/index.js +2 -1
  28. package/dist_ts/opsserver/classes.opsserver.d.ts +1 -0
  29. package/dist_ts/opsserver/classes.opsserver.js +3 -1
  30. package/dist_ts/opsserver/handlers/certificate.handler.d.ts +1 -0
  31. package/dist_ts/opsserver/handlers/certificate.handler.js +35 -5
  32. package/dist_ts/opsserver/handlers/config-event.handler.d.ts +12 -0
  33. package/dist_ts/opsserver/handlers/config-event.handler.js +47 -0
  34. package/dist_ts/opsserver/handlers/dns-record.handler.d.ts +1 -0
  35. package/dist_ts/opsserver/handlers/dns-record.handler.js +82 -3
  36. package/dist_ts/opsserver/handlers/gatewayclient.handler.d.ts +9 -1
  37. package/dist_ts/opsserver/handlers/gatewayclient.handler.js +714 -162
  38. package/dist_ts/opsserver/handlers/index.d.ts +1 -0
  39. package/dist_ts/opsserver/handlers/index.js +2 -1
  40. package/dist_ts/opsserver/helpers/gateway-client-auth.d.ts +25 -0
  41. package/dist_ts/opsserver/helpers/gateway-client-auth.js +168 -0
  42. package/dist_ts/plugins.d.ts +2 -1
  43. package/dist_ts/plugins.js +3 -2
  44. package/dist_ts_apiclient/classes.gatewayclient.d.ts +13 -1
  45. package/dist_ts_apiclient/classes.gatewayclient.js +18 -2
  46. package/dist_ts_interfaces/data/config-events.d.ts +34 -0
  47. package/dist_ts_interfaces/data/config-events.js +8 -0
  48. package/dist_ts_interfaces/data/index.d.ts +1 -0
  49. package/dist_ts_interfaces/data/index.js +2 -1
  50. package/dist_ts_interfaces/data/route-management.d.ts +28 -1
  51. package/dist_ts_interfaces/data/route-management.js +3 -1
  52. package/dist_ts_interfaces/data/workhoster.d.ts +1 -0
  53. package/dist_ts_interfaces/requests/config-events.d.ts +35 -0
  54. package/dist_ts_interfaces/requests/config-events.js +4 -0
  55. package/dist_ts_interfaces/requests/index.d.ts +1 -0
  56. package/dist_ts_interfaces/requests/index.js +2 -1
  57. package/dist_ts_interfaces/requests/realtime.d.ts +1 -1
  58. package/dist_ts_interfaces/requests/realtime.js +2 -1
  59. package/dist_ts_migrations/index.js +148 -9
  60. package/dist_ts_web/00_commitinfo_data.js +1 -1
  61. package/dist_ts_web/appstate/config-events.d.ts +19 -0
  62. package/dist_ts_web/appstate/config-events.js +68 -0
  63. package/dist_ts_web/appstate/runtime.js +7 -1
  64. package/dist_ts_web/appstate.d.ts +1 -0
  65. package/dist_ts_web/appstate.js +2 -1
  66. package/dist_ts_web/elements/ops-platform-events.d.ts +21 -0
  67. package/dist_ts_web/elements/ops-platform-events.js +361 -0
  68. package/dist_ts_web/elements/ops-view-logs.d.ts +4 -0
  69. package/dist_ts_web/elements/ops-view-logs.js +96 -9
  70. package/package.json +3 -3
  71. package/ts/00_commitinfo_data.ts +1 -1
  72. package/ts/classes.dcrouter.ts +48 -1
  73. package/ts/config/classes.api-token-manager.ts +237 -4
  74. package/ts/config/classes.gateway-client-manager.ts +463 -19
  75. package/ts/db/documents/classes.api-token.doc.ts +8 -1
  76. package/ts/db/documents/classes.gateway-client.doc.ts +86 -0
  77. package/ts/db/documents/classes.ops-config-event.doc.ts +257 -0
  78. package/ts/db/documents/index.ts +1 -0
  79. package/ts/dns/classes.gateway-route-dns-reconciler.ts +801 -0
  80. package/ts/dns/index.ts +1 -0
  81. package/ts/dns/manager.dns.ts +36 -0
  82. package/ts/monitoring/classes.ops-event-manager.ts +99 -0
  83. package/ts/monitoring/index.ts +2 -1
  84. package/ts/opsserver/classes.opsserver.ts +2 -0
  85. package/ts/opsserver/handlers/certificate.handler.ts +51 -4
  86. package/ts/opsserver/handlers/config-event.handler.ts +62 -0
  87. package/ts/opsserver/handlers/dns-record.handler.ts +101 -2
  88. package/ts/opsserver/handlers/gatewayclient.handler.ts +804 -136
  89. package/ts/opsserver/handlers/index.ts +1 -0
  90. package/ts/opsserver/helpers/gateway-client-auth.ts +198 -0
  91. package/ts/plugins.ts +2 -0
  92. package/ts_apiclient/classes.gatewayclient.ts +45 -0
  93. package/ts_web/00_commitinfo_data.ts +1 -1
  94. package/ts_web/appstate/config-events.ts +98 -0
  95. package/ts_web/appstate/runtime.ts +8 -1
  96. package/ts_web/appstate.ts +1 -0
  97. package/ts_web/elements/ops-platform-events.ts +315 -0
  98. package/ts_web/elements/ops-view-logs.ts +84 -7
@@ -34,6 +34,7 @@ var __runInitializers = (this && this.__runInitializers) || function (thisArg, i
34
34
  };
35
35
  import * as shared from './shared/index.js';
36
36
  import * as appstate from '../appstate.js';
37
+ import './ops-platform-events.js';
37
38
  import { DeesElement, customElement, html, state, css, cssManager, } from '@design.estate/dees-element';
38
39
  let OpsViewLogs = (() => {
39
40
  let _classDecorators = [customElement('ops-view-logs')];
@@ -44,12 +45,22 @@ let OpsViewLogs = (() => {
44
45
  let _logState_decorators;
45
46
  let _logState_initializers = [];
46
47
  let _logState_extraInitializers = [];
48
+ let _activeTab_decorators;
49
+ let _activeTab_initializers = [];
50
+ let _activeTab_extraInitializers = [];
51
+ let _unacknowledgedCount_decorators;
52
+ let _unacknowledgedCount_initializers = [];
53
+ let _unacknowledgedCount_extraInitializers = [];
47
54
  var OpsViewLogs = class extends _classSuper {
48
55
  static { _classThis = this; }
49
56
  static {
50
57
  const _metadata = typeof Symbol === "function" && Symbol.metadata ? Object.create(_classSuper[Symbol.metadata] ?? null) : void 0;
51
58
  _logState_decorators = [state()];
59
+ _activeTab_decorators = [state()];
60
+ _unacknowledgedCount_decorators = [state()];
52
61
  __esDecorate(this, null, _logState_decorators, { kind: "accessor", name: "logState", static: false, private: false, access: { has: obj => "logState" in obj, get: obj => obj.logState, set: (obj, value) => { obj.logState = value; } }, metadata: _metadata }, _logState_initializers, _logState_extraInitializers);
62
+ __esDecorate(this, null, _activeTab_decorators, { kind: "accessor", name: "activeTab", static: false, private: false, access: { has: obj => "activeTab" in obj, get: obj => obj.activeTab, set: (obj, value) => { obj.activeTab = value; } }, metadata: _metadata }, _activeTab_initializers, _activeTab_extraInitializers);
63
+ __esDecorate(this, null, _unacknowledgedCount_decorators, { kind: "accessor", name: "unacknowledgedCount", static: false, private: false, access: { has: obj => "unacknowledgedCount" in obj, get: obj => obj.unacknowledgedCount, set: (obj, value) => { obj.unacknowledgedCount = value; } }, metadata: _metadata }, _unacknowledgedCount_initializers, _unacknowledgedCount_extraInitializers);
53
64
  __esDecorate(null, _classDescriptor = { value: _classThis }, _classDecorators, { kind: "class", name: _classThis.name, metadata: _metadata }, null, _classExtraInitializers);
54
65
  OpsViewLogs = _classThis = _classDescriptor.value;
55
66
  if (_metadata) Object.defineProperty(_classThis, Symbol.metadata, { enumerable: true, configurable: true, writable: true, value: _metadata });
@@ -61,7 +72,13 @@ let OpsViewLogs = (() => {
61
72
  });
62
73
  get logState() { return this.#logState_accessor_storage; }
63
74
  set logState(value) { this.#logState_accessor_storage = value; }
64
- lastPushedCount = (__runInitializers(this, _logState_extraInitializers), 0);
75
+ #activeTab_accessor_storage = (__runInitializers(this, _logState_extraInitializers), __runInitializers(this, _activeTab_initializers, 'application'));
76
+ get activeTab() { return this.#activeTab_accessor_storage; }
77
+ set activeTab(value) { this.#activeTab_accessor_storage = value; }
78
+ #unacknowledgedCount_accessor_storage = (__runInitializers(this, _activeTab_extraInitializers), __runInitializers(this, _unacknowledgedCount_initializers, 0));
79
+ get unacknowledgedCount() { return this.#unacknowledgedCount_accessor_storage; }
80
+ set unacknowledgedCount(value) { this.#unacknowledgedCount_accessor_storage = value; }
81
+ lastPushedCount = (__runInitializers(this, _unacknowledgedCount_extraInitializers), 0);
65
82
  constructor() {
66
83
  super();
67
84
  const subscription = appstate.logStatePart
@@ -70,27 +87,97 @@ let OpsViewLogs = (() => {
70
87
  this.logState = logState;
71
88
  });
72
89
  this.rxSubscriptions.push(subscription);
90
+ const configEventSubscription = appstate.configEventStatePart
91
+ .select((stateArg) => stateArg)
92
+ .subscribe((eventState) => {
93
+ this.unacknowledgedCount = eventState.unacknowledgedCount;
94
+ });
95
+ this.rxSubscriptions.push(configEventSubscription);
73
96
  }
74
97
  static styles = [
75
98
  cssManager.defaultStyles,
76
99
  shared.viewHostCss,
77
- css ``,
100
+ css `
101
+ .logTabs {
102
+ display: flex;
103
+ gap: 8px;
104
+ margin-bottom: 12px;
105
+ }
106
+
107
+ .logTab {
108
+ border: 1px solid var(--dees-color-border-default);
109
+ background: transparent;
110
+ color: var(--dees-color-text-secondary);
111
+ border-radius: 6px;
112
+ padding: 4px 12px;
113
+ font-size: 12px;
114
+ cursor: pointer;
115
+ transition: all 0.15s ease;
116
+ }
117
+
118
+ .logTab:hover {
119
+ background: var(--dees-color-hover);
120
+ color: var(--dees-color-text-primary);
121
+ }
122
+
123
+ .logTab.active {
124
+ border-color: var(--dees-color-border-strong);
125
+ color: var(--dees-color-text-primary);
126
+ }
127
+
128
+ .logTab .countBadge {
129
+ display: inline-block;
130
+ margin-left: 6px;
131
+ padding: 0 6px;
132
+ border-radius: 8px;
133
+ font-size: 11px;
134
+ font-weight: 600;
135
+ background: ${cssManager.bdTheme('rgba(220, 38, 38, 0.1)', 'rgba(220, 38, 38, 0.25)')};
136
+ color: ${cssManager.bdTheme('#dc2626', '#f87171')};
137
+ }
138
+ `,
78
139
  ];
79
140
  render() {
80
141
  return html `
81
142
  <dees-heading level="3">Logs</dees-heading>
82
143
 
83
- <dees-chart-log
84
- .label=${'Application Logs'}
85
- .autoScroll=${true}
86
- .maxEntries=${2000}
87
- .showMetrics=${true}
88
- ></dees-chart-log>
144
+ <div class="logTabs">
145
+ <button
146
+ class="logTab ${this.activeTab === 'application' ? 'active' : ''}"
147
+ @click=${() => this.switchTab('application')}
148
+ >Application Logs</button>
149
+ <button
150
+ class="logTab ${this.activeTab === 'platform' ? 'active' : ''}"
151
+ @click=${() => this.switchTab('platform')}
152
+ >Platform Events${this.unacknowledgedCount > 0 ? html `<span class="countBadge">${this.unacknowledgedCount}</span>` : ''}</button>
153
+ </div>
154
+
155
+ ${this.activeTab === 'application' ? html `
156
+ <dees-chart-log
157
+ .label=${'Application Logs'}
158
+ .autoScroll=${true}
159
+ .maxEntries=${2000}
160
+ .showMetrics=${true}
161
+ ></dees-chart-log>
162
+ ` : html `
163
+ <ops-platform-events></ops-platform-events>
164
+ `}
89
165
  `;
90
166
  }
167
+ switchTab(tab) {
168
+ if (this.activeTab === tab)
169
+ return;
170
+ this.activeTab = tab;
171
+ if (tab === 'application') {
172
+ // The chart element is recreated on tab switch; re-push everything.
173
+ this.lastPushedCount = 0;
174
+ }
175
+ }
91
176
  async connectedCallback() {
92
177
  super.connectedCallback();
93
178
  this.lastPushedCount = 0;
179
+ // Populate the platform-events badge without requiring a tab visit.
180
+ void appstate.configEventStatePart.dispatchAction(appstate.fetchConfigEventsAction, null);
94
181
  // Only fetch if state is empty (streaming will handle new entries)
95
182
  if (this.logState.recentLogs.length === 0) {
96
183
  await appstate.logStatePart.dispatchAction(appstate.fetchRecentLogsAction, { limit: 100 });
@@ -156,4 +243,4 @@ let OpsViewLogs = (() => {
156
243
  return OpsViewLogs = _classThis;
157
244
  })();
158
245
  export { OpsViewLogs };
159
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib3BzLXZpZXctbG9ncy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3RzX3dlYi9lbGVtZW50cy9vcHMtdmlldy1sb2dzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7QUFBQSxPQUFPLEtBQUssTUFBTSxNQUFNLG1CQUFtQixDQUFDO0FBQzVDLE9BQU8sS0FBSyxRQUFRLE1BQU0sZ0JBQWdCLENBQUM7QUFFM0MsT0FBTyxFQUNMLFdBQVcsRUFDWCxhQUFhLEVBQ2IsSUFBSSxFQUNKLEtBQUssRUFDTCxHQUFHLEVBQ0gsVUFBVSxHQUNYLE1BQU0sNkJBQTZCLENBQUM7SUFHeEIsV0FBVzs0QkFEdkIsYUFBYSxDQUFDLGVBQWUsQ0FBQzs7OztzQkFDRSxXQUFXOzs7OzJCQUFuQixTQUFRLFdBQVc7Ozs7b0NBQ3pDLEtBQUssRUFBRTtZQUNSLDZLQUFTLFFBQVEsNkJBQVIsUUFBUSwyRkFJZjtZQU5KLDZLQXNHQzs7OztRQXBHQyw2RUFBd0M7WUFDdEMsVUFBVSxFQUFFLEVBQUU7WUFDZCxXQUFXLEVBQUUsS0FBSztZQUNsQixPQUFPLEVBQUUsRUFBRTtTQUNaLEVBQUM7UUFKRixJQUFTLFFBQVEsOENBSWY7UUFKRixJQUFTLFFBQVEsb0RBSWY7UUFFTSxlQUFlLDBEQUFHLENBQUMsRUFBQztRQUU1QjtZQUNFLEtBQUssRUFBRSxDQUFDO1lBQ1IsTUFBTSxZQUFZLEdBQUcsUUFBUSxDQUFDLFlBQVk7aUJBQ3ZDLE1BQU0sQ0FBQyxDQUFDLFFBQVEsRUFBRSxFQUFFLENBQUMsUUFBUSxDQUFDO2lCQUM5QixTQUFTLENBQUMsQ0FBQyxRQUFRLEVBQUUsRUFBRTtnQkFDdEIsSUFBSSxDQUFDLFFBQVEsR0FBRyxRQUFRLENBQUM7WUFDM0IsQ0FBQyxDQUFDLENBQUM7WUFDTCxJQUFJLENBQUMsZUFBZSxDQUFDLElBQUksQ0FBQyxZQUFZLENBQUMsQ0FBQztRQUMxQyxDQUFDO1FB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
246
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib3BzLXZpZXctbG9ncy5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3RzX3dlYi9lbGVtZW50cy9vcHMtdmlldy1sb2dzLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7Ozs7QUFBQSxPQUFPLEtBQUssTUFBTSxNQUFNLG1CQUFtQixDQUFDO0FBQzVDLE9BQU8sS0FBSyxRQUFRLE1BQU0sZ0JBQWdCLENBQUM7QUFDM0MsT0FBTywwQkFBMEIsQ0FBQztBQUVsQyxPQUFPLEVBQ0wsV0FBVyxFQUNYLGFBQWEsRUFDYixJQUFJLEVBQ0osS0FBSyxFQUNMLEdBQUcsRUFDSCxVQUFVLEdBQ1gsTUFBTSw2QkFBNkIsQ0FBQztJQUd4QixXQUFXOzRCQUR2QixhQUFhLENBQUMsZUFBZSxDQUFDOzs7O3NCQUNFLFdBQVc7Ozs7Ozs7Ozs7MkJBQW5CLFNBQVEsV0FBVzs7OztvQ0FDekMsS0FBSyxFQUFFO3FDQU9QLEtBQUssRUFBRTsrQ0FHUCxLQUFLLEVBQUU7WUFUUiw2S0FBUyxRQUFRLDZCQUFSLFFBQVEsMkZBSWY7WUFHRixnTEFBUyxTQUFTLDZCQUFULFNBQVMsNkZBQTZDO1lBRy9ELDhNQUFTLG1CQUFtQiw2QkFBbkIsbUJBQW1CLGlIQUFLO1lBWm5DLDZLQWtMQzs7OztRQWhMQyw2RUFBd0M7WUFDdEMsVUFBVSxFQUFFLEVBQUU7WUFDZCxXQUFXLEVBQUUsS0FBSztZQUNsQixPQUFPLEVBQUUsRUFBRTtTQUNaLEVBQUM7UUFKRixJQUFTLFFBQVEsOENBSWY7UUFKRixJQUFTLFFBQVEsb0RBSWY7UUFHRixzSUFBaUQsYUFBYSxHQUFDO1FBQS9ELElBQVMsU0FBUywrQ0FBNkM7UUFBL0QsSUFBUyxTQUFTLHFEQUE2QztRQUcvRCwySkFBK0IsQ0FBQyxHQUFDO1FBQWpDLElBQVMsbUJBQW1CLHlEQUFLO1FBQWpDLElBQVMsbUJBQW1CLCtEQUFLO1FBRXpCLGVBQWUscUVBQUcsQ0FBQyxF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
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@serve.zone/dcrouter",
3
3
  "private": false,
4
- "version": "17.2.3",
4
+ "version": "17.3.0",
5
5
  "description": "A multifaceted routing service handling mail and SMS delivery functions.",
6
6
  "type": "module",
7
7
  "bin": {
@@ -62,8 +62,8 @@
62
62
  "@push.rocks/smartunique": "^3.0.10",
63
63
  "@push.rocks/smartvpn": "1.21.0",
64
64
  "@push.rocks/taskbuffer": "^8.0.2",
65
- "@serve.zone/catalog": "^2.15.0",
66
- "@serve.zone/interfaces": "^16.3.0",
65
+ "@serve.zone/catalog": "^2.16.0",
66
+ "@serve.zone/interfaces": "^17.1.0",
67
67
  "@serve.zone/remoteingress": "^5.0.0",
68
68
  "@tsclass/tsclass": "^9.5.1",
69
69
  "@types/qrcode": "^1.5.6",
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '17.2.3',
6
+ version: '17.3.0',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }
@@ -25,7 +25,7 @@ import { createMigrationRunner } from '../ts_migrations/index.js';
25
25
  import { commitinfo } from './00_commitinfo_data.js';
26
26
 
27
27
  import { OpsServer } from './opsserver/index.js';
28
- import { MetricsManager } from './monitoring/index.js';
28
+ import { MetricsManager, OpsEventManager } from './monitoring/index.js';
29
29
  import { RadiusServer, type IRadiusServerConfig } from './radius/index.js';
30
30
  import { RemoteIngressHubLifecycle, RemoteIngressManager, TunnelManager, buildEdgeOnlyDerivationRoutes } from './remoteingress/index.js';
31
31
  import { VpnAccessResolver, VpnManager, type IVpnManagerConfig } from './vpn/index.js';
@@ -35,6 +35,7 @@ import { SecurityLogger, ContentScanner, IPReputationChecker, SecurityPolicyMana
35
35
  import { type IHttp3Config, augmentRoutesWithHttp3 } from './http3/index.js';
36
36
  import { DnsManager } from './dns/manager.dns.js';
37
37
  import { DnsServerRuntime } from './dns/classes.dns-server-runtime.js';
38
+ import { GatewayRouteDnsReconciler } from './dns/classes.gateway-route-dns-reconciler.js';
38
39
  import { AcmeConfigManager } from './acme/manager.acme-config.js';
39
40
  import { SmartAcmeLifecycle } from './acme/classes.smartacme-lifecycle.js';
40
41
  import { AcceptedEmailSpool, EmailDomainManager, EmailRouteBuilder, EmailSettingsManager, MailDnsSync, MailEdgeEligibility, MailEgressCoordinator, ReleasedRemoteIngressEgressIdentitySource, SmartMtaBlobStorageManager, SmartMtaStorageManager, WorkAppMailManager, type ISmartMtaBlobStorageConfig, type TSmartMtaQueueItemLike } from './email/index.js';
@@ -311,6 +312,10 @@ export class DcRouter {
311
312
 
312
313
  // Domain / DNS management (DB-backed providers, domains, records)
313
314
  public dnsManager?: DnsManager;
315
+ public gatewayRouteDnsReconciler?: GatewayRouteDnsReconciler;
316
+
317
+ // Durable, acknowledgeable platform configuration events (DB-backed)
318
+ public opsEventManager?: OpsEventManager;
314
319
 
315
320
  // ACME configuration (DB-backed singleton, replaces tls.contactEmail)
316
321
  public acmeConfigManager?: AcmeConfigManager;
@@ -464,6 +469,27 @@ export class DcRouter {
464
469
  .withRetry({ maxRetries: 1, baseDelayMs: 1000 }),
465
470
  );
466
471
 
472
+ // OpsEventManager: optional, depends on DcRouterDb — durable acknowledgeable
473
+ // platform configuration events (config conflicts, not runtime errors).
474
+ if (this.options.dbConfig?.enabled !== false) {
475
+ this.serviceManager.addService(
476
+ new plugins.taskbuffer.Service('OpsEventManager')
477
+ .optional()
478
+ .dependsOn('DcRouterDb')
479
+ .withStart(async () => {
480
+ this.opsEventManager = new OpsEventManager();
481
+ await this.opsEventManager.start();
482
+ })
483
+ .withStop(async () => {
484
+ if (this.opsEventManager) {
485
+ await this.opsEventManager.stop();
486
+ this.opsEventManager = undefined;
487
+ }
488
+ })
489
+ .withRetry({ maxRetries: 1, baseDelayMs: 500 }),
490
+ );
491
+ }
492
+
467
493
  // DnsManager: optional, depends on DcRouterDb — owns DB-backed DNS state
468
494
  // (providers, domains, records). Must run before SmartProxy so ACME DNS-01
469
495
  // wiring can look up providers.
@@ -727,6 +753,27 @@ export class DcRouter {
727
753
  );
728
754
  }
729
755
 
756
+ // Gateway route DNS reconciliation starts only after durable route config
757
+ // and DNS providers are available.
758
+ if (this.options.dbConfig?.enabled !== false) {
759
+ this.serviceManager.addService(
760
+ new plugins.taskbuffer.Service('GatewayRouteDnsReconciler')
761
+ .optional()
762
+ .dependsOn('ConfigManagers', 'DnsManager')
763
+ .withStart(async () => {
764
+ this.gatewayRouteDnsReconciler = new GatewayRouteDnsReconciler(this);
765
+ await this.gatewayRouteDnsReconciler.start();
766
+ })
767
+ .withStop(async () => {
768
+ if (this.gatewayRouteDnsReconciler) {
769
+ await this.gatewayRouteDnsReconciler.stop();
770
+ this.gatewayRouteDnsReconciler = undefined;
771
+ }
772
+ })
773
+ .withRetry({ maxRetries: 1, baseDelayMs: 500 }),
774
+ );
775
+ }
776
+
730
777
  // Email Server: optional, depends on SmartProxy and durable DB persistence.
731
778
  if (this.options.dbConfig?.enabled !== false) {
732
779
  const emailServiceDeps = ['SmartProxy', 'MetricsManager'];
@@ -3,6 +3,7 @@ import { logger } from '../logger.js';
3
3
  import { ApiTokenDoc } from '../db/index.js';
4
4
  import type {
5
5
  IApiTokenPolicy,
6
+ IGatewayCredentialLifecycle,
6
7
  IStoredApiToken,
7
8
  IApiTokenInfo,
8
9
  TApiTokenScope,
@@ -11,9 +12,11 @@ import type {
11
12
  const TOKEN_PREFIX_STR = 'dcr_';
12
13
  const ENV_ADMIN_TOKEN_ID = 'env-admin-token';
13
14
  const ENV_ADMIN_TOKEN_CREATED_BY = 'dcrouter-env';
15
+ const LAST_USED_PERSIST_INTERVAL_MS = 60_000;
14
16
 
15
17
  export class ApiTokenManager {
16
18
  private tokens = new Map<string, IStoredApiToken>();
19
+ private lastUsedPersistedAt = new Map<string, number>();
17
20
 
18
21
  constructor() {}
19
22
 
@@ -38,6 +41,7 @@ export class ApiTokenManager {
38
41
  expiresInDays: number | null,
39
42
  createdBy: string,
40
43
  policy?: IApiTokenPolicy,
44
+ gatewayCredentialLifecycle: IGatewayCredentialLifecycle | null = null,
41
45
  ): Promise<{ id: string; rawToken: string }> {
42
46
  const id = plugins.uuid.v4();
43
47
  const randomBytes = plugins.crypto.randomBytes(32);
@@ -53,6 +57,7 @@ export class ApiTokenManager {
53
57
  tokenHash,
54
58
  scopes,
55
59
  policy,
60
+ gatewayCredentialLifecycle: this.normalizeGatewayCredentialLifecycle(gatewayCredentialLifecycle, id),
56
61
  createdAt: now,
57
62
  expiresAt: expiresInDays != null ? now + expiresInDays * 86400000 : null,
58
63
  lastUsedAt: null,
@@ -66,6 +71,33 @@ export class ApiTokenManager {
66
71
  return { id, rawToken };
67
72
  }
68
73
 
74
+ public async createProvisionedGatewayCredentialCandidate(
75
+ name: string,
76
+ scopes: TApiTokenScope[],
77
+ expiresInDays: number | null,
78
+ createdBy: string,
79
+ policy: IApiTokenPolicy,
80
+ credentialSequence: number,
81
+ policyDigest: string,
82
+ ): Promise<{ id: string; rawToken: string }> {
83
+ if (policy.role !== 'gatewayClient' || !policy.gatewayClient?.id
84
+ || !Number.isSafeInteger(policy.gatewayClient.policyGeneration)
85
+ || !Number.isSafeInteger(credentialSequence) || credentialSequence < 1
86
+ || !/^[a-f0-9]{64}$/.test(policyDigest)) {
87
+ throw new Error('Provisioned gateway credentials require a bound gateway-client policy');
88
+ }
89
+ return await this.createToken(name, scopes, expiresInDays, createdBy, policy, {
90
+ source: 'provisioned',
91
+ state: 'candidate',
92
+ sequence: credentialSequence,
93
+ policyGeneration: policy.gatewayClient.policyGeneration,
94
+ policyDigest,
95
+ finalizedAt: null,
96
+ cleanupCompletedAt: null,
97
+ revokedCredentialCount: null,
98
+ });
99
+ }
100
+
69
101
  /**
70
102
  * Validate a raw token string. Returns the stored token if valid, null otherwise.
71
103
  * Also updates lastUsedAt.
@@ -80,9 +112,18 @@ export class ApiTokenManager {
80
112
  if (!stored.enabled) return null;
81
113
  if (stored.expiresAt !== null && stored.expiresAt < Date.now()) return null;
82
114
 
83
- // Update lastUsedAt (fire and forget)
84
- stored.lastUsedAt = Date.now();
85
- this.persistToken(stored).catch(() => {});
115
+ const now = Date.now();
116
+ stored.lastUsedAt = now;
117
+ const persistedAt = this.lastUsedPersistedAt.get(stored.id) || 0;
118
+ if (now - persistedAt >= LAST_USED_PERSIST_INTERVAL_MS) {
119
+ this.lastUsedPersistedAt.set(stored.id, now);
120
+ try {
121
+ await this.persistToken(stored);
122
+ } catch (error) {
123
+ this.lastUsedPersistedAt.set(stored.id, persistedAt);
124
+ throw error;
125
+ }
126
+ }
86
127
  return stored;
87
128
  }
88
129
  }
@@ -146,13 +187,148 @@ export class ApiTokenManager {
146
187
  public async revokeToken(id: string): Promise<boolean> {
147
188
  if (!this.tokens.has(id)) return false;
148
189
  const token = this.tokens.get(id)!;
149
- this.tokens.delete(id);
190
+ // Disable durably before deletion so a failed delete cannot resurrect the
191
+ // credential after a restart.
192
+ token.enabled = false;
193
+ await this.persistToken(token);
150
194
  const doc = await ApiTokenDoc.findById(id);
151
195
  if (doc) await doc.delete();
196
+ this.tokens.delete(id);
197
+ this.lastUsedPersistedAt.delete(id);
152
198
  logger.log('info', `API token '${token.name}' revoked (id: ${id})`);
153
199
  return true;
154
200
  }
155
201
 
202
+ /** Revoke only ordinary gateway-client credentials bound to one live client. */
203
+ public async revokeGatewayClientCredentials(
204
+ gatewayClientId: string,
205
+ excludingTokenId?: string,
206
+ ): Promise<number> {
207
+ const ids = Array.from(this.tokens.values())
208
+ .filter((token) => token.id !== excludingTokenId)
209
+ .filter((token) => token.id !== ENV_ADMIN_TOKEN_ID)
210
+ .filter((token) => token.policy?.role === 'gatewayClient')
211
+ .filter((token) => token.policy?.gatewayClient?.id === gatewayClientId)
212
+ .map((token) => token.id);
213
+ let revoked = 0;
214
+ for (const id of ids) {
215
+ if (await this.revokeToken(id)) revoked++;
216
+ }
217
+ return revoked;
218
+ }
219
+
220
+ /** Revoke only older provisioned credentials during a finalized handover. */
221
+ public async revokeOlderProvisionedGatewayCredentials(
222
+ gatewayClientId: string,
223
+ finalizedSequence: number,
224
+ excludingTokenId: string,
225
+ ): Promise<number> {
226
+ if (!Number.isSafeInteger(finalizedSequence) || finalizedSequence < 1) {
227
+ throw new Error('Gateway credential finalization sequence is invalid');
228
+ }
229
+ const ids = Array.from(this.tokens.values())
230
+ .filter((token) => token.id !== excludingTokenId)
231
+ .filter((token) => token.policy?.role === 'gatewayClient')
232
+ .filter((token) => token.policy?.gatewayClient?.id === gatewayClientId)
233
+ .filter((token) => token.gatewayCredentialLifecycle?.source === 'provisioned')
234
+ .filter((token) => Number(token.gatewayCredentialLifecycle?.sequence) < finalizedSequence)
235
+ .map((token) => token.id);
236
+ let revoked = 0;
237
+ for (const id of ids) {
238
+ if (await this.revokeToken(id)) revoked++;
239
+ }
240
+ return revoked;
241
+ }
242
+
243
+ /** Durably activate a provisioned candidate while preserving retry identity. */
244
+ public async activateProvisionedGatewayCredential(
245
+ tokenId: string,
246
+ gatewayClientId: string,
247
+ ): Promise<{
248
+ finalizedAt: number;
249
+ wasAlreadyActive: boolean;
250
+ lifecycle: IGatewayCredentialLifecycle;
251
+ }> {
252
+ const token = this.tokens.get(tokenId);
253
+ if (!token || token.policy?.role !== 'gatewayClient'
254
+ || token.policy.gatewayClient?.id !== gatewayClientId) {
255
+ throw new Error('Provisioned gateway credential is missing or has the wrong owner');
256
+ }
257
+ const lifecycle = this.normalizeGatewayCredentialLifecycle(
258
+ token.gatewayCredentialLifecycle,
259
+ token.id,
260
+ );
261
+ if (!lifecycle) {
262
+ throw new Error('Manual gateway credentials cannot be finalized');
263
+ }
264
+ if (lifecycle.state === 'active') {
265
+ return {
266
+ finalizedAt: lifecycle.finalizedAt!,
267
+ wasAlreadyActive: true,
268
+ lifecycle,
269
+ };
270
+ }
271
+
272
+ const previousLifecycle = lifecycle;
273
+ const finalizedAt = Date.now();
274
+ token.gatewayCredentialLifecycle = {
275
+ ...lifecycle,
276
+ state: 'active',
277
+ finalizedAt,
278
+ cleanupCompletedAt: null,
279
+ revokedCredentialCount: null,
280
+ };
281
+ try {
282
+ await this.persistToken(token);
283
+ } catch (error) {
284
+ token.gatewayCredentialLifecycle = previousLifecycle;
285
+ throw error;
286
+ }
287
+ return {
288
+ finalizedAt,
289
+ wasAlreadyActive: false,
290
+ lifecycle: token.gatewayCredentialLifecycle,
291
+ };
292
+ }
293
+
294
+ /** Mark sibling cleanup complete so later retries never touch newer tokens. */
295
+ public async completeProvisionedGatewayCredentialHandover(
296
+ tokenId: string,
297
+ gatewayClientId: string,
298
+ revokedCredentialCount: number,
299
+ ): Promise<IGatewayCredentialLifecycle> {
300
+ const token = this.tokens.get(tokenId);
301
+ if (!token || token.policy?.role !== 'gatewayClient'
302
+ || token.policy.gatewayClient?.id !== gatewayClientId) {
303
+ throw new Error('Provisioned gateway credential is missing or has the wrong owner');
304
+ }
305
+ const lifecycle = this.normalizeGatewayCredentialLifecycle(
306
+ token.gatewayCredentialLifecycle,
307
+ token.id,
308
+ );
309
+ if (!lifecycle || lifecycle.state !== 'active') {
310
+ throw new Error('Provisioned gateway credential is not active');
311
+ }
312
+ if (lifecycle.cleanupCompletedAt !== null) return lifecycle;
313
+ if (!Number.isSafeInteger(revokedCredentialCount) || revokedCredentialCount < 0) {
314
+ throw new Error('Revoked credential count is invalid');
315
+ }
316
+ const previousLifecycle = lifecycle;
317
+ token.gatewayCredentialLifecycle = {
318
+ ...lifecycle,
319
+ cleanupCompletedAt: Date.now(),
320
+ revokedCredentialCount,
321
+ };
322
+ try {
323
+ await this.persistToken(token);
324
+ } catch (error) {
325
+ token.gatewayCredentialLifecycle = previousLifecycle;
326
+ throw error;
327
+ }
328
+ return token.gatewayCredentialLifecycle;
329
+ }
330
+
331
+
156
332
  /**
157
333
  * Roll (regenerate) a token's secret while keeping its identity.
158
334
  * Returns the new raw token value (shown once).
@@ -197,12 +373,14 @@ export class ApiTokenManager {
197
373
  tokenHash: doc.tokenHash,
198
374
  scopes: doc.scopes,
199
375
  policy: doc.policy,
376
+ gatewayCredentialLifecycle: this.normalizeGatewayCredentialLifecycle(doc.gatewayCredentialLifecycle, doc.id),
200
377
  createdAt: doc.createdAt,
201
378
  expiresAt: doc.expiresAt,
202
379
  lastUsedAt: doc.lastUsedAt,
203
380
  createdBy: doc.createdBy,
204
381
  enabled: doc.enabled,
205
382
  });
383
+ this.lastUsedPersistedAt.set(doc.id, doc.lastUsedAt || 0);
206
384
  }
207
385
  }
208
386
  }
@@ -226,6 +404,7 @@ export class ApiTokenManager {
226
404
  tokenHash: this.hashToken(rawToken),
227
405
  scopes: ['*'],
228
406
  policy: { role: 'admin' },
407
+ gatewayCredentialLifecycle: null,
229
408
  createdAt: existing?.createdAt || now,
230
409
  expiresAt: null,
231
410
  lastUsedAt: existing?.lastUsedAt || null,
@@ -238,6 +417,58 @@ export class ApiTokenManager {
238
417
  logger.log('info', `Environment admin API token ensured (id: ${stored.id})`);
239
418
  }
240
419
 
420
+ private normalizeGatewayCredentialLifecycle(
421
+ valueArg: unknown,
422
+ tokenIdArg: string,
423
+ ): IGatewayCredentialLifecycle | null {
424
+ if (valueArg == null) return null;
425
+ if (typeof valueArg !== 'object' || Array.isArray(valueArg)) {
426
+ throw new Error(`API token '${tokenIdArg}' has malformed gateway credential lifecycle`);
427
+ }
428
+ const value = valueArg as Partial<IGatewayCredentialLifecycle>;
429
+ if (value.source !== 'provisioned') {
430
+ throw new Error(`API token '${tokenIdArg}' has malformed gateway credential lifecycle`);
431
+ }
432
+ if (!Number.isSafeInteger(value.sequence) || Number(value.sequence) < 1
433
+ || !Number.isSafeInteger(value.policyGeneration) || Number(value.policyGeneration) < 1
434
+ || typeof value.policyDigest !== 'string' || !/^[a-f0-9]{64}$/.test(value.policyDigest)) {
435
+ throw new Error(`API token '${tokenIdArg}' has malformed gateway credential lifecycle`);
436
+ }
437
+ const base = {
438
+ source: 'provisioned' as const,
439
+ sequence: Number(value.sequence),
440
+ policyGeneration: Number(value.policyGeneration),
441
+ policyDigest: value.policyDigest,
442
+ };
443
+ if (value.state === 'candidate' && value.finalizedAt === null
444
+ && value.cleanupCompletedAt === null && value.revokedCredentialCount === null) {
445
+ return {
446
+ ...base,
447
+ state: 'candidate',
448
+ finalizedAt: null,
449
+ cleanupCompletedAt: null,
450
+ revokedCredentialCount: null,
451
+ };
452
+ }
453
+ if (value.state === 'active' && Number.isSafeInteger(value.finalizedAt) && Number(value.finalizedAt) > 0) {
454
+ const cleanupPending = value.cleanupCompletedAt === null && value.revokedCredentialCount === null;
455
+ const cleanupComplete = Number.isSafeInteger(value.cleanupCompletedAt)
456
+ && Number(value.cleanupCompletedAt) > 0
457
+ && Number.isSafeInteger(value.revokedCredentialCount)
458
+ && Number(value.revokedCredentialCount) >= 0;
459
+ if (cleanupPending || cleanupComplete) {
460
+ return {
461
+ ...base,
462
+ state: 'active',
463
+ finalizedAt: Number(value.finalizedAt),
464
+ cleanupCompletedAt: cleanupComplete ? Number(value.cleanupCompletedAt) : null,
465
+ revokedCredentialCount: cleanupComplete ? Number(value.revokedCredentialCount) : null,
466
+ };
467
+ }
468
+ }
469
+ throw new Error(`API token '${tokenIdArg}' has malformed gateway credential lifecycle`);
470
+ }
471
+
241
472
  private hashToken(rawToken: string): string {
242
473
  return plugins.crypto.createHash('sha256').update(rawToken).digest('hex');
243
474
  }
@@ -249,6 +480,7 @@ export class ApiTokenManager {
249
480
  existing.tokenHash = stored.tokenHash;
250
481
  existing.scopes = stored.scopes;
251
482
  existing.policy = stored.policy;
483
+ existing.gatewayCredentialLifecycle = stored.gatewayCredentialLifecycle ?? null;
252
484
  existing.createdAt = stored.createdAt;
253
485
  existing.expiresAt = stored.expiresAt;
254
486
  existing.lastUsedAt = stored.lastUsedAt;
@@ -262,6 +494,7 @@ export class ApiTokenManager {
262
494
  doc.tokenHash = stored.tokenHash;
263
495
  doc.scopes = stored.scopes;
264
496
  doc.policy = stored.policy;
497
+ doc.gatewayCredentialLifecycle = stored.gatewayCredentialLifecycle ?? null;
265
498
  doc.createdAt = stored.createdAt;
266
499
  doc.expiresAt = stored.expiresAt;
267
500
  doc.lastUsedAt = stored.lastUsedAt;