rdapper 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,6 @@
1
+ import type { DomainRecord } from "../types.js";
2
+ /**
3
+ * Convert RDAP JSON into our normalized DomainRecord.
4
+ * This function is defensive: RDAP servers vary in completeness and field naming.
5
+ */
6
+ export declare function normalizeRdap(inputDomain: string, tld: string, rdap: unknown, rdapServersTried: string[], fetchedAtISO: string, includeRaw?: boolean): DomainRecord;
@@ -0,0 +1,214 @@
1
+ import { toISO } from "../lib/dates.js";
2
+ import { asDateLike, asString, asStringArray, uniq } from "../lib/text.js";
3
+ /**
4
+ * Convert RDAP JSON into our normalized DomainRecord.
5
+ * This function is defensive: RDAP servers vary in completeness and field naming.
6
+ */
7
+ export function normalizeRdap(inputDomain, tld, rdap, rdapServersTried, fetchedAtISO, includeRaw = false) {
8
+ const doc = (rdap ?? {});
9
+ // Safe helpers for optional fields
10
+ const _get = (obj, path) => path.reduce((o, k) => o?.[k] ?? undefined, obj);
11
+ // Prefer ldhName (punycode) and unicodeName if provided
12
+ const ldhName = asString(doc.ldhName) || asString(doc.handle);
13
+ const unicodeName = asString(doc.unicodeName);
14
+ // Registrar entity can be provided with role "registrar"
15
+ const registrar = extractRegistrar(doc.entities);
16
+ // Nameservers: normalize host + IPs
17
+ const nameservers = Array.isArray(doc.nameservers)
18
+ ? doc.nameservers
19
+ .map((ns) => {
20
+ const host = (asString(ns.ldhName) ??
21
+ asString(ns.unicodeName) ??
22
+ "").toLowerCase();
23
+ const ip = ns.ipAddresses;
24
+ const ipv4 = asStringArray(ip?.v4);
25
+ const ipv6 = asStringArray(ip?.v6);
26
+ const n = { host };
27
+ if (ipv4?.length)
28
+ n.ipv4 = ipv4;
29
+ if (ipv6?.length)
30
+ n.ipv6 = ipv6;
31
+ return n;
32
+ })
33
+ .filter((n) => !!n.host)
34
+ : undefined;
35
+ // Contacts: RDAP entities include roles like registrant, administrative, technical, billing, abuse
36
+ const contacts = extractContacts(doc.entities);
37
+ // RDAP uses IANA EPP status values. Preserve raw plus a description if any remarks are present.
38
+ const statuses = Array.isArray(doc.status)
39
+ ? doc.status
40
+ .filter((s) => typeof s === "string")
41
+ .map((s) => ({ status: s, raw: s }))
42
+ : undefined;
43
+ // Secure DNS info
44
+ const secureDNS = doc.secureDNS;
45
+ const dnssec = secureDNS
46
+ ? {
47
+ enabled: !!secureDNS.delegationSigned,
48
+ dsRecords: Array.isArray(secureDNS.dsData)
49
+ ? secureDNS.dsData.map((d) => ({
50
+ keyTag: d.keyTag,
51
+ algorithm: d.algorithm,
52
+ digestType: d.digestType,
53
+ digest: d.digest,
54
+ }))
55
+ : undefined,
56
+ }
57
+ : undefined;
58
+ const events = Array.isArray(doc.events)
59
+ ? doc.events
60
+ : [];
61
+ const byAction = (action) => events.find((e) => typeof e?.eventAction === "string" &&
62
+ e.eventAction.toLowerCase().includes(action));
63
+ const creationDate = toISO(asDateLike(byAction("registration")?.eventDate) ??
64
+ asDateLike(doc.registrationDate));
65
+ const updatedDate = toISO(asDateLike(byAction("last changed")?.eventDate) ??
66
+ asDateLike(doc.lastChangedDate));
67
+ const expirationDate = toISO(asDateLike(byAction("expiration")?.eventDate) ??
68
+ asDateLike(doc.expirationDate));
69
+ const deletionDate = toISO(asDateLike(byAction("deletion")?.eventDate) ?? asDateLike(doc.deletionDate));
70
+ // Derive a simple transfer lock flag from statuses
71
+ const transferLock = !!statuses?.some((s) => /transferprohibited/i.test(s.status));
72
+ // The RDAP document may include "port43" pointer to authoritative WHOIS
73
+ const whoisServer = asString(doc.port43);
74
+ const record = {
75
+ domain: unicodeName || ldhName || inputDomain,
76
+ tld,
77
+ isRegistered: true,
78
+ isIDN: /(^|\.)xn--/i.test(ldhName || inputDomain),
79
+ unicodeName: unicodeName || undefined,
80
+ punycodeName: ldhName || undefined,
81
+ registry: undefined, // RDAP rarely includes a clean registry operator name
82
+ registrar: registrar,
83
+ reseller: undefined,
84
+ statuses: statuses,
85
+ creationDate,
86
+ updatedDate,
87
+ expirationDate,
88
+ deletionDate,
89
+ transferLock,
90
+ dnssec,
91
+ nameservers: nameservers
92
+ ? uniq(nameservers.map((n) => ({ ...n, host: n.host.toLowerCase() })))
93
+ : undefined,
94
+ contacts,
95
+ whoisServer,
96
+ rdapServers: rdapServersTried,
97
+ rawRdap: includeRaw ? rdap : undefined,
98
+ rawWhois: undefined,
99
+ source: "rdap",
100
+ fetchedAt: fetchedAtISO,
101
+ warnings: undefined,
102
+ };
103
+ return record;
104
+ }
105
+ function extractRegistrar(entities) {
106
+ if (!Array.isArray(entities))
107
+ return undefined;
108
+ for (const ent of entities) {
109
+ const roles = Array.isArray(ent?.roles)
110
+ ? ent.roles.filter((r) => typeof r === "string")
111
+ : [];
112
+ if (!roles.some((r) => /registrar/i.test(r)))
113
+ continue;
114
+ const v = parseVcard(ent?.vcardArray);
115
+ const ianaId = Array.isArray(ent?.publicIds)
116
+ ? ent.publicIds.find((id) => /iana\s*registrar\s*id/i.test(String(id?.type)))?.identifier
117
+ : undefined;
118
+ return {
119
+ name: v.fn || v.org || asString(ent?.handle) || undefined,
120
+ ianaId: asString(ianaId),
121
+ url: v.url ?? undefined,
122
+ email: v.email ?? undefined,
123
+ phone: v.tel ?? undefined,
124
+ };
125
+ }
126
+ return undefined;
127
+ }
128
+ function extractContacts(entities) {
129
+ if (!Array.isArray(entities))
130
+ return undefined;
131
+ const out = [];
132
+ for (const ent of entities) {
133
+ const roles = Array.isArray(ent?.roles)
134
+ ? ent.roles.filter((r) => typeof r === "string")
135
+ : [];
136
+ const v = parseVcard(ent?.vcardArray);
137
+ const type = roles.find((r) => /registrant|administrative|technical|billing|abuse|reseller/i.test(r));
138
+ if (!type)
139
+ continue;
140
+ const map = {
141
+ registrant: "registrant",
142
+ administrative: "admin",
143
+ technical: "tech",
144
+ billing: "billing",
145
+ abuse: "abuse",
146
+ reseller: "reseller",
147
+ };
148
+ const roleKey = (map[type.toLowerCase()] ?? "unknown");
149
+ out.push({
150
+ type: roleKey,
151
+ name: v.fn,
152
+ organization: v.org,
153
+ email: v.email,
154
+ phone: v.tel,
155
+ fax: v.fax,
156
+ street: v.street,
157
+ city: v.locality,
158
+ state: v.region,
159
+ postalCode: v.postcode,
160
+ country: v.country,
161
+ countryCode: v.countryCode,
162
+ });
163
+ }
164
+ return out.length ? out : undefined;
165
+ }
166
+ // Parse a minimal subset of vCard 4.0 arrays as used in RDAP "vcardArray" fields
167
+ function parseVcard(vcardArray) {
168
+ // vcardArray is typically ["vcard", [["version",{} ,"text","4.0"], ["fn",{} ,"text","Example"], ...]]
169
+ if (!Array.isArray(vcardArray) ||
170
+ vcardArray[0] !== "vcard" ||
171
+ !Array.isArray(vcardArray[1]))
172
+ return {};
173
+ const entries = vcardArray[1];
174
+ const out = {};
175
+ for (const e of entries) {
176
+ const key = e?.[0];
177
+ const _valueType = e?.[2];
178
+ const value = e?.[3];
179
+ if (!key)
180
+ continue;
181
+ switch (String(key).toLowerCase()) {
182
+ case "fn":
183
+ out.fn = asString(value);
184
+ break;
185
+ case "org":
186
+ out.org = Array.isArray(value)
187
+ ? value.map((x) => String(x)).join(" ")
188
+ : asString(value);
189
+ break;
190
+ case "email":
191
+ out.email = asString(value);
192
+ break;
193
+ case "tel":
194
+ out.tel = asString(value);
195
+ break;
196
+ case "url":
197
+ out.url = asString(value);
198
+ break;
199
+ case "adr": {
200
+ // adr value is [postOfficeBox, extendedAddress, street, locality, region, postalCode, country]
201
+ if (Array.isArray(value)) {
202
+ out.street = value[2] ? String(value[2]).split(/\n|,\s*/) : undefined;
203
+ out.locality = asString(value[3]);
204
+ out.region = asString(value[4]);
205
+ out.postcode = asString(value[5]);
206
+ out.country = asString(value[6]);
207
+ }
208
+ break;
209
+ }
210
+ }
211
+ }
212
+ // Best effort country code from country name (often omitted). Leaving undefined unless explicitly provided.
213
+ return out;
214
+ }
@@ -0,0 +1,83 @@
1
+ export type LookupSource = "rdap" | "whois";
2
+ export interface RegistrarInfo {
3
+ name?: string;
4
+ ianaId?: string;
5
+ url?: string;
6
+ email?: string;
7
+ phone?: string;
8
+ }
9
+ export interface Contact {
10
+ type: "registrant" | "admin" | "tech" | "billing" | "abuse" | "registrar" | "reseller" | "unknown";
11
+ name?: string;
12
+ organization?: string;
13
+ email?: string | string[];
14
+ phone?: string | string[];
15
+ fax?: string | string[];
16
+ street?: string[];
17
+ city?: string;
18
+ state?: string;
19
+ postalCode?: string;
20
+ country?: string;
21
+ countryCode?: string;
22
+ }
23
+ export interface Nameserver {
24
+ host: string;
25
+ ipv4?: string[];
26
+ ipv6?: string[];
27
+ }
28
+ export interface StatusEvent {
29
+ status: string;
30
+ description?: string;
31
+ raw?: string;
32
+ }
33
+ export interface DomainRecord {
34
+ domain: string;
35
+ tld: string;
36
+ isRegistered: boolean;
37
+ isIDN?: boolean;
38
+ unicodeName?: string;
39
+ punycodeName?: string;
40
+ registry?: string;
41
+ registrar?: RegistrarInfo;
42
+ reseller?: string;
43
+ statuses?: StatusEvent[];
44
+ creationDate?: string;
45
+ updatedDate?: string;
46
+ expirationDate?: string;
47
+ deletionDate?: string;
48
+ transferLock?: boolean;
49
+ dnssec?: {
50
+ enabled: boolean;
51
+ dsRecords?: Array<{
52
+ keyTag?: number;
53
+ algorithm?: number;
54
+ digestType?: number;
55
+ digest?: string;
56
+ }>;
57
+ };
58
+ nameservers?: Nameserver[];
59
+ contacts?: Contact[];
60
+ whoisServer?: string;
61
+ rdapServers?: string[];
62
+ rawRdap?: unknown;
63
+ rawWhois?: string;
64
+ source: LookupSource;
65
+ fetchedAt: string;
66
+ warnings?: string[];
67
+ }
68
+ export interface LookupOptions {
69
+ timeoutMs?: number;
70
+ rdapOnly?: boolean;
71
+ whoisOnly?: boolean;
72
+ followWhoisReferral?: boolean;
73
+ customBootstrapUrl?: string;
74
+ whoisHints?: Record<string, string>;
75
+ includeRaw?: boolean;
76
+ signal?: AbortSignal;
77
+ }
78
+ export interface LookupResult {
79
+ ok: boolean;
80
+ record?: DomainRecord;
81
+ error?: string;
82
+ }
83
+ export type FetchLike = (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>;
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,10 @@
1
+ import type { LookupOptions } from "../types.js";
2
+ export interface WhoisQueryResult {
3
+ serverQueried: string;
4
+ text: string;
5
+ }
6
+ /**
7
+ * Perform a WHOIS query against an RFC 3912 server over TCP 43.
8
+ * Returns the raw text and the server used.
9
+ */
10
+ export declare function whoisQuery(server: string, query: string, options?: LookupOptions): Promise<WhoisQueryResult>;
@@ -0,0 +1,46 @@
1
+ import { createConnection } from "node:net";
2
+ import { withTimeout } from "../lib/async.js";
3
+ import { DEFAULT_TIMEOUT_MS } from "../lib/constants.js";
4
+ /**
5
+ * Perform a WHOIS query against an RFC 3912 server over TCP 43.
6
+ * Returns the raw text and the server used.
7
+ */
8
+ export async function whoisQuery(server, query, options) {
9
+ const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
10
+ const port = 43;
11
+ const host = server.replace(/^whois:\/\//i, "");
12
+ const text = await withTimeout(queryTcp(host, port, query, options), timeoutMs, "WHOIS timeout");
13
+ return { serverQueried: server, text };
14
+ }
15
+ // Low-level WHOIS TCP client. Some registries require CRLF after the domain query.
16
+ function queryTcp(host, port, query, options) {
17
+ return new Promise((resolve, reject) => {
18
+ const socket = createConnection({ host, port });
19
+ let data = "";
20
+ let done = false;
21
+ const cleanup = () => {
22
+ if (done)
23
+ return;
24
+ done = true;
25
+ socket.destroy();
26
+ };
27
+ socket.setTimeout((options?.timeoutMs ?? DEFAULT_TIMEOUT_MS) - 1000, () => {
28
+ cleanup();
29
+ reject(new Error("WHOIS socket timeout"));
30
+ });
31
+ socket.on("error", (err) => {
32
+ cleanup();
33
+ reject(err);
34
+ });
35
+ socket.on("data", (chunk) => {
36
+ data += chunk.toString("utf8");
37
+ });
38
+ socket.on("end", () => {
39
+ cleanup();
40
+ resolve(data);
41
+ });
42
+ socket.on("connect", () => {
43
+ socket.write(`${query}\r\n`);
44
+ });
45
+ });
46
+ }
@@ -0,0 +1,9 @@
1
+ import type { LookupOptions } from "../types.js";
2
+ /**
3
+ * Best-effort discovery of the authoritative WHOIS server for a TLD via IANA root DB.
4
+ */
5
+ export declare function ianaWhoisServerForTld(tld: string, options?: LookupOptions): Promise<string | undefined>;
6
+ /**
7
+ * Extract registrar referral WHOIS server from a WHOIS response, if present.
8
+ */
9
+ export declare function extractWhoisReferral(text: string): string | undefined;
@@ -0,0 +1,50 @@
1
+ import { whoisQuery } from "./client.js";
2
+ import { WHOIS_TLD_EXCEPTIONS } from "./servers.js";
3
+ /**
4
+ * Best-effort discovery of the authoritative WHOIS server for a TLD via IANA root DB.
5
+ */
6
+ export async function ianaWhoisServerForTld(tld, options) {
7
+ const key = tld.toLowerCase();
8
+ // 1) Explicit hint override
9
+ const hint = options?.whoisHints?.[key];
10
+ if (hint)
11
+ return normalizeServer(hint);
12
+ // 2) IANA WHOIS authoritative discovery over TCP 43
13
+ try {
14
+ const res = await whoisQuery("whois.iana.org", key, options);
15
+ const txt = res.text;
16
+ const m = txt.match(/^whois:\s*(\S+)/im) ||
17
+ txt.match(/^refer:\s*(\S+)/im) ||
18
+ txt.match(/^whois server:\s*(\S+)/im);
19
+ const server = m?.[1];
20
+ if (server)
21
+ return normalizeServer(server);
22
+ }
23
+ catch {
24
+ // fallthrough to exceptions/guess
25
+ }
26
+ // 3) Curated exceptions
27
+ const exception = WHOIS_TLD_EXCEPTIONS[key];
28
+ if (exception)
29
+ return normalizeServer(exception);
30
+ return undefined;
31
+ }
32
+ /**
33
+ * Extract registrar referral WHOIS server from a WHOIS response, if present.
34
+ */
35
+ export function extractWhoisReferral(text) {
36
+ const patterns = [
37
+ /^Registrar WHOIS Server:\s*(.+)$/im,
38
+ /^Whois Server:\s*(.+)$/im,
39
+ /^ReferralServer:\s*whois:\/\/(.+)$/im,
40
+ ];
41
+ for (const re of patterns) {
42
+ const m = text.match(re);
43
+ if (m?.[1])
44
+ return m[1].trim();
45
+ }
46
+ return undefined;
47
+ }
48
+ function normalizeServer(server) {
49
+ return server.replace(/^whois:\/\//i, "").replace(/\/$/, "");
50
+ }
@@ -0,0 +1,6 @@
1
+ import type { DomainRecord } from "../types.js";
2
+ /**
3
+ * Convert raw WHOIS text into our normalized DomainRecord.
4
+ * Heuristics cover many gTLD and ccTLD formats; exact fields vary per registry.
5
+ */
6
+ export declare function normalizeWhois(domain: string, tld: string, whoisText: string, whoisServer: string | undefined, fetchedAtISO: string, includeRaw?: boolean): DomainRecord;
@@ -0,0 +1,214 @@
1
+ import { toISO } from "../lib/dates.js";
2
+ import { isWhoisAvailable } from "../lib/domain.js";
3
+ import { parseKeyValueLines, uniq } from "../lib/text.js";
4
+ /**
5
+ * Convert raw WHOIS text into our normalized DomainRecord.
6
+ * Heuristics cover many gTLD and ccTLD formats; exact fields vary per registry.
7
+ */
8
+ export function normalizeWhois(domain, tld, whoisText, whoisServer, fetchedAtISO, includeRaw = false) {
9
+ const map = parseKeyValueLines(whoisText);
10
+ // Date extraction across common synonyms
11
+ const creationDate = anyValue(map, [
12
+ "creation date",
13
+ "created on",
14
+ "registered on",
15
+ "domain registration date",
16
+ "domain create date",
17
+ "created",
18
+ "registered",
19
+ ]);
20
+ const updatedDate = anyValue(map, [
21
+ "updated date",
22
+ "last updated",
23
+ "last modified",
24
+ "modified",
25
+ ]);
26
+ const expirationDate = anyValue(map, [
27
+ "registry expiry date",
28
+ "expiry date",
29
+ "expiration date",
30
+ "paid-till",
31
+ "expires on",
32
+ "renewal date",
33
+ ]);
34
+ // Registrar info (thin registries like .com/.net require referral follow for full data)
35
+ const registrar = (() => {
36
+ const name = anyValue(map, [
37
+ "registrar",
38
+ "sponsoring registrar",
39
+ "registrar name",
40
+ ]);
41
+ const ianaId = anyValue(map, ["registrar iana id", "iana id"]);
42
+ const url = anyValue(map, [
43
+ "registrar url",
44
+ "url of the registrar",
45
+ "referrer",
46
+ ]);
47
+ const abuseEmail = anyValue(map, [
48
+ "registrar abuse contact email",
49
+ "abuse contact email",
50
+ ]);
51
+ const abusePhone = anyValue(map, [
52
+ "registrar abuse contact phone",
53
+ "abuse contact phone",
54
+ ]);
55
+ if (!name && !ianaId && !url && !abuseEmail && !abusePhone)
56
+ return undefined;
57
+ return {
58
+ name: name || undefined,
59
+ ianaId: ianaId || undefined,
60
+ url: url || undefined,
61
+ email: abuseEmail || undefined,
62
+ phone: abusePhone || undefined,
63
+ };
64
+ })();
65
+ // Statuses: multiple entries are expected; keep raw
66
+ const statusLines = map["domain status"] || map.status || [];
67
+ const statuses = statusLines.length
68
+ ? statusLines.map((line) => ({ status: line.split(/\s+/)[0], raw: line }))
69
+ : undefined;
70
+ // Nameservers: also appear as "nserver" on some ccTLDs (.de, .ru) and as "name server"
71
+ const nsLines = [
72
+ ...(map["name server"] || []),
73
+ ...(map.nameserver || []),
74
+ ...(map["name servers"] || []),
75
+ ...(map.nserver || []),
76
+ ];
77
+ const nameservers = nsLines.length
78
+ ? uniq(nsLines
79
+ .map((line) => line.trim())
80
+ .filter(Boolean)
81
+ .map((line) => {
82
+ // Common formats: "ns1.example.com" or "ns1.example.com 192.0.2.1" or "ns1.example.com 2001:db8::1"
83
+ const parts = line.split(/\s+/);
84
+ const host = parts.shift()?.toLowerCase() || "";
85
+ const ipv4 = [];
86
+ const ipv6 = [];
87
+ for (const p of parts) {
88
+ if (/^\d+\.\d+\.\d+\.\d+$/.test(p))
89
+ ipv4.push(p);
90
+ else if (/^[0-9a-f:]+$/i.test(p))
91
+ ipv6.push(p);
92
+ }
93
+ if (!host)
94
+ return undefined;
95
+ const ns = { host };
96
+ if (ipv4.length)
97
+ ns.ipv4 = ipv4;
98
+ if (ipv6.length)
99
+ ns.ipv6 = ipv6;
100
+ return ns;
101
+ })
102
+ .filter((x) => !!x))
103
+ : undefined;
104
+ // Contacts: best-effort parse common keys
105
+ const contacts = collectContacts(map);
106
+ const dnssecRaw = (map.dnssec?.[0] || "").toLowerCase();
107
+ const dnssec = dnssecRaw
108
+ ? { enabled: /signed|yes|true/.test(dnssecRaw) }
109
+ : undefined;
110
+ // Simple lock derivation from statuses
111
+ const transferLock = !!statuses?.some((s) => /transferprohibited/i.test(s.status));
112
+ const record = {
113
+ domain,
114
+ tld,
115
+ isRegistered: !isWhoisAvailable(whoisText),
116
+ isIDN: /(^|\.)xn--/i.test(domain),
117
+ unicodeName: undefined,
118
+ punycodeName: undefined,
119
+ registry: undefined,
120
+ registrar,
121
+ reseller: anyValue(map, ["reseller"]) || undefined,
122
+ statuses,
123
+ creationDate: toISO(creationDate || undefined),
124
+ updatedDate: toISO(updatedDate || undefined),
125
+ expirationDate: toISO(expirationDate || undefined),
126
+ deletionDate: undefined,
127
+ transferLock,
128
+ dnssec,
129
+ nameservers,
130
+ contacts,
131
+ whoisServer,
132
+ rdapServers: undefined,
133
+ rawRdap: undefined,
134
+ rawWhois: includeRaw ? whoisText : undefined,
135
+ source: "whois",
136
+ fetchedAt: fetchedAtISO,
137
+ warnings: undefined,
138
+ };
139
+ return record;
140
+ }
141
+ function anyValue(map, keys) {
142
+ for (const k of keys) {
143
+ const v = map[k];
144
+ if (v?.length)
145
+ return v[0];
146
+ }
147
+ return undefined;
148
+ }
149
+ function collectContacts(map) {
150
+ const roles = [
151
+ { role: "registrant", prefix: "registrant" },
152
+ { role: "admin", prefix: "admin" },
153
+ { role: "tech", prefix: "tech" },
154
+ { role: "billing", prefix: "billing" },
155
+ { role: "abuse", prefix: "abuse" },
156
+ ];
157
+ const contacts = [];
158
+ for (const r of roles) {
159
+ const name = anyValue(map, [
160
+ `${r.prefix} name`,
161
+ `${r.prefix} contact name`,
162
+ `${r.prefix}`,
163
+ ]);
164
+ const org = anyValue(map, [`${r.prefix} organization`, `${r.prefix} org`]);
165
+ const email = anyValue(map, [
166
+ `${r.prefix} email`,
167
+ `${r.prefix} contact email`,
168
+ `${r.prefix} e-mail`,
169
+ ]);
170
+ const phone = anyValue(map, [
171
+ `${r.prefix} phone`,
172
+ `${r.prefix} contact phone`,
173
+ `${r.prefix} telephone`,
174
+ ]);
175
+ const fax = anyValue(map, [`${r.prefix} fax`, `${r.prefix} facsimile`]);
176
+ const street = multi(map, [`${r.prefix} street`, `${r.prefix} address`]);
177
+ const city = anyValue(map, [`${r.prefix} city`]);
178
+ const state = anyValue(map, [
179
+ `${r.prefix} state`,
180
+ `${r.prefix} province`,
181
+ `${r.prefix} state/province`,
182
+ ]);
183
+ const postalCode = anyValue(map, [
184
+ `${r.prefix} postal code`,
185
+ `${r.prefix} postcode`,
186
+ `${r.prefix} zip`,
187
+ ]);
188
+ const country = anyValue(map, [`${r.prefix} country`]);
189
+ if (name || org || email || phone || street?.length) {
190
+ contacts.push({
191
+ type: r.role,
192
+ name: name || undefined,
193
+ organization: org || undefined,
194
+ email: email || undefined,
195
+ phone: phone || undefined,
196
+ fax: fax || undefined,
197
+ street: street,
198
+ city: city || undefined,
199
+ state: state || undefined,
200
+ postalCode: postalCode || undefined,
201
+ country: country || undefined,
202
+ });
203
+ }
204
+ }
205
+ return contacts.length ? contacts : undefined;
206
+ }
207
+ function multi(map, keys) {
208
+ for (const k of keys) {
209
+ const v = map[k];
210
+ if (v?.length)
211
+ return v;
212
+ }
213
+ return undefined;
214
+ }
@@ -0,0 +1 @@
1
+ export declare const WHOIS_TLD_EXCEPTIONS: Record<string, string>;