@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.
- package/.smartconfig.json +5 -2
- package/deno.json +1 -1
- package/dist_serve/bundle.js +1316 -1151
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.d.ts +3 -1
- package/dist_ts/classes.dcrouter.js +110 -49
- package/dist_ts/db/classes.cache.cleaner.d.ts +3 -0
- package/dist_ts/db/classes.cache.cleaner.js +7 -3
- package/dist_ts/db/classes.dcrouter-db.js +1 -2
- package/dist_ts/db/documents/classes.cached.email.d.ts +41 -4
- package/dist_ts/db/documents/classes.cached.email.js +36 -5
- package/dist_ts/db/documents/classes.dns-record.doc.js +3 -1
- package/dist_ts/dns/classes.dns-server-runtime.d.ts +0 -1
- package/dist_ts/dns/classes.dns-server-runtime.js +4 -36
- package/dist_ts/dns/index.d.ts +1 -0
- package/dist_ts/dns/index.js +2 -1
- package/dist_ts/dns/manager.dns.d.ts +1 -0
- package/dist_ts/dns/manager.dns.js +49 -14
- package/dist_ts/dns/providers/cloudflare.provider.js +19 -4
- package/dist_ts/dns/txt-record-presentation.d.ts +20 -0
- package/dist_ts/dns/txt-record-presentation.js +131 -0
- package/dist_ts/email/classes.accepted-email-spool.d.ts +32 -3
- package/dist_ts/email/classes.accepted-email-spool.js +174 -13
- package/dist_ts/email/classes.email-domain.manager.d.ts +14 -1
- package/dist_ts/email/classes.email-domain.manager.js +151 -8
- package/dist_ts/email/classes.email-settings.manager.js +2 -2
- package/dist_ts/email/classes.mail-dns-sync.d.ts +31 -5
- package/dist_ts/email/classes.mail-dns-sync.js +508 -117
- package/dist_ts/email/classes.smartmta-blob-storage-manager.d.ts +2 -2
- package/dist_ts/email/classes.smartmta-blob-storage-manager.js +4 -3
- package/dist_ts/email/classes.workapp-mail-manager.d.ts +1 -0
- package/dist_ts/email/classes.workapp-mail-manager.js +26 -2
- package/dist_ts/email/index.d.ts +2 -0
- package/dist_ts/email/index.js +3 -1
- package/dist_ts/email/mail-dns-txt-policy.d.ts +18 -0
- package/dist_ts/email/mail-dns-txt-policy.js +466 -0
- package/dist_ts/email/reconcile-mail-dns-singleton.d.ts +47 -0
- package/dist_ts/email/reconcile-mail-dns-singleton.js +159 -0
- package/dist_ts/opsserver/classes.opsserver.d.ts +8 -0
- package/dist_ts/opsserver/classes.opsserver.js +34 -1
- package/dist_ts/opsserver/handlers/certificate.handler.js +25 -4
- package/dist_ts/opsserver/handlers/config.handler.js +9 -5
- package/dist_ts/opsserver/handlers/dns-provider.handler.js +19 -2
- package/dist_ts/opsserver/handlers/dns-record.handler.js +25 -4
- package/dist_ts/opsserver/handlers/domain.handler.js +37 -3
- package/dist_ts/opsserver/handlers/email-domain.handler.js +5 -7
- package/dist_ts/opsserver/handlers/email-ops.handler.d.ts +1 -0
- package/dist_ts/opsserver/handlers/email-ops.handler.js +26 -7
- package/dist_ts/opsserver/handlers/index.d.ts +1 -0
- package/dist_ts/opsserver/handlers/index.js +2 -1
- package/dist_ts/opsserver/handlers/logs.handler.d.ts +1 -1
- package/dist_ts/opsserver/handlers/logs.handler.js +3 -3
- package/dist_ts/opsserver/handlers/realtime.handler.d.ts +37 -0
- package/dist_ts/opsserver/handlers/realtime.handler.js +154 -0
- package/dist_ts/opsserver/handlers/remoteingress.handler.js +22 -1
- package/dist_ts/opsserver/handlers/route-management.handler.js +47 -5
- package/dist_ts/paths.d.ts +0 -11
- package/dist_ts/paths.js +1 -22
- package/dist_ts/plugins.d.ts +0 -34
- package/dist_ts/plugins.js +1 -62
- package/dist_ts_interfaces/data/email-domain.d.ts +14 -0
- package/dist_ts_interfaces/requests/config.d.ts +1 -1
- package/dist_ts_interfaces/requests/email-ops.d.ts +34 -1
- package/dist_ts_interfaces/requests/index.d.ts +1 -0
- package/dist_ts_interfaces/requests/index.js +2 -1
- package/dist_ts_interfaces/requests/realtime.d.ts +38 -0
- package/dist_ts_interfaces/requests/realtime.js +11 -0
- package/dist_ts_migrations/index.d.ts +6 -0
- package/dist_ts_migrations/index.js +204 -9
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/dist_ts_web/appstate/certificates.js +9 -2
- package/dist_ts_web/appstate/domains.js +16 -5
- package/dist_ts_web/appstate/email-domains.d.ts +3 -0
- package/dist_ts_web/appstate/email-domains.js +47 -14
- package/dist_ts_web/appstate/email-ops.d.ts +1 -0
- package/dist_ts_web/appstate/email-ops.js +20 -2
- package/dist_ts_web/appstate/realtime.d.ts +27 -0
- package/dist_ts_web/appstate/realtime.js +71 -0
- package/dist_ts_web/appstate/remoteingress.js +9 -3
- package/dist_ts_web/appstate/routes.js +16 -5
- package/dist_ts_web/appstate/runtime.js +135 -7
- package/dist_ts_web/appstate.d.ts +1 -0
- package/dist_ts_web/appstate.js +2 -1
- package/dist_ts_web/elements/email/email-domain-creation-progress.d.ts +33 -0
- package/dist_ts_web/elements/email/email-domain-creation-progress.js +257 -0
- package/dist_ts_web/elements/email/ops-view-email-domains.js +233 -68
- package/dist_ts_web/elements/email/ops-view-emails.d.ts +4 -2
- package/dist_ts_web/elements/email/ops-view-emails.js +25 -13
- package/dist_ts_web/elements/overview/ops-view-config.js +5 -5
- package/package.json +22 -19
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +121 -54
- package/ts/db/classes.cache.cleaner.ts +18 -3
- package/ts/db/classes.dcrouter-db.ts +0 -2
- package/ts/db/documents/classes.cached.email.ts +78 -4
- package/ts/db/documents/classes.dns-record.doc.ts +1 -0
- package/ts/dns/classes.dns-server-runtime.ts +9 -45
- package/ts/dns/index.ts +1 -0
- package/ts/dns/manager.dns.ts +50 -16
- package/ts/dns/providers/cloudflare.provider.ts +21 -3
- package/ts/dns/txt-record-presentation.ts +134 -0
- package/ts/email/classes.accepted-email-spool.ts +220 -14
- package/ts/email/classes.email-domain.manager.ts +201 -7
- package/ts/email/classes.email-settings.manager.ts +1 -1
- package/ts/email/classes.mail-dns-sync.ts +588 -122
- package/ts/email/classes.smartmta-blob-storage-manager.ts +3 -2
- package/ts/email/classes.workapp-mail-manager.ts +34 -1
- package/ts/email/index.ts +2 -0
- package/ts/email/mail-dns-txt-policy.ts +505 -0
- package/ts/email/reconcile-mail-dns-singleton.ts +224 -0
- package/ts/opsserver/classes.opsserver.ts +42 -0
- package/ts/opsserver/handlers/certificate.handler.ts +24 -3
- package/ts/opsserver/handlers/config.handler.ts +9 -5
- package/ts/opsserver/handlers/dns-provider.handler.ts +18 -1
- package/ts/opsserver/handlers/dns-record.handler.ts +24 -3
- package/ts/opsserver/handlers/domain.handler.ts +36 -2
- package/ts/opsserver/handlers/email-domain.handler.ts +4 -5
- package/ts/opsserver/handlers/email-ops.handler.ts +29 -6
- package/ts/opsserver/handlers/index.ts +1 -0
- package/ts/opsserver/handlers/logs.handler.ts +2 -2
- package/ts/opsserver/handlers/realtime.handler.ts +222 -0
- package/ts/opsserver/handlers/remoteingress.handler.ts +21 -0
- package/ts/opsserver/handlers/route-management.handler.ts +46 -4
- package/ts/paths.ts +0 -25
- package/ts/plugins.ts +0 -68
- package/ts_web/00_commitinfo_data.ts +1 -1
- package/ts_web/appstate/certificates.ts +6 -1
- package/ts_web/appstate/domains.ts +11 -4
- package/ts_web/appstate/email-domains.ts +68 -14
- package/ts_web/appstate/email-ops.ts +20 -1
- package/ts_web/appstate/realtime.ts +109 -0
- package/ts_web/appstate/remoteingress.ts +6 -2
- package/ts_web/appstate/routes.ts +11 -4
- package/ts_web/appstate/runtime.ts +176 -7
- package/ts_web/appstate.ts +1 -0
- package/ts_web/elements/email/email-domain-creation-progress.ts +340 -0
- package/ts_web/elements/email/ops-view-email-domains.ts +273 -74
- package/ts_web/elements/email/ops-view-emails.ts +23 -16
- 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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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: '
|
|
39
|
-
? '
|
|
40
|
-
: '
|
|
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?.
|
|
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?.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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(
|
|
168
|
+
private async mapCachedEmailToEmailDetail(
|
|
169
|
+
doc: CachedEmail,
|
|
170
|
+
): Promise<interfaces.requests.IEmailDetail> {
|
|
169
171
|
const base = this.mapCachedEmailToEmail(doc);
|
|
170
|
-
|
|
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
|
*/
|