@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.
@@ -18,7 +18,7 @@ import type {
18
18
  TldPricing,
19
19
  TransferDomainInput,
20
20
  } from '../types';
21
- import { createDomain, filterDomains, settableForwards } from '../utils';
21
+ import { asRead, createDomain, filterDomains, settableForwards } from '../utils';
22
22
  import { NotFoundError, toRegistrarError } from '../errors';
23
23
  import { ensureArray, parseXml } from '../xml';
24
24
  import { BaseRegistrar, selectBaseUrl } from '../registrar';
@@ -486,7 +486,7 @@ export class NameSiloRegistrar extends BaseRegistrar {
486
486
  opts?: RequestOptions
487
487
  ): Promise<OperationResult> {
488
488
  try {
489
- const res = await this.call('dnsSecListRecords', { domain: domainName }, opts);
489
+ const res = await this.call('dnsSecListRecords', { domain: domainName }, asRead(opts));
490
490
  if (!replyOk(res)) return statusResult(res);
491
491
  for (const d of ensureArray<NsDsRecord>(res.reply?.ds_record)) {
492
492
  const del = await this.call(
@@ -574,7 +574,7 @@ export class NameSiloRegistrar extends BaseRegistrar {
574
574
  desired.set(f.alias, list);
575
575
  }
576
576
  const currentAliases = new Set(
577
- (await this.listEmailForwards(domainName, opts)).map(r => r.alias).filter(Boolean)
577
+ (await this.listEmailForwards(domainName, asRead(opts))).map(r => r.alias).filter(Boolean)
578
578
  );
579
579
 
580
580
  for (const alias of currentAliases) {
@@ -696,7 +696,7 @@ export class NameSiloRegistrar extends BaseRegistrar {
696
696
  records: DnsRecord[],
697
697
  opts?: RequestOptions
698
698
  ): Promise<OperationResult> {
699
- const listRes = await this.call('dnsListRecords', { domain: domainName }, opts);
699
+ const listRes = await this.call('dnsListRecords', { domain: domainName }, asRead(opts));
700
700
  if (!replyOk(listRes)) return { success: false, message: replyDetail(listRes) };
701
701
  const existing = ensureArray(listRes.reply?.resource_record);
702
702
 
@@ -17,7 +17,7 @@ import type {
17
17
  TldPricing,
18
18
  TransferDomainInput,
19
19
  } from '../types';
20
- import { createDomain, filterDomains, normalizeDomain, settableForwards } from '../utils';
20
+ import { asRead, createDomain, filterDomains, normalizeDomain, settableForwards } from '../utils';
21
21
  import { NotFoundError, NotImplementedError, toRegistrarError } from '../errors';
22
22
  import { BaseRegistrar, selectBaseUrl } from '../registrar';
23
23
  import { Feature, type RegistrarFeature } from '../features';
@@ -520,7 +520,7 @@ export class PorkbunRegistrar extends BaseRegistrar {
520
520
  _years = 1,
521
521
  opts?: RequestOptions
522
522
  ): Promise<OperationResult> {
523
- const pricing = await this.tldPricing(extractTld(domainName), opts);
523
+ const pricing = await this.tldPricing(extractTld(domainName), asRead(opts));
524
524
  if (pricing.renewal == null) {
525
525
  throw new Error(`${this.name}: could not determine the renewal price for ${domainName}`);
526
526
  }
@@ -563,7 +563,7 @@ export class PorkbunRegistrar extends BaseRegistrar {
563
563
  input: TransferDomainInput,
564
564
  opts?: RequestOptions
565
565
  ): Promise<OperationResult> {
566
- const pricing = await this.tldPricing(extractTld(domainName), opts);
566
+ const pricing = await this.tldPricing(extractTld(domainName), asRead(opts));
567
567
  if (pricing.transfer == null) {
568
568
  throw new Error(`${this.name}: could not determine the transfer price for ${domainName}`);
569
569
  }
@@ -14,6 +14,7 @@ import type {
14
14
  TldPricing,
15
15
  TransferDomainInput,
16
16
  } from '../types';
17
+ import { REST_SAFE_METHODS } from '../http';
17
18
  import { createDomain, filterDomains, requireConsent, sleep } from '../utils';
18
19
  import { NotImplementedError, toRegistrarError } from '../errors';
19
20
  import { BaseRegistrar, selectBaseUrl } from '../registrar';
@@ -131,6 +132,8 @@ const WRITABLE_DNS_TYPES = new Set(['A', 'AAAA', 'CNAME', 'NS', 'TXT', 'MX']);
131
132
  */
132
133
  export class SpaceshipRegistrar extends BaseRegistrar {
133
134
  readonly name = 'spaceship';
135
+ // a REST API: GET and HEAD never change state
136
+ static override readonly safeMethods = REST_SAFE_METHODS;
134
137
 
135
138
  static readonly displayName = 'Spaceship';
136
139
  static readonly website = 'spaceship.com';
package/src/types.ts CHANGED
@@ -12,10 +12,34 @@ export interface RegistrarClientOptions {
12
12
  backoff: number;
13
13
  // optional AbortSignal to cancel in-flight requests
14
14
  signal?: AbortSignal;
15
+ // fetch implementation used for every request, e.g. one that routes through a
16
+ // proxy. Defaults to the global `fetch`. A transport that fails before the
17
+ // request leaves (proxy refused, tunnel not established) should throw an
18
+ // error marked with `markNotSent` so the failure is retried for any call.
19
+ fetch?: typeof globalThis.fetch;
20
+ }
21
+
22
+ // whether a call only reads registrar state or may change it. Decides what is
23
+ // safe to re-send after a failure; see `FEATURE_CALLS` in features.ts.
24
+ export type RequestIntent = 'read' | 'write';
25
+
26
+ // per-call bookkeeping the library threads through `RequestOptions` from a
27
+ // feature method down to the HTTP layer. Set by `BaseRegistrar`; callers never
28
+ // need to supply it.
29
+ export interface FeatureCall {
30
+ intent: RequestIntent;
31
+ // the feature method being served, e.g. "renewDomain"
32
+ feature?: string;
33
+ // set by the HTTP layer when a write's outcome could not be determined, so
34
+ // it survives providers that fold errors into an `OperationResult`
35
+ outcomeUnknown?: Error;
15
36
  }
16
37
 
17
38
  // per-request options that can override client-level options
18
- export type RequestOptions = Partial<RegistrarClientOptions>;
39
+ export interface RequestOptions extends Partial<RegistrarClientOptions> {
40
+ // library-internal; see `FeatureCall`
41
+ call?: FeatureCall;
42
+ }
19
43
 
20
44
  // options for `listDomains`. Extends `RequestOptions` so timeout/retries/signal
21
45
  // still flow through a single argument. `listDomains` always returns the full
@@ -56,6 +80,11 @@ export interface ConnectionResult {
56
80
  export interface OperationResult {
57
81
  success: boolean;
58
82
  message: string;
83
+ // Present only on a failed write whose request may have reached the
84
+ // registrar (timed out, connection dropped mid-response, or a 5xx). The
85
+ // change may or may not have been applied: re-read the domain before trying
86
+ // again. The library never re-sends such a request itself.
87
+ outcome?: 'unknown';
59
88
  }
60
89
 
61
90
  // a configuration field a registrar needs (used to build credential UIs generically)
package/src/utils.ts CHANGED
@@ -1,4 +1,10 @@
1
- import type { DomainForward, Domain, DomainInput, RegistrationConsent } from './types';
1
+ import type {
2
+ DomainForward,
3
+ Domain,
4
+ DomainInput,
5
+ RegistrationConsent,
6
+ RequestOptions,
7
+ } from './types';
2
8
  import { ConsentRequiredError } from './errors';
3
9
 
4
10
  // sleep helper for retry backoff
@@ -133,3 +139,12 @@ export function createDomain(data: DomainInput = {}): Domain {
133
139
  deleted: data.deleted ?? false,
134
140
  };
135
141
  }
142
+
143
+ // Marks a request made inside a write feature as a read: a lookup the write
144
+ // needs first (current records, a price, an auth token). It is retried like any
145
+ // read, and its failure is never reported as a write of unknown outcome. For
146
+ // APIs whose HTTP method can't tell reads from writes; REST providers get this
147
+ // from `safeMethods`.
148
+ export function asRead<T extends RequestOptions>(opts?: T): T {
149
+ return { ...opts, call: { ...opts?.call, intent: 'read' } } as T;
150
+ }