rdapper 0.9.1 → 0.10.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
@@ -2,12 +2,17 @@
2
2
 
3
3
  RDAP‑first domain registration lookups with WHOIS fallback. Produces a single, normalized record shape regardless of source.
4
4
 
5
- - RDAP discovery via IANA bootstrap (`https://data.iana.org/rdap/dns.json`)
6
- - WHOIS TCP 43 client with TLD discovery, registrar referral follow, and curated exceptions
7
- - Normalized output: registrar, contacts, nameservers, statuses, dates, DNSSEC, privacy flag, source metadata
8
- - TypeScript types included; ESM‑only; no external HTTP client (uses global `fetch`)
5
+ - RDAP‑first lookup via [IANA bootstrap](https://data.iana.org/rdap/dns.json) with automatic WHOIS fallback when needed
6
+ - Smart WHOIS client (TCP 43): authoritative TLD discovery, registrar referral follow, and curated exceptions
7
+ - Rich, normalized results: registrar, contacts, nameservers, EPP statuses, key dates, DNSSEC, privacy flag, source metadata
8
+ - RDAP enrichment: follows related/entity/registrar links (bounded) to fill in missing details
9
+ - TypeScript‑first: shipped types, ESM‑only, zero external HTTP client (uses global `fetch`)
9
10
 
10
- ### [🦉 See it in action on hoot.sh!](https://hoot.sh)
11
+ > [!IMPORTANT]
12
+ > Edge runtimes (e.g., Vercel Edge, Cloudflare Workers) do not support WHOIS (TCP 43 via `node:net`). Use RDAP‑only mode by setting `{ rdapOnly: true }`.
13
+
14
+ > [!TIP]
15
+ > See `rdapper` in action on [**Domainstack**](https://domainstack.io)!
11
16
 
12
17
  ## Install
13
18
 
@@ -18,23 +23,14 @@ npm install rdapper
18
23
  ## Quick Start
19
24
 
20
25
  ```ts
21
- import { lookupDomain } from "rdapper";
26
+ import { lookup } from "rdapper";
22
27
 
23
- const { ok, record, error } = await lookupDomain("example.com");
28
+ const { ok, record, error } = await lookup("example.com");
24
29
 
25
30
  if (!ok) throw new Error(error);
26
31
  console.log(record); // normalized DomainRecord
27
32
  ```
28
33
 
29
- Also available:
30
-
31
- ```ts
32
- import { isRegistered, isAvailable } from "rdapper";
33
-
34
- await isRegistered("example.com"); // => true
35
- await isAvailable("likely-unregistered-thing-320485230458.com"); // => false
36
- ```
37
-
38
34
  Normalize arbitrary input (domain or URL) to its registrable domain (eTLD+1):
39
35
 
40
36
  ```ts
@@ -45,11 +41,25 @@ toRegistrableDomain("spark-public.s3.amazonaws.com"); // => "amazonaws.com" (I
45
41
  toRegistrableDomain("192.168.0.1"); // => null
46
42
  ```
47
43
 
44
+ Convenience helpers to quickly check availability:
45
+
46
+ ```ts
47
+ import { isRegistered, isAvailable } from "rdapper";
48
+
49
+ await isRegistered("example.com"); // => true
50
+ await isRegistered("likely-unregistered-thing-320485230458.com"); // => false
51
+ await isAvailable("example.com"); // => false
52
+ await isAvailable("likely-unregistered-thing-320485230458.com"); // => true
53
+ ```
54
+
48
55
  ## API
49
56
 
50
- - `lookupDomain(domain, options?) => Promise<LookupResult>`
57
+ - `lookup(domain, options?) => Promise<LookupResult>`
51
58
  - Tries RDAP first if supported by the domain’s TLD; if unavailable or fails, falls back to WHOIS (unless toggled off).
52
59
  - Result is `{ ok: boolean, record?: DomainRecord, error?: string }`.
60
+ - `toRegistrableDomain(input, options?) => string | null`
61
+ - Normalizes a domain or URL to its registrable domain (eTLD+1).
62
+ - Returns the registrable domain string, or `null` for IPs/invalid input; [options](https://github.com/remusao/tldts/blob/master/packages/tldts-core/src/options.ts) are forwarded to `tldts` (e.g., `allowPrivateDomains`).
53
63
  - `isRegistered(domain, options?) => Promise<boolean>`
54
64
  - `isAvailable(domain, options?) => Promise<boolean>`
55
65
 
@@ -64,17 +74,17 @@ echo "example.com" | npx rdapper
64
74
 
65
75
  ### Edge runtimes (e.g., Vercel Edge)
66
76
 
67
- 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:
77
+ WHOIS requires a raw TCP connection over port 43 via `node:net`, which is not available on edge runtimes. rdapper lazily loads `node:net` only when the WHOIS path is taken.
68
78
 
69
- - Prefer RDAP only:
79
+ - Prefer RDAP only on edge:
70
80
 
71
81
  ```ts
72
- import { lookupDomain } from "rdapper";
82
+ import { lookup } from "rdapper";
73
83
 
74
- const res = await lookupDomain("example.com", { rdapOnly: true });
84
+ const res = await lookup("example.com", { rdapOnly: true });
75
85
  ```
76
86
 
77
- - 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`.
87
+ - If `rdapOnly` is omitted and the code path reaches WHOIS on edge, rdapper throws a clear runtime error advising to run in Node or set `{ rdapOnly: true }`.
78
88
 
79
89
  ### Options
80
90
 
@@ -98,7 +108,7 @@ The exact presence of fields depends on registry/registrar data and whether RDAP
98
108
  ```ts
99
109
  interface DomainRecord {
100
110
  domain: string; // normalized name (unicode when available)
101
- tld: string; // terminal TLD label (e.g., "com")
111
+ tld: string; // public suffix (can be multi-label, e.g., "com", "co.uk")
102
112
  isRegistered: boolean; // availability heuristic (WHOIS) or true (RDAP)
103
113
  isIDN?: boolean; // uses punycode labels (xn--)
104
114
  unicodeName?: string; // RDAP unicodeName when provided
@@ -152,7 +162,7 @@ interface DomainRecord {
152
162
  }>;
153
163
  privacyEnabled?: boolean; // registrant appears privacy-redacted based on keyword heuristics
154
164
  whoisServer?: string; // authoritative WHOIS queried (if any)
155
- rdapServers?: string[]; // RDAP base URLs tried
165
+ rdapServers?: string[]; // RDAP URLs tried (bootstrap bases and related/entity links)
156
166
  rawRdap?: unknown; // raw RDAP JSON (only when options.includeRaw)
157
167
  rawWhois?: string; // raw WHOIS text (only when options.includeRaw)
158
168
  source: "rdap" | "whois"; // which path produced data
@@ -191,13 +201,13 @@ Timeouts are enforced per request using a simple race against `timeoutMs` (defau
191
201
 
192
202
  ## Development
193
203
 
194
- - Build: `npm run build`
195
- - Test: `npm test` (Vitest)
204
+ - Build: `npm run build` ([tsdown](https://tsdown.dev/))
205
+ - Test: `npm test` ([Vitest](https://vitest.dev/))
196
206
  - By default, tests are offline/deterministic.
197
- - Watch mode: `npm run test:watch`
198
- - Coverage: `npm run test:coverage`
199
- - Smoke tests that hit the network are gated by `SMOKE=1`, e.g. `SMOKE=1 npm run test:smoke`.
200
- - Lint/format: `npm run lint` (Biome)
207
+ - Watch mode: `npm run dev`
208
+ - Coverage: `npm run test:run -- --coverage`
209
+ - Smoke tests that hit the network are gated by `SMOKE=1`, e.g. `SMOKE=1 npm test`.
210
+ - Lint/format: `npm run lint` ([Biome](https://biomejs.dev/))
201
211
 
202
212
  Project layout:
203
213
 
@@ -214,7 +224,7 @@ Project layout:
214
224
  - Some TLDs provide no RDAP service; `rdapOnly: true` will fail for them.
215
225
  - Registries may throttle or block WHOIS; respect rate limits and usage policies.
216
226
  - Field presence depends on source and privacy policies (e.g., redaction/withholding).
217
- - 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).
227
+ - Public suffix detection uses `tldts` with ICANN‑only defaults (Private section is ignored). You can pass options through to `tldts` via `toRegistrableDomain`/`getDomainParts`/`getDomainTld` (e.g., `allowPrivateDomains`) to customize behavior. See: [tldts migration notes](https://github.com/remusao/tldts#migrating-from-other-libraries).
218
228
 
219
229
  ## License
220
230
 
package/bin/cli.js CHANGED
@@ -6,14 +6,14 @@
6
6
  // echo "example.com" | npx rdapper
7
7
 
8
8
  import { createInterface } from "node:readline";
9
- import { lookupDomain } from "../dist/index.js";
9
+ import { lookup } from "../dist/index.js";
10
10
 
11
11
  async function main() {
12
12
  if (process.argv.length > 2) {
13
13
  // URL(s) specified in the command arguments
14
14
  console.log(
15
15
  JSON.stringify(
16
- await lookupDomain(process.argv[process.argv.length - 1]),
16
+ await lookup(process.argv[process.argv.length - 1]),
17
17
  null,
18
18
  2,
19
19
  ),
@@ -24,7 +24,7 @@ async function main() {
24
24
  input: process.stdin,
25
25
  });
26
26
  rlInterface.on("line", async (line) => {
27
- console.log(JSON.stringify(await lookupDomain(line), null, 2));
27
+ console.log(JSON.stringify(await lookup(line), null, 2));
28
28
  });
29
29
  }
30
30
  }
package/dist/index.d.ts CHANGED
@@ -129,11 +129,14 @@ type FetchLike = (input: RequestInfo | URL, init?: RequestInit) => Promise<Respo
129
129
  //#region src/lib/domain.d.ts
130
130
  type ParseOptions = Parameters<typeof parse>[1];
131
131
  /**
132
- * Parse a domain into its parts. Accepts options which are passed to tldts.parse().
132
+ * Parse a domain into its parts. Passes options to `tldts.parse()`.
133
133
  * @see https://github.com/remusao/tldts/blob/master/packages/tldts-core/src/options.ts
134
134
  */
135
135
  declare function getDomainParts(domain: string, opts?: ParseOptions): ReturnType<typeof parse>;
136
- /** Get the TLD (ICANN-only public suffix) of a domain. */
136
+ /**
137
+ * Get the TLD (ICANN-only public suffix) of a domain. Passes options to `tldts.parse()`.
138
+ * @see https://github.com/remusao/tldts/blob/master/packages/tldts-core/src/options.ts
139
+ */
137
140
  declare function getDomainTld(domain: string, opts?: ParseOptions): string | null;
138
141
  /**
139
142
  * Basic domain validity check (hostname-like), not performing DNS or RDAP.
@@ -141,7 +144,9 @@ declare function getDomainTld(domain: string, opts?: ParseOptions): string | nul
141
144
  declare function isLikelyDomain(value: string): boolean;
142
145
  /**
143
146
  * Normalize arbitrary input (domain or URL) to its registrable domain (eTLD+1).
144
- * Returns null when the input is not a valid ICANN domain (e.g., invalid TLD, IPs).
147
+ * Passes options to `tldts.parse()`.
148
+ * Returns null when the input is not a valid ICANN domain (e.g., invalid TLD, IPs)
149
+ * @see https://github.com/remusao/tldts/blob/master/packages/tldts-core/src/options.ts
145
150
  */
146
151
  declare function toRegistrableDomain(input: string, opts?: ParseOptions): string | null;
147
152
  //#endregion
@@ -150,12 +155,20 @@ declare function toRegistrableDomain(input: string, opts?: ParseOptions): string
150
155
  * High-level lookup that prefers RDAP and falls back to WHOIS.
151
156
  * Ensures a standardized DomainRecord, independent of the source.
152
157
  */
153
- declare function lookupDomain(domain: string, opts?: LookupOptions): Promise<LookupResult>;
154
- /** Determine if a domain appears available (not registered).
155
- * Performs a lookup and resolves to a boolean. Rejects on lookup error. */
158
+ declare function lookup(domain: string, opts?: LookupOptions): Promise<LookupResult>;
159
+ /**
160
+ * Determine if a domain appears available (not registered).
161
+ * Performs a lookup and resolves to a boolean. Rejects on lookup error.
162
+ */
156
163
  declare function isAvailable(domain: string, opts?: LookupOptions): Promise<boolean>;
157
- /** Determine if a domain appears registered.
158
- * Performs a lookup and resolves to a boolean. Rejects on lookup error. */
164
+ /**
165
+ * Determine if a domain appears registered.
166
+ * Performs a lookup and resolves to a boolean. Rejects on lookup error.
167
+ */
159
168
  declare function isRegistered(domain: string, opts?: LookupOptions): Promise<boolean>;
169
+ /**
170
+ * @deprecated Use `lookup` instead.
171
+ */
172
+ declare const lookupDomain: typeof lookup;
160
173
  //#endregion
161
- export { Contact, DomainRecord, FetchLike, LookupOptions, LookupResult, LookupSource, Nameserver, RegistrarInfo, StatusEvent, getDomainParts, getDomainTld, isAvailable, isLikelyDomain, isRegistered, lookupDomain, toRegistrableDomain };
174
+ export { Contact, DomainRecord, FetchLike, LookupOptions, LookupResult, LookupSource, Nameserver, RegistrarInfo, StatusEvent, getDomainParts, getDomainTld, isAvailable, isLikelyDomain, isRegistered, lookup, lookupDomain, toRegistrableDomain };
package/dist/index.js CHANGED
@@ -2,13 +2,16 @@ import { parse } from "tldts";
2
2
 
3
3
  //#region src/lib/domain.ts
4
4
  /**
5
- * Parse a domain into its parts. Accepts options which are passed to tldts.parse().
5
+ * Parse a domain into its parts. Passes options to `tldts.parse()`.
6
6
  * @see https://github.com/remusao/tldts/blob/master/packages/tldts-core/src/options.ts
7
7
  */
8
8
  function getDomainParts(domain, opts) {
9
9
  return parse(domain, { ...opts });
10
10
  }
11
- /** Get the TLD (ICANN-only public suffix) of a domain. */
11
+ /**
12
+ * Get the TLD (ICANN-only public suffix) of a domain. Passes options to `tldts.parse()`.
13
+ * @see https://github.com/remusao/tldts/blob/master/packages/tldts-core/src/options.ts
14
+ */
12
15
  function getDomainTld(domain, opts) {
13
16
  return getDomainParts(domain, {
14
17
  allowPrivateDomains: false,
@@ -24,7 +27,9 @@ function isLikelyDomain(value) {
24
27
  }
25
28
  /**
26
29
  * Normalize arbitrary input (domain or URL) to its registrable domain (eTLD+1).
27
- * Returns null when the input is not a valid ICANN domain (e.g., invalid TLD, IPs).
30
+ * Passes options to `tldts.parse()`.
31
+ * Returns null when the input is not a valid ICANN domain (e.g., invalid TLD, IPs)
32
+ * @see https://github.com/remusao/tldts/blob/master/packages/tldts-core/src/options.ts
28
33
  */
29
34
  function toRegistrableDomain(input, opts) {
30
35
  const raw = (input ?? "").trim();
@@ -39,26 +44,6 @@ function toRegistrableDomain(input, opts) {
39
44
  if (domain === "") return null;
40
45
  return domain.toLowerCase();
41
46
  }
42
- const WHOIS_AVAILABLE_PATTERNS = [
43
- /\bno match\b/i,
44
- /\bnot found\b/i,
45
- /\bno entries found\b/i,
46
- /\bno data found\b/i,
47
- /\bavailable for registration\b/i,
48
- /\bdomain\s+available\b/i,
49
- /\bdomain status[:\s]+available\b/i,
50
- /\bobject 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
57
- ];
58
- function isWhoisAvailable(text) {
59
- if (!text) return false;
60
- return WHOIS_AVAILABLE_PATTERNS.some((re) => re.test(text));
61
- }
62
47
 
63
48
  //#endregion
64
49
  //#region src/lib/async.ts
@@ -839,6 +824,29 @@ function preferLatestIso(a, b) {
839
824
 
840
825
  //#endregion
841
826
  //#region src/whois/normalize.ts
827
+ const WHOIS_AVAILABLE_PATTERNS = [
828
+ /\bno match\b/i,
829
+ /\bnot found\b/i,
830
+ /\bno entries found\b/i,
831
+ /\bno data found\b/i,
832
+ /\bavailable for registration\b/i,
833
+ /\bdomain\s+available\b/i,
834
+ /\bdomain status[:\s]+available\b/i,
835
+ /\bobject does not exist\b/i,
836
+ /\bthe queried object does not exist\b/i,
837
+ /\bstatus:\s*free\b/i,
838
+ /\bstatus:\s*available\b/i,
839
+ /\bno object found\b/i,
840
+ /\bnicht gefunden\b/i,
841
+ /\bpending release\b/i
842
+ ];
843
+ /**
844
+ * Best-effort heuristic to determine if a WHOIS response indicates the domain is available.
845
+ */
846
+ function isAvailableByWhois(text) {
847
+ if (!text) return false;
848
+ return WHOIS_AVAILABLE_PATTERNS.some((re) => re.test(text));
849
+ }
842
850
  /**
843
851
  * Convert raw WHOIS text into our normalized DomainRecord.
844
852
  * Heuristics cover many gTLD and ccTLD formats; exact fields vary per registry.
@@ -937,7 +945,7 @@ function normalizeWhois(domain, tld, whoisText, whoisServer, includeRaw = false)
937
945
  return {
938
946
  domain,
939
947
  tld,
940
- isRegistered: !isWhoisAvailable(whoisText),
948
+ isRegistered: !isAvailableByWhois(whoisText),
941
949
  isIDN: /(^|\.)xn--/i.test(domain),
942
950
  unicodeName: void 0,
943
951
  punycodeName: void 0,
@@ -1066,8 +1074,8 @@ async function followWhoisReferrals(initialServer, domain, opts) {
1066
1074
  visited.add(normalized);
1067
1075
  try {
1068
1076
  const res = await whoisQuery(next, domain, opts);
1069
- const registeredBefore = !isWhoisAvailable(current.text);
1070
- const registeredAfter = !isWhoisAvailable(res.text);
1077
+ const registeredBefore = !isAvailableByWhois(current.text);
1078
+ const registeredAfter = !isAvailableByWhois(res.text);
1071
1079
  if (registeredBefore && !registeredAfter) break;
1072
1080
  current = res;
1073
1081
  } catch {
@@ -1099,8 +1107,8 @@ async function collectWhoisReferralChain(initialServer, domain, opts) {
1099
1107
  visited.add(normalized);
1100
1108
  try {
1101
1109
  const res = await whoisQuery(next, domain, opts);
1102
- const registeredBefore = !isWhoisAvailable(current.text);
1103
- const registeredAfter = !isWhoisAvailable(res.text);
1110
+ const registeredBefore = !isAvailableByWhois(current.text);
1111
+ const registeredAfter = !isAvailableByWhois(res.text);
1104
1112
  if (registeredBefore && !registeredAfter) break;
1105
1113
  results.push(res);
1106
1114
  current = res;
@@ -1121,7 +1129,7 @@ function normalize(server) {
1121
1129
  * High-level lookup that prefers RDAP and falls back to WHOIS.
1122
1130
  * Ensures a standardized DomainRecord, independent of the source.
1123
1131
  */
1124
- async function lookupDomain(domain, opts) {
1132
+ async function lookup(domain, opts) {
1125
1133
  try {
1126
1134
  if (!isLikelyDomain(domain)) return {
1127
1135
  ok: false,
@@ -1181,20 +1189,28 @@ async function lookupDomain(domain, opts) {
1181
1189
  };
1182
1190
  }
1183
1191
  }
1184
- /** Determine if a domain appears available (not registered).
1185
- * Performs a lookup and resolves to a boolean. Rejects on lookup error. */
1192
+ /**
1193
+ * Determine if a domain appears available (not registered).
1194
+ * Performs a lookup and resolves to a boolean. Rejects on lookup error.
1195
+ */
1186
1196
  async function isAvailable(domain, opts) {
1187
- const res = await lookupDomain(domain, opts);
1197
+ const res = await lookup(domain, opts);
1188
1198
  if (!res.ok || !res.record) throw new Error(res.error || "Lookup failed");
1189
1199
  return res.record.isRegistered === false;
1190
1200
  }
1191
- /** Determine if a domain appears registered.
1192
- * Performs a lookup and resolves to a boolean. Rejects on lookup error. */
1201
+ /**
1202
+ * Determine if a domain appears registered.
1203
+ * Performs a lookup and resolves to a boolean. Rejects on lookup error.
1204
+ */
1193
1205
  async function isRegistered(domain, opts) {
1194
- const res = await lookupDomain(domain, opts);
1206
+ const res = await lookup(domain, opts);
1195
1207
  if (!res.ok || !res.record) throw new Error(res.error || "Lookup failed");
1196
1208
  return res.record.isRegistered === true;
1197
1209
  }
1210
+ /**
1211
+ * @deprecated Use `lookup` instead.
1212
+ */
1213
+ const lookupDomain = lookup;
1198
1214
 
1199
1215
  //#endregion
1200
- export { getDomainParts, getDomainTld, isAvailable, isLikelyDomain, isRegistered, lookupDomain, toRegistrableDomain };
1216
+ export { getDomainParts, getDomainTld, isAvailable, isLikelyDomain, isRegistered, lookup, lookupDomain, toRegistrableDomain };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rdapper",
3
- "version": "0.9.1",
3
+ "version": "0.10.0",
4
4
  "license": "MIT",
5
5
  "description": "🎩 RDAP/WHOIS fetcher, parser, and normalizer for Node",
6
6
  "repository": {