@serve.zone/dcrouter 17.0.0 → 17.1.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 (141) hide show
  1. package/.smartconfig.json +7 -4
  2. package/deno.json +1 -1
  3. package/dist_serve/bundle.js +1316 -1151
  4. package/dist_ts/00_commitinfo_data.js +1 -1
  5. package/dist_ts/classes.dcrouter.d.ts +3 -1
  6. package/dist_ts/classes.dcrouter.js +113 -52
  7. package/dist_ts/db/classes.cache.cleaner.d.ts +3 -0
  8. package/dist_ts/db/classes.cache.cleaner.js +7 -3
  9. package/dist_ts/db/classes.dcrouter-db.js +1 -2
  10. package/dist_ts/db/documents/classes.cached.email.d.ts +41 -4
  11. package/dist_ts/db/documents/classes.cached.email.js +36 -5
  12. package/dist_ts/db/documents/classes.dns-record.doc.js +3 -1
  13. package/dist_ts/dns/classes.dns-server-runtime.d.ts +0 -1
  14. package/dist_ts/dns/classes.dns-server-runtime.js +4 -36
  15. package/dist_ts/dns/index.d.ts +1 -0
  16. package/dist_ts/dns/index.js +2 -1
  17. package/dist_ts/dns/manager.dns.d.ts +1 -0
  18. package/dist_ts/dns/manager.dns.js +49 -14
  19. package/dist_ts/dns/providers/cloudflare.provider.js +19 -4
  20. package/dist_ts/dns/txt-record-presentation.d.ts +20 -0
  21. package/dist_ts/dns/txt-record-presentation.js +131 -0
  22. package/dist_ts/email/classes.accepted-email-spool.d.ts +32 -3
  23. package/dist_ts/email/classes.accepted-email-spool.js +174 -13
  24. package/dist_ts/email/classes.email-domain.manager.d.ts +14 -1
  25. package/dist_ts/email/classes.email-domain.manager.js +151 -8
  26. package/dist_ts/email/classes.email-settings.manager.js +2 -2
  27. package/dist_ts/email/classes.mail-dns-sync.d.ts +31 -5
  28. package/dist_ts/email/classes.mail-dns-sync.js +508 -117
  29. package/dist_ts/email/classes.smartmta-blob-storage-manager.d.ts +2 -2
  30. package/dist_ts/email/classes.smartmta-blob-storage-manager.js +4 -3
  31. package/dist_ts/email/classes.workapp-mail-manager.d.ts +1 -0
  32. package/dist_ts/email/classes.workapp-mail-manager.js +26 -2
  33. package/dist_ts/email/index.d.ts +2 -0
  34. package/dist_ts/email/index.js +3 -1
  35. package/dist_ts/email/mail-dns-txt-policy.d.ts +18 -0
  36. package/dist_ts/email/mail-dns-txt-policy.js +466 -0
  37. package/dist_ts/email/reconcile-mail-dns-singleton.d.ts +47 -0
  38. package/dist_ts/email/reconcile-mail-dns-singleton.js +159 -0
  39. package/dist_ts/opsserver/classes.opsserver.d.ts +8 -0
  40. package/dist_ts/opsserver/classes.opsserver.js +34 -1
  41. package/dist_ts/opsserver/handlers/certificate.handler.js +25 -4
  42. package/dist_ts/opsserver/handlers/config.handler.js +9 -5
  43. package/dist_ts/opsserver/handlers/dns-provider.handler.js +19 -2
  44. package/dist_ts/opsserver/handlers/dns-record.handler.js +25 -4
  45. package/dist_ts/opsserver/handlers/domain.handler.js +37 -3
  46. package/dist_ts/opsserver/handlers/email-domain.handler.js +5 -7
  47. package/dist_ts/opsserver/handlers/email-ops.handler.d.ts +1 -0
  48. package/dist_ts/opsserver/handlers/email-ops.handler.js +26 -7
  49. package/dist_ts/opsserver/handlers/index.d.ts +1 -0
  50. package/dist_ts/opsserver/handlers/index.js +2 -1
  51. package/dist_ts/opsserver/handlers/logs.handler.d.ts +1 -1
  52. package/dist_ts/opsserver/handlers/logs.handler.js +3 -3
  53. package/dist_ts/opsserver/handlers/realtime.handler.d.ts +37 -0
  54. package/dist_ts/opsserver/handlers/realtime.handler.js +154 -0
  55. package/dist_ts/opsserver/handlers/remoteingress.handler.js +22 -1
  56. package/dist_ts/opsserver/handlers/route-management.handler.js +47 -5
  57. package/dist_ts/opsserver/handlers/stats.handler.js +5 -1
  58. package/dist_ts/paths.d.ts +0 -11
  59. package/dist_ts/paths.js +1 -22
  60. package/dist_ts/plugins.d.ts +0 -34
  61. package/dist_ts/plugins.js +1 -62
  62. package/dist_ts_interfaces/data/email-domain.d.ts +14 -0
  63. package/dist_ts_interfaces/requests/config.d.ts +1 -1
  64. package/dist_ts_interfaces/requests/email-ops.d.ts +34 -1
  65. package/dist_ts_interfaces/requests/index.d.ts +1 -0
  66. package/dist_ts_interfaces/requests/index.js +2 -1
  67. package/dist_ts_interfaces/requests/realtime.d.ts +38 -0
  68. package/dist_ts_interfaces/requests/realtime.js +11 -0
  69. package/dist_ts_migrations/index.d.ts +6 -0
  70. package/dist_ts_migrations/index.js +208 -9
  71. package/dist_ts_web/00_commitinfo_data.js +1 -1
  72. package/dist_ts_web/appstate/certificates.js +9 -2
  73. package/dist_ts_web/appstate/domains.js +16 -5
  74. package/dist_ts_web/appstate/email-domains.d.ts +3 -0
  75. package/dist_ts_web/appstate/email-domains.js +47 -14
  76. package/dist_ts_web/appstate/email-ops.d.ts +1 -0
  77. package/dist_ts_web/appstate/email-ops.js +20 -2
  78. package/dist_ts_web/appstate/realtime.d.ts +27 -0
  79. package/dist_ts_web/appstate/realtime.js +71 -0
  80. package/dist_ts_web/appstate/remoteingress.js +9 -3
  81. package/dist_ts_web/appstate/routes.js +16 -5
  82. package/dist_ts_web/appstate/runtime.js +135 -7
  83. package/dist_ts_web/appstate.d.ts +1 -0
  84. package/dist_ts_web/appstate.js +2 -1
  85. package/dist_ts_web/elements/email/email-domain-creation-progress.d.ts +33 -0
  86. package/dist_ts_web/elements/email/email-domain-creation-progress.js +257 -0
  87. package/dist_ts_web/elements/email/ops-view-email-domains.js +233 -68
  88. package/dist_ts_web/elements/email/ops-view-emails.d.ts +4 -2
  89. package/dist_ts_web/elements/email/ops-view-emails.js +25 -13
  90. package/dist_ts_web/elements/overview/ops-view-config.js +5 -5
  91. package/package.json +22 -19
  92. package/ts/00_commitinfo_data.ts +1 -1
  93. package/ts/classes.dcrouter.ts +124 -57
  94. package/ts/db/classes.cache.cleaner.ts +18 -3
  95. package/ts/db/classes.dcrouter-db.ts +0 -2
  96. package/ts/db/documents/classes.cached.email.ts +78 -4
  97. package/ts/db/documents/classes.dns-record.doc.ts +1 -0
  98. package/ts/dns/classes.dns-server-runtime.ts +9 -45
  99. package/ts/dns/index.ts +1 -0
  100. package/ts/dns/manager.dns.ts +50 -16
  101. package/ts/dns/providers/cloudflare.provider.ts +21 -3
  102. package/ts/dns/txt-record-presentation.ts +134 -0
  103. package/ts/email/classes.accepted-email-spool.ts +220 -14
  104. package/ts/email/classes.email-domain.manager.ts +201 -7
  105. package/ts/email/classes.email-settings.manager.ts +1 -1
  106. package/ts/email/classes.mail-dns-sync.ts +588 -122
  107. package/ts/email/classes.smartmta-blob-storage-manager.ts +3 -2
  108. package/ts/email/classes.workapp-mail-manager.ts +34 -1
  109. package/ts/email/index.ts +2 -0
  110. package/ts/email/mail-dns-txt-policy.ts +505 -0
  111. package/ts/email/reconcile-mail-dns-singleton.ts +224 -0
  112. package/ts/opsserver/classes.opsserver.ts +42 -0
  113. package/ts/opsserver/handlers/certificate.handler.ts +24 -3
  114. package/ts/opsserver/handlers/config.handler.ts +9 -5
  115. package/ts/opsserver/handlers/dns-provider.handler.ts +18 -1
  116. package/ts/opsserver/handlers/dns-record.handler.ts +24 -3
  117. package/ts/opsserver/handlers/domain.handler.ts +36 -2
  118. package/ts/opsserver/handlers/email-domain.handler.ts +4 -5
  119. package/ts/opsserver/handlers/email-ops.handler.ts +29 -6
  120. package/ts/opsserver/handlers/index.ts +1 -0
  121. package/ts/opsserver/handlers/logs.handler.ts +2 -2
  122. package/ts/opsserver/handlers/realtime.handler.ts +222 -0
  123. package/ts/opsserver/handlers/remoteingress.handler.ts +21 -0
  124. package/ts/opsserver/handlers/route-management.handler.ts +46 -4
  125. package/ts/opsserver/handlers/stats.handler.ts +4 -0
  126. package/ts/paths.ts +0 -25
  127. package/ts/plugins.ts +0 -68
  128. package/ts_web/00_commitinfo_data.ts +1 -1
  129. package/ts_web/appstate/certificates.ts +6 -1
  130. package/ts_web/appstate/domains.ts +11 -4
  131. package/ts_web/appstate/email-domains.ts +68 -14
  132. package/ts_web/appstate/email-ops.ts +20 -1
  133. package/ts_web/appstate/realtime.ts +109 -0
  134. package/ts_web/appstate/remoteingress.ts +6 -2
  135. package/ts_web/appstate/routes.ts +11 -4
  136. package/ts_web/appstate/runtime.ts +176 -7
  137. package/ts_web/appstate.ts +1 -0
  138. package/ts_web/elements/email/email-domain-creation-progress.ts +340 -0
  139. package/ts_web/elements/email/ops-view-email-domains.ts +273 -74
  140. package/ts_web/elements/email/ops-view-emails.ts +23 -16
  141. package/ts_web/elements/overview/ops-view-config.ts +4 -4
