mirror of
https://github.com/jakejarvis/rdapper.git
synced 2026-09-23 00:15:31 -04:00
refactor: extract vCard parser into its own module and expand parsed fields
- Move `parseVcard` out of `normalize.ts` into a new `src/rdap/vcard.ts` module; export `readJCard` (jCard → `VCardProp[]`) and `paramList` as reusable primitives - Parse `KIND` into a typed `VCardKind` union and split structured `ORG` values into a top-level `org` plus an `orgUnits` array for sub-levels - Parse `TITLE` and `ROLE` properties from the vCard - Extract the PO box (element 0) and extended address (element 1) from `ADR` into `poBox` and the beginning of `street` respectively, rather than discarding them - Strip `tel:` URI scheme from `TEL` entries whose `valueType` is `uri` (RFC 6350) - Add `kind`, `organizationUnits`, `title`, `role`, and `poBox` to the `Contact` type and wire them through `extractContacts` in `normalize.ts` - Document new `Contact` fields in README - Add `vcard.test.ts` covering malformed input tolerance, `paramList` normalisation, ORG level splitting, unknown KIND, PO box extraction, and `tel:` URI stripping
This commit is contained in:
@@ -544,10 +544,15 @@ interface DomainRecord {
|
|||||||
type:
|
type:
|
||||||
"registrant" | "admin" | "tech" | "billing" | "abuse" | "registrar" | "reseller" | "unknown";
|
"registrant" | "admin" | "tech" | "billing" | "abuse" | "registrar" | "reseller" | "unknown";
|
||||||
name?: string;
|
name?: string;
|
||||||
|
kind?: "individual" | "org" | "group" | "location"; // vCard KIND (RDAP only, when provided)
|
||||||
organization?: string;
|
organization?: string;
|
||||||
|
organizationUnits?: string[]; // vCard ORG levels below the organization (RDAP only)
|
||||||
|
title?: string; // RDAP only
|
||||||
|
role?: string; // RDAP only
|
||||||
email?: string | string[];
|
email?: string | string[];
|
||||||
phone?: string | string[];
|
phone?: string | string[];
|
||||||
fax?: string | string[];
|
fax?: string | string[];
|
||||||
|
poBox?: string; // RDAP only
|
||||||
street?: string[];
|
street?: string[];
|
||||||
city?: string;
|
city?: string;
|
||||||
state?: string;
|
state?: string;
|
||||||
|
|||||||
@@ -306,3 +306,21 @@ test("normalizeRdap flags contacts with placeholder values or matching redaction
|
|||||||
expect(tech?.redacted).toBe(true);
|
expect(tech?.redacted).toBe(true);
|
||||||
expect(admin?.redacted).toBeUndefined();
|
expect(admin?.redacted).toBeUndefined();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("normalizeRdap resolves registrar countryCode from the country name", () => {
|
||||||
|
const rec = normalizeRdap(
|
||||||
|
"example.com",
|
||||||
|
"com",
|
||||||
|
{
|
||||||
|
ldhName: "example.com",
|
||||||
|
entities: [
|
||||||
|
{
|
||||||
|
roles: ["registrar"],
|
||||||
|
vcardArray: ["vcard", [["adr", {}, "text", ["", "", "", "", "", "", "Canada"]]]],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
[],
|
||||||
|
);
|
||||||
|
expect(rec.registrar).toMatchObject({ country: "Canada", countryCode: "CA" });
|
||||||
|
});
|
||||||
|
|||||||
+8
-71
@@ -1,7 +1,9 @@
|
|||||||
|
import { resolveCountry } from "../lib/countries";
|
||||||
import { finalizeContact } from "../lib/contacts";
|
import { finalizeContact } from "../lib/contacts";
|
||||||
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 { parseVcard } from "./vcard";
|
||||||
import type { Contact, DomainRecord, Nameserver, Redaction, RegistrarInfo } from "../types";
|
import type { Contact, DomainRecord, Nameserver, Redaction, RegistrarInfo } from "../types";
|
||||||
|
|
||||||
type RdapDoc = Record<string, unknown>;
|
type RdapDoc = Record<string, unknown>;
|
||||||
@@ -204,8 +206,7 @@ function extractRegistrar(entities: unknown): RegistrarInfo | undefined {
|
|||||||
city: v.locality,
|
city: v.locality,
|
||||||
state: v.region,
|
state: v.region,
|
||||||
postalCode: v.postcode,
|
postalCode: v.postcode,
|
||||||
country: v.country,
|
...resolveCountry(v.country, v.countryCode),
|
||||||
countryCode: v.countryCode,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return undefined;
|
return undefined;
|
||||||
@@ -242,9 +243,14 @@ function extractContacts(entities: unknown, redactions?: Redaction[]): Contact[]
|
|||||||
type: roleKey,
|
type: roleKey,
|
||||||
name: v.fn,
|
name: v.fn,
|
||||||
organization: v.org,
|
organization: v.org,
|
||||||
|
organizationUnits: v.orgUnits,
|
||||||
|
kind: v.kind,
|
||||||
|
title: v.title,
|
||||||
|
role: v.role,
|
||||||
email: single(v.email),
|
email: single(v.email),
|
||||||
phone: single(v.tel),
|
phone: single(v.tel),
|
||||||
fax: single(v.fax),
|
fax: single(v.fax),
|
||||||
|
poBox: v.poBox,
|
||||||
street: v.street,
|
street: v.street,
|
||||||
city: v.locality,
|
city: v.locality,
|
||||||
state: v.region,
|
state: v.region,
|
||||||
@@ -263,72 +269,3 @@ function single(list: string[] | undefined): string | string[] | undefined {
|
|||||||
if (!list?.length) return undefined;
|
if (!list?.length) return undefined;
|
||||||
return list.length === 1 ? list[0] : list;
|
return list.length === 1 ? list[0] : list;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface ParsedVCard {
|
|
||||||
fn?: string;
|
|
||||||
org?: string;
|
|
||||||
email?: string[];
|
|
||||||
tel?: string[];
|
|
||||||
fax?: string[];
|
|
||||||
url?: string;
|
|
||||||
street?: string[];
|
|
||||||
locality?: string;
|
|
||||||
region?: string;
|
|
||||||
postcode?: string;
|
|
||||||
country?: string;
|
|
||||||
countryCode?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Parse a minimal subset of vCard 4.0 arrays as used in RDAP "vcardArray" fields
|
|
||||||
function parseVcard(vcardArray: unknown): ParsedVCard {
|
|
||||||
// vcardArray is typically ["vcard", [["version",{} ,"text","4.0"], ["fn",{} ,"text","Example"], ...]]
|
|
||||||
if (!Array.isArray(vcardArray) || vcardArray[0] !== "vcard" || !Array.isArray(vcardArray[1]))
|
|
||||||
return {};
|
|
||||||
const entries = vcardArray[1] as Array<[string, Record<string, unknown>, string, unknown]>;
|
|
||||||
const out: ParsedVCard = {};
|
|
||||||
for (const e of entries) {
|
|
||||||
const key = e?.[0];
|
|
||||||
const value = e?.[3];
|
|
||||||
if (!key) continue;
|
|
||||||
switch (String(key).toLowerCase()) {
|
|
||||||
case "fn":
|
|
||||||
out.fn = asString(value);
|
|
||||||
break;
|
|
||||||
case "org":
|
|
||||||
out.org = Array.isArray(value) ? value.map((x) => String(x)).join(" ") : asString(value);
|
|
||||||
break;
|
|
||||||
case "email": {
|
|
||||||
const v = asString(value);
|
|
||||||
if (v) (out.email ??= []).push(v);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
case "url":
|
|
||||||
out.url = asString(value);
|
|
||||||
break;
|
|
||||||
case "adr": {
|
|
||||||
// adr value is [postOfficeBox, extendedAddress, street, locality, region, postalCode, country]
|
|
||||||
if (Array.isArray(value)) {
|
|
||||||
out.street = value[2] ? String(value[2]).split(/\r?\n/).filter(Boolean) : undefined;
|
|
||||||
out.locality = asString(value[3]);
|
|
||||||
out.region = asString(value[4]);
|
|
||||||
out.postcode = asString(value[5]);
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -0,0 +1,62 @@
|
|||||||
|
import { expect, test } from "vitest";
|
||||||
|
import { paramList, parseVcard, readJCard } from "./vcard";
|
||||||
|
|
||||||
|
const jcard = (props: unknown[]) => ["vcard", [["version", {}, "text", "4.0"], ...props]];
|
||||||
|
|
||||||
|
test("readJCard tolerates malformed input", () => {
|
||||||
|
expect(readJCard(undefined)).toEqual([]);
|
||||||
|
expect(readJCard(["vcard", "nope"])).toEqual([]);
|
||||||
|
expect(readJCard(["vcard", [null, 5, ["fn"], ["FN", {}, "text", "A"]]])).toMatchObject([
|
||||||
|
{ name: "fn", valueType: "text", value: undefined },
|
||||||
|
{ name: "fn", valueType: "text", value: "A" },
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("paramList normalizes string/array params", () => {
|
||||||
|
expect(paramList("FAX")).toEqual(["fax"]);
|
||||||
|
expect(paramList(["Work", "fax"])).toEqual(["work", "fax"]);
|
||||||
|
expect(paramList(undefined)).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parseVcard splits org levels and reads kind/title/role", () => {
|
||||||
|
const v = parseVcard(
|
||||||
|
jcard([
|
||||||
|
["kind", {}, "text", "ORG"],
|
||||||
|
["org", {}, "text", ["Acme Corp", "Sales", "EMEA"]],
|
||||||
|
["title", {}, "text", "Head of Domains"],
|
||||||
|
["role", {}, "text", "Admin"],
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
expect(v).toMatchObject({
|
||||||
|
kind: "org",
|
||||||
|
org: "Acme Corp",
|
||||||
|
orgUnits: ["Sales", "EMEA"],
|
||||||
|
title: "Head of Domains",
|
||||||
|
role: "Admin",
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parseVcard handles string org and ignores unknown kind", () => {
|
||||||
|
const v = parseVcard(
|
||||||
|
jcard([
|
||||||
|
["kind", {}, "text", "robot"],
|
||||||
|
["org", {}, "text", "Solo Inc"],
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
expect(v.kind).toBeUndefined();
|
||||||
|
expect(v.org).toBe("Solo Inc");
|
||||||
|
expect(v.orgUnits).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
test("parseVcard keeps PO box and extended address, and strips tel: URIs", () => {
|
||||||
|
const v = parseVcard(
|
||||||
|
jcard([
|
||||||
|
["adr", { cc: "nl" }, "text", ["PO Box 1", "Suite 5", "1 Main St", "Town", "", "1000", "NL"]],
|
||||||
|
["tel", { type: "voice" }, "uri", "tel:+31.201234567"],
|
||||||
|
]),
|
||||||
|
);
|
||||||
|
expect(v.poBox).toBe("PO Box 1");
|
||||||
|
expect(v.street).toEqual(["Suite 5", "1 Main St"]);
|
||||||
|
expect(v.countryCode).toBe("NL");
|
||||||
|
expect(v.tel).toEqual(["+31.201234567"]);
|
||||||
|
});
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
import { asString } from "../lib/text";
|
||||||
|
|
||||||
|
/** One property of a jCard (RFC 7095): [name, params, valueType, ...values]. */
|
||||||
|
export interface VCardProp {
|
||||||
|
name: string;
|
||||||
|
params: Record<string, unknown>;
|
||||||
|
valueType: string;
|
||||||
|
value: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type VCardKind = "individual" | "org" | "group" | "location";
|
||||||
|
|
||||||
|
const KINDS: readonly string[] = ["individual", "org", "group", "location"];
|
||||||
|
|
||||||
|
/** Flatten an RDAP "vcardArray" into a property list. Tolerates malformed input. */
|
||||||
|
export function readJCard(vcardArray: unknown): VCardProp[] {
|
||||||
|
// vcardArray is ["vcard", [["version", {}, "text", "4.0"], ["fn", {}, "text", "Example"], ...]]
|
||||||
|
if (!Array.isArray(vcardArray) || vcardArray[0] !== "vcard" || !Array.isArray(vcardArray[1])) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const out: VCardProp[] = [];
|
||||||
|
for (const raw of vcardArray[1] as unknown[]) {
|
||||||
|
if (!Array.isArray(raw) || typeof raw[0] !== "string") continue;
|
||||||
|
const params = raw[1] && typeof raw[1] === "object" ? (raw[1] as Record<string, unknown>) : {};
|
||||||
|
out.push({
|
||||||
|
name: raw[0].toLowerCase(),
|
||||||
|
params,
|
||||||
|
valueType: typeof raw[2] === "string" ? raw[2].toLowerCase() : "text",
|
||||||
|
value: raw[3],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** A jCard parameter (e.g. TYPE) as a lowercase string list; it may be a string or an array. */
|
||||||
|
export function paramList(param: unknown): string[] {
|
||||||
|
const list = Array.isArray(param) ? param : param === undefined ? [] : [param];
|
||||||
|
return list.filter((x) => typeof x === "string").map((x) => (x as string).toLowerCase());
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Element `i` of a structured value, as a non-empty string. */
|
||||||
|
export function part(value: unknown, i: number): string | undefined {
|
||||||
|
return Array.isArray(value) ? asString(value[i]) || undefined : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Text value; `tel:` URIs (RFC 6350 allows TEL as uri) are reduced to the number. */
|
||||||
|
function textValue(p: VCardProp): string | undefined {
|
||||||
|
const v = asString(p.value);
|
||||||
|
if (!v) return undefined;
|
||||||
|
return p.valueType === "uri" || /^tel:/i.test(v) ? v.replace(/^tel:/i, "") : v;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ParsedVCard {
|
||||||
|
fn?: string;
|
||||||
|
kind?: VCardKind;
|
||||||
|
/** First ORG level: the organization name */
|
||||||
|
org?: string;
|
||||||
|
/** Remaining ORG levels (organizational units) */
|
||||||
|
orgUnits?: string[];
|
||||||
|
title?: string;
|
||||||
|
role?: string;
|
||||||
|
email?: string[];
|
||||||
|
tel?: string[];
|
||||||
|
fax?: string[];
|
||||||
|
url?: string;
|
||||||
|
poBox?: string;
|
||||||
|
street?: string[];
|
||||||
|
locality?: string;
|
||||||
|
region?: string;
|
||||||
|
postcode?: string;
|
||||||
|
country?: string;
|
||||||
|
countryCode?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lines = (s: string | undefined) => (s ? s.split(/\r?\n/).filter(Boolean) : []);
|
||||||
|
|
||||||
|
/** Extract the fields rdapper cares about from an RDAP "vcardArray". */
|
||||||
|
export function parseVcard(vcardArray: unknown): ParsedVCard {
|
||||||
|
const out: ParsedVCard = {};
|
||||||
|
for (const p of readJCard(vcardArray)) {
|
||||||
|
switch (p.name) {
|
||||||
|
case "fn":
|
||||||
|
out.fn ??= asString(p.value);
|
||||||
|
break;
|
||||||
|
case "kind": {
|
||||||
|
const k = asString(p.value)?.toLowerCase();
|
||||||
|
if (k && KINDS.includes(k)) out.kind ??= k as VCardKind;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "org": {
|
||||||
|
if (out.org !== undefined) break;
|
||||||
|
// ORG is structured: a string, or [name, unit, unit, ...]
|
||||||
|
const levels = (Array.isArray(p.value) ? p.value : [p.value])
|
||||||
|
.filter((x): x is string => typeof x === "string")
|
||||||
|
.map((x) => x.trim())
|
||||||
|
.filter(Boolean);
|
||||||
|
if (levels.length) {
|
||||||
|
out.org = levels[0];
|
||||||
|
if (levels.length > 1) out.orgUnits = levels.slice(1);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "title":
|
||||||
|
out.title ??= asString(p.value);
|
||||||
|
break;
|
||||||
|
case "role":
|
||||||
|
out.role ??= asString(p.value);
|
||||||
|
break;
|
||||||
|
case "email": {
|
||||||
|
const v = asString(p.value);
|
||||||
|
if (v) (out.email ??= []).push(v);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "tel": {
|
||||||
|
const v = textValue(p);
|
||||||
|
if (!v) break;
|
||||||
|
// TYPE may be a string or an array (e.g. "fax", ["work", "fax"])
|
||||||
|
(paramList(p.params.type).includes("fax") ? (out.fax ??= []) : (out.tel ??= [])).push(v);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "url":
|
||||||
|
out.url ??= asString(p.value);
|
||||||
|
break;
|
||||||
|
case "adr": {
|
||||||
|
if (!Array.isArray(p.value) || out.country !== undefined || out.locality !== undefined) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
// [postOfficeBox, extendedAddress, street, locality, region, postalCode, country]
|
||||||
|
out.poBox = part(p.value, 0);
|
||||||
|
const street = [...lines(part(p.value, 1)), ...lines(part(p.value, 2))];
|
||||||
|
out.street = street.length ? street : undefined;
|
||||||
|
out.locality = part(p.value, 3);
|
||||||
|
out.region = part(p.value, 4);
|
||||||
|
out.postcode = part(p.value, 5);
|
||||||
|
out.country = part(p.value, 6);
|
||||||
|
// RFC 8605: ISO 3166-1 alpha-2 code lives in the "cc" parameter
|
||||||
|
const cc = asString(p.params.cc);
|
||||||
|
if (cc) out.countryCode = cc.toUpperCase();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
}
|
||||||
+64
-24
@@ -23,13 +23,17 @@ export interface RegistrarInfo {
|
|||||||
email?: string;
|
email?: string;
|
||||||
/** Registrar contact phone number */
|
/** Registrar contact phone number */
|
||||||
phone?: string;
|
phone?: string;
|
||||||
/** Registrar street address lines */
|
/** Street address lines (RDAP only) */
|
||||||
street?: string[];
|
street?: string[];
|
||||||
|
/** City or locality (RDAP only) */
|
||||||
city?: string;
|
city?: string;
|
||||||
|
/** State, province, or region (RDAP only) */
|
||||||
state?: string;
|
state?: string;
|
||||||
|
/** Postal or ZIP code (RDAP only) */
|
||||||
postalCode?: string;
|
postalCode?: string;
|
||||||
|
/** Country name (RDAP only) */
|
||||||
country?: string;
|
country?: string;
|
||||||
/** ISO 3166-1 alpha-2 country code, when provided */
|
/** ISO 3166-1 alpha-2 country code, from the vCard `cc` parameter or resolved from `country` (RDAP only) */
|
||||||
countryCode?: string;
|
countryCode?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -41,6 +45,7 @@ export interface RegistrarInfo {
|
|||||||
* varies by TLD, registrar, and privacy policies (GDPR, WHOIS privacy services).
|
* varies by TLD, registrar, and privacy policies (GDPR, WHOIS privacy services).
|
||||||
*/
|
*/
|
||||||
export interface Contact {
|
export interface Contact {
|
||||||
|
/** Role this contact plays for the domain */
|
||||||
type:
|
type:
|
||||||
| "registrant"
|
| "registrant"
|
||||||
| "admin"
|
| "admin"
|
||||||
@@ -50,19 +55,39 @@ export interface Contact {
|
|||||||
| "registrar"
|
| "registrar"
|
||||||
| "reseller"
|
| "reseller"
|
||||||
| "unknown";
|
| "unknown";
|
||||||
|
/** Contact name as reported: usually a person, sometimes an organization */
|
||||||
name?: string;
|
name?: string;
|
||||||
|
/** vCard KIND, when the registry provides it (RDAP only) */
|
||||||
|
kind?: "individual" | "org" | "group" | "location";
|
||||||
|
/** Organization name (the first vCard ORG level in RDAP) */
|
||||||
organization?: string;
|
organization?: string;
|
||||||
|
/** Organizational units below `organization` (vCard ORG levels 2+, RDAP only) */
|
||||||
|
organizationUnits?: string[];
|
||||||
|
/** Job title (vCard TITLE, RDAP only) */
|
||||||
|
title?: string;
|
||||||
|
/** Role within the organization (vCard ROLE, RDAP only) */
|
||||||
|
role?: string;
|
||||||
|
/** Email address; an array when the source lists several. Placeholder text is dropped. */
|
||||||
email?: string | string[];
|
email?: string | string[];
|
||||||
|
/** Voice phone number; an array when the source lists several. Placeholder text is dropped. */
|
||||||
phone?: string | string[];
|
phone?: string | string[];
|
||||||
|
/** Fax number; an array when the source lists several */
|
||||||
fax?: string | string[];
|
fax?: string | string[];
|
||||||
|
/** Post office box (RDAP only) */
|
||||||
|
poBox?: string;
|
||||||
|
/** Street address lines */
|
||||||
street?: string[];
|
street?: string[];
|
||||||
|
/** City or locality */
|
||||||
city?: string;
|
city?: string;
|
||||||
|
/** State, province, or region */
|
||||||
state?: string;
|
state?: string;
|
||||||
|
/** Postal or ZIP code */
|
||||||
postalCode?: string;
|
postalCode?: string;
|
||||||
|
/** Country name; filled from `countryCode` when only the code is given */
|
||||||
country?: string;
|
country?: string;
|
||||||
/** ISO 3166-1 alpha-2 country code */
|
/** ISO 3166-1 alpha-2 country code; filled from `country` when it can be resolved */
|
||||||
countryCode?: string;
|
countryCode?: string;
|
||||||
/** True when some of this contact's data was redacted, withheld, or replaced by a placeholder */
|
/** True when any of this contact's data was redacted, withheld, or replaced by a placeholder */
|
||||||
redacted?: boolean;
|
redacted?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -111,7 +136,7 @@ export interface Nameserver {
|
|||||||
* @see {@link https://www.icann.org/resources/pages/epp-status-codes-2014-06-16-en ICANN EPP Status Codes}
|
* @see {@link https://www.icann.org/resources/pages/epp-status-codes-2014-06-16-en ICANN EPP Status Codes}
|
||||||
*/
|
*/
|
||||||
export interface StatusEvent {
|
export interface StatusEvent {
|
||||||
/** Normalized status code (e.g., "clientTransferProhibited") */
|
/** Status code (e.g., "clientTransferProhibited") */
|
||||||
status: string;
|
status: string;
|
||||||
/** Human-readable description of the status, if available */
|
/** Human-readable description of the status, if available */
|
||||||
description?: string;
|
description?: string;
|
||||||
@@ -145,43 +170,49 @@ export interface StatusEvent {
|
|||||||
* ```
|
* ```
|
||||||
*/
|
*/
|
||||||
export interface DomainRecord {
|
export interface DomainRecord {
|
||||||
/** Normalized domain name */
|
/** Domain name (Unicode form when available, otherwise as queried) */
|
||||||
domain: string;
|
domain: string;
|
||||||
/** Terminal TLD */
|
/** Top-level domain the lookup was routed by (e.g., "com") */
|
||||||
tld: string;
|
tld: string;
|
||||||
/** Whether the domain is registered */
|
/** Whether the domain is registered */
|
||||||
isRegistered: boolean;
|
isRegistered: boolean;
|
||||||
/** Whether the domain is internationalized (IDN) */
|
/** Whether the domain is internationalized (IDN) */
|
||||||
isIDN?: boolean;
|
isIDN?: boolean;
|
||||||
/** Unicode name */
|
/** Unicode (IDN) form of the name, when provided */
|
||||||
unicodeName?: string;
|
unicodeName?: string;
|
||||||
/** Punycode name */
|
/** Punycode (ASCII) form of the name, when provided */
|
||||||
punycodeName?: string;
|
punycodeName?: string;
|
||||||
/** Registry operator */
|
/** Registry operator name (rarely available) */
|
||||||
registry?: string;
|
registry?: string;
|
||||||
/** Registrar */
|
/** Registrar responsible for the registration */
|
||||||
registrar?: RegistrarInfo;
|
registrar?: RegistrarInfo;
|
||||||
/** Reseller (if applicable) */
|
/** Reseller name, if the source provides one */
|
||||||
reseller?: string;
|
reseller?: string;
|
||||||
/** EPP status codes */
|
/** EPP status codes and registry-specific statuses */
|
||||||
statuses?: StatusEvent[];
|
statuses?: StatusEvent[];
|
||||||
/** Creation date in ISO 8601 */
|
/** When the domain was registered (ISO 8601) */
|
||||||
creationDate?: string;
|
creationDate?: string;
|
||||||
/** Updated date in ISO 8601 */
|
/** When the record was last changed (ISO 8601) */
|
||||||
updatedDate?: string;
|
updatedDate?: string;
|
||||||
/** Expiration date in ISO 8601 */
|
/** When the registration expires (ISO 8601) */
|
||||||
expirationDate?: string;
|
expirationDate?: string;
|
||||||
/** Deletion date in ISO 8601 */
|
/** When the domain was or will be deleted (ISO 8601), if reported (RDAP only) */
|
||||||
deletionDate?: string;
|
deletionDate?: string;
|
||||||
/** Transfer lock */
|
/** Whether a transfer-prohibited status is set (client or server) */
|
||||||
transferLock?: boolean;
|
transferLock?: boolean;
|
||||||
/** DNSSEC data (if available) */
|
/** DNSSEC data (if available) */
|
||||||
dnssec?: {
|
dnssec?: {
|
||||||
|
/** Whether the delegation is signed */
|
||||||
enabled: boolean;
|
enabled: boolean;
|
||||||
|
/** DS records published for the delegation */
|
||||||
dsRecords?: Array<{
|
dsRecords?: Array<{
|
||||||
|
/** Key tag */
|
||||||
keyTag?: number;
|
keyTag?: number;
|
||||||
|
/** DNSSEC algorithm number */
|
||||||
algorithm?: number;
|
algorithm?: number;
|
||||||
|
/** Digest type number */
|
||||||
digestType?: number;
|
digestType?: number;
|
||||||
|
/** Digest, as a hex string */
|
||||||
digest?: string;
|
digest?: string;
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
@@ -189,21 +220,21 @@ export interface DomainRecord {
|
|||||||
nameservers?: Nameserver[];
|
nameservers?: Nameserver[];
|
||||||
/** Contacts (registrant, admin, tech, billing, abuse, etc.) */
|
/** Contacts (registrant, admin, tech, billing, abuse, etc.) */
|
||||||
contacts?: Contact[];
|
contacts?: Contact[];
|
||||||
/** Best guess as to whether registrant is redacted based on keywords */
|
/** Best guess that the registrant is hidden behind a privacy service or redacted, from name/organization phrases or RFC 9537 redactions */
|
||||||
privacyEnabled?: boolean;
|
privacyEnabled?: boolean;
|
||||||
/** RFC 9537 redaction metadata reported by RDAP, if any */
|
/** RFC 9537 redaction metadata reported by RDAP, if any */
|
||||||
redactions?: Redaction[];
|
redactions?: Redaction[];
|
||||||
/** Authoritative WHOIS queried (if any) */
|
/** WHOIS server that answered, or the RDAP `port43` pointer (if any) */
|
||||||
whoisServer?: string;
|
whoisServer?: string;
|
||||||
/** RDAP base URLs tried */
|
/** RDAP base URLs tried */
|
||||||
rdapServers?: string[];
|
rdapServers?: string[];
|
||||||
/** Raw RDAP JSON */
|
/** Raw RDAP JSON (only with `includeRaw`) */
|
||||||
rawRdap?: unknown;
|
rawRdap?: unknown;
|
||||||
/** Raw WHOIS text (last authoritative) */
|
/** Raw WHOIS text from the last authoritative server (only with `includeRaw`) */
|
||||||
rawWhois?: string;
|
rawWhois?: string;
|
||||||
/** Which source produced data */
|
/** Which source produced the data */
|
||||||
source: LookupSource;
|
source: LookupSource;
|
||||||
/** Warnings generated during lookup */
|
/** Non-fatal warnings from the lookup (currently WHOIS referral problems, e.g. a skipped unsafe host or a referral that returned no data) */
|
||||||
warnings?: string[];
|
warnings?: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -419,6 +450,8 @@ export interface LookupResult {
|
|||||||
/**
|
/**
|
||||||
* Machine-readable reason a lookup (or one attempt within it) failed.
|
* Machine-readable reason a lookup (or one attempt within it) failed.
|
||||||
*
|
*
|
||||||
|
* - `invalid_input`: the input does not look like a domain name
|
||||||
|
* - `invalid_tld`: the TLD is not valid
|
||||||
* - `timeout`: any timeout, including the overall `deadlineMs`
|
* - `timeout`: any timeout, including the overall `deadlineMs`
|
||||||
* - `aborted`: the caller's `AbortSignal` fired
|
* - `aborted`: the caller's `AbortSignal` fired
|
||||||
* - `connect_failed`: network-level failure (ECONNREFUSED, ECONNRESET, ENOTFOUND, ...)
|
* - `connect_failed`: network-level failure (ECONNREFUSED, ECONNRESET, ENOTFOUND, ...)
|
||||||
@@ -428,7 +461,9 @@ export interface LookupResult {
|
|||||||
* - `rate_limited`: the server throttled the query (RDAP 429, or a WHOIS throttle notice)
|
* - `rate_limited`: the server throttled the query (RDAP 429, or a WHOIS throttle notice)
|
||||||
* - `blocked`: the WHOIS server refuses this client outright (retrying will not help)
|
* - `blocked`: the WHOIS server refuses this client outright (retrying will not help)
|
||||||
* - `unparseable`: WHOIS replied with text that is neither an availability notice nor a record
|
* - `unparseable`: WHOIS replied with text that is neither an availability notice nor a record
|
||||||
|
* - `no_data`: a WHOIS server accepted the connection but closed it without sending anything
|
||||||
* - `unsupported_runtime`: WHOIS needs `node:net`, which this runtime lacks
|
* - `unsupported_runtime`: WHOIS needs `node:net`, which this runtime lacks
|
||||||
|
* - `unknown`: any failure not covered above
|
||||||
*/
|
*/
|
||||||
export type LookupErrorCode =
|
export type LookupErrorCode =
|
||||||
| "invalid_input"
|
| "invalid_input"
|
||||||
@@ -448,12 +483,17 @@ export type LookupErrorCode =
|
|||||||
|
|
||||||
/** One network operation performed during a lookup. */
|
/** One network operation performed during a lookup. */
|
||||||
export interface LookupAttempt {
|
export interface LookupAttempt {
|
||||||
|
/** Step of the lookup this attempt belongs to (IANA bootstrap fetch, RDAP query, RDAP link follow, IANA WHOIS discovery, WHOIS query) */
|
||||||
phase: "rdap_bootstrap" | "rdap" | "rdap_link" | "iana" | "whois";
|
phase: "rdap_bootstrap" | "rdap" | "rdap_link" | "iana" | "whois";
|
||||||
/** RDAP base/link URL or WHOIS host */
|
/** RDAP base/link URL or WHOIS host */
|
||||||
server: string;
|
server: string;
|
||||||
|
/** Whether this attempt succeeded */
|
||||||
ok: boolean;
|
ok: boolean;
|
||||||
|
/** How long the attempt took, in milliseconds */
|
||||||
durationMs: number;
|
durationMs: number;
|
||||||
|
/** Machine-readable failure reason, present when ok is false */
|
||||||
errorCode?: LookupErrorCode;
|
errorCode?: LookupErrorCode;
|
||||||
|
/** Error message, present when ok is false */
|
||||||
error?: string;
|
error?: string;
|
||||||
/** `rate_limited` RDAP failures only: the server's `Retry-After`, in milliseconds */
|
/** `rate_limited` RDAP failures only: the server's `Retry-After`, in milliseconds */
|
||||||
retryAfterMs?: number;
|
retryAfterMs?: number;
|
||||||
|
|||||||
Reference in New Issue
Block a user