@serve.zone/dcrouter 17.0.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 (139) 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 +3 -1
  6. package/dist_ts/classes.dcrouter.js +110 -49
  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/paths.d.ts +0 -11
  58. package/dist_ts/paths.js +1 -22
  59. package/dist_ts/plugins.d.ts +0 -34
  60. package/dist_ts/plugins.js +1 -62
  61. package/dist_ts_interfaces/data/email-domain.d.ts +14 -0
  62. package/dist_ts_interfaces/requests/config.d.ts +1 -1
  63. package/dist_ts_interfaces/requests/email-ops.d.ts +34 -1
  64. package/dist_ts_interfaces/requests/index.d.ts +1 -0
  65. package/dist_ts_interfaces/requests/index.js +2 -1
  66. package/dist_ts_interfaces/requests/realtime.d.ts +38 -0
  67. package/dist_ts_interfaces/requests/realtime.js +11 -0
  68. package/dist_ts_migrations/index.d.ts +6 -0
  69. package/dist_ts_migrations/index.js +204 -9
  70. package/dist_ts_web/00_commitinfo_data.js +1 -1
  71. package/dist_ts_web/appstate/certificates.js +9 -2
  72. package/dist_ts_web/appstate/domains.js +16 -5
  73. package/dist_ts_web/appstate/email-domains.d.ts +3 -0
  74. package/dist_ts_web/appstate/email-domains.js +47 -14
  75. package/dist_ts_web/appstate/email-ops.d.ts +1 -0
  76. package/dist_ts_web/appstate/email-ops.js +20 -2
  77. package/dist_ts_web/appstate/realtime.d.ts +27 -0
  78. package/dist_ts_web/appstate/realtime.js +71 -0
  79. package/dist_ts_web/appstate/remoteingress.js +9 -3
  80. package/dist_ts_web/appstate/routes.js +16 -5
  81. package/dist_ts_web/appstate/runtime.js +135 -7
  82. package/dist_ts_web/appstate.d.ts +1 -0
  83. package/dist_ts_web/appstate.js +2 -1
  84. package/dist_ts_web/elements/email/email-domain-creation-progress.d.ts +33 -0
  85. package/dist_ts_web/elements/email/email-domain-creation-progress.js +257 -0
  86. package/dist_ts_web/elements/email/ops-view-email-domains.js +233 -68
  87. package/dist_ts_web/elements/email/ops-view-emails.d.ts +4 -2
  88. package/dist_ts_web/elements/email/ops-view-emails.js +25 -13
  89. package/dist_ts_web/elements/overview/ops-view-config.js +5 -5
  90. package/package.json +22 -19
  91. package/ts/00_commitinfo_data.ts +1 -1
  92. package/ts/classes.dcrouter.ts +121 -54
  93. package/ts/db/classes.cache.cleaner.ts +18 -3
  94. package/ts/db/classes.dcrouter-db.ts +0 -2
  95. package/ts/db/documents/classes.cached.email.ts +78 -4
  96. package/ts/db/documents/classes.dns-record.doc.ts +1 -0
  97. package/ts/dns/classes.dns-server-runtime.ts +9 -45
  98. package/ts/dns/index.ts +1 -0
  99. package/ts/dns/manager.dns.ts +50 -16
  100. package/ts/dns/providers/cloudflare.provider.ts +21 -3
  101. package/ts/dns/txt-record-presentation.ts +134 -0
  102. package/ts/email/classes.accepted-email-spool.ts +220 -14
  103. package/ts/email/classes.email-domain.manager.ts +201 -7
  104. package/ts/email/classes.email-settings.manager.ts +1 -1
  105. package/ts/email/classes.mail-dns-sync.ts +588 -122
  106. package/ts/email/classes.smartmta-blob-storage-manager.ts +3 -2
  107. package/ts/email/classes.workapp-mail-manager.ts +34 -1
  108. package/ts/email/index.ts +2 -0
  109. package/ts/email/mail-dns-txt-policy.ts +505 -0
  110. package/ts/email/reconcile-mail-dns-singleton.ts +224 -0
  111. package/ts/opsserver/classes.opsserver.ts +42 -0
  112. package/ts/opsserver/handlers/certificate.handler.ts +24 -3
  113. package/ts/opsserver/handlers/config.handler.ts +9 -5
  114. package/ts/opsserver/handlers/dns-provider.handler.ts +18 -1
  115. package/ts/opsserver/handlers/dns-record.handler.ts +24 -3
  116. package/ts/opsserver/handlers/domain.handler.ts +36 -2
  117. package/ts/opsserver/handlers/email-domain.handler.ts +4 -5
  118. package/ts/opsserver/handlers/email-ops.handler.ts +29 -6
  119. package/ts/opsserver/handlers/index.ts +1 -0
  120. package/ts/opsserver/handlers/logs.handler.ts +2 -2
  121. package/ts/opsserver/handlers/realtime.handler.ts +222 -0
  122. package/ts/opsserver/handlers/remoteingress.handler.ts +21 -0
  123. package/ts/opsserver/handlers/route-management.handler.ts +46 -4
  124. package/ts/paths.ts +0 -25
  125. package/ts/plugins.ts +0 -68
  126. package/ts_web/00_commitinfo_data.ts +1 -1
  127. package/ts_web/appstate/certificates.ts +6 -1
  128. package/ts_web/appstate/domains.ts +11 -4
  129. package/ts_web/appstate/email-domains.ts +68 -14
  130. package/ts_web/appstate/email-ops.ts +20 -1
  131. package/ts_web/appstate/realtime.ts +109 -0
  132. package/ts_web/appstate/remoteingress.ts +6 -2
  133. package/ts_web/appstate/routes.ts +11 -4
  134. package/ts_web/appstate/runtime.ts +176 -7
  135. package/ts_web/appstate.ts +1 -0
  136. package/ts_web/elements/email/email-domain-creation-progress.ts +340 -0
  137. package/ts_web/elements/email/ops-view-email-domains.ts +273 -74
  138. package/ts_web/elements/email/ops-view-emails.ts +23 -16
  139. package/ts_web/elements/overview/ops-view-config.ts +4 -4
