fix: fill the RDAP registrar from every registrar entity

record.registrar came from the first registrar entity, usually the registry's
copy with only a name and IANA ID, so details in the registrar's own document
(URL, address, phone) never appeared. Fill the first copy's gaps from later
ones with the same IANA ID, and fall back to the abuse contact only after that.
Checked live: google.com's registrar now carries MarkMonitor's address.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
jakeandClaude Opus 5.5 committed 2026-10-03 15:27:57 -04:00
1 parent 3071c4867a
commit c85e5cd9cb
2 files changed
+95 -21

No files matched your search

+54
View File
@@ -553,3 +553,57 @@ test("normalizeRdap combines registry and registrar copies of a contact and spli
["admin", "Ops Team", "ops@acme.example"],
]);
});
test("normalizeRdap fills registrar gaps from the registrar's own copy", () => {
const ids = [{ type: "IANA Registrar ID", identifier: "292" }];
const rec = normalizeRdap(
"google.com",
"com",
{
ldhName: "google.com",
entities: [
// Registry copy: name and IANA ID, abuse contact nested
{
roles: ["registrar"],
publicIds: ids,
vcardArray: ["vcard", [["fn", {}, "text", "MarkMonitor Inc."]]],
entities: [
{
roles: ["abuse"],
vcardArray: ["vcard", [["email", {}, "text", "abuse@markmonitor.com"]]],
},
],
},
// The registrar's own copy, from its RDAP document
{
roles: ["registrar"],
publicIds: ids,
vcardArray: [
"vcard",
[
["fn", {}, "text", "Markmonitor Inc."],
["url", {}, "uri", "https://www.markmonitor.com"],
["tel", {}, "uri", "tel:+1.2083895740"],
["adr", {}, "text", ["", "", "3540 E Longwing Ln", "Meridian", "ID", "83646", "US"]],
],
],
},
// A different registrar's entity is not mixed in
{
roles: ["registrar"],
publicIds: [{ type: "IANA Registrar ID", identifier: "1" }],
vcardArray: ["vcard", [["email", {}, "text", "x@other.example"]]],
},
],
},
[],
);
expect(rec.registrar).toMatchObject({
name: "MarkMonitor Inc.",
ianaId: "292",
url: "https://www.markmonitor.com",
phone: "+1.2083895740",
email: "abuse@markmonitor.com",
city: "Meridian",
});
});
+41 -21
View File
@@ -206,32 +206,52 @@ function nestedAbuseVcard(entities: unknown): ParsedVCard | undefined {
return undefined;
}
/**
* The registrar, from every registrar entity: a merged document holds the registry's copy (often
* just a name and IANA ID) and the registrar's own (URL, address, phone), so later copies with
* the same IANA ID fill the first one's gaps.
*/
function extractRegistrar(entities: unknown): RegistrarInfo | undefined {
if (!Array.isArray(entities)) return undefined;
let info: RegistrarInfo | undefined;
let abuse: ParsedVCard | undefined;
for (const ent of entities) {
if (!rolesOf(ent).some((r) => /registrar/i.test(r))) continue;
const v = parseVcard((ent as RdapDoc)?.vcardArray);
const abuse = nestedAbuseVcard((ent as RdapDoc)?.entities);
const ianaId = Array.isArray((ent as RdapDoc)?.publicIds)
? ((ent as RdapDoc).publicIds as Array<RdapDoc>).find((id) =>
/iana\s*registrar\s*id/i.test(String(id?.type)),
)?.identifier
: undefined;
return {
name: v.fn || v.org || asString((ent as RdapDoc)?.handle) || undefined,
ianaId: asString(ianaId),
url: v.url ?? undefined,
// Fall back to the abuse contact, matching WHOIS "Registrar Abuse Contact Email/Phone"
email: v.email?.[0] ?? abuse?.email?.[0],
phone: v.tel?.[0] ?? abuse?.tel?.[0],
street: v.street,
city: v.locality,
state: v.region,
postalCode: v.postcode,
...resolveCountry(v.country, v.countryCode),
};
const next = registrarInfo(ent);
if (info?.ianaId && next.ianaId && info.ianaId !== next.ianaId) continue;
if (!info) info = next;
else {
const record = info as Record<string, unknown>;
for (const [key, value] of Object.entries(next)) record[key] ??= value;
}
abuse ??= nestedAbuseVcard((ent as RdapDoc)?.entities);
}
return undefined;
if (!info) return undefined;
// Fall back to the abuse contact, matching WHOIS "Registrar Abuse Contact Email/Phone"
info.email ??= abuse?.email?.[0];
info.phone ??= abuse?.tel?.[0];
return info;
}
function registrarInfo(ent: unknown): RegistrarInfo {
const v = parseVcard((ent as RdapDoc)?.vcardArray);
const ianaId = Array.isArray((ent as RdapDoc)?.publicIds)
? ((ent as RdapDoc).publicIds as Array<RdapDoc>).find((id) =>
/iana\s*registrar\s*id/i.test(String(id?.type)),
)?.identifier
: undefined;
return {
name: v.fn || v.org || asString((ent as RdapDoc)?.handle) || undefined,
ianaId: asString(ianaId),
url: v.url ?? undefined,
email: v.email?.[0],
phone: v.tel?.[0],
street: v.street,
city: v.locality,
state: v.region,
postalCode: v.postcode,
...resolveCountry(v.country, v.countryCode),
};
}
/** Does an RFC 9537 redaction refer to the entity with this RDAP role (e.g. "registrant")? */