@serve.zone/dcrouter 16.5.0 → 17.1.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 (153) hide show
  1. package/.smartconfig.json +5 -2
  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 +6 -1
  6. package/dist_ts/classes.dcrouter.js +119 -41
  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/db/documents/classes.smartmta-storage.doc.d.ts +8 -0
  14. package/dist_ts/db/documents/classes.smartmta-storage.doc.js +91 -0
  15. package/dist_ts/db/documents/index.d.ts +1 -0
  16. package/dist_ts/db/documents/index.js +2 -1
  17. package/dist_ts/dns/classes.dns-server-runtime.d.ts +0 -1
  18. package/dist_ts/dns/classes.dns-server-runtime.js +4 -36
  19. package/dist_ts/dns/index.d.ts +1 -0
  20. package/dist_ts/dns/index.js +2 -1
  21. package/dist_ts/dns/manager.dns.d.ts +1 -0
  22. package/dist_ts/dns/manager.dns.js +49 -14
  23. package/dist_ts/dns/providers/cloudflare.provider.js +19 -4
  24. package/dist_ts/dns/txt-record-presentation.d.ts +20 -0
  25. package/dist_ts/dns/txt-record-presentation.js +131 -0
  26. package/dist_ts/email/classes.accepted-email-spool.d.ts +32 -3
  27. package/dist_ts/email/classes.accepted-email-spool.js +174 -13
  28. package/dist_ts/email/classes.email-domain.manager.d.ts +14 -1
  29. package/dist_ts/email/classes.email-domain.manager.js +151 -8
  30. package/dist_ts/email/classes.email-settings.manager.js +2 -1
  31. package/dist_ts/email/classes.mail-dns-sync.d.ts +31 -5
  32. package/dist_ts/email/classes.mail-dns-sync.js +508 -117
  33. package/dist_ts/email/classes.smartmta-blob-storage-manager.d.ts +27 -0
  34. package/dist_ts/email/classes.smartmta-blob-storage-manager.js +110 -0
  35. package/dist_ts/email/classes.smartmta-storage-manager.d.ts +6 -8
  36. package/dist_ts/email/classes.smartmta-storage-manager.js +63 -86
  37. package/dist_ts/email/classes.workapp-mail-manager.d.ts +1 -0
  38. package/dist_ts/email/classes.workapp-mail-manager.js +31 -4
  39. package/dist_ts/email/index.d.ts +4 -0
  40. package/dist_ts/email/index.js +5 -1
  41. package/dist_ts/email/mail-dns-txt-policy.d.ts +18 -0
  42. package/dist_ts/email/mail-dns-txt-policy.js +466 -0
  43. package/dist_ts/email/reconcile-mail-dns-singleton.d.ts +47 -0
  44. package/dist_ts/email/reconcile-mail-dns-singleton.js +159 -0
  45. package/dist_ts/email/smartmta-storage-policy.d.ts +9 -0
  46. package/dist_ts/email/smartmta-storage-policy.js +58 -0
  47. package/dist_ts/opsserver/classes.opsserver.d.ts +8 -0
  48. package/dist_ts/opsserver/classes.opsserver.js +34 -1
  49. package/dist_ts/opsserver/handlers/certificate.handler.js +25 -4
  50. package/dist_ts/opsserver/handlers/config.handler.js +9 -5
  51. package/dist_ts/opsserver/handlers/dns-provider.handler.js +19 -2
  52. package/dist_ts/opsserver/handlers/dns-record.handler.js +25 -4
  53. package/dist_ts/opsserver/handlers/domain.handler.js +37 -3
  54. package/dist_ts/opsserver/handlers/email-domain.handler.js +5 -7
  55. package/dist_ts/opsserver/handlers/email-ops.handler.d.ts +1 -0
  56. package/dist_ts/opsserver/handlers/email-ops.handler.js +26 -7
  57. package/dist_ts/opsserver/handlers/index.d.ts +1 -0
  58. package/dist_ts/opsserver/handlers/index.js +2 -1
  59. package/dist_ts/opsserver/handlers/logs.handler.d.ts +1 -1
  60. package/dist_ts/opsserver/handlers/logs.handler.js +3 -3
  61. package/dist_ts/opsserver/handlers/realtime.handler.d.ts +37 -0
  62. package/dist_ts/opsserver/handlers/realtime.handler.js +154 -0
  63. package/dist_ts/opsserver/handlers/remoteingress.handler.js +22 -1
  64. package/dist_ts/opsserver/handlers/route-management.handler.js +47 -5
  65. package/dist_ts/opsserver/handlers/stats.handler.js +3 -5
  66. package/dist_ts/paths.d.ts +0 -11
  67. package/dist_ts/paths.js +1 -22
  68. package/dist_ts/plugins.d.ts +2 -35
  69. package/dist_ts/plugins.js +3 -63
  70. package/dist_ts_interfaces/data/email-domain.d.ts +14 -0
  71. package/dist_ts_interfaces/requests/config.d.ts +1 -1
  72. package/dist_ts_interfaces/requests/email-ops.d.ts +34 -1
  73. package/dist_ts_interfaces/requests/index.d.ts +1 -0
  74. package/dist_ts_interfaces/requests/index.js +2 -1
  75. package/dist_ts_interfaces/requests/realtime.d.ts +38 -0
  76. package/dist_ts_interfaces/requests/realtime.js +11 -0
  77. package/dist_ts_migrations/index.d.ts +8 -0
  78. package/dist_ts_migrations/index.js +489 -12
  79. package/dist_ts_web/00_commitinfo_data.js +1 -1
  80. package/dist_ts_web/appstate/certificates.js +9 -2
  81. package/dist_ts_web/appstate/domains.js +16 -5
  82. package/dist_ts_web/appstate/email-domains.d.ts +3 -0
  83. package/dist_ts_web/appstate/email-domains.js +47 -14
  84. package/dist_ts_web/appstate/email-ops.d.ts +1 -0
  85. package/dist_ts_web/appstate/email-ops.js +20 -2
  86. package/dist_ts_web/appstate/realtime.d.ts +27 -0
  87. package/dist_ts_web/appstate/realtime.js +71 -0
  88. package/dist_ts_web/appstate/remoteingress.js +9 -3
  89. package/dist_ts_web/appstate/routes.js +16 -5
  90. package/dist_ts_web/appstate/runtime.js +135 -7
  91. package/dist_ts_web/appstate.d.ts +1 -0
  92. package/dist_ts_web/appstate.js +2 -1
  93. package/dist_ts_web/elements/email/email-domain-creation-progress.d.ts +33 -0
  94. package/dist_ts_web/elements/email/email-domain-creation-progress.js +257 -0
  95. package/dist_ts_web/elements/email/ops-view-email-domains.js +233 -68
  96. package/dist_ts_web/elements/email/ops-view-emails.d.ts +4 -2
  97. package/dist_ts_web/elements/email/ops-view-emails.js +25 -13
  98. package/dist_ts_web/elements/overview/ops-view-config.js +5 -5
  99. package/package.json +23 -19
  100. package/ts/00_commitinfo_data.ts +1 -1
  101. package/ts/classes.dcrouter.ts +136 -44
  102. package/ts/db/classes.cache.cleaner.ts +18 -3
  103. package/ts/db/classes.dcrouter-db.ts +0 -2
  104. package/ts/db/documents/classes.cached.email.ts +78 -4
  105. package/ts/db/documents/classes.dns-record.doc.ts +1 -0
  106. package/ts/db/documents/classes.smartmta-storage.doc.ts +25 -0
  107. package/ts/db/documents/index.ts +1 -0
  108. package/ts/dns/classes.dns-server-runtime.ts +9 -45
  109. package/ts/dns/index.ts +1 -0
  110. package/ts/dns/manager.dns.ts +50 -16
  111. package/ts/dns/providers/cloudflare.provider.ts +21 -3
  112. package/ts/dns/txt-record-presentation.ts +134 -0
  113. package/ts/email/classes.accepted-email-spool.ts +220 -14
  114. package/ts/email/classes.email-domain.manager.ts +201 -7
  115. package/ts/email/classes.email-settings.manager.ts +1 -0
  116. package/ts/email/classes.mail-dns-sync.ts +588 -122
  117. package/ts/email/classes.smartmta-blob-storage-manager.ts +139 -0
  118. package/ts/email/classes.smartmta-storage-manager.ts +86 -83
  119. package/ts/email/classes.workapp-mail-manager.ts +47 -4
  120. package/ts/email/index.ts +4 -0
  121. package/ts/email/mail-dns-txt-policy.ts +505 -0
  122. package/ts/email/reconcile-mail-dns-singleton.ts +224 -0
  123. package/ts/email/smartmta-storage-policy.ts +73 -0
  124. package/ts/opsserver/classes.opsserver.ts +42 -0
  125. package/ts/opsserver/handlers/certificate.handler.ts +24 -3
  126. package/ts/opsserver/handlers/config.handler.ts +9 -5
  127. package/ts/opsserver/handlers/dns-provider.handler.ts +18 -1
  128. package/ts/opsserver/handlers/dns-record.handler.ts +24 -3
  129. package/ts/opsserver/handlers/domain.handler.ts +36 -2
  130. package/ts/opsserver/handlers/email-domain.handler.ts +4 -5
  131. package/ts/opsserver/handlers/email-ops.handler.ts +29 -6
  132. package/ts/opsserver/handlers/index.ts +1 -0
  133. package/ts/opsserver/handlers/logs.handler.ts +2 -2
  134. package/ts/opsserver/handlers/realtime.handler.ts +222 -0
  135. package/ts/opsserver/handlers/remoteingress.handler.ts +21 -0
  136. package/ts/opsserver/handlers/route-management.handler.ts +46 -4
  137. package/ts/opsserver/handlers/stats.handler.ts +2 -4
  138. package/ts/paths.ts +0 -25
  139. package/ts/plugins.ts +2 -69
  140. package/ts_web/00_commitinfo_data.ts +1 -1
  141. package/ts_web/appstate/certificates.ts +6 -1
  142. package/ts_web/appstate/domains.ts +11 -4
  143. package/ts_web/appstate/email-domains.ts +68 -14
  144. package/ts_web/appstate/email-ops.ts +20 -1
  145. package/ts_web/appstate/realtime.ts +109 -0
  146. package/ts_web/appstate/remoteingress.ts +6 -2
  147. package/ts_web/appstate/routes.ts +11 -4
  148. package/ts_web/appstate/runtime.ts +176 -7
  149. package/ts_web/appstate.ts +1 -0
  150. package/ts_web/elements/email/email-domain-creation-progress.ts +340 -0
  151. package/ts_web/elements/email/ops-view-email-domains.ts +273 -74
  152. package/ts_web/elements/email/ops-view-emails.ts +23 -16
  153. package/ts_web/elements/overview/ops-view-config.ts +4 -4