@@ -0,0 +1,224 @@
1
+ import { DnsRecordDoc } from '../db/documents/classes.dns-record.doc.js';
2
+ import type {
3
+ IEmailDnsRecordIntent,
4
+ IEmailDomainOperationError,
5
+ TEmailDnsVerificationOutcome,
6
+ } from '../../ts_interfaces/data/email-domain.js';
7
+ import {
8
+ classifyMailTxtRecord,
9
+ type IMailDnsTxtPolicyResult,
10
+ } from './mail-dns-txt-policy.js';
11
+
12
+ export interface IMailDnsMutationClient {
13
+ createRecord(args: {
14
+ domainId: string;
15
+ name: string;
16
+ type: 'MX' | 'TXT' | 'A' | 'AAAA';
17
+ value: string;
18
+ ttl: number;
19
+ createdBy: string;
20
+ managedBy: string;
21
+ managedOwnerId: string;
22
+ managedRecordKey: string;
23
+ }): Promise<{ success: boolean; id?: string; message?: string }>;
24
+ updateRecord(args: {
25
+ id: string;
26
+ name?: string;
27
+ value?: string;
28
+ ttl?: number;
29
+ }): Promise<{ success: boolean; message?: string }>;
30
+ deleteRecord(id: string): Promise<{ success: boolean; message?: string }>;
31
+ }
32
+
33
+ export interface IReconcileMailDnsSingletonOptions {
34
+ intent: IEmailDnsRecordIntent;
35
+ rrset: DnsRecordDoc[];
36
+ dnsManager: IMailDnsMutationClient;
37
+ managedBy: string;
38
+ createdBy: string;
39
+ evaluate: (
40
+ intent: IEmailDnsRecordIntent,
41
+ values: string[],
42
+ ) => Promise<IMailDnsTxtPolicyResult>;
43
+ }
44
+
45
+ const outcomeError = (
46
+ intent: IEmailDnsRecordIntent,
47
+ outcome: Exclude<TEmailDnsVerificationOutcome, 'valid'>,
48
+ message: string,
49
+ ): IEmailDomainOperationError => {
50
+ let code: string;
51
+ if (outcome === 'missing') code = 'DNS_RECORD_MISSING';
52
+ else if (outcome === 'propagation') code = 'DNS_PROPAGATION_PENDING';
53
+ else if (outcome === 'duplicate') code = 'DNS_PUBLIC_SINGLETON_DUPLICATE';
54
+ else if (outcome === 'conflict') code = 'DNS_SINGLETON_CONFLICT';
55
+ else if (outcome === 'invalid') code = 'DNS_SINGLETON_INVALID';
56
+ else {
57
+ code = intent.value.trim().toLowerCase().startsWith('v=spf1')
58
+ ? 'DNS_SPF_AUTHORIZATION_LOOKUP_FAILED'
59
+ : 'DNS_LOOKUP_FAILED';
60
+ }
61
+ return {
62
+ code,
63
+ message,
64
+ recordKey: intent.key,
65
+ retryable: outcome === 'missing' || outcome === 'propagation' || outcome === 'lookup-error',
66
+ };
67
+ };
68
+
69
+ export function applyMailDnsVerificationResult(
70
+ intent: IEmailDnsRecordIntent,
71
+ result: IMailDnsTxtPolicyResult,
72
+ phase: 'reconcile' | 'verify',
73
+ ): void {
74
+ intent.verificationOutcome = result.outcome;
75
+ if (result.outcome === 'valid') {
76
+ intent.error = undefined;
77
+ intent.effectiveValue = result.effectiveValue;
78
+ intent.status = phase === 'verify' ? 'valid' : 'satisfied';
79
+ if (phase === 'verify') intent.validatedAt = new Date().toISOString();
80
+ return;
81
+ }
82
+ intent.status = result.outcome === 'conflict' || result.outcome === 'duplicate'
83
+ ? 'conflict'
84
+ : result.outcome === 'invalid'
85
+ ? 'failed'
86
+ : 'pending';
87
+ intent.error = outcomeError(
88
+ intent,
89
+ result.outcome,
90
+ result.reason || `${intent.type} ${intent.name} did not validate`,
91
+ );
92
+ }
93
+
94
+ function isSingletonAuthenticationIntent(intent: IEmailDnsRecordIntent): boolean {
95
+ if (intent.type !== 'TXT') return false;
96
+ const normalized = intent.value.trim().toLowerCase();
97
+ return normalized.startsWith('v=spf1')
98
+ || normalized.startsWith('v=dmarc1')
99
+ || normalized.startsWith('v=dkim1');
100
+ }
101
+
102
+ function exactIntentOwner(
103
+ record: DnsRecordDoc,
104
+ intent: IEmailDnsRecordIntent,
105
+ managedBy: string,
106
+ ): boolean {
107
+ return record.managedBy === managedBy
108
+ && record.managedOwnerId === intent.ownerId
109
+ && record.managedRecordKey === intent.key;
110
+ }
111
+
112
+ async function deleteOwnedRecords(
113
+ records: DnsRecordDoc[],
114
+ options: IReconcileMailDnsSingletonOptions,
115
+ ): Promise<boolean> {
116
+ for (const record of records) {
117
+ const result = await options.dnsManager.deleteRecord(record.id);
118
+ if (!result.success) {
119
+ options.intent.status = 'pending';
120
+ options.intent.verificationOutcome = 'duplicate';
121
+ options.intent.error = {
122
+ code: 'DNS_SINGLETON_DUPLICATE_CLEANUP_FAILED',
123
+ message: result.message || `Failed to remove duplicate ${options.intent.type} ${options.intent.name}`,
124
+ recordKey: options.intent.key,
125
+ retryable: true,
126
+ };
127
+ return false;
128
+ }
129
+ }
130
+ return true;
131
+ }
132
+
133
+ /**
134
+ * Reconcile SPF, DMARC, and DKIM singleton TXT records without adopting or
135
+ * mutating operator policy. Returns false for non-singleton intents.
136
+ */
137
+ export async function reconcileMailDnsSingleton(
138
+ options: IReconcileMailDnsSingletonOptions,
139
+ ): Promise<boolean> {
140
+ const { intent, rrset, managedBy } = options;
141
+ if (!isSingletonAuthenticationIntent(intent)) return false;
142
+
143
+ const exactOwned = rrset
144
+ .filter((record) => exactIntentOwner(record, intent, managedBy))
145
+ .sort((left, right) => left.id.localeCompare(right.id));
146
+ const otherManaged = rrset.filter((record) => (
147
+ record.managedBy === managedBy && !exactIntentOwner(record, intent, managedBy)
148
+ ));
149
+ const operator = rrset.filter((record) => record.managedBy !== managedBy);
150
+ const relevantOtherManaged = otherManaged.filter((record) => (
151
+ classifyMailTxtRecord(intent, record.value).relevant
152
+ ));
153
+ const relevantOperator = operator.filter((record) => (
154
+ classifyMailTxtRecord(intent, record.value).relevant
155
+ ));
156
+
157
+ if (relevantOperator.length > 0) {
158
+ if (!await deleteOwnedRecords(exactOwned, options)) return true;
159
+ if (relevantOtherManaged.length > 0) {
160
+ applyMailDnsVerificationResult(intent, {
161
+ outcome: 'conflict',
162
+ reason: `Another managed owner controls ${intent.type} ${intent.name}`,
163
+ }, 'reconcile');
164
+ return true;
165
+ }
166
+ const result = await options.evaluate(intent, relevantOperator.map((record) => record.value));
167
+ applyMailDnsVerificationResult(intent, result, 'reconcile');
168
+ if (result.outcome === 'valid') intent.satisfactionSource = 'operator';
169
+ return true;
170
+ }
171
+
172
+ if (relevantOtherManaged.length > 0) {
173
+ if (!await deleteOwnedRecords(exactOwned, options)) return true;
174
+ applyMailDnsVerificationResult(intent, {
175
+ outcome: 'conflict',
176
+ reason: `Another managed owner controls ${intent.type} ${intent.name}`,
177
+ }, 'reconcile');
178
+ return true;
179
+ }
180
+
181
+ const managed = exactOwned[0];
182
+ if (exactOwned.length > 1 && !await deleteOwnedRecords(exactOwned.slice(1), options)) {
183
+ return true;
184
+ }
185
+ if (managed) {
186
+ if (managed.value.trim() !== intent.value.trim() || managed.name !== intent.name) {
187
+ const result = await options.dnsManager.updateRecord({
188
+ id: managed.id,
189
+ name: intent.name,
190
+ value: intent.value,
191
+ ttl: intent.ttl,
192
+ });
193
+ if (!result.success) throw new Error(result.message || `Failed to update ${intent.key}`);
194
+ }
195
+ intent.providerRecordId = managed.providerRecordId;
196
+ intent.status = 'satisfied';
197
+ intent.satisfactionSource = 'managed';
198
+ intent.effectiveValue = intent.value;
199
+ intent.verificationOutcome = undefined;
200
+ intent.error = undefined;
201
+ intent.lastAttemptAt = new Date().toISOString();
202
+ return true;
203
+ }
204
+
205
+ const result = await options.dnsManager.createRecord({
206
+ domainId: intent.domainId,
207
+ name: intent.name,
208
+ type: intent.type,
209
+ value: intent.value,
210
+ ttl: intent.ttl,
211
+ createdBy: options.createdBy,
212
+ managedBy,
213
+ managedOwnerId: intent.ownerId,
214
+ managedRecordKey: intent.key,
215
+ });
216
+ if (!result.success) throw new Error(result.message || `Failed to create ${intent.key}`);
217
+ intent.status = 'satisfied';
218
+ intent.satisfactionSource = 'managed';
219
+ intent.effectiveValue = intent.value;
220
+ intent.verificationOutcome = undefined;
221
+ intent.error = undefined;
222
+ intent.lastAttemptAt = new Date().toISOString();
223
+ return true;
224
+ }
@@ -18,6 +18,7 @@ export class OpsServer {
18
18
 
19
19
  // Handler instances
20
20
  public adminHandler!: handlers.AdminHandler;
21
+ public realtimeHandler!: handlers.RealtimeHandler;
21
22
  private configHandler!: handlers.ConfigHandler;
22
23
  private logsHandler!: handlers.LogsHandler;
23
24
  private securityHandler!: handlers.SecurityHandler;
@@ -40,6 +41,8 @@ export class OpsServer {
40
41
  private acmeConfigHandler!: handlers.AcmeConfigHandler;
41
42
  private emailDomainHandler!: handlers.EmailDomainHandler;
42
43
  private gatewayClientHandler!: handlers.GatewayClientHandler;
44
+ private smtpRealtimeUnsubscribe?: () => void;
45
+ private emailQueueRealtimeUnsubscribe?: () => void;
43
46
 
44
47
  constructor(dcRouterRefArg: DcRouter) {
45
48
  this.dcRouterRef = dcRouterRefArg;
@@ -79,6 +82,7 @@ export class OpsServer {
79
82
  this.typedrouter.addTypedRouter(this.adminRouter);
80
83
 
81
84
  // Instantiate all handlers — they self-register with the appropriate router
85
+ this.realtimeHandler = new handlers.RealtimeHandler(this);
82
86
  this.configHandler = new handlers.ConfigHandler(this);
83
87
  this.logsHandler = new handlers.LogsHandler(this);
84
88
  this.securityHandler = new handlers.SecurityHandler(this);
@@ -101,7 +105,21 @@ export class OpsServer {
101
105
  this.acmeConfigHandler = new handlers.AcmeConfigHandler(this);
102
106
  this.emailDomainHandler = new handlers.EmailDomainHandler(this);
103
107
  this.gatewayClientHandler = new handlers.GatewayClientHandler(this);
108
+ this.smtpRealtimeUnsubscribe =
109
+ this.dcRouterRef.acceptedEmailSpool?.onSmtpTransactionPersisted?.((eventArg) => {
110
+ this.invalidateRealtime(['smtpAttempts', 'emailQueue'], {
111
+ changedIds: [eventArg.cachedEmailId],
112
+ reason: 'smtp-transaction-persisted',
113
+ });
114
+ });
104
115
 
116
+ this.emailQueueRealtimeUnsubscribe =
117
+ this.dcRouterRef.acceptedEmailSpool?.onEmailQueuePersisted?.((eventArg) => {
118
+ this.invalidateRealtime('emailQueue', {
119
+ changedIds: [eventArg.cachedEmailId],
120
+ reason: eventArg.reason,
121
+ });
122
+ });
105
123
  console.log('✅ OpsServer TypedRequest handlers initialized');
106
124
  }
107
125
 
@@ -114,10 +132,34 @@ export class OpsServer {
114
132
  );
115
133
  }
116
134
 
135
+ public invalidateRealtime(
136
+ resourcesArg:
137
+ | interfaces.requests.TOpsRealtimeResource
138
+ | interfaces.requests.TOpsRealtimeResource[],
139
+ optionsArg: { changedIds?: string[]; reason?: string } = {},
140
+ ): void {
141
+ this.realtimeHandler?.invalidate(resourcesArg, optionsArg);
142
+ }
143
+
144
+ public async getAuthenticatedRealtimeConnections(): Promise<
145
+ plugins.typedsocket.ISmartServeConnectionWrapper[]
146
+ > {
147
+ return this.realtimeHandler
148
+ ? await this.realtimeHandler.getAuthenticatedConnections()
149
+ : [];
150
+ }
151
+
117
152
  public async stop() {
118
153
  if (this.adminHandler) {
119
154
  await this.adminHandler.stop();
120
155
  }
156
+ this.smtpRealtimeUnsubscribe?.();
157
+ this.smtpRealtimeUnsubscribe = undefined;
158
+ this.emailQueueRealtimeUnsubscribe?.();
159
+ this.emailQueueRealtimeUnsubscribe = undefined;
160
+ if (this.realtimeHandler) {
161
+ this.realtimeHandler.cleanup();
162
+ }
121
163
  // Clean up log handler streams and push destination before stopping the server
122
164
  if (this.logsHandler) {
123
165
  this.logsHandler.cleanup();
@@ -67,7 +67,14 @@ export class CertificateHandler {
67
67
  'reprovisionCertificateDomain',
68
68
  async (dataArg) => {
69
69
  await this.requireAuth(dataArg, 'certificates:write');
70
- return this.reprovisionCertificateDomain(dataArg.domain, dataArg.forceRenew);
70
+ const result = await this.reprovisionCertificateDomain(dataArg.domain, dataArg.forceRenew);
71
+ if (result.success) {
72
+ this.opsServerRef.invalidateRealtime?.('certificates', {
73
+ changedIds: [dataArg.domain],
74
+ reason: 'certificate-reprovisioned',
75
+ });
76
+ }
77
+ return result;
71
78
  }
72
79
  )
73
80
  );
@@ -78,7 +85,14 @@ export class CertificateHandler {
78
85
  'deleteCertificate',
79
86
  async (dataArg) => {
80
87
  await this.requireAuth(dataArg, 'certificates:write');
81
- return this.deleteCertificate(dataArg.domain);
88
+ const result = await this.deleteCertificate(dataArg.domain);
89
+ if (result.success) {
90
+ this.opsServerRef.invalidateRealtime?.('certificates', {
91
+ changedIds: [dataArg.domain],
92
+ reason: 'certificate-deleted',
93
+ });
94
+ }
95
+ return result;
82
96
  }
83
97
  )
84
98
  );
@@ -100,7 +114,14 @@ export class CertificateHandler {
100
114
  'importCertificate',
101
115
  async (dataArg) => {
102
116
  await this.requireAuth(dataArg, 'certificates:write');
103
- return this.importCertificate(dataArg.cert);
117
+ const result = await this.importCertificate(dataArg.cert);
118
+ if (result.success) {
119
+ this.opsServerRef.invalidateRealtime?.('certificates', {
120
+ changedIds: [dataArg.cert.domainName],
121
+ reason: 'certificate-imported',
122
+ });
123
+ }
124
+ return result;
104
125
  }
105
126
  )
106
127
  );
@@ -35,9 +35,9 @@ export class ConfigHandler {
35
35
  const resolvedPaths = dcRouter.resolvedPaths;
36
36
 
37
37
  // --- System ---
38
- const storageBackend: 'filesystem' | 'custom' | 'memory' = opts.dbConfig?.mongoDbUrl
39
- ? 'custom'
40
- : 'filesystem';
38
+ const storageBackend: 'smartdata-external' | 'smartdata-local' = opts.dbConfig?.mongoDbUrl
39
+ ? 'smartdata-external'
40
+ : 'smartdata-local';
41
41
 
42
42
  const proxyIps = opts.proxyIps || [];
43
43
 
@@ -48,7 +48,9 @@ export class ConfigHandler {
48
48
  proxyIps,
49
49
  uptime: Math.floor(process.uptime()),
50
50
  storageBackend,
51
- storagePath: opts.dbConfig?.storagePath || resolvedPaths.defaultTsmDbPath,
51
+ storagePath: opts.dbConfig?.mongoDbUrl
52
+ ? null
53
+ : opts.dbConfig?.storagePath || resolvedPaths.defaultTsmDbPath,
52
54
  };
53
55
 
54
56
  // --- SmartProxy ---
@@ -161,7 +163,9 @@ export class ConfigHandler {
161
163
  const dbConfig = opts.dbConfig;
162
164
  const cache: interfaces.requests.IConfigData['cache'] = {
163
165
  enabled: dbConfig?.enabled !== false,
164
- storagePath: dbConfig?.storagePath || resolvedPaths.defaultTsmDbPath,
166
+ storagePath: dbConfig?.mongoDbUrl
167
+ ? null
168
+ : dbConfig?.storagePath || resolvedPaths.defaultTsmDbPath,
165
169
  dbName: dbConfig?.dbName || 'dcrouter',
166
170
  defaultTTLDays: 30,
167
171
  cleanupIntervalHours: dbConfig?.cleanupIntervalHours || 1,
@@ -90,6 +90,10 @@ export class DnsProviderHandler {
90
90
  credentials: dataArg.credentials,
91
91
  createdBy: userId,
92
92
  });
93
+ this.opsServerRef.invalidateRealtime?.('dns', {
94
+ changedIds: [id],
95
+ reason: 'dns-provider-created',
96
+ });
93
97
  return { success: true, id };
94
98
  },
95
99
  ),
@@ -110,6 +114,12 @@ export class DnsProviderHandler {
110
114
  name: dataArg.name,
111
115
  credentials: dataArg.credentials,
112
116
  });
117
+ if (ok) {
118
+ this.opsServerRef.invalidateRealtime?.('dns', {
119
+ changedIds: [dataArg.id],
120
+ reason: 'dns-provider-updated',
121
+ });
122
+ }
113
123
  return ok ? { success: true } : { success: false, message: 'Provider not found' };
114
124
  },
