@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/package.json CHANGED
@@ -1,29 +1,36 @@
1
1
  {
2
2
  "name": "@cleandns/whois-rdap",
3
- "version": "1.0.52",
4
3
  "description": "",
5
- "type": "module",
6
- "main": "dist/index.js",
7
- "scripts": {
8
- "build": "tsc",
9
- "upload": "npm run build && npm version patch && npm publish"
10
- },
11
- "repository": {
12
- "type": "git",
13
- "url": "git+https://github.com/cleandns-inc/tool-whois.git"
14
- },
4
+ "version": "1.0.55",
15
5
  "author": "",
16
- "license": "ISC",
17
6
  "bugs": {
18
7
  "url": "https://github.com/cleandns-inc/tool-whois/issues"
19
8
  },
20
- "homepage": "https://github.com/cleandns-inc/tool-whois#readme",
9
+ "dependencies": {
10
+ "debug": "^4.4.3",
11
+ "parse-domain": "^8.0.2",
12
+ "promise-socket": "^7.0.0"
13
+ },
21
14
  "devDependencies": {
15
+ "@types/debug": "^4.1.12",
22
16
  "@types/node": "^22.0.2",
23
17
  "typescript": "^5.4.3"
24
18
  },
25
- "dependencies": {
26
- "parse-domain": "^8.0.2",
27
- "promise-socket": "^7.0.0"
28
- }
19
+ "engines": {
20
+ "node": ">= 18"
21
+ },
22
+ "homepage": "https://github.com/cleandns-inc/tool-whois#readme",
23
+ "license": "ISC",
24
+ "main": "dist/index.js",
25
+ "packageManager": "pnpm@9",
26
+ "repository": {
27
+ "type": "git",
28
+ "url": "git+https://github.com/cleandns-inc/tool-whois.git"
29
+ },
30
+ "scripts": {
31
+ "build": "tsc",
32
+ "test": "npm run build && node --test dist/*.test.js",
33
+ "upload": "npm run build && npm version patch && npm publish"
34
+ },
35
+ "type": "module"
29
36
  }
