@serve.zone/dcrouter 18.6.2 → 18.7.1

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 (41) hide show
  1. package/deno.json +1 -1
  2. package/dist_ts/00_commitinfo_data.js +1 -1
  3. package/dist_ts/classes.dcrouter.js +4 -1
  4. package/dist_ts/dns/classes.dns-server-runtime.js +3 -3
  5. package/dist_ts/dns/manager.dns.d.ts +16 -0
  6. package/dist_ts/dns/manager.dns.js +193 -40
  7. package/dist_ts/dns/providers/cloudflare.provider.d.ts +5 -0
  8. package/dist_ts/dns/providers/cloudflare.provider.js +78 -23
  9. package/dist_ts/dns/providers/interfaces.d.ts +2 -0
  10. package/dist_ts/email/classes.email-domain.manager.d.ts +1 -0
  11. package/dist_ts/email/classes.email-domain.manager.js +36 -8
  12. package/dist_ts/email/classes.mail-dns-sync.d.ts +8 -0
  13. package/dist_ts/email/classes.mail-dns-sync.js +239 -55
  14. package/dist_ts/email/classes.mail-edge-eligibility.js +44 -17
  15. package/dist_ts/email/classes.mail-egress-coordinator.js +13 -3
  16. package/dist_ts/email/reconcile-mail-dns-singleton.d.ts +1 -0
  17. package/dist_ts/email/reconcile-mail-dns-singleton.js +6 -2
  18. package/dist_ts/opsserver/handlers/dns-provider.handler.js +3 -1
  19. package/dist_ts/opsserver/handlers/dns-record.handler.js +44 -35
  20. package/dist_ts/opsserver/handlers/domain.handler.js +13 -2
  21. package/dist_ts/remoteingress/classes.hub-lifecycle.js +2 -1
  22. package/dist_ts_interfaces/data/email-domain.d.ts +2 -0
  23. package/dist_ts_web/00_commitinfo_data.js +1 -1
  24. package/package.json +4 -4
  25. package/readme.md +7 -7
  26. package/ts/00_commitinfo_data.ts +1 -1
  27. package/ts/classes.dcrouter.ts +4 -0
  28. package/ts/dns/classes.dns-server-runtime.ts +2 -2
  29. package/ts/dns/manager.dns.ts +235 -35
  30. package/ts/dns/providers/cloudflare.provider.ts +93 -26
  31. package/ts/dns/providers/interfaces.ts +3 -0
  32. package/ts/email/classes.email-domain.manager.ts +38 -6
  33. package/ts/email/classes.mail-dns-sync.ts +246 -53
  34. package/ts/email/classes.mail-edge-eligibility.ts +53 -15
  35. package/ts/email/classes.mail-egress-coordinator.ts +18 -4
  36. package/ts/email/reconcile-mail-dns-singleton.ts +8 -1
  37. package/ts/opsserver/handlers/dns-provider.handler.ts +2 -0
  38. package/ts/opsserver/handlers/dns-record.handler.ts +58 -49
  39. package/ts/opsserver/handlers/domain.handler.ts +11 -1
  40. package/ts/remoteingress/classes.hub-lifecycle.ts +1 -0
  41. 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 && await this.dcRouterRef.emailDomainManager?.getByDomain(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 (await import('../db/documents/classes.email-domain.doc.js')).EmailDomainDoc.findByDomain(context.senderDomain)
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(eligible.edges.map((candidate) => [candidate.edge.id, candidate]));
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 (managed.value.trim() !== intent.value.trim() || managed.name !== intent.name) {
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 claimError = await this.rejectClaimedHostnameMutation(
69
- dataArg.domainId,
70
- dataArg.name,
71
- dataArg.type,
72
- userId,
73
- 'create',
74
- );
75
- if (claimError) return { success: false, message: claimError };
76
- const result = await dnsManager.createRecord({
77
- domainId: dataArg.domainId,
78
- name: dataArg.name,
79
- type: dataArg.type,
80
- value: dataArg.value,
81
- ttl: dataArg.ttl,
82
- proxied: dataArg.proxied,
83
- createdBy: userId,
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 existing = await dnsManager.getRecord(dataArg.id);
105
- if (existing) {
106
- const currentClaimError = await this.rejectClaimedHostnameMutation(
107
- existing.domainId,
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
- dataArg.name,
112
+ existing.name,
118
113
  existing.type,
119
114
  userId,
120
115
  'update',
121
116
  );
122
- if (nextClaimError) return { success: false, message: nextClaimError };
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
- const result = await dnsManager.updateRecord({
126
- id: dataArg.id,
127
- name: dataArg.name,
128
- value: dataArg.value,
129
- ttl: dataArg.ttl,
130
- proxied: dataArg.proxied,
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 existing = await dnsManager.getRecord(dataArg.id);
152
- if (existing) {
153
- const claimError = await this.rejectClaimedHostnameMutation(
154
- existing.domainId,
155
- existing.name,
156
- existing.type,
157
- userId,
158
- 'delete',
159
- );
160
- if (claimError) return { success: false, message: claimError };
161
- }
162
- const result = await dnsManager.deleteRecord(dataArg.id);
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
- const ok = await dnsManager.deleteDomain(dataArg.id);
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> {
@@ -3,6 +3,6 @@
3
3
  */
4
4
  export const commitinfo = {
5
5
  name: '@serve.zone/dcrouter',
6
- version: '18.6.2',
6
+ version: '18.7.1',
7
7
  description: 'A multifaceted routing service handling mail and SMS delivery functions.'
8
8
  }