115
125
  ),
@@ -126,7 +136,14 @@ export class DnsProviderHandler {
126
136
  }
127
137
  const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
128
138
  if (!dnsManager) return { success: false, message: 'DnsManager not initialized' };
129
- return await dnsManager.deleteProvider(dataArg.id, dataArg.force ?? false);
139
+ const result = await dnsManager.deleteProvider(dataArg.id, dataArg.force ?? false);
140
+ if (result.success) {
141
+ this.opsServerRef.invalidateRealtime?.(['dns', 'emailDomains'], {
142
+ changedIds: [dataArg.id],
143
+ reason: 'dns-provider-deleted',
144
+ });
145
+ }
146
+ return result;
130
147
  },
131
148
  ),
132
149
  );
@@ -62,7 +62,7 @@ export class DnsRecordHandler {
62
62
  const userId = await this.requireAuth(dataArg, 'dns-records:write');
63
63
  const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
64
64
  if (!dnsManager) return { success: false, message: 'DnsManager not initialized' };
65
- return await dnsManager.createRecord({
65
+ const result = await dnsManager.createRecord({
66
66
  domainId: dataArg.domainId,
67
67
  name: dataArg.name,
68
68
  type: dataArg.type,
@@ -71,6 +71,13 @@ export class DnsRecordHandler {
71
71
  proxied: dataArg.proxied,
72
72
  createdBy: userId,
73
73
  });
74
+ if (result.success) {
75
+ this.opsServerRef.invalidateRealtime?.(['dns', 'emailDomains'], {
76
+ changedIds: result.id ? [result.id] : undefined,
77
+ reason: 'dns-record-created',
78
+ });
79
+ }
80
+ return result;
74
81
  },
75
82
  ),
76
83
  );
@@ -83,13 +90,20 @@ export class DnsRecordHandler {
83
90
  await this.requireAuth(dataArg, 'dns-records:write');
84
91
  const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
85
92
  if (!dnsManager) return { success: false, message: 'DnsManager not initialized' };
86
- return await dnsManager.updateRecord({
93
+ const result = await dnsManager.updateRecord({
87
94
  id: dataArg.id,
88
95
  name: dataArg.name,
89
96
  value: dataArg.value,
90
97
  ttl: dataArg.ttl,
91
98
  proxied: dataArg.proxied,
92
99
  });
100
+ if (result.success) {
101
+ this.opsServerRef.invalidateRealtime?.(['dns', 'emailDomains'], {
102
+ changedIds: [dataArg.id],
103
+ reason: 'dns-record-updated',
104
+ });
105
+ }
106
+ return result;
93
107
  },
94
108
  ),
95
109
  );
@@ -102,7 +116,14 @@ export class DnsRecordHandler {
102
116
  await this.requireAuth(dataArg, 'dns-records:write');
103
117
  const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
104
118
  if (!dnsManager) return { success: false, message: 'DnsManager not initialized' };
105
- return await dnsManager.deleteRecord(dataArg.id);
119
+ const result = await dnsManager.deleteRecord(dataArg.id);
120
+ if (result.success) {
121
+ this.opsServerRef.invalidateRealtime?.(['dns', 'emailDomains'], {
122
+ changedIds: [dataArg.id],
123
+ reason: 'dns-record-deleted',
124
+ });
125
+ }
126
+ return result;
106
127
  },
107
128
  ),
