1
mirror of https://github.com/jakejarvis/rdapper.git synced 2025-10-18 14:24:29 -04:00
Files
rdapper/README.md

8.7 KiB
Raw Permalink Blame History

🎩 rdapper

RDAPfirst 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; ESMonly; 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 domains 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 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.

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 IANAs 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 (besteffort 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 reexports API and types
  • bin/cli.js simple CLI for quick checks

Caveats

  • WHOIS text formats vary significantly across registries/registrars; normalization is besteffort.
  • 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 ICANNonly 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 ICANNonly by default). See: tldts migration notes.

License

MIT