ninepm 0.72.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 +30 -7
- package/dist/dns-host.d.ts +8 -0
- package/dist/dns-host.js +16 -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 +48 -46
- package/dist/index.js +337 -118
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -62,6 +62,8 @@ Deploy a static directory:
|
|
|
62
62
|
9pm deploy . --name my-app --slug my-app
|
|
63
63
|
```
|
|
64
64
|
|
|
65
|
+
Deploying again under the same slug replaces the live app. To try a change without touching it, deploy under a different slug, which creates a separate app at its own address. On Free, your one app slot can't also hold a separate preview app. Deploying to your existing app updates its live version. Practice mode rehearses the current app's workflow. Testing a changed version separately requires another app slot: a plan with more apps (Hobby allows 3).
|
|
66
|
+
|
|
65
67
|
### How long an app's address stays the same
|
|
66
68
|
|
|
67
69
|
Check this before printing a link or QR code. The live URL is `<slug>-<suffix>.9pm.ai`, and the suffix comes from the app's internal id, not from the slug:
|
|
@@ -223,16 +225,37 @@ cannot be compared from outside, so that row follows the platform's own routing
|
|
|
223
225
|
9pm domains wait my-app www.example.com
|
|
224
226
|
```
|
|
225
227
|
|
|
226
|
-
`wait` exits `0` once the domain is live, `10` if it timed out while
|
|
227
|
-
not be checked,
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
228
|
+
`wait` exits `0` once the domain is live, `10` if it timed out while you have something to do (a
|
|
229
|
+
record is missing or could not be checked, or a CAA record blocks the certificate even though every
|
|
230
|
+
record is found), `11` if it timed out with every record checked and found and nothing left for you
|
|
231
|
+
(the platform is still finishing; wait again), `20` if the domain failed or expired, and `1` on a
|
|
232
|
+
usage, sign-in or API error. Since 0.74.0 a CAA block exits `10`; earlier versions exited `11`. Since 0.77.0 a bare domain whose DNS host cannot point a root
|
|
233
|
+
domain at a hostname (GoDaddy, Route 53, IONOS, Wix) makes `wait` stop at once with `10`: its `next`
|
|
234
|
+
is `use_www` with the `www` hostname to add instead, since no record at the root can finish it.
|
|
235
|
+
`9pm domains status <app> <hostname> --json` prints `{hostname, app, state, reason, next, routing,
|
|
236
|
+
tls, provider, providerId, nameservers, zone, records, advisories, error, expiresAt, checkedAt}`, where `provider` is
|
|
237
|
+
the DNS host (where the nameservers point, not necessarily where the domain was bought), `reason` says why
|
|
238
|
+
(`reason.providerMessage` carries the provider's own words when it reported any, with `source`
|
|
239
|
+
saying whether they are the certificate authority's),
|
|
240
|
+
`next` says who acts next and what to do, and `state` is `live`,
|
|
231
241
|
`waiting_for_dns`, `dns_unverified` (routing is active but the records could not be checked),
|
|
232
242
|
`issuing_certificate`, `failed` or `expired`. Each record's `ok` is `null` when the check cannot
|
|
233
|
-
say, and its `check` says why (`not_checked`, or `apex_addresses`).
|
|
243
|
+
say, and its `check` says why (`not_checked`, or `apex_addresses`). A record whose value was found
|
|
244
|
+
at the name with the domain typed twice (`_acme-challenge.www.example.com.example.com`) has
|
|
245
|
+
`check: "doubled"` and `doubledAt`: enter only the short name shown. `advisories` lists hints that
|
|
246
|
+
the hostname may be a typo, such as a domain that is not in DNS; the domain is added either way.
|
|
247
|
+
Since 0.78.0 each record also carries `typeAtProvider`, `nameAtProvider` (`""` means leave the Host
|
|
248
|
+
field empty) and `valueAtProvider`: the record as a recognised DNS host's editor wants it.
|
|
249
|
+
`typeAtProvider` is `null` for a root record the host cannot hold (GoDaddy, Route 53, IONOS, Wix);
|
|
250
|
+
the text output then suggests connecting `www` instead. `list`, `status` and `wait`
|
|
234
251
|
only read; `add`, `check`, `restart` and `remove` need permission to configure the app.
|
|
235
252
|
|
|
253
|
+
Every `9pm deploy` lists the app's custom domains under its `Deployed` line: `Also serving at
|
|
254
|
+
https://<domain>` for a live one, and for any other its state and the `9pm domains status`
|
|
255
|
+
command to run. If the domains cannot be read within a few seconds, the deploy says so in one
|
|
256
|
+
line instead; the deploy itself is never held back by it. `9pm delete` names the custom domains
|
|
257
|
+
it removed.
|
|
258
|
+
|
|
236
259
|
## Commands
|
|
237
260
|
|
|
238
261
|
```sh
|
|
@@ -259,7 +282,7 @@ only read; `add`, `check`, `restart` and `remove` need permission to configure t
|
|
|
259
282
|
9pm domains status <app> <hostname> [--json]
|
|
260
283
|
9pm domains check <app> <hostname> [--json]
|
|
261
284
|
9pm domains wait <app> <hostname> [--timeout 30m] [--interval 20s] [--json]
|
|
262
|
-
9pm domains restart <app> <hostname>
|
|
285
|
+
9pm domains restart <app> <hostname> # only after a certificate failed; record values may change
|
|
263
286
|
9pm domains remove <app> <hostname> --confirm <hostname>
|
|
264
287
|
9pm access show <site>
|
|
265
288
|
9pm access public <site> [--switch-mode]
|
package/dist/dns-host.d.ts
CHANGED
|
@@ -5,3 +5,11 @@ export declare function guessZoneApex(hostname: string): string;
|
|
|
5
5
|
* `www.tmrwtitans.com` -> `www`, the apex itself -> `@`. A name outside the zone stays whole.
|
|
6
6
|
*/
|
|
7
7
|
export declare function relativeRecordName(recordHost: string, apex: string): string;
|
|
8
|
+
/**
|
|
9
|
+
* The zone a hostname's records go in: the zone the DNS check found by walking up to the first
|
|
10
|
+
* name that answers NS, when it has one, otherwise the guess. The found zone is used only when
|
|
11
|
+
* the hostname is inside it and it is no shorter than the guess: a shorter one (`co.uk`) means
|
|
12
|
+
* the walk climbed past a domain that does not exist, which says nothing about where its
|
|
13
|
+
* records would go.
|
|
14
|
+
*/
|
|
15
|
+
export declare function zoneApexFor(hostname: string, checkedZone?: unknown): string;
|
package/dist/dns-host.js
CHANGED
|
@@ -25,3 +25,19 @@ export function relativeRecordName(recordHost, apex) {
|
|
|
25
25
|
return "@";
|
|
26
26
|
return host.endsWith(`.${apex}`) ? host.slice(0, -(apex.length + 1)) : host;
|
|
27
27
|
}
|
|
28
|
+
/**
|
|
29
|
+
* The zone a hostname's records go in: the zone the DNS check found by walking up to the first
|
|
30
|
+
* name that answers NS, when it has one, otherwise the guess. The found zone is used only when
|
|
31
|
+
* the hostname is inside it and it is no shorter than the guess: a shorter one (`co.uk`) means
|
|
32
|
+
* the walk climbed past a domain that does not exist, which says nothing about where its
|
|
33
|
+
* records would go.
|
|
34
|
+
*/
|
|
35
|
+
export function zoneApexFor(hostname, checkedZone) {
|
|
36
|
+
const guess = guessZoneApex(hostname);
|
|
37
|
+
if (typeof checkedZone !== "string" || checkedZone === "")
|
|
38
|
+
return guess;
|
|
39
|
+
const host = hostname.toLowerCase().replace(/\.$/, "");
|
|
40
|
+
const zone = checkedZone.toLowerCase().replace(/\.$/, "");
|
|
41
|
+
const inside = host === zone || host.endsWith(`.${zone}`);
|
|
42
|
+
return inside && zone.split(".").length >= guess.split(".").length ? zone : guess;
|
|
43
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
export declare const DNS_PROVIDER_SOURCE = "registrars-and-dns-automation.md, 2026-10-09 (Route 53 rechecked 2026-10-13)";
|
|
2
|
+
/** What can stand in for a root-domain record when the host cannot point the root at a hostname. */
|
|
3
|
+
export type ApexFallback =
|
|
4
|
+
/** Connect www.<zone> to the app instead: an ordinary CNAME, always possible. */
|
|
5
|
+
{
|
|
6
|
+
kind: "use_www";
|
|
7
|
+
}
|
|
8
|
+
/** The host's own forwarding of the root to www. Unverified: mentioned as a possibility, never as a step. */
|
|
9
|
+
| {
|
|
10
|
+
kind: "provider_forwarding";
|
|
11
|
+
verified: boolean;
|
|
12
|
+
https: "yes" | "unverified";
|
|
13
|
+
}
|
|
14
|
+
/** Route 53 only: an S3 redirect bucket behind CloudFront, aliased at the root. AWS-documented, not tested by us. */
|
|
15
|
+
| {
|
|
16
|
+
kind: "aws_redirect";
|
|
17
|
+
}
|
|
18
|
+
/** Move DNS hosting to a host that offers ALIAS or flattening, copying every existing record first. */
|
|
19
|
+
| {
|
|
20
|
+
kind: "move_dns_host";
|
|
21
|
+
};
|
|
22
|
+
export type ApexSupport = {
|
|
23
|
+
support: "alias";
|
|
24
|
+
typeLabel: "ALIAS" | "ANAME";
|
|
25
|
+
hostField: "blank" | "@";
|
|
26
|
+
} | {
|
|
27
|
+
support: "cname_flattening";
|
|
28
|
+
typeLabel: "CNAME";
|
|
29
|
+
hostField: "@";
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* `fallbacks` is ordered and always starts with use_www. `hostField` is the root's Host
|
|
33
|
+
* convention where the source states one, used only when routing is already active and the
|
|
34
|
+
* root row is shown after all; absent means `@`.
|
|
35
|
+
*/
|
|
36
|
+
| {
|
|
37
|
+
support: "none";
|
|
38
|
+
fallbacks: ApexFallback[];
|
|
39
|
+
hostField?: "blank";
|
|
40
|
+
};
|
|
41
|
+
/** A note shown under the "your DNS is at" line. Only verified notes are rendered. */
|
|
42
|
+
export interface DnsProviderNote {
|
|
43
|
+
id: string;
|
|
44
|
+
verified: boolean;
|
|
45
|
+
}
|
|
46
|
+
export interface DnsProviderProfile {
|
|
47
|
+
apex: ApexSupport;
|
|
48
|
+
/**
|
|
49
|
+
* Whether the editor appends the zone to the name typed. True: the full name is never
|
|
50
|
+
* offered, since typing it creates `www.example.com.example.com`. "unknown" keeps showing it.
|
|
51
|
+
*/
|
|
52
|
+
appendsZone: true | false | "unknown";
|
|
53
|
+
/** The editor makes a hostname value relative unless it ends in a dot (Gandi). */
|
|
54
|
+
valueNeedsTrailingDot?: true;
|
|
55
|
+
notes: DnsProviderNote[];
|
|
56
|
+
source: string;
|
|
57
|
+
}
|
|
58
|
+
export interface DnsProvider {
|
|
59
|
+
/** Stable key, sent as `dnsCheck.providerId`; the display name may change. */
|
|
60
|
+
id: string;
|
|
61
|
+
/** What `dnsCheck.provider` says. */
|
|
62
|
+
name: string;
|
|
63
|
+
nameservers: RegExp[];
|
|
64
|
+
/** Absent for a host nobody has researched: it is named, and gets the generic instructions. */
|
|
65
|
+
profile?: DnsProviderProfile;
|
|
66
|
+
}
|
|
67
|
+
export declare const DNS_PROVIDERS: readonly DnsProvider[];
|
|
68
|
+
/** The DNS host a zone's nameservers belong to, or null for one not in the table. Entries that are not names are skipped. */
|
|
69
|
+
export declare function dnsProviderForNameservers(nameservers: readonly unknown[]): DnsProvider | null;
|
|
70
|
+
/** The profile for a `dnsCheck.providerId`; undefined for an unknown id or a host with no profile. */
|
|
71
|
+
export declare function dnsProviderProfile(providerId: unknown): DnsProviderProfile | undefined;
|
|
72
|
+
/** How one routing or TXT record is entered at a given host. */
|
|
73
|
+
export interface RecordAtProvider {
|
|
74
|
+
/** The record type the host's editor calls it; null when the host cannot hold it at all (a root record at a no-ALIAS host). */
|
|
75
|
+
type: string | null;
|
|
76
|
+
/** The name to type; "" means leave the Host field empty. */
|
|
77
|
+
name: string;
|
|
78
|
+
/** The value to type, with a trailing dot where the editor needs one. */
|
|
79
|
+
value: string;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The record as the host's editor wants it. `apex` is whether this is the routing record at
|
|
83
|
+
* the zone root; `name` is the relative name (`@` at the root). Without a profile this is the
|
|
84
|
+
* record unchanged, with the root's type named the way every host that can hold it does.
|
|
85
|
+
*/
|
|
86
|
+
export declare function recordAtProvider(record: {
|
|
87
|
+
type: string;
|
|
88
|
+
name: string;
|
|
89
|
+
value: string;
|
|
90
|
+
apex: boolean;
|
|
91
|
+
}, profile: DnsProviderProfile | undefined): RecordAtProvider;
|
|
@@ -0,0 +1,194 @@
|
|
|
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
|
+
// DNS hosts: how each is recognised from the nameservers a custom domain's zone answers with,
|
|
4
|
+
// and, for the ones researched, how its record editor wants the records we ask for. Detection
|
|
5
|
+
// and profiles live in one table so a host can never be named without the rules that follow
|
|
6
|
+
// from it. Read by the control plane's DNS check, the dashboard and `9pm domains`, and copied
|
|
7
|
+
// into the CLI tarball by scripts/generate-cli-package.mjs, so it must import nothing.
|
|
8
|
+
//
|
|
9
|
+
// Every rule is a property of the DNS HOST (where the nameservers point), never the registrar:
|
|
10
|
+
// records only take effect at the host, whoever sold the domain.
|
|
11
|
+
//
|
|
12
|
+
// Facts come from the BYOD walkthrough's research note (registrars-and-dns-automation.md,
|
|
13
|
+
// researched 2026-10-09 from each provider's help center; the Route 53 row rechecked against
|
|
14
|
+
// AWS's docs on 2026-10-13). That note is not in this repository, so each row cites the
|
|
15
|
+
// provider's own page it relied on, and keeps the note's verified/unverified marks. A fact the note marks unverified or
|
|
16
|
+
// inferred is used only where being wrong is harmless: it never removes information (an
|
|
17
|
+
// "unknown" appendsZone keeps the full-name line) and never becomes a button or a step.
|
|
18
|
+
// Re-check the source before widening any row.
|
|
19
|
+
export const DNS_PROVIDER_SOURCE = "registrars-and-dns-automation.md, 2026-10-09 (Route 53 rechecked 2026-10-13)";
|
|
20
|
+
// Order matters only where two patterns could match one nameserver; none do today.
|
|
21
|
+
export const DNS_PROVIDERS = [
|
|
22
|
+
{
|
|
23
|
+
id: "namecheap",
|
|
24
|
+
name: "Namecheap",
|
|
25
|
+
nameservers: [/(^|\.)registrar-servers\.com$/],
|
|
26
|
+
// Documented: "ALIAS Record", host @; "enter your subdomain just like mysubdomain and not like mysubdomain.domain.tld".
|
|
27
|
+
// https://www.namecheap.com/support/knowledgebase/article.aspx/10128/2237/how-to-create-an-alias-record
|
|
28
|
+
// https://www.namecheap.com/support/knowledgebase/article.aspx/9646/2237/how-to-create-a-cname-record-for-your-domain
|
|
29
|
+
profile: { apex: { support: "alias", typeLabel: "ALIAS", hostField: "@" }, appendsZone: true, notes: [], source: DNS_PROVIDER_SOURCE },
|
|
30
|
+
},
|
|
31
|
+
{
|
|
32
|
+
id: "godaddy",
|
|
33
|
+
name: "GoDaddy",
|
|
34
|
+
nameservers: [/(^|\.)domaincontrol\.com$/],
|
|
35
|
+
// No ALIAS/ANAME; a CNAME Name "cannot be the @ symbol". Forwarding exists (GoDaddy's own
|
|
36
|
+
// pages); whether it answers over HTTPS is unverified. appendsZone is inferred from the
|
|
37
|
+
// Name rules, so true is used only to hide the full-name line, which GoDaddy would double.
|
|
38
|
+
// https://www.godaddy.com/en/help/add-a-cname-record-19236
|
|
39
|
+
// https://www.godaddy.com/resources/news/beyond-cname-flattening
|
|
40
|
+
profile: {
|
|
41
|
+
apex: { support: "none", fallbacks: [{ kind: "use_www" }, { kind: "provider_forwarding", verified: true, https: "unverified" }] },
|
|
42
|
+
appendsZone: true,
|
|
43
|
+
notes: [],
|
|
44
|
+
source: DNS_PROVIDER_SOURCE,
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
id: "cloudflare",
|
|
49
|
+
name: "Cloudflare",
|
|
50
|
+
nameservers: [/(^|\.)ns\.cloudflare\.com$/],
|
|
51
|
+
// A CNAME at the root is flattened. Both name forms are accepted (inferred), so the full
|
|
52
|
+
// name stays visible. No DCV advice until #1990 settles the _acme-challenge question.
|
|
53
|
+
// https://developers.cloudflare.com/dns/cname-flattening/
|
|
54
|
+
// https://developers.cloudflare.com/dns/manage-dns-records/how-to/create-zone-apex/
|
|
55
|
+
profile: { apex: { support: "cname_flattening", typeLabel: "CNAME", hostField: "@" }, appendsZone: "unknown", notes: [], source: DNS_PROVIDER_SOURCE },
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: "route53",
|
|
59
|
+
name: "Amazon Route 53",
|
|
60
|
+
nameservers: [/awsdns-\d+\./],
|
|
61
|
+
// ALIAS targets only AWS resources or same-zone records, and Route 53 has no forwarding
|
|
62
|
+
// service: so www, a redirect the owner runs inside AWS, or another DNS host. The root's
|
|
63
|
+
// record name is left blank; "@" outside a zone-file import creates a literal label.
|
|
64
|
+
// https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/resource-record-sets-choosing-alias-non-alias.html
|
|
65
|
+
// https://repost.aws/questions/QUt4eylJxXQ5On1SEzH09USQ/route-53-records
|
|
66
|
+
profile: {
|
|
67
|
+
apex: { support: "none", fallbacks: [{ kind: "use_www" }, { kind: "aws_redirect" }, { kind: "move_dns_host" }], hostField: "blank" },
|
|
68
|
+
appendsZone: "unknown",
|
|
69
|
+
notes: [{ id: "route53TxtQuotes", verified: false }],
|
|
70
|
+
source: DNS_PROVIDER_SOURCE,
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
// `googledomains.com` may be Squarespace Domains rather than Google Cloud DNS (unverified), so no profile.
|
|
74
|
+
{ id: "google-cloud-dns", name: "Google Cloud DNS", nameservers: [/(^|\.)googledomains\.com$/] },
|
|
75
|
+
{ id: "squarespace", name: "Squarespace", nameservers: [/(^|\.)squarespacedns\.com$/] },
|
|
76
|
+
{
|
|
77
|
+
id: "porkbun",
|
|
78
|
+
name: "Porkbun",
|
|
79
|
+
nameservers: [/(^|\.)porkbun\.com$/],
|
|
80
|
+
// Documented: host blank for the root, "enter only the subdomain".
|
|
81
|
+
// https://kb.porkbun.com/article/231-how-to-add-dns-records-on-porkbun
|
|
82
|
+
profile: { apex: { support: "alias", typeLabel: "ALIAS", hostField: "blank" }, appendsZone: true, notes: [], source: DNS_PROVIDER_SOURCE },
|
|
83
|
+
},
|
|
84
|
+
{
|
|
85
|
+
id: "namecom",
|
|
86
|
+
name: "Name.com",
|
|
87
|
+
nameservers: [/(^|\.)name\.com$/],
|
|
88
|
+
// https://www.name.com/support/articles/115010493967-adding-an-aname-alias-record
|
|
89
|
+
profile: { apex: { support: "alias", typeLabel: "ANAME", hostField: "blank" }, appendsZone: true, notes: [], source: DNS_PROVIDER_SOURCE },
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
id: "gandi",
|
|
93
|
+
name: "Gandi",
|
|
94
|
+
nameservers: [/(^|\.)gandi\.net$/],
|
|
95
|
+
// LiveDNS only; the value needs a trailing dot or the zone is appended to it; no A/AAAA may
|
|
96
|
+
// stay at @ beside the ALIAS.
|
|
97
|
+
// https://docs.gandi.net/en/domain_names/faq/record_types/alias_record.html
|
|
98
|
+
profile: {
|
|
99
|
+
apex: { support: "alias", typeLabel: "ALIAS", hostField: "@" },
|
|
100
|
+
appendsZone: true,
|
|
101
|
+
valueNeedsTrailingDot: true,
|
|
102
|
+
notes: [{ id: "gandiLiveDns", verified: true }],
|
|
103
|
+
source: DNS_PROVIDER_SOURCE,
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
{ id: "ovhcloud", name: "OVHcloud", nameservers: [/(^|\.)ovh\.net$/] },
|
|
107
|
+
{
|
|
108
|
+
id: "ionos",
|
|
109
|
+
name: "IONOS",
|
|
110
|
+
nameservers: [/(^|\.)ui-dns\.(com|de|org|biz)$/],
|
|
111
|
+
// "CNAME only on subdomains" is IONOS's own; "only HTTP forwarding" for the root is one
|
|
112
|
+
// integrator's report, so forwarding is unverified and rendered as a possibility only.
|
|
113
|
+
// https://www.ionos.com/help/domains/configuring-cname-records-for-subdomains/configuring-a-cname-record-for-a-subdomain/
|
|
114
|
+
profile: {
|
|
115
|
+
apex: { support: "none", fallbacks: [{ kind: "use_www" }, { kind: "provider_forwarding", verified: false, https: "unverified" }] },
|
|
116
|
+
appendsZone: true,
|
|
117
|
+
notes: [],
|
|
118
|
+
source: DNS_PROVIDER_SOURCE,
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
id: "hostinger",
|
|
123
|
+
name: "Hostinger",
|
|
124
|
+
nameservers: [/(^|\.)dns-parking\.com$/],
|
|
125
|
+
// A CNAME at @ is shown as ALIAS, one per zone; "@ is appended by default".
|
|
126
|
+
// https://www.hostinger.com/support/10085192-cname-vs-alias-records-at-hostinger/
|
|
127
|
+
profile: {
|
|
128
|
+
apex: { support: "alias", typeLabel: "ALIAS", hostField: "@" },
|
|
129
|
+
appendsZone: true,
|
|
130
|
+
notes: [{ id: "hostingerCnameAtRoot", verified: true }],
|
|
131
|
+
source: DNS_PROVIDER_SOURCE,
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
{ id: "hover", name: "Hover", nameservers: [/(^|\.)hover\.com$/] },
|
|
135
|
+
{
|
|
136
|
+
id: "wix",
|
|
137
|
+
name: "Wix",
|
|
138
|
+
nameservers: [/(^|\.)wixdns\.net$/],
|
|
139
|
+
// No ALIAS found and no forwarding researched, so no forwarding is offered. Wix's own
|
|
140
|
+
// instruction is to leave the Host blank wherever another one says "@".
|
|
141
|
+
// https://support.wix.com/en/article/adding-or-updating-cname-records-in-your-wix-account
|
|
142
|
+
profile: {
|
|
143
|
+
apex: { support: "none", fallbacks: [{ kind: "use_www" }, { kind: "move_dns_host" }], hostField: "blank" },
|
|
144
|
+
appendsZone: true,
|
|
145
|
+
notes: [],
|
|
146
|
+
source: DNS_PROVIDER_SOURCE,
|
|
147
|
+
},
|
|
148
|
+
},
|
|
149
|
+
{ id: "vercel", name: "Vercel", nameservers: [/(^|\.)vercel-dns\.com$/] },
|
|
150
|
+
{ id: "ns1", name: "NS1", nameservers: [/(^|\.)nsone\.net$/] },
|
|
151
|
+
{ id: "digitalocean", name: "DigitalOcean", nameservers: [/(^|\.)digitalocean\.com$/] },
|
|
152
|
+
{ id: "azure", name: "Azure DNS", nameservers: [/azure-dns\./] },
|
|
153
|
+
{ id: "hetzner", name: "Hetzner", nameservers: [/(^|\.)hetzner\.(com|de)$/] },
|
|
154
|
+
// ALIAS support is second-hand and the host convention unknown, so no profile yet.
|
|
155
|
+
{ id: "dnsimple", name: "DNSimple", nameservers: [/(^|\.)dnsimple\.com$/] },
|
|
156
|
+
{ id: "netlify", name: "Netlify", nameservers: [/(^|\.)netlify\.com$/] },
|
|
157
|
+
];
|
|
158
|
+
/** The DNS host a zone's nameservers belong to, or null for one not in the table. Entries that are not names are skipped. */
|
|
159
|
+
export function dnsProviderForNameservers(nameservers) {
|
|
160
|
+
if (!Array.isArray(nameservers))
|
|
161
|
+
return null;
|
|
162
|
+
for (const ns of nameservers) {
|
|
163
|
+
if (typeof ns !== "string")
|
|
164
|
+
continue;
|
|
165
|
+
const name = ns.toLowerCase().replace(/\.$/, "");
|
|
166
|
+
const hit = DNS_PROVIDERS.find((provider) => provider.nameservers.some((pattern) => pattern.test(name)));
|
|
167
|
+
if (hit)
|
|
168
|
+
return hit;
|
|
169
|
+
}
|
|
170
|
+
return null;
|
|
171
|
+
}
|
|
172
|
+
/** The profile for a `dnsCheck.providerId`; undefined for an unknown id or a host with no profile. */
|
|
173
|
+
export function dnsProviderProfile(providerId) {
|
|
174
|
+
if (typeof providerId !== "string" || providerId === "")
|
|
175
|
+
return undefined;
|
|
176
|
+
return DNS_PROVIDERS.find((provider) => provider.id === providerId)?.profile;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* The record as the host's editor wants it. `apex` is whether this is the routing record at
|
|
180
|
+
* the zone root; `name` is the relative name (`@` at the root). Without a profile this is the
|
|
181
|
+
* record unchanged, with the root's type named the way every host that can hold it does.
|
|
182
|
+
*/
|
|
183
|
+
export function recordAtProvider(record, profile) {
|
|
184
|
+
const type = String(record.type).toUpperCase();
|
|
185
|
+
const value = profile?.valueNeedsTrailingDot && typeof record.value === "string" && type === "CNAME" && !record.value.endsWith(".") ? `${record.value}.` : record.value;
|
|
186
|
+
if (!record.apex)
|
|
187
|
+
return { type, name: record.name, value };
|
|
188
|
+
if (!profile)
|
|
189
|
+
return { type: "ALIAS", name: record.name, value };
|
|
190
|
+
const apex = profile.apex;
|
|
191
|
+
if (apex.support === "none")
|
|
192
|
+
return { type: null, name: apex.hostField === "blank" ? "" : "@", value };
|
|
193
|
+
return { type: apex.typeLabel, name: apex.hostField === "blank" ? "" : "@", value };
|
|
194
|
+
}
|
|
@@ -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;
|