@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
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
|
-
|
|
391
|
-
Transport/parse failures after a purchase report an
|
|
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`.
|
|
486
|
-
|
|
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,67 @@ 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
|
+
HTTP 429 honors valid `Retry-After` delay-seconds and HTTP dates. Missing or
|
|
507
|
+
invalid headers use the provider's fallback or the configured backoff: Dynadot
|
|
508
|
+
waits a conservative 60 seconds, and Name.com waits until its
|
|
509
|
+
`X-RateLimit-Reset` time. An explicit zero or past date remains
|
|
510
|
+
an immediate retry instruction from the server.
|
|
511
|
+
|
|
512
|
+
| Failure | Read | Write |
|
|
513
|
+
| -------------------------------------------------------------------------- | ----- | -------------------------------------- |
|
|
514
|
+
| The registrar never saw it: DNS failure, connection refused, TLS handshake | Retry | Retry |
|
|
515
|
+
| `429`, honoring `Retry-After` | Retry | Retry |
|
|
516
|
+
| Outcome unknown: timeout, connection dropped mid-response, `5xx` | Retry | **Not re-sent.** `OutcomeUnknownError` |
|
|
517
|
+
| Any other response | Fail | Fail |
|
|
518
|
+
|
|
519
|
+
Reads and writes are told apart by feature, not by HTTP method (`FEATURE_CALLS`
|
|
520
|
+
lists every one). The method is no guide: Namecheap sends writes as `GET`, and
|
|
521
|
+
Porkbun sends reads as `POST`. `getAuthCode` counts as a write because some
|
|
522
|
+
registrars regenerate the code.
|
|
523
|
+
|
|
524
|
+
A write often reads first: current DNS records, a price, a zone id. Those
|
|
525
|
+
lookups changed nothing, so they are retried like any read and never reported as
|
|
526
|
+
an unknown outcome. REST providers get this from the HTTP method
|
|
527
|
+
(`static safeMethods = REST_SAFE_METHODS` treats `GET` and `HEAD` as reads);
|
|
528
|
+
providers whose method means nothing wrap the lookup's options in `asRead(opts)`.
|
|
529
|
+
|
|
530
|
+
A write whose outcome is unknown may have been applied, and a second renewal
|
|
531
|
+
charges twice. So the library reports it and leaves the decision to you: re-read
|
|
532
|
+
the domain, then retry if nothing changed. Providers that throw raise
|
|
533
|
+
`OutcomeUnknownError`; providers that return an `OperationResult` set
|
|
534
|
+
`outcome: 'unknown'` on it:
|
|
535
|
+
|
|
536
|
+
```ts
|
|
537
|
+
const result = await client.renewDomain('example.com', 1);
|
|
538
|
+
if (!result.success && result.outcome === 'unknown') {
|
|
539
|
+
const domain = await client.getDomain('example.com'); // did it go through?
|
|
540
|
+
}
|
|
541
|
+
```
|
|
542
|
+
|
|
543
|
+
Only positive evidence counts as "never sent": a pre-connection error code from
|
|
544
|
+
Node's `fetch`, or an error a custom transport marked with `markNotSent`.
|
|
545
|
+
Browsers and Workers report network failures opaquely, so there they count as
|
|
546
|
+
unknown.
|
|
547
|
+
|
|
548
|
+
### Custom transport
|
|
549
|
+
|
|
550
|
+
Pass `fetch` to route every request through your own transport, such as a
|
|
551
|
+
proxy:
|
|
552
|
+
|
|
553
|
+
```ts
|
|
554
|
+
const registrar = createRegistrar('namecheap', credentials, { fetch: proxiedFetch });
|
|
555
|
+
```
|
|
556
|
+
|
|
557
|
+
If the transport can fail before the request leaves, for instance a proxy that
|
|
558
|
+
refuses the tunnel, throw `markNotSent(error)` so the failure is retried even
|
|
559
|
+
for writes.
|
|
560
|
+
|
|
496
561
|
## Security
|
|
497
562
|
|
|
498
563
|
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
|
-
|
|
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;
|
|
69
|
+
}
|
|
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;
|
|
63
81
|
}
|
|
64
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
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);
|
|
@@ -503,8 +599,8 @@ var HttpClient = class {
|
|
|
503
599
|
case 404:
|
|
504
600
|
return new NotFoundError(message);
|
|
505
601
|
case 429: {
|
|
506
|
-
const retryAfter =
|
|
507
|
-
return new RateLimitError(message,
|
|
602
|
+
const retryAfter = parseRetryAfter(response.headers.get("retry-after")) ?? this.config.rateLimitFallbackSeconds;
|
|
603
|
+
return new RateLimitError(message, retryAfter);
|
|
508
604
|
}
|
|
509
605
|
default: {
|
|
510
606
|
if (response.status >= 500) {
|
|
@@ -517,6 +613,23 @@ var HttpClient = class {
|
|
|
517
613
|
}
|
|
518
614
|
}
|
|
519
615
|
};
|
|
616
|
+
function parseRetryAfter(value) {
|
|
617
|
+
const text2 = value?.trim();
|
|
618
|
+
if (!text2) return void 0;
|
|
619
|
+
if (/^\d+$/.test(text2)) {
|
|
620
|
+
const seconds = Number(text2);
|
|
621
|
+
return Number.isSafeInteger(seconds) ? seconds : void 0;
|
|
622
|
+
}
|
|
623
|
+
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;
|
|
624
|
+
const asctimeDate = /^[a-z]{3} [a-z]{3} (?: \d|\d{2}) \d{2}:\d{2}:\d{2} \d{4}$/i;
|
|
625
|
+
if (!zonedDate.test(text2) && !asctimeDate.test(text2)) return void 0;
|
|
626
|
+
const date = Date.parse(asctimeDate.test(text2) ? `${text2} GMT` : text2);
|
|
627
|
+
return Number.isFinite(date) ? Math.max(0, Math.ceil((date - Date.now()) / 1e3)) : void 0;
|
|
628
|
+
}
|
|
629
|
+
function isOutcomeUnknown(error) {
|
|
630
|
+
if (error instanceof TimeoutError || error instanceof InvalidResponseError) return true;
|
|
631
|
+
return error instanceof ConnectionError && !error.notSent;
|
|
632
|
+
}
|
|
520
633
|
function isAbortError(error) {
|
|
521
634
|
return typeof error === "object" && error !== null && "name" in error && error.name === "AbortError";
|
|
522
635
|
}
|
|
@@ -613,6 +726,33 @@ var CORE_FEATURE_SET = new Set(CORE_FEATURES);
|
|
|
613
726
|
function isCoreFeature(feature) {
|
|
614
727
|
return CORE_FEATURE_SET.has(feature);
|
|
615
728
|
}
|
|
729
|
+
var FEATURE_CALLS = {
|
|
730
|
+
testConnection: { intent: "read", optsIndex: 0 },
|
|
731
|
+
listDomains: { intent: "read", optsIndex: 0 },
|
|
732
|
+
getDomain: { intent: "read", optsIndex: 1 },
|
|
733
|
+
checkAvailability: { intent: "read", optsIndex: 1 },
|
|
734
|
+
getPricing: { intent: "read", optsIndex: 1 },
|
|
735
|
+
registerDomain: { intent: "write", optsIndex: 2 },
|
|
736
|
+
renewDomain: { intent: "write", optsIndex: 2 },
|
|
737
|
+
setAutoRenew: { intent: "write", optsIndex: 2 },
|
|
738
|
+
transferIn: { intent: "write", optsIndex: 2 },
|
|
739
|
+
updateNameservers: { intent: "write", optsIndex: 2 },
|
|
740
|
+
getNameservers: { intent: "read", optsIndex: 1 },
|
|
741
|
+
lockDomain: { intent: "write", optsIndex: 1 },
|
|
742
|
+
unlockDomain: { intent: "write", optsIndex: 1 },
|
|
743
|
+
setPrivacy: { intent: "write", optsIndex: 2 },
|
|
744
|
+
getContacts: { intent: "read", optsIndex: 1 },
|
|
745
|
+
updateContacts: { intent: "write", optsIndex: 2 },
|
|
746
|
+
getDnsRecords: { intent: "read", optsIndex: 1 },
|
|
747
|
+
setDnsRecords: { intent: "write", optsIndex: 2 },
|
|
748
|
+
getAuthCode: { intent: "write", optsIndex: 1 },
|
|
749
|
+
getDnssec: { intent: "read", optsIndex: 1 },
|
|
750
|
+
disableDnssec: { intent: "write", optsIndex: 1 },
|
|
751
|
+
getEmailForwarding: { intent: "read", optsIndex: 1 },
|
|
752
|
+
setEmailForwarding: { intent: "write", optsIndex: 2 },
|
|
753
|
+
getDomainForwarding: { intent: "read", optsIndex: 1 },
|
|
754
|
+
setDomainForwarding: { intent: "write", optsIndex: 2 }
|
|
755
|
+
};
|
|
616
756
|
|
|
617
757
|
// src/registrar.ts
|
|
618
758
|
function selectBaseUrl(registrar, environment, urls) {
|
|
@@ -638,6 +778,9 @@ var BaseRegistrar = class {
|
|
|
638
778
|
static get features() {
|
|
639
779
|
return [...CORE_FEATURES, ...this.extendedFeatures];
|
|
640
780
|
}
|
|
781
|
+
// HTTP methods this provider's API reserves for reads; see
|
|
782
|
+
// `HttpClientConfig.safeMethods`. REST providers set `REST_SAFE_METHODS`.
|
|
783
|
+
static safeMethods = [];
|
|
641
784
|
// instance mirror of the static `features`
|
|
642
785
|
get features() {
|
|
643
786
|
return this.constructor.features;
|
|
@@ -656,7 +799,39 @@ var BaseRegistrar = class {
|
|
|
656
799
|
this.credentials = credentials;
|
|
657
800
|
this.environment = environment;
|
|
658
801
|
this.options = { ...DEFAULT_OPTIONS, ...clientOptions };
|
|
659
|
-
this.http = new HttpClient({
|
|
802
|
+
this.http = new HttpClient({
|
|
803
|
+
safeMethods: this.constructor.safeMethods,
|
|
804
|
+
...httpConfig,
|
|
805
|
+
options: this.options
|
|
806
|
+
});
|
|
807
|
+
this.threadFeatureCalls();
|
|
808
|
+
}
|
|
809
|
+
// Wrap each feature method so its `RequestOptions` carry a `FeatureCall`:
|
|
810
|
+
// the read/write intent the HTTP layer needs to decide what may be re-sent,
|
|
811
|
+
// and a slot where it reports a write whose outcome is unknown. Providers
|
|
812
|
+
// already forward their options into every request, so nothing else changes.
|
|
813
|
+
//
|
|
814
|
+
// A feature calling another feature (a write re-reading the domain, say)
|
|
815
|
+
// goes through that feature's own wrapper, so the inner read is still a read.
|
|
816
|
+
// Providers fold most failures into an `OperationResult`; the unknown outcome
|
|
817
|
+
// is copied onto that result so it is not lost along with the error.
|
|
818
|
+
threadFeatureCalls() {
|
|
819
|
+
const self = this;
|
|
820
|
+
for (const [feature, { intent, optsIndex }] of Object.entries(FEATURE_CALLS)) {
|
|
821
|
+
const original = self[feature];
|
|
822
|
+
if (typeof original !== "function") continue;
|
|
823
|
+
self[feature] = (...args) => {
|
|
824
|
+
const call = { intent, feature };
|
|
825
|
+
const next = [...args];
|
|
826
|
+
while (next.length <= optsIndex) next.push(void 0);
|
|
827
|
+
next[optsIndex] = { ...next[optsIndex], call };
|
|
828
|
+
const result = original.apply(this, next);
|
|
829
|
+
if (!(result instanceof Promise)) return result;
|
|
830
|
+
return result.then(
|
|
831
|
+
(value) => call.outcomeUnknown && value !== null && typeof value === "object" && value.success === false ? { ...value, outcome: "unknown" } : value
|
|
832
|
+
);
|
|
833
|
+
};
|
|
834
|
+
}
|
|
660
835
|
}
|
|
661
836
|
testConnection(_opts) {
|
|
662
837
|
return Promise.reject(
|
|
@@ -747,6 +922,8 @@ var BaseRegistrar = class {
|
|
|
747
922
|
// src/registrars/cloudflare.ts
|
|
748
923
|
var CloudflareRegistrar = class _CloudflareRegistrar extends BaseRegistrar {
|
|
749
924
|
name = "cloudflare";
|
|
925
|
+
// a REST API: GET and HEAD never change state
|
|
926
|
+
static safeMethods = REST_SAFE_METHODS;
|
|
750
927
|
static displayName = "Cloudflare";
|
|
751
928
|
static website = "cloudflare.com";
|
|
752
929
|
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 +1781,8 @@ var SEARCH_BATCH_SIZE = 100;
|
|
|
1604
1781
|
var SUPPORTED_DNS_TYPES = /* @__PURE__ */ new Set(["A", "AAAA", "CNAME", "MX", "TXT", "NS", "SRV", "CAA"]);
|
|
1605
1782
|
var DynadotRegistrar = class extends BaseRegistrar {
|
|
1606
1783
|
name = "dynadot";
|
|
1784
|
+
// a REST API: GET and HEAD never change state
|
|
1785
|
+
static safeMethods = REST_SAFE_METHODS;
|
|
1607
1786
|
static displayName = "Dynadot";
|
|
1608
1787
|
static website = "dynadot.com";
|
|
1609
1788
|
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.";
|
|
@@ -1634,7 +1813,10 @@ var DynadotRegistrar = class extends BaseRegistrar {
|
|
|
1634
1813
|
production: "https://api.dynadot.com",
|
|
1635
1814
|
sandbox: "https://api-sandbox.dynadot.com"
|
|
1636
1815
|
}),
|
|
1637
|
-
headers: { Authorization: `Bearer ${credentials.apiKey}` }
|
|
1816
|
+
headers: { Authorization: `Bearer ${credentials.apiKey}` },
|
|
1817
|
+
// HTTP 429 can omit Retry-After. Avoid exhausting all attempts inside
|
|
1818
|
+
// Dynadot's regular-tier minute window in that case.
|
|
1819
|
+
rateLimitFallbackSeconds: 60
|
|
1638
1820
|
},
|
|
1639
1821
|
options
|
|
1640
1822
|
);
|
|
@@ -2355,6 +2537,8 @@ var GANDI_TYPE_TO_FORWARD = {
|
|
|
2355
2537
|
};
|
|
2356
2538
|
var GandiRegistrar = class extends BaseRegistrar {
|
|
2357
2539
|
name = "gandi";
|
|
2540
|
+
// a REST API: GET and HEAD never change state
|
|
2541
|
+
static safeMethods = REST_SAFE_METHODS;
|
|
2358
2542
|
static displayName = "Gandi.net";
|
|
2359
2543
|
static website = "gandi.net";
|
|
2360
2544
|
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 +3226,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
3226
|
var PRICE_MICRO_UNITS = 1e6;
|
|
3043
3227
|
var GoDaddyRegistrar = class _GoDaddyRegistrar extends BaseRegistrar {
|
|
3044
3228
|
name = "godaddy";
|
|
3229
|
+
// a REST API: GET and HEAD never change state
|
|
3230
|
+
static safeMethods = REST_SAFE_METHODS;
|
|
3045
3231
|
static displayName = "GoDaddy";
|
|
3046
3232
|
static website = "godaddy.com";
|
|
3047
3233
|
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 +4108,8 @@ function dnsFieldsDiffer(a, b) {
|
|
|
3922
4108
|
var TOKEN_URL = "https://api.namebright.com/auth/token";
|
|
3923
4109
|
var NameBrightRegistrar = class extends BaseRegistrar {
|
|
3924
4110
|
name = "namebright";
|
|
4111
|
+
// a REST API: GET and HEAD never change state
|
|
4112
|
+
static safeMethods = REST_SAFE_METHODS;
|
|
3925
4113
|
static displayName = "NameBright";
|
|
3926
4114
|
static website = "namebright.com";
|
|
3927
4115
|
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 +4148,8 @@ var NameBrightRegistrar = class extends BaseRegistrar {
|
|
|
3960
4148
|
client_id: this.credentials.clientId,
|
|
3961
4149
|
client_secret: this.credentials.clientSecret
|
|
3962
4150
|
},
|
|
3963
|
-
|
|
4151
|
+
// issuing a token changes nothing on the account, whatever it precedes
|
|
4152
|
+
...asRead(opts)
|
|
3964
4153
|
});
|
|
3965
4154
|
if (!res.access_token) {
|
|
3966
4155
|
throw new AuthenticationError("NameBright: token endpoint returned no access_token");
|
|
@@ -4568,9 +4757,10 @@ var NamecheapRegistrar = class extends BaseRegistrar {
|
|
|
4568
4757
|
}
|
|
4569
4758
|
/**
|
|
4570
4759
|
* Availability via domains.check (batched at 50 per request, Namecheap's cap).
|
|
4571
|
-
* The check response only carries
|
|
4572
|
-
* (`PremiumRegistrationPrice`); regular
|
|
4573
|
-
* `getPricing`, so `price`
|
|
4760
|
+
* The check response only carries prices for *premium* names
|
|
4761
|
+
* (`PremiumRegistrationPrice`, `PremiumRenewalPrice`); regular pricing comes
|
|
4762
|
+
* from `getPricing`, so `price` and `renewalPrice` are set only for premium
|
|
4763
|
+
* results.
|
|
4574
4764
|
*/
|
|
4575
4765
|
async checkAvailability(domainNames, opts) {
|
|
4576
4766
|
const results = [];
|
|
@@ -4583,12 +4773,16 @@ var NamecheapRegistrar = class extends BaseRegistrar {
|
|
|
4583
4773
|
);
|
|
4584
4774
|
for (const el of ensureArray(cr.DomainCheckResult)) {
|
|
4585
4775
|
const premium = el["@_IsPremiumName"] === "true";
|
|
4586
|
-
const premiumPrice =
|
|
4776
|
+
const premiumPrice = (attr) => {
|
|
4777
|
+
const n = premium ? Number(attr) : NaN;
|
|
4778
|
+
return Number.isFinite(n) && n > 0 ? n : void 0;
|
|
4779
|
+
};
|
|
4587
4780
|
results.push({
|
|
4588
4781
|
domainName: el["@_Domain"] ?? "",
|
|
4589
4782
|
available: el["@_Available"] === "true",
|
|
4590
4783
|
premium,
|
|
4591
|
-
price:
|
|
4784
|
+
price: premiumPrice(el["@_PremiumRegistrationPrice"]),
|
|
4785
|
+
renewalPrice: premiumPrice(el["@_PremiumRenewalPrice"])
|
|
4592
4786
|
});
|
|
4593
4787
|
}
|
|
4594
4788
|
}
|
|
@@ -4739,7 +4933,11 @@ var NamecheapRegistrar = class extends BaseRegistrar {
|
|
|
4739
4933
|
* has no WhoisGuard allotted (e.g. TLDs that don't offer it).
|
|
4740
4934
|
*/
|
|
4741
4935
|
async setPrivacy(domainName, enabled, opts) {
|
|
4742
|
-
const cr = await this.command(
|
|
4936
|
+
const cr = await this.command(
|
|
4937
|
+
"namecheap.domains.getInfo",
|
|
4938
|
+
{ DomainName: domainName },
|
|
4939
|
+
asRead(opts)
|
|
4940
|
+
);
|
|
4743
4941
|
const id = cr.DomainGetInfoResult?.Whoisguard?.ID;
|
|
4744
4942
|
if (id == null || String(id) === "0") {
|
|
4745
4943
|
throw new Error(`${this.name}: ${domainName} has no WhoisGuard subscription to toggle`);
|
|
@@ -4979,7 +5177,7 @@ var NamecomHttpClient = class extends HttpClient {
|
|
|
4979
5177
|
} catch (error) {
|
|
4980
5178
|
if (error instanceof ParsingError) throw new ParsingError("namecom: invalid JSON response");
|
|
4981
5179
|
if (error instanceof ConnectionError)
|
|
4982
|
-
throw new ConnectionError("namecom: network request failed");
|
|
5180
|
+
throw new ConnectionError("namecom: network request failed", { notSent: error.notSent });
|
|
4983
5181
|
throw error;
|
|
4984
5182
|
}
|
|
4985
5183
|
}
|
|
@@ -4992,18 +5190,19 @@ var NamecomHttpClient = class extends HttpClient {
|
|
|
4992
5190
|
}),
|
|
4993
5191
|
url
|
|
4994
5192
|
);
|
|
4995
|
-
if (error instanceof RateLimitError) {
|
|
4996
|
-
const
|
|
4997
|
-
|
|
4998
|
-
|
|
4999
|
-
|
|
5000
|
-
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;
|
|
5193
|
+
if (error instanceof RateLimitError && error.retryAfter === void 0) {
|
|
5194
|
+
const reset = response.headers.get("x-ratelimit-reset")?.trim();
|
|
5195
|
+
if (reset && /^\d+(?:\.\d+)?$/.test(reset)) {
|
|
5196
|
+
error.retryAfter = Math.max(0, Math.ceil(Number(reset) - Date.now() / 1e3));
|
|
5197
|
+
}
|
|
5001
5198
|
}
|
|
5002
5199
|
return error;
|
|
5003
5200
|
}
|
|
5004
5201
|
};
|
|
5005
5202
|
var NamecomRegistrar = class extends BaseRegistrar {
|
|
5006
5203
|
name = "namecom";
|
|
5204
|
+
// a REST API: GET and HEAD never change state
|
|
5205
|
+
static safeMethods = REST_SAFE_METHODS;
|
|
5007
5206
|
static displayName = "Name.com";
|
|
5008
5207
|
static website = "name.com";
|
|
5009
5208
|
static supportsSandbox = true;
|
|
@@ -5032,7 +5231,11 @@ var NamecomRegistrar = class extends BaseRegistrar {
|
|
|
5032
5231
|
headers: { Authorization: `Basic ${auth}` }
|
|
5033
5232
|
};
|
|
5034
5233
|
super(credentials, config, options);
|
|
5035
|
-
this.http = new NamecomHttpClient({
|
|
5234
|
+
this.http = new NamecomHttpClient({
|
|
5235
|
+
...config,
|
|
5236
|
+
safeMethods: REST_SAFE_METHODS,
|
|
5237
|
+
options: this.options
|
|
5238
|
+
});
|
|
5036
5239
|
}
|
|
5037
5240
|
async testConnection(opts) {
|
|
5038
5241
|
try {
|
|
@@ -5260,7 +5463,6 @@ var NamecomRegistrar = class extends BaseRegistrar {
|
|
|
5260
5463
|
const old = index >= 0 ? remaining.splice(index, 1)[0] : void 0;
|
|
5261
5464
|
await this.http.request({
|
|
5262
5465
|
...opts,
|
|
5263
|
-
retries: 0,
|
|
5264
5466
|
method: old ? "PUT" : "POST",
|
|
5265
5467
|
path: old ? `${path}/${old.id}` : path,
|
|
5266
5468
|
body: record
|
|
@@ -5269,7 +5471,6 @@ var NamecomRegistrar = class extends BaseRegistrar {
|
|
|
5269
5471
|
for (const old of remaining) {
|
|
5270
5472
|
await this.http.request({
|
|
5271
5473
|
...opts,
|
|
5272
|
-
retries: 0,
|
|
5273
5474
|
method: "DELETE",
|
|
5274
5475
|
path: `${path}/${old.id}`
|
|
5275
5476
|
});
|
|
@@ -5333,7 +5534,7 @@ var NamecomRegistrar = class extends BaseRegistrar {
|
|
|
5333
5534
|
}
|
|
5334
5535
|
async mutate(req, opts) {
|
|
5335
5536
|
try {
|
|
5336
|
-
await this.http.request({ ...req, ...opts
|
|
5537
|
+
await this.http.request({ ...req, ...opts });
|
|
5337
5538
|
return { success: true, message: "Domain updated successfully" };
|
|
5338
5539
|
} catch (error) {
|
|
5339
5540
|
return { success: false, message: toRegistrarError(error).message };
|
|
@@ -5746,7 +5947,7 @@ var NameSiloRegistrar = class extends BaseRegistrar {
|
|
|
5746
5947
|
*/
|
|
5747
5948
|
async disableDnssec(domainName, opts) {
|
|
5748
5949
|
try {
|
|
5749
|
-
const res = await this.call("dnsSecListRecords", { domain: domainName }, opts);
|
|
5950
|
+
const res = await this.call("dnsSecListRecords", { domain: domainName }, asRead(opts));
|
|
5750
5951
|
if (!replyOk(res)) return statusResult2(res);
|
|
5751
5952
|
for (const d of ensureArray(res.reply?.ds_record)) {
|
|
5752
5953
|
const del = await this.call(
|
|
@@ -5819,7 +6020,7 @@ var NameSiloRegistrar = class extends BaseRegistrar {
|
|
|
5819
6020
|
desired.set(f.alias, list);
|
|
5820
6021
|
}
|
|
5821
6022
|
const currentAliases = new Set(
|
|
5822
|
-
(await this.listEmailForwards(domainName, opts)).map((r) => r.alias).filter(Boolean)
|
|
6023
|
+
(await this.listEmailForwards(domainName, asRead(opts))).map((r) => r.alias).filter(Boolean)
|
|
5823
6024
|
);
|
|
5824
6025
|
for (const alias of currentAliases) {
|
|
5825
6026
|
if (!desired.has(alias)) {
|
|
@@ -5910,7 +6111,7 @@ var NameSiloRegistrar = class extends BaseRegistrar {
|
|
|
5910
6111
|
* delegation nameservers aren't part of this list (see `updateNameservers`).
|
|
5911
6112
|
*/
|
|
5912
6113
|
async setDnsRecords(domainName, records, opts) {
|
|
5913
|
-
const listRes = await this.call("dnsListRecords", { domain: domainName }, opts);
|
|
6114
|
+
const listRes = await this.call("dnsListRecords", { domain: domainName }, asRead(opts));
|
|
5914
6115
|
if (!replyOk(listRes)) return { success: false, message: replyDetail(listRes) };
|
|
5915
6116
|
const existing = ensureArray(listRes.reply?.resource_record);
|
|
5916
6117
|
const existingByKey = /* @__PURE__ */ new Map();
|
|
@@ -6437,7 +6638,7 @@ var PorkbunRegistrar = class extends BaseRegistrar {
|
|
|
6437
6638
|
* renewals aren't supported via the API.
|
|
6438
6639
|
*/
|
|
6439
6640
|
async renewDomain(domainName, _years = 1, opts) {
|
|
6440
|
-
const pricing = await this.tldPricing(extractTld(domainName), opts);
|
|
6641
|
+
const pricing = await this.tldPricing(extractTld(domainName), asRead(opts));
|
|
6441
6642
|
if (pricing.renewal == null) {
|
|
6442
6643
|
throw new Error(`${this.name}: could not determine the renewal price for ${domainName}`);
|
|
6443
6644
|
}
|
|
@@ -6468,7 +6669,7 @@ var PorkbunRegistrar = class extends BaseRegistrar {
|
|
|
6468
6669
|
* aren't supported via the API.
|
|
6469
6670
|
*/
|
|
6470
6671
|
async transferIn(domainName, input, opts) {
|
|
6471
|
-
const pricing = await this.tldPricing(extractTld(domainName), opts);
|
|
6672
|
+
const pricing = await this.tldPricing(extractTld(domainName), asRead(opts));
|
|
6472
6673
|
if (pricing.transfer == null) {
|
|
6473
6674
|
throw new Error(`${this.name}: could not determine the transfer price for ${domainName}`);
|
|
6474
6675
|
}
|
|
@@ -6761,6 +6962,8 @@ function toDnsRecord3(r, domainName) {
|
|
|
6761
6962
|
var WRITABLE_DNS_TYPES = /* @__PURE__ */ new Set(["A", "AAAA", "CNAME", "NS", "TXT", "MX"]);
|
|
6762
6963
|
var SpaceshipRegistrar = class extends BaseRegistrar {
|
|
6763
6964
|
name = "spaceship";
|
|
6965
|
+
// a REST API: GET and HEAD never change state
|
|
6966
|
+
static safeMethods = REST_SAFE_METHODS;
|
|
6764
6967
|
static displayName = "Spaceship";
|
|
6765
6968
|
static website = "spaceship.com";
|
|
6766
6969
|
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 +7497,7 @@ exports.ConsentRequiredError = ConsentRequiredError;
|
|
|
7294
7497
|
exports.DEFAULT_OPTIONS = DEFAULT_OPTIONS;
|
|
7295
7498
|
exports.DynadotRegistrar = DynadotRegistrar;
|
|
7296
7499
|
exports.EXTENDED_FEATURES = EXTENDED_FEATURES;
|
|
7500
|
+
exports.FEATURE_CALLS = FEATURE_CALLS;
|
|
7297
7501
|
exports.Feature = Feature;
|
|
7298
7502
|
exports.GandiRegistrar = GandiRegistrar;
|
|
7299
7503
|
exports.GoDaddyRegistrar = GoDaddyRegistrar;
|
|
@@ -7306,8 +7510,10 @@ exports.NamecheapRegistrar = NamecheapRegistrar;
|
|
|
7306
7510
|
exports.NamecomRegistrar = NamecomRegistrar;
|
|
7307
7511
|
exports.NotFoundError = NotFoundError;
|
|
7308
7512
|
exports.NotImplementedError = NotImplementedError;
|
|
7513
|
+
exports.OutcomeUnknownError = OutcomeUnknownError;
|
|
7309
7514
|
exports.ParsingError = ParsingError;
|
|
7310
7515
|
exports.PorkbunRegistrar = PorkbunRegistrar;
|
|
7516
|
+
exports.REST_SAFE_METHODS = REST_SAFE_METHODS;
|
|
7311
7517
|
exports.RateLimitError = RateLimitError;
|
|
7312
7518
|
exports.RegistrarClient = RegistrarClient;
|
|
7313
7519
|
exports.RegistrarError = RegistrarError;
|
|
@@ -7315,6 +7521,7 @@ exports.RetryableRegistrarError = RetryableRegistrarError;
|
|
|
7315
7521
|
exports.SpaceshipRegistrar = SpaceshipRegistrar;
|
|
7316
7522
|
exports.TimeoutError = TimeoutError;
|
|
7317
7523
|
exports.USER_AGENT = USER_AGENT;
|
|
7524
|
+
exports.asRead = asRead;
|
|
7318
7525
|
exports.backoffDelay = backoffDelay;
|
|
7319
7526
|
exports.createDomain = createDomain;
|
|
7320
7527
|
exports.createRegistrar = createRegistrar;
|
|
@@ -7322,6 +7529,7 @@ exports.ensureArray = ensureArray;
|
|
|
7322
7529
|
exports.filterDomains = filterDomains;
|
|
7323
7530
|
exports.isCoreFeature = isCoreFeature;
|
|
7324
7531
|
exports.listPortfolio = listPortfolio;
|
|
7532
|
+
exports.markNotSent = markNotSent;
|
|
7325
7533
|
exports.normalizeDomain = normalizeDomain;
|
|
7326
7534
|
exports.normalizeNameservers = normalizeNameservers;
|
|
7327
7535
|
exports.parseDate = parseDate;
|
|
@@ -7332,5 +7540,6 @@ exports.selectBaseUrl = selectBaseUrl;
|
|
|
7332
7540
|
exports.settableForwards = settableForwards;
|
|
7333
7541
|
exports.sleep = sleep;
|
|
7334
7542
|
exports.toRegistrarError = toRegistrarError;
|
|
7543
|
+
exports.wasNotSent = wasNotSent;
|
|
7335
7544
|
//# sourceMappingURL=index.cjs.map
|
|
7336
7545
|
//# sourceMappingURL=index.cjs.map
|