rdapper 0.8.0 → 0.9.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.
Files changed (2) hide show
  1. package/dist/index.js +146 -16
  2. package/package.json +8 -6
package/dist/index.js CHANGED
@@ -48,7 +48,12 @@ const WHOIS_AVAILABLE_PATTERNS = [
48
48
  /\bdomain\s+available\b/i,
49
49
  /\bdomain status[:\s]+available\b/i,
50
50
  /\bobject does not exist\b/i,
51
- /\bthe queried object does not exist\b/i
51
+ /\bthe queried object does not exist\b/i,
52
+ /\bstatus:\s*free\b/i,
53
+ /\bstatus:\s*available\b/i,
54
+ /\bno object found\b/i,
55
+ /\bnicht gefunden\b/i,
56
+ /\bpending release\b/i
52
57
  ];
53
58
  function isWhoisAvailable(text) {
54
59
  if (!text) return false;
@@ -90,9 +95,9 @@ async function getRdapBaseUrlsForTld(tld, options) {
90
95
  const target = tld.toLowerCase();
91
96
  const bases = [];
92
97
  for (const svc of data.services) {
93
- const tlds = svc[0];
98
+ const tlds = svc[0].map((x) => x.toLowerCase());
94
99
  const urls = svc[1];
95
- if (tlds.map((x) => x.toLowerCase()).includes(target)) for (const u of urls) {
100
+ if (tlds.includes(target)) for (const u of urls) {
96
101
  const base = u.endsWith("/") ? u : `${u}/`;
97
102
  bases.push(base);
98
103
  }
@@ -117,10 +122,9 @@ async function fetchRdapDomain(domain, baseUrl, options) {
117
122
  const bodyText = await res.text();
118
123
  throw new Error(`RDAP ${res.status}: ${bodyText.slice(0, 500)}`);
119
124
  }
120
- const json = await res.json();
121
125
  return {
122
126
  url,
123
- json
127
+ json: await res.json()
124
128
  };
125
129
  }
126
130
 
@@ -221,10 +225,9 @@ async function fetchRdapUrl(url, options) {
221
225
  const bodyText = await res.text();
222
226
  throw new Error(`RDAP ${res.status}: ${bodyText.slice(0, 500)}`);
223
227
  }
224
- const json = await res.json();
225
228
  return {
226
229
  url,
227
- json
230
+ json: await res.json()
228
231
  };
229
232
  }
230
233
  function toArray(val) {
@@ -564,12 +567,9 @@ function parseVcard(vcardArray) {
564
567
  */
565
568
  async function whoisQuery(server, query, options) {
566
569
  const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
567
- const port = 43;
568
- const host = server.replace(/^whois:\/\//i, "");
569
- const text = await withTimeout(queryTcp(host, port, query, options), timeoutMs, "WHOIS timeout");
570
570
  return {
571
571
  serverQueried: server,
572
- text
572
+ text: await withTimeout(queryTcp(server.replace(/^whois:\/\//i, ""), 43, query, options), timeoutMs, "WHOIS timeout")
573
573
  };
574
574
  }
575
575
  async function queryTcp(host, port, query, options) {
@@ -754,6 +754,89 @@ function normalizeServer(server) {
754
754
  return server.replace(/^whois:\/\//i, "").replace(/\/$/, "");
755
755
  }
756
756
 
757
+ //#endregion
758
+ //#region src/whois/merge.ts
759
+ function dedupeStatuses(a, b) {
760
+ const list = [...a || [], ...b || []];
761
+ const seen = /* @__PURE__ */ new Set();
762
+ const out = [];
763
+ for (const s of list) {
764
+ const key = (s?.status || "").toLowerCase();
765
+ if (!key || seen.has(key)) continue;
766
+ seen.add(key);
767
+ out.push(s);
768
+ }
769
+ return out.length ? out : void 0;
770
+ }
771
+ function dedupeNameservers(a, b) {
772
+ const map = /* @__PURE__ */ new Map();
773
+ for (const ns of [...a || [], ...b || []]) {
774
+ const host = ns.host.toLowerCase();
775
+ const prev = map.get(host);
776
+ if (!prev) {
777
+ map.set(host, {
778
+ ...ns,
779
+ host
780
+ });
781
+ continue;
782
+ }
783
+ const ipv4 = uniq([...prev.ipv4 || [], ...ns.ipv4 || []]);
784
+ const ipv6 = uniq([...prev.ipv6 || [], ...ns.ipv6 || []]);
785
+ map.set(host, {
786
+ host,
787
+ ipv4,
788
+ ipv6
789
+ });
790
+ }
791
+ const out = Array.from(map.values());
792
+ return out.length ? out : void 0;
793
+ }
794
+ function dedupeContacts(a, b) {
795
+ const list = [...a || [], ...b || []];
796
+ const seen = /* @__PURE__ */ new Set();
797
+ const out = [];
798
+ for (const c of list) {
799
+ const key = `${c.type}|${(c.organization || c.name || c.email || "").toString().toLowerCase()}`;
800
+ if (seen.has(key)) continue;
801
+ seen.add(key);
802
+ out.push(c);
803
+ }
804
+ return out.length ? out : void 0;
805
+ }
806
+ /** Conservative merge: start with base; fill missing scalars; union arrays; prefer more informative dates. */
807
+ function mergeWhoisRecords(base, others) {
808
+ const merged = { ...base };
809
+ for (const cur of others) {
810
+ merged.isRegistered = merged.isRegistered || cur.isRegistered;
811
+ merged.registry = merged.registry ?? cur.registry;
812
+ merged.registrar = merged.registrar ?? cur.registrar;
813
+ merged.reseller = merged.reseller ?? cur.reseller;
814
+ merged.statuses = dedupeStatuses(merged.statuses, cur.statuses);
815
+ merged.creationDate = preferEarliestIso(merged.creationDate, cur.creationDate);
816
+ merged.updatedDate = preferLatestIso(merged.updatedDate, cur.updatedDate);
817
+ merged.expirationDate = preferLatestIso(merged.expirationDate, cur.expirationDate);
818
+ merged.deletionDate = merged.deletionDate ?? cur.deletionDate;
819
+ merged.transferLock = Boolean(merged.transferLock || cur.transferLock);
820
+ merged.dnssec = merged.dnssec ?? cur.dnssec;
821
+ merged.nameservers = dedupeNameservers(merged.nameservers, cur.nameservers);
822
+ merged.contacts = dedupeContacts(merged.contacts, cur.contacts);
823
+ merged.privacyEnabled = merged.privacyEnabled ?? cur.privacyEnabled;
824
+ merged.whoisServer = cur.whoisServer ?? merged.whoisServer;
825
+ merged.rawWhois = cur.rawWhois ?? merged.rawWhois;
826
+ }
827
+ return merged;
828
+ }
829
+ function preferEarliestIso(a, b) {
830
+ if (!a) return b;
831
+ if (!b) return a;
832
+ return new Date(a) <= new Date(b) ? a : b;
833
+ }
834
+ function preferLatestIso(a, b) {
835
+ if (!a) return b;
836
+ if (!b) return a;
837
+ return new Date(a) >= new Date(b) ? a : b;
838
+ }
839
+
757
840
  //#endregion
758
841
  //#region src/whois/normalize.ts
759
842
  /**
@@ -770,11 +853,13 @@ function normalizeWhois(domain, tld, whoisText, whoisServer, includeRaw = false)
770
853
  "domain create date",
771
854
  "created",
772
855
  "registered",
856
+ "registration time",
773
857
  "domain record activated"
774
858
  ]);
775
859
  const updatedDate = anyValue(map, [
776
860
  "updated date",
777
861
  "last updated",
862
+ "last update",
778
863
  "last modified",
779
864
  "modified",
780
865
  "domain record last updated"
@@ -784,6 +869,8 @@ function normalizeWhois(domain, tld, whoisText, whoisServer, includeRaw = false)
784
869
  "registry expiration date",
785
870
  "expiry date",
786
871
  "expiration date",
872
+ "expire date",
873
+ "expiration time",
787
874
  "registrar registration expiration date",
788
875
  "registrar registration expiry date",
789
876
  "registrar expiration date",
@@ -791,6 +878,7 @@ function normalizeWhois(domain, tld, whoisText, whoisServer, includeRaw = false)
791
878
  "domain expires",
792
879
  "paid-till",
793
880
  "expires on",
881
+ "expires",
794
882
  "renewal date"
795
883
  ]);
796
884
  const registrar = (() => {
@@ -989,6 +1077,40 @@ async function followWhoisReferrals(initialServer, domain, opts) {
989
1077
  }
990
1078
  return current;
991
1079
  }
1080
+ /**
1081
+ * Collect the WHOIS referral chain starting from the TLD server.
1082
+ * Always includes the initial TLD response; may include one or more registrar responses.
1083
+ * Stops on contradiction (registrar claims availability) or failures.
1084
+ */
1085
+ async function collectWhoisReferralChain(initialServer, domain, opts) {
1086
+ const results = [];
1087
+ const maxHops = Math.max(0, opts?.maxWhoisReferralHops ?? 2);
1088
+ const first = await whoisQuery(initialServer, domain, opts);
1089
+ results.push(first);
1090
+ if (opts?.followWhoisReferral === false || maxHops === 0) return results;
1091
+ const visited = new Set([normalize(first.serverQueried)]);
1092
+ let current = first;
1093
+ let hops = 0;
1094
+ while (hops < maxHops) {
1095
+ const next = extractWhoisReferral(current.text);
1096
+ if (!next) break;
1097
+ const normalized = normalize(next);
1098
+ if (visited.has(normalized)) break;
1099
+ visited.add(normalized);
1100
+ try {
1101
+ const res = await whoisQuery(next, domain, opts);
1102
+ const registeredBefore = !isWhoisAvailable(current.text);
1103
+ const registeredAfter = !isWhoisAvailable(res.text);
1104
+ if (registeredBefore && !registeredAfter) break;
1105
+ results.push(res);
1106
+ current = res;
1107
+ } catch {
1108
+ break;
1109
+ }
1110
+ hops += 1;
1111
+ }
1112
+ return results;
1113
+ }
992
1114
  function normalize(server) {
993
1115
  return server.replace(/^whois:\/\//i, "").toLowerCase();
994
1116
  }
@@ -1011,7 +1133,8 @@ async function lookupDomain(domain, opts) {
1011
1133
  error: "Invalid TLD"
1012
1134
  };
1013
1135
  if (!opts?.whoisOnly) {
1014
- const bases = await getRdapBaseUrlsForTld(tld, opts);
1136
+ let bases = await getRdapBaseUrlsForTld(tld, opts);
1137
+ if (bases.length === 0 && tld.includes(".")) bases = await getRdapBaseUrlsForTld(tld.split(".").pop() ?? tld, opts);
1015
1138
  const tried = [];
1016
1139
  for (const base of bases) {
1017
1140
  tried.push(base);
@@ -1033,16 +1156,23 @@ async function lookupDomain(domain, opts) {
1033
1156
  if (!whoisServer) {
1034
1157
  const ianaText = await getIanaWhoisTextForTld(tld, opts);
1035
1158
  const regUrl = ianaText ? parseIanaRegistrationInfoUrl(ianaText) : void 0;
1036
- const hint = regUrl ? ` See registration info at ${regUrl}.` : "";
1037
1159
  return {
1038
1160
  ok: false,
1039
- error: `No WHOIS server discovered for TLD '${tld}'. This registry may not publish public WHOIS over port 43.${hint}`
1161
+ error: `No WHOIS server discovered for TLD '${tld}'. This registry may not publish public WHOIS over port 43.${regUrl ? ` See registration info at ${regUrl}.` : ""}`
1162
+ };
1163
+ }
1164
+ const chain = await collectWhoisReferralChain(whoisServer, domain, opts);
1165
+ if (chain.length === 0) {
1166
+ const res = await followWhoisReferrals(whoisServer, domain, opts);
1167
+ return {
1168
+ ok: true,
1169
+ record: normalizeWhois(domain, tld, res.text, res.serverQueried, !!opts?.includeRaw)
1040
1170
  };
1041
1171
  }
1042
- const res = await followWhoisReferrals(whoisServer, domain, opts);
1172
+ const [first, ...rest] = chain.map((r) => normalizeWhois(domain, tld, r.text, r.serverQueried, !!opts?.includeRaw));
1043
1173
  return {
1044
1174
  ok: true,
1045
- record: normalizeWhois(domain, tld, res.text, res.serverQueried, !!opts?.includeRaw)
1175
+ record: rest.length ? mergeWhoisRecords(first, rest) : first
1046
1176
  };
1047
1177
  } catch (err) {
1048
1178
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rdapper",
3
- "version": "0.8.0",
3
+ "version": "0.9.1",
4
4
  "license": "MIT",
5
5
  "description": "🎩 RDAP/WHOIS fetcher, parser, and normalizer for Node",
6
6
  "repository": {
@@ -22,12 +22,14 @@
22
22
  "main": "./dist/index.js",
23
23
  "module": "./dist/index.js",
24
24
  "types": "./dist/index.d.ts",
25
- "bin": "./bin/cli.js",
25
+ "bin": {
26
+ "rdapper": "bin/cli.js"
27
+ },
26
28
  "exports": {
27
29
  ".": {
28
- "default": "./dist/index.js",
30
+ "types": "./dist/index.d.ts",
29
31
  "import": "./dist/index.js",
30
- "types": "./dist/index.d.ts"
32
+ "default": "./dist/index.js"
31
33
  }
32
34
  },
33
35
  "files": [
@@ -48,8 +50,8 @@
48
50
  },
49
51
  "devDependencies": {
50
52
  "@biomejs/biome": "2.2.6",
51
- "@types/node": "24.8.1",
52
- "tsdown": "0.15.7",
53
+ "@types/node": "24.9.0",
54
+ "tsdown": "0.15.9",
53
55
  "typescript": "5.9.3",
54
56
  "vitest": "^3.2.4"
55
57
  },