@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
@@ -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
+ }
@@ -0,0 +1,73 @@
1
+ import * as plugins from '../plugins.js';
2
+
3
+ export interface ISmartMtaTextNamespace {
4
+ prefix: string;
5
+ maxBytes: number;
6
+ }
7
+
8
+ export const smartMtaTextNamespaces: readonly ISmartMtaTextNamespace[] = Object.freeze([
9
+ { prefix: '/email/dkim/', maxBytes: 128 * 1024 },
10
+ { prefix: '/email/dns/', maxBytes: 1024 * 1024 },
11
+ { prefix: '/email/bounces/', maxBytes: 8 * 1024 * 1024 },
12
+ { prefix: '/email/templates/', maxBytes: 4 * 1024 * 1024 },
13
+ { prefix: '/email/routes/config.json', maxBytes: 4 * 1024 * 1024 },
14
+ { prefix: '/security/ip-reputation-cache.json', maxBytes: 8 * 1024 * 1024 },
15
+ { prefix: '/workhosters/mail-identities.json', maxBytes: 4 * 1024 * 1024 },
16
+ ]);
17
+
18
+ export function normalizeSmartMtaStorageKey(key: string): string {
19
+ if (typeof key !== 'string' || key.length === 0) {
20
+ throw new Error('SmartMTA storage key must be a non-empty string');
21
+ }
22
+ if (key.includes('\\') || key.includes('\0')) {
23
+ throw new Error(`Invalid SmartMTA storage key: ${key}`);
24
+ }
25
+
26
+ const withLeadingSlash = key.startsWith('/') ? key : `/${key}`;
27
+ const segments = withLeadingSlash.split('/').slice(1);
28
+ if (
29
+ segments.length === 0
30
+ || segments.some((segment, index) => segment === '.' || segment === '..' || (segment === '' && index < segments.length - 1))
31
+ ) {
32
+ throw new Error(`Invalid SmartMTA storage key: ${key}`);
33
+ }
34
+
35
+ const normalized = `/${segments.filter(Boolean).join('/')}`;
36
+ if (normalized.length > 1024) {
37
+ throw new Error('SmartMTA storage key exceeds 1024 characters');
38
+ }
39
+ return normalized;
40
+ }
41
+
42
+ export function getSmartMtaTextNamespace(key: string): ISmartMtaTextNamespace {
43
+ const normalizedKey = normalizeSmartMtaStorageKey(key);
44
+ const namespace = smartMtaTextNamespaces.find((candidate) => {
45
+ return candidate.prefix.endsWith('/')
46
+ ? normalizedKey.startsWith(candidate.prefix)
47
+ : normalizedKey === candidate.prefix;
48
+ });
49
+ if (!namespace) {
50
+ throw new Error(`Unsupported SmartMTA text storage namespace: ${normalizedKey}`);
51
+ }
52
+ return namespace;
53
+ }
54
+
55
+ export function validateSmartMtaTextValue(key: string, value: string): void {
56
+ if (typeof value !== 'string') {
57
+ throw new Error('SmartMTA text storage only accepts string values');
58
+ }
59
+ const namespace = getSmartMtaTextNamespace(key);
60
+ const size = Buffer.byteLength(value, 'utf8');
61
+ if (size > namespace.maxBytes) {
62
+ throw new Error(
63
+ `SmartMTA storage value for ${normalizeSmartMtaStorageKey(key)} exceeds ${namespace.maxBytes} bytes`,
64
+ );
65
+ }
66
+ }
67
+
68
+ export function hashSmartMtaStorageKey(key: string): string {
69
+ return plugins.crypto
70
+ .createHash('sha256')
71
+ .update(normalizeSmartMtaStorageKey(key))
72
+ .digest('hex');
73
+ }
@@ -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 = (