rdapper 0.9.1 → 0.10.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/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.
@@ -853,8 +861,10 @@ function normalizeWhois(domain, tld, whoisText, whoisServer, includeRaw = false)
853
861
  "domain create date",
854
862
  "created",
855
863
  "registered",
864
+ "domain name commencement date",
856
865
  "registration time",
857
- "domain record activated"
866
+ "domain record activated",
867
+ "assigned"
858
868
  ]);
859
869
  const updatedDate = anyValue(map, [
860
870
  "updated date",
@@ -879,7 +889,9 @@ function normalizeWhois(domain, tld, whoisText, whoisServer, includeRaw = false)
879
889
  "paid-till",
880
890
  "expires on",
881
891
  "expires",
882
- "renewal date"
892
+ "expire",
893
+ "renewal date",
894
+ "validity"
883
895
  ]);
884
896
  const registrar = (() => {
885
897
  const name = anyValue(map, [
@@ -937,7 +949,7 @@ function normalizeWhois(domain, tld, whoisText, whoisServer, includeRaw = false)
937
949
  return {
938
950
  domain,
939
951
  tld,
940
- isRegistered: !isWhoisAvailable(whoisText),
952
+ isRegistered: !isAvailableByWhois(whoisText),
941
953
  isIDN: /(^|\.)xn--/i.test(domain),
942
954
  unicodeName: void 0,
943
955
  punycodeName: void 0,
@@ -1066,8 +1078,8 @@ async function followWhoisReferrals(initialServer, domain, opts) {
1066
1078
  visited.add(normalized);
1067
1079
  try {
1068
1080
  const res = await whoisQuery(next, domain, opts);
1069
- const registeredBefore = !isWhoisAvailable(current.text);
1070
- const registeredAfter = !isWhoisAvailable(res.text);
1081
+ const registeredBefore = !isAvailableByWhois(current.text);
1082
+ const registeredAfter = !isAvailableByWhois(res.text);
1071
1083
  if (registeredBefore && !registeredAfter) break;
1072
1084
  current = res;
1073
1085
  } catch {
@@ -1099,8 +1111,8 @@ async function collectWhoisReferralChain(initialServer, domain, opts) {
1099
1111
  visited.add(normalized);
1100
1112
  try {
1101
1113
  const res = await whoisQuery(next, domain, opts);
1102
- const registeredBefore = !isWhoisAvailable(current.text);
1103
- const registeredAfter = !isWhoisAvailable(res.text);
1114
+ const registeredBefore = !isAvailableByWhois(current.text);
1115
+ const registeredAfter = !isAvailableByWhois(res.text);
1104
1116
  if (registeredBefore && !registeredAfter) break;
1105
1117
  results.push(res);
1106
1118
  current = res;
@@ -1121,7 +1133,7 @@ function normalize(server) {
1121
1133
  * High-level lookup that prefers RDAP and falls back to WHOIS.
1122
1134
  * Ensures a standardized DomainRecord, independent of the source.
1123
1135
  */
1124
- async function lookupDomain(domain, opts) {
1136
+ async function lookup(domain, opts) {
1125
1137
  try {
1126
1138
  if (!isLikelyDomain(domain)) return {
1127
1139
  ok: false,
@@ -1181,20 +1193,28 @@ async function lookupDomain(domain, opts) {
1181
1193
  };
1182
1194
  }
1183
1195
  }
1184
- /** Determine if a domain appears available (not registered).
1185
- * Performs a lookup and resolves to a boolean. Rejects on lookup error. */
1196
+ /**
1197
+ * Determine if a domain appears available (not registered).
1198
+ * Performs a lookup and resolves to a boolean. Rejects on lookup error.
1199
+ */
1186
1200
  async function isAvailable(domain, opts) {
1187
- const res = await lookupDomain(domain, opts);
1201
+ const res = await lookup(domain, opts);
1188
1202
  if (!res.ok || !res.record) throw new Error(res.error || "Lookup failed");
1189
1203
  return res.record.isRegistered === false;
1190
1204
  }
1191
- /** Determine if a domain appears registered.
1192
- * Performs a lookup and resolves to a boolean. Rejects on lookup error. */
1205
+ /**
1206
+ * Determine if a domain appears registered.
1207
+ * Performs a lookup and resolves to a boolean. Rejects on lookup error.
1208
+ */
1193
1209
  async function isRegistered(domain, opts) {
1194
- const res = await lookupDomain(domain, opts);
1210
+ const res = await lookup(domain, opts);
1195
1211
  if (!res.ok || !res.record) throw new Error(res.error || "Lookup failed");
1196
1212
  return res.record.isRegistered === true;
1197
1213
  }
1214
+ /**
1215
+ * @deprecated Use `lookup` instead.
1216
+ */
1217
+ const lookupDomain = lookup;
1198
1218
 
1199
1219
  //#endregion
1200
- export { getDomainParts, getDomainTld, isAvailable, isLikelyDomain, isRegistered, lookupDomain, toRegistrableDomain };
1220
+ 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.1",
4
4
  "license": "MIT",
5
5
  "description": "🎩 RDAP/WHOIS fetcher, parser, and normalizer for Node",
6
6
  "repository": {