Implement WHOIS referral chain collection and merging logic (fixes #11)

- Introduced `collectWhoisReferralChain` to gather WHOIS responses while avoiding contradictory data from registrars.
- Updated `lookupDomain` to utilize the new chain collection method, ensuring TLD responses are prioritized.
- Added `mergeWhoisRecords` function to consolidate WHOIS data from multiple sources.
- Enhanced tests for referral handling and merging behavior, ensuring accurate data retention across scenarios.
This commit is contained in:
2025-10-20 14:08:26 -04:00
parent b00d0bbb0a
commit 2a1b7529cc
8 changed files with 250 additions and 20 deletions
+32 -10
View File
@@ -9,8 +9,12 @@ import {
ianaWhoisServerForTld,
parseIanaRegistrationInfoUrl,
} from "./whois/discovery";
import { mergeWhoisRecords } from "./whois/merge";
import { normalizeWhois } from "./whois/normalize";
import { followWhoisReferrals } from "./whois/referral";
import {
collectWhoisReferralChain,
followWhoisReferrals,
} from "./whois/referral";
/**
* High-level lookup that prefers RDAP and falls back to WHOIS.
@@ -32,7 +36,13 @@ export async function lookupDomain(
// If WHOIS-only, skip RDAP path
if (!opts?.whoisOnly) {
const bases = await getRdapBaseUrlsForTld(tld, opts);
let bases = await getRdapBaseUrlsForTld(tld, opts);
// Some ccTLD registries publish RDAP only at the registry TLD (e.g., br),
// while the public suffix can be multi-label (e.g., com.br). Fallback to last label.
if (bases.length === 0 && tld.includes(".")) {
const registryTld = tld.split(".").pop() ?? tld;
bases = await getRdapBaseUrlsForTld(registryTld, opts);
}
const tried: string[] = [];
for (const base of bases) {
tried.push(base);
@@ -80,16 +90,28 @@ export async function lookupDomain(
}
// Query the TLD server first; optionally follow registrar referrals (multi-hop)
const res = await followWhoisReferrals(whoisServer, domain, opts);
// Collect the chain and coalesce so we don't lose details when a registrar returns minimal/empty data.
const chain = await collectWhoisReferralChain(whoisServer, domain, opts);
if (chain.length === 0) {
// Fallback to previous behavior as a safety net
const res = await followWhoisReferrals(whoisServer, domain, opts);
const record: DomainRecord = normalizeWhois(
domain,
tld,
res.text,
res.serverQueried,
!!opts?.includeRaw,
);
return { ok: true, record };
}
const record: DomainRecord = normalizeWhois(
domain,
tld,
res.text,
res.serverQueried,
!!opts?.includeRaw,
// Normalize all WHOIS texts in the chain and merge conservatively
const normalizedRecords = chain.map((r) =>
normalizeWhois(domain, tld, r.text, r.serverQueried, !!opts?.includeRaw),
);
return { ok: true, record };
const [first, ...rest] = normalizedRecords;
const mergedRecord = rest.length ? mergeWhoisRecords(first, rest) : first;
return { ok: true, record: mergedRecord };
} catch (err: unknown) {
const message = err instanceof Error ? err.message : String(err);
return { ok: false, error: message };