@vincentt-xr/cli 0.15.1 → 0.16.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/dist/cli/args.js +1 -0
- package/dist/cli/index.js +177 -4
- package/dist/cli/usage-text.js +27 -0
- package/dist/cli/verb-surface.d.ts +3 -1
- package/dist/cli/verb-surface.js +12 -0
- package/dist/domain/api.d.ts +51 -0
- package/dist/domain/api.js +145 -0
- package/dist/domain/command.d.ts +75 -0
- package/dist/domain/command.js +467 -0
- package/dist/domain/copy.d.ts +334 -0
- package/dist/domain/copy.js +952 -0
- package/dist/domain/hostname.d.ts +57 -0
- package/dist/domain/hostname.js +116 -0
- package/dist/domain/names.d.ts +78 -0
- package/dist/domain/names.js +113 -0
- package/dist/domain/routedVerbs.d.ts +10 -0
- package/dist/domain/routedVerbs.js +30 -0
- package/dist/domain/wire.d.ts +140 -0
- package/dist/domain/wire.js +174 -0
- package/dist/dx/clusters.js +10 -1
- package/dist/dx/command.d.ts +13 -4
- package/dist/dx/command.js +26 -5
- package/dist/dx/report.d.ts +39 -7
- package/dist/dx/report.js +71 -20
- package/dist/dx/send.d.ts +22 -4
- package/dist/dx/send.js +29 -8
- package/dist/dx/skeleton.js +19 -0
- package/dist/dx/unbiddenVerbs.d.ts +25 -0
- package/dist/dx/unbiddenVerbs.js +39 -0
- package/dist/lib/exitCodes.d.ts +24 -0
- package/dist/lib/exitCodes.js +28 -0
- package/dist/lib/initCommand.d.ts +3 -1
- package/dist/lib/initCommand.js +10 -0
- package/dist/lib/initFrom.js +12 -2
- package/dist/lib/lifecycle.d.ts +5 -1
- package/dist/lib/projects.d.ts +8 -1
- package/dist/lib/projects.js +21 -4
- package/dist/platform/allowlist.d.ts +36 -0
- package/dist/platform/allowlist.js +34 -0
- package/dist/platform/behind.d.ts +41 -0
- package/dist/platform/behind.js +82 -0
- package/dist/platform/command.d.ts +16 -0
- package/dist/platform/command.js +90 -0
- package/dist/platform/footer.d.ts +60 -0
- package/dist/platform/footer.js +101 -0
- package/dist/platform/notice.d.ts +29 -0
- package/dist/platform/notice.js +66 -0
- package/dist/platform/offSwitch.d.ts +19 -0
- package/dist/platform/offSwitch.js +21 -0
- package/dist/platform/outdated.d.ts +25 -0
- package/dist/platform/outdated.js +246 -0
- package/dist/platform/releaseCache.d.ts +69 -0
- package/dist/platform/releaseCache.js +140 -0
- package/dist/platform/releaseFetch.d.ts +51 -0
- package/dist/platform/releaseFetch.js +150 -0
- package/dist/platform/resolve.d.ts +30 -0
- package/dist/platform/resolve.js +128 -0
- package/dist/platform/templateUpgrade.d.ts +56 -0
- package/dist/platform/templateUpgrade.js +476 -0
- package/dist/platform/upgradeReport.d.ts +132 -0
- package/dist/platform/upgradeReport.js +258 -0
- package/dist/platform/versionSummary.d.ts +82 -0
- package/dist/platform/versionSummary.js +181 -0
- package/dist/platform/zonePath.d.ts +154 -0
- package/dist/platform/zonePath.js +286 -0
- package/dist/platform/zones.d.ts +64 -0
- package/dist/platform/zones.js +79 -0
- package/dist/publish/api.d.ts +15 -4
- package/dist/publish/api.js +19 -5
- package/dist/publish/command.d.ts +10 -0
- package/dist/publish/command.js +7 -1
- package/dist/publish/copy.d.ts +14 -1
- package/dist/publish/copy.js +32 -3
- package/dist/scaffold/index.d.ts +50 -3
- package/dist/scaffold/index.js +61 -1
- package/dist/shared/config.d.ts +16 -0
- package/dist/shared/config.js +9 -1
- package/package.json +1 -1
- package/dist/dx/qa.f7.fixtures.d.ts +0 -73
- package/dist/dx/qa.f7.fixtures.js +0 -58
- package/dist/publish/qa.f4.fixtures.d.ts +0 -25
- package/dist/publish/qa.f4.fixtures.js +0 -140
- package/dist/relay/qa.f12.fixtures.d.ts +0 -154
- package/dist/relay/qa.f12.fixtures.js +0 -358
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The registrable domains the platform owns (how-it-works.md § The
|
|
3
|
+
* platform-hostname refusal).
|
|
4
|
+
*
|
|
5
|
+
* EXACT MATCH, or SUFFIX WITH A LEADING DOT. The leading dot is the label
|
|
6
|
+
* boundary; its absence is the `evilvincentt.app` bug — that name is a DIFFERENT
|
|
7
|
+
* registrable domain and is correctly ACCEPTED.
|
|
8
|
+
*
|
|
9
|
+
* This list is duplicated from the Go implementation deliberately and it is safe
|
|
10
|
+
* to duplicate for the reason the record gives: it changes only when the company
|
|
11
|
+
* buys a domain, an event with a purchase order attached. The server checks it
|
|
12
|
+
* too, and the server's answer is the one that decides — this copy exists so the
|
|
13
|
+
* refusal costs no round trip, not so the CLI can be the authority.
|
|
14
|
+
*/
|
|
15
|
+
export declare const PLATFORM_REGISTRABLE_DOMAINS: readonly ["vincentt.app", "vincentt.dev", "vincentt.studio"];
|
|
16
|
+
/**
|
|
17
|
+
* Shape the creator's argument for the path segment.
|
|
18
|
+
*
|
|
19
|
+
* Lowercase, trim surrounding whitespace, strip a trailing dot, and strip a
|
|
20
|
+
* scheme and any path a creator pasted from a browser bar. The trailing-dot strip
|
|
21
|
+
* PRECEDES the port truncation, matching the server spec's load-bearing ordering
|
|
22
|
+
* — without that order the composed case `host.example.com.:8443` normalizes
|
|
23
|
+
* wrong.
|
|
24
|
+
*
|
|
25
|
+
* Anything else — a name with an invalid character, an over-long label, a
|
|
26
|
+
* punycode label — is passed THROUGH to the server unchanged, which refuses it
|
|
27
|
+
* with the copy it owns. A client-side reject for those would be a second spec.
|
|
28
|
+
*/
|
|
29
|
+
export declare function normalizeArgument(input: string): string;
|
|
30
|
+
/**
|
|
31
|
+
* Is this a hostname under a registrable domain the platform owns?
|
|
32
|
+
*
|
|
33
|
+
* Exact match or leading-dot suffix. Case-folded because the caller may not have
|
|
34
|
+
* normalized yet.
|
|
35
|
+
*/
|
|
36
|
+
export declare function isPlatformHostname(host: string): boolean;
|
|
37
|
+
/**
|
|
38
|
+
* Does this name have no subdomain — i.e. is it a root domain?
|
|
39
|
+
*
|
|
40
|
+
* Two labels or fewer. This is a LABEL COUNT, not a public-suffix computation,
|
|
41
|
+
* and the difference matters in one direction only: `acme.co.uk` is a root domain
|
|
42
|
+
* that this returns false for, so the CLI sends it and the SERVER refuses it with
|
|
43
|
+
* the same copy. That is the correct failure direction — a client-side check that
|
|
44
|
+
* guessed at public suffixes would refuse `ar.acme.co.uk`, a name f16 accepts.
|
|
45
|
+
*/
|
|
46
|
+
export declare function isRootDomain(host: string): boolean;
|
|
47
|
+
/** A local refusal, decided with no network call. */
|
|
48
|
+
export type LocalRefusal = "platform_host" | "root_domain" | "empty";
|
|
49
|
+
/**
|
|
50
|
+
* The local refusal for this argument, or undefined to send it on.
|
|
51
|
+
*
|
|
52
|
+
* ORDER MATTERS: the platform check runs FIRST. `vincentt.app` is both a
|
|
53
|
+
* two-label root domain and a platform address, and "that's already a Vincentt
|
|
54
|
+
* address" is the true and useful answer while "root domains aren't supported"
|
|
55
|
+
* would send the creator to try `thing.vincentt.app`, which is also refused.
|
|
56
|
+
*/
|
|
57
|
+
export declare function localRefusal(input: string): LocalRefusal | undefined;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
// Client-side hostname handling for `vincentt domain <verb> <hostname>`.
|
|
2
|
+
//
|
|
3
|
+
// THE SERVER IS AUTHORITATIVE. The API normalizes the hostname by its own
|
|
4
|
+
// 12-step ordered specification and refuses what it will not accept, and the
|
|
5
|
+
// routes are hostname-keyed with the path segment normalized before any store
|
|
6
|
+
// read. Everything here is a LOCAL CONVENIENCE with two jobs and no third:
|
|
7
|
+
//
|
|
8
|
+
// 1. Put the argument into the shape the path segment expects, so a creator
|
|
9
|
+
// typing `AR.Acme.COM.` reaches the same row as `ar.acme.com`.
|
|
10
|
+
// 2. Refuse, with no network call, the two cases whose copy is a fact about the
|
|
11
|
+
// creator's own registrar rather than about our state: a root domain and a
|
|
12
|
+
// platform address.
|
|
13
|
+
//
|
|
14
|
+
// It deliberately does NOT reimplement the server's 12 steps. A second
|
|
15
|
+
// implementation of a normalization spec is a second thing that can drift, and
|
|
16
|
+
// the record names exactly three implementations of that spec (Go, the edge
|
|
17
|
+
// Function, the meter's reducer) with a shared fixture pinning them. Adding a
|
|
18
|
+
// fourth in the CLI, unpinned by that fixture, is how the CLI comes to accept a
|
|
19
|
+
// name the API rejects — or worse, to rewrite one into a different valid name.
|
|
20
|
+
//
|
|
21
|
+
// WHAT THE CLI DISPLAYS IS ALWAYS WHAT THE API RETURNED. `normalizeArgument` only
|
|
22
|
+
// shapes the value we SEND; once a response is in hand, every rendered hostname
|
|
23
|
+
// comes from `domain` on the wire.
|
|
24
|
+
/**
|
|
25
|
+
* The registrable domains the platform owns (how-it-works.md § The
|
|
26
|
+
* platform-hostname refusal).
|
|
27
|
+
*
|
|
28
|
+
* EXACT MATCH, or SUFFIX WITH A LEADING DOT. The leading dot is the label
|
|
29
|
+
* boundary; its absence is the `evilvincentt.app` bug — that name is a DIFFERENT
|
|
30
|
+
* registrable domain and is correctly ACCEPTED.
|
|
31
|
+
*
|
|
32
|
+
* This list is duplicated from the Go implementation deliberately and it is safe
|
|
33
|
+
* to duplicate for the reason the record gives: it changes only when the company
|
|
34
|
+
* buys a domain, an event with a purchase order attached. The server checks it
|
|
35
|
+
* too, and the server's answer is the one that decides — this copy exists so the
|
|
36
|
+
* refusal costs no round trip, not so the CLI can be the authority.
|
|
37
|
+
*/
|
|
38
|
+
export const PLATFORM_REGISTRABLE_DOMAINS = [
|
|
39
|
+
"vincentt.app",
|
|
40
|
+
"vincentt.dev",
|
|
41
|
+
"vincentt.studio",
|
|
42
|
+
];
|
|
43
|
+
/**
|
|
44
|
+
* Shape the creator's argument for the path segment.
|
|
45
|
+
*
|
|
46
|
+
* Lowercase, trim surrounding whitespace, strip a trailing dot, and strip a
|
|
47
|
+
* scheme and any path a creator pasted from a browser bar. The trailing-dot strip
|
|
48
|
+
* PRECEDES the port truncation, matching the server spec's load-bearing ordering
|
|
49
|
+
* — without that order the composed case `host.example.com.:8443` normalizes
|
|
50
|
+
* wrong.
|
|
51
|
+
*
|
|
52
|
+
* Anything else — a name with an invalid character, an over-long label, a
|
|
53
|
+
* punycode label — is passed THROUGH to the server unchanged, which refuses it
|
|
54
|
+
* with the copy it owns. A client-side reject for those would be a second spec.
|
|
55
|
+
*/
|
|
56
|
+
export function normalizeArgument(input) {
|
|
57
|
+
let s = input.trim().toLowerCase();
|
|
58
|
+
// A creator or an agent pasting from a browser hands over a URL. Taking the
|
|
59
|
+
// host out of it is unambiguous; guessing at anything else is not.
|
|
60
|
+
s = s.replace(/^[a-z][a-z0-9+.-]*:\/\//, "");
|
|
61
|
+
const slash = s.indexOf("/");
|
|
62
|
+
if (slash !== -1)
|
|
63
|
+
s = s.slice(0, slash);
|
|
64
|
+
// Trailing dot BEFORE the colon, per the spec's ordering.
|
|
65
|
+
if (s.endsWith("."))
|
|
66
|
+
s = s.slice(0, -1);
|
|
67
|
+
const colon = s.lastIndexOf(":");
|
|
68
|
+
if (colon !== -1)
|
|
69
|
+
s = s.slice(0, colon);
|
|
70
|
+
if (s.endsWith("."))
|
|
71
|
+
s = s.slice(0, -1);
|
|
72
|
+
return s;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Is this a hostname under a registrable domain the platform owns?
|
|
76
|
+
*
|
|
77
|
+
* Exact match or leading-dot suffix. Case-folded because the caller may not have
|
|
78
|
+
* normalized yet.
|
|
79
|
+
*/
|
|
80
|
+
export function isPlatformHostname(host) {
|
|
81
|
+
const h = normalizeArgument(host);
|
|
82
|
+
return PLATFORM_REGISTRABLE_DOMAINS.some((d) => h === d || h.endsWith(`.${d}`));
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Does this name have no subdomain — i.e. is it a root domain?
|
|
86
|
+
*
|
|
87
|
+
* Two labels or fewer. This is a LABEL COUNT, not a public-suffix computation,
|
|
88
|
+
* and the difference matters in one direction only: `acme.co.uk` is a root domain
|
|
89
|
+
* that this returns false for, so the CLI sends it and the SERVER refuses it with
|
|
90
|
+
* the same copy. That is the correct failure direction — a client-side check that
|
|
91
|
+
* guessed at public suffixes would refuse `ar.acme.co.uk`, a name f16 accepts.
|
|
92
|
+
*/
|
|
93
|
+
export function isRootDomain(host) {
|
|
94
|
+
const h = normalizeArgument(host);
|
|
95
|
+
if (!h)
|
|
96
|
+
return false;
|
|
97
|
+
return h.split(".").filter((l) => l.length > 0).length <= 2;
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The local refusal for this argument, or undefined to send it on.
|
|
101
|
+
*
|
|
102
|
+
* ORDER MATTERS: the platform check runs FIRST. `vincentt.app` is both a
|
|
103
|
+
* two-label root domain and a platform address, and "that's already a Vincentt
|
|
104
|
+
* address" is the true and useful answer while "root domains aren't supported"
|
|
105
|
+
* would send the creator to try `thing.vincentt.app`, which is also refused.
|
|
106
|
+
*/
|
|
107
|
+
export function localRefusal(input) {
|
|
108
|
+
const h = normalizeArgument(input);
|
|
109
|
+
if (!h)
|
|
110
|
+
return "empty";
|
|
111
|
+
if (isPlatformHostname(h))
|
|
112
|
+
return "platform_host";
|
|
113
|
+
if (isRootDomain(h))
|
|
114
|
+
return "root_domain";
|
|
115
|
+
return undefined;
|
|
116
|
+
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two forms of one record name, labelled by the OBSERVABLE TELL rather than
|
|
3
|
+
* by the DNS concept.
|
|
4
|
+
*
|
|
5
|
+
* `relative` may be undefined: see `relativeName`. When it is, only the FQDN
|
|
6
|
+
* prints, because inventing a relative form we cannot derive is exactly the
|
|
7
|
+
* wrong-form paste this whole module exists to prevent.
|
|
8
|
+
*/
|
|
9
|
+
export interface NamePair {
|
|
10
|
+
/** The short form. Printed FIRST — the majority want it, and the first line is what a hurrying creator copies. */
|
|
11
|
+
relative?: string;
|
|
12
|
+
/** The fully-qualified name, exactly as the API sent it. */
|
|
13
|
+
fqdn: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* The relative form of `fqdn` within `zone`, or undefined when it cannot be
|
|
17
|
+
* derived.
|
|
18
|
+
*
|
|
19
|
+
* Undefined in three cases, and each is a deliberate refusal rather than a gap:
|
|
20
|
+
*
|
|
21
|
+
* 1. `fqdn` is not inside `zone`. Deriving a relative name against the wrong
|
|
22
|
+
* zone produces a plausible string that creates the wrong record.
|
|
23
|
+
* 2. `fqdn` IS the zone. The relative form is the registrar's own apex
|
|
24
|
+
* convention (`@`, blank, or the zone name depending on vendor), and there
|
|
25
|
+
* is no single value that is right everywhere. f16 refuses root domains at
|
|
26
|
+
* attach anyway, so this arm is unreachable from the attach path — it is
|
|
27
|
+
* here because a caller could reach it from a record on a zone-level CAA.
|
|
28
|
+
* 3. The zone is empty or the fqdn is empty.
|
|
29
|
+
*
|
|
30
|
+
* The comparison is CASE-INSENSITIVE on the suffix but the returned value is a
|
|
31
|
+
* slice of the ORIGINAL `fqdn`, so a mixed-case name the API sent renders in the
|
|
32
|
+
* case the API sent it. The API always sends the normalized lowercase form; the
|
|
33
|
+
* slice-not-lowercase choice means this function cannot itself introduce a
|
|
34
|
+
* discrepancy between what we display and what we were given.
|
|
35
|
+
*/
|
|
36
|
+
export declare function relativeName(fqdn: string, zone: string): string | undefined;
|
|
37
|
+
/**
|
|
38
|
+
* The pair for one record name, given the domain being attached.
|
|
39
|
+
*
|
|
40
|
+
* `zone` is the ATTACHED DOMAIN (`ar.acme.com`), not the registrable domain
|
|
41
|
+
* (`acme.com`), and that is the correct choice for the reason the transcripts
|
|
42
|
+
* show: the creator is editing DNS for `acme.com`, so the relative name of
|
|
43
|
+
* `_vincentt-challenge.ar.acme.com` is `_vincentt-challenge.ar`.
|
|
44
|
+
*
|
|
45
|
+
* That means the zone passed here is the REGISTRABLE domain in practice, and the
|
|
46
|
+
* caller decides which — `recordNamePairs` takes it explicitly rather than
|
|
47
|
+
* guessing, because a guess at the registrable domain requires a public-suffix
|
|
48
|
+
* list the CLI deliberately does not ship (`ar.acme.co.uk` would break a
|
|
49
|
+
* two-label heuristic).
|
|
50
|
+
*/
|
|
51
|
+
export declare function namePair(fqdn: string, zone: string): NamePair;
|
|
52
|
+
/**
|
|
53
|
+
* The zone a relative name is relative TO, derived from the attached domain by
|
|
54
|
+
* dropping its first label.
|
|
55
|
+
*
|
|
56
|
+
* `ar.acme.com` → `acme.com`. This is NOT a public-suffix computation and does
|
|
57
|
+
* not claim to be one: it answers "what will the registrar's form append", and
|
|
58
|
+
* the registrar whose form appends anything is administering the zone the
|
|
59
|
+
* attached name sits directly inside. For `ar.acme.co.uk` this yields
|
|
60
|
+
* `acme.co.uk`, which is the correct zone for that record, while a
|
|
61
|
+
* public-suffix-based "registrable domain" would also yield `acme.co.uk`. The
|
|
62
|
+
* two agree wherever the attached name is one label below its zone, which is
|
|
63
|
+
* every name f16 accepts (root domains are refused at attach).
|
|
64
|
+
*
|
|
65
|
+
* Returns undefined for a single-label input, which attach refuses anyway.
|
|
66
|
+
*/
|
|
67
|
+
export declare function zoneOf(domain: string): string | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* Every record's name pair, in the order the API sent the records.
|
|
70
|
+
*
|
|
71
|
+
* The order is the API's and is never re-sorted here: the transcripts put the
|
|
72
|
+
* pointer CNAME first, the ownership TXT second and the certificate validation
|
|
73
|
+
* CNAME third, and that order is the API's to choose. A client that sorted would
|
|
74
|
+
* put the keep-this-one caption on whichever record happened to land third.
|
|
75
|
+
*/
|
|
76
|
+
export declare function recordNamePairs(records: readonly {
|
|
77
|
+
name: string;
|
|
78
|
+
}[], domain: string): NamePair[];
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// The two-form name problem — "the single most likely real-world failure in the
|
|
2
|
+
// feature" (implementation.md § The contract trap) and "the most expensive copy
|
|
3
|
+
// decision" (how-it-works.md Flow C1).
|
|
4
|
+
//
|
|
5
|
+
// The API emits `records[].name` as the FULLY-QUALIFIED name. Roughly half of
|
|
6
|
+
// registrar UIs append the zone themselves and expect the RELATIVE name. Pasting
|
|
7
|
+
// the wrong form produces a record that never verifies, and the ticket reads as
|
|
8
|
+
// our bug.
|
|
9
|
+
//
|
|
10
|
+
// This module derives the relative form. It is the ONE derivation the CLI performs
|
|
11
|
+
// on a record, and it is a SUBTRACTION from the value the API sent rather than an
|
|
12
|
+
// assembly from parts — which is what keeps "records are printed verbatim" true.
|
|
13
|
+
// The FQDN is always carried through untouched.
|
|
14
|
+
/**
|
|
15
|
+
* Strip a trailing dot for comparison only. The API's own normalization already
|
|
16
|
+
* removes it, but a record name is a string a registrar or a future API version
|
|
17
|
+
* may hand back rooted, and a comparison that failed on the dot would silently
|
|
18
|
+
* drop the relative form on every record.
|
|
19
|
+
*/
|
|
20
|
+
function unroot(host) {
|
|
21
|
+
return host.endsWith(".") ? host.slice(0, -1) : host;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* The relative form of `fqdn` within `zone`, or undefined when it cannot be
|
|
25
|
+
* derived.
|
|
26
|
+
*
|
|
27
|
+
* Undefined in three cases, and each is a deliberate refusal rather than a gap:
|
|
28
|
+
*
|
|
29
|
+
* 1. `fqdn` is not inside `zone`. Deriving a relative name against the wrong
|
|
30
|
+
* zone produces a plausible string that creates the wrong record.
|
|
31
|
+
* 2. `fqdn` IS the zone. The relative form is the registrar's own apex
|
|
32
|
+
* convention (`@`, blank, or the zone name depending on vendor), and there
|
|
33
|
+
* is no single value that is right everywhere. f16 refuses root domains at
|
|
34
|
+
* attach anyway, so this arm is unreachable from the attach path — it is
|
|
35
|
+
* here because a caller could reach it from a record on a zone-level CAA.
|
|
36
|
+
* 3. The zone is empty or the fqdn is empty.
|
|
37
|
+
*
|
|
38
|
+
* The comparison is CASE-INSENSITIVE on the suffix but the returned value is a
|
|
39
|
+
* slice of the ORIGINAL `fqdn`, so a mixed-case name the API sent renders in the
|
|
40
|
+
* case the API sent it. The API always sends the normalized lowercase form; the
|
|
41
|
+
* slice-not-lowercase choice means this function cannot itself introduce a
|
|
42
|
+
* discrepancy between what we display and what we were given.
|
|
43
|
+
*/
|
|
44
|
+
export function relativeName(fqdn, zone) {
|
|
45
|
+
const f = unroot(fqdn);
|
|
46
|
+
const z = unroot(zone);
|
|
47
|
+
if (!f || !z)
|
|
48
|
+
return undefined;
|
|
49
|
+
const lf = f.toLowerCase();
|
|
50
|
+
const lz = z.toLowerCase();
|
|
51
|
+
// Case 2 — the name IS the zone. No universal relative spelling exists.
|
|
52
|
+
if (lf === lz)
|
|
53
|
+
return undefined;
|
|
54
|
+
// Case 1 — must be a strict subdomain, on a LABEL BOUNDARY. The leading dot is
|
|
55
|
+
// what makes this a label-boundary test rather than a suffix test: without it,
|
|
56
|
+
// zone `acme.com` would match `evilacme.com` and return `evil` — the same class
|
|
57
|
+
// of bug the platform-hostname refusal's leading dot exists to prevent.
|
|
58
|
+
if (!lf.endsWith(`.${lz}`))
|
|
59
|
+
return undefined;
|
|
60
|
+
return f.slice(0, f.length - z.length - 1);
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* The pair for one record name, given the domain being attached.
|
|
64
|
+
*
|
|
65
|
+
* `zone` is the ATTACHED DOMAIN (`ar.acme.com`), not the registrable domain
|
|
66
|
+
* (`acme.com`), and that is the correct choice for the reason the transcripts
|
|
67
|
+
* show: the creator is editing DNS for `acme.com`, so the relative name of
|
|
68
|
+
* `_vincentt-challenge.ar.acme.com` is `_vincentt-challenge.ar`.
|
|
69
|
+
*
|
|
70
|
+
* That means the zone passed here is the REGISTRABLE domain in practice, and the
|
|
71
|
+
* caller decides which — `recordNamePairs` takes it explicitly rather than
|
|
72
|
+
* guessing, because a guess at the registrable domain requires a public-suffix
|
|
73
|
+
* list the CLI deliberately does not ship (`ar.acme.co.uk` would break a
|
|
74
|
+
* two-label heuristic).
|
|
75
|
+
*/
|
|
76
|
+
export function namePair(fqdn, zone) {
|
|
77
|
+
return { relative: relativeName(fqdn, zone), fqdn };
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The zone a relative name is relative TO, derived from the attached domain by
|
|
81
|
+
* dropping its first label.
|
|
82
|
+
*
|
|
83
|
+
* `ar.acme.com` → `acme.com`. This is NOT a public-suffix computation and does
|
|
84
|
+
* not claim to be one: it answers "what will the registrar's form append", and
|
|
85
|
+
* the registrar whose form appends anything is administering the zone the
|
|
86
|
+
* attached name sits directly inside. For `ar.acme.co.uk` this yields
|
|
87
|
+
* `acme.co.uk`, which is the correct zone for that record, while a
|
|
88
|
+
* public-suffix-based "registrable domain" would also yield `acme.co.uk`. The
|
|
89
|
+
* two agree wherever the attached name is one label below its zone, which is
|
|
90
|
+
* every name f16 accepts (root domains are refused at attach).
|
|
91
|
+
*
|
|
92
|
+
* Returns undefined for a single-label input, which attach refuses anyway.
|
|
93
|
+
*/
|
|
94
|
+
export function zoneOf(domain) {
|
|
95
|
+
const d = unroot(domain);
|
|
96
|
+
const dot = d.indexOf(".");
|
|
97
|
+
if (dot <= 0)
|
|
98
|
+
return undefined;
|
|
99
|
+
const zone = d.slice(dot + 1);
|
|
100
|
+
return zone.includes(".") ? zone : zone || undefined;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Every record's name pair, in the order the API sent the records.
|
|
104
|
+
*
|
|
105
|
+
* The order is the API's and is never re-sorted here: the transcripts put the
|
|
106
|
+
* pointer CNAME first, the ownership TXT second and the certificate validation
|
|
107
|
+
* CNAME third, and that order is the API's to choose. A client that sorted would
|
|
108
|
+
* put the keep-this-one caption on whichever record happened to land third.
|
|
109
|
+
*/
|
|
110
|
+
export function recordNamePairs(records, domain) {
|
|
111
|
+
const zone = zoneOf(domain);
|
|
112
|
+
return records.map((r) => (zone ? namePair(r.name, zone) : { fqdn: r.name }));
|
|
113
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Spellings a creator or an agent plausibly types for the domain verb.
|
|
3
|
+
*
|
|
4
|
+
* `domains` is the plural of the real verb and is the most likely of the three.
|
|
5
|
+
* It is NOT pinned in PINNED_VERBS: it names no capability of its own, and
|
|
6
|
+
* pinning it would freeze a misspelling into the append-only verb surface
|
|
7
|
+
* forever.
|
|
8
|
+
*/
|
|
9
|
+
export declare const DOMAIN_ROUTED_VERBS: readonly string[];
|
|
10
|
+
export declare function isDomainRoutedVerb(verb: string): boolean;
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// The plausible spellings of `domain` that are NOT the verb, routed to an
|
|
2
|
+
// answer rather than to "unknown command".
|
|
3
|
+
//
|
|
4
|
+
// This is ARGUMENT PARSING. Exit 1, no network call, nothing looked up.
|
|
5
|
+
//
|
|
6
|
+
// It is deliberately NOT the console-only mechanism. That one exists for acts
|
|
7
|
+
// that live on another SURFACE ("rolling back is a console action"); these are
|
|
8
|
+
// acts that live right here under a different spelling, so the answer names the
|
|
9
|
+
// command rather than a screen.
|
|
10
|
+
//
|
|
11
|
+
// Routing them rather than ignoring them is about the actor. A human who types
|
|
12
|
+
// `vincentt domains` and gets "unrecognised command" shrugs and reads the help.
|
|
13
|
+
// AN AGENT RETRIES, or guesses a neighbouring verb, and neither terminates.
|
|
14
|
+
/**
|
|
15
|
+
* Spellings a creator or an agent plausibly types for the domain verb.
|
|
16
|
+
*
|
|
17
|
+
* `domains` is the plural of the real verb and is the most likely of the three.
|
|
18
|
+
* It is NOT pinned in PINNED_VERBS: it names no capability of its own, and
|
|
19
|
+
* pinning it would freeze a misspelling into the append-only verb surface
|
|
20
|
+
* forever.
|
|
21
|
+
*/
|
|
22
|
+
export const DOMAIN_ROUTED_VERBS = [
|
|
23
|
+
"domains",
|
|
24
|
+
"custom-domain",
|
|
25
|
+
"custom-domains",
|
|
26
|
+
"dns",
|
|
27
|
+
];
|
|
28
|
+
export function isDomainRoutedVerb(verb) {
|
|
29
|
+
return DOMAIN_ROUTED_VERBS.includes(verb);
|
|
30
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The closed state enum (implementation.md § Wire fields).
|
|
3
|
+
*
|
|
4
|
+
* `DomainState` is a union of the seven the API can send. An unrecognized state
|
|
5
|
+
* is NOT coerced into one of these — `parseDomain` keeps the raw string, because
|
|
6
|
+
* a newer API sending an eighth state must render as itself rather than as a
|
|
7
|
+
* plausible neighbour. Rendering a state we do not know as `pending` would tell a
|
|
8
|
+
* creator nothing is wrong when something might be.
|
|
9
|
+
*/
|
|
10
|
+
export declare const DOMAIN_STATES: readonly ["pending", "verified", "live", "failing", "held", "cert_failed", "detached"];
|
|
11
|
+
export type DomainState = (typeof DOMAIN_STATES)[number];
|
|
12
|
+
export declare function isDomainState(s: string): s is DomainState;
|
|
13
|
+
/**
|
|
14
|
+
* A DNS record the creator must create at their registrar.
|
|
15
|
+
*
|
|
16
|
+
* `name` is the FULLY-QUALIFIED name, as the API emits it. It is not normalized,
|
|
17
|
+
* not lowercased, and not reassembled here.
|
|
18
|
+
*/
|
|
19
|
+
export interface DnsRecord {
|
|
20
|
+
type: string;
|
|
21
|
+
name: string;
|
|
22
|
+
value: string;
|
|
23
|
+
ttl?: number;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* The closed findings enum (how-it-works.md Scenario 4). Each member carries an
|
|
27
|
+
* `observed` string.
|
|
28
|
+
*
|
|
29
|
+
* The severity is NOT on the wire as a field the CLI trusts blindly — see
|
|
30
|
+
* `findingSeverity` in copy.ts, which maps the enum to blocking/advisory. That
|
|
31
|
+
* mapping lives with the copy because the exit code follows the glyph, and the
|
|
32
|
+
* glyph is a copy decision.
|
|
33
|
+
*/
|
|
34
|
+
export declare const FINDING_CODES: readonly ["TOKEN_MISSING", "TOKEN_MISMATCH", "TOKEN_STALE_PRESENT", "WRONG_RECORD_TYPE", "CNAME_WRONG_TARGET", "CONFLICTING_RECORDS", "UNEXPECTED_AAAA", "PROXY_IN_FRONT", "CAA_BLOCKS_ISSUANCE", "STALE_ACME_CHALLENGE", "VALIDATION_RECORD_GONE", "MALFORMED_ZONE"];
|
|
35
|
+
export type FindingCode = (typeof FINDING_CODES)[number];
|
|
36
|
+
/**
|
|
37
|
+
* D-44's finding, as a NAMED CONSTANT rather than a literal at each site.
|
|
38
|
+
*
|
|
39
|
+
* Two places branch on it — the severity table and the record re-print — and
|
|
40
|
+
* they were briefly spelled differently, which printed the finding while
|
|
41
|
+
* silently omitting the record that IS its remedy. A shared symbol makes that
|
|
42
|
+
* particular disagreement unexpressible.
|
|
43
|
+
*/
|
|
44
|
+
export declare const VALIDATION_RECORD_GONE: FindingCode;
|
|
45
|
+
/**
|
|
46
|
+
* A finding as the API emits it.
|
|
47
|
+
*
|
|
48
|
+
* `observed` is REQUIRED by the design — *"a finding rendered without its
|
|
49
|
+
* observed value is a restatement of the instructions, and the pattern refuses
|
|
50
|
+
* one structurally"*. The type makes it required so a renderer cannot omit it;
|
|
51
|
+
* `parseFinding` drops a finding whose `observed` is absent rather than printing
|
|
52
|
+
* a half one, and says so in the diagnostics count.
|
|
53
|
+
*/
|
|
54
|
+
export interface Finding {
|
|
55
|
+
code: string;
|
|
56
|
+
observed: string;
|
|
57
|
+
/**
|
|
58
|
+
* The expected value, as its OWN field, so the renderer can stack it above
|
|
59
|
+
* `observed` in an aligned pair.
|
|
60
|
+
*
|
|
61
|
+
* OPTIONAL, and the optionality is a live cross-repo question rather than a
|
|
62
|
+
* design choice. Part 2B requires *"every finding carries expected+observed"*
|
|
63
|
+
* and CLI convention 2 makes the stacked pair structural — but the producer's
|
|
64
|
+
* `domain.Finding` today carries only `{code, observed}` and folds the
|
|
65
|
+
* expectation INTO the observed string. Where that happens the pair collapses
|
|
66
|
+
* into one line and the aligned comparison the ruling rests on is lost.
|
|
67
|
+
*
|
|
68
|
+
* The renderer therefore degrades to the observed line alone rather than
|
|
69
|
+
* inventing an expectation, and never re-splits a folded string: parsing
|
|
70
|
+
* "(expected …)" back out of `observed` would be the client re-rendering the
|
|
71
|
+
* one field the wire declares un-restructurable.
|
|
72
|
+
*/
|
|
73
|
+
expected?: string;
|
|
74
|
+
/**
|
|
75
|
+
* The API's own severity, when it sends one (`fault` | `advisory`).
|
|
76
|
+
*
|
|
77
|
+
* READ AND HONORED, because the exit code the CLI returns must not disagree
|
|
78
|
+
* with the state the server put the domain in — and the server is the party
|
|
79
|
+
* that made the observation. Absent, the renderer falls back to its own
|
|
80
|
+
* design-derived table.
|
|
81
|
+
*/
|
|
82
|
+
severity?: string;
|
|
83
|
+
}
|
|
84
|
+
/** A domain row as the API emits it. */
|
|
85
|
+
export interface DomainView {
|
|
86
|
+
/**
|
|
87
|
+
* ALWAYS the normalized form. A client sending `AR.Acme.COM.` gets
|
|
88
|
+
* `ar.acme.com` back and MUST display what it got, not what it sent — so
|
|
89
|
+
* nothing in the CLI echoes the caller's argument once a response is in hand.
|
|
90
|
+
*/
|
|
91
|
+
domain: string;
|
|
92
|
+
/** The raw wire state. `state` is the parsed union when recognized. */
|
|
93
|
+
rawState: string;
|
|
94
|
+
state?: DomainState;
|
|
95
|
+
projectId?: string;
|
|
96
|
+
projectName?: string;
|
|
97
|
+
/** Printed verbatim. No client constructs one. */
|
|
98
|
+
records: DnsRecord[];
|
|
99
|
+
findings: Finding[];
|
|
100
|
+
/** ISO8601, when serving stops if unfixed. Null/absent unless `failing`. */
|
|
101
|
+
holdsAt?: string;
|
|
102
|
+
attachedAt?: string;
|
|
103
|
+
/** The project's permanent Vincentt address. Server-computed; never assembled here. */
|
|
104
|
+
permanentAddress?: string;
|
|
105
|
+
}
|
|
106
|
+
/** The org-scoped list envelope, carrying the count against the entitlement. */
|
|
107
|
+
export interface DomainListView {
|
|
108
|
+
domains: DomainView[];
|
|
109
|
+
/** The attached count and the plan's ceiling, as the API resolves them. */
|
|
110
|
+
count?: number;
|
|
111
|
+
limit?: number;
|
|
112
|
+
planName?: string;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* Parse one record. `type`, `name` and `value` are all required: a record
|
|
116
|
+
* missing any of the three cannot be created at a registrar, and printing a
|
|
117
|
+
* partial row would send a creator to paste a blank into a form.
|
|
118
|
+
*/
|
|
119
|
+
export declare function parseRecord(raw: unknown): DnsRecord | undefined;
|
|
120
|
+
/**
|
|
121
|
+
* Parse one finding. A finding with no `observed` is DROPPED rather than
|
|
122
|
+
* rendered, because the shape's whole contract is that it reports what was seen
|
|
123
|
+
* — and a finding that restates the instructions is the thing the design refuses.
|
|
124
|
+
* Dropping is visible: `parseDomain` counts what it dropped.
|
|
125
|
+
*/
|
|
126
|
+
export declare function parseFinding(raw: unknown): Finding | undefined;
|
|
127
|
+
export interface ParsedDomain {
|
|
128
|
+
domain: DomainView;
|
|
129
|
+
/** Findings the API sent that carried no `observed`. A defect signal, not a normal state. */
|
|
130
|
+
droppedFindings: number;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Parse a domain row.
|
|
134
|
+
*
|
|
135
|
+
* `domain` is the only structurally required field: without a hostname there is
|
|
136
|
+
* nothing to render a row about. Everything else degrades to an absent line.
|
|
137
|
+
*/
|
|
138
|
+
export declare function parseDomain(raw: unknown): ParsedDomain | undefined;
|
|
139
|
+
/** Parse the list envelope. A row that cannot be parsed is skipped, never faked. */
|
|
140
|
+
export declare function parseDomainList(raw: unknown): DomainListView;
|