@@ -0,0 +1,440 @@
1
+ import { describe, it } from "node:test";
2
+ import assert from "node:assert";
3
+
4
+ // Import polyfills first
5
+ import "./polyfills.js";
6
+
7
+ import { validateDomain } from "./utils/validateDomain.js";
8
+ import { escapeRegex } from "./utils/escapeRegex.js";
9
+ import { toArray } from "./utils/toArray.js";
10
+
11
+ // ============================================================================
12
+ // toArray utility tests
13
+ // ============================================================================
14
+
15
+ describe("toArray utility function", () => {
16
+ it("returns empty array for null", () => {
17
+ assert.deepStrictEqual(toArray(null), []);
18
+ });
19
+
20
+ it("returns empty array for undefined", () => {
21
+ assert.deepStrictEqual(toArray(undefined), []);
22
+ });
23
+
24
+ it("returns same array if already an array", () => {
25
+ const arr = [1, 2, 3];
26
+ assert.deepStrictEqual(toArray(arr), arr);
27
+ });
28
+
29
+ it("converts object to array of values", () => {
30
+ const obj = { a: 1, b: 2, c: 3 };
31
+ assert.deepStrictEqual(toArray(obj), [1, 2, 3]);
32
+ });
33
+
34
+ it("handles empty array", () => {
35
+ assert.deepStrictEqual(toArray([]), []);
36
+ });
37
+
38
+ it("handles empty object", () => {
39
+ assert.deepStrictEqual(toArray({}), []);
40
+ });
41
+
42
+ it("handles nested arrays", () => {
43
+ const arr = [[1, 2], [3, 4]];
44
+ assert.deepStrictEqual(toArray(arr), [[1, 2], [3, 4]]);
45
+ });
46
+
47
+ it("handles array-like objects with numeric keys", () => {
48
+ const obj = { "0": "a", "1": "b", "2": "c" };
49
+ assert.deepStrictEqual(toArray(obj), ["a", "b", "c"]);
50
+ });
51
+ });
52
+
53
+ describe("toArray handles RDAP edge cases (issue #16)", () => {
54
+ it("handles entities as object instead of array", () => {
55
+ const entities = {
56
+ "0": { roles: ["registrar"], handle: "123" },
57
+ "1": { roles: ["abuse"], handle: "456" },
58
+ };
59
+ const result = toArray(entities);
60
+ assert.strictEqual(Array.isArray(result), true);
61
+ assert.strictEqual(result.length, 2);
62
+ assert.deepStrictEqual(result[0], { roles: ["registrar"], handle: "123" });
63
+ });
64
+
65
+ it("handles nested entities being null", () => {
66
+ const result = toArray(null);
67
+ assert.deepStrictEqual(result, []);
68
+ });
69
+
70
+ it("handles nested entities being undefined", () => {
71
+ const result = toArray(undefined);
72
+ assert.deepStrictEqual(result, []);
73
+ });
74
+ });
75
+
76
+ // ============================================================================
77
+ // escapeRegex utility tests (ReDoS prevention)
78
+ // ============================================================================
79
+
80
+ describe("escapeRegex utility function", () => {
81
+ it("escapes special regex characters", () => {
82
+ const input = "test.*+?^${}()|[]\\";
83
+ const escaped = escapeRegex(input);
84
+ assert.strictEqual(escaped, "test\\.\\*\\+\\?\\^\\$\\{\\}\\(\\)\\|\\[\\]\\\\");
85
+ });
86
+
87
+ it("leaves normal strings unchanged", () => {
88
+ const input = "GoDaddy Inc";
89
+ const escaped = escapeRegex(input);
90
+ assert.strictEqual(escaped, "GoDaddy Inc");
91
+ });
92
+
93
+ it("handles empty string", () => {
94
+ const escaped = escapeRegex("");
95
+ assert.strictEqual(escaped, "");
96
+ });
97
+
98
+ it("handles string with only special characters", () => {
99
+ const input = ".*+";
100
+ const escaped = escapeRegex(input);
101
+ assert.strictEqual(escaped, "\\.\\*\\+");
102
+ });
103
+
104
+ it("escaped string can be used safely in RegExp", () => {
105
+ const malicious = "test(.*)+$";
106
+ const escaped = escapeRegex(malicious);
107
+ // Should not throw when creating RegExp
108
+ const regex = new RegExp(escaped, "i");
109
+ // The escaped string should match literally
110
+ assert.strictEqual(regex.test("test(.*)+$"), true);
111
+ assert.strictEqual(regex.test("testABC"), false);
112
+ });
113
+ });
114
+
115
+ // ============================================================================
116
+ // validateDomain utility tests
117
+ // ============================================================================
118
+
119
+ describe("validateDomain utility function", () => {
120
+ it("accepts valid domain", () => {
121
+ const result = validateDomain("example.com");
122
+ assert.strictEqual(result, "example.com");
123
+ });
124
+
125
+ it("trims whitespace", () => {
126
+ const result = validateDomain(" example.com ");
127
+ assert.strictEqual(result, "example.com");
128
+ });
129
+
130
+ it("converts to lowercase", () => {
131
+ const result = validateDomain("EXAMPLE.COM");
132
+ assert.strictEqual(result, "example.com");
133
+ });
134
+
135
+ it("throws on empty string", () => {
136
+ assert.throws(() => validateDomain(""), /non-empty string/);
137
+ });
138
+
139
+ it("throws on whitespace-only string", () => {
140
+ assert.throws(() => validateDomain(" "), /non-empty string/);
141
+ });
142
+
143
+ it("throws on null", () => {
144
+ assert.throws(() => validateDomain(null as any), /non-empty string/);
145
+ });
146
+
147
+ it("throws on undefined", () => {
148
+ assert.throws(() => validateDomain(undefined as any), /non-empty string/);
149
+ });
150
+
151
+ it("throws on domain too long", () => {
152
+ const longDomain = "a".repeat(254) + ".com";
153
+ assert.throws(() => validateDomain(longDomain), /too long/);
154
+ });
155
+
156
+ it("accepts domain at max length", () => {
157
+ const maxDomain = "a".repeat(249) + ".com"; // 253 chars
158
+ const result = validateDomain(maxDomain);
159
+ assert.strictEqual(result.length, 253);
160
+ });
161
+ });
162
+
163
+ // ============================================================================
164
+ // vcardArray safety checks (issue #12)
165
+ // ============================================================================
166
+
167
+ describe("vcardArray safety checks (issue #12)", () => {
168
+ it("Array.isArray correctly identifies arrays", () => {
169
+ const vcardArray: any[] = ["vcard", [["fn", {}, "text", "Test"]]];
170
+ assert.strictEqual(Array.isArray(vcardArray[1]), true);
171
+ assert.strictEqual(typeof (vcardArray[1] as any[]).find, "function");
172
+ });
173
+
174
+ it("Array.isArray returns false for non-arrays", () => {
175
+ const vcardArray: any[] = ["vcard", "not-an-array"];
176
+ assert.strictEqual(Array.isArray(vcardArray[1]), false);
177
+ });
178
+
179
+ it("Array.isArray returns false for undefined", () => {
180
+ const vcardArray: any[] = ["vcard"];
181
+ assert.strictEqual(Array.isArray(vcardArray[1]), false);
182
+ });
183
+
184
+ it("safe access pattern prevents TypeError", () => {
185
+ const vcardArray: any[] = ["vcard"];
186
+ const hasFn = vcardArray && Array.isArray(vcardArray[1]) && (vcardArray[1] as any[]).find((el: any) => el[0] === 'fn');
187
+ assert.strictEqual(hasFn, false);
188
+ });
189
+
190
+ it("safe access pattern works with valid data", () => {
191
+ const vcardArray: any[] = ["vcard", [["fn", {}, "text", "Test Registrar"]]];
192
+ const hasFn = vcardArray && Array.isArray(vcardArray[1]) && (vcardArray[1] as any[]).find((el: any) => el[0] === 'fn');
193
+ assert.deepStrictEqual(hasFn, ["fn", {}, "text", "Test Registrar"]);
194
+ });
195
+ });
196
+
197
+ // ============================================================================
198
+ // debug module integration (issue #13)
199
+ // ============================================================================
200
+
201
+ describe("debug module integration (issue #13)", () => {
202
+ it("debug module is importable", async () => {
203
+ const createDebug = (await import("debug")).default;
204
+ assert.strictEqual(typeof createDebug, "function");
205
+ });
206
+
207
+ it("debug instance can be created", async () => {
208
+ const createDebug = (await import("debug")).default;
209
+ const debug = createDebug("test:namespace");
210
+ assert.strictEqual(typeof debug, "function");
211
+ });
212
+
213
+ it("debug function can be called without error", async () => {
214
+ const createDebug = (await import("debug")).default;
215
+ const debug = createDebug("test:namespace");
216
+ // Should not throw
217
+ debug("test message %s", "arg");
218
+ });
219
+ });
220
+
221
+ // ============================================================================
222
+ // findTimestamps anti-pattern fix (commit comment)
223
+ // ============================================================================
224
+
225
+ describe("findTimestamps behavior", () => {
226
+ it("properly extracts timestamps from events array", () => {
227
+ // This tests the fix for the anti-pattern where events.find was used with side effects
228
+ const events = [
229
+ { eventAction: "registration", eventDate: "2020-01-01T00:00:00Z" },
230
+ { eventAction: "last changed", eventDate: "2023-06-15T12:00:00Z" },
231
+ { eventAction: "expiration", eventDate: "2025-01-01T00:00:00Z" },
232
+ ];
233
+
234
+ // The function should properly iterate and extract all timestamps
235
+ // without relying on side effects in find()
236
+ const created = events.find(ev => ev.eventAction === "registration")?.eventDate;
237
+ const updated = events.find(ev => ev.eventAction === "last changed")?.eventDate;
238
+ const expires = events.find(ev => ev.eventAction === "expiration")?.eventDate;
239
+
240
+ assert.strictEqual(created, "2020-01-01T00:00:00Z");
241
+ assert.strictEqual(updated, "2023-06-15T12:00:00Z");
242
+ assert.strictEqual(expires, "2025-01-01T00:00:00Z");
243
+ });
244
+
245
+ it("handles events with invalid dates", () => {
246
+ const events = [
247
+ { eventAction: "registration", eventDate: "invalid-date" },
248
+ { eventAction: "registration", eventDate: "2020-01-01T00:00:00Z" },
249
+ ];
250
+
251
+ // Should skip invalid dates and find valid one
252
+ let validDate = null;
253
+ for (const ev of events) {
254
+ if (ev.eventAction === "registration" && ev.eventDate) {
255
+ const d = new Date(ev.eventDate);
256
+ if (!isNaN(d.valueOf())) {
257
+ validDate = d;
258
+ break;
259
+ }
260
+ }
261
+ }
262
+
263
+ assert.notStrictEqual(validDate, null);
264
+ assert.strictEqual(validDate?.toISOString(), "2020-01-01T00:00:00.000Z");
265
+ });
266
+
267
+ it("handles +0000Z date format", () => {
268
+ const dateStr = "2020-01-01T00:00:00+0000Z";
269
+ const normalized = dateStr.replace(/\+0000Z$/, "Z");
270
+ const d = new Date(normalized);
271
+ assert.strictEqual(isNaN(d.valueOf()), false);
272
+ assert.strictEqual(d.toISOString(), "2020-01-01T00:00:00.000Z");
273
+ });
274
+ });
275
+
276
+ // ============================================================================
277
+ // findInObject null safety
278
+ // ============================================================================
279
+
280
+ describe("findInObject null safety", () => {
281
+ it("handles null input", async () => {
282
+ const { findInObject } = await import("./utils/findInObject.js");
283
+ const result = findInObject(null as any, () => true, (el) => el, "fallback");
284
+ assert.strictEqual(result, "fallback");
285
+ });
286
+
287
+ it("handles undefined input", async () => {
288
+ const { findInObject } = await import("./utils/findInObject.js");
289
+ const result = findInObject(undefined as any, () => true, (el) => el, "fallback");
290
+ assert.strictEqual(result, "fallback");
291
+ });
292
+
293
+ it("handles object with null values", async () => {
294
+ const { findInObject } = await import("./utils/findInObject.js");
295
+ const obj = { a: null, b: { c: "found" } };
296
+ const result = findInObject(obj, (el) => el === "found", (el) => el, "fallback");
297
+ assert.strictEqual(result, "found");
298
+ });
299
+
300
+ it("finds nested value", async () => {
301
+ const { findInObject } = await import("./utils/findInObject.js");
302
+ const obj = { a: { b: { c: ["fn", {}, "text", "Test"] } } };
303
+ const result = findInObject(
304
+ obj,
305
+ (el) => Array.isArray(el) && el[0] === "fn",
306
+ (el) => el[3],
307
+ "fallback"
308
+ );
309
+ assert.strictEqual(result, "Test");
310
+ });
311
+ });
312
+
313
+ // ============================================================================
314
+ // IP response parsing null safety
315
+ // ============================================================================
316
+
317
+ describe("IP response parsing", () => {
318
+ it("handles missing port43 field", async () => {
319
+ const { parseIpResponse } = await import("./ip.js");
320
+ const response: any = {
321
+ found: false,
322
+ registrar: { id: 0, name: null },
323
+ };
324
+ const rdap = {
325
+ handle: "NET-1-0-0-0-1",
326
+ startAddress: "1.0.0.0",
327
+ endAddress: "1.255.255.255",
328
+ // port43 is missing
329
+ };
330
+
331
+ // Should not throw
332
+ parseIpResponse("1.0.0.1", rdap, response);
333
+ assert.strictEqual(response.registrar.name, "");
334
+ });
335
+
336
+ it("handles port43 without expected pattern", async () => {
337
+ const { parseIpResponse } = await import("./ip.js");
338
+ const response: any = {
339
+ found: false,
340
+ registrar: { id: 0, name: null },
341
+ };
342
+ const rdap = {
343
+ handle: "NET-1-0-0-0-1",
344
+ port43: "whois-server", // No dots
345
+ };
346
+
347
+ // Should not throw
348
+ parseIpResponse("1.0.0.1", rdap, response);
349
+ assert.strictEqual(response.registrar.name, "");
350
+ });
351
+
352
+ it("extracts registry from valid port43", async () => {
353
+ const { parseIpResponse } = await import("./ip.js");
354
+ const response: any = {
355
+ found: false,
356
+ registrar: { id: 0, name: null },
357
+ };
358
+ const rdap = {
359
+ handle: "NET-1-0-0-0-1",
360
+ port43: "whois.arin.net",
361
+ };
362
+
363
+ parseIpResponse("1.0.0.1", rdap, response);
364
+ assert.strictEqual(response.registrar.name, "ARIN");
365
+ });
366
+ });
367
+
368
+
369
+ // ============================================================================
370
+ // String.prototype.toWellFormed polyfill tests (Node.js 18 compatibility)
371
+ // ============================================================================
372
+
373
+ describe("String.prototype.toWellFormed polyfill", () => {
374
+ it("toWellFormed is available as a function", () => {
375
+ assert.strictEqual(typeof "".toWellFormed, "function");
376
+ });
377
+
378
+ it("returns same string for well-formed input", () => {
379
+ const str = "Hello, World!";
380
+ assert.strictEqual(str.toWellFormed(), str);
381
+ });
382
+
383
+ it("handles empty string", () => {
384
+ assert.strictEqual("".toWellFormed(), "");
385
+ });
386
+
387
+ it("handles valid surrogate pairs (emoji)", () => {
388
+ const emoji = "😀"; // U+1F600 = \uD83D\uDE00
389
+ assert.strictEqual(emoji.toWellFormed(), emoji);
390
+ });
391
+
392
+ it("replaces lone high surrogate with U+FFFD", () => {
393
+ const loneHigh = "abc\uD800def";
394
+ assert.strictEqual(loneHigh.toWellFormed(), "abc\uFFFDdef");
395
+ });
396
+
397
+ it("replaces lone low surrogate with U+FFFD", () => {
398
+ const loneLow = "abc\uDC00def";
399
+ assert.strictEqual(loneLow.toWellFormed(), "abc\uFFFDdef");
400
+ });
401
+
402
+ it("replaces lone high surrogate at end", () => {
403
+ const str = "test\uD800";
404
+ assert.strictEqual(str.toWellFormed(), "test\uFFFD");
405
+ });
406
+
407
+ it("handles multiple lone surrogates", () => {
408
+ const str = "\uD800\uD800";
409
+ assert.strictEqual(str.toWellFormed(), "\uFFFD\uFFFD");
410
+ });
411
+
412
+ it("preserves valid surrogate pairs among lone surrogates", () => {
413
+ const str = "\uD800\uD83D\uDE00\uDC00"; // lone high, valid pair, lone low
414
+ assert.strictEqual(str.toWellFormed(), "\uFFFD😀\uFFFD");
415
+ });
416
+ });
417
+
418
+ describe("String.prototype.isWellFormed polyfill", () => {
419
+ it("isWellFormed is available as a function", () => {
420
+ assert.strictEqual(typeof "".isWellFormed, "function");
421
+ });
422
+
423
+ it("returns true for well-formed strings", () => {
424
+ assert.strictEqual("Hello".isWellFormed(), true);
425
+ assert.strictEqual("".isWellFormed(), true);
426
+ assert.strictEqual("😀".isWellFormed(), true);
427
+ });
428
+
429
+ it("returns false for lone high surrogate", () => {
430
+ assert.strictEqual("test\uD800".isWellFormed(), false);
431
+ });
432
+
433
+ it("returns false for lone low surrogate", () => {
434
+ assert.strictEqual("test\uDC00".isWellFormed(), false);
435
+ });
436
+
437
+ it("returns true for valid surrogate pair", () => {
438
+ assert.strictEqual("\uD83D\uDE00".isWellFormed(), true);
439
+ });
440
+ });