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