@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
@@ -0,0 +1,360 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert";
3
+ // Import polyfills first
4
+ import "./polyfills.js";
5
+ import { validateDomain } from "./utils/validateDomain.js";
6
+ import { escapeRegex } from "./utils/escapeRegex.js";
7
+ import { toArray } from "./utils/toArray.js";
8
+ // ============================================================================
9
+ // toArray utility tests
10
+ // ============================================================================
11
+ describe("toArray utility function", () => {
12
+ it("returns empty array for null", () => {
13
+ assert.deepStrictEqual(toArray(null), []);
14
+ });
15
+ it("returns empty array for undefined", () => {
16
+ assert.deepStrictEqual(toArray(undefined), []);
17
+ });
18
+ it("returns same array if already an array", () => {
19
+ const arr = [1, 2, 3];
20
+ assert.deepStrictEqual(toArray(arr), arr);
21
+ });
22
+ it("converts object to array of values", () => {
23
+ const obj = { a: 1, b: 2, c: 3 };
24
+ assert.deepStrictEqual(toArray(obj), [1, 2, 3]);
25
+ });
26
+ it("handles empty array", () => {
27
+ assert.deepStrictEqual(toArray([]), []);
28
+ });
29
+ it("handles empty object", () => {
30
+ assert.deepStrictEqual(toArray({}), []);
31
+ });
32
+ it("handles nested arrays", () => {
33
+ const arr = [[1, 2], [3, 4]];
34
+ assert.deepStrictEqual(toArray(arr), [[1, 2], [3, 4]]);
35
+ });
36
+ it("handles array-like objects with numeric keys", () => {
37
+ const obj = { "0": "a", "1": "b", "2": "c" };
38
+ assert.deepStrictEqual(toArray(obj), ["a", "b", "c"]);
39
+ });
40
+ });
41
+ describe("toArray handles RDAP edge cases (issue #16)", () => {
42
+ it("handles entities as object instead of array", () => {
43
+ const entities = {
44
+ "0": { roles: ["registrar"], handle: "123" },
45
+ "1": { roles: ["abuse"], handle: "456" },
46
+ };
47
+ const result = toArray(entities);
48
+ assert.strictEqual(Array.isArray(result), true);
49
+ assert.strictEqual(result.length, 2);
50
+ assert.deepStrictEqual(result[0], { roles: ["registrar"], handle: "123" });
51
+ });
52
+ it("handles nested entities being null", () => {
53
+ const result = toArray(null);
54
+ assert.deepStrictEqual(result, []);
55
+ });
56
+ it("handles nested entities being undefined", () => {
57
+ const result = toArray(undefined);
58
+ assert.deepStrictEqual(result, []);
59
+ });
60
+ });
61
+ // ============================================================================
62
+ // escapeRegex utility tests (ReDoS prevention)
63
+ // ============================================================================
64
+ describe("escapeRegex utility function", () => {
65
+ it("escapes special regex characters", () => {
66
+ const input = "test.*+?^${}()|[]\\";
67
+ const escaped = escapeRegex(input);
68
+ assert.strictEqual(escaped, "test\\.\\*\\+\\?\\^\\$\\{\\}\\(\\)\\|\\[\\]\\\\");
69
+ });
70
+ it("leaves normal strings unchanged", () => {
71
+ const input = "GoDaddy Inc";
72
+ const escaped = escapeRegex(input);
73
+ assert.strictEqual(escaped, "GoDaddy Inc");
74
+ });
75
+ it("handles empty string", () => {
76
+ const escaped = escapeRegex("");
77
+ assert.strictEqual(escaped, "");
78
+ });
79
+ it("handles string with only special characters", () => {
80
+ const input = ".*+";
81
+ const escaped = escapeRegex(input);
82
+ assert.strictEqual(escaped, "\\.\\*\\+");
83
+ });
84
+ it("escaped string can be used safely in RegExp", () => {
85
+ const malicious = "test(.*)+$";
86
+ const escaped = escapeRegex(malicious);
87
+ // Should not throw when creating RegExp
88
+ const regex = new RegExp(escaped, "i");
89
+ // The escaped string should match literally
90
+ assert.strictEqual(regex.test("test(.*)+$"), true);
91
+ assert.strictEqual(regex.test("testABC"), false);
92
+ });
93
+ });
94
+ // ============================================================================
95
+ // validateDomain utility tests
96
+ // ============================================================================
97
+ describe("validateDomain utility function", () => {
98
+ it("accepts valid domain", () => {
99
+ const result = validateDomain("example.com");
100
+ assert.strictEqual(result, "example.com");
101
+ });
102
+ it("trims whitespace", () => {
103
+ const result = validateDomain(" example.com ");
104
+ assert.strictEqual(result, "example.com");
105
+ });
106
+ it("converts to lowercase", () => {
107
+ const result = validateDomain("EXAMPLE.COM");
108
+ assert.strictEqual(result, "example.com");
109
+ });
110
+ it("throws on empty string", () => {
111
+ assert.throws(() => validateDomain(""), /non-empty string/);
112
+ });
113
+ it("throws on whitespace-only string", () => {
114
+ assert.throws(() => validateDomain(" "), /non-empty string/);
115
+ });
116
+ it("throws on null", () => {
117
+ assert.throws(() => validateDomain(null), /non-empty string/);
118
+ });
119
+ it("throws on undefined", () => {
120
+ assert.throws(() => validateDomain(undefined), /non-empty string/);
121
+ });
122
+ it("throws on domain too long", () => {
123
+ const longDomain = "a".repeat(254) + ".com";
124
+ assert.throws(() => validateDomain(longDomain), /too long/);
125
+ });
126
+ it("accepts domain at max length", () => {
127
+ const maxDomain = "a".repeat(249) + ".com"; // 253 chars
128
+ const result = validateDomain(maxDomain);
129
+ assert.strictEqual(result.length, 253);
130
+ });
131
+ });
132
+ // ============================================================================
133
+ // vcardArray safety checks (issue #12)
134
+ // ============================================================================
135
+ describe("vcardArray safety checks (issue #12)", () => {
136
+ it("Array.isArray correctly identifies arrays", () => {
137
+ const vcardArray = ["vcard", [["fn", {}, "text", "Test"]]];
138
+ assert.strictEqual(Array.isArray(vcardArray[1]), true);
139
+ assert.strictEqual(typeof vcardArray[1].find, "function");
140
+ });
141
+ it("Array.isArray returns false for non-arrays", () => {
142
+ const vcardArray = ["vcard", "not-an-array"];
143
+ assert.strictEqual(Array.isArray(vcardArray[1]), false);
144
+ });
145
+ it("Array.isArray returns false for undefined", () => {
146
+ const vcardArray = ["vcard"];
147
+ assert.strictEqual(Array.isArray(vcardArray[1]), false);
148
+ });
149
+ it("safe access pattern prevents TypeError", () => {
150
+ const vcardArray = ["vcard"];
151
+ const hasFn = vcardArray && Array.isArray(vcardArray[1]) && vcardArray[1].find((el) => el[0] === 'fn');
152
+ assert.strictEqual(hasFn, false);
153
+ });
154
+ it("safe access pattern works with valid data", () => {
155
+ const vcardArray = ["vcard", [["fn", {}, "text", "Test Registrar"]]];
156
+ const hasFn = vcardArray && Array.isArray(vcardArray[1]) && vcardArray[1].find((el) => el[0] === 'fn');
157
+ assert.deepStrictEqual(hasFn, ["fn", {}, "text", "Test Registrar"]);
158
+ });
159
+ });
160
+ // ============================================================================
161
+ // debug module integration (issue #13)
162
+ // ============================================================================
163
+ describe("debug module integration (issue #13)", () => {
164
+ it("debug module is importable", async () => {
165
+ const createDebug = (await import("debug")).default;
166
+ assert.strictEqual(typeof createDebug, "function");
167
+ });
168
+ it("debug instance can be created", async () => {
169
+ const createDebug = (await import("debug")).default;
170
+ const debug = createDebug("test:namespace");
171
+ assert.strictEqual(typeof debug, "function");
172
+ });
173
+ it("debug function can be called without error", async () => {
174
+ const createDebug = (await import("debug")).default;
175
+ const debug = createDebug("test:namespace");
176
+ // Should not throw
177
+ debug("test message %s", "arg");
178
+ });
179
+ });
180
+ // ============================================================================
181
+ // findTimestamps anti-pattern fix (commit comment)
182
+ // ============================================================================
183
+ describe("findTimestamps behavior", () => {
184
+ it("properly extracts timestamps from events array", () => {
185
+ var _a, _b, _c;
186
+ // This tests the fix for the anti-pattern where events.find was used with side effects
187
+ const events = [
188
+ { eventAction: "registration", eventDate: "2020-01-01T00:00:00Z" },
189
+ { eventAction: "last changed", eventDate: "2023-06-15T12:00:00Z" },
190
+ { eventAction: "expiration", eventDate: "2025-01-01T00:00:00Z" },
191
+ ];
192
+ // The function should properly iterate and extract all timestamps
193
+ // without relying on side effects in find()
194
+ const created = (_a = events.find(ev => ev.eventAction === "registration")) === null || _a === void 0 ? void 0 : _a.eventDate;
195
+ const updated = (_b = events.find(ev => ev.eventAction === "last changed")) === null || _b === void 0 ? void 0 : _b.eventDate;
196
+ const expires = (_c = events.find(ev => ev.eventAction === "expiration")) === null || _c === void 0 ? void 0 : _c.eventDate;
197
+ assert.strictEqual(created, "2020-01-01T00:00:00Z");
198
+ assert.strictEqual(updated, "2023-06-15T12:00:00Z");
199
+ assert.strictEqual(expires, "2025-01-01T00:00:00Z");
200
+ });
201
+ it("handles events with invalid dates", () => {
202
+ const events = [
203
+ { eventAction: "registration", eventDate: "invalid-date" },
204
+ { eventAction: "registration", eventDate: "2020-01-01T00:00:00Z" },
205
+ ];
206
+ // Should skip invalid dates and find valid one
207
+ let validDate = null;
208
+ for (const ev of events) {
209
+ if (ev.eventAction === "registration" && ev.eventDate) {
210
+ const d = new Date(ev.eventDate);
211
+ if (!isNaN(d.valueOf())) {
212
+ validDate = d;
213
+ break;
214
+ }
215
+ }
216
+ }
217
+ assert.notStrictEqual(validDate, null);
218
+ assert.strictEqual(validDate === null || validDate === void 0 ? void 0 : validDate.toISOString(), "2020-01-01T00:00:00.000Z");
219
+ });
220
+ it("handles +0000Z date format", () => {
221
+ const dateStr = "2020-01-01T00:00:00+0000Z";
222
+ const normalized = dateStr.replace(/\+0000Z$/, "Z");
223
+ const d = new Date(normalized);
224
+ assert.strictEqual(isNaN(d.valueOf()), false);
225
+ assert.strictEqual(d.toISOString(), "2020-01-01T00:00:00.000Z");
226
+ });
227
+ });
228
+ // ============================================================================
229
+ // findInObject null safety
230
+ // ============================================================================
231
+ describe("findInObject null safety", () => {
232
+ it("handles null input", async () => {
233
+ const { findInObject } = await import("./utils/findInObject.js");
234
+ const result = findInObject(null, () => true, (el) => el, "fallback");
235
+ assert.strictEqual(result, "fallback");
236
+ });
237
+ it("handles undefined input", async () => {
238
+ const { findInObject } = await import("./utils/findInObject.js");
239
+ const result = findInObject(undefined, () => true, (el) => el, "fallback");
240
+ assert.strictEqual(result, "fallback");
241
+ });
242
+ it("handles object with null values", async () => {
243
+ const { findInObject } = await import("./utils/findInObject.js");
244
+ const obj = { a: null, b: { c: "found" } };
245
+ const result = findInObject(obj, (el) => el === "found", (el) => el, "fallback");
246
+ assert.strictEqual(result, "found");
247
+ });
248
+ it("finds nested value", async () => {
249
+ const { findInObject } = await import("./utils/findInObject.js");
250
+ const obj = { a: { b: { c: ["fn", {}, "text", "Test"] } } };
251
+ const result = findInObject(obj, (el) => Array.isArray(el) && el[0] === "fn", (el) => el[3], "fallback");
252
+ assert.strictEqual(result, "Test");
253
+ });
254
+ });
255
+ // ============================================================================
256
+ // IP response parsing null safety
257
+ // ============================================================================
258
+ describe("IP response parsing", () => {
259
+ it("handles missing port43 field", async () => {
260
+ const { parseIpResponse } = await import("./ip.js");
261
+ const response = {
262
+ found: false,
263
+ registrar: { id: 0, name: null },
264
+ };
265
+ const rdap = {
266
+ handle: "NET-1-0-0-0-1",
267
+ startAddress: "1.0.0.0",
268
+ endAddress: "1.255.255.255",
269
+ // port43 is missing
270
+ };
271
+ // Should not throw
272
+ parseIpResponse("1.0.0.1", rdap, response);
273
+ assert.strictEqual(response.registrar.name, "");
274
+ });
275
+ it("handles port43 without expected pattern", async () => {
276
+ const { parseIpResponse } = await import("./ip.js");
277
+ const response = {
278
+ found: false,
279
+ registrar: { id: 0, name: null },
280
+ };
281
+ const rdap = {
282
+ handle: "NET-1-0-0-0-1",
283
+ port43: "whois-server", // No dots
284
+ };
285
+ // Should not throw
286
+ parseIpResponse("1.0.0.1", rdap, response);
287
+ assert.strictEqual(response.registrar.name, "");
288
+ });
289
+ it("extracts registry from valid port43", async () => {
290
+ const { parseIpResponse } = await import("./ip.js");
291
+ const response = {
292
+ found: false,
293
+ registrar: { id: 0, name: null },
294
+ };
295
+ const rdap = {
296
+ handle: "NET-1-0-0-0-1",
297
+ port43: "whois.arin.net",
298
+ };
299
+ parseIpResponse("1.0.0.1", rdap, response);
300
+ assert.strictEqual(response.registrar.name, "ARIN");
301
+ });
302
+ });
303
+ // ============================================================================
304
+ // String.prototype.toWellFormed polyfill tests (Node.js 18 compatibility)
305
+ // ============================================================================
306
+ describe("String.prototype.toWellFormed polyfill", () => {
307
+ it("toWellFormed is available as a function", () => {
308
+ assert.strictEqual(typeof "".toWellFormed, "function");
309
+ });
310
+ it("returns same string for well-formed input", () => {
311
+ const str = "Hello, World!";
312
+ assert.strictEqual(str.toWellFormed(), str);
313
+ });
314
+ it("handles empty string", () => {
315
+ assert.strictEqual("".toWellFormed(), "");
316
+ });
317
+ it("handles valid surrogate pairs (emoji)", () => {
318
+ const emoji = "😀"; // U+1F600 = \uD83D\uDE00
319
+ assert.strictEqual(emoji.toWellFormed(), emoji);
320
+ });
321
+ it("replaces lone high surrogate with U+FFFD", () => {
322
+ const loneHigh = "abc\uD800def";
323
+ assert.strictEqual(loneHigh.toWellFormed(), "abc\uFFFDdef");
324
+ });
325
+ it("replaces lone low surrogate with U+FFFD", () => {
326
+ const loneLow = "abc\uDC00def";
327
+ assert.strictEqual(loneLow.toWellFormed(), "abc\uFFFDdef");
328
+ });
329
+ it("replaces lone high surrogate at end", () => {
330
+ const str = "test\uD800";
331
+ assert.strictEqual(str.toWellFormed(), "test\uFFFD");
332
+ });
333
+ it("handles multiple lone surrogates", () => {
334
+ const str = "\uD800\uD800";
335
+ assert.strictEqual(str.toWellFormed(), "\uFFFD\uFFFD");
336
+ });
337
+ it("preserves valid surrogate pairs among lone surrogates", () => {
338
+ const str = "\uD800\uD83D\uDE00\uDC00"; // lone high, valid pair, lone low
339
+ assert.strictEqual(str.toWellFormed(), "\uFFFD😀\uFFFD");
340
+ });
341
+ });
342
+ describe("String.prototype.isWellFormed polyfill", () => {
343
+ it("isWellFormed is available as a function", () => {
344
+ assert.strictEqual(typeof "".isWellFormed, "function");
345
+ });
346
+ it("returns true for well-formed strings", () => {
347
+ assert.strictEqual("Hello".isWellFormed(), true);
348
+ assert.strictEqual("".isWellFormed(), true);
349
+ assert.strictEqual("😀".isWellFormed(), true);
350
+ });
351
+ it("returns false for lone high surrogate", () => {
352
+ assert.strictEqual("test\uD800".isWellFormed(), false);
353
+ });
354
+ it("returns false for lone low surrogate", () => {
355
+ assert.strictEqual("test\uDC00".isWellFormed(), false);
356
+ });
357
+ it("returns true for valid surrogate pair", () => {
358
+ assert.strictEqual("\uD83D\uDE00".isWellFormed(), true);
359
+ });
360
+ });
package/dist/ip.js CHANGED
@@ -1,7 +1,14 @@
1
1
  export function parseIpResponse(ip, rdap, response) {
2
2
  var _a, _b, _c;
3
3
  response.found = Boolean(rdap.handle);
4
- const registry = rdap.port43 ? rdap.port43.match(/\.(\w+)\./)[1].toUpperCase() : '';
4
+ // Safely extract registry from port43 with null check
5
+ let registry = '';
6
+ if (rdap.port43) {
7
+ const match = rdap.port43.match(/\.(\w+)\./);
8
+ if (match) {
9
+ registry = match[1].toUpperCase();
10
+ }
11
+ }
5
12
  const realRdapServer = (_c = (_b = (_a = rdap.links) === null || _a === void 0 ? void 0 : _a.find(({ rel }) => rel === 'self')) === null || _b === void 0 ? void 0 : _b.value) === null || _c === void 0 ? void 0 : _c.replace(/\/ip\/.*/, '/ip/');
6
13
  response.server = realRdapServer || 'https://rdap.org/ip/';
7
14
  response.identity = {
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Polyfills for Node.js 18 compatibility.
3
+ * String.prototype.toWellFormed was added in Node.js 20.
4
+ */
5
+ declare global {
6
+ interface String {
7
+ toWellFormed(): string;
8
+ isWellFormed(): boolean;
9
+ }
10
+ }
11
+ export {};
@@ -0,0 +1,64 @@
1
+ /**
2
+ * Polyfills for Node.js 18 compatibility.
3
+ * String.prototype.toWellFormed was added in Node.js 20.
4
+ */
5
+ // Polyfill for String.prototype.toWellFormed (ES2024)
6
+ // Replaces lone surrogates with U+FFFD (replacement character)
7
+ if (typeof String.prototype.toWellFormed !== 'function') {
8
+ String.prototype.toWellFormed = function () {
9
+ const str = String(this);
10
+ const len = str.length;
11
+ let result = '';
12
+ for (let i = 0; i < len; i++) {
13
+ const code = str.charCodeAt(i);
14
+ // Check for lone surrogates
15
+ if (code >= 0xD800 && code <= 0xDBFF) {
16
+ // High surrogate
17
+ if (i + 1 < len) {
18
+ const next = str.charCodeAt(i + 1);
19
+ if (next >= 0xDC00 && next <= 0xDFFF) {
20
+ // Valid surrogate pair
21
+ result += str[i] + str[i + 1];
22
+ i++;
23
+ continue;
24
+ }
25
+ }
26
+ // Lone high surrogate - replace with U+FFFD
27
+ result += '\uFFFD';
28
+ }
29
+ else if (code >= 0xDC00 && code <= 0xDFFF) {
30
+ // Lone low surrogate - replace with U+FFFD
31
+ result += '\uFFFD';
32
+ }
33
+ else {
34
+ result += str[i];
35
+ }
36
+ }
37
+ return result;
38
+ };
39
+ }
40
+ // Polyfill for String.prototype.isWellFormed (ES2024)
41
+ if (typeof String.prototype.isWellFormed !== 'function') {
42
+ String.prototype.isWellFormed = function () {
43
+ const str = String(this);
44
+ const len = str.length;
45
+ for (let i = 0; i < len; i++) {
46
+ const code = str.charCodeAt(i);
47
+ if (code >= 0xD800 && code <= 0xDBFF) {
48
+ // High surrogate - check for valid pair
49
+ if (i + 1 >= len)
50
+ return false;
51
+ const next = str.charCodeAt(i + 1);
52
+ if (next < 0xDC00 || next > 0xDFFF)
53
+ return false;
54
+ i++; // Skip the low surrogate
55
+ }
56
+ else if (code >= 0xDC00 && code <= 0xDFFF) {
57
+ // Lone low surrogate
58
+ return false;
59
+ }
60
+ }
61
+ return true;
62
+ };
63
+ }
64
+ export {};
package/dist/port43.js CHANGED
@@ -4,6 +4,10 @@ import { Socket } from "net";
4
4
  import { port43servers, port43parsers } from "./port43servers.js";
5
5
  import { ianaToRegistrarCache } from "./utils/ianaIdToRegistrar.js";
6
6
  import { normalizeWhoisStatus } from "./whoisStatus.js";
7
+ import createDebug from "debug";
8
+ import { escapeRegex } from "./utils/escapeRegex.js";
9
+ // Debug logger - enable with DEBUG=whois:* environment variable
10
+ const debug = createDebug("whois:port43");
7
11
  export function determinePort43Domain(actor) {
8
12
  const parsed = parseDomain(actor);
9
13
  if (parsed.type === ParseResultType.Listed) {
@@ -30,7 +34,7 @@ export async function port43(actor, _fetch) {
30
34
  ? opts.query.replace("$addr", domain)
31
35
  : `${domain}\r\n`;
32
36
  const port = (opts === null || opts === void 0 ? void 0 : opts.port) || 43;
33
- // console.log(`looking up ${domain} on ${server}`);
37
+ debug("looking up %s on %s:%d", domain, server, port);
34
38
  const response = {
35
39
  found: true,
36
40
  statusCode: 200,
@@ -73,7 +77,7 @@ export async function port43(actor, _fetch) {
73
77
  }
74
78
  }
75
79
  catch (error) {
76
- console.warn({ port, server, query, error: error.message });
80
+ debug("port43 lookup error: %O", { port, server, query, error: error.message });
77
81
  response.found = false;
78
82
  response.statusCode = 500;
79
83
  response.error = error.message || "Unknown error during port 43 lookup";
@@ -82,7 +86,6 @@ export async function port43(actor, _fetch) {
82
86
  return response;
83
87
  }
84
88
  port43response = port43response.replace(/^[ \t]+/gm, "");
85
- // console.log(port43response);
86
89
  let m;
87
90
  if (m = port43response.match(/^%*\s*(NOT FOUND|No match|NO OBJECT FOUND|No entries found|No Data Found|Domain is available for registration|No information available|Status: free)\b/im)) {
88
91
  response.found = false;
@@ -112,10 +115,6 @@ export async function port43(actor, _fetch) {
112
115
  !response.reseller &&
113
116
  (m = port43response.match(/^(?:Reseller(?: Name)?|reseller_name|reseller):[ \t]*(\S.+)/im)) &&
114
117
  (response.reseller = m[1].trim());
115
- // console.log(port43response)
116
- // Updated Date: 2024-11-21T13:42:54Z
117
- // Creation Date: 2017-12-16T02:11:08Z
118
- // Registry Expiry Date: 2031-07-10T02:11:08Z
119
118
  !response.ts.updated &&
120
119
  (m = port43response.match(/^(?:Last Modified|Updated Date|Last updated on|domain_datelastmodified|last-update|modified|last modified)\.*:[ \t]*(\S.+)/im)) &&
121
120
  (response.ts.updated = new Date(reformatDate(m[1])) || null);
@@ -169,6 +168,7 @@ export async function port43(actor, _fetch) {
169
168
  response.ts.updated = null;
170
169
  if (response.ts.expires && !response.ts.expires.valueOf())
171
170
  response.ts.expires = null;
171
+ // Match registrar name against IANA cache using escaped regex to prevent ReDoS
172
172
  if (response.registrar.id === 0 && response.registrar.name !== "") {
173
173
  for (const [id, { name }] of ianaToRegistrarCache.entries()) {
174
174
  if (name === response.registrar.name) {
@@ -177,19 +177,33 @@ export async function port43(actor, _fetch) {
177
177
  }
178
178
  }
179
179
  }
180
- if (response.registrar.id === 0 && response.registrar.name !== "") {
180
+ if (response.registrar.id === 0 && response.registrar.name && response.registrar.name !== "") {
181
+ const escapedName = escapeRegex(response.registrar.name);
181
182
  for (const [id, { name }] of ianaToRegistrarCache.entries()) {
182
- if (name.match(new RegExp(`\\b${response.registrar.name}\\b`, "i"))) {
183
- response.registrar.id = id;
184
- break;
183
+ try {
184
+ if (name.match(new RegExp(`\\b${escapedName}\\b`, "i"))) {
185
+ response.registrar.id = id;
186
+ break;
187
+ }
188
+ }
189
+ catch (_o) {
190
+ // Skip if regex still fails for some reason
191
+ continue;
185
192
  }
186
193
  }
187
194
  }
188
195
  if (response.registrar.id === 0 && response.registrar.name) {
196
+ const escapedName = escapeRegex(response.registrar.name.replace(/,.*/, ""));
189
197
  for (const [id, { name }] of ianaToRegistrarCache.entries()) {
190
- if (name.match(new RegExp(`\\b${response.registrar.name.replace(/,.*/, "")}\\b`, "i"))) {
191
- response.registrar.id = id;
192
- break;
198
+ try {
199
+ if (name.match(new RegExp(`\\b${escapedName}\\b`, "i"))) {
200
+ response.registrar.id = id;
201
+ break;
202
+ }
203
+ }
204
+ catch (_p) {
205
+ // Skip if regex still fails for some reason
206
+ continue;
193
207
  }
194
208
  }
195
209
  }
@@ -0,0 +1,5 @@
1
+ /**
2
+ * Escapes special regex characters in a string.
3
+ * Prevents ReDoS attacks when using user input in RegExp constructor.
4
+ */
5
+ export declare function escapeRegex(str: string): string;
@@ -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) {
6
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
7
+ }
@@ -3,15 +3,25 @@ export function findInObject(obj, condition, extractor, fallback) {
3
3
  return found === undefined ? fallback : extractor(found);
4
4
  }
5
5
  function _findInObject(obj, condition) {
6
+ // Handle null/undefined
7
+ if (obj === null || obj === undefined) {
8
+ return undefined;
9
+ }
6
10
  for (const key in obj) {
7
- if (condition(obj[key])) {
8
- return obj[key];
11
+ // Skip inherited properties
12
+ if (!Object.prototype.hasOwnProperty.call(obj, key)) {
13
+ continue;
14
+ }
15
+ const value = obj[key];
16
+ if (condition(value)) {
17
+ return value;
9
18
  }
10
- if (typeof obj[key] === "object") {
11
- const result = _findInObject(obj[key], condition);
19
+ if (value !== null && typeof value === "object") {
20
+ const result = _findInObject(value, condition);
12
21
  if (result !== undefined) {
13
22
  return result;
14
23
  }
15
24
  }
16
25
  }
26
+ return undefined;
17
27
  }
@@ -0,0 +1 @@
1
+ export declare function findNameservers(values: any[]): string[];
@@ -0,0 +1,15 @@
1
+ export function findNameservers(values) {
2
+ let nameservers = [];
3
+ if (Array.isArray(values)) {
4
+ nameservers = values;
5
+ }
6
+ else if (typeof values === "object") {
7
+ nameservers = Object.values(values);
8
+ }
9
+ return nameservers
10
+ .map((ns) => { var _a; return ns.ldhName || ns.ldnName || ((_a = ns.ipAddresses) === null || _a === void 0 ? void 0 : _a.v4); })
11
+ .flat()
12
+ .filter((ns) => ns)
13
+ .map((ns) => (ns.stringValue || ns).toLocaleLowerCase())
14
+ .sort();
15
+ }
@@ -0,0 +1 @@
1
+ export declare function findStatus(statuses: string | string[], domain: string): string[];
@@ -0,0 +1,10 @@
1
+ import { normalizeWhoisStatus } from "../whoisStatus.js";
2
+ export function findStatus(statuses, domain) {
3
+ return (Array.isArray(statuses)
4
+ ? statuses
5
+ : statuses && typeof statuses === "object"
6
+ ? Object.keys(statuses)
7
+ : typeof statuses === "string"
8
+ ? statuses.trim().split(/\s*,\s*/)
9
+ : []).map((status) => normalizeWhoisStatus(status));
10
+ }
@@ -0,0 +1,6 @@
1
+ import { WhoisTimestampFields } from "../../whois.js";
2
+ /**
3
+ * Extracts timestamps from RDAP events array.
4
+ * Properly iterates through events and breaks when a match is found.
5
+ */
6
+ export declare function findTimestamps(values: any[]): Record<WhoisTimestampFields, Date | null>;