rdapper 0.1.0 → 0.2.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.
- package/README.md +5 -3
- package/dist/index.d.ts +91 -5
- package/dist/index.js +857 -103
- package/package.json +14 -6
- package/dist/lib/async.d.ts +0 -2
- package/dist/lib/async.js +0 -18
- package/dist/lib/constants.d.ts +0 -1
- package/dist/lib/constants.js +0 -1
- package/dist/lib/dates.d.ts +0 -1
- package/dist/lib/dates.js +0 -84
- package/dist/lib/domain.d.ts +0 -8
- package/dist/lib/domain.js +0 -64
- package/dist/lib/text.d.ts +0 -6
- package/dist/lib/text.js +0 -77
- package/dist/rdap/bootstrap.d.ts +0 -6
- package/dist/rdap/bootstrap.js +0 -30
- package/dist/rdap/client.d.ts +0 -9
- package/dist/rdap/client.js +0 -21
- package/dist/rdap/normalize.d.ts +0 -6
- package/dist/rdap/normalize.js +0 -214
- package/dist/types.d.ts +0 -83
- package/dist/types.js +0 -1
- package/dist/whois/client.d.ts +0 -10
- package/dist/whois/client.js +0 -46
- package/dist/whois/discovery.d.ts +0 -9
- package/dist/whois/discovery.js +0 -50
- package/dist/whois/normalize.d.ts +0 -6
- package/dist/whois/normalize.js +0 -214
- package/dist/whois/servers.d.ts +0 -1
- package/dist/whois/servers.js +0 -64
package/dist/index.js
CHANGED
|
@@ -1,109 +1,863 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
1
|
+
import psl from "psl";
|
|
2
|
+
import { createConnection } from "node:net";
|
|
3
|
+
|
|
4
|
+
//#region src/lib/dates.ts
|
|
5
|
+
function toISO(dateLike) {
|
|
6
|
+
if (dateLike == null) return void 0;
|
|
7
|
+
if (dateLike instanceof Date) return toIsoFromDate(dateLike);
|
|
8
|
+
if (typeof dateLike === "number") return toIsoFromDate(new Date(dateLike));
|
|
9
|
+
const raw = String(dateLike).trim();
|
|
10
|
+
if (!raw) return void 0;
|
|
11
|
+
for (const re of [
|
|
12
|
+
/^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})(?:Z)?$/,
|
|
13
|
+
/^(\d{4})\/(\d{2})\/(\d{2})[ T](\d{2}):(\d{2}):(\d{2})$/,
|
|
14
|
+
/^(\d{2})-([A-Za-z]{3})-(\d{4})$/,
|
|
15
|
+
/^([A-Za-z]{3})\s+(\d{1,2})\s+(\d{4})$/
|
|
16
|
+
]) {
|
|
17
|
+
const m = raw.match(re);
|
|
18
|
+
if (!m) continue;
|
|
19
|
+
const d = parseWithRegex(m, re);
|
|
20
|
+
if (d) return toIsoFromDate(d);
|
|
21
|
+
}
|
|
22
|
+
const native = new Date(raw);
|
|
23
|
+
if (!Number.isNaN(native.getTime())) return toIsoFromDate(native);
|
|
24
|
+
}
|
|
25
|
+
function toIsoFromDate(d) {
|
|
26
|
+
try {
|
|
27
|
+
return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(), 0)).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
28
|
+
} catch {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function parseWithRegex(m, _re) {
|
|
33
|
+
const monthMap = {
|
|
34
|
+
jan: 0,
|
|
35
|
+
feb: 1,
|
|
36
|
+
mar: 2,
|
|
37
|
+
apr: 3,
|
|
38
|
+
may: 4,
|
|
39
|
+
jun: 5,
|
|
40
|
+
jul: 6,
|
|
41
|
+
aug: 7,
|
|
42
|
+
sep: 8,
|
|
43
|
+
oct: 9,
|
|
44
|
+
nov: 10,
|
|
45
|
+
dec: 11
|
|
46
|
+
};
|
|
47
|
+
try {
|
|
48
|
+
if (m[0].includes(":")) {
|
|
49
|
+
const [_$1, y, mo, d, hh, mm, ss] = m;
|
|
50
|
+
return new Date(Date.UTC(Number(y), Number(mo) - 1, Number(d), Number(hh), Number(mm), Number(ss)));
|
|
51
|
+
}
|
|
52
|
+
if (m[0].includes("-")) {
|
|
53
|
+
const [_$1, dd$1, monStr$1, yyyy$1] = m;
|
|
54
|
+
const mon$1 = monthMap[monStr$1.toLowerCase()];
|
|
55
|
+
return new Date(Date.UTC(Number(yyyy$1), mon$1, Number(dd$1)));
|
|
56
|
+
}
|
|
57
|
+
const [_, monStr, dd, yyyy] = m;
|
|
58
|
+
const mon = monthMap[monStr.toLowerCase()];
|
|
59
|
+
return new Date(Date.UTC(Number(yyyy), mon, Number(dd)));
|
|
60
|
+
} catch {}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
//#endregion
|
|
64
|
+
//#region src/lib/domain.ts
|
|
65
|
+
function getDomainParts(domain) {
|
|
66
|
+
const lower = domain.toLowerCase().trim();
|
|
67
|
+
let publicSuffix;
|
|
68
|
+
try {
|
|
69
|
+
publicSuffix = (psl.parse?.(lower))?.tld;
|
|
70
|
+
} catch {}
|
|
71
|
+
if (!publicSuffix) {
|
|
72
|
+
const parts = lower.split(".").filter(Boolean);
|
|
73
|
+
publicSuffix = parts.length ? parts[parts.length - 1] : lower;
|
|
74
|
+
}
|
|
75
|
+
const labels = publicSuffix.split(".").filter(Boolean);
|
|
76
|
+
const tld = labels.length ? labels[labels.length - 1] : publicSuffix;
|
|
77
|
+
return {
|
|
78
|
+
publicSuffix,
|
|
79
|
+
tld
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
function isLikelyDomain(input) {
|
|
83
|
+
return /^[a-z0-9.-]+$/i.test(input) && input.includes(".");
|
|
84
|
+
}
|
|
85
|
+
const WHOIS_AVAILABLE_PATTERNS = [
|
|
86
|
+
/\bno match\b/i,
|
|
87
|
+
/\bnot found\b/i,
|
|
88
|
+
/\bno entries found\b/i,
|
|
89
|
+
/\bno data found\b/i,
|
|
90
|
+
/\bavailable for registration\b/i,
|
|
91
|
+
/\bdomain\s+available\b/i,
|
|
92
|
+
/\bdomain status[:\s]+available\b/i,
|
|
93
|
+
/\bobject does not exist\b/i,
|
|
94
|
+
/\bthe queried object does not exist\b/i
|
|
95
|
+
];
|
|
96
|
+
function isWhoisAvailable(text) {
|
|
97
|
+
if (!text) return false;
|
|
98
|
+
return WHOIS_AVAILABLE_PATTERNS.some((re) => re.test(text));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
//#endregion
|
|
102
|
+
//#region src/lib/async.ts
|
|
103
|
+
function withTimeout(promise, timeoutMs, reason = "Timeout") {
|
|
104
|
+
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0) return promise;
|
|
105
|
+
let timer;
|
|
106
|
+
const timeout = new Promise((_, reject) => {
|
|
107
|
+
timer = setTimeout(() => reject(new Error(reason)), timeoutMs);
|
|
108
|
+
});
|
|
109
|
+
return Promise.race([promise.finally(() => {
|
|
110
|
+
if (timer !== void 0) clearTimeout(timer);
|
|
111
|
+
}), timeout]);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
//#endregion
|
|
115
|
+
//#region src/lib/constants.ts
|
|
116
|
+
const DEFAULT_TIMEOUT_MS = 15e3;
|
|
117
|
+
|
|
118
|
+
//#endregion
|
|
119
|
+
//#region src/rdap/bootstrap.ts
|
|
120
|
+
/**
|
|
121
|
+
* Resolve RDAP base URLs for a given TLD using IANA's bootstrap registry.
|
|
122
|
+
* Returns zero or more base URLs (always suffixed with a trailing slash).
|
|
123
|
+
*/
|
|
124
|
+
async function getRdapBaseUrlsForTld(tld, options) {
|
|
125
|
+
const bootstrapUrl = options?.customBootstrapUrl ?? "https://data.iana.org/rdap/dns.json";
|
|
126
|
+
const res = await withTimeout(fetch(bootstrapUrl, {
|
|
127
|
+
method: "GET",
|
|
128
|
+
headers: { accept: "application/json" },
|
|
129
|
+
signal: options?.signal
|
|
130
|
+
}), options?.timeoutMs ?? DEFAULT_TIMEOUT_MS, "RDAP bootstrap timeout");
|
|
131
|
+
if (!res.ok) return [];
|
|
132
|
+
const data = await res.json();
|
|
133
|
+
const target = tld.toLowerCase();
|
|
134
|
+
const bases = [];
|
|
135
|
+
for (const svc of data.services) {
|
|
136
|
+
const tlds = svc[0];
|
|
137
|
+
const urls = svc[1];
|
|
138
|
+
if (tlds.map((x) => x.toLowerCase()).includes(target)) for (const u of urls) {
|
|
139
|
+
const base = u.endsWith("/") ? u : `${u}/`;
|
|
140
|
+
bases.push(base);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return Array.from(new Set(bases));
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
//#endregion
|
|
147
|
+
//#region src/rdap/client.ts
|
|
148
|
+
/**
|
|
149
|
+
* Fetch RDAP JSON for a domain from a specific RDAP base URL.
|
|
150
|
+
* Throws on HTTP >= 400 (includes RDAP error JSON payloads).
|
|
151
|
+
*/
|
|
152
|
+
async function fetchRdapDomain(domain, baseUrl, options) {
|
|
153
|
+
const url = new URL(`domain/${encodeURIComponent(domain)}`, baseUrl).toString();
|
|
154
|
+
const res = await withTimeout(fetch(url, {
|
|
155
|
+
method: "GET",
|
|
156
|
+
headers: { accept: "application/rdap+json, application/json" },
|
|
157
|
+
signal: options?.signal
|
|
158
|
+
}), options?.timeoutMs ?? DEFAULT_TIMEOUT_MS, "RDAP lookup timeout");
|
|
159
|
+
if (!res.ok) {
|
|
160
|
+
const bodyText = await res.text();
|
|
161
|
+
throw new Error(`RDAP ${res.status}: ${bodyText.slice(0, 500)}`);
|
|
162
|
+
}
|
|
163
|
+
const json = await res.json();
|
|
164
|
+
return {
|
|
165
|
+
url,
|
|
166
|
+
json
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
//#endregion
|
|
171
|
+
//#region src/lib/text.ts
|
|
172
|
+
function uniq(arr) {
|
|
173
|
+
if (!arr) return void 0;
|
|
174
|
+
return Array.from(new Set(arr));
|
|
175
|
+
}
|
|
176
|
+
function parseKeyValueLines(text) {
|
|
177
|
+
const map = /* @__PURE__ */ new Map();
|
|
178
|
+
const lines = text.split(/\r?\n/);
|
|
179
|
+
let lastKey;
|
|
180
|
+
for (const rawLine of lines) {
|
|
181
|
+
const line = rawLine.replace(/\s+$/, "");
|
|
182
|
+
if (!line.trim()) continue;
|
|
183
|
+
const bracket = line.match(/^\s*\[([^\]]+)\]\s*(.*)$/);
|
|
184
|
+
if (bracket) {
|
|
185
|
+
const key = bracket[1].trim().toLowerCase();
|
|
186
|
+
const value = bracket[2].trim();
|
|
187
|
+
const list = map.get(key) ?? [];
|
|
188
|
+
if (value) list.push(value);
|
|
189
|
+
map.set(key, list);
|
|
190
|
+
lastKey = key;
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
const idx = line.indexOf(":");
|
|
194
|
+
if (idx !== -1) {
|
|
195
|
+
const key = line.slice(0, idx).trim().toLowerCase();
|
|
196
|
+
const value = line.slice(idx + 1).trim();
|
|
197
|
+
if (!key) {
|
|
198
|
+
lastKey = void 0;
|
|
199
|
+
continue;
|
|
200
|
+
}
|
|
201
|
+
const list = map.get(key) ?? [];
|
|
202
|
+
if (value) list.push(value);
|
|
203
|
+
map.set(key, list);
|
|
204
|
+
lastKey = key;
|
|
205
|
+
continue;
|
|
206
|
+
}
|
|
207
|
+
if (lastKey && /^\s+/.test(line)) {
|
|
208
|
+
const value = line.trim();
|
|
209
|
+
if (value) {
|
|
210
|
+
const list = map.get(lastKey) ?? [];
|
|
211
|
+
list.push(value);
|
|
212
|
+
map.set(lastKey, list);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
return Object.fromEntries(map);
|
|
217
|
+
}
|
|
218
|
+
function asString(value) {
|
|
219
|
+
return typeof value === "string" ? value : void 0;
|
|
220
|
+
}
|
|
221
|
+
function asStringArray(value) {
|
|
222
|
+
return Array.isArray(value) ? value.filter((x) => typeof x === "string") : void 0;
|
|
223
|
+
}
|
|
224
|
+
function asDateLike(value) {
|
|
225
|
+
if (typeof value === "string" || typeof value === "number" || value instanceof Date) return value;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
//#endregion
|
|
229
|
+
//#region src/rdap/normalize.ts
|
|
230
|
+
/**
|
|
231
|
+
* Convert RDAP JSON into our normalized DomainRecord.
|
|
232
|
+
* This function is defensive: RDAP servers vary in completeness and field naming.
|
|
233
|
+
*/
|
|
234
|
+
function normalizeRdap(inputDomain, tld, rdap, rdapServersTried, fetchedAtISO, includeRaw = false) {
|
|
235
|
+
const doc = rdap ?? {};
|
|
236
|
+
const ldhName = asString(doc.ldhName) || asString(doc.handle);
|
|
237
|
+
const unicodeName = asString(doc.unicodeName);
|
|
238
|
+
const registrar = extractRegistrar(doc.entities);
|
|
239
|
+
const nameservers = Array.isArray(doc.nameservers) ? doc.nameservers.map((ns) => {
|
|
240
|
+
const host = (asString(ns.ldhName) ?? asString(ns.unicodeName) ?? "").toLowerCase();
|
|
241
|
+
const ip = ns.ipAddresses;
|
|
242
|
+
const ipv4 = asStringArray(ip?.v4);
|
|
243
|
+
const ipv6 = asStringArray(ip?.v6);
|
|
244
|
+
const n = { host };
|
|
245
|
+
if (ipv4?.length) n.ipv4 = ipv4;
|
|
246
|
+
if (ipv6?.length) n.ipv6 = ipv6;
|
|
247
|
+
return n;
|
|
248
|
+
}).filter((n) => !!n.host) : void 0;
|
|
249
|
+
const contacts = extractContacts(doc.entities);
|
|
250
|
+
const statuses = Array.isArray(doc.status) ? doc.status.filter((s) => typeof s === "string").map((s) => ({
|
|
251
|
+
status: s,
|
|
252
|
+
raw: s
|
|
253
|
+
})) : void 0;
|
|
254
|
+
const secureDNS = doc.secureDNS;
|
|
255
|
+
const dnssec = secureDNS ? {
|
|
256
|
+
enabled: !!secureDNS.delegationSigned,
|
|
257
|
+
dsRecords: Array.isArray(secureDNS.dsData) ? secureDNS.dsData.map((d) => ({
|
|
258
|
+
keyTag: d.keyTag,
|
|
259
|
+
algorithm: d.algorithm,
|
|
260
|
+
digestType: d.digestType,
|
|
261
|
+
digest: d.digest
|
|
262
|
+
})) : void 0
|
|
263
|
+
} : void 0;
|
|
264
|
+
const events = Array.isArray(doc.events) ? doc.events : [];
|
|
265
|
+
const byAction = (action) => events.find((e) => typeof e?.eventAction === "string" && e.eventAction.toLowerCase().includes(action));
|
|
266
|
+
const creationDate = toISO(asDateLike(byAction("registration")?.eventDate) ?? asDateLike(doc.registrationDate));
|
|
267
|
+
const updatedDate = toISO(asDateLike(byAction("last changed")?.eventDate) ?? asDateLike(doc.lastChangedDate));
|
|
268
|
+
const expirationDate = toISO(asDateLike(byAction("expiration")?.eventDate) ?? asDateLike(doc.expirationDate));
|
|
269
|
+
const deletionDate = toISO(asDateLike(byAction("deletion")?.eventDate) ?? asDateLike(doc.deletionDate));
|
|
270
|
+
const transferLock = !!statuses?.some((s) => /transferprohibited/i.test(s.status));
|
|
271
|
+
const whoisServer = asString(doc.port43);
|
|
272
|
+
return {
|
|
273
|
+
domain: unicodeName || ldhName || inputDomain,
|
|
274
|
+
tld,
|
|
275
|
+
isRegistered: true,
|
|
276
|
+
isIDN: /(^|\.)xn--/i.test(ldhName || inputDomain),
|
|
277
|
+
unicodeName: unicodeName || void 0,
|
|
278
|
+
punycodeName: ldhName || void 0,
|
|
279
|
+
registry: void 0,
|
|
280
|
+
registrar,
|
|
281
|
+
reseller: void 0,
|
|
282
|
+
statuses,
|
|
283
|
+
creationDate,
|
|
284
|
+
updatedDate,
|
|
285
|
+
expirationDate,
|
|
286
|
+
deletionDate,
|
|
287
|
+
transferLock,
|
|
288
|
+
dnssec,
|
|
289
|
+
nameservers: nameservers ? uniq(nameservers.map((n) => ({
|
|
290
|
+
...n,
|
|
291
|
+
host: n.host.toLowerCase()
|
|
292
|
+
}))) : void 0,
|
|
293
|
+
contacts,
|
|
294
|
+
whoisServer,
|
|
295
|
+
rdapServers: rdapServersTried,
|
|
296
|
+
rawRdap: includeRaw ? rdap : void 0,
|
|
297
|
+
rawWhois: void 0,
|
|
298
|
+
source: "rdap",
|
|
299
|
+
fetchedAt: fetchedAtISO,
|
|
300
|
+
warnings: void 0
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
function extractRegistrar(entities) {
|
|
304
|
+
if (!Array.isArray(entities)) return void 0;
|
|
305
|
+
for (const ent of entities) {
|
|
306
|
+
if (!(Array.isArray(ent?.roles) ? ent.roles.filter((r) => typeof r === "string") : []).some((r) => /registrar/i.test(r))) continue;
|
|
307
|
+
const v = parseVcard(ent?.vcardArray);
|
|
308
|
+
const ianaId = Array.isArray(ent?.publicIds) ? ent.publicIds.find((id) => /iana\s*registrar\s*id/i.test(String(id?.type)))?.identifier : void 0;
|
|
309
|
+
return {
|
|
310
|
+
name: v.fn || v.org || asString(ent?.handle) || void 0,
|
|
311
|
+
ianaId: asString(ianaId),
|
|
312
|
+
url: v.url ?? void 0,
|
|
313
|
+
email: v.email ?? void 0,
|
|
314
|
+
phone: v.tel ?? void 0
|
|
315
|
+
};
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
function extractContacts(entities) {
|
|
319
|
+
if (!Array.isArray(entities)) return void 0;
|
|
320
|
+
const out = [];
|
|
321
|
+
for (const ent of entities) {
|
|
322
|
+
const roles = Array.isArray(ent?.roles) ? ent.roles.filter((r) => typeof r === "string") : [];
|
|
323
|
+
const v = parseVcard(ent?.vcardArray);
|
|
324
|
+
const type = roles.find((r) => /registrant|administrative|technical|billing|abuse|reseller/i.test(r));
|
|
325
|
+
if (!type) continue;
|
|
326
|
+
const roleKey = {
|
|
327
|
+
registrant: "registrant",
|
|
328
|
+
administrative: "admin",
|
|
329
|
+
technical: "tech",
|
|
330
|
+
billing: "billing",
|
|
331
|
+
abuse: "abuse",
|
|
332
|
+
reseller: "reseller"
|
|
333
|
+
}[type.toLowerCase()] ?? "unknown";
|
|
334
|
+
out.push({
|
|
335
|
+
type: roleKey,
|
|
336
|
+
name: v.fn,
|
|
337
|
+
organization: v.org,
|
|
338
|
+
email: v.email,
|
|
339
|
+
phone: v.tel,
|
|
340
|
+
fax: v.fax,
|
|
341
|
+
street: v.street,
|
|
342
|
+
city: v.locality,
|
|
343
|
+
state: v.region,
|
|
344
|
+
postalCode: v.postcode,
|
|
345
|
+
country: v.country,
|
|
346
|
+
countryCode: v.countryCode
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
return out.length ? out : void 0;
|
|
350
|
+
}
|
|
351
|
+
function parseVcard(vcardArray) {
|
|
352
|
+
if (!Array.isArray(vcardArray) || vcardArray[0] !== "vcard" || !Array.isArray(vcardArray[1])) return {};
|
|
353
|
+
const entries = vcardArray[1];
|
|
354
|
+
const out = {};
|
|
355
|
+
for (const e of entries) {
|
|
356
|
+
const key = e?.[0];
|
|
357
|
+
const value = e?.[3];
|
|
358
|
+
if (!key) continue;
|
|
359
|
+
switch (String(key).toLowerCase()) {
|
|
360
|
+
case "fn":
|
|
361
|
+
out.fn = asString(value);
|
|
362
|
+
break;
|
|
363
|
+
case "org":
|
|
364
|
+
out.org = Array.isArray(value) ? value.map((x) => String(x)).join(" ") : asString(value);
|
|
365
|
+
break;
|
|
366
|
+
case "email":
|
|
367
|
+
out.email = asString(value);
|
|
368
|
+
break;
|
|
369
|
+
case "tel":
|
|
370
|
+
out.tel = asString(value);
|
|
371
|
+
break;
|
|
372
|
+
case "url":
|
|
373
|
+
out.url = asString(value);
|
|
374
|
+
break;
|
|
375
|
+
case "adr":
|
|
376
|
+
if (Array.isArray(value)) {
|
|
377
|
+
out.street = value[2] ? String(value[2]).split(/\n|,\s*/) : void 0;
|
|
378
|
+
out.locality = asString(value[3]);
|
|
379
|
+
out.region = asString(value[4]);
|
|
380
|
+
out.postcode = asString(value[5]);
|
|
381
|
+
out.country = asString(value[6]);
|
|
382
|
+
}
|
|
383
|
+
break;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
return out;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
//#endregion
|
|
390
|
+
//#region src/whois/client.ts
|
|
391
|
+
/**
|
|
392
|
+
* Perform a WHOIS query against an RFC 3912 server over TCP 43.
|
|
393
|
+
* Returns the raw text and the server used.
|
|
394
|
+
*/
|
|
395
|
+
async function whoisQuery(server, query, options) {
|
|
396
|
+
const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
397
|
+
const port = 43;
|
|
398
|
+
const host = server.replace(/^whois:\/\//i, "");
|
|
399
|
+
const text = await withTimeout(queryTcp(host, port, query, options), timeoutMs, "WHOIS timeout");
|
|
400
|
+
return {
|
|
401
|
+
serverQueried: server,
|
|
402
|
+
text
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
function queryTcp(host, port, query, options) {
|
|
406
|
+
return new Promise((resolve, reject) => {
|
|
407
|
+
const socket = createConnection({
|
|
408
|
+
host,
|
|
409
|
+
port
|
|
410
|
+
});
|
|
411
|
+
let data = "";
|
|
412
|
+
let done = false;
|
|
413
|
+
const cleanup = () => {
|
|
414
|
+
if (done) return;
|
|
415
|
+
done = true;
|
|
416
|
+
socket.destroy();
|
|
417
|
+
};
|
|
418
|
+
socket.setTimeout((options?.timeoutMs ?? DEFAULT_TIMEOUT_MS) - 1e3, () => {
|
|
419
|
+
cleanup();
|
|
420
|
+
reject(/* @__PURE__ */ new Error("WHOIS socket timeout"));
|
|
421
|
+
});
|
|
422
|
+
socket.on("error", (err) => {
|
|
423
|
+
cleanup();
|
|
424
|
+
reject(err);
|
|
425
|
+
});
|
|
426
|
+
socket.on("data", (chunk) => {
|
|
427
|
+
data += chunk.toString("utf8");
|
|
428
|
+
});
|
|
429
|
+
socket.on("end", () => {
|
|
430
|
+
cleanup();
|
|
431
|
+
resolve(data);
|
|
432
|
+
});
|
|
433
|
+
socket.on("connect", () => {
|
|
434
|
+
socket.write(`${query}\r\n`);
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
//#endregion
|
|
440
|
+
//#region src/whois/servers.ts
|
|
441
|
+
const WHOIS_TLD_EXCEPTIONS = {
|
|
442
|
+
com: "whois.verisign-grs.com",
|
|
443
|
+
net: "whois.verisign-grs.com",
|
|
444
|
+
org: "whois.publicinterestregistry.org",
|
|
445
|
+
biz: "whois.nic.biz",
|
|
446
|
+
name: "whois.nic.name",
|
|
447
|
+
edu: "whois.educause.edu",
|
|
448
|
+
gov: "whois.nic.gov",
|
|
449
|
+
de: "whois.denic.de",
|
|
450
|
+
jp: "whois.jprs.jp",
|
|
451
|
+
fr: "whois.nic.fr",
|
|
452
|
+
it: "whois.nic.it",
|
|
453
|
+
pl: "whois.dns.pl",
|
|
454
|
+
nl: "whois.domain-registry.nl",
|
|
455
|
+
be: "whois.dns.be",
|
|
456
|
+
se: "whois.iis.se",
|
|
457
|
+
no: "whois.norid.no",
|
|
458
|
+
fi: "whois.fi",
|
|
459
|
+
cz: "whois.nic.cz",
|
|
460
|
+
es: "whois.nic.es",
|
|
461
|
+
br: "whois.registro.br",
|
|
462
|
+
ca: "whois.cira.ca",
|
|
463
|
+
dk: "whois.punktum.dk",
|
|
464
|
+
hk: "whois.hkirc.hk",
|
|
465
|
+
sg: "whois.sgnic.sg",
|
|
466
|
+
in: "whois.nixiregistry.in",
|
|
467
|
+
nz: "whois.irs.net.nz",
|
|
468
|
+
ch: "whois.nic.ch",
|
|
469
|
+
li: "whois.nic.li",
|
|
470
|
+
io: "whois.nic.io",
|
|
471
|
+
ai: "whois.nic.ai",
|
|
472
|
+
ru: "whois.tcinet.ru",
|
|
473
|
+
su: "whois.tcinet.ru",
|
|
474
|
+
us: "whois.nic.us",
|
|
475
|
+
co: "whois.nic.co",
|
|
476
|
+
me: "whois.nic.me",
|
|
477
|
+
tv: "whois.nic.tv",
|
|
478
|
+
cc: "ccwhois.verisign-grs.com",
|
|
479
|
+
eu: "whois.eu",
|
|
480
|
+
au: "whois.auda.org.au",
|
|
481
|
+
kr: "whois.kr",
|
|
482
|
+
tw: "whois.twnic.net.tw",
|
|
483
|
+
uk: "whois.nic.uk",
|
|
484
|
+
nu: "whois.iis.nu",
|
|
485
|
+
"xn--p1ai": "whois.tcinet.ru",
|
|
486
|
+
"uk.com": "whois.centralnic.com",
|
|
487
|
+
"uk.net": "whois.centralnic.com",
|
|
488
|
+
"gb.com": "whois.centralnic.com",
|
|
489
|
+
"gb.net": "whois.centralnic.com",
|
|
490
|
+
"eu.com": "whois.centralnic.com",
|
|
491
|
+
"us.com": "whois.centralnic.com",
|
|
492
|
+
"se.com": "whois.centralnic.com",
|
|
493
|
+
"de.com": "whois.centralnic.com",
|
|
494
|
+
"br.com": "whois.centralnic.com",
|
|
495
|
+
"ru.com": "whois.centralnic.com",
|
|
496
|
+
"cn.com": "whois.centralnic.com",
|
|
497
|
+
"sa.com": "whois.centralnic.com",
|
|
498
|
+
"co.com": "whois.centralnic.com"
|
|
499
|
+
};
|
|
500
|
+
|
|
501
|
+
//#endregion
|
|
502
|
+
//#region src/whois/discovery.ts
|
|
503
|
+
/**
|
|
504
|
+
* Parse the IANA WHOIS response for a TLD and extract the WHOIS server
|
|
505
|
+
* without crossing line boundaries. Some TLDs (e.g. .np) leave the field
|
|
506
|
+
* blank, in which case this returns undefined.
|
|
507
|
+
*/
|
|
508
|
+
function parseIanaWhoisServer(text) {
|
|
509
|
+
const fields = [
|
|
510
|
+
"whois",
|
|
511
|
+
"refer",
|
|
512
|
+
"whois server"
|
|
513
|
+
];
|
|
514
|
+
const lines = String(text).split(/\r?\n/);
|
|
515
|
+
for (const field of fields) for (const raw of lines) {
|
|
516
|
+
const line = raw.trimEnd();
|
|
517
|
+
const re = new RegExp(`^\\s*${field}\\s*:\\s*(.*?)$`, "i");
|
|
518
|
+
const m = line.match(re);
|
|
519
|
+
if (m) {
|
|
520
|
+
const value = (m[1] || "").trim();
|
|
521
|
+
if (value) return value;
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
/**
|
|
526
|
+
* Parse a likely registration information URL from an IANA WHOIS response.
|
|
527
|
+
* Looks at lines like:
|
|
528
|
+
* remarks: Registration information: http://example.tld
|
|
529
|
+
* url: https://registry.example
|
|
530
|
+
*/
|
|
531
|
+
function parseIanaRegistrationInfoUrl(text) {
|
|
532
|
+
const lines = String(text).split(/\r?\n/);
|
|
533
|
+
for (const raw of lines) {
|
|
534
|
+
const line = raw.trim();
|
|
535
|
+
if (!/^\s*(remarks|url|website)\s*:/i.test(line)) continue;
|
|
536
|
+
const urlMatch = line.match(/https?:\/\/\S+/i);
|
|
537
|
+
if (urlMatch?.[0]) return urlMatch[0];
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
/** Fetch raw IANA WHOIS text for a TLD (best-effort). */
|
|
541
|
+
async function getIanaWhoisTextForTld(tld, options) {
|
|
542
|
+
try {
|
|
543
|
+
return (await whoisQuery("whois.iana.org", tld.toLowerCase(), options)).text;
|
|
544
|
+
} catch {
|
|
545
|
+
return;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
/**
|
|
549
|
+
* Best-effort discovery of the authoritative WHOIS server for a TLD via IANA root DB.
|
|
550
|
+
*/
|
|
551
|
+
async function ianaWhoisServerForTld(tld, options) {
|
|
552
|
+
const key = tld.toLowerCase();
|
|
553
|
+
const hint = options?.whoisHints?.[key];
|
|
554
|
+
if (hint) return normalizeServer(hint);
|
|
555
|
+
try {
|
|
556
|
+
const txt = (await whoisQuery("whois.iana.org", key, options)).text;
|
|
557
|
+
const server = parseIanaWhoisServer(txt);
|
|
558
|
+
if (server) return normalizeServer(server);
|
|
559
|
+
} catch {}
|
|
560
|
+
const exception = WHOIS_TLD_EXCEPTIONS[key];
|
|
561
|
+
if (exception) return normalizeServer(exception);
|
|
562
|
+
}
|
|
563
|
+
/**
|
|
564
|
+
* Extract registrar referral WHOIS server from a WHOIS response, if present.
|
|
565
|
+
*/
|
|
566
|
+
function extractWhoisReferral(text) {
|
|
567
|
+
for (const re of [
|
|
568
|
+
/^Registrar WHOIS Server:\s*(.+)$/im,
|
|
569
|
+
/^Whois Server:\s*(.+)$/im,
|
|
570
|
+
/^ReferralServer:\s*whois:\/\/(.+)$/im
|
|
571
|
+
]) {
|
|
572
|
+
const m = text.match(re);
|
|
573
|
+
if (m?.[1]) return m[1].trim();
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
function normalizeServer(server) {
|
|
577
|
+
return server.replace(/^whois:\/\//i, "").replace(/\/$/, "");
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
//#endregion
|
|
581
|
+
//#region src/whois/normalize.ts
|
|
582
|
+
/**
|
|
583
|
+
* Convert raw WHOIS text into our normalized DomainRecord.
|
|
584
|
+
* Heuristics cover many gTLD and ccTLD formats; exact fields vary per registry.
|
|
585
|
+
*/
|
|
586
|
+
function normalizeWhois(domain, tld, whoisText, whoisServer, fetchedAtISO, includeRaw = false) {
|
|
587
|
+
const map = parseKeyValueLines(whoisText);
|
|
588
|
+
const creationDate = anyValue(map, [
|
|
589
|
+
"creation date",
|
|
590
|
+
"created on",
|
|
591
|
+
"registered on",
|
|
592
|
+
"domain registration date",
|
|
593
|
+
"domain create date",
|
|
594
|
+
"created",
|
|
595
|
+
"registered"
|
|
596
|
+
]);
|
|
597
|
+
const updatedDate = anyValue(map, [
|
|
598
|
+
"updated date",
|
|
599
|
+
"last updated",
|
|
600
|
+
"last modified",
|
|
601
|
+
"modified"
|
|
602
|
+
]);
|
|
603
|
+
const expirationDate = anyValue(map, [
|
|
604
|
+
"registry expiry date",
|
|
605
|
+
"expiry date",
|
|
606
|
+
"expiration date",
|
|
607
|
+
"paid-till",
|
|
608
|
+
"expires on",
|
|
609
|
+
"renewal date"
|
|
610
|
+
]);
|
|
611
|
+
const registrar = (() => {
|
|
612
|
+
const name = anyValue(map, [
|
|
613
|
+
"registrar",
|
|
614
|
+
"sponsoring registrar",
|
|
615
|
+
"registrar name"
|
|
616
|
+
]);
|
|
617
|
+
const ianaId = anyValue(map, ["registrar iana id", "iana id"]);
|
|
618
|
+
const url = anyValue(map, [
|
|
619
|
+
"registrar url",
|
|
620
|
+
"url of the registrar",
|
|
621
|
+
"referrer"
|
|
622
|
+
]);
|
|
623
|
+
const abuseEmail = anyValue(map, ["registrar abuse contact email", "abuse contact email"]);
|
|
624
|
+
const abusePhone = anyValue(map, ["registrar abuse contact phone", "abuse contact phone"]);
|
|
625
|
+
if (!name && !ianaId && !url && !abuseEmail && !abusePhone) return void 0;
|
|
626
|
+
return {
|
|
627
|
+
name: name || void 0,
|
|
628
|
+
ianaId: ianaId || void 0,
|
|
629
|
+
url: url || void 0,
|
|
630
|
+
email: abuseEmail || void 0,
|
|
631
|
+
phone: abusePhone || void 0
|
|
632
|
+
};
|
|
633
|
+
})();
|
|
634
|
+
const statusLines = map["domain status"] || map.status || [];
|
|
635
|
+
const statuses = statusLines.length ? statusLines.map((line) => ({
|
|
636
|
+
status: line.split(/\s+/)[0],
|
|
637
|
+
raw: line
|
|
638
|
+
})) : void 0;
|
|
639
|
+
const nsLines = [
|
|
640
|
+
...map["name server"] || [],
|
|
641
|
+
...map.nameserver || [],
|
|
642
|
+
...map["name servers"] || [],
|
|
643
|
+
...map.nserver || []
|
|
644
|
+
];
|
|
645
|
+
const nameservers = nsLines.length ? uniq(nsLines.map((line) => line.trim()).filter(Boolean).map((line) => {
|
|
646
|
+
const parts = line.split(/\s+/);
|
|
647
|
+
const host = parts.shift()?.toLowerCase() || "";
|
|
648
|
+
const ipv4 = [];
|
|
649
|
+
const ipv6 = [];
|
|
650
|
+
for (const p of parts) if (/^\d+\.\d+\.\d+\.\d+$/.test(p)) ipv4.push(p);
|
|
651
|
+
else if (/^[0-9a-f:]+$/i.test(p)) ipv6.push(p);
|
|
652
|
+
if (!host) return void 0;
|
|
653
|
+
const ns = { host };
|
|
654
|
+
if (ipv4.length) ns.ipv4 = ipv4;
|
|
655
|
+
if (ipv6.length) ns.ipv6 = ipv6;
|
|
656
|
+
return ns;
|
|
657
|
+
}).filter((x) => !!x)) : void 0;
|
|
658
|
+
const contacts = collectContacts(map);
|
|
659
|
+
const dnssecRaw = (map.dnssec?.[0] || "").toLowerCase();
|
|
660
|
+
const dnssec = dnssecRaw ? { enabled: /signed|yes|true/.test(dnssecRaw) } : void 0;
|
|
661
|
+
const transferLock = !!statuses?.some((s) => /transferprohibited/i.test(s.status));
|
|
662
|
+
return {
|
|
663
|
+
domain,
|
|
664
|
+
tld,
|
|
665
|
+
isRegistered: !isWhoisAvailable(whoisText),
|
|
666
|
+
isIDN: /(^|\.)xn--/i.test(domain),
|
|
667
|
+
unicodeName: void 0,
|
|
668
|
+
punycodeName: void 0,
|
|
669
|
+
registry: void 0,
|
|
670
|
+
registrar,
|
|
671
|
+
reseller: anyValue(map, ["reseller"]) || void 0,
|
|
672
|
+
statuses,
|
|
673
|
+
creationDate: toISO(creationDate || void 0),
|
|
674
|
+
updatedDate: toISO(updatedDate || void 0),
|
|
675
|
+
expirationDate: toISO(expirationDate || void 0),
|
|
676
|
+
deletionDate: void 0,
|
|
677
|
+
transferLock,
|
|
678
|
+
dnssec,
|
|
679
|
+
nameservers,
|
|
680
|
+
contacts,
|
|
681
|
+
whoisServer,
|
|
682
|
+
rdapServers: void 0,
|
|
683
|
+
rawRdap: void 0,
|
|
684
|
+
rawWhois: includeRaw ? whoisText : void 0,
|
|
685
|
+
source: "whois",
|
|
686
|
+
fetchedAt: fetchedAtISO,
|
|
687
|
+
warnings: void 0
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
function anyValue(map, keys) {
|
|
691
|
+
for (const k of keys) {
|
|
692
|
+
const v = map[k];
|
|
693
|
+
if (v?.length) return v[0];
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
function collectContacts(map) {
|
|
697
|
+
const roles = [
|
|
698
|
+
{
|
|
699
|
+
role: "registrant",
|
|
700
|
+
prefix: "registrant"
|
|
701
|
+
},
|
|
702
|
+
{
|
|
703
|
+
role: "admin",
|
|
704
|
+
prefix: "admin"
|
|
705
|
+
},
|
|
706
|
+
{
|
|
707
|
+
role: "tech",
|
|
708
|
+
prefix: "tech"
|
|
709
|
+
},
|
|
710
|
+
{
|
|
711
|
+
role: "billing",
|
|
712
|
+
prefix: "billing"
|
|
713
|
+
},
|
|
714
|
+
{
|
|
715
|
+
role: "abuse",
|
|
716
|
+
prefix: "abuse"
|
|
717
|
+
}
|
|
718
|
+
];
|
|
719
|
+
const contacts = [];
|
|
720
|
+
for (const r of roles) {
|
|
721
|
+
const name = anyValue(map, [
|
|
722
|
+
`${r.prefix} name`,
|
|
723
|
+
`${r.prefix} contact name`,
|
|
724
|
+
`${r.prefix}`
|
|
725
|
+
]);
|
|
726
|
+
const org = anyValue(map, [`${r.prefix} organization`, `${r.prefix} org`]);
|
|
727
|
+
const email = anyValue(map, [
|
|
728
|
+
`${r.prefix} email`,
|
|
729
|
+
`${r.prefix} contact email`,
|
|
730
|
+
`${r.prefix} e-mail`
|
|
731
|
+
]);
|
|
732
|
+
const phone = anyValue(map, [
|
|
733
|
+
`${r.prefix} phone`,
|
|
734
|
+
`${r.prefix} contact phone`,
|
|
735
|
+
`${r.prefix} telephone`
|
|
736
|
+
]);
|
|
737
|
+
const fax = anyValue(map, [`${r.prefix} fax`, `${r.prefix} facsimile`]);
|
|
738
|
+
const street = multi(map, [`${r.prefix} street`, `${r.prefix} address`]);
|
|
739
|
+
const city = anyValue(map, [`${r.prefix} city`]);
|
|
740
|
+
const state = anyValue(map, [
|
|
741
|
+
`${r.prefix} state`,
|
|
742
|
+
`${r.prefix} province`,
|
|
743
|
+
`${r.prefix} state/province`
|
|
744
|
+
]);
|
|
745
|
+
const postalCode = anyValue(map, [
|
|
746
|
+
`${r.prefix} postal code`,
|
|
747
|
+
`${r.prefix} postcode`,
|
|
748
|
+
`${r.prefix} zip`
|
|
749
|
+
]);
|
|
750
|
+
const country = anyValue(map, [`${r.prefix} country`]);
|
|
751
|
+
if (name || org || email || phone || street?.length) contacts.push({
|
|
752
|
+
type: r.role,
|
|
753
|
+
name: name || void 0,
|
|
754
|
+
organization: org || void 0,
|
|
755
|
+
email: email || void 0,
|
|
756
|
+
phone: phone || void 0,
|
|
757
|
+
fax: fax || void 0,
|
|
758
|
+
street,
|
|
759
|
+
city: city || void 0,
|
|
760
|
+
state: state || void 0,
|
|
761
|
+
postalCode: postalCode || void 0,
|
|
762
|
+
country: country || void 0
|
|
763
|
+
});
|
|
764
|
+
}
|
|
765
|
+
return contacts.length ? contacts : void 0;
|
|
766
|
+
}
|
|
767
|
+
function multi(map, keys) {
|
|
768
|
+
for (const k of keys) {
|
|
769
|
+
const v = map[k];
|
|
770
|
+
if (v?.length) return v;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
//#endregion
|
|
775
|
+
//#region src/index.ts
|
|
10
776
|
/**
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
}
|
|
81
|
-
catch {
|
|
82
|
-
// try next
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
}
|
|
86
|
-
const record = normalizeWhois(domain, tld, res.text, res.serverQueried, now, !!opts?.includeRaw);
|
|
87
|
-
return { ok: true, record };
|
|
88
|
-
}
|
|
89
|
-
catch (err) {
|
|
90
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
91
|
-
return { ok: false, error: message };
|
|
92
|
-
}
|
|
777
|
+
* High-level lookup that prefers RDAP and falls back to WHOIS.
|
|
778
|
+
* Ensures a standardized DomainRecord, independent of the source.
|
|
779
|
+
*/
|
|
780
|
+
async function lookupDomain(domain, opts) {
|
|
781
|
+
try {
|
|
782
|
+
if (!isLikelyDomain(domain)) return {
|
|
783
|
+
ok: false,
|
|
784
|
+
error: "Input does not look like a domain"
|
|
785
|
+
};
|
|
786
|
+
const { publicSuffix, tld } = getDomainParts(domain);
|
|
787
|
+
const now = toISO(/* @__PURE__ */ new Date()) ?? (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d{3}Z$/, "Z");
|
|
788
|
+
if (!opts?.whoisOnly) {
|
|
789
|
+
const bases = await getRdapBaseUrlsForTld(tld, opts);
|
|
790
|
+
const tried = [];
|
|
791
|
+
for (const base of bases) {
|
|
792
|
+
tried.push(base);
|
|
793
|
+
try {
|
|
794
|
+
const { json } = await fetchRdapDomain(domain, base, opts);
|
|
795
|
+
return {
|
|
796
|
+
ok: true,
|
|
797
|
+
record: normalizeRdap(domain, tld, json, tried, now, !!opts?.includeRaw)
|
|
798
|
+
};
|
|
799
|
+
} catch {}
|
|
800
|
+
}
|
|
801
|
+
if (opts?.rdapOnly) return {
|
|
802
|
+
ok: false,
|
|
803
|
+
error: `RDAP not available or failed for TLD '${tld}'. Many TLDs do not publish RDAP; try WHOIS fallback (omit rdapOnly).`
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
const whoisServer = await ianaWhoisServerForTld(tld, opts);
|
|
807
|
+
if (!whoisServer) {
|
|
808
|
+
const ianaText = await getIanaWhoisTextForTld(tld, opts);
|
|
809
|
+
const regUrl = ianaText ? parseIanaRegistrationInfoUrl(ianaText) : void 0;
|
|
810
|
+
const hint = regUrl ? ` See registration info at ${regUrl}.` : "";
|
|
811
|
+
return {
|
|
812
|
+
ok: false,
|
|
813
|
+
error: `No WHOIS server discovered for TLD '${tld}'. This registry may not publish public WHOIS over port 43.${hint}`
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
let res = await whoisQuery(whoisServer, domain, opts);
|
|
817
|
+
if (opts?.followWhoisReferral !== false) {
|
|
818
|
+
const referral = extractWhoisReferral(res.text);
|
|
819
|
+
if (referral && referral.toLowerCase() !== whoisServer.toLowerCase()) try {
|
|
820
|
+
res = await whoisQuery(referral, domain, opts);
|
|
821
|
+
} catch {}
|
|
822
|
+
}
|
|
823
|
+
if (publicSuffix.includes(".") && /no match|not found/i.test(res.text) && opts?.followWhoisReferral !== false) {
|
|
824
|
+
const candidates = [];
|
|
825
|
+
const ps = publicSuffix.toLowerCase();
|
|
826
|
+
const exception = WHOIS_TLD_EXCEPTIONS[ps];
|
|
827
|
+
if (exception) candidates.push(exception);
|
|
828
|
+
for (const server of candidates) try {
|
|
829
|
+
const alt = await whoisQuery(server, domain, opts);
|
|
830
|
+
if (alt.text && !/error/i.test(alt.text)) {
|
|
831
|
+
res = alt;
|
|
832
|
+
break;
|
|
833
|
+
}
|
|
834
|
+
} catch {}
|
|
835
|
+
}
|
|
836
|
+
return {
|
|
837
|
+
ok: true,
|
|
838
|
+
record: normalizeWhois(domain, tld, res.text, res.serverQueried, now, !!opts?.includeRaw)
|
|
839
|
+
};
|
|
840
|
+
} catch (err) {
|
|
841
|
+
return {
|
|
842
|
+
ok: false,
|
|
843
|
+
error: err instanceof Error ? err.message : String(err)
|
|
844
|
+
};
|
|
845
|
+
}
|
|
93
846
|
}
|
|
94
847
|
/** Determine if a domain appears available (not registered).
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
return res.record.isRegistered === false;
|
|
848
|
+
* Performs a lookup and resolves to a boolean. Rejects on lookup error. */
|
|
849
|
+
async function isAvailable(domain, opts) {
|
|
850
|
+
const res = await lookupDomain(domain, opts);
|
|
851
|
+
if (!res.ok || !res.record) throw new Error(res.error || "Lookup failed");
|
|
852
|
+
return res.record.isRegistered === false;
|
|
101
853
|
}
|
|
102
854
|
/** Determine if a domain appears registered.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
return res.record.isRegistered === true;
|
|
855
|
+
* Performs a lookup and resolves to a boolean. Rejects on lookup error. */
|
|
856
|
+
async function isRegistered(domain, opts) {
|
|
857
|
+
const res = await lookupDomain(domain, opts);
|
|
858
|
+
if (!res.ok || !res.record) throw new Error(res.error || "Lookup failed");
|
|
859
|
+
return res.record.isRegistered === true;
|
|
109
860
|
}
|
|
861
|
+
|
|
862
|
+
//#endregion
|
|
863
|
+
export { isAvailable, isRegistered, lookupDomain };
|