ninepm 0.78.0 → 0.79.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 +11 -1
- package/dist/domain-state.d.ts +39 -10
- package/dist/domain-state.js +49 -4
- package/dist/index.d.ts +2 -1
- package/dist/index.js +53 -6
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -242,7 +242,17 @@ saying whether they are the certificate authority's),
|
|
|
242
242
|
`issuing_certificate`, `failed` or `expired`. Each record's `ok` is `null` when the check cannot
|
|
243
243
|
say, and its `check` says why (`not_checked`, or `apex_addresses`). A record whose value was found
|
|
244
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.
|
|
245
|
+
`check: "doubled"` and `doubledAt`: enter only the short name shown. Since 0.79.0, against a server
|
|
246
|
+
that sends them: a record answered by a wildcard record of the DNS host (`*.example.com`) rather
|
|
247
|
+
than one of its own has `check: "wildcard"` and `wildcard`, and the row says to add a record at that
|
|
248
|
+
name (the wildcard can stay; a TXT value is always added beside the values already there);
|
|
249
|
+
`resolvers` lists what each public resolver answered when two were asked, and when one shows the
|
|
250
|
+
expected value and another does not, the record is `check: "resolvers_disagree"` with `ok: null`:
|
|
251
|
+
neither cache proves which answer is current, so confirm the record at your DNS host and check again.
|
|
252
|
+
A record that does not match carries `ttlSeconds`, how long that answer may stay cached. A root
|
|
253
|
+
answering with addresses while routing is pending asks you to confirm the ALIAS or ANAME target,
|
|
254
|
+
since a parking page answers with addresses too. With every record found, the text names the
|
|
255
|
+
platform's next automatic check from `next.nextCheckAt`. `advisories` lists hints that
|
|
246
256
|
the hostname may be a typo, such as a domain that is not in DNS; the domain is added either way.
|
|
247
257
|
Since 0.78.0 each record also carries `typeAtProvider`, `nameAtProvider` (`""` means leave the Host
|
|
248
258
|
field empty) and `valueAtProvider`: the record as a recognised DNS host's editor wants it.
|
package/dist/domain-state.d.ts
CHANGED
|
@@ -29,16 +29,32 @@ export interface DomainStateDnsCheck {
|
|
|
29
29
|
nameservers: string[];
|
|
30
30
|
/** The zone the records go in, as the check found it. Absent from older servers and when no name answered NS. */
|
|
31
31
|
zone?: string;
|
|
32
|
-
records:
|
|
33
|
-
type: string;
|
|
34
|
-
host: string;
|
|
35
|
-
expected: string;
|
|
36
|
-
current: string[];
|
|
37
|
-
matches: boolean;
|
|
38
|
-
doubledAt?: string;
|
|
39
|
-
}>;
|
|
32
|
+
records: DomainStateDnsRecord[];
|
|
40
33
|
caa?: DomainStateCaaCheck;
|
|
41
34
|
}
|
|
35
|
+
/** What one public resolver answered for a record (#1991). */
|
|
36
|
+
export interface DomainStateResolverAnswer {
|
|
37
|
+
resolver: string;
|
|
38
|
+
current: string[];
|
|
39
|
+
matches: boolean;
|
|
40
|
+
/** Seconds it may go on serving this answer from its cache. */
|
|
41
|
+
ttlSeconds?: number;
|
|
42
|
+
}
|
|
43
|
+
export interface DomainStateDnsRecord {
|
|
44
|
+
type: string;
|
|
45
|
+
host: string;
|
|
46
|
+
expected: string;
|
|
47
|
+
current: string[];
|
|
48
|
+
matches: boolean;
|
|
49
|
+
/** The value is published at `<host>.<zone>` instead, the provider having added the zone twice. */
|
|
50
|
+
doubledAt?: string;
|
|
51
|
+
/** The answer is a wildcard's (`*.<parent>`): the name has no record of its own. */
|
|
52
|
+
wildcard?: string;
|
|
53
|
+
/** Seconds `current` may still be served from a resolver's cache; set only when it does not match. */
|
|
54
|
+
ttlSeconds?: number;
|
|
55
|
+
/** Each public resolver's answer, when a second one was asked. `matches` above is the first resolver's, so a disagreement is never a match. */
|
|
56
|
+
resolvers?: DomainStateResolverAnswer[];
|
|
57
|
+
}
|
|
42
58
|
/** What the derivation reads: a domain as any of the domain endpoints returns it. */
|
|
43
59
|
export interface DomainStateInput {
|
|
44
60
|
hostname: string;
|
|
@@ -53,7 +69,7 @@ export interface DomainStateInput {
|
|
|
53
69
|
providerMessageSource?: string | null;
|
|
54
70
|
}
|
|
55
71
|
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";
|
|
72
|
+
export type DomainRecordCheck = "found" | "missing" | "different" | "doubled" | "wildcard" | "resolvers_disagree" | "not_checked" | "apex_addresses";
|
|
57
73
|
export interface DomainRecordStatus {
|
|
58
74
|
type: string;
|
|
59
75
|
name: string;
|
|
@@ -63,10 +79,23 @@ export interface DomainRecordStatus {
|
|
|
63
79
|
ok: boolean | null;
|
|
64
80
|
check: DomainRecordCheck;
|
|
65
81
|
doubledAt?: string;
|
|
82
|
+
wildcard?: string;
|
|
83
|
+
ttlSeconds?: number;
|
|
84
|
+
resolvers?: DomainStateResolverAnswer[];
|
|
66
85
|
apex: boolean;
|
|
67
86
|
optional: boolean;
|
|
68
87
|
}
|
|
69
88
|
export declare function domainRecordStatuses(domain: DomainStateInput): DomainRecordStatus[];
|
|
89
|
+
export declare function domainRecordCheck(seen: DomainStateDnsRecord | undefined, atApex: boolean): DomainRecordCheck;
|
|
90
|
+
/**
|
|
91
|
+
* Each resolver's answer as the server reported it, with malformed entries dropped, so a bad entry
|
|
92
|
+
* neither breaks a row nor counts towards a disagreement.
|
|
93
|
+
*/
|
|
94
|
+
export declare function resolverAnswers(seen: DomainStateDnsRecord | undefined): DomainStateResolverAnswer[];
|
|
95
|
+
/** Whether one resolver shows the expected value while another shows something else. */
|
|
96
|
+
export declare function resolversDisagree(seen: DomainStateDnsRecord | undefined): boolean;
|
|
97
|
+
/** A cache's time left in whole minutes, at least one: "up to N min". */
|
|
98
|
+
export declare function cacheMinutes(ttlSeconds: number): number;
|
|
70
99
|
/**
|
|
71
100
|
* The `www.<domain>` to connect instead, when this hostname is a bare domain whose DNS host
|
|
72
101
|
* cannot point a root at a hostname at all (#1996): GoDaddy, Route 53, IONOS, Wix. Read only
|
|
@@ -85,7 +114,7 @@ export declare function showsUseWww(next: {
|
|
|
85
114
|
action?: string;
|
|
86
115
|
} | null | undefined): boolean;
|
|
87
116
|
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";
|
|
117
|
+
export type DomainReasonCode = "records_missing" | "record_points_elsewhere" | "dns_changed_after_live" | "dns_not_checked" | "apex_unverifiable" | "dns_resolvers_disagree" | "dns_host_cannot_point_root" | "caa_blocks" | "certificate_pending" | "certificate_failed" | "hostname_failed" | "service_mode" | "claimed_elsewhere" | "platform_not_configured" | "provider_error" | "expired";
|
|
89
118
|
export interface DomainReason {
|
|
90
119
|
code: DomainReasonCode;
|
|
91
120
|
/**
|
package/dist/domain-state.js
CHANGED
|
@@ -49,15 +49,20 @@ export function domainRecordStatuses(domain) {
|
|
|
49
49
|
const seen = domain.dnsCheck?.records.find((entry) => entry.type.toUpperCase() === type && normalizeDnsName(entry.host) === fqdn && entry.expected === expected);
|
|
50
50
|
const atApex = type === "CNAME" && fqdn === apex;
|
|
51
51
|
const check = domainRecordCheck(seen, atApex);
|
|
52
|
+
const resolvers = resolverAnswers(seen);
|
|
53
|
+
const ttl = seen?.ttlSeconds;
|
|
52
54
|
return {
|
|
53
55
|
type,
|
|
54
56
|
name: relativeRecordName(fqdn, apex),
|
|
55
57
|
fqdn,
|
|
56
58
|
expected: instruction.value,
|
|
57
59
|
current: seen ? seen.current : null,
|
|
58
|
-
ok: check === "found" ? true : check === "missing" || check === "different" || check === "doubled" ? false : null,
|
|
60
|
+
ok: check === "found" ? true : check === "missing" || check === "different" || check === "doubled" || check === "wildcard" ? false : null,
|
|
59
61
|
check,
|
|
60
62
|
...(check === "doubled" && seen?.doubledAt ? { doubledAt: seen.doubledAt } : {}),
|
|
63
|
+
...(check === "wildcard" && seen?.wildcard ? { wildcard: seen.wildcard } : {}),
|
|
64
|
+
...(check !== "found" && typeof ttl === "number" && Number.isFinite(ttl) && ttl >= 0 ? { ttlSeconds: ttl } : {}),
|
|
65
|
+
...(resolvers.length > 0 ? { resolvers } : {}),
|
|
61
66
|
apex: atApex,
|
|
62
67
|
optional: instruction.optional === true,
|
|
63
68
|
};
|
|
@@ -67,10 +72,14 @@ const IP_ADDRESS = /^(\d{1,3}(\.\d{1,3}){3}|[0-9a-f:]*:[0-9a-f:.]+)$/i;
|
|
|
67
72
|
// The server looks a CNAME up and, finding none, falls back to the name's A records. At the
|
|
68
73
|
// zone root that is exactly what a correct ALIAS/ANAME or flattened CNAME answers: addresses,
|
|
69
74
|
// 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
|
-
|
|
75
|
+
// cannot be compared from outside at all. Exported so a client that renders records without a
|
|
76
|
+
// full status (the dashboard's table) reads the same verdict per row.
|
|
77
|
+
export function domainRecordCheck(seen, atApex) {
|
|
72
78
|
if (!seen)
|
|
73
79
|
return "not_checked";
|
|
80
|
+
// Ahead of `matches`, so no shape of answer turns a disagreement between caches into "found".
|
|
81
|
+
if (resolversDisagree(seen))
|
|
82
|
+
return "resolvers_disagree";
|
|
74
83
|
if (seen.matches)
|
|
75
84
|
return "found";
|
|
76
85
|
if (seen.doubledAt)
|
|
@@ -79,8 +88,37 @@ function domainRecordCheck(seen, atApex) {
|
|
|
79
88
|
return "missing";
|
|
80
89
|
if (atApex && seen.current.every((value) => IP_ADDRESS.test(value.trim())))
|
|
81
90
|
return "apex_addresses";
|
|
91
|
+
if (typeof seen.wildcard === "string" && seen.wildcard !== "")
|
|
92
|
+
return "wildcard";
|
|
82
93
|
return "different";
|
|
83
94
|
}
|
|
95
|
+
/**
|
|
96
|
+
* Each resolver's answer as the server reported it, with malformed entries dropped, so a bad entry
|
|
97
|
+
* neither breaks a row nor counts towards a disagreement.
|
|
98
|
+
*/
|
|
99
|
+
export function resolverAnswers(seen) {
|
|
100
|
+
if (!seen || !Array.isArray(seen.resolvers))
|
|
101
|
+
return [];
|
|
102
|
+
return seen.resolvers.flatMap((answer) => {
|
|
103
|
+
if (answer === null || typeof answer !== "object")
|
|
104
|
+
return [];
|
|
105
|
+
const entry = answer;
|
|
106
|
+
if (typeof entry.resolver !== "string" || typeof entry.matches !== "boolean" || !Array.isArray(entry.current))
|
|
107
|
+
return [];
|
|
108
|
+
const current = entry.current.filter((value) => typeof value === "string");
|
|
109
|
+
const ttl = entry.ttlSeconds;
|
|
110
|
+
return [{ resolver: entry.resolver, current, matches: entry.matches, ...(typeof ttl === "number" && Number.isFinite(ttl) && ttl >= 0 ? { ttlSeconds: ttl } : {}) }];
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
/** Whether one resolver shows the expected value while another shows something else. */
|
|
114
|
+
export function resolversDisagree(seen) {
|
|
115
|
+
const answers = resolverAnswers(seen);
|
|
116
|
+
return answers.some((answer) => answer.matches) && answers.some((answer) => !answer.matches);
|
|
117
|
+
}
|
|
118
|
+
/** A cache's time left in whole minutes, at least one: "up to N min". */
|
|
119
|
+
export function cacheMinutes(ttlSeconds) {
|
|
120
|
+
return Math.max(1, Math.ceil(ttlSeconds / 60));
|
|
121
|
+
}
|
|
84
122
|
// Unproven is decided by `ok`, not by listing check values: a check this code does not know yet
|
|
85
123
|
// carries ok null and so reads as unknown, never as found. The one exception is an apex
|
|
86
124
|
// answering with addresses, which the platform's own active routing vouches for.
|
|
@@ -232,7 +270,14 @@ export function deriveDomainStatus(domain, options = {}) {
|
|
|
232
270
|
}
|
|
233
271
|
const routingActive = domain.routingStatus === "active";
|
|
234
272
|
const unproven = required.filter((record) => isUnproven(record, routingActive));
|
|
235
|
-
|
|
273
|
+
// A disagreement is named only when every unproven record is one, or an apex that cannot be
|
|
274
|
+
// compared from outside: a record nobody could read still makes it "not checked".
|
|
275
|
+
const explained = unproven.length > 0 && unproven.every((record) => record.check === "apex_addresses" || record.check === "resolvers_disagree");
|
|
276
|
+
const code = !explained
|
|
277
|
+
? "dns_not_checked"
|
|
278
|
+
: unproven.some((record) => record.check === "resolvers_disagree")
|
|
279
|
+
? "dns_resolvers_disagree"
|
|
280
|
+
: "apex_unverifiable";
|
|
236
281
|
return {
|
|
237
282
|
state,
|
|
238
283
|
reason: withMessage(code),
|
package/dist/index.d.ts
CHANGED
|
@@ -127,7 +127,7 @@ export interface CreatedServiceToken {
|
|
|
127
127
|
clientSecret: string;
|
|
128
128
|
}
|
|
129
129
|
export type FetchLike = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
|
130
|
-
export declare const CLI_VERSION = "0.
|
|
130
|
+
export declare const CLI_VERSION = "0.79.0";
|
|
131
131
|
type CredentialBackend = "macos-keychain" | "windows-dpapi" | "linux-secret-service" | "file-plaintext" | "none";
|
|
132
132
|
export interface StoredCredentialEntry {
|
|
133
133
|
token: string;
|
|
@@ -789,6 +789,7 @@ export declare const DOMAIN_WAIT_EXIT: {
|
|
|
789
789
|
export declare function domainStatusJson(domain: CliDomain, app: string, checkedAt: string): DomainStatusJson;
|
|
790
790
|
export declare function domainStateLabel(state: string): string;
|
|
791
791
|
export declare function domainWaitExitCode(status: Pick<DomainStatusJson, "state" | "next">): number;
|
|
792
|
+
export declare function nextCheckText(nextCheckAt: unknown): string | null;
|
|
792
793
|
export interface DomainFormatOptions {
|
|
793
794
|
owner?: string;
|
|
794
795
|
target?: AccessApiTarget;
|
package/dist/index.js
CHANGED
|
@@ -15,7 +15,7 @@ import { managedHostnamesIn, siblingCallWarning, siblingsOf } from "./sibling-ho
|
|
|
15
15
|
import { MAX_STATIC_ASSET_FILE_BYTES, MAX_WORKER_ENTRY_COMPRESSED_BYTES, MAX_WORKER_SCRIPT_COMPRESSED_BYTES, WORKER_ENTRY_COMPRESSED_WARN_BYTES, assetFileTooLargeMessage, formatMebibytes, formatMegabytes, oversizedAssets, workerBundleNearLimitMessage, workerBundleTooLargeMessage, workerEntryNearLimitMessage, workerEntryTooLargeMessage, } from "./deploy-limits.js";
|
|
16
16
|
import { guessZoneApex, relativeRecordName, zoneApexFor } from "./dns-host.js";
|
|
17
17
|
import { dnsProviderProfile, recordAtProvider } from "./dns-providers.js";
|
|
18
|
-
import { deriveDomainStatus, domainRecordStatuses, showsUseWww, } from "./domain-state.js";
|
|
18
|
+
import { cacheMinutes, deriveDomainStatus, domainRecordStatuses, showsUseWww, } from "./domain-state.js";
|
|
19
19
|
import { buildFileManifest, readManifestFile } from "./manifest.js";
|
|
20
20
|
import { NO_PUBLIC_ASSETS, normalizeAssetsDirectory, privateLookingPaths, publicAssetPath } from "./public-assets.js";
|
|
21
21
|
import { API_KEY_HELP, API_URL_HELP, CLI_CLIENT_ID, DEFAULT_API_URL, reportCliError } from "./cli-config.js";
|
|
@@ -52,7 +52,7 @@ export function deployDatabaseClause(databases) {
|
|
|
52
52
|
const listed = databases.map((database) => (database.name ? `${database.bindingName}:${database.name}` : database.bindingName)).join(", ");
|
|
53
53
|
return `${databases.length === 1 ? "database" : "databases"} ${listed}`;
|
|
54
54
|
}
|
|
55
|
-
export const CLI_VERSION = "0.
|
|
55
|
+
export const CLI_VERSION = "0.79.0";
|
|
56
56
|
const PUBLIC_DASHBOARD_URL = "https://9pm.ai/dashboard";
|
|
57
57
|
const TOKEN_SERVICE = "9pm";
|
|
58
58
|
const TOKEN_ACCOUNT = "default";
|
|
@@ -3328,9 +3328,33 @@ function providerMessageLine(reason) {
|
|
|
3328
3328
|
return `${who} reported (${message.seenAt}): ${message.text}`;
|
|
3329
3329
|
}
|
|
3330
3330
|
const TXT_KEEP_OTHERS = "add this value as its own TXT record; other TXT values can stay";
|
|
3331
|
+
// The server's next background check as "HH:MM UTC" (UTC so the line reads the same on any
|
|
3332
|
+
// machine); null when there is none or it is not a date, so no time is promised from it.
|
|
3333
|
+
export function nextCheckText(nextCheckAt) {
|
|
3334
|
+
if (typeof nextCheckAt !== "string" || nextCheckAt === "")
|
|
3335
|
+
return null;
|
|
3336
|
+
const at = new Date(nextCheckAt);
|
|
3337
|
+
return Number.isNaN(at.getTime()) ? null : `${at.toISOString().slice(11, 16)} UTC`;
|
|
3338
|
+
}
|
|
3339
|
+
// "up to N min" for a cache's time left, or nothing when the resolver did not say.
|
|
3340
|
+
function forUpTo(ttlSeconds) {
|
|
3341
|
+
return typeof ttlSeconds === "number" ? ` for up to ${cacheMinutes(ttlSeconds)} min` : "";
|
|
3342
|
+
}
|
|
3343
|
+
function answerText(current) {
|
|
3344
|
+
return current.length > 0 ? current.join(", ") : "nothing";
|
|
3345
|
+
}
|
|
3331
3346
|
function recordStateText(record, routing) {
|
|
3332
3347
|
if (record.ok === true)
|
|
3333
3348
|
return "found";
|
|
3349
|
+
if (record.check === "resolvers_disagree") {
|
|
3350
|
+
// Two caches that disagree prove neither answer current: no "found", no "replace", and neither answer called old.
|
|
3351
|
+
const answers = record.resolvers ?? [];
|
|
3352
|
+
const seeing = answers.filter((answer) => answer.matches).map((answer) => answer.resolver);
|
|
3353
|
+
const others = answers
|
|
3354
|
+
.filter((answer) => !answer.matches)
|
|
3355
|
+
.map((answer) => `${answer.resolver} sees ${answerText(answer.current)}${typeof answer.ttlSeconds === "number" ? ` (and may keep that answer${forUpTo(answer.ttlSeconds)})` : ""}`);
|
|
3356
|
+
return `public resolvers disagree: ${seeing.join(" and ")} ${seeing.length > 1 ? "see" : "sees"} the value shown, ${others.join("; ")}. That usually means a recent change is still spreading; make sure your DNS host shows the value shown, then check again`;
|
|
3357
|
+
}
|
|
3334
3358
|
if (record.check === "doubled") {
|
|
3335
3359
|
// The same name the NAME column shows, so the hint never contradicts the host's convention.
|
|
3336
3360
|
const name = record.nameAtProvider ?? record.name;
|
|
@@ -3342,17 +3366,32 @@ function recordStateText(record, routing) {
|
|
|
3342
3366
|
if (record.check === "apex_addresses") {
|
|
3343
3367
|
return routing === "active"
|
|
3344
3368
|
? "looks connected: the root answers with addresses, as an ALIAS does, and routing is active"
|
|
3345
|
-
:
|
|
3369
|
+
: // A parking page answers with addresses too, so nothing here says the record is right: the owner confirms it.
|
|
3370
|
+
`answers with addresses (${(record.current ?? []).join(", ")}); an ALIAS can't be verified from outside, and a parking page answers with addresses too: make sure the ALIAS or ANAME record at the root points at the value shown. The platform's routing check confirms it`;
|
|
3346
3371
|
}
|
|
3347
3372
|
if (record.ok === null || record.current === null)
|
|
3348
3373
|
return "not checked: couldn't read your DNS just now";
|
|
3374
|
+
// TXT before wildcard: a name may hold several TXT values another certificate still needs, so a
|
|
3375
|
+
// TXT value is always added beside them, never "changed", whatever answers at the name.
|
|
3349
3376
|
if (record.type === "TXT") {
|
|
3350
|
-
const
|
|
3377
|
+
const from = record.check === "wildcard" && record.wildcard ? ` (from the wildcard ${record.wildcard})` : "";
|
|
3378
|
+
const now = record.current.length === 0 ? "not published yet" : `currently ${record.current.join(", ")}${from}`;
|
|
3351
3379
|
return `${now}: ${TXT_KEEP_OTHERS}`;
|
|
3352
3380
|
}
|
|
3381
|
+
if (record.check === "wildcard") {
|
|
3382
|
+
const name = record.nameAtProvider ?? record.name;
|
|
3383
|
+
// A name holding a CNAME can hold nothing else, so an explicit record equal to the wildcard's answer is changed, not joined.
|
|
3384
|
+
return `no ${record.type} record of its own yet: your DNS host's wildcard ${record.wildcard ?? ""} answers for it (${record.current.join(", ")}). Add one at ${name === "" ? "the root" : name} with the value shown; the wildcard can stay. If your DNS host already lists a record at this name, change that one instead`;
|
|
3385
|
+
}
|
|
3353
3386
|
if (record.current.length === 0)
|
|
3354
3387
|
return "not published yet";
|
|
3355
|
-
|
|
3388
|
+
// A changed record can keep its old answer in public caches for its TTL, which is said so the owner does not change it twice.
|
|
3389
|
+
const recent = typeof record.ttlSeconds === "number" ? ` (if you changed it in the last ${cacheMinutes(record.ttlSeconds)} min, this may be the old answer still cached)` : "";
|
|
3390
|
+
// Two resolvers can each hold a different old answer; both are shown, as neither is the expected value.
|
|
3391
|
+
const shown = record.current.join(", ");
|
|
3392
|
+
const others = (record.resolvers ?? []).filter((answer) => answer.current.join(", ") !== shown);
|
|
3393
|
+
const also = others.map((answer) => `; ${answer.resolver} answers ${answerText(answer.current)}`).join("");
|
|
3394
|
+
return `currently ${shown}${also}${recent}: point it at the value shown`;
|
|
3356
3395
|
}
|
|
3357
3396
|
// A printed command carries --owner and, when the destination is not the built-in default,
|
|
3358
3397
|
// --api-url: pasted into a shell without NINEPM_API_URL it would otherwise act on production.
|
|
@@ -3510,6 +3549,10 @@ export function formatDomainStatus(status, options = {}) {
|
|
|
3510
3549
|
}
|
|
3511
3550
|
else if (status.state === "issuing_certificate") {
|
|
3512
3551
|
lines.push("Every required record is published. The platform finishes on its own, usually within minutes.");
|
|
3552
|
+
const nextCheck = nextCheckText(status.next?.nextCheckAt);
|
|
3553
|
+
// A restart is not offered here: nothing measured says it speeds issuance up.
|
|
3554
|
+
if (nextCheck)
|
|
3555
|
+
lines.push(`The platform checks it again around ${nextCheck} on its own; nothing needs restarting.`);
|
|
3513
3556
|
}
|
|
3514
3557
|
lines.push(`Then run: ${domainCommand("wait", status.app, status.hostname, options)}`);
|
|
3515
3558
|
return lines;
|
|
@@ -3525,12 +3568,16 @@ export function domainWaitTimeoutMessage(status) {
|
|
|
3525
3568
|
return "Timed out: a CAA record stops the certificate from being issued. Change it as described above, then wait again.";
|
|
3526
3569
|
}
|
|
3527
3570
|
if (status.state === "issuing_certificate") {
|
|
3528
|
-
|
|
3571
|
+
const nextCheck = nextCheckText(status.next?.nextCheckAt);
|
|
3572
|
+
return `Timed out: the records are published and the platform is still finishing. Wait again; nothing needs changing.${nextCheck ? ` Its next automatic check is around ${nextCheck}.` : ""}`;
|
|
3529
3573
|
}
|
|
3530
3574
|
const required = status.records.filter((record) => !record.optional);
|
|
3531
3575
|
if (required.some((record) => record.ok === false)) {
|
|
3532
3576
|
return "Timed out: records above are still missing. Publish them, then wait again.";
|
|
3533
3577
|
}
|
|
3578
|
+
if (required.some((record) => record.check === "resolvers_disagree")) {
|
|
3579
|
+
return "Timed out: public resolvers still disagree about a record above. Make sure your DNS host shows the value shown, then wait again.";
|
|
3580
|
+
}
|
|
3534
3581
|
return "Timed out: the DNS records could not be checked. Make sure they are published as shown, then wait again.";
|
|
3535
3582
|
}
|
|
3536
3583
|
export function formatDomainsList(statuses) {
|