mirror of
https://github.com/jakejarvis/rdapper.git
synced 2026-09-23 00:15:31 -04:00
222 lines
8.7 KiB
Markdown
222 lines
8.7 KiB
Markdown
# 🎩 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!](https://hoot.sh)
|
||
|
||
## Install
|
||
|
||
```bash
|
||
npm install rdapper
|
||
```
|
||
|
||
## Quick Start
|
||
|
||
```ts
|
||
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:
|
||
|
||
```ts
|
||
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):
|
||
|
||
```ts
|
||
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:
|
||
|
||
```bash
|
||
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:
|
||
|
||
```ts
|
||
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 set `rdapOnly: true`.
|
||
|
||
### Options
|
||
|
||
- `timeoutMs?: number` – Total timeout budget per network operation (default `15000`).
|
||
- `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 (default `true`).
|
||
- `maxWhoisReferralHops?: number` – Maximum registrar WHOIS referral hops to follow (default `2`).
|
||
- `rdapFollowLinks?: boolean` – Follow related/entity RDAP links to enrich data (default `true`).
|
||
- `maxRdapLinkHops?: number` – Maximum RDAP related link hops to follow (default `2`).
|
||
- `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 omit `whois://`).
|
||
- `includeRaw?: boolean` – Include `rawRdap`/`rawWhois` in the returned record (default `false`).
|
||
- `signal?: AbortSignal` – Optional cancellation signal.
|
||
|
||
### `DomainRecord` schema
|
||
|
||
The exact presence of fields depends on registry/registrar data and whether RDAP or WHOIS was used.
|
||
|
||
```ts
|
||
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
|
||
|
||
```json
|
||
{
|
||
"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).
|
||
|
||
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 normalization
|
||
- `src/whois/` – WHOIS TCP client, discovery/referral, normalization, exceptions
|
||
- `src/lib/` – utilities for dates, text parsing, domain processing, async
|
||
- `src/types.ts` – public types; `src/index.ts` re‑exports API and types
|
||
- `bin/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 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).
|
||
|
||
## License
|
||
|
||
[MIT](LICENSE)
|