rdapper 0.1.1 → 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/dist/index.d.ts +91 -5
- package/dist/index.js +857 -114
- package/package.json +12 -9
- 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 -24
- package/dist/whois/discovery.js +0 -99
- 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/package.json
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rdapper",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "🎩 RDAP/WHOIS fetcher, parser, and normalizer for Node",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
8
8
|
"url": "git+https://github.com/jakejarvis/rdapper.git"
|
|
9
9
|
},
|
|
10
|
+
"bugs": {
|
|
11
|
+
"url": "https://github.com/jakejarvis/rdapper/issues"
|
|
12
|
+
},
|
|
10
13
|
"author": {
|
|
11
14
|
"name": "Jake Jarvis",
|
|
12
15
|
"email": "jake@jarv.is",
|
|
@@ -14,6 +17,7 @@
|
|
|
14
17
|
},
|
|
15
18
|
"type": "module",
|
|
16
19
|
"main": "./dist/index.js",
|
|
20
|
+
"module": "./dist/index.js",
|
|
17
21
|
"types": "./dist/index.d.ts",
|
|
18
22
|
"exports": {
|
|
19
23
|
".": {
|
|
@@ -26,13 +30,12 @@
|
|
|
26
30
|
"dist"
|
|
27
31
|
],
|
|
28
32
|
"scripts": {
|
|
29
|
-
"
|
|
30
|
-
"
|
|
31
|
-
"
|
|
32
|
-
"test
|
|
33
|
-
"test:
|
|
34
|
-
"
|
|
35
|
-
"lint": "biome check --write",
|
|
33
|
+
"build": "tsdown",
|
|
34
|
+
"dev": "tsdown --watch",
|
|
35
|
+
"typecheck": "tsc --noEmit",
|
|
36
|
+
"test": "vitest",
|
|
37
|
+
"test:run": "vitest run",
|
|
38
|
+
"lint": "biome check",
|
|
36
39
|
"prepublishOnly": "npm run build"
|
|
37
40
|
},
|
|
38
41
|
"dependencies": {
|
|
@@ -42,7 +45,7 @@
|
|
|
42
45
|
"@biomejs/biome": "2.2.4",
|
|
43
46
|
"@types/node": "24.5.2",
|
|
44
47
|
"@types/psl": "1.1.3",
|
|
45
|
-
"
|
|
48
|
+
"tsdown": "0.15.5",
|
|
46
49
|
"typescript": "5.9.2",
|
|
47
50
|
"vitest": "^3.2.4"
|
|
48
51
|
},
|
package/dist/lib/async.d.ts
DELETED
package/dist/lib/async.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
export function withTimeout(promise, timeoutMs, reason = "Timeout") {
|
|
2
|
-
if (!Number.isFinite(timeoutMs) || timeoutMs <= 0)
|
|
3
|
-
return promise;
|
|
4
|
-
let timer;
|
|
5
|
-
const timeout = new Promise((_, reject) => {
|
|
6
|
-
timer = setTimeout(() => reject(new Error(reason)), timeoutMs);
|
|
7
|
-
});
|
|
8
|
-
return Promise.race([
|
|
9
|
-
promise.finally(() => {
|
|
10
|
-
if (timer !== undefined)
|
|
11
|
-
clearTimeout(timer);
|
|
12
|
-
}),
|
|
13
|
-
timeout,
|
|
14
|
-
]);
|
|
15
|
-
}
|
|
16
|
-
export function sleep(ms) {
|
|
17
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
18
|
-
}
|
package/dist/lib/constants.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare const DEFAULT_TIMEOUT_MS = 15000;
|
package/dist/lib/constants.js
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export const DEFAULT_TIMEOUT_MS = 15000;
|
package/dist/lib/dates.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function toISO(dateLike: string | number | Date | undefined | null): string | undefined;
|
package/dist/lib/dates.js
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
// Lightweight date parsing helpers to avoid external dependencies.
|
|
2
|
-
// We aim to parse common RDAP and WHOIS date representations and return a UTC ISO string.
|
|
3
|
-
export function toISO(dateLike) {
|
|
4
|
-
if (dateLike == null)
|
|
5
|
-
return undefined;
|
|
6
|
-
if (dateLike instanceof Date)
|
|
7
|
-
return toIsoFromDate(dateLike);
|
|
8
|
-
if (typeof dateLike === "number")
|
|
9
|
-
return toIsoFromDate(new Date(dateLike));
|
|
10
|
-
const raw = String(dateLike).trim();
|
|
11
|
-
if (!raw)
|
|
12
|
-
return undefined;
|
|
13
|
-
// Try several structured formats seen in WHOIS outputs (treat as UTC when no TZ provided)
|
|
14
|
-
const tryFormats = [
|
|
15
|
-
// 2023-01-02 03:04:05Z or without Z
|
|
16
|
-
/^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2}):(\d{2})(?:Z)?$/,
|
|
17
|
-
// 2023/01/02 03:04:05
|
|
18
|
-
/^(\d{4})\/(\d{2})\/(\d{2})[ T](\d{2}):(\d{2}):(\d{2})$/,
|
|
19
|
-
// 02-Jan-2023
|
|
20
|
-
/^(\d{2})-([A-Za-z]{3})-(\d{4})$/,
|
|
21
|
-
// Jan 02 2023
|
|
22
|
-
/^([A-Za-z]{3})\s+(\d{1,2})\s+(\d{4})$/,
|
|
23
|
-
];
|
|
24
|
-
for (const re of tryFormats) {
|
|
25
|
-
const m = raw.match(re);
|
|
26
|
-
if (!m)
|
|
27
|
-
continue;
|
|
28
|
-
const d = parseWithRegex(m, re);
|
|
29
|
-
if (d)
|
|
30
|
-
return toIsoFromDate(d);
|
|
31
|
-
}
|
|
32
|
-
// Fallback to native Date parsing (handles ISO and RFC2822 with TZ)
|
|
33
|
-
const native = new Date(raw);
|
|
34
|
-
if (!Number.isNaN(native.getTime()))
|
|
35
|
-
return toIsoFromDate(native);
|
|
36
|
-
return undefined;
|
|
37
|
-
}
|
|
38
|
-
function toIsoFromDate(d) {
|
|
39
|
-
try {
|
|
40
|
-
return new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate(), d.getUTCHours(), d.getUTCMinutes(), d.getUTCSeconds(), 0))
|
|
41
|
-
.toISOString()
|
|
42
|
-
.replace(/\.\d{3}Z$/, "Z");
|
|
43
|
-
}
|
|
44
|
-
catch {
|
|
45
|
-
return undefined;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
function parseWithRegex(m, _re) {
|
|
49
|
-
const monthMap = {
|
|
50
|
-
jan: 0,
|
|
51
|
-
feb: 1,
|
|
52
|
-
mar: 2,
|
|
53
|
-
apr: 3,
|
|
54
|
-
may: 4,
|
|
55
|
-
jun: 5,
|
|
56
|
-
jul: 6,
|
|
57
|
-
aug: 7,
|
|
58
|
-
sep: 8,
|
|
59
|
-
oct: 9,
|
|
60
|
-
nov: 10,
|
|
61
|
-
dec: 11,
|
|
62
|
-
};
|
|
63
|
-
try {
|
|
64
|
-
// If the matched string contains time components, parse as Y-M-D H:M:S
|
|
65
|
-
if (m[0].includes(":")) {
|
|
66
|
-
const [_, y, mo, d, hh, mm, ss] = m;
|
|
67
|
-
return new Date(Date.UTC(Number(y), Number(mo) - 1, Number(d), Number(hh), Number(mm), Number(ss)));
|
|
68
|
-
}
|
|
69
|
-
// If the matched string contains hyphens, treat as DD-MMM-YYYY
|
|
70
|
-
if (m[0].includes("-")) {
|
|
71
|
-
const [_, dd, monStr, yyyy] = m;
|
|
72
|
-
const mon = monthMap[monStr.toLowerCase()];
|
|
73
|
-
return new Date(Date.UTC(Number(yyyy), mon, Number(dd)));
|
|
74
|
-
}
|
|
75
|
-
// Otherwise treat as MMM DD YYYY
|
|
76
|
-
const [_, monStr, dd, yyyy] = m;
|
|
77
|
-
const mon = monthMap[monStr.toLowerCase()];
|
|
78
|
-
return new Date(Date.UTC(Number(yyyy), mon, Number(dd)));
|
|
79
|
-
}
|
|
80
|
-
catch {
|
|
81
|
-
// fall through to undefined
|
|
82
|
-
}
|
|
83
|
-
return undefined;
|
|
84
|
-
}
|
package/dist/lib/domain.d.ts
DELETED
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
export declare function extractTld(domain: string): string;
|
|
2
|
-
export declare function getDomainParts(domain: string): {
|
|
3
|
-
publicSuffix: string;
|
|
4
|
-
tld: string;
|
|
5
|
-
};
|
|
6
|
-
export declare function isLikelyDomain(input: string): boolean;
|
|
7
|
-
export declare function punyToUnicode(domain: string): string;
|
|
8
|
-
export declare function isWhoisAvailable(text: string | undefined): boolean;
|
package/dist/lib/domain.js
DELETED
|
@@ -1,64 +0,0 @@
|
|
|
1
|
-
import psl from "psl";
|
|
2
|
-
export function extractTld(domain) {
|
|
3
|
-
const lower = domain.trim().toLowerCase();
|
|
4
|
-
try {
|
|
5
|
-
const parsed = psl.parse?.(lower);
|
|
6
|
-
const suffix = parsed?.tld;
|
|
7
|
-
if (suffix) {
|
|
8
|
-
const labels = String(suffix).split(".").filter(Boolean);
|
|
9
|
-
if (labels.length)
|
|
10
|
-
return labels[labels.length - 1];
|
|
11
|
-
}
|
|
12
|
-
}
|
|
13
|
-
catch {
|
|
14
|
-
// ignore and fall back
|
|
15
|
-
}
|
|
16
|
-
const parts = lower.split(".").filter(Boolean);
|
|
17
|
-
return parts[parts.length - 1] ?? lower;
|
|
18
|
-
}
|
|
19
|
-
export function getDomainParts(domain) {
|
|
20
|
-
const lower = domain.toLowerCase().trim();
|
|
21
|
-
let publicSuffix;
|
|
22
|
-
try {
|
|
23
|
-
const parsed = psl.parse?.(lower);
|
|
24
|
-
publicSuffix = parsed?.tld;
|
|
25
|
-
}
|
|
26
|
-
catch {
|
|
27
|
-
// ignore
|
|
28
|
-
}
|
|
29
|
-
if (!publicSuffix) {
|
|
30
|
-
const parts = lower.split(".").filter(Boolean);
|
|
31
|
-
publicSuffix = parts.length ? parts[parts.length - 1] : lower;
|
|
32
|
-
}
|
|
33
|
-
const labels = publicSuffix.split(".").filter(Boolean);
|
|
34
|
-
const tld = labels.length ? labels[labels.length - 1] : publicSuffix;
|
|
35
|
-
return { publicSuffix, tld };
|
|
36
|
-
}
|
|
37
|
-
export function isLikelyDomain(input) {
|
|
38
|
-
return /^[a-z0-9.-]+$/i.test(input) && input.includes(".");
|
|
39
|
-
}
|
|
40
|
-
export function punyToUnicode(domain) {
|
|
41
|
-
try {
|
|
42
|
-
return domain.normalize("NFC");
|
|
43
|
-
}
|
|
44
|
-
catch {
|
|
45
|
-
return domain;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
// Common WHOIS availability phrases seen across registries/registrars
|
|
49
|
-
const WHOIS_AVAILABLE_PATTERNS = [
|
|
50
|
-
/\bno match\b/i,
|
|
51
|
-
/\bnot found\b/i,
|
|
52
|
-
/\bno entries found\b/i,
|
|
53
|
-
/\bno data found\b/i,
|
|
54
|
-
/\bavailable for registration\b/i,
|
|
55
|
-
/\bdomain\s+available\b/i,
|
|
56
|
-
/\bdomain status[:\s]+available\b/i,
|
|
57
|
-
/\bobject does not exist\b/i,
|
|
58
|
-
/\bthe queried object does not exist\b/i,
|
|
59
|
-
];
|
|
60
|
-
export function isWhoisAvailable(text) {
|
|
61
|
-
if (!text)
|
|
62
|
-
return false;
|
|
63
|
-
return WHOIS_AVAILABLE_PATTERNS.some((re) => re.test(text));
|
|
64
|
-
}
|
package/dist/lib/text.d.ts
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
export declare function uniq<T>(arr: T[] | undefined | null): T[] | undefined;
|
|
2
|
-
export declare function parseKeyValueLines(text: string): Record<string, string[]>;
|
|
3
|
-
export declare function parseCsv(value: string | undefined): string[] | undefined;
|
|
4
|
-
export declare function asString(value: unknown): string | undefined;
|
|
5
|
-
export declare function asStringArray(value: unknown): string[] | undefined;
|
|
6
|
-
export declare function asDateLike(value: unknown): string | number | Date | undefined;
|
package/dist/lib/text.js
DELETED
|
@@ -1,77 +0,0 @@
|
|
|
1
|
-
export function uniq(arr) {
|
|
2
|
-
if (!arr)
|
|
3
|
-
return undefined;
|
|
4
|
-
return Array.from(new Set(arr));
|
|
5
|
-
}
|
|
6
|
-
export function parseKeyValueLines(text) {
|
|
7
|
-
const map = new Map();
|
|
8
|
-
const lines = text.split(/\r?\n/);
|
|
9
|
-
let lastKey;
|
|
10
|
-
for (const rawLine of lines) {
|
|
11
|
-
const line = rawLine.replace(/\s+$/, "");
|
|
12
|
-
if (!line.trim())
|
|
13
|
-
continue;
|
|
14
|
-
// Bracketed form: [Key] value (common in .jp and some ccTLDs)
|
|
15
|
-
const bracket = line.match(/^\s*\[([^\]]+)\]\s*(.*)$/);
|
|
16
|
-
if (bracket) {
|
|
17
|
-
const key = bracket[1].trim().toLowerCase();
|
|
18
|
-
const value = bracket[2].trim();
|
|
19
|
-
const list = map.get(key) ?? [];
|
|
20
|
-
if (value)
|
|
21
|
-
list.push(value);
|
|
22
|
-
map.set(key, list);
|
|
23
|
-
lastKey = key;
|
|
24
|
-
continue;
|
|
25
|
-
}
|
|
26
|
-
// Colon form: Key: value
|
|
27
|
-
const idx = line.indexOf(":");
|
|
28
|
-
if (idx !== -1) {
|
|
29
|
-
const key = line.slice(0, idx).trim().toLowerCase();
|
|
30
|
-
const value = line.slice(idx + 1).trim();
|
|
31
|
-
if (!key) {
|
|
32
|
-
lastKey = undefined;
|
|
33
|
-
continue;
|
|
34
|
-
}
|
|
35
|
-
const list = map.get(key) ?? [];
|
|
36
|
-
if (value)
|
|
37
|
-
list.push(value);
|
|
38
|
-
map.set(key, list);
|
|
39
|
-
lastKey = key;
|
|
40
|
-
continue;
|
|
41
|
-
}
|
|
42
|
-
// Continuation line: starts with indentation after a key appeared
|
|
43
|
-
if (lastKey && /^\s+/.test(line)) {
|
|
44
|
-
const value = line.trim();
|
|
45
|
-
if (value) {
|
|
46
|
-
const list = map.get(lastKey) ?? [];
|
|
47
|
-
list.push(value);
|
|
48
|
-
map.set(lastKey, list);
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
// Otherwise ignore non key-value lines
|
|
52
|
-
}
|
|
53
|
-
return Object.fromEntries(map);
|
|
54
|
-
}
|
|
55
|
-
export function parseCsv(value) {
|
|
56
|
-
if (!value)
|
|
57
|
-
return undefined;
|
|
58
|
-
return value
|
|
59
|
-
.split(/[,\s]+/)
|
|
60
|
-
.map((s) => s.trim())
|
|
61
|
-
.filter(Boolean);
|
|
62
|
-
}
|
|
63
|
-
export function asString(value) {
|
|
64
|
-
return typeof value === "string" ? value : undefined;
|
|
65
|
-
}
|
|
66
|
-
export function asStringArray(value) {
|
|
67
|
-
return Array.isArray(value)
|
|
68
|
-
? value.filter((x) => typeof x === "string")
|
|
69
|
-
: undefined;
|
|
70
|
-
}
|
|
71
|
-
export function asDateLike(value) {
|
|
72
|
-
if (typeof value === "string" ||
|
|
73
|
-
typeof value === "number" ||
|
|
74
|
-
value instanceof Date)
|
|
75
|
-
return value;
|
|
76
|
-
return undefined;
|
|
77
|
-
}
|
package/dist/rdap/bootstrap.d.ts
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import type { LookupOptions } from "../types.js";
|
|
2
|
-
/**
|
|
3
|
-
* Resolve RDAP base URLs for a given TLD using IANA's bootstrap registry.
|
|
4
|
-
* Returns zero or more base URLs (always suffixed with a trailing slash).
|
|
5
|
-
*/
|
|
6
|
-
export declare function getRdapBaseUrlsForTld(tld: string, options?: LookupOptions): Promise<string[]>;
|
package/dist/rdap/bootstrap.js
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { withTimeout } from "../lib/async.js";
|
|
2
|
-
import { DEFAULT_TIMEOUT_MS } from "../lib/constants.js";
|
|
3
|
-
/**
|
|
4
|
-
* Resolve RDAP base URLs for a given TLD using IANA's bootstrap registry.
|
|
5
|
-
* Returns zero or more base URLs (always suffixed with a trailing slash).
|
|
6
|
-
*/
|
|
7
|
-
export async function getRdapBaseUrlsForTld(tld, options) {
|
|
8
|
-
const bootstrapUrl = options?.customBootstrapUrl ?? "https://data.iana.org/rdap/dns.json";
|
|
9
|
-
const res = await withTimeout(fetch(bootstrapUrl, {
|
|
10
|
-
method: "GET",
|
|
11
|
-
headers: { accept: "application/json" },
|
|
12
|
-
signal: options?.signal,
|
|
13
|
-
}), options?.timeoutMs ?? DEFAULT_TIMEOUT_MS, "RDAP bootstrap timeout");
|
|
14
|
-
if (!res.ok)
|
|
15
|
-
return [];
|
|
16
|
-
const data = (await res.json());
|
|
17
|
-
const target = tld.toLowerCase();
|
|
18
|
-
const bases = [];
|
|
19
|
-
for (const svc of data.services) {
|
|
20
|
-
const tlds = svc[0];
|
|
21
|
-
const urls = svc[1];
|
|
22
|
-
if (tlds.map((x) => x.toLowerCase()).includes(target)) {
|
|
23
|
-
for (const u of urls) {
|
|
24
|
-
const base = u.endsWith("/") ? u : `${u}/`;
|
|
25
|
-
bases.push(base);
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
}
|
|
29
|
-
return Array.from(new Set(bases));
|
|
30
|
-
}
|
package/dist/rdap/client.d.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
import type { LookupOptions } from "../types.js";
|
|
2
|
-
/**
|
|
3
|
-
* Fetch RDAP JSON for a domain from a specific RDAP base URL.
|
|
4
|
-
* Throws on HTTP >= 400 (includes RDAP error JSON payloads).
|
|
5
|
-
*/
|
|
6
|
-
export declare function fetchRdapDomain(domain: string, baseUrl: string, options?: LookupOptions): Promise<{
|
|
7
|
-
url: string;
|
|
8
|
-
json: unknown;
|
|
9
|
-
}>;
|
package/dist/rdap/client.js
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { withTimeout } from "../lib/async.js";
|
|
2
|
-
import { DEFAULT_TIMEOUT_MS } from "../lib/constants.js";
|
|
3
|
-
// Use global fetch (Node 18+). For large JSON we keep it simple.
|
|
4
|
-
/**
|
|
5
|
-
* Fetch RDAP JSON for a domain from a specific RDAP base URL.
|
|
6
|
-
* Throws on HTTP >= 400 (includes RDAP error JSON payloads).
|
|
7
|
-
*/
|
|
8
|
-
export async function fetchRdapDomain(domain, baseUrl, options) {
|
|
9
|
-
const url = new URL(`domain/${encodeURIComponent(domain)}`, baseUrl).toString();
|
|
10
|
-
const res = await withTimeout(fetch(url, {
|
|
11
|
-
method: "GET",
|
|
12
|
-
headers: { accept: "application/rdap+json, application/json" },
|
|
13
|
-
signal: options?.signal,
|
|
14
|
-
}), options?.timeoutMs ?? DEFAULT_TIMEOUT_MS, "RDAP lookup timeout");
|
|
15
|
-
if (!res.ok) {
|
|
16
|
-
const bodyText = await res.text();
|
|
17
|
-
throw new Error(`RDAP ${res.status}: ${bodyText.slice(0, 500)}`);
|
|
18
|
-
}
|
|
19
|
-
const json = await res.json();
|
|
20
|
-
return { url, json };
|
|
21
|
-
}
|
package/dist/rdap/normalize.d.ts
DELETED
|
@@ -1,6 +0,0 @@
|
|
|
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;
|
package/dist/rdap/normalize.js
DELETED
|
@@ -1,214 +0,0 @@
|
|
|
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
|
-
}
|