@vincentt-xr/cli 0.15.2 → 0.17.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.
Files changed (83) hide show
  1. package/dist/cli/args.js +1 -0
  2. package/dist/cli/index.js +198 -3
  3. package/dist/cli/usage-text.js +27 -0
  4. package/dist/cli/verb-surface.d.ts +3 -1
  5. package/dist/cli/verb-surface.js +12 -0
  6. package/dist/domain/api.d.ts +51 -0
  7. package/dist/domain/api.js +145 -0
  8. package/dist/domain/command.d.ts +75 -0
  9. package/dist/domain/command.js +467 -0
  10. package/dist/domain/copy.d.ts +334 -0
  11. package/dist/domain/copy.js +952 -0
  12. package/dist/domain/hostname.d.ts +57 -0
  13. package/dist/domain/hostname.js +116 -0
  14. package/dist/domain/names.d.ts +78 -0
  15. package/dist/domain/names.js +113 -0
  16. package/dist/domain/routedVerbs.d.ts +10 -0
  17. package/dist/domain/routedVerbs.js +30 -0
  18. package/dist/domain/wire.d.ts +140 -0
  19. package/dist/domain/wire.js +174 -0
  20. package/dist/dx/command.d.ts +13 -4
  21. package/dist/dx/command.js +26 -5
  22. package/dist/dx/report.d.ts +39 -7
  23. package/dist/dx/report.js +71 -20
  24. package/dist/dx/send.d.ts +22 -4
  25. package/dist/dx/send.js +29 -8
  26. package/dist/dx/skeleton.js +19 -0
  27. package/dist/dx/unbiddenVerbs.d.ts +25 -0
  28. package/dist/dx/unbiddenVerbs.js +39 -0
  29. package/dist/lib/initCommand.d.ts +3 -1
  30. package/dist/lib/initCommand.js +10 -0
  31. package/dist/lib/projects.d.ts +8 -1
  32. package/dist/lib/projects.js +12 -2
  33. package/dist/platform/allowlist.d.ts +36 -0
  34. package/dist/platform/allowlist.js +34 -0
  35. package/dist/platform/behind.d.ts +41 -0
  36. package/dist/platform/behind.js +82 -0
  37. package/dist/platform/command.d.ts +16 -0
  38. package/dist/platform/command.js +90 -0
  39. package/dist/platform/footer.d.ts +60 -0
  40. package/dist/platform/footer.js +101 -0
  41. package/dist/platform/notice.d.ts +29 -0
  42. package/dist/platform/notice.js +66 -0
  43. package/dist/platform/offSwitch.d.ts +19 -0
  44. package/dist/platform/offSwitch.js +21 -0
  45. package/dist/platform/outdated.d.ts +25 -0
  46. package/dist/platform/outdated.js +246 -0
  47. package/dist/platform/releaseCache.d.ts +69 -0
  48. package/dist/platform/releaseCache.js +140 -0
  49. package/dist/platform/releaseFetch.d.ts +51 -0
  50. package/dist/platform/releaseFetch.js +150 -0
  51. package/dist/platform/resolve.d.ts +30 -0
  52. package/dist/platform/resolve.js +128 -0
  53. package/dist/platform/templateUpgrade.d.ts +56 -0
  54. package/dist/platform/templateUpgrade.js +476 -0
  55. package/dist/platform/upgradeReport.d.ts +132 -0
  56. package/dist/platform/upgradeReport.js +258 -0
  57. package/dist/platform/versionSummary.d.ts +82 -0
  58. package/dist/platform/versionSummary.js +181 -0
  59. package/dist/platform/zonePath.d.ts +154 -0
  60. package/dist/platform/zonePath.js +286 -0
  61. package/dist/platform/zones.d.ts +64 -0
  62. package/dist/platform/zones.js +79 -0
  63. package/dist/preview/api.d.ts +19 -0
  64. package/dist/preview/api.js +29 -0
  65. package/dist/publish/api.d.ts +32 -5
  66. package/dist/publish/api.js +19 -5
  67. package/dist/publish/command.d.ts +10 -0
  68. package/dist/publish/command.js +30 -2
  69. package/dist/publish/copy.d.ts +14 -1
  70. package/dist/publish/copy.js +32 -3
  71. package/dist/scaffold/index.d.ts +50 -3
  72. package/dist/scaffold/index.js +61 -1
  73. package/dist/shared/config.d.ts +16 -0
  74. package/dist/shared/config.js +9 -1
  75. package/dist/shared/countLimit.d.ts +109 -0
  76. package/dist/shared/countLimit.js +169 -0
  77. package/package.json +1 -1
  78. package/dist/dx/qa.f7.fixtures.d.ts +0 -73
  79. package/dist/dx/qa.f7.fixtures.js +0 -58
  80. package/dist/publish/qa.f4.fixtures.d.ts +0 -25
  81. package/dist/publish/qa.f4.fixtures.js +0 -140
  82. package/dist/relay/qa.f12.fixtures.d.ts +0 -154
  83. 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;