@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,952 @@
|
|
|
1
|
+
// Every line `vincentt domain` prints (how-it-works.md Part 2B, Flows C1-C7).
|
|
2
|
+
// Pure functions returning string arrays, so the transcripts are assertable
|
|
3
|
+
// without a terminal.
|
|
4
|
+
//
|
|
5
|
+
// THE VOCABULARY, fixed before anything else (Part 2B § The vocabulary). Eight
|
|
6
|
+
// words, each refusing a plausible alternative:
|
|
7
|
+
// - "Attach" / "Detach". `add`/`remove` are SILENT aliases. *Remove* reads as
|
|
8
|
+
// delete the row, and the row is NEVER deleted — its survival across a detach
|
|
9
|
+
// IS the takeover defence.
|
|
10
|
+
// - "Your Vincentt address", never "the default", "the fallback", "the free
|
|
11
|
+
// address". *Fallback* inverts B-F2-7 in a single word.
|
|
12
|
+
// - "Serving" / "not serving", never active/inactive, enabled/disabled,
|
|
13
|
+
// up/down. Serving is the only verb true in every state that blames nobody.
|
|
14
|
+
// - "DNS changed", never misconfigured/broken/invalid/error. The creator
|
|
15
|
+
// changed a record they own, which they are entitled to do (B-F1-7).
|
|
16
|
+
// - "Not connected", never removed/deleted/expired/suspended/offline.
|
|
17
|
+
// *Offline* is f4's word for a creator's deliberate choice.
|
|
18
|
+
// - "The value", never token/secret/key. Public in DNS by construction — the
|
|
19
|
+
// wrong noun makes creators hide it and reviewers think we leaked it.
|
|
20
|
+
// - "Whoever manages DNS for acme.com", not "your registrar", wherever the
|
|
21
|
+
// reader may not control the zone.
|
|
22
|
+
// - "Keep this one", on exactly one record.
|
|
23
|
+
//
|
|
24
|
+
// NEVER PRINTED, in any state: the org id, project id, PAT, distribution id,
|
|
25
|
+
// certificate ARN, KVS key, any resolver hostname of ours, a raw DNS library
|
|
26
|
+
// error, or a stack trace. THE VERIFICATION VALUE IS PRINTED — public by
|
|
27
|
+
// construction — and is never called a token, secret or key.
|
|
28
|
+
import { VALIDATION_RECORD_GONE, } from "./wire.js";
|
|
29
|
+
import { recordNamePairs } from "./names.js";
|
|
30
|
+
/** The section rule. Not f4's fenced `─── FIRST PUBLISH ───`: nothing here is irreversible. */
|
|
31
|
+
const RULE_WIDTH = 74;
|
|
32
|
+
function sectionRule(label) {
|
|
33
|
+
const head = ` ─── ${label} `;
|
|
34
|
+
const dashes = Math.max(3, RULE_WIDTH - head.length);
|
|
35
|
+
return `${head}${"─".repeat(dashes)}`;
|
|
36
|
+
}
|
|
37
|
+
function closingRule() {
|
|
38
|
+
return ` ${"─".repeat(RULE_WIDTH - 2)}`;
|
|
39
|
+
}
|
|
40
|
+
// ---- the state vocabulary ----------------------------------------------------
|
|
41
|
+
/**
|
|
42
|
+
* The state words are the console's badge words, LOWERCASED (transcript L43
|
|
43
|
+
* decision 3). One word set across both surfaces — if the console badge and this
|
|
44
|
+
* column disagree, the console label is wrong.
|
|
45
|
+
*
|
|
46
|
+
* An unrecognized state renders as ITSELF rather than as a plausible neighbour.
|
|
47
|
+
* A newer API sending an eighth state must not read as `not verified`, which
|
|
48
|
+
* would tell a creator nothing is wrong when something might be.
|
|
49
|
+
*/
|
|
50
|
+
export function stateWord(view) {
|
|
51
|
+
switch (view.state) {
|
|
52
|
+
case "pending":
|
|
53
|
+
return "not verified";
|
|
54
|
+
case "verified":
|
|
55
|
+
return "setting up";
|
|
56
|
+
case "live":
|
|
57
|
+
return "live";
|
|
58
|
+
case "failing":
|
|
59
|
+
return "dns changed";
|
|
60
|
+
case "held":
|
|
61
|
+
return "not connected";
|
|
62
|
+
case "cert_failed":
|
|
63
|
+
return "no certificate";
|
|
64
|
+
case "detached":
|
|
65
|
+
return "detached";
|
|
66
|
+
default:
|
|
67
|
+
return view.rawState || "unknown";
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
/** Is the domain serving right now? Drives the alias-stance line. */
|
|
71
|
+
export function isServing(view) {
|
|
72
|
+
return view.state === "live" || view.state === "failing";
|
|
73
|
+
}
|
|
74
|
+
// ---- the record block, BOTH NAME FORMS ---------------------------------------
|
|
75
|
+
/**
|
|
76
|
+
* The label for each name form. It names THE BOX ON THEIR SCREEN, never the DNS
|
|
77
|
+
* concept (Flow C1 rule 4): *relative* and *fully-qualified* are correct and
|
|
78
|
+
* useless — a creator who knew those words would not make this mistake.
|
|
79
|
+
*/
|
|
80
|
+
const SHORT_TELL = "← most registrars want this";
|
|
81
|
+
const LONG_TELL_1 = "← only if the field already";
|
|
82
|
+
const LONG_TELL_2 = "shows your full domain";
|
|
83
|
+
/**
|
|
84
|
+
* The rule label for one record, naming it by WHAT IT IS FOR.
|
|
85
|
+
*
|
|
86
|
+
* `CNAME · certificate` is what distinguishes the validation record at a glance
|
|
87
|
+
* from the pointer CNAME, which is the same type. The label is derived from the
|
|
88
|
+
* record's ROLE when the API names one and falls back to the bare type — it is
|
|
89
|
+
* never invented from the record's name, because a name-sniffing heuristic
|
|
90
|
+
* (`starts with _`) would mislabel a future record.
|
|
91
|
+
*/
|
|
92
|
+
export function recordLabel(record, role) {
|
|
93
|
+
if (role === "certificate")
|
|
94
|
+
return `${record.type} · certificate`;
|
|
95
|
+
return record.type;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Which record carries the keep-this-one caption?
|
|
99
|
+
*
|
|
100
|
+
* The certificate validation record, and ONLY it (Flow C1). Its deletion is the
|
|
101
|
+
* only fault in this feature with a delayed consequence — every other record
|
|
102
|
+
* fails immediately and visibly. `"keep this"` beside three records reads as
|
|
103
|
+
* boilerplate, which is why exactly one carries it.
|
|
104
|
+
*
|
|
105
|
+
* Decided from the ROLE the API names, never sniffed from the value. A heuristic
|
|
106
|
+
* on `acm-validations.aws` would break the day the CA changes, and it would break
|
|
107
|
+
* silently — into the arm where the caption is ABSENT, which is the expensive
|
|
108
|
+
* direction.
|
|
109
|
+
*/
|
|
110
|
+
export function carriesKeepCaption(role) {
|
|
111
|
+
return role === "certificate";
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* The keep-this-one caption. The caption NAMES THE DELAY explicitly, because a
|
|
115
|
+
* creator who reads only the first clause must not conclude they can ignore it.
|
|
116
|
+
*
|
|
117
|
+
* It is NOT an alert and carries no tint — a warning colour on a record someone
|
|
118
|
+
* is being asked to CREATE reads as an incident at the moment of creation.
|
|
119
|
+
*/
|
|
120
|
+
export function keepCaption() {
|
|
121
|
+
return [
|
|
122
|
+
" Keep this one. It is what renews the certificate, on its own, for as long",
|
|
123
|
+
" as it is published. Deleting it doesn't break anything today - it stops",
|
|
124
|
+
" the renewal months later.",
|
|
125
|
+
];
|
|
126
|
+
}
|
|
127
|
+
const NAME_COLUMN = 12;
|
|
128
|
+
const VALUE_COLUMN = 44;
|
|
129
|
+
/**
|
|
130
|
+
* One record's block, with BOTH NAME FORMS, SHORT FIRST.
|
|
131
|
+
*
|
|
132
|
+
* Four rules from Flow C1, and every one of them is load-bearing:
|
|
133
|
+
* 1. NEVER ONE FORM. Printing the FQDN alone is what every researched platform
|
|
134
|
+
* does, and it is why this failure is universal.
|
|
135
|
+
* 2. NEVER "or". That hands the creator a decision with no basis for making it;
|
|
136
|
+
* two labelled lines give them a test they can run against the screen in
|
|
137
|
+
* front of them.
|
|
138
|
+
* 3. THE SHORT FORM IS FIRST — the majority want it, and the first line is what
|
|
139
|
+
* a hurrying creator copies. (QA-F16-C1 asserts this.)
|
|
140
|
+
* 4. Labels name the box on their screen, never the DNS concept.
|
|
141
|
+
*
|
|
142
|
+
* When the relative form cannot be derived (see names.ts) only the FQDN prints,
|
|
143
|
+
* WITHOUT the short-form tell. Printing an underived relative name would be the
|
|
144
|
+
* wrong-form paste this block exists to prevent, and printing the tell beside a
|
|
145
|
+
* lone FQDN would claim most registrars want the value on that line.
|
|
146
|
+
*/
|
|
147
|
+
export function recordBlock(record, pair, role) {
|
|
148
|
+
const out = ["", sectionRule(recordLabel(record, role)), ""];
|
|
149
|
+
const label = (s) => ` ${s.padEnd(NAME_COLUMN - 4)}`;
|
|
150
|
+
if (pair.relative !== undefined) {
|
|
151
|
+
// The tell column is computed from the ACTUAL widths of the two names, never
|
|
152
|
+
// from a fixed constant. A name longer than the constant used to run into
|
|
153
|
+
// the `←`, which reads as one token and is exactly the ambiguity this block
|
|
154
|
+
// exists to remove. The minimum keeps the common case aligned with the value
|
|
155
|
+
// rows below it.
|
|
156
|
+
// TWO spaces minimum, not one: at one the longest name and the `←` read as a
|
|
157
|
+
// single token, which is the ambiguity this block exists to remove.
|
|
158
|
+
const nameWidth = Math.max(VALUE_COLUMN - NAME_COLUMN, pair.relative.length + 2, pair.fqdn.length + 2);
|
|
159
|
+
const gutter = " ".repeat(NAME_COLUMN - 4);
|
|
160
|
+
out.push(`${label("Name")}${pair.relative.padEnd(nameWidth)}${SHORT_TELL}`);
|
|
161
|
+
out.push(` ${gutter}${pair.fqdn.padEnd(nameWidth)}${LONG_TELL_1}`);
|
|
162
|
+
// The continuation of the long tell hangs under its first line, indented to
|
|
163
|
+
// the tell's own column rather than to the name column.
|
|
164
|
+
out.push(` ${gutter}${" ".repeat(nameWidth)} ${LONG_TELL_2}`);
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
out.push(`${label("Name")}${pair.fqdn}`);
|
|
168
|
+
}
|
|
169
|
+
out.push(`${label("Value")}${record.value}`);
|
|
170
|
+
// The TTL line names no number of ours. A TTL the platform printed becomes a
|
|
171
|
+
// value a creator tries to match exactly, and registrars offer different sets.
|
|
172
|
+
out.push(`${label("TTL")}whatever your registrar offers; the lowest is fine`);
|
|
173
|
+
if (carriesKeepCaption(role)) {
|
|
174
|
+
out.push("", ...keepCaption());
|
|
175
|
+
}
|
|
176
|
+
return out;
|
|
177
|
+
}
|
|
178
|
+
/**
|
|
179
|
+
* The disambiguating paragraph. PROSE, not a footnote, and it names the VISIBLE
|
|
180
|
+
* TELL — *"if yours already shows `.acme.com` beside the box"*. That is the one
|
|
181
|
+
* observation that resolves the ambiguity without teaching DNS, and it is the
|
|
182
|
+
* sentence that has to survive a skim.
|
|
183
|
+
*/
|
|
184
|
+
export function twoFormParagraph(zone) {
|
|
185
|
+
return [
|
|
186
|
+
` Registrars disagree about this field. If yours already shows ".${zone}"`,
|
|
187
|
+
" beside the box, use the short name. If it wants the whole thing, use the",
|
|
188
|
+
" long one. Picking the wrong one creates a record that never verifies.",
|
|
189
|
+
];
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* The whole three-record block, fenced together ONCE.
|
|
193
|
+
*
|
|
194
|
+
* `roles` is parallel to `records` and comes from the API. The CLI does not
|
|
195
|
+
* decide which record is the certificate one.
|
|
196
|
+
*/
|
|
197
|
+
export function allRecordBlocks(records, domain, roles = []) {
|
|
198
|
+
const pairs = recordNamePairs(records, domain);
|
|
199
|
+
const out = [];
|
|
200
|
+
records.forEach((r, i) => out.push(...recordBlock(r, pairs[i], roles[i])));
|
|
201
|
+
out.push("", closingRule());
|
|
202
|
+
return out;
|
|
203
|
+
}
|
|
204
|
+
// ---- the alias stance --------------------------------------------------------
|
|
205
|
+
/**
|
|
206
|
+
* The permanent address, named UNPROMPTED, in every state where the custom domain
|
|
207
|
+
* is not working (CLI convention 4).
|
|
208
|
+
*
|
|
209
|
+
* "Your Vincentt address" — never "the default", "the fallback", "the free
|
|
210
|
+
* address". The line costs one line and it is the single most valuable thing this
|
|
211
|
+
* feature does.
|
|
212
|
+
*
|
|
213
|
+
* Returns NOTHING when the API did not send a permanent address. A line naming an
|
|
214
|
+
* address the CLI assembled itself would print the wrong host on staging and dev,
|
|
215
|
+
* for the reason `liveUrl` is server-computed everywhere else in this CLI.
|
|
216
|
+
*/
|
|
217
|
+
export function aliasStance(domain, permanentAddress, serving) {
|
|
218
|
+
if (!permanentAddress)
|
|
219
|
+
return [];
|
|
220
|
+
if (serving) {
|
|
221
|
+
return [` It serves the same version as ${permanentAddress}, which is unchanged.`];
|
|
222
|
+
}
|
|
223
|
+
return [
|
|
224
|
+
` ${domain} is not serving yet. ${permanentAddress} is, and is`,
|
|
225
|
+
" not affected by anything here.",
|
|
226
|
+
];
|
|
227
|
+
}
|
|
228
|
+
/** The same fact for a domain that WAS serving and stopped. */
|
|
229
|
+
export function aliasStanceStopped(permanentAddress) {
|
|
230
|
+
if (!permanentAddress)
|
|
231
|
+
return [];
|
|
232
|
+
return [` ${permanentAddress} is serving, and is not affected.`];
|
|
233
|
+
}
|
|
234
|
+
// ---- the findings, the feature's differentiation -----------------------------
|
|
235
|
+
/**
|
|
236
|
+
* Severity per finding code. THE GLYPH CARRIES THE DISTINCTION AND THE EXIT CODE
|
|
237
|
+
* FOLLOWS THE GLYPH, NOT THE COUNT (CLI convention 3).
|
|
238
|
+
*
|
|
239
|
+
* `✗` blocking = this is why it does not work.
|
|
240
|
+
* `!` advisory = observed, and it blocks nothing.
|
|
241
|
+
*
|
|
242
|
+
* PROXY_IN_FRONT is advisory and NEVER blocking: a creator who wants a proxy in
|
|
243
|
+
* front is making a choice we do not own, the detection is heuristic, and the
|
|
244
|
+
* register must survive being wrong.
|
|
245
|
+
*
|
|
246
|
+
* VALIDATION_RECORD_GONE is advisory (D-44) — the row works and the horizon
|
|
247
|
+
* is months. Escalating its glyph would teach a creator that `✗` sometimes means
|
|
248
|
+
* *working fine*, which is how the severity ladder stops carrying information.
|
|
249
|
+
*
|
|
250
|
+
* An UNRECOGNIZED code defaults to ADVISORY. That direction is deliberate: a
|
|
251
|
+
* newer API's unknown finding must not fail a verify that the server considered
|
|
252
|
+
* successful, because the exit code is what an agent branches on. The finding
|
|
253
|
+
* still PRINTS in full — nothing is hidden, only its effect on the exit code is
|
|
254
|
+
* conservative.
|
|
255
|
+
*/
|
|
256
|
+
const BLOCKING_CODES = new Set([
|
|
257
|
+
"TOKEN_MISSING",
|
|
258
|
+
"TOKEN_MISMATCH",
|
|
259
|
+
"WRONG_RECORD_TYPE",
|
|
260
|
+
"CNAME_WRONG_TARGET",
|
|
261
|
+
"CONFLICTING_RECORDS",
|
|
262
|
+
"CAA_BLOCKS_ISSUANCE",
|
|
263
|
+
"MALFORMED_ZONE",
|
|
264
|
+
]);
|
|
265
|
+
/**
|
|
266
|
+
* The severity of one finding.
|
|
267
|
+
*
|
|
268
|
+
* `wireSeverity` is the API's own classification when it sends one, and it WINS:
|
|
269
|
+
* the exit code the CLI returns must not disagree with the state the server put
|
|
270
|
+
* the domain in, and the server is the party that performed the observation.
|
|
271
|
+
*
|
|
272
|
+
* The local table is the fallback for an API that sends no severity, and it is
|
|
273
|
+
* derived from the design record rather than from the producer:
|
|
274
|
+
* - PROXY_IN_FRONT is advisory and NEVER blocking (Scenario 4's accepted
|
|
275
|
+
* constraint: heuristic, worded as an observation, "never blocking").
|
|
276
|
+
* - STALE_ACME_CHALLENGE is advisory ("it blocks nothing; you can delete it").
|
|
277
|
+
* - UNEXPECTED_AAAA and TOKEN_STALE_PRESENT are advisory: each is an
|
|
278
|
+
* observation beside a row whose failure, if any, is named by another finding.
|
|
279
|
+
*
|
|
280
|
+
* KNOWN DIVERGENCE, reported rather than papered over: the producer's
|
|
281
|
+
* `DomainFinding.Severity()` currently classes ONLY VALIDATION_RECORD_GONE as
|
|
282
|
+
* advisory, which would make a proxy observation alone exit non-zero and
|
|
283
|
+
* contradict "an all-advisory report exits 0". Honoring a wire severity means
|
|
284
|
+
* the CLI tracks the server once that is settled instead of holding a second
|
|
285
|
+
* opinion in a second language.
|
|
286
|
+
*/
|
|
287
|
+
export function findingSeverity(code, wireSeverity) {
|
|
288
|
+
if (wireSeverity === "advisory")
|
|
289
|
+
return "advisory";
|
|
290
|
+
if (wireSeverity === "fault" || wireSeverity === "blocking")
|
|
291
|
+
return "blocking";
|
|
292
|
+
return BLOCKING_CODES.has(code) ? "blocking" : "advisory";
|
|
293
|
+
}
|
|
294
|
+
export function severityGlyph(s) {
|
|
295
|
+
return s === "blocking" ? "✗" : "!";
|
|
296
|
+
}
|
|
297
|
+
/**
|
|
298
|
+
* The title and remedy body for each finding code. COPY IS FIXED IN ONE FILE,
|
|
299
|
+
* KEYED BY THE CLOSED ENUM, so neither surface invents a string.
|
|
300
|
+
*
|
|
301
|
+
* Every entry has a title naming THE SPECIFIC FAULT and a body naming the remedy
|
|
302
|
+
* IN THE CREATOR'S TERMS. Neither ever says "contact support" — that implies a
|
|
303
|
+
* queue, a state machine and an SLA, none of which exist.
|
|
304
|
+
*/
|
|
305
|
+
const FINDING_COPY = {
|
|
306
|
+
TOKEN_MISSING: {
|
|
307
|
+
title: () => "We can't find the TXT record",
|
|
308
|
+
body: [
|
|
309
|
+
"Create it exactly as shown above. If you just added it, DNS can take a",
|
|
310
|
+
"few minutes to publish.",
|
|
311
|
+
],
|
|
312
|
+
},
|
|
313
|
+
TOKEN_MISMATCH: {
|
|
314
|
+
title: () => "The TXT record holds a different value",
|
|
315
|
+
body: ["Usually means it was copied from another workspace or another domain."],
|
|
316
|
+
},
|
|
317
|
+
TOKEN_STALE_PRESENT: {
|
|
318
|
+
title: () => "An older verification value is still published",
|
|
319
|
+
body: [
|
|
320
|
+
"There is more than one value at that name. Delete the ones that don't",
|
|
321
|
+
"match what's shown above.",
|
|
322
|
+
],
|
|
323
|
+
},
|
|
324
|
+
WRONG_RECORD_TYPE: {
|
|
325
|
+
title: (d) => `${d} is an A record, not a CNAME`,
|
|
326
|
+
body: [
|
|
327
|
+
"An A record points at a fixed address; ours moves. Delete the A record",
|
|
328
|
+
"and create the CNAME. Most registrars won't let both exist.",
|
|
329
|
+
],
|
|
330
|
+
},
|
|
331
|
+
CNAME_WRONG_TARGET: {
|
|
332
|
+
title: () => "The CNAME points somewhere else",
|
|
333
|
+
body: ["Change it to the value shown above."],
|
|
334
|
+
},
|
|
335
|
+
CONFLICTING_RECORDS: {
|
|
336
|
+
title: (d) => `${d} has both an A record and a CNAME`,
|
|
337
|
+
body: [
|
|
338
|
+
"Only one can apply. Delete the A record and keep the CNAME shown above.",
|
|
339
|
+
],
|
|
340
|
+
},
|
|
341
|
+
UNEXPECTED_AAAA: {
|
|
342
|
+
title: () => "There's also an AAAA record",
|
|
343
|
+
body: [
|
|
344
|
+
"IPv6 visitors will go to that address instead of ours. Delete it unless",
|
|
345
|
+
"you meant it.",
|
|
346
|
+
],
|
|
347
|
+
},
|
|
348
|
+
PROXY_IN_FRONT: {
|
|
349
|
+
// Worded as an OBSERVATION, never a verdict. The mechanism names it heuristic
|
|
350
|
+
// and never blocking, and the register must survive being wrong. It reports a
|
|
351
|
+
// proxy when it sees one and NEVER asserts absence.
|
|
352
|
+
title: () => "The address resolves into another provider's network",
|
|
353
|
+
body: [
|
|
354
|
+
"Something is proxying this domain in front of us. That may be",
|
|
355
|
+
"deliberate and nothing here blocks it - but while it's on, the",
|
|
356
|
+
"certificate we issue may not be the one your visitors receive.",
|
|
357
|
+
],
|
|
358
|
+
},
|
|
359
|
+
CAA_BLOCKS_ISSUANCE: {
|
|
360
|
+
title: () => "A CAA record stops us issuing",
|
|
361
|
+
body: [
|
|
362
|
+
"This is often set on the parent domain rather than on this name, and",
|
|
363
|
+
"often by whoever manages the company's DNS rather than by you.",
|
|
364
|
+
],
|
|
365
|
+
},
|
|
366
|
+
STALE_ACME_CHALLENGE: {
|
|
367
|
+
title: () => "An old certificate-validation record is still published",
|
|
368
|
+
body: ["Left over from a previous host. It blocks nothing; you can delete it."],
|
|
369
|
+
},
|
|
370
|
+
VALIDATION_RECORD_GONE: {
|
|
371
|
+
// D-44. NO DATE, NO DAY COUNT — the remaining-days figure IS on the wire, for
|
|
372
|
+
// the operator, and the finding shape carries NO NUMERIC SLOT AT ALL, so it
|
|
373
|
+
// cannot reach this surface. A number converts an act-now into a
|
|
374
|
+
// schedule-later, and the honest remedy is *put it back now*.
|
|
375
|
+
//
|
|
376
|
+
// The reassurance clause is INVERTED here: elsewhere it reassures about the
|
|
377
|
+
// Vincentt address; here the domain itself is fine, so it reassures about
|
|
378
|
+
// TODAY and names the delay — in that order.
|
|
379
|
+
title: () => "The certificate's validation record is gone",
|
|
380
|
+
body: [
|
|
381
|
+
"is serving normally and nothing stops today. But that",
|
|
382
|
+
"record is what renews the certificate, so put it back before the",
|
|
383
|
+
"current one runs out.",
|
|
384
|
+
],
|
|
385
|
+
},
|
|
386
|
+
MALFORMED_ZONE: {
|
|
387
|
+
title: () => "We couldn't read the DNS for this name",
|
|
388
|
+
body: [
|
|
389
|
+
"The zone returned more records than we can read, or returned them",
|
|
390
|
+
"malformed. Whoever manages DNS for this domain can see more than we can.",
|
|
391
|
+
],
|
|
392
|
+
},
|
|
393
|
+
};
|
|
394
|
+
const OBSERVED_LABEL_WIDTH = 10;
|
|
395
|
+
/**
|
|
396
|
+
* One finding, rendered.
|
|
397
|
+
*
|
|
398
|
+
* `expected` / `observed` are STACKED AND COLUMN-ALIGNED, never inline — f2's
|
|
399
|
+
* rule for two values a creator must judge, and the shape that makes the
|
|
400
|
+
* difference visible at a glance. This is why f16 beats every researched
|
|
401
|
+
* competitor, all of whom print only the expected record.
|
|
402
|
+
*
|
|
403
|
+
* `observed` IS PRINTED AS THE STRING THE WIRE SENT. It is not parsed, not
|
|
404
|
+
* re-rendered, and not reformatted. The wire says it is a string and never
|
|
405
|
+
* structured precisely so a client cannot lose the fidelity that is the point of
|
|
406
|
+
* the ruling.
|
|
407
|
+
*
|
|
408
|
+
* `expected` is omitted when the API sent none — the pair degrades to the
|
|
409
|
+
* observed line alone rather than printing an invented expectation.
|
|
410
|
+
*/
|
|
411
|
+
export function findingLines(f, domain) {
|
|
412
|
+
const severity = findingSeverity(f.code, f.severity);
|
|
413
|
+
const copy = FINDING_COPY[f.code];
|
|
414
|
+
const glyph = severityGlyph(severity);
|
|
415
|
+
// An unrecognized code still prints, with the code itself as the title. A
|
|
416
|
+
// finding the CLI cannot name is strictly better rendered as an opaque title
|
|
417
|
+
// plus its observed value than dropped — the observed value is the diagnosis.
|
|
418
|
+
const title = copy ? copy.title(domain) : f.code;
|
|
419
|
+
const out = [` ${glyph} ${title}`];
|
|
420
|
+
const label = (l) => ` ${l.padEnd(OBSERVED_LABEL_WIDTH)}`;
|
|
421
|
+
if (f.expected !== undefined)
|
|
422
|
+
out.push(`${label("expected")}${f.expected}`);
|
|
423
|
+
out.push(`${label("observed")}${f.observed}`);
|
|
424
|
+
if (copy) {
|
|
425
|
+
const indent = " ";
|
|
426
|
+
// The certificate advisory's body opens mid-sentence so the domain leads it.
|
|
427
|
+
const body = f.code === VALIDATION_RECORD_GONE
|
|
428
|
+
? [`${domain} ${copy.body[0]}`, ...copy.body.slice(1)]
|
|
429
|
+
: copy.body;
|
|
430
|
+
for (const line of body)
|
|
431
|
+
out.push(`${indent}${line}`);
|
|
432
|
+
}
|
|
433
|
+
return out;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* The whole findings report, ORDERED `✗` BEFORE `!`.
|
|
437
|
+
*
|
|
438
|
+
* A creator who reads only the first block reads the thing that is actually
|
|
439
|
+
* stopping them. Within each severity the API's own order is preserved — it
|
|
440
|
+
* observed them and it orders cause before consequence.
|
|
441
|
+
*/
|
|
442
|
+
export function findingsReport(findings, domain) {
|
|
443
|
+
const blocking = findings.filter((f) => findingSeverity(f.code, f.severity) === "blocking");
|
|
444
|
+
const advisory = findings.filter((f) => findingSeverity(f.code, f.severity) === "advisory");
|
|
445
|
+
const out = [];
|
|
446
|
+
for (const f of [...blocking, ...advisory]) {
|
|
447
|
+
out.push("", ...findingLines(f, domain));
|
|
448
|
+
}
|
|
449
|
+
return out;
|
|
450
|
+
}
|
|
451
|
+
/**
|
|
452
|
+
* THE EXIT CODE FOLLOWS THE GLYPH, NEVER THE COUNT (CLI convention 3,
|
|
453
|
+
* QA-F16-C2).
|
|
454
|
+
*
|
|
455
|
+
* An all-advisory report exits 0. A report with ten advisories and no blocking
|
|
456
|
+
* finding exits 0. A report with one blocking finding exits 1 regardless of how
|
|
457
|
+
* many advisories sit beside it.
|
|
458
|
+
*
|
|
459
|
+
* This is the whole of the mapping and it is deliberately a function of the
|
|
460
|
+
* findings alone, so it cannot be reached through a code path that also consults
|
|
461
|
+
* a count.
|
|
462
|
+
*/
|
|
463
|
+
export function exitCodeForFindings(findings) {
|
|
464
|
+
return findings.some((f) => findingSeverity(f.code, f.severity) === "blocking")
|
|
465
|
+
? 1
|
|
466
|
+
: 0;
|
|
467
|
+
}
|
|
468
|
+
/** f4's after-a-non-zero-exit rule. An agent's most expensive failure is retrying against a half-applied state. */
|
|
469
|
+
export const NOTHING_WAS_CHANGED = " Nothing was changed.";
|
|
470
|
+
export const NOTHING_WAS_CLAIMED = " Nothing was claimed.";
|
|
471
|
+
// ---- the refusals ------------------------------------------------------------
|
|
472
|
+
/**
|
|
473
|
+
* At the ceiling. A LIMIT IS PRINTED AS A NUMBER, NEVER AS A POLICY OR A PLAN
|
|
474
|
+
* (f4's CLI rule), and the remedy names a HUMAN ROLE per f12's rule that unbidden
|
|
475
|
+
* output names a fact, never an instruction to buy.
|
|
476
|
+
*
|
|
477
|
+
* No price, no plan name other than the one they are on, no URL. Clause 4 reaches
|
|
478
|
+
* the CLI too.
|
|
479
|
+
*/
|
|
480
|
+
export function refusedAtCeiling(limit) {
|
|
481
|
+
return [
|
|
482
|
+
"",
|
|
483
|
+
`✗ This workspace is using all ${limit} of the custom domains its plan carries.`,
|
|
484
|
+
"",
|
|
485
|
+
" Detach one to attach this:",
|
|
486
|
+
" vincentt domain list",
|
|
487
|
+
" vincentt domain detach <domain>",
|
|
488
|
+
"",
|
|
489
|
+
" An owner can change the workspace's plan in the console.",
|
|
490
|
+
"",
|
|
491
|
+
NOTHING_WAS_CLAIMED,
|
|
492
|
+
"",
|
|
493
|
+
];
|
|
494
|
+
}
|
|
495
|
+
/**
|
|
496
|
+
* Already attached — THE ANTI-ORACLE ANSWER.
|
|
497
|
+
*
|
|
498
|
+
* BYTE-IDENTICAL whether the name is held by this workspace or by a stranger's.
|
|
499
|
+
* It never says *another workspace*, never says *yours*, never names a project.
|
|
500
|
+
* f2's slug-collision discipline applied to the second global host namespace.
|
|
501
|
+
*
|
|
502
|
+
* The `list` line is safe precisely because `list` only ever shows the caller's
|
|
503
|
+
* own domains — a creator who genuinely holds the name finds it there.
|
|
504
|
+
*
|
|
505
|
+
* IT TAKES NO ARGUMENT BEYOND THE HOSTNAME. A "you already have this"
|
|
506
|
+
* convenience message is the exact shape that breaks this, and it is the first
|
|
507
|
+
* thing a tidying PR adds.
|
|
508
|
+
*/
|
|
509
|
+
export function refusedAlreadyAttached(domain) {
|
|
510
|
+
return [
|
|
511
|
+
"",
|
|
512
|
+
`✗ ${domain} is already attached to a project.`,
|
|
513
|
+
"",
|
|
514
|
+
" If it's yours, find it with:",
|
|
515
|
+
" vincentt domain list",
|
|
516
|
+
"",
|
|
517
|
+
NOTHING_WAS_CLAIMED,
|
|
518
|
+
"",
|
|
519
|
+
];
|
|
520
|
+
}
|
|
521
|
+
/** A platform address. */
|
|
522
|
+
export function refusedPlatformHost() {
|
|
523
|
+
return [
|
|
524
|
+
"",
|
|
525
|
+
"✗ That's already a Vincentt address.",
|
|
526
|
+
"",
|
|
527
|
+
" Custom domains are for names you own at your own registrar.",
|
|
528
|
+
"",
|
|
529
|
+
NOTHING_WAS_CLAIMED,
|
|
530
|
+
"",
|
|
531
|
+
];
|
|
532
|
+
}
|
|
533
|
+
/**
|
|
534
|
+
* A root domain. The reason is one clause and it is ABOUT THE CREATOR'S
|
|
535
|
+
* REGISTRAR, not about our infrastructure.
|
|
536
|
+
*
|
|
537
|
+
* A refusal with no reason gets retried; a refusal whose reason is *"our CDN
|
|
538
|
+
* doesn't support it"* sounds like something we might fix on request.
|
|
539
|
+
*/
|
|
540
|
+
export function refusedRootDomain(domain) {
|
|
541
|
+
const zone = domain || "acme.com";
|
|
542
|
+
return [
|
|
543
|
+
"",
|
|
544
|
+
`✗ Root domains aren't supported - use a subdomain like ar.${zone}.`,
|
|
545
|
+
"",
|
|
546
|
+
" A root domain needs a record type that works differently at every",
|
|
547
|
+
" registrar. A subdomain uses a plain CNAME, which is the same everywhere.",
|
|
548
|
+
"",
|
|
549
|
+
NOTHING_WAS_CLAIMED,
|
|
550
|
+
"",
|
|
551
|
+
];
|
|
552
|
+
}
|
|
553
|
+
/** Not on a plan that carries custom domains. Names the plan they are on and nothing else. */
|
|
554
|
+
export function refusedNoEntitlement(planName) {
|
|
555
|
+
const plan = planName ? ` This workspace is on ${planName}.` : "";
|
|
556
|
+
return [
|
|
557
|
+
"",
|
|
558
|
+
`✗ Custom domains are part of the paid plans.${plan}`,
|
|
559
|
+
"",
|
|
560
|
+
" An owner can change the workspace's plan in the console.",
|
|
561
|
+
"",
|
|
562
|
+
NOTHING_WAS_CLAIMED,
|
|
563
|
+
"",
|
|
564
|
+
];
|
|
565
|
+
}
|
|
566
|
+
/**
|
|
567
|
+
* A verify refused by the rate limit.
|
|
568
|
+
*
|
|
569
|
+
* `Retry-After` says HOW LONG, never WHICH LIMIT. All refusal bodies are
|
|
570
|
+
* byte-identical across causes, so this copy names no limit family.
|
|
571
|
+
*/
|
|
572
|
+
export function refusedTooSoon(retryAfterSec) {
|
|
573
|
+
const when = retryAfterSec ? ` Try again in ${approxWait(retryAfterSec)}.` : "";
|
|
574
|
+
return [
|
|
575
|
+
"",
|
|
576
|
+
`✗ That was checked very recently.${when}`,
|
|
577
|
+
"",
|
|
578
|
+
" We also check on our own every few hours.",
|
|
579
|
+
"",
|
|
580
|
+
NOTHING_WAS_CHANGED,
|
|
581
|
+
"",
|
|
582
|
+
];
|
|
583
|
+
}
|
|
584
|
+
function approxWait(seconds) {
|
|
585
|
+
if (seconds < 90)
|
|
586
|
+
return "about a minute";
|
|
587
|
+
const minutes = Math.round(seconds / 60);
|
|
588
|
+
if (minutes < 60)
|
|
589
|
+
return `about ${minutes} minutes`;
|
|
590
|
+
const hours = Math.round(minutes / 60);
|
|
591
|
+
return `about ${hours} hour${hours === 1 ? "" : "s"}`;
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* The uniform not-found. A hostname not attached to THIS project returns the same
|
|
595
|
+
* 404 as one that does not exist, one held by a stranger, and a version that does
|
|
596
|
+
* not exist — so the CLI's copy for it must be equally uniform.
|
|
597
|
+
*/
|
|
598
|
+
export function notAttached(domain) {
|
|
599
|
+
return [
|
|
600
|
+
"",
|
|
601
|
+
`✗ ${domain} isn't attached to this project.`,
|
|
602
|
+
"",
|
|
603
|
+
" See what is:",
|
|
604
|
+
" vincentt domain list",
|
|
605
|
+
"",
|
|
606
|
+
NOTHING_WAS_CHANGED,
|
|
607
|
+
"",
|
|
608
|
+
];
|
|
609
|
+
}
|
|
610
|
+
export function attachSuccess(i) {
|
|
611
|
+
const zone = i.domain.slice(i.domain.indexOf(".") + 1);
|
|
612
|
+
const header = i.projectName
|
|
613
|
+
? [i.orgName ? `→ ${i.projectName} · ${i.orgName}` : `→ ${i.projectName}`]
|
|
614
|
+
: [];
|
|
615
|
+
return [
|
|
616
|
+
"",
|
|
617
|
+
...header,
|
|
618
|
+
`✓ Claimed ${i.domain}`,
|
|
619
|
+
"",
|
|
620
|
+
` Create these three records at whoever manages DNS for ${zone}.`,
|
|
621
|
+
" All three are required - the CNAME sends visitors here, the TXT proves the",
|
|
622
|
+
" domain is yours, and the third keeps the certificate renewing.",
|
|
623
|
+
...allRecordBlocks(i.records, i.domain, i.roles ?? []),
|
|
624
|
+
"",
|
|
625
|
+
...twoFormParagraph(zone),
|
|
626
|
+
"",
|
|
627
|
+
...aliasStance(i.domain, i.permanentAddress, false),
|
|
628
|
+
"",
|
|
629
|
+
" Check when the records are in:",
|
|
630
|
+
` vincentt domain verify ${i.domain}`,
|
|
631
|
+
"",
|
|
632
|
+
" We also check on our own every few hours.",
|
|
633
|
+
"",
|
|
634
|
+
];
|
|
635
|
+
}
|
|
636
|
+
// ---- verify ------------------------------------------------------------------
|
|
637
|
+
export function verifyReading(domain) {
|
|
638
|
+
return `→ Reading DNS for ${domain}…`;
|
|
639
|
+
}
|
|
640
|
+
/**
|
|
641
|
+
* Not ready. Every finding names what was actually observed.
|
|
642
|
+
*
|
|
643
|
+
* The remedy is THE EXACT COMMAND, last. Not "try again", not a URL.
|
|
644
|
+
*/
|
|
645
|
+
export function verifyNotReady(domain, findings, permanentAddress) {
|
|
646
|
+
return [
|
|
647
|
+
"",
|
|
648
|
+
`✗ ${domain} isn't ready yet.`,
|
|
649
|
+
...findingsReport(findings, domain),
|
|
650
|
+
"",
|
|
651
|
+
...aliasStanceStopped(permanentAddress),
|
|
652
|
+
"",
|
|
653
|
+
" Fix what you can, then:",
|
|
654
|
+
` vincentt domain verify ${domain}`,
|
|
655
|
+
"",
|
|
656
|
+
NOTHING_WAS_CHANGED,
|
|
657
|
+
"",
|
|
658
|
+
];
|
|
659
|
+
}
|
|
660
|
+
/**
|
|
661
|
+
* Ready, and live.
|
|
662
|
+
*
|
|
663
|
+
* The `!` about hard-coded canonical/og:url sits BELOW the success because the
|
|
664
|
+
* creator's next act is to share the address, and the advisory is about what
|
|
665
|
+
* happens when they do. IT IS AN ADVISORY AND NEVER AN EDIT — B-F4-2.
|
|
666
|
+
*/
|
|
667
|
+
export function verifyLive(domain, permanentAddress, liveVersion) {
|
|
668
|
+
const version = liveVersion === undefined ? "" : ` (v${liveVersion})`;
|
|
669
|
+
const same = permanentAddress
|
|
670
|
+
? [
|
|
671
|
+
"",
|
|
672
|
+
` It serves the same version as ${permanentAddress}${version},`,
|
|
673
|
+
" which is unchanged.",
|
|
674
|
+
]
|
|
675
|
+
: [];
|
|
676
|
+
return [
|
|
677
|
+
`✓ ${domain} is yours - the TXT record matches.`,
|
|
678
|
+
"✓ The CNAME points at us.",
|
|
679
|
+
`✓ Live at https://${domain}`,
|
|
680
|
+
...same,
|
|
681
|
+
"",
|
|
682
|
+
"! If your build hard-codes its Vincentt address in a canonical or og:url",
|
|
683
|
+
" tag, link previews on the new address will still point at the old one.",
|
|
684
|
+
" We don't change your files - edit the source and publish again.",
|
|
685
|
+
"",
|
|
686
|
+
];
|
|
687
|
+
}
|
|
688
|
+
/**
|
|
689
|
+
* Ready, serving, but with advisory findings (D-44's case among them).
|
|
690
|
+
*
|
|
691
|
+
* EXIT 0, because every finding is `!` and the domain is serving. The success
|
|
692
|
+
* lines above are UNCHANGED — escalating the glyph would teach a creator that `✗`
|
|
693
|
+
* sometimes means *working fine*.
|
|
694
|
+
*
|
|
695
|
+
* When the certificate validation record is the missing one, THE RECORD IS
|
|
696
|
+
* RE-PRINTED: the entire cause of this state is a record the creator tidied away,
|
|
697
|
+
* and the remedy has to be on the screen rather than a command away. This is the
|
|
698
|
+
* one place a `verify` success prints a record block.
|
|
699
|
+
*/
|
|
700
|
+
export function verifyServingWithAdvisories(domain, findings, reprint, roles) {
|
|
701
|
+
const out = [
|
|
702
|
+
`✓ ${domain} is yours - the TXT record matches.`,
|
|
703
|
+
"✓ The CNAME points at us.",
|
|
704
|
+
`✓ Serving at https://${domain}`,
|
|
705
|
+
...findingsReport(findings, domain),
|
|
706
|
+
];
|
|
707
|
+
if (reprint.length > 0) {
|
|
708
|
+
out.push(...allRecordBlocks(reprint, domain, roles));
|
|
709
|
+
}
|
|
710
|
+
out.push("");
|
|
711
|
+
return out;
|
|
712
|
+
}
|
|
713
|
+
/**
|
|
714
|
+
* Flow C5 — `failing`: STILL SERVING, and the reader can still prevent the stop.
|
|
715
|
+
*
|
|
716
|
+
* This arm exists because the serving-with-advisories one is FALSE here. That
|
|
717
|
+
* one opens *"✓ ar.acme.com is yours — the TXT record matches"*, which sat
|
|
718
|
+
* directly above *"✗ We can't find the TXT record"* — the screen contradicting
|
|
719
|
+
* itself in two adjacent lines. A `failing` domain is serving on a record we can
|
|
720
|
+
* no longer see, so nothing about the DNS may be reported as confirmed.
|
|
721
|
+
*
|
|
722
|
+
* The date is ABSOLUTE, never a countdown, and the stop is described BY WHAT A
|
|
723
|
+
* VISITOR SEES rather than by an internal state — the fact that makes a creator
|
|
724
|
+
* act. The reassurance clause is mandatory: their first fear on reading this is
|
|
725
|
+
* that they are losing their work.
|
|
726
|
+
*/
|
|
727
|
+
export function verifyFailing(domain, findings, holdsAt, permanentAddress) {
|
|
728
|
+
const when = shortDate(holdsAt);
|
|
729
|
+
const deadline = when
|
|
730
|
+
? [
|
|
731
|
+
` ${domain} is still serving, and keeps serving until ${when}.`,
|
|
732
|
+
` If the record is back before then, nothing happens and this goes away`,
|
|
733
|
+
" on its own. If it isn't, visitors see a Vincentt page under your name.",
|
|
734
|
+
]
|
|
735
|
+
: [
|
|
736
|
+
` ${domain} is still serving. If the record stays missing it stops, and`,
|
|
737
|
+
" visitors see a Vincentt page under your name.",
|
|
738
|
+
];
|
|
739
|
+
return [
|
|
740
|
+
"",
|
|
741
|
+
`✗ We can't find ${domain}'s verification record.`,
|
|
742
|
+
...findingsReport(findings, domain),
|
|
743
|
+
"",
|
|
744
|
+
...deadline,
|
|
745
|
+
"",
|
|
746
|
+
...aliasStanceStopped(permanentAddress),
|
|
747
|
+
"",
|
|
748
|
+
" Once the record is back:",
|
|
749
|
+
` vincentt domain verify ${domain}`,
|
|
750
|
+
"",
|
|
751
|
+
NOTHING_WAS_CHANGED,
|
|
752
|
+
"",
|
|
753
|
+
];
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* The certificate did not come.
|
|
757
|
+
*
|
|
758
|
+
* The one thing this state does that no other does: IT NAMES A DIFFERENT ACTOR.
|
|
759
|
+
* A CAA record usually lives on the parent zone and is usually somebody else's,
|
|
760
|
+
* and copy that assumes the reader controls it sends them looking in the wrong
|
|
761
|
+
* place. It never says "contact support" — that implies a queue this product does
|
|
762
|
+
* not have.
|
|
763
|
+
*/
|
|
764
|
+
export function verifyCertFailed(domain, findings, permanentAddress) {
|
|
765
|
+
return [
|
|
766
|
+
`✓ ${domain} is yours - the TXT record matches.`,
|
|
767
|
+
"✓ The CNAME points at us.",
|
|
768
|
+
`✗ We couldn't get a certificate for ${domain}.`,
|
|
769
|
+
...findingsReport(findings, domain),
|
|
770
|
+
"",
|
|
771
|
+
" The domain is proven yours; a browser just won't trust it until there",
|
|
772
|
+
" is a certificate. We keep retrying on our own.",
|
|
773
|
+
"",
|
|
774
|
+
...aliasStanceStopped(permanentAddress),
|
|
775
|
+
"",
|
|
776
|
+
` vincentt domain verify ${domain}`,
|
|
777
|
+
"",
|
|
778
|
+
NOTHING_WAS_CHANGED,
|
|
779
|
+
"",
|
|
780
|
+
];
|
|
781
|
+
}
|
|
782
|
+
// ---- list --------------------------------------------------------------------
|
|
783
|
+
/** The absolute date a creator reads. NEVER "in 13 days", which drifts while a tab is open. */
|
|
784
|
+
const MONTHS = [
|
|
785
|
+
"Jan", "Feb", "Mar", "Apr", "May", "Jun",
|
|
786
|
+
"Jul", "Aug", "Sep", "Oct", "Nov", "Dec",
|
|
787
|
+
];
|
|
788
|
+
export function shortDate(iso) {
|
|
789
|
+
if (!iso)
|
|
790
|
+
return "";
|
|
791
|
+
const d = new Date(iso);
|
|
792
|
+
if (Number.isNaN(d.getTime()))
|
|
793
|
+
return "";
|
|
794
|
+
return `${d.getUTCDate()} ${MONTHS[d.getUTCMonth()]}`;
|
|
795
|
+
}
|
|
796
|
+
/**
|
|
797
|
+
* Flow L43 — a listing verb REPORTS; it never asks and never picks.
|
|
798
|
+
*
|
|
799
|
+
* A fact longer than a column goes on an INDENTED CONTINUATION LINE at a constant
|
|
800
|
+
* indent (f2's rule, verbatim). The stop-date and the resume sentence are the two
|
|
801
|
+
* facts that do not fit a column and must not become a fifth one.
|
|
802
|
+
*
|
|
803
|
+
* `list` shows ONLY THIS WORKSPACE'S domains — which is what makes the
|
|
804
|
+
* anti-oracle collision message safe.
|
|
805
|
+
*/
|
|
806
|
+
const LIST_CONTINUATION = " ";
|
|
807
|
+
export function listRows(domains) {
|
|
808
|
+
if (domains.length === 0)
|
|
809
|
+
return [];
|
|
810
|
+
const dw = Math.max(6, ...domains.map((d) => d.domain.length));
|
|
811
|
+
const pw = Math.max(7, ...domains.map((d) => (d.projectName ?? "").length));
|
|
812
|
+
const sw = Math.max(5, ...domains.map((d) => stateWord(d).length));
|
|
813
|
+
const out = [
|
|
814
|
+
` ${"DOMAIN".padEnd(dw)} ${"PROJECT".padEnd(pw)} ${"STATE".padEnd(sw)} SINCE`,
|
|
815
|
+
];
|
|
816
|
+
for (const d of domains) {
|
|
817
|
+
out.push(` ${d.domain.padEnd(dw)} ${(d.projectName ?? "").padEnd(pw)} ${stateWord(d).padEnd(sw)} ${shortDate(d.attachedAt)}`);
|
|
818
|
+
if (d.state === "failing" && d.holdsAt) {
|
|
819
|
+
out.push(`${LIST_CONTINUATION}stops serving on ${shortDate(d.holdsAt)} unless the TXT record is back`);
|
|
820
|
+
}
|
|
821
|
+
if (d.state === "held") {
|
|
822
|
+
out.push(`${LIST_CONTINUATION}put the TXT record back and it resumes on its own`);
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
return out;
|
|
826
|
+
}
|
|
827
|
+
/**
|
|
828
|
+
* The entitlement line — A BARE COUNT AND THE PLAN NAME. No price, no ladder, no
|
|
829
|
+
* *"upgrade for more"*. Clause 4 reaches the CLI too.
|
|
830
|
+
*
|
|
831
|
+
* A zero denominator is never rendered, and an absent limit renders the count
|
|
832
|
+
* alone rather than inventing one.
|
|
833
|
+
*/
|
|
834
|
+
export function entitlementLine(count, limit, planName) {
|
|
835
|
+
if (count === undefined)
|
|
836
|
+
return [];
|
|
837
|
+
const plan = planName ? ` · ${planName} plan` : "";
|
|
838
|
+
if (limit === undefined || limit <= 0) {
|
|
839
|
+
return [` ${count} custom domain${count === 1 ? "" : "s"}${plan}`];
|
|
840
|
+
}
|
|
841
|
+
return [` ${count} of ${limit} custom domains${plan}`];
|
|
842
|
+
}
|
|
843
|
+
export function listEmpty(planName) {
|
|
844
|
+
const plan = planName ? ` · ${planName} plan` : "";
|
|
845
|
+
return [
|
|
846
|
+
"",
|
|
847
|
+
` No custom domains${plan}`,
|
|
848
|
+
"",
|
|
849
|
+
" Attach one to a published project:",
|
|
850
|
+
" vincentt domain attach ar.example.com",
|
|
851
|
+
"",
|
|
852
|
+
];
|
|
853
|
+
}
|
|
854
|
+
// ---- detach ------------------------------------------------------------------
|
|
855
|
+
/**
|
|
856
|
+
* Flow C7 — the confirmation.
|
|
857
|
+
*
|
|
858
|
+
* `[y/N]`, AND IT IS NOT f4's IRREVERSIBLE CASING BEING MISAPPLIED. Detaching IS
|
|
859
|
+
* undoable — but the default is No because the act is IMMEDIATELY VISIBLE TO
|
|
860
|
+
* STRANGERS, and an accidental Enter would take a client's advertised address dark
|
|
861
|
+
* mid-campaign. The bracket casing tracks *would an accidental Enter be
|
|
862
|
+
* expensive*, and this one would (shared.md § the refined convention).
|
|
863
|
+
*
|
|
864
|
+
* THE COPY DELIBERATELY CONTRADICTS f4's PERMANENCE SENTENCE. A detached custom
|
|
865
|
+
* domain is NOT burned — the platform has no authority over the creator's
|
|
866
|
+
* registrar, and copy that "consistently" applied f4's permanence language would
|
|
867
|
+
* be a false claim about someone else's property. Stated positively: the name
|
|
868
|
+
* stays yours, and you can delete our three records.
|
|
869
|
+
*
|
|
870
|
+
* No fenced block: f4 reserves fencing for the irreversible.
|
|
871
|
+
*/
|
|
872
|
+
export function detachDisclosure(domain, permanentAddress) {
|
|
873
|
+
const alias = permanentAddress
|
|
874
|
+
? [` ${permanentAddress} is unaffected and keeps serving.`]
|
|
875
|
+
: [];
|
|
876
|
+
return [
|
|
877
|
+
"",
|
|
878
|
+
` ${domain} stops serving immediately.`,
|
|
879
|
+
...alias,
|
|
880
|
+
"",
|
|
881
|
+
" The name stays yours - point it wherever you like, or attach it again",
|
|
882
|
+
" later. You can delete the three Vincentt records at your registrar.",
|
|
883
|
+
"",
|
|
884
|
+
];
|
|
885
|
+
}
|
|
886
|
+
export function detachPrompt(domain) {
|
|
887
|
+
return ` Detach ${domain}? [y/N] `;
|
|
888
|
+
}
|
|
889
|
+
export function detachDone(domain) {
|
|
890
|
+
return ["", `✓ Detached ${domain}`, "", " It no longer serves this project.", ""];
|
|
891
|
+
}
|
|
892
|
+
/** Declined. Exit 0, ZERO network calls. Declining is not a failure; it is the prompt working. */
|
|
893
|
+
export function detachDeclined(domain) {
|
|
894
|
+
return ["", ` ${domain} is unchanged and still serving.`, ""];
|
|
895
|
+
}
|
|
896
|
+
/**
|
|
897
|
+
* Already detached — a SILENT RESOLVE, not an error. The creator asked for a
|
|
898
|
+
* state the world is already in.
|
|
899
|
+
*/
|
|
900
|
+
export function detachAlready(domain) {
|
|
901
|
+
return ["", `✓ ${domain} is not attached to this project.`, ""];
|
|
902
|
+
}
|
|
903
|
+
/**
|
|
904
|
+
* A non-TTY detach with no explicit flag. REFUSE; NEVER ASSUME YES.
|
|
905
|
+
*
|
|
906
|
+
* A pipe cannot consent. The remedy names the flag, because an agent that reaches
|
|
907
|
+
* this needs to know the act is available rather than concluding detach is
|
|
908
|
+
* console-only.
|
|
909
|
+
*/
|
|
910
|
+
export function detachNeedsConsent(domain) {
|
|
911
|
+
return [
|
|
912
|
+
"",
|
|
913
|
+
"✗ Detaching takes a confirmation, and there's no terminal to ask on.",
|
|
914
|
+
"",
|
|
915
|
+
` Re-run with --yes to detach ${domain} without being asked.`,
|
|
916
|
+
"",
|
|
917
|
+
NOTHING_WAS_CHANGED,
|
|
918
|
+
"",
|
|
919
|
+
];
|
|
920
|
+
}
|
|
921
|
+
// ---- the routed non-verbs -----------------------------------------------------
|
|
922
|
+
/**
|
|
923
|
+
* `renew` gets a FACT rather than a redirect, because the honest answer is that
|
|
924
|
+
* the act does not exist. f4's rule: a creator typing a plausible verb has told us
|
|
925
|
+
* exactly what they want.
|
|
926
|
+
*/
|
|
927
|
+
export function noRenewVerb() {
|
|
928
|
+
return [
|
|
929
|
+
"",
|
|
930
|
+
"✗ There's no `domain renew` command.",
|
|
931
|
+
"",
|
|
932
|
+
" Certificates renew on their own. There's nothing to run.",
|
|
933
|
+
"",
|
|
934
|
+
];
|
|
935
|
+
}
|
|
936
|
+
export function unknownDomainSub(sub) {
|
|
937
|
+
return [
|
|
938
|
+
"",
|
|
939
|
+
`✗ There's no \`domain ${sub}\` command. You want:`,
|
|
940
|
+
" vincentt domain list",
|
|
941
|
+
"",
|
|
942
|
+
];
|
|
943
|
+
}
|
|
944
|
+
/** `vincentt domains` / `custom-domain` / `dns` — plausible verbs, answered. */
|
|
945
|
+
export function domainVerbRedirect(typed) {
|
|
946
|
+
return [
|
|
947
|
+
"",
|
|
948
|
+
`✗ There's no \`${typed}\` command. You want:`,
|
|
949
|
+
" vincentt domain list",
|
|
950
|
+
"",
|
|
951
|
+
];
|
|
952
|
+
}
|