@serve.zone/dcrouter 18.6.1 → 18.7.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/deno.json +1 -1
- package/dist_ts/00_commitinfo_data.js +1 -1
- package/dist_ts/classes.dcrouter.js +4 -1
- package/dist_ts/dns/manager.dns.d.ts +16 -0
- package/dist_ts/dns/manager.dns.js +192 -39
- package/dist_ts/dns/providers/cloudflare.provider.d.ts +5 -0
- package/dist_ts/dns/providers/cloudflare.provider.js +78 -23
- package/dist_ts/dns/providers/interfaces.d.ts +2 -0
- package/dist_ts/email/classes.email-domain.manager.d.ts +1 -0
- package/dist_ts/email/classes.email-domain.manager.js +36 -8
- package/dist_ts/email/classes.mail-dns-sync.d.ts +8 -0
- package/dist_ts/email/classes.mail-dns-sync.js +239 -55
- package/dist_ts/email/classes.mail-edge-eligibility.js +44 -17
- package/dist_ts/email/classes.mail-egress-coordinator.js +13 -3
- package/dist_ts/email/reconcile-mail-dns-singleton.d.ts +1 -0
- package/dist_ts/email/reconcile-mail-dns-singleton.js +6 -2
- package/dist_ts/opsserver/handlers/dns-provider.handler.js +3 -1
- package/dist_ts/opsserver/handlers/dns-record.handler.js +44 -35
- package/dist_ts/opsserver/handlers/domain.handler.js +13 -2
- package/dist_ts/remoteingress/classes.hub-lifecycle.js +2 -1
- package/dist_ts_interfaces/data/email-domain.d.ts +2 -0
- package/dist_ts_web/00_commitinfo_data.js +1 -1
- package/package.json +4 -4
- package/readme.md +7 -7
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/classes.dcrouter.ts +4 -0
- package/ts/dns/manager.dns.ts +234 -34
- package/ts/dns/providers/cloudflare.provider.ts +93 -26
- package/ts/dns/providers/interfaces.ts +3 -0
- package/ts/email/classes.email-domain.manager.ts +38 -6
- package/ts/email/classes.mail-dns-sync.ts +246 -53
- package/ts/email/classes.mail-edge-eligibility.ts +53 -15
- package/ts/email/classes.mail-egress-coordinator.ts +18 -4
- package/ts/email/reconcile-mail-dns-singleton.ts +8 -1
- package/ts/opsserver/handlers/dns-provider.handler.ts +2 -0
- package/ts/opsserver/handlers/dns-record.handler.ts +58 -49
- package/ts/opsserver/handlers/domain.handler.ts +11 -1
- package/ts/remoteingress/classes.hub-lifecycle.ts +1 -0
- package/ts_web/00_commitinfo_data.ts +1 -1
|
@@ -6,7 +6,8 @@ import * as plugins from '../plugins.js';
|
|
|
6
6
|
import { logger } from '../logger.js';
|
|
7
7
|
import type { IEmailOutboundEgressStatus, TEmailOutboundMode } from '../../ts_interfaces/data/email-settings.js';
|
|
8
8
|
import type { DcRouter } from '../classes.dcrouter.js';
|
|
9
|
-
import type { MailEdgeEligibility } from './classes.mail-edge-eligibility.js';
|
|
9
|
+
import type { IEligibleMailEdge, MailEdgeEligibility } from './classes.mail-edge-eligibility.js';
|
|
10
|
+
import { EmailDomainDoc } from '../db/documents/classes.email-domain.doc.js';
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Routes outbound SMTP deliveries over RemoteIngress edges: resolves the
|
|
@@ -33,7 +34,10 @@ export class MailEgressCoordinator {
|
|
|
33
34
|
): Promise<IOutboundConnectionProxy | null> {
|
|
34
35
|
const mode = this.resolveOutboundModeStrict();
|
|
35
36
|
if (mode === 'direct') {
|
|
36
|
-
if (context.senderDomain &&
|
|
37
|
+
if (context.senderDomain && !this.dcRouterRef.emailDomainManager) {
|
|
38
|
+
throw new Error('EmailDomainManager is unavailable; refusing to classify a sender for direct hub delivery');
|
|
39
|
+
}
|
|
40
|
+
if (context.senderDomain && await this.dcRouterRef.emailDomainManager!.getByDomain(context.senderDomain)) {
|
|
37
41
|
throw new Error(
|
|
38
42
|
`Managed sender domain ${context.senderDomain} has no validated direct-egress identity policy; refusing direct hub delivery`,
|
|
39
43
|
);
|
|
@@ -101,8 +105,16 @@ export class MailEgressCoordinator {
|
|
|
101
105
|
await this.assertTargetIsNotOwnMailEdge(context);
|
|
102
106
|
|
|
103
107
|
const managedDoc = context.senderDomain
|
|
104
|
-
? await
|
|
108
|
+
? await EmailDomainDoc.findByDomain(context.senderDomain)
|
|
105
109
|
: null;
|
|
110
|
+
if (managedDoc) {
|
|
111
|
+
const readiness = await this.dcRouterRef.emailDomainManager?.getOutboundReadiness(managedDoc.domain);
|
|
112
|
+
if (!readiness?.ready) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`Outbound SMTP is not eligible for ${managedDoc.domain}: ${readiness?.reason || 'EmailDomainManager is unavailable'}`,
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
106
118
|
const eligible = managedDoc
|
|
107
119
|
? await this.edgeEligibility.resolveActiveForDomain(managedDoc)
|
|
108
120
|
: await this.edgeEligibility.resolveForDomain();
|
|
@@ -111,7 +123,9 @@ export class MailEgressCoordinator {
|
|
|
111
123
|
`Outbound SMTP is not eligible${context.senderDomain ? ` for ${context.senderDomain}` : ''}: ${eligible.reason || 'no validated mail edge'}`,
|
|
112
124
|
);
|
|
113
125
|
}
|
|
114
|
-
const eligibleById = new Map
|
|
126
|
+
const eligibleById = new Map<string, IEligibleMailEdge>(
|
|
127
|
+
eligible.edges.map((candidate) => [candidate.edge.id, candidate]),
|
|
128
|
+
);
|
|
115
129
|
const edgeFilter = [...eligibleById.keys()];
|
|
116
130
|
|
|
117
131
|
const connectionTimeoutMs = this.dcRouterRef.options.emailConfig?.outbound?.connectionTimeout ?? 30_000;
|
|
@@ -24,6 +24,7 @@ export interface IMailDnsMutationClient {
|
|
|
24
24
|
updateRecord(args: {
|
|
25
25
|
id: string;
|
|
26
26
|
name?: string;
|
|
27
|
+
type?: 'MX' | 'TXT' | 'A' | 'AAAA';
|
|
27
28
|
value?: string;
|
|
28
29
|
ttl?: number;
|
|
29
30
|
}): Promise<{ success: boolean; message?: string }>;
|
|
@@ -183,10 +184,16 @@ export async function reconcileMailDnsSingleton(
|
|
|
183
184
|
return true;
|
|
184
185
|
}
|
|
185
186
|
if (managed) {
|
|
186
|
-
if (
|
|
187
|
+
if (
|
|
188
|
+
managed.value.trim() !== intent.value.trim()
|
|
189
|
+
|| managed.name !== intent.name
|
|
190
|
+
|| managed.type !== intent.type
|
|
191
|
+
|| managed.ttl !== intent.ttl
|
|
192
|
+
) {
|
|
187
193
|
const result = await options.dnsManager.updateRecord({
|
|
188
194
|
id: managed.id,
|
|
189
195
|
name: intent.name,
|
|
196
|
+
type: intent.type,
|
|
190
197
|
value: intent.value,
|
|
191
198
|
ttl: intent.ttl,
|
|
192
199
|
});
|
|
@@ -119,6 +119,7 @@ export class DnsProviderHandler {
|
|
|
119
119
|
changedIds: [dataArg.id],
|
|
120
120
|
reason: 'dns-provider-updated',
|
|
121
121
|
});
|
|
122
|
+
this.opsServerRef.dcRouterRef.mailDnsSync.requestSync('DNS provider updated');
|
|
122
123
|
}
|
|
123
124
|
return ok ? { success: true } : { success: false, message: 'Provider not found' };
|
|
124
125
|
},
|
|
@@ -148,6 +149,7 @@ export class DnsProviderHandler {
|
|
|
148
149
|
changedIds: [dataArg.id],
|
|
149
150
|
reason: 'dns-provider-deleted',
|
|
150
151
|
});
|
|
152
|
+
this.opsServerRef.dcRouterRef.mailDnsSync.requestSync('DNS provider deleted');
|
|
151
153
|
}
|
|
152
154
|
return result;
|
|
153
155
|
},
|
|
@@ -65,28 +65,31 @@ export class DnsRecordHandler {
|
|
|
65
65
|
const userId = await this.requireAuth(dataArg, 'dns-records:write');
|
|
66
66
|
const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
|
|
67
67
|
if (!dnsManager) return { success: false, message: 'DnsManager not initialized' };
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
68
|
+
const result = await dnsManager.runManagedMailDnsMutationExclusive(async () => {
|
|
69
|
+
const claimError = await this.rejectClaimedHostnameMutation(
|
|
70
|
+
dataArg.domainId,
|
|
71
|
+
dataArg.name,
|
|
72
|
+
dataArg.type,
|
|
73
|
+
userId,
|
|
74
|
+
'create',
|
|
75
|
+
);
|
|
76
|
+
if (claimError) return { success: false, message: claimError };
|
|
77
|
+
return await dnsManager.createRecord({
|
|
78
|
+
domainId: dataArg.domainId,
|
|
79
|
+
name: dataArg.name,
|
|
80
|
+
type: dataArg.type,
|
|
81
|
+
value: dataArg.value,
|
|
82
|
+
ttl: dataArg.ttl,
|
|
83
|
+
proxied: dataArg.proxied,
|
|
84
|
+
createdBy: userId,
|
|
85
|
+
});
|
|
84
86
|
});
|
|
85
87
|
if (result.success) {
|
|
86
88
|
this.opsServerRef.invalidateRealtime?.(['dns', 'emailDomains'], {
|
|
87
89
|
changedIds: result.id ? [result.id] : undefined,
|
|
88
90
|
reason: 'dns-record-created',
|
|
89
91
|
});
|
|
92
|
+
this.opsServerRef.dcRouterRef.mailDnsSync.requestSync('operator DNS record created');
|
|
90
93
|
}
|
|
91
94
|
return result;
|
|
92
95
|
},
|
|
@@ -101,39 +104,42 @@ export class DnsRecordHandler {
|
|
|
101
104
|
const userId = await this.requireAuth(dataArg, 'dns-records:write');
|
|
102
105
|
const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
|
|
103
106
|
if (!dnsManager) return { success: false, message: 'DnsManager not initialized' };
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
existing.name,
|
|
109
|
-
existing.type,
|
|
110
|
-
userId,
|
|
111
|
-
'update',
|
|
112
|
-
);
|
|
113
|
-
if (currentClaimError) return { success: false, message: currentClaimError };
|
|
114
|
-
if (dataArg.name !== undefined) {
|
|
115
|
-
const nextClaimError = await this.rejectClaimedHostnameMutation(
|
|
107
|
+
const result = await dnsManager.runManagedMailDnsMutationExclusive(async () => {
|
|
108
|
+
const existing = await dnsManager.getRecord(dataArg.id);
|
|
109
|
+
if (existing) {
|
|
110
|
+
const currentClaimError = await this.rejectClaimedHostnameMutation(
|
|
116
111
|
existing.domainId,
|
|
117
|
-
|
|
112
|
+
existing.name,
|
|
118
113
|
existing.type,
|
|
119
114
|
userId,
|
|
120
115
|
'update',
|
|
121
116
|
);
|
|
122
|
-
if (
|
|
117
|
+
if (currentClaimError) return { success: false, message: currentClaimError };
|
|
118
|
+
if (dataArg.name !== undefined) {
|
|
119
|
+
const nextClaimError = await this.rejectClaimedHostnameMutation(
|
|
120
|
+
existing.domainId,
|
|
121
|
+
dataArg.name,
|
|
122
|
+
existing.type,
|
|
123
|
+
userId,
|
|
124
|
+
'update',
|
|
125
|
+
);
|
|
126
|
+
if (nextClaimError) return { success: false, message: nextClaimError };
|
|
127
|
+
}
|
|
123
128
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
129
|
+
return await dnsManager.updateRecord({
|
|
130
|
+
id: dataArg.id,
|
|
131
|
+
name: dataArg.name,
|
|
132
|
+
value: dataArg.value,
|
|
133
|
+
ttl: dataArg.ttl,
|
|
134
|
+
proxied: dataArg.proxied,
|
|
135
|
+
});
|
|
131
136
|
});
|
|
132
137
|
if (result.success) {
|
|
133
138
|
this.opsServerRef.invalidateRealtime?.(['dns', 'emailDomains'], {
|
|
134
139
|
changedIds: [dataArg.id],
|
|
135
140
|
reason: 'dns-record-updated',
|
|
136
141
|
});
|
|
142
|
+
this.opsServerRef.dcRouterRef.mailDnsSync.requestSync('operator DNS record updated');
|
|
137
143
|
}
|
|
138
144
|
return result;
|
|
139
145
|
},
|
|
@@ -148,23 +154,26 @@ export class DnsRecordHandler {
|
|
|
148
154
|
const userId = await this.requireAuth(dataArg, 'dns-records:write');
|
|
149
155
|
const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
|
|
150
156
|
if (!dnsManager) return { success: false, message: 'DnsManager not initialized' };
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
157
|
+
const result = await dnsManager.runManagedMailDnsMutationExclusive(async () => {
|
|
158
|
+
const existing = await dnsManager.getRecord(dataArg.id);
|
|
159
|
+
if (existing) {
|
|
160
|
+
const claimError = await this.rejectClaimedHostnameMutation(
|
|
161
|
+
existing.domainId,
|
|
162
|
+
existing.name,
|
|
163
|
+
existing.type,
|
|
164
|
+
userId,
|
|
165
|
+
'delete',
|
|
166
|
+
);
|
|
167
|
+
if (claimError) return { success: false, message: claimError };
|
|
168
|
+
}
|
|
169
|
+
return await dnsManager.deleteRecord(dataArg.id);
|
|
170
|
+
});
|
|
163
171
|
if (result.success) {
|
|
164
172
|
this.opsServerRef.invalidateRealtime?.(['dns', 'emailDomains'], {
|
|
165
173
|
changedIds: [dataArg.id],
|
|
166
174
|
reason: 'dns-record-deleted',
|
|
167
175
|
});
|
|
176
|
+
this.opsServerRef.dcRouterRef.mailDnsSync.requestSync('operator DNS record deleted');
|
|
168
177
|
}
|
|
169
178
|
return result;
|
|
170
179
|
},
|
|
@@ -72,6 +72,7 @@ export class DomainHandler {
|
|
|
72
72
|
changedIds: [id],
|
|
73
73
|
reason: 'domain-created',
|
|
74
74
|
});
|
|
75
|
+
this.opsServerRef.dcRouterRef.mailDnsSync.requestSync('DNS domain created');
|
|
75
76
|
return { success: true, id };
|
|
76
77
|
} catch (err: unknown) {
|
|
77
78
|
return { success: false, message: (err as Error).message };
|
|
@@ -98,6 +99,7 @@ export class DomainHandler {
|
|
|
98
99
|
changedIds: importedIds,
|
|
99
100
|
reason: 'domains-imported',
|
|
100
101
|
});
|
|
102
|
+
this.opsServerRef.dcRouterRef.mailDnsSync.requestSync('DNS provider domains imported');
|
|
101
103
|
return { success: true, importedIds };
|
|
102
104
|
} catch (err: unknown) {
|
|
103
105
|
return { success: false, message: (err as Error).message };
|
|
@@ -136,7 +138,12 @@ export class DomainHandler {
|
|
|
136
138
|
await this.requireAuth(dataArg, 'domains:write');
|
|
137
139
|
const dnsManager = this.opsServerRef.dcRouterRef.dnsManager;
|
|
138
140
|
if (!dnsManager) return { success: false, message: 'DnsManager not initialized' };
|
|
139
|
-
|
|
141
|
+
let ok: boolean;
|
|
142
|
+
try {
|
|
143
|
+
ok = await dnsManager.deleteDomain(dataArg.id);
|
|
144
|
+
} catch (error: unknown) {
|
|
145
|
+
return { success: false, message: (error as Error).message };
|
|
146
|
+
}
|
|
140
147
|
if (ok) {
|
|
141
148
|
// The private-route DNS overlay is derived from routes but gated on
|
|
142
149
|
// domain ownership, so deleting a domain invalidates it even though no
|
|
@@ -149,6 +156,7 @@ export class DomainHandler {
|
|
|
149
156
|
changedIds: [dataArg.id],
|
|
150
157
|
reason: 'domain-deleted',
|
|
151
158
|
});
|
|
159
|
+
this.opsServerRef.dcRouterRef.mailDnsSync.requestSync('DNS domain deleted');
|
|
152
160
|
}
|
|
153
161
|
return ok ? { success: true } : { success: false, message: 'Domain not found' };
|
|
154
162
|
},
|
|
@@ -169,6 +177,7 @@ export class DomainHandler {
|
|
|
169
177
|
changedIds: [dataArg.id],
|
|
170
178
|
reason: 'domain-synced',
|
|
171
179
|
});
|
|
180
|
+
this.opsServerRef.dcRouterRef.mailDnsSync.requestSync('DNS provider domain synchronized');
|
|
172
181
|
}
|
|
173
182
|
return result;
|
|
174
183
|
},
|
|
@@ -199,6 +208,7 @@ export class DomainHandler {
|
|
|
199
208
|
changedIds: [dataArg.id],
|
|
200
209
|
reason: 'domain-migrated',
|
|
201
210
|
});
|
|
211
|
+
this.opsServerRef.dcRouterRef.mailDnsSync.requestSync('DNS domain migrated');
|
|
202
212
|
}
|
|
203
213
|
return result;
|
|
204
214
|
},
|
|
@@ -176,6 +176,7 @@ export class RemoteIngressHubLifecycle {
|
|
|
176
176
|
this.dcRouterRef.tunnelManager = undefined;
|
|
177
177
|
}
|
|
178
178
|
}
|
|
179
|
+
this.dcRouterRef.mailDnsSync.requestEdgeEligibilityCheck('RemoteIngress hub stopped');
|
|
179
180
|
}
|
|
180
181
|
|
|
181
182
|
private async restartTunnelHubLocked(): Promise<void> {
|