@aoxborrow/registrar-client 0.1.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/LICENSE +21 -0
- package/README.md +415 -0
- package/dist/index.cjs +6798 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +768 -0
- package/dist/index.d.ts +768 -0
- package/dist/index.js +6747 -0
- package/dist/index.js.map +1 -0
- package/package.json +110 -0
- package/src/client.ts +200 -0
- package/src/constants.ts +11 -0
- package/src/errors.ts +192 -0
- package/src/features.ts +104 -0
- package/src/http.ts +230 -0
- package/src/index.ts +39 -0
- package/src/portfolio.ts +65 -0
- package/src/registrar.ts +240 -0
- package/src/registrars/cloudflare.ts +1205 -0
- package/src/registrars/dynadot.ts +1097 -0
- package/src/registrars/gandi.ts +970 -0
- package/src/registrars/godaddy.ts +1342 -0
- package/src/registrars/index.ts +41 -0
- package/src/registrars/namebright.ts +775 -0
- package/src/registrars/namecheap.ts +870 -0
- package/src/registrars/namesilo.ts +997 -0
- package/src/registrars/porkbun.ts +930 -0
- package/src/registrars/spaceship.ts +736 -0
- package/src/types.ts +446 -0
- package/src/utils.ts +135 -0
- package/src/xml.ts +39 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,446 @@
|
|
|
1
|
+
// Shared, provider-agnostic types for the registrar client.
|
|
2
|
+
|
|
3
|
+
import type { RegistrarFeature } from './features';
|
|
4
|
+
|
|
5
|
+
// client-level request options
|
|
6
|
+
export interface RegistrarClientOptions {
|
|
7
|
+
// request timeout in milliseconds
|
|
8
|
+
timeout: number;
|
|
9
|
+
// number of retry attempts for retryable errors
|
|
10
|
+
retries: number;
|
|
11
|
+
// base delay for exponential backoff, in milliseconds
|
|
12
|
+
backoff: number;
|
|
13
|
+
// optional AbortSignal to cancel in-flight requests
|
|
14
|
+
signal?: AbortSignal;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
// per-request options that can override client-level options
|
|
18
|
+
export type RequestOptions = Partial<RegistrarClientOptions>;
|
|
19
|
+
|
|
20
|
+
// options for `listDomains`. Extends `RequestOptions` so timeout/retries/signal
|
|
21
|
+
// still flow through a single argument. `listDomains` always returns the full
|
|
22
|
+
// account, paginating internally at each provider's maximum page size.
|
|
23
|
+
export interface ListDomainsOptions extends RequestOptions {
|
|
24
|
+
// case-insensitive substring to match against the domain name. Applied
|
|
25
|
+
// server-side where the API supports it (Namecheap `SearchTerm`, Gandi
|
|
26
|
+
// `fqdn`) and client-side otherwise.
|
|
27
|
+
search?: string;
|
|
28
|
+
// Fetch full per-domain detail after listing. Several providers' list
|
|
29
|
+
// endpoints return only a summary and omit fields like `nameservers`,
|
|
30
|
+
// `privacy`, or the transfer `locked` flag, which then read as their defaults
|
|
31
|
+
// (`[]` / `false`). With `detailed: true`, the client follows up with a
|
|
32
|
+
// `getDomain` call per domain (bounded concurrency) and merges the detail,
|
|
33
|
+
// so those fields are accurate. Costs one extra request per domain, so it is
|
|
34
|
+
// opt-in — leave it off for a fast summary listing. Honored by the
|
|
35
|
+
// `RegistrarClient` facade (and `listPortfolio` over facade sources).
|
|
36
|
+
detailed?: boolean;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// which API environment a provider targets. "sandbox" covers registrar test
|
|
40
|
+
// environments (GoDaddy calls theirs "OTE") so integration tests never touch
|
|
41
|
+
// real domains. Requesting "sandbox" on a provider without one throws.
|
|
42
|
+
export type RegistrarEnvironment = 'production' | 'sandbox';
|
|
43
|
+
|
|
44
|
+
// options passed when constructing a registrar provider
|
|
45
|
+
export interface RegistrarOptions extends Partial<RegistrarClientOptions> {
|
|
46
|
+
environment?: RegistrarEnvironment;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// result of a connection/credential test
|
|
50
|
+
export interface ConnectionResult {
|
|
51
|
+
success: boolean;
|
|
52
|
+
message: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// result of a mutating operation (renew, nameserver update, lock/unlock)
|
|
56
|
+
export interface OperationResult {
|
|
57
|
+
success: boolean;
|
|
58
|
+
message: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// a configuration field a registrar needs (used to build credential UIs generically)
|
|
62
|
+
export interface ConfigField {
|
|
63
|
+
// property name on the credentials object
|
|
64
|
+
name: string;
|
|
65
|
+
// human-readable label
|
|
66
|
+
label: string;
|
|
67
|
+
// input type
|
|
68
|
+
type: 'text' | 'password' | 'select';
|
|
69
|
+
// whether the field is required
|
|
70
|
+
required: boolean;
|
|
71
|
+
// allowed values for `select` fields
|
|
72
|
+
options?: string[];
|
|
73
|
+
// default value
|
|
74
|
+
default?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// normalized, cross-registrar view of a domain
|
|
78
|
+
export interface Domain {
|
|
79
|
+
domainName: string;
|
|
80
|
+
// id of the registrar that owns this record, e.g. "cloudflare"
|
|
81
|
+
registrar: string;
|
|
82
|
+
// registry/registrar status, always lowercased
|
|
83
|
+
status: string;
|
|
84
|
+
createdDate: Date | null;
|
|
85
|
+
expirationDate: Date | null;
|
|
86
|
+
renewalDate: Date | null;
|
|
87
|
+
autoRenew: boolean;
|
|
88
|
+
locked: boolean;
|
|
89
|
+
privacy: boolean;
|
|
90
|
+
nameservers: string[];
|
|
91
|
+
// when this record was fetched
|
|
92
|
+
syncedAt: Date;
|
|
93
|
+
// whether the domain has been removed at the registrar
|
|
94
|
+
deleted: boolean;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// loose input accepted by `createDomain`; providers pass raw-ish API values
|
|
98
|
+
export interface DomainInput {
|
|
99
|
+
domainName?: string;
|
|
100
|
+
registrar?: string;
|
|
101
|
+
status?: string;
|
|
102
|
+
createdDate?: Date | string | number | null;
|
|
103
|
+
expirationDate?: Date | string | number | null;
|
|
104
|
+
renewalDate?: Date | string | number | null;
|
|
105
|
+
autoRenew?: boolean;
|
|
106
|
+
locked?: boolean;
|
|
107
|
+
privacy?: boolean;
|
|
108
|
+
// accepts string[], objects with a `hosts` array, or objects with ServerName
|
|
109
|
+
nameservers?: unknown;
|
|
110
|
+
syncedAt?: Date;
|
|
111
|
+
deleted?: boolean;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// A normalized registrant/admin/tech/billing contact, mapped to and from each
|
|
115
|
+
// registrar's own contact shape.
|
|
116
|
+
export interface Contact {
|
|
117
|
+
firstName: string;
|
|
118
|
+
lastName: string;
|
|
119
|
+
organization?: string;
|
|
120
|
+
email: string;
|
|
121
|
+
// international phone format, e.g. "+1.4805551234"
|
|
122
|
+
phone: string;
|
|
123
|
+
fax?: string;
|
|
124
|
+
address1: string;
|
|
125
|
+
address2?: string;
|
|
126
|
+
city: string;
|
|
127
|
+
// state / province / region; may be empty where the country has none
|
|
128
|
+
state?: string;
|
|
129
|
+
postalCode: string;
|
|
130
|
+
// ISO 3166-1 alpha-2 country code, e.g. "US"
|
|
131
|
+
country: string;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// The set of contacts on a domain. `registrant` is the legal owner; most
|
|
135
|
+
// registrars also track admin, tech, and billing roles (often the same person).
|
|
136
|
+
export interface ContactSet {
|
|
137
|
+
registrant?: Contact;
|
|
138
|
+
admin?: Contact;
|
|
139
|
+
tech?: Contact;
|
|
140
|
+
billing?: Contact;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// A single DNS record in provider-agnostic form.
|
|
144
|
+
export interface DnsRecord {
|
|
145
|
+
// record type, uppercased: "A", "AAAA", "CNAME", "MX", "TXT", "NS", "SRV", "CAA", …
|
|
146
|
+
type: string;
|
|
147
|
+
// host/subdomain relative to the zone apex; "@" denotes the apex
|
|
148
|
+
name: string;
|
|
149
|
+
// record data / value (target host, IP address, text, etc.)
|
|
150
|
+
value: string;
|
|
151
|
+
// time-to-live in seconds
|
|
152
|
+
ttl?: number;
|
|
153
|
+
// priority, for MX and SRV records
|
|
154
|
+
priority?: number;
|
|
155
|
+
// SRV-only fields
|
|
156
|
+
weight?: number;
|
|
157
|
+
port?: number;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Result of an availability check for a single domain.
|
|
161
|
+
export interface DomainAvailability {
|
|
162
|
+
domainName: string;
|
|
163
|
+
available: boolean;
|
|
164
|
+
// whether the name is a premium/aftermarket registration
|
|
165
|
+
premium?: boolean;
|
|
166
|
+
// registration price in major currency units (e.g. 11.99), when the API reports it
|
|
167
|
+
price?: number;
|
|
168
|
+
// ISO 4217 currency code, e.g. "USD"
|
|
169
|
+
currency?: string;
|
|
170
|
+
// the registration period the price covers, in years
|
|
171
|
+
period?: number;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Pricing for a TLD (or a specific domain), in major currency units. Fields are
|
|
175
|
+
// optional because not every registrar exposes all three price points.
|
|
176
|
+
export interface TldPricing {
|
|
177
|
+
// the TLD the prices apply to, without a leading dot, e.g. "com"
|
|
178
|
+
tld: string;
|
|
179
|
+
// ISO 4217 currency code, e.g. "USD"
|
|
180
|
+
currency: string;
|
|
181
|
+
registration?: number;
|
|
182
|
+
renewal?: number;
|
|
183
|
+
transfer?: number;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Consent to a registrar's registration agreements, supplied per registration.
|
|
187
|
+
// Registering a domain forms a legal contract with the registry, so providers
|
|
188
|
+
// that require it reject `registerDomain` without this. The provider fetches the
|
|
189
|
+
// specific per-TLD agreement documents itself; the caller only affirms who is
|
|
190
|
+
// consenting (and optionally when).
|
|
191
|
+
export interface RegistrationConsent {
|
|
192
|
+
// identifier of the consenting party. Registrars that record an "agreed by"
|
|
193
|
+
// value expect the consenting user's IP address here (e.g. GoDaddy).
|
|
194
|
+
agreedBy: string;
|
|
195
|
+
// ISO 8601 timestamp of when consent was given; defaults to the current time.
|
|
196
|
+
agreedAt?: string;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Input for transferring a domain in. `authCode` (the EPP/auth code from the
|
|
200
|
+
// losing registrar) is always required; the rest mirror registration and are
|
|
201
|
+
// used by the registrars that need them (GoDaddy/Spaceship want contacts +
|
|
202
|
+
// consent, Namecheap/Dynadot rely on account defaults and just need the code).
|
|
203
|
+
export interface TransferDomainInput {
|
|
204
|
+
// the EPP / authorization code obtained from the current registrar
|
|
205
|
+
authCode: string;
|
|
206
|
+
// registration length to add as part of the transfer, in years
|
|
207
|
+
years?: number;
|
|
208
|
+
// contacts for the transferred domain, where the registrar requires them
|
|
209
|
+
contacts?: ContactSet;
|
|
210
|
+
// consent to the registrar's transfer agreements, where required (e.g. GoDaddy)
|
|
211
|
+
consent?: RegistrationConsent;
|
|
212
|
+
// enable WHOIS privacy on transfer, where supported
|
|
213
|
+
privacy?: boolean;
|
|
214
|
+
// enable auto-renew on transfer
|
|
215
|
+
autoRenew?: boolean;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// Input for registering a new domain.
|
|
219
|
+
export interface RegisterDomainInput {
|
|
220
|
+
// registration length in years (defaults to 1)
|
|
221
|
+
years?: number;
|
|
222
|
+
// contacts for the registration; every registrar requires at least a registrant
|
|
223
|
+
contacts: ContactSet;
|
|
224
|
+
// initial nameservers; omitting them uses the registrar's defaults
|
|
225
|
+
nameservers?: string[];
|
|
226
|
+
// enable WHOIS privacy at registration, where supported
|
|
227
|
+
privacy?: boolean;
|
|
228
|
+
// enable auto-renew at registration
|
|
229
|
+
autoRenew?: boolean;
|
|
230
|
+
// consent to the registrar's registration agreements; required by providers
|
|
231
|
+
// that gate registration behind an explicit legal agreement (e.g. GoDaddy).
|
|
232
|
+
// Omitting it where required throws `ConsentRequiredError`.
|
|
233
|
+
consent?: RegistrationConsent;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// credentials for a registrar, as a flat string map keyed by `ConfigField.name`
|
|
237
|
+
export type RegistrarCredentials = Record<string, string>;
|
|
238
|
+
|
|
239
|
+
// --- extended-feature payloads ---
|
|
240
|
+
|
|
241
|
+
// An alias-style email forward: mail sent to `alias`@domain is redirected to
|
|
242
|
+
// `forwardTo`. This is redirect-only forwarding — no real mailbox is provisioned.
|
|
243
|
+
export interface EmailForward {
|
|
244
|
+
// the local part of the source address at the domain, e.g. "hello" for
|
|
245
|
+
// hello@example.com. Where the registrar supports it, "@" or "*" denotes a
|
|
246
|
+
// catch-all.
|
|
247
|
+
alias: string;
|
|
248
|
+
// the destination address mail is forwarded to
|
|
249
|
+
forwardTo: string;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// The redirect style for a DomainForward:
|
|
253
|
+
// - "temporary" temporary (302) redirect; the destination URL shows in the bar
|
|
254
|
+
// - "permanent" permanent (301) redirect; the destination URL shows in the bar
|
|
255
|
+
// - "masked" masked/framed/"cloaked" redirect: an iframe keeps the source
|
|
256
|
+
// URL in the address bar. READ-ONLY — `getDomainForwarding` reports
|
|
257
|
+
// it for forwards a provider already has, but `setDomainForwarding`
|
|
258
|
+
// rejects it. Masking breaks HTTPS, SEO, and modern browser
|
|
259
|
+
// protections, and several registrars (e.g. Cloudflare) can't do
|
|
260
|
+
// it at all, so the library never creates one.
|
|
261
|
+
export type DomainForwardType = 'temporary' | 'permanent' | 'masked';
|
|
262
|
+
|
|
263
|
+
// A URL/domain forwarding rule: requests to `host` at the domain are redirected
|
|
264
|
+
// to `url`.
|
|
265
|
+
export interface DomainForward {
|
|
266
|
+
// the source host at the domain, relative to the apex; "@" denotes the apex,
|
|
267
|
+
// "www" the www subdomain, etc.
|
|
268
|
+
host: string;
|
|
269
|
+
// the destination URL requests are forwarded to
|
|
270
|
+
url: string;
|
|
271
|
+
// the redirect style
|
|
272
|
+
type: DomainForwardType;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// A DNSSEC delegation-signer (DS) record published at the parent zone. Fields
|
|
276
|
+
// mirror the DS RR (RFC 4034). Registrars that expose DNSSEC only as DNSKEY data
|
|
277
|
+
// may not populate every field; `digest` is the hex delegation digest.
|
|
278
|
+
export interface DsRecord {
|
|
279
|
+
// the key tag identifying the DNSKEY this DS refers to
|
|
280
|
+
keyTag: number;
|
|
281
|
+
// DNSSEC algorithm number (e.g. 8 = RSA/SHA-256, 13 = ECDSA P-256)
|
|
282
|
+
algorithm: number;
|
|
283
|
+
// digest type (1 = SHA-1, 2 = SHA-256, 4 = SHA-384)
|
|
284
|
+
digestType: number;
|
|
285
|
+
// the delegation digest, hex-encoded
|
|
286
|
+
digest: string;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
// DNSSEC status for a domain. `enabled` is the headline signal (is DNSSEC turned
|
|
290
|
+
// on at the registrar); `dsRecords` carries the published DS records when the
|
|
291
|
+
// registrar exposes them (may be empty even when enabled).
|
|
292
|
+
export interface DnssecStatus {
|
|
293
|
+
enabled: boolean;
|
|
294
|
+
dsRecords: DsRecord[];
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// The core interface every registrar implementation fulfils. `BaseRegistrar`
|
|
298
|
+
// (src/registrar.ts) provides the shared plumbing and `NotImplementedError`
|
|
299
|
+
// defaults; concrete providers under src/registrars/ extend it.
|
|
300
|
+
export interface Registrar {
|
|
301
|
+
// a short identifier for the provider, e.g. "cloudflare"
|
|
302
|
+
readonly name: string;
|
|
303
|
+
|
|
304
|
+
// the API environment this instance targets
|
|
305
|
+
readonly environment: RegistrarEnvironment;
|
|
306
|
+
|
|
307
|
+
// the capabilities this provider supports (core contract + its extended features)
|
|
308
|
+
readonly features: readonly RegistrarFeature[];
|
|
309
|
+
|
|
310
|
+
// whether this provider supports a given capability
|
|
311
|
+
supports(feature: RegistrarFeature): boolean;
|
|
312
|
+
|
|
313
|
+
// verify the configured credentials work
|
|
314
|
+
testConnection(opts?: RequestOptions): Promise<ConnectionResult>;
|
|
315
|
+
|
|
316
|
+
// list domains in the account (capped and optionally filtered via `opts`)
|
|
317
|
+
listDomains(opts?: ListDomainsOptions): Promise<Domain[]>;
|
|
318
|
+
|
|
319
|
+
// fetch a single domain's details
|
|
320
|
+
getDomain(domainName: string, opts?: RequestOptions): Promise<Domain>;
|
|
321
|
+
|
|
322
|
+
// check whether one or more domains are available to register
|
|
323
|
+
checkAvailability(domainNames: string[], opts?: RequestOptions): Promise<DomainAvailability[]>;
|
|
324
|
+
|
|
325
|
+
// look up pricing for a TLD (or a specific domain)
|
|
326
|
+
getPricing(tldOrDomain: string, opts?: RequestOptions): Promise<TldPricing>;
|
|
327
|
+
|
|
328
|
+
// register a new domain
|
|
329
|
+
registerDomain(
|
|
330
|
+
domainName: string,
|
|
331
|
+
input: RegisterDomainInput,
|
|
332
|
+
opts?: RequestOptions
|
|
333
|
+
): Promise<OperationResult>;
|
|
334
|
+
|
|
335
|
+
// renew a domain for `years` years
|
|
336
|
+
renewDomain(domainName: string, years?: number, opts?: RequestOptions): Promise<OperationResult>;
|
|
337
|
+
|
|
338
|
+
// toggle auto-renew for a domain
|
|
339
|
+
setAutoRenew(
|
|
340
|
+
domainName: string,
|
|
341
|
+
enabled: boolean,
|
|
342
|
+
opts?: RequestOptions
|
|
343
|
+
): Promise<OperationResult>;
|
|
344
|
+
|
|
345
|
+
// transfer a domain into the account using its auth/EPP code (+ any contacts /
|
|
346
|
+
// consent the registrar requires, via `TransferDomainInput`)
|
|
347
|
+
transferIn(
|
|
348
|
+
domainName: string,
|
|
349
|
+
input: TransferDomainInput,
|
|
350
|
+
opts?: RequestOptions
|
|
351
|
+
): Promise<OperationResult>;
|
|
352
|
+
|
|
353
|
+
// replace the nameservers for a domain
|
|
354
|
+
updateNameservers(
|
|
355
|
+
domainName: string,
|
|
356
|
+
nameservers: string[],
|
|
357
|
+
opts?: RequestOptions
|
|
358
|
+
): Promise<OperationResult>;
|
|
359
|
+
|
|
360
|
+
// read the nameservers currently set on a domain
|
|
361
|
+
getNameservers(domainName: string, opts?: RequestOptions): Promise<string[]>;
|
|
362
|
+
|
|
363
|
+
// lock a domain against transfers
|
|
364
|
+
lockDomain(domainName: string, opts?: RequestOptions): Promise<OperationResult>;
|
|
365
|
+
|
|
366
|
+
// unlock a domain
|
|
367
|
+
unlockDomain(domainName: string, opts?: RequestOptions): Promise<OperationResult>;
|
|
368
|
+
|
|
369
|
+
// toggle WHOIS privacy for a domain
|
|
370
|
+
setPrivacy(domainName: string, enabled: boolean, opts?: RequestOptions): Promise<OperationResult>;
|
|
371
|
+
|
|
372
|
+
// read the registrant/admin/tech/billing contacts for a domain
|
|
373
|
+
getContacts(domainName: string, opts?: RequestOptions): Promise<ContactSet>;
|
|
374
|
+
|
|
375
|
+
// update the contacts for a domain
|
|
376
|
+
updateContacts(
|
|
377
|
+
domainName: string,
|
|
378
|
+
contacts: ContactSet,
|
|
379
|
+
opts?: RequestOptions
|
|
380
|
+
): Promise<OperationResult>;
|
|
381
|
+
|
|
382
|
+
// read the DNS records for a domain
|
|
383
|
+
getDnsRecords(domainName: string, opts?: RequestOptions): Promise<DnsRecord[]>;
|
|
384
|
+
|
|
385
|
+
// replace the DNS records for a domain
|
|
386
|
+
setDnsRecords(
|
|
387
|
+
domainName: string,
|
|
388
|
+
records: DnsRecord[],
|
|
389
|
+
opts?: RequestOptions
|
|
390
|
+
): Promise<OperationResult>;
|
|
391
|
+
|
|
392
|
+
// --- extended capabilities ---
|
|
393
|
+
//
|
|
394
|
+
// Opt-in operations beyond the core contract. A provider genuinely supports one
|
|
395
|
+
// only if it declares the matching Feature (see `features`/`supports`); every
|
|
396
|
+
// other provider inherits BaseRegistrar's default, which rejects with
|
|
397
|
+
// NotImplementedError. Callers should gate on `supports(...)` before use.
|
|
398
|
+
|
|
399
|
+
// read the alias-style email forwarding rules for a domain
|
|
400
|
+
getEmailForwarding(domainName: string, opts?: RequestOptions): Promise<EmailForward[]>;
|
|
401
|
+
|
|
402
|
+
// replace the alias-style email forwarding rules for a domain (full replace: any
|
|
403
|
+
// rule not included is removed; an empty array clears all forwarding)
|
|
404
|
+
setEmailForwarding(
|
|
405
|
+
domainName: string,
|
|
406
|
+
forwards: EmailForward[],
|
|
407
|
+
opts?: RequestOptions
|
|
408
|
+
): Promise<OperationResult>;
|
|
409
|
+
|
|
410
|
+
// read the URL/domain forwarding rules for a domain
|
|
411
|
+
getDomainForwarding(domainName: string, opts?: RequestOptions): Promise<DomainForward[]>;
|
|
412
|
+
|
|
413
|
+
// replace the URL/domain forwarding rules for a domain (full replace: any rule
|
|
414
|
+
// not included is removed; an empty array clears all forwarding)
|
|
415
|
+
setDomainForwarding(
|
|
416
|
+
domainName: string,
|
|
417
|
+
forwards: DomainForward[],
|
|
418
|
+
opts?: RequestOptions
|
|
419
|
+
): Promise<OperationResult>;
|
|
420
|
+
|
|
421
|
+
// retrieve the transfer authorization (EPP/auth) code used to transfer the
|
|
422
|
+
// domain away to another registrar
|
|
423
|
+
getAuthCode(domainName: string, opts?: RequestOptions): Promise<string>;
|
|
424
|
+
|
|
425
|
+
// read DNSSEC status for a domain (whether it's enabled, plus any DS records)
|
|
426
|
+
getDnssec(domainName: string, opts?: RequestOptions): Promise<DnssecStatus>;
|
|
427
|
+
|
|
428
|
+
// disable/turn off DNSSEC for a domain (removes the registrar's DS records at
|
|
429
|
+
// the parent). Enabling DNSSEC is intentionally not part of this interface.
|
|
430
|
+
disableDnssec(domainName: string, opts?: RequestOptions): Promise<OperationResult>;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// static side of a registrar class: the constructor plus discovery metadata
|
|
434
|
+
// (display name, the credential fields it needs, where to get them, and whether
|
|
435
|
+
// it offers a sandbox/test environment).
|
|
436
|
+
export interface RegistrarConstructor {
|
|
437
|
+
new (credentials: RegistrarCredentials, options?: RegistrarOptions): Registrar;
|
|
438
|
+
readonly displayName: string;
|
|
439
|
+
readonly configFields: ConfigField[];
|
|
440
|
+
readonly helpText: string;
|
|
441
|
+
readonly supportsSandbox: boolean;
|
|
442
|
+
// extended capabilities this provider opts into, beyond the core contract
|
|
443
|
+
readonly extendedFeatures: readonly RegistrarFeature[];
|
|
444
|
+
// the full capability set: the core contract plus `extendedFeatures`
|
|
445
|
+
readonly features: readonly RegistrarFeature[];
|
|
446
|
+
}
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type { DomainForward, Domain, DomainInput, RegistrationConsent } from './types';
|
|
2
|
+
import { ConsentRequiredError } from './errors';
|
|
3
|
+
|
|
4
|
+
// sleep helper for retry backoff
|
|
5
|
+
export function sleep(ms: number): Promise<void> {
|
|
6
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
// a DomainForward that is safe to write: masked/framed forwarding is read-only.
|
|
10
|
+
export type SettableDomainForward = DomainForward & { type: 'temporary' | 'permanent' };
|
|
11
|
+
|
|
12
|
+
export const MASKED_FORWARD_UNSUPPORTED =
|
|
13
|
+
'masked/framed forwarding is not supported by this library; use "temporary" or "permanent"';
|
|
14
|
+
|
|
15
|
+
// Guard for `setDomainForwarding`: reject any masked/framed forward up front (so
|
|
16
|
+
// no partial write happens) and narrow the list to the settable types. Providers
|
|
17
|
+
// call this before writing.
|
|
18
|
+
export function settableForwards(forwards: DomainForward[]): SettableDomainForward[] {
|
|
19
|
+
for (const f of forwards) {
|
|
20
|
+
if (f.type === 'masked') throw new Error(MASKED_FORWARD_UNSUPPORTED);
|
|
21
|
+
}
|
|
22
|
+
return forwards as SettableDomainForward[];
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// guard for register/transfer: registration forms a legal contract with the
|
|
26
|
+
// registry, so a provider must be given explicit consent. Throws
|
|
27
|
+
// ConsentRequiredError (distinct from NotImplementedError) when it's absent.
|
|
28
|
+
export function requireConsent(
|
|
29
|
+
provider: string,
|
|
30
|
+
consent: RegistrationConsent | undefined
|
|
31
|
+
): RegistrationConsent {
|
|
32
|
+
if (!consent) {
|
|
33
|
+
throw new ConsentRequiredError(
|
|
34
|
+
`${provider}: this operation requires consent — supply \`consent\` ` +
|
|
35
|
+
'(accepting the registrar’s registration agreements)'
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
return consent;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// filter a domain list by a case-insensitive substring of the domain name.
|
|
42
|
+
// Providers that filter server-side still call this — the re-filter is a
|
|
43
|
+
// harmless no-op there — so every provider applies `search` consistently.
|
|
44
|
+
export function filterDomains(domains: Domain[], search?: string): Domain[] {
|
|
45
|
+
const q = search?.trim().toLowerCase();
|
|
46
|
+
if (!q) return domains;
|
|
47
|
+
return domains.filter(d => d.domainName.toLowerCase().includes(q));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// normalize a domain name: trim, lowercase, strip a single trailing dot
|
|
51
|
+
export function normalizeDomain(domain: string): string {
|
|
52
|
+
return domain.trim().toLowerCase().replace(/\.$/, '');
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// exponential backoff delay: backoff * 2^attempt
|
|
56
|
+
export function backoffDelay(backoff: number, attempt: number): number {
|
|
57
|
+
return backoff * Math.pow(2, attempt);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// parse a variety of date representations into a Date (or null)
|
|
61
|
+
// handles ISO strings, epoch millis (as number or numeric string), and Date
|
|
62
|
+
export function parseDate(value: Date | string | number | null | undefined): Date | null {
|
|
63
|
+
if (value == null || value === '') return null;
|
|
64
|
+
if (value instanceof Date) return isNaN(value.getTime()) ? null : value;
|
|
65
|
+
|
|
66
|
+
// numeric epoch millis (e.g. Dynadot returns ms timestamps)
|
|
67
|
+
if (typeof value === 'number') {
|
|
68
|
+
const d = new Date(value);
|
|
69
|
+
return isNaN(d.getTime()) ? null : d;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// numeric string that looks like an epoch-millis timestamp
|
|
73
|
+
const asNumber = Number(value);
|
|
74
|
+
if (!isNaN(asNumber) && asNumber > 1_000_000_000_000) {
|
|
75
|
+
const d = new Date(asNumber);
|
|
76
|
+
return isNaN(d.getTime()) ? null : d;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const d = new Date(value);
|
|
80
|
+
return isNaN(d.getTime()) ? null : d;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// normalize nameservers into a flat array of hostname strings.
|
|
84
|
+
// accepts: string[], { hosts: string[] } (Spaceship), or objects with a
|
|
85
|
+
// ServerName property (Dynadot), falling back to String() for anything else.
|
|
86
|
+
// Hostnames are trimmed and lowercased (they're case-insensitive, but providers
|
|
87
|
+
// return mixed case), and blanks are dropped.
|
|
88
|
+
export function normalizeNameservers(nameservers: unknown): string[] {
|
|
89
|
+
if (!nameservers) return [];
|
|
90
|
+
|
|
91
|
+
let raw: unknown[];
|
|
92
|
+
|
|
93
|
+
// object with a `hosts` array (Spaceship shape)
|
|
94
|
+
if (
|
|
95
|
+
typeof nameservers === 'object' &&
|
|
96
|
+
!Array.isArray(nameservers) &&
|
|
97
|
+
Array.isArray((nameservers as { hosts?: unknown }).hosts)
|
|
98
|
+
) {
|
|
99
|
+
raw = (nameservers as { hosts: unknown[] }).hosts;
|
|
100
|
+
} else if (Array.isArray(nameservers)) {
|
|
101
|
+
raw = nameservers;
|
|
102
|
+
} else {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return raw
|
|
107
|
+
.map(ns => {
|
|
108
|
+
if (typeof ns === 'string') return ns;
|
|
109
|
+
if (ns && typeof ns === 'object' && 'ServerName' in ns) {
|
|
110
|
+
return String(ns.ServerName);
|
|
111
|
+
}
|
|
112
|
+
return String(ns);
|
|
113
|
+
})
|
|
114
|
+
.map(host => host.trim().toLowerCase())
|
|
115
|
+
.filter(Boolean);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// build a normalized Domain from loose provider input:
|
|
119
|
+
// lowercases status, normalizes nameservers, and coerces dates.
|
|
120
|
+
export function createDomain(data: DomainInput = {}): Domain {
|
|
121
|
+
return {
|
|
122
|
+
domainName: data.domainName ?? '',
|
|
123
|
+
registrar: data.registrar ?? '',
|
|
124
|
+
status: (data.status ?? '').toLowerCase(),
|
|
125
|
+
createdDate: parseDate(data.createdDate),
|
|
126
|
+
expirationDate: parseDate(data.expirationDate),
|
|
127
|
+
renewalDate: parseDate(data.renewalDate),
|
|
128
|
+
autoRenew: data.autoRenew ?? false,
|
|
129
|
+
locked: data.locked ?? false,
|
|
130
|
+
privacy: data.privacy ?? false,
|
|
131
|
+
nameservers: normalizeNameservers(data.nameservers),
|
|
132
|
+
syncedAt: data.syncedAt ?? new Date(),
|
|
133
|
+
deleted: data.deleted ?? false,
|
|
134
|
+
};
|
|
135
|
+
}
|
package/src/xml.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { XMLParser, type X2jOptions } from 'fast-xml-parser';
|
|
2
|
+
import { ParsingError } from './errors';
|
|
3
|
+
|
|
4
|
+
// Shared XML parsing for registrar APIs that speak XML (e.g. Namecheap, Enom).
|
|
5
|
+
// fast-xml-parser is pure JS with no Node built-ins, so this stays safe in
|
|
6
|
+
// browsers, Cloudflare Workers, Deno, Bun, and Node.
|
|
7
|
+
|
|
8
|
+
// default options applied to all registrar XML responses:
|
|
9
|
+
// - attributes are kept (prefixed with `@_`)
|
|
10
|
+
// - values are left as raw strings; providers coerce them explicitly, so a
|
|
11
|
+
// ZIP like "01234" or a flag like "true" never changes type unexpectedly
|
|
12
|
+
const DEFAULT_XML_OPTIONS: Partial<X2jOptions> = {
|
|
13
|
+
ignoreAttributes: false,
|
|
14
|
+
attributeNamePrefix: '@_',
|
|
15
|
+
textNodeName: '#text',
|
|
16
|
+
parseAttributeValue: false,
|
|
17
|
+
parseTagValue: false,
|
|
18
|
+
trimValues: true,
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// parse an XML string into a plain object, throwing ParsingError on failure
|
|
22
|
+
export function parseXml<T = unknown>(xml: string, options?: Partial<X2jOptions>): T {
|
|
23
|
+
try {
|
|
24
|
+
const parser = new XMLParser({ ...DEFAULT_XML_OPTIONS, ...options });
|
|
25
|
+
return parser.parse(xml) as T;
|
|
26
|
+
} catch (error) {
|
|
27
|
+
throw new ParsingError(
|
|
28
|
+
`Failed to parse XML: ${error instanceof Error ? error.message : String(error)}`
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// coerce a value that may be a single item or an array into an array.
|
|
34
|
+
// XML parsers collapse a single repeated element into one object; this
|
|
35
|
+
// normalizes both cases to an array.
|
|
36
|
+
export function ensureArray<T>(value: T | T[] | undefined | null): T[] {
|
|
37
|
+
if (value == null) return [];
|
|
38
|
+
return Array.isArray(value) ? value : [value];
|
|
39
|
+
}
|