rdapper 0.3.0 → 0.4.1

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
@@ -4,7 +4,7 @@ RDAP‑first domain registration lookups with WHOIS fallback. Produces a single,
4
4
 
5
5
  - RDAP discovery via IANA bootstrap (`https://data.iana.org/rdap/dns.json`)
6
6
  - WHOIS TCP 43 client with TLD discovery, registrar referral follow, and curated exceptions
7
- - Normalized output: registrar, contacts, nameservers, statuses, dates, DNSSEC, source metadata
7
+ - Normalized output: registrar, contacts, nameservers, statuses, dates, DNSSEC, privacy flag, source metadata
8
8
  - TypeScript types included; ESM‑only; no external HTTP client (uses global `fetch`)
9
9
 
10
10
  ### [🦉 See it in action on hoot.sh!](https://hoot.sh)
@@ -117,12 +117,12 @@ interface DomainRecord {
117
117
  country?: string;
118
118
  countryCode?: string;
119
119
  }>;
120
+ privacyEnabled?: boolean; // registrant appears privacy-redacted based on keyword heuristics
120
121
  whoisServer?: string; // authoritative WHOIS queried (if any)
121
122
  rdapServers?: string[]; // RDAP base URLs tried
122
123
  rawRdap?: unknown; // raw RDAP JSON (only when options.includeRaw)
123
124
  rawWhois?: string; // raw WHOIS text (only when options.includeRaw)
124
125
  source: "rdap" | "whois"; // which path produced data
125
- fetchedAt: string; // ISO 8601 timestamp
126
126
  warnings?: string[];
127
127
  }
128
128
  ```
@@ -138,8 +138,7 @@ interface DomainRecord {
138
138
  "statuses": [{ "status": "clientTransferProhibited" }],
139
139
  "nameservers": [{ "host": "a.iana-servers.net" }, { "host": "b.iana-servers.net" }],
140
140
  "dnssec": { "enabled": true },
141
- "source": "rdap",
142
- "fetchedAt": "2025-01-01T00:00:00Z"
141
+ "source": "rdap"
143
142
  }
144
143
  ```
145
144
 
package/dist/index.d.ts CHANGED
@@ -32,21 +32,37 @@ interface StatusEvent {
32
32
  raw?: string;
33
33
  }
