ninepm 0.69.0 → 0.78.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/README.md +86 -10
- package/dist/cli-types.d.ts +2 -0
- package/dist/dns-host.d.ts +15 -0
- package/dist/dns-host.js +43 -0
- package/dist/dns-providers.d.ts +91 -0
- package/dist/dns-providers.js +194 -0
- package/dist/domain-state.d.ts +147 -0
- package/dist/domain-state.js +248 -0
- package/dist/index.d.ts +185 -7
- package/dist/index.js +906 -24
- package/package.json +1 -1
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
export declare const CUSTOM_DOMAIN_EXPIRED_MESSAGE = "This domain was removed because its DNS verification was never completed. Add the domain again to retry.";
|
|
2
|
+
/** The name the CLI has always exported the expired marker under. */
|
|
3
|
+
export declare const CUSTOM_DOMAIN_EXPIRED_TEXT = "This domain was removed because its DNS verification was never completed. Add the domain again to retry.";
|
|
4
|
+
export declare const CUSTOM_DOMAINS_SERVICE_MODE_MESSAGE = "Custom domains are not available for service-token apps, so this domain's hostname was removed. Switching the app to another access mode sets it up again, with new DNS verification values to publish.";
|
|
5
|
+
export declare const LEGACY_SERVICE_MODE_MESSAGE = "Custom domains are available only for public apps.";
|
|
6
|
+
/** Whether a stored error is the service-mode teardown marker, in either wording. */
|
|
7
|
+
export declare function isServiceModeProvisioningError(error: string | null | undefined): boolean;
|
|
8
|
+
export declare const CUSTOM_DOMAINS_NOT_CONFIGURED_MESSAGE = "Custom domains are not fully configured on this platform.";
|
|
9
|
+
export declare const CUSTOM_DOMAIN_ROUTE_CONFLICT_MESSAGE = "This domain is already claimed elsewhere on the platform. Contact support to release it.";
|
|
10
|
+
export interface DomainStateDnsInstruction {
|
|
11
|
+
type: string;
|
|
12
|
+
host: string;
|
|
13
|
+
value: string;
|
|
14
|
+
optional?: boolean;
|
|
15
|
+
}
|
|
16
|
+
/** Whether CAA records let one of the platform's certificate authorities issue (#1999). */
|
|
17
|
+
export interface DomainStateCaaCheck {
|
|
18
|
+
status: "none" | "allows" | "blocks";
|
|
19
|
+
recordName: string | null;
|
|
20
|
+
via?: string;
|
|
21
|
+
ownerUnknown?: true;
|
|
22
|
+
allowedIssuers: string[];
|
|
23
|
+
platformIssuers: string[];
|
|
24
|
+
}
|
|
25
|
+
export interface DomainStateDnsCheck {
|
|
26
|
+
provider: string | null;
|
|
27
|
+
/** The DNS host's stable id (src/shared/dns-providers.ts). Absent for an unknown host and from older servers. */
|
|
28
|
+
providerId?: string;
|
|
29
|
+
nameservers: string[];
|
|
30
|
+
/** The zone the records go in, as the check found it. Absent from older servers and when no name answered NS. */
|
|
31
|
+
zone?: string;
|
|
32
|
+
records: Array<{
|
|
33
|
+
type: string;
|
|
34
|
+
host: string;
|
|
35
|
+
expected: string;
|
|
36
|
+
current: string[];
|
|
37
|
+
matches: boolean;
|
|
38
|
+
doubledAt?: string;
|
|
39
|
+
}>;
|
|
40
|
+
caa?: DomainStateCaaCheck;
|
|
41
|
+
}
|
|
42
|
+
/** What the derivation reads: a domain as any of the domain endpoints returns it. */
|
|
43
|
+
export interface DomainStateInput {
|
|
44
|
+
hostname: string;
|
|
45
|
+
routingStatus: string;
|
|
46
|
+
tlsStatus: string;
|
|
47
|
+
dnsInstructions: DomainStateDnsInstruction[];
|
|
48
|
+
provisioningError: string | null;
|
|
49
|
+
activatedAt?: string | null;
|
|
50
|
+
dnsCheck?: DomainStateDnsCheck;
|
|
51
|
+
providerMessage?: string | null;
|
|
52
|
+
providerMessageAt?: string | null;
|
|
53
|
+
providerMessageSource?: string | null;
|
|
54
|
+
}
|
|
55
|
+
export type DomainState = "live" | "waiting_for_dns" | "dns_unverified" | "issuing_certificate" | "failed" | "expired";
|
|
56
|
+
export type DomainRecordCheck = "found" | "missing" | "different" | "doubled" | "not_checked" | "apex_addresses";
|
|
57
|
+
export interface DomainRecordStatus {
|
|
58
|
+
type: string;
|
|
59
|
+
name: string;
|
|
60
|
+
fqdn: string;
|
|
61
|
+
expected: string;
|
|
62
|
+
current: string[] | null;
|
|
63
|
+
ok: boolean | null;
|
|
64
|
+
check: DomainRecordCheck;
|
|
65
|
+
doubledAt?: string;
|
|
66
|
+
apex: boolean;
|
|
67
|
+
optional: boolean;
|
|
68
|
+
}
|
|
69
|
+
export declare function domainRecordStatuses(domain: DomainStateInput): DomainRecordStatus[];
|
|
70
|
+
/**
|
|
71
|
+
* The `www.<domain>` to connect instead, when this hostname is a bare domain whose DNS host
|
|
72
|
+
* cannot point a root at a hostname at all (#1996): GoDaddy, Route 53, IONOS, Wix. Read only
|
|
73
|
+
* from a host the check both named and identified, so an unknown host, an older server and a
|
|
74
|
+
* domain with no check keep the ordinary records. Routing that is already active is the
|
|
75
|
+
* platform's own evidence that the root record exists, and wins over the profile.
|
|
76
|
+
*/
|
|
77
|
+
export declare function rootUnsupportedWwwHostname(domain: DomainStateInput): string | null;
|
|
78
|
+
/**
|
|
79
|
+
* Whether a domain's instructions show "connect www instead" in place of the root record. It
|
|
80
|
+
* reads the status's own next step, so the display and the status cannot disagree: routing
|
|
81
|
+
* that is already active, an unknown host or an older server never show it, and a server's
|
|
82
|
+
* verdict is honoured even for a host this copy's table does not know.
|
|
83
|
+
*/
|
|
84
|
+
export declare function showsUseWww(next: {
|
|
85
|
+
action?: string;
|
|
86
|
+
} | null | undefined): boolean;
|
|
87
|
+
export declare function deriveDomainState(domain: DomainStateInput, records?: DomainRecordStatus[]): DomainState;
|
|
88
|
+
export type DomainReasonCode = "records_missing" | "record_points_elsewhere" | "dns_changed_after_live" | "dns_not_checked" | "apex_unverifiable" | "dns_host_cannot_point_root" | "caa_blocks" | "certificate_pending" | "certificate_failed" | "hostname_failed" | "service_mode" | "claimed_elsewhere" | "platform_not_configured" | "provider_error" | "expired";
|
|
89
|
+
export interface DomainReason {
|
|
90
|
+
code: DomainReasonCode;
|
|
91
|
+
/**
|
|
92
|
+
* The provider's own text, verbatim, when one was reported. `source` says whose it is: only
|
|
93
|
+
* "certificate_authority" may be presented as the certificate authority's; "hostname" is the
|
|
94
|
+
* hostname's own verification note and is worded neutrally.
|
|
95
|
+
*/
|
|
96
|
+
providerMessage?: {
|
|
97
|
+
text: string;
|
|
98
|
+
seenAt: string;
|
|
99
|
+
source: "certificate_authority" | "hostname";
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
export type DomainNextAction = "none" | "add_records" | "replace_record" | "confirm_records" | "use_www" | "allow_ca_in_your_zone" | "ask_caa_controller" | "check_caa" | "wait" | "start_over" | "change_access_mode" | "add_again" | "recheck" | "contact_support";
|
|
103
|
+
export interface DomainNextRecord {
|
|
104
|
+
type: string;
|
|
105
|
+
host: string;
|
|
106
|
+
value: string;
|
|
107
|
+
/**
|
|
108
|
+
* What to do with this one record. A step can mix them: a routing record pointing elsewhere is
|
|
109
|
+
* "replace" while a missing TXT value beside it is "add", because a name may hold several TXT
|
|
110
|
+
* values and replacing one could remove a value that must stay. TXT is never "replace".
|
|
111
|
+
*/
|
|
112
|
+
action: "add" | "replace" | "confirm";
|
|
113
|
+
check: DomainRecordCheck;
|
|
114
|
+
}
|
|
115
|
+
export interface DomainNext {
|
|
116
|
+
/** Who has to act; absent when nobody does (a live domain). */
|
|
117
|
+
who?: "owner" | "platform";
|
|
118
|
+
action: DomainNextAction;
|
|
119
|
+
/** add/replace name only rows checked missing or different; not_checked rows only ever get confirm_records. */
|
|
120
|
+
records?: DomainNextRecord[];
|
|
121
|
+
/** CAA only: the name whose records decide, and the name of the owner's that points at it. */
|
|
122
|
+
caa?: {
|
|
123
|
+
recordName: string;
|
|
124
|
+
via?: string;
|
|
125
|
+
ownerUnknown?: true;
|
|
126
|
+
platformIssuers: string[];
|
|
127
|
+
};
|
|
128
|
+
/** The platform's next background re-read. It reads the status again and restarts nothing. */
|
|
129
|
+
nextCheckAt?: string;
|
|
130
|
+
/** use_www only: the hostname to add to the app instead. */
|
|
131
|
+
hostname?: string;
|
|
132
|
+
}
|
|
133
|
+
export interface DomainStatus {
|
|
134
|
+
state: DomainState;
|
|
135
|
+
/** Null only for a live domain. */
|
|
136
|
+
reason: DomainReason | null;
|
|
137
|
+
next: DomainNext;
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* The state, why, and the one next step. `state` is exactly deriveDomainState's; `reason` and
|
|
141
|
+
* `next` only refine it. Two rules hold throughout: a record nobody looked at is never called
|
|
142
|
+
* missing (it gets confirm_records, never add), and nothing here restarts validation or
|
|
143
|
+
* suggests that a restart speeds things up - Start over is named only for a failed certificate.
|
|
144
|
+
*/
|
|
145
|
+
export declare function deriveDomainStatus(domain: DomainStateInput, options?: {
|
|
146
|
+
nextCheckAt?: string | null;
|
|
147
|
+
}): DomainStatus;
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
// AUTO-GENERATED by scripts/generate-cli-package.mjs from the in-repo CLI source -- do not edit.
|
|
2
|
+
// Edit src/cli or src/shared instead; this is regenerated on typecheck/test/build/pack.
|
|
3
|
+
// One plain-language status for a custom domain (#1994): which of six states it is in, why,
|
|
4
|
+
// and the one thing that happens next. The control plane computes it after its DNS check and
|
|
5
|
+
// sends it on every domain; the CLI and the dashboard derive it from this same file when they
|
|
6
|
+
// meet a server that predates the field, so the two paths cannot disagree. Copied into the CLI
|
|
7
|
+
// tarball by scripts/generate-cli-package.mjs, so it imports nothing but dns-host and
|
|
8
|
+
// dns-providers, which are copied beside it.
|
|
9
|
+
import { relativeRecordName, zoneApexFor } from "./dns-host.js";
|
|
10
|
+
import { dnsProviderProfile } from "./dns-providers.js";
|
|
11
|
+
// The markers the control plane writes into provisioningError. Matched as exact strings, so a
|
|
12
|
+
// wording change here changes what every reader recognises at once; the server's reconcilers
|
|
13
|
+
// import them from this file for the same reason.
|
|
14
|
+
// #751: the expired marker. Deliberately static - no configured day-count inside it - because
|
|
15
|
+
// the reconcilers, claim checks and quota carve-outs key on this exact string.
|
|
16
|
+
export const CUSTOM_DOMAIN_EXPIRED_MESSAGE = "This domain was removed because its DNS verification was never completed. Add the domain again to retry.";
|
|
17
|
+
/** The name the CLI has always exported the expired marker under. */
|
|
18
|
+
export const CUSTOM_DOMAIN_EXPIRED_TEXT = CUSTOM_DOMAIN_EXPIRED_MESSAGE;
|
|
19
|
+
// Written onto a domain row when its app is in private_service mode, the only mode whose
|
|
20
|
+
// domains are torn down (#750 kept them for otp/idp).
|
|
21
|
+
export const CUSTOM_DOMAINS_SERVICE_MODE_MESSAGE = "Custom domains are not available for service-token apps, so this domain's hostname was removed. Switching the app to another access mode sets it up again, with new DNS verification values to publish.";
|
|
22
|
+
// The wording rows torn down by earlier releases still carry. It says custom domains are
|
|
23
|
+
// public-only, which is false (otp/idp apps keep theirs), so it is recognised and replaced.
|
|
24
|
+
export const LEGACY_SERVICE_MODE_MESSAGE = "Custom domains are available only for public apps.";
|
|
25
|
+
/** Whether a stored error is the service-mode teardown marker, in either wording. */
|
|
26
|
+
export function isServiceModeProvisioningError(error) {
|
|
27
|
+
return error === CUSTOM_DOMAINS_SERVICE_MODE_MESSAGE || error === LEGACY_SERVICE_MODE_MESSAGE;
|
|
28
|
+
}
|
|
29
|
+
// Written when the platform has no fallback script, so nothing can create, re-adopt or tear
|
|
30
|
+
// down this domain's route.
|
|
31
|
+
export const CUSTOM_DOMAINS_NOT_CONFIGURED_MESSAGE = "Custom domains are not fully configured on this platform.";
|
|
32
|
+
// Written when another script's route already holds the hostname.
|
|
33
|
+
export const CUSTOM_DOMAIN_ROUTE_CONFLICT_MESSAGE = "This domain is already claimed elsewhere on the platform. Contact support to release it.";
|
|
34
|
+
function normalizeDnsName(value) {
|
|
35
|
+
return value.trim().toLowerCase().replace(/\.$/, "");
|
|
36
|
+
}
|
|
37
|
+
// The value the server's DNS check compares against: TXT verbatim, names lowercased without a root dot.
|
|
38
|
+
function checkedRecordValue(type, value) {
|
|
39
|
+
return type === "TXT" ? value : normalizeDnsName(value);
|
|
40
|
+
}
|
|
41
|
+
export function domainRecordStatuses(domain) {
|
|
42
|
+
const apex = zoneApexFor(domain.hostname, domain.dnsCheck?.zone);
|
|
43
|
+
return domain.dnsInstructions.map((instruction) => {
|
|
44
|
+
const type = instruction.type.toUpperCase();
|
|
45
|
+
const fqdn = normalizeDnsName(instruction.host);
|
|
46
|
+
const expected = checkedRecordValue(type, instruction.value);
|
|
47
|
+
// Matched on the expected value as well as host and type: the two TXT values of one name
|
|
48
|
+
// are separate rows, and keying on host and type alone gives both rows one verdict (#1986).
|
|
49
|
+
const seen = domain.dnsCheck?.records.find((entry) => entry.type.toUpperCase() === type && normalizeDnsName(entry.host) === fqdn && entry.expected === expected);
|
|
50
|
+
const atApex = type === "CNAME" && fqdn === apex;
|
|
51
|
+
const check = domainRecordCheck(seen, atApex);
|
|
52
|
+
return {
|
|
53
|
+
type,
|
|
54
|
+
name: relativeRecordName(fqdn, apex),
|
|
55
|
+
fqdn,
|
|
56
|
+
expected: instruction.value,
|
|
57
|
+
current: seen ? seen.current : null,
|
|
58
|
+
ok: check === "found" ? true : check === "missing" || check === "different" || check === "doubled" ? false : null,
|
|
59
|
+
check,
|
|
60
|
+
...(check === "doubled" && seen?.doubledAt ? { doubledAt: seen.doubledAt } : {}),
|
|
61
|
+
apex: atApex,
|
|
62
|
+
optional: instruction.optional === true,
|
|
63
|
+
};
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
const IP_ADDRESS = /^(\d{1,3}(\.\d{1,3}){3}|[0-9a-f:]*:[0-9a-f:.]+)$/i;
|
|
67
|
+
// The server looks a CNAME up and, finding none, falls back to the name's A records. At the
|
|
68
|
+
// zone root that is exactly what a correct ALIAS/ANAME or flattened CNAME answers: addresses,
|
|
69
|
+
// never the target name. So addresses at the apex are not evidence of a wrong record; they
|
|
70
|
+
// cannot be compared from outside at all.
|
|
71
|
+
function domainRecordCheck(seen, atApex) {
|
|
72
|
+
if (!seen)
|
|
73
|
+
return "not_checked";
|
|
74
|
+
if (seen.matches)
|
|
75
|
+
return "found";
|
|
76
|
+
if (seen.doubledAt)
|
|
77
|
+
return "doubled";
|
|
78
|
+
if (seen.current.length === 0)
|
|
79
|
+
return "missing";
|
|
80
|
+
if (atApex && seen.current.every((value) => IP_ADDRESS.test(value.trim())))
|
|
81
|
+
return "apex_addresses";
|
|
82
|
+
return "different";
|
|
83
|
+
}
|
|
84
|
+
// Unproven is decided by `ok`, not by listing check values: a check this code does not know yet
|
|
85
|
+
// carries ok null and so reads as unknown, never as found. The one exception is an apex
|
|
86
|
+
// answering with addresses, which the platform's own active routing vouches for.
|
|
87
|
+
function isUnproven(record, routingActive) {
|
|
88
|
+
if (record.ok !== null)
|
|
89
|
+
return false;
|
|
90
|
+
return !(record.check === "apex_addresses" && routingActive);
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The `www.<domain>` to connect instead, when this hostname is a bare domain whose DNS host
|
|
94
|
+
* cannot point a root at a hostname at all (#1996): GoDaddy, Route 53, IONOS, Wix. Read only
|
|
95
|
+
* from a host the check both named and identified, so an unknown host, an older server and a
|
|
96
|
+
* domain with no check keep the ordinary records. Routing that is already active is the
|
|
97
|
+
* platform's own evidence that the root record exists, and wins over the profile.
|
|
98
|
+
*/
|
|
99
|
+
export function rootUnsupportedWwwHostname(domain) {
|
|
100
|
+
const check = domain.dnsCheck;
|
|
101
|
+
if (!check?.provider || domain.routingStatus === "active")
|
|
102
|
+
return null;
|
|
103
|
+
if (dnsProviderProfile(check.providerId)?.apex.support !== "none")
|
|
104
|
+
return null;
|
|
105
|
+
const host = domain.hostname.toLowerCase().replace(/\.$/, "");
|
|
106
|
+
return host === zoneApexFor(host, check.zone) ? `www.${host}` : null;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* Whether a domain's instructions show "connect www instead" in place of the root record. It
|
|
110
|
+
* reads the status's own next step, so the display and the status cannot disagree: routing
|
|
111
|
+
* that is already active, an unknown host or an older server never show it, and a server's
|
|
112
|
+
* verdict is honoured even for a host this copy's table does not know.
|
|
113
|
+
*/
|
|
114
|
+
export function showsUseWww(next) {
|
|
115
|
+
return next?.action === "use_www";
|
|
116
|
+
}
|
|
117
|
+
export function deriveDomainState(domain, records = domainRecordStatuses(domain)) {
|
|
118
|
+
if (domain.provisioningError === CUSTOM_DOMAIN_EXPIRED_MESSAGE)
|
|
119
|
+
return "expired";
|
|
120
|
+
if (domain.routingStatus === "failed" || domain.tlsStatus === "failed" || domain.provisioningError)
|
|
121
|
+
return "failed";
|
|
122
|
+
if (domain.routingStatus === "active" && domain.tlsStatus === "active")
|
|
123
|
+
return "live";
|
|
124
|
+
// No record the owner can publish will finish this domain: it waits on the owner choosing www.
|
|
125
|
+
if (rootUnsupportedWwwHostname(domain))
|
|
126
|
+
return "waiting_for_dns";
|
|
127
|
+
const required = records.filter((record) => !record.optional);
|
|
128
|
+
if (required.some((record) => record.ok === false))
|
|
129
|
+
return "waiting_for_dns";
|
|
130
|
+
const routingActive = domain.routingStatus === "active";
|
|
131
|
+
// An apex answering with addresses defers to the platform's own routing check: active means
|
|
132
|
+
// it found the record, pending means it has not yet.
|
|
133
|
+
const unproven = required.some((record) => isUnproven(record, routingActive));
|
|
134
|
+
if (unproven || (!domain.dnsCheck && required.length === 0)) {
|
|
135
|
+
// Routing still pending is the platform's own evidence that it has not seen the records.
|
|
136
|
+
return routingActive ? "dns_unverified" : "waiting_for_dns";
|
|
137
|
+
}
|
|
138
|
+
return "issuing_certificate";
|
|
139
|
+
}
|
|
140
|
+
function nextRecord(record) {
|
|
141
|
+
const action = record.ok === false ? (record.check === "different" && record.type !== "TXT" ? "replace" : "add") : "confirm";
|
|
142
|
+
return { type: record.type, host: record.fqdn, value: record.expected, action, check: record.check };
|
|
143
|
+
}
|
|
144
|
+
// Who controls the CAA record that decides, which decides the advice: in the owner's own zone
|
|
145
|
+
// they can add an entry; at a CNAME target only whoever runs that name can; and when the
|
|
146
|
+
// resolver did not say, nothing is handed over to edit at a name the owner may not control.
|
|
147
|
+
function caaNext(caa) {
|
|
148
|
+
const summary = {
|
|
149
|
+
recordName: caa.recordName,
|
|
150
|
+
...(caa.via ? { via: caa.via } : {}),
|
|
151
|
+
...(caa.ownerUnknown ? { ownerUnknown: true } : {}),
|
|
152
|
+
platformIssuers: caa.platformIssuers,
|
|
153
|
+
};
|
|
154
|
+
if (caa.via)
|
|
155
|
+
return { who: "owner", action: "ask_caa_controller", caa: summary };
|
|
156
|
+
if (caa.ownerUnknown)
|
|
157
|
+
return { who: "owner", action: "check_caa", caa: summary };
|
|
158
|
+
const issuer = caa.platformIssuers[0];
|
|
159
|
+
return {
|
|
160
|
+
who: "owner",
|
|
161
|
+
action: "allow_ca_in_your_zone",
|
|
162
|
+
caa: summary,
|
|
163
|
+
...(issuer ? { records: [{ type: "CAA", host: caa.recordName, value: `0 issue "${issuer}"`, action: "add", check: "missing" }] } : {}),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* The state, why, and the one next step. `state` is exactly deriveDomainState's; `reason` and
|
|
168
|
+
* `next` only refine it. Two rules hold throughout: a record nobody looked at is never called
|
|
169
|
+
* missing (it gets confirm_records, never add), and nothing here restarts validation or
|
|
170
|
+
* suggests that a restart speeds things up - Start over is named only for a failed certificate.
|
|
171
|
+
*/
|
|
172
|
+
export function deriveDomainStatus(domain, options = {}) {
|
|
173
|
+
const records = domainRecordStatuses(domain);
|
|
174
|
+
const state = deriveDomainState(domain, records);
|
|
175
|
+
const source = domain.providerMessageSource === "certificate_authority" ? "certificate_authority" : "hostname";
|
|
176
|
+
// A hostname's own note while the owner is still publishing records is the expected "does not
|
|
177
|
+
// CNAME to this zone": it says nothing the records table does not, so it is not surfaced there.
|
|
178
|
+
const showMessage = Boolean(domain.providerMessage && domain.providerMessageAt) && state !== "live" && !(source === "hostname" && state === "waiting_for_dns");
|
|
179
|
+
const withMessage = (code) => showMessage ? { code, providerMessage: { text: domain.providerMessage, seenAt: domain.providerMessageAt, source } } : { code };
|
|
180
|
+
const nextCheckAt = options.nextCheckAt ? { nextCheckAt: options.nextCheckAt } : {};
|
|
181
|
+
const required = records.filter((record) => !record.optional);
|
|
182
|
+
if (state === "expired")
|
|
183
|
+
return { state, reason: withMessage("expired"), next: { who: "owner", action: "add_again" } };
|
|
184
|
+
if (state === "live")
|
|
185
|
+
return { state, reason: null, next: { action: "none" } };
|
|
186
|
+
if (state === "failed") {
|
|
187
|
+
const error = domain.provisioningError;
|
|
188
|
+
if (isServiceModeProvisioningError(error)) {
|
|
189
|
+
return { state, reason: withMessage("service_mode"), next: { who: "owner", action: "change_access_mode" } };
|
|
190
|
+
}
|
|
191
|
+
if (error === CUSTOM_DOMAINS_NOT_CONFIGURED_MESSAGE) {
|
|
192
|
+
return { state, reason: withMessage("platform_not_configured"), next: { who: "platform", action: "contact_support" } };
|
|
193
|
+
}
|
|
194
|
+
if (error === CUSTOM_DOMAIN_ROUTE_CONFLICT_MESSAGE) {
|
|
195
|
+
return { state, reason: withMessage("claimed_elsewhere"), next: { who: "platform", action: "contact_support" } };
|
|
196
|
+
}
|
|
197
|
+
if (!error && domain.tlsStatus === "failed") {
|
|
198
|
+
const fix = required.filter((record) => record.ok === false).map(nextRecord);
|
|
199
|
+
return {
|
|
200
|
+
state,
|
|
201
|
+
reason: withMessage("certificate_failed"),
|
|
202
|
+
next: { who: "owner", action: "start_over", ...(fix.length > 0 ? { records: fix } : {}) },
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
if (!error) {
|
|
206
|
+
// Routing failed with nothing recorded to say why. Nothing measured says the owner can
|
|
207
|
+
// fix it, so DNS edits are not prescribed: support is, and Re-check stays available.
|
|
208
|
+
return { state, reason: withMessage("hostname_failed"), next: { who: "platform", action: "contact_support" } };
|
|
209
|
+
}
|
|
210
|
+
// An upstream call threw; the background reconcile retries it, and Re-check retries now.
|
|
211
|
+
return { state, reason: withMessage("provider_error"), next: { who: "platform", action: "recheck" } };
|
|
212
|
+
}
|
|
213
|
+
const www = rootUnsupportedWwwHostname(domain);
|
|
214
|
+
if (www) {
|
|
215
|
+
return { state, reason: withMessage("dns_host_cannot_point_root"), next: { who: "owner", action: "use_www", hostname: www } };
|
|
216
|
+
}
|
|
217
|
+
if (state === "waiting_for_dns" || state === "dns_unverified") {
|
|
218
|
+
const wrong = required.filter((record) => record.ok === false);
|
|
219
|
+
if (wrong.length > 0) {
|
|
220
|
+
// TXT is never "replace": a name may hold several TXT values, so a missing one is added beside them.
|
|
221
|
+
const pointsElsewhere = wrong.some((record) => record.type !== "TXT" && record.check === "different");
|
|
222
|
+
const code = domain.activatedAt
|
|
223
|
+
? "dns_changed_after_live"
|
|
224
|
+
: pointsElsewhere
|
|
225
|
+
? "record_points_elsewhere"
|
|
226
|
+
: "records_missing";
|
|
227
|
+
return {
|
|
228
|
+
state,
|
|
229
|
+
reason: withMessage(code),
|
|
230
|
+
next: { who: "owner", action: pointsElsewhere ? "replace_record" : "add_records", records: wrong.map(nextRecord), ...nextCheckAt },
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
const routingActive = domain.routingStatus === "active";
|
|
234
|
+
const unproven = required.filter((record) => isUnproven(record, routingActive));
|
|
235
|
+
const code = unproven.length > 0 && unproven.every((record) => record.check === "apex_addresses") ? "apex_unverifiable" : "dns_not_checked";
|
|
236
|
+
return {
|
|
237
|
+
state,
|
|
238
|
+
reason: withMessage(code),
|
|
239
|
+
next: { who: "owner", action: "confirm_records", ...(unproven.length > 0 ? { records: unproven.map(nextRecord) } : {}), ...nextCheckAt },
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
// issuing_certificate: every required record was checked and found.
|
|
243
|
+
const caa = domain.dnsCheck?.caa;
|
|
244
|
+
if (caa?.status === "blocks" && caa.recordName) {
|
|
245
|
+
return { state, reason: withMessage("caa_blocks"), next: caaNext({ ...caa, recordName: caa.recordName }) };
|
|
246
|
+
}
|
|
247
|
+
return { state, reason: withMessage("certificate_pending"), next: { who: "platform", action: "wait", ...nextCheckAt } };
|
|
248
|
+
}
|