mirror of
https://github.com/jakejarvis/rdapper.git
synced 2025-10-18 14:24:29 -04:00
8.7 KiB
8.7 KiB
🎩 rdapper
RDAP‑first domain registration lookups with WHOIS fallback. Produces a single, normalized record shape regardless of source.
- RDAP discovery via IANA bootstrap (
https://data.iana.org/rdap/dns.json
) - WHOIS TCP 43 client with TLD discovery, registrar referral follow, and curated exceptions
- Normalized output: registrar, contacts, nameservers, statuses, dates, DNSSEC, privacy flag, source metadata
- TypeScript types included; ESM‑only; no external HTTP client (uses global
fetch
)
🦉 See it in action on hoot.sh!
Install
npm install rdapper
Quick Start
import { lookupDomain } from "rdapper";
const { ok, record, error } = await lookupDomain("example.com");
if (!ok) throw new Error(error);
console.log(record); // normalized DomainRecord
Also available:
import { isRegistered, isAvailable } from "rdapper";
await isRegistered("example.com"); // => true
await isAvailable("likely-unregistered-thing-320485230458.com"); // => false
Normalize arbitrary input (domain or URL) to its registrable domain (eTLD+1):
import { toRegistrableDomain } from "rdapper";
toRegistrableDomain("https://sub.example.co.uk/page"); // => "example.co.uk"
toRegistrableDomain("spark-public.s3.amazonaws.com"); // => "amazonaws.com" (ICANN-only default)
toRegistrableDomain("192.168.0.1"); // => null
API
lookupDomain(domain, options?) => Promise<LookupResult>
- Tries RDAP first if supported by the domain’s TLD; if unavailable or fails, falls back to WHOIS (unless toggled off).
- Result is
{ ok: boolean, record?: DomainRecord, error?: string }
.
isRegistered(domain, options?) => Promise<boolean>
isAvailable(domain, options?) => Promise<boolean>
CLI
For quick checks, a minimal CLI is included:
npx rdapper example.com
echo "example.com" | npx rdapper
Edge runtimes (e.g., Vercel Edge)
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:
- Prefer RDAP only:
import { lookupDomain } from "rdapper";
const res = await lookupDomain("example.com", { rdapOnly: true });
- 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 setrdapOnly: true
.
Options
timeoutMs?: number
– Total timeout budget per network operation (default15000
).rdapOnly?: boolean
– Only attempt RDAP; do not fall back to WHOIS.whoisOnly?: boolean
– Skip RDAP and query WHOIS directly.followWhoisReferral?: boolean
– Follow registrar referral from the TLD WHOIS (defaulttrue
).maxWhoisReferralHops?: number
– Maximum registrar WHOIS referral hops to follow (default2
).rdapFollowLinks?: boolean
– Follow related/entity RDAP links to enrich data (defaulttrue
).maxRdapLinkHops?: number
– Maximum RDAP related link hops to follow (default2
).rdapLinkRels?: string[]
– RDAP link rel values to consider (default["related","entity","registrar","alternate"]
).customBootstrapUrl?: string
– Override RDAP bootstrap URL.whoisHints?: Record<string, string>
– Override/add authoritative WHOIS per TLD (keys are lowercase TLDs, values may include or omitwhois://
).includeRaw?: boolean
– IncluderawRdap
/rawWhois
in the returned record (defaultfalse
).signal?: AbortSignal
– Optional cancellation signal.
DomainRecord
schema
The exact presence of fields depends on registry/registrar data and whether RDAP or WHOIS was used.
interface DomainRecord {
domain: string; // normalized name (unicode when available)
tld: string; // terminal TLD label (e.g., "com")
isRegistered: boolean; // availability heuristic (WHOIS) or true (RDAP)
isIDN?: boolean; // uses punycode labels (xn--)
unicodeName?: string; // RDAP unicodeName when provided
punycodeName?: string; // RDAP ldhName when provided
registry?: string; // registry operator (rarely available)
registrar?: {
name?: string;
ianaId?: string;
url?: string;
email?: string;
phone?: string;
};
reseller?: string;
statuses?: Array<{
status: string;
description?: string;
raw?: string;
}>;
creationDate?: string; // ISO 8601 (UTC)
updatedDate?: string; // ISO 8601 (UTC)
expirationDate?: string; // ISO 8601 (UTC)
deletionDate?: string; // ISO 8601 (UTC)
transferLock?: boolean; // derived from EPP statuses
dnssec?: {
enabled: boolean;
dsRecords?: Array<{
keyTag?: number;
algorithm?: number;
digestType?: number;
digest?: string;
}>;
};
nameservers?: Array<{
host: string;
ipv4?: string[];
ipv6?: string[];
}>;
contacts?: Array<{
type: "registrant" | "admin" | "tech" | "billing" | "abuse" | "registrar" | "reseller" | "unknown";
name?: string;
organization?: string;
email?: string | string[];
phone?: string | string[];
fax?: string | string[];
street?: string[];
city?: string;
state?: string;
postalCode?: string;
country?: string;
countryCode?: string;
}>;
privacyEnabled?: boolean; // registrant appears privacy-redacted based on keyword heuristics
whoisServer?: string; // authoritative WHOIS queried (if any)
rdapServers?: string[]; // RDAP base URLs tried
rawRdap?: unknown; // raw RDAP JSON (only when options.includeRaw)
rawWhois?: string; // raw WHOIS text (only when options.includeRaw)
source: "rdap" | "whois"; // which path produced data
warnings?: string[];
}
Example output
{
"domain": "example.com",
"tld": "com",
"isRegistered": true,
"registrar": { "name": "Internet Assigned Numbers Authority", "ianaId": "376" },
"statuses": [{ "status": "clientTransferProhibited" }],
"nameservers": [{ "host": "a.iana-servers.net" }, { "host": "b.iana-servers.net" }],
"dnssec": { "enabled": true },
"source": "rdap"
}
How it works
- RDAP
- Discovers base URLs for the TLD via IANA’s RDAP bootstrap JSON.
- Tries each base until one responds successfully; parses standard RDAP domain JSON.
- Optionally follows related/entity links to registrar RDAP resources and merges results (bounded by hop limits).
- Normalizes registrar (from
entities
), contacts (vCard), nameservers (ipAddresses
), events (created/changed/expiration), statuses, and DNSSEC (secureDNS
).
- WHOIS
- Discovers the authoritative TLD WHOIS via
whois.iana.org
(TCP 43), with curated exceptions for tricky zones and public SLDs. - Queries the TLD WHOIS and follows registrar referrals recursively up to
maxWhoisReferralHops
(unless disabled). - Normalizes common key/value variants across gTLD/ccTLD formats (dates, statuses, nameservers, contacts). Availability is inferred from common phrases (best‑effort heuristic).
- Discovers the authoritative TLD WHOIS via
Timeouts are enforced per request using a simple race against timeoutMs
(default 15s). All network I/O is performed with global fetch
(RDAP) and a raw TCP socket (WHOIS).
Development
- Build:
npm run build
- Test:
npm test
(Vitest)- By default, tests are offline/deterministic.
- Watch mode:
npm run test:watch
- Coverage:
npm run test:coverage
- Smoke tests that hit the network are gated by
SMOKE=1
, e.g.SMOKE=1 npm run test:smoke
.
- Lint/format:
npm run lint
(Biome)
Project layout:
src/rdap/
– RDAP bootstrap, client, and normalizationsrc/whois/
– WHOIS TCP client, discovery/referral, normalization, exceptionssrc/lib/
– utilities for dates, text parsing, domain processing, asyncsrc/types.ts
– public types;src/index.ts
re‑exports API and typesbin/cli.js
– simple CLI for quick checks
Caveats
- WHOIS text formats vary significantly across registries/registrars; normalization is best‑effort.
- Availability detection relies on common WHOIS phrases and is not authoritative.
- Some TLDs provide no RDAP service;
rdapOnly: true
will fail for them. - Registries may throttle or block WHOIS; respect rate limits and usage policies.
- Field presence depends on source and privacy policies (e.g., redaction/withholding).
- Public suffix detection uses
tldts
with ICANN‑only defaults (Private section is ignored). If you need behavior closer topsl
that considers private suffixes, see theallowPrivateDomains
option in thetldts
docs (rdapper currently sticks to ICANN‑only by default). See: tldts migration notes.