108
129
  );
@@ -68,6 +68,10 @@ export class DomainHandler {
68
68
  description: dataArg.description,
69
69
  createdBy: userId,
70
70
  });
71
+ this.opsServerRef.invalidateRealtime?.(['dns', 'emailDomains'], {
72
+ changedIds: [id],
73
+ reason: 'domain-created',
74
+ });
71
75
  return { success: true, id };
72
76
  } catch (err: unknown) {
73
77
  return { success: false, message: (err as Error).message };
@@ -90,6 +94,10 @@ export class DomainHandler {
90
94
  domainNames: dataArg.domainNames,
91
95
  createdBy: userId,
92
96
  });
97
+ this.opsServerRef.invalidateRealtime?.(['dns', 'emailDomains'], {
98
+ changedIds: importedIds,
99
+ reason: 'domains-imported',
100
+ });
93
101
  return { success: true, importedIds };
94
102
  } catch (err: unknown) {
95
103
  return { success: false, message: (err as Error).message };
@@ -109,6 +117,12 @@ export class DomainHandler {
109
117
  const ok = await dnsManager.updateDomain(dataArg.id, {
110
118
  description: dataArg.description,
111
119
  });
120
+ if (ok) {
121
+ this.opsServerRef.invalidateRealtime?.(['dns', 'emailDomains'], {
122
+ changedIds: [dataArg.id],
123
+ reason: 'domain-updated',
124
+ });
125
+ }
112
126
  return ok ? { success: true } : { success: false, message: 'Domain not found' };
113
127
  },
114
128
  ),
