@serve.zone/dcrouter 17.10.1 → 18.0.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_serve/bundle.js +360 -360
- package/dist_ts/00_commitinfo_data.js +2 -2
- package/dist_ts/acme/acme-failure-classification.d.ts +64 -0
- package/dist_ts/acme/acme-failure-classification.js +114 -0
- package/dist_ts/acme/classes.smartacme-lifecycle.d.ts +42 -0
- package/dist_ts/acme/classes.smartacme-lifecycle.js +75 -4
- package/dist_ts/acme/index.d.ts +1 -0
- package/dist_ts/acme/index.js +2 -1
- package/dist_ts/classes.dcrouter.d.ts +33 -9
- package/dist_ts/classes.dcrouter.js +145 -11
- package/dist_ts/config/classes.route-config-manager.d.ts +56 -0
- package/dist_ts/config/classes.route-config-manager.js +161 -1
- package/dist_ts/db/documents/classes.dns-authority.doc.d.ts +30 -0
- package/dist_ts/db/documents/classes.dns-authority.doc.js +108 -0
- package/dist_ts/db/documents/index.d.ts +1 -0
- package/dist_ts/db/documents/index.js +2 -1
- package/dist_ts/dns/classes.dns-server-runtime.d.ts +109 -1
- package/dist_ts/dns/classes.dns-server-runtime.js +212 -34
- package/dist_ts/dns/classes.gateway-route-dns-reconciler.d.ts +7 -0
- package/dist_ts/dns/classes.gateway-route-dns-reconciler.js +37 -6
- package/dist_ts/dns/domain-ownership.d.ts +111 -0
- package/dist_ts/dns/domain-ownership.js +152 -0
- package/dist_ts/dns/index.d.ts +2 -0
- package/dist_ts/dns/index.js +3 -1
- package/dist_ts/dns/manager.dns-authority.d.ts +143 -0
- package/dist_ts/dns/manager.dns-authority.js +481 -0
- package/dist_ts/dns/manager.dns.d.ts +121 -12
- package/dist_ts/dns/manager.dns.js +298 -28
- package/dist_ts/errors/error.codes.d.ts +4 -0
- package/dist_ts/errors/error.codes.js +5 -1
- package/dist_ts/opsserver/classes.opsserver.d.ts +1 -0
- package/dist_ts/opsserver/classes.opsserver.js +3 -1
- package/dist_ts/opsserver/handlers/acme-config.handler.js +6 -1
- package/dist_ts/opsserver/handlers/certificate.handler.d.ts +16 -0
- package/dist_ts/opsserver/handlers/certificate.handler.js +96 -10
- package/dist_ts/opsserver/handlers/config.handler.js +4 -2
- package/dist_ts/opsserver/handlers/dns-authority.handler.d.ts +20 -0
- package/dist_ts/opsserver/handlers/dns-authority.handler.js +108 -0
- package/dist_ts/opsserver/handlers/dns-provider.handler.js +5 -1
- package/dist_ts/opsserver/handlers/domain.handler.js +9 -1
- package/dist_ts/opsserver/handlers/gatewayclient.handler.js +2 -2
- package/dist_ts/opsserver/handlers/index.d.ts +1 -0
- package/dist_ts/opsserver/handlers/index.js +2 -1
- package/dist_ts_interfaces/data/dns-authority.d.ts +98 -0
- package/dist_ts_interfaces/data/dns-authority.js +26 -0
- package/dist_ts_interfaces/data/index.d.ts +1 -0
- package/dist_ts_interfaces/data/index.js +2 -1
- package/dist_ts_interfaces/data/route-management.d.ts +9 -2
- package/dist_ts_interfaces/data/route-management.js +3 -1
- package/dist_ts_interfaces/requests/certificate.d.ts +23 -0
- package/dist_ts_interfaces/requests/certificate.js +1 -1
- package/dist_ts_interfaces/requests/dns-authority.d.ts +80 -0
- package/dist_ts_interfaces/requests/dns-authority.js +3 -0
- package/dist_ts_interfaces/requests/index.d.ts +1 -0
- package/dist_ts_interfaces/requests/index.js +2 -1
- package/dist_ts_migrations/index.js +122 -9
- package/dist_ts_oci_container/index.js +9 -4
- package/dist_ts_web/00_commitinfo_data.js +2 -2
- package/package.json +1 -1
- package/readme.hints.md +412 -0
- package/readme.md +48 -6
- package/ts/00_commitinfo_data.ts +1 -1
- package/ts/acme/acme-failure-classification.ts +201 -0
- package/ts/acme/classes.smartacme-lifecycle.ts +104 -3
- package/ts/acme/index.ts +1 -0
- package/ts/classes.dcrouter.ts +193 -23
- package/ts/config/classes.route-config-manager.ts +197 -0
- package/ts/db/documents/classes.dns-authority.doc.ts +49 -0
- package/ts/db/documents/index.ts +1 -0
- package/ts/dns/classes.dns-server-runtime.ts +257 -38
- package/ts/dns/classes.gateway-route-dns-reconciler.ts +41 -6
- package/ts/dns/domain-ownership.ts +272 -0
- package/ts/dns/index.ts +2 -0
- package/ts/dns/manager.dns-authority.ts +558 -0
- package/ts/dns/manager.dns.ts +373 -27
- package/ts/errors/error.codes.ts +4 -0
- package/ts/opsserver/classes.opsserver.ts +2 -0
- package/ts/opsserver/handlers/acme-config.handler.ts +7 -0
- package/ts/opsserver/handlers/certificate.handler.ts +103 -8
- package/ts/opsserver/handlers/config.handler.ts +3 -1
- package/ts/opsserver/handlers/dns-authority.handler.ts +142 -0
- package/ts/opsserver/handlers/dns-provider.handler.ts +6 -0
- package/ts/opsserver/handlers/domain.handler.ts +12 -0
- package/ts/opsserver/handlers/gatewayclient.handler.ts +1 -1
- package/ts/opsserver/handlers/index.ts +1 -0
- package/ts/readme.md +1 -1
- package/ts_web/00_commitinfo_data.ts +1 -1
|
@@ -266,22 +266,35 @@ export class CertificateHandler {
|
|
|
266
266
|
}
|
|
267
267
|
}
|
|
268
268
|
|
|
269
|
-
//
|
|
270
|
-
|
|
269
|
+
// What the running proxy actually serves. Always queried, not only as a
|
|
270
|
+
// fallback: the served state is an independent fact from the stored one and
|
|
271
|
+
// the two must be comparable.
|
|
272
|
+
let served: interfaces.requests.ICertificateServedState | undefined;
|
|
273
|
+
if (info.routeNames.length > 0) {
|
|
271
274
|
try {
|
|
272
275
|
const rustStatus = await smartProxy.getCertificateStatus(info.routeNames[0]);
|
|
273
276
|
if (rustStatus) {
|
|
274
|
-
|
|
275
|
-
expiryDate
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
277
|
+
served = {
|
|
278
|
+
expiryDate: rustStatus.expiresAt > 0
|
|
279
|
+
? new Date(rustStatus.expiresAt).toISOString()
|
|
280
|
+
: undefined,
|
|
281
|
+
isValid: rustStatus.isValid,
|
|
282
|
+
source: rustStatus.source || undefined,
|
|
283
|
+
};
|
|
279
284
|
}
|
|
280
285
|
} catch {
|
|
281
|
-
// Rust bridge may not support this command yet —
|
|
286
|
+
// Rust bridge may not support this command yet — leave served unknown.
|
|
282
287
|
}
|
|
283
288
|
}
|
|
284
289
|
|
|
290
|
+
// With no event data, the served state is also the best available view of
|
|
291
|
+
// the issued one.
|
|
292
|
+
if (status === 'unknown' && served) {
|
|
293
|
+
if (served.expiryDate) expiryDate = served.expiryDate;
|
|
294
|
+
if (served.source) issuer = served.source;
|
|
295
|
+
status = served.isValid ? 'valid' : 'expired';
|
|
296
|
+
}
|
|
297
|
+
|
|
285
298
|
// Check persisted cert data from smartdata document classes
|
|
286
299
|
if (status === 'unknown') {
|
|
287
300
|
const cleanDomain = domain.replace(/^\*\.?/, '');
|
|
@@ -352,6 +365,23 @@ export class CertificateHandler {
|
|
|
352
365
|
error = error || backoffInfo.lastError;
|
|
353
366
|
}
|
|
354
367
|
|
|
368
|
+
// Issued is not served. A certificate valid in storage while the engine
|
|
369
|
+
// still presents an invalid or expired one is a failure, however healthy
|
|
370
|
+
// the issuance path looks — this is exactly what hid a 21-day outage.
|
|
371
|
+
const servedMismatch = Boolean(
|
|
372
|
+
served
|
|
373
|
+
&& !served.isValid
|
|
374
|
+
&& (status === 'valid' || status === 'expiring'),
|
|
375
|
+
);
|
|
376
|
+
if (servedMismatch) {
|
|
377
|
+
status = 'failed';
|
|
378
|
+
error = error
|
|
379
|
+
|| `A valid certificate is stored for '${domain}'`
|
|
380
|
+
+ `${expiryDate ? ` (expires ${expiryDate})` : ''}, but the running proxy is serving an invalid one`
|
|
381
|
+
+ `${served?.expiryDate ? ` (expired ${served.expiryDate})` : ''}. `
|
|
382
|
+
+ 'The issued certificate has not reached the engine.';
|
|
383
|
+
}
|
|
384
|
+
|
|
355
385
|
certificates.push({
|
|
356
386
|
domain,
|
|
357
387
|
routeNames: info.routeNames,
|
|
@@ -363,6 +393,8 @@ export class CertificateHandler {
|
|
|
363
393
|
issuedAt,
|
|
364
394
|
error,
|
|
365
395
|
canReprovision: info.canReprovision,
|
|
396
|
+
served,
|
|
397
|
+
...(servedMismatch ? { servedMismatch: true } : {}),
|
|
366
398
|
backoffInfo,
|
|
367
399
|
});
|
|
368
400
|
}
|
|
@@ -419,6 +451,21 @@ export class CertificateHandler {
|
|
|
419
451
|
return { success: false, message: `No routes found for domain '${domain}'` };
|
|
420
452
|
}
|
|
421
453
|
|
|
454
|
+
// Refuse before triggering an order that cannot succeed. forceRenew is an
|
|
455
|
+
// operator override for backoff, not for missing ownership: without a zone we
|
|
456
|
+
// can prove is ours there is nowhere to place the DNS-01 challenge record.
|
|
457
|
+
if (dcRouter.dnsManager) {
|
|
458
|
+
const ownership = await dcRouter.dnsManager.resolveDomainOwnership(domain);
|
|
459
|
+
if (!ownership.verified) {
|
|
460
|
+
return {
|
|
461
|
+
success: false,
|
|
462
|
+
message:
|
|
463
|
+
`Cannot provision a certificate for '${domain}': domain ownership is unverified `
|
|
464
|
+
+ `(${ownership.reason}) — ${ownership.detail}`,
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
422
469
|
// Respect existing backoff unless explicitly overridden. Automated
|
|
423
470
|
// callers (gateway clients retrying cert fetches) used to reset the
|
|
424
471
|
// backoff and trigger a route re-apply on every request, producing a
|
|
@@ -488,12 +535,60 @@ export class CertificateHandler {
|
|
|
488
535
|
// Fallback when DB is disabled and there is no RouteConfigManager
|
|
489
536
|
await smartProxy.updateRoutes(smartProxy.routeManager.getRoutes());
|
|
490
537
|
}
|
|
538
|
+
const propagation = await this.describeCertPropagationGap(domain, routeNames);
|
|
539
|
+
if (propagation) {
|
|
540
|
+
return { success: false, message: propagation };
|
|
541
|
+
}
|
|
491
542
|
return { success: true, message: forceRenew ? `Certificate force-renewed for domain '${domain}'` : `Certificate reprovisioning triggered for domain '${domain}'` };
|
|
492
543
|
} catch (err: unknown) {
|
|
493
544
|
return { success: false, message: (err as Error).message || `Failed to reprovision certificate for ${domain}` };
|
|
494
545
|
}
|
|
495
546
|
}
|
|
496
547
|
|
|
548
|
+
/**
|
|
549
|
+
* After triggering the apply pipeline, check whether the certificate the proxy
|
|
550
|
+
* is actually serving is still expired. If it is, the reprovision did not
|
|
551
|
+
* propagate and reporting success would be a lie.
|
|
552
|
+
*
|
|
553
|
+
* The known cause is SmartProxy's own per-domain provisioning cooldown
|
|
554
|
+
* (`certProvisionFailureCooldownMs`, 30 min default). It is private in-memory
|
|
555
|
+
* state with no invalidation path: a provisioning sweep skips a cooling-down
|
|
556
|
+
* domain, and because the sweep returns before its summary log when every
|
|
557
|
+
* domain is skipped, the skip is not logged either. So a valid certificate can
|
|
558
|
+
* sit in storage while the proxy keeps serving the expired one, silently.
|
|
559
|
+
*
|
|
560
|
+
* Returns undefined when propagation looks fine or cannot be determined —
|
|
561
|
+
* `getCertificateStatus` goes through the Rust bridge and may be unavailable.
|
|
562
|
+
*/
|
|
563
|
+
private async describeCertPropagationGap(
|
|
564
|
+
domain: string,
|
|
565
|
+
routeNames: string[],
|
|
566
|
+
): Promise<string | undefined> {
|
|
567
|
+
const smartProxy = this.opsServerRef.dcRouterRef.smartProxy;
|
|
568
|
+
if (!smartProxy || routeNames.length === 0) {
|
|
569
|
+
return undefined;
|
|
570
|
+
}
|
|
571
|
+
let status: Awaited<ReturnType<typeof smartProxy.getCertificateStatus>>;
|
|
572
|
+
try {
|
|
573
|
+
status = await smartProxy.getCertificateStatus(routeNames[0]);
|
|
574
|
+
} catch {
|
|
575
|
+
// Bridge command unsupported — cannot determine, so do not claim a gap.
|
|
576
|
+
return undefined;
|
|
577
|
+
}
|
|
578
|
+
if (!status || status.isValid || !status.expiresAt || status.expiresAt <= 0) {
|
|
579
|
+
return undefined;
|
|
580
|
+
}
|
|
581
|
+
const servedUntil = new Date(status.expiresAt).toISOString();
|
|
582
|
+
const message =
|
|
583
|
+
`Certificate for '${domain}' was issued and stored, but route '${routeNames[0]}' is still serving a `
|
|
584
|
+
+ `certificate that expired at ${servedUntil}. The new certificate did not reach the running proxy: `
|
|
585
|
+
+ 'SmartProxy keeps a private per-domain provisioning cooldown that a sweep cannot clear while it is '
|
|
586
|
+
+ 'active, and it exposes no per-domain invalidation or hot-load API. Toggling this single route off and '
|
|
587
|
+
+ 'back on is the only supported way to force a fresh attempt without restarting dcrouter.';
|
|
588
|
+
logger.log('error', message, { domain, routeName: routeNames[0] });
|
|
589
|
+
return message;
|
|
590
|
+
}
|
|
591
|
+
|
|
497
592
|
/**
|
|
498
593
|
* After a force-renew, walk every route in the smartproxy that resolves to
|
|
499
594
|
* the same cert identity as `forcedDomain` and write the freshly-issued cert
|
|
@@ -137,7 +137,9 @@ export class ConfigHandler {
|
|
|
137
137
|
enabled: !!dcRouter.dnsServer,
|
|
138
138
|
port: 53,
|
|
139
139
|
nsDomains: opts.dnsNsDomains || [],
|
|
140
|
-
|
|
140
|
+
// The delegation-verified authority set. Deployment configuration no
|
|
141
|
+
// longer contributes: this is database state, and it is the only state.
|
|
142
|
+
scopes: dcRouter.dnsAuthorityManager?.getEffectiveZoneNames() || [],
|
|
141
143
|
recordCount: dnsRecords.length,
|
|
142
144
|
records: dnsRecords,
|
|
143
145
|
dnsChallenge: dnsChallengeEnabled,
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import * as plugins from '../../plugins.js';
|
|
2
|
+
import type { OpsServer } from '../classes.opsserver.js';
|
|
3
|
+
import * as interfaces from '../../../ts_interfaces/index.js';
|
|
4
|
+
import { requireOpsAuth } from '../helpers/auth.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* DNS authority handlers.
|
|
8
|
+
*
|
|
9
|
+
* Adding a zone here is an authority claim — after it succeeds dcrouter answers
|
|
10
|
+
* for that zone with the `aa` flag on a public port and will request
|
|
11
|
+
* certificates for it. So it carries the same posture as the other DNS write
|
|
12
|
+
* surfaces (admin identity or a `:write`-scoped token) *and* the claim itself is
|
|
13
|
+
* gated on a delegation proof the caller cannot forge. Authorization decides who
|
|
14
|
+
* may ask; the probe decides whether the answer is yes.
|
|
15
|
+
*/
|
|
16
|
+
export class DnsAuthorityHandler {
|
|
17
|
+
public typedrouter = new plugins.typedrequest.TypedRouter();
|
|
18
|
+
|
|
19
|
+
constructor(private opsServerRef: OpsServer) {
|
|
20
|
+
this.opsServerRef.typedrouter.addTypedRouter(this.typedrouter);
|
|
21
|
+
this.registerHandlers();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
private async requireAuth(
|
|
25
|
+
request: { identity?: interfaces.data.IIdentity; apiToken?: string },
|
|
26
|
+
requiredScope?: interfaces.data.TApiTokenScope,
|
|
27
|
+
): Promise<string> {
|
|
28
|
+
const auth = await requireOpsAuth(this.opsServerRef, request, {
|
|
29
|
+
scope: requiredScope,
|
|
30
|
+
requireAdminIdentity: requiredScope?.endsWith(':write'),
|
|
31
|
+
});
|
|
32
|
+
return auth.userId;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
private registerHandlers(): void {
|
|
36
|
+
// Read the authority set: delegation-verified zones, and nothing else.
|
|
37
|
+
this.typedrouter.addTypedHandler(
|
|
38
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetDnsAuthority>(
|
|
39
|
+
'getDnsAuthority',
|
|
40
|
+
async (dataArg) => {
|
|
41
|
+
await this.requireAuth(dataArg, 'dns-authority:read');
|
|
42
|
+
const manager = this.opsServerRef.dcRouterRef.dnsAuthorityManager;
|
|
43
|
+
if (!manager) {
|
|
44
|
+
// No manager means the set was never loaded. Reported as
|
|
45
|
+
// 'unavailable' rather than as an empty set, so a reader cannot
|
|
46
|
+
// mistake "we could not look" for "there is nothing".
|
|
47
|
+
return {
|
|
48
|
+
settings: {
|
|
49
|
+
zones: [],
|
|
50
|
+
state: 'unavailable' as const,
|
|
51
|
+
expectedNameservers: [],
|
|
52
|
+
updatedAt: 0,
|
|
53
|
+
updatedBy: '',
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return { settings: manager.getSettings() };
|
|
58
|
+
},
|
|
59
|
+
),
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
// Probe a zone's delegation without changing anything
|
|
63
|
+
this.typedrouter.addTypedHandler(
|
|
64
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_ProbeDnsAuthorityZone>(
|
|
65
|
+
'probeDnsAuthorityZone',
|
|
66
|
+
async (dataArg) => {
|
|
67
|
+
await this.requireAuth(dataArg, 'dns-authority:read');
|
|
68
|
+
const manager = this.opsServerRef.dcRouterRef.dnsAuthorityManager;
|
|
69
|
+
if (!manager) {
|
|
70
|
+
return {
|
|
71
|
+
probe: {
|
|
72
|
+
zone: dataArg.zone,
|
|
73
|
+
verdict: 'undeterminable' as const,
|
|
74
|
+
observedNameservers: [],
|
|
75
|
+
expectedNameservers: [],
|
|
76
|
+
detail: 'DnsAuthorityManager is not initialized',
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
return { probe: await manager.probeDelegation(dataArg.zone) };
|
|
81
|
+
},
|
|
82
|
+
),
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// Claim authority over a zone, against a delegation proof
|
|
86
|
+
this.typedrouter.addTypedHandler(
|
|
87
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_VerifyDnsAuthorityZone>(
|
|
88
|
+
'verifyDnsAuthorityZone',
|
|
89
|
+
async (dataArg) => {
|
|
90
|
+
const userId = await this.requireAuth(dataArg, 'dns-authority:write');
|
|
91
|
+
const manager = this.opsServerRef.dcRouterRef.dnsAuthorityManager;
|
|
92
|
+
if (!manager) {
|
|
93
|
+
return { success: false, message: 'DnsAuthorityManager is not initialized' };
|
|
94
|
+
}
|
|
95
|
+
const result = await manager.verifyZone(dataArg.zone, userId);
|
|
96
|
+
if (result.success) {
|
|
97
|
+
this.opsServerRef.invalidateRealtime?.(['dns', 'routes'], {
|
|
98
|
+
reason: 'dns-authority-verified',
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
return result;
|
|
102
|
+
},
|
|
103
|
+
),
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
// Drop a verified zone. Every zone is revocable — there is no bootstrap floor.
|
|
107
|
+
this.typedrouter.addTypedHandler(
|
|
108
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_RevokeDnsAuthorityZone>(
|
|
109
|
+
'revokeDnsAuthorityZone',
|
|
110
|
+
async (dataArg) => {
|
|
111
|
+
const userId = await this.requireAuth(dataArg, 'dns-authority:write');
|
|
112
|
+
const manager = this.opsServerRef.dcRouterRef.dnsAuthorityManager;
|
|
113
|
+
if (!manager) {
|
|
114
|
+
return { success: false, message: 'DnsAuthorityManager is not initialized' };
|
|
115
|
+
}
|
|
116
|
+
const result = await manager.revokeZone(dataArg.zone, userId);
|
|
117
|
+
if (result.success) {
|
|
118
|
+
this.opsServerRef.invalidateRealtime?.(['dns', 'routes'], {
|
|
119
|
+
reason: 'dns-authority-revoked',
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return result;
|
|
123
|
+
},
|
|
124
|
+
),
|
|
125
|
+
);
|
|
126
|
+
|
|
127
|
+
// Compare declared authority against observed delegation, both directions
|
|
128
|
+
this.typedrouter.addTypedHandler(
|
|
129
|
+
new plugins.typedrequest.TypedHandler<interfaces.requests.IReq_GetDnsAuthorityDrift>(
|
|
130
|
+
'getDnsAuthorityDrift',
|
|
131
|
+
async (dataArg) => {
|
|
132
|
+
await this.requireAuth(dataArg, 'dns-authority:read');
|
|
133
|
+
const manager = this.opsServerRef.dcRouterRef.dnsAuthorityManager;
|
|
134
|
+
if (!manager) {
|
|
135
|
+
return { drift: [] };
|
|
136
|
+
}
|
|
137
|
+
return { drift: await manager.auditDelegationDrift() };
|
|
138
|
+
},
|
|
139
|
+
),
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
@@ -138,6 +138,12 @@ export class DnsProviderHandler {
|
|
|
138
138
|
if (!dnsManager) return { success: false, message: 'DnsManager not initialized' };
|
|
139
139
|
const result = await dnsManager.deleteProvider(dataArg.id, dataArg.force ?? false);
|
|
140
140
|
if (result.success) {
|
|
141
|
+
// Removing a provider removes the credentialed zone listing that
|
|
142
|
+
// proved ownership for its domains, so the ownership-gated private
|
|
143
|
+
// route DNS overlay must be re-derived rather than left serving.
|
|
144
|
+
await this.opsServerRef.dcRouterRef.resyncPrivateRouteDnsOverlay(
|
|
145
|
+
'DNS provider deletion',
|
|
146
|
+
);
|
|
141
147
|
this.opsServerRef.invalidateRealtime?.(['dns', 'emailDomains'], {
|
|
142
148
|
changedIds: [dataArg.id],
|
|
143
149
|
reason: 'dns-provider-deleted',
|
|
@@ -138,6 +138,13 @@ export class DomainHandler {
|
|
|
138
138
|
if (!dnsManager) return { success: false, message: 'DnsManager not initialized' };
|
|
139
139
|
const ok = await dnsManager.deleteDomain(dataArg.id);
|
|
140
140
|
if (ok) {
|
|
141
|
+
// The private-route DNS overlay is derived from routes but gated on
|
|
142
|
+
// domain ownership, so deleting a domain invalidates it even though no
|
|
143
|
+
// route changed. Without this it keeps answering authoritatively for a
|
|
144
|
+
// zone we no longer manage until an unrelated route apply.
|
|
145
|
+
await this.opsServerRef.dcRouterRef.resyncPrivateRouteDnsOverlay(
|
|
146
|
+
'domain deletion',
|
|
147
|
+
);
|
|
141
148
|
this.opsServerRef.invalidateRealtime?.(['dns', 'emailDomains'], {
|
|
142
149
|
changedIds: [dataArg.id],
|
|
143
150
|
reason: 'domain-deleted',
|
|
@@ -183,6 +190,11 @@ export class DomainHandler {
|
|
|
183
190
|
deleteExistingProviderRecords: dataArg.deleteExistingProviderRecords,
|
|
184
191
|
});
|
|
185
192
|
if (result.success) {
|
|
193
|
+
// Migrating away from a provider can drop the proof that made the
|
|
194
|
+
// overlay's hostnames servable, so re-derive it here too.
|
|
195
|
+
await this.opsServerRef.dcRouterRef.resyncPrivateRouteDnsOverlay(
|
|
196
|
+
'domain migration',
|
|
197
|
+
);
|
|
186
198
|
this.opsServerRef.invalidateRealtime?.(['dns', 'emailDomains'], {
|
|
187
199
|
changedIds: [dataArg.id],
|
|
188
200
|
reason: 'domain-migrated',
|
|
@@ -604,7 +604,7 @@ export class GatewayClientHandler {
|
|
|
604
604
|
enabled: Boolean(dcRouter.remoteIngressManager?.getHubSettings().enabled),
|
|
605
605
|
},
|
|
606
606
|
dns: {
|
|
607
|
-
authoritative: Boolean(dcRouter.
|
|
607
|
+
authoritative: Boolean(dcRouter.dnsAuthorityManager?.getEffectiveZoneNames().length),
|
|
608
608
|
providerManaged: Boolean(dcRouter.dnsManager),
|
|
609
609
|
},
|
|
610
610
|
http3: {
|
|
@@ -17,6 +17,7 @@ export * from './network-target.handler.js';
|
|
|
17
17
|
export * from './users.handler.js';
|
|
18
18
|
export * from './dns-provider.handler.js';
|
|
19
19
|
export * from './domain.handler.js';
|
|
20
|
+
export * from './dns-authority.handler.js';
|
|
20
21
|
export * from './dns-record.handler.js';
|
|
21
22
|
export * from './acme-config.handler.js';
|
|
22
23
|
export * from './email-domain.handler.js';
|
package/ts/readme.md
CHANGED
|
@@ -58,7 +58,7 @@ await router.start();
|
|
|
58
58
|
- SmartProxy for HTTP/HTTPS/TCP routes
|
|
59
59
|
- `UnifiedEmailServer` for SMTP ingress, delivery, managed app address bindings, and outbound SMTP submission when `emailConfig` is present
|
|
60
60
|
- DB-backed managers for routes, API tokens, target profiles, domains, records, ACME config, and email domains when the DB is enabled
|
|
61
|
-
- embedded authoritative DNS and DoH route generation from `dnsNsDomains`
|
|
61
|
+
- embedded authoritative DNS and DoH route generation from `dnsNsDomains` plus the delegation-verified zones held in the database
|
|
62
62
|
- VPN, RADIUS, and remote ingress services when their config blocks are enabled
|
|
63
63
|
- OpsServer and the dashboard, which start on every boot
|
|
64
64
|
- an admin-JWT authenticated read-only MCP endpoint at `/mcp` for safe route, DNS, email, RemoteIngress, and VPN summaries
|