@aoxborrow/registrar-client 0.5.0 → 0.7.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/index.js CHANGED
@@ -55,11 +55,61 @@ var TimeoutError = class extends RetryableRegistrarError {
55
55
  var ConnectionError = class extends RetryableRegistrarError {
56
56
  status = 503;
57
57
  // Service Unavailable
58
- constructor(message) {
58
+ // true when the failure is known to have happened before the request left:
59
+ // DNS, connection refused, TLS handshake, or a proxy that never opened its
60
+ // tunnel. The registrar cannot have acted on it, so it is safe to re-send
61
+ // even for a write.
62
+ notSent;
63
+ constructor(message, details = {}) {
59
64
  super(message);
60
65
  this.name = "ConnectionError";
66
+ this.notSent = details.notSent ?? false;
67
+ }
68
+ };
69
+ var OutcomeUnknownError = class extends RegistrarError {
70
+ status = 504;
71
+ // Gateway Timeout
72
+ feature;
73
+ cause;
74
+ constructor(message, details = {}) {
75
+ super(message);
76
+ this.name = "OutcomeUnknownError";
77
+ this.feature = details.feature;
78
+ this.cause = details.cause;
61
79
  }
62
80
  };
81
+ var NOT_SENT = /* @__PURE__ */ Symbol.for("registrar-client.notSent");
82
+ function markNotSent(error) {
83
+ if (typeof error === "object" && error !== null) {
84
+ Object.defineProperty(error, NOT_SENT, { value: true, enumerable: false });
85
+ }
86
+ return error;
87
+ }
88
+ var PRE_SEND_CODES = /* @__PURE__ */ new Set([
89
+ "ENOTFOUND",
90
+ "EAI_AGAIN",
91
+ "ECONNREFUSED",
92
+ "EHOSTUNREACH",
93
+ "ENETUNREACH",
94
+ "EADDRNOTAVAIL",
95
+ "UND_ERR_CONNECT_TIMEOUT",
96
+ "CERT_HAS_EXPIRED",
97
+ "CERT_NOT_YET_VALID",
98
+ "DEPTH_ZERO_SELF_SIGNED_CERT",
99
+ "SELF_SIGNED_CERT_IN_CHAIN",
100
+ "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
101
+ "UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
102
+ "ERR_TLS_CERT_ALTNAME_INVALID"
103
+ ]);
104
+ function wasNotSent(error) {
105
+ for (let e = error, depth = 0; e && typeof e === "object" && depth < 5; depth++) {
106
+ if (e[NOT_SENT] === true) return true;
107
+ const code = e.code;
108
+ if (typeof code === "string" && PRE_SEND_CODES.has(code)) return true;
109
+ e = e.cause;
110
+ }
111
+ return false;
112
+ }
63
113
  var AuthenticationError = class extends RegistrarError {
64
114
  status = 401;
65
115
  // Unauthorized
@@ -226,6 +276,9 @@ function createDomain(data = {}) {
226
276
  deleted: data.deleted ?? false
227
277
  };
228
278
  }
279
+ function asRead(opts) {
280
+ return { ...opts, call: { ...opts?.call, intent: "read" } };
281
+ }
229
282
 
230
283
  // src/client.ts
231
284
  var RegistrarClient = class {
@@ -376,6 +429,7 @@ var DEFAULT_OPTIONS = {
376
429
  var USER_AGENT = "@aoxborrow/registrar-client";
377
430
 
378
431
  // src/http.ts
432
+ var REST_SAFE_METHODS = ["GET", "HEAD"];
379
433
  var HttpClient = class {
380
434
  constructor(config) {
381
435
  this.config = config;
@@ -399,7 +453,20 @@ var HttpClient = class {
399
453
  async requestText(req) {
400
454
  return this.withRetries(req, () => this.send(req));
401
455
  }
402
- // shared retry-with-backoff wrapper around a single-attempt operation
456
+ // Shared retry-with-backoff wrapper around a single-attempt operation.
457
+ //
458
+ // What may be re-sent depends on where the failure happened and on whether
459
+ // the call reads or writes:
460
+ //
461
+ // failure read write
462
+ // never sent (DNS, refused, TLS, proxy stage) retry retry
463
+ // 429 retry retry
464
+ // unknown (timeout, dropped mid-response, 5xx) retry OutcomeUnknownError
465
+ // any other response fail fail
466
+ //
467
+ // A write whose outcome is unknown may have been applied, so it is never
468
+ // re-sent: a second renewal charges twice, a second record-create leaves a
469
+ // duplicate. The caller re-reads the domain instead.
403
470
  async withRetries(req, attemptFn) {
404
471
  const retries = req.retries ?? this.config.options.retries;
405
472
  const backoff = req.backoff ?? this.config.options.backoff;
@@ -409,6 +476,14 @@ var HttpClient = class {
409
476
  return await attemptFn();
410
477
  } catch (error) {
411
478
  lastError = toRegistrarError(error);
479
+ if (this.intentOf(req) === "write" && isOutcomeUnknown(lastError)) {
480
+ const unknown = new OutcomeUnknownError(
481
+ `The registrar did not confirm this change, so it may or may not have been applied. Check the domain before trying again. (${lastError.message})`,
482
+ { feature: req.call?.feature, cause: lastError }
483
+ );
484
+ if (req.call) req.call.outcomeUnknown = unknown;
485
+ throw unknown;
486
+ }
412
487
  if (attempt >= retries || !lastError.shouldRetry()) {
413
488
  throw lastError;
414
489
  }
@@ -418,6 +493,14 @@ var HttpClient = class {
418
493
  }
419
494
  throw lastError ?? new RegistrarError("Request failed");
420
495
  }
496
+ // Whether a request only reads. A method the API reserves for reads wins;
497
+ // otherwise it is whatever the feature being served is, and a request that
498
+ // says nothing is assumed to write.
499
+ intentOf(req) {
500
+ const method = (req.method ?? "GET").toUpperCase();
501
+ if (this.config.safeMethods?.includes(method)) return "read";
502
+ return req.call?.intent ?? "write";
503
+ }
421
504
  // build the full URL for a request, applying query parameters
422
505
  buildUrl(req) {
423
506
  let base;
@@ -460,7 +543,8 @@ var HttpClient = class {
460
543
  try {
461
544
  let response;
462
545
  try {
463
- response = await fetch(url, {
546
+ const doFetch = req.fetch ?? this.config.options.fetch ?? fetch;
547
+ response = await doFetch(url, {
464
548
  method: req.method ?? "GET",
465
549
  headers,
466
550
  body: req.body !== void 0 ? JSON.stringify(req.body) : void 0,
@@ -471,7 +555,9 @@ var HttpClient = class {
471
555
  if (externalSignal?.aborted) throw new AbortError("Request was aborted");
472
556
  throw new TimeoutError(`Request to '${url}' timed out after ${timeout}ms`);
473
557
  }
474
- throw new ConnectionError(`Failed to reach '${url}': ${errorMessage(error)}`);
558
+ throw new ConnectionError(`Failed to reach '${url}': ${errorMessage(error)}`, {
559
+ notSent: wasNotSent(error)
560
+ });
475
561
  }
476
562
  if (!response.ok) {
477
563
  throw await this.toStatusError(response, url);
@@ -479,7 +565,17 @@ var HttpClient = class {
479
565
  if (response.status === 204) {
480
566
  return "";
481
567
  }
482
- return await response.text();
568
+ try {
569
+ return await response.text();
570
+ } catch (error) {
571
+ if (isAbortError(error)) {
572
+ if (externalSignal?.aborted) throw new AbortError("Request was aborted");
573
+ throw new TimeoutError(`Response from '${url}' timed out after ${timeout}ms`);
574
+ }
575
+ throw new ConnectionError(
576
+ `Connection to '${url}' was lost while reading the response: ${errorMessage(error)}`
577
+ );
578
+ }
483
579
  } finally {
484
580
  clearTimeout(timeoutId);
485
581
  externalSignal?.removeEventListener("abort", onAbort);
@@ -515,6 +611,10 @@ var HttpClient = class {
515
611
  }
516
612
  }
517
613
  };
614
+ function isOutcomeUnknown(error) {
615
+ if (error instanceof TimeoutError || error instanceof InvalidResponseError) return true;
616
+ return error instanceof ConnectionError && !error.notSent;
617
+ }
518
618
  function isAbortError(error) {
519
619
  return typeof error === "object" && error !== null && "name" in error && error.name === "AbortError";
520
620
  }
@@ -611,6 +711,33 @@ var CORE_FEATURE_SET = new Set(CORE_FEATURES);
611
711
  function isCoreFeature(feature) {
612
712
  return CORE_FEATURE_SET.has(feature);
613
713
  }
714
+ var FEATURE_CALLS = {
715
+ testConnection: { intent: "read", optsIndex: 0 },
716
+ listDomains: { intent: "read", optsIndex: 0 },
717
+ getDomain: { intent: "read", optsIndex: 1 },
718
+ checkAvailability: { intent: "read", optsIndex: 1 },
719
+ getPricing: { intent: "read", optsIndex: 1 },
720
+ registerDomain: { intent: "write", optsIndex: 2 },
721
+ renewDomain: { intent: "write", optsIndex: 2 },
722
+ setAutoRenew: { intent: "write", optsIndex: 2 },
723
+ transferIn: { intent: "write", optsIndex: 2 },
724
+ updateNameservers: { intent: "write", optsIndex: 2 },
725
+ getNameservers: { intent: "read", optsIndex: 1 },
726
+ lockDomain: { intent: "write", optsIndex: 1 },
727
+ unlockDomain: { intent: "write", optsIndex: 1 },
728
+ setPrivacy: { intent: "write", optsIndex: 2 },
729
+ getContacts: { intent: "read", optsIndex: 1 },
730
+ updateContacts: { intent: "write", optsIndex: 2 },
731
+ getDnsRecords: { intent: "read", optsIndex: 1 },
732
+ setDnsRecords: { intent: "write", optsIndex: 2 },
733
+ getAuthCode: { intent: "write", optsIndex: 1 },
734
+ getDnssec: { intent: "read", optsIndex: 1 },
735
+ disableDnssec: { intent: "write", optsIndex: 1 },
736
+ getEmailForwarding: { intent: "read", optsIndex: 1 },
737
+ setEmailForwarding: { intent: "write", optsIndex: 2 },
738
+ getDomainForwarding: { intent: "read", optsIndex: 1 },
739
+ setDomainForwarding: { intent: "write", optsIndex: 2 }
740
+ };
614
741
 
615
742
  // src/registrar.ts
616
743
  function selectBaseUrl(registrar, environment, urls) {
@@ -636,6 +763,9 @@ var BaseRegistrar = class {
636
763
  static get features() {
637
764
  return [...CORE_FEATURES, ...this.extendedFeatures];
638
765
  }
766
+ // HTTP methods this provider's API reserves for reads; see
767
+ // `HttpClientConfig.safeMethods`. REST providers set `REST_SAFE_METHODS`.
768
+ static safeMethods = [];
639
769
  // instance mirror of the static `features`
640
770
  get features() {
641
771
  return this.constructor.features;
@@ -654,7 +784,39 @@ var BaseRegistrar = class {
654
784
  this.credentials = credentials;
655
785
  this.environment = environment;
656
786
  this.options = { ...DEFAULT_OPTIONS, ...clientOptions };
657
- this.http = new HttpClient({ ...httpConfig, options: this.options });
787
+ this.http = new HttpClient({
788
+ safeMethods: this.constructor.safeMethods,
789
+ ...httpConfig,
790
+ options: this.options
791
+ });
792
+ this.threadFeatureCalls();
793
+ }
794
+ // Wrap each feature method so its `RequestOptions` carry a `FeatureCall`:
795
+ // the read/write intent the HTTP layer needs to decide what may be re-sent,
796
+ // and a slot where it reports a write whose outcome is unknown. Providers
797
+ // already forward their options into every request, so nothing else changes.
798
+ //
799
+ // A feature calling another feature (a write re-reading the domain, say)
800
+ // goes through that feature's own wrapper, so the inner read is still a read.
801
+ // Providers fold most failures into an `OperationResult`; the unknown outcome
802
+ // is copied onto that result so it is not lost along with the error.
803
+ threadFeatureCalls() {
804
+ const self = this;
805
+ for (const [feature, { intent, optsIndex }] of Object.entries(FEATURE_CALLS)) {
806
+ const original = self[feature];
807
+ if (typeof original !== "function") continue;
808
+ self[feature] = (...args) => {
809
+ const call = { intent, feature };
810
+ const next = [...args];
811
+ while (next.length <= optsIndex) next.push(void 0);
812
+ next[optsIndex] = { ...next[optsIndex], call };
813
+ const result = original.apply(this, next);
814
+ if (!(result instanceof Promise)) return result;
815
+ return result.then(
816
+ (value) => call.outcomeUnknown && value !== null && typeof value === "object" && value.success === false ? { ...value, outcome: "unknown" } : value
817
+ );
818
+ };
819
+ }
658
820
  }
659
821
  testConnection(_opts) {
660
822
  return Promise.reject(
@@ -745,6 +907,8 @@ var BaseRegistrar = class {
745
907
  // src/registrars/cloudflare.ts
746
908
  var CloudflareRegistrar = class _CloudflareRegistrar extends BaseRegistrar {
747
909
  name = "cloudflare";
910
+ // a REST API: GET and HEAD never change state
911
+ static safeMethods = REST_SAFE_METHODS;
748
912
  static displayName = "Cloudflare";
749
913
  static website = "cloudflare.com";
750
914
  static helpText = 'Create an API token in your Cloudflare dashboard under My Profile > API Tokens. The token needs "Account.Registrar" read/write permissions. Find your Account ID in the URL or on the Overview page of any zone.';
@@ -1026,8 +1190,8 @@ var CloudflareRegistrar = class _CloudflareRegistrar extends BaseRegistrar {
1026
1190
  available: c?.registrable ?? false
1027
1191
  };
1028
1192
  if (c?.tier) availability.premium = c.tier === "premium";
1029
- const price = toPrice(c?.pricing?.registration_cost);
1030
- if (price != null) availability.price = price;
1193
+ const price2 = toPrice(c?.pricing?.registration_cost);
1194
+ if (price2 != null) availability.price = price2;
1031
1195
  if (c?.pricing?.currency) availability.currency = c.pricing.currency;
1032
1196
  return availability;
1033
1197
  });
@@ -1602,6 +1766,8 @@ var SEARCH_BATCH_SIZE = 100;
1602
1766
  var SUPPORTED_DNS_TYPES = /* @__PURE__ */ new Set(["A", "AAAA", "CNAME", "MX", "TXT", "NS", "SRV", "CAA"]);
1603
1767
  var DynadotRegistrar = class extends BaseRegistrar {
1604
1768
  name = "dynadot";
1769
+ // a REST API: GET and HEAD never change state
1770
+ static safeMethods = REST_SAFE_METHODS;
1605
1771
  static displayName = "Dynadot";
1606
1772
  static website = "dynadot.com";
1607
1773
  static helpText = "Under Tools > API in your Dynadot account, generate an API Key and API Secret. The account must be unlocked with API access enabled. The key identifies you; the secret signs requests (X-Signature) and is never sent directly.";
@@ -1789,14 +1955,14 @@ ${bodyStr}`;
1789
1955
  opts
1790
1956
  );
1791
1957
  for (const r of data.domain_result_list ?? []) {
1792
- const price = oneYearPrice(r.price_list);
1958
+ const price2 = oneYearPrice(r.price_list);
1793
1959
  results.push({
1794
1960
  domainName: r.domain_name ?? "",
1795
1961
  available: r.available === "Yes",
1796
1962
  premium: r.premium != null ? r.premium.toLowerCase() === "yes" : void 0,
1797
- price: price?.registration,
1798
- currency: price?.currency,
1799
- period: price ? 1 : void 0
1963
+ price: price2?.registration,
1964
+ currency: price2?.currency,
1965
+ period: price2 ? 1 : void 0
1800
1966
  });
1801
1967
  }
1802
1968
  }
@@ -1833,13 +1999,13 @@ ${bodyStr}`;
1833
1999
  opts
1834
2000
  );
1835
2001
  const result = (data.domain_result_list ?? [])[0];
1836
- const price = oneYearPrice(result?.price_list);
2002
+ const price2 = oneYearPrice(result?.price_list);
1837
2003
  return {
1838
2004
  tld,
1839
- currency: price?.currency ?? "USD",
1840
- registration: price?.registration,
1841
- renewal: price?.renewal,
1842
- transfer: price?.transfer
2005
+ currency: price2?.currency ?? "USD",
2006
+ registration: price2?.registration,
2007
+ renewal: price2?.renewal,
2008
+ transfer: price2?.transfer
1843
2009
  };
1844
2010
  }
1845
2011
  /**
@@ -2353,6 +2519,8 @@ var GANDI_TYPE_TO_FORWARD = {
2353
2519
  };
2354
2520
  var GandiRegistrar = class extends BaseRegistrar {
2355
2521
  name = "gandi";
2522
+ // a REST API: GET and HEAD never change state
2523
+ static safeMethods = REST_SAFE_METHODS;
2356
2524
  static displayName = "Gandi.net";
2357
2525
  static website = "gandi.net";
2358
2526
  static helpText = "Generate a Personal Access Token (PAT) in your Gandi account under Account Settings > Security. The token needs permission to manage domains. (Gandi has deprecated the legacy API Key; use a PAT.)";
@@ -2555,17 +2723,17 @@ var GandiRegistrar = class extends BaseRegistrar {
2555
2723
  const products = resp.products ?? [];
2556
2724
  const product = products.find((p) => p.process === "create") ?? products[0];
2557
2725
  const status = product?.status ?? "";
2558
- const price = product?.prices?.[0];
2726
+ const price2 = product?.prices?.[0];
2559
2727
  const result = {
2560
2728
  domainName: product?.name ?? domainName,
2561
2729
  available: status.startsWith("available"),
2562
- premium: status.includes("premium") || price?.type === "premium"
2730
+ premium: status.includes("premium") || price2?.type === "premium"
2563
2731
  };
2564
- const amount = pickPrice(price);
2732
+ const amount = pickPrice(price2);
2565
2733
  if (amount != null) result.price = amount;
2566
2734
  if (resp.currency) result.currency = resp.currency;
2567
- if (price?.duration_unit === "y" && price.min_duration != null) {
2568
- result.period = price.min_duration;
2735
+ if (price2?.duration_unit === "y" && price2.min_duration != null) {
2736
+ result.period = price2.min_duration;
2569
2737
  }
2570
2738
  results.push(result);
2571
2739
  }
@@ -3003,9 +3171,9 @@ function fromGandiContact(c) {
3003
3171
  country: c.country ?? ""
3004
3172
  };
3005
3173
  }
3006
- function pickPrice(price) {
3007
- if (!price) return void 0;
3008
- const value = price.price_after_taxes ?? price.price_before_taxes;
3174
+ function pickPrice(price2) {
3175
+ if (!price2) return void 0;
3176
+ const value = price2.price_after_taxes ?? price2.price_before_taxes;
3009
3177
  return typeof value === "number" && Number.isFinite(value) ? value : void 0;
3010
3178
  }
3011
3179
  function parseDsRecord(ds) {
@@ -3040,6 +3208,8 @@ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
3040
3208
  var PRICE_MICRO_UNITS = 1e6;
3041
3209
  var GoDaddyRegistrar = class _GoDaddyRegistrar extends BaseRegistrar {
3042
3210
  name = "godaddy";
3211
+ // a REST API: GET and HEAD never change state
3212
+ static safeMethods = REST_SAFE_METHODS;
3043
3213
  static displayName = "GoDaddy";
3044
3214
  static website = "godaddy.com";
3045
3215
  static helpText = "Create a Personal Access Token (PAT) at https://developer.godaddy.com and paste it here. The PAT is GoDaddy's modern credential: it unlocks the v3 API (discovery, registration, DNS, nameservers) and also authenticates the legacy v1 API for management operations (renew, transfer, auto-renew, lock, privacy, contacts) \u2014 one token covers everything.";
@@ -3920,6 +4090,8 @@ function dnsFieldsDiffer(a, b) {
3920
4090
  var TOKEN_URL = "https://api.namebright.com/auth/token";
3921
4091
  var NameBrightRegistrar = class extends BaseRegistrar {
3922
4092
  name = "namebright";
4093
+ // a REST API: GET and HEAD never change state
4094
+ static safeMethods = REST_SAFE_METHODS;
3923
4095
  static displayName = "NameBright";
3924
4096
  static website = "namebright.com";
3925
4097
  static helpText = 'API access is not enabled by default \u2014 you must request it from NameBright (contact support / your account manager) before the API Management page is available. Once enabled, create an API Application under my.namebright.com > My Account > API Management. The Client ID is "<accountName>:<applicationName>"; NameBright issues the Client Secret. Note the application also enforces an IP whitelist.';
@@ -3958,7 +4130,8 @@ var NameBrightRegistrar = class extends BaseRegistrar {
3958
4130
  client_id: this.credentials.clientId,
3959
4131
  client_secret: this.credentials.clientSecret
3960
4132
  },
3961
- ...opts
4133
+ // issuing a token changes nothing on the account, whatever it precedes
4134
+ ...asRead(opts)
3962
4135
  });
3963
4136
  if (!res.access_token) {
3964
4137
  throw new AuthenticationError("NameBright: token endpoint returned no access_token");
@@ -4089,13 +4262,13 @@ var NameBrightRegistrar = class extends BaseRegistrar {
4089
4262
  { path: `purchase/availability/${encodeURIComponent(domainName)}` },
4090
4263
  opts
4091
4264
  );
4092
- const price = res.Promotion?.PromotionPrice ?? res.UnitPrice;
4265
+ const price2 = res.Promotion?.PromotionPrice ?? res.UnitPrice;
4093
4266
  const result = {
4094
4267
  domainName: res.DomainName ?? domainName,
4095
4268
  available: res.Status === "AvailableForRegistration"
4096
4269
  };
4097
- if (typeof price === "number" && Number.isFinite(price) && price > 0) {
4098
- result.price = price;
4270
+ if (typeof price2 === "number" && Number.isFinite(price2) && price2 > 0) {
4271
+ result.price = price2;
4099
4272
  result.currency = "USD";
4100
4273
  }
4101
4274
  results.push(result);
@@ -4737,7 +4910,11 @@ var NamecheapRegistrar = class extends BaseRegistrar {
4737
4910
  * has no WhoisGuard allotted (e.g. TLDs that don't offer it).
4738
4911
  */
4739
4912
  async setPrivacy(domainName, enabled, opts) {
4740
- const cr = await this.command("namecheap.domains.getInfo", { DomainName: domainName }, opts);
4913
+ const cr = await this.command(
4914
+ "namecheap.domains.getInfo",
4915
+ { DomainName: domainName },
4916
+ asRead(opts)
4917
+ );
4741
4918
  const id = cr.DomainGetInfoResult?.Whoisguard?.ID;
4742
4919
  if (id == null || String(id) === "0") {
4743
4920
  throw new Error(`${this.name}: ${domainName} has no WhoisGuard subscription to toggle`);
@@ -4928,9 +5105,9 @@ function splitDomain(domain) {
4928
5105
  const dot = domain.indexOf(".");
4929
5106
  return dot === -1 ? { sld: domain, tld: "" } : { sld: domain.slice(0, dot), tld: domain.slice(dot + 1) };
4930
5107
  }
4931
- function toPrice2(price) {
4932
- if (!price?.["@_Price"]) return void 0;
4933
- const n = Number(price["@_Price"]);
5108
+ function toPrice2(price2) {
5109
+ if (!price2?.["@_Price"]) return void 0;
5110
+ const n = Number(price2["@_Price"]);
4934
5111
  return Number.isFinite(n) ? n : void 0;
4935
5112
  }
4936
5113
  function fromNcContact(c) {
@@ -4969,6 +5146,505 @@ function toNcContactParams(prefix, c) {
4969
5146
  return params;
4970
5147
  }
4971
5148
 
5149
+ // src/registrars/namecom.ts
5150
+ var NamecomHttpClient = class extends HttpClient {
5151
+ async request(req) {
5152
+ try {
5153
+ return await super.request(req);
5154
+ } catch (error) {
5155
+ if (error instanceof ParsingError) throw new ParsingError("namecom: invalid JSON response");
5156
+ if (error instanceof ConnectionError)
5157
+ throw new ConnectionError("namecom: network request failed", { notSent: error.notSent });
5158
+ throw error;
5159
+ }
5160
+ }
5161
+ async toStatusError(response, url) {
5162
+ await response.body?.cancel().catch(() => void 0);
5163
+ const error = await super.toStatusError(
5164
+ new Response(null, {
5165
+ status: response.status,
5166
+ headers: response.headers
5167
+ }),
5168
+ url
5169
+ );
5170
+ if (error instanceof RateLimitError) {
5171
+ const retryAfter = response.headers.get("retry-after");
5172
+ const seconds = retryAfter == null ? NaN : Number(retryAfter);
5173
+ const date = retryAfter == null ? NaN : Date.parse(retryAfter);
5174
+ const reset = response.headers.get("x-ratelimit-reset");
5175
+ error.retryAfter = Number.isFinite(seconds) ? Math.max(0, seconds) : Number.isFinite(date) ? Math.max(0, (date - Date.now()) / 1e3) : reset != null && Number.isFinite(Number(reset)) ? Math.max(0, Number(reset) - Date.now() / 1e3) : void 0;
5176
+ }
5177
+ return error;
5178
+ }
5179
+ };
5180
+ var NamecomRegistrar = class extends BaseRegistrar {
5181
+ name = "namecom";
5182
+ // a REST API: GET and HEAD never change state
5183
+ static safeMethods = REST_SAFE_METHODS;
5184
+ static displayName = "Name.com";
5185
+ static website = "name.com";
5186
+ static supportsSandbox = true;
5187
+ static configFields = [
5188
+ { name: "username", label: "Username", type: "text", required: true },
5189
+ { name: "apiToken", label: "API token", type: "password", required: true }
5190
+ ];
5191
+ static helpText = "Get an API token at https://www.name.com/account/settings/api. Accounts with 2FA must enable API Access. Sandbox requires your -test username and a separate sandbox token; select the sandbox environment explicitly.";
5192
+ static extendedFeatures = [Feature.GetAuthCode];
5193
+ requiresNameserversFetch = false;
5194
+ constructor(credentials, options) {
5195
+ const username = credentials.username ?? "";
5196
+ if (username.includes(":"))
5197
+ throw new ConfigurationError("namecom: username cannot contain a colon");
5198
+ const auth = btoa(
5199
+ Array.from(
5200
+ new TextEncoder().encode(`${username}:${credentials.apiToken ?? ""}`),
5201
+ (byte) => String.fromCharCode(byte)
5202
+ ).join("")
5203
+ );
5204
+ const config = {
5205
+ baseUrl: selectBaseUrl("namecom", options?.environment, {
5206
+ production: "https://api.name.com/core/v1",
5207
+ sandbox: "https://api.dev.name.com/core/v1"
5208
+ }),
5209
+ headers: { Authorization: `Basic ${auth}` }
5210
+ };
5211
+ super(credentials, config, options);
5212
+ this.http = new NamecomHttpClient({
5213
+ ...config,
5214
+ safeMethods: REST_SAFE_METHODS,
5215
+ options: this.options
5216
+ });
5217
+ }
5218
+ async testConnection(opts) {
5219
+ try {
5220
+ await this.http.request({ path: "/domains", query: { perPage: 1, page: 1 }, ...opts });
5221
+ return { success: true, message: "Connection successful" };
5222
+ } catch (error) {
5223
+ return { success: false, message: toRegistrarError(error).message };
5224
+ }
5225
+ }
5226
+ async listDomains(opts) {
5227
+ const { search, detailed: _detailed, ...requestOpts } = opts ?? {};
5228
+ const domains = await this.pages("/domains", "domains", requestOpts);
5229
+ return filterDomains(
5230
+ domains.map((d) => this.toDomain(d)),
5231
+ search
5232
+ );
5233
+ }
5234
+ async getDomain(domainName, opts) {
5235
+ return this.toDomain(await this.rawDomain(domainName, opts));
5236
+ }
5237
+ async getNameservers(domainName, opts) {
5238
+ return (await this.getDomain(domainName, opts)).nameservers;
5239
+ }
5240
+ async getAuthCode(domainName, opts) {
5241
+ const result = await this.http.request({
5242
+ path: `${domainPath(domainName)}:getAuthCode`,
5243
+ ...opts
5244
+ });
5245
+ if (!result?.authCode) throw new InvalidResponseError("namecom: missing authorization code");
5246
+ return result.authCode;
5247
+ }
5248
+ async checkAvailability(domainNames, opts) {
5249
+ const results = [];
5250
+ for (let i = 0; i < domainNames.length; i += 50) {
5251
+ const res = await this.http.request({
5252
+ method: "POST",
5253
+ path: "/domains:checkAvailability",
5254
+ body: {
5255
+ domainNames: domainNames.slice(i, i + 50).map(normalizeDomain),
5256
+ purchaseType: "registration"
5257
+ },
5258
+ ...opts
5259
+ });
5260
+ if (!Array.isArray(res?.results))
5261
+ throw new InvalidResponseError("namecom: missing availability results");
5262
+ results.push(
5263
+ ...res.results.map((r) => ({
5264
+ domainName: r.domainName,
5265
+ available: (r.purchasable ?? false) && (!r.purchaseType || r.purchaseType === "registration"),
5266
+ premium: r.premium ?? false,
5267
+ price: price(r.purchasePrice),
5268
+ currency: "USD"
5269
+ // Discovery quotes the TLD's minimum term, which isn't always one year.
5270
+ }))
5271
+ );
5272
+ }
5273
+ return results;
5274
+ }
5275
+ async getPricing(tldOrDomain, opts) {
5276
+ const name = normalizeDomain(tldOrDomain);
5277
+ if (name.startsWith(".") || !name.includes(".")) {
5278
+ const tld = name.replace(/^\./, "");
5279
+ const entries = await this.pages("/tldpricing", "pricing", opts, {
5280
+ tlds: tld,
5281
+ duration: 1
5282
+ });
5283
+ const entry = entries.find((p) => p.tld.toLowerCase() === tld && p.duration === 1);
5284
+ if (!entry) throw new NotFoundError(`namecom: no one-year pricing for .${tld}`);
5285
+ return {
5286
+ tld,
5287
+ currency: "USD",
5288
+ registration: price(entry.registrationPrice),
5289
+ renewal: price(entry.renewalPrice),
5290
+ transfer: price(entry.transferInPrice)
5291
+ };
5292
+ }
5293
+ const result = await this.domainPricing(name, 1, opts);
5294
+ return {
5295
+ tld: name.slice(name.indexOf(".") + 1),
5296
+ currency: "USD",
5297
+ registration: price(result.purchasePrice),
5298
+ renewal: price(result.renewalPrice),
5299
+ transfer: price(result.transferPrice)
5300
+ };
5301
+ }
5302
+ async registerDomain(domainName, input, opts) {
5303
+ const years = validYears(input.years ?? 1);
5304
+ if (!input.contacts.registrant)
5305
+ throw new ConfigurationError("namecom: a registrant contact is required");
5306
+ const contacts = toContacts(input.contacts);
5307
+ const availability = (await this.checkAvailability([domainName], opts)).find(
5308
+ (result) => normalizeDomain(result.domainName) === normalizeDomain(domainName)
5309
+ );
5310
+ if (!availability?.available)
5311
+ return { success: false, message: "namecom: domain is not available for registration" };
5312
+ const pricing = await this.domainPricing(domainName, years, opts);
5313
+ const purchasePrice = purchaseQuote(pricing, "purchasePrice", availability.premium);
5314
+ return this.purchase(
5315
+ "/domains",
5316
+ {
5317
+ domain: {
5318
+ domainName: normalizeDomain(domainName),
5319
+ contacts,
5320
+ nameservers: input.nameservers,
5321
+ privacyEnabled: input.privacy,
5322
+ autorenewEnabled: input.autoRenew
5323
+ },
5324
+ purchaseType: "registration",
5325
+ years,
5326
+ purchasePrice
5327
+ },
5328
+ domainName,
5329
+ "registration",
5330
+ opts
5331
+ );
5332
+ }
5333
+ async renewDomain(domainName, years = 1, opts) {
5334
+ validYears(years);
5335
+ const pricing = await this.domainPricing(domainName, years, opts);
5336
+ return this.purchase(
5337
+ `${domainPath(domainName)}:renew`,
5338
+ {
5339
+ years,
5340
+ purchasePrice: purchaseQuote(pricing, "renewalPrice")
5341
+ },
5342
+ domainName,
5343
+ "renewal",
5344
+ opts
5345
+ );
5346
+ }
5347
+ async transferIn(domainName, input, opts) {
5348
+ if (!input.authCode) throw new ConfigurationError("namecom: an authorization code is required");
5349
+ if (input.years != null || input.contacts != null || input.autoRenew != null) {
5350
+ throw new ConfigurationError(
5351
+ "namecom: transferIn does not accept years, contacts or autoRenew; the registry determines the term"
5352
+ );
5353
+ }
5354
+ const pricing = await this.domainPricing(domainName, void 0, opts);
5355
+ return this.purchase(
5356
+ "/transfers",
5357
+ {
5358
+ domainName: normalizeDomain(domainName),
5359
+ authCode: input.authCode,
5360
+ privacyEnabled: input.privacy,
5361
+ purchasePrice: purchaseQuote(pricing, "transferPrice")
5362
+ },
5363
+ domainName,
5364
+ "transfer",
5365
+ opts
5366
+ );
5367
+ }
5368
+ setAutoRenew(domainName, enabled, opts) {
5369
+ return this.patchDomain(domainName, { autorenewEnabled: enabled }, opts);
5370
+ }
5371
+ lockDomain(domainName, opts) {
5372
+ return this.patchDomain(domainName, { locked: true }, opts);
5373
+ }
5374
+ unlockDomain(domainName, opts) {
5375
+ return this.patchDomain(domainName, { locked: false }, opts);
5376
+ }
5377
+ setPrivacy(domainName, enabled, opts) {
5378
+ return this.patchDomain(domainName, { privacyEnabled: enabled }, opts);
5379
+ }
5380
+ updateNameservers(domainName, nameservers, opts) {
5381
+ return this.mutate(
5382
+ { method: "POST", path: `${domainPath(domainName)}:setNameservers`, body: { nameservers } },
5383
+ opts
5384
+ );
5385
+ }
5386
+ async getContacts(domainName, opts) {
5387
+ const result = {};
5388
+ const raw = await this.rawDomain(domainName, opts);
5389
+ for (const role of CONTACT_ROLES) {
5390
+ const c = raw.contacts?.[role];
5391
+ if (c)
5392
+ result[role] = {
5393
+ firstName: c.firstName ?? "",
5394
+ lastName: c.lastName ?? "",
5395
+ organization: c.companyName ?? void 0,
5396
+ email: c.email ?? "",
5397
+ phone: c.phone ?? "",
5398
+ fax: c.fax ?? void 0,
5399
+ address1: c.address1 ?? "",
5400
+ address2: c.address2 ?? void 0,
5401
+ city: c.city ?? "",
5402
+ state: c.state ?? void 0,
5403
+ postalCode: c.zip ?? "",
5404
+ country: c.country ?? ""
5405
+ };
5406
+ }
5407
+ return result;
5408
+ }
5409
+ updateContacts(domainName, contacts, opts) {
5410
+ return this.mutate(
5411
+ {
5412
+ method: "POST",
5413
+ path: `${domainPath(domainName)}:setContacts`,
5414
+ body: { contacts: toContacts(contacts) }
5415
+ },
5416
+ opts
5417
+ );
5418
+ }
5419
+ async getDnsRecords(domainName, opts) {
5420
+ return (await this.pages(`${domainPath(domainName)}/records`, "records", opts)).map(fromRecord);
5421
+ }
5422
+ // No bulk replace endpoint. Reconcile by id: preserve identical records, update
5423
+ // existing type/host pairs, create additions, then delete stale records. This
5424
+ // avoids duplicate-record errors and does not empty the zone before creation.
5425
+ async setDnsRecords(domainName, records, opts) {
5426
+ const desired = records.map(toRecord).filter((record, index, all) => all.findIndex((other) => sameRecord(other, record)) === index);
5427
+ try {
5428
+ const path = `${domainPath(domainName)}/records`;
5429
+ const existing = await this.pages(path, "records", opts);
5430
+ if (existing.some((r) => !Number.isInteger(r.id) || r.id <= 0))
5431
+ throw new InvalidResponseError("namecom: DNS response missing record ids");
5432
+ const remaining = [...existing];
5433
+ const pending = [];
5434
+ for (const record of desired) {
5435
+ const index = remaining.findIndex((r) => sameRecord(r, record));
5436
+ if (index >= 0) remaining.splice(index, 1);
5437
+ else if (!pending.some((r) => sameRecord(r, record))) pending.push(record);
5438
+ }
5439
+ for (const record of pending) {
5440
+ const index = remaining.findIndex((r) => recordKey(r) === recordKey(record));
5441
+ const old = index >= 0 ? remaining.splice(index, 1)[0] : void 0;
5442
+ await this.http.request({
5443
+ ...opts,
5444
+ method: old ? "PUT" : "POST",
5445
+ path: old ? `${path}/${old.id}` : path,
5446
+ body: record
5447
+ });
5448
+ }
5449
+ for (const old of remaining) {
5450
+ await this.http.request({
5451
+ ...opts,
5452
+ method: "DELETE",
5453
+ path: `${path}/${old.id}`
5454
+ });
5455
+ }
5456
+ return { success: true, message: "DNS records updated successfully" };
5457
+ } catch (error) {
5458
+ return {
5459
+ success: false,
5460
+ message: `${toRegistrarError(error).message}. DNS replacement is not atomic; read the zone before retrying.`
5461
+ };
5462
+ }
5463
+ }
5464
+ patchDomain(domainName, body, opts) {
5465
+ return this.mutate({ method: "PATCH", path: domainPath(domainName), body }, opts);
5466
+ }
5467
+ rawDomain(domainName, opts) {
5468
+ return this.http.request({ path: domainPath(domainName), ...opts });
5469
+ }
5470
+ domainPricing(domainName, years, opts) {
5471
+ return this.http.request({
5472
+ path: `${domainPath(domainName)}:getPricing`,
5473
+ query: { years },
5474
+ ...opts
5475
+ });
5476
+ }
5477
+ toDomain(d) {
5478
+ if (!d?.domainName)
5479
+ throw new InvalidResponseError("namecom: domain response missing domainName");
5480
+ return createDomain({
5481
+ domainName: normalizeDomain(d.domainName),
5482
+ registrar: this.name,
5483
+ // Core exposes locks, not a lifecycle status or a scheduled renewal date.
5484
+ status: (d.locks ?? []).join(","),
5485
+ createdDate: d.createDate,
5486
+ expirationDate: d.expireDate,
5487
+ autoRenew: d.autorenewEnabled ?? false,
5488
+ locked: d.locked ?? false,
5489
+ privacy: d.privacyEnabled ?? false,
5490
+ nameservers: d.nameservers
5491
+ });
5492
+ }
5493
+ async pages(path, key, opts, query) {
5494
+ const result = [];
5495
+ let page = 1;
5496
+ while (true) {
5497
+ const res = await this.http.request({
5498
+ path,
5499
+ query: { perPage: 1e3, page, ...query },
5500
+ ...opts
5501
+ });
5502
+ const items = res?.[key];
5503
+ if (!res || items != null && !Array.isArray(items))
5504
+ throw new InvalidResponseError("namecom: invalid list response");
5505
+ result.push(...items ?? []);
5506
+ const next = res.nextPage;
5507
+ if (next == null || next === 0) return result;
5508
+ if (!Number.isInteger(next) || next <= page)
5509
+ throw new InvalidResponseError("namecom: invalid pagination cursor");
5510
+ page = next;
5511
+ }
5512
+ }
5513
+ async mutate(req, opts) {
5514
+ try {
5515
+ await this.http.request({ ...req, ...opts });
5516
+ return { success: true, message: "Domain updated successfully" };
5517
+ } catch (error) {
5518
+ return { success: false, message: toRegistrarError(error).message };
5519
+ }
5520
+ }
5521
+ async purchase(path, body, domainName, operation, opts) {
5522
+ try {
5523
+ const res = await this.http.request({
5524
+ method: "POST",
5525
+ path,
5526
+ body,
5527
+ ...opts,
5528
+ retries: 0
5529
+ });
5530
+ const returnedName = res?.domain?.domainName ?? res?.transfer?.domainName;
5531
+ if (!Number.isInteger(res?.order) || (res.order ?? 0) <= 0 || !returnedName || normalizeDomain(returnedName) !== normalizeDomain(domainName)) {
5532
+ throw new InvalidResponseError(
5533
+ "namecom: purchase response missing order or matching domain"
5534
+ );
5535
+ }
5536
+ return {
5537
+ success: true,
5538
+ message: `Domain ${operation} request accepted (order ${res.order}); verify final domain/transfer status`
5539
+ };
5540
+ } catch (error) {
5541
+ const e = toRegistrarError(error);
5542
+ const uncertain = e.status < 0 || e.status === 408 || e.status >= 500 || e.name === "AbortError" || e instanceof ParsingError;
5543
+ return {
5544
+ success: false,
5545
+ message: e.message + (uncertain ? ". Outcome unknown; check Name.com orders and domain/transfer status before retrying." : "")
5546
+ };
5547
+ }
5548
+ }
5549
+ };
5550
+ var CONTACT_ROLES = ["registrant", "admin", "tech", "billing"];
5551
+ function toContacts(contacts) {
5552
+ const result = {};
5553
+ for (const role of CONTACT_ROLES) {
5554
+ const c = contacts[role];
5555
+ if (!c) continue;
5556
+ const required = [
5557
+ "firstName",
5558
+ "lastName",
5559
+ "email",
5560
+ "phone",
5561
+ "address1",
5562
+ "city",
5563
+ "state",
5564
+ "postalCode",
5565
+ "country"
5566
+ ];
5567
+ if (required.some((k) => !c[k]?.trim()))
5568
+ throw new ConfigurationError(`namecom: ${role} must be a complete contact, including state`);
5569
+ result[role] = {
5570
+ firstName: c.firstName,
5571
+ lastName: c.lastName,
5572
+ companyName: c.organization,
5573
+ email: c.email,
5574
+ phone: c.phone.replace(".", ""),
5575
+ fax: c.fax?.replace(".", ""),
5576
+ address1: c.address1,
5577
+ address2: c.address2,
5578
+ city: c.city,
5579
+ state: c.state,
5580
+ zip: c.postalCode,
5581
+ country: c.country.toUpperCase()
5582
+ };
5583
+ }
5584
+ if (Object.keys(result).length === 0)
5585
+ throw new ConfigurationError("namecom: at least one contact is required");
5586
+ return result;
5587
+ }
5588
+ function domainPath(domainName) {
5589
+ return `/domains/${encodeURIComponent(normalizeDomain(domainName))}`;
5590
+ }
5591
+ function validYears(years) {
5592
+ if (!Number.isInteger(years) || years < 1 || years > 10)
5593
+ throw new ConfigurationError("namecom: years must be an integer from 1 to 10");
5594
+ return years;
5595
+ }
5596
+ function price(value) {
5597
+ return typeof value === "number" && Number.isFinite(value) && value >= 0 ? value : void 0;
5598
+ }
5599
+ function purchaseQuote(pricing, field, premium = false) {
5600
+ const value = price(pricing?.[field]);
5601
+ if (value == null)
5602
+ throw new RegistrarError(
5603
+ `namecom: ${field} unavailable for the requested term; no purchase submitted`
5604
+ );
5605
+ return premium || pricing.premium ? value : void 0;
5606
+ }
5607
+ var DNS_TYPES = /* @__PURE__ */ new Set(["A", "AAAA", "ANAME", "CNAME", "MX", "NS", "SRV", "TXT"]);
5608
+ function toRecord(r) {
5609
+ const type = r.type.toUpperCase();
5610
+ if (!DNS_TYPES.has(type))
5611
+ throw new ConfigurationError(`namecom: DNS type ${type} is not supported`);
5612
+ const ttl = r.ttl ?? 300;
5613
+ if (!Number.isInteger(ttl) || ttl < 300)
5614
+ throw new ConfigurationError("namecom: DNS TTL must be an integer of at least 300 seconds");
5615
+ for (const value of [r.priority, r.weight, r.port]) {
5616
+ if (value != null && (!Number.isInteger(value) || value < 0 || value > 65535))
5617
+ throw new ConfigurationError("namecom: invalid DNS priority, weight or port");
5618
+ }
5619
+ if (!r.value.trim()) throw new ConfigurationError("namecom: DNS answer is required");
5620
+ return {
5621
+ type,
5622
+ host: !r.name || r.name === "@" ? "" : r.name,
5623
+ answer: type === "SRV" ? `${r.weight ?? 0} ${r.port ?? 0} ${r.value}` : r.value,
5624
+ ttl,
5625
+ ...["MX", "SRV"].includes(type) ? { priority: r.priority ?? 0 } : {}
5626
+ };
5627
+ }
5628
+ function fromRecord(r) {
5629
+ const type = r.type.toUpperCase();
5630
+ const record = { type, name: r.host || "@", value: r.answer, ttl: r.ttl };
5631
+ if (type === "MX" || type === "SRV") record.priority = r.priority ?? 0;
5632
+ if (type === "SRV") {
5633
+ const match = /^(\d+)\s+(\d+)\s+(.+)$/.exec(r.answer);
5634
+ if (!match) throw new InvalidResponseError("namecom: invalid SRV answer");
5635
+ record.weight = Number(match[1]);
5636
+ record.port = Number(match[2]);
5637
+ record.value = match[3];
5638
+ }
5639
+ return record;
5640
+ }
5641
+ function recordKey(r) {
5642
+ return `${r.type.toUpperCase()}\0${(r.host === "@" ? "" : r.host ?? "").toLowerCase()}`;
5643
+ }
5644
+ function sameRecord(a, b) {
5645
+ return recordKey(a) === recordKey(b) && a.answer === b.answer && (a.ttl ?? 300) === (b.ttl ?? 300) && (a.priority ?? 0) === (b.priority ?? 0);
5646
+ }
5647
+
4972
5648
  // src/registrars/namesilo.ts
4973
5649
  var NameSiloRegistrar = class extends BaseRegistrar {
4974
5650
  name = "namesilo";
@@ -5159,13 +5835,13 @@ var NameSiloRegistrar = class extends BaseRegistrar {
5159
5835
  if (!node || typeof node !== "object") {
5160
5836
  throw new NotFoundError(`NameSilo: no pricing found for TLD '${tld}'`);
5161
5837
  }
5162
- const price = node;
5838
+ const price2 = node;
5163
5839
  return {
5164
5840
  tld,
5165
5841
  currency: "USD",
5166
- registration: toPrice3(price.registration),
5167
- renewal: toPrice3(price.renew),
5168
- transfer: toPrice3(price.transfer)
5842
+ registration: toPrice3(price2.registration),
5843
+ renewal: toPrice3(price2.renew),
5844
+ transfer: toPrice3(price2.transfer)
5169
5845
  };
5170
5846
  }
5171
5847
  /**
@@ -5249,7 +5925,7 @@ var NameSiloRegistrar = class extends BaseRegistrar {
5249
5925
  */
5250
5926
  async disableDnssec(domainName, opts) {
5251
5927
  try {
5252
- const res = await this.call("dnsSecListRecords", { domain: domainName }, opts);
5928
+ const res = await this.call("dnsSecListRecords", { domain: domainName }, asRead(opts));
5253
5929
  if (!replyOk(res)) return statusResult2(res);
5254
5930
  for (const d of ensureArray(res.reply?.ds_record)) {
5255
5931
  const del = await this.call(
@@ -5322,7 +5998,7 @@ var NameSiloRegistrar = class extends BaseRegistrar {
5322
5998
  desired.set(f.alias, list);
5323
5999
  }
5324
6000
  const currentAliases = new Set(
5325
- (await this.listEmailForwards(domainName, opts)).map((r) => r.alias).filter(Boolean)
6001
+ (await this.listEmailForwards(domainName, asRead(opts))).map((r) => r.alias).filter(Boolean)
5326
6002
  );
5327
6003
  for (const alias of currentAliases) {
5328
6004
  if (!desired.has(alias)) {
@@ -5413,7 +6089,7 @@ var NameSiloRegistrar = class extends BaseRegistrar {
5413
6089
  * delegation nameservers aren't part of this list (see `updateNameservers`).
5414
6090
  */
5415
6091
  async setDnsRecords(domainName, records, opts) {
5416
- const listRes = await this.call("dnsListRecords", { domain: domainName }, opts);
6092
+ const listRes = await this.call("dnsListRecords", { domain: domainName }, asRead(opts));
5417
6093
  if (!replyOk(listRes)) return { success: false, message: replyDetail(listRes) };
5418
6094
  const existing = ensureArray(listRes.reply?.resource_record);
5419
6095
  const existingByKey = /* @__PURE__ */ new Map();
@@ -5582,8 +6258,8 @@ function extractAvailEntries(group) {
5582
6258
  const premiumRaw = o.premium ?? o["@premium"] ?? o["@_premium"];
5583
6259
  const durationRaw = o.duration ?? o["@duration"] ?? o["@_duration"];
5584
6260
  const entry = { name };
5585
- const price = toPrice3(priceRaw);
5586
- if (price != null && price > 0) entry.price = price;
6261
+ const price2 = toPrice3(priceRaw);
6262
+ if (price2 != null && price2 > 0) entry.price = price2;
5587
6263
  if (premiumRaw != null && text(premiumRaw) !== "") entry.premium = isYes(premiumRaw);
5588
6264
  const duration = toPrice3(durationRaw);
5589
6265
  if (duration != null && duration > 0) entry.duration = duration;
@@ -5827,17 +6503,17 @@ var PorkbunRegistrar = class extends BaseRegistrar {
5827
6503
  async tldPricing(tld, opts) {
5828
6504
  const res = await this.call("/pricing/get", {}, opts);
5829
6505
  if (!isOk3(res)) throw new Error(res.message ?? "API request failed");
5830
- const price = res.pricing?.[tld];
5831
- if (!price) {
6506
+ const price2 = res.pricing?.[tld];
6507
+ if (!price2) {
5832
6508
  throw new NotFoundError(`${this.name}: no pricing found for TLD .${tld}`);
5833
6509
  }
5834
6510
  return {
5835
6511
  tld,
5836
6512
  currency: "USD",
5837
6513
  // Porkbun prices are always quoted in USD
5838
- registration: toPrice4(price.registration),
5839
- renewal: toPrice4(price.renewal),
5840
- transfer: toPrice4(price.transfer)
6514
+ registration: toPrice4(price2.registration),
6515
+ renewal: toPrice4(price2.renewal),
6516
+ transfer: toPrice4(price2.transfer)
5841
6517
  };
5842
6518
  }
5843
6519
  /**
@@ -5877,14 +6553,14 @@ var PorkbunRegistrar = class extends BaseRegistrar {
5877
6553
  );
5878
6554
  if (!isOk3(res)) throw new Error(res.message ?? "API request failed");
5879
6555
  const info = res.response ?? {};
5880
- const price = toPrice4(info.price);
6556
+ const price2 = toPrice4(info.price);
5881
6557
  results.push({
5882
6558
  domainName,
5883
6559
  available: info.avail === "yes",
5884
6560
  premium: info.premium === "yes",
5885
- price,
5886
- currency: price != null ? "USD" : void 0,
5887
- period: price != null ? 1 : void 0
6561
+ price: price2,
6562
+ currency: price2 != null ? "USD" : void 0,
6563
+ period: price2 != null ? 1 : void 0
5888
6564
  });
5889
6565
  }
5890
6566
  return results;
@@ -5940,7 +6616,7 @@ var PorkbunRegistrar = class extends BaseRegistrar {
5940
6616
  * renewals aren't supported via the API.
5941
6617
  */
5942
6618
  async renewDomain(domainName, _years = 1, opts) {
5943
- const pricing = await this.tldPricing(extractTld(domainName), opts);
6619
+ const pricing = await this.tldPricing(extractTld(domainName), asRead(opts));
5944
6620
  if (pricing.renewal == null) {
5945
6621
  throw new Error(`${this.name}: could not determine the renewal price for ${domainName}`);
5946
6622
  }
@@ -5971,7 +6647,7 @@ var PorkbunRegistrar = class extends BaseRegistrar {
5971
6647
  * aren't supported via the API.
5972
6648
  */
5973
6649
  async transferIn(domainName, input, opts) {
5974
- const pricing = await this.tldPricing(extractTld(domainName), opts);
6650
+ const pricing = await this.tldPricing(extractTld(domainName), asRead(opts));
5975
6651
  if (pricing.transfer == null) {
5976
6652
  throw new Error(`${this.name}: could not determine the transfer price for ${domainName}`);
5977
6653
  }
@@ -6264,6 +6940,8 @@ function toDnsRecord3(r, domainName) {
6264
6940
  var WRITABLE_DNS_TYPES = /* @__PURE__ */ new Set(["A", "AAAA", "CNAME", "NS", "TXT", "MX"]);
6265
6941
  var SpaceshipRegistrar = class extends BaseRegistrar {
6266
6942
  name = "spaceship";
6943
+ // a REST API: GET and HEAD never change state
6944
+ static safeMethods = REST_SAFE_METHODS;
6267
6945
  static displayName = "Spaceship";
6268
6946
  static website = "spaceship.com";
6269
6947
  static helpText = 'Generate your API key and secret in the API Manager at Spaceship using the "New API key" button. The API requires both X-API-Key and X-API-Secret headers.';
@@ -6570,8 +7248,8 @@ var SpaceshipRegistrar = class extends BaseRegistrar {
6570
7248
  ...opts
6571
7249
  });
6572
7250
  }
6573
- const keep = new Set(items.map((r) => recordKey(r.type, r.name)));
6574
- const stale = existing.filter((r) => (r.group?.type ?? "custom") === "custom").filter((r) => !keep.has(recordKey(r.type, r.name))).map(({ group: _group, ...rr }) => rr);
7251
+ const keep = new Set(items.map((r) => recordKey2(r.type, r.name)));
7252
+ const stale = existing.filter((r) => (r.group?.type ?? "custom") === "custom").filter((r) => !keep.has(recordKey2(r.type, r.name))).map(({ group: _group, ...rr }) => rr);
6575
7253
  if (stale.length > 0) {
6576
7254
  await this.http.request({
6577
7255
  method: "DELETE",
@@ -6730,7 +7408,7 @@ function toSpaceshipRecord(r) {
6730
7408
  }
6731
7409
  return record;
6732
7410
  }
6733
- function recordKey(type, name) {
7411
+ function recordKey2(type, name) {
6734
7412
  return `${(type ?? "").toUpperCase()}\0${name ?? "@"}`;
6735
7413
  }
6736
7414
  function fromSpaceshipContact(c) {
@@ -6774,6 +7452,7 @@ var registrars = {
6774
7452
  godaddy: GoDaddyRegistrar,
6775
7453
  namebright: NameBrightRegistrar,
6776
7454
  namecheap: NamecheapRegistrar,
7455
+ namecom: NamecomRegistrar,
6777
7456
  namesilo: NameSiloRegistrar,
6778
7457
  porkbun: PorkbunRegistrar,
6779
7458
  spaceship: SpaceshipRegistrar
@@ -6783,6 +7462,6 @@ function createRegistrar(name, credentials, options) {
6783
7462
  return new RegistrarClass(credentials, options);
6784
7463
  }
6785
7464
 
6786
- export { ALL_FEATURES, AbortError, AuthenticationError, AuthorizationError, BaseRegistrar, CORE_FEATURES, CloudflareRegistrar, ConfigurationError, ConnectionError, ConsentRequiredError, DEFAULT_OPTIONS, DynadotRegistrar, EXTENDED_FEATURES, Feature, GandiRegistrar, GoDaddyRegistrar, HttpClient, InvalidResponseError, MASKED_FORWARD_UNSUPPORTED, NameBrightRegistrar, NameSiloRegistrar, NamecheapRegistrar, NotFoundError, NotImplementedError, ParsingError, PorkbunRegistrar, RateLimitError, RegistrarClient, RegistrarError, RetryableRegistrarError, SpaceshipRegistrar, TimeoutError, USER_AGENT, backoffDelay, createDomain, createRegistrar, ensureArray, filterDomains, isCoreFeature, listPortfolio, normalizeDomain, normalizeNameservers, parseDate, parseXml, registrars, requireConsent, selectBaseUrl, settableForwards, sleep, toRegistrarError };
7465
+ export { ALL_FEATURES, AbortError, AuthenticationError, AuthorizationError, BaseRegistrar, CORE_FEATURES, CloudflareRegistrar, ConfigurationError, ConnectionError, ConsentRequiredError, DEFAULT_OPTIONS, DynadotRegistrar, EXTENDED_FEATURES, FEATURE_CALLS, Feature, GandiRegistrar, GoDaddyRegistrar, HttpClient, InvalidResponseError, MASKED_FORWARD_UNSUPPORTED, NameBrightRegistrar, NameSiloRegistrar, NamecheapRegistrar, NamecomRegistrar, NotFoundError, NotImplementedError, OutcomeUnknownError, ParsingError, PorkbunRegistrar, REST_SAFE_METHODS, RateLimitError, RegistrarClient, RegistrarError, RetryableRegistrarError, SpaceshipRegistrar, TimeoutError, USER_AGENT, asRead, backoffDelay, createDomain, createRegistrar, ensureArray, filterDomains, isCoreFeature, listPortfolio, markNotSent, normalizeDomain, normalizeNameservers, parseDate, parseXml, registrars, requireConsent, selectBaseUrl, settableForwards, sleep, toRegistrarError, wasNotSent };
6787
7466
  //# sourceMappingURL=index.js.map
6788
7467
  //# sourceMappingURL=index.js.map