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