@aoxborrow/registrar-client 0.6.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/README.md CHANGED
@@ -387,9 +387,9 @@ a provider-specific workflow. Transfers accept `authCode` and optional `privacy`
387
387
  explicit `years`, `contacts` or `autoRenew` are rejected before submission because
388
388
  Core cannot apply them in its transfer request. Update settings after completion.
389
389
 
390
- Every mutation disables automatic retries, including per-request retry overrides.
391
- Transport/parse failures after a purchase report an unknown outcome: inspect
392
- Name.com orders and domain/transfer status before retrying. An accepted order is
390
+ Mutations follow the shared [retry rules](#retries): a request Name.com may have
391
+ received is never re-sent. Transport/parse failures after a purchase report an
392
+ unknown outcome: inspect Name.com orders and domain/transfer status before retrying. An accepted order is
393
393
  reported as accepted, not proof of final registry completion. Read operations
394
394
  retain the shared retry policy and honor `Retry-After` / `X-RateLimit-Reset`.
395
395
  HTTP errors retain typed status information but omit provider response bodies,
@@ -482,8 +482,12 @@ implementations**:
482
482
  Extend `BaseRegistrar`, pass a `baseUrl` + auth headers to `super()`, add the
483
483
  static `displayName` / `configFields` / `helpText` metadata, override the
484
484
  operations the API supports (mapping payloads to the shared types in
485
- `src/types.ts`), and add the class to `src/registrars/index.ts`. The existing
486
- providers under `src/registrars/` are working references.
485
+ `src/types.ts`), and add the class to `src/registrars/index.ts`. Forward the
486
+ `opts` you are given into every `this.http` request: they carry the timeout and
487
+ signal, and tell the [retry rules](#retries) whether the call reads or writes.
488
+ For a REST API set `static safeMethods = REST_SAFE_METHODS`; otherwise wrap any
489
+ lookup a write performs first in `asRead(opts)`. The existing providers under
490
+ `src/registrars/` are working references.
487
491
 
488
492
  **New providers are welcome** — if you've wired up a registrar, open a PR.
489
493
 
@@ -493,6 +497,61 @@ All failures throw a typed subclass of `RegistrarError` (`AuthenticationError`,
493
497
  `NotFoundError`, `RateLimitError`, `TimeoutError`, `NotImplementedError`, …),
494
498
  each carrying an HTTP-style `status`.
495
499
 
500
+ ## Retries
501
+
502
+ Requests retry with exponential backoff (`retries`, default 2). What may be
503
+ re-sent depends on where the failure happened and on whether the call reads or
504
+ writes, and is the same for every provider:
505
+
506
+ | Failure | Read | Write |
507
+ | -------------------------------------------------------------------------- | ----- | -------------------------------------- |
508
+ | The registrar never saw it: DNS failure, connection refused, TLS handshake | Retry | Retry |
509
+ | `429`, honoring `Retry-After` | Retry | Retry |
510
+ | Outcome unknown: timeout, connection dropped mid-response, `5xx` | Retry | **Not re-sent.** `OutcomeUnknownError` |
511
+ | Any other response | Fail | Fail |
512
+
513
+ Reads and writes are told apart by feature, not by HTTP method (`FEATURE_CALLS`
514
+ lists every one). The method is no guide: Namecheap sends writes as `GET`, and
515
+ Porkbun sends reads as `POST`. `getAuthCode` counts as a write because some
516
+ registrars regenerate the code.
517
+
518
+ A write often reads first: current DNS records, a price, a zone id. Those
519
+ lookups changed nothing, so they are retried like any read and never reported as
520
+ an unknown outcome. REST providers get this from the HTTP method
521
+ (`static safeMethods = REST_SAFE_METHODS` treats `GET` and `HEAD` as reads);
522
+ providers whose method means nothing wrap the lookup's options in `asRead(opts)`.
523
+
524
+ A write whose outcome is unknown may have been applied, and a second renewal
525
+ charges twice. So the library reports it and leaves the decision to you: re-read
526
+ the domain, then retry if nothing changed. Providers that throw raise
527
+ `OutcomeUnknownError`; providers that return an `OperationResult` set
528
+ `outcome: 'unknown'` on it:
529
+
530
+ ```ts
531
+ const result = await client.renewDomain('example.com', 1);
532
+ if (!result.success && result.outcome === 'unknown') {
533
+ const domain = await client.getDomain('example.com'); // did it go through?
534
+ }
535
+ ```
536
+
537
+ Only positive evidence counts as "never sent": a pre-connection error code from
538
+ Node's `fetch`, or an error a custom transport marked with `markNotSent`.
539
+ Browsers and Workers report network failures opaquely, so there they count as
540
+ unknown.
541
+
542
+ ### Custom transport
543
+
544
+ Pass `fetch` to route every request through your own transport, such as a
545
+ proxy:
546
+
547
+ ```ts
548
+ const registrar = createRegistrar('namecheap', credentials, { fetch: proxiedFetch });
549
+ ```
550
+
551
+ If the transport can fail before the request leaves, for instance a proxy that
552
+ refuses the tunnel, throw `markNotSent(error)` so the failure is retried even
553
+ for writes.
554
+
496
555
  ## Security
497
556
 
498
557
  Credentials are sent as request **headers** wherever the registrar's API
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;
63
69
  }