34
34
  interface DomainRecord {
35
+ /** Normalized domain name */
35
36
  domain: string;
37
+ /** Terminal TLD */
36
38
  tld: string;
39
+ /** Whether the domain is registered */
37
40
  isRegistered: boolean;
41
+ /** Whether the domain is internationalized (IDN) */
38
42
  isIDN?: boolean;
43
+ /** Unicode name */
39
44
  unicodeName?: string;
45
+ /** Punycode name */
40
46
  punycodeName?: string;
47
+ /** Registry operator */
41
48
  registry?: string;
49
+ /** Registrar */
42
50
  registrar?: RegistrarInfo;
51
+ /** Reseller (if applicable) */
43
52
  reseller?: string;
53
+ /** EPP status codes */
44
54
  statuses?: StatusEvent[];
55
+ /** Creation date in ISO 8601 */
45
56
  creationDate?: string;
57
+ /** Updated date in ISO 8601 */
46
58
  updatedDate?: string;
59
+ /** Expiration date in ISO 8601 */
47
60
  expirationDate?: string;
61
+ /** Deletion date in ISO 8601 */
48
62
  deletionDate?: string;
63
+ /** Transfer lock */
49
64
  transferLock?: boolean;
65
+ /** DNSSEC data (if available) */
50
66
  dnssec?: {
51
67
  enabled: boolean;
52
68
  dsRecords?: Array<{
@@ -56,28 +72,49 @@ interface DomainRecord {
56
72
  digest?: string;
57
73
  }>;
58
74
  };
75
+ /** Nameservers */
59
76
  nameservers?: Nameserver[];
77
+ /** Contacts (registrant, admin, tech, billing, abuse, etc.) */
60
78
  contacts?: Contact[];
79
+ /** Best guess as to whether registrant is redacted based on keywords */
80
+ privacyEnabled?: boolean;
81
+ /** Authoritative WHOIS queried (if any) */
61
82
  whoisServer?: string;
83
+ /** RDAP base URLs tried */
62
84
  rdapServers?: string[];
85
+ /** Raw RDAP JSON */
63
86
  rawRdap?: unknown;
87
+ /** Raw WHOIS text (last authoritative) */
64
88
  rawWhois?: string;
89
+ /** Which source produced data */
65
90
  source: LookupSource;
66
- fetchedAt: string;
91
+ /** Warnings generated during lookup */
67
92
  warnings?: string[];
68
93
  }
69
94
  interface LookupOptions {
95
+ /** Total timeout budget */
70
96
  timeoutMs?: number;
97
+ /** Don't fall back to WHOIS */
71
98
  rdapOnly?: boolean;
99
+ /** Don't attempt RDAP */
72
100
  whoisOnly?: boolean;
101
+ /** Follow referral server (default true) */
73
102
  followWhoisReferral?: boolean;
103
+ /** Maximum registrar WHOIS referral hops (default 2) */
74
104
  maxWhoisReferralHops?: number;
105
+ /** Follow RDAP related/entity links (default true) */
75
106
  rdapFollowLinks?: boolean;
107
+ /** Maximum RDAP related link fetches (default 2) */
76
108
  maxRdapLinkHops?: number;
109
+ /** RDAP link rels to consider (default ["related","entity","registrar","alternate"]) */
77
110
  rdapLinkRels?: string[];
111
+ /** Override IANA bootstrap */
78
112
  customBootstrapUrl?: string;
113
+ /** Override/add authoritative WHOIS per TLD */
79
114
  whoisHints?: Record<string, string>;
115
+ /** Include rawRdap/rawWhois in results (default false) */
80
116
  includeRaw?: boolean;
117
+ /** Optional cancellation signal */
81
118
  signal?: AbortSignal;
82
119
  }
83
120
  interface LookupResult {
package/dist/index.js CHANGED
@@ -1,74 +1,6 @@
1
1
  import psl from "psl";
2
2
  import { createConnection } from "node:net";
3
3
 
4
- //#region src/lib/dates.ts
5
- function toISO(dateLike) {
6
- if (dateLike == null) return void 0;
7
- if (dateLike instanceof Date) return toIsoFromDate(dateLike);
8
- if (typeof dateLike === "number") return toIsoFromDate(new Date(dateLike));
9
- const raw = String(dateLike).trim();
10
- if (!raw) return void 0;
11
- for (const re of [
12
- /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})(?:Z|([+-]\d{2})(?::?(\d{2}))?)?$/,
13
- /^(\d{4})\/(\d{2})\/(\d{2})[ T](\d{2}):(\d{2}):(\d{2})(?:Z|([+-]\d{2})(?::?(\d{2}))?)?$/,
14
- /^(\d{2})-([A-Za-z]{3})-(\d{4})$/,
15
- /^([A-Za-z]{3})\s+(\d{1,2})\s+(\d{4})$/
16
- ]) {
17
- const m = raw.match(re);
18
- if (!m) continue;
19
- const d = parseDateWithRegex(m, re);
20
- if (d) return toIsoFromDate(d);
21
- }
22
- const native = new Date(raw);
23
- if (!Number.isNaN(native.getTime())) return toIsoFromDate(native);
24
- }
25
- function toIsoFromDate(d) {
26
- try {
27
- return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(), 0)).toISOString().replace(/\.\d{3}Z$/, "Z");
28
- } catch {
29
- return;
30
- }
31
- }
32
- function parseDateWithRegex(m, _re) {
33
- const monthMap = {
34
- jan: 0,
35
- feb: 1,
36
- mar: 2,
37
- apr: 3,
38
- may: 4,
39
- jun: 5,
40
- jul: 6,
41
- aug: 7,
42
- sep: 8,
43
- oct: 9,
44
- nov: 10,
45
- dec: 11
46
- };
47
- try {
48
- if (m[0].includes(":")) {
49
- const [_$1, y, mo, d, hh, mm, ss, offH, offM] = m;
50
- let dt = Date.UTC(Number(y), Number(mo) - 1, Number(d), Number(hh), Number(mm), Number(ss));
51
- if (offH) {
52
- const sign = offH.startsWith("-") ? -1 : 1;
53
- const hours = Math.abs(Number(offH));
54
- const minutes = offM ? Number(offM) : 0;
55
- const offsetMs = sign * (hours * 60 + minutes) * 60 * 1e3;
56
- dt -= offsetMs;
57
- }
58
- return new Date(dt);
59
- }
60
- if (m[0].includes("-")) {
61
- const [_$1, dd$1, monStr$1, yyyy$1] = m;
62
- const mon$1 = monthMap[monStr$1.toLowerCase()];
63
- return new Date(Date.UTC(Number(yyyy$1), mon$1, Number(dd$1)));
64
- }
65
- const [_, monStr, dd, yyyy] = m;
66
- const mon = monthMap[monStr.toLowerCase()];
67
- return new Date(Date.UTC(Number(yyyy), mon, Number(dd)));
68
- } catch {}
69
- }
70
-
71
- //#endregion
72
4
  //#region src/lib/domain.ts
