fix: don't mark registered WHOIS records available over a remark

Availability phrases were matched anywhere in the reply, so a registered record
whose remarks or footer used one was reported available: nic.ir ("only
available for registration under certain conditions"), or any record with
"Registrant Fax: not found" or "This WHOIS service is free of charge". The
referral chain used the same check and could drop a registrar's reply.

Phrases now count only when the reply has no registration data (dates,
registrar, nameservers); explicit statuses such as "status: free" or .br's
"release process: waiting" still win. Checked live: unregistered names under
.com, .ir, .it, .jp, .fi, .cz, .nl, and .br are still reported available.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jakeandClaude Opus 5.5 committed 2026-10-03 15:15:52 -04:00
1 parent 58f5ef2277
commit bca20bc5f0
2 files changed
+52 -7

No files matched your search

+28
View File
@@ -457,3 +457,31 @@ nserver: ho1.ns.hostmaster.ua
const rec = normalizeWhois("hostmaster.ua", "ua", text, "whois.ua");
expect(rec.nameservers?.map((n) => n.host)).toEqual(["bg.ns.ua", "ho1.ns.hostmaster.ua"]);
});
test("WHOIS availability phrases in remarks of a real record don't mark it available", () => {
// Excerpt of whois.nic.ir for nic.ir: the remarks mention "available for registration"
const ir = `
domain: nic.ir
holder-c: ir00-irnic
nserver: ns1.nic.ir
nserver: ns2.nic.ir
source: IRNIC
remarks: This domain is only available for registration under certain conditions
`;
expect(normalizeWhois("nic.ir", "ir", ir, "whois.nic.ir").isRegistered).toBe(true);
const fax =
"Domain Name: example.com\nCreation Date: 2020-01-01T00:00:00Z\nRegistrant Fax: not found\n";
expect(normalizeWhois("example.com", "com", fax, undefined).isRegistered).toBe(true);
expect(isAvailableByWhois(ir)).toBe(false);
});
test("WHOIS availability phrases still mark a reply with no record data available", () => {
const text =
'No match for "EXAMPLE-AVAILABLE.COM".\n>>> Last update of whois database: 2026-10-03T18:00:00Z <<<\n';
expect(normalizeWhois("example-available.com", "com", text, undefined).isRegistered).toBe(false);
// A release status wins even though the old record data is still shown
const releasing =
"domain: example.com.br\ncreated: 20100101\nnserver: ns1.example.com.br\nstatus: release process: waiting\n";
expect(normalizeWhois("example.com.br", "com.br", releasing, undefined).isRegistered).toBe(false);
});
+24 -7
View File
@@ -5,8 +5,11 @@ import { isEppStatus, normalizeEppStatus } from "../lib/status";
import { parseKeyValueBlocks, parseKeyValueLines } from "../lib/text";
import type { Contact, DomainRecord, Nameserver, RegistrarInfo } from "../types";
// Common WHOIS availability phrases seen across registries/registrars
const WHOIS_AVAILABLE_PATTERNS: RegExp[] = [
// Phrases registries use to say a name is not registered. They also turn up in the remarks and
// footers of real records ("only available for registration under certain conditions" on .ir,
// "Registrant Fax: not found", "This WHOIS service is free of charge"), so they count only when
// the reply carries no registration data.
const WHOIS_AVAILABLE_PHRASES: RegExp[] = [
/\bno match\b/i,
/\bnot found\b/i,
/\bno entries found\b/i,
@@ -16,7 +19,6 @@ const WHOIS_AVAILABLE_PATTERNS: RegExp[] = [
/\bno data was 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,
/\bqueried object does not exist\b/i,
@@ -26,12 +28,18 @@ const WHOIS_AVAILABLE_PATTERNS: RegExp[] = [
/\bunassignable\b/i,
/\bis free\b/i,
// Common variants across ccTLDs/registrars
/\bstatus:\s*free\b/i,
/\bstatus:\s*available\b/i,
/\bno object found\b/i,
/\bobject_not_found\b/i,
/\bno se encuentra registrado\b/i, // Spanish: "not found registered"
/\bnicht gefunden\b/i, // German: "not found"
];
// Statuses that mark a name unregistered even though the record still carries data, as a name
// being released keeps its old dates and nameservers
const WHOIS_AVAILABLE_STATUSES: RegExp[] = [
/\bdomain status[:\s]+available\b/i,
/\bstatus:\s*free\b/i,
/\bstatus:\s*available\b/i,
/\bpending release\b/i, // often signals not registered/being deleted
/\brelease process:\s*waiting\b/i, // .br: expired, awaiting release
];
@@ -41,7 +49,13 @@ const WHOIS_AVAILABLE_PATTERNS: RegExp[] = [
*/
export function isAvailableByWhois(text: string | undefined): boolean {
if (!text) return false;
return WHOIS_AVAILABLE_PATTERNS.some((re) => re.test(text));
return !normalizeWhois("", "", text, undefined).isRegistered;
}
/** Whether a reply says the name is unregistered, given whether it carries registration data. */
function saysAvailable(text: string, hasRegistrationData: boolean): boolean {
if (WHOIS_AVAILABLE_STATUSES.some((re) => re.test(text))) return true;
return !hasRegistrationData && WHOIS_AVAILABLE_PHRASES.some((re) => re.test(text));
}
/**
@@ -251,7 +265,10 @@ export function normalizeWhois(
const record: DomainRecord = {
domain,
tld,
isRegistered: !isAvailableByWhois(whoisText),
isRegistered: !saysAvailable(
whoisText,
!!(creationDate || expirationDate || registrar || nameservers?.length),
),
isIDN: /(^|\.)xn--/i.test(domain),
unicodeName: undefined,
punycodeName: undefined,