@cleandns/whois-rdap 1.0.52 → 1.0.55

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.
Files changed (40) hide show
  1. package/.github/workflows/test.yml +38 -0
  2. package/dist/index.d.ts +3 -1
  3. package/dist/index.js +69 -88
  4. package/dist/index.test.d.ts +1 -0
  5. package/dist/index.test.js +360 -0
  6. package/dist/ip.js +8 -1
  7. package/dist/polyfills.d.ts +11 -0
  8. package/dist/polyfills.js +64 -0
  9. package/dist/port43.js +28 -14
  10. package/dist/utils/escapeRegex.d.ts +5 -0
  11. package/dist/utils/escapeRegex.js +7 -0
  12. package/dist/utils/findInObject.js +14 -4
  13. package/dist/utils/findNameservers.d.ts +1 -0
  14. package/dist/utils/findNameservers.js +15 -0
  15. package/dist/utils/findStatus.d.ts +1 -0
  16. package/dist/utils/findStatus.js +10 -0
  17. package/dist/utils/findTimestamps.d.ts +6 -0
  18. package/dist/utils/findTimestamps.js +39 -0
  19. package/dist/utils/toArray.d.ts +5 -0
  20. package/dist/utils/toArray.js +17 -0
  21. package/dist/utils/validateDomain.d.ts +5 -0
  22. package/dist/utils/validateDomain.js +18 -0
  23. package/github-workflow.patch +1044 -0
  24. package/package.json +24 -17
  25. package/src/index.test.ts +440 -0
  26. package/src/index.ts +65 -89
  27. package/src/ip.ts +9 -1
  28. package/src/polyfills.ts +76 -0
  29. package/src/port43.ts +29 -21
  30. package/src/utils/escapeRegex.ts +7 -0
  31. package/src/utils/findInObject.ts +19 -4
  32. package/src/utils/findNameservers.ts +15 -0
  33. package/src/utils/findStatus.ts +12 -0
  34. package/src/utils/findTimestamps.ts +44 -0
  35. package/src/utils/toArray.ts +17 -0
  36. package/src/utils/validateDomain.ts +18 -0
  37. package/tsconfig.json +1 -1
  38. package/dist/wwwservers.d.ts +0 -1
  39. package/dist/wwwservers.js +0 -3
  40. package/pnpm-workspace.yaml +0 -2
package/src/index.ts CHANGED
@@ -1,3 +1,6 @@
1
+ // Polyfills for Node.js 18 compatibility
2
+ import "./polyfills.js";
3
+
1
4
  import { WhoisOptions, WhoisResponse, WhoisTimestampFields } from "../whois.js";
2
5
  import { parseIpResponse } from "./ip.js";
3
6
  import { determinePort43Domain, port43 } from "./port43.js";
@@ -5,10 +8,19 @@ import { findInObject } from "./utils/findInObject.js";
5
8
  import { fixArrays } from "./utils/fixArrays.js";
6
9
  import { ianaIdToRegistrar } from "./utils/ianaIdToRegistrar.js";
7
10
  import { tldToRdap } from "./utils/tldToRdap.js";
8
- import { normalizeWhoisStatus } from "./whoisStatus.js";
9
11
  import { resolve4 } from "dns/promises";