@@ -123,6 +137,12 @@ export class DomainHandler {
123
137
  const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
124
138
  if (!dnsManager) return { success: false, message: 'DnsManager not initialized' };
125
139
  const ok = await dnsManager.deleteDomain(dataArg.id);
140
+ if (ok) {
141
+ this.opsServerRef.invalidateRealtime?.(['dns', 'emailDomains'], {
142
+ changedIds: [dataArg.id],
143
+ reason: 'domain-deleted',
144
+ });
145
+ }
126
146
  return ok ? { success: true } : { success: false, message: 'Domain not found' };
127
147
  },
128
148
  ),
@@ -136,7 +156,14 @@ export class DomainHandler {
136
156
  await this.requireAuth(dataArg, 'domains:write');
137
157
  const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
138
158
  if (!dnsManager) return { success: false, message: 'DnsManager not initialized' };
139
- return await dnsManager.syncDomain(dataArg.id);
159
+ const result = await dnsManager.syncDomain(dataArg.id);
160
+ if (result.success) {
161
+ this.opsServerRef.invalidateRealtime?.(['dns', 'emailDomains'], {
162
+ changedIds: [dataArg.id],
163
+ reason: 'domain-synced',
164
+ });
165
+ }
166
+ return result;
140
167
  },
141
168
  ),
