rdapper 0.10.4 → 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 +335 -1
- package/dist/index.d.ts +239 -3
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +94 -18
- package/dist/index.js.map +1 -0
- package/package.json +5 -5
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
|
-
- `
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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(
|
|
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(
|
|
269
|
+
const res = await withTimeout(resolveFetch(options)(url, {
|
|
205
270
|
method: "GET",
|
|
206
271
|
headers: { accept: "application/rdap+json, application/json" },
|
|
207
272
|
signal: options?.signal
|
|
@@ -287,6 +352,7 @@ function parseDateWithRegex(m, _re) {
|
|
|
287
352
|
try {
|
|
288
353
|
if (m[0].includes(":")) {
|
|
289
354
|
const [_$1, y, mo, d, hh, mm, ss, offH, offM] = m;
|
|
355
|
+
if (!y || !mo || !d || !hh || !mm || !ss) return void 0;
|
|
290
356
|
let dt = Date.UTC(Number(y), Number(mo) - 1, Number(d), Number(hh), Number(mm), Number(ss));
|
|
291
357
|
if (offH) {
|
|
292
358
|
const sign = offH.startsWith("-") ? -1 : 1;
|
|
@@ -299,11 +365,13 @@ function parseDateWithRegex(m, _re) {
|
|
|
299
365
|
}
|
|
300
366
|
if (m[0].includes("-")) {
|
|
301
367
|
const [_$1, dd$1, monStr$1, yyyy$1] = m;
|
|
368
|
+
if (!monStr$1 || !dd$1 || !yyyy$1) return void 0;
|
|
302
369
|
if (/^\d+$/.test(monStr$1)) return new Date(Date.UTC(Number(yyyy$1), Number(monStr$1) - 1, Number(dd$1)));
|
|
303
370
|
const mon$1 = monthMap[monStr$1.toLowerCase()];
|
|
304
371
|
return new Date(Date.UTC(Number(yyyy$1), mon$1, Number(dd$1)));
|
|
305
372
|
}
|
|
306
373
|
const [_, monStr, dd, yyyy] = m;
|
|
374
|
+
if (!monStr || !dd || !yyyy) return void 0;
|
|
307
375
|
const mon = monthMap[monStr.toLowerCase()];
|
|
308
376
|
return new Date(Date.UTC(Number(yyyy), mon, Number(dd)));
|
|
309
377
|
} catch {}
|
|
@@ -362,7 +430,7 @@ function parseKeyValueLines(text) {
|
|
|
362
430
|
const line = rawLine.replace(/\s+$/, "");
|
|
363
431
|
if (!line.trim()) continue;
|
|
364
432
|
const bracket = line.match(/^\s*\[([^\]]+)\]\s*(.*)$/);
|
|
365
|
-
if (bracket) {
|
|
433
|
+
if (bracket?.[1] !== void 0 && bracket?.[2] !== void 0) {
|
|
366
434
|
const key = bracket[1].trim().toLowerCase();
|
|
367
435
|
const value = bracket[2].trim();
|
|
368
436
|
const list = map.get(key) ?? [];
|
|
@@ -976,10 +1044,13 @@ function normalizeWhois(domain, tld, whoisText, whoisServer, includeRaw = false)
|
|
|
976
1044
|
};
|
|
977
1045
|
})();
|
|
978
1046
|
const statusLines = map["domain status"] || map.status || map.flags || map.state || map["registration status"] || map.eppstatus || [];
|
|
979
|
-
const statuses = statusLines.length ? statusLines.map((line) =>
|
|
980
|
-
status
|
|
981
|
-
|
|
982
|
-
|
|
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;
|
|
983
1054
|
const nsLines = [
|
|
984
1055
|
...map["name server"] || [],
|
|
985
1056
|
...map.nameserver || [],
|
|
@@ -1015,7 +1086,7 @@ function normalizeWhois(domain, tld, whoisText, whoisServer, includeRaw = false)
|
|
|
1015
1086
|
const privacyEnabled = !!(registrant && [registrant.name, registrant.organization].filter(Boolean).some(isPrivacyName));
|
|
1016
1087
|
const dnssecRaw = (map.dnssec?.[0] || "").toLowerCase();
|
|
1017
1088
|
const dnssec = dnssecRaw ? { enabled: /signed|yes|true/.test(dnssecRaw) } : void 0;
|
|
1018
|
-
const transferLock = !!statuses?.some((s) => /transferprohibited/i.test(s.status));
|
|
1089
|
+
const transferLock = !!statuses?.some((s) => s.status && /transferprohibited/i.test(s.status));
|
|
1019
1090
|
return {
|
|
1020
1091
|
domain,
|
|
1021
1092
|
tld,
|
|
@@ -1266,6 +1337,10 @@ async function lookup(domain, opts) {
|
|
|
1266
1337
|
};
|
|
1267
1338
|
}
|
|
1268
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
|
+
};
|
|
1269
1344
|
return {
|
|
1270
1345
|
ok: true,
|
|
1271
1346
|
record: rest.length ? mergeWhoisRecords(first, rest) : first
|
|
@@ -1301,4 +1376,5 @@ async function isRegistered(domain, opts) {
|
|
|
1301
1376
|
const lookupDomain = lookup;
|
|
1302
1377
|
|
|
1303
1378
|
//#endregion
|
|
1304
|
-
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.
|
|
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
|
|
53
|
-
"@types/node": "24.9.
|
|
54
|
-
"tsdown": "0.15.
|
|
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": "^
|
|
56
|
+
"vitest": "^4.0.0"
|
|
57
57
|
},
|
|
58
58
|
"engines": {
|
|
59
59
|
"node": ">=18.17"
|