@serve.zone/dcrouter 17.10.2 → 18.0.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 (89) hide show
  1. package/deno.json +1 -1
  2. package/dist_serve/bundle.js +360 -360
  3. package/dist_ts/00_commitinfo_data.js +2 -2
  4. package/dist_ts/acme/acme-failure-classification.d.ts +64 -0
  5. package/dist_ts/acme/acme-failure-classification.js +114 -0
  6. package/dist_ts/acme/classes.smartacme-lifecycle.d.ts +42 -0
  7. package/dist_ts/acme/classes.smartacme-lifecycle.js +75 -4
  8. package/dist_ts/acme/index.d.ts +1 -0
  9. package/dist_ts/acme/index.js +2 -1
  10. package/dist_ts/classes.dcrouter.d.ts +33 -9
  11. package/dist_ts/classes.dcrouter.js +145 -11
  12. package/dist_ts/config/classes.route-config-manager.d.ts +56 -0
  13. package/dist_ts/config/classes.route-config-manager.js +161 -1
  14. package/dist_ts/db/documents/classes.dns-authority.doc.d.ts +30 -0
  15. package/dist_ts/db/documents/classes.dns-authority.doc.js +108 -0
  16. package/dist_ts/db/documents/index.d.ts +1 -0
  17. package/dist_ts/db/documents/index.js +2 -1
  18. package/dist_ts/dns/classes.dns-server-runtime.d.ts +109 -1
  19. package/dist_ts/dns/classes.dns-server-runtime.js +212 -34
  20. package/dist_ts/dns/domain-ownership.d.ts +111 -0
  21. package/dist_ts/dns/domain-ownership.js +152 -0
  22. package/dist_ts/dns/index.d.ts +2 -0
  23. package/dist_ts/dns/index.js +3 -1
  24. package/dist_ts/dns/manager.dns-authority.d.ts +143 -0
  25. package/dist_ts/dns/manager.dns-authority.js +481 -0
  26. package/dist_ts/dns/manager.dns.d.ts +121 -12
  27. package/dist_ts/dns/manager.dns.js +298 -28
  28. package/dist_ts/email/classes.accepted-email-spool.d.ts +15 -0
  29. package/dist_ts/email/classes.accepted-email-spool.js +102 -23
  30. package/dist_ts/email/classes.smartmta-blob-storage-manager.js +12 -3
  31. package/dist_ts/errors/error.codes.d.ts +4 -0
  32. package/dist_ts/errors/error.codes.js +5 -1
  33. package/dist_ts/opsserver/classes.opsserver.d.ts +1 -0
  34. package/dist_ts/opsserver/classes.opsserver.js +3 -1
  35. package/dist_ts/opsserver/handlers/acme-config.handler.js +6 -1
  36. package/dist_ts/opsserver/handlers/certificate.handler.d.ts +16 -0
  37. package/dist_ts/opsserver/handlers/certificate.handler.js +96 -10
  38. package/dist_ts/opsserver/handlers/config.handler.js +4 -2
  39. package/dist_ts/opsserver/handlers/dns-authority.handler.d.ts +20 -0
  40. package/dist_ts/opsserver/handlers/dns-authority.handler.js +108 -0
  41. package/dist_ts/opsserver/handlers/dns-provider.handler.js +5 -1
  42. package/dist_ts/opsserver/handlers/domain.handler.js +9 -1
  43. package/dist_ts/opsserver/handlers/gatewayclient.handler.js +2 -2
  44. package/dist_ts/opsserver/handlers/index.d.ts +1 -0
  45. package/dist_ts/opsserver/handlers/index.js +2 -1
  46. package/dist_ts_interfaces/data/dns-authority.d.ts +98 -0
  47. package/dist_ts_interfaces/data/dns-authority.js +26 -0
  48. package/dist_ts_interfaces/data/index.d.ts +1 -0
  49. package/dist_ts_interfaces/data/index.js +2 -1
  50. package/dist_ts_interfaces/data/route-management.d.ts +9 -2
  51. package/dist_ts_interfaces/data/route-management.js +3 -1
  52. package/dist_ts_interfaces/requests/certificate.d.ts +23 -0
  53. package/dist_ts_interfaces/requests/certificate.js +1 -1
  54. package/dist_ts_interfaces/requests/dns-authority.d.ts +80 -0
  55. package/dist_ts_interfaces/requests/dns-authority.js +3 -0
  56. package/dist_ts_interfaces/requests/index.d.ts +1 -0
  57. package/dist_ts_interfaces/requests/index.js +2 -1
  58. package/dist_ts_oci_container/index.js +9 -4
  59. package/dist_ts_web/00_commitinfo_data.js +2 -2
  60. package/package.json +1 -1
  61. package/readme.hints.md +412 -0
  62. package/readme.md +48 -6
  63. package/ts/00_commitinfo_data.ts +1 -1
  64. package/ts/acme/acme-failure-classification.ts +201 -0
  65. package/ts/acme/classes.smartacme-lifecycle.ts +104 -3
  66. package/ts/acme/index.ts +1 -0
  67. package/ts/classes.dcrouter.ts +193 -23
  68. package/ts/config/classes.route-config-manager.ts +197 -0
  69. package/ts/db/documents/classes.dns-authority.doc.ts +49 -0
  70. package/ts/db/documents/index.ts +1 -0
  71. package/ts/dns/classes.dns-server-runtime.ts +257 -38
  72. package/ts/dns/domain-ownership.ts +272 -0
  73. package/ts/dns/index.ts +2 -0
  74. package/ts/dns/manager.dns-authority.ts +558 -0
  75. package/ts/dns/manager.dns.ts +373 -27
  76. package/ts/email/classes.accepted-email-spool.ts +98 -21
  77. package/ts/email/classes.smartmta-blob-storage-manager.ts +10 -2
  78. package/ts/errors/error.codes.ts +4 -0
  79. package/ts/opsserver/classes.opsserver.ts +2 -0
  80. package/ts/opsserver/handlers/acme-config.handler.ts +7 -0
  81. package/ts/opsserver/handlers/certificate.handler.ts +103 -8
  82. package/ts/opsserver/handlers/config.handler.ts +3 -1
  83. package/ts/opsserver/handlers/dns-authority.handler.ts +142 -0
  84. package/ts/opsserver/handlers/dns-provider.handler.ts +6 -0
  85. package/ts/opsserver/handlers/domain.handler.ts +12 -0
  86. package/ts/opsserver/handlers/gatewayclient.handler.ts +1 -1
  87. package/ts/opsserver/handlers/index.ts +1 -0
  88. package/ts/readme.md +1 -1
  89. package/ts_web/00_commitinfo_data.ts +1 -1
