chore: replace Biome with oxlint and oxfmt

This commit is contained in:
2026-09-18 14:21:15 -04:00
parent 5ff837b2bb
commit 87e20f4a47
28 changed files with 1154 additions and 629 deletions
+3 -9
View File
@@ -15,9 +15,7 @@ maybeTest("lookup smoke test (example.com)", async () => {
expect(res.ok, res.error).toBe(true);
expect(Boolean(res.record?.domain)).toBe(true);
expect(Boolean(res.record?.tld)).toBe(true);
expect(res.record?.source === "rdap" || res.record?.source === "whois").toBe(
true,
);
expect(res.record?.source === "rdap" || res.record?.source === "whois").toBe(true);
});
// RDAP-only smoke for reserved example domains (.com/.net/.org)
@@ -42,9 +40,7 @@ for (const c of rdapCases) {
if (c.tld !== "org") {
// .com/.net often include the IANA reserved name explicitly
expect(
(rec.registrar?.name || "")
.toLowerCase()
.includes("internet assigned numbers authority"),
(rec.registrar?.name || "").toLowerCase().includes("internet assigned numbers authority"),
).toBe(true);
}
// IANA nameservers
@@ -112,9 +108,7 @@ maybeTest("WHOIS-only lookup for example.io", async () => {
});
maybeTest("isRegistered true for example.com", async () => {
await expect(isRegistered("example.com", { timeoutMs: 15000 })).resolves.toBe(
true,
);
await expect(isRegistered("example.com", { timeoutMs: 15000 })).resolves.toBe(true);
});
maybeTest("isAvailable true for an unlikely .com", async () => {
+9 -21
View File
@@ -30,18 +30,13 @@ vi.mock("./whois/referral.js", async () => {
const client = await import("./whois/client.js");
return {
followWhoisReferrals: vi.fn(
async (
server: string,
domain: string,
opts?: import("./types").LookupOptions,
) => client.whoisQuery(server, domain, opts),
async (server: string, domain: string, opts?: import("./types").LookupOptions) =>
client.whoisQuery(server, domain, opts),
),
collectWhoisReferralChain: vi.fn(
async (
server: string,
domain: string,
opts?: import("./types").LookupOptions,
) => [await client.whoisQuery(server, domain, opts)],
async (server: string, domain: string, opts?: import("./types").LookupOptions) => [
await client.whoisQuery(server, domain, opts),
],
),
};
});
@@ -55,8 +50,7 @@ vi.mock("./whois/discovery.js", async () => {
});
vi.mock("./lib/domain.js", async () => {
const actual =
await vi.importActual<typeof import("./lib/domain.js")>("./lib/domain.js");
const actual = await vi.importActual<typeof import("./lib/domain.js")>("./lib/domain.js");
return {
...actual,
// Default to actual behavior; specific tests can override
@@ -75,9 +69,7 @@ import * as whoisReferral from "./whois/referral";
describe("lookup orchestration", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(discovery.ianaWhoisServerForTld).mockResolvedValue(
"whois.verisign-grs.com",
);
vi.mocked(discovery.ianaWhoisServerForTld).mockResolvedValue("whois.verisign-grs.com");
});
it("uses RDAP when available and does not call WHOIS", async () => {
@@ -89,9 +81,7 @@ describe("lookup orchestration", () => {
});
it("falls back to WHOIS when RDAP fails", async () => {
vi.mocked(rdapClient.fetchRdapDomain).mockRejectedValueOnce(
new Error("rdap down"),
);
vi.mocked(rdapClient.fetchRdapDomain).mockRejectedValueOnce(new Error("rdap down"));
const res = await lookup("example.com", { timeoutMs: 200 });
expect(res.ok, res.error).toBe(true);
expect(res.record?.source).toBe("whois");
@@ -155,9 +145,7 @@ describe("RDAP 404 handling", () => {
describe("WHOIS referral & includeRaw", () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(discovery.ianaWhoisServerForTld).mockResolvedValue(
"whois.verisign-grs.com",
);
vi.mocked(discovery.ianaWhoisServerForTld).mockResolvedValue("whois.verisign-grs.com");
});
it("does not follow referral when followWhoisReferral is false", async () => {
+7 -30
View File
@@ -11,19 +11,13 @@ import {
} from "./whois/discovery";
import { mergeWhoisRecords } from "./whois/merge";
import { normalizeWhois } from "./whois/normalize";
import {
collectWhoisReferralChain,
followWhoisReferrals,
} from "./whois/referral";
import { collectWhoisReferralChain, followWhoisReferrals } from "./whois/referral";
/**
* High-level lookup that prefers RDAP and falls back to WHOIS.
* Ensures a standardized DomainRecord, independent of the source.
*/
export async function lookup(
domain: string,
opts?: LookupOptions,
): Promise<LookupResult> {
export async function lookup(domain: string, opts?: LookupOptions): Promise<LookupResult> {
try {
if (!isLikelyDomain(domain)) {
return { ok: false, error: "Input does not look like a domain" };
@@ -61,11 +55,7 @@ export async function lookup(
return { ok: true, record };
}
const rdapEnriched = await fetchAndMergeRdapRelated(
domain,
json,
opts,
);
const rdapEnriched = await fetchAndMergeRdapRelated(domain, json, opts);
const record: DomainRecord = normalizeRdap(
domain,
tld,
@@ -92,9 +82,7 @@ export async function lookup(
if (!whoisServer) {
// Provide a clearer, actionable message
const ianaText = await getIanaWhoisTextForTld(tld, opts);
const regUrl = ianaText
? parseIanaRegistrationInfoUrl(ianaText)
: undefined;
const regUrl = ianaText ? parseIanaRegistrationInfoUrl(ianaText) : undefined;
const hint = regUrl ? ` See registration info at ${regUrl}.` : "";
return {
ok: false,
@@ -138,10 +126,7 @@ export async function lookup(
* Determine if a domain appears available (not registered).
* Performs a lookup and resolves to a boolean. Rejects on lookup error.
*/
export async function isAvailable(
domain: string,
opts?: LookupOptions,
): Promise<boolean> {
export async function isAvailable(domain: string, opts?: LookupOptions): Promise<boolean> {
const res = await lookup(domain, opts);
if (!res.ok || !res.record) throw new Error(res.error || "Lookup failed");
return res.record.isRegistered === false;
@@ -151,10 +136,7 @@ export async function isAvailable(
* Determine if a domain appears registered.
* Performs a lookup and resolves to a boolean. Rejects on lookup error.
*/
export async function isRegistered(
domain: string,
opts?: LookupOptions,
): Promise<boolean> {
export async function isRegistered(domain: string, opts?: LookupOptions): Promise<boolean> {
const res = await lookup(domain, opts);
if (!res.ok || !res.record) throw new Error(res.error || "Lookup failed");
return res.record.isRegistered === true;
@@ -165,10 +147,5 @@ export async function isRegistered(
*/
export const lookupDomain = lookup;
export {
getDomainParts,
getDomainTld,
isLikelyDomain,
toRegistrableDomain,
} from "./lib/domain";
export { getDomainParts, getDomainTld, isLikelyDomain, toRegistrableDomain } from "./lib/domain";
export type * from "./types";
+3 -15
View File
@@ -1,8 +1,6 @@
// Lightweight date parsing helpers to avoid external dependencies.
// We aim to parse common RDAP and WHOIS date representations and return a UTC ISO string.
export function toISO(
dateLike: string | number | Date | undefined | null,
): string | undefined {
export function toISO(dateLike: string | number | Date | undefined | null): string | undefined {
if (dateLike == null) return undefined;
if (dateLike instanceof Date) return toIsoFromDate(dateLike);
if (typeof dateLike === "number") return toIsoFromDate(new Date(dateLike));
@@ -53,10 +51,7 @@ function toIsoFromDate(d: Date): string | undefined {
}
}
function parseDateWithRegex(
m: RegExpMatchArray,
_re: RegExp,
): Date | undefined {
function parseDateWithRegex(m: RegExpMatchArray, _re: RegExp): Date | undefined {
const monthMap: Record<string, number> = {
jan: 0,
feb: 1,
@@ -77,14 +72,7 @@ function parseDateWithRegex(
const [_, y, mo, d, hh, mm, ss, offH, offM] = m;
if (!y || !mo || !d || !hh || !mm || !ss) return undefined;
// Base time as UTC
let dt = Date.UTC(
Number(y),
Number(mo) - 1,
Number(d),
Number(hh),
Number(mm),
Number(ss),
);
let dt = Date.UTC(Number(y), Number(mo) - 1, Number(d), Number(hh), Number(mm), Number(ss));
// Apply timezone offset if present (e.g., +0000, -0500, +05:30)
if (offH) {
const sign = offH.startsWith("-") ? -1 : 1;
+2 -6
View File
@@ -14,15 +14,11 @@ test("isLikelyDomain", () => {
test("toRegistrableDomain normalizes eTLD+1 and rejects non-ICANN", () => {
// Basic domains
expect(toRegistrableDomain("example.com")).toBe("example.com");
expect(toRegistrableDomain("http://www.writethedocs.org/conf")).toBe(
"writethedocs.org",
);
expect(toRegistrableDomain("http://www.writethedocs.org/conf")).toBe("writethedocs.org");
// Private/public SLDs should collapse to ICANN TLD + SLD by default
// (ICANN-only behavior; private suffixes ignored)
expect(toRegistrableDomain("spark-public.s3.amazonaws.com")).toBe(
"amazonaws.com",
);
expect(toRegistrableDomain("spark-public.s3.amazonaws.com")).toBe("amazonaws.com");
// Reject IPs and invalid inputs
expect(toRegistrableDomain("192.168.0.1")).toBeNull();
+3 -12
View File
@@ -6,10 +6,7 @@ type ParseOptions = Parameters<typeof parse>[1];
* Parse a domain into its parts. Passes options to `tldts.parse()`.
* @see https://github.com/remusao/tldts/blob/master/packages/tldts-core/src/options.ts
*/
export function getDomainParts(
domain: string,
opts?: ParseOptions,
): ReturnType<typeof parse> {
export function getDomainParts(domain: string, opts?: ParseOptions): ReturnType<typeof parse> {
return parse(domain, { ...opts });
}
@@ -17,10 +14,7 @@ export function getDomainParts(
* Get the TLD (ICANN-only public suffix) of a domain. Passes options to `tldts.parse()`.
* @see https://github.com/remusao/tldts/blob/master/packages/tldts-core/src/options.ts
*/
export function getDomainTld(
domain: string,
opts?: ParseOptions,
): string | null {
export function getDomainTld(domain: string, opts?: ParseOptions): string | null {
const result = getDomainParts(domain, {
allowPrivateDomains: false,
...opts,
@@ -54,10 +48,7 @@ export function punyToUnicode(domain: string): string {
* Returns null when the input is not a valid ICANN domain (e.g., invalid TLD, IPs)
* @see https://github.com/remusao/tldts/blob/master/packages/tldts-core/src/options.ts
*/
export function toRegistrableDomain(
input: string,
opts?: ParseOptions,
): string | null {
export function toRegistrableDomain(input: string, opts?: ParseOptions): string | null {
const raw = (input ?? "").trim();
if (raw === "") return null;
+1 -6
View File
@@ -69,11 +69,6 @@ export function asStringArray(value: unknown): string[] | undefined {
}
export function asDateLike(value: unknown): string | number | Date | undefined {
if (
typeof value === "string" ||
typeof value === "number" ||
value instanceof Date
)
return value;
if (typeof value === "string" || typeof value === "number" || value instanceof Date) return value;
return undefined;
}
+2 -12
View File
@@ -1,12 +1,4 @@
import {
afterAll,
beforeAll,
beforeEach,
describe,
expect,
it,
vi,
} from "vitest";
import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
import type { BootstrapData } from "../types";
import { getRdapBaseUrlsForTld } from "./bootstrap";
@@ -110,9 +102,7 @@ describe("getRdapBaseUrlsForTld with customBootstrapData", () => {
const dataWithDuplicates: BootstrapData = {
version: "1.0",
publication: "2025-01-15T12:00:00Z",
services: [
[["test"], ["https://rdap.example.com/", "https://rdap.example.com"]],
],
services: [[["test"], ["https://rdap.example.com/", "https://rdap.example.com"]]],
};
const urls = await getRdapBaseUrlsForTld("test", {
+1 -4
View File
@@ -24,10 +24,7 @@ export async function fetchRdapDomain(
baseUrl: string,
options?: LookupOptions,
): Promise<RdapFetchResult> {
const url = new URL(
`domain/${encodeURIComponent(domain)}`,
baseUrl,
).toString();
const url = new URL(`domain/${encodeURIComponent(domain)}`, baseUrl).toString();
const fetchFn = resolveFetch(options);
const res = await withTimeout(
fetchFn(url, {
+1 -3
View File
@@ -13,9 +13,7 @@ export function extractRdapRelatedLinks(
opts?: Pick<LookupOptions, "rdapLinkRels">,
): string[] {
const rels = (
opts?.rdapLinkRels?.length
? opts.rdapLinkRels
: ["related", "entity", "registrar", "alternate"]
opts?.rdapLinkRels?.length ? opts.rdapLinkRels : ["related", "entity", "registrar", "alternate"]
).map((r) => r.toLowerCase());
const d = (doc ?? {}) as Record<string, unknown> & { links?: RdapLink[] };
const arr = Array.isArray(d?.links) ? (d.links as RdapLink[]) : [];
+15 -14
View File
@@ -6,38 +6,40 @@ import { extractRdapRelatedLinks } from "./links";
type Json = Record<string, unknown>;
/** Coerce a loosely-typed JSON field to a string ("" for null/objects/etc.). */
function str(val: unknown): string {
return typeof val === "string" || typeof val === "number" || typeof val === "boolean"
? String(val)
: "";
}
/** Merge RDAP documents with a conservative, additive strategy. */
export function mergeRdapDocs(baseDoc: unknown, others: unknown[]): unknown {
const merged: Json = { ...(baseDoc as Json) };
for (const doc of others) {
const cur = (doc ?? {}) as Json;
// status: array of strings
merged.status = uniqStrings([
...toStringArray(merged.status),
...toStringArray(cur.status),
]);
merged.status = uniqStrings([...toStringArray(merged.status), ...toStringArray(cur.status)]);
// events: array of objects; dedupe by eventAction + eventDate
merged.events = uniqBy(
[...toArray<Json>(merged.events), ...toArray<Json>(cur.events)],
(e) =>
`${String(e?.eventAction ?? "").toLowerCase()}|${String(e?.eventDate ?? "")}`,
(e) => `${str(e?.eventAction).toLowerCase()}|${str(e?.eventDate)}`,
);
// nameservers: array of objects; dedupe by ldhName/unicodeName
merged.nameservers = uniqBy(
[...toArray<Json>(merged.nameservers), ...toArray<Json>(cur.nameservers)],
(n) => `${String(n?.ldhName ?? n?.unicodeName ?? "").toLowerCase()}`,
(n) => `${str(n?.ldhName ?? n?.unicodeName).toLowerCase()}`,
);
// entities: array; dedupe by handle if present, else by roles+vcard hash
merged.entities = uniqBy(
[...toArray<Json>(merged.entities), ...toArray<Json>(cur.entities)],
(e) =>
`${String(e?.handle ?? "").toLowerCase()}|${String(
`${str(e?.handle).toLowerCase()}|${String(
JSON.stringify(e?.roles || []),
).toLowerCase()}|${String(JSON.stringify(e?.vcardArray || [])).toLowerCase()}`,
);
// secureDNS: prefer existing; fill if missing
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
if (merged.port43 == null && cur.port43 != null) merged.port43 = cur.port43;
// remarks: concat simple strings if present
@@ -59,8 +61,7 @@ export async function fetchAndMergeRdapRelated(
opts?: LookupOptions,
): Promise<{ merged: unknown; serversTried: string[] }> {
const tried: string[] = [];
if (opts?.rdapFollowLinks === false)
return { merged: baseDoc, serversTried: tried };
if (opts?.rdapFollowLinks === false) return { merged: baseDoc, serversTried: tried };
const maxHops = Math.max(0, opts?.maxRdapLinkHops ?? 2);
if (maxHops === 0) return { merged: baseDoc, serversTried: tried };
@@ -83,8 +84,8 @@ export async function fetchAndMergeRdapRelated(
tried.push(url);
// only accept docs that appear related to the same domain when possible
// if ldhName/unicodeName present, they should match the queried domain (case-insensitive)
const ldh = String((json as Json)?.ldhName ?? "").toLowerCase();
const uni = String((json as Json)?.unicodeName ?? "").toLowerCase();
const ldh = str((json as Json)?.ldhName).toLowerCase();
const uni = str((json as Json)?.unicodeName).toLowerCase();
if (ldh && !sameDomain(ldh, domain)) continue;
if (uni && !sameDomain(uni, domain)) continue;
fetchedDocs.push(json);
+3 -9
View File
@@ -47,9 +47,7 @@ test("normalizeRdap maps registrar, contacts, nameservers, events, dnssec", () =
],
secureDNS: {
delegationSigned: true,
dsData: [
{ keyTag: 12345, algorithm: 13, digestType: 2, digest: "ABCDEF" },
],
dsData: [{ keyTag: 12345, algorithm: 13, digestType: 2, digest: "ABCDEF" }],
},
events: [
{ eventAction: "registration", eventDate: "2020-01-02T03:04:05Z" },
@@ -59,9 +57,7 @@ test("normalizeRdap maps registrar, contacts, nameservers, events, dnssec", () =
status: ["clientTransferProhibited"],
port43: "whois.example-registrar.test",
};
const rec = normalizeRdap("example.com", "com", rdap, [
"https://rdap.example/",
]);
const rec = normalizeRdap("example.com", "com", rdap, ["https://rdap.example/"]);
expect(rec.domain).toBe("example.com");
expect(rec.tld).toBe("com");
expect(rec.registrar?.name).toBe("Registrar LLC");
@@ -95,9 +91,7 @@ test("normalizeRdap derives privacyEnabled from registrant keywords", () => {
},
],
};
const rec = normalizeRdap("example.com", "com", rdap, [
"https://rdap.example/",
]);
const rec = normalizeRdap("example.com", "com", rdap, ["https://rdap.example/"]);
expect(rec.privacyEnabled).toBe(true);
});
+15 -48
View File
@@ -1,12 +1,7 @@
import { toISO } from "../lib/dates";
import { isPrivacyName } from "../lib/privacy";
import { asDateLike, asString, asStringArray, uniq } from "../lib/text";
import type {
Contact,
DomainRecord,
Nameserver,
RegistrarInfo,
} from "../types";
import type { Contact, DomainRecord, Nameserver, RegistrarInfo } from "../types";
type RdapDoc = Record<string, unknown>;
@@ -24,24 +19,17 @@ export function normalizeRdap(
const doc = (rdap ?? {}) as RdapDoc;
// Prefer ldhName (punycode) and unicodeName if provided
const ldhName: string | undefined =
asString(doc.ldhName) || asString(doc.handle);
const ldhName: string | undefined = asString(doc.ldhName) || asString(doc.handle);
const unicodeName: string | undefined = asString(doc.unicodeName);
// Registrar entity can be provided with role "registrar"
const registrar: RegistrarInfo | undefined = extractRegistrar(
doc.entities as unknown,
);
const registrar: RegistrarInfo | undefined = extractRegistrar(doc.entities as unknown);
// Nameservers: normalize host + IPs
const nameservers: Nameserver[] | undefined = Array.isArray(doc.nameservers)
? (doc.nameservers as RdapDoc[])
.map((ns) => {
const host = (
asString(ns.ldhName) ??
asString(ns.unicodeName) ??
""
).toLowerCase();
const host = (asString(ns.ldhName) ?? asString(ns.unicodeName) ?? "").toLowerCase();
const ip = ns.ipAddresses as RdapDoc | undefined;
const ipv4 = asStringArray(ip?.v4);
const ipv6 = asStringArray(ip?.v6);
@@ -54,17 +42,13 @@ export function normalizeRdap(
: undefined;
// 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
const registrant = contacts?.find((c) => c.type === "registrant");
const privacyEnabled = !!(
registrant &&
(
[registrant.name, registrant.organization].filter(Boolean) as string[]
).some(isPrivacyName)
([registrant.name, registrant.organization].filter(Boolean) as string[]).some(isPrivacyName)
);
// RDAP uses IANA EPP status values. Preserve raw plus a description if any remarks are present.
@@ -99,21 +83,16 @@ export function normalizeRdap(
: [];
const byAction = (action: string) =>
events.find(
(e) =>
typeof e?.eventAction === "string" &&
e.eventAction.toLowerCase().includes(action),
(e) => typeof e?.eventAction === "string" && e.eventAction.toLowerCase().includes(action),
);
const creationDate = toISO(
asDateLike(byAction("registration")?.eventDate) ??
asDateLike(doc.registrationDate),
asDateLike(byAction("registration")?.eventDate) ?? asDateLike(doc.registrationDate),
);
const updatedDate = toISO(
asDateLike(byAction("last changed")?.eventDate) ??
asDateLike(doc.lastChangedDate),
asDateLike(byAction("last changed")?.eventDate) ?? asDateLike(doc.lastChangedDate),
);
const expirationDate = toISO(
asDateLike(byAction("expiration")?.eventDate) ??
asDateLike(doc.expirationDate),
asDateLike(byAction("expiration")?.eventDate) ?? asDateLike(doc.expirationDate),
);
const deletionDate = toISO(
asDateLike(byAction("deletion")?.eventDate) ?? asDateLike(doc.deletionDate),
@@ -164,9 +143,7 @@ function extractRegistrar(entities: unknown): RegistrarInfo | undefined {
if (!Array.isArray(entities)) return undefined;
for (const ent of entities) {
const roles: string[] = Array.isArray((ent as RdapDoc)?.roles)
? ((ent as RdapDoc).roles as unknown[]).filter(
(r): r is string => typeof r === "string",
)
? ((ent as RdapDoc).roles as unknown[]).filter((r): r is string => typeof r === "string")
: [];
if (!roles.some((r) => /registrar/i.test(r))) continue;
const v = parseVcard((ent as RdapDoc)?.vcardArray);
@@ -191,9 +168,7 @@ function extractContacts(entities: unknown): Contact[] | undefined {
const out: Contact[] = [];
for (const ent of entities) {
const roles: string[] = Array.isArray((ent as RdapDoc)?.roles)
? ((ent as RdapDoc).roles as unknown[]).filter(
(r): r is string => typeof r === "string",
)
? ((ent as RdapDoc).roles as unknown[]).filter((r): r is string => typeof r === "string")
: [];
const v = parseVcard((ent as RdapDoc)?.vcardArray);
const type = roles.find((r) =>
@@ -245,15 +220,9 @@ interface ParsedVCard {
// 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])
)
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 entries = vcardArray[1] as Array<[string, Record<string, unknown>, string, unknown]>;
const out: ParsedVCard = {};
for (const e of entries) {
const key = e?.[0];
@@ -264,9 +233,7 @@ function parseVcard(vcardArray: unknown): ParsedVCard {
out.fn = asString(value);
break;
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;
case "email":
out.email = asString(value);
+1 -4
View File
@@ -372,7 +372,4 @@ export interface LookupResult {
* Used internally for dependency injection and testing. Matches the signature
* of the global `fetch` function available in Node.js 18+ and browsers.
*/
export type FetchLike = (
input: string | URL,
init?: RequestInit,
) => Promise<Response>;
export type FetchLike = (input: string | URL, init?: RequestInit) => Promise<Response>;
+1 -7
View File
@@ -31,13 +31,7 @@ describe("WHOIS coalescing", () => {
const [first] = chain;
if (!first) throw new Error("Expected first record");
const base = normalizeWhois(
"gitpod.io",
"io",
first.text,
first.serverQueried,
false,
);
const base = normalizeWhois("gitpod.io", "io", first.text, first.serverQueried, false);
const merged = mergeWhoisRecords(base, []);
expect(merged.isRegistered).toBe(true);
expect(merged.creationDate).toBeDefined();
+4 -16
View File
@@ -1,10 +1,7 @@
import { uniq } from "../lib/text";
import type { Contact, DomainRecord, Nameserver } from "../types";
function dedupeStatuses(
a?: DomainRecord["statuses"],
b?: DomainRecord["statuses"],
) {
function dedupeStatuses(a?: DomainRecord["statuses"], b?: DomainRecord["statuses"]) {
const list = [...(a || []), ...(b || [])];
const seen = new Set<string>();
const out: NonNullable<DomainRecord["statuses"]> = [];
@@ -48,10 +45,7 @@ function dedupeContacts(a?: Contact[], b?: Contact[]) {
}
/** Conservative merge: start with base; fill missing scalars; union arrays; prefer more informative dates. */
export function mergeWhoisRecords(
base: DomainRecord,
others: DomainRecord[],
): DomainRecord {
export function mergeWhoisRecords(base: DomainRecord, others: DomainRecord[]): DomainRecord {
const merged: DomainRecord = { ...base };
for (const cur of others) {
merged.isRegistered = merged.isRegistered || cur.isRegistered;
@@ -60,15 +54,9 @@ export function mergeWhoisRecords(
merged.reseller = merged.reseller ?? cur.reseller;
merged.statuses = dedupeStatuses(merged.statuses, cur.statuses);
// Dates: prefer earliest creation, latest updated/expiration when available
merged.creationDate = preferEarliestIso(
merged.creationDate,
cur.creationDate,
);
merged.creationDate = preferEarliestIso(merged.creationDate, cur.creationDate);
merged.updatedDate = preferLatestIso(merged.updatedDate, cur.updatedDate);
merged.expirationDate = preferLatestIso(
merged.expirationDate,
cur.expirationDate,
);
merged.expirationDate = preferLatestIso(merged.expirationDate, cur.expirationDate);
merged.deletionDate = merged.deletionDate ?? cur.deletionDate;
merged.transferLock = Boolean(merged.transferLock || cur.transferLock);
merged.dnssec = merged.dnssec ?? cur.dnssec;
+2 -12
View File
@@ -159,12 +159,7 @@ Name Server: NS2.EXAMPLE.COM
DNSSEC: unsigned
Domain Status: clientTransferProhibited https://icann.org/epp#clientTransferProhibited
`;
const rec = normalizeWhois(
"example.com",
"com",
text,
"whois.verisign-grs.com",
);
const rec = normalizeWhois("example.com", "com", text, "whois.verisign-grs.com");
expect(Boolean(rec.creationDate)).toBe(true);
expect(Boolean(rec.expirationDate)).toBe(true);
expect(rec.source).toBe("whois");
@@ -178,12 +173,7 @@ Registrar URL: http://www.registrar.test
Registrant Name: REDACTED FOR PRIVACY
Registrant Organization: Example Org
`;
const rec = normalizeWhois(
"example.com",
"com",
text,
"whois.verisign-grs.com",
);
const rec = normalizeWhois("example.com", "com", text, "whois.verisign-grs.com");
expect(rec.privacyEnabled).toBe(true);
});
+15 -65
View File
@@ -1,12 +1,7 @@
import { toISO } from "../lib/dates";
import { isPrivacyName } from "../lib/privacy";
import { parseKeyValueLines, uniq } from "../lib/text";
import type {
Contact,
DomainRecord,
Nameserver,
RegistrarInfo,
} from "../types";
import type { Contact, DomainRecord, Nameserver, RegistrarInfo } from "../types";
// Common WHOIS availability phrases seen across registries/registrars
const WHOIS_AVAILABLE_PATTERNS: RegExp[] = [
@@ -123,11 +118,7 @@ export function normalizeWhois(
"organisation",
"record maintained by",
]);
const ianaId = anyValue(map, [
"registrar iana id",
"sponsoring registrar iana id",
"iana id",
]);
const ianaId = anyValue(map, ["registrar iana id", "sponsoring registrar iana id", "iana id"]);
const url = anyValue(map, [
"registrar url",
"registrar website",
@@ -135,16 +126,9 @@ export function normalizeWhois(
"url of the registrar",
"referrer",
]);
const abuseEmail = anyValue(map, [
"registrar abuse contact email",
"abuse contact email",
]);
const abusePhone = anyValue(map, [
"registrar abuse contact phone",
"abuse contact phone",
]);
if (!name && !ianaId && !url && !abuseEmail && !abusePhone)
return undefined;
const abuseEmail = anyValue(map, ["registrar abuse contact email", "abuse contact email"]);
const abusePhone = anyValue(map, ["registrar abuse contact phone", "abuse contact phone"]);
if (!name && !ianaId && !url && !abuseEmail && !abusePhone) return undefined;
return {
name: name || undefined,
ianaId: ianaId || undefined,
@@ -222,15 +206,11 @@ export function normalizeWhois(
const registrant = contacts?.find((c) => c.type === "registrant");
const privacyEnabled = !!(
registrant &&
(
[registrant.name, registrant.organization].filter(Boolean) as string[]
).some(isPrivacyName)
([registrant.name, registrant.organization].filter(Boolean) as string[]).some(isPrivacyName)
);
const dnssecRaw = (map.dnssec?.[0] || "").toLowerCase();
const dnssec = dnssecRaw
? { enabled: /signed|yes|true/.test(dnssecRaw) }
: undefined;
const dnssec = dnssecRaw ? { enabled: /signed|yes|true/.test(dnssecRaw) } : undefined;
// Simple lock derivation from statuses
const transferLock = !!statuses?.some((s) =>
@@ -268,10 +248,7 @@ export function normalizeWhois(
return record;
}
function anyValue(
map: Record<string, string[]>,
keys: string[],
): string | undefined {
function anyValue(map: Record<string, string[]>, keys: string[]): string | undefined {
for (const k of keys) {
const v = map[k];
if (v?.length) return v[0];
@@ -327,11 +304,7 @@ function collectContacts(map: Record<string, string[]>): Contact[] | undefined {
nameKeys.push("owner name"); // .tm
}
orgKeys.push(
`${prefix} organization`,
`${prefix} organisation`,
`${prefix} org`,
);
orgKeys.push(`${prefix} organization`, `${prefix} organisation`, `${prefix} org`);
if (prefix === "registrant") {
orgKeys.push("trading as"); // .uk, .co.uk
orgKeys.push("org"); // .ru
@@ -340,42 +313,22 @@ function collectContacts(map: Record<string, string[]>): Contact[] | undefined {
orgKeys.push("owner orgname"); // .tm
}
emailKeys.push(
`${prefix} email`,
`${prefix} contact email`,
`${prefix} e-mail`,
);
emailKeys.push(`${prefix} email`, `${prefix} contact email`, `${prefix} e-mail`);
phoneKeys.push(
`${prefix} phone`,
`${prefix} contact phone`,
`${prefix} telephone`,
);
phoneKeys.push(`${prefix} phone`, `${prefix} contact phone`, `${prefix} telephone`);
faxKeys.push(`${prefix} fax`, `${prefix} facsimile`);
streetKeys.push(
`${prefix} street`,
`${prefix} address`,
`${prefix}'s address`,
);
streetKeys.push(`${prefix} street`, `${prefix} address`, `${prefix}'s address`);
if (prefix === "owner") {
streetKeys.push("owner addr"); // .tm
}
cityKeys.push(`${prefix} city`);
stateKeys.push(
`${prefix} state`,
`${prefix} province`,
`${prefix} state/province`,
);
stateKeys.push(`${prefix} state`, `${prefix} province`, `${prefix} state/province`);
postalCodeKeys.push(
`${prefix} postal code`,
`${prefix} postcode`,
`${prefix} zip`,
);
postalCodeKeys.push(`${prefix} postal code`, `${prefix} postcode`, `${prefix} zip`);
countryKeys.push(`${prefix} country`);
}
@@ -410,10 +363,7 @@ function collectContacts(map: Record<string, string[]>): Contact[] | undefined {
return contacts.length ? contacts : undefined;
}
function multi(
map: Record<string, string[]>,
keys: string[],
): string[] | undefined {
function multi(map: Record<string, string[]>, keys: string[]): string[] | undefined {
for (const k of keys) {
const v = map[k];
if (v?.length) return v;
+4 -5
View File
@@ -32,11 +32,10 @@ describe("WHOIS referral contradiction handling", () => {
});
it("collects chain and does not append contradictory registrar", async () => {
const chain = await collectWhoisReferralChain(
"whois.nic.io",
"raindrop.io",
{ followWhoisReferral: true, maxWhoisReferralHops: 2 },
);
const chain = await collectWhoisReferralChain("whois.nic.io", "raindrop.io", {
followWhoisReferral: true,
maxWhoisReferralHops: 2,
});
expect(Array.isArray(chain)).toBe(true);
// Mocked registrar is contradictory, so chain should contain only the TLD response
expect(chain.length).toBe(1);