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:
|
||||
"registrant" | "admin" | "tech" | "billing" | "abuse" | "registrar" | "reseller" | "unknown";
|
||||
name?: string;
|
||||
kind?: "individual" | "org" | "group" | "location"; // vCard KIND (RDAP only, when provided)
|
||||
organization?: string;
|
||||
organizationUnits?: string[]; // vCard ORG levels below the organization (RDAP only)
|
||||
title?: string; // RDAP only
|
||||
role?: string; // RDAP only
|
||||
email?: string | string[];
|
||||
phone?: string | string[];
|
||||
fax?: string | string[];
|
||||
poBox?: string; // RDAP only
|
||||
street?: string[];
|
||||
city?: string;
|
||||
state?: string;
|
||||
|
||||
@@ -306,3 +306,21 @@ test("normalizeRdap flags contacts with placeholder values or matching redaction
|
||||
expect(tech?.redacted).toBe(true);
|
||||
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 { toISO } from "../lib/dates";
|
||||
import { isPrivacyName } from "../lib/privacy";
|
||||
import { asDateLike, asString, asStringArray, uniq } from "../lib/text";
|
||||
import { parseVcard } from "./vcard";
|
||||
import type { Contact, DomainRecord, Nameserver, Redaction, RegistrarInfo } from "../types";
|
||||
|
||||
type RdapDoc = Record<string, unknown>;
|
||||
@@ -204,8 +206,7 @@ function extractRegistrar(entities: unknown): RegistrarInfo | undefined {
|
||||
city: v.locality,
|
||||
state: v.region,
|
||||
postalCode: v.postcode,
|
||||
country: v.country,
|
||||
countryCode: v.countryCode,
|
||||
...resolveCountry(v.country, v.countryCode),
|
||||
};
|
||||
}
|
||||
return undefined;
|
||||
@@ -242,9 +243,14 @@ function extractContacts(entities: unknown, redactions?: Redaction[]): Contact[]
|
||||
type: roleKey,
|
||||
name: v.fn,
|
||||
organization: v.org,
|
||||
organizationUnits: v.orgUnits,
|
||||
kind: v.kind,
|
||||
title: v.title,
|
||||
role: v.role,
|
||||
email: single(v.email),
|
||||
phone: single(v.tel),
|
||||
fax: single(v.fax),
|
||||
poBox: v.poBox,
|
||||
street: v.street,
|
||||
city: v.locality,
|
||||
state: v.region,
|
||||
@@ -263,72 +269,3 @@ function single(list: string[] | undefined): string | string[] | undefined {
|
||||
if (!list?.length) return undefined;
|
||||
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;
|
||||
/** Registrar contact phone number */
|
||||
phone?: string;
|
||||
/** Registrar street address lines */
|
||||
/** Street address lines (RDAP only) */
|
||||
street?: string[];
|
||||
/** City or locality (RDAP only) */
|
||||
city?: string;
|
||||
/** State, province, or region (RDAP only) */
|
||||
state?: string;
|
||||
/** Postal or ZIP code (RDAP only) */
|
||||
postalCode?: string;
|
||||
/** Country name (RDAP only) */
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -41,6 +45,7 @@ export interface RegistrarInfo {
|
||||
* varies by TLD, registrar, and privacy policies (GDPR, WHOIS privacy services).
|
||||
*/
|
||||
export interface Contact {
|
||||
/** Role this contact plays for the domain */
|
||||
type:
|
||||
| "registrant"
|
||||
| "admin"
|
||||
@@ -50,19 +55,39 @@ export interface Contact {
|
||||
| "registrar"
|
||||
| "reseller"
|
||||
| "unknown";
|
||||
/** Contact name as reported: usually a person, sometimes an organization */
|
||||
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;
|
||||
/** 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[];
|
||||
/** Voice phone number; an array when the source lists several. Placeholder text is dropped. */
|
||||
phone?: string | string[];
|
||||
/** Fax number; an array when the source lists several */
|
||||
fax?: string | string[];
|
||||
/** Post office box (RDAP only) */
|
||||
poBox?: string;
|
||||
/** Street address lines */
|
||||
street?: string[];
|
||||
/** City or locality */
|
||||
city?: string;
|
||||
/** State, province, or region */
|
||||
state?: string;
|
||||
/** Postal or ZIP code */
|
||||
postalCode?: string;
|
||||
/** Country name; filled from `countryCode` when only the code is given */
|
||||
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;
|
||||
/** 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;
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
*/
|
||||
export interface StatusEvent {
|
||||
/** Normalized status code (e.g., "clientTransferProhibited") */
|
||||
/** Status code (e.g., "clientTransferProhibited") */
|
||||
status: string;
|
||||
/** Human-readable description of the status, if available */
|
||||
description?: string;
|
||||
@@ -145,43 +170,49 @@ export interface StatusEvent {
|
||||
* ```
|
||||
*/
|
||||
export interface DomainRecord {
|
||||
/** Normalized domain name */
|
||||
/** Domain name (Unicode form when available, otherwise as queried) */
|
||||
domain: string;
|
||||
/** Terminal TLD */
|
||||
/** Top-level domain the lookup was routed by (e.g., "com") */
|
||||
tld: string;
|
||||
/** Whether the domain is registered */
|
||||
isRegistered: boolean;
|
||||
/** Whether the domain is internationalized (IDN) */
|
||||
isIDN?: boolean;
|
||||
/** Unicode name */
|
||||
/** Unicode (IDN) form of the name, when provided */
|
||||
unicodeName?: string;
|
||||
/** Punycode name */
|
||||
/** Punycode (ASCII) form of the name, when provided */
|
||||
punycodeName?: string;
|
||||
/** Registry operator */
|
||||
/** Registry operator name (rarely available) */
|
||||
registry?: string;
|
||||
/** Registrar */
|
||||
/** Registrar responsible for the registration */
|
||||
registrar?: RegistrarInfo;
|
||||
/** Reseller (if applicable) */
|
||||
/** Reseller name, if the source provides one */
|
||||
reseller?: string;
|
||||
/** EPP status codes */
|
||||
/** EPP status codes and registry-specific statuses */
|
||||
statuses?: StatusEvent[];
|
||||
/** Creation date in ISO 8601 */
|
||||
/** When the domain was registered (ISO 8601) */
|
||||
creationDate?: string;
|
||||
/** Updated date in ISO 8601 */
|
||||
/** When the record was last changed (ISO 8601) */
|
||||
updatedDate?: string;
|
||||
/** Expiration date in ISO 8601 */
|
||||
/** When the registration expires (ISO 8601) */
|
||||
expirationDate?: string;
|
||||
/** Deletion date in ISO 8601 */
|
||||
/** When the domain was or will be deleted (ISO 8601), if reported (RDAP only) */
|
||||
deletionDate?: string;
|
||||
/** Transfer lock */
|
||||
/** Whether a transfer-prohibited status is set (client or server) */
|
||||
transferLock?: boolean;
|
||||
/** DNSSEC data (if available) */
|
||||
dnssec?: {
|
||||
/** Whether the delegation is signed */
|
||||
enabled: boolean;
|
||||
/** DS records published for the delegation */
|
||||
dsRecords?: Array<{
|
||||
/** Key tag */
|
||||
keyTag?: number;
|
||||
/** DNSSEC algorithm number */
|
||||
algorithm?: number;
|
||||
/** Digest type number */
|
||||
digestType?: number;
|
||||
/** Digest, as a hex string */
|
||||
digest?: string;
|
||||
}>;
|
||||
};
|
||||
@@ -189,21 +220,21 @@ export interface DomainRecord {
|
||||
nameservers?: Nameserver[];
|
||||
/** Contacts (registrant, admin, tech, billing, abuse, etc.) */
|
||||
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;
|
||||
/** RFC 9537 redaction metadata reported by RDAP, if any */
|
||||
redactions?: Redaction[];
|
||||
/** Authoritative WHOIS queried (if any) */
|
||||
/** WHOIS server that answered, or the RDAP `port43` pointer (if any) */
|
||||
whoisServer?: string;
|
||||
/** RDAP base URLs tried */
|
||||
rdapServers?: string[];
|
||||
/** Raw RDAP JSON */
|
||||
/** Raw RDAP JSON (only with `includeRaw`) */
|
||||
rawRdap?: unknown;
|
||||
/** Raw WHOIS text (last authoritative) */
|
||||
/** Raw WHOIS text from the last authoritative server (only with `includeRaw`) */
|
||||
rawWhois?: string;
|
||||
/** Which source produced data */
|
||||
/** Which source produced the data */
|
||||
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[];
|
||||
}
|
||||
|
||||
@@ -419,6 +450,8 @@ export interface LookupResult {
|
||||
/**
|
||||
* 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`
|
||||
* - `aborted`: the caller's `AbortSignal` fired
|
||||
* - `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)
|
||||
* - `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
|
||||
* - `no_data`: a WHOIS server accepted the connection but closed it without sending anything
|
||||
* - `unsupported_runtime`: WHOIS needs `node:net`, which this runtime lacks
|
||||
* - `unknown`: any failure not covered above
|
||||
*/
|
||||
export type LookupErrorCode =
|
||||
| "invalid_input"
|
||||
@@ -448,12 +483,17 @@ export type LookupErrorCode =
|
||||
|
||||
/** One network operation performed during a lookup. */
|
||||
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";
|
||||
/** RDAP base/link URL or WHOIS host */
|
||||
server: string;
|
||||
/** Whether this attempt succeeded */
|
||||
ok: boolean;
|
||||
/** How long the attempt took, in milliseconds */
|
||||
durationMs: number;
|
||||
/** Machine-readable failure reason, present when ok is false */
|
||||
errorCode?: LookupErrorCode;
|
||||
/** Error message, present when ok is false */
|
||||
error?: string;
|
||||
/** `rate_limited` RDAP failures only: the server's `Retry-After`, in milliseconds */
|
||||
retryAfterMs?: number;
|
||||
|
||||
Reference in New Issue
Block a user