@@ -0,0 +1,111 @@
1
+ import { PlatformError, type IErrorContext } from '../errors/base.errors.js';
2
+ import type { TDomainSource } from '../../dist_ts_interfaces/data/domain.js';
3
+ /**
4
+ * Domain ownership verification.
5
+ *
6
+ * dcrouter may only take two kinds of action on a hostname if we can prove the
7
+ * zone is ours: request an ACME certificate for it, and answer DNS queries for
8
+ * it authoritatively. Both used to be reachable without any ownership record at
9
+ * all, which produced two production failures:
10
+ *
11
+ * - Routes with `tls.certificate === 'auto'` were created for zones that had no
12
+ * `DomainDoc`. DNS-01 could never place the challenge TXT, so the per-domain
13
+ * provisioning budget was consumed against a cause no retry can fix and the
14
+ * certificates silently expired.
15
+ * - `DomainDoc`s created through the ops API set `authoritative = true`
16
+ * unconditionally, and the embedded smartdns server marks *any* answer a
17
+ * registered handler produces as authoritative (`aa`) regardless of
18
+ * `authoritativeZones`. dcrouter therefore served apex NS records and an
19
+ * RFC1918 A record, publicly, for zones whose real delegation belonged to
20
+ * third parties.
21
+ *
22
+ * There are exactly two proofs available in-process, neither of which an ops-API
23
+ * caller can forge:
24
+ *
25
+ * - `provider-zone`: the zone has a `DomainDoc` with `source === 'provider'` and
26
+ * a `providerId`. It only gets there through `importDomainsFromProvider()`,
27
+ * which requires the zone to be listed by a credentialed provider account.
28
+ * - `delegation-verified-zone`: the zone is in the DNS authority set, which a
29
+ * zone only enters by having its public NS records observed naming our
30
+ * nameservers. An ops-API caller cannot repoint somebody else's delegation,
31
+ * so writing the record is not the same as manufacturing the proof.
32
+ *
33
+ * This used to read `options.dnsScopes` instead — deployment configuration,
34
+ * trusted because only a redeploy could change it. That trust was real but the
35
+ * cost was a second, un-reconcilable representation of DNS authority, so it is
36
+ * gone: the authority set now comes from the database and carries its evidence.
37
+ *
38
+ * Anything else is unverified and must fail closed.
39
+ */
40
+ export type TDomainOwnershipMethod = 'provider-zone' | 'delegation-verified-zone';
41
+ export type TDomainOwnershipFailure =
42
+ /** No DomainDoc covers the hostname at all. */
43
+ 'no-managed-domain'
44
+ /** Provider-sourced DomainDoc without a providerId — the credentialed link is gone. */
45
+ | 'provider-link-missing'
46
+ /** dcrouter-hosted DomainDoc outside every verified zone: self-asserted authority. */
47
+ | 'unverified-dcrouter-zone'
48
+ /** The hostname is not a usable FQDN (wildcard-only, empty, malformed labels). */
49
+ | 'invalid-hostname';
50
+ export interface IDomainOwnershipZone {
51
+ name: string;
52
+ source: TDomainSource;
53
+ providerId?: string;
54
+ }
55
+ export interface IDomainOwnershipVerified {
56
+ verified: true;
57
+ fqdn: string;
58
+ zone: string;
59
+ method: TDomainOwnershipMethod;
60
+ evidence: string;
61
+ }
62
+ export interface IDomainOwnershipUnverified {
63
+ verified: false;
64
+ fqdn: string;
65
+ zone?: string;
66
+ reason: TDomainOwnershipFailure;
67
+ detail: string;
68
+ }
69
+ export type TDomainOwnership = IDomainOwnershipVerified | IDomainOwnershipUnverified;
70
+ /**
71
+ * Normalize a route/record hostname to the FQDN whose ownership must be proven.
72
+ *
73
+ * A wildcard is proven by the zone beneath it, so a single leading `*` is
74
+ * stripped in both forms SmartProxy accepts for certificate provisioning:
75
+ * `*.example.com` and the routing-glob `*example.com` (see
76
+ * `normalizeDomainsForCertProvisioning` in smartproxy). A bare `*` normalizes to
77
+ * nothing and is rejected — no certificate can be issued for it, so it must fail
78
+ * loudly rather than reach ACME.
79
+ *
80
+ * Returns undefined for anything that is not a usable single hostname.
81
+ */
82
+ export declare const normalizeOwnershipHostname: (hostnameArg: string) => string | undefined;
83
+ /**
84
+ * The authority zone covering `fqdn`, if any (the zone itself or a subzone of
85
+ * one). Subzones count: being authoritative for `example.com` means
86
+ * `internal.example.com` is ours too.
87
+ */
88
+ export declare const findCoveringAuthorityZone: (fqdnArg: string, authorityZones?: string[]) => string | undefined;
89
+ /**
90
+ * Resolve whether we can prove ownership of `fqdn`. Pure: callers pass the zone
91
+ * set and the authority set so this stays testable and does exactly one DB
92
+ * read per audit pass rather than one per hostname.
93
+ */
94
+ export declare const resolveDomainOwnership: (args: {
95
+ fqdn: string;
96
+ zones: IDomainOwnershipZone[];
97
+ authorityZones?: string[];
98
+ }) => TDomainOwnership;
99
+ export declare const buildDomainOwnershipMessage: (ownership: IDomainOwnershipUnverified, operation: string) => string;
100
+ /**
101
+ * Thrown wherever an unverified domain would otherwise gain a certificate
102
+ * requirement or authoritative DNS. HIGH severity so PlatformError's automatic
103
+ * log lands at `error` (this must never be a debuggable-later warning), and
104
+ * NON_RECOVERABLE by construction: no retry can turn an unowned domain into an
105
+ * owned one, so retry layers must classify it as permanent.
106
+ */
107
+ export declare class DomainOwnershipError extends PlatformError {
108
+ readonly ownership: IDomainOwnershipUnverified;
109
+ constructor(ownership: IDomainOwnershipUnverified, operation: string, component: string, context?: IErrorContext);
110
+ protected createWithContext(context: IErrorContext): PlatformError;
111
+ }
@@ -0,0 +1,152 @@
1
+ import { PlatformError } from '../errors/base.errors.js';
2
+ import { DCR_DOMAIN_OWNERSHIP_UNVERIFIED, ErrorCategory, ErrorRecoverability, ErrorSeverity, } from '../errors/error.codes.js';
3
+ /**
4
+ * Normalize a route/record hostname to the FQDN whose ownership must be proven.
5
+ *
6
+ * A wildcard is proven by the zone beneath it, so a single leading `*` is
7
+ * stripped in both forms SmartProxy accepts for certificate provisioning:
8
+ * `*.example.com` and the routing-glob `*example.com` (see
9
+ * `normalizeDomainsForCertProvisioning` in smartproxy). A bare `*` normalizes to
10
+ * nothing and is rejected — no certificate can be issued for it, so it must fail
11
+ * loudly rather than reach ACME.
12
+ *
13
+ * Returns undefined for anything that is not a usable single hostname.
14
+ */
15
+ export const normalizeOwnershipHostname = (hostnameArg) => {
16
+ const hostname = hostnameArg
17
+ .trim()
18
+ .toLowerCase()
19
+ .replace(/\.$/, '')
20
+ .replace(/^\*\.?/, '');
21
+ if (!hostname || hostname.length > 253)
22
+ return undefined;
23
+ if (hostname.includes('*') || hostname.includes(',') || hostname.includes(' '))
24
+ return undefined;
25
+ const labels = hostname.split('.');
26
+ if (labels.length < 2)
27
+ return undefined;
28
+ if (labels.some((labelArg) => !labelArg || labelArg.length > 63
29
+ || !/^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/.test(labelArg)))
30
+ return undefined;
31
+ return hostname;
32
+ };
33
+ const normalizeZoneName = (zoneArg) => zoneArg.trim().toLowerCase().replace(/\.$/, '');
34
+ const coversFqdn = (zone, fqdn) => Boolean(zone) && (fqdn === zone || fqdn.endsWith(`.${zone}`));
35
+ /**
36
+ * The authority zone covering `fqdn`, if any (the zone itself or a subzone of
37
+ * one). Subzones count: being authoritative for `example.com` means
38
+ * `internal.example.com` is ours too.
39
+ */
40
+ export const findCoveringAuthorityZone = (fqdnArg, authorityZones) => {
41
+ const fqdn = normalizeZoneName(fqdnArg);
42
+ return (authorityZones || [])
43
+ .map(normalizeZoneName)
44
+ .filter(Boolean)
45
+ .sort((a, b) => b.length - a.length)
46
+ .find((zone) => coversFqdn(zone, fqdn));
47
+ };
48
+ /**
49
+ * Resolve whether we can prove ownership of `fqdn`. Pure: callers pass the zone
50
+ * set and the authority set so this stays testable and does exactly one DB
51
+ * read per audit pass rather than one per hostname.
52
+ */
53
+ export const resolveDomainOwnership = (args) => {
54
+ const fqdn = normalizeOwnershipHostname(args.fqdn);
55
+ if (!fqdn) {
56
+ return {
57
+ verified: false,
58
+ fqdn: args.fqdn,
59
+ reason: 'invalid-hostname',
60
+ detail: `'${args.fqdn}' is not a usable hostname, so its ownership cannot be established`,
61
+ };
62
+ }
63
+ // A delegation-verified zone is proof on its own: the public NS records were
64
+ // observed naming our nameservers, which no ops-API caller can arrange.
65
+ const coveringZone = findCoveringAuthorityZone(fqdn, args.authorityZones);
66
+ if (coveringZone) {
67
+ return {
68
+ verified: true,
69
+ fqdn,
70
+ zone: coveringZone,
71
+ method: 'delegation-verified-zone',
72
+ evidence: `dns-authority:${coveringZone}`,
73
+ };
74
+ }
75
+ // Otherwise the proof must come from a credentialed provider zone. Most
76
+ // specific zone first, but every covering zone is a candidate: owning
77
+ // example.com still proves sub.example.com even when a more specific
78
+ // dcrouter-hosted doc for the subzone exists. The first verified candidate
79
+ // wins; otherwise the most specific failure is reported.
80
+ const candidates = args.zones
81
+ .map((zoneArg) => ({ ...zoneArg, name: normalizeZoneName(zoneArg.name) }))
82
+ .filter((zoneArg) => coversFqdn(zoneArg.name, fqdn))
83
+ .sort((a, b) => b.name.length - a.name.length);
84
+ if (candidates.length === 0) {
85
+ return {
86
+ verified: false,
87
+ fqdn,
88
+ reason: 'no-managed-domain',
89
+ detail: `no managed domain and no delegation-verified zone covers ${fqdn}; import the zone from a DNS provider, or point its NS records at our nameservers and verify it, before requesting certificates or serving DNS for it`,
90
+ };
91
+ }
92
+ const failures = [];
93
+ for (const zone of candidates) {
94
+ if (zone.source === 'provider') {
95
+ if (!zone.providerId) {
96
+ failures.push({
97
+ verified: false,
98
+ fqdn,
99
+ zone: zone.name,
100
+ reason: 'provider-link-missing',
101
+ detail: `managed domain ${zone.name} is provider-sourced but has no providerId, so the credentialed zone listing that proved ownership is gone`,
102
+ });
103
+ continue;
104
+ }
105
+ return {
106
+ verified: true,
107
+ fqdn,
108
+ zone: zone.name,
109
+ method: 'provider-zone',
110
+ evidence: `provider:${zone.providerId}`,
111
+ };
112
+ }
113
+ failures.push({
114
+ verified: false,
115
+ fqdn,
116
+ zone: zone.name,
117
+ reason: 'unverified-dcrouter-zone',
118
+ detail: `managed domain ${zone.name} is dcrouter-hosted but is not in the DNS authority set, so nothing proves the zone is delegated to us; verify its delegation (dns-authority:write) or import it from the DNS provider that holds it`,
119
+ });
120
+ }
121
+ return failures[0];
122
+ };
123
+ export const buildDomainOwnershipMessage = (ownership, operation) => `${operation} refused for '${ownership.fqdn}': domain ownership is unverified (${ownership.reason}) — ${ownership.detail}`;
124
+ /**
125
+ * Thrown wherever an unverified domain would otherwise gain a certificate
126
+ * requirement or authoritative DNS. HIGH severity so PlatformError's automatic
127
+ * log lands at `error` (this must never be a debuggable-later warning), and
128
+ * NON_RECOVERABLE by construction: no retry can turn an unowned domain into an
129
+ * owned one, so retry layers must classify it as permanent.
130
+ */
131
+ export class DomainOwnershipError extends PlatformError {
132
+ ownership;
133
+ constructor(ownership, operation, component, context = {}) {
134
+ super(buildDomainOwnershipMessage(ownership, operation), DCR_DOMAIN_OWNERSHIP_UNVERIFIED, ErrorSeverity.HIGH, ErrorCategory.CONFIGURATION, ErrorRecoverability.NON_RECOVERABLE, {
135
+ component,
136
+ operation,
137
+ userMessage: `Ownership of '${ownership.fqdn}' is not verified: ${ownership.detail}`,
138
+ ...context,
139
+ data: {
140
+ fqdn: ownership.fqdn,
141
+ zone: ownership.zone,
142
+ reason: ownership.reason,
143
+ ...context.data,
144
+ },
145
+ });
146
+ this.ownership = ownership;
147
+ }
148
+ createWithContext(context) {
149
+ return new DomainOwnershipError(this.ownership, this.context.operation || 'operation', this.context.component || 'domain-ownership', context);
150
+ }
151
+ }
152
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiZG9tYWluLW93bmVyc2hpcC5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uLy4uLy4uL3RzL2Rucy9kb21haW4tb3duZXJzaGlwLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBLE9BQU8sRUFBRSxhQUFhLEVBQXNCLE1BQU0sMEJBQTBCLENBQUM7QUFDN0UsT0FBTyxFQUNMLCtCQUErQixFQUMvQixhQUFhLEVBQ2IsbUJBQW1CLEVBQ25CLGFBQWEsR0FDZCxNQUFNLDBCQUEwQixDQUFDO0FBNkVsQzs7Ozs7Ozs7Ozs7R0FXRztBQUNILE1BQU0sQ0FBQyxNQUFNLDBCQUEwQixHQUFHLENBQUMsV0FBbUIsRUFBc0IsRUFBRTtJQUNwRixNQUFNLFFBQVEsR0FBRyxXQUFXO1NBQ3pCLElBQUksRUFBRTtTQUNOLFdBQVcsRUFBRTtTQUNiLE9BQU8sQ0FBQyxLQUFLLEVBQUUsRUFBRSxDQUFDO1NBQ2xCLE9BQU8sQ0FBQyxRQUFRLEVBQUUsRUFBRSxDQUFDLENBQUM7SUFDekIsSUFBSSxDQUFDLFFBQVEsSUFBSSxRQUFRLENBQUMsTUFBTSxHQUFHLEdBQUc7UUFBRSxPQUFPLFNBQVMsQ0FBQztJQUN6RCxJQUFJLFFBQVEsQ0FBQyxRQUFRLENBQUMsR0FBRyxDQUFDLElBQUksUUFBUSxDQUFDLFFBQVEsQ0FBQyxHQUFHLENBQUMsSUFBSSxRQUFRLENBQUMsUUFBUSxDQUFDLEdBQUcsQ0FBQztRQUFFLE9BQU8sU0FBUyxDQUFDO0lBQ2pHLE1BQU0sTUFBTSxHQUFHLFFBQVEsQ0FBQyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUM7SUFDbkMsSUFBSSxNQUFNLENBQUMsTUFBTSxHQUFHLENBQUM7UUFBRSxPQUFPLFNBQVMsQ0FBQztJQUN4QyxJQUFJLE1BQU0sQ0FBQyxJQUFJLENBQUMsQ0FBQyxRQUFRLEVBQUUsRUFBRSxDQUFDLENBQUMsUUFBUSxJQUFJLFFBQVEsQ0FBQyxNQUFNLEdBQUcsRUFBRTtXQUMxRCxDQUFDLG1DQUFtQyxDQUFDLElBQUks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
@@ -1,4 +1,6 @@
1
1
  export * from './manager.dns.js';
