rdapper 0.10.3 → 0.11.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 CHANGED
@@ -86,6 +86,338 @@ const res = await lookup("example.com", { rdapOnly: true });
86
86
 
87
87
  - If `rdapOnly` is omitted and the code path reaches WHOIS on edge, rdapper throws a clear runtime error advising to run in Node or set `{ rdapOnly: true }`.
88
88
 
89
+ ### Bootstrap Data Caching
90
+
91
+ By default, rdapper fetches IANA's RDAP bootstrap registry from [`https://data.iana.org/rdap/dns.json`](https://data.iana.org/rdap/dns.json) on every RDAP lookup to discover the authoritative RDAP servers for a given TLD. While this ensures you always have up-to-date server mappings, it also adds latency and a network dependency to each lookup.
92
+
93
+ For production applications that perform many domain lookups, you can take control of bootstrap data caching by fetching and caching the data yourself, then passing it to rdapper using the `customBootstrapData` option. This eliminates redundant network requests and gives you full control over cache invalidation.
94
+
95
+ #### Why cache bootstrap data?
96
+
97
+ - **Performance**: Eliminate an extra HTTP request per lookup (or per TLD if you're looking up many domains)
98
+ - **Reliability**: Reduce dependency on IANA's availability during lookups
99
+ - **Control**: Manage cache TTL and invalidation according to your needs (IANA updates this file infrequently)
100
+ - **Cost**: Reduce bandwidth and API calls in high-volume scenarios
101
+
102
+ #### Example: In-memory caching with TTL
103
+
104
+ ```ts
105
+ import { lookup, type BootstrapData } from 'rdapper';
106
+
107
+ // Simple in-memory cache with TTL
108
+ let cachedBootstrap: BootstrapData | null = null;
109
+ let cacheExpiry = 0;
110
+ const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
111
+
112
+ async function getBootstrapData(): Promise<BootstrapData> {
113
+ const now = Date.now();
114
+
115
+ // Return cached data if still valid
116
+ if (cachedBootstrap && now < cacheExpiry) {
117
+ return cachedBootstrap;
118
+ }
119
+
120
+ // Fetch fresh data
121
+ const response = await fetch('https://data.iana.org/rdap/dns.json');
122
+ if (!response.ok) {
123
+ throw new Error(`Failed to load bootstrap data: ${response.status} ${response.statusText}`);
124
+ }
125
+ const data: BootstrapData = await response.json();
126
+
127
+ // Update cache
128
+ cachedBootstrap = data;
129
+ cacheExpiry = now + CACHE_TTL_MS;
130
+
131
+ return data;
132
+ }
133
+
134
+ // Use the cached bootstrap data in lookups
135
+ const bootstrapData = await getBootstrapData();
136
+ const result = await lookup('example.com', {
137
+ customBootstrapData: bootstrapData
138
+ });
139
+ ```
140
+
141
+ #### Example: Redis caching
142
+
143
+ ```ts
144
+ import { lookup, type BootstrapData } from 'rdapper';
145
+ import { createClient } from 'redis';
146
+
147
+ const redis = createClient();
148
+ await redis.connect();
149
+
150
+ const CACHE_KEY = 'rdap:bootstrap:dns';
151
+ const CACHE_TTL_SECONDS = 24 * 60 * 60; // 24 hours
152
+
153
+ async function getBootstrapData(): Promise<BootstrapData> {
154
+ // Try to get from Redis first
155
+ const cached = await redis.get(CACHE_KEY);
156
+ if (cached) {
157
+ return JSON.parse(cached);
158
+ }
159
+
160
+ // Fetch fresh data
161
+ const response = await fetch('https://data.iana.org/rdap/dns.json');
162
+ if (!response.ok) {
163
+ throw new Error(`Failed to load bootstrap data: ${response.status} ${response.statusText}`);
164
+ }
165
+ const data: BootstrapData = await response.json();
166
+
167
+ // Store in Redis with TTL
168
+ await redis.setEx(CACHE_KEY, CACHE_TTL_SECONDS, JSON.stringify(data));
169
+
170
+ return data;
171
+ }
172
+
173
+ // Use the cached bootstrap data in lookups
174
+ const bootstrapData = await getBootstrapData();
175
+ const result = await lookup('example.com', {
176
+ customBootstrapData: bootstrapData
177
+ });
178
+ ```
179
+
180
+ #### Example: Filesystem caching
181
+
182
+ ```ts
183
+ import { lookup, type BootstrapData } from 'rdapper';
184
+ import { readFile, writeFile, stat } from 'node:fs/promises';
185
+
186
+ const CACHE_FILE = './cache/rdap-bootstrap.json';
187
+ const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
188
+
189
+ async function getBootstrapData(): Promise<BootstrapData> {
190
+ try {
191
+ // Check if cache file exists and is fresh
192
+ const stats = await stat(CACHE_FILE);
193
+ const age = Date.now() - stats.mtimeMs;
194
+
195
+ if (age < CACHE_TTL_MS) {
196
+ const cached = await readFile(CACHE_FILE, 'utf-8');
197
+ return JSON.parse(cached);
198
+ }
199
+ } catch {
200
+ // Cache file doesn't exist or is unreadable, will fetch fresh
201
+ }
202
+
203
+ // Fetch fresh data
204
+ const response = await fetch('https://data.iana.org/rdap/dns.json');
205
+ if (!response.ok) {
206
+ throw new Error(`Failed to load bootstrap data: ${response.status} ${response.statusText}`);
207
+ }
208
+ const data: BootstrapData = await response.json();
209
+
210
+ // Write to cache file
211
+ await writeFile(CACHE_FILE, JSON.stringify(data, null, 2), 'utf-8');
212
+
213
+ return data;
214
+ }
215
+
216
+ // Use the cached bootstrap data in lookups
217
+ const bootstrapData = await getBootstrapData();
218
+ const result = await lookup('example.com', {
219
+ customBootstrapData: bootstrapData
220
+ });
221
+ ```
222
+
223
+ #### Bootstrap data structure
224
+
225
+ The `BootstrapData` type matches IANA's published format:
226
+
227
+ ```ts
228
+ interface BootstrapData {
229
+ version: string; // e.g., "1.0"
230
+ publication: string; // ISO 8601 timestamp
231
+ description?: string;
232
+ services: string[][][]; // Array of [TLDs, base URLs] tuples
233
+ }
234
+ ```
235
+
236
+ See the full documentation at [RFC 7484 - Finding the Authoritative RDAP Service](https://datatracker.ietf.org/doc/html/rfc7484).
237
+
238
+ **Note**: The bootstrap data structure is stable and rarely changes. IANA updates the _contents_ (server mappings) periodically as TLDs are added or servers change, but a 24-hour cache TTL is typically safe for most applications.
239
+
240
+ ### Custom Fetch Implementation
241
+
242
+ For advanced use cases, rdapper allows you to provide a custom `fetch` implementation that will be used for **all HTTP requests** in the library. This enables powerful patterns for caching, logging, retry logic, and more.
243
+
244
+ #### What requests are affected?
245
+
246
+ Your custom fetch will be used for:
247
+ - **RDAP bootstrap registry requests** (fetching `dns.json` from IANA, unless `customBootstrapData` is provided)
248
+ - **RDAP domain lookups** (querying RDAP servers for domain data)
249
+ - **RDAP related/entity link requests** (following links to registrar information)
250
+
251
+ #### Why use custom fetch?
252
+
253
+ - **Caching**: Implement sophisticated caching strategies for all RDAP requests
254
+ - **Logging & Monitoring**: Track all outgoing requests and responses
255
+ - **Retry Logic**: Add exponential backoff for failed requests
256
+ - **Rate Limiting**: Control request frequency to respect API limits
257
+ - **Proxies & Authentication**: Route requests through proxies or add auth headers
258
+ - **Testing**: Inject mock responses without network calls
259
+
260
+ #### Example 1: Simple in-memory cache
261
+
262
+ ```ts
263
+ import { lookup } from 'rdapper';
264
+
265
+ const cache = new Map<string, Response>();
266
+
267
+ const cachedFetch: typeof fetch = async (input, init) => {
268
+ const url = typeof input === 'string' ? input : input.toString();
269
+
270
+ // Check cache first
271
+ if (cache.has(url)) {
272
+ console.log('[Cache Hit]', url);
273
+ return cache.get(url)!.clone();
274
+ }
275
+
276
+ // Fetch and cache
277
+ console.log('[Cache Miss]', url);
278
+ const response = await fetch(input, init);
279
+ cache.set(url, response.clone());
280
+ return response;
281
+ };
282
+
283
+ const result = await lookup('example.com', { customFetch: cachedFetch });
284
+ ```
285
+
286
+ #### Example 2: Request logging and monitoring
287
+
288
+ ```ts
289
+ import { lookup } from 'rdapper';
290
+
291
+ const loggingFetch: typeof fetch = async (input, init) => {
292
+ const url = typeof input === 'string' ? input : input.toString();
293
+ const start = Date.now();
294
+
295
+ console.log(`[→] ${init?.method || 'GET'} ${url}`);
296
+
297
+ try {
298
+ const response = await fetch(input, init);
299
+ const duration = Date.now() - start;
300
+ console.log(`[←] ${response.status} ${url} (${duration}ms)`);
301
+ return response;
302
+ } catch (error) {
303
+ const duration = Date.now() - start;
304
+ console.error(`[✗] ${url} failed after ${duration}ms:`, error);
305
+ throw error;
306
+ }
307
+ };
308
+
309
+ const result = await lookup('example.com', { customFetch: loggingFetch });
310
+ ```
311
+
312
+ #### Example 3: Retry logic with exponential backoff
313
+
314
+ ```ts
315
+ import { lookup } from 'rdapper';
316
+
317
+ async function fetchWithRetry(
318
+ input: RequestInfo | URL,
319
+ init?: RequestInit,
320
+ maxRetries = 3
321
+ ): Promise<Response> {
322
+ let lastError: Error | undefined;
323
+
324
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
325
+ try {
326
+ const response = await fetch(input, init);
327
+
328
+ // Retry on 5xx errors
329
+ if (response.status >= 500 && attempt < maxRetries) {
330
+ const delay = Math.min(1000 * 2 ** attempt, 10000);
331
+ console.log(`Retrying after ${delay}ms (attempt ${attempt + 1}/${maxRetries})`);
332
+ await new Promise(resolve => setTimeout(resolve, delay));
333
+ continue;
334
+ }
335
+
336
+ return response;
337
+ } catch (error) {
338
+ lastError = error as Error;
339
+ if (attempt < maxRetries) {
340
+ const delay = Math.min(1000 * 2 ** attempt, 10000);
341
+ await new Promise(resolve => setTimeout(resolve, delay));
342
+ continue;
343
+ }
344
+ }
345
+ }
346
+
347
+ throw lastError || new Error('Max retries exceeded');
348
+ }
349
+
350
+ const result = await lookup('example.com', { customFetch: fetchWithRetry });
351
+ ```
352
+
353
+ #### Example 4: HTTP caching with cache-control headers
354
+
355
+ ```ts
356
+ import { lookup } from 'rdapper';
357
+
358
+ interface CachedResponse {
359
+ response: Response;
360
+ expiresAt: number;
361
+ }
362
+
363
+ const httpCache = new Map<string, CachedResponse>();
364
+
365
+ const httpCachingFetch: typeof fetch = async (input, init) => {
366
+ const url = typeof input === 'string' ? input : input.toString();
367
+ const now = Date.now();
368
+
369
+ // Check if we have a valid cached response
370
+ const cached = httpCache.get(url);
371
+ if (cached && cached.expiresAt > now) {
372
+ return cached.response.clone();
373
+ }
374
+
375
+ // Fetch fresh response
376
+ const response = await fetch(input, init);
377
+
378
+ // Parse Cache-Control header
379
+ const cacheControl = response.headers.get('cache-control');
380
+ if (cacheControl) {
381
+ const maxAgeMatch = cacheControl.match(/max-age=(\d+)/);
382
+ if (maxAgeMatch) {
383
+ const maxAge = parseInt(maxAgeMatch[1], 10);
384
+ httpCache.set(url, {
385
+ response: response.clone(),
386
+ expiresAt: now + maxAge * 1000,
387
+ });
388
+ }
389
+ }
390
+
391
+ return response;
392
+ };
393
+
394
+ const result = await lookup('example.com', { customFetch: httpCachingFetch });
395
+ ```
396
+
397
+ #### Example 5: Combining with customBootstrapData
398
+
399
+ You can use both `customFetch` and `customBootstrapData` together for maximum control:
400
+
401
+ ```ts
402
+ import { lookup, type BootstrapData } from 'rdapper';
403
+
404
+ // Pre-load bootstrap data (no fetch needed for this)
405
+ const bootstrapData: BootstrapData = await getFromCache('bootstrap');
406
+
407
+ // Use custom fetch for all other RDAP requests
408
+ const cachedFetch: typeof fetch = async (input, init) => {
409
+ // Your caching logic for RDAP domain and entity lookups
410
+ return fetch(input, init);
411
+ };
412
+
413
+ const result = await lookup('example.com', {
414
+ customBootstrapData: bootstrapData,
415
+ customFetch: cachedFetch,
416
+ });
417
+ ```
418
+
419
+ **Note**: When `customBootstrapData` is provided, the bootstrap registry will not be fetched, so your custom fetch will only be used for RDAP domain and entity/related link requests.
420
+
89
421
  ### Options
90
422
 
91
423
  - `timeoutMs?: number` – Total timeout budget per network operation (default `15000`).
@@ -96,7 +428,9 @@ const res = await lookup("example.com", { rdapOnly: true });
96
428
  - `rdapFollowLinks?: boolean` – Follow related/entity RDAP links to enrich data (default `true`).
97
429
  - `maxRdapLinkHops?: number` – Maximum RDAP related link hops to follow (default `2`).
98
430
  - `rdapLinkRels?: string[]` – RDAP link rel values to consider (default `["related","entity","registrar","alternate"]`).
99
- - `customBootstrapUrl?: string` – Override RDAP bootstrap URL.
431
+ - `customBootstrapData?: BootstrapData` – Pre-loaded RDAP bootstrap data for caching control (see [Bootstrap Data Caching](#bootstrap-data-caching)).
432
+ - `customBootstrapUrl?: string` – Override RDAP bootstrap URL (ignored if `customBootstrapData` is provided).
433
+ - `customFetch?: FetchLike` – Custom fetch implementation for all HTTP requests (see [Custom Fetch Implementation](#custom-fetch-implementation)).
100
434
  - `whoisHints?: Record<string, string>` – Override/add authoritative WHOIS per TLD (keys are lowercase TLDs, values may include or omit `whois://`).
101
435
  - `includeRaw?: boolean` – Include `rawRdap`/`rawWhois` in the returned record (default `false`).
102
436
  - `signal?: AbortSignal` – Optional cancellation signal.
package/dist/index.d.ts CHANGED
@@ -1,14 +1,39 @@
1
1
  import { parse } from "tldts";
2
2
 
3
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
+ */
4
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
+ */
5
18
  interface RegistrarInfo {
19
+ /** Registrar name (e.g., "GoDaddy.com, LLC") */
6
20
  name?: string;
21
+ /** IANA-assigned registrar ID */
7
22
  ianaId?: string;
23
+ /** Registrar website URL */
8
24
  url?: string;
25
+ /** Registrar contact email address */
9
26
  email?: string;
27
+ /** Registrar contact phone number */
10
28
  phone?: string;
11
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
+ */
12
37
  interface Contact {
13
38
  type: "registrant" | "admin" | "tech" | "billing" | "abuse" | "registrar" | "reseller" | "unknown";
14
39
  name?: string;
@@ -23,16 +48,64 @@ interface Contact {
23
48
  country?: string;
24
49
  countryCode?: string;
25
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
+ */
26
57
  interface Nameserver {
58
+ /** Nameserver hostname (e.g., "ns1.example.com") */
27
59
  host: string;
60
+ /** IPv4 glue records, if provided */
28
61
  ipv4?: string[];
62
+ /** IPv6 glue records, if provided */
29
63
  ipv6?: string[];
30
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
+ */
31
76
  interface StatusEvent {
77
+ /** Normalized status code (e.g., "clientTransferProhibited") */
32
78
  status: string;
79
+ /** Human-readable description of the status, if available */
33
80
  description?: string;
81
+ /** Original raw status string from the source */
34
82
  raw?: string;
35
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
+ */
36
109
  interface DomainRecord {
37
110
  /** Normalized domain name */
38
111
  domain: string;
@@ -93,6 +166,65 @@ interface DomainRecord {
93
166
  /** Warnings generated during lookup */
94
167
  warnings?: string[];
95
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
+ */
96
228
  interface LookupOptions {
97
229
  /** Total timeout budget */
98
230
  timeoutMs?: number;
@@ -110,8 +242,83 @@ interface LookupOptions {
110
242
  maxRdapLinkHops?: number;
111
243
  /** RDAP link rels to consider (default ["related","entity","registrar","alternate"]) */
112
244
  rdapLinkRels?: string[];
113
- /** Override IANA bootstrap */
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) */
114
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;
115
322
  /** Override/add authoritative WHOIS per TLD */
116
323
  whoisHints?: Record<string, string>;
117
324
  /** Include rawRdap/rawWhois in results (default false) */
@@ -119,12 +326,40 @@ interface LookupOptions {
119
326
  /** Optional cancellation signal */
120
327
  signal?: AbortSignal;
121
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
+ */
122
348
  interface LookupResult {
349
+ /** Whether the lookup completed successfully */
123
350
  ok: boolean;
351
+ /** The normalized domain record, present when ok is true */
124
352
  record?: DomainRecord;
353
+ /** Error message describing why the lookup failed, present when ok is false */
125
354
  error?: string;
126
355
  }
127
- type FetchLike = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
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>;
128
363
  //#endregion
129
364
  //#region src/lib/domain.d.ts
130
365
  type ParseOptions = Parameters<typeof parse>[1];
@@ -171,4 +406,5 @@ declare function isRegistered(domain: string, opts?: LookupOptions): Promise<boo
171
406
  */
172
407
  declare const lookupDomain: typeof lookup;
173
408
  //#endregion
174
- export { Contact, DomainRecord, FetchLike, LookupOptions, LookupResult, LookupSource, Nameserver, RegistrarInfo, StatusEvent, getDomainParts, getDomainTld, isAvailable, isLikelyDomain, isRegistered, lookup, lookupDomain, toRegistrableDomain };
409
+ export { BootstrapData, Contact, DomainRecord, FetchLike, LookupOptions, LookupResult, LookupSource, Nameserver, RegistrarInfo, StatusEvent, getDomainParts, getDomainTld, isAvailable, isLikelyDomain, isRegistered, lookup, lookupDomain, toRegistrableDomain };
410
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","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"}
package/dist/index.js CHANGED
@@ -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
@@ -251,6 +316,7 @@ function toISO(dateLike) {
251
316
  /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})(?:Z|([+-]\d{2})(?::?(\d{2}))?)?$/,
252
317
  /^(\d{4})\/(\d{2})\/(\d{2})[ T](\d{2}):(\d{2}):(\d{2})(?:Z|([+-]\d{2})(?::?(\d{2}))?)?$/,
253
318
  /^(\d{2})-([A-Za-z]{3})-(\d{4})$/,
319
+ /^(\d{2})-(\d{2})-(\d{4})$/,
254
320
  /^([A-Za-z]{3})\s+(\d{1,2})\s+(\d{4})$/
255
321
  ]) {
256
322
  const m = raw.match(re);
@@ -286,6 +352,7 @@ function parseDateWithRegex(m, _re) {
286
352
  try {
287
353
  if (m[0].includes(":")) {
288
354
  const [_$1, y, mo, d, hh, mm, ss, offH, offM] = m;
355
+ if (!y || !mo || !d || !hh || !mm || !ss) return void 0;
289
356
  let dt = Date.UTC(Number(y), Number(mo) - 1, Number(d), Number(hh), Number(mm), Number(ss));
290
357
  if (offH) {
291
358
  const sign = offH.startsWith("-") ? -1 : 1;
@@ -298,10 +365,13 @@ function parseDateWithRegex(m, _re) {
298
365
  }
299
366
  if (m[0].includes("-")) {
300
367
  const [_$1, dd$1, monStr$1, yyyy$1] = m;
368
+ if (!monStr$1 || !dd$1 || !yyyy$1) return void 0;
369
+ if (/^\d+$/.test(monStr$1)) return new Date(Date.UTC(Number(yyyy$1), Number(monStr$1) - 1, Number(dd$1)));
301
370
  const mon$1 = monthMap[monStr$1.toLowerCase()];
302
371
  return new Date(Date.UTC(Number(yyyy$1), mon$1, Number(dd$1)));
303
372
  }
304
373
  const [_, monStr, dd, yyyy] = m;
374
+ if (!monStr || !dd || !yyyy) return void 0;
305
375
  const mon = monthMap[monStr.toLowerCase()];
306
376
  return new Date(Date.UTC(Number(yyyy), mon, Number(dd)));
307
377
  } catch {}
@@ -360,7 +430,7 @@ function parseKeyValueLines(text) {
360
430
  const line = rawLine.replace(/\s+$/, "");
361
431
  if (!line.trim()) continue;
362
432
  const bracket = line.match(/^\s*\[([^\]]+)\]\s*(.*)$/);
363
- if (bracket) {
433
+ if (bracket?.[1] !== void 0 && bracket?.[2] !== void 0) {
364
434
  const key = bracket[1].trim().toLowerCase();
365
435
  const value = bracket[2].trim();
366
436
  const list = map.get(key) ?? [];
@@ -974,10 +1044,13 @@ function normalizeWhois(domain, tld, whoisText, whoisServer, includeRaw = false)
974
1044
  };
975
1045
  })();
976
1046
  const statusLines = map["domain status"] || map.status || map.flags || map.state || map["registration status"] || map.eppstatus || [];
977
- const statuses = statusLines.length ? statusLines.map((line) => ({
978
- status: line.split(/\s+/)[0],
979
- raw: line
980
- })) : void 0;
1047
+ const statuses = statusLines.length ? statusLines.map((line) => {
1048
+ const status = line.split(/\s+/)[0];
1049
+ return status ? {
1050
+ status,
1051
+ raw: line
1052
+ } : null;
1053
+ }).filter((s) => s !== null) : void 0;
981
1054
  const nsLines = [
982
1055
  ...map["name server"] || [],
983
1056
  ...map.nameserver || [],
@@ -1013,7 +1086,7 @@ function normalizeWhois(domain, tld, whoisText, whoisServer, includeRaw = false)
1013
1086
  const privacyEnabled = !!(registrant && [registrant.name, registrant.organization].filter(Boolean).some(isPrivacyName));
1014
1087
  const dnssecRaw = (map.dnssec?.[0] || "").toLowerCase();
1015
1088
  const dnssec = dnssecRaw ? { enabled: /signed|yes|true/.test(dnssecRaw) } : void 0;
1016
- const transferLock = !!statuses?.some((s) => /transferprohibited/i.test(s.status));
1089
+ const transferLock = !!statuses?.some((s) => s.status && /transferprohibited/i.test(s.status));
1017
1090
  return {
1018
1091
  domain,
1019
1092
  tld,
@@ -1264,6 +1337,10 @@ async function lookup(domain, opts) {
1264
1337
  };
1265
1338
  }
1266
1339
  const [first, ...rest] = chain.map((r) => normalizeWhois(domain, tld, r.text, r.serverQueried, !!opts?.includeRaw));
1340
+ if (!first) return {
1341
+ ok: false,
1342
+ error: "No WHOIS data retrieved"
1343
+ };
1267
1344
  return {
1268
1345
  ok: true,
1269
1346
  record: rest.length ? mergeWhoisRecords(first, rest) : first
@@ -1299,4 +1376,5 @@ async function isRegistered(domain, opts) {
1299
1376
  const lookupDomain = lookup;
1300
1377
 
1301
1378
  //#endregion
1302
- export { getDomainParts, getDomainTld, isAvailable, isLikelyDomain, isRegistered, lookup, lookupDomain, toRegistrableDomain };
1379
+ export { getDomainParts, getDomainTld, isAvailable, isLikelyDomain, isRegistered, lookup, lookupDomain, toRegistrableDomain };
1380
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","names":["timer: ReturnType<typeof setTimeout> | undefined","data: BootstrapData","err: unknown","bases: string[]","out: string[]","merged: Json","tried: string[]","fetchedDocs: unknown[]","out: T[]","monthMap: Record<string, number>","_","dd","monStr","yyyy","mon","lastKey: string | undefined","ldhName: string | undefined","unicodeName: string | undefined","registrar: RegistrarInfo | undefined","nameservers: Nameserver[] | undefined","n: Nameserver","contacts: Contact[] | undefined","events: RdapEvent[]","whoisServer: string | undefined","out: Contact[]","roles: string[]","out: ParsedVCard","WHOIS_QUERY_TRANSFORMERS: Record<string, (query: string) => string>","net: typeof import(\"node:net\") | null","out: NonNullable<DomainRecord[\"statuses\"]>","out: Contact[]","merged: DomainRecord","WHOIS_AVAILABLE_PATTERNS: RegExp[]","registrar: RegistrarInfo | undefined","nsLines: string[]","nameservers: Nameserver[] | undefined","ipv4: string[]","ipv6: string[]","ns: Nameserver","roles: Array<{\n role: Contact[\"type\"];\n prefixes: string[];\n }>","contacts: Contact[]","nameKeys: string[]","orgKeys: string[]","emailKeys: string[]","phoneKeys: string[]","faxKeys: string[]","streetKeys: string[]","cityKeys: string[]","stateKeys: string[]","postalCodeKeys: string[]","countryKeys: string[]","results: WhoisQueryResult[]","tried: string[]","err: unknown"],"sources":["../src/lib/domain.ts","../src/lib/async.ts","../src/lib/constants.ts","../src/lib/fetch.ts","../src/rdap/bootstrap.ts","../src/rdap/client.ts","../src/rdap/links.ts","../src/rdap/merge.ts","../src/lib/dates.ts","../src/lib/privacy.ts","../src/lib/text.ts","../src/rdap/normalize.ts","../src/whois/client.ts","../src/whois/servers.ts","../src/whois/discovery.ts","../src/whois/merge.ts","../src/whois/normalize.ts","../src/whois/referral.ts","../src/index.ts"],"sourcesContent":["import { parse } from \"tldts\";\n\ntype ParseOptions = Parameters<typeof parse>[1];\n\n/**\n * Parse a domain into its parts. Passes options to `tldts.parse()`.\n * @see https://github.com/remusao/tldts/blob/master/packages/tldts-core/src/options.ts\n */\nexport function getDomainParts(\n domain: string,\n opts?: ParseOptions,\n): ReturnType<typeof parse> {\n return parse(domain, { ...opts });\n}\n\n/**\n * Get the TLD (ICANN-only public suffix) of a domain. Passes options to `tldts.parse()`.\n * @see https://github.com/remusao/tldts/blob/master/packages/tldts-core/src/options.ts\n */\nexport function getDomainTld(\n domain: string,\n opts?: ParseOptions,\n): string | null {\n const result = getDomainParts(domain, {\n allowPrivateDomains: false,\n ...opts,\n });\n return result.publicSuffix ?? null;\n}\n\n/**\n * Basic domain validity check (hostname-like), not performing DNS or RDAP.\n */\nexport function isLikelyDomain(value: string): boolean {\n const v = (value ?? \"\").trim();\n // Accept punycoded labels (xn--) by allowing digits and hyphens in TLD as well,\n // while disallowing leading/trailing hyphens in any label.\n return /^(?=.{1,253}$)(?:(?!-)[a-z0-9-]{1,63}(?<!-)\\.)+(?!-)[a-z0-9-]{2,63}(?<!-)$/.test(\n v.toLowerCase(),\n );\n}\n\nexport function punyToUnicode(domain: string): string {\n try {\n return domain.normalize(\"NFC\");\n } catch {\n return domain;\n }\n}\n\n/**\n * Normalize arbitrary input (domain or URL) to its registrable domain (eTLD+1).\n * Passes options to `tldts.parse()`.\n * Returns null when the input is not a valid ICANN domain (e.g., invalid TLD, IPs)\n * @see https://github.com/remusao/tldts/blob/master/packages/tldts-core/src/options.ts\n */\nexport function toRegistrableDomain(\n input: string,\n opts?: ParseOptions,\n): string | null {\n const raw = (input ?? \"\").trim();\n if (raw === \"\") return null;\n\n const result = getDomainParts(raw, {\n allowPrivateDomains: false,\n ...opts,\n });\n\n // Reject IPs and non-ICANN/public suffixes.\n if (result.isIp) return null;\n if (!result.isIcann) return null;\n\n const domain = result.domain ?? \"\";\n if (domain === \"\") return null;\n return domain.toLowerCase();\n}\n","export function withTimeout<T>(\n promise: Promise<T>,\n timeoutMs: number,\n reason = \"Timeout\",\n): Promise<T> {\n if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return promise;\n let timer: ReturnType<typeof setTimeout> | undefined;\n const timeout = new Promise<never>((_, reject) => {\n timer = setTimeout(() => reject(new Error(reason)), timeoutMs);\n });\n return Promise.race([\n promise.finally(() => {\n if (timer !== undefined) clearTimeout(timer);\n }),\n timeout,\n ]);\n}\n\nexport function sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\n","/**\n * The timeout for HTTP requests in milliseconds. Defaults to 10 seconds.\n */\nexport const DEFAULT_TIMEOUT_MS = 10_000 as const;\n\n/**\n * The default URL for the IANA RDAP bootstrap file.\n *\n * @see {@link https://data.iana.org/rdap/dns.json IANA RDAP Bootstrap File (dns.json)}\n */\nexport const DEFAULT_BOOTSTRAP_URL = \"https://data.iana.org/rdap/dns.json\";\n","import type { FetchLike } from \"../types\";\n\n/**\n * Resolve the fetch implementation to use for HTTP requests.\n *\n * Returns the custom fetch from options if provided, otherwise falls back\n * to the global fetch function. This centralized helper ensures consistent\n * fetch resolution across all RDAP HTTP operations.\n *\n * Used internally by:\n * - Bootstrap registry fetching (`src/rdap/bootstrap.ts`)\n * - RDAP domain lookups (`src/rdap/client.ts`)\n * - RDAP related/entity link requests (`src/rdap/merge.ts`)\n *\n * @param options - Any object that may contain a custom fetch implementation\n * @returns The fetch function to use for HTTP requests\n *\n * @example\n * ```ts\n * import { resolveFetch } from './lib/fetch';\n *\n * const fetchFn = resolveFetch(options);\n * const response = await fetchFn('https://example.com/api', { method: 'GET' });\n * ```\n */\nexport function resolveFetch(options?: { customFetch?: FetchLike }): FetchLike {\n return options?.customFetch ?? fetch;\n}\n","import { withTimeout } from \"../lib/async\";\nimport { DEFAULT_BOOTSTRAP_URL, DEFAULT_TIMEOUT_MS } from \"../lib/constants\";\nimport { resolveFetch } from \"../lib/fetch\";\nimport type { BootstrapData, LookupOptions } from \"../types\";\n\n/**\n * Resolve RDAP base URLs for a given TLD using IANA's bootstrap registry.\n * Returns zero or more base URLs (always suffixed with a trailing slash).\n *\n * Bootstrap data is resolved in the following priority order:\n * 1. `options.customBootstrapData` - pre-loaded bootstrap data (no fetch)\n * 2. `options.customBootstrapUrl` - custom URL to fetch bootstrap data from\n * 3. Default IANA URL - https://data.iana.org/rdap/dns.json\n *\n * @param tld - The top-level domain to look up (e.g., \"com\", \"co.uk\")\n * @param options - Optional lookup options including custom bootstrap data/URL\n * @returns Array of RDAP base URLs for the TLD, or empty array if none found\n */\nexport async function getRdapBaseUrlsForTld(\n tld: string,\n options?: LookupOptions,\n): Promise<string[]> {\n let data: BootstrapData;\n\n // Priority 1: Use pre-loaded bootstrap data if provided (no fetch)\n if (options && \"customBootstrapData\" in options) {\n const provided = options.customBootstrapData;\n // Validate the structure to provide helpful error messages\n if (!provided || typeof provided !== \"object\") {\n throw new Error(\n \"Invalid customBootstrapData: expected an object. See BootstrapData type for required structure.\",\n );\n }\n if (!Array.isArray(provided.services)) {\n throw new Error(\n 'Invalid customBootstrapData: missing or invalid \"services\" array. See BootstrapData type for required structure.',\n );\n }\n provided.services.forEach((svc, idx) => {\n if (\n !Array.isArray(svc) ||\n svc.length < 2 ||\n !Array.isArray(svc[0]) ||\n !Array.isArray(svc[1])\n ) {\n throw new Error(\n `Invalid customBootstrapData: services[${idx}] must be a tuple of [string[], string[]].`,\n );\n }\n });\n data = provided;\n } else {\n // Priority 2 & 3: Fetch from custom URL or default IANA URL\n // Use custom fetch implementation if provided for caching/logging/monitoring\n const fetchFn = resolveFetch(options);\n const bootstrapUrl = options?.customBootstrapUrl ?? DEFAULT_BOOTSTRAP_URL;\n try {\n const res = await withTimeout(\n fetchFn(bootstrapUrl, {\n method: \"GET\",\n headers: { accept: \"application/json\" },\n signal: options?.signal,\n }),\n options?.timeoutMs ?? DEFAULT_TIMEOUT_MS,\n \"RDAP bootstrap timeout\",\n );\n if (!res.ok) return [];\n data = (await res.json()) as BootstrapData;\n } catch (err: unknown) {\n // Preserve caller cancellation behavior - rethrow if explicitly aborted\n if (err instanceof Error && err.name === \"AbortError\") {\n throw err;\n }\n // Network, timeout, or JSON parse errors - return empty array to fall back to WHOIS\n return [];\n }\n }\n\n // Parse the bootstrap data to find matching base URLs for the TLD\n const target = tld.toLowerCase();\n const bases: string[] = [];\n for (const svc of data.services) {\n if (!svc[0] || !svc[1]) continue;\n const tlds = svc[0].map((x) => x.toLowerCase());\n const urls = svc[1];\n // Match exact TLD, and also support multi-label public suffixes present in IANA (rare)\n if (tlds.includes(target)) {\n for (const u of urls) {\n const base = u.endsWith(\"/\") ? u : `${u}/`;\n bases.push(base);\n }\n }\n }\n return Array.from(new Set(bases));\n}\n","import { withTimeout } from \"../lib/async\";\nimport { DEFAULT_TIMEOUT_MS } from \"../lib/constants\";\nimport { resolveFetch } from \"../lib/fetch\";\nimport type { LookupOptions } from \"../types\";\n\n/**\n * Fetch RDAP JSON for a domain from a specific RDAP base URL.\n * Throws on HTTP >= 400 (includes RDAP error JSON payloads).\n */\nexport async function fetchRdapDomain(\n domain: string,\n baseUrl: string,\n options?: LookupOptions,\n): Promise<{ url: string; json: unknown }> {\n const url = new URL(\n `domain/${encodeURIComponent(domain)}`,\n baseUrl,\n ).toString();\n const fetchFn = resolveFetch(options);\n const res = await withTimeout(\n fetchFn(url, {\n method: \"GET\",\n headers: { accept: \"application/rdap+json, application/json\" },\n signal: options?.signal,\n }),\n options?.timeoutMs ?? DEFAULT_TIMEOUT_MS,\n \"RDAP lookup timeout\",\n );\n if (!res.ok) {\n const bodyText = await res.text();\n throw new Error(`RDAP ${res.status}: ${bodyText.slice(0, 500)}`);\n }\n const json = await res.json();\n return { url, json };\n}\n","import type { LookupOptions } from \"../types\";\n\ntype RdapLink = {\n value?: string;\n rel?: string;\n href?: string;\n type?: string;\n};\n\n/** Extract candidate RDAP link URLs from an RDAP document. */\nexport function extractRdapRelatedLinks(\n doc: unknown,\n opts?: Pick<LookupOptions, \"rdapLinkRels\">,\n): string[] {\n const rels = (\n opts?.rdapLinkRels?.length\n ? opts.rdapLinkRels\n : [\"related\", \"entity\", \"registrar\", \"alternate\"]\n ).map((r) => r.toLowerCase());\n const d = (doc ?? {}) as Record<string, unknown> & { links?: RdapLink[] };\n const arr = Array.isArray(d?.links) ? (d.links as RdapLink[]) : [];\n const out: string[] = [];\n for (const link of arr) {\n const rel = String(link.rel || \"\").toLowerCase();\n const type = String(link.type || \"\").toLowerCase();\n if (!rels.includes(rel)) continue;\n if (type && !/application\\/rdap\\+json/i.test(type)) continue;\n const url = link.href || link.value;\n if (url && /^https?:\\/\\//i.test(url)) out.push(url);\n }\n return Array.from(new Set(out));\n}\n","import { withTimeout } from \"../lib/async\";\nimport { DEFAULT_TIMEOUT_MS } from \"../lib/constants\";\nimport { resolveFetch } from \"../lib/fetch\";\nimport type { LookupOptions } from \"../types\";\nimport { extractRdapRelatedLinks } from \"./links\";\n\ntype Json = Record<string, unknown>;\n\n/** Merge RDAP documents with a conservative, additive strategy. */\nexport function mergeRdapDocs(baseDoc: unknown, others: unknown[]): unknown {\n const merged: Json = { ...(baseDoc as Json) };\n for (const doc of others) {\n const cur = (doc ?? {}) as Json;\n // status: array of strings\n merged.status = uniqStrings([\n ...toStringArray(merged.status),\n ...toStringArray(cur.status),\n ]);\n // events: array of objects; dedupe by eventAction + eventDate\n merged.events = uniqBy(\n [...toArray<Json>(merged.events), ...toArray<Json>(cur.events)],\n (e) =>\n `${String(e?.eventAction ?? \"\").toLowerCase()}|${String(e?.eventDate ?? \"\")}`,\n );\n // nameservers: array of objects; dedupe by ldhName/unicodeName\n merged.nameservers = uniqBy(\n [...toArray<Json>(merged.nameservers), ...toArray<Json>(cur.nameservers)],\n (n) => `${String(n?.ldhName ?? n?.unicodeName ?? \"\").toLowerCase()}`,\n );\n // entities: array; dedupe by handle if present, else by roles+vcard hash\n merged.entities = uniqBy(\n [...toArray<Json>(merged.entities), ...toArray<Json>(cur.entities)],\n (e) =>\n `${String(e?.handle ?? \"\").toLowerCase()}|${String(\n JSON.stringify(e?.roles || []),\n ).toLowerCase()}|${String(JSON.stringify(e?.vcardArray || [])).toLowerCase()}`,\n );\n // secureDNS: prefer existing; fill if missing\n if (merged.secureDNS == null && cur.secureDNS != null)\n merged.secureDNS = cur.secureDNS;\n // port43 (authoritative WHOIS): prefer existing; fill if missing\n if (merged.port43 == null && cur.port43 != null) merged.port43 = cur.port43;\n // remarks: concat simple strings if present\n const mergedRemarks = (merged as { remarks?: Json[] }).remarks;\n const curRemarks = (cur as { remarks?: Json[] }).remarks;\n if (Array.isArray(mergedRemarks) || Array.isArray(curRemarks)) {\n const a = toArray<Json>(mergedRemarks);\n const b = toArray<Json>(curRemarks);\n (merged as { remarks?: Json[] }).remarks = [...a, ...b];\n }\n }\n return merged;\n}\n\n/** Fetch and merge RDAP related documents up to a hop limit. */\nexport async function fetchAndMergeRdapRelated(\n domain: string,\n baseDoc: unknown,\n opts?: LookupOptions,\n): Promise<{ merged: unknown; serversTried: string[] }> {\n const tried: string[] = [];\n if (opts?.rdapFollowLinks === false)\n return { merged: baseDoc, serversTried: tried };\n const maxHops = Math.max(0, opts?.maxRdapLinkHops ?? 2);\n if (maxHops === 0) return { merged: baseDoc, serversTried: tried };\n\n const visited = new Set<string>();\n let current = baseDoc;\n let hops = 0;\n\n // BFS: collect links from the latest merged doc only to keep it simple and bounded\n while (hops < maxHops) {\n const links = extractRdapRelatedLinks(current, {\n rdapLinkRels: opts?.rdapLinkRels,\n });\n const nextBatch = links.filter((u) => !visited.has(u));\n if (nextBatch.length === 0) break;\n const fetchedDocs: unknown[] = [];\n for (const url of nextBatch) {\n visited.add(url);\n try {\n const { json } = await fetchRdapUrl(url, opts);\n tried.push(url);\n // only accept docs that appear related to the same domain when possible\n // if ldhName/unicodeName present, they should match the queried domain (case-insensitive)\n const ldh = String((json as Json)?.ldhName ?? \"\").toLowerCase();\n const uni = String((json as Json)?.unicodeName ?? \"\").toLowerCase();\n if (ldh && !sameDomain(ldh, domain)) continue;\n if (uni && !sameDomain(uni, domain)) continue;\n fetchedDocs.push(json);\n } catch {\n // ignore failures and continue\n }\n }\n if (fetchedDocs.length === 0) break;\n current = mergeRdapDocs(current, fetchedDocs);\n hops += 1;\n }\n return { merged: current, serversTried: tried };\n}\n\nasync function fetchRdapUrl(\n url: string,\n options?: LookupOptions,\n): Promise<{ url: string; json: unknown }> {\n const fetchFn = resolveFetch(options);\n const res = await withTimeout(\n fetchFn(url, {\n method: \"GET\",\n headers: { accept: \"application/rdap+json, application/json\" },\n signal: options?.signal,\n }),\n options?.timeoutMs ?? DEFAULT_TIMEOUT_MS,\n \"RDAP link fetch timeout\",\n );\n if (!res.ok) {\n const bodyText = await res.text();\n throw new Error(`RDAP ${res.status}: ${bodyText.slice(0, 500)}`);\n }\n const json = await res.json();\n // Optionally parse Link header for future iterations; the main loop inspects body.links\n return { url, json };\n}\n\nfunction toArray<T>(val: unknown): T[] {\n return Array.isArray(val) ? (val as T[]) : [];\n}\nfunction toStringArray(val: unknown): string[] {\n return Array.isArray(val) ? (val as unknown[]).map((v) => String(v)) : [];\n}\nfunction uniqStrings(arr: string[]): string[] {\n return Array.from(new Set(arr));\n}\nfunction uniqBy<T>(arr: T[], key: (t: T) => string): T[] {\n const seen = new Set<string>();\n const out: T[] = [];\n for (const item of arr) {\n const k = key(item);\n if (seen.has(k)) continue;\n seen.add(k);\n out.push(item);\n }\n return out;\n}\nfunction sameDomain(a: string, b: string): boolean {\n return a.toLowerCase() === b.toLowerCase();\n}\n","// Lightweight date parsing helpers to avoid external dependencies.\n// We aim to parse common RDAP and WHOIS date representations and return a UTC ISO string.\nexport function toISO(\n dateLike: string | number | Date | undefined | null,\n): string | undefined {\n if (dateLike == null) return undefined;\n if (dateLike instanceof Date) return toIsoFromDate(dateLike);\n if (typeof dateLike === \"number\") return toIsoFromDate(new Date(dateLike));\n const raw = String(dateLike).trim();\n if (!raw) return undefined;\n // Try several structured formats seen in WHOIS outputs (treat as UTC when no TZ provided)\n const tryFormats = [\n // 2023-01-02 03:04:05Z or without Z\n /^(\\d{4})-(\\d{2})-(\\d{2})[ T](\\d{2}):(\\d{2}):(\\d{2})(?:Z|([+-]\\d{2})(?::?(\\d{2}))?)?$/,\n // 2023/01/02 03:04:05\n /^(\\d{4})\\/(\\d{2})\\/(\\d{2})[ T](\\d{2}):(\\d{2}):(\\d{2})(?:Z|([+-]\\d{2})(?::?(\\d{2}))?)?$/,\n // 02-Jan-2023\n /^(\\d{2})-([A-Za-z]{3})-(\\d{4})$/,\n // 21-07-2026 (DD-MM-YYYY used by .il, .hk)\n /^(\\d{2})-(\\d{2})-(\\d{4})$/,\n // Jan 02 2023\n /^([A-Za-z]{3})\\s+(\\d{1,2})\\s+(\\d{4})$/,\n ];\n for (const re of tryFormats) {\n const m = raw.match(re);\n if (!m) continue;\n const d = parseDateWithRegex(m, re);\n if (d) return toIsoFromDate(d);\n }\n // Fallback to native Date parsing (handles ISO and RFC2822 with TZ)\n const native = new Date(raw);\n if (!Number.isNaN(native.getTime())) return toIsoFromDate(native);\n return undefined;\n}\n\nfunction toIsoFromDate(d: Date): string | undefined {\n try {\n return new Date(\n Date.UTC(\n d.getUTCFullYear(),\n d.getUTCMonth(),\n d.getUTCDate(),\n d.getUTCHours(),\n d.getUTCMinutes(),\n d.getUTCSeconds(),\n 0,\n ),\n )\n .toISOString()\n .replace(/\\.\\d{3}Z$/, \"Z\");\n } catch {\n return undefined;\n }\n}\n\nfunction parseDateWithRegex(\n m: RegExpMatchArray,\n _re: RegExp,\n): Date | undefined {\n const monthMap: Record<string, number> = {\n jan: 0,\n feb: 1,\n mar: 2,\n apr: 3,\n may: 4,\n jun: 5,\n jul: 6,\n aug: 7,\n sep: 8,\n oct: 9,\n nov: 10,\n dec: 11,\n };\n try {\n // If the matched string contains time components, parse as Y-M-D H:M:S\n if (m[0].includes(\":\")) {\n const [_, y, mo, d, hh, mm, ss, offH, offM] = m;\n if (!y || !mo || !d || !hh || !mm || !ss) return undefined;\n // Base time as UTC\n let dt = Date.UTC(\n Number(y),\n Number(mo) - 1,\n Number(d),\n Number(hh),\n Number(mm),\n Number(ss),\n );\n // Apply timezone offset if present (e.g., +0000, -0500, +05:30)\n if (offH) {\n const sign = offH.startsWith(\"-\") ? -1 : 1;\n const hours = Math.abs(Number(offH));\n const minutes = offM ? Number(offM) : 0;\n const offsetMs = sign * (hours * 60 + minutes) * 60 * 1000;\n // The captured time is local with an explicit offset; convert to UTC\n dt -= offsetMs;\n }\n return new Date(dt);\n }\n // If the matched string contains hyphens, check if numeric (DD-MM-YYYY) or alpha (DD-MMM-YYYY)\n if (m[0].includes(\"-\")) {\n const [_, dd, monStr, yyyy] = m;\n if (!monStr || !dd || !yyyy) return undefined;\n // Check if month component is numeric (DD-MM-YYYY) or alphabetic (DD-MMM-YYYY)\n if (/^\\d+$/.test(monStr)) {\n // DD-MM-YYYY format (e.g., 21-07-2026)\n return new Date(Date.UTC(Number(yyyy), Number(monStr) - 1, Number(dd)));\n }\n // DD-MMM-YYYY format (e.g., 02-Jan-2023)\n const mon = monthMap[monStr.toLowerCase()];\n return new Date(Date.UTC(Number(yyyy), mon, Number(dd)));\n }\n // Otherwise treat as MMM DD YYYY\n const [_, monStr, dd, yyyy] = m;\n if (!monStr || !dd || !yyyy) return undefined;\n const mon = monthMap[monStr.toLowerCase()];\n return new Date(Date.UTC(Number(yyyy), mon, Number(dd)));\n } catch {\n // fall through to undefined\n }\n return undefined;\n}\n","export const PRIVACY_NAME_KEYWORDS = [\n \"redacted\",\n \"privacy\",\n \"private\",\n \"withheld\",\n \"not disclosed\",\n \"protected\",\n \"protection\",\n \"privado\", // Spanish\n \"datos privados\", // Spanish\n \"data protected\",\n \"data redacted\",\n \"gdpr redacted\",\n \"gdpr masked\",\n \"non-public data\",\n \"statutory masking\",\n \"redacted.forprivacy\",\n \"registration private\",\n \"hidden upon user request\",\n \"not available from registry\",\n];\n\n// Completely unusable/empty values that should be filtered\nexport const NO_DATA_VALUES = [\n \"-\",\n \".\",\n \"n/a\",\n \"na\",\n \"no data\",\n \"not available\",\n \"not applicable\",\n \"none\",\n];\n\nexport function isPrivacyName(value: string): boolean {\n const v = value.toLowerCase().trim();\n // Check for complete no-data values\n if (NO_DATA_VALUES.includes(v)) return true;\n // Check for privacy keywords\n return PRIVACY_NAME_KEYWORDS.some((k) => v.includes(k));\n}\n","export function uniq<T>(arr: T[] | undefined | null): T[] | undefined {\n if (!arr) return undefined;\n return Array.from(new Set(arr));\n}\n\nexport function parseKeyValueLines(text: string): Record<string, string[]> {\n const map = new Map<string, string[]>();\n const lines = text.split(/\\r?\\n/);\n let lastKey: string | undefined;\n for (const rawLine of lines) {\n const line = rawLine.replace(/\\s+$/, \"\");\n if (!line.trim()) continue;\n // Bracketed form: [Key] value (common in .jp and some ccTLDs)\n const bracket = line.match(/^\\s*\\[([^\\]]+)\\]\\s*(.*)$/);\n if (bracket?.[1] !== undefined && bracket?.[2] !== undefined) {\n const key = bracket[1].trim().toLowerCase();\n const value = bracket[2].trim();\n const list = map.get(key) ?? [];\n if (value) list.push(value);\n map.set(key, list);\n lastKey = key;\n continue;\n }\n // Colon form: Key: value\n const idx = line.indexOf(\":\");\n if (idx !== -1) {\n const key = line.slice(0, idx).trim().toLowerCase();\n const value = line.slice(idx + 1).trim();\n if (!key) {\n lastKey = undefined;\n continue;\n }\n const list = map.get(key) ?? [];\n if (value) list.push(value);\n map.set(key, list);\n lastKey = key;\n continue;\n }\n // Continuation line: starts with indentation after a key appeared\n if (lastKey && /^\\s+/.test(line)) {\n const value = line.trim();\n if (value) {\n const list = map.get(lastKey) ?? [];\n list.push(value);\n map.set(lastKey, list);\n }\n }\n // Otherwise ignore non key-value lines\n }\n return Object.fromEntries(map);\n}\n\nexport function parseCsv(value: string | undefined): string[] | undefined {\n if (!value) return undefined;\n return value\n .split(/[,\\s]+/)\n .map((s) => s.trim())\n .filter(Boolean);\n}\n\nexport function asString(value: unknown): string | undefined {\n return typeof value === \"string\" ? value : undefined;\n}\n\nexport function asStringArray(value: unknown): string[] | undefined {\n return Array.isArray(value)\n ? (value.filter((x) => typeof x === \"string\") as string[])\n : undefined;\n}\n\nexport function asDateLike(value: unknown): string | number | Date | undefined {\n if (\n typeof value === \"string\" ||\n typeof value === \"number\" ||\n value instanceof Date\n )\n return value;\n return undefined;\n}\n","import { toISO } from \"../lib/dates\";\nimport { isPrivacyName } from \"../lib/privacy\";\nimport { asDateLike, asString, asStringArray, uniq } from \"../lib/text\";\nimport type {\n Contact,\n DomainRecord,\n Nameserver,\n RegistrarInfo,\n} from \"../types\";\n\ntype RdapDoc = Record<string, unknown>;\n\n/**\n * Convert RDAP JSON into our normalized DomainRecord.\n * This function is defensive: RDAP servers vary in completeness and field naming.\n */\nexport function normalizeRdap(\n inputDomain: string,\n tld: string,\n rdap: unknown,\n rdapServersTried: string[],\n includeRaw = false,\n): DomainRecord {\n const doc = (rdap ?? {}) as RdapDoc;\n\n // Prefer ldhName (punycode) and unicodeName if provided\n const ldhName: string | undefined =\n asString(doc.ldhName) || asString(doc.handle);\n const unicodeName: string | undefined = asString(doc.unicodeName);\n\n // Registrar entity can be provided with role \"registrar\"\n const registrar: RegistrarInfo | undefined = extractRegistrar(\n doc.entities as unknown,\n );\n\n // Nameservers: normalize host + IPs\n const nameservers: Nameserver[] | undefined = Array.isArray(doc.nameservers)\n ? (doc.nameservers as RdapDoc[])\n .map((ns) => {\n const host = (\n asString(ns.ldhName) ??\n asString(ns.unicodeName) ??\n \"\"\n ).toLowerCase();\n const ip = ns.ipAddresses as RdapDoc | undefined;\n const ipv4 = asStringArray(ip?.v4);\n const ipv6 = asStringArray(ip?.v6);\n const n: Nameserver = { host };\n if (ipv4?.length) n.ipv4 = ipv4;\n if (ipv6?.length) n.ipv6 = ipv6;\n return n;\n })\n .filter((n) => !!n.host)\n : undefined;\n\n // Contacts: RDAP entities include roles like registrant, administrative, technical, billing, abuse\n const contacts: Contact[] | undefined = extractContacts(\n doc.entities as unknown,\n );\n\n // Derive privacy flag from registrant name/org keywords\n const registrant = contacts?.find((c) => c.type === \"registrant\");\n const privacyEnabled = !!(\n registrant &&\n (\n [registrant.name, registrant.organization].filter(Boolean) as string[]\n ).some(isPrivacyName)\n );\n\n // RDAP uses IANA EPP status values. Preserve raw plus a description if any remarks are present.\n const statuses = Array.isArray(doc.status)\n ? (doc.status as unknown[])\n .filter((s): s is string => typeof s === \"string\")\n .map((s) => ({ status: s, raw: s }))\n : undefined;\n\n // Secure DNS info\n const secureDNS = doc.secureDNS as\n | { delegationSigned?: unknown; dsData?: Array<Record<string, unknown>> }\n | undefined;\n const dnssec = secureDNS\n ? {\n enabled: !!secureDNS.delegationSigned,\n dsRecords: Array.isArray(secureDNS.dsData)\n ? (secureDNS.dsData as Array<Record<string, unknown>>).map((d) => ({\n keyTag: d.keyTag as number | undefined,\n algorithm: d.algorithm as number | undefined,\n digestType: d.digestType as number | undefined,\n digest: d.digest as string | undefined,\n }))\n : undefined,\n }\n : undefined;\n\n // RDAP \"events\" contain timestamps for registration, last changed, expiration, deletion, etc.\n type RdapEvent = { eventAction?: string; eventDate?: string | number | Date };\n const events: RdapEvent[] = Array.isArray(doc.events)\n ? (doc.events as unknown[] as RdapEvent[])\n : [];\n const byAction = (action: string) =>\n events.find(\n (e) =>\n typeof e?.eventAction === \"string\" &&\n e.eventAction.toLowerCase().includes(action),\n );\n const creationDate = toISO(\n asDateLike(byAction(\"registration\")?.eventDate) ??\n asDateLike(doc.registrationDate),\n );\n const updatedDate = toISO(\n asDateLike(byAction(\"last changed\")?.eventDate) ??\n asDateLike(doc.lastChangedDate),\n );\n const expirationDate = toISO(\n asDateLike(byAction(\"expiration\")?.eventDate) ??\n asDateLike(doc.expirationDate),\n );\n const deletionDate = toISO(\n asDateLike(byAction(\"deletion\")?.eventDate) ?? asDateLike(doc.deletionDate),\n );\n\n // Derive a simple transfer lock flag from statuses\n const transferLock = !!statuses?.some((s: { status: string }) =>\n /transferprohibited/i.test(s.status),\n );\n\n // The RDAP document may include \"port43\" pointer to authoritative WHOIS\n const whoisServer: string | undefined = asString(doc.port43);\n\n const record: DomainRecord = {\n domain: unicodeName || ldhName || inputDomain,\n tld,\n isRegistered: true,\n isIDN: /(^|\\.)xn--/i.test(ldhName || inputDomain),\n unicodeName: unicodeName || undefined,\n punycodeName: ldhName || undefined,\n registry: undefined, // RDAP rarely includes a clean registry operator name\n registrar: registrar,\n reseller: undefined,\n statuses: statuses,\n creationDate,\n updatedDate,\n expirationDate,\n deletionDate,\n transferLock,\n dnssec,\n nameservers: nameservers\n ? uniq(nameservers.map((n) => ({ ...n, host: n.host.toLowerCase() })))\n : undefined,\n contacts,\n privacyEnabled: privacyEnabled ? true : undefined,\n whoisServer,\n rdapServers: rdapServersTried,\n rawRdap: includeRaw ? rdap : undefined,\n rawWhois: undefined,\n source: \"rdap\",\n warnings: undefined,\n };\n\n return record;\n}\n\nfunction extractRegistrar(entities: unknown): RegistrarInfo | undefined {\n if (!Array.isArray(entities)) return undefined;\n for (const ent of entities) {\n const roles: string[] = Array.isArray((ent as RdapDoc)?.roles)\n ? ((ent as RdapDoc).roles as unknown[]).filter(\n (r): r is string => typeof r === \"string\",\n )\n : [];\n if (!roles.some((r) => /registrar/i.test(r))) continue;\n const v = parseVcard((ent as RdapDoc)?.vcardArray);\n const ianaId = Array.isArray((ent as RdapDoc)?.publicIds)\n ? ((ent as RdapDoc).publicIds as Array<RdapDoc>).find((id) =>\n /iana\\s*registrar\\s*id/i.test(String(id?.type)),\n )?.identifier\n : undefined;\n return {\n name: v.fn || v.org || asString((ent as RdapDoc)?.handle) || undefined,\n ianaId: asString(ianaId),\n url: v.url ?? undefined,\n email: v.email ?? undefined,\n phone: v.tel ?? undefined,\n };\n }\n return undefined;\n}\n\nfunction extractContacts(entities: unknown): Contact[] | undefined {\n if (!Array.isArray(entities)) return undefined;\n const out: Contact[] = [];\n for (const ent of entities) {\n const roles: string[] = Array.isArray((ent as RdapDoc)?.roles)\n ? ((ent as RdapDoc).roles as unknown[]).filter(\n (r): r is string => typeof r === \"string\",\n )\n : [];\n const v = parseVcard((ent as RdapDoc)?.vcardArray);\n const type = roles.find((r) =>\n /registrant|administrative|technical|billing|abuse|reseller/i.test(r),\n );\n if (!type) continue;\n const map: Record<string, Contact[\"type\"]> = {\n registrant: \"registrant\",\n administrative: \"admin\",\n technical: \"tech\",\n billing: \"billing\",\n abuse: \"abuse\",\n reseller: \"reseller\",\n } as const;\n const roleKey = (map[type.toLowerCase()] ?? \"unknown\") as Contact[\"type\"];\n out.push({\n type: roleKey,\n name: v.fn,\n organization: v.org,\n email: v.email,\n phone: v.tel,\n fax: v.fax,\n street: v.street,\n city: v.locality,\n state: v.region,\n postalCode: v.postcode,\n country: v.country,\n countryCode: v.countryCode,\n });\n }\n return out.length ? out : undefined;\n}\n\ninterface ParsedVCard {\n fn?: string;\n org?: string;\n email?: string;\n tel?: string;\n fax?: string;\n url?: string;\n street?: string[];\n locality?: string;\n region?: string;\n postcode?: string;\n country?: string;\n countryCode?: string;\n}\n\n// Parse a minimal subset of vCard 4.0 arrays as used in RDAP \"vcardArray\" fields\nfunction parseVcard(vcardArray: unknown): ParsedVCard {\n // vcardArray is typically [\"vcard\", [[\"version\",{} ,\"text\",\"4.0\"], [\"fn\",{} ,\"text\",\"Example\"], ...]]\n if (\n !Array.isArray(vcardArray) ||\n vcardArray[0] !== \"vcard\" ||\n !Array.isArray(vcardArray[1])\n )\n return {};\n const entries = vcardArray[1] as Array<\n [string, Record<string, unknown>, string, unknown]\n >;\n const out: ParsedVCard = {};\n for (const e of entries) {\n const key = e?.[0];\n const value = e?.[3];\n if (!key) continue;\n switch (String(key).toLowerCase()) {\n case \"fn\":\n out.fn = asString(value);\n break;\n case \"org\":\n out.org = Array.isArray(value)\n ? value.map((x) => String(x)).join(\" \")\n : asString(value);\n break;\n case \"email\":\n out.email = asString(value);\n break;\n case \"tel\":\n out.tel = asString(value);\n break;\n case \"url\":\n out.url = asString(value);\n break;\n case \"adr\": {\n // adr value is [postOfficeBox, extendedAddress, street, locality, region, postalCode, country]\n if (Array.isArray(value)) {\n out.street = value[2] ? String(value[2]).split(/\\n|,\\s*/) : undefined;\n out.locality = asString(value[3]);\n out.region = asString(value[4]);\n out.postcode = asString(value[5]);\n out.country = asString(value[6]);\n }\n break;\n }\n }\n }\n // Best effort country code from country name (often omitted). Leaving undefined unless explicitly provided.\n return out;\n}\n","import { withTimeout } from \"../lib/async\";\nimport { DEFAULT_TIMEOUT_MS } from \"../lib/constants\";\nimport type { LookupOptions } from \"../types\";\n\nexport interface WhoisQueryResult {\n serverQueried: string;\n text: string;\n}\n\n/**\n * Some WHOIS servers default to non-English responses. This mapping allows automatic\n * query transformation to request English-only output for easier parsing.\n *\n * To add new servers: Add an entry with the hostname and transformation function:\n * \"whois.example.org\": (query) => `${query}/english`,\n */\nconst WHOIS_QUERY_TRANSFORMERS: Record<string, (query: string) => string> = {\n \"whois.jprs.jp\": (query) => `${query}/e`, // Append /e for English-only response\n};\n\n/**\n * Perform a WHOIS query against an RFC 3912 server over TCP 43.\n * Returns the raw text and the server used.\n */\nexport async function whoisQuery(\n server: string,\n query: string,\n options?: LookupOptions,\n): Promise<WhoisQueryResult> {\n const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;\n const port = 43;\n const host = server.replace(/^whois:\\/\\//i, \"\");\n\n // Transform query if server requires special formatting\n const transformer = WHOIS_QUERY_TRANSFORMERS[host];\n const transformedQuery = transformer ? transformer(query) : query;\n\n const text = await withTimeout(\n queryTcp(host, port, transformedQuery, options),\n timeoutMs,\n \"WHOIS timeout\",\n );\n return { serverQueried: server, text };\n}\n\n// Low-level WHOIS TCP client. Some registries require CRLF after the domain query.\nasync function queryTcp(\n host: string,\n port: number,\n query: string,\n options?: LookupOptions,\n): Promise<string> {\n let net: typeof import(\"node:net\") | null;\n try {\n net = await import(\"node:net\");\n } catch {\n net = null;\n }\n\n if (!net?.createConnection) {\n throw new Error(\n \"WHOIS client is only available in Node.js runtimes; try setting `rdapOnly: true`.\",\n );\n }\n\n return new Promise((resolve, reject) => {\n const socket = net.createConnection({ host, port });\n let data = \"\";\n let done = false;\n const cleanup = () => {\n if (done) return;\n done = true;\n socket.destroy();\n };\n socket.setTimeout((options?.timeoutMs ?? DEFAULT_TIMEOUT_MS) - 1000, () => {\n cleanup();\n reject(new Error(\"WHOIS socket timeout\"));\n });\n socket.on(\"error\", (err) => {\n cleanup();\n reject(err);\n });\n socket.on(\"data\", (chunk) => {\n data += chunk.toString(\"utf8\");\n });\n socket.on(\"end\", () => {\n cleanup();\n resolve(data);\n });\n socket.on(\"connect\", () => {\n socket.write(`${query}\\r\\n`);\n });\n });\n}\n","// Curated authoritative WHOIS servers by TLD (exceptions to default/referral logic)\n// Source of truth checked against IANA delegation records; prefer RDAP first.\nexport const WHOIS_TLD_EXCEPTIONS = {\n // gTLDs (port-43 still available at registry)\n com: \"whois.verisign-grs.com\",\n net: \"whois.verisign-grs.com\",\n org: \"whois.publicinterestregistry.org\", // PIR\n biz: \"whois.nic.biz\",\n name: \"whois.nic.name\",\n edu: \"whois.educause.edu\",\n gov: \"whois.nic.gov\", // was whois.dotgov.gov\n\n // ccTLDs & other TLDs with working port-43 WHOIS\n de: \"whois.denic.de\",\n jp: \"whois.jprs.jp\",\n fr: \"whois.nic.fr\",\n it: \"whois.nic.it\",\n pl: \"whois.dns.pl\",\n nl: \"whois.domain-registry.nl\",\n be: \"whois.dns.be\",\n se: \"whois.iis.se\",\n no: \"whois.norid.no\",\n fi: \"whois.fi\",\n cz: \"whois.nic.cz\",\n es: \"whois.nic.es\",\n br: \"whois.registro.br\",\n ca: \"whois.cira.ca\",\n dk: \"whois.punktum.dk\", // was whois.dk-hostmaster.dk\n hk: \"whois.hkirc.hk\",\n sg: \"whois.sgnic.sg\",\n in: \"whois.nixiregistry.in\", // was whois.registry.in\n nz: \"whois.irs.net.nz\", // was whois.srs.net.nz\n ch: \"whois.nic.ch\",\n li: \"whois.nic.li\",\n io: \"whois.nic.io\",\n ai: \"whois.nic.ai\",\n ru: \"whois.tcinet.ru\",\n su: \"whois.tcinet.ru\",\n us: \"whois.nic.us\",\n co: \"whois.nic.co\",\n me: \"whois.nic.me\",\n tv: \"whois.nic.tv\",\n cc: \"ccwhois.verisign-grs.com\",\n eu: \"whois.eu\",\n au: \"whois.auda.org.au\",\n kr: \"whois.kr\",\n tw: \"whois.twnic.net.tw\",\n uk: \"whois.nic.uk\",\n nu: \"whois.iis.nu\",\n \"xn--p1ai\": \"whois.tcinet.ru\", // .рф\n\n // CentralNic-operated public SLD zones (still WHOIS @ centralnic)\n \"uk.com\": \"whois.centralnic.com\",\n \"uk.net\": \"whois.centralnic.com\",\n \"gb.com\": \"whois.centralnic.com\",\n \"gb.net\": \"whois.centralnic.com\",\n \"eu.com\": \"whois.centralnic.com\",\n \"us.com\": \"whois.centralnic.com\",\n \"se.com\": \"whois.centralnic.com\",\n \"de.com\": \"whois.centralnic.com\",\n \"br.com\": \"whois.centralnic.com\",\n \"ru.com\": \"whois.centralnic.com\",\n \"cn.com\": \"whois.centralnic.com\",\n \"sa.com\": \"whois.centralnic.com\",\n \"co.com\": \"whois.centralnic.com\",\n} as Record<string, string>;\n","import type { LookupOptions } from \"../types\";\nimport { whoisQuery } from \"./client\";\nimport { WHOIS_TLD_EXCEPTIONS } from \"./servers\";\n\n/**\n * Parse the IANA WHOIS response for a TLD and extract the WHOIS server\n * without crossing line boundaries. Some TLDs (e.g. .np) leave the field\n * blank, in which case this returns undefined.\n */\nexport function parseIanaWhoisServer(text: string): string | undefined {\n // Search lines in priority order: whois, refer, whois server\n const fields = [\"whois\", \"refer\", \"whois server\"];\n const lines = String(text).split(/\\r?\\n/);\n for (const field of fields) {\n for (const raw of lines) {\n const line = raw.trimEnd();\n // Match beginning of line, allowing leading spaces, case-insensitive\n const re = new RegExp(`^\\\\s*${field}\\\\s*:\\\\s*(.*?)$`, \"i\");\n const m = line.match(re);\n if (m) {\n const value = (m[1] || \"\").trim();\n if (value) return value;\n }\n }\n }\n return undefined;\n}\n\n/**\n * Parse a likely registration information URL from an IANA WHOIS response.\n * Looks at lines like:\n * remarks: Registration information: http://example.tld\n * url: https://registry.example\n */\nexport function parseIanaRegistrationInfoUrl(text: string): string | undefined {\n const lines = String(text).split(/\\r?\\n/);\n for (const raw of lines) {\n const line = raw.trim();\n if (!/^\\s*(remarks|url|website)\\s*:/i.test(line)) continue;\n const urlMatch = line.match(/https?:\\/\\/\\S+/i);\n if (urlMatch?.[0]) return urlMatch[0];\n }\n return undefined;\n}\n\n/** Fetch raw IANA WHOIS text for a TLD (best-effort). */\nexport async function getIanaWhoisTextForTld(\n tld: string,\n options?: LookupOptions,\n): Promise<string | undefined> {\n try {\n const res = await whoisQuery(\"whois.iana.org\", tld.toLowerCase(), options);\n return res.text;\n } catch {\n return undefined;\n }\n}\n\n/**\n * Best-effort discovery of the authoritative WHOIS server for a TLD via IANA root DB.\n */\nexport async function ianaWhoisServerForTld(\n tld: string,\n options?: LookupOptions,\n): Promise<string | undefined> {\n const key = tld.toLowerCase();\n // 1) Explicit hint override\n const hint = options?.whoisHints?.[key];\n if (hint) return normalizeServer(hint);\n\n // 2) IANA WHOIS authoritative discovery over TCP 43\n try {\n const res = await whoisQuery(\"whois.iana.org\", key, options);\n const txt = res.text;\n const server = parseIanaWhoisServer(txt);\n if (server) return normalizeServer(server);\n } catch {\n // fallthrough to exceptions/guess\n }\n\n // 3) Curated exceptions\n const exception = WHOIS_TLD_EXCEPTIONS[key];\n if (exception) return normalizeServer(exception);\n\n return undefined;\n}\n\n/**\n * Extract registrar referral WHOIS server from a WHOIS response, if present.\n */\nexport function extractWhoisReferral(text: string): string | undefined {\n const patterns = [\n /^Registrar WHOIS Server:\\s*(.+)$/im,\n /^Whois Server:\\s*(.+)$/im,\n /^ReferralServer:\\s*whois:\\/\\/(.+)$/im,\n ];\n for (const re of patterns) {\n const m = text.match(re);\n if (m?.[1]) return m[1].trim();\n }\n return undefined;\n}\n\nfunction normalizeServer(server: string): string {\n return server.replace(/^whois:\\/\\//i, \"\").replace(/\\/$/, \"\");\n}\n","import { uniq } from \"../lib/text\";\nimport type { Contact, DomainRecord, Nameserver } from \"../types\";\n\nfunction dedupeStatuses(\n a?: DomainRecord[\"statuses\"],\n b?: DomainRecord[\"statuses\"],\n) {\n const list = [...(a || []), ...(b || [])];\n const seen = new Set<string>();\n const out: NonNullable<DomainRecord[\"statuses\"]> = [];\n for (const s of list) {\n const key = (s?.status || \"\").toLowerCase();\n if (!key || seen.has(key)) continue;\n seen.add(key);\n out.push(s);\n }\n return out.length ? out : undefined;\n}\n\nfunction dedupeNameservers(a?: Nameserver[], b?: Nameserver[]) {\n const map = new Map<string, Nameserver>();\n for (const ns of [...(a || []), ...(b || [])]) {\n const host = ns.host.toLowerCase();\n const prev = map.get(host);\n if (!prev) {\n map.set(host, { ...ns, host });\n continue;\n }\n const ipv4 = uniq([...(prev.ipv4 || []), ...(ns.ipv4 || [])]);\n const ipv6 = uniq([...(prev.ipv6 || []), ...(ns.ipv6 || [])]);\n map.set(host, { host, ipv4, ipv6 });\n }\n const out = Array.from(map.values());\n return out.length ? out : undefined;\n}\n\nfunction dedupeContacts(a?: Contact[], b?: Contact[]) {\n const list = [...(a || []), ...(b || [])];\n const seen = new Set<string>();\n const out: Contact[] = [];\n for (const c of list) {\n const key = `${c.type}|${(c.organization || c.name || c.email || \"\").toString().toLowerCase()}`;\n if (seen.has(key)) continue;\n seen.add(key);\n out.push(c);\n }\n return out.length ? out : undefined;\n}\n\n/** Conservative merge: start with base; fill missing scalars; union arrays; prefer more informative dates. */\nexport function mergeWhoisRecords(\n base: DomainRecord,\n others: DomainRecord[],\n): DomainRecord {\n const merged: DomainRecord = { ...base };\n for (const cur of others) {\n merged.isRegistered = merged.isRegistered || cur.isRegistered;\n merged.registry = merged.registry ?? cur.registry;\n merged.registrar = merged.registrar ?? cur.registrar;\n merged.reseller = merged.reseller ?? cur.reseller;\n merged.statuses = dedupeStatuses(merged.statuses, cur.statuses);\n // Dates: prefer earliest creation, latest updated/expiration when available\n merged.creationDate = preferEarliestIso(\n merged.creationDate,\n cur.creationDate,\n );\n merged.updatedDate = preferLatestIso(merged.updatedDate, cur.updatedDate);\n merged.expirationDate = preferLatestIso(\n merged.expirationDate,\n cur.expirationDate,\n );\n merged.deletionDate = merged.deletionDate ?? cur.deletionDate;\n merged.transferLock = Boolean(merged.transferLock || cur.transferLock);\n merged.dnssec = merged.dnssec ?? cur.dnssec;\n merged.nameservers = dedupeNameservers(merged.nameservers, cur.nameservers);\n merged.contacts = dedupeContacts(merged.contacts, cur.contacts);\n merged.privacyEnabled = merged.privacyEnabled ?? cur.privacyEnabled;\n // Keep whoisServer pointing to the latest contributing authoritative server\n merged.whoisServer = cur.whoisServer ?? merged.whoisServer;\n // rawWhois: keep last contributing text\n merged.rawWhois = cur.rawWhois ?? merged.rawWhois;\n }\n return merged;\n}\n\nfunction preferEarliestIso(a?: string, b?: string): string | undefined {\n if (!a) return b;\n if (!b) return a;\n return new Date(a) <= new Date(b) ? a : b;\n}\n\nfunction preferLatestIso(a?: string, b?: string): string | undefined {\n if (!a) return b;\n if (!b) return a;\n return new Date(a) >= new Date(b) ? a : b;\n}\n","import { toISO } from \"../lib/dates\";\nimport { isPrivacyName } from \"../lib/privacy\";\nimport { parseKeyValueLines, uniq } from \"../lib/text\";\nimport type {\n Contact,\n DomainRecord,\n Nameserver,\n RegistrarInfo,\n} from \"../types\";\n\n// Common WHOIS availability phrases seen across registries/registrars\nconst WHOIS_AVAILABLE_PATTERNS: RegExp[] = [\n /\\bno match\\b/i,\n /\\bnot found\\b/i,\n /\\bno entries found\\b/i,\n /\\bno data found\\b/i,\n /\\bavailable for registration\\b/i,\n /\\bdomain\\s+available\\b/i,\n /\\bdomain status[:\\s]+available\\b/i,\n /\\bobject does not exist\\b/i,\n /\\bthe queried object does not exist\\b/i,\n /\\bqueried object does not exist\\b/i,\n /\\breturned 0 objects\\b/i,\n // Common variants across ccTLDs/registrars\n /\\bstatus:\\s*free\\b/i,\n /\\bstatus:\\s*available\\b/i,\n /\\bno object found\\b/i,\n /\\bnicht gefunden\\b/i, // German: \"not found\"\n /\\bpending release\\b/i, // often signals not registered/being deleted\n];\n\n/**\n * Best-effort heuristic to determine if a WHOIS response indicates the domain is available.\n */\nexport function isAvailableByWhois(text: string | undefined): boolean {\n if (!text) return false;\n return WHOIS_AVAILABLE_PATTERNS.some((re) => re.test(text));\n}\n\n/**\n * Convert raw WHOIS text into our normalized DomainRecord.\n * Heuristics cover many gTLD and ccTLD formats; exact fields vary per registry.\n */\nexport function normalizeWhois(\n domain: string,\n tld: string,\n whoisText: string,\n whoisServer: string | undefined,\n includeRaw = false,\n): DomainRecord {\n const map = parseKeyValueLines(whoisText);\n\n // Date extraction across common synonyms\n const creationDate = anyValue(map, [\n \"creation date\",\n \"created on\",\n \"created\",\n \"registered on\",\n \"registered\",\n \"registration date\",\n \"domain registration date\",\n \"domain create date\",\n \"domain name commencement date\",\n \"registration time\", // .cn\n \"domain record activated\", // .edu\n \"domain registered\",\n \"registered date\", // .co.jp\n \"assigned\", // .il\n ]);\n const updatedDate = anyValue(map, [\n \"updated date\",\n \"updated\",\n \"last updated\",\n \"last updated on\", // .mx\n \"last update\", // .co.jp\n \"last-update\", // .fr\n \"last modified\",\n \"modified\",\n \"changed\",\n \"modification date\",\n \"domain record last updated\", // .edu\n ]);\n const expirationDate = anyValue(map, [\n \"registry expiry date\",\n \"registry expiration date\",\n \"registrar registration expiration date\",\n \"registrar registration expiry date\",\n \"registrar expiration date\",\n \"registrar expiry date\",\n \"expiry date\",\n \"expiration date\",\n \"expiry\",\n \"expire date\", // .it\n \"expire\",\n \"expired\", // .ly\n \"expires on\",\n \"expires\",\n \"expiration time\", // .cn\n \"domain expires\", // .edu\n \"paid-till\",\n \"renewal date\", // .pl\n \"validity\", // .il\n \"record will expire on\",\n ]);\n\n // Registrar info (thin registries like .com/.net require referral follow for full data)\n const registrar: RegistrarInfo | undefined = (() => {\n const name = anyValue(map, [\n \"registrar\",\n \"registrar name\",\n \"registrar organization\",\n \"registrar organization name\", // .tr\n \"sponsoring registrar\",\n \"organisation\",\n \"record maintained by\",\n ]);\n const ianaId = anyValue(map, [\n \"registrar iana id\",\n \"sponsoring registrar iana id\",\n \"iana id\",\n ]);\n const url = anyValue(map, [\n \"registrar url\",\n \"registrar website\",\n \"registrar web\", // .it\n \"url of the registrar\",\n \"referrer\",\n ]);\n const abuseEmail = anyValue(map, [\n \"registrar abuse contact email\",\n \"abuse contact email\",\n ]);\n const abusePhone = anyValue(map, [\n \"registrar abuse contact phone\",\n \"abuse contact phone\",\n ]);\n if (!name && !ianaId && !url && !abuseEmail && !abusePhone)\n return undefined;\n return {\n name: name || undefined,\n ianaId: ianaId || undefined,\n url: url || undefined,\n email: abuseEmail || undefined,\n phone: abusePhone || undefined,\n };\n })();\n\n // Statuses: multiple entries are expected; keep raw\n const statusLines =\n map[\"domain status\"] ||\n map.status ||\n map.flags ||\n map.state || // .ru\n map[\"registration status\"] ||\n map.eppstatus || // .fr\n [];\n const statuses = statusLines.length\n ? statusLines\n .map((line) => {\n const status = line.split(/\\s+/)[0];\n return status ? { status, raw: line } : null;\n })\n .filter((s): s is { status: string; raw: string } => s !== null)\n : undefined;\n\n // Nameservers: also appear as \"nserver\" on some ccTLDs (.de, .ru) and as \"name server\"\n const nsLines: string[] = [\n ...(map[\"name server\"] || []),\n ...(map.nameserver || []),\n ...(map[\"name servers\"] || []),\n ...(map.nserver || []),\n ...(map[\"name server information\"] || []),\n ...(map.dns || []),\n ...(map.hostname || []),\n ...(map[\"domain nameservers\"] || []),\n ...(map[\"domain servers in listed order\"] || []), // .ly\n ...(map[\"domain servers\"] || []), // .tr\n ...(map[\"name servers dns\"] || []), // .mx\n ...(map[\"ns 1\"] || []),\n ...(map[\"ns 2\"] || []),\n ...(map[\"ns 3\"] || []),\n ...(map[\"ns 4\"] || []),\n ];\n const nameservers: Nameserver[] | undefined = nsLines.length\n ? (uniq(\n nsLines\n .map((line) => line.trim())\n .filter(Boolean)\n .map((line) => {\n // Common formats: \"ns1.example.com\" or \"ns1.example.com 192.0.2.1\" or \"ns1.example.com 2001:db8::1\"\n const parts = line.split(/\\s+/);\n const host = parts.shift()?.toLowerCase() || \"\";\n const ipv4: string[] = [];\n const ipv6: string[] = [];\n for (const p of parts) {\n if (/^\\d+\\.\\d+\\.\\d+\\.\\d+$/.test(p)) ipv4.push(p);\n else if (/^[0-9a-f:]+$/i.test(p)) ipv6.push(p);\n }\n if (!host) return undefined;\n const ns: Nameserver = { host };\n if (ipv4.length) ns.ipv4 = ipv4;\n if (ipv6.length) ns.ipv6 = ipv6;\n return ns;\n })\n .filter((x): x is Nameserver => !!x),\n ) as Nameserver[])\n : undefined;\n\n // Contacts: best-effort parse common keys\n const contacts = collectContacts(map);\n\n // Derive privacy flag from registrant name/org keywords\n const registrant = contacts?.find((c) => c.type === \"registrant\");\n const privacyEnabled = !!(\n registrant &&\n (\n [registrant.name, registrant.organization].filter(Boolean) as string[]\n ).some(isPrivacyName)\n );\n\n const dnssecRaw = (map.dnssec?.[0] || \"\").toLowerCase();\n const dnssec = dnssecRaw\n ? { enabled: /signed|yes|true/.test(dnssecRaw) }\n : undefined;\n\n // Simple lock derivation from statuses\n const transferLock = !!statuses?.some(\n (s) => s.status && /transferprohibited/i.test(s.status),\n );\n\n const record: DomainRecord = {\n domain,\n tld,\n isRegistered: !isAvailableByWhois(whoisText),\n isIDN: /(^|\\.)xn--/i.test(domain),\n unicodeName: undefined,\n punycodeName: undefined,\n registry: undefined,\n registrar,\n reseller: anyValue(map, [\"reseller\"]) || undefined,\n statuses,\n creationDate: toISO(creationDate || undefined),\n updatedDate: toISO(updatedDate || undefined),\n expirationDate: toISO(expirationDate || undefined),\n deletionDate: undefined,\n transferLock,\n dnssec,\n nameservers,\n contacts,\n privacyEnabled: privacyEnabled ? true : undefined,\n whoisServer,\n rdapServers: undefined,\n rawRdap: undefined,\n rawWhois: includeRaw ? whoisText : undefined,\n source: \"whois\",\n warnings: undefined,\n };\n\n return record;\n}\n\nfunction anyValue(\n map: Record<string, string[]>,\n keys: string[],\n): string | undefined {\n for (const k of keys) {\n const v = map[k];\n if (v?.length) return v[0];\n }\n return undefined;\n}\n\nfunction collectContacts(map: Record<string, string[]>): Contact[] | undefined {\n const roles: Array<{\n role: Contact[\"type\"];\n prefixes: string[];\n }> = [\n {\n role: \"registrant\",\n prefixes: [\"registrant\", \"owner\", \"holder\"],\n },\n {\n role: \"admin\",\n prefixes: [\"admin\", \"administrative\"],\n },\n {\n role: \"tech\",\n prefixes: [\"tech\", \"technical\"],\n },\n {\n role: \"billing\",\n prefixes: [\"billing\"],\n },\n {\n role: \"abuse\",\n prefixes: [\"abuse\"],\n },\n ];\n const contacts: Contact[] = [];\n for (const r of roles) {\n const nameKeys: string[] = [];\n const orgKeys: string[] = [];\n const emailKeys: string[] = [];\n const phoneKeys: string[] = [];\n const faxKeys: string[] = [];\n const streetKeys: string[] = [];\n const cityKeys: string[] = [];\n const stateKeys: string[] = [];\n const postalCodeKeys: string[] = [];\n const countryKeys: string[] = [];\n\n for (const prefix of r.prefixes) {\n nameKeys.push(`${prefix} name`, `${prefix} contact name`, `${prefix}`);\n if (prefix === \"registrant\") {\n nameKeys.push(\"registrant person\"); // .ua\n }\n if (prefix === \"owner\") {\n nameKeys.push(\"owner name\"); // .tm\n }\n\n orgKeys.push(\n `${prefix} organization`,\n `${prefix} organisation`,\n `${prefix} org`,\n );\n if (prefix === \"registrant\") {\n orgKeys.push(\"trading as\"); // .uk, .co.uk\n orgKeys.push(\"org\"); // .ru\n }\n if (prefix === \"owner\") {\n orgKeys.push(\"owner orgname\"); // .tm\n }\n\n emailKeys.push(\n `${prefix} email`,\n `${prefix} contact email`,\n `${prefix} e-mail`,\n );\n\n phoneKeys.push(\n `${prefix} phone`,\n `${prefix} contact phone`,\n `${prefix} telephone`,\n );\n\n faxKeys.push(`${prefix} fax`, `${prefix} facsimile`);\n\n streetKeys.push(\n `${prefix} street`,\n `${prefix} address`,\n `${prefix}'s address`,\n );\n if (prefix === \"owner\") {\n streetKeys.push(\"owner addr\"); // .tm\n }\n\n cityKeys.push(`${prefix} city`);\n\n stateKeys.push(\n `${prefix} state`,\n `${prefix} province`,\n `${prefix} state/province`,\n );\n\n postalCodeKeys.push(\n `${prefix} postal code`,\n `${prefix} postcode`,\n `${prefix} zip`,\n );\n\n countryKeys.push(`${prefix} country`);\n }\n\n const name = anyValue(map, nameKeys);\n const org = anyValue(map, orgKeys);\n const email = anyValue(map, emailKeys);\n const phone = anyValue(map, phoneKeys);\n const fax = anyValue(map, faxKeys);\n const street = multi(map, streetKeys);\n const city = anyValue(map, cityKeys);\n const state = anyValue(map, stateKeys);\n const postalCode = anyValue(map, postalCodeKeys);\n const country = anyValue(map, countryKeys);\n\n if (name || org || email || phone || street?.length) {\n contacts.push({\n type: r.role,\n name: name || undefined,\n organization: org || undefined,\n email: email || undefined,\n phone: phone || undefined,\n fax: fax || undefined,\n street: street,\n city: city || undefined,\n state: state || undefined,\n postalCode: postalCode || undefined,\n country: country || undefined,\n });\n }\n }\n return contacts.length ? contacts : undefined;\n}\n\nfunction multi(\n map: Record<string, string[]>,\n keys: string[],\n): string[] | undefined {\n for (const k of keys) {\n const v = map[k];\n if (v?.length) return v;\n }\n return undefined;\n}\n","import type { LookupOptions } from \"../types\";\nimport type { WhoisQueryResult } from \"./client\";\nimport { whoisQuery } from \"./client\";\nimport { extractWhoisReferral } from \"./discovery\";\nimport { isAvailableByWhois } from \"./normalize\";\n\n/**\n * Follow registrar WHOIS referrals up to a configured hop limit.\n * Returns the last successful WHOIS response (best-effort; keeps original on failures).\n */\nexport async function followWhoisReferrals(\n initialServer: string,\n domain: string,\n opts?: LookupOptions,\n): Promise<WhoisQueryResult> {\n const maxHops = Math.max(0, opts?.maxWhoisReferralHops ?? 2);\n // First query against the provided server\n let current = await whoisQuery(initialServer, domain, opts);\n if (opts?.followWhoisReferral === false || maxHops === 0) return current;\n\n const visited = new Set<string>([normalize(current.serverQueried)]);\n let hops = 0;\n // Iterate while we see a new referral and are under hop limit\n while (hops < maxHops) {\n const next = extractWhoisReferral(current.text);\n if (!next) break;\n const normalized = normalize(next);\n if (visited.has(normalized)) break; // cycle protection / same as current\n visited.add(normalized);\n try {\n const res = await whoisQuery(next, domain, opts);\n // Prefer authoritative TLD response when registrar contradicts availability\n const registeredBefore = !isAvailableByWhois(current.text);\n const registeredAfter = !isAvailableByWhois(res.text);\n if (registeredBefore && !registeredAfter) {\n // Registrar claims availability but TLD shows registered: keep TLD\n break;\n }\n current = res; // adopt registrar when it does not downgrade registration\n } catch {\n // If referral server fails, stop following and keep the last good response\n break;\n }\n hops += 1;\n }\n return current;\n}\n\n/**\n * Collect the WHOIS referral chain starting from the TLD server.\n * Always includes the initial TLD response; may include one or more registrar responses.\n * Stops on contradiction (registrar claims availability) or failures.\n */\nexport async function collectWhoisReferralChain(\n initialServer: string,\n domain: string,\n opts?: LookupOptions,\n): Promise<WhoisQueryResult[]> {\n const results: WhoisQueryResult[] = [];\n const maxHops = Math.max(0, opts?.maxWhoisReferralHops ?? 2);\n const first = await whoisQuery(initialServer, domain, opts);\n results.push(first);\n if (opts?.followWhoisReferral === false || maxHops === 0) return results;\n\n const visited = new Set<string>([normalize(first.serverQueried)]);\n let current = first;\n let hops = 0;\n while (hops < maxHops) {\n const next = extractWhoisReferral(current.text);\n if (!next) break;\n const normalized = normalize(next);\n if (visited.has(normalized)) break;\n visited.add(normalized);\n try {\n const res = await whoisQuery(next, domain, opts);\n // If registrar claims availability while TLD indicated registered, stop.\n const registeredBefore = !isAvailableByWhois(current.text);\n const registeredAfter = !isAvailableByWhois(res.text);\n if (registeredBefore && !registeredAfter) {\n // Do not adopt or append contradictory registrar; keep authoritative TLD only.\n break;\n }\n results.push(res);\n current = res;\n } catch {\n break;\n }\n hops += 1;\n }\n return results;\n}\n\nfunction normalize(server: string): string {\n return server.replace(/^whois:\\/\\//i, \"\").toLowerCase();\n}\n","import { getDomainParts, isLikelyDomain } from \"./lib/domain\";\nimport { getRdapBaseUrlsForTld } from \"./rdap/bootstrap\";\nimport { fetchRdapDomain } from \"./rdap/client\";\nimport { fetchAndMergeRdapRelated } from \"./rdap/merge\";\nimport { normalizeRdap } from \"./rdap/normalize\";\nimport type { DomainRecord, LookupOptions, LookupResult } from \"./types\";\nimport {\n getIanaWhoisTextForTld,\n ianaWhoisServerForTld,\n parseIanaRegistrationInfoUrl,\n} from \"./whois/discovery\";\nimport { mergeWhoisRecords } from \"./whois/merge\";\nimport { normalizeWhois } from \"./whois/normalize\";\nimport {\n collectWhoisReferralChain,\n followWhoisReferrals,\n} from \"./whois/referral\";\n\n/**\n * High-level lookup that prefers RDAP and falls back to WHOIS.\n * Ensures a standardized DomainRecord, independent of the source.\n */\nexport async function lookup(\n domain: string,\n opts?: LookupOptions,\n): Promise<LookupResult> {\n try {\n if (!isLikelyDomain(domain)) {\n return { ok: false, error: \"Input does not look like a domain\" };\n }\n\n const { publicSuffix: tld } = getDomainParts(domain);\n if (!tld) {\n return { ok: false, error: \"Invalid TLD\" };\n }\n\n // If WHOIS-only, skip RDAP path\n if (!opts?.whoisOnly) {\n let bases = await getRdapBaseUrlsForTld(tld, opts);\n // Some ccTLD registries publish RDAP only at the registry TLD (e.g., br),\n // while the public suffix can be multi-label (e.g., com.br). Fallback to last label.\n if (bases.length === 0 && tld.includes(\".\")) {\n const registryTld = tld.split(\".\").pop() ?? tld;\n bases = await getRdapBaseUrlsForTld(registryTld, opts);\n }\n const tried: string[] = [];\n for (const base of bases) {\n tried.push(base);\n try {\n const { json } = await fetchRdapDomain(domain, base, opts);\n const rdapEnriched = await fetchAndMergeRdapRelated(\n domain,\n json,\n opts,\n );\n const record: DomainRecord = normalizeRdap(\n domain,\n tld,\n rdapEnriched.merged,\n [...tried, ...rdapEnriched.serversTried],\n !!opts?.includeRaw,\n );\n return { ok: true, record };\n } catch {\n // try next base\n }\n }\n // Some TLDs are not in bootstrap yet; continue to WHOIS fallback unless rdapOnly\n if (opts?.rdapOnly) {\n return {\n ok: false,\n error: `RDAP not available or failed for TLD '${tld}'. Many TLDs do not publish RDAP; try WHOIS fallback (omit rdapOnly).`,\n };\n }\n }\n\n // WHOIS fallback path\n const whoisServer = await ianaWhoisServerForTld(tld, opts);\n if (!whoisServer) {\n // Provide a clearer, actionable message\n const ianaText = await getIanaWhoisTextForTld(tld, opts);\n const regUrl = ianaText\n ? parseIanaRegistrationInfoUrl(ianaText)\n : undefined;\n const hint = regUrl ? ` See registration info at ${regUrl}.` : \"\";\n return {\n ok: false,\n error: `No WHOIS server discovered for TLD '${tld}'. This registry may not publish public WHOIS over port 43.${hint}`,\n };\n }\n\n // Query the TLD server first; optionally follow registrar referrals (multi-hop)\n // Collect the chain and coalesce so we don't lose details when a registrar returns minimal/empty data.\n const chain = await collectWhoisReferralChain(whoisServer, domain, opts);\n if (chain.length === 0) {\n // Fallback to previous behavior as a safety net\n const res = await followWhoisReferrals(whoisServer, domain, opts);\n const record: DomainRecord = normalizeWhois(\n domain,\n tld,\n res.text,\n res.serverQueried,\n !!opts?.includeRaw,\n );\n return { ok: true, record };\n }\n\n // Normalize all WHOIS texts in the chain and merge conservatively\n const normalizedRecords = chain.map((r) =>\n normalizeWhois(domain, tld, r.text, r.serverQueried, !!opts?.includeRaw),\n );\n const [first, ...rest] = normalizedRecords;\n if (!first) {\n return { ok: false, error: \"No WHOIS data retrieved\" };\n }\n const mergedRecord = rest.length ? mergeWhoisRecords(first, rest) : first;\n return { ok: true, record: mergedRecord };\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n return { ok: false, error: message };\n }\n}\n\n/**\n * Determine if a domain appears available (not registered).\n * Performs a lookup and resolves to a boolean. Rejects on lookup error.\n */\nexport async function isAvailable(\n domain: string,\n opts?: LookupOptions,\n): Promise<boolean> {\n const res = await lookup(domain, opts);\n if (!res.ok || !res.record) throw new Error(res.error || \"Lookup failed\");\n return res.record.isRegistered === false;\n}\n\n/**\n * Determine if a domain appears registered.\n * Performs a lookup and resolves to a boolean. Rejects on lookup error.\n */\nexport async function isRegistered(\n domain: string,\n opts?: LookupOptions,\n): Promise<boolean> {\n const res = await lookup(domain, opts);\n if (!res.ok || !res.record) throw new Error(res.error || \"Lookup failed\");\n return res.record.isRegistered === true;\n}\n\n/**\n * @deprecated Use `lookup` instead.\n */\nexport const lookupDomain = lookup;\n\nexport {\n getDomainParts,\n getDomainTld,\n isLikelyDomain,\n toRegistrableDomain,\n} from \"./lib/domain\";\nexport type * from \"./types\";\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rdapper",
3
- "version": "0.10.3",
3
+ "version": "0.11.0",
4
4
  "license": "MIT",
5
5
  "description": "🎩 RDAP/WHOIS fetcher, parser, and normalizer for Node",
6
6
  "repository": {
@@ -49,11 +49,11 @@
49
49
  "tldts": "7.0.17"
50
50
  },
51
51
  "devDependencies": {
52
- "@biomejs/biome": "2.2.6",
53
- "@types/node": "24.9.0",
54
- "tsdown": "0.15.9",
52
+ "@biomejs/biome": "2.3.2",
53
+ "@types/node": "24.9.2",
54
+ "tsdown": "0.15.12",
55
55
  "typescript": "5.9.3",
56
- "vitest": "^3.2.4"
56
+ "vitest": "^4.0.0"
57
57
  },
58
58
  "engines": {
59
59
  "node": ">=18.17"