142
169
  );
@@ -149,12 +176,19 @@ export class DomainHandler {
149
176
  await this.requireAuth(dataArg, 'domains:write');
150
177
  const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
151
178
  if (!dnsManager) return { success: false, message: 'DnsManager not initialized' };
152
- return await dnsManager.migrateDomain({
179
+ const result = await dnsManager.migrateDomain({
153
180
  id: dataArg.id,
154
181
  targetSource: dataArg.targetSource,
155
182
  targetProviderId: dataArg.targetProviderId,
156
183
  deleteExistingProviderRecords: dataArg.deleteExistingProviderRecords,
157
184
  });
185
+ if (result.success) {
186
+ this.opsServerRef.invalidateRealtime?.(['dns', 'emailDomains'], {
187
+ changedIds: [dataArg.id],
188
+ reason: 'domain-migrated',
189
+ });
190
+ }
191
+ return result;
158
192
  },
159
193
  ),
160
194
  );
@@ -178,8 +178,6 @@ export class EmailDomainHandler {
178
178
  remoteIngress: dataArg.remoteIngress,
179
179
  });
180
180
  const result = normalizeEmailDomainActionResult(rawResult);
181
- if (!result.success) return result;
182
-
183
181
  const resultRecord = isRecord(rawResult) ? rawResult : undefined;
