rdapper 0.10.4 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -86,6 +86,338 @@ const res = await lookup("example.com", { rdapOnly: true });
86
86
 
87
87
  - If `rdapOnly` is omitted and the code path reaches WHOIS on edge, rdapper throws a clear runtime error advising to run in Node or set `{ rdapOnly: true }`.
88
88
 
89
+ ### Bootstrap Data Caching
90
+
91
+ By default, rdapper fetches IANA's RDAP bootstrap registry from [`https://data.iana.org/rdap/dns.json`](https://data.iana.org/rdap/dns.json) on every RDAP lookup to discover the authoritative RDAP servers for a given TLD. While this ensures you always have up-to-date server mappings, it also adds latency and a network dependency to each lookup.
92
+
93
+ For production applications that perform many domain lookups, you can take control of bootstrap data caching by fetching and caching the data yourself, then passing it to rdapper using the `customBootstrapData` option. This eliminates redundant network requests and gives you full control over cache invalidation.
94
+
95
+ #### Why cache bootstrap data?
96
+
97
+ - **Performance**: Eliminate an extra HTTP request per lookup (or per TLD if you're looking up many domains)
98
+ - **Reliability**: Reduce dependency on IANA's availability during lookups
99
+ - **Control**: Manage cache TTL and invalidation according to your needs (IANA updates this file infrequently)
100
+ - **Cost**: Reduce bandwidth and API calls in high-volume scenarios
101
+
102
+ #### Example: In-memory caching with TTL
103
+
104
+ ```ts
105
+ import { lookup, type BootstrapData } from 'rdapper';
106
+
107
+ // Simple in-memory cache with TTL
108
+ let cachedBootstrap: BootstrapData | null = null;
109
+ let cacheExpiry = 0;
110
+ const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
111
+
112
+ async function getBootstrapData(): Promise<BootstrapData> {
113
+ const now = Date.now();
114
+
115
+ // Return cached data if still valid
116
+ if (cachedBootstrap && now < cacheExpiry) {
117
+ return cachedBootstrap;
118
+ }
119
+
120
+ // Fetch fresh data
121
+ const response = await fetch('https://data.iana.org/rdap/dns.json');
122
+ if (!response.ok) {
123
+ throw new Error(`Failed to load bootstrap data: ${response.status} ${response.statusText}`);
124
+ }
125
+ const data: BootstrapData = await response.json();
126
+
127
+ // Update cache
128
+ cachedBootstrap = data;
129
+ cacheExpiry = now + CACHE_TTL_MS;
130
+
131
+ return data;
132
+ }
133
+
134
+ // Use the cached bootstrap data in lookups
135
+ const bootstrapData = await getBootstrapData();
136
+ const result = await lookup('example.com', {
137
+ customBootstrapData: bootstrapData
138
+ });
139
+ ```
140
+
141
+ #### Example: Redis caching
142
+
143
+ ```ts
144
+ import { lookup, type BootstrapData } from 'rdapper';
145
+ import { createClient } from 'redis';
146
+
147
+ const redis = createClient();
148
+ await redis.connect();
149
+
150
+ const CACHE_KEY = 'rdap:bootstrap:dns';
151
+ const CACHE_TTL_SECONDS = 24 * 60 * 60; // 24 hours
152
+
153
+ async function getBootstrapData(): Promise<BootstrapData> {
154
+ // Try to get from Redis first
155
+ const cached = await redis.get(CACHE_KEY);
156
+ if (cached) {
157
+ return JSON.parse(cached);
158
+ }
159
+
160
+ // Fetch fresh data
161
+ const response = await fetch('https://data.iana.org/rdap/dns.json');
162
+ if (!response.ok) {
163
+ throw new Error(`Failed to load bootstrap data: ${response.status} ${response.statusText}`);
164
+ }
165
+ const data: BootstrapData = await response.json();
166
+
167
+ // Store in Redis with TTL
168
+ await redis.setEx(CACHE_KEY, CACHE_TTL_SECONDS, JSON.stringify(data));
169
+
170
+ return data;
171
+ }
172
+
173
+ // Use the cached bootstrap data in lookups
174
+ const bootstrapData = await getBootstrapData();
175
+ const result = await lookup('example.com', {
176
+ customBootstrapData: bootstrapData
177
+ });
178
+ ```
179
+
180
+ #### Example: Filesystem caching
181
+
182
+ ```ts
183
+ import { lookup, type BootstrapData } from 'rdapper';
184
+ import { readFile, writeFile, stat } from 'node:fs/promises';
185
+
186
+ const CACHE_FILE = './cache/rdap-bootstrap.json';
187
+ const CACHE_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
188
+
189
+ async function getBootstrapData(): Promise<BootstrapData> {
190
+ try {
191
+ // Check if cache file exists and is fresh
192
+ const stats = await stat(CACHE_FILE);
193
+ const age = Date.now() - stats.mtimeMs;
194
+
195
+ if (age < CACHE_TTL_MS) {
196
+ const cached = await readFile(CACHE_FILE, 'utf-8');
197
+ return JSON.parse(cached);
198
+ }
199
+ } catch {
200
+ // Cache file doesn't exist or is unreadable, will fetch fresh
201
+ }
202
+
203
+ // Fetch fresh data
204
+ const response = await fetch('https://data.iana.org/rdap/dns.json');
205
+ if (!response.ok) {
206
+ throw new Error(`Failed to load bootstrap data: ${response.status} ${response.statusText}`);
207
+ }
208
+ const data: BootstrapData = await response.json();
209
+
210
+ // Write to cache file
211
+ await writeFile(CACHE_FILE, JSON.stringify(data, null, 2), 'utf-8');
212
+
213
+ return data;
214
+ }
215
+
216
+ // Use the cached bootstrap data in lookups
217
+ const bootstrapData = await getBootstrapData();
218
+ const result = await lookup('example.com', {
219
+ customBootstrapData: bootstrapData
220
+ });
221
+ ```
222
+
223
+ #### Bootstrap data structure
224
+
225
+ The `BootstrapData` type matches IANA's published format:
226
+
227
+ ```ts
228
+ interface BootstrapData {
229
+ version: string; // e.g., "1.0"
230
+ publication: string; // ISO 8601 timestamp
231
+ description?: string;
232
+ services: string[][][]; // Array of [TLDs, base URLs] tuples
233
+ }
234
+ ```
235
+
236
+ See the full documentation at [RFC 7484 - Finding the Authoritative RDAP Service](https://datatracker.ietf.org/doc/html/rfc7484).
237
+
238
+ **Note**: The bootstrap data structure is stable and rarely changes. IANA updates the _contents_ (server mappings) periodically as TLDs are added or servers change, but a 24-hour cache TTL is typically safe for most applications.
239
+
240
+ ### Custom Fetch Implementation
241
+
242
+ For advanced use cases, rdapper allows you to provide a custom `fetch` implementation that will be used for **all HTTP requests** in the library. This enables powerful patterns for caching, logging, retry logic, and more.
243
+
244
+ #### What requests are affected?
245
+
246
+ Your custom fetch will be used for:
247
+ - **RDAP bootstrap registry requests** (fetching `dns.json` from IANA, unless `customBootstrapData` is provided)
248
+ - **RDAP domain lookups** (querying RDAP servers for domain data)
249
+ - **RDAP related/entity link requests** (following links to registrar information)
250
+
251
+ #### Why use custom fetch?
252
+
253
+ - **Caching**: Implement sophisticated caching strategies for all RDAP requests
254
+ - **Logging & Monitoring**: Track all outgoing requests and responses
255
+ - **Retry Logic**: Add exponential backoff for failed requests
256
+ - **Rate Limiting**: Control request frequency to respect API limits
257
+ - **Proxies & Authentication**: Route requests through proxies or add auth headers
258
+ - **Testing**: Inject mock responses without network calls
259
+
260
+ #### Example 1: Simple in-memory cache
261
+
262
+ ```ts
263
+ import { lookup } from 'rdapper';
264
+
265
+ const cache = new Map<string, Response>();
266
+
267
+ const cachedFetch: typeof fetch = async (input, init) => {
268
+ const url = typeof input === 'string' ? input : input.toString();
269
+
270
+ // Check cache first
271
+ if (cache.has(url)) {
272
+ console.log('[Cache Hit]', url);
273
+ return cache.get(url)!.clone();
274
+ }
275
+
276
+ // Fetch and cache
277
+ console.log('[Cache Miss]', url);
278
+ const response = await fetch(input, init);
279
+ cache.set(url, response.clone());
280
+ return response;
281
+ };
282
+
283
+ const result = await lookup('example.com', { customFetch: cachedFetch });
284
+ ```
285
+
286
+ #### Example 2: Request logging and monitoring
287
+
288
+ ```ts
289
+ import { lookup } from 'rdapper';
290
+
291
+ const loggingFetch: typeof fetch = async (input, init) => {
292
+ const url = typeof input === 'string' ? input : input.toString();
293
+ const start = Date.now();
294
+
295
+ console.log(`[→] ${init?.method || 'GET'} ${url}`);
296
+
297
+ try {
298
+ const response = await fetch(input, init);
299
+ const duration = Date.now() - start;
300
+ console.log(`[←] ${response.status} ${url} (${duration}ms)`);
301
+ return response;
302
+ } catch (error) {
303
+ const duration = Date.now() - start;
304
+ console.error(`[✗] ${url} failed after ${duration}ms:`, error);
305
+ throw error;
306
+ }
307
+ };
308
+
309
+ const result = await lookup('example.com', { customFetch: loggingFetch });
310
+ ```
311
+
312
+ #### Example 3: Retry logic with exponential backoff
313
+
314
+ ```ts
315
+ import { lookup } from 'rdapper';
316
+
317
+ async function fetchWithRetry(
318
+ input: RequestInfo | URL,
319
+ init?: RequestInit,
320
+ maxRetries = 3
321
+ ): Promise<Response> {
322
+ let lastError: Error | undefined;
323
+
324
+ for (let attempt = 0; attempt <= maxRetries; attempt++) {
325
+ try {
326
+ const response = await fetch(input, init);
327
+
328
+ // Retry on 5xx errors
329
+ if (response.status >= 500 && attempt < maxRetries) {
330
+ const delay = Math.min(1000 * 2 ** attempt, 10000);
331
+ console.log(`Retrying after ${delay}ms (attempt ${attempt + 1}/${maxRetries})`);
332
+ await new Promise(resolve => setTimeout(resolve, delay));
333
+ continue;
334
+ }
335
+
336
+ return response;
337
+ } catch (error) {
338
+ lastError = error as Error;
339
+ if (attempt < maxRetries) {
340
+ const delay = Math.min(1000 * 2 ** attempt, 10000);
341
+ await new Promise(resolve => setTimeout(resolve, delay));
342
+ continue;
343
+ }
344
+ }
345
+ }
346
+
347
+ throw lastError || new Error('Max retries exceeded');
348
+ }
349
+
350
+ const result = await lookup('example.com', { customFetch: fetchWithRetry });
351
+ ```
352
+
353
+ #### Example 4: HTTP caching with cache-control headers
354
+
355
+ ```ts
356
+ import { lookup } from 'rdapper';
357
+
358
+ interface CachedResponse {
359
+ response: Response;
360
+ expiresAt: number;
361
+ }
362
+
363
+ const httpCache = new Map<string, CachedResponse>();
364
+
365
+ const httpCachingFetch: typeof fetch = async (input, init) => {
366
+ const url = typeof input === 'string' ? input : input.toString();
367
+ const now = Date.now();
368
+
369
+ // Check if we have a valid cached response
370
+ const cached = httpCache.get(url);
371
+ if (cached && cached.expiresAt > now) {
372
+ return cached.response.clone();
373
+ }
374
+
375
+ // Fetch fresh response
376
+ const response = await fetch(input, init);
377
+
378
+ // Parse Cache-Control header
379
+ const cacheControl = response.headers.get('cache-control');
380
+ if (cacheControl) {
381
+ const maxAgeMatch = cacheControl.match(/max-age=(\d+)/);
382
+ if (maxAgeMatch) {
383
+ const maxAge = parseInt(maxAgeMatch[1], 10);
384
+ httpCache.set(url, {
385
+ response: response.clone(),
386
+ expiresAt: now + maxAge * 1000,
387
+ });
388
+ }
389
+ }
390
+
391
+ return response;
392
+ };
393
+
394
+ const result = await lookup('example.com', { customFetch: httpCachingFetch });
395
+ ```
396
+
397
+ #### Example 5: Combining with customBootstrapData
398
+
399
+ You can use both `customFetch` and `customBootstrapData` together for maximum control:
400
+
401
+ ```ts
402
+ import { lookup, type BootstrapData } from 'rdapper';
403
+
404
+ // Pre-load bootstrap data (no fetch needed for this)
405
+ const bootstrapData: BootstrapData = await getFromCache('bootstrap');
406
+
407
+ // Use custom fetch for all other RDAP requests
408
+ const cachedFetch: typeof fetch = async (input, init) => {
409
+ // Your caching logic for RDAP domain and entity lookups
410
+ return fetch(input, init);
411
+ };
412
+
413
+ const result = await lookup('example.com', {
414
+ customBootstrapData: bootstrapData,
415
+ customFetch: cachedFetch,
416
+ });
417
+ ```
418
+
419
+ **Note**: When `customBootstrapData` is provided, the bootstrap registry will not be fetched, so your custom fetch will only be used for RDAP domain and entity/related link requests.
420
+
89
421
  ### Options
90
422
 
91
423
  - `timeoutMs?: number` – Total timeout budget per network operation (default `15000`).
@@ -96,7 +428,9 @@ const res = await lookup("example.com", { rdapOnly: true });
96
428
  - `rdapFollowLinks?: boolean` – Follow related/entity RDAP links to enrich data (default `true`).
97
429
  - `maxRdapLinkHops?: number` – Maximum RDAP related link hops to follow (default `2`).
98
430
  - `rdapLinkRels?: string[]` – RDAP link rel values to consider (default `["related","entity","registrar","alternate"]`).
99
- - `customBootstrapUrl?: string` – Override RDAP bootstrap URL.
431
+ - `customBootstrapData?: BootstrapData` – Pre-loaded RDAP bootstrap data for caching control (see [Bootstrap Data Caching](#bootstrap-data-caching)).
432
+ - `customBootstrapUrl?: string` – Override RDAP bootstrap URL (ignored if `customBootstrapData` is provided).
433
+ - `customFetch?: FetchLike` – Custom fetch implementation for all HTTP requests (see [Custom Fetch Implementation](#custom-fetch-implementation)).
100
434
  - `whoisHints?: Record<string, string>` – Override/add authoritative WHOIS per TLD (keys are lowercase TLDs, values may include or omit `whois://`).
101
435
  - `includeRaw?: boolean` – Include `rawRdap`/`rawWhois` in the returned record (default `false`).
102
436
  - `signal?: AbortSignal` – Optional cancellation signal.