mirror of
https://github.com/jakejarvis/rdapper.git
synced 2026-10-26 08:26:22 -04:00
fix: take WHOIS statuses from the domain's block, whole
Statuses came from whichever key matched first across the whole reply, and only a value's first word was kept: - .ua listed the registrant contact's statuses, and .mx the contacts' "State: Nuevo Leon" as statuses ["Nuevo", ...] - .fr showed "ok" from "status: ACTIVE" and missed its "eppstatus" EPP codes - .be showed "NOT" and missed the EPP codes under its "Flags:" header - comma lists kept one code with a trailing comma (.sk "serverRenewProhibited,"), and phrases lost all but a word (.il "Transfer", .rs "Registry", .cz "Sponsoring") Read statuses from the first block that has any, preferring "eppstatus" and taking "state" only beside the domain line; add .be's EPP flags; split comma lists; keep a registry's own phrase whole; map FRED's "Sponsoring registrar change forbidden" (confirmed against .cz RDAP) and its siblings to EPP codes; and derive transferLock from the EPP code as well as the raw text. .fr, .be, and .cz now report the transfer lock RDAP does. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2 files changed
+101
-22
No files matched your search
@@ -485,3 +485,43 @@ test("WHOIS availability phrases still mark a reply with no record data availabl
|
||||
"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);
|
||||
});
|
||||
|
||||
test("WHOIS statuses come from the domain's block, whole and split on commas", () => {
|
||||
const status = (text: string) =>
|
||||
normalizeWhois("example.test", "test", text, undefined).statuses?.map((s) => s.status);
|
||||
// .ua: contact blocks repeat "status:"
|
||||
expect(status("domain: x.ua\nstatus: ok\n\ncontact: H1\nstatus: ok\nstatus: linked\n")).toEqual([
|
||||
"ok",
|
||||
]);
|
||||
// .fr: EPP codes under "eppstatus", contacts repeat it
|
||||
expect(
|
||||
status(
|
||||
"domain: x.fr\nstatus: ACTIVE\neppstatus: serverTransferProhibited\n\nnic-hdl: A1\neppstatus: active\n",
|
||||
),
|
||||
).toEqual(["serverTransferProhibited"]);
|
||||
// .mx: "State" is a contact's region
|
||||
expect(status("Domain Name: x.mx\n\nRegistrant:\n State: Nuevo Leon\n")).toBeUndefined();
|
||||
// .ru: "state" beside the domain line
|
||||
expect(status("domain: X.RU\nstate: REGISTERED, DELEGATED, VERIFIED\n")).toEqual([
|
||||
"REGISTERED",
|
||||
"DELEGATED",
|
||||
"VERIFIED",
|
||||
]);
|
||||
// .sk comma list, .il phrase, .cz FRED wording
|
||||
expect(
|
||||
status("Domain: x.sk\nDomain Status: serverRenewProhibited, serverUpdateProhibited\n"),
|
||||
).toEqual(["serverRenewProhibited", "serverUpdateProhibited"]);
|
||||
expect(status("domain: x.il\nstatus: Transfer Locked\n")).toEqual(["Transfer Locked"]);
|
||||
const cz = normalizeWhois(
|
||||
"x.cz",
|
||||
"cz",
|
||||
"domain: x.cz\nstatus: Sponsoring registrar change forbidden\n",
|
||||
undefined,
|
||||
);
|
||||
expect(cz.statuses?.map((s) => s.status)).toEqual(["serverTransferProhibited"]);
|
||||
expect(cz.transferLock).toBe(true);
|
||||
// .be: EPP statuses under a separate "Flags:" header
|
||||
expect(
|
||||
status("Domain: x.be\nStatus: NOT AVAILABLE\n\nFlags:\n\tclientTransferProhibited\n"),
|
||||
).toEqual(["NOT AVAILABLE", "clientTransferProhibited"]);
|
||||
});
|
||||
+61
-22
@@ -2,7 +2,7 @@ import { toISOFromTokens } from "../lib/dates";
|
||||
import { finalizeContact, isPrivacyContact } from "../lib/contacts";
|
||||
import { mergeNameservers } from "../lib/nameservers";
|
||||
import { isEppStatus, normalizeEppStatus } from "../lib/status";
|
||||
import { parseKeyValueBlocks, parseKeyValueLines } from "../lib/text";
|
||||
import { parseKeyValueBlocks, parseKeyValueLines, uniqBy } from "../lib/text";
|
||||
import type { Contact, DomainRecord, Nameserver, RegistrarInfo } from "../types";
|
||||
|
||||
// Phrases registries use to say a name is not registered. They also turn up in the remarks and
|
||||
@@ -177,26 +177,19 @@ export function normalizeWhois(
|
||||
};
|
||||
})();
|
||||
|
||||
// Statuses: multiple entries are expected; keep raw
|
||||
const statusLines =
|
||||
map["domain status"] ||
|
||||
map.status ||
|
||||
map.flags ||
|
||||
map.state || // .ru
|
||||
map["registration status"] ||
|
||||
map.eppstatus || // .fr
|
||||
[];
|
||||
// Report EPP codes, as RDAP does; a status may be spelled out ("client transfer prohibited")
|
||||
// Statuses come from the first block that lists any, as contact blocks repeat "status:" (.ua,
|
||||
// .fr). .be lists its EPP statuses under a separate "Flags:" header.
|
||||
const statusLines = [
|
||||
...(statusLinesOf(blocks) ?? statusLinesOf([map], false) ?? []),
|
||||
...blocks.flatMap((b) => b.flags ?? []).filter((f) => isEppStatus(f)),
|
||||
];
|
||||
const statuses = statusLines.length
|
||||
? statusLines
|
||||
.map((line) => {
|
||||
const phrase = line.replace(/\s*(?:\(?https?:\/\/\S*\)?|\(.*\)).*$/, "");
|
||||
const status = normalizeEppStatus(
|
||||
isEppStatus(phrase) ? phrase : (line.split(/\s+/)[0] ?? ""),
|
||||
);
|
||||
return status ? { status, raw: line } : null;
|
||||
})
|
||||
.filter((s): s is { status: string; raw: string } => s !== null)
|
||||
? uniqBy(
|
||||
statusLines.flatMap((line) =>
|
||||
parseStatusLine(line).map((status) => ({ status, raw: line })),
|
||||
),
|
||||
(s) => s.status.toLowerCase(),
|
||||
)
|
||||
: undefined;
|
||||
|
||||
// Some registries (.ua) publish expiry only as a status, e.g. "OK-UNTIL 20261004161638"
|
||||
@@ -257,9 +250,9 @@ export function normalizeWhois(
|
||||
? { enabled: dnssecValues.some((v) => /^(?:signed|yes|true|active)/i.test(v.trim())) }
|
||||
: undefined;
|
||||
|
||||
// Simple lock derivation from statuses
|
||||
// Simple lock derivation from statuses (the EPP code, or the registry's wording in raw)
|
||||
const transferLock = !!statuses?.some((s) =>
|
||||
/transfer[-\s]*prohibited/i.test(s.raw || s.status || ""),
|
||||
/transfer[-\s]*prohibited/i.test(`${s.status} ${s.raw ?? ""}`),
|
||||
);
|
||||
|
||||
const record: DomainRecord = {
|
||||
@@ -296,6 +289,52 @@ export function normalizeWhois(
|
||||
return record;
|
||||
}
|
||||
|
||||
// Status keys by preference: .fr lists EPP codes as "eppstatus" beside a plain "status: ACTIVE"
|
||||
const STATUS_KEYS = ["eppstatus", "domain status", "status", "registration status"];
|
||||
|
||||
/**
|
||||
* Status lines of the first block that has any. "state" is .ru's status key but a contact's
|
||||
* region elsewhere ("State: Nuevo Leon" on .mx), so it only counts beside the domain line.
|
||||
*/
|
||||
function statusLinesOf(
|
||||
blocks: Array<Record<string, string[]>>,
|
||||
allowState = true,
|
||||
): string[] | undefined {
|
||||
for (const block of blocks) {
|
||||
const key = STATUS_KEYS.find((k) => block[k]?.length);
|
||||
if (key) return block[key];
|
||||
if (allowState && block.state?.length && (block.domain || block["domain name"])) {
|
||||
return block.state;
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// FRED registries (.cz, .mk, ...) describe server statuses in words
|
||||
const FRED_STATUSES: Record<string, string> = {
|
||||
"sponsoring registrar change forbidden": "serverTransferProhibited",
|
||||
"deletion forbidden": "serverDeleteProhibited",
|
||||
"update forbidden": "serverUpdateProhibited",
|
||||
};
|
||||
|
||||
/**
|
||||
* Statuses in one WHOIS status line, as EPP codes where they have one, otherwise as the
|
||||
* registry's own phrase: "clientTransferProhibited https://icann.org/epp#...", "ok (paid and in
|
||||
* zone)", "REGISTERED, DELEGATED, VERIFIED", "Transfer Locked", "OK-UNTIL 20261004161638".
|
||||
*/
|
||||
function parseStatusLine(line: string): string[] {
|
||||
const phrase = line.replace(/\s*(?:\(?https?:\/\/\S*\)?|\(.*\)).*$/, "");
|
||||
return phrase
|
||||
.split(",")
|
||||
.map((part) => {
|
||||
const p = part.trim().replace(/\.$/, "");
|
||||
if (isEppStatus(p)) return normalizeEppStatus(p);
|
||||
// A trailing date explains the status rather than naming it (.ua "OK-UNTIL <date>")
|
||||
return FRED_STATUSES[p.toLowerCase()] ?? p.replace(/(?:\s+\d\S*)+$/, "");
|
||||
})
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
/** First value that parses as a date, taking blocks in order and keys by priority within each. */
|
||||
function firstDate(blocks: Array<Record<string, string[]>>, keys: string[]): string | undefined {
|
||||
for (const block of blocks) {
|
||||
|
||||
Reference in new issue
Block a user