@@ -134,7 +134,7 @@ export class EmailOpsHandler {
134
134
  if (this.isCachedEmailStoreAvailable()) {
135
135
  const cachedEmail = await CachedEmail.findById(emailId);
136
136
  if (cachedEmail) {
137
- return this.mapCachedEmailToEmailDetail(cachedEmail);
137
+ return await this.mapCachedEmailToEmailDetail(cachedEmail);
138
138
  }
139
139
  }
140
140
  return null;
@@ -158,23 +158,34 @@ export class EmailOpsHandler {
158
158
  subject: doc.subject || '',
159
159
  timestamp: new Date(doc.acceptedAt || 0).toISOString(),
160
160
  messageId: doc.messageId || '',
161
- size: this.formatSize(doc.rawContent?.length || 0),
161
+ size: this.formatSize(doc.rawContentSize || 0),
162
162
  };
163
163
  }
164
164
 
165
165
  /**
166
166
  * Map a CachedEmail row to catalog IEmailDetail format
167
167
  */
168
- private mapCachedEmailToEmailDetail(doc: CachedEmail): interfaces.requests.IEmailDetail {
168
+ private async mapCachedEmailToEmailDetail(
169
+ doc: CachedEmail,
170
+ ): Promise<interfaces.requests.IEmailDetail> {
169
171
  const base = this.mapCachedEmailToEmail(doc);
170
- const { headers, body } = this.parseRawEmail(doc.rawContent || '');
172
+ let rawContent = '';
173
+ try {
174
+ rawContent = (await this.opsServerRef.dcRouterRef.acceptedEmailSpool.readRawMessage(doc))
175
+ .toString('utf8');
176
+ } catch {
177
+ // Transaction history and delivery state remain available when a retained
178
+ // legacy row no longer has its raw payload.
179
+ }
180
+ const { headers, body } = this.parseRawEmail(rawContent);
171
181
  const session = this.parseCachedEmailSession(doc);
172
182
 
173
183
  return {
174
184
  ...base,
185
+ smtpTransactions: structuredClone(doc.smtpTransactions || []),
175
186
  toList: doc.to || [],
176
187
  cc: doc.cc || [],
177
- smtpLog: [],
188
+ smtpLog: this.flattenSmtpTransactions(doc.smtpTransactions || []),
178
189
  connectionInfo: {
179
190
  sourceIp: session.remoteAddress || '',
180
191
  sourceHostname: session.clientHostname || '',
@@ -344,9 +355,10 @@ export class EmailOpsHandler {
344
355
 
345
356
  return {
346
357
  ...base,
358
+ smtpTransactions: structuredClone(item.smtpTransactions || []),
347
359
  toList,
348
360
  cc,
349
- smtpLog: [],
361
+ smtpLog: this.flattenSmtpTransactions(item.smtpTransactions || []),
350
362
  connectionInfo: {
351
363
  sourceIp: '',
352
364
  sourceHostname: '',
@@ -393,6 +405,17 @@ export class EmailOpsHandler {
393
405
  }
394
406
  }
395
407
 
408
+ private flattenSmtpTransactions(
409
+ transactionsArg: interfaces.requests.ISmtpTransactionAttempt[],
410
+ ): interfaces.requests.ISmtpLogEntry[] {
411
+ return transactionsArg.flatMap((transaction) => transaction.transcript.map((entry) => ({
412
+ timestamp: new Date(entry.timestampMs).toISOString(),
413
+ direction: entry.direction,
414
+ command: entry.text,
415
+ responseCode: entry.responseCode,
416
+ })));
417
+ }
418
+
396
419
  /**
397
420
  * Format byte size to human-readable string
398
421
  */
@@ -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
  );
@@ -562,10 +562,8 @@ export class StatsHandler {
562
562
  const emailLike = item.processingResult;
563
563
  const recipients = Array.isArray(emailLike?.to)
564
564
  ? emailLike.to
565
- : Array.isArray(emailLike?.email?.to)
566
- ? emailLike.email.to
567
- : [];
568
- const subject = emailLike?.subject || emailLike?.email?.subject || '';
565
+ : [];
566
+ const subject = emailLike?.subject || '';
569
567
  return {
570
568
  id: item.id,
571
569
  recipient: recipients[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
@@ -54,6 +54,7 @@ export {
54
54
  import * as projectinfo from '@push.rocks/projectinfo';
55
55
  import * as qenv from '@push.rocks/qenv';
56
56
  import * as smartacme from '@push.rocks/smartacme';
57
+ import * as smartbucket from '@push.rocks/smartbucket';
57
58
  import * as smartchallenge from '@push.rocks/smartchallenge';
58
59
  import * as smartdata from '@push.rocks/smartdata';
59
60
  import * as smartdns from '@push.rocks/smartdns';
@@ -76,7 +77,7 @@ import * as smartrx from '@push.rocks/smartrx';
76
77
  import * as smartunique from '@push.rocks/smartunique';
77
78
  import * as taskbuffer from '@push.rocks/taskbuffer';
78
79
 
79
- export { projectinfo, qenv, smartacme, smartchallenge, smartdata, smartdns, smartfs, smartguard, smartjwt, smartlog, smartmetrics, smartmcp, smartdb, smartmta, smartnetwork, smartpath, smartproxy, smartpromise, smartradius, smartrequest, smartrx, smartunique, smartvpn, taskbuffer };
80
+ export { projectinfo, qenv, smartacme, smartbucket, smartchallenge, smartdata, smartdns, smartfs, smartguard, smartjwt, smartlog, smartmetrics, smartmcp, smartdb, smartmta, smartnetwork, smartpath, smartproxy, smartpromise, smartradius, smartrequest, smartrx, smartunique, smartvpn, taskbuffer };
80
81
 
81
82
  // Define SmartLog types for use in error handling
82
83
  export type TLogLevel = 'error' | 'warn' | 'info' | 'success' | 'debug';
@@ -101,71 +102,3 @@ import * as uuid from 'uuid';
101
102
  export {
102
103
  uuid,
103
104
  }
104
-
105
- // Filesystem utilities
106
- export const fsUtils = {
107
- /**
108
- * Ensure a directory exists, creating it recursively if needed (sync)
109
- */
110
- ensureDirSync: (dirPath: string): void => {
111
- fs.mkdirSync(dirPath, { recursive: true });
112
- },
113
-
114
- /**
115
- * Ensure a directory exists, creating it recursively if needed (async)
116
- */
117
- ensureDir: async (dirPath: string): Promise<void> => {
118
- await fs.promises.mkdir(dirPath, { recursive: true });
119
- },
120
-
121
- /**
122
- * Write JSON content to a file synchronously
123
- */
124
- toFsSync: (content: any, filePath: string): void => {
125
- const data = typeof content === 'string' ? content : JSON.stringify(content, null, 2);
126
- fs.writeFileSync(filePath, data);
127
- },
128
-
129
- /**
130
- * Write JSON content to a file asynchronously
131
- */
132
- toFs: async (content: any, filePath: string): Promise<void> => {
133
- const data = typeof content === 'string' ? content : JSON.stringify(content, null, 2);
134
- await fs.promises.writeFile(filePath, data);
135
- },
136
-
137
- /**
138
- * Check if a file or directory exists
139
- */
140
- fileExistsSync: (filePath: string): boolean => {
141
- return fs.existsSync(filePath);
142
- },
143
-
144
- /**
145
- * Check if a file or directory exists (async)
146
- */
147
- fileExists: async (filePath: string): Promise<boolean> => {
148
- try {
149
- await fs.promises.access(filePath);
150
- return true;
151
- } catch {
152
- return false;
153
- }
154
- },
155
-
156
- /**
157
- * Read a JSON file synchronously
158
- */
159
- toObjectSync: <T = any>(filePath: string): T => {
160
- const content = fs.readFileSync(filePath, 'utf8');
161
- return JSON.parse(content) as T;
162
- },
163
-
164
- /**
165
- * Read a JSON file asynchronously
166
- */
167
- toObject: async <T = any>(filePath: string): Promise<T> => {
168
- const content = await fs.promises.readFile(filePath, 'utf8');
169
- return JSON.parse(content) as T;
170
- },
171
- };
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '16.5.0',
6
+ version: '17.1.0',
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
  };