@indigoai-us/hq-cli 5.345.197 → 5.345.198
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/CHANGELOG.md +66 -0
- package/dist/command-catalog.generated.d.ts +287 -6
- package/dist/command-catalog.generated.js +352 -6
- package/dist/commands/core-resolve-company-native.d.ts +2 -1
- package/dist/commands/core-resolve-company-native.js +2 -24
- package/dist/commands/deploy-domains.d.ts +14 -2
- package/dist/commands/deploy-domains.js +726 -42
- package/dist/commands/integrations-connect.js +41 -3
- package/dist/commands/secrets.js +27 -6
- package/dist/lib/bot/company-bind.d.ts +34 -0
- package/dist/lib/bot/company-bind.js +112 -0
- package/dist/lib/bot/config.js +3 -1
- package/dist/lib/bot/fleet-local.d.ts +12 -0
- package/dist/lib/bot/fleet-local.js +40 -10
- package/dist/lib/bot/run.js +35 -8
- package/dist/lib/bot/session.d.ts +19 -1
- package/dist/lib/bot/session.js +75 -28
- package/dist/lib/core-utils/manifest-slugs.d.ts +2 -0
- package/dist/lib/core-utils/manifest-slugs.js +26 -0
- package/dist/lib/signals/steps/extract.d.ts +7 -0
- package/dist/lib/signals/steps/extract.js +11 -2
- package/dist/secrets-env-action.d.ts +70 -0
- package/dist/secrets-env-action.js +220 -2
- package/package.json +9 -7
|
@@ -112,6 +112,9 @@ const SETTLED_PURCHASE_CODES = new Set([
|
|
|
112
112
|
"INVALID_EXPECTED_PRICE",
|
|
113
113
|
"INVALID_IDEMPOTENCY_KEY",
|
|
114
114
|
"INVALID_REGISTRANT",
|
|
115
|
+
"CONSENT_REQUIRED",
|
|
116
|
+
"DOMAIN_LIMIT_REACHED",
|
|
117
|
+
"DOMAIN_SPEND_LIMIT_REACHED",
|
|
115
118
|
]);
|
|
116
119
|
/** The sentence `buy` adds when the order's outcome is not known. */
|
|
117
120
|
export function resumeOrderSentence(idempotencyKey) {
|
|
@@ -149,6 +152,9 @@ const PURCHASE_MESSAGES = {
|
|
|
149
152
|
INVALID_EXPECTED_PRICE: "--price must be the first-year price shown by 'hq deploy domains search'.",
|
|
150
153
|
INVALID_IDEMPOTENCY_KEY: "--idempotency-key must be 16 to 64 letters, digits, '-' or '_'.",
|
|
151
154
|
INVALID_REGISTRANT: "The registrant details are missing or invalid. The file needs firstName, lastName, address1, city, state, zip, a 2-letter country, email, and phone in +15551234567 form.",
|
|
155
|
+
CONSENT_REQUIRED: "The purchase needs acceptance of the terms, ICANN consent, and the 60-day transfer lock. Run buy again and accept the confirmation.",
|
|
156
|
+
DOMAIN_LIMIT_REACHED: "This company already has the maximum of 10 purchased domains. Nothing was charged. Let one lapse before buying another.",
|
|
157
|
+
DOMAIN_SPEND_LIMIT_REACHED: "This purchase would pass the company's domain spending limit for the last 30 days. Nothing was charged. Wait until that window moves, or ask an owner to raise the limit, then run buy again.",
|
|
152
158
|
};
|
|
153
159
|
function apiCode(body, status) {
|
|
154
160
|
const nested = body.error && typeof body.error === "object" ? body.error.code : undefined;
|
|
@@ -407,8 +413,12 @@ export function addOnConfirmQuestion(domain, environment, target, pricing) {
|
|
|
407
413
|
: `Each custom domain is a monthly add-on billed to ${company}, unless its plan includes custom domains.`;
|
|
408
414
|
return `Add ${displayText(domain)} to ${displayText(environment)} of app '${displayText(target.appLabel)}' in ${company}? ${price}`;
|
|
409
415
|
}
|
|
410
|
-
/** Shown in the buy prompt when GET /v1/pricing
|
|
411
|
-
export const PURCHASE_TERMS_UNAVAILABLE = "The renewal
|
|
416
|
+
/** Shown in the buy prompt when neither search nor GET /v1/pricing names a renewal price. */
|
|
417
|
+
export const PURCHASE_TERMS_UNAVAILABLE = "The renewal price could not be loaded. Run 'hq deploy domains search' and check the renewal price before you buy.";
|
|
418
|
+
/** Sandbox label. Every search and buy surface prints this when the server says testMode. */
|
|
419
|
+
export const TEST_MODE_LABEL = "Test mode: sandbox prices. Nothing real is registered or charged.";
|
|
420
|
+
export const TRANSFER_LOCK_NOTICE = "Domains cannot be transferred to another registrar for 60 days after purchase.";
|
|
421
|
+
export const PURCHASES_ARE_FINAL = "Purchases are final.";
|
|
412
422
|
/** The purchase terms published as usage.domains.purchase. Null when not published. */
|
|
413
423
|
export function purchaseTermsLine(pricing) {
|
|
414
424
|
const purchase = domainsBlock(pricing)?.purchase;
|
|
@@ -416,12 +426,11 @@ export function purchaseTermsLine(pricing) {
|
|
|
416
426
|
return null;
|
|
417
427
|
const p = purchase;
|
|
418
428
|
const markup = typeof p.markupUsdPerYear === "number" ? p.markupUsdPerYear : null;
|
|
419
|
-
|
|
420
|
-
if (markup === null || grace === null)
|
|
429
|
+
if (markup === null)
|
|
421
430
|
return null;
|
|
422
431
|
const renewal = p.renewal === "yearly" ? "yearly" : String(p.renewal ?? "yearly");
|
|
423
432
|
const before = p.chargedBeforeRenewal === true ? ", charged before each renewal" : "";
|
|
424
|
-
return `It renews ${renewal} at the registrar's renewal price plus ${usd(markup)}${before}
|
|
433
|
+
return `It renews ${renewal} at the registrar's renewal price plus ${usd(markup)}${before}.`;
|
|
425
434
|
}
|
|
426
435
|
// ---------------------------------------------------------------------------
|
|
427
436
|
// Output
|
|
@@ -475,15 +484,21 @@ async function run(json, action) {
|
|
|
475
484
|
emitError(error, json === true);
|
|
476
485
|
}
|
|
477
486
|
}
|
|
478
|
-
|
|
487
|
+
function requireConfirmable(yes, what) {
|
|
479
488
|
if (yes)
|
|
480
489
|
return;
|
|
481
490
|
if (!deployDomainsPrompt.isInteractive()) {
|
|
482
491
|
throw new DeployDomainsError(`Pass --yes to ${what}. Nothing was changed.`, "CONFIRMATION_REQUIRED");
|
|
483
492
|
}
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
493
|
+
}
|
|
494
|
+
async function confirm(question, yes, what, accepted = ["yes"]) {
|
|
495
|
+
requireConfirmable(yes, what);
|
|
496
|
+
if (yes)
|
|
497
|
+
return;
|
|
498
|
+
const promptWord = accepted.includes("y") ? "y" : "yes";
|
|
499
|
+
const answer = (await deployDomainsPrompt.ask(`${question} Type '${promptWord}' to continue: `)).trim().toLowerCase();
|
|
500
|
+
if (!accepted.includes(answer))
|
|
501
|
+
throw new DeployDomainsError("Canceled. Nothing was changed.", "CANCELLED");
|
|
487
502
|
}
|
|
488
503
|
// ---------------------------------------------------------------------------
|
|
489
504
|
// Actions
|
|
@@ -832,13 +847,13 @@ async function baseRemoveAction(options) {
|
|
|
832
847
|
else if (billingRemovalPending)
|
|
833
848
|
console.log("Its charge could not be stopped right away. HQ keeps retrying until the charge is removed.");
|
|
834
849
|
}
|
|
835
|
-
async function purchaseRequest(token, companyUid, suffix, body) {
|
|
850
|
+
async function purchaseRequest(token, companyUid, suffix, body, method = "POST") {
|
|
836
851
|
try {
|
|
837
852
|
return await vaultApiFetch({
|
|
838
853
|
token,
|
|
839
854
|
path: `/v1/billing/companies/${encodeURIComponent(companyUid)}/domain-purchase${suffix}`,
|
|
840
|
-
method
|
|
841
|
-
body,
|
|
855
|
+
method,
|
|
856
|
+
...(body === undefined ? {} : { body }),
|
|
842
857
|
});
|
|
843
858
|
}
|
|
844
859
|
catch {
|
|
@@ -866,6 +881,72 @@ async function purchaseCompanyUid(token, slug) {
|
|
|
866
881
|
throw new DeployDomainsError(`Could not look up the company '${displayText(slug)}'${cause ? `: ${cause}` : "."}`, "COMPANY_LOOKUP_FAILED");
|
|
867
882
|
}
|
|
868
883
|
}
|
|
884
|
+
function availabilityOf(result) {
|
|
885
|
+
const raw = result.availability;
|
|
886
|
+
if (raw === "available" || raw === "taken" || raw === "delegated" || raw === "premium" || raw === "unsupported") {
|
|
887
|
+
return raw;
|
|
888
|
+
}
|
|
889
|
+
if (result.available === true)
|
|
890
|
+
return "available";
|
|
891
|
+
if (result.available === false)
|
|
892
|
+
return "legacy-unavailable";
|
|
893
|
+
return "unknown";
|
|
894
|
+
}
|
|
895
|
+
function availabilityLabel(kind) {
|
|
896
|
+
switch (kind) {
|
|
897
|
+
case "available": return "available";
|
|
898
|
+
case "taken": return "taken";
|
|
899
|
+
case "delegated": return "not available (already delegated)";
|
|
900
|
+
case "premium": return "premium";
|
|
901
|
+
case "unsupported": return "unsupported";
|
|
902
|
+
case "legacy-unavailable": return "not available";
|
|
903
|
+
default: return "not available";
|
|
904
|
+
}
|
|
905
|
+
}
|
|
906
|
+
/** Taken and delegated names are listed so the reason is visible, and never offered for purchase. */
|
|
907
|
+
function isOfferable(kind) {
|
|
908
|
+
return kind === "available" || kind === "premium";
|
|
909
|
+
}
|
|
910
|
+
function moneyFromCents(cents, currency) {
|
|
911
|
+
if (typeof cents !== "number" || !Number.isFinite(cents) || cents < 0 || !Number.isInteger(cents))
|
|
912
|
+
return undefined;
|
|
913
|
+
const amount = cents / 100;
|
|
914
|
+
const text = Number.isInteger(amount) ? `$${amount}` : `$${amount.toFixed(2)}`;
|
|
915
|
+
if (typeof currency === "string" && currency !== "" && currency !== "USD")
|
|
916
|
+
return `${text} ${currency}`;
|
|
917
|
+
return text;
|
|
918
|
+
}
|
|
919
|
+
function quoteOf(result) {
|
|
920
|
+
const registrar = result.registrarPrice;
|
|
921
|
+
const block = registrar && typeof registrar === "object" ? registrar : undefined;
|
|
922
|
+
const currency = block?.currency;
|
|
923
|
+
const firstFromCents = moneyFromCents(block?.firstYearCents, currency);
|
|
924
|
+
const renewalFromCents = moneyFromCents(block?.renewalCents, currency);
|
|
925
|
+
const firstYearUsd = typeof block?.firstYearCents === "number"
|
|
926
|
+
? block.firstYearCents / 100
|
|
927
|
+
: typeof result.priceUsd === "number" ? result.priceUsd : undefined;
|
|
928
|
+
return {
|
|
929
|
+
firstYear: firstFromCents ?? (typeof result.priceUsd === "number" ? usd(result.priceUsd) : undefined),
|
|
930
|
+
renewal: renewalFromCents ?? (typeof result.renewalPriceUsd === "number" ? usd(result.renewalPriceUsd) : undefined),
|
|
931
|
+
...(firstYearUsd === undefined ? {} : { firstYearUsd }),
|
|
932
|
+
};
|
|
933
|
+
}
|
|
934
|
+
function requireObjectList(value, message) {
|
|
935
|
+
if (!Array.isArray(value)) {
|
|
936
|
+
throw new DeployDomainsError(message, "UNEXPECTED_RESPONSE");
|
|
937
|
+
}
|
|
938
|
+
return value.filter((item) => typeof item === "object" && item !== null);
|
|
939
|
+
}
|
|
940
|
+
function searchResultLine(result) {
|
|
941
|
+
const domain = displayText(result.domain);
|
|
942
|
+
const kind = availabilityOf(result);
|
|
943
|
+
const label = availabilityLabel(kind);
|
|
944
|
+
const quote = quoteOf(result);
|
|
945
|
+
if (!isOfferable(kind) || !quote.firstYear)
|
|
946
|
+
return `${domain} | ${label}`;
|
|
947
|
+
const renewal = quote.renewal ? `, renews at ${quote.renewal}/year` : "";
|
|
948
|
+
return `${domain} | ${label} | ${quote.firstYear} for the first year${renewal}`;
|
|
949
|
+
}
|
|
869
950
|
async function searchAction(query, options) {
|
|
870
951
|
const token = await ensureCognitoToken();
|
|
871
952
|
const companyUid = await purchaseCompanyUid(token, purchaseSlug(options.company));
|
|
@@ -873,26 +954,26 @@ async function searchAction(query, options) {
|
|
|
873
954
|
if (!response.ok)
|
|
874
955
|
throw await purchaseError(response);
|
|
875
956
|
const body = await readBody(response);
|
|
876
|
-
const results =
|
|
877
|
-
? body.results.filter((item) => typeof item === "object" && item !== null)
|
|
878
|
-
: [];
|
|
957
|
+
const results = requireObjectList(body.results, "HQ did not return a list of domains. Run 'hq deploy domains search' again in a minute.");
|
|
879
958
|
if (options.json) {
|
|
880
|
-
console.log(safeJson(
|
|
959
|
+
console.log(safeJson(body));
|
|
881
960
|
return;
|
|
882
961
|
}
|
|
962
|
+
if (body.testMode === true)
|
|
963
|
+
console.log(TEST_MODE_LABEL);
|
|
883
964
|
if (results.length === 0) {
|
|
884
965
|
console.log("No domains found.");
|
|
885
966
|
return;
|
|
886
967
|
}
|
|
968
|
+
let offerable = 0;
|
|
887
969
|
for (const result of results) {
|
|
888
|
-
if (result
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
console.log(
|
|
970
|
+
if (isOfferable(availabilityOf(result)))
|
|
971
|
+
offerable += 1;
|
|
972
|
+
console.log(searchResultLine(result));
|
|
973
|
+
}
|
|
974
|
+
if (offerable > 0) {
|
|
975
|
+
console.log("To buy one: hq deploy domains buy <domain> --use app:<app-id>|base|hold --price <first-year price> --registrant-file <contact.json>");
|
|
894
976
|
}
|
|
895
|
-
console.log("To buy one: hq deploy domains buy <domain> --price <first-year price> --registrant-file <contact.json>");
|
|
896
977
|
}
|
|
897
978
|
function readRegistrant(file) {
|
|
898
979
|
let parsed;
|
|
@@ -921,23 +1002,189 @@ export function parsePrice(raw) {
|
|
|
921
1002
|
}
|
|
922
1003
|
return value;
|
|
923
1004
|
}
|
|
1005
|
+
/** What the purchase is for. `app:<id>` attaches to that deployment, `base` is the company domain, `hold` registers only. */
|
|
1006
|
+
export function parseUse(raw) {
|
|
1007
|
+
const trimmed = raw.trim();
|
|
1008
|
+
if (trimmed === "base")
|
|
1009
|
+
return { kind: "base" };
|
|
1010
|
+
if (trimmed === "hold")
|
|
1011
|
+
return { kind: "hold" };
|
|
1012
|
+
const match = /^app:(.+)$/.exec(trimmed);
|
|
1013
|
+
if (match && APP_ID_PATTERN.test(match[1]))
|
|
1014
|
+
return { kind: "app", appId: match[1] };
|
|
1015
|
+
throw new DeployDomainsError("Pass --use app:<app id>, base, or hold. For an app, use the id from 'hq deploy list', for example --use app:app_123.", "INVALID_USE");
|
|
1016
|
+
}
|
|
1017
|
+
function useBody(use) {
|
|
1018
|
+
return use.kind === "app" ? { kind: "app", appId: use.appId } : { kind: use.kind };
|
|
1019
|
+
}
|
|
1020
|
+
function useLabel(use) {
|
|
1021
|
+
if (!use || typeof use !== "object")
|
|
1022
|
+
return "unknown";
|
|
1023
|
+
const record = use;
|
|
1024
|
+
if (record.kind === "app" && typeof record.appId === "string")
|
|
1025
|
+
return `app ${record.appId}`;
|
|
1026
|
+
if (record.kind === "base" || record.kind === "hold")
|
|
1027
|
+
return record.kind;
|
|
1028
|
+
return "unknown";
|
|
1029
|
+
}
|
|
1030
|
+
/** Hosting tier, separate from the registrar price. Hold is register-only. */
|
|
1031
|
+
export function hostingTierLine(use, pricing) {
|
|
1032
|
+
if (use.kind === "hold")
|
|
1033
|
+
return "Hosting: $0 per month (register only).";
|
|
1034
|
+
if (use.kind === "base") {
|
|
1035
|
+
const premium = domainsBlock(pricing)?.premiumUsd;
|
|
1036
|
+
const amount = typeof premium === "number" && premium >= 0 ? premium : 500;
|
|
1037
|
+
return `Hosting: ${usd(amount)} per month (company base domain).`;
|
|
1038
|
+
}
|
|
1039
|
+
const standard = domainsBlock(pricing)?.standardUsd;
|
|
1040
|
+
const amount = typeof standard === "number" && standard >= 0 ? standard : 20;
|
|
1041
|
+
return `Hosting: ${usd(amount)} per month (app domain).`;
|
|
1042
|
+
}
|
|
1043
|
+
function sameUsd(left, right) {
|
|
1044
|
+
return Math.round(left * 100) === Math.round(right * 100);
|
|
1045
|
+
}
|
|
1046
|
+
function registrarCurrency(result) {
|
|
1047
|
+
const registrar = result.registrarPrice;
|
|
1048
|
+
if (!registrar || typeof registrar !== "object")
|
|
1049
|
+
return undefined;
|
|
1050
|
+
const currency = registrar.currency;
|
|
1051
|
+
return typeof currency === "string" && currency !== "" ? currency : undefined;
|
|
1052
|
+
}
|
|
1053
|
+
/** First-year amount comparable to --price. A quote in another currency is not one. */
|
|
1054
|
+
function usdFirstYearAmount(result) {
|
|
1055
|
+
const currency = registrarCurrency(result);
|
|
1056
|
+
if (currency !== undefined && currency !== "USD")
|
|
1057
|
+
return undefined;
|
|
1058
|
+
const registrar = result.registrarPrice;
|
|
1059
|
+
const block = registrar && typeof registrar === "object" ? registrar : undefined;
|
|
1060
|
+
const cents = block?.firstYearCents;
|
|
1061
|
+
if (typeof cents === "number" && Number.isInteger(cents) && cents >= 0)
|
|
1062
|
+
return cents / 100;
|
|
1063
|
+
const price = result.priceUsd;
|
|
1064
|
+
if (typeof price === "number" && Number.isFinite(price) && price >= 0)
|
|
1065
|
+
return price;
|
|
1066
|
+
return undefined;
|
|
1067
|
+
}
|
|
1068
|
+
function assertMatchingUsdPrice(match, quote, expectedPriceUsd) {
|
|
1069
|
+
const currency = registrarCurrency(match);
|
|
1070
|
+
if (currency !== undefined && currency !== "USD") {
|
|
1071
|
+
const shown = quote.firstYear
|
|
1072
|
+
? `The registrar's first-year price is ${quote.firstYear}, not US dollars.`
|
|
1073
|
+
: "The registrar's first-year price is not in US dollars.";
|
|
1074
|
+
throw new DeployDomainsError(`${shown} Nothing was charged. Run 'hq deploy domains search' and buy a name priced in USD.`, "PRICE_CHANGED");
|
|
1075
|
+
}
|
|
1076
|
+
const amount = usdFirstYearAmount(match);
|
|
1077
|
+
if (amount === undefined) {
|
|
1078
|
+
throw new DeployDomainsError("The registrar did not give a US dollar first-year price. Nothing was charged. Run 'hq deploy domains search' and buy a name that shows a price.", "PRICE_CHANGED");
|
|
1079
|
+
}
|
|
1080
|
+
if (!sameUsd(amount, expectedPriceUsd)) {
|
|
1081
|
+
throw new DeployDomainsError(`The registrar's first-year price is ${quote.firstYear ?? usd(amount)}. Nothing was charged. Run buy again with --price ${usdFlag(amount)}.`, "PRICE_CHANGED");
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
function usdFlag(value) {
|
|
1085
|
+
const cents = Math.round(value * 100);
|
|
1086
|
+
const text = (cents / 100).toFixed(2);
|
|
1087
|
+
return text.endsWith(".00") ? String(cents / 100) : text;
|
|
1088
|
+
}
|
|
1089
|
+
const PURCHASE_CONSENT = {
|
|
1090
|
+
termsAccepted: true,
|
|
1091
|
+
icannConsent: true,
|
|
1092
|
+
transferLockNoticeShown: true,
|
|
1093
|
+
};
|
|
1094
|
+
async function loadBuyQuote(token, companyUid, domain, expectedPriceUsd, replay) {
|
|
1095
|
+
let response;
|
|
1096
|
+
try {
|
|
1097
|
+
response = await purchaseRequest(token, companyUid, "/search", { query: domain });
|
|
1098
|
+
}
|
|
1099
|
+
catch {
|
|
1100
|
+
throw new DeployDomainsError("Could not reach HQ to check the domain. Nothing was charged. Run 'hq deploy domains search' and try buy again.", "NETWORK");
|
|
1101
|
+
}
|
|
1102
|
+
if (!response.ok)
|
|
1103
|
+
throw await purchaseError(response);
|
|
1104
|
+
const body = await readBody(response);
|
|
1105
|
+
const results = Array.isArray(body.results)
|
|
1106
|
+
? body.results.filter((item) => typeof item === "object" && item !== null)
|
|
1107
|
+
: [];
|
|
1108
|
+
const match = results.find((item) => typeof item.domain === "string" && item.domain.toLowerCase() === domain);
|
|
1109
|
+
// After the registrar has the name, search reports it missing or taken, and
|
|
1110
|
+
// often has no fresh USD quote. A retry with --idempotency-key still confirms,
|
|
1111
|
+
// then POSTs that same key so hq-pro can replay the order.
|
|
1112
|
+
const replayDespiteSearch = replay && (!match || availabilityOf(match) === "taken");
|
|
1113
|
+
if (!match && !replayDespiteSearch) {
|
|
1114
|
+
throw new DeployDomainsError(`${displayText(domain)} was not in the search results. Nothing was charged. Run 'hq deploy domains search ${displayText(domain)}' and buy a name it lists.`, "DOMAIN_NOT_AVAILABLE");
|
|
1115
|
+
}
|
|
1116
|
+
const kind = match ? availabilityOf(match) : undefined;
|
|
1117
|
+
if (match && kind && !replayDespiteSearch && !isOfferable(kind)) {
|
|
1118
|
+
const reason = kind === "delegated"
|
|
1119
|
+
? `${displayText(domain)} is not available (already delegated).`
|
|
1120
|
+
: kind === "taken" || kind === "legacy-unavailable"
|
|
1121
|
+
? `${displayText(domain)} is not available to buy.`
|
|
1122
|
+
: `${displayText(domain)} is not supported for purchase.`;
|
|
1123
|
+
throw new DeployDomainsError(`${reason} Nothing was charged. Run 'hq deploy domains search' and pick an available name.`, "DOMAIN_NOT_AVAILABLE");
|
|
1124
|
+
}
|
|
1125
|
+
const quote = match ? quoteOf(match) : {};
|
|
1126
|
+
if (match && !replayDespiteSearch)
|
|
1127
|
+
assertMatchingUsdPrice(match, quote, expectedPriceUsd);
|
|
1128
|
+
return {
|
|
1129
|
+
testMode: body.testMode === true,
|
|
1130
|
+
...(quote.renewal ? { renewal: quote.renewal } : {}),
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
function buySummary(domain, slug, expectedPriceUsd, use, pricing, quote, terms) {
|
|
1134
|
+
const renewal = quote.renewal
|
|
1135
|
+
? `Renewal: ${quote.renewal} per year`
|
|
1136
|
+
: `Renewal: ${terms ?? PURCHASE_TERMS_UNAVAILABLE}`;
|
|
1137
|
+
const lines = [
|
|
1138
|
+
`Buy ${displayText(domain)} for the company '${slug}'?`,
|
|
1139
|
+
`Registrar, first year: ${usd(expectedPriceUsd)}`,
|
|
1140
|
+
renewal,
|
|
1141
|
+
terms ?? PURCHASE_TERMS_UNAVAILABLE,
|
|
1142
|
+
hostingTierLine(use, pricing),
|
|
1143
|
+
TRANSFER_LOCK_NOTICE,
|
|
1144
|
+
PURCHASES_ARE_FINAL,
|
|
1145
|
+
];
|
|
1146
|
+
if (quote.testMode)
|
|
1147
|
+
lines.push(TEST_MODE_LABEL);
|
|
1148
|
+
return lines.join("\n");
|
|
1149
|
+
}
|
|
1150
|
+
function printReceiptNotices(testMode) {
|
|
1151
|
+
console.log(TRANSFER_LOCK_NOTICE);
|
|
1152
|
+
console.log(PURCHASES_ARE_FINAL);
|
|
1153
|
+
if (testMode)
|
|
1154
|
+
console.log(TEST_MODE_LABEL);
|
|
1155
|
+
}
|
|
924
1156
|
async function buyAction(domainArg, options) {
|
|
925
1157
|
const domain = normalizeDomain(domainArg);
|
|
926
1158
|
const expectedPriceUsd = parsePrice(options.price);
|
|
1159
|
+
const use = parseUse(options.use);
|
|
927
1160
|
if (options.idempotencyKey !== undefined && !IDEMPOTENCY_KEY_PATTERN.test(options.idempotencyKey)) {
|
|
928
1161
|
throw new DeployDomainsError(PURCHASE_MESSAGES.INVALID_IDEMPOTENCY_KEY, "INVALID_IDEMPOTENCY_KEY");
|
|
929
1162
|
}
|
|
930
1163
|
const registrant = readRegistrant(options.registrantFile);
|
|
931
|
-
const idempotencyKey = options.idempotencyKey ?? `cli_${randomUUID().replace(/-/g, "")}`;
|
|
932
1164
|
// Resolve the paying company before the prompt so the approval names it.
|
|
933
1165
|
const slug = purchaseSlug(options.company);
|
|
934
1166
|
const token = await ensureCognitoToken();
|
|
935
1167
|
const companyUid = await purchaseCompanyUid(token, slug);
|
|
936
|
-
|
|
937
|
-
|
|
1168
|
+
// A non-interactive buy without --yes must stop before the price check, so nothing is called.
|
|
1169
|
+
requireConfirmable(options.yes, "buy the domain");
|
|
1170
|
+
const pricing = await fetchPricingBody();
|
|
1171
|
+
const terms = purchaseTermsLine(pricing);
|
|
1172
|
+
const quote = await loadBuyQuote(token, companyUid, domain, expectedPriceUsd, options.idempotencyKey !== undefined);
|
|
1173
|
+
const summary = buySummary(domain, slug, expectedPriceUsd, use, pricing, quote, terms);
|
|
1174
|
+
if (options.yes)
|
|
1175
|
+
console.error(summary);
|
|
1176
|
+
await confirm(summary, options.yes, "buy the domain", ["y", "yes"]);
|
|
1177
|
+
const idempotencyKey = options.idempotencyKey ?? `cli_${randomUUID().replace(/-/g, "")}`;
|
|
938
1178
|
let response;
|
|
939
1179
|
try {
|
|
940
|
-
response = await purchaseRequest(token, companyUid, "", {
|
|
1180
|
+
response = await purchaseRequest(token, companyUid, "", {
|
|
1181
|
+
domain,
|
|
1182
|
+
expectedPriceUsd,
|
|
1183
|
+
idempotencyKey,
|
|
1184
|
+
registrant,
|
|
1185
|
+
use: useBody(use),
|
|
1186
|
+
consent: { ...PURCHASE_CONSENT },
|
|
1187
|
+
});
|
|
941
1188
|
}
|
|
942
1189
|
catch (error) {
|
|
943
1190
|
// The request may have reached hq-pro, so the order's key is the only safe retry.
|
|
@@ -948,10 +1195,15 @@ async function buyAction(domainArg, options) {
|
|
|
948
1195
|
throw await purchaseError(response, idempotencyKey);
|
|
949
1196
|
const body = await readBody(response);
|
|
950
1197
|
if (options.json) {
|
|
951
|
-
console.log(safeJson({ ok: true, idempotencyKey, ...body }));
|
|
1198
|
+
console.log(safeJson({ ok: true, idempotencyKey, ...body, approvedTerms: terms ?? PURCHASE_TERMS_UNAVAILABLE }));
|
|
952
1199
|
return;
|
|
953
1200
|
}
|
|
954
1201
|
const expires = typeof body.expiresAt === "string" ? ` It is registered until ${displayText(body.expiresAt)}.` : "";
|
|
1202
|
+
const testMode = body.testMode === true || quote.testMode;
|
|
1203
|
+
const hostingCents = body.hostingPriceCents;
|
|
1204
|
+
const hosting = typeof hostingCents === "number" && Number.isFinite(hostingCents) && hostingCents >= 0
|
|
1205
|
+
? `Hosting: ${moneyFromCents(hostingCents, "USD")} per month.`
|
|
1206
|
+
: hostingTierLine(use, pricing);
|
|
955
1207
|
if (body.replayed === true) {
|
|
956
1208
|
console.log(`${displayText(domain)} was already bought by this order.${expires}`);
|
|
957
1209
|
}
|
|
@@ -961,13 +1213,407 @@ async function buyAction(domainArg, options) {
|
|
|
961
1213
|
// registration for this order and captures again (hq-pro domain-purchase.ts).
|
|
962
1214
|
console.log(`Registered ${displayText(domain)}.${expires} The card was authorized for ${usd(expectedPriceUsd)}, ` +
|
|
963
1215
|
`but the charge did not complete. ${retryChargeSentence(idempotencyKey)}`);
|
|
1216
|
+
console.log(terms ?? PURCHASE_TERMS_UNAVAILABLE);
|
|
1217
|
+
printReceiptNotices(testMode);
|
|
1218
|
+
return;
|
|
964
1219
|
}
|
|
965
1220
|
else {
|
|
966
1221
|
console.log(`Bought ${displayText(domain)} for ${usd(expectedPriceUsd)}.${expires}`);
|
|
967
1222
|
}
|
|
968
|
-
if (
|
|
969
|
-
console.log(
|
|
970
|
-
console.log(
|
|
1223
|
+
if (quote.renewal)
|
|
1224
|
+
console.log(`Renewal: ${quote.renewal} per year.`);
|
|
1225
|
+
console.log(terms ?? PURCHASE_TERMS_UNAVAILABLE);
|
|
1226
|
+
console.log(hosting);
|
|
1227
|
+
console.log(`Use: ${useLabel(useBody(use))}.`);
|
|
1228
|
+
printReceiptNotices(testMode);
|
|
1229
|
+
if (use.kind === "hold") {
|
|
1230
|
+
console.log(`Registered only. To serve an app on it later: hq deploy domains add ${displayText(domain)} --app <app>`);
|
|
1231
|
+
}
|
|
1232
|
+
else {
|
|
1233
|
+
console.log(`Check it with: hq deploy domains show ${displayText(domain)}`);
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
// ---------------------------------------------------------------------------
|
|
1237
|
+
// Purchased domains: list, detail, auto-renew (hq-pro)
|
|
1238
|
+
function ownedLine(row) {
|
|
1239
|
+
const expires = typeof row.expiresAt === "string" ? displayText(row.expiresAt) : "unknown";
|
|
1240
|
+
const renew = row.autoRenew === true ? "auto-renew on" : "auto-renew off";
|
|
1241
|
+
const testMode = row.testMode === true ? "test mode on" : "test mode off";
|
|
1242
|
+
return [
|
|
1243
|
+
displayText(row.domain),
|
|
1244
|
+
displayText(row.status ?? "unknown"),
|
|
1245
|
+
useLabel(row.use),
|
|
1246
|
+
expires,
|
|
1247
|
+
renew,
|
|
1248
|
+
testMode,
|
|
1249
|
+
].join(" | ");
|
|
1250
|
+
}
|
|
1251
|
+
function eventLine(event) {
|
|
1252
|
+
if (typeof event === "string")
|
|
1253
|
+
return displayText(event);
|
|
1254
|
+
if (!event || typeof event !== "object")
|
|
1255
|
+
return "";
|
|
1256
|
+
const record = event;
|
|
1257
|
+
const kind = record.type ?? record.kind ?? record.event ?? "event";
|
|
1258
|
+
const at = record.at ?? record.createdAt ?? record.date ?? "";
|
|
1259
|
+
const detail = record.message ?? record.detail ?? "";
|
|
1260
|
+
return [kind, at, detail]
|
|
1261
|
+
.filter((part) => typeof part === "string" && part !== "")
|
|
1262
|
+
.map((part) => displayText(part))
|
|
1263
|
+
.join(" ");
|
|
1264
|
+
}
|
|
1265
|
+
function attachedLine(attached) {
|
|
1266
|
+
if (!attached || typeof attached !== "object")
|
|
1267
|
+
return "Attached: not attached";
|
|
1268
|
+
const record = attached;
|
|
1269
|
+
if (record.kind !== "app" && record.kind !== "base")
|
|
1270
|
+
return "Attached: not attached";
|
|
1271
|
+
const appId = typeof record.appId === "string" && record.appId !== "" ? displayText(record.appId) : undefined;
|
|
1272
|
+
const hostname = typeof record.hostname === "string" && record.hostname !== "" ? displayText(record.hostname) : undefined;
|
|
1273
|
+
if (appId && hostname)
|
|
1274
|
+
return `Attached: ${record.kind} ${appId} (${hostname})`;
|
|
1275
|
+
if (appId)
|
|
1276
|
+
return `Attached: ${record.kind} ${appId}`;
|
|
1277
|
+
if (hostname)
|
|
1278
|
+
return `Attached: ${record.kind} ${hostname}`;
|
|
1279
|
+
return `Attached: ${record.kind}`;
|
|
1280
|
+
}
|
|
1281
|
+
function registrarNameserversLine(value) {
|
|
1282
|
+
if (value === true)
|
|
1283
|
+
return "Registrar nameservers: set";
|
|
1284
|
+
if (value === false)
|
|
1285
|
+
return "Registrar nameservers: not set";
|
|
1286
|
+
return "Registrar nameservers: unknown";
|
|
1287
|
+
}
|
|
1288
|
+
function printDomainDetail(body) {
|
|
1289
|
+
console.log(ownedLine(body));
|
|
1290
|
+
console.log(attachedLine(body.attached));
|
|
1291
|
+
console.log(registrarNameserversLine(body.registrarNameserversSet));
|
|
1292
|
+
const nameservers = Array.isArray(body.nameservers)
|
|
1293
|
+
? body.nameservers.filter((item) => typeof item === "string")
|
|
1294
|
+
: [];
|
|
1295
|
+
console.log(nameservers.length === 0 ? "Nameservers: none" : "Nameservers:");
|
|
1296
|
+
for (const nameserver of nameservers)
|
|
1297
|
+
console.log(` ${displayText(nameserver)}`);
|
|
1298
|
+
const events = Array.isArray(body.events) ? body.events : [];
|
|
1299
|
+
console.log(events.length === 0 ? "Events: none" : "Events:");
|
|
1300
|
+
for (const event of events) {
|
|
1301
|
+
const line = eventLine(event);
|
|
1302
|
+
if (line)
|
|
1303
|
+
console.log(` ${line}`);
|
|
1304
|
+
}
|
|
1305
|
+
if (body.testMode === true)
|
|
1306
|
+
console.log(TEST_MODE_LABEL);
|
|
1307
|
+
}
|
|
1308
|
+
async function ownedDomainError(response, domain) {
|
|
1309
|
+
const error = await purchaseError(response);
|
|
1310
|
+
if (response.status === 404) {
|
|
1311
|
+
return new DeployDomainsError(`This company does not own ${displayText(domain)}. Run 'hq deploy domains owned' to see purchased domains.`, error.code === "HTTP_404" ? "DOMAIN_NOT_OWNED" : error.code, response.status);
|
|
1312
|
+
}
|
|
1313
|
+
return error;
|
|
1314
|
+
}
|
|
1315
|
+
async function ownedAction(options) {
|
|
1316
|
+
const token = await ensureCognitoToken();
|
|
1317
|
+
const companyUid = await purchaseCompanyUid(token, purchaseSlug(options.company));
|
|
1318
|
+
const response = await purchaseRequest(token, companyUid, "/domains", undefined, "GET");
|
|
1319
|
+
if (!response.ok)
|
|
1320
|
+
throw await purchaseError(response);
|
|
1321
|
+
const body = await readBody(response);
|
|
1322
|
+
const domains = requireObjectList(body.domains, "HQ did not return the purchased-domain list. Run 'hq deploy domains owned' again in a minute.");
|
|
1323
|
+
if (options.json) {
|
|
1324
|
+
console.log(safeJson(body));
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
if (domains.some((item) => item.testMode === true))
|
|
1328
|
+
console.log(TEST_MODE_LABEL);
|
|
1329
|
+
if (domains.length === 0) {
|
|
1330
|
+
console.log("No purchased domains.");
|
|
1331
|
+
return;
|
|
1332
|
+
}
|
|
1333
|
+
for (const domain of domains)
|
|
1334
|
+
console.log(ownedLine(domain));
|
|
1335
|
+
}
|
|
1336
|
+
async function showAction(domainArg, options) {
|
|
1337
|
+
const domain = normalizeDomain(domainArg);
|
|
1338
|
+
const token = await ensureCognitoToken();
|
|
1339
|
+
const companyUid = await purchaseCompanyUid(token, purchaseSlug(options.company));
|
|
1340
|
+
const response = await purchaseRequest(token, companyUid, `/domains/${encodeURIComponent(domain)}`, undefined, "GET");
|
|
1341
|
+
if (!response.ok)
|
|
1342
|
+
throw await ownedDomainError(response, domain);
|
|
1343
|
+
const body = await readBody(response);
|
|
1344
|
+
if (options.json) {
|
|
1345
|
+
console.log(safeJson(body));
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
printDomainDetail(body);
|
|
1349
|
+
}
|
|
1350
|
+
async function renewAction(domainArg, autoRenew, options) {
|
|
1351
|
+
const domain = normalizeDomain(domainArg);
|
|
1352
|
+
const slug = purchaseSlug(options.company);
|
|
1353
|
+
requireConfirmable(options.yes, "change automatic renewal");
|
|
1354
|
+
const token = await ensureCognitoToken();
|
|
1355
|
+
const companyUid = await purchaseCompanyUid(token, slug);
|
|
1356
|
+
const detailResponse = await purchaseRequest(token, companyUid, "/domains/" + encodeURIComponent(domain), undefined, "GET");
|
|
1357
|
+
if (!detailResponse.ok)
|
|
1358
|
+
throw await ownedDomainError(detailResponse, domain);
|
|
1359
|
+
const detail = await readBody(detailResponse);
|
|
1360
|
+
const pricing = await fetchPricingBody();
|
|
1361
|
+
const terms = purchaseTermsLine(pricing);
|
|
1362
|
+
const cents = detail.renewalPriceCents;
|
|
1363
|
+
const currency = detail.currency;
|
|
1364
|
+
const amount = moneyFromCents(cents, currency);
|
|
1365
|
+
if (typeof cents !== "number" || !Number.isSafeInteger(cents) || cents < 0 || currency !== "USD" || !amount || !terms) {
|
|
1366
|
+
throw new DeployDomainsError("Could not load complete USD renewal terms. Nothing was changed. Try again later.", "RENEWAL_TERMS_UNAVAILABLE");
|
|
1367
|
+
}
|
|
1368
|
+
const nextRenewal = detail.nextRenewalAt ?? detail.expiresAt;
|
|
1369
|
+
const summary = [
|
|
1370
|
+
"Turn auto-renew " + (autoRenew ? "on" : "off") + " for " + displayText(domain) + " for the company '" + displayText(slug) + "'?",
|
|
1371
|
+
"Registrar renewal: " + amount + " USD per year.",
|
|
1372
|
+
terms,
|
|
1373
|
+
typeof nextRenewal === "string" ? "Next renewal: " + displayText(nextRenewal) : "Next renewal date: unavailable.",
|
|
1374
|
+
].join("\n");
|
|
1375
|
+
if (options.yes)
|
|
1376
|
+
console.error(summary);
|
|
1377
|
+
await confirm(summary, options.yes, "change automatic renewal", ["y", "yes"]);
|
|
1378
|
+
const response = await purchaseRequest(token, companyUid, `/domains/${encodeURIComponent(domain)}`, { autoRenew }, "PATCH");
|
|
1379
|
+
if (!response.ok)
|
|
1380
|
+
throw await ownedDomainError(response, domain);
|
|
1381
|
+
const body = await readBody(response);
|
|
1382
|
+
if (options.json) {
|
|
1383
|
+
console.log(safeJson(body));
|
|
1384
|
+
return;
|
|
1385
|
+
}
|
|
1386
|
+
console.log(autoRenew
|
|
1387
|
+
? `Auto-renew is on for ${displayText(domain)}.`
|
|
1388
|
+
: `Auto-renew is off for ${displayText(domain)}. It will not renew.`);
|
|
1389
|
+
}
|
|
1390
|
+
// ---------------------------------------------------------------------------
|
|
1391
|
+
// DNS records on a purchased zone (hq-deploy)
|
|
1392
|
+
const RECORD_TYPES = new Set(["A", "AAAA", "CNAME", "MX", "TXT", "SRV", "CAA"]);
|
|
1393
|
+
const RECORD_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9_.:-]{0,127}$/;
|
|
1394
|
+
function collectRepeated(value, previous) {
|
|
1395
|
+
return [...previous, value];
|
|
1396
|
+
}
|
|
1397
|
+
function parseRecordType(raw) {
|
|
1398
|
+
const type = raw.trim().toUpperCase();
|
|
1399
|
+
if (!RECORD_TYPES.has(type)) {
|
|
1400
|
+
throw new DeployDomainsError("Record type must be A, AAAA, CNAME, MX, TXT, SRV, or CAA. Check the type and run the command again.", "RECORD_INVALID");
|
|
1401
|
+
}
|
|
1402
|
+
return type;
|
|
1403
|
+
}
|
|
1404
|
+
function parseRecordName(raw) {
|
|
1405
|
+
const name = raw.trim();
|
|
1406
|
+
if (!name || /[\s/\\]/.test(name) || name.length > 253) {
|
|
1407
|
+
throw new DeployDomainsError("Pass a DNS name, such as www or the full domain. It cannot contain spaces. Check the name and run the command again.", "RECORD_INVALID");
|
|
1408
|
+
}
|
|
1409
|
+
return name;
|
|
1410
|
+
}
|
|
1411
|
+
function parseTtl(raw) {
|
|
1412
|
+
if (raw === undefined)
|
|
1413
|
+
return undefined;
|
|
1414
|
+
if (!/^\d+$/.test(raw.trim())) {
|
|
1415
|
+
throw new DeployDomainsError("--ttl must be a whole number of seconds, from 0 to 2147483647. Check it and run the command again.", "RECORD_INVALID");
|
|
1416
|
+
}
|
|
1417
|
+
const ttl = Number(raw.trim());
|
|
1418
|
+
if (ttl > 2_147_483_647) {
|
|
1419
|
+
throw new DeployDomainsError("--ttl must be a whole number of seconds, from 0 to 2147483647. Check it and run the command again.", "RECORD_INVALID");
|
|
1420
|
+
}
|
|
1421
|
+
return ttl;
|
|
1422
|
+
}
|
|
1423
|
+
function parseRecordValues(values, required) {
|
|
1424
|
+
if (!required && (!values || values.length === 0))
|
|
1425
|
+
return undefined;
|
|
1426
|
+
const parsed = (values ?? []).map((value) => value.trim()).filter((value) => value.length > 0);
|
|
1427
|
+
if (parsed.length === 0) {
|
|
1428
|
+
throw new DeployDomainsError("Pass at least one --value. Check the record and run the command again.", "RECORD_INVALID");
|
|
1429
|
+
}
|
|
1430
|
+
return parsed;
|
|
1431
|
+
}
|
|
1432
|
+
function parseRecordId(raw) {
|
|
1433
|
+
const id = raw.trim();
|
|
1434
|
+
if (!RECORD_ID_PATTERN.test(id)) {
|
|
1435
|
+
throw new DeployDomainsError("That record id is not valid. Run 'hq deploy domains records list <domain>' and pass --record with an id from the list.", "RECORD_INVALID");
|
|
1436
|
+
}
|
|
1437
|
+
return id;
|
|
1438
|
+
}
|
|
1439
|
+
function recordsRoute(orgSlug, domain, recordId) {
|
|
1440
|
+
const base = `/api/orgs/${encodeURIComponent(orgSlug)}/purchased-domains/${encodeURIComponent(domain)}/records`;
|
|
1441
|
+
return recordId ? `${base}/${encodeURIComponent(recordId)}` : base;
|
|
1442
|
+
}
|
|
1443
|
+
async function recordsError(response, orgSlug, domain) {
|
|
1444
|
+
const body = await readBody(response.clone());
|
|
1445
|
+
const code = apiCode(body, response.status);
|
|
1446
|
+
const serverMessage = apiField(body, "message");
|
|
1447
|
+
if (code === "RECORD_LOCKED") {
|
|
1448
|
+
return new DeployDomainsError("That record is locked by HQ because it serves the domain or validates the certificate. It cannot be changed. Add a different name, or edit a record you created.", code, response.status);
|
|
1449
|
+
}
|
|
1450
|
+
if (code === "APEX_CNAME") {
|
|
1451
|
+
return new DeployDomainsError("A CNAME cannot be added at the domain itself. Use an A or AAAA record at the apex, or add the CNAME on a name under the domain.", code, response.status);
|
|
1452
|
+
}
|
|
1453
|
+
if (code === "RECORD_EXISTS") {
|
|
1454
|
+
return new DeployDomainsError("That record already exists. Edit it with records set, or delete it first.", code, response.status);
|
|
1455
|
+
}
|
|
1456
|
+
if (code === "RECORD_INVALID") {
|
|
1457
|
+
const detail = typeof serverMessage === "string" && serverMessage.trim()
|
|
1458
|
+
? displayText(serverMessage.trim())
|
|
1459
|
+
: "That record is not valid.";
|
|
1460
|
+
const message = /try again|run the command/i.test(detail)
|
|
1461
|
+
? detail
|
|
1462
|
+
: `${detail} Check the type, name, and value, then run the command again.`;
|
|
1463
|
+
return new DeployDomainsError(message, code, response.status);
|
|
1464
|
+
}
|
|
1465
|
+
if (response.status === 404) {
|
|
1466
|
+
return new DeployDomainsError(`HQ could not find ${displayText(domain)} or that record in '${orgSlug}'. Run 'hq deploy domains owned' to see purchased domains, or 'hq deploy domains records list ${displayText(domain)}' to see records.`, code === "HTTP_404" ? "DOMAIN_NOT_OWNED" : code, response.status);
|
|
1467
|
+
}
|
|
1468
|
+
if (response.status === 401) {
|
|
1469
|
+
return new DeployDomainsError("hq-deploy rejected your sign-in. Run 'hq login' and retry.", code, response.status);
|
|
1470
|
+
}
|
|
1471
|
+
if (response.status === 403) {
|
|
1472
|
+
return new DeployDomainsError("Only a company owner or admin can change DNS records on a purchased domain. Ask an admin to do it.", code, response.status);
|
|
1473
|
+
}
|
|
1474
|
+
return new DeployDomainsError(`The DNS record request failed (HTTP ${response.status}). Try again in a minute.`, code, response.status);
|
|
1475
|
+
}
|
|
1476
|
+
function lockMarker(record) {
|
|
1477
|
+
if (record.locked !== true)
|
|
1478
|
+
return "";
|
|
1479
|
+
if (record.lockedReason === "serving" || record.lockedReason === "validation") {
|
|
1480
|
+
return ` [locked: ${record.lockedReason}]`;
|
|
1481
|
+
}
|
|
1482
|
+
return " [locked]";
|
|
1483
|
+
}
|
|
1484
|
+
function recordValuesText(record) {
|
|
1485
|
+
if (!Array.isArray(record.values))
|
|
1486
|
+
return "";
|
|
1487
|
+
return record.values
|
|
1488
|
+
.filter((value) => typeof value === "string")
|
|
1489
|
+
.map((value) => displayText(value))
|
|
1490
|
+
.join(", ");
|
|
1491
|
+
}
|
|
1492
|
+
async function recordsListAction(domainArg, options) {
|
|
1493
|
+
const domain = normalizeDomain(domainArg);
|
|
1494
|
+
const token = await ensureCognitoToken();
|
|
1495
|
+
const orgSlug = purchaseSlug(options.company);
|
|
1496
|
+
const response = await request(token, orgSlug, recordsRoute(orgSlug, domain), "GET");
|
|
1497
|
+
if (!response.ok)
|
|
1498
|
+
throw await recordsError(response, orgSlug, domain);
|
|
1499
|
+
const body = await readBody(response);
|
|
1500
|
+
const records = requireObjectList(body.records, `HQ did not return the DNS records for ${displayText(domain)}. Run 'hq deploy domains records list ${displayText(domain)}' again in a minute.`);
|
|
1501
|
+
if (options.json) {
|
|
1502
|
+
console.log(safeJson(body));
|
|
1503
|
+
return;
|
|
1504
|
+
}
|
|
1505
|
+
const nameservers = Array.isArray(body.nameservers)
|
|
1506
|
+
? body.nameservers.filter((item) => typeof item === "string")
|
|
1507
|
+
: [];
|
|
1508
|
+
if (nameservers.length > 0) {
|
|
1509
|
+
console.log("Nameservers:");
|
|
1510
|
+
for (const nameserver of nameservers)
|
|
1511
|
+
console.log(` ${displayText(nameserver)}`);
|
|
1512
|
+
}
|
|
1513
|
+
if (records.length === 0) {
|
|
1514
|
+
console.log("No DNS records on this domain.");
|
|
1515
|
+
return;
|
|
1516
|
+
}
|
|
1517
|
+
for (const record of records) {
|
|
1518
|
+
const id = typeof record.id === "string" && record.id !== "" ? `${displayText(record.id)} ` : "";
|
|
1519
|
+
console.log(` ${id}${displayText(record.type)} ${displayText(record.name)} -> ${recordValuesText(record)}${lockMarker(record)}`);
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
async function recordsAddAction(domainArg, options) {
|
|
1523
|
+
const domain = normalizeDomain(domainArg);
|
|
1524
|
+
if (!options.type || !options.name) {
|
|
1525
|
+
throw new DeployDomainsError("Pass --type and --name. Example: hq deploy domains records add example.com --type TXT --name _verify --value hello", "RECORD_INVALID");
|
|
1526
|
+
}
|
|
1527
|
+
const type = parseRecordType(options.type);
|
|
1528
|
+
const name = parseRecordName(options.name);
|
|
1529
|
+
const values = parseRecordValues(options.value, true);
|
|
1530
|
+
const ttl = parseTtl(options.ttl);
|
|
1531
|
+
const token = await ensureCognitoToken();
|
|
1532
|
+
const orgSlug = purchaseSlug(options.company);
|
|
1533
|
+
const response = await request(token, orgSlug, recordsRoute(orgSlug, domain), "POST", {
|
|
1534
|
+
name,
|
|
1535
|
+
type,
|
|
1536
|
+
values,
|
|
1537
|
+
...(ttl === undefined ? {} : { ttl }),
|
|
1538
|
+
});
|
|
1539
|
+
if (!response.ok)
|
|
1540
|
+
throw await recordsError(response, orgSlug, domain);
|
|
1541
|
+
const body = await readBody(response);
|
|
1542
|
+
if (options.json) {
|
|
1543
|
+
console.log(safeJson(body));
|
|
1544
|
+
return;
|
|
1545
|
+
}
|
|
1546
|
+
console.log(`Added ${type} ${displayText(name)} to ${displayText(domain)}.`);
|
|
1547
|
+
}
|
|
1548
|
+
async function resolveRecordId(token, orgSlug, domain, options) {
|
|
1549
|
+
if (options.record && (options.name || options.type)) {
|
|
1550
|
+
throw new DeployDomainsError("Pass either --record or --name with --type, not both. Nothing was changed.", "RECORD_INVALID");
|
|
1551
|
+
}
|
|
1552
|
+
if (options.record)
|
|
1553
|
+
return parseRecordId(options.record);
|
|
1554
|
+
if (!options.name || !options.type) {
|
|
1555
|
+
throw new DeployDomainsError("Pass --record <id>, or both --name and --type. Run 'hq deploy domains records list <domain>' to see ids.", "RECORD_INVALID");
|
|
1556
|
+
}
|
|
1557
|
+
const type = parseRecordType(options.type);
|
|
1558
|
+
const name = parseRecordName(options.name);
|
|
1559
|
+
const response = await request(token, orgSlug, recordsRoute(orgSlug, domain), "GET");
|
|
1560
|
+
if (!response.ok)
|
|
1561
|
+
throw await recordsError(response, orgSlug, domain);
|
|
1562
|
+
const body = await readBody(response);
|
|
1563
|
+
const records = Array.isArray(body.records)
|
|
1564
|
+
? body.records.filter((item) => typeof item === "object" && item !== null)
|
|
1565
|
+
: [];
|
|
1566
|
+
const matches = records.filter((record) => String(record.type ?? "").toUpperCase() === type
|
|
1567
|
+
&& String(record.name ?? "").toLowerCase() === name.toLowerCase());
|
|
1568
|
+
if (matches.length === 0) {
|
|
1569
|
+
throw new DeployDomainsError(`No ${type} record named ${displayText(name)} on ${displayText(domain)}. Run 'hq deploy domains records list ${displayText(domain)}' to see records.`, "RECORD_NOT_FOUND");
|
|
1570
|
+
}
|
|
1571
|
+
if (matches.length > 1) {
|
|
1572
|
+
throw new DeployDomainsError(`More than one ${type} record is named ${displayText(name)}. Pass --record <id> from 'hq deploy domains records list ${displayText(domain)}'.`, "RECORD_AMBIGUOUS");
|
|
1573
|
+
}
|
|
1574
|
+
const id = matches[0]?.id;
|
|
1575
|
+
if (typeof id !== "string" || !RECORD_ID_PATTERN.test(id)) {
|
|
1576
|
+
throw new DeployDomainsError("HQ could not read that record's id. Run 'hq deploy domains records list' and pass --record <id>.", "RECORD_INVALID");
|
|
1577
|
+
}
|
|
1578
|
+
return id;
|
|
1579
|
+
}
|
|
1580
|
+
async function recordsSetAction(domainArg, options) {
|
|
1581
|
+
const domain = normalizeDomain(domainArg);
|
|
1582
|
+
const ttl = parseTtl(options.ttl);
|
|
1583
|
+
const values = parseRecordValues(options.value, false);
|
|
1584
|
+
if (ttl === undefined && values === undefined) {
|
|
1585
|
+
throw new DeployDomainsError("Pass --ttl or at least one --value to change the record. Nothing was changed.", "RECORD_INVALID");
|
|
1586
|
+
}
|
|
1587
|
+
const token = await ensureCognitoToken();
|
|
1588
|
+
const orgSlug = purchaseSlug(options.company);
|
|
1589
|
+
const recordId = await resolveRecordId(token, orgSlug, domain, options);
|
|
1590
|
+
const response = await request(token, orgSlug, recordsRoute(orgSlug, domain, recordId), "PATCH", {
|
|
1591
|
+
...(ttl === undefined ? {} : { ttl }),
|
|
1592
|
+
...(values === undefined ? {} : { values }),
|
|
1593
|
+
});
|
|
1594
|
+
if (!response.ok)
|
|
1595
|
+
throw await recordsError(response, orgSlug, domain);
|
|
1596
|
+
const body = await readBody(response);
|
|
1597
|
+
if (options.json) {
|
|
1598
|
+
console.log(safeJson(body));
|
|
1599
|
+
return;
|
|
1600
|
+
}
|
|
1601
|
+
console.log(`Updated record ${displayText(recordId)} on ${displayText(domain)}.`);
|
|
1602
|
+
}
|
|
1603
|
+
async function recordsDeleteAction(domainArg, options) {
|
|
1604
|
+
const domain = normalizeDomain(domainArg);
|
|
1605
|
+
const token = await ensureCognitoToken();
|
|
1606
|
+
const orgSlug = purchaseSlug(options.company);
|
|
1607
|
+
const recordId = await resolveRecordId(token, orgSlug, domain, options);
|
|
1608
|
+
const response = await request(token, orgSlug, recordsRoute(orgSlug, domain, recordId), "DELETE");
|
|
1609
|
+
if (!response.ok)
|
|
1610
|
+
throw await recordsError(response, orgSlug, domain);
|
|
1611
|
+
if (options.json) {
|
|
1612
|
+
const body = await readBody(response);
|
|
1613
|
+
console.log(safeJson({ ok: true, recordId, ...body }));
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
console.log(`Deleted record ${displayText(recordId)} on ${displayText(domain)}.`);
|
|
971
1617
|
}
|
|
972
1618
|
// ---------------------------------------------------------------------------
|
|
973
1619
|
// Registration
|
|
@@ -981,9 +1627,9 @@ function targetOptions(command) {
|
|
|
981
1627
|
export function registerDeployDomainsCommand(deploy) {
|
|
982
1628
|
const domains = deploy
|
|
983
1629
|
.command("domains")
|
|
984
|
-
.description("Manage custom domains on your deployed apps, and
|
|
985
|
-
.addHelpText("after", "\nadd, remove and buy ask for approval in an interactive terminal.
|
|
986
|
-
"or a Windows shell that does not report a terminal, such as Git Bash under mintty), pass --yes.");
|
|
1630
|
+
.description("Manage custom domains on your deployed apps, buy domains, and edit their DNS")
|
|
1631
|
+
.addHelpText("after", "\nadd, remove and buy ask for approval in an interactive terminal. buy accepts y or --yes. " +
|
|
1632
|
+
"Without a terminal (a script, CI, or a Windows shell that does not report a terminal, such as Git Bash under mintty), pass --yes.");
|
|
987
1633
|
targetOptions(domains.command("list").description("List the custom domains on an app"))
|
|
988
1634
|
.action((options) => run(options.json, () => listAction(options)));
|
|
989
1635
|
targetOptions(domains
|
|
@@ -1019,21 +1665,59 @@ export function registerDeployDomainsCommand(deploy) {
|
|
|
1019
1665
|
.command("remove")
|
|
1020
1666
|
.description("Release the company deploy domain")
|
|
1021
1667
|
.option("--yes", "remove without a prompt")).action((options) => run(options.json, () => baseRemoveAction(options)));
|
|
1022
|
-
|
|
1023
|
-
.command("search <query>")
|
|
1024
|
-
.description("Check whether a domain can be bought, and its price")
|
|
1668
|
+
const purchaseOptions = (command) => command
|
|
1025
1669
|
.option("--company <slug>", "use this company instead of the active company")
|
|
1026
|
-
.option("--json", "print machine-readable JSON")
|
|
1027
|
-
|
|
1670
|
+
.option("--json", "print machine-readable JSON");
|
|
1671
|
+
purchaseOptions(domains
|
|
1672
|
+
.command("search <query>")
|
|
1673
|
+
.description("Check whether a domain can be bought, with availability and registrar prices")).action((query, options) => run(options.json, () => searchAction(query, options)));
|
|
1028
1674
|
domains
|
|
1029
1675
|
.command("buy <domain>")
|
|
1030
|
-
.description("Buy a domain for the company (owner or admin)")
|
|
1031
|
-
.requiredOption("--
|
|
1676
|
+
.description("Buy a domain for the company and say what it is for (owner or admin)")
|
|
1677
|
+
.requiredOption("--use <use>", "what the domain is for: app:<app id>, base, or hold")
|
|
1678
|
+
.requiredOption("--price <usd>", "first-year registrar price shown by 'hq deploy domains search'")
|
|
1032
1679
|
.requiredOption("--registrant-file <path>", "JSON file with the registrant contact")
|
|
1033
1680
|
.option("--idempotency-key <key>", "retry an earlier order that did not finish")
|
|
1034
1681
|
.option("--company <slug>", "use this company instead of the active company")
|
|
1035
|
-
.option("--yes", "buy without a prompt")
|
|
1682
|
+
.option("--yes", "buy without a prompt, after you have read the transfer lock and final-purchase notice")
|
|
1036
1683
|
.option("--json", "print machine-readable JSON")
|
|
1037
1684
|
.action((domain, options) => run(options.json, () => buyAction(domain, options)));
|
|
1685
|
+
purchaseOptions(domains.command("owned").description("List domains this company bought")).action((options) => run(options.json, () => ownedAction(options)));
|
|
1686
|
+
purchaseOptions(domains
|
|
1687
|
+
.command("show <domain>")
|
|
1688
|
+
.description("Show one purchased domain, including nameservers and events")).action((domain, options) => run(options.json, () => showAction(domain, options)));
|
|
1689
|
+
const renew = domains
|
|
1690
|
+
.command("renew")
|
|
1691
|
+
.description("Turn automatic renewal off or back on for a purchased domain");
|
|
1692
|
+
purchaseOptions(renew.command("cancel <domain>").description("Stop automatic renewal for a purchased domain").option("--yes", "Confirm the billing setting change")).action((domain, options) => run(options.json, () => renewAction(domain, false, options)));
|
|
1693
|
+
purchaseOptions(renew.command("resume <domain>").description("Turn automatic renewal back on for a purchased domain").option("--yes", "Confirm the billing setting change")).action((domain, options) => run(options.json, () => renewAction(domain, true, options)));
|
|
1694
|
+
const records = domains
|
|
1695
|
+
.command("records")
|
|
1696
|
+
.description("Manage DNS records on a domain this company bought");
|
|
1697
|
+
const recordOptions = (command) => command
|
|
1698
|
+
.option("--company <slug>", "use this company instead of the active company")
|
|
1699
|
+
.option("--json", "print machine-readable JSON");
|
|
1700
|
+
recordOptions(records.command("list <domain>").description("List DNS records, marking the ones HQ has locked")).action((domain, options) => run(options.json, () => recordsListAction(domain, options)));
|
|
1701
|
+
recordOptions(records
|
|
1702
|
+
.command("add <domain>")
|
|
1703
|
+
.description("Add a DNS record")
|
|
1704
|
+
.requiredOption("--type <type>", "A, AAAA, CNAME, MX, TXT, SRV, or CAA")
|
|
1705
|
+
.requiredOption("--name <name>", "record name, such as www or the full domain")
|
|
1706
|
+
.option("--value <value>", "record value (repeat for more than one)", collectRepeated, [])
|
|
1707
|
+
.option("--ttl <seconds>", "time to live in seconds")).action((domain, options) => run(options.json, () => recordsAddAction(domain, options)));
|
|
1708
|
+
recordOptions(records
|
|
1709
|
+
.command("set <domain>")
|
|
1710
|
+
.description("Change a DNS record by id, or by name and type")
|
|
1711
|
+
.option("--record <id>", "record id from 'hq deploy domains records list'")
|
|
1712
|
+
.option("--name <name>", "record name, together with --type")
|
|
1713
|
+
.option("--type <type>", "record type, together with --name")
|
|
1714
|
+
.option("--value <value>", "replacement value (repeat for more than one)", collectRepeated, [])
|
|
1715
|
+
.option("--ttl <seconds>", "replacement time to live in seconds")).action((domain, options) => run(options.json, () => recordsSetAction(domain, options)));
|
|
1716
|
+
recordOptions(records
|
|
1717
|
+
.command("delete <domain>")
|
|
1718
|
+
.description("Delete a DNS record by id, or by name and type")
|
|
1719
|
+
.option("--record <id>", "record id from 'hq deploy domains records list'")
|
|
1720
|
+
.option("--name <name>", "record name, together with --type")
|
|
1721
|
+
.option("--type <type>", "record type, together with --name")).action((domain, options) => run(options.json, () => recordsDeleteAction(domain, options)));
|
|
1038
1722
|
}
|
|
1039
1723
|
//# sourceMappingURL=deploy-domains.js.map
|