10
-
11
- const eventMap = new Map<string, WhoisTimestampFields>([
12
+ import { toArray } from "./utils/toArray.js";
13
+ import { validateDomain } from "./utils/validateDomain.js";
14
+ import { findStatus } from "./utils/findStatus.js";
15
+ import { findNameservers } from "./utils/findNameservers.js";
16
+ import { findTimestamps } from "./utils/findTimestamps.js";
17
+ import createDebug from "debug";
18
+ import { escapeRegex } from "./utils/escapeRegex.js";
19
+
20
+ // Debug logger - enable with DEBUG=whois:* environment variable
21
+ const debug = createDebug("whois:rdap");
22
+
23
+ export const eventMap = new Map<string, WhoisTimestampFields>([
12
24
  ["registration", "created"],
13
25
  ["last changed", "updated"],
14
26
  ["expiration", "expires"],
@@ -21,10 +33,27 @@ export async function whois(
21
33
  ): Promise<WhoisResponse> {
22
34
  const _fetch = options.fetch || fetch;
23
35
 
24
- let domain = origDomain;
36
+ // Validate and sanitize input
37
+ let domain: string;
38
+ try {
39
+ domain = validateDomain(origDomain);
40
+ } catch (e: any) {
41
+ return {
42
+ found: false,
43
+ statusCode: 400,
44
+ error: e.message,
45
+ registrar: { id: 0, name: null },
46
+ reseller: null,
47
+ status: [],
48
+ statusDelta: [],
49
+ nameservers: [],
50
+ ts: { created: null, updated: null, expires: null },
51
+ };
52
+ }
53
+
25
54
  let url: string | null = null;
26
55
 
27
- [domain, url] = await tldToRdap(origDomain);
56
+ [domain, url] = await tldToRdap(domain);
28
57
 
29
58
  const response: WhoisResponse = {
30
59
  found: false,
@@ -59,7 +88,6 @@ export async function whois(
59
88
  thinResponse = await _fetch(thinRdap)
60
89
  .then((r) => {
61
90
  response.statusCode = r.status;
62
- // console.log({ ok: r.ok, status: r.status, statusText: r.statusText });
63
91
  if (r.status >= 200 && r.status < 400) {
64
92
  return r.json() as any;
65
93
  }
@@ -67,7 +95,7 @@ export async function whois(
67
95
  return null;
68
96
  })
69
97
  .catch((error: Error) => {
70
- console.warn(`thin RDAP lookup failure: ${error.message}`);
98
+ debug("thin RDAP lookup failure for %s: %s", domain, error.message);
71
99
  return null;
72
100
  });
73
101
 
@@ -94,14 +122,14 @@ export async function whois(
94
122
  let thickResponse: any = null;
95
123
 
96
124
  if (!options.thinOnly && thickRdap) {
97
- // console.log(`fetching thick RDAP: ${thickRdap}`);
125
+ debug("fetching thick RDAP: %s", thickRdap);
98
126
  thickResponse = await _fetch(thickRdap)
99
127
  .then((r) => r.json() as any)
100
128
  .catch(() => null);
101
129
  if (thickResponse && !thickResponse.errorCode && !thickResponse.error) {
102
130
  } else {
103
131
  thickResponse = null;
104
- // console.warn(`thick RDAP failed for ${domain}`);
132
+ debug("thick RDAP failed for %s", domain);
105
133
  }
106
134
  }
107
135
 
@@ -113,10 +141,14 @@ export async function whois(
113
141
  const resellers: any[] = [];
114
142
 
115
143
  async function extractRegistrarsAndResellers(response: any, url: string, isThick?: boolean) {
116
- for (const ent of [
117
- ...(response.entities || []),
144
+ // Use toArray to safely handle entities that might not be iterable
145
+ const entities = toArray(response.entities);
146
+ const entityList = [
147
+ ...entities,
118
148
  response.entity ? { events: response.events, ...response.entity } : null,
119
- ].filter(Boolean)) {
149
+ ].filter(Boolean);
150
+
151
+ for (const ent of entityList) {
120
152
  if (ent.roles?.includes("registrar") || ent.role === "registrar") {
121
153
  const pubIds: any[] = [];
122
154
  if (ent.publicIds) {
@@ -145,7 +177,6 @@ export async function whois(
145
177
  ;
146
178
 
147
179
  if (reg) {
148
- // console.log(ent.vcardArray);
149
180
  const id = typeof reg === 'object' ? 0 : reg;
150
181
  const name =
151
182
  (parseInt(id) == id
@@ -157,8 +188,10 @@ export async function whois(
157
188
  (el: any[]) => el[3],
158
189
  reg
159
190
  );
191
+ // Safely handle ent.entities
192
+ const entEntities = toArray(ent.entities);
160
193
  const email =
161
- [ent, ...(ent.entities || [])]
194
+ [ent, ...entEntities]
162
195
  .filter((e) => e?.vcardArray)
163
196
  .map((e) =>
164
197
  findInObject(
@@ -171,7 +204,7 @@ export async function whois(
171
204
  .filter(Boolean)?.[0] || "";
172
205
 
173
206
  const abuseEmail =
174
- [ent, ...(ent.entities || [])]
207
+ [ent, ...entEntities]
175
208
  .filter((e) => e?.vcardArray)
176
209
  .map((e) =>
177
210
  findInObject(
@@ -187,10 +220,11 @@ export async function whois(
187
220
  ent.events || response.events || ent.enents || response.enents;
188
221
  registrars.push({ id, name, email, abuseEmail, events });
189
222
  }
190
- // handles .ca
223
+ // handles .ca - with safe optional chaining
191
224
  else if (ent.vcardArray?.[1]?.[3]?.[3] === 'registrar') {
225
+ const entEntities = toArray(ent.entities);
192
226
  const email =
193
- [ent, ...(ent.entities || [])]
227
+ [ent, ...entEntities]
194
228
  .filter((e) => e?.vcardArray)
195
229
  .map((e) =>
196
230
  findInObject(
@@ -203,7 +237,7 @@ export async function whois(
203
237
  .filter(Boolean)?.[0] || "";
204
238
 
205
239
  const abuseEmail =
206
- [ent, ...(ent.entities || [])]
240
+ [ent, ...entEntities]
207
241
  .filter((e) => e?.vcardArray)
208
242
  .map((e) =>
209
243
  findInObject(
@@ -215,12 +249,14 @@ export async function whois(
215
249
  )
216
250
  .filter(Boolean)?.[0] || "";
217
251
 
218
- registrars.push({ id: 0, name: ent.vcardArray[1][1][3], email, abuseEmail, events: ent.events || response.events || ent.enents || response.enents });
252
+ const vcardName = ent.vcardArray?.[1]?.[1]?.[3] || '';
253
+ registrars.push({ id: 0, name: vcardName, email, abuseEmail, events: ent.events || response.events || ent.enents || response.enents });
219
254
  }
220
- // handles .si
221
- else if (ent.vcardArray && ent.vcardArray[1] && ent.vcardArray[1].find((el: string[]) => el[0] === 'fn')) {
255
+ // handles .si - with safe array access
256
+ else if (ent.vcardArray && Array.isArray(ent.vcardArray[1]) && ent.vcardArray[1].find((el: string[]) => el[0] === 'fn')) {
257
+ const entEntities = toArray(ent.entities);
222
258
  const email =
223
- [ent, ...(ent.entities || [])]
259
+ [ent, ...entEntities]
224
260
  .filter((e) => e?.vcardArray)
225
261
  .map((e) =>
226
262
  findInObject(
@@ -233,7 +269,7 @@ export async function whois(
233
269
  .filter(Boolean)?.[0] || "";
234
270
 
235
271
  const abuseEmail =
236
- [ent, ...(ent.entities || [])]
272
+ [ent, ...entEntities]
237
273
  .filter((e) => e?.vcardArray)
238
274
  .map((e) =>
239
275
  findInObject(
@@ -260,7 +296,9 @@ export async function whois(
260
296
  registrars.push({ id, name, email, abuseEmail, events: ent.events || response.events || ent.enents || response.enents });
261
297
  }
262
298
  else {
263
- registrars.push({ id: ent.handle || 0, name: ent.vcardArray[1].find((el: string[]) => el[0] === 'fn')[3], email, abuseEmail, events: ent.events || response.events || ent.enents || response.enents });
299
+ const fnEntry = ent.vcardArray[1].find((el: string[]) => el[0] === 'fn');
300
+ const name = fnEntry ? fnEntry[3] : ent.handle || '';
301
+ registrars.push({ id: ent.handle || 0, name, email, abuseEmail, events: ent.events || response.events || ent.enents || response.enents });
264
302
  }
265
303
  }
266
304
  // handles .ar
@@ -285,8 +323,9 @@ export async function whois(
285
323
  (el: any[]) => el[3],
286
324
  id
287
325
  );
326
+ const entEntities = toArray(ent.entities);
288
327
  const email =
289
- [ent, ...(ent.entities || [])]
328
+ [ent, ...entEntities]
290
329
  .filter((e) => e?.vcardArray)
291
330
  .map((e) =>
292
331
  findInObject(
@@ -299,7 +338,7 @@ export async function whois(
299
338
  .filter(Boolean)?.[0] || "";
300
339
 
301
340
  const abuseEmail =
302
- [ent, ...(ent.entities || [])]
341
+ [ent, ...entEntities]
303
342
  .filter((e) => e?.vcardArray)
304
343
  .map((e) =>
305
344
  findInObject(
@@ -392,66 +431,3 @@ export async function whois(
392
431
 
393
432
  return response;
394
433
  }
395
-
396
- function findStatus(statuses: string | string[], domain: string): string[] {
397
- // console.warn({ domain, statuses });
398
-
399
- return (Array.isArray(statuses)
400
- ? statuses
401
- : statuses && typeof statuses === "object"
402
- ? Object.keys(statuses)
403
- : typeof statuses === "string"
404
- ? statuses.trim().split(/\s*,\s*/)
405
- : []
406
- ).map((status) => normalizeWhoisStatus(status));
407
- }
408
-
409
- function findNameservers(values: any[]): string[] {
410
- let nameservers: any[] = [];
411
- if (Array.isArray(values)) {
412
- nameservers = values;
413
- } else if (typeof values === "object") {
414
- nameservers = Object.values(values);
415
- }
416
-
417
- return nameservers
418
- .map((ns) => ns.ldhName || ns.ldnName || ns.ipAddresses?.v4)
419
- .flat()
420
- .filter((ns) => ns)
421
- .map((ns) => (ns.stringValue || ns).toLocaleLowerCase())
422
- .sort();
423
- }
424
-
425
- function findTimestamps(values: any[]) {
426
- const ts: Record<WhoisTimestampFields, Date | null> = {
427
- created: null,
428
- updated: null,
429
- expires: null,
430
- };
431
-
432
- let events: any = [];
433
-
434
- if (Array.isArray(values)) {
435
- events = values;
436
- } else if (typeof values === "object") {
437
- events = Object.values(values);
438
- }
439
-
440
- for (const [event, field] of eventMap) {
441
- events.find(
442
- (ev: any) => {
443
- const isMatch = ev?.eventAction?.toLocaleLowerCase() === event && ev.eventDate;
444
- if (isMatch) {
445
- const d = new Date(ev.eventDate.toString().replace(/\+0000Z$/, "Z"));
446
- // console.log(field, ev.eventDate, d);
447
- if (!isNaN(d.valueOf())) {
448
- ts[field] = d;
449
- return true;
450
- }
451
- }
452
- }
453
- );
454
- }
455
-
456
- return ts;
457
- }
package/src/ip.ts CHANGED
@@ -3,7 +3,15 @@ import { WhoisResponse } from "../whois.js";
3
3
  export function parseIpResponse(ip: string, rdap: any, response: WhoisResponse) {
4
4
  response.found = Boolean(rdap.handle);
5
5
 
6
- const registry = rdap.port43 ? rdap.port43.match(/\.(\w+)\./)[1].toUpperCase() : '';
6
+ // Safely extract registry from port43 with null check
7
+ let registry = '';
8
+ if (rdap.port43) {
9
+ const match = rdap.port43.match(/\.(\w+)\./);
10
+ if (match) {
11
+ registry = match[1].toUpperCase();
12
+ }
13
+ }
14
+
7
15
  const realRdapServer = rdap.links?.find(({ rel }: { rel: string }) => rel === 'self')?.value?.replace(/\/ip\/.*/, '/ip/');
8
16
 
9
17
  response.server = realRdapServer || 'https://rdap.org/ip/';
@@ -0,0 +1,76 @@
1
+ /**
2
+ * Polyfills for Node.js 18 compatibility.
3
+ * String.prototype.toWellFormed was added in Node.js 20.
4
+ */
5
+
6
+ // Extend String interface for TypeScript
7
+ declare global {
8
+ interface String {
9
+ toWellFormed(): string;
10
+ isWellFormed(): boolean;
11
+ }
12
+ }
13
+
14
+ // Polyfill for String.prototype.toWellFormed (ES2024)
15
+ // Replaces lone surrogates with U+FFFD (replacement character)
16
+ if (typeof String.prototype.toWellFormed !== 'function') {
17
+ String.prototype.toWellFormed = function () {
18
+ const str = String(this);
19
+ const len = str.length;
20
+ let result = '';
21
+
22
+ for (let i = 0; i < len; i++) {
23
+ const code = str.charCodeAt(i);
24
+
25
+ // Check for lone surrogates
26
+ if (code >= 0xD800 && code <= 0xDBFF) {
27
+ // High surrogate
28
+ if (i + 1 < len) {
29
+ const next = str.charCodeAt(i + 1);
30
+ if (next >= 0xDC00 && next <= 0xDFFF) {
31
+ // Valid surrogate pair
32
+ result += str[i] + str[i + 1];
33
+ i++;
34
+ continue;
35
+ }
36
+ }
37
+ // Lone high surrogate - replace with U+FFFD
38
+ result += '\uFFFD';
39
+ } else if (code >= 0xDC00 && code <= 0xDFFF) {
40
+ // Lone low surrogate - replace with U+FFFD
41
+ result += '\uFFFD';
42
+ } else {
43
+ result += str[i];
44
+ }
45
+ }
46
+
47
+ return result;
48
+ };
49
+ }
50
+
51
+ // Polyfill for String.prototype.isWellFormed (ES2024)
52
+ if (typeof String.prototype.isWellFormed !== 'function') {
53
+ String.prototype.isWellFormed = function () {
54
+ const str = String(this);
55
+ const len = str.length;
56
+
57
+ for (let i = 0; i < len; i++) {
58
+ const code = str.charCodeAt(i);
59
+
60
+ if (code >= 0xD800 && code <= 0xDBFF) {
61
+ // High surrogate - check for valid pair
62
+ if (i + 1 >= len) return false;
63
+ const next = str.charCodeAt(i + 1);
64
+ if (next < 0xDC00 || next > 0xDFFF) return false;
65
+ i++; // Skip the low surrogate
66
+ } else if (code >= 0xDC00 && code <= 0xDFFF) {
67
+ // Lone low surrogate
68
+ return false;
69
+ }
70
+ }
71
+
72
+ return true;
73
+ };
74
+ }
75
+
76
+ export {};
package/src/port43.ts CHANGED
@@ -5,6 +5,12 @@ import { port43servers, port43parsers } from "./port43servers.js";
5
5
  import { ianaToRegistrarCache } from "./utils/ianaIdToRegistrar.js";
6
6
  import { WhoisResponse } from "../whois.js";
7
7
  import { normalizeWhoisStatus } from "./whoisStatus.js";
8
+ import createDebug from "debug";
9
+ import { escapeRegex } from "./utils/escapeRegex.js";
10
+
11
+ // Debug logger - enable with DEBUG=whois:* environment variable
12
+ const debug = createDebug("whois:port43");
13
+
8
14
 
9
15
  export function determinePort43Domain(actor: string) {
10
16
  const parsed = parseDomain(actor);
@@ -36,7 +42,7 @@ export async function port43(actor: string, _fetch: typeof fetch): Promise<Whois
36
42
  : `${domain}\r\n`;
37
43
  const port = opts?.port || 43;
38
44
 
39
- // console.log(`looking up ${domain} on ${server}`);
45
+ debug("looking up %s on %s:%d", domain, server, port);
40
46
 
41
47
  const response: WhoisResponse = {
42
48
  found: true,
@@ -82,7 +88,7 @@ export async function port43(actor: string, _fetch: typeof fetch): Promise<Whois
82
88
  }
83
89
  }
84
90
  } catch (error: any) {
85
- console.warn({ port, server, query, error: error.message });
91
+ debug("port43 lookup error: %O", { port, server, query, error: error.message });
86
92
  response.found = false;
87
93
  response.statusCode = 500;
88
94
  response.error = error.message || "Unknown error during port 43 lookup";
@@ -93,7 +99,6 @@ export async function port43(actor: string, _fetch: typeof fetch): Promise<Whois
93
99
  }
94
100
 
95
101
  port43response = port43response.replace(/^[ \t]+/gm, "");
96
- // console.log(port43response);
97
102
 
98
103
  let m;
99
104
 
@@ -145,12 +150,6 @@ export async function port43(actor: string, _fetch: typeof fetch): Promise<Whois
145
150
  )) &&
146
151
  (response.reseller = m[1].trim());
147
152
 
148
- // console.log(port43response)
149
-
150
- // Updated Date: 2024-11-21T13:42:54Z
151
- // Creation Date: 2017-12-16T02:11:08Z
152
- // Registry Expiry Date: 2031-07-10T02:11:08Z
153
-
154
153
  !response.ts.updated &&
155
154
  (m = port43response.match(
156
155
  /^(?:Last Modified|Updated Date|Last updated on|domain_datelastmodified|last-update|modified|last modified)\.*:[ \t]*(\S.+)/im
@@ -235,6 +234,7 @@ export async function port43(actor: string, _fetch: typeof fetch): Promise<Whois
235
234
  if (response.ts.updated && !response.ts.updated.valueOf()) response.ts.updated = null;
236
235
  if (response.ts.expires && !response.ts.expires.valueOf()) response.ts.expires = null;
237
236
 
237
+ // Match registrar name against IANA cache using escaped regex to prevent ReDoS
238
238
  if (response.registrar.id === 0 && response.registrar.name !== "") {
239
239
  for (const [id, { name }] of ianaToRegistrarCache.entries()) {
240
240
  if (name === response.registrar.name) {
@@ -244,24 +244,32 @@ export async function port43(actor: string, _fetch: typeof fetch): Promise<Whois
244
244
  }
245
245
  }
246
246
 
247
- if (response.registrar.id === 0 && response.registrar.name !== "") {
247
+ if (response.registrar.id === 0 && response.registrar.name && response.registrar.name !== "") {
248
+ const escapedName = escapeRegex(response.registrar.name);
248
249
  for (const [id, { name }] of ianaToRegistrarCache.entries()) {
249
- if (name.match(new RegExp(`\\b${response.registrar.name}\\b`, "i"))) {
250
- response.registrar.id = id;
251
- break;
250
+ try {
251
+ if (name.match(new RegExp(`\\b${escapedName}\\b`, "i"))) {
252
+ response.registrar.id = id;
253
+ break;
254
+ }
255
+ } catch {
256
+ // Skip if regex still fails for some reason
257
+ continue;
252
258
  }
253
259
  }
254
260
  }
255
261
 
256
262
  if (response.registrar.id === 0 && response.registrar.name) {
263
+ const escapedName = escapeRegex(response.registrar.name.replace(/,.*/, ""));
257
264
  for (const [id, { name }] of ianaToRegistrarCache.entries()) {
258
- if (
259
- name.match(
260
- new RegExp(`\\b${response.registrar.name.replace(/,.*/, "")}\\b`, "i")
261
- )
262
- ) {
263
- response.registrar.id = id;
264
- break;
265
+ try {
266
+ if (name.match(new RegExp(`\\b${escapedName}\\b`, "i"))) {
267
+ response.registrar.id = id;
268
+ break;
269
+ }
270
+ } catch {
271
+ // Skip if regex still fails for some reason
272
+ continue;
265
273
  }
266
274
  }
267
275
  }
@@ -292,4 +300,4 @@ function reformatDate(date: string) {
292
300
  }
293
301
 
294
302
  return date;
295
- }
303
+ }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Escapes special regex characters in a string.
3
+ * Prevents ReDoS attacks when using user input in RegExp constructor.
4
+ */
5
+ export function escapeRegex(str: string): string {
6
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
7
+ }
@@ -7,17 +7,32 @@ export function findInObject(
7
7
  const found = _findInObject(obj, condition);
8
8
  return found === undefined ? fallback : extractor(found);
9
9
  }
10
+
10
11
  function _findInObject(obj: any, condition: (el: any) => boolean): any {
12
+ // Handle null/undefined
13
+ if (obj === null || obj === undefined) {
14
+ return undefined;
15
+ }
16
+
11
17
  for (const key in obj) {
12
- if (condition(obj[key])) {
13
- return obj[key];
18
+ // Skip inherited properties
19
+ if (!Object.prototype.hasOwnProperty.call(obj, key)) {
20
+ continue;
14
21
  }
15
22
 
16
- if (typeof obj[key] === "object") {
17
- const result = _findInObject(obj[key], condition);
23
+ const value = obj[key];
24
+
25
+ if (condition(value)) {
26
+ return value;
27
+ }
28
+
29
+ if (value !== null && typeof value === "object") {
30
+ const result = _findInObject(value, condition);
18
31
  if (result !== undefined) {
19
32
  return result;
20
33
  }
21
34
  }
22
35
  }
36
+
37
+ return undefined;
23
38
  }
@@ -0,0 +1,15 @@
1
+ export function findNameservers(values: any[]): string[] {
2
+ let nameservers: any[] = [];
3
+ if (Array.isArray(values)) {
4
+ nameservers = values;
5
+ } else if (typeof values === "object") {
6
+ nameservers = Object.values(values);
7
+ }
8
+
9
+ return nameservers
10
+ .map((ns) => ns.ldhName || ns.ldnName || ns.ipAddresses?.v4)
11
+ .flat()
12
+ .filter((ns) => ns)
13
+ .map((ns) => (ns.stringValue || ns).toLocaleLowerCase())
14
+ .sort();
15
+ }
@@ -0,0 +1,12 @@
1
+ import { normalizeWhoisStatus } from "../whoisStatus.js";
2
+
3
+ export function findStatus(statuses: string | string[], domain: string): string[] {
4
+ return (Array.isArray(statuses)
5
+ ? statuses
6
+ : statuses && typeof statuses === "object"
7
+ ? Object.keys(statuses)
8
+ : typeof statuses === "string"
9
+ ? statuses.trim().split(/\s*,\s*/)
10
+ : []
11
+ ).map((status) => normalizeWhoisStatus(status));
12
+ }
@@ -0,0 +1,44 @@
1
+ import { WhoisTimestampFields } from "../../whois.js";
2
+ import { eventMap } from "../index.js";
3
+
4
+ /**
5
+ * Extracts timestamps from RDAP events array.
6
+ * Properly iterates through events and breaks when a match is found.
7
+ */
8
+ export function findTimestamps(values: any[]) {
9
+ const ts: Record<WhoisTimestampFields, Date | null> = {
10
+ created: null,
11
+ updated: null,
12
+ expires: null,
13
+ };
14
+
15
+ let events: any[] = [];
16
+
17
+ if (Array.isArray(values)) {
18
+ events = values;
19
+ } else if (typeof values === "object" && values !== null) {
20
+ events = Object.values(values);
21
+ }
22
+
23
+ // Iterate through each event type we're looking for
24
+ for (const [eventAction, field] of eventMap) {
25
+ // Skip if we already have a value for this field
26
+ if (ts[field] !== null) {
27
+ continue;
28
+ }
29
+
30
+ // Find matching event and extract date
31
+ for (const ev of events) {
32
+ if (ev?.eventAction?.toLocaleLowerCase() === eventAction && ev.eventDate) {
33
+ const dateStr = ev.eventDate.toString().replace(/\+0000Z$/, "Z");
34
+ const d = new Date(dateStr);
35
+ if (!isNaN(d.valueOf())) {
36
+ ts[field] = d;
37
+ break; // Found valid date, stop searching for this field
38
+ }
39
+ }
40
+ }
41
+ }
42
+
43
+ return ts;
44
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Safely converts a value to an array.
3
+ * Handles cases where the value might be null, undefined, or a non-iterable object.
4
+ */
5
+ export function toArray<T>(value: T | T[] | null | undefined): T[] {
6
+ if (value === null || value === undefined) {
7
+ return [];
8
+ }
9
+ if (Array.isArray(value)) {
10
+ return value;
11
+ }
12
+ // Handle object case - some RDAP responses return objects instead of arrays
13
+ if (typeof value === "object") {
14
+ return Object.values(value) as T[];
15
+ }
16
+ return [];
17
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Validates domain input format.
3
+ * Returns sanitized domain or throws on invalid input.
4
+ */
5
+ export function validateDomain(domain: string): string {
6
+ if (!domain || typeof domain !== 'string') {
7
+ throw new Error('Domain must be a non-empty string');
8
+ }
9
+ // Basic sanitization - trim whitespace
10
+ const sanitized = domain.trim().toLowerCase();
11
+ if (sanitized.length === 0) {
12
+ throw new Error('Domain must be a non-empty string');
13
+ }
14
+ if (sanitized.length > 253) {
15
+ throw new Error('Domain name too long');
16
+ }
17
+ return sanitized;
18
+ }
package/tsconfig.json CHANGED
@@ -26,7 +26,7 @@
26
26
 
27
27
  /* Modules */
28
28
  "module": "nodenext", /* Specify what module code is generated. */
29
- // "rootDir": "./", /* Specify the root folder within your source files. */
29
+ "rootDir": "./src", /* Specify the root folder within your source files. */
30
30
  // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
31
31
  // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
32
32
  // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
@@ -1 +0,0 @@
1
- export declare const loadWWWServers: Record<string, string>;
@@ -1,3 +0,0 @@
1
- export const loadWWWServers = {
2
- "it.com": `https://engine.itcomdomains.com/api/v1/domain/%%domain%%/whois_info`
3
- };
@@ -1,2 +0,0 @@
1
- onlyBuiltDependencies:
2
- - core-js