rdapper 0.1.0 → 0.1.1
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.js +14 -3
- package/dist/whois/discovery.d.ts +15 -0
- package/dist/whois/discovery.js +53 -4
- package/package.json +8 -3
package/README.md
CHANGED
|
@@ -7,7 +7,7 @@ RDAP‑first domain registration lookups with WHOIS fallback. Produces a single,
|
|
|
7
7
|
- Normalized output: registrar, contacts, nameservers, statuses, dates, DNSSEC, source metadata
|
|
8
8
|
- TypeScript types included; ESM‑only; no external HTTP client (uses global `fetch`)
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
### [🦉 See it in action on hoot.sh!](https://hoot.sh)
|
|
11
11
|
|
|
12
12
|
## Install
|
|
13
13
|
|
|
@@ -155,9 +155,11 @@ Timeouts are enforced per request using a simple race against `timeoutMs` (defau
|
|
|
155
155
|
## Development
|
|
156
156
|
|
|
157
157
|
- Build: `npm run build`
|
|
158
|
-
- Test: `npm test`
|
|
158
|
+
- Test: `npm test` (Vitest)
|
|
159
159
|
- By default, tests are offline/deterministic.
|
|
160
|
-
-
|
|
160
|
+
- Watch mode: `npm run test:watch`
|
|
161
|
+
- Coverage: `npm run test:coverage`
|
|
162
|
+
- Smoke tests that hit the network are gated by `SMOKE=1`, e.g. `SMOKE=1 npm run test:smoke`.
|
|
161
163
|
- Lint/format: `npm run lint` (Biome)
|
|
162
164
|
|
|
163
165
|
Project layout:
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import { getRdapBaseUrlsForTld } from "./rdap/bootstrap.js";
|
|
|
4
4
|
import { fetchRdapDomain } from "./rdap/client.js";
|
|
5
5
|
import { normalizeRdap } from "./rdap/normalize.js";
|
|
6
6
|
import { whoisQuery } from "./whois/client.js";
|
|
7
|
-
import { extractWhoisReferral, ianaWhoisServerForTld, } from "./whois/discovery.js";
|
|
7
|
+
import { extractWhoisReferral, ianaWhoisServerForTld, getIanaWhoisTextForTld, parseIanaRegistrationInfoUrl, } from "./whois/discovery.js";
|
|
8
8
|
import { normalizeWhois } from "./whois/normalize.js";
|
|
9
9
|
import { WHOIS_TLD_EXCEPTIONS } from "./whois/servers.js";
|
|
10
10
|
/**
|
|
@@ -38,14 +38,25 @@ export async function lookupDomain(domain, opts) {
|
|
|
38
38
|
if (opts?.rdapOnly) {
|
|
39
39
|
return {
|
|
40
40
|
ok: false,
|
|
41
|
-
error:
|
|
41
|
+
error: `RDAP not available or failed for TLD '${tld}'. Many TLDs do not publish RDAP; try WHOIS fallback (omit rdapOnly).`,
|
|
42
42
|
};
|
|
43
43
|
}
|
|
44
44
|
}
|
|
45
45
|
// WHOIS fallback path
|
|
46
46
|
const whoisServer = await ianaWhoisServerForTld(tld, opts);
|
|
47
47
|
if (!whoisServer) {
|
|
48
|
-
|
|
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
|
+
};
|
|
49
60
|
}
|
|
50
61
|
// Query the TLD server first; if it returns a referral, we follow it below.
|
|
51
62
|
let res = await whoisQuery(whoisServer, domain, opts);
|
|
@@ -1,4 +1,19 @@
|
|
|
1
1
|
import type { LookupOptions } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Parse the IANA WHOIS response for a TLD and extract the WHOIS server
|
|
4
|
+
* without crossing line boundaries. Some TLDs (e.g. .np) leave the field
|
|
5
|
+
* blank, in which case this returns undefined.
|
|
6
|
+
*/
|
|
7
|
+
export declare function parseIanaWhoisServer(text: string): string | undefined;
|
|
8
|
+
/**
|
|
9
|
+
* Parse a likely registration information URL from an IANA WHOIS response.
|
|
10
|
+
* Looks at lines like:
|
|
11
|
+
* remarks: Registration information: http://example.tld
|
|
12
|
+
* url: https://registry.example
|
|
13
|
+
*/
|
|
14
|
+
export declare function parseIanaRegistrationInfoUrl(text: string): string | undefined;
|
|
15
|
+
/** Fetch raw IANA WHOIS text for a TLD (best-effort). */
|
|
16
|
+
export declare function getIanaWhoisTextForTld(tld: string, options?: LookupOptions): Promise<string | undefined>;
|
|
2
17
|
/**
|
|
3
18
|
* Best-effort discovery of the authoritative WHOIS server for a TLD via IANA root DB.
|
|
4
19
|
*/
|
package/dist/whois/discovery.js
CHANGED
|
@@ -1,5 +1,57 @@
|
|
|
1
1
|
import { whoisQuery } from "./client.js";
|
|
2
2
|
import { WHOIS_TLD_EXCEPTIONS } from "./servers.js";
|
|
3
|
+
/**
|
|
4
|
+
* Parse the IANA WHOIS response for a TLD and extract the WHOIS server
|
|
5
|
+
* without crossing line boundaries. Some TLDs (e.g. .np) leave the field
|
|
6
|
+
* blank, in which case this returns undefined.
|
|
7
|
+
*/
|
|
8
|
+
export function parseIanaWhoisServer(text) {
|
|
9
|
+
// Search lines in priority order: whois, refer, whois server
|
|
10
|
+
const fields = ["whois", "refer", "whois server"];
|
|
11
|
+
const lines = String(text).split(/\r?\n/);
|
|
12
|
+
for (const field of fields) {
|
|
13
|
+
for (const raw of lines) {
|
|
14
|
+
const line = raw.trimEnd();
|
|
15
|
+
// Match beginning of line, allowing leading spaces, case-insensitive
|
|
16
|
+
const re = new RegExp(`^\\s*${field}\\s*:\\s*(.*?)$`, "i");
|
|
17
|
+
const m = line.match(re);
|
|
18
|
+
if (m) {
|
|
19
|
+
const value = (m[1] || "").trim();
|
|
20
|
+
if (value)
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Parse a likely registration information URL from an IANA WHOIS response.
|
|
29
|
+
* Looks at lines like:
|
|
30
|
+
* remarks: Registration information: http://example.tld
|
|
31
|
+
* url: https://registry.example
|
|
32
|
+
*/
|
|
33
|
+
export function parseIanaRegistrationInfoUrl(text) {
|
|
34
|
+
const lines = String(text).split(/\r?\n/);
|
|
35
|
+
for (const raw of lines) {
|
|
36
|
+
const line = raw.trim();
|
|
37
|
+
if (!/^\s*(remarks|url|website)\s*:/i.test(line))
|
|
38
|
+
continue;
|
|
39
|
+
const urlMatch = line.match(/https?:\/\/\S+/i);
|
|
40
|
+
if (urlMatch?.[0])
|
|
41
|
+
return urlMatch[0];
|
|
42
|
+
}
|
|
43
|
+
return undefined;
|
|
44
|
+
}
|
|
45
|
+
/** Fetch raw IANA WHOIS text for a TLD (best-effort). */
|
|
46
|
+
export async function getIanaWhoisTextForTld(tld, options) {
|
|
47
|
+
try {
|
|
48
|
+
const res = await whoisQuery("whois.iana.org", tld.toLowerCase(), options);
|
|
49
|
+
return res.text;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
3
55
|
/**
|
|
4
56
|
* Best-effort discovery of the authoritative WHOIS server for a TLD via IANA root DB.
|
|
5
57
|
*/
|
|
@@ -13,10 +65,7 @@ export async function ianaWhoisServerForTld(tld, options) {
|
|
|
13
65
|
try {
|
|
14
66
|
const res = await whoisQuery("whois.iana.org", key, options);
|
|
15
67
|
const txt = res.text;
|
|
16
|
-
const
|
|
17
|
-
txt.match(/^refer:\s*(\S+)/im) ||
|
|
18
|
-
txt.match(/^whois server:\s*(\S+)/im);
|
|
19
|
-
const server = m?.[1];
|
|
68
|
+
const server = parseIanaWhoisServer(txt);
|
|
20
69
|
if (server)
|
|
21
70
|
return normalizeServer(server);
|
|
22
71
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rdapper",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.1",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"description": "🎩 RDAP/WHOIS fetcher, parser, and normalizer for Node",
|
|
6
6
|
"repository": {
|
|
@@ -28,7 +28,10 @@
|
|
|
28
28
|
"scripts": {
|
|
29
29
|
"clean": "rm -rf dist",
|
|
30
30
|
"build": "npm run clean && tsc -p tsconfig.build.json",
|
|
31
|
-
"test": "npm run clean && tsc -p tsconfig.json &&
|
|
31
|
+
"test": "npm run clean && tsc -p tsconfig.json && vitest run",
|
|
32
|
+
"test:watch": "npm run clean && tsc -p tsconfig.json && vitest",
|
|
33
|
+
"test:coverage": "npm run clean && tsc -p tsconfig.json && vitest run --coverage",
|
|
34
|
+
"test:smoke": "SMOKE=1 npm run test",
|
|
32
35
|
"lint": "biome check --write",
|
|
33
36
|
"prepublishOnly": "npm run build"
|
|
34
37
|
},
|
|
@@ -39,7 +42,9 @@
|
|
|
39
42
|
"@biomejs/biome": "2.2.4",
|
|
40
43
|
"@types/node": "24.5.2",
|
|
41
44
|
"@types/psl": "1.1.3",
|
|
42
|
-
"
|
|
45
|
+
"@vitest/coverage-v8": "^3.2.4",
|
|
46
|
+
"typescript": "5.9.2",
|
|
47
|
+
"vitest": "^3.2.4"
|
|
43
48
|
},
|
|
44
49
|
"engines": {
|
|
45
50
|
"node": ">=18.17"
|