73
5
  function getDomainParts(domain) {
74
6
  const lower = domain.toLowerCase().trim();
@@ -302,6 +234,90 @@ function sameDomain(a, b) {
302
234
  return a.toLowerCase() === b.toLowerCase();
303
235
  }
304
236
 
237
+ //#endregion
238
+ //#region src/lib/dates.ts
239
+ function toISO(dateLike) {
240
+ if (dateLike == null) return void 0;
241
+ if (dateLike instanceof Date) return toIsoFromDate(dateLike);
242
+ if (typeof dateLike === "number") return toIsoFromDate(new Date(dateLike));
243
+ const raw = String(dateLike).trim();
244
+ if (!raw) return void 0;
245
+ for (const re of [
246
+ /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})(?:Z|([+-]\d{2})(?::?(\d{2}))?)?$/,
247
+ /^(\d{4})\/(\d{2})\/(\d{2})[ T](\d{2}):(\d{2}):(\d{2})(?:Z|([+-]\d{2})(?::?(\d{2}))?)?$/,
248
+ /^(\d{2})-([A-Za-z]{3})-(\d{4})$/,
249
+ /^([A-Za-z]{3})\s+(\d{1,2})\s+(\d{4})$/
250
+ ]) {
251
+ const m = raw.match(re);
252
+ if (!m) continue;
253
+ const d = parseDateWithRegex(m, re);
254
+ if (d) return toIsoFromDate(d);
255
+ }
256
+ const native = new Date(raw);
257
+ if (!Number.isNaN(native.getTime())) return toIsoFromDate(native);
258
+ }
259
+ function toIsoFromDate(d) {
260
+ try {
261
+ return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(), 0)).toISOString().replace(/\.\d{3}Z$/, "Z");
262
+ } catch {
263
+ return;
264
+ }
265
+ }
266
+ function parseDateWithRegex(m, _re) {
267
+ const monthMap = {
268
+ jan: 0,
269
+ feb: 1,
270
+ mar: 2,
271
+ apr: 3,
272
+ may: 4,
273
+ jun: 5,
274
+ jul: 6,
275
+ aug: 7,
276
+ sep: 8,
277
+ oct: 9,
278
+ nov: 10,
279
+ dec: 11
280
+ };
281
+ try {
282
+ if (m[0].includes(":")) {
283
+ const [_$1, y, mo, d, hh, mm, ss, offH, offM] = m;
284
+ let dt = Date.UTC(Number(y), Number(mo) - 1, Number(d), Number(hh), Number(mm), Number(ss));
285
+ if (offH) {
286
+ const sign = offH.startsWith("-") ? -1 : 1;
287
+ const hours = Math.abs(Number(offH));
288
+ const minutes = offM ? Number(offM) : 0;
289
+ const offsetMs = sign * (hours * 60 + minutes) * 60 * 1e3;
290
+ dt -= offsetMs;
291
+ }
292
+ return new Date(dt);
293
+ }
294
+ if (m[0].includes("-")) {
295
+ const [_$1, dd$1, monStr$1, yyyy$1] = m;
296
+ const mon$1 = monthMap[monStr$1.toLowerCase()];
297
+ return new Date(Date.UTC(Number(yyyy$1), mon$1, Number(dd$1)));
298
+ }
299
+ const [_, monStr, dd, yyyy] = m;
300
+ const mon = monthMap[monStr.toLowerCase()];
301
+ return new Date(Date.UTC(Number(yyyy), mon, Number(dd)));
302
+ } catch {}
303
+ }
304
+
305
+ //#endregion
306
+ //#region src/lib/privacy.ts
307
+ const PRIVACY_NAME_KEYWORDS = [
308
+ "redacted",
309
+ "privacy",
310
+ "private",
311
+ "withheld",
312
+ "not disclosed",
313
+ "protected",
314
+ "protection"
315
+ ];
316
+ function isPrivacyName(value) {
317
+ const v = value.toLowerCase();
318
+ return PRIVACY_NAME_KEYWORDS.some((k) => v.includes(k));
319
+ }
320
+
305
321
  //#endregion