184
182
  const domain = isRecord(resultRecord?.domain)
185
183
  ? resultRecord.domain as unknown as interfaces.data.IEmailDomain
@@ -210,7 +208,8 @@ export class EmailDomainHandler {
210
208
  rateLimits: dataArg.rateLimits,
211
209
  remoteIngress: dataArg.remoteIngress,
212
210
  });
213
- return normalizeEmailDomainActionResult(rawResult);
211
+ const result = normalizeEmailDomainActionResult(rawResult);
212
+ return result;
214
213
  } catch (err: unknown) {
215
214
  return normalizeEmailDomainActionFailure(err, 'Failed to update email domain');
216
215
  }
@@ -229,7 +228,8 @@ export class EmailDomainHandler {
229
228
  }
230
229
  try {
231
230
  const rawResult: unknown = await this.manager.deleteEmailDomain(dataArg.id);
232
- return normalizeEmailDomainActionResult(rawResult);
231
+ const result = normalizeEmailDomainActionResult(rawResult);
232
+ return result;
233
233
  } catch (err: unknown) {
234
234
  return normalizeEmailDomainActionFailure(err, 'Failed to delete email domain');
235
235
  }
@@ -249,7 +249,6 @@ export class EmailDomainHandler {
249
249
  try {
250
250
  const rawResult: unknown = await this.manager.validateDns(dataArg.id);
251
251
  const result = normalizeEmailDomainActionResult(rawResult);
252
- if (!result.success) return result;
253
252
 
254
253
  const resultRecord = isRecord(rawResult) ? rawResult : undefined;
255
254
  const records = (
@@ -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
  */