rdapper 0.4.1 → 0.6.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 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>`
@@ -43,6 +53,20 @@ await isAvailable("likely-unregistered-thing-320485230458.com"); // => false
43
53
  - `isRegistered(domain, options?) => Promise<boolean>`
44
54
  - `isAvailable(domain, options?) => Promise<boolean>`
45
55
 
56
+ ### Edge runtimes (e.g., Vercel Edge)
57
+
58
+ WHOIS requires a raw TCP connection over port 43 via `node:net`, which is not available on edge runtimes. This package lazily loads `node:net` only when the WHOIS code path runs. To use rdapper safely on edge:
59
+
60
+ - Prefer RDAP only:
61
+
62
+ ```ts
63
+ import { lookupDomain } from "rdapper";
64
+
65
+ const res = await lookupDomain("example.com", { rdapOnly: true });
66
+ ```
67
+
68
+ - If `rdapOnly` is omitted and the code path reaches WHOIS on edge, rdapper throws a clear runtime error indicating WHOIS is unsupported on edge and to run in Node or set `rdapOnly: true`.
69
+
46
70
  ### Options
47
71
 
48
72
  - `timeoutMs?: number` – Total timeout budget per network operation (default `15000`).
@@ -181,6 +205,7 @@ Project layout:
181
205
  - Some TLDs provide no RDAP service; `rdapOnly: true` will fail for them.
182
206
  - Registries may throttle or block WHOIS; respect rate limits and usage policies.
183
207
  - Field presence depends on source and privacy policies (e.g., redaction/withholding).
208
+ - 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).
184
209
 
185
210
  ## License
186
211
 
package/dist/index.d.ts CHANGED
@@ -124,6 +124,13 @@ interface LookupResult {
124
124
  }
125
125
  type FetchLike = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
126
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
127
134
  //#region src/index.d.ts
128
135
  /**
129
136
  * High-level lookup that prefers RDAP and falls back to WHOIS.
@@ -137,4 +144,4 @@ declare function isAvailable(domain: string, opts?: LookupOptions): Promise<bool
137
144
  * Performs a lookup and resolves to a boolean. Rejects on lookup error. */
138
145
  declare function isRegistered(domain: string, opts?: LookupOptions): Promise<boolean>;
139
146
  //#endregion
140
- 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,26 +1,32 @@
1
- import psl from "psl";
2
- import { createConnection } from "node:net";
1
+ import { parse } from "tldts";
3
2
 
4
3
  //#region src/lib/domain.ts
4
+ /**
5
+ * Parse a domain into its parts.
6
+ */
5
7
  function getDomainParts(domain) {
6
- const lower = domain.toLowerCase().trim();
7
- let publicSuffix;
8
- try {
9
- publicSuffix = (psl.parse?.(lower))?.tld;
10
- } catch {}
11
- if (!publicSuffix) {
12
- const parts = lower.split(".").filter(Boolean);
13
- publicSuffix = parts.length ? parts[parts.length - 1] : lower;
14
- }
15
- const labels = publicSuffix.split(".").filter(Boolean);
16
- const tld = labels.length ? labels[labels.length - 1] : publicSuffix;
17
- return {
18
- publicSuffix,
19
- tld
20
- };
8
+ return parse(domain);
21
9
  }
22
- function isLikelyDomain(input) {
23
- return /^[a-z0-9.-]+$/i.test(input) && input.includes(".");
10
+ /**
11
+ * Basic domain validity check (hostname-like), not performing DNS or RDAP.
12
+ */
13
+ function isLikelyDomain(value) {
14
+ const v = (value ?? "").trim();
15
+ return /^(?=.{1,253}$)(?:(?!-)[a-z0-9-]{1,63}(?<!-)\.)+(?!-)[a-z0-9-]{2,63}(?<!-)$/.test(v.toLowerCase());
16
+ }
17
+ /**
18
+ * Normalize arbitrary input (domain or URL) to its registrable domain (eTLD+1).
19
+ * Returns null when the input is not a valid ICANN domain (e.g., invalid TLD, IPs).
20
+ */
21
+ function toRegistrableDomain(input) {
22
+ const raw = (input ?? "").trim();
23
+ if (raw === "") return null;
24
+ const result = parse(raw);
25
+ if (result.isIp) return null;
26
+ if (!result.isIcann) return null;
27
+ const domain = result.domain ?? "";
28
+ if (domain === "") return null;
29
+ return domain.toLowerCase();
24
30
  }
25
31
  const WHOIS_AVAILABLE_PATTERNS = [
26
32
  /\bno match\b/i,
@@ -555,9 +561,16 @@ async function whoisQuery(server, query, options) {
555
561
  text
556
562
  };
557
563
  }
558
- function queryTcp(host, port, query, options) {
564
+ async function queryTcp(host, port, query, options) {
565
+ let net;
566
+ try {
567
+ net = await import("net");
568
+ } catch {
569
+ net = null;
570
+ }
571
+ if (!net?.createConnection) throw new Error("WHOIS client is only available in Node.js runtimes; try setting `rdapOnly: true`.");
559
572
  return new Promise((resolve, reject) => {
560
- const socket = createConnection({
573
+ const socket = net.createConnection({
561
574
  host,
562
575
  port
563
576
  });
@@ -977,7 +990,11 @@ async function lookupDomain(domain, opts) {
977
990
  ok: false,
978
991
  error: "Input does not look like a domain"
979
992
  };
980
- const { publicSuffix, tld } = getDomainParts(domain);
993
+ const { publicSuffix: tld } = getDomainParts(domain);
994
+ if (!tld) return {
995
+ ok: false,
996
+ error: "Invalid TLD"
997
+ };
981
998
  if (!opts?.whoisOnly) {
982
999
  const bases = await getRdapBaseUrlsForTld(tld, opts);
983
1000
  const tried = [];
@@ -1008,19 +1025,6 @@ async function lookupDomain(domain, opts) {
1008
1025
  };
1009
1026
  }
1010
1027
  const res = await followWhoisReferrals(whoisServer, domain, opts);
1011
- if (publicSuffix.includes(".") && /no match|not found/i.test(res.text) && opts?.followWhoisReferral !== false) {
1012
- const candidates = [];
1013
- const ps = publicSuffix.toLowerCase();
1014
- const exception = WHOIS_TLD_EXCEPTIONS[ps];
1015
- if (exception) candidates.push(exception);
1016
- for (const server of candidates) try {
1017
- const alt = await whoisQuery(server, domain, opts);
1018
- if (alt.text && !/error/i.test(alt.text)) return {
1019
- ok: true,
1020
- record: normalizeWhois(domain, tld, alt.text, alt.serverQueried, !!opts?.includeRaw)
1021
- };
1022
- } catch {}
1023
- }
1024
1028
  return {
1025
1029
  ok: true,
1026
1030
  record: normalizeWhois(domain, tld, res.text, res.serverQueried, !!opts?.includeRaw)
@@ -1048,4 +1052,4 @@ async function isRegistered(domain, opts) {
1048
1052
  }
1049
1053
 
1050
1054
  //#endregion
1051
- export { isAvailable, isRegistered, lookupDomain };
1055
+ export { isAvailable, isRegistered, lookupDomain, toRegistrableDomain };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rdapper",
3
- "version": "0.4.1",
3
+ "version": "0.6.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
- "psl": "1.15.0"
42
+ "tldts": "7.0.17"
43
43
  },
44
44
  "devDependencies": {
45
- "@biomejs/biome": "2.2.4",
46
- "@types/node": "24.5.2",
47
- "@types/psl": "1.1.3",
48
- "tsdown": "0.15.5",
49
- "typescript": "5.9.2",
45
+ "@biomejs/biome": "2.2.5",
46
+ "@types/node": "24.7.2",
47
+ "tsdown": "0.15.6",
48
+ "typescript": "5.9.3",
50
49
  "vitest": "^3.2.4"
51
50
  },
52
51
  "engines": {