@fluid-app/fluid-cli-domains 0.0.0-stage → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +16 -2
- package/dist/index.d.mts +14 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +254 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +81 -3
package/README.md
CHANGED
|
@@ -1,3 +1,17 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @fluid-app/fluid-cli-domains
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Fluid CLI plugin for custom storefront domains.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
npm install -g @fluid-app/fluid-cli @fluid-app/fluid-cli-domains
|
|
7
|
+
fluid login
|
|
8
|
+
fluid domains add shop.example.com --yes # add it and print the DNS record to create
|
|
9
|
+
fluid domains status shop.example.com # check once and print what to do next
|
|
10
|
+
fluid domains list
|
|
11
|
+
fluid domains remove shop.example.com --yes
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Every command prints JSON; failures print a JSON `error` on stderr and exit
|
|
15
|
+
non-zero. `add` and `remove` change live configuration, so they ask for
|
|
16
|
+
confirmation unless you pass `--yes`. The full walkthrough is the custom-domain
|
|
17
|
+
guided setup at https://docs.fluid.app/setup/custom-domain.
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { CommandContext, FluidPlugin } from "@fluid-app/fluid-cli";
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
|
|
4
|
+
//#region src/index.d.ts
|
|
5
|
+
/**
|
|
6
|
+
* Changes to a live domain need an explicit yes. Scripts and agents pass
|
|
7
|
+
* `--yes`; an interactive terminal is asked. With neither, nothing runs.
|
|
8
|
+
*/
|
|
9
|
+
declare function confirmChange(question: string, yes: boolean, interactive?: boolean): Promise<void>;
|
|
10
|
+
declare function registerDomainCommands(parent: Command, ctx: CommandContext): void;
|
|
11
|
+
declare const plugin: FluidPlugin;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { confirmChange, plugin as default, registerDomainCommands };
|
|
14
|
+
//# sourceMappingURL=index.d.mts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.mts","names":[],"sources":["../src/index.ts"],"mappings":";;;;;;AAuBA;;iBAAsB,aAAA,CACpB,QAAA,UACA,GAAA,WACA,WAAA,aACC,OAAA;AAAA,iBAsCa,sBAAA,CACd,MAAA,EAAQ,OAAA,EACR,GAAA,EAAK,cAAA;AAAA,cAqDD,MAAA,EAAQ,WAAA"}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import prompts from "prompts";
|
|
2
|
+
import { createDomainCommand } from "@fluid-app/fluid-cli";
|
|
3
|
+
//#region src/status.ts
|
|
4
|
+
/** Fallback targets, used only when a reconcile has not returned
|
|
5
|
+
* diagnostics for the domain yet. */
|
|
6
|
+
const APEX_A_RECORD = "34.49.150.251";
|
|
7
|
+
const CNAME_TARGET = "host.fluid.app";
|
|
8
|
+
const FAILURE_MESSAGES = {
|
|
9
|
+
auth_failed: "Fluid couldn't create the DNS ownership authorization. This is usually transient; check status again to retry.",
|
|
10
|
+
cname_failed: "The ownership CNAME doesn't match what Fluid expects yet. Check the record value at your DNS provider, then check status again.",
|
|
11
|
+
cert_failed: "SSL certificate provisioning failed or timed out. Check status again to retry.",
|
|
12
|
+
map_failed: "Fluid couldn't set up routing for this domain. Check status again to retry, and contact support if it persists.",
|
|
13
|
+
dns_failed: "This domain doesn't point at Fluid yet. Add the DNS record below, then check status again.",
|
|
14
|
+
failed: "Domain setup hit a problem. Check status again to retry."
|
|
15
|
+
};
|
|
16
|
+
function domainPhase(verificationStatus) {
|
|
17
|
+
if (verificationStatus === "verified") return "connected";
|
|
18
|
+
if (verificationStatus === "failed" || verificationStatus.endsWith("_failed")) return "failed";
|
|
19
|
+
if (verificationStatus === "cname_verified" || verificationStatus === "pending_dns") return "provisioning_ssl";
|
|
20
|
+
if (verificationStatus === "pending_shared") return "point_dns";
|
|
21
|
+
return "verify_ownership";
|
|
22
|
+
}
|
|
23
|
+
/** Which DNS step still needs a record: 1 is ownership, 2 is routing. */
|
|
24
|
+
function stepInProgress(phase, verificationStatus) {
|
|
25
|
+
if (phase === "verify_ownership") return 1;
|
|
26
|
+
if (phase === "point_dns") return 2;
|
|
27
|
+
if (phase !== "failed") return null;
|
|
28
|
+
if ([
|
|
29
|
+
"auth_failed",
|
|
30
|
+
"cname_failed",
|
|
31
|
+
"cert_failed"
|
|
32
|
+
].includes(verificationStatus)) return 1;
|
|
33
|
+
if (["map_failed", "dns_failed"].includes(verificationStatus)) return 2;
|
|
34
|
+
return "both";
|
|
35
|
+
}
|
|
36
|
+
/** The DNS host to publish under, relative to the domain's apex:
|
|
37
|
+
* `deep.shop` for `deep.shop.example.com`, not just `deep`. DNS names are
|
|
38
|
+
* case-insensitive, so the match is too. */
|
|
39
|
+
function relativeLabel(hostname, apexDomain) {
|
|
40
|
+
const suffix = `.${apexDomain}`;
|
|
41
|
+
if (hostname.length > suffix.length && hostname.toLowerCase().endsWith(suffix.toLowerCase())) return hostname.slice(0, -suffix.length);
|
|
42
|
+
return hostname.split(".")[0] || hostname;
|
|
43
|
+
}
|
|
44
|
+
function isApexHostname(hostname, apexDomain) {
|
|
45
|
+
return hostname.toLowerCase() === apexDomain.toLowerCase();
|
|
46
|
+
}
|
|
47
|
+
function stringAt(config, key) {
|
|
48
|
+
const value = config?.[key];
|
|
49
|
+
return typeof value === "string" ? value : void 0;
|
|
50
|
+
}
|
|
51
|
+
function dnsInstructions(domain, diagnostics, step) {
|
|
52
|
+
if (step === null) return [];
|
|
53
|
+
const instructions = [];
|
|
54
|
+
const authCname = stringAt(domain.dns_config, "auth_cname");
|
|
55
|
+
const authHostname = stringAt(domain.dns_config, "auth_hostname");
|
|
56
|
+
const isApex = isApexHostname(domain.hostname, domain.apex_domain);
|
|
57
|
+
const label = relativeLabel(domain.hostname, domain.apex_domain);
|
|
58
|
+
if ((step === 1 || step === "both") && authCname && authHostname) instructions.push({
|
|
59
|
+
purpose: "ownership_verification",
|
|
60
|
+
type: "CNAME",
|
|
61
|
+
host: relativeLabel(authHostname.replace(/\.$/, ""), domain.apex_domain),
|
|
62
|
+
value: authCname
|
|
63
|
+
});
|
|
64
|
+
if (step === 2 || step === "both") {
|
|
65
|
+
const expected = diagnostics?.expected;
|
|
66
|
+
instructions.push(expected ? {
|
|
67
|
+
purpose: "routing",
|
|
68
|
+
type: expected.type,
|
|
69
|
+
host: isApex ? "@" : label,
|
|
70
|
+
value: expected.value
|
|
71
|
+
} : isApex ? {
|
|
72
|
+
purpose: "routing",
|
|
73
|
+
type: "A",
|
|
74
|
+
host: "@",
|
|
75
|
+
value: APEX_A_RECORD
|
|
76
|
+
} : {
|
|
77
|
+
purpose: "routing",
|
|
78
|
+
type: "CNAME",
|
|
79
|
+
host: label,
|
|
80
|
+
value: CNAME_TARGET
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return instructions;
|
|
84
|
+
}
|
|
85
|
+
function statusMessage(phase, hostname, failureMessage, hasInstructions) {
|
|
86
|
+
switch (phase) {
|
|
87
|
+
case "verify_ownership": return hasInstructions ? `${hostname} needs a DNS record to verify you own it. Add the CNAME record below at your DNS provider, then check status again. It can take a few minutes to propagate.` : `${hostname} is being set up for ownership verification, and Fluid hasn't generated the DNS record yet. Check status again shortly.`;
|
|
88
|
+
case "point_dns": return `Ownership of ${hostname} is verified. Add the DNS record below to point it at Fluid, then check status again. Propagation can take from a few minutes to a few hours.`;
|
|
89
|
+
case "provisioning_ssl": return hasInstructions ? `Ownership of ${hostname} is verified and the SSL certificate is ready, but public DNS doesn't point at Fluid yet. Add the DNS record below, replacing any existing records on the same host, then check status again.` : `${hostname} points at Fluid, and Fluid is finishing the certificate and routing setup. This is automatic and usually takes a few minutes. Check status again shortly.`;
|
|
90
|
+
case "connected": return `${hostname} is connected and serving traffic over HTTPS.`;
|
|
91
|
+
case "failed": return failureMessage ?? FAILURE_MESSAGES["failed"];
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function summarizeDomain(wire) {
|
|
95
|
+
const phase = domainPhase(wire.verification_status);
|
|
96
|
+
return {
|
|
97
|
+
id: wire.id,
|
|
98
|
+
hostname: wire.hostname,
|
|
99
|
+
apexDomain: wire.apex_domain,
|
|
100
|
+
isApex: isApexHostname(wire.hostname, wire.apex_domain),
|
|
101
|
+
primary: wire.primary ?? false,
|
|
102
|
+
verificationStatus: wire.verification_status,
|
|
103
|
+
phase,
|
|
104
|
+
connected: phase === "connected",
|
|
105
|
+
certCreated: wire.cert_created ?? false,
|
|
106
|
+
redirectionDomainHostname: wire.redirection_domain_hostname ?? null
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
function describeStatus(wire, diagnostics) {
|
|
110
|
+
const summary = summarizeDomain(wire);
|
|
111
|
+
let step = stepInProgress(summary.phase, wire.verification_status);
|
|
112
|
+
if (step === null && summary.phase === "provisioning_ssl" && diagnostics?.dns_match === false) step = 2;
|
|
113
|
+
const failureMessage = summary.phase === "failed" ? FAILURE_MESSAGES[wire.verification_status] ?? FAILURE_MESSAGES["failed"] : null;
|
|
114
|
+
const instructions = dnsInstructions(wire, diagnostics, step);
|
|
115
|
+
return {
|
|
116
|
+
...summary,
|
|
117
|
+
dnsMatch: diagnostics?.dns_match ?? null,
|
|
118
|
+
dnsInstructions: instructions,
|
|
119
|
+
actualDnsRecords: diagnostics?.actual ?? [],
|
|
120
|
+
nameservers: diagnostics?.nameservers ?? [],
|
|
121
|
+
failureMessage,
|
|
122
|
+
message: statusMessage(summary.phase, wire.hostname, failureMessage, instructions.length > 0)
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
const HOSTNAME_RE = /^(?!-)[a-z0-9-]{1,63}(?<!-)(\.(?!-)[a-z0-9-]{1,63}(?<!-))+$/i;
|
|
126
|
+
/** Shape check before a hostname reaches the API: no scheme, path or
|
|
127
|
+
* spaces, at least one dot. The API remains the final authority. */
|
|
128
|
+
function isValidHostname(hostname) {
|
|
129
|
+
return HOSTNAME_RE.test(hostname.trim());
|
|
130
|
+
}
|
|
131
|
+
//#endregion
|
|
132
|
+
//#region src/api.ts
|
|
133
|
+
/** The list endpoint defaults to ten per page and sorts oldest first, so
|
|
134
|
+
* an unpaged read hides the newest domains. */
|
|
135
|
+
const PAGE_SIZE = 100;
|
|
136
|
+
const MAX_PAGES = 25;
|
|
137
|
+
var DomainsError = class extends Error {
|
|
138
|
+
constructor(message) {
|
|
139
|
+
super(message);
|
|
140
|
+
this.name = "DomainsError";
|
|
141
|
+
}
|
|
142
|
+
};
|
|
143
|
+
async function listDomains(client) {
|
|
144
|
+
const all = [];
|
|
145
|
+
for (let page = 1; page <= MAX_PAGES; page += 1) {
|
|
146
|
+
const data = await client.get("/api/domains", {
|
|
147
|
+
page,
|
|
148
|
+
per_page: PAGE_SIZE
|
|
149
|
+
});
|
|
150
|
+
const domains = data.domains ?? [];
|
|
151
|
+
all.push(...domains.map(summarizeDomain));
|
|
152
|
+
const totalPages = data.meta?.total_pages;
|
|
153
|
+
if (totalPages != null ? page >= totalPages : domains.length < PAGE_SIZE) return all;
|
|
154
|
+
}
|
|
155
|
+
throw new DomainsError(`This company has more than ${MAX_PAGES * PAGE_SIZE} domains; refusing to return a partial list.`);
|
|
156
|
+
}
|
|
157
|
+
/** Resolve a hostname or numeric id to a domain id. */
|
|
158
|
+
async function resolveDomainId(client, hostnameOrId) {
|
|
159
|
+
const trimmed = hostnameOrId.trim();
|
|
160
|
+
if (/^\d+$/.test(trimmed)) return Number(trimmed);
|
|
161
|
+
const wanted = trimmed.toLowerCase();
|
|
162
|
+
const match = (await listDomains(client)).find((domain) => domain.hostname.toLowerCase() === wanted);
|
|
163
|
+
if (!match) throw new DomainsError(`No domain named "${trimmed}" on this company.`);
|
|
164
|
+
return match.id;
|
|
165
|
+
}
|
|
166
|
+
/** Run one bounded server-side check and describe the result. It does not
|
|
167
|
+
* wait for DNS propagation. */
|
|
168
|
+
async function checkDomain(client, id) {
|
|
169
|
+
const data = await client.patch(`/api/domains/${encodeURIComponent(String(id))}/reconcile`);
|
|
170
|
+
if (!data.domain) throw new DomainsError("The status check returned no domain.");
|
|
171
|
+
return describeStatus(data.domain, data.meta?.diagnostics);
|
|
172
|
+
}
|
|
173
|
+
/** Create a domain, then run one check so the result already carries the
|
|
174
|
+
* DNS record to add. */
|
|
175
|
+
async function addDomain(client, hostname) {
|
|
176
|
+
const normalized = hostname.trim().toLowerCase();
|
|
177
|
+
if (!isValidHostname(normalized)) throw new DomainsError(`"${hostname}" isn't a valid domain. Expected something like "shop.example.com".`);
|
|
178
|
+
const created = await client.post("/api/domains", { domain: { hostname: normalized } });
|
|
179
|
+
if (!created.domain) throw new DomainsError("The domain was created, but the response didn't include it.");
|
|
180
|
+
try {
|
|
181
|
+
return await checkDomain(client, created.domain.id);
|
|
182
|
+
} catch {
|
|
183
|
+
return describeStatus(created.domain, void 0);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
async function removeDomain(client, id) {
|
|
187
|
+
await client.delete(`/api/domains/${encodeURIComponent(String(id))}`);
|
|
188
|
+
return {
|
|
189
|
+
removed: true,
|
|
190
|
+
id
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
//#endregion
|
|
194
|
+
//#region src/index.ts
|
|
195
|
+
async function client(ctx) {
|
|
196
|
+
return await ctx.getClient();
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Changes to a live domain need an explicit yes. Scripts and agents pass
|
|
200
|
+
* `--yes`; an interactive terminal is asked. With neither, nothing runs.
|
|
201
|
+
*/
|
|
202
|
+
async function confirmChange(question, yes, interactive = Boolean(process.stdin.isTTY)) {
|
|
203
|
+
if (yes) return;
|
|
204
|
+
if (!interactive) throw new DomainsError(`${question} Pass --yes to confirm.`);
|
|
205
|
+
if (!(await prompts({
|
|
206
|
+
type: "confirm",
|
|
207
|
+
name: "ok",
|
|
208
|
+
message: question,
|
|
209
|
+
initial: false
|
|
210
|
+
})).ok) throw new DomainsError("Cancelled.");
|
|
211
|
+
}
|
|
212
|
+
/** Print a failure as JSON on stderr and exit non-zero, so scripts and
|
|
213
|
+
* agents can read it the same way as a success. */
|
|
214
|
+
function fail(err) {
|
|
215
|
+
const status = typeof err.status === "number" ? err.status : void 0;
|
|
216
|
+
const body = err.body;
|
|
217
|
+
const message = typeof body?.error_message === "string" ? body.error_message : err instanceof Error ? err.message : String(err);
|
|
218
|
+
process.stderr.write(JSON.stringify({
|
|
219
|
+
error: message,
|
|
220
|
+
...status ? { status } : {}
|
|
221
|
+
}) + "\n");
|
|
222
|
+
process.exitCode = 1;
|
|
223
|
+
}
|
|
224
|
+
function run(action) {
|
|
225
|
+
return action().catch(fail);
|
|
226
|
+
}
|
|
227
|
+
function registerDomainCommands(parent, ctx) {
|
|
228
|
+
parent.command("list").description("List the company's storefront domains and their phase").action(() => run(async () => ctx.output(await listDomains(await client(ctx)))));
|
|
229
|
+
parent.command("add <hostname>").description("Add a storefront domain and print the DNS record to create (changes live config)").option("--yes", "Confirm without a prompt", false).action((hostname, opts) => run(async () => {
|
|
230
|
+
await confirmChange(`Add ${hostname} to this company?`, opts.yes);
|
|
231
|
+
ctx.output(await addDomain(await client(ctx), hostname));
|
|
232
|
+
}));
|
|
233
|
+
parent.command("status <domain>").description("Check a domain once (hostname or id) and print what to do next; does not wait for DNS").action((domain) => run(async () => {
|
|
234
|
+
const api = await client(ctx);
|
|
235
|
+
ctx.output(await checkDomain(api, await resolveDomainId(api, domain)));
|
|
236
|
+
}));
|
|
237
|
+
parent.command("remove <domain>").description("Remove a domain (hostname or id); a live domain goes offline immediately").option("--yes", "Confirm without a prompt", false).action((domain, opts) => run(async () => {
|
|
238
|
+
const api = await client(ctx);
|
|
239
|
+
const id = await resolveDomainId(api, domain);
|
|
240
|
+
await confirmChange(`Remove ${domain}? A live domain goes offline immediately.`, opts.yes);
|
|
241
|
+
ctx.output(await removeDomain(api, id));
|
|
242
|
+
}));
|
|
243
|
+
}
|
|
244
|
+
const plugin = {
|
|
245
|
+
name: "@fluid-app/fluid-cli-domains",
|
|
246
|
+
version: "0.1.0",
|
|
247
|
+
register(ctx) {
|
|
248
|
+
ctx.program.addCommand(createDomainCommand("domains", "Set up and check custom storefront domains", registerDomainCommands));
|
|
249
|
+
}
|
|
250
|
+
};
|
|
251
|
+
//#endregion
|
|
252
|
+
export { confirmChange, plugin as default, registerDomainCommands };
|
|
253
|
+
|
|
254
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.mjs","names":[],"sources":["../src/status.ts","../src/api.ts","../src/index.ts"],"sourcesContent":["// What a storefront domain's setup status means, and what to do next.\n//\n// The Domains API reports a raw `verification_status` (a dozen values).\n// This collapses it into a phase, the DNS records the customer still has to\n// add, and a one-paragraph message, so a person or an agent can act on one\n// `fluid domains status` result without knowing the state machine.\n// The same meanings are documented in the custom-domain setup guide.\n\n/** Fallback targets, used only when a reconcile has not returned\n * diagnostics for the domain yet. */\nexport const APEX_A_RECORD = \"34.49.150.251\";\nexport const CNAME_TARGET = \"host.fluid.app\";\n\nexport type DomainPhase =\n | \"verify_ownership\"\n | \"point_dns\"\n | \"provisioning_ssl\"\n | \"connected\"\n | \"failed\";\n\nexport interface DnsRecord {\n type: string;\n value: string;\n}\n\nexport interface DnsInstruction extends DnsRecord {\n purpose: \"ownership_verification\" | \"routing\";\n host: string;\n}\n\n/** The fields of a domain resource this module reads. */\nexport interface DomainWire {\n id: number;\n hostname: string;\n apex_domain: string;\n verification_status: string;\n dns_config?: Record<string, unknown> | null;\n primary?: boolean;\n wildcard_subdomain?: boolean;\n cert_created?: boolean | null;\n redirection_domain_hostname?: string | null;\n}\n\nexport interface DnsDiagnostics {\n expected?: DnsRecord;\n actual?: DnsRecord[];\n dns_match?: boolean;\n nameservers?: string[];\n}\n\nexport interface DomainSummary {\n id: number;\n hostname: string;\n apexDomain: string;\n isApex: boolean;\n primary: boolean;\n verificationStatus: string;\n phase: DomainPhase;\n connected: boolean;\n certCreated: boolean;\n redirectionDomainHostname: string | null;\n}\n\nexport interface DomainStatus extends DomainSummary {\n /** Whether public DNS matched the expected record on the last check;\n * null when the check returned no diagnostics. */\n dnsMatch: boolean | null;\n /** Records still to add for the current step. Empty once connected. */\n dnsInstructions: DnsInstruction[];\n /** What public DNS serves now. Diagnostic only: never records to add. */\n actualDnsRecords: DnsRecord[];\n nameservers: string[];\n failureMessage: string | null;\n /** What is happening and what to do next, ready to show a person. */\n message: string;\n}\n\nconst FAILURE_MESSAGES: Record<string, string> = {\n auth_failed:\n \"Fluid couldn't create the DNS ownership authorization. This is usually transient; check status again to retry.\",\n cname_failed:\n \"The ownership CNAME doesn't match what Fluid expects yet. Check the record value at your DNS provider, then check status again.\",\n cert_failed:\n \"SSL certificate provisioning failed or timed out. Check status again to retry.\",\n map_failed:\n \"Fluid couldn't set up routing for this domain. Check status again to retry, and contact support if it persists.\",\n dns_failed:\n \"This domain doesn't point at Fluid yet. Add the DNS record below, then check status again.\",\n failed: \"Domain setup hit a problem. Check status again to retry.\",\n};\n\nexport function domainPhase(verificationStatus: string): DomainPhase {\n if (verificationStatus === \"verified\") return \"connected\";\n if (\n verificationStatus === \"failed\" ||\n verificationStatus.endsWith(\"_failed\")\n ) {\n return \"failed\";\n }\n if (\n verificationStatus === \"cname_verified\" ||\n verificationStatus === \"pending_dns\"\n ) {\n return \"provisioning_ssl\";\n }\n // A subdomain sharing its parent's certificate skips the ownership step.\n if (verificationStatus === \"pending_shared\") return \"point_dns\";\n // pending_cname, auth_created, cname_added, unknown.\n return \"verify_ownership\";\n}\n\n/** Which DNS step still needs a record: 1 is ownership, 2 is routing. */\nfunction stepInProgress(\n phase: DomainPhase,\n verificationStatus: string,\n): 1 | 2 | \"both\" | null {\n if (phase === \"verify_ownership\") return 1;\n if (phase === \"point_dns\") return 2;\n if (phase !== \"failed\") return null;\n if (\n [\"auth_failed\", \"cname_failed\", \"cert_failed\"].includes(verificationStatus)\n ) {\n return 1;\n }\n if ([\"map_failed\", \"dns_failed\"].includes(verificationStatus)) return 2;\n return \"both\";\n}\n\n/** The DNS host to publish under, relative to the domain's apex:\n * `deep.shop` for `deep.shop.example.com`, not just `deep`. DNS names are\n * case-insensitive, so the match is too. */\nexport function relativeLabel(hostname: string, apexDomain: string): string {\n const suffix = `.${apexDomain}`;\n if (\n hostname.length > suffix.length &&\n hostname.toLowerCase().endsWith(suffix.toLowerCase())\n ) {\n return hostname.slice(0, -suffix.length);\n }\n return hostname.split(\".\")[0] || hostname;\n}\n\nfunction isApexHostname(hostname: string, apexDomain: string): boolean {\n return hostname.toLowerCase() === apexDomain.toLowerCase();\n}\n\nfunction stringAt(\n config: Record<string, unknown> | null | undefined,\n key: string,\n): string | undefined {\n const value = config?.[key];\n return typeof value === \"string\" ? value : undefined;\n}\n\nfunction dnsInstructions(\n domain: DomainWire,\n diagnostics: DnsDiagnostics | undefined,\n step: 1 | 2 | \"both\" | null,\n): DnsInstruction[] {\n if (step === null) return [];\n const instructions: DnsInstruction[] = [];\n const authCname = stringAt(domain.dns_config, \"auth_cname\");\n const authHostname = stringAt(domain.dns_config, \"auth_hostname\");\n const isApex = isApexHostname(domain.hostname, domain.apex_domain);\n const label = relativeLabel(domain.hostname, domain.apex_domain);\n\n if ((step === 1 || step === \"both\") && authCname && authHostname) {\n instructions.push({\n purpose: \"ownership_verification\",\n type: \"CNAME\",\n host: relativeLabel(authHostname.replace(/\\.$/, \"\"), domain.apex_domain),\n value: authCname,\n });\n }\n if (step === 2 || step === \"both\") {\n const expected = diagnostics?.expected;\n instructions.push(\n expected\n ? {\n purpose: \"routing\",\n type: expected.type,\n host: isApex ? \"@\" : label,\n value: expected.value,\n }\n : isApex\n ? { purpose: \"routing\", type: \"A\", host: \"@\", value: APEX_A_RECORD }\n : {\n purpose: \"routing\",\n type: \"CNAME\",\n host: label,\n value: CNAME_TARGET,\n },\n );\n }\n return instructions;\n}\n\nfunction statusMessage(\n phase: DomainPhase,\n hostname: string,\n failureMessage: string | null,\n hasInstructions: boolean,\n): string {\n switch (phase) {\n case \"verify_ownership\":\n return hasInstructions\n ? `${hostname} needs a DNS record to verify you own it. Add the CNAME record below at your DNS provider, then check status again. It can take a few minutes to propagate.`\n : `${hostname} is being set up for ownership verification, and Fluid hasn't generated the DNS record yet. Check status again shortly.`;\n case \"point_dns\":\n return `Ownership of ${hostname} is verified. Add the DNS record below to point it at Fluid, then check status again. Propagation can take from a few minutes to a few hours.`;\n case \"provisioning_ssl\":\n return hasInstructions\n ? `Ownership of ${hostname} is verified and the SSL certificate is ready, but public DNS doesn't point at Fluid yet. Add the DNS record below, replacing any existing records on the same host, then check status again.`\n : `${hostname} points at Fluid, and Fluid is finishing the certificate and routing setup. This is automatic and usually takes a few minutes. Check status again shortly.`;\n case \"connected\":\n return `${hostname} is connected and serving traffic over HTTPS.`;\n case \"failed\":\n return failureMessage ?? FAILURE_MESSAGES[\"failed\"]!;\n }\n}\n\nexport function summarizeDomain(wire: DomainWire): DomainSummary {\n const phase = domainPhase(wire.verification_status);\n return {\n id: wire.id,\n hostname: wire.hostname,\n apexDomain: wire.apex_domain,\n isApex: isApexHostname(wire.hostname, wire.apex_domain),\n primary: wire.primary ?? false,\n verificationStatus: wire.verification_status,\n phase,\n connected: phase === \"connected\",\n certCreated: wire.cert_created ?? false,\n redirectionDomainHostname: wire.redirection_domain_hostname ?? null,\n };\n}\n\nexport function describeStatus(\n wire: DomainWire,\n diagnostics: DnsDiagnostics | undefined,\n): DomainStatus {\n const summary = summarizeDomain(wire);\n let step = stepInProgress(summary.phase, wire.verification_status);\n // The certificate can be ready while traffic still doesn't point at\n // Fluid. Keep the routing record in the result so it isn't reported as\n // done.\n if (\n step === null &&\n summary.phase === \"provisioning_ssl\" &&\n diagnostics?.dns_match === false\n ) {\n step = 2;\n }\n const failureMessage =\n summary.phase === \"failed\"\n ? (FAILURE_MESSAGES[wire.verification_status] ??\n FAILURE_MESSAGES[\"failed\"]!)\n : null;\n const instructions = dnsInstructions(wire, diagnostics, step);\n return {\n ...summary,\n dnsMatch: diagnostics?.dns_match ?? null,\n dnsInstructions: instructions,\n actualDnsRecords: diagnostics?.actual ?? [],\n nameservers: diagnostics?.nameservers ?? [],\n failureMessage,\n message: statusMessage(\n summary.phase,\n wire.hostname,\n failureMessage,\n instructions.length > 0,\n ),\n };\n}\n\nconst HOSTNAME_RE =\n /^(?!-)[a-z0-9-]{1,63}(?<!-)(\\.(?!-)[a-z0-9-]{1,63}(?<!-))+$/i;\n\n/** Shape check before a hostname reaches the API: no scheme, path or\n * spaces, at least one dot. The API remains the final authority. */\nexport function isValidHostname(hostname: string): boolean {\n return HOSTNAME_RE.test(hostname.trim());\n}\n","// Calls to the company Domains API (`/api/domains`, company-v0).\n\nimport {\n describeStatus,\n isValidHostname,\n summarizeDomain,\n type DnsDiagnostics,\n type DomainStatus,\n type DomainSummary,\n type DomainWire,\n} from \"./status.js\";\n\n/** The slice of the CLI's API client these calls use. */\nexport interface DomainsClient {\n get<T>(endpoint: string, params?: Record<string, unknown>): Promise<T>;\n post<T>(endpoint: string, body?: unknown): Promise<T>;\n patch<T>(endpoint: string, body?: unknown): Promise<T>;\n delete<T>(endpoint: string): Promise<T>;\n}\n\ninterface ListResponse {\n domains?: DomainWire[] | null;\n meta?: { total_pages?: number };\n}\n\ninterface DomainResponse {\n domain?: DomainWire;\n meta?: { diagnostics?: DnsDiagnostics };\n}\n\n/** The list endpoint defaults to ten per page and sorts oldest first, so\n * an unpaged read hides the newest domains. */\nconst PAGE_SIZE = 100;\nconst MAX_PAGES = 25;\n\nexport class DomainsError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"DomainsError\";\n }\n}\n\nexport async function listDomains(\n client: DomainsClient,\n): Promise<DomainSummary[]> {\n const all: DomainSummary[] = [];\n for (let page = 1; page <= MAX_PAGES; page += 1) {\n const data = await client.get<ListResponse>(\"/api/domains\", {\n page,\n per_page: PAGE_SIZE,\n });\n const domains = data.domains ?? [];\n all.push(...domains.map(summarizeDomain));\n const totalPages = data.meta?.total_pages;\n const done =\n totalPages != null ? page >= totalPages : domains.length < PAGE_SIZE;\n if (done) return all;\n }\n throw new DomainsError(\n `This company has more than ${MAX_PAGES * PAGE_SIZE} domains; refusing to return a partial list.`,\n );\n}\n\n/** Resolve a hostname or numeric id to a domain id. */\nexport async function resolveDomainId(\n client: DomainsClient,\n hostnameOrId: string,\n): Promise<number> {\n const trimmed = hostnameOrId.trim();\n if (/^\\d+$/.test(trimmed)) return Number(trimmed);\n const wanted = trimmed.toLowerCase();\n const match = (await listDomains(client)).find(\n (domain) => domain.hostname.toLowerCase() === wanted,\n );\n if (!match) {\n throw new DomainsError(`No domain named \"${trimmed}\" on this company.`);\n }\n return match.id;\n}\n\n/** Run one bounded server-side check and describe the result. It does not\n * wait for DNS propagation. */\nexport async function checkDomain(\n client: DomainsClient,\n id: number,\n): Promise<DomainStatus> {\n const data = await client.patch<DomainResponse>(\n `/api/domains/${encodeURIComponent(String(id))}/reconcile`,\n );\n if (!data.domain) {\n throw new DomainsError(\"The status check returned no domain.\");\n }\n return describeStatus(data.domain, data.meta?.diagnostics);\n}\n\n/** Create a domain, then run one check so the result already carries the\n * DNS record to add. */\nexport async function addDomain(\n client: DomainsClient,\n hostname: string,\n): Promise<DomainStatus> {\n const normalized = hostname.trim().toLowerCase();\n if (!isValidHostname(normalized)) {\n throw new DomainsError(\n `\"${hostname}\" isn't a valid domain. Expected something like \"shop.example.com\".`,\n );\n }\n const created = await client.post<DomainResponse>(\"/api/domains\", {\n domain: { hostname: normalized },\n });\n if (!created.domain) {\n throw new DomainsError(\n \"The domain was created, but the response didn't include it.\",\n );\n }\n try {\n return await checkDomain(client, created.domain.id);\n } catch {\n // The domain exists; report it with the fallback records rather than\n // failing an add that succeeded.\n return describeStatus(created.domain, undefined);\n }\n}\n\nexport async function removeDomain(\n client: DomainsClient,\n id: number,\n): Promise<{ removed: true; id: number }> {\n await client.delete(`/api/domains/${encodeURIComponent(String(id))}`);\n return { removed: true, id };\n}\n","import type { Command } from \"commander\";\nimport prompts from \"prompts\";\nimport type { CommandContext, FluidPlugin } from \"@fluid-app/fluid-cli\";\nimport { createDomainCommand } from \"@fluid-app/fluid-cli\";\n\nimport {\n addDomain,\n checkDomain,\n DomainsError,\n listDomains,\n removeDomain,\n resolveDomainId,\n type DomainsClient,\n} from \"./api.js\";\n\nasync function client(ctx: CommandContext): Promise<DomainsClient> {\n return (await ctx.getClient()) as unknown as DomainsClient;\n}\n\n/**\n * Changes to a live domain need an explicit yes. Scripts and agents pass\n * `--yes`; an interactive terminal is asked. With neither, nothing runs.\n */\nexport async function confirmChange(\n question: string,\n yes: boolean,\n interactive: boolean = Boolean(process.stdin.isTTY),\n): Promise<void> {\n if (yes) return;\n if (!interactive) {\n throw new DomainsError(`${question} Pass --yes to confirm.`);\n }\n const answer = await prompts({\n type: \"confirm\",\n name: \"ok\",\n message: question,\n initial: false,\n });\n if (!answer.ok) throw new DomainsError(\"Cancelled.\");\n}\n\n/** Print a failure as JSON on stderr and exit non-zero, so scripts and\n * agents can read it the same way as a success. */\nfunction fail(err: unknown): void {\n const status =\n typeof (err as { status?: unknown }).status === \"number\"\n ? (err as { status: number }).status\n : undefined;\n const body = (err as { body?: { error_message?: unknown } | null }).body;\n const message =\n typeof body?.error_message === \"string\"\n ? body.error_message\n : err instanceof Error\n ? err.message\n : String(err);\n process.stderr.write(\n JSON.stringify({ error: message, ...(status ? { status } : {}) }) + \"\\n\",\n );\n process.exitCode = 1;\n}\n\nfunction run(action: () => Promise<void>): Promise<void> {\n return action().catch(fail);\n}\n\nexport function registerDomainCommands(\n parent: Command,\n ctx: CommandContext,\n): void {\n parent\n .command(\"list\")\n .description(\"List the company's storefront domains and their phase\")\n .action(() =>\n run(async () => ctx.output(await listDomains(await client(ctx)))),\n );\n\n parent\n .command(\"add <hostname>\")\n .description(\n \"Add a storefront domain and print the DNS record to create (changes live config)\",\n )\n .option(\"--yes\", \"Confirm without a prompt\", false)\n .action((hostname: string, opts: { yes: boolean }) =>\n run(async () => {\n await confirmChange(`Add ${hostname} to this company?`, opts.yes);\n ctx.output(await addDomain(await client(ctx), hostname));\n }),\n );\n\n parent\n .command(\"status <domain>\")\n .description(\n \"Check a domain once (hostname or id) and print what to do next; does not wait for DNS\",\n )\n .action((domain: string) =>\n run(async () => {\n const api = await client(ctx);\n ctx.output(await checkDomain(api, await resolveDomainId(api, domain)));\n }),\n );\n\n parent\n .command(\"remove <domain>\")\n .description(\n \"Remove a domain (hostname or id); a live domain goes offline immediately\",\n )\n .option(\"--yes\", \"Confirm without a prompt\", false)\n .action((domain: string, opts: { yes: boolean }) =>\n run(async () => {\n const api = await client(ctx);\n const id = await resolveDomainId(api, domain);\n await confirmChange(\n `Remove ${domain}? A live domain goes offline immediately.`,\n opts.yes,\n );\n ctx.output(await removeDomain(api, id));\n }),\n );\n}\n\nconst plugin: FluidPlugin = {\n name: \"@fluid-app/fluid-cli-domains\",\n version: \"0.1.0\",\n register(ctx) {\n ctx.program.addCommand(\n createDomainCommand(\n \"domains\",\n \"Set up and check custom storefront domains\",\n registerDomainCommands,\n ),\n );\n },\n};\n\nexport default plugin;\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,84 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@fluid-app/fluid-cli-domains",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Fluid CLI plugin for custom storefront domains — add, check, list, remove",
|
|
5
|
+
"files": [
|
|
6
|
+
"dist",
|
|
7
|
+
"README.md"
|
|
8
|
+
],
|
|
9
|
+
"type": "module",
|
|
10
|
+
"main": "./dist/index.mjs",
|
|
11
|
+
"types": "./dist/index.d.mts",
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"import": {
|
|
15
|
+
"types": "./dist/index.d.mts",
|
|
16
|
+
"default": "./dist/index.mjs"
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
},
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@fluid-app/fluid-cli": "0.1.36",
|
|
25
|
+
"commander": "^12.0.0",
|
|
26
|
+
"prompts": "^2.4.2"
|
|
27
|
+
},
|
|
28
|
+
"devDependencies": {
|
|
29
|
+
"@fluid-app/typescript-config": "0.0.0",
|
|
30
|
+
"@swc/core": "^1.15.18",
|
|
31
|
+
"@swc/jest": "^0.2.39",
|
|
32
|
+
"@types/jest": "^29.5.14",
|
|
33
|
+
"@types/node": "24.10.12",
|
|
34
|
+
"@types/prompts": "^2.4.9",
|
|
35
|
+
"jest": "^29.7.0",
|
|
36
|
+
"tsdown": "^0.21.0",
|
|
37
|
+
"typescript": "^5"
|
|
38
|
+
},
|
|
39
|
+
"engines": {
|
|
40
|
+
"node": ">=24.0.0"
|
|
41
|
+
},
|
|
42
|
+
"nx": {
|
|
43
|
+
"targets": {
|
|
44
|
+
"typecheck": {
|
|
45
|
+
"dependsOn": [
|
|
46
|
+
{
|
|
47
|
+
"target": "build",
|
|
48
|
+
"projects": [
|
|
49
|
+
"@fluid-app/fluid-cli"
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
},
|
|
54
|
+
"test": {
|
|
55
|
+
"dependsOn": [
|
|
56
|
+
{
|
|
57
|
+
"target": "build",
|
|
58
|
+
"projects": [
|
|
59
|
+
"@fluid-app/fluid-cli"
|
|
60
|
+
]
|
|
61
|
+
}
|
|
62
|
+
]
|
|
63
|
+
},
|
|
64
|
+
"build": {
|
|
65
|
+
"dependsOn": [
|
|
66
|
+
"typecheck",
|
|
67
|
+
{
|
|
68
|
+
"target": "build",
|
|
69
|
+
"projects": [
|
|
70
|
+
"@fluid-app/fluid-cli"
|
|
71
|
+
]
|
|
72
|
+
}
|
|
73
|
+
]
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
"scripts": {
|
|
78
|
+
"build": "tsdown",
|
|
79
|
+
"dev": "tsdown --watch",
|
|
80
|
+
"typecheck": "tsgo --noEmit",
|
|
81
|
+
"lint": "oxlint",
|
|
82
|
+
"test": "jest"
|
|
83
|
+
}
|
|
6
84
|
}
|