306
322
  //#region src/lib/text.ts
307
323
  function uniq(arr) {
@@ -366,7 +382,7 @@ function asDateLike(value) {
366
382
  * Convert RDAP JSON into our normalized DomainRecord.
367
383
  * This function is defensive: RDAP servers vary in completeness and field naming.
368
384
  */
369
- function normalizeRdap(inputDomain, tld, rdap, rdapServersTried, fetchedAtISO, includeRaw = false) {
385
+ function normalizeRdap(inputDomain, tld, rdap, rdapServersTried, includeRaw = false) {
370
386
  const doc = rdap ?? {};
371
387
  const ldhName = asString(doc.ldhName) || asString(doc.handle);
372
388
  const unicodeName = asString(doc.unicodeName);
@@ -382,6 +398,8 @@ function normalizeRdap(inputDomain, tld, rdap, rdapServersTried, fetchedAtISO, i
382
398
  return n;
383
399
  }).filter((n) => !!n.host) : void 0;
384
400
  const contacts = extractContacts(doc.entities);
401
+ const registrant = contacts?.find((c) => c.type === "registrant");
402
+ const privacyEnabled = !!(registrant && [registrant.name, registrant.organization].filter(Boolean).some(isPrivacyName));
385
403
  const statuses = Array.isArray(doc.status) ? doc.status.filter((s) => typeof s === "string").map((s) => ({
386
404
  status: s,
387
405
  raw: s
@@ -426,12 +444,12 @@ function normalizeRdap(inputDomain, tld, rdap, rdapServersTried, fetchedAtISO, i
426
444
  host: n.host.toLowerCase()
427
445
  }))) : void 0,
428
446
  contacts,
447
+ privacyEnabled: privacyEnabled ? true : void 0,
429
448
  whoisServer,
430
449
  rdapServers: rdapServersTried,
431
450
  rawRdap: includeRaw ? rdap : void 0,
432
451
  rawWhois: void 0,
433
452
  source: "rdap",
434
- fetchedAt: fetchedAtISO,
435
453
  warnings: void 0
436
454
  };
437
455
  }
@@ -718,7 +736,7 @@ function normalizeServer(server) {
718
736
  * Convert raw WHOIS text into our normalized DomainRecord.
719
737
  * Heuristics cover many gTLD and ccTLD formats; exact fields vary per registry.
720
738
  */
721
- function normalizeWhois(domain, tld, whoisText, whoisServer, fetchedAtISO, includeRaw = false) {
739
+ function normalizeWhois(domain, tld, whoisText, whoisServer, includeRaw = false) {
722
740
  const map = parseKeyValueLines(whoisText);
723
741
  const creationDate = anyValue(map, [
724
742
  "creation date",
@@ -799,6 +817,8 @@ function normalizeWhois(domain, tld, whoisText, whoisServer, fetchedAtISO, inclu
799
817
  return ns;
800
818
  }).filter((x) => !!x)) : void 0;
801
819
  const contacts = collectContacts(map);
820
+ const registrant = contacts?.find((c) => c.type === "registrant");
821
+ const privacyEnabled = !!(registrant && [registrant.name, registrant.organization].filter(Boolean).some(isPrivacyName));
802
822
  const dnssecRaw = (map.dnssec?.[0] || "").toLowerCase();
803
823
  const dnssec = dnssecRaw ? { enabled: /signed|yes|true/.test(dnssecRaw) } : void 0;
804
824
  const transferLock = !!statuses?.some((s) => /transferprohibited/i.test(s.status));
@@ -821,12 +841,12 @@ function normalizeWhois(domain, tld, whoisText, whoisServer, fetchedAtISO, inclu
821
841
  dnssec,
822
842
  nameservers,
823
843
  contacts,
844
+ privacyEnabled: privacyEnabled ? true : void 0,
824
845
  whoisServer,
825
846
  rdapServers: void 0,
826
847
  rawRdap: void 0,
827
848
  rawWhois: includeRaw ? whoisText : void 0,
828
849
  source: "whois",
829
- fetchedAt: fetchedAtISO,
830
850
  warnings: void 0
831
851
  };
832
852
  }
@@ -958,7 +978,6 @@ async function lookupDomain(domain, opts) {
958
978
  error: "Input does not look like a domain"
959
979
  };
960
980
  const { publicSuffix, tld } = getDomainParts(domain);
961
- const now = toISO(/* @__PURE__ */ new Date()) ?? (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
962
981
  if (!opts?.whoisOnly) {
963
982
  const bases = await getRdapBaseUrlsForTld(tld, opts);
964
983
  const tried = [];
@@ -969,7 +988,7 @@ async function lookupDomain(domain, opts) {
969
988
  const rdapEnriched = await fetchAndMergeRdapRelated(domain, json, opts);
970
989
  return {
971
990
  ok: true,
972
- record: normalizeRdap(domain, tld, rdapEnriched.merged, [...tried, ...rdapEnriched.serversTried], now, !!opts?.includeRaw)
991
+ record: normalizeRdap(domain, tld, rdapEnriched.merged, [...tried, ...rdapEnriched.serversTried], !!opts?.includeRaw)
973
992
  };
974
993
  } catch {}
975
994
  }
@@ -998,13 +1017,13 @@ async function lookupDomain(domain, opts) {
998
1017
  const alt = await whoisQuery(server, domain, opts);
999
1018
  if (alt.text && !/error/i.test(alt.text)) return {
1000
1019
  ok: true,
1001
- record: normalizeWhois(domain, tld, alt.text, alt.serverQueried, now, !!opts?.includeRaw)
1020
+ record: normalizeWhois(domain, tld, alt.text, alt.serverQueried, !!opts?.includeRaw)
1002
1021
  };
1003
1022
  } catch {}
1004
1023
  }
1005
1024
  return {
1006
1025
  ok: true,
1007
- record: normalizeWhois(domain, tld, res.text, res.serverQueried, now, !!opts?.includeRaw)
1026
+ record: normalizeWhois(domain, tld, res.text, res.serverQueried, !!opts?.includeRaw)
1008
1027
  };
1009
1028
  } catch (err) {
1010
1029
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rdapper",
3
- "version": "0.3.0",
3
+ "version": "0.4.1",
4
4
  "license": "MIT",
5
5
  "description": "🎩 RDAP/WHOIS fetcher, parser, and normalizer for Node",
6
6
  "repository": {