diff --git a/README.md b/README.md index 58f2d36..a8bb48f 100644 --- a/README.md +++ b/README.md @@ -23,23 +23,14 @@ npm install rdapper ## Quick Start ```ts -import { lookupDomain } from "rdapper"; +import { lookup } from "rdapper"; -const { ok, record, error } = await lookupDomain("example.com"); +const { ok, record, error } = await lookup("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 @@ -50,11 +41,25 @@ toRegistrableDomain("spark-public.s3.amazonaws.com"); // => "amazonaws.com" (I toRegistrableDomain("192.168.0.1"); // => null ``` +Convenience helpers to quickly check availability: + +```ts +import { isRegistered, isAvailable } from "rdapper"; + +await isRegistered("example.com"); // => true +await isRegistered("likely-unregistered-thing-320485230458.com"); // => false +await isAvailable("example.com"); // => false +await isAvailable("likely-unregistered-thing-320485230458.com"); // => true +``` + ## API -- `lookupDomain(domain, options?) => Promise` +- `lookup(domain, options?) => Promise` - 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 }`. +- `toRegistrableDomain(input, options?) => string | null` + - Normalizes a domain or URL to its registrable domain (eTLD+1). + - 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`). - `isRegistered(domain, options?) => Promise` - `isAvailable(domain, options?) => Promise` @@ -74,9 +79,9 @@ WHOIS requires a raw TCP connection over port 43 via `node:net`, which is not av - Prefer RDAP only on edge: ```ts -import { lookupDomain } from "rdapper"; +import { lookup } from "rdapper"; -const res = await lookupDomain("example.com", { rdapOnly: true }); +const res = await lookup("example.com", { rdapOnly: true }); ``` - 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 }`. diff --git a/bin/cli.js b/bin/cli.js index 875163b..010c39a 100755 --- a/bin/cli.js +++ b/bin/cli.js @@ -6,14 +6,14 @@ // echo "example.com" | npx rdapper import { createInterface } from "node:readline"; -import { lookupDomain } from "../dist/index.js"; +import { lookup } from "../dist/index.js"; async function main() { if (process.argv.length > 2) { // URL(s) specified in the command arguments console.log( JSON.stringify( - await lookupDomain(process.argv[process.argv.length - 1]), + await lookup(process.argv[process.argv.length - 1]), null, 2, ), @@ -24,7 +24,7 @@ async function main() { input: process.stdin, }); rlInterface.on("line", async (line) => { - console.log(JSON.stringify(await lookupDomain(line), null, 2)); + console.log(JSON.stringify(await lookup(line), null, 2)); }); } } diff --git a/src/index.ts b/src/index.ts index 444bd05..48f0468 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,7 +20,7 @@ import { * High-level lookup that prefers RDAP and falls back to WHOIS. * Ensures a standardized DomainRecord, independent of the source. */ -export async function lookupDomain( +export async function lookup( domain: string, opts?: LookupOptions, ): Promise { @@ -118,28 +118,37 @@ export async function lookupDomain( } } -/** Determine if a domain appears available (not registered). - * Performs a lookup and resolves to a boolean. Rejects on lookup error. */ +/** + * Determine if a domain appears available (not registered). + * Performs a lookup and resolves to a boolean. Rejects on lookup error. + */ export async function isAvailable( domain: string, opts?: LookupOptions, ): Promise { - const res = await lookupDomain(domain, opts); + const res = await lookup(domain, opts); if (!res.ok || !res.record) throw new Error(res.error || "Lookup failed"); return res.record.isRegistered === false; } -/** Determine if a domain appears registered. - * Performs a lookup and resolves to a boolean. Rejects on lookup error. */ +/** + * Determine if a domain appears registered. + * Performs a lookup and resolves to a boolean. Rejects on lookup error. + */ export async function isRegistered( domain: string, opts?: LookupOptions, ): Promise { - const res = await lookupDomain(domain, opts); + const res = await lookup(domain, opts); if (!res.ok || !res.record) throw new Error(res.error || "Lookup failed"); return res.record.isRegistered === true; } +/** + * @deprecated Use `lookup` instead. + */ +export const lookupDomain = lookup; + export { getDomainParts, getDomainTld, diff --git a/src/lib/domain.ts b/src/lib/domain.ts index 8a6c83c..92956f7 100644 --- a/src/lib/domain.ts +++ b/src/lib/domain.ts @@ -3,7 +3,7 @@ import { parse } from "tldts"; type ParseOptions = Parameters[1]; /** - * Parse a domain into its parts. Accepts options which are passed to tldts.parse(). + * Parse a domain into its parts. Passes options to `tldts.parse()`. * @see https://github.com/remusao/tldts/blob/master/packages/tldts-core/src/options.ts */ export function getDomainParts( @@ -13,7 +13,10 @@ export function getDomainParts( return parse(domain, { ...opts }); } -/** Get the TLD (ICANN-only public suffix) of a domain. */ +/** + * Get the TLD (ICANN-only public suffix) of a domain. Passes options to `tldts.parse()`. + * @see https://github.com/remusao/tldts/blob/master/packages/tldts-core/src/options.ts + */ export function getDomainTld( domain: string, opts?: ParseOptions, @@ -47,7 +50,9 @@ export function punyToUnicode(domain: string): string { /** * Normalize arbitrary input (domain or URL) to its registrable domain (eTLD+1). - * Returns null when the input is not a valid ICANN domain (e.g., invalid TLD, IPs). + * Passes options to `tldts.parse()`. + * Returns null when the input is not a valid ICANN domain (e.g., invalid TLD, IPs) + * @see https://github.com/remusao/tldts/blob/master/packages/tldts-core/src/options.ts */ export function toRegistrableDomain( input: string, @@ -69,27 +74,3 @@ export function toRegistrableDomain( if (domain === "") return null; return domain.toLowerCase(); } - -// Common WHOIS availability phrases seen across registries/registrars -const WHOIS_AVAILABLE_PATTERNS: RegExp[] = [ - /\bno match\b/i, - /\bnot found\b/i, - /\bno entries found\b/i, - /\bno data found\b/i, - /\bavailable for registration\b/i, - /\bdomain\s+available\b/i, - /\bdomain status[:\s]+available\b/i, - /\bobject does not exist\b/i, - /\bthe queried object does not exist\b/i, - // Common variants across ccTLDs/registrars - /\bstatus:\s*free\b/i, - /\bstatus:\s*available\b/i, - /\bno object found\b/i, - /\bnicht gefunden\b/i, - /\bpending release\b/i, // often signals not registered/being deleted -]; - -export function isWhoisAvailable(text: string | undefined): boolean { - if (!text) return false; - return WHOIS_AVAILABLE_PATTERNS.some((re) => re.test(text)); -} diff --git a/src/whois/normalize.ts b/src/whois/normalize.ts index d68ede6..cb4c388 100644 --- a/src/whois/normalize.ts +++ b/src/whois/normalize.ts @@ -1,5 +1,4 @@ import { toISO } from "../lib/dates"; -import { isWhoisAvailable } from "../lib/domain"; import { isPrivacyName } from "../lib/privacy"; import { parseKeyValueLines, uniq } from "../lib/text"; import type { @@ -9,6 +8,33 @@ import type { RegistrarInfo, } from "../types"; +// Common WHOIS availability phrases seen across registries/registrars +const WHOIS_AVAILABLE_PATTERNS: RegExp[] = [ + /\bno match\b/i, + /\bnot found\b/i, + /\bno entries found\b/i, + /\bno data found\b/i, + /\bavailable for registration\b/i, + /\bdomain\s+available\b/i, + /\bdomain status[:\s]+available\b/i, + /\bobject does not exist\b/i, + /\bthe queried object does not exist\b/i, + // Common variants across ccTLDs/registrars + /\bstatus:\s*free\b/i, + /\bstatus:\s*available\b/i, + /\bno object found\b/i, + /\bnicht gefunden\b/i, + /\bpending release\b/i, // often signals not registered/being deleted +]; + +/** + * Best-effort heuristic to determine if a WHOIS response indicates the domain is available. + */ +export function isAvailableByWhois(text: string | undefined): boolean { + if (!text) return false; + return WHOIS_AVAILABLE_PATTERNS.some((re) => re.test(text)); +} + /** * Convert raw WHOIS text into our normalized DomainRecord. * Heuristics cover many gTLD and ccTLD formats; exact fields vary per registry. @@ -163,7 +189,7 @@ export function normalizeWhois( const record: DomainRecord = { domain, tld, - isRegistered: !isWhoisAvailable(whoisText), + isRegistered: !isAvailableByWhois(whoisText), isIDN: /(^|\.)xn--/i.test(domain), unicodeName: undefined, punycodeName: undefined, diff --git a/src/whois/referral.ts b/src/whois/referral.ts index 4d9ac17..a8c736d 100644 --- a/src/whois/referral.ts +++ b/src/whois/referral.ts @@ -1,8 +1,8 @@ -import { isWhoisAvailable } from "../lib/domain"; import type { LookupOptions } from "../types"; import type { WhoisQueryResult } from "./client"; import { whoisQuery } from "./client"; import { extractWhoisReferral } from "./discovery"; +import { isAvailableByWhois } from "./normalize"; /** * Follow registrar WHOIS referrals up to a configured hop limit. @@ -30,8 +30,8 @@ export async function followWhoisReferrals( try { const res = await whoisQuery(next, domain, opts); // Prefer authoritative TLD response when registrar contradicts availability - const registeredBefore = !isWhoisAvailable(current.text); - const registeredAfter = !isWhoisAvailable(res.text); + const registeredBefore = !isAvailableByWhois(current.text); + const registeredAfter = !isAvailableByWhois(res.text); if (registeredBefore && !registeredAfter) { // Registrar claims availability but TLD shows registered: keep TLD break; @@ -74,8 +74,8 @@ export async function collectWhoisReferralChain( try { const res = await whoisQuery(next, domain, opts); // If registrar claims availability while TLD indicated registered, stop. - const registeredBefore = !isWhoisAvailable(current.text); - const registeredAfter = !isWhoisAvailable(res.text); + const registeredBefore = !isAvailableByWhois(current.text); + const registeredAfter = !isAvailableByWhois(res.text); if (registeredBefore && !registeredAfter) { // Do not adopt or append contradictory registrar; keep authoritative TLD only. break;