@aoxborrow/registrar-client 0.7.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 +6 -0
- package/dist/index.cjs +34 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +34 -14
- package/dist/index.js.map +1 -1
- package/package.json +9 -9
- package/src/http.ts +26 -2
- package/src/registrars/dynadot.ts +3 -0
- package/src/registrars/namecheap.ts +11 -5
- package/src/registrars/namecom.ts +8 -12
- package/src/types.ts +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aoxborrow/registrar-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "One consistent, fully-typed TypeScript interface to many domain registrar APIs (GoDaddy, Cloudflare, Namecheap, Spaceship, Dynadot, Porkbun, and more) behind a single pluggable multi-provider client",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "Aaron Oxborrow",
|
|
@@ -89,20 +89,20 @@
|
|
|
89
89
|
},
|
|
90
90
|
"devDependencies": {
|
|
91
91
|
"@eslint/js": "^10.0.1",
|
|
92
|
-
"@types/node": "^26.2
|
|
93
|
-
"@typescript-eslint/eslint-plugin": "^8.
|
|
94
|
-
"@typescript-eslint/parser": "^8.
|
|
92
|
+
"@types/node": "^26.6.2",
|
|
93
|
+
"@typescript-eslint/eslint-plugin": "^8.71.1",
|
|
94
|
+
"@typescript-eslint/parser": "^8.71.1",
|
|
95
95
|
"@vitest/coverage-v8": "^4.1.11",
|
|
96
|
-
"dotenv": "^
|
|
97
|
-
"eslint": "^10.
|
|
96
|
+
"dotenv": "^18.0.1",
|
|
97
|
+
"eslint": "^10.11.0",
|
|
98
98
|
"eslint-config-prettier": "^10.1.8",
|
|
99
99
|
"eslint-import-resolver-typescript": "^4.4.4",
|
|
100
100
|
"eslint-plugin-import-x": "^4.17.1",
|
|
101
|
-
"globals": "^17.
|
|
102
|
-
"prettier": "^3.
|
|
101
|
+
"globals": "^17.12.0",
|
|
102
|
+
"prettier": "^3.9.8",
|
|
103
103
|
"tsup": "^8.5.0",
|
|
104
104
|
"typescript": "^5.9.3",
|
|
105
|
-
"typescript-eslint": "^8.
|
|
105
|
+
"typescript-eslint": "^8.71.1",
|
|
106
106
|
"vitest": "^4.1.11"
|
|
107
107
|
},
|
|
108
108
|
"dependencies": {
|
package/src/http.ts
CHANGED
|
@@ -31,6 +31,9 @@ export interface HttpClientConfig {
|
|
|
31
31
|
// unset for APIs where the method means nothing: Namecheap and NameSilo send
|
|
32
32
|
// writes as GET. Those mark their nested reads with `asRead` instead.
|
|
33
33
|
safeMethods?: readonly string[];
|
|
34
|
+
// A provider-specific delay for HTTP 429 without a usable Retry-After.
|
|
35
|
+
// Otherwise the normal exponential backoff applies.
|
|
36
|
+
rateLimitFallbackSeconds?: number;
|
|
34
37
|
}
|
|
35
38
|
|
|
36
39
|
export const REST_SAFE_METHODS: readonly string[] = ['GET', 'HEAD'];
|
|
@@ -260,8 +263,10 @@ export class HttpClient {
|
|
|
260
263
|
case 404:
|
|
261
264
|
return new NotFoundError(message);
|
|
262
265
|
case 429: {
|
|
263
|
-
const retryAfter =
|
|
264
|
-
|
|
266
|
+
const retryAfter =
|
|
267
|
+
parseRetryAfter(response.headers.get('retry-after')) ??
|
|
268
|
+
this.config.rateLimitFallbackSeconds;
|
|
269
|
+
return new RateLimitError(message, retryAfter);
|
|
265
270
|
}
|
|
266
271
|
default: {
|
|
267
272
|
if (response.status >= 500) {
|
|
@@ -277,6 +282,25 @@ export class HttpClient {
|
|
|
277
282
|
}
|
|
278
283
|
}
|
|
279
284
|
|
|
285
|
+
// RFC 9110 Retry-After is delay-seconds or an HTTP date. Absence must stay
|
|
286
|
+
// undefined: Number(null) is zero and would bypass the configured backoff.
|
|
287
|
+
function parseRetryAfter(value: string | null): number | undefined {
|
|
288
|
+
const text = value?.trim();
|
|
289
|
+
if (!text) return undefined;
|
|
290
|
+
if (/^\d+$/.test(text)) {
|
|
291
|
+
const seconds = Number(text);
|
|
292
|
+
return Number.isSafeInteger(seconds) ? seconds : undefined;
|
|
293
|
+
}
|
|
294
|
+
// Accept the HTTP-date formats from RFC 9110, not Date.parse's permissive
|
|
295
|
+
// numeric/calendar inputs. The obsolete asctime form has implicit UTC.
|
|
296
|
+
const zonedDate =
|
|
297
|
+
/^(?:[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;
|
|
298
|
+
const asctimeDate = /^[a-z]{3} [a-z]{3} (?: \d|\d{2}) \d{2}:\d{2}:\d{2} \d{4}$/i;
|
|
299
|
+
if (!zonedDate.test(text) && !asctimeDate.test(text)) return undefined;
|
|
300
|
+
const date = Date.parse(asctimeDate.test(text) ? `${text} GMT` : text);
|
|
301
|
+
return Number.isFinite(date) ? Math.max(0, Math.ceil((date - Date.now()) / 1000)) : undefined;
|
|
302
|
+
}
|
|
303
|
+
|
|
280
304
|
// A retryable failure that leaves a write's result unknown: the request may
|
|
281
305
|
// have reached the registrar. A rate limit is an explicit rejection, and a
|
|
282
306
|
// connection error known to precede sending never reached it.
|
|
@@ -216,6 +216,9 @@ export class DynadotRegistrar extends BaseRegistrar {
|
|
|
216
216
|
sandbox: 'https://api-sandbox.dynadot.com',
|
|
217
217
|
}),
|
|
218
218
|
headers: { Authorization: `Bearer ${credentials.apiKey}` },
|
|
219
|
+
// HTTP 429 can omit Retry-After. Avoid exhausting all attempts inside
|
|
220
|
+
// Dynadot's regular-tier minute window in that case.
|
|
221
|
+
rateLimitFallbackSeconds: 60,
|
|
219
222
|
},
|
|
220
223
|
options
|
|
221
224
|
);
|
|
@@ -60,6 +60,7 @@ interface NcCheckEl {
|
|
|
60
60
|
'@_Available'?: string;
|
|
61
61
|
'@_IsPremiumName'?: string;
|
|
62
62
|
'@_PremiumRegistrationPrice'?: string;
|
|
63
|
+
'@_PremiumRenewalPrice'?: string;
|
|
63
64
|
}
|
|
64
65
|
|
|
65
66
|
// a contact group in namecheap.domains.getContacts (children are text elements)
|
|
@@ -367,9 +368,10 @@ export class NamecheapRegistrar extends BaseRegistrar {
|
|
|
367
368
|
|
|
368
369
|
/**
|
|
369
370
|
* Availability via domains.check (batched at 50 per request, Namecheap's cap).
|
|
370
|
-
* The check response only carries
|
|
371
|
-
* (`PremiumRegistrationPrice`); regular
|
|
372
|
-
* `getPricing`, so `price`
|
|
371
|
+
* The check response only carries prices for *premium* names
|
|
372
|
+
* (`PremiumRegistrationPrice`, `PremiumRenewalPrice`); regular pricing comes
|
|
373
|
+
* from `getPricing`, so `price` and `renewalPrice` are set only for premium
|
|
374
|
+
* results.
|
|
373
375
|
*/
|
|
374
376
|
override async checkAvailability(
|
|
375
377
|
domainNames: string[],
|
|
@@ -385,12 +387,16 @@ export class NamecheapRegistrar extends BaseRegistrar {
|
|
|
385
387
|
);
|
|
386
388
|
for (const el of ensureArray(cr.DomainCheckResult)) {
|
|
387
389
|
const premium = el['@_IsPremiumName'] === 'true';
|
|
388
|
-
const premiumPrice =
|
|
390
|
+
const premiumPrice = (attr: string | undefined) => {
|
|
391
|
+
const n = premium ? Number(attr) : NaN;
|
|
392
|
+
return Number.isFinite(n) && n > 0 ? n : undefined;
|
|
393
|
+
};
|
|
389
394
|
results.push({
|
|
390
395
|
domainName: el['@_Domain'] ?? '',
|
|
391
396
|
available: el['@_Available'] === 'true',
|
|
392
397
|
premium,
|
|
393
|
-
price:
|
|
398
|
+
price: premiumPrice(el['@_PremiumRegistrationPrice']),
|
|
399
|
+
renewalPrice: premiumPrice(el['@_PremiumRenewalPrice']),
|
|
394
400
|
});
|
|
395
401
|
}
|
|
396
402
|
}
|
|
@@ -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
|
}
|
package/src/types.ts
CHANGED
|
@@ -194,6 +194,9 @@ export interface DomainAvailability {
|
|
|
194
194
|
premium?: boolean;
|
|
195
195
|
// registration price in major currency units (e.g. 11.99), when the API reports it
|
|
196
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;
|
|
197
200
|
// ISO 4217 currency code, e.g. "USD"
|
|
198
201
|
currency?: string;
|
|
199
202
|
// the registration period the price covers, in years
|