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.
- package/dist/index.js +146 -16
- 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.
|
|
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
|
-
|
|
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.${
|
|
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
|
|
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:
|
|
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.
|
|
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":
|
|
25
|
+
"bin": {
|
|
26
|
+
"rdapper": "bin/cli.js"
|
|
27
|
+
},
|
|
26
28
|
"exports": {
|
|
27
29
|
".": {
|
|
28
|
-
"
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
29
31
|
"import": "./dist/index.js",
|
|
30
|
-
"
|
|
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.
|
|
52
|
-
"tsdown": "0.15.
|
|
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
|
},
|