@aoxborrow/registrar-client 0.6.0 → 0.8.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);
@@ -501,8 +597,8 @@ var HttpClient = class {
501
597
  case 404:
502
598
  return new NotFoundError(message);
503
599
  case 429: {
504
- const retryAfter = Number(response.headers.get("retry-after"));
505
- return new RateLimitError(message, Number.isFinite(retryAfter) ? retryAfter : void 0);
600
+ const retryAfter = parseRetryAfter(response.headers.get("retry-after")) ?? this.config.rateLimitFallbackSeconds;
601
+ return new RateLimitError(message, retryAfter);
506
602
  }
507
603
  default: {
508
604
  if (response.status >= 500) {
@@ -515,6 +611,23 @@ var HttpClient = class {
515
611
  }
516
612
  }
517
613
  };
614
+ function parseRetryAfter(value) {
615
+ const text2 = value?.trim();
616
+ if (!text2) return void 0;
617
+ if (/^\d+$/.test(text2)) {
618
+ const seconds = Number(text2);
619
+ return Number.isSafeInteger(seconds) ? seconds : void 0;
620
+ }
621
+ const zonedDate = /^(?:[a-z]{3}, \d{2} [a-z]{3} \d{4}|[a-z]+, \d{2}-[a-z]{3}-\d{2}) \d{2}:\d{2}:\d{2} GMT$/i;
622
+ const asctimeDate = /^[a-z]{3} [a-z]{3} (?: \d|\d{2}) \d{2}:\d{2}:\d{2} \d{4}$/i;
623
+ if (!zonedDate.test(text2) && !asctimeDate.test(text2)) return void 0;
624
+ const date = Date.parse(asctimeDate.test(text2) ? `${text2} GMT` : text2);
625
+ return Number.isFinite(date) ? Math.max(0, Math.ceil((date - Date.now()) / 1e3)) : void 0;
626
+ }
627
+ function isOutcomeUnknown(error) {
628
+ if (error instanceof TimeoutError || error instanceof InvalidResponseError) return true;
629
+ return error instanceof ConnectionError && !error.notSent;
630
+ }
518
631
  function isAbortError(error) {
519
632
  return typeof error === "object" && error !== null && "name" in error && error.name === "AbortError";
520
633
  }
@@ -611,6 +724,33 @@ var CORE_FEATURE_SET = new Set(CORE_FEATURES);
611
724
  function isCoreFeature(feature) {
612
725
  return CORE_FEATURE_SET.has(feature);
613
726
  }
727
+ var FEATURE_CALLS = {
728
+ testConnection: { intent: "read", optsIndex: 0 },
729
+ listDomains: { intent: "read", optsIndex: 0 },
730
+ getDomain: { intent: "read", optsIndex: 1 },
731
+ checkAvailability: { intent: "read", optsIndex: 1 },
732
+ getPricing: { intent: "read", optsIndex: 1 },
733
+ registerDomain: { intent: "write", optsIndex: 2 },
734
+ renewDomain: { intent: "write", optsIndex: 2 },
735
+ setAutoRenew: { intent: "write", optsIndex: 2 },
736
+ transferIn: { intent: "write", optsIndex: 2 },
737
+ updateNameservers: { intent: "write", optsIndex: 2 },
738
+ getNameservers: { intent: "read", optsIndex: 1 },
739
+ lockDomain: { intent: "write", optsIndex: 1 },
740
+ unlockDomain: { intent: "write", optsIndex: 1 },
741
+ setPrivacy: { intent: "write", optsIndex: 2 },
742
+ getContacts: { intent: "read", optsIndex: 1 },
743
+ updateContacts: { intent: "write", optsIndex: 2 },
744
+ getDnsRecords: { intent: "read", optsIndex: 1 },
745
+ setDnsRecords: { intent: "write", optsIndex: 2 },
746
+ getAuthCode: { intent: "write", optsIndex: 1 },
747
+ getDnssec: { intent: "read", optsIndex: 1 },
748
+ disableDnssec: { intent: "write", optsIndex: 1 },
749
+ getEmailForwarding: { intent: "read", optsIndex: 1 },
750
+ setEmailForwarding: { intent: "write", optsIndex: 2 },
751
+ getDomainForwarding: { intent: "read", optsIndex: 1 },
752
+ setDomainForwarding: { intent: "write", optsIndex: 2 }
753
+ };
614
754
 
615
755
  // src/registrar.ts
616
756
  function selectBaseUrl(registrar, environment, urls) {
@@ -636,6 +776,9 @@ var BaseRegistrar = class {
636
776
  static get features() {
637
777
  return [...CORE_FEATURES, ...this.extendedFeatures];
638
778
  }
779
+ // HTTP methods this provider's API reserves for reads; see
780
+ // `HttpClientConfig.safeMethods`. REST providers set `REST_SAFE_METHODS`.
781
+ static safeMethods = [];
639
782
  // instance mirror of the static `features`
640
783
  get features() {
641
784
  return this.constructor.features;
@@ -654,7 +797,39 @@ var BaseRegistrar = class {
654
797
  this.credentials = credentials;
655
798
  this.environment = environment;
656
799
  this.options = { ...DEFAULT_OPTIONS, ...clientOptions };
657
- this.http = new HttpClient({ ...httpConfig, options: this.options });
800
+ this.http = new HttpClient({
801
+ safeMethods: this.constructor.safeMethods,
802
+ ...httpConfig,
803
+ options: this.options
804
+ });
805
+ this.threadFeatureCalls();
806
+ }
807
+ // Wrap each feature method so its `RequestOptions` carry a `FeatureCall`:
808
+ // the read/write intent the HTTP layer needs to decide what may be re-sent,
809
+ // and a slot where it reports a write whose outcome is unknown. Providers
810
+ // already forward their options into every request, so nothing else changes.
811
+ //
812
+ // A feature calling another feature (a write re-reading the domain, say)
813
+ // goes through that feature's own wrapper, so the inner read is still a read.
814
+ // Providers fold most failures into an `OperationResult`; the unknown outcome
815
+ // is copied onto that result so it is not lost along with the error.
816
+ threadFeatureCalls() {
817
+ const self = this;
818
+ for (const [feature, { intent, optsIndex }] of Object.entries(FEATURE_CALLS)) {
819
+ const original = self[feature];
820
+ if (typeof original !== "function") continue;
821
+ self[feature] = (...args) => {
822
+ const call = { intent, feature };
823
+ const next = [...args];
824
+ while (next.length <= optsIndex) next.push(void 0);
825
+ next[optsIndex] = { ...next[optsIndex], call };
826
+ const result = original.apply(this, next);
827
+ if (!(result instanceof Promise)) return result;
828
+ return result.then(
829
+ (value) => call.outcomeUnknown && value !== null && typeof value === "object" && value.success === false ? { ...value, outcome: "unknown" } : value
830
+ );
831
+ };
832
+ }
658
833
  }
659
834
  testConnection(_opts) {
660
835
  return Promise.reject(
@@ -745,6 +920,8 @@ var BaseRegistrar = class {
745
920
  // src/registrars/cloudflare.ts
746
921
  var CloudflareRegistrar = class _CloudflareRegistrar extends BaseRegistrar {
747
922
  name = "cloudflare";
923
+ // a REST API: GET and HEAD never change state
924
+ static safeMethods = REST_SAFE_METHODS;
748
925
  static displayName = "Cloudflare";
749
926
  static website = "cloudflare.com";
750
927
  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.';
@@ -1602,6 +1779,8 @@ var SEARCH_BATCH_SIZE = 100;
1602
1779
  var SUPPORTED_DNS_TYPES = /* @__PURE__ */ new Set(["A", "AAAA", "CNAME", "MX", "TXT", "NS", "SRV", "CAA"]);
1603
1780
  var DynadotRegistrar = class extends BaseRegistrar {
1604
1781
  name = "dynadot";
1782
+ // a REST API: GET and HEAD never change state
1783
+ static safeMethods = REST_SAFE_METHODS;
1605
1784
  static displayName = "Dynadot";
1606
1785
  static website = "dynadot.com";
1607
1786
  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.";
@@ -1632,7 +1811,10 @@ var DynadotRegistrar = class extends BaseRegistrar {
1632
1811
  production: "https://api.dynadot.com",
1633
1812
  sandbox: "https://api-sandbox.dynadot.com"
1634
1813
  }),
1635
- headers: { Authorization: `Bearer ${credentials.apiKey}` }
1814
+ headers: { Authorization: `Bearer ${credentials.apiKey}` },
1815
+ // HTTP 429 can omit Retry-After. Avoid exhausting all attempts inside
1816
+ // Dynadot's regular-tier minute window in that case.
1817
+ rateLimitFallbackSeconds: 60
1636
1818
  },
1637
1819
  options
1638
1820
  );
@@ -2353,6 +2535,8 @@ var GANDI_TYPE_TO_FORWARD = {
2353
2535
  };
2354
2536
  var GandiRegistrar = class extends BaseRegistrar {
2355
2537
  name = "gandi";
2538
+ // a REST API: GET and HEAD never change state
2539
+ static safeMethods = REST_SAFE_METHODS;
2356
2540
  static displayName = "Gandi.net";
2357
2541
  static website = "gandi.net";
2358
2542
  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.)";
@@ -3040,6 +3224,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
3224
  var PRICE_MICRO_UNITS = 1e6;
3041
3225
  var GoDaddyRegistrar = class _GoDaddyRegistrar extends BaseRegistrar {
3042
3226
  name = "godaddy";
3227
+ // a REST API: GET and HEAD never change state
3228
+ static safeMethods = REST_SAFE_METHODS;
3043
3229
  static displayName = "GoDaddy";
3044
3230
  static website = "godaddy.com";
3045
3231
  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 +4106,8 @@ function dnsFieldsDiffer(a, b) {
3920
4106
  var TOKEN_URL = "https://api.namebright.com/auth/token";
3921
4107
  var NameBrightRegistrar = class extends BaseRegistrar {
3922
4108
  name = "namebright";
4109
+ // a REST API: GET and HEAD never change state
4110
+ static safeMethods = REST_SAFE_METHODS;
3923
4111
  static displayName = "NameBright";
3924
4112
  static website = "namebright.com";
3925
4113
  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 +4146,8 @@ var NameBrightRegistrar = class extends BaseRegistrar {
3958
4146
  client_id: this.credentials.clientId,
3959
4147
  client_secret: this.credentials.clientSecret
3960
4148
  },
3961
- ...opts
4149
+ // issuing a token changes nothing on the account, whatever it precedes
4150
+ ...asRead(opts)
3962
4151
  });
3963
4152
  if (!res.access_token) {
3964
4153
  throw new AuthenticationError("NameBright: token endpoint returned no access_token");
@@ -4566,9 +4755,10 @@ var NamecheapRegistrar = class extends BaseRegistrar {
4566
4755
  }
4567
4756
  /**
4568
4757
  * Availability via domains.check (batched at 50 per request, Namecheap's cap).
4569
- * The check response only carries a price for *premium* names
4570
- * (`PremiumRegistrationPrice`); regular registration pricing comes from
4571
- * `getPricing`, so `price` is set only for premium results.
4758
+ * The check response only carries prices for *premium* names
4759
+ * (`PremiumRegistrationPrice`, `PremiumRenewalPrice`); regular pricing comes
4760
+ * from `getPricing`, so `price` and `renewalPrice` are set only for premium
4761
+ * results.
4572
4762
  */
4573
4763
  async checkAvailability(domainNames, opts) {
4574
4764
  const results = [];
@@ -4581,12 +4771,16 @@ var NamecheapRegistrar = class extends BaseRegistrar {
4581
4771
  );
4582
4772
  for (const el of ensureArray(cr.DomainCheckResult)) {
4583
4773
  const premium = el["@_IsPremiumName"] === "true";
4584
- const premiumPrice = premium ? Number(el["@_PremiumRegistrationPrice"]) : NaN;
4774
+ const premiumPrice = (attr) => {
4775
+ const n = premium ? Number(attr) : NaN;
4776
+ return Number.isFinite(n) && n > 0 ? n : void 0;
4777
+ };
4585
4778
  results.push({
4586
4779
  domainName: el["@_Domain"] ?? "",
4587
4780
  available: el["@_Available"] === "true",
4588
4781
  premium,
4589
- price: Number.isFinite(premiumPrice) && premiumPrice > 0 ? premiumPrice : void 0
4782
+ price: premiumPrice(el["@_PremiumRegistrationPrice"]),
4783
+ renewalPrice: premiumPrice(el["@_PremiumRenewalPrice"])
4590
4784
  });
4591
4785
  }
4592
4786
  }
@@ -4737,7 +4931,11 @@ var NamecheapRegistrar = class extends BaseRegistrar {
4737
4931
  * has no WhoisGuard allotted (e.g. TLDs that don't offer it).
4738
4932
  */
4739
4933
  async setPrivacy(domainName, enabled, opts) {
4740
- const cr = await this.command("namecheap.domains.getInfo", { DomainName: domainName }, opts);
4934
+ const cr = await this.command(
4935
+ "namecheap.domains.getInfo",
4936
+ { DomainName: domainName },
4937
+ asRead(opts)
4938
+ );
4741
4939
  const id = cr.DomainGetInfoResult?.Whoisguard?.ID;
4742
4940
  if (id == null || String(id) === "0") {
4743
4941
  throw new Error(`${this.name}: ${domainName} has no WhoisGuard subscription to toggle`);
@@ -4977,7 +5175,7 @@ var NamecomHttpClient = class extends HttpClient {
4977
5175
  } catch (error) {
4978
5176
  if (error instanceof ParsingError) throw new ParsingError("namecom: invalid JSON response");
4979
5177
  if (error instanceof ConnectionError)
4980
- throw new ConnectionError("namecom: network request failed");
5178
+ throw new ConnectionError("namecom: network request failed", { notSent: error.notSent });
4981
5179
  throw error;
4982
5180
  }
4983
5181
  }
@@ -4990,18 +5188,19 @@ var NamecomHttpClient = class extends HttpClient {
4990
5188
  }),
4991
5189
  url
4992
5190
  );
4993
- if (error instanceof RateLimitError) {
4994
- const retryAfter = response.headers.get("retry-after");
4995
- const seconds = retryAfter == null ? NaN : Number(retryAfter);
4996
- const date = retryAfter == null ? NaN : Date.parse(retryAfter);
4997
- const reset = response.headers.get("x-ratelimit-reset");
4998
- 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;
5191
+ if (error instanceof RateLimitError && error.retryAfter === void 0) {
5192
+ const reset = response.headers.get("x-ratelimit-reset")?.trim();
5193
+ if (reset && /^\d+(?:\.\d+)?$/.test(reset)) {
5194
+ error.retryAfter = Math.max(0, Math.ceil(Number(reset) - Date.now() / 1e3));
5195
+ }
4999
5196
  }
5000
5197
  return error;
5001
5198
  }
5002
5199
  };
5003
5200
  var NamecomRegistrar = class extends BaseRegistrar {
5004
5201
  name = "namecom";
5202
+ // a REST API: GET and HEAD never change state
5203
+ static safeMethods = REST_SAFE_METHODS;
5005
5204
  static displayName = "Name.com";
5006
5205
  static website = "name.com";
5007
5206
  static supportsSandbox = true;
@@ -5030,7 +5229,11 @@ var NamecomRegistrar = class extends BaseRegistrar {
5030
5229
  headers: { Authorization: `Basic ${auth}` }
5031
5230
  };
5032
5231
  super(credentials, config, options);
5033
- this.http = new NamecomHttpClient({ ...config, options: this.options });
5232
+ this.http = new NamecomHttpClient({
5233
+ ...config,
5234
+ safeMethods: REST_SAFE_METHODS,
5235
+ options: this.options
5236
+ });
5034
5237
  }
5035
5238
  async testConnection(opts) {
5036
5239
  try {
@@ -5258,7 +5461,6 @@ var NamecomRegistrar = class extends BaseRegistrar {
5258
5461
  const old = index >= 0 ? remaining.splice(index, 1)[0] : void 0;
5259
5462
  await this.http.request({
5260
5463
  ...opts,
5261
- retries: 0,
5262
5464
  method: old ? "PUT" : "POST",
5263
5465
  path: old ? `${path}/${old.id}` : path,
5264
5466
  body: record
@@ -5267,7 +5469,6 @@ var NamecomRegistrar = class extends BaseRegistrar {
5267
5469
  for (const old of remaining) {
5268
5470
  await this.http.request({
5269
5471
  ...opts,
5270
- retries: 0,
5271
5472
  method: "DELETE",
5272
5473
  path: `${path}/${old.id}`
5273
5474
  });
@@ -5331,7 +5532,7 @@ var NamecomRegistrar = class extends BaseRegistrar {
5331
5532
  }
5332
5533
  async mutate(req, opts) {
5333
5534
  try {
5334
- await this.http.request({ ...req, ...opts, retries: 0 });
5535
+ await this.http.request({ ...req, ...opts });
5335
5536
  return { success: true, message: "Domain updated successfully" };
5336
5537
  } catch (error) {
5337
5538
  return { success: false, message: toRegistrarError(error).message };
@@ -5744,7 +5945,7 @@ var NameSiloRegistrar = class extends BaseRegistrar {
5744
5945
  */
5745
5946
  async disableDnssec(domainName, opts) {
5746
5947
  try {
5747
- const res = await this.call("dnsSecListRecords", { domain: domainName }, opts);
5948
+ const res = await this.call("dnsSecListRecords", { domain: domainName }, asRead(opts));
5748
5949
  if (!replyOk(res)) return statusResult2(res);
5749
5950
  for (const d of ensureArray(res.reply?.ds_record)) {
5750
5951
  const del = await this.call(
@@ -5817,7 +6018,7 @@ var NameSiloRegistrar = class extends BaseRegistrar {
5817
6018
  desired.set(f.alias, list);
5818
6019
  }
5819
6020
  const currentAliases = new Set(
5820
- (await this.listEmailForwards(domainName, opts)).map((r) => r.alias).filter(Boolean)
6021
+ (await this.listEmailForwards(domainName, asRead(opts))).map((r) => r.alias).filter(Boolean)
5821
6022
  );
5822
6023
  for (const alias of currentAliases) {
5823
6024
  if (!desired.has(alias)) {
@@ -5908,7 +6109,7 @@ var NameSiloRegistrar = class extends BaseRegistrar {
5908
6109
  * delegation nameservers aren't part of this list (see `updateNameservers`).
5909
6110
  */
5910
6111
  async setDnsRecords(domainName, records, opts) {
5911
- const listRes = await this.call("dnsListRecords", { domain: domainName }, opts);
6112
+ const listRes = await this.call("dnsListRecords", { domain: domainName }, asRead(opts));
5912
6113
  if (!replyOk(listRes)) return { success: false, message: replyDetail(listRes) };
5913
6114
  const existing = ensureArray(listRes.reply?.resource_record);
5914
6115
  const existingByKey = /* @__PURE__ */ new Map();
@@ -6435,7 +6636,7 @@ var PorkbunRegistrar = class extends BaseRegistrar {
6435
6636
  * renewals aren't supported via the API.
6436
6637
  */
6437
6638
  async renewDomain(domainName, _years = 1, opts) {
6438
- const pricing = await this.tldPricing(extractTld(domainName), opts);
6639
+ const pricing = await this.tldPricing(extractTld(domainName), asRead(opts));
6439
6640
  if (pricing.renewal == null) {
6440
6641
  throw new Error(`${this.name}: could not determine the renewal price for ${domainName}`);
6441
6642
  }
@@ -6466,7 +6667,7 @@ var PorkbunRegistrar = class extends BaseRegistrar {
6466
6667
  * aren't supported via the API.
6467
6668
  */
6468
6669
  async transferIn(domainName, input, opts) {
6469
- const pricing = await this.tldPricing(extractTld(domainName), opts);
6670
+ const pricing = await this.tldPricing(extractTld(domainName), asRead(opts));
6470
6671
  if (pricing.transfer == null) {
6471
6672
  throw new Error(`${this.name}: could not determine the transfer price for ${domainName}`);
6472
6673
  }
@@ -6759,6 +6960,8 @@ function toDnsRecord3(r, domainName) {
6759
6960
  var WRITABLE_DNS_TYPES = /* @__PURE__ */ new Set(["A", "AAAA", "CNAME", "NS", "TXT", "MX"]);
6760
6961
  var SpaceshipRegistrar = class extends BaseRegistrar {
6761
6962
  name = "spaceship";
6963
+ // a REST API: GET and HEAD never change state
6964
+ static safeMethods = REST_SAFE_METHODS;
6762
6965
  static displayName = "Spaceship";
6763
6966
  static website = "spaceship.com";
6764
6967
  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.';
@@ -7279,6 +7482,6 @@ function createRegistrar(name, credentials, options) {
7279
7482
  return new RegistrarClass(credentials, options);
7280
7483
  }
7281
7484
 
7282
- 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, NamecomRegistrar, 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 };
7485
+ 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 };
7283
7486
  //# sourceMappingURL=index.js.map
7284
7487
  //# sourceMappingURL=index.js.map