@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/README.md +70 -5
- package/dist/index.cjs +241 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +48 -3
- package/dist/index.d.ts +48 -3
- package/dist/index.js +236 -33
- package/dist/index.js.map +1 -1
- package/package.json +9 -9
- package/src/errors.ts +69 -1
- package/src/features.ts +42 -0
- package/src/http.ts +102 -7
- package/src/index.ts +9 -2
- package/src/registrar.ts +47 -2
- package/src/registrars/cloudflare.ts +3 -0
- package/src/registrars/dynadot.ts +6 -0
- package/src/registrars/gandi.ts +3 -0
- package/src/registrars/godaddy.ts +3 -0
- package/src/registrars/namebright.ts +6 -2
- package/src/registrars/namecheap.ts +17 -6
- package/src/registrars/namecom.ts +18 -18
- package/src/registrars/namesilo.ts +4 -4
- package/src/registrars/porkbun.ts +3 -3
- package/src/registrars/spaceship.ts +3 -0
- package/src/types.ts +33 -1
- package/src/utils.ts +16 -1
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
toRegistrarError,
|
|
10
10
|
} from '../errors';
|
|
11
11
|
import { Feature, type RegistrarFeature } from '../features';
|
|
12
|
-
import { HttpClient, type RequestConfig } from '../http';
|
|
12
|
+
import { HttpClient, type RequestConfig, REST_SAFE_METHODS } from '../http';
|
|
13
13
|
import { BaseRegistrar, selectBaseUrl } from '../registrar';
|
|
14
14
|
import type {
|
|
15
15
|
ConfigField,
|
|
@@ -94,7 +94,7 @@ class NamecomHttpClient extends HttpClient {
|
|
|
94
94
|
} catch (error) {
|
|
95
95
|
if (error instanceof ParsingError) throw new ParsingError('namecom: invalid JSON response');
|
|
96
96
|
if (error instanceof ConnectionError)
|
|
97
|
-
throw new ConnectionError('namecom: network request failed');
|
|
97
|
+
throw new ConnectionError('namecom: network request failed', { notSent: error.notSent });
|
|
98
98
|
throw error;
|
|
99
99
|
}
|
|
100
100
|
}
|
|
@@ -108,18 +108,14 @@ class NamecomHttpClient extends HttpClient {
|
|
|
108
108
|
}),
|
|
109
109
|
url
|
|
110
110
|
);
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const reset = response.headers.get('x-ratelimit-reset');
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
? Math.max(0, (date - Date.now()) / 1000)
|
|
120
|
-
: reset != null && Number.isFinite(Number(reset))
|
|
121
|
-
? Math.max(0, Number(reset) - Date.now() / 1000)
|
|
122
|
-
: undefined;
|
|
111
|
+
// The shared client has already read Retry-After. Without a usable one,
|
|
112
|
+
// fall back to X-RateLimit-Reset (Unix seconds); otherwise leave it unset
|
|
113
|
+
// so the configured backoff applies.
|
|
114
|
+
if (error instanceof RateLimitError && error.retryAfter === undefined) {
|
|
115
|
+
const reset = response.headers.get('x-ratelimit-reset')?.trim();
|
|
116
|
+
if (reset && /^\d+(?:\.\d+)?$/.test(reset)) {
|
|
117
|
+
error.retryAfter = Math.max(0, Math.ceil(Number(reset) - Date.now() / 1000));
|
|
118
|
+
}
|
|
123
119
|
}
|
|
124
120
|
return error;
|
|
125
121
|
}
|
|
@@ -127,6 +123,8 @@ class NamecomHttpClient extends HttpClient {
|
|
|
127
123
|
|
|
128
124
|
export class NamecomRegistrar extends BaseRegistrar {
|
|
129
125
|
readonly name = 'namecom';
|
|
126
|
+
// a REST API: GET and HEAD never change state
|
|
127
|
+
static override readonly safeMethods = REST_SAFE_METHODS;
|
|
130
128
|
static readonly displayName = 'Name.com';
|
|
131
129
|
static readonly website = 'name.com';
|
|
132
130
|
static readonly supportsSandbox = true;
|
|
@@ -158,7 +156,11 @@ export class NamecomRegistrar extends BaseRegistrar {
|
|
|
158
156
|
headers: { Authorization: `Basic ${auth}` },
|
|
159
157
|
};
|
|
160
158
|
super(credentials, config, options);
|
|
161
|
-
this.http = new NamecomHttpClient({
|
|
159
|
+
this.http = new NamecomHttpClient({
|
|
160
|
+
...config,
|
|
161
|
+
safeMethods: REST_SAFE_METHODS,
|
|
162
|
+
options: this.options,
|
|
163
|
+
});
|
|
162
164
|
}
|
|
163
165
|
|
|
164
166
|
override async testConnection(opts?: RequestOptions): Promise<ConnectionResult> {
|
|
@@ -444,7 +446,6 @@ export class NamecomRegistrar extends BaseRegistrar {
|
|
|
444
446
|
const old = index >= 0 ? remaining.splice(index, 1)[0] : undefined;
|
|
445
447
|
await this.http.request({
|
|
446
448
|
...opts,
|
|
447
|
-
retries: 0,
|
|
448
449
|
method: old ? 'PUT' : 'POST',
|
|
449
450
|
path: old ? `${path}/${old.id!}` : path,
|
|
450
451
|
body: record,
|
|
@@ -453,7 +454,6 @@ export class NamecomRegistrar extends BaseRegistrar {
|
|
|
453
454
|
for (const old of remaining) {
|
|
454
455
|
await this.http.request({
|
|
455
456
|
...opts,
|
|
456
|
-
retries: 0,
|
|
457
457
|
method: 'DELETE',
|
|
458
458
|
path: `${path}/${old.id!}`,
|
|
459
459
|
});
|
|
@@ -532,7 +532,7 @@ export class NamecomRegistrar extends BaseRegistrar {
|
|
|
532
532
|
}
|
|
533
533
|
private async mutate(req: RequestConfig, opts?: RequestOptions): Promise<OperationResult> {
|
|
534
534
|
try {
|
|
535
|
-
await this.http.request({ ...req, ...opts
|
|
535
|
+
await this.http.request({ ...req, ...opts });
|
|
536
536
|
return { success: true, message: 'Domain updated successfully' };
|
|
537
537
|
} catch (error) {
|
|
538
538
|
return { success: false, message: toRegistrarError(error).message };
|
|
@@ -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
|
|
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)
|
|
@@ -165,6 +194,9 @@ export interface DomainAvailability {
|
|
|
165
194
|
premium?: boolean;
|
|
166
195
|
// registration price in major currency units (e.g. 11.99), when the API reports it
|
|
167
196
|
price?: number;
|
|
197
|
+
// annual renewal price in major currency units, when the API reports one for
|
|
198
|
+
// this specific name (e.g. a premium name's renewal)
|
|
199
|
+
renewalPrice?: number;
|
|
168
200
|
// ISO 4217 currency code, e.g. "USD"
|
|
169
201
|
currency?: string;
|
|
170
202
|
// the registration period the price covers, in years
|
package/src/utils.ts
CHANGED
|
@@ -1,4 +1,10 @@
|
|
|
1
|
-
import type {
|
|
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
|
+
}
|