rdapper 0.10.4 → 0.12.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.
@@ -0,0 +1,410 @@
1
+ import { parse } from "tldts";
2
+
3
+ //#region src/types.d.ts
4
+
5
+ /**
6
+ * The data source used to retrieve domain information.
7
+ *
8
+ * - `rdap`: Data was retrieved via RDAP (Registration Data Access Protocol)
9
+ * - `whois`: Data was retrieved via WHOIS (port 43)
10
+ */
11
+ type LookupSource = "rdap" | "whois";
12
+ /**
13
+ * Domain registrar information.
14
+ *
15
+ * Contains identifying details about the registrar responsible for the domain registration.
16
+ * Fields may be incomplete depending on the data source and registry policies.
17
+ */
18
+ interface RegistrarInfo {
19
+ /** Registrar name (e.g., "GoDaddy.com, LLC") */
20
+ name?: string;
21
+ /** IANA-assigned registrar ID */
22
+ ianaId?: string;
23
+ /** Registrar website URL */
24
+ url?: string;
25
+ /** Registrar contact email address */
26
+ email?: string;
27
+ /** Registrar contact phone number */
28
+ phone?: string;
29
+ }
30
+ /**
31
+ * Contact information for various roles associated with a domain.
32
+ *
33
+ * Contacts may represent individuals or organizations responsible for different
34
+ * aspects of domain management. Availability and completeness of contact data
35
+ * varies by TLD, registrar, and privacy policies (GDPR, WHOIS privacy services).
36
+ */
37
+ interface Contact {
38
+ type: "registrant" | "admin" | "tech" | "billing" | "abuse" | "registrar" | "reseller" | "unknown";
39
+ name?: string;
40
+ organization?: string;
41
+ email?: string | string[];
42
+ phone?: string | string[];
43
+ fax?: string | string[];
44
+ street?: string[];
45
+ city?: string;
46
+ state?: string;
47
+ postalCode?: string;
48
+ country?: string;
49
+ countryCode?: string;
50
+ }
51
+ /**
52
+ * DNS nameserver information.
53
+ *
54
+ * Represents a nameserver authoritative for the domain, including its hostname
55
+ * and optional glue records (IP addresses).
56
+ */
57
+ interface Nameserver {
58
+ /** Nameserver hostname (e.g., "ns1.example.com") */
59
+ host: string;
60
+ /** IPv4 glue records, if provided */
61
+ ipv4?: string[];
62
+ /** IPv6 glue records, if provided */
63
+ ipv6?: string[];
64
+ }
65
+ /**
66
+ * Domain status information.
67
+ *
68
+ * Represents EPP status codes and registry-specific statuses that indicate
69
+ * the operational state and restrictions on a domain.
70
+ *
71
+ * Common EPP statuses include: clientTransferProhibited, serverHold,
72
+ * serverDeleteProhibited, etc.
73
+ *
74
+ * @see {@link https://www.icann.org/resources/pages/epp-status-codes-2014-06-16-en ICANN EPP Status Codes}
75
+ */
76
+ interface StatusEvent {
77
+ /** Normalized status code (e.g., "clientTransferProhibited") */
78
+ status: string;
79
+ /** Human-readable description of the status, if available */
80
+ description?: string;
81
+ /** Original raw status string from the source */
82
+ raw?: string;
83
+ }
84
+ /**
85
+ * Normalized domain registration record.
86
+ *
87
+ * This is the primary data structure returned by domain lookups. It provides a unified
88
+ * view of domain registration data regardless of whether the information was obtained
89
+ * via RDAP or WHOIS.
90
+ *
91
+ * Field availability varies by:
92
+ * - TLD and registry policies
93
+ * - Data source (RDAP typically more structured than WHOIS)
94
+ * - Privacy protections (GDPR, WHOIS privacy services)
95
+ * - Registrar practices
96
+ *
97
+ * @example
98
+ * ```ts
99
+ * import { lookup } from 'rdapper';
100
+ *
101
+ * const { ok, record } = await lookup('example.com');
102
+ * if (ok && record) {
103
+ * console.log(record.registrar?.name); // "Example Registrar, Inc."
104
+ * console.log(record.isRegistered); // true
105
+ * console.log(record.source); // "rdap"
106
+ * }
107
+ * ```
108
+ */
109
+ interface DomainRecord {
110
+ /** Normalized domain name */
111
+ domain: string;
112
+ /** Terminal TLD */
113
+ tld: string;
114
+ /** Whether the domain is registered */
115
+ isRegistered: boolean;
116
+ /** Whether the domain is internationalized (IDN) */
117
+ isIDN?: boolean;
118
+ /** Unicode name */
119
+ unicodeName?: string;
120
+ /** Punycode name */
121
+ punycodeName?: string;
122
+ /** Registry operator */
123
+ registry?: string;
124
+ /** Registrar */
125
+ registrar?: RegistrarInfo;
126
+ /** Reseller (if applicable) */
127
+ reseller?: string;
128
+ /** EPP status codes */
129
+ statuses?: StatusEvent[];
130
+ /** Creation date in ISO 8601 */
131
+ creationDate?: string;
132
+ /** Updated date in ISO 8601 */
133
+ updatedDate?: string;
134
+ /** Expiration date in ISO 8601 */
135
+ expirationDate?: string;
136
+ /** Deletion date in ISO 8601 */
137
+ deletionDate?: string;
138
+ /** Transfer lock */
139
+ transferLock?: boolean;
140
+ /** DNSSEC data (if available) */
141
+ dnssec?: {
142
+ enabled: boolean;
143
+ dsRecords?: Array<{
144
+ keyTag?: number;
145
+ algorithm?: number;
146
+ digestType?: number;
147
+ digest?: string;
148
+ }>;
149
+ };
150
+ /** Nameservers */
151
+ nameservers?: Nameserver[];
152
+ /** Contacts (registrant, admin, tech, billing, abuse, etc.) */
153
+ contacts?: Contact[];
154
+ /** Best guess as to whether registrant is redacted based on keywords */
155
+ privacyEnabled?: boolean;
156
+ /** Authoritative WHOIS queried (if any) */
157
+ whoisServer?: string;
158
+ /** RDAP base URLs tried */
159
+ rdapServers?: string[];
160
+ /** Raw RDAP JSON */
161
+ rawRdap?: unknown;
162
+ /** Raw WHOIS text (last authoritative) */
163
+ rawWhois?: string;
164
+ /** Which source produced data */
165
+ source: LookupSource;
166
+ /** Warnings generated during lookup */
167
+ warnings?: string[];
168
+ }
169
+ /**
170
+ * RDAP bootstrap JSON format as published by IANA at https://data.iana.org/rdap/dns.json
171
+ *
172
+ * This interface describes the structure of the RDAP bootstrap registry, which maps
173
+ * top-level domains to their authoritative RDAP servers.
174
+ *
175
+ * @example
176
+ * ```json
177
+ * {
178
+ * "version": "1.0",
179
+ * "publication": "2025-01-15T12:00:00Z",
180
+ * "description": "RDAP Bootstrap file for DNS top-level domains",
181
+ * "services": [
182
+ * [["com", "net"], ["https://rdap.verisign.com/com/v1/"]],
183
+ * [["org"], ["https://rdap.publicinterestregistry.org/"]]
184
+ * ]
185
+ * }
186
+ * ```
187
+ *
188
+ * @see {@link https://datatracker.ietf.org/doc/html/rfc7484 RFC 7484 - Finding the Authoritative RDAP Service}
189
+ */
190
+ interface BootstrapData {
191
+ /** Bootstrap file format version */
192
+ version: string;
193
+ /** ISO 8601 timestamp of when this bootstrap data was published */
194
+ publication: string;
195
+ /** Optional human-readable description of the bootstrap file */
196
+ description?: string;
197
+ /**
198
+ * Service mappings array. Each entry is a tuple of [TLDs, base URLs]:
199
+ * - First element: array of TLD strings (e.g., ["com", "net"])
200
+ * - Second element: array of RDAP base URL strings (e.g., ["https://rdap.verisign.com/com/v1/"])
201
+ */
202
+ services: string[][][];
203
+ }
204
+ /**
205
+ * Configuration options for domain lookups.
206
+ *
207
+ * Controls the lookup behavior, including which protocols to use (RDAP/WHOIS),
208
+ * timeout settings, referral following, and caching options.
209
+ *
210
+ * @example
211
+ * ```ts
212
+ * import { lookup } from 'rdapper';
213
+ *
214
+ * // RDAP-only lookup for edge runtime compatibility
215
+ * const result = await lookup('example.com', {
216
+ * rdapOnly: true,
217
+ * timeoutMs: 10000
218
+ * });
219
+ *
220
+ * // Cached bootstrap data for high-volume scenarios
221
+ * const cachedBootstrap = await getFromCache();
222
+ * const result = await lookup('example.com', {
223
+ * customBootstrapData: cachedBootstrap,
224
+ * includeRaw: true
225
+ * });
226
+ * ```
227
+ */
228
+ interface LookupOptions {
229
+ /** Total timeout budget */
230
+ timeoutMs?: number;
231
+ /** Don't fall back to WHOIS */
232
+ rdapOnly?: boolean;
233
+ /** Don't attempt RDAP */
234
+ whoisOnly?: boolean;
235
+ /** Follow referral server (default true) */
236
+ followWhoisReferral?: boolean;
237
+ /** Maximum registrar WHOIS referral hops (default 2) */
238
+ maxWhoisReferralHops?: number;
239
+ /** Follow RDAP related/entity links (default true) */
240
+ rdapFollowLinks?: boolean;
241
+ /** Maximum RDAP related link fetches (default 2) */
242
+ maxRdapLinkHops?: number;
243
+ /** RDAP link rels to consider (default ["related","entity","registrar","alternate"]) */
244
+ rdapLinkRels?: string[];
245
+ /**
246
+ * Pre-loaded RDAP bootstrap data to use instead of fetching from IANA.
247
+ *
248
+ * Pass your own cached version of https://data.iana.org/rdap/dns.json to control
249
+ * caching behavior and avoid redundant network requests. This is useful when you want
250
+ * to cache the bootstrap data in Redis, memory, filesystem, or any other caching layer.
251
+ *
252
+ * If provided, this takes precedence over `customBootstrapUrl` and the default IANA URL.
253
+ *
254
+ * @example
255
+ * ```ts
256
+ * import { lookup, type BootstrapData } from 'rdapper';
257
+ *
258
+ * // Fetch and cache the bootstrap data yourself
259
+ * const bootstrapData: BootstrapData = await fetchFromCache()
260
+ * ?? await fetchAndCache('https://data.iana.org/rdap/dns.json');
261
+ *
262
+ * // Pass the cached data to rdapper
263
+ * const result = await lookup('example.com', {
264
+ * customBootstrapData: bootstrapData
265
+ * });
266
+ * ```
267
+ *
268
+ * @see {@link BootstrapData} for the expected data structure
269
+ */
270
+ customBootstrapData?: BootstrapData;
271
+ /** Override IANA bootstrap URL (ignored if customBootstrapData is provided) */
272
+ customBootstrapUrl?: string;
273
+ /**
274
+ * Custom fetch implementation to use for all HTTP requests.
275
+ *
276
+ * Provides complete control over how HTTP requests are made, enabling advanced use cases:
277
+ * - **Caching**: Cache bootstrap data, RDAP responses, and related link responses
278
+ * - **Logging**: Log all outgoing requests and responses for monitoring
279
+ * - **Retry Logic**: Implement custom retry strategies with exponential backoff
280
+ * - **Rate Limiting**: Control request frequency to respect API limits
281
+ * - **Proxies/Auth**: Route requests through proxies or add authentication headers
282
+ * - **Testing**: Inject mock responses for testing without network calls
283
+ *
284
+ * The custom fetch will be used for:
285
+ * - RDAP bootstrap registry requests (unless `customBootstrapData` is provided)
286
+ * - RDAP domain lookup requests
287
+ * - RDAP related/entity link requests
288
+ *
289
+ * If not provided, the global `fetch` function is used (Node.js 18+ or browser).
290
+ *
291
+ * @example
292
+ * ```ts
293
+ * import { lookup } from 'rdapper';
294
+ *
295
+ * // Example 1: Simple in-memory cache
296
+ * const cache = new Map<string, Response>();
297
+ * const cachedFetch: typeof fetch = async (input, init) => {
298
+ * const key = typeof input === 'string' ? input : input.toString();
299
+ * if (cache.has(key)) return cache.get(key)!.clone();
300
+ * const response = await fetch(input, init);
301
+ * cache.set(key, response.clone());
302
+ * return response;
303
+ * };
304
+ *
305
+ * await lookup('example.com', { customFetch: cachedFetch });
306
+ *
307
+ * // Example 2: Request logging
308
+ * const loggingFetch: typeof fetch = async (input, init) => {
309
+ * const url = typeof input === 'string' ? input : input.toString();
310
+ * console.log('[Fetch]', url);
311
+ * const response = await fetch(input, init);
312
+ * console.log('[Response]', response.status, url);
313
+ * return response;
314
+ * };
315
+ *
316
+ * await lookup('example.com', { customFetch: loggingFetch });
317
+ * ```
318
+ *
319
+ * @see {@link FetchLike} for the expected function signature
320
+ */
321
+ customFetch?: FetchLike;
322
+ /** Override/add authoritative WHOIS per TLD */
323
+ whoisHints?: Record<string, string>;
324
+ /** Include rawRdap/rawWhois in results (default false) */
325
+ includeRaw?: boolean;
326
+ /** Optional cancellation signal */
327
+ signal?: AbortSignal;
328
+ }
329
+ /**
330
+ * Result of a domain lookup operation.
331
+ *
332
+ * Provides a structured response indicating success or failure, with either
333
+ * a normalized domain record or an error message.
334
+ *
335
+ * @example
336
+ * ```ts
337
+ * import { lookup } from 'rdapper';
338
+ *
339
+ * const result = await lookup('example.com');
340
+ * if (result.ok) {
341
+ * console.log('Domain:', result.record.domain);
342
+ * console.log('Registered:', result.record.isRegistered);
343
+ * } else {
344
+ * console.error('Lookup failed:', result.error);
345
+ * }
346
+ * ```
347
+ */
348
+ interface LookupResult {
349
+ /** Whether the lookup completed successfully */
350
+ ok: boolean;
351
+ /** The normalized domain record, present when ok is true */
352
+ record?: DomainRecord;
353
+ /** Error message describing why the lookup failed, present when ok is false */
354
+ error?: string;
355
+ }
356
+ /**
357
+ * Fetch-compatible function signature.
358
+ *
359
+ * Used internally for dependency injection and testing. Matches the signature
360
+ * of the global `fetch` function available in Node.js 18+ and browsers.
361
+ */
362
+ type FetchLike = (input: string | URL, init?: RequestInit) => Promise<Response>;
363
+ //#endregion
364
+ //#region src/lib/domain.d.ts
365
+ type ParseOptions = Parameters<typeof parse>[1];
366
+ /**
367
+ * Parse a domain into its parts. Passes options to `tldts.parse()`.
368
+ * @see https://github.com/remusao/tldts/blob/master/packages/tldts-core/src/options.ts
369
+ */
370
+ declare function getDomainParts(domain: string, opts?: ParseOptions): ReturnType<typeof parse>;
371
+ /**
372
+ * Get the TLD (ICANN-only public suffix) of a domain. Passes options to `tldts.parse()`.
373
+ * @see https://github.com/remusao/tldts/blob/master/packages/tldts-core/src/options.ts
374
+ */
375
+ declare function getDomainTld(domain: string, opts?: ParseOptions): string | null;
376
+ /**
377
+ * Basic domain validity check (hostname-like), not performing DNS or RDAP.
378
+ */
379
+ declare function isLikelyDomain(value: string): boolean;
380
+ /**
381
+ * Normalize arbitrary input (domain or URL) to its registrable domain (eTLD+1).
382
+ * Passes options to `tldts.parse()`.
383
+ * Returns null when the input is not a valid ICANN domain (e.g., invalid TLD, IPs)
384
+ * @see https://github.com/remusao/tldts/blob/master/packages/tldts-core/src/options.ts
385
+ */
386
+ declare function toRegistrableDomain(input: string, opts?: ParseOptions): string | null;
387
+ //#endregion
388
+ //#region src/index.d.ts
389
+ /**
390
+ * High-level lookup that prefers RDAP and falls back to WHOIS.
391
+ * Ensures a standardized DomainRecord, independent of the source.
392
+ */
393
+ declare function lookup(domain: string, opts?: LookupOptions): Promise<LookupResult>;
394
+ /**
395
+ * Determine if a domain appears available (not registered).
396
+ * Performs a lookup and resolves to a boolean. Rejects on lookup error.
397
+ */
398
+ declare function isAvailable(domain: string, opts?: LookupOptions): Promise<boolean>;
399
+ /**
400
+ * Determine if a domain appears registered.
401
+ * Performs a lookup and resolves to a boolean. Rejects on lookup error.
402
+ */
403
+ declare function isRegistered(domain: string, opts?: LookupOptions): Promise<boolean>;
404
+ /**
405
+ * @deprecated Use `lookup` instead.
406
+ */
407
+ declare const lookupDomain: typeof lookup;
408
+ //#endregion
409
+ export { BootstrapData, Contact, DomainRecord, FetchLike, LookupOptions, LookupResult, LookupSource, Nameserver, RegistrarInfo, StatusEvent, getDomainParts, getDomainTld, isAvailable, isLikelyDomain, isRegistered, lookup, lookupDomain, toRegistrableDomain };
410
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types.ts","../src/lib/domain.ts","../src/index.ts"],"sourcesContent":[],"mappings":";;;;;;;AAMA;AAQA;AAoBA;AA6BiB,KAzDL,YAAA,GAyDe,MAAA,GAAA,OAAA;AAoB3B;AAkCA;;;;;AA4Ca,UAnJI,aAAA,CAmJJ;EAYH;EAAY,IAAA,CAAA,EAAA,MAAA;EA0BL;EAuCA,MAAA,CAAA,EAAA,MAAA;EA0CO;EAmDR,GAAA,CAAA,EAAA,MAAA;EAED;EAIJ,KAAA,CAAA,EAAA,MAAA;EAAW;EAsBL,KAAA,CAAA,EAAA,MAAA;AAejB;;;;;;;;UApViB,OAAA;EChCZ,IAAA,EAAA,YAAY,GAAA,OAAqB,GAAA,MAAlB,GAAA,SAAU,GAAA,OAAA,GAAA,WAAA,GAAA,UAAA,GAAA,SAAA;EAMd,IAAA,CAAA,EAAA,MAAA;EAEP,YAAA,CAAA,EAAA,MAAA;EACY,KAAA,CAAA,EAAA,MAAA,GAAA,MAAA,EAAA;EAAlB,KAAA,CAAA,EAAA,MAAA,GAAA,MAAA,EAAA;EAAU,GAAA,CAAA,EAAA,MAAA,GAAA,MAAA,EAAA;EAQG,MAAA,CAAA,EAAA,MAAY,EAAA;EAcZ,IAAA,CAAA,EAAA,MAAA;EAuBA,KAAA,CAAA,EAAA,MAAA;;;;AClChB;;;;;AAyGA;AAaA;AAYa,UFzFI,UAAA,CEyFiB;;;;;;;;;;;;;;;;;;;UFrEjB,WAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAkCA,YAAA;;;;;;;;;;;;;;;;cAgBH;;;;aAID;;;;;;;;;;;;;;gBAcG;;;;;;;;gBAQA;;aAEH;;;;;;;;;;;;UAYH;;;;;;;;;;;;;;;;;;;;;;;;;UA0BO,aAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;UAuCA,aAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;wBA0CO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;gBAmDR;;eAED;;;;WAIJ;;;;;;;;;;;;;;;;;;;;;UAsBM,YAAA;;;;WAIN;;;;;;;;;;KAWC,SAAA,oBACM,YACT,gBACJ,QAAQ;;;KCvXR,YAAA,GAAe,kBAAkB;;ADItC;AAQA;AAoBA;AA6BiB,iBCvDD,cAAA,CDuDW,MAAA,EAAA,MAAA,EAAA,IAAA,CAAA,ECrDlB,YDqDkB,CAAA,ECpDxB,UDoDwB,CAAA,OCpDN,KDoDM,CAAA;AAoB3B;AAkCA;;;AAkCgB,iBCpIA,YAAA,CDoIA,MAAA,EAAA,MAAA,EAAA,IAAA,CAAA,EClIP,YDkIO,CAAA,EAAA,MAAA,GAAA,IAAA;;;;AAsBM,iBC5IN,cAAA,CD4IM,KAAA,EAAA,MAAA,CAAA,EAAA,OAAA;AAiEtB;;;;;;AAyHiB,iBC/SD,mBAAA,CDmTO,KAAA,EAAA,MAAA,EAAA,IAAA,CAAA,ECjTd,YDiTc,CAAA,EAAA,MAAA,GAAA,IAAA;;;;;AArWvB;AAQA;AAoBiB,iBEZK,MAAA,CFYE,MAAA,EAAA,MAAA,EAAA,IAAA,CAAA,EEVf,aFUe,CAAA,EETrB,OFSqB,CETb,YFSa,CAAA;AA6BxB;AAoBA;AAkCA;;AAoBa,iBEVS,WAAA,CFUT,MAAA,EAAA,MAAA,EAAA,IAAA,CAAA,EERJ,aFQI,CAAA,EEPV,OFOU,CAAA,OAAA,CAAA;;;;;AAoCS,iBEjCA,YAAA,CFiCA,MAAA,EAAA,MAAA,EAAA,IAAA,CAAA,EE/Bb,aF+Ba,CAAA,EE9BnB,OF8BmB,CAAA,OAAA,CAAA;AA0BtB;AAuCA;;AA6FgB,cEnLH,YFmLG,EAAA,OEnLS,MFmLT"}
@@ -60,26 +60,91 @@ function withTimeout(promise, timeoutMs, reason = "Timeout") {
60
60
 
61
61
  //#endregion
62
62
  //#region src/lib/constants.ts
63
- const DEFAULT_TIMEOUT_MS = 15e3;
63
+ /**
64
+ * The timeout for HTTP requests in milliseconds. Defaults to 10 seconds.
65
+ */
66
+ const DEFAULT_TIMEOUT_MS = 1e4;
67
+ /**
68
+ * The default URL for the IANA RDAP bootstrap file.
69
+ *
70
+ * @see {@link https://data.iana.org/rdap/dns.json IANA RDAP Bootstrap File (dns.json)}
71
+ */
72
+ const DEFAULT_BOOTSTRAP_URL = "https://data.iana.org/rdap/dns.json";
73
+
74
+ //#endregion
75
+ //#region src/lib/fetch.ts
76
+ /**
77
+ * Resolve the fetch implementation to use for HTTP requests.
78
+ *
79
+ * Returns the custom fetch from options if provided, otherwise falls back
80
+ * to the global fetch function. This centralized helper ensures consistent
81
+ * fetch resolution across all RDAP HTTP operations.
82
+ *
83
+ * Used internally by:
84
+ * - Bootstrap registry fetching (`src/rdap/bootstrap.ts`)
85
+ * - RDAP domain lookups (`src/rdap/client.ts`)
86
+ * - RDAP related/entity link requests (`src/rdap/merge.ts`)
87
+ *
88
+ * @param options - Any object that may contain a custom fetch implementation
89
+ * @returns The fetch function to use for HTTP requests
90
+ *
91
+ * @example
92
+ * ```ts
93
+ * import { resolveFetch } from './lib/fetch';
94
+ *
95
+ * const fetchFn = resolveFetch(options);
96
+ * const response = await fetchFn('https://example.com/api', { method: 'GET' });
97
+ * ```
98
+ */
99
+ function resolveFetch(options) {
100
+ return options?.customFetch ?? fetch;
101
+ }
64
102
 
65
103
  //#endregion
66
104
  //#region src/rdap/bootstrap.ts
67
105
  /**
68
106
  * Resolve RDAP base URLs for a given TLD using IANA's bootstrap registry.
69
107
  * Returns zero or more base URLs (always suffixed with a trailing slash).
108
+ *
109
+ * Bootstrap data is resolved in the following priority order:
110
+ * 1. `options.customBootstrapData` - pre-loaded bootstrap data (no fetch)
111
+ * 2. `options.customBootstrapUrl` - custom URL to fetch bootstrap data from
112
+ * 3. Default IANA URL - https://data.iana.org/rdap/dns.json
113
+ *
114
+ * @param tld - The top-level domain to look up (e.g., "com", "co.uk")
115
+ * @param options - Optional lookup options including custom bootstrap data/URL
116
+ * @returns Array of RDAP base URLs for the TLD, or empty array if none found
70
117
  */
71
118
  async function getRdapBaseUrlsForTld(tld, options) {
72
- const bootstrapUrl = options?.customBootstrapUrl ?? "https://data.iana.org/rdap/dns.json";
73
- const res = await withTimeout(fetch(bootstrapUrl, {
74
- method: "GET",
75
- headers: { accept: "application/json" },
76
- signal: options?.signal
77
- }), options?.timeoutMs ?? DEFAULT_TIMEOUT_MS, "RDAP bootstrap timeout");
78
- if (!res.ok) return [];
79
- const data = await res.json();
119
+ let data;
120
+ if (options && "customBootstrapData" in options) {
121
+ const provided = options.customBootstrapData;
122
+ if (!provided || typeof provided !== "object") throw new Error("Invalid customBootstrapData: expected an object. See BootstrapData type for required structure.");
123
+ if (!Array.isArray(provided.services)) throw new Error("Invalid customBootstrapData: missing or invalid \"services\" array. See BootstrapData type for required structure.");
124
+ provided.services.forEach((svc, idx) => {
125
+ if (!Array.isArray(svc) || svc.length < 2 || !Array.isArray(svc[0]) || !Array.isArray(svc[1])) throw new Error(`Invalid customBootstrapData: services[${idx}] must be a tuple of [string[], string[]].`);
126
+ });
127
+ data = provided;
128
+ } else {
129
+ const fetchFn = resolveFetch(options);
130
+ const bootstrapUrl = options?.customBootstrapUrl ?? DEFAULT_BOOTSTRAP_URL;
131
+ try {
132
+ const res = await withTimeout(fetchFn(bootstrapUrl, {
133
+ method: "GET",
134
+ headers: { accept: "application/json" },
135
+ signal: options?.signal
136
+ }), options?.timeoutMs ?? DEFAULT_TIMEOUT_MS, "RDAP bootstrap timeout");
137
+ if (!res.ok) return [];
138
+ data = await res.json();
139
+ } catch (err) {
140
+ if (err instanceof Error && err.name === "AbortError") throw err;
141
+ return [];
142
+ }
143
+ }
80
144
  const target = tld.toLowerCase();
81
145
  const bases = [];
82
146
  for (const svc of data.services) {
147
+ if (!svc[0] || !svc[1]) continue;
83
148
  const tlds = svc[0].map((x) => x.toLowerCase());
84
149
  const urls = svc[1];
85
150
  if (tlds.includes(target)) for (const u of urls) {
@@ -98,7 +163,7 @@ async function getRdapBaseUrlsForTld(tld, options) {
98
163
  */
99
164
  async function fetchRdapDomain(domain, baseUrl, options) {
100
165
  const url = new URL(`domain/${encodeURIComponent(domain)}`, baseUrl).toString();
101
- const res = await withTimeout(fetch(url, {
166
+ const res = await withTimeout(resolveFetch(options)(url, {
102
167
  method: "GET",
103
168
  headers: { accept: "application/rdap+json, application/json" },
104
169
  signal: options?.signal
@@ -201,7 +266,7 @@ async function fetchAndMergeRdapRelated(domain, baseDoc, opts) {
201
266
  };
202
267
  }
203
268
  async function fetchRdapUrl(url, options) {
204
- const res = await withTimeout(fetch(url, {
269
+ const res = await withTimeout(resolveFetch(options)(url, {
205
270
  method: "GET",
206
271
  headers: { accept: "application/rdap+json, application/json" },
207
272
  signal: options?.signal
@@ -287,6 +352,7 @@ function parseDateWithRegex(m, _re) {
287
352
  try {
288
353
  if (m[0].includes(":")) {
289
354
  const [_$1, y, mo, d, hh, mm, ss, offH, offM] = m;
355
+ if (!y || !mo || !d || !hh || !mm || !ss) return void 0;
290
356
  let dt = Date.UTC(Number(y), Number(mo) - 1, Number(d), Number(hh), Number(mm), Number(ss));
291
357
  if (offH) {
292
358
  const sign = offH.startsWith("-") ? -1 : 1;
@@ -299,11 +365,13 @@ function parseDateWithRegex(m, _re) {
299
365
  }
300
366
  if (m[0].includes("-")) {
301
367
  const [_$1, dd$1, monStr$1, yyyy$1] = m;
368
+ if (!monStr$1 || !dd$1 || !yyyy$1) return void 0;
302
369
  if (/^\d+$/.test(monStr$1)) return new Date(Date.UTC(Number(yyyy$1), Number(monStr$1) - 1, Number(dd$1)));
303
370
  const mon$1 = monthMap[monStr$1.toLowerCase()];
304
371
  return new Date(Date.UTC(Number(yyyy$1), mon$1, Number(dd$1)));
305
372
  }
306
373
  const [_, monStr, dd, yyyy] = m;
374
+ if (!monStr || !dd || !yyyy) return void 0;
307
375
  const mon = monthMap[monStr.toLowerCase()];
308
376
  return new Date(Date.UTC(Number(yyyy), mon, Number(dd)));
309
377
  } catch {}
@@ -362,7 +430,7 @@ function parseKeyValueLines(text) {
362
430
  const line = rawLine.replace(/\s+$/, "");
363
431
  if (!line.trim()) continue;
364
432
  const bracket = line.match(/^\s*\[([^\]]+)\]\s*(.*)$/);
365
- if (bracket) {
433
+ if (bracket?.[1] !== void 0 && bracket?.[2] !== void 0) {
366
434
  const key = bracket[1].trim().toLowerCase();
367
435
  const value = bracket[2].trim();
368
436
  const list = map.get(key) ?? [];
@@ -450,7 +518,7 @@ function normalizeRdap(inputDomain, tld, rdap, rdapServersTried, includeRaw = fa
450
518
  const updatedDate = toISO(asDateLike(byAction("last changed")?.eventDate) ?? asDateLike(doc.lastChangedDate));
451
519
  const expirationDate = toISO(asDateLike(byAction("expiration")?.eventDate) ?? asDateLike(doc.expirationDate));
452
520
  const deletionDate = toISO(asDateLike(byAction("deletion")?.eventDate) ?? asDateLike(doc.deletionDate));
453
- const transferLock = !!statuses?.some((s) => /transferprohibited/i.test(s.status));
521
+ const transferLock = !!statuses?.some((s) => /transfer[-\s]*prohibited/i.test(s.status));
454
522
  const whoisServer = asString(doc.port43);
455
523
  return {
456
524
  domain: unicodeName || ldhName || inputDomain,
@@ -865,16 +933,25 @@ const WHOIS_AVAILABLE_PATTERNS = [
865
933
  /\bnot found\b/i,
866
934
  /\bno entries found\b/i,
867
935
  /\bno data found\b/i,
936
+ /\bno information available\b/i,
937
+ /\bno information was found\b/i,
938
+ /\bno data was found\b/i,
868
939
  /\bavailable for registration\b/i,
869
940
  /\bdomain\s+available\b/i,
870
941
  /\bdomain status[:\s]+available\b/i,
871
942
  /\bobject does not exist\b/i,
872
943
  /\bthe queried object does not exist\b/i,
873
944
  /\bqueried object does not exist\b/i,
945
+ /\bdoes not exist\b/i,
874
946
  /\breturned 0 objects\b/i,
947
+ /\bnot been registered\b/i,
948
+ /\bunassignable\b/i,
949
+ /\bis free\b/i,
875
950
  /\bstatus:\s*free\b/i,
876
951
  /\bstatus:\s*available\b/i,
877
952
  /\bno object found\b/i,
953
+ /\bobject_not_found\b/i,
954
+ /\bno se encuentra registrado\b/i,
878
955
  /\bnicht gefunden\b/i,
879
956
  /\bpending release\b/i
880
957
  ];
@@ -976,10 +1053,13 @@ function normalizeWhois(domain, tld, whoisText, whoisServer, includeRaw = false)
976
1053
  };
977
1054
  })();
978
1055
  const statusLines = map["domain status"] || map.status || map.flags || map.state || map["registration status"] || map.eppstatus || [];
979
- const statuses = statusLines.length ? statusLines.map((line) => ({
980
- status: line.split(/\s+/)[0],
981
- raw: line
982
- })) : void 0;
1056
+ const statuses = statusLines.length ? statusLines.map((line) => {
1057
+ const status = line.split(/\s+/)[0];
1058
+ return status ? {
1059
+ status,
1060
+ raw: line
1061
+ } : null;
1062
+ }).filter((s) => s !== null) : void 0;
983
1063
  const nsLines = [
984
1064
  ...map["name server"] || [],
985
1065
  ...map.nameserver || [],
@@ -1015,7 +1095,7 @@ function normalizeWhois(domain, tld, whoisText, whoisServer, includeRaw = false)
1015
1095
  const privacyEnabled = !!(registrant && [registrant.name, registrant.organization].filter(Boolean).some(isPrivacyName));
1016
1096
  const dnssecRaw = (map.dnssec?.[0] || "").toLowerCase();
1017
1097
  const dnssec = dnssecRaw ? { enabled: /signed|yes|true/.test(dnssecRaw) } : void 0;
1018
- const transferLock = !!statuses?.some((s) => /transferprohibited/i.test(s.status));
1098
+ const transferLock = !!statuses?.some((s) => /transfer[-\s]*prohibited/i.test(s.raw || s.status || ""));
1019
1099
  return {
1020
1100
  domain,
1021
1101
  tld,
@@ -1266,6 +1346,10 @@ async function lookup(domain, opts) {
1266
1346
  };
1267
1347
  }
1268
1348
  const [first, ...rest] = chain.map((r) => normalizeWhois(domain, tld, r.text, r.serverQueried, !!opts?.includeRaw));
1349
+ if (!first) return {
1350
+ ok: false,
1351
+ error: "No WHOIS data retrieved"
1352
+ };
1269
1353
  return {
1270
1354
  ok: true,
1271
1355
  record: rest.length ? mergeWhoisRecords(first, rest) : first
@@ -1301,4 +1385,5 @@ async function isRegistered(domain, opts) {
1301
1385
  const lookupDomain = lookup;
1302
1386
 
1303
1387
  //#endregion
1304
- export { getDomainParts, getDomainTld, isAvailable, isLikelyDomain, isRegistered, lookup, lookupDomain, toRegistrableDomain };
1388
+ export { getDomainParts, getDomainTld, isAvailable, isLikelyDomain, isRegistered, lookup, lookupDomain, toRegistrableDomain };
1389
+ //# sourceMappingURL=index.mjs.map