64
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;
81
+ }
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.';
@@ -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.";
@@ -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.)";
@@ -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");
@@ -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`);
@@ -4979,7 +5156,7 @@ var NamecomHttpClient = class extends HttpClient {
4979
5156
  } catch (error) {
4980
5157
  if (error instanceof ParsingError) throw new ParsingError("namecom: invalid JSON response");
4981
5158
  if (error instanceof ConnectionError)
4982
- throw new ConnectionError("namecom: network request failed");
5159
+ throw new ConnectionError("namecom: network request failed", { notSent: error.notSent });
4983
5160
  throw error;
4984
5161
  }
4985
5162
  }
@@ -5004,6 +5181,8 @@ var NamecomHttpClient = class extends HttpClient {
5004
5181
  };
5005
5182
  var NamecomRegistrar = class extends BaseRegistrar {
5006
5183
  name = "namecom";
5184
+ // a REST API: GET and HEAD never change state
5185
+ static safeMethods = REST_SAFE_METHODS;
5007
5186
  static displayName = "Name.com";
5008
5187
  static website = "name.com";
5009
5188
  static supportsSandbox = true;
@@ -5032,7 +5211,11 @@ var NamecomRegistrar = class extends BaseRegistrar {
5032
5211
  headers: { Authorization: `Basic ${auth}` }
5033
5212
  };
5034
5213
  super(credentials, config, options);
5035
- this.http = new NamecomHttpClient({ ...config, options: this.options });
5214
+ this.http = new NamecomHttpClient({
5215
+ ...config,
5216
+ safeMethods: REST_SAFE_METHODS,
5217
+ options: this.options
5218
+ });
5036
5219
  }
5037
5220
  async testConnection(opts) {
5038
5221
  try {
@@ -5260,7 +5443,6 @@ var NamecomRegistrar = class extends BaseRegistrar {
5260
5443
  const old = index >= 0 ? remaining.splice(index, 1)[0] : void 0;
5261
5444
  await this.http.request({
5262
5445
  ...opts,
5263
- retries: 0,
5264
5446
  method: old ? "PUT" : "POST",
5265
5447
  path: old ? `${path}/${old.id}` : path,
5266
5448
  body: record
@@ -5269,7 +5451,6 @@ var NamecomRegistrar = class extends BaseRegistrar {
5269
5451
  for (const old of remaining) {
5270
5452
  await this.http.request({
5271
5453
  ...opts,
5272
- retries: 0,
5273
5454
  method: "DELETE",
5274
5455
  path: `${path}/${old.id}`
5275
5456
  });
@@ -5333,7 +5514,7 @@ var NamecomRegistrar = class extends BaseRegistrar {
5333
5514
  }
5334
5515
  async mutate(req, opts) {
5335
5516
  try {
5336
- await this.http.request({ ...req, ...opts, retries: 0 });
5517
+ await this.http.request({ ...req, ...opts });
5337
5518
  return { success: true, message: "Domain updated successfully" };
5338
5519
  } catch (error) {
5339
5520
  return { success: false, message: toRegistrarError(error).message };
@@ -5746,7 +5927,7 @@ var NameSiloRegistrar = class extends BaseRegistrar {
5746
5927
  */
5747
5928
  async disableDnssec(domainName, opts) {
5748
5929
  try {
5749
- const res = await this.call("dnsSecListRecords", { domain: domainName }, opts);
5930
+ const res = await this.call("dnsSecListRecords", { domain: domainName }, asRead(opts));
5750
5931
  if (!replyOk(res)) return statusResult2(res);
5751
5932
  for (const d of ensureArray(res.reply?.ds_record)) {
5752
5933
  const del = await this.call(
@@ -5819,7 +6000,7 @@ var NameSiloRegistrar = class extends BaseRegistrar {
5819
6000
  desired.set(f.alias, list);
5820
6001
  }
5821
6002
  const currentAliases = new Set(
5822
- (await this.listEmailForwards(domainName, opts)).map((r) => r.alias).filter(Boolean)
6003
+ (await this.listEmailForwards(domainName, asRead(opts))).map((r) => r.alias).filter(Boolean)
5823
6004
  );
5824
6005
  for (const alias of currentAliases) {
5825
6006
  if (!desired.has(alias)) {
@@ -5910,7 +6091,7 @@ var NameSiloRegistrar = class extends BaseRegistrar {
5910
6091
  * delegation nameservers aren't part of this list (see `updateNameservers`).
5911
6092
  */
5912
6093
  async setDnsRecords(domainName, records, opts) {
5913
- const listRes = await this.call("dnsListRecords", { domain: domainName }, opts);
6094
+ const listRes = await this.call("dnsListRecords", { domain: domainName }, asRead(opts));
5914
6095
  if (!replyOk(listRes)) return { success: false, message: replyDetail(listRes) };
5915
6096
  const existing = ensureArray(listRes.reply?.resource_record);
5916
6097
  const existingByKey = /* @__PURE__ */ new Map();
@@ -6437,7 +6618,7 @@ var PorkbunRegistrar = class extends BaseRegistrar {
6437
6618
  * renewals aren't supported via the API.
6438
6619
  */
6439
6620
  async renewDomain(domainName, _years = 1, opts) {
6440
- const pricing = await this.tldPricing(extractTld(domainName), opts);
6621
+ const pricing = await this.tldPricing(extractTld(domainName), asRead(opts));
6441
6622
  if (pricing.renewal == null) {
6442
6623
  throw new Error(`${this.name}: could not determine the renewal price for ${domainName}`);
6443
6624
  }
@@ -6468,7 +6649,7 @@ var PorkbunRegistrar = class extends BaseRegistrar {
6468
6649
  * aren't supported via the API.
6469
6650
  */
6470
6651
  async transferIn(domainName, input, opts) {
6471
- const pricing = await this.tldPricing(extractTld(domainName), opts);
6652
+ const pricing = await this.tldPricing(extractTld(domainName), asRead(opts));
6472
6653
  if (pricing.transfer == null) {
6473
6654
  throw new Error(`${this.name}: could not determine the transfer price for ${domainName}`);
6474
6655
  }
@@ -6761,6 +6942,8 @@ function toDnsRecord3(r, domainName) {
6761
6942
  var WRITABLE_DNS_TYPES = /* @__PURE__ */ new Set(["A", "AAAA", "CNAME", "NS", "TXT", "MX"]);
6762
6943
  var SpaceshipRegistrar = class extends BaseRegistrar {
6763
6944
  name = "spaceship";
6945
+ // a REST API: GET and HEAD never change state
6946
+ static safeMethods = REST_SAFE_METHODS;
6764
6947
  static displayName = "Spaceship";
6765
6948
  static website = "spaceship.com";
6766
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.';
@@ -7294,6 +7477,7 @@ exports.ConsentRequiredError = ConsentRequiredError;
7294
7477
  exports.DEFAULT_OPTIONS = DEFAULT_OPTIONS;
7295
7478
  exports.DynadotRegistrar = DynadotRegistrar;
7296
7479
  exports.EXTENDED_FEATURES = EXTENDED_FEATURES;
7480
+ exports.FEATURE_CALLS = FEATURE_CALLS;
7297
7481
  exports.Feature = Feature;
7298
7482
  exports.GandiRegistrar = GandiRegistrar;
7299
7483
  exports.GoDaddyRegistrar = GoDaddyRegistrar;
@@ -7306,8 +7490,10 @@ exports.NamecheapRegistrar = NamecheapRegistrar;
7306
7490
  exports.NamecomRegistrar = NamecomRegistrar;
7307
7491
  exports.NotFoundError = NotFoundError;
7308
7492
  exports.NotImplementedError = NotImplementedError;
7493
+ exports.OutcomeUnknownError = OutcomeUnknownError;
7309
7494
  exports.ParsingError = ParsingError;
7310
7495
  exports.PorkbunRegistrar = PorkbunRegistrar;
7496
+ exports.REST_SAFE_METHODS = REST_SAFE_METHODS;
7311
7497
  exports.RateLimitError = RateLimitError;
7312
7498
  exports.RegistrarClient = RegistrarClient;
7313
7499
  exports.RegistrarError = RegistrarError;
@@ -7315,6 +7501,7 @@ exports.RetryableRegistrarError = RetryableRegistrarError;
7315
7501
  exports.SpaceshipRegistrar = SpaceshipRegistrar;
7316
7502
  exports.TimeoutError = TimeoutError;
7317
7503
  exports.USER_AGENT = USER_AGENT;
7504
+ exports.asRead = asRead;
7318
7505
  exports.backoffDelay = backoffDelay;
7319
7506
  exports.createDomain = createDomain;
7320
7507
  exports.createRegistrar = createRegistrar;
@@ -7322,6 +7509,7 @@ exports.ensureArray = ensureArray;
7322
7509
  exports.filterDomains = filterDomains;
7323
7510
  exports.isCoreFeature = isCoreFeature;
7324
7511
  exports.listPortfolio = listPortfolio;
7512
+ exports.markNotSent = markNotSent;
7325
7513
  exports.normalizeDomain = normalizeDomain;
7326
7514
  exports.normalizeNameservers = normalizeNameservers;
7327
7515
  exports.parseDate = parseDate;
@@ -7332,5 +7520,6 @@ exports.selectBaseUrl = selectBaseUrl;
7332
7520
  exports.settableForwards = settableForwards;
7333
7521
  exports.sleep = sleep;
7334
7522
  exports.toRegistrarError = toRegistrarError;
7523
+ exports.wasNotSent = wasNotSent;
7335
7524
  //# sourceMappingURL=index.cjs.map
7336
7525
  //# sourceMappingURL=index.cjs.map