2
+ export * from './domain-ownership.js';
3
+ export * from './manager.dns-authority.js';
2
4
  export * from './providers/index.js';
3
5
  export * from './classes.dns-server-runtime.js';
4
6
  export * from './classes.gateway-route-dns-reconciler.js';
@@ -1,6 +1,8 @@
1
1
  export * from './manager.dns.js';
2
+ export * from './domain-ownership.js';
3
+ export * from './manager.dns-authority.js';
2
4
  export * from './providers/index.js';
3
5
  export * from './classes.dns-server-runtime.js';
4
6
  export * from './classes.gateway-route-dns-reconciler.js';
5
7
  export * from './txt-record-presentation.js';
6
- //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi90cy9kbnMvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsY0FBYyxrQkFBa0IsQ0FBQztBQUNqQyxjQUFjLHNCQUFzQixDQUFDO0FBQ3JDLGNBQWMsaUNBQWlDLENBQUM7QUFDaEQsY0FBYywyQ0FBMkMsQ0FBQztBQUMxRCxjQUFjLDhCQUE4QixDQUFDIn0=
8
+ //# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiaW5kZXguanMiLCJzb3VyY2VSb290IjoiIiwic291cmNlcyI6WyIuLi8uLi8uLi90cy9kbnMvaW5kZXgudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IkFBQUEsY0FBYyxrQkFBa0IsQ0FBQztBQUNqQyxjQUFjLHVCQUF1QixDQUFDO0FBQ3RDLGNBQWMsNEJBQTRCLENBQUM7QUFDM0MsY0FBYyxzQkFBc0IsQ0FBQztBQUNyQyxjQUFjLGlDQUFpQyxDQUFDO0FBQ2hELGNBQWMsMkNBQTJDLENBQUM7QUFDMUQsY0FBYyw4QkFBOEIsQ0FBQyJ9
@@ -0,0 +1,143 @@
1
+ import type { IDnsAuthorityDrift, IDnsAuthoritySettings, IDnsDelegationProbe, TDnsAuthorityState } from '../../dist_ts_interfaces/data/dns-authority.js';
2
+ /**
3
+ * Result of applying an authority change to the running process.
4
+ * Reconciliation is all-or-nothing: a partial application is rolled back.
5
+ */
6
+ export interface IDnsAuthorityMutationResult {
7
+ success: boolean;
8
+ message?: string;
9
+ probe?: IDnsDelegationProbe;
10
+ settings?: IDnsAuthoritySettings;
11
+ }
12
+ /** Re-derives runtime state after the effective authority set changed. */
13
+ export type TDnsAuthorityReconciler = (reasonArg: string) => Promise<void>;
14
+ /**
15
+ * DnsAuthorityManager — owns which zones dcrouter may answer for
16
+ * authoritatively, and proves that claim rather than accepting it.
17
+ *
18
+ * Why this exists: `dnsScopes` was the last un-migrated bootstrap option in an
19
+ * otherwise DB-driven router. It was read once at startup and had no mutation
20
+ * path, so claiming a newly delegated zone required a restart — measured at
21
+ * 30–60 s of total public outage. But simply making the declared list editable
22
+ * through the ops API would have been worse than the disease: the domain
23
+ * ownership predicate accepts zone coverage as *proof* precisely because a
24
+ * caller cannot write it. A `setDnsScopes` mutation would let an operator append
25
+ * any domain and manufacture that proof — the identical self-assertion vector
26
+ * that `createDcrouterDomain` used to have.
27
+ *
28
+ * So a zone earns authority by delegation instead: its public NS records must
29
+ * name our `dnsNsDomains`. An ops-API caller cannot fake that without actually
30
+ * controlling the domain, so the proof stays unforgeable while becoming
31
+ * mutable at runtime. It is also the comparison that was missing — claimed
32
+ * authority versus real delegation, checked by the mechanism itself rather than
33
+ * by an audit bolted on afterwards.
34
+ *
35
+ * **There is exactly one source of authority: this database document.** An
36
+ * earlier revision kept bootstrap `dnsScopes` as an always-in-effect floor and
37
+ * unioned it with the verified set. That is gone. A floor is a second
38
+ * representation of the same fact, it can only be changed by a redeploy, and it
39
+ * cannot be revoked through the API — all three of which are the properties
40
+ * that produced the outage this path exists to prevent.
41
+ *
42
+ * Removing the floor makes the *unreadable database* case load-bearing, so it is
43
+ * handled explicitly rather than by accident. Three situations, three answers:
44
+ *
45
+ * - **document missing** — a readable database that has never been seeded. The
46
+ * authority set is known, and it is empty: claim nothing. Logged at `error`
47
+ * with the remediation, and the startup drift audit enumerates every zone
48
+ * delegated to us that we are refusing to serve, which is the worklist.
49
+ * - **document present, no zones** — identical handling, different message:
50
+ * somebody revoked everything, which is a legitimate state we must not
51
+ * silently repopulate.
52
+ * - **document unreadable** — the authority set is *unknown*. `start()` throws
53
+ * rather than reporting an empty set, because rendering an unknown as "claim
54
+ * nothing" is exactly how a transient database fault would take every zone
55
+ * off the air. `DnsServerRuntime.setup()` then refuses to start on
56
+ * `getState() === 'unavailable'`. That second check is not redundant:
57
+ * `DnsServer.dependsOn('DnsManager')` only *orders* startup, and taskbuffer
58
+ * starts later levels even when an earlier optional service failed, so the
59
+ * consumer has to read the state itself. Both services stay failed and
60
+ * dcrouter runs degraded until it is restarted with a readable database —
61
+ * DNS down and visibly failed, rather than up and asserting nothing.
62
+ *
63
+ * Fail closed on authority, never silently on service: with an empty set the
64
+ * DNS server still starts, still serves DoH, and still picks zones up the moment
65
+ * one is verified — without a restart.
66
+ */
67
+ export declare class DnsAuthorityManager {
68
+ private getExpectedNameservers;
69
+ private verifiedZones;
70
+ private updatedAt;
71
+ private updatedBy;
72
+ private state;
73
+ private reconciler?;
74
+ constructor(getExpectedNameservers: () => string[]);
75
+ start(): Promise<void>;
76
+ stop(): Promise<void>;
77
+ /**
78
+ * Wire the callback that re-derives runtime state (zone handlers, route
79
+ * certificate warnings, private-route overlay) after the effective set changes.
80
+ */
81
+ setReconciler(reconciler?: TDnsAuthorityReconciler): void;
82
+ /**
83
+ * The authority set: delegation-verified zones, sorted so every consumer that
84
+ * needs a stable "first zone" (the DNSSEC signing zone, for one) gets the same
85
+ * answer across restarts regardless of database insertion order.
86
+ *
87
+ * This is what the ownership predicate consumes, and it is the only input to
88
+ * it. There is no bootstrap contribution.
89
+ */
90
+ getEffectiveZoneNames(): string[];
91
+ /** Whether the stored authority set was readable when it was last loaded. */
92
+ getState(): TDnsAuthorityState;
93
+ getSettings(): IDnsAuthoritySettings;
94
+ /**
95
+ * Ask the public DNS whether `zone` is delegated to our nameservers.
96
+ *
97
+ * Deliberately uses `strategy: 'doh'` — a single DNS-over-HTTPS attempt
98
+ * against a public resolver, with no system-resolver fallback. smartdns'
99
+ * `getNameServers()` uses `dns.resolveNs`, i.e. the *system* resolver, which on
100
+ * a dcrouter host may be dcrouter itself: it would happily answer with the very
101
+ * NS records we generated, making the probe self-confirming. An independent
102
+ * vantage point is the whole point of the proof.
103
+ *
104
+ * The three verdicts are kept distinct on purpose. A timeout is not evidence
105
+ * that a zone is not ours; collapsing 'undeterminable' into 'not-delegated'
106
+ * would let a transient resolver fault revoke authority.
107
+ */
108
+ probeDelegation(zoneArg: string): Promise<IDnsDelegationProbe>;
109
+ /**
110
+ * Claim authority over a zone, but only against a positive delegation proof.
111
+ * An undeterminable probe refuses the mutation — never assume either way.
112
+ */
113
+ verifyZone(zoneArg: string, verifiedBy: string): Promise<IDnsAuthorityMutationResult>;
114
+ /**
115
+ * Drop a verified zone. Every zone is revocable — there is no undroppable
116
+ * deployment-declared floor any more, so withdrawing authority never needs a
117
+ * redeploy and never leaves the router asserting a claim it cannot retract.
118
+ */
119
+ revokeZone(zoneArg: string, updatedBy: string): Promise<IDnsAuthorityMutationResult>;
120
+ /**
121
+ * Persist a new zone set and reconcile the running process against it.
122
+ *
123
+ * Fail closed: if reconciliation throws, the previous set is restored in the
124
+ * database and re-reconciled, so the router is never left half-converted.
125
+ * Reconciliation is idempotent, which is what makes the rollback safe.
126
+ */
127
+ private applyZones;
128
+ private persist;
129
+ /**
130
+ * Compare claimed authority against what is actually true, in every direction.
131
+ *
132
+ * This is the check whose absence let one zone sit declared in `dnsScopes`
133
+ * while four live zones were delegated to our nameservers and unclaimed.
134
+ *
135
+ * Advisory by design. It never mutates the authority set: a resolver blip at
136
+ * startup must not revoke authority for every zone and convert a transient
137
+ * fault into the outage this whole path exists to avoid. Undeterminable probes
138
+ * are skipped rather than reported as drift.
139
+ */
140
+ auditDelegationDrift(): Promise<IDnsAuthorityDrift[]>;
141
+ /** Run the audit and log every finding at `error`. Never throws. */
142
+ logDelegationDrift(): Promise<void>;
143
+ }