mirror of
https://github.com/jakejarvis/rdapper.git
synced 2026-09-23 00:15:31 -04:00
feat: add RFC 9537 redaction support, vCard address/fax parsing, and registrar address fields
- Parse the top-level RDAP `redacted` array (RFC 9537) into a new `redactions` field on `DomainRecord`; set `privacyEnabled` when any redaction references "registrant" - Expose `street`, `city`, `state`, `postalCode`, `country`, and `countryCode` on both `RegistrarInfo` and `Contact`; extract ISO 3166-1 alpha-2 from the vCard `cc` parameter (RFC 8605) - Split `tel` entries by fax type parameter (`"fax"` / `["work","fax"]`) into separate `tel` and `fax` lists; collect multiple email addresses as an array, collapsing singletons to a plain string - Prefer an exact `registration` event match over `reregistration` when extracting `creationDate` (RFC 9083) - Merge `redacted` arrays in `mergeRdapDocs`, deduplicating by JSON identity - Split `adr` street on `\r?\n` only (not commas) to avoid splitting inline comma-separated addresses
This commit is contained in:
@@ -508,6 +508,12 @@ interface DomainRecord {
|
|||||||
url?: string;
|
url?: string;
|
||||||
email?: string;
|
email?: string;
|
||||||
phone?: string;
|
phone?: string;
|
||||||
|
street?: string[]; // address (RDAP only)
|
||||||
|
city?: string;
|
||||||
|
state?: string;
|
||||||
|
postalCode?: string;
|
||||||
|
country?: string;
|
||||||
|
countryCode?: string; // ISO 3166-1 alpha-2, from the vCard "cc" parameter
|
||||||
};
|
};
|
||||||
reseller?: string;
|
reseller?: string;
|
||||||
statuses?: Array<{
|
statuses?: Array<{
|
||||||
@@ -549,7 +555,15 @@ interface DomainRecord {
|
|||||||
country?: string;
|
country?: string;
|
||||||
countryCode?: string;
|
countryCode?: string;
|
||||||
}>;
|
}>;
|
||||||
privacyEnabled?: boolean; // registrant appears privacy-redacted based on keyword heuristics
|
privacyEnabled?: boolean; // registrant appears privacy-redacted based on keyword heuristics or RFC 9537 redactions
|
||||||
|
redactions?: Array<{
|
||||||
|
name: string; // e.g. "Registrant Email"
|
||||||
|
prePath?: string;
|
||||||
|
postPath?: string;
|
||||||
|
replacementPath?: string;
|
||||||
|
method?: string; // e.g. "emptyValue", "partialValue"
|
||||||
|
reason?: string;
|
||||||
|
}>; // RFC 9537 redaction metadata (RDAP only)
|
||||||
whoisServer?: string; // authoritative WHOIS queried (if any)
|
whoisServer?: string; // authoritative WHOIS queried (if any)
|
||||||
rdapServers?: string[]; // RDAP URLs tried (bootstrap bases and related/entity links)
|
rdapServers?: string[]; // RDAP URLs tried (bootstrap bases and related/entity links)
|
||||||
rawRdap?: unknown; // raw RDAP JSON (only when options.includeRaw)
|
rawRdap?: unknown; // raw RDAP JSON (only when options.includeRaw)
|
||||||
|
|||||||
@@ -43,6 +43,13 @@ export function mergeRdapDocs(baseDoc: unknown, others: unknown[]): unknown {
|
|||||||
if (merged.secureDNS == null && cur.secureDNS != null) merged.secureDNS = cur.secureDNS;
|
if (merged.secureDNS == null && cur.secureDNS != null) merged.secureDNS = cur.secureDNS;
|
||||||
// port43 (authoritative WHOIS): prefer existing; fill if missing
|
// port43 (authoritative WHOIS): prefer existing; fill if missing
|
||||||
if (merged.port43 == null && cur.port43 != null) merged.port43 = cur.port43;
|
if (merged.port43 == null && cur.port43 != null) merged.port43 = cur.port43;
|
||||||
|
// redacted (RFC 9537): concat, dedupe by JSON
|
||||||
|
if (merged.redacted != null || cur.redacted != null) {
|
||||||
|
merged.redacted = uniqBy(
|
||||||
|
[...toArray<Json>(merged.redacted), ...toArray<Json>(cur.redacted)],
|
||||||
|
(r) => JSON.stringify(r),
|
||||||
|
);
|
||||||
|
}
|
||||||
// remarks: concat simple strings if present
|
// remarks: concat simple strings if present
|
||||||
const mergedRemarks = (merged as { remarks?: Json[] }).remarks;
|
const mergedRemarks = (merged as { remarks?: Json[] }).remarks;
|
||||||
const curRemarks = (cur as { remarks?: Json[] }).remarks;
|
const curRemarks = (cur as { remarks?: Json[] }).remarks;
|
||||||
|
|||||||
@@ -135,3 +135,135 @@ test("normalizeRdap treats release-pending statuses as not registered", () => {
|
|||||||
);
|
);
|
||||||
expect(active.isRegistered).toBe(true);
|
expect(active.isRegistered).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("normalizeRdap reads vCard adr cc parameter into countryCode", () => {
|
||||||
|
const rec = normalizeRdap(
|
||||||
|
"example.com",
|
||||||
|
"com",
|
||||||
|
{
|
||||||
|
ldhName: "example.com",
|
||||||
|
entities: [
|
||||||
|
{
|
||||||
|
roles: ["registrant"],
|
||||||
|
vcardArray: [
|
||||||
|
"vcard",
|
||||||
|
[["adr", { cc: "us" }, "text", ["", "", "1 Main St", "Town", "CA", "90000", "USA"]]],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
expect(rec.contacts?.[0]?.country).toBe("USA");
|
||||||
|
expect(rec.contacts?.[0]?.countryCode).toBe("US");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("normalizeRdap parses RFC 9537 redacted array and flags privacy", () => {
|
||||||
|
const rec = normalizeRdap(
|
||||||
|
"example.com",
|
||||||
|
"com",
|
||||||
|
{
|
||||||
|
ldhName: "example.com",
|
||||||
|
redacted: [
|
||||||
|
{
|
||||||
|
name: { description: "Registrant Email" },
|
||||||
|
prePath: "$.entities[?(@.roles[0]=='registrant')].vcardArray[1][?(@[0]=='email')]",
|
||||||
|
method: "emptyValue",
|
||||||
|
reason: { description: "Server policy" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
expect(rec.redactions).toEqual([
|
||||||
|
expect.objectContaining({
|
||||||
|
name: "Registrant Email",
|
||||||
|
method: "emptyValue",
|
||||||
|
reason: "Server policy",
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
expect(rec.privacyEnabled).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("normalizeRdap separates fax from tel and keeps multiple emails", () => {
|
||||||
|
const rec = normalizeRdap(
|
||||||
|
"example.com",
|
||||||
|
"com",
|
||||||
|
{
|
||||||
|
ldhName: "example.com",
|
||||||
|
entities: [
|
||||||
|
{
|
||||||
|
roles: ["registrant"],
|
||||||
|
vcardArray: [
|
||||||
|
"vcard",
|
||||||
|
[
|
||||||
|
["tel", { type: "voice" }, "text", "+1.111"],
|
||||||
|
["tel", { type: ["work", "fax"] }, "text", "+1.222"],
|
||||||
|
["email", {}, "text", "a@example.com"],
|
||||||
|
["email", {}, "text", "b@example.com"],
|
||||||
|
["adr", {}, "text", ["", "", "Suite 5, 1 Main St", "Town", "", "", ""]],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
const c = rec.contacts?.[0];
|
||||||
|
expect(c?.phone).toBe("+1.111");
|
||||||
|
expect(c?.fax).toBe("+1.222");
|
||||||
|
expect(c?.email).toEqual(["a@example.com", "b@example.com"]);
|
||||||
|
expect(c?.street).toEqual(["Suite 5, 1 Main St"]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("normalizeRdap prefers registration event over reregistration", () => {
|
||||||
|
const rec = normalizeRdap(
|
||||||
|
"example.com",
|
||||||
|
"com",
|
||||||
|
{
|
||||||
|
ldhName: "example.com",
|
||||||
|
events: [
|
||||||
|
{ eventAction: "reregistration", eventDate: "2024-01-01T00:00:00Z" },
|
||||||
|
{ eventAction: "registration", eventDate: "2020-01-01T00:00:00Z" },
|
||||||
|
],
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
expect(rec.creationDate).toBe("2020-01-01T00:00:00Z");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("normalizeRdap maps registrar adr and cc", () => {
|
||||||
|
const rec = normalizeRdap(
|
||||||
|
"example.com",
|
||||||
|
"com",
|
||||||
|
{
|
||||||
|
ldhName: "example.com",
|
||||||
|
entities: [
|
||||||
|
{
|
||||||
|
roles: ["registrar"],
|
||||||
|
vcardArray: [
|
||||||
|
"vcard",
|
||||||
|
[
|
||||||
|
["fn", {}, "text", "Registrar LLC"],
|
||||||
|
[
|
||||||
|
"adr",
|
||||||
|
{ cc: "CA" },
|
||||||
|
"text",
|
||||||
|
["", "", "5 King St", "Toronto", "ON", "M5H", "Canada"],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
expect(rec.registrar).toMatchObject({
|
||||||
|
street: ["5 King St"],
|
||||||
|
city: "Toronto",
|
||||||
|
state: "ON",
|
||||||
|
postalCode: "M5H",
|
||||||
|
country: "Canada",
|
||||||
|
countryCode: "CA",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
+77
-23
@@ -1,7 +1,7 @@
|
|||||||
import { toISO } from "../lib/dates";
|
import { toISO } from "../lib/dates";
|
||||||
import { isPrivacyName } from "../lib/privacy";
|
import { isPrivacyName } from "../lib/privacy";
|
||||||
import { asDateLike, asString, asStringArray, uniq } from "../lib/text";
|
import { asDateLike, asString, asStringArray, uniq } from "../lib/text";
|
||||||
import type { Contact, DomainRecord, Nameserver, RegistrarInfo } from "../types";
|
import type { Contact, DomainRecord, Nameserver, Redaction, RegistrarInfo } from "../types";
|
||||||
|
|
||||||
type RdapDoc = Record<string, unknown>;
|
type RdapDoc = Record<string, unknown>;
|
||||||
|
|
||||||
@@ -44,12 +44,16 @@ export function normalizeRdap(
|
|||||||
// Contacts: RDAP entities include roles like registrant, administrative, technical, billing, abuse
|
// Contacts: RDAP entities include roles like registrant, administrative, technical, billing, abuse
|
||||||
const contacts: Contact[] | undefined = extractContacts(doc.entities as unknown);
|
const contacts: Contact[] | undefined = extractContacts(doc.entities as unknown);
|
||||||
|
|
||||||
// Derive privacy flag from registrant name/org keywords
|
// RFC 9537 redaction metadata
|
||||||
|
const redactions = extractRedactions(doc.redacted);
|
||||||
|
|
||||||
|
// Derive privacy flag from registrant name/org keywords or RFC 9537 registrant redactions
|
||||||
const registrant = contacts?.find((c) => c.type === "registrant");
|
const registrant = contacts?.find((c) => c.type === "registrant");
|
||||||
const privacyEnabled = !!(
|
const privacyEnabled =
|
||||||
registrant &&
|
!!(
|
||||||
([registrant.name, registrant.organization].filter(Boolean) as string[]).some(isPrivacyName)
|
registrant &&
|
||||||
);
|
([registrant.name, registrant.organization].filter(Boolean) as string[]).some(isPrivacyName)
|
||||||
|
) || !!redactions?.some((r) => /registrant/i.test(`${r.prePath ?? ""} ${r.name}`));
|
||||||
|
|
||||||
// RDAP uses IANA EPP status values. Preserve raw plus a description if any remarks are present.
|
// RDAP uses IANA EPP status values. Preserve raw plus a description if any remarks are present.
|
||||||
const statuses = Array.isArray(doc.status)
|
const statuses = Array.isArray(doc.status)
|
||||||
@@ -81,10 +85,14 @@ export function normalizeRdap(
|
|||||||
const events: RdapEvent[] = Array.isArray(doc.events)
|
const events: RdapEvent[] = Array.isArray(doc.events)
|
||||||
? (doc.events as unknown[] as RdapEvent[])
|
? (doc.events as unknown[] as RdapEvent[])
|
||||||
: [];
|
: [];
|
||||||
|
const actionOf = (e: RdapEvent) => (typeof e?.eventAction === "string" ? e.eventAction : "");
|
||||||
|
// Prefer an exact match, then a substring match that ignores "reregistration" (RFC 9083)
|
||||||
const byAction = (action: string) =>
|
const byAction = (action: string) =>
|
||||||
events.find(
|
events.find((e) => actionOf(e).toLowerCase() === action) ??
|
||||||
(e) => typeof e?.eventAction === "string" && e.eventAction.toLowerCase().includes(action),
|
events.find((e) => {
|
||||||
);
|
const a = actionOf(e).toLowerCase();
|
||||||
|
return a.includes(action) && a !== "reregistration";
|
||||||
|
});
|
||||||
const creationDate = toISO(
|
const creationDate = toISO(
|
||||||
asDateLike(byAction("registration")?.eventDate) ?? asDateLike(doc.registrationDate),
|
asDateLike(byAction("registration")?.eventDate) ?? asDateLike(doc.registrationDate),
|
||||||
);
|
);
|
||||||
@@ -135,6 +143,7 @@ export function normalizeRdap(
|
|||||||
: undefined,
|
: undefined,
|
||||||
contacts,
|
contacts,
|
||||||
privacyEnabled: privacyEnabled ? true : undefined,
|
privacyEnabled: privacyEnabled ? true : undefined,
|
||||||
|
redactions,
|
||||||
whoisServer,
|
whoisServer,
|
||||||
rdapServers: rdapServersTried,
|
rdapServers: rdapServersTried,
|
||||||
rawRdap: includeRaw ? rdap : undefined,
|
rawRdap: includeRaw ? rdap : undefined,
|
||||||
@@ -146,6 +155,29 @@ export function normalizeRdap(
|
|||||||
return record;
|
return record;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Parse the RFC 9537 top-level "redacted" array. */
|
||||||
|
function extractRedactions(redacted: unknown): Redaction[] | undefined {
|
||||||
|
if (!Array.isArray(redacted)) return undefined;
|
||||||
|
const out: Redaction[] = [];
|
||||||
|
for (const item of redacted) {
|
||||||
|
if (!item || typeof item !== "object") continue;
|
||||||
|
const r = item as RdapDoc;
|
||||||
|
const nameObj = (r.name ?? {}) as RdapDoc;
|
||||||
|
const name = asString(nameObj.description) || asString(nameObj.type);
|
||||||
|
if (!name) continue;
|
||||||
|
const reason = (r.reason ?? {}) as RdapDoc;
|
||||||
|
out.push({
|
||||||
|
name,
|
||||||
|
prePath: asString(r.prePath) || undefined,
|
||||||
|
postPath: asString(r.postPath) || undefined,
|
||||||
|
replacementPath: asString(r.replacementPath) || undefined,
|
||||||
|
method: asString(r.method) || undefined,
|
||||||
|
reason: asString(reason.description) || asString(reason.type) || undefined,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return out.length ? out : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
function extractRegistrar(entities: unknown): RegistrarInfo | undefined {
|
function extractRegistrar(entities: unknown): RegistrarInfo | undefined {
|
||||||
if (!Array.isArray(entities)) return undefined;
|
if (!Array.isArray(entities)) return undefined;
|
||||||
for (const ent of entities) {
|
for (const ent of entities) {
|
||||||
@@ -163,8 +195,14 @@ function extractRegistrar(entities: unknown): RegistrarInfo | undefined {
|
|||||||
name: v.fn || v.org || asString((ent as RdapDoc)?.handle) || undefined,
|
name: v.fn || v.org || asString((ent as RdapDoc)?.handle) || undefined,
|
||||||
ianaId: asString(ianaId),
|
ianaId: asString(ianaId),
|
||||||
url: v.url ?? undefined,
|
url: v.url ?? undefined,
|
||||||
email: v.email ?? undefined,
|
email: v.email?.[0],
|
||||||
phone: v.tel ?? undefined,
|
phone: v.tel?.[0],
|
||||||
|
street: v.street,
|
||||||
|
city: v.locality,
|
||||||
|
state: v.region,
|
||||||
|
postalCode: v.postcode,
|
||||||
|
country: v.country,
|
||||||
|
countryCode: v.countryCode,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -195,9 +233,9 @@ function extractContacts(entities: unknown): Contact[] | undefined {
|
|||||||
type: roleKey,
|
type: roleKey,
|
||||||
name: v.fn,
|
name: v.fn,
|
||||||
organization: v.org,
|
organization: v.org,
|
||||||
email: v.email,
|
email: single(v.email),
|
||||||
phone: v.tel,
|
phone: single(v.tel),
|
||||||
fax: v.fax,
|
fax: single(v.fax),
|
||||||
street: v.street,
|
street: v.street,
|
||||||
city: v.locality,
|
city: v.locality,
|
||||||
state: v.region,
|
state: v.region,
|
||||||
@@ -209,12 +247,18 @@ function extractContacts(entities: unknown): Contact[] | undefined {
|
|||||||
return out.length ? out : undefined;
|
return out.length ? out : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Collapse a list to undefined, a single string, or the array when there are several. */
|
||||||
|
function single(list: string[] | undefined): string | string[] | undefined {
|
||||||
|
if (!list?.length) return undefined;
|
||||||
|
return list.length === 1 ? list[0] : list;
|
||||||
|
}
|
||||||
|
|
||||||
interface ParsedVCard {
|
interface ParsedVCard {
|
||||||
fn?: string;
|
fn?: string;
|
||||||
org?: string;
|
org?: string;
|
||||||
email?: string;
|
email?: string[];
|
||||||
tel?: string;
|
tel?: string[];
|
||||||
fax?: string;
|
fax?: string[];
|
||||||
url?: string;
|
url?: string;
|
||||||
street?: string[];
|
street?: string[];
|
||||||
locality?: string;
|
locality?: string;
|
||||||
@@ -242,28 +286,38 @@ function parseVcard(vcardArray: unknown): ParsedVCard {
|
|||||||
case "org":
|
case "org":
|
||||||
out.org = Array.isArray(value) ? value.map((x) => String(x)).join(" ") : asString(value);
|
out.org = Array.isArray(value) ? value.map((x) => String(x)).join(" ") : asString(value);
|
||||||
break;
|
break;
|
||||||
case "email":
|
case "email": {
|
||||||
out.email = asString(value);
|
const v = asString(value);
|
||||||
|
if (v) (out.email ??= []).push(v);
|
||||||
break;
|
break;
|
||||||
case "tel":
|
}
|
||||||
out.tel = asString(value);
|
case "tel": {
|
||||||
|
const v = asString(value);
|
||||||
|
if (!v) break;
|
||||||
|
// RFC 6350 TYPE parameter may be a string or array (e.g. "fax", ["work", "fax"])
|
||||||
|
const type = e?.[1]?.type;
|
||||||
|
const types = (Array.isArray(type) ? type : [type]).map((t) => String(t).toLowerCase());
|
||||||
|
(types.includes("fax") ? (out.fax ??= []) : (out.tel ??= [])).push(v);
|
||||||
break;
|
break;
|
||||||
|
}
|
||||||
case "url":
|
case "url":
|
||||||
out.url = asString(value);
|
out.url = asString(value);
|
||||||
break;
|
break;
|
||||||
case "adr": {
|
case "adr": {
|
||||||
// adr value is [postOfficeBox, extendedAddress, street, locality, region, postalCode, country]
|
// adr value is [postOfficeBox, extendedAddress, street, locality, region, postalCode, country]
|
||||||
if (Array.isArray(value)) {
|
if (Array.isArray(value)) {
|
||||||
out.street = value[2] ? String(value[2]).split(/\n|,\s*/) : undefined;
|
out.street = value[2] ? String(value[2]).split(/\r?\n/).filter(Boolean) : undefined;
|
||||||
out.locality = asString(value[3]);
|
out.locality = asString(value[3]);
|
||||||
out.region = asString(value[4]);
|
out.region = asString(value[4]);
|
||||||
out.postcode = asString(value[5]);
|
out.postcode = asString(value[5]);
|
||||||
out.country = asString(value[6]);
|
out.country = asString(value[6]);
|
||||||
|
// RFC 8605: ISO 3166-1 alpha-2 code lives in the "cc" parameter
|
||||||
|
const cc = asString(e?.[1]?.cc);
|
||||||
|
if (cc) out.countryCode = cc.toUpperCase();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Best effort country code from country name (often omitted). Leaving undefined unless explicitly provided.
|
|
||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,6 +23,14 @@ export interface RegistrarInfo {
|
|||||||
email?: string;
|
email?: string;
|
||||||
/** Registrar contact phone number */
|
/** Registrar contact phone number */
|
||||||
phone?: string;
|
phone?: string;
|
||||||
|
/** Registrar street address lines */
|
||||||
|
street?: string[];
|
||||||
|
city?: string;
|
||||||
|
state?: string;
|
||||||
|
postalCode?: string;
|
||||||
|
country?: string;
|
||||||
|
/** ISO 3166-1 alpha-2 country code, when provided */
|
||||||
|
countryCode?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -55,6 +63,24 @@ export interface Contact {
|
|||||||
countryCode?: string;
|
countryCode?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An RFC 9537 redaction entry describing a field the registry withheld.
|
||||||
|
*/
|
||||||
|
export interface Redaction {
|
||||||
|
/** Human-readable name/type of the redacted field (e.g., "Registrant Email") */
|
||||||
|
name: string;
|
||||||
|
/** JSONPath to the redacted field's parent/location */
|
||||||
|
prePath?: string;
|
||||||
|
/** JSONPath to the redacted field itself, if present */
|
||||||
|
postPath?: string;
|
||||||
|
/** JSONPath to a replacement value, if any */
|
||||||
|
replacementPath?: string;
|
||||||
|
/** Redaction method (e.g., "emptyValue", "partialValue", "replacementValue", "removal") */
|
||||||
|
method?: string;
|
||||||
|
/** Reason for redaction (description or type) */
|
||||||
|
reason?: string;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* DNS nameserver information.
|
* DNS nameserver information.
|
||||||
*
|
*
|
||||||
@@ -162,6 +188,8 @@ export interface DomainRecord {
|
|||||||
contacts?: Contact[];
|
contacts?: Contact[];
|
||||||
/** Best guess as to whether registrant is redacted based on keywords */
|
/** Best guess as to whether registrant is redacted based on keywords */
|
||||||
privacyEnabled?: boolean;
|
privacyEnabled?: boolean;
|
||||||
|
/** RFC 9537 redaction metadata reported by RDAP, if any */
|
||||||
|
redactions?: Redaction[];
|
||||||
/** Authoritative WHOIS queried (if any) */
|
/** Authoritative WHOIS queried (if any) */
|
||||||
whoisServer?: string;
|
whoisServer?: string;
|
||||||
/** RDAP base URLs tried */
|
/** RDAP base URLs tried */
|
||||||
|
|||||||
@@ -370,6 +370,8 @@ function collectContacts(map: Record<string, string[]>): Contact[] | undefined {
|
|||||||
state: state || undefined,
|
state: state || undefined,
|
||||||
postalCode: postalCode || undefined,
|
postalCode: postalCode || undefined,
|
||||||
country: country || undefined,
|
country: country || undefined,
|
||||||
|
// Many registries print the ISO 3166-1 alpha-2 code directly
|
||||||
|
countryCode: country && /^[A-Za-z]{2}$/.test(country) ? country.toUpperCase() : undefined,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user