rdapper 0.4.0 → 0.5.0
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 +12 -3
- package/dist/index.d.ts +8 -3
- package/dist/index.js +103 -108
- package/package.json +6 -7
package/README.md
CHANGED
|
@@ -35,6 +35,16 @@ await isRegistered("example.com"); // => true
|
|
|
35
35
|
await isAvailable("likely-unregistered-thing-320485230458.com"); // => false
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
+
Normalize arbitrary input (domain or URL) to its registrable domain (eTLD+1):
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { toRegistrableDomain } from "rdapper";
|
|
42
|
+
|
|
43
|
+
toRegistrableDomain("https://sub.example.co.uk/page"); // => "example.co.uk"
|
|
44
|
+
toRegistrableDomain("spark-public.s3.amazonaws.com"); // => "amazonaws.com" (ICANN-only default)
|
|
45
|
+
toRegistrableDomain("192.168.0.1"); // => null
|
|
46
|
+
```
|
|
47
|
+
|
|
38
48
|
## API
|
|
39
49
|
|
|
40
50
|
- `lookupDomain(domain, options?) => Promise<LookupResult>`
|
|
@@ -123,7 +133,6 @@ interface DomainRecord {
|
|
|
123
133
|
rawRdap?: unknown; // raw RDAP JSON (only when options.includeRaw)
|
|
124
134
|
rawWhois?: string; // raw WHOIS text (only when options.includeRaw)
|
|
125
135
|
source: "rdap" | "whois"; // which path produced data
|
|
126
|
-
fetchedAt: string; // ISO 8601 timestamp
|
|
127
136
|
warnings?: string[];
|
|
128
137
|
}
|
|
129
138
|
```
|
|
@@ -139,8 +148,7 @@ interface DomainRecord {
|
|
|
139
148
|
"statuses": [{ "status": "clientTransferProhibited" }],
|
|
140
149
|
"nameservers": [{ "host": "a.iana-servers.net" }, { "host": "b.iana-servers.net" }],
|
|
141
150
|
"dnssec": { "enabled": true },
|
|
142
|
-
"source": "rdap"
|
|
143
|
-
"fetchedAt": "2025-01-01T00:00:00Z"
|
|
151
|
+
"source": "rdap"
|
|
144
152
|
}
|
|
145
153
|
```
|
|
146
154
|
|
|
@@ -183,6 +191,7 @@ Project layout:
|
|
|
183
191
|
- Some TLDs provide no RDAP service; `rdapOnly: true` will fail for them.
|
|
184
192
|
- Registries may throttle or block WHOIS; respect rate limits and usage policies.
|
|
185
193
|
- Field presence depends on source and privacy policies (e.g., redaction/withholding).
|
|
194
|
+
- Public suffix detection uses `tldts` with ICANN‑only defaults (Private section is ignored). If you need behavior closer to `psl` that considers private suffixes, see the `allowPrivateDomains` option in the `tldts` docs (rdapper currently sticks to ICANN‑only by default). See: [tldts migration notes](https://github.com/remusao/tldts#migrating-from-other-libraries).
|
|
186
195
|
|
|
187
196
|
## License
|
|
188
197
|
|
package/dist/index.d.ts
CHANGED
|
@@ -88,8 +88,6 @@ interface DomainRecord {
|
|
|
88
88
|
rawWhois?: string;
|
|
89
89
|
/** Which source produced data */
|
|
90
90
|
source: LookupSource;
|
|
91
|
-
/** ISO 8601 timestamp at time of lookup */
|
|
92
|
-
fetchedAt: string;
|
|
93
91
|
/** Warnings generated during lookup */
|
|
94
92
|
warnings?: string[];
|
|
95
93
|
}
|
|
@@ -126,6 +124,13 @@ interface LookupResult {
|
|
|
126
124
|
}
|
|
127
125
|
type FetchLike = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
|
|
128
126
|
//#endregion
|
|
127
|
+
//#region src/lib/domain.d.ts
|
|
128
|
+
/**
|
|
129
|
+
* Normalize arbitrary input (domain or URL) to its registrable domain (eTLD+1).
|
|
130
|
+
* Returns null when the input is not a valid ICANN domain (e.g., invalid TLD, IPs).
|
|
131
|
+
*/
|
|
132
|
+
declare function toRegistrableDomain(input: string): string | null;
|
|
133
|
+
//#endregion
|
|
129
134
|
//#region src/index.d.ts
|
|
130
135
|
/**
|
|
131
136
|
* High-level lookup that prefers RDAP and falls back to WHOIS.
|
|
@@ -139,4 +144,4 @@ declare function isAvailable(domain: string, opts?: LookupOptions): Promise<bool
|
|
|
139
144
|
* Performs a lookup and resolves to a boolean. Rejects on lookup error. */
|
|
140
145
|
declare function isRegistered(domain: string, opts?: LookupOptions): Promise<boolean>;
|
|
141
146
|
//#endregion
|
|
142
|
-
export { Contact, DomainRecord, FetchLike, LookupOptions, LookupResult, LookupSource, Nameserver, RegistrarInfo, StatusEvent, isAvailable, isRegistered, lookupDomain };
|
|
147
|
+
export { Contact, DomainRecord, FetchLike, LookupOptions, LookupResult, LookupSource, Nameserver, RegistrarInfo, StatusEvent, isAvailable, isRegistered, lookupDomain, toRegistrableDomain };
|
package/dist/index.js
CHANGED
|
@@ -1,94 +1,33 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { parse } from "tldts";
|
|
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
|
|
5
|
+
/**
|
|
6
|
+
* Parse a domain into its parts.
|
|
7
|
+
*/
|
|
73
8
|
function getDomainParts(domain) {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
83
|
-
const labels = publicSuffix.split(".").filter(Boolean);
|
|
84
|
-
const tld = labels.length ? labels[labels.length - 1] : publicSuffix;
|
|
85
|
-
return {
|
|
86
|
-
publicSuffix,
|
|
87
|
-
tld
|
|
88
|
-
};
|
|
9
|
+
return parse(domain);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* Basic domain validity check (hostname-like), not performing DNS or RDAP.
|
|
13
|
+
*/
|
|
14
|
+
function isLikelyDomain(value) {
|
|
15
|
+
const v = (value ?? "").trim();
|
|
16
|
+
return /^(?=.{1,253}$)(?:(?!-)[a-z0-9-]{1,63}(?<!-)\.)+(?!-)[a-z0-9-]{2,63}(?<!-)$/.test(v.toLowerCase());
|
|
89
17
|
}
|
|
90
|
-
|
|
91
|
-
|
|
18
|
+
/**
|
|
19
|
+
* Normalize arbitrary input (domain or URL) to its registrable domain (eTLD+1).
|
|
20
|
+
* Returns null when the input is not a valid ICANN domain (e.g., invalid TLD, IPs).
|
|
21
|
+
*/
|
|
22
|
+
function toRegistrableDomain(input) {
|
|
23
|
+
const raw = (input ?? "").trim();
|
|
24
|
+
if (raw === "") return null;
|
|
25
|
+
const result = parse(raw);
|
|
26
|
+
if (result.isIp) return null;
|
|
27
|
+
if (!result.isIcann) return null;
|
|
28
|
+
const domain = result.domain ?? "";
|
|
29
|
+
if (domain === "") return null;
|
|
30
|
+
return domain.toLowerCase();
|
|
92
31
|
}
|
|
93
32
|
const WHOIS_AVAILABLE_PATTERNS = [
|
|
94
33
|
/\bno match\b/i,
|
|
@@ -302,6 +241,74 @@ function sameDomain(a, b) {
|
|
|
302
241
|
return a.toLowerCase() === b.toLowerCase();
|
|
303
242
|
}
|
|
304
243
|
|
|
244
|
+
//#endregion
|
|
245
|
+
//#region src/lib/dates.ts
|
|
246
|
+
function toISO(dateLike) {
|
|
247
|
+
if (dateLike == null) return void 0;
|
|
248
|
+
if (dateLike instanceof Date) return toIsoFromDate(dateLike);
|
|
249
|
+
if (typeof dateLike === "number") return toIsoFromDate(new Date(dateLike));
|
|
250
|
+
const raw = String(dateLike).trim();
|
|
251
|
+
if (!raw) return void 0;
|
|
252
|
+
for (const re of [
|
|
253
|
+
/^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})(?:Z|([+-]\d{2})(?::?(\d{2}))?)?$/,
|
|
254
|
+
/^(\d{4})\/(\d{2})\/(\d{2})[ T](\d{2}):(\d{2}):(\d{2})(?:Z|([+-]\d{2})(?::?(\d{2}))?)?$/,
|
|
255
|
+
/^(\d{2})-([A-Za-z]{3})-(\d{4})$/,
|
|
256
|
+
/^([A-Za-z]{3})\s+(\d{1,2})\s+(\d{4})$/
|
|
257
|
+
]) {
|
|
258
|
+
const m = raw.match(re);
|
|
259
|
+
if (!m) continue;
|
|
260
|
+
const d = parseDateWithRegex(m, re);
|
|
261
|
+
if (d) return toIsoFromDate(d);
|
|
262
|
+
}
|
|
263
|
+
const native = new Date(raw);
|
|
264
|
+
if (!Number.isNaN(native.getTime())) return toIsoFromDate(native);
|
|
265
|
+
}
|
|
266
|
+
function toIsoFromDate(d) {
|
|
267
|
+
try {
|
|
268
|
+
return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(), 0)).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
269
|
+
} catch {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
function parseDateWithRegex(m, _re) {
|
|
274
|
+
const monthMap = {
|
|
275
|
+
jan: 0,
|
|
276
|
+
feb: 1,
|
|
277
|
+
mar: 2,
|
|
278
|
+
apr: 3,
|
|
279
|
+
may: 4,
|
|
280
|
+
jun: 5,
|
|
281
|
+
jul: 6,
|
|
282
|
+
aug: 7,
|
|
283
|
+
sep: 8,
|
|
284
|
+
oct: 9,
|
|
285
|
+
nov: 10,
|
|
286
|
+
dec: 11
|
|
287
|
+
};
|
|
288
|
+
try {
|
|
289
|
+
if (m[0].includes(":")) {
|
|
290
|
+
const [_$1, y, mo, d, hh, mm, ss, offH, offM] = m;
|
|
291
|
+
let dt = Date.UTC(Number(y), Number(mo) - 1, Number(d), Number(hh), Number(mm), Number(ss));
|
|
292
|
+
if (offH) {
|
|
293
|
+
const sign = offH.startsWith("-") ? -1 : 1;
|
|
294
|
+
const hours = Math.abs(Number(offH));
|
|
295
|
+
const minutes = offM ? Number(offM) : 0;
|
|
296
|
+
const offsetMs = sign * (hours * 60 + minutes) * 60 * 1e3;
|
|
297
|
+
dt -= offsetMs;
|
|
298
|
+
}
|
|
299
|
+
return new Date(dt);
|
|
300
|
+
}
|
|
301
|
+
if (m[0].includes("-")) {
|
|
302
|
+
const [_$1, dd$1, monStr$1, yyyy$1] = m;
|
|
303
|
+
const mon$1 = monthMap[monStr$1.toLowerCase()];
|
|
304
|
+
return new Date(Date.UTC(Number(yyyy$1), mon$1, Number(dd$1)));
|
|
305
|
+
}
|
|
306
|
+
const [_, monStr, dd, yyyy] = m;
|
|
307
|
+
const mon = monthMap[monStr.toLowerCase()];
|
|
308
|
+
return new Date(Date.UTC(Number(yyyy), mon, Number(dd)));
|
|
309
|
+
} catch {}
|
|
310
|
+
}
|
|
311
|
+
|
|
305
312
|
//#endregion
|
|
306
313
|
//#region src/lib/privacy.ts
|
|
307
314
|
const PRIVACY_NAME_KEYWORDS = [
|
|
@@ -382,7 +389,7 @@ function asDateLike(value) {
|
|
|
382
389
|
* Convert RDAP JSON into our normalized DomainRecord.
|
|
383
390
|
* This function is defensive: RDAP servers vary in completeness and field naming.
|
|
384
391
|
*/
|
|
385
|
-
function normalizeRdap(inputDomain, tld, rdap, rdapServersTried,
|
|
392
|
+
function normalizeRdap(inputDomain, tld, rdap, rdapServersTried, includeRaw = false) {
|
|
386
393
|
const doc = rdap ?? {};
|
|
387
394
|
const ldhName = asString(doc.ldhName) || asString(doc.handle);
|
|
388
395
|
const unicodeName = asString(doc.unicodeName);
|
|
@@ -450,7 +457,6 @@ function normalizeRdap(inputDomain, tld, rdap, rdapServersTried, fetchedAtISO, i
|
|
|
450
457
|
rawRdap: includeRaw ? rdap : void 0,
|
|
451
458
|
rawWhois: void 0,
|
|
452
459
|
source: "rdap",
|
|
453
|
-
fetchedAt: fetchedAtISO,
|
|
454
460
|
warnings: void 0
|
|
455
461
|
};
|
|
456
462
|
}
|
|
@@ -737,7 +743,7 @@ function normalizeServer(server) {
|
|
|
737
743
|
* Convert raw WHOIS text into our normalized DomainRecord.
|
|
738
744
|
* Heuristics cover many gTLD and ccTLD formats; exact fields vary per registry.
|
|
739
745
|
*/
|
|
740
|
-
function normalizeWhois(domain, tld, whoisText, whoisServer,
|
|
746
|
+
function normalizeWhois(domain, tld, whoisText, whoisServer, includeRaw = false) {
|
|
741
747
|
const map = parseKeyValueLines(whoisText);
|
|
742
748
|
const creationDate = anyValue(map, [
|
|
743
749
|
"creation date",
|
|
@@ -848,7 +854,6 @@ function normalizeWhois(domain, tld, whoisText, whoisServer, fetchedAtISO, inclu
|
|
|
848
854
|
rawRdap: void 0,
|
|
849
855
|
rawWhois: includeRaw ? whoisText : void 0,
|
|
850
856
|
source: "whois",
|
|
851
|
-
fetchedAt: fetchedAtISO,
|
|
852
857
|
warnings: void 0
|
|
853
858
|
};
|
|
854
859
|
}
|
|
@@ -979,8 +984,11 @@ async function lookupDomain(domain, opts) {
|
|
|
979
984
|
ok: false,
|
|
980
985
|
error: "Input does not look like a domain"
|
|
981
986
|
};
|
|
982
|
-
const { publicSuffix
|
|
983
|
-
|
|
987
|
+
const { publicSuffix: tld } = getDomainParts(domain);
|
|
988
|
+
if (!tld) return {
|
|
989
|
+
ok: false,
|
|
990
|
+
error: "Invalid TLD"
|
|
991
|
+
};
|
|
984
992
|
if (!opts?.whoisOnly) {
|
|
985
993
|
const bases = await getRdapBaseUrlsForTld(tld, opts);
|
|
986
994
|
const tried = [];
|
|
@@ -991,7 +999,7 @@ async function lookupDomain(domain, opts) {
|
|
|
991
999
|
const rdapEnriched = await fetchAndMergeRdapRelated(domain, json, opts);
|
|
992
1000
|
return {
|
|
993
1001
|
ok: true,
|
|
994
|
-
record: normalizeRdap(domain, tld, rdapEnriched.merged, [...tried, ...rdapEnriched.serversTried],
|
|
1002
|
+
record: normalizeRdap(domain, tld, rdapEnriched.merged, [...tried, ...rdapEnriched.serversTried], !!opts?.includeRaw)
|
|
995
1003
|
};
|
|
996
1004
|
} catch {}
|
|
997
1005
|
}
|
|
@@ -1011,22 +1019,9 @@ async function lookupDomain(domain, opts) {
|
|
|
1011
1019
|
};
|
|
1012
1020
|
}
|
|
1013
1021
|
const res = await followWhoisReferrals(whoisServer, domain, opts);
|
|
1014
|
-
if (publicSuffix.includes(".") && /no match|not found/i.test(res.text) && opts?.followWhoisReferral !== false) {
|
|
1015
|
-
const candidates = [];
|
|
1016
|
-
const ps = publicSuffix.toLowerCase();
|
|
1017
|
-
const exception = WHOIS_TLD_EXCEPTIONS[ps];
|
|
1018
|
-
if (exception) candidates.push(exception);
|
|
1019
|
-
for (const server of candidates) try {
|
|
1020
|
-
const alt = await whoisQuery(server, domain, opts);
|
|
1021
|
-
if (alt.text && !/error/i.test(alt.text)) return {
|
|
1022
|
-
ok: true,
|
|
1023
|
-
record: normalizeWhois(domain, tld, alt.text, alt.serverQueried, now, !!opts?.includeRaw)
|
|
1024
|
-
};
|
|
1025
|
-
} catch {}
|
|
1026
|
-
}
|
|
1027
1022
|
return {
|
|
1028
1023
|
ok: true,
|
|
1029
|
-
record: normalizeWhois(domain, tld, res.text, res.serverQueried,
|
|
1024
|
+
record: normalizeWhois(domain, tld, res.text, res.serverQueried, !!opts?.includeRaw)
|
|
1030
1025
|
};
|
|
1031
1026
|
} catch (err) {
|
|
1032
1027
|
return {
|
|
@@ -1051,4 +1046,4 @@ async function isRegistered(domain, opts) {
|
|
|
1051
1046
|
}
|
|
1052
1047
|
|
|
1053
1048
|
//#endregion
|
|
1054
|
-
export { isAvailable, isRegistered, lookupDomain };
|
|
1049
|
+
export { isAvailable, isRegistered, lookupDomain, toRegistrableDomain };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rdapper",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "🎩 RDAP/WHOIS fetcher, parser, and normalizer for Node",
|
|
6
6
|
"repository": {
|
|
@@ -39,14 +39,13 @@
|
|
|
39
39
|
"prepublishOnly": "npm run build"
|
|
40
40
|
},
|
|
41
41
|
"dependencies": {
|
|
42
|
-
"
|
|
42
|
+
"tldts": "7.0.17"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
|
-
"@biomejs/biome": "2.2.
|
|
46
|
-
"@types/node": "24.
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
"typescript": "5.9.2",
|
|
45
|
+
"@biomejs/biome": "2.2.5",
|
|
46
|
+
"@types/node": "24.7.1",
|
|
47
|
+
"tsdown": "0.15.6",
|
|
48
|
+
"typescript": "5.9.3",
|
|
50
49
|
"vitest": "^3.2.4"
|
|
51
50
|
},
|
|
52
51
|
"engines": {
|