@@ -21,3 +21,4 @@ export * from './dns-record.handler.js';
21
21
  export * from './acme-config.handler.js';
22
22
  export * from './email-domain.handler.js';
23
23
  export * from './gatewayclient.handler.js';
24
+ export * from './realtime.handler.js';
@@ -193,7 +193,7 @@ export class LogsHandler {
193
193
 
194
194
  /**
195
195
  * Add a log destination to the base logger that pushes entries
196
- * to all connected ops_dashboard TypedSocket clients.
196
+ * only to server-authenticated Ops UI realtime subscribers.
197
197
  *
198
198
  * Uses a module-level singleton so the destination is added only once,
199
199
  * even across OpsServer restart cycles. The destination reads
@@ -218,7 +218,7 @@ export class LogsHandler {
218
218
 
219
219
  let connections: any[];
220
220
  try {
221
- connections = await typedsocket.findAllTargetConnectionsByTag('role', 'ops_dashboard');
221
+ connections = await opsServer.getAuthenticatedRealtimeConnections();
222
222
  } catch {
223
223
  return;
224
224
  }
@@ -0,0 +1,222 @@
1
+ import * as plugins from '../../plugins.js';
2
+ import type { OpsServer } from '../classes.opsserver.js';
3
+ import * as interfaces from '../../../ts_interfaces/index.js';
4
+ import { requireOpsAuth } from '../helpers/auth.js';
5
+
6
+ const REALTIME_TAG_PREFIX = 'dcrouterOpsRealtime:user:';
7
+
8
+ export const getOpsRealtimeUserTag = (userIdArg: string): string => {
9
+ return REALTIME_TAG_PREFIX + encodeURIComponent(userIdArg);
10
+ };
11
+
12
+ const createInitialRevisions = (): interfaces.requests.IOpsRealtimeRevisionMap => {
13
+ return Object.fromEntries(
14
+ interfaces.requests.opsRealtimeResources.map((resource) => [resource, 1]),
15
+ ) as unknown as interfaces.requests.IOpsRealtimeRevisionMap;
16
+ };
17
+
18
+ export class OpsRealtimeRevisionClock {
19
+ public readonly serverEpoch: string;
20
+ private revisions = createInitialRevisions();
21
+
22
+ constructor(serverEpochArg = plugins.uuid.v4()) {
23
+ this.serverEpoch = serverEpochArg;
24
+ }
25
+
26
+ public getRevisions(): interfaces.requests.IOpsRealtimeRevisionMap {
27
+ return { ...this.revisions };
28
+ }
29
+
30
+ public invalidate(
31
+ resourceArg: interfaces.requests.TOpsRealtimeResource,
32
+ optionsArg: {
33
+ changedIds?: string[];
34
+ reason?: string;
35
+ timestamp?: number;
36
+ } = {},
37
+ ): interfaces.requests.IOpsRealtimeInvalidation {
38
+ const revision = this.revisions[resourceArg] + 1;
39
+ this.revisions[resourceArg] = revision;
40
+ return {
41
+ resource: resourceArg,
42
+ revision,
43
+ changedIds: optionsArg.changedIds?.length
44
+ ? Array.from(new Set(optionsArg.changedIds))
45
+ : undefined,
46
+ reason: optionsArg.reason,
47
+ timestamp: optionsArg.timestamp ?? Date.now(),
48
+ };
49
+ }
50
+
51
+ public getCatchup(
52
+ knownServerEpochArg?: string,
53
+ knownRevisionsArg: Partial<interfaces.requests.IOpsRealtimeRevisionMap> = {},
54
+ ): interfaces.requests.IOpsRealtimeInvalidation[] {
55
+ const epochChanged = knownServerEpochArg !== this.serverEpoch;
56
+ return interfaces.requests.opsRealtimeResources
57
+ .filter((resource) => epochChanged || (knownRevisionsArg[resource] ?? 0) < this.revisions[resource])
58
+ .map((resource) => ({
59
+ resource,
60
+ revision: this.revisions[resource],
61
+ timestamp: Date.now(),
62
+ reason: epochChanged ? 'initial-catchup' : 'revision-catchup',
63
+ }));
64
+ }
65
+ }
66
+
67
+ interface IWebSocketPeerForRealtime {
68
+ id: string;
69
+ tags: Set<string>;
70
+ }
71
+
72
+ interface IRealtimeInvalidationOptions {
73
+ changedIds?: string[];
74
+ reason?: string;
75
+ }
76
+
77
+ export class RealtimeHandler {
78
+ public typedrouter = new plugins.typedrequest.TypedRouter();
79
+ public readonly revisionClock = new OpsRealtimeRevisionClock();
80
+
81
+ private subscribedUserIds = new Set<string>();
82
+ private pendingInvalidations = new Map<
83
+ interfaces.requests.TOpsRealtimeResource,
84
+ interfaces.requests.IOpsRealtimeInvalidation
85
+ >();
86
+ private pushScheduled = false;
87
+ private stopped = false;
88
+
89
+ constructor(private opsServerRef: OpsServer) {
90
+ this.opsServerRef.viewRouter.addTypedRouter(this.typedrouter);
91
+ this.registerHandlers();
92
+ }
93
+
94
+ public invalidate(
95
+ resourcesArg:
96
+ | interfaces.requests.TOpsRealtimeResource
97
+ | interfaces.requests.TOpsRealtimeResource[],
98
+ optionsArg: IRealtimeInvalidationOptions = {},
99
+ ): void {
100
+ if (this.stopped) return;
101
+ const resources = Array.isArray(resourcesArg) ? resourcesArg : [resourcesArg];
102
+ for (const resource of new Set(resources)) {
103
+ const invalidation = this.revisionClock.invalidate(resource, optionsArg);
104
+ const existing = this.pendingInvalidations.get(resource);
105
+ this.pendingInvalidations.set(resource, {
106
+ ...invalidation,
107
+ changedIds: Array.from(new Set([
108
+ ...(existing?.changedIds || []),
109
+ ...(invalidation.changedIds || []),
110
+ ])),
111
+ });
112
+ }
113
+ this.schedulePush();
114
+ }
115
+
116
+ public async getAuthenticatedConnections(): Promise<
117
+ plugins.typedsocket.ISmartServeConnectionWrapper[]
118
+ > {
119
+ const typedsocket = this.opsServerRef.server?.typedserver?.typedsocket;
120
+ if (!typedsocket) return [];
121
+
122
+ const connections = new Map<string, plugins.typedsocket.ISmartServeConnectionWrapper>();
123
+ for (const userId of Array.from(this.subscribedUserIds)) {
124
+ const userConnections = await typedsocket.findAllTargetConnectionsByTag(
125
+ getOpsRealtimeUserTag(userId),
126
+ );
127
+ if (userConnections.length === 0) {
128
+ this.subscribedUserIds.delete(userId);
129
+ continue;
130
+ }
131
+ for (const connection of userConnections) {
132
+ connections.set(connection.peer.id, connection);
133
+ }
134
+ }
135
+ return Array.from(connections.values());
136
+ }
137
+
138
+ public cleanup(): void {
139
+ this.stopped = true;
140
+ this.pendingInvalidations.clear();
141
+ this.subscribedUserIds.clear();
142
+ }
143
+
144
+ private registerHandlers(): void {
145
+ this.typedrouter.addTypedHandler(
146
+ new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_SubscribeOpsRealtime>(
147
+ 'subscribeOpsRealtime',
148
+ async (dataArg, toolsArg) => {
149
+ const auth = await requireOpsAuth(this.opsServerRef, dataArg);
150
+ if (auth.type !== 'identity') {
151
+ throw new plugins.typedrequest.TypedResponseError(
152
+ 'Realtime UI subscriptions require a user identity',
153
+ );
154
+ }
155
+
156
+ const peer = toolsArg?.localData?.peer as IWebSocketPeerForRealtime | undefined;
157
+ if (!peer?.id || !peer.tags) {
158
+ throw new plugins.typedrequest.TypedResponseError(
159
+ 'Realtime subscriptions require a WebSocket connection',
160
+ );
161
+ }
162
+
163
+ for (const tag of Array.from(peer.tags)) {
164
+ if (tag.startsWith(REALTIME_TAG_PREFIX)) {
165
+ peer.tags.delete(tag);
166
+ }
167
+ }
168
+ peer.tags.add(getOpsRealtimeUserTag(auth.userId));
169
+ this.subscribedUserIds.add(auth.userId);
170
+
171
+ return {
172
+ serverEpoch: this.revisionClock.serverEpoch,
173
+ revisions: this.revisionClock.getRevisions(),
174
+ invalidations: this.revisionClock.getCatchup(
175
+ dataArg.knownServerEpoch,
176
+ dataArg.knownRevisions,
177
+ ),
178
+ };
179
+ },
180
+ ),
181
+ );
182
+ }
183
+
184
+ private schedulePush(): void {
185
+ if (this.pushScheduled) return;
186
+ this.pushScheduled = true;
187
+ queueMicrotask(() => {
188
+ this.pushScheduled = false;
189
+ void this.flushPushes();
190
+ });
191
+ }
192
+
193
+ private async flushPushes(): Promise<void> {
194
+ if (this.stopped || this.pendingInvalidations.size === 0) return;
195
+ const invalidations = Array.from(this.pendingInvalidations.values());
196
+ this.pendingInvalidations.clear();
197
+
198
+ let connections: plugins.typedsocket.ISmartServeConnectionWrapper[];
199
+ try {
200
+ connections = await this.getAuthenticatedConnections();
201
+ } catch {
202
+ return;
203
+ }
204
+
205
+ const typedsocket = this.opsServerRef.server?.typedserver?.typedsocket;
206
+ if (!typedsocket) return;
207
+
208
+ await Promise.allSettled(
209
+ connections.map(async (connection) => {
210
+ const request =
211
+ typedsocket.createTypedRequest<interfaces.requests.IReq_PushOpsRealtimeInvalidation>(
212
+ 'pushOpsRealtimeInvalidation',
213
+ connection,
214
+ );
215
+ await request.fire({
216
+ serverEpoch: this.revisionClock.serverEpoch,
217
+ invalidations,
218
+ });
219
+ }),
220
+ );
221
+ }
222
+ }
@@ -70,6 +70,10 @@ export class RemoteIngressHandler {
70
70
  mailHostname: dataArg.mailHostname,
71
71
  },
72
72
  ));
73
+ this.opsServerRef.invalidateRealtime?.(['edges', 'emailDomains', 'dns', 'routes'], {
74
+ changedIds: [edge.id],
75
+ reason: 'remote-ingress-created',
76
+ });
73
77
  return { success: true, edge };
74
78
  } catch (err: unknown) {
75
79
  return {
@@ -100,6 +104,12 @@ export class RemoteIngressHandler {
100
104
  throw err;
101
105
  });
102
106
 
107
+ if (deleted) {
108
+ this.opsServerRef.invalidateRealtime?.(['edges', 'emailDomains', 'dns', 'routes'], {
109
+ changedIds: [dataArg.id],
110
+ reason: 'remote-ingress-deleted',
111
+ });
112
+ }
103
113
  return {
104
114
  success: deleted,
105
115
  message: deleted ? undefined : 'Edge not found or RemoteIngress not configured',
@@ -155,6 +165,10 @@ export class RemoteIngressHandler {
155
165
  if (!result) {
156
166
  return { success: false, edge: null as any, message: failureMessage };
157
167
  }
168
+ this.opsServerRef.invalidateRealtime?.(['edges', 'emailDomains', 'dns', 'routes'], {
169
+ changedIds: [dataArg.id],
170
+ reason: 'remote-ingress-updated',
171
+ });
158
172
  return {
159
173
  success: true,
160
174
  edge: result,
@@ -184,6 +198,10 @@ export class RemoteIngressHandler {
184
198
  return { success: false, secret: '' };
185
199
  }
186
200
 
201
+ this.opsServerRef.invalidateRealtime?.('edges', {
202
+ changedIds: [dataArg.id],
203
+ reason: 'remote-ingress-secret-regenerated',
204
+ });
187
205
  return { success: true, secret };
188
206
  },
189
207
  ),
@@ -252,6 +270,9 @@ export class RemoteIngressHandler {
252
270
  updates,
253
271
  auth.userId,
254
272
  );
273
+ this.opsServerRef.invalidateRealtime?.(['edges', 'emailDomains', 'dns', 'routes'], {
274
+ reason: 'remote-ingress-hub-settings-updated',
275
+ });
255
276
  return { success: true, settings };
256
277
  } catch (err: unknown) {
257
278
  return { success: false, message: (err as Error).message };
@@ -68,6 +68,10 @@ export class RouteManagementHandler {
68
68
  return { success: false, message: 'Route management not initialized' };
69
69
  }
70
70
  const id = await manager.createRoute(dataArg.route, userId, dataArg.enabled ?? true, dataArg.metadata);
71
+ this.opsServerRef.invalidateRealtime?.('routes', {
72
+ changedIds: [id],
73
+ reason: 'route-created',
74
+ });
71
75
  return { success: true, routeId: id };
72
76
  },
73
77
  ),
@@ -88,6 +92,12 @@ export class RouteManagementHandler {
88
92
  enabled: dataArg.enabled,
89
93
  metadata: dataArg.metadata,
90
94
  });
95
+ if (result.success) {
96
+ this.opsServerRef.invalidateRealtime?.('routes', {
97
+ changedIds: [dataArg.id],
98
+ reason: 'route-updated',
99
+ });
100
+ }
91
101
  return result;
92
102
  },
93
103
  ),
@@ -103,7 +113,14 @@ export class RouteManagementHandler {
103
113
  if (!manager) {
104
114
  return { success: false, message: 'Route management not initialized' };
105
115
  }
106
- return manager.deleteRoute(dataArg.id);
116
+ const result = await manager.deleteRoute(dataArg.id);
117
+ if (result.success) {
118
+ this.opsServerRef.invalidateRealtime?.('routes', {
119
+ changedIds: [dataArg.id],
120
+ reason: 'route-deleted',
121
+ });
122
+ }
123
+ return result;
107
124
  },
108
125
  ),
109
126
  );
@@ -118,7 +135,14 @@ export class RouteManagementHandler {
118
135
  if (!manager) {
119
136
  return { success: false, message: 'Route management not initialized' };
120
137
  }
121
- return manager.toggleRoute(dataArg.id, dataArg.enabled);
138
+ const result = await manager.toggleRoute(dataArg.id, dataArg.enabled);
139
+ if (result.success) {
140
+ this.opsServerRef.invalidateRealtime?.('routes', {
141
+ changedIds: [dataArg.id],
142
+ reason: 'route-toggled',
143
+ });
144
+ }
145
+ return result;
122
146
  },
123
147
  ),
124
148
  );
@@ -138,6 +162,10 @@ export class RouteManagementHandler {
138
162
  userId,
139
163
  dataArg.enabled ?? true,
140
164
  );
165
+ this.opsServerRef.invalidateRealtime?.('routes', {
166
+ changedIds: [routeId],
167
+ reason: 'lets-encrypt-forward-created',
168
+ });
141
169
  return { success: true, routeId };
142
170
  } catch (error) {
143
171
  return { success: false, message: (error as Error).message };
@@ -156,11 +184,18 @@ export class RouteManagementHandler {
156
184
  return { success: false, message: 'Route management not initialized' };
157
185
  }
158
186
  try {
159
- return await manager.updateLetsEncryptHttp01Forward(
187
+ const result = await manager.updateLetsEncryptHttp01Forward(
160
188
  dataArg.id,
161
189
  dataArg.config,
162
190
  dataArg.enabled,
163
191
  );
192
+ if (result.success) {
193
+ this.opsServerRef.invalidateRealtime?.('routes', {
194
+ changedIds: [dataArg.id],
195
+ reason: 'lets-encrypt-forward-updated',
196
+ });
197
+ }
198
+ return result;
164
199
  } catch (error) {
165
200
  return { success: false, message: (error as Error).message };
166
201
  }
@@ -177,7 +212,14 @@ export class RouteManagementHandler {
177
212
  if (!manager) {
178
213
  return { success: false, message: 'Route management not initialized' };
179
214
  }
180
- return await manager.deleteLetsEncryptHttp01Forward(dataArg.id);
215
+ const result = await manager.deleteLetsEncryptHttp01Forward(dataArg.id);
216
+ if (result.success) {
217
+ this.opsServerRef.invalidateRealtime?.('routes', {
218
+ changedIds: [dataArg.id],
219
+ reason: 'lets-encrypt-forward-deleted',
220
+ });
221
+ }
222
+ return result;
181
223
  },
182
224
  ),
183
225
  );
@@ -432,6 +432,7 @@ export class StatsHandler {
432
432
  // Fallback to basic stats if MetricsManager not available
433
433
  const uptime = process.uptime();
434
434
  const memUsage = process.memoryUsage();
435
+ const hostMemoryBytes = Math.max(1, plugins.os.totalmem());
435
436
  const cpuUsage = plugins.os.loadavg()[0] * 100 / plugins.os.cpus().length;
436
437
 
437
438
  return {
@@ -445,6 +446,9 @@ export class StatsHandler {
445
446
  heapTotal: memUsage.heapTotal,
446
447
  external: memUsage.external,
447
448
  rss: memUsage.rss,
449
+ maxMemoryMB: hostMemoryBytes / (1024 * 1024),
450
+ actualUsageBytes: memUsage.rss,
451
+ actualUsagePercentage: (memUsage.rss / hostMemoryBytes) * 100,
448
452
  },
449
453
  requestsPerSecond: 0,
450
454
  activeConnections: 0,
package/ts/paths.ts CHANGED
@@ -10,18 +10,9 @@ export const distServe = plugins.path.join(packageDir, './dist_serve');
10
10
  // Default base for all dcrouter data (always user-writable)
11
11
  export const dcrouterHomeDir = plugins.path.join(plugins.os.homedir(), '.serve.zone', 'dcrouter');
12
12
 
13
- // Configure data directory with environment variable or default to ~/.serve.zone/dcrouter/data
14
- const DEFAULT_DATA_PATH = plugins.path.join(dcrouterHomeDir, 'data');
15
- export const dataDir = process.env.DATA_DIR
16
- ? process.env.DATA_DIR
17
- : DEFAULT_DATA_PATH;
18
-
19
13
  // Default TsmDB path for CacheDb
20
14
  export const defaultTsmDbPath = plugins.path.join(dcrouterHomeDir, 'tsmdb');
21
15
 
22
- // DNS records directory (only surviving MTA directory reference)
23
- export const dnsRecordsDir = plugins.path.join(dataDir, 'dns');
24
-
25
16
  /**
26
17
  * Resolve all data paths from a given baseDir.
27
18
  * When no baseDir is provided, falls back to ~/.serve.zone/dcrouter.
@@ -34,21 +25,5 @@ export function resolvePaths(baseDir?: string) {
34
25
  dcrouterHomeDir: root,
35
26
  dataDir: resolvedDataDir,
36
27
  defaultTsmDbPath: plugins.path.join(root, 'tsmdb'),
37
- dnsRecordsDir: plugins.path.join(resolvedDataDir, 'dns'),
38
28
  };
39
29
  }
40
-
41
- /**
42
- * Ensure only the data directories that are actually used exist.
43
- */
44
- export function ensureDataDirectories(resolvedPaths: ReturnType<typeof resolvePaths>) {
45
- plugins.fsUtils.ensureDirSync(resolvedPaths.dataDir);
46
- plugins.fsUtils.ensureDirSync(resolvedPaths.dnsRecordsDir);
47
- }
48
-
49
- /**
50
- * Legacy wrapper — delegates to ensureDataDirectories with module-level defaults.
51
- */
52
- export function ensureDirectories() {
53
- ensureDataDirectories(resolvePaths());
54
- }
package/ts/plugins.ts CHANGED
@@ -102,71 +102,3 @@ import * as uuid from 'uuid';
102
102
  export {
103
103
  uuid,
104
104
  }
105
-
106
- // Filesystem utilities
107
- export const fsUtils = {
108
- /**
109
- * Ensure a directory exists, creating it recursively if needed (sync)
110
- */
111
- ensureDirSync: (dirPath: string): void => {
112
- fs.mkdirSync(dirPath, { recursive: true });
113
- },
114
-
115
- /**
116
- * Ensure a directory exists, creating it recursively if needed (async)
117
- */
118
- ensureDir: async (dirPath: string): Promise<void> => {
119
- await fs.promises.mkdir(dirPath, { recursive: true });
120
- },
121
-
122
- /**
123
- * Write JSON content to a file synchronously
124
- */
125
- toFsSync: (content: any, filePath: string): void => {
126
- const data = typeof content === 'string' ? content : JSON.stringify(content, null, 2);
127
- fs.writeFileSync(filePath, data);
128
- },
129
-
130
- /**
131
- * Write JSON content to a file asynchronously
132
- */
133
- toFs: async (content: any, filePath: string): Promise<void> => {
134
- const data = typeof content === 'string' ? content : JSON.stringify(content, null, 2);
135
- await fs.promises.writeFile(filePath, data);
136
- },
137
-
138
- /**
139
- * Check if a file or directory exists
140
- */
141
- fileExistsSync: (filePath: string): boolean => {
142
- return fs.existsSync(filePath);
143
- },
144
-
145
- /**
146
- * Check if a file or directory exists (async)
147
- */
148
- fileExists: async (filePath: string): Promise<boolean> => {
149
- try {
150
- await fs.promises.access(filePath);
151
- return true;
152
- } catch {
153
- return false;
154
- }
155
- },
156
-
157
- /**
158
- * Read a JSON file synchronously
159
- */
160
- toObjectSync: <T = any>(filePath: string): T => {
161
- const content = fs.readFileSync(filePath, 'utf8');
162
- return JSON.parse(content) as T;
163
- },
164
-
165
- /**
166
- * Read a JSON file asynchronously
167
- */
168
- toObject: async <T = any>(filePath: string): Promise<T> => {
169
- const content = await fs.promises.readFile(filePath, 'utf8');
170
- return JSON.parse(content) as T;
171
- },
172
- };
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '17.0.0',
6
+ version: '17.1.1',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }
@@ -3,6 +3,7 @@ import type * as servezoneInterfaces from '@serve.zone/interfaces';
3
3
  import * as interfaces from '../../ts_interfaces/index.js';
4
4
  import { appState } from './shared.js';
5
5
  import { getActionContext } from './login.js';
6
+ import { captureRealtimeTicket, isRealtimeTicketCurrent } from './realtime.js';
6
7
 
7
8
  export interface ICertificateState {
8
9
  certificates: interfaces.requests.ICertificateInfo[];
@@ -31,6 +32,7 @@ export const fetchCertificateOverviewAction = certificateStatePart.createAction(
31
32
  const context = getActionContext();
32
33
  const currentState = statePartArg.getState()!;
33
34
  if (!context.identity) return currentState;
35
+ const ticket = captureRealtimeTicket('certificates');
34
36
 
35
37
  try {
36
38
  const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
@@ -40,8 +42,10 @@ export const fetchCertificateOverviewAction = certificateStatePart.createAction(
40
42
  const response = await request.fire({
41
43
  identity: context.identity,
42
44
  });
45
+ if (!isRealtimeTicketCurrent(ticket)) return statePartArg.getState()!;
43
46
 
44
47
  return {
48
+ ...statePartArg.getState()!,
45
49
  certificates: response.certificates,
46
50
  summary: response.summary,
47
51
  isLoading: false,
@@ -49,8 +53,9 @@ export const fetchCertificateOverviewAction = certificateStatePart.createAction(
49
53
  lastUpdated: Date.now(),
50
54
  };
51
55
  } catch (error) {
56
+ if (!isRealtimeTicketCurrent(ticket)) return statePartArg.getState()!;
52
57
  return {
53
- ...currentState,
58
+ ...statePartArg.getState()!,
54
59
  isLoading: false,
55
60
  error: error instanceof Error ? error.message : 'Failed to fetch certificate overview',
56
61
  };
@@ -2,6 +2,7 @@ import * as plugins from '../plugins.js';
2
2
  import * as interfaces from '../../ts_interfaces/index.js';
3
3
  import { appState } from './shared.js';
4
4
  import { getActionContext } from './login.js';
5
+ import { captureRealtimeTicket, isRealtimeTicketCurrent } from './realtime.js';
5
6
 
6
7
  // ============================================================================
7
8
  // Domains State (DNS providers + domains + records)
@@ -37,6 +38,7 @@ export const fetchDomainsAndProvidersAction = domainsStatePart.createAction(
37
38
  const context = getActionContext();
38
39
  const currentState = statePartArg.getState()!;
39
40
  if (!context.identity) return currentState;
41
+ const ticket = captureRealtimeTicket('dns');
40
42
 
41
43
  try {
42
44
  const providersRequest = new plugins.domtools.plugins.typedrequest.TypedRequest<
@@ -50,9 +52,10 @@ export const fetchDomainsAndProvidersAction = domainsStatePart.createAction(
50
52
  providersRequest.fire({ identity: context.identity }),
51
53
  domainsRequest.fire({ identity: context.identity }),
52
54
  ]);
55
+ if (!isRealtimeTicketCurrent(ticket)) return statePartArg.getState()!;
53
56
 
54
57
  return {
55
- ...currentState,
58
+ ...statePartArg.getState()!,
56
59
  providers: providersResponse.providers,
57
60
  domains: domainsResponse.domains,
58
61
  isLoading: false,
@@ -60,8 +63,9 @@ export const fetchDomainsAndProvidersAction = domainsStatePart.createAction(
60
63
  lastUpdated: Date.now(),
61
64
  };
62
65
  } catch (error: unknown) {
66
+ if (!isRealtimeTicketCurrent(ticket)) return statePartArg.getState()!;
63
67
  return {
64
- ...currentState,
68
+ ...statePartArg.getState()!,
65
69
  isLoading: false,
66
70
  error: error instanceof Error ? error.message : 'Failed to fetch domains/providers',
67
71
  };
@@ -74,6 +78,7 @@ export const fetchDnsRecordsForDomainAction = domainsStatePart.createAction<{ do
74
78
  const context = getActionContext();
75
79
  const currentState = statePartArg.getState()!;
76
80
  if (!context.identity) return currentState;
81
+ const ticket = captureRealtimeTicket('dns');
77
82
 
78
83
  try {
79
84
  const request = new plugins.domtools.plugins.typedrequest.TypedRequest<
@@ -83,15 +88,17 @@ export const fetchDnsRecordsForDomainAction = domainsStatePart.createAction<{ do
83
88
  identity: context.identity,
84
89
  domainId: dataArg.domainId,
85
90
  });
91
+ if (!isRealtimeTicketCurrent(ticket)) return statePartArg.getState()!;
86
92
  return {
87
- ...currentState,
93
+ ...statePartArg.getState()!,
88
94
  records: response.records,
89
95
  selectedDomainId: dataArg.domainId,
90
96
  error: null,
91
97
  };
92
98
  } catch (error: unknown) {
99
+ if (!isRealtimeTicketCurrent(ticket)) return statePartArg.getState()!;
93
100
  return {
94
- ...currentState,
101
+ ...statePartArg.getState()!,
95
102
  error: error instanceof Error ? error.message : 'Failed to fetch DNS records',
96
103
  };
97
104
  }