@dakaio/domainscout-mcp 0.0.0-stage → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (125) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +192 -2
  3. package/dist/checkers/dns.js +58 -0
  4. package/dist/checkers/dns.js.map +1 -0
  5. package/dist/checkers/rdap-bootstrap.js +55 -0
  6. package/dist/checkers/rdap-bootstrap.js.map +1 -0
  7. package/dist/checkers/rdap.js +75 -0
  8. package/dist/checkers/rdap.js.map +1 -0
  9. package/dist/checkers/whois-client.js +45 -0
  10. package/dist/checkers/whois-client.js.map +1 -0
  11. package/dist/checkers/whois-reply.js +109 -0
  12. package/dist/checkers/whois-reply.js.map +1 -0
  13. package/dist/checkers/whois.js +100 -0
  14. package/dist/checkers/whois.js.map +1 -0
  15. package/dist/cli/cli-args.js +50 -0
  16. package/dist/cli/cli-args.js.map +1 -0
  17. package/dist/cli/cli.js +62 -0
  18. package/dist/cli/cli.js.map +1 -0
  19. package/dist/config/config-reader.js +104 -0
  20. package/dist/config/config-reader.js.map +1 -0
  21. package/dist/config/config.js +24 -0
  22. package/dist/config/config.js.map +1 -0
  23. package/dist/config/version.js +7 -0
  24. package/dist/config/version.js.map +1 -0
  25. package/dist/confirm/apis/cloudflare-api.js +121 -0
  26. package/dist/confirm/apis/cloudflare-api.js.map +1 -0
  27. package/dist/confirm/apis/namecom-api.js +82 -0
  28. package/dist/confirm/apis/namecom-api.js.map +1 -0
  29. package/dist/confirm/apis/porkbun-api.js +96 -0
  30. package/dist/confirm/apis/porkbun-api.js.map +1 -0
  31. package/dist/confirm/apis/spaceship-api.js +97 -0
  32. package/dist/confirm/apis/spaceship-api.js.map +1 -0
  33. package/dist/confirm/confirmer.js +101 -0
  34. package/dist/confirm/confirmer.js.map +1 -0
  35. package/dist/confirm/json-client.js +45 -0
  36. package/dist/confirm/json-client.js.map +1 -0
  37. package/dist/confirm/outcome.js +60 -0
  38. package/dist/confirm/outcome.js.map +1 -0
  39. package/dist/confirm/registrar-api.js +2 -0
  40. package/dist/confirm/registrar-api.js.map +1 -0
  41. package/dist/confirm/registrar-apis.js +20 -0
  42. package/dist/confirm/registrar-apis.js.map +1 -0
  43. package/dist/constants.js +309 -0
  44. package/dist/constants.js.map +1 -0
  45. package/dist/core/arrays.js +9 -0
  46. package/dist/core/arrays.js.map +1 -0
  47. package/dist/core/checker.js +147 -0
  48. package/dist/core/checker.js.map +1 -0
  49. package/dist/core/domain-name.js +89 -0
  50. package/dist/core/domain-name.js.map +1 -0
  51. package/dist/core/errors.js +42 -0
  52. package/dist/core/errors.js.map +1 -0
  53. package/dist/core/evidence.js +50 -0
  54. package/dist/core/evidence.js.map +1 -0
  55. package/dist/core/host-queues.js +60 -0
  56. package/dist/core/host-queues.js.map +1 -0
  57. package/dist/core/http-status.js +15 -0
  58. package/dist/core/http-status.js.map +1 -0
  59. package/dist/core/http.js +42 -0
  60. package/dist/core/http.js.map +1 -0
  61. package/dist/core/network.js +64 -0
  62. package/dist/core/network.js.map +1 -0
  63. package/dist/core/promise-cache.js +28 -0
  64. package/dist/core/promise-cache.js.map +1 -0
  65. package/dist/core/retry.js +27 -0
  66. package/dist/core/retry.js.map +1 -0
  67. package/dist/core/services.js +2 -0
  68. package/dist/core/services.js.map +1 -0
  69. package/dist/core/signals.js +6 -0
  70. package/dist/core/signals.js.map +1 -0
  71. package/dist/core/status.js +29 -0
  72. package/dist/core/status.js.map +1 -0
  73. package/dist/core/target-parser.js +79 -0
  74. package/dist/core/target-parser.js.map +1 -0
  75. package/dist/core/types.js +63 -0
  76. package/dist/core/types.js.map +1 -0
  77. package/dist/core/verdicts.js +39 -0
  78. package/dist/core/verdicts.js.map +1 -0
  79. package/dist/index.js +39 -0
  80. package/dist/index.js.map +1 -0
  81. package/dist/messages/cli.js +29 -0
  82. package/dist/messages/cli.js.map +1 -0
  83. package/dist/messages/config.js +19 -0
  84. package/dist/messages/config.js.map +1 -0
  85. package/dist/messages/errors.js +41 -0
  86. package/dist/messages/errors.js.map +1 -0
  87. package/dist/messages/index.js +10 -0
  88. package/dist/messages/index.js.map +1 -0
  89. package/dist/messages/notes.js +28 -0
  90. package/dist/messages/notes.js.map +1 -0
  91. package/dist/messages/prices.js +8 -0
  92. package/dist/messages/prices.js.map +1 -0
  93. package/dist/messages/report.js +80 -0
  94. package/dist/messages/report.js.map +1 -0
  95. package/dist/messages/tool.js +50 -0
  96. package/dist/messages/tool.js.map +1 -0
  97. package/dist/pricing/cloudflare-prices.js +32 -0
  98. package/dist/pricing/cloudflare-prices.js.map +1 -0
  99. package/dist/pricing/porkbun-prices.js +33 -0
  100. package/dist/pricing/porkbun-prices.js.map +1 -0
  101. package/dist/pricing/price-book.js +71 -0
  102. package/dist/pricing/price-book.js.map +1 -0
  103. package/dist/pricing/price-source.js +2 -0
  104. package/dist/pricing/price-source.js.map +1 -0
  105. package/dist/pricing/price.js +56 -0
  106. package/dist/pricing/price.js.map +1 -0
  107. package/dist/registrars/affiliates.js +28 -0
  108. package/dist/registrars/affiliates.js.map +1 -0
  109. package/dist/registrars/buy-links.js +47 -0
  110. package/dist/registrars/buy-links.js.map +1 -0
  111. package/dist/registrars/registrars.js +68 -0
  112. package/dist/registrars/registrars.js.map +1 -0
  113. package/dist/report/money.js +18 -0
  114. package/dist/report/money.js.map +1 -0
  115. package/dist/report/summary.js +24 -0
  116. package/dist/report/summary.js.map +1 -0
  117. package/dist/report/text-report.js +185 -0
  118. package/dist/report/text-report.js.map +1 -0
  119. package/dist/scout/domain-scout.js +136 -0
  120. package/dist/scout/domain-scout.js.map +1 -0
  121. package/dist/server/schemas.js +83 -0
  122. package/dist/server/schemas.js.map +1 -0
  123. package/dist/server/server.js +72 -0
  124. package/dist/server/server.js.map +1 -0
  125. package/package.json +67 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 dakaio.com
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,193 @@
1
- # Temporary Holding Version
1
+ # DomainScout (@dakaio/domainscout-mcp)
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Every good .com is taken. Find the ones that aren't.
4
+
5
+ DomainScout is an MCP server for bulk domain search. Your AI assistant brainstorms hundreds of
6
+ names, and DomainScout checks all of them in one call: which are free, what they cost and where
7
+ to buy them. It runs on your own machine. You need no account and no server of ours, and API keys
8
+ are optional.
9
+
10
+ Website: [domainscout.dakaio.com](https://domainscout.dakaio.com)
11
+
12
+ You ask your assistant *"I'm naming a note-taking app. Brainstorm 500 names and tell me which .com
13
+ domains are free."* The assistant calls `check_domains` once with all 500 names and gets back:
14
+
15
+ ```text
16
+ Checked 500 domain(s) in 21.8s: 27 available, 473 taken.
17
+
18
+ AVAILABLE (cheapest known offer for each):
19
+ - quillloom.com: $10.46/yr at Cloudflare https://www.cloudflare.com/domains/search/?q=quillloom.com
20
+ - scribeloom.com: $10.46/yr at Cloudflare https://www.cloudflare.com/domains/search/?q=scribeloom.com
21
+ - scribebloom.com: $10.46/yr at Cloudflare https://www.cloudflare.com/domains/search/?q=scribebloom.com
22
+ ...
23
+ TAKEN: quillwise.com, quillnest.com, quillflow.com, ...
24
+ ```
25
+
26
+ Ask about a few favourites afterwards and each one gets every registrar's link and price. More
27
+ real output, including the command line and JSON: [examples](examples/README.md).
28
+
29
+ ## Built for long lists
30
+
31
+ - **One call for the whole brainstorm.** Up to 500 domains per call by default
32
+ (`DOMAINSCOUT_MAX_DOMAINS`); 500 .com names take about 20 seconds.
33
+ - **Readable answers.** With more than five free domains, each one gets a single line with its
34
+ cheapest offer, so the answer stays short; every registrar's link is still in the structured
35
+ result.
36
+ - **Polite to the registries.** Taken names usually stop at DNS, so the registries only see the
37
+ names that might be free. Every server gets its own rate limit.
38
+
39
+ ## How it checks
40
+
41
+ For each domain, in parallel and within polite rate limits:
42
+
43
+ 1. **DNS.** If the TLD zone delegates the name, it is taken. This step is free and saves the
44
+ registries' rate limits.
45
+ 2. **The registry over RDAP**, the authoritative source. If the domain is found, it is taken;
46
+ the result includes expiry, and a note when the domain is being deleted and may become
47
+ available soon. A 404 means it is likely available.
48
+ 3. **WHOIS**, only for TLDs without RDAP (for example `.io`, `.ru`, `.de`) or when RDAP fails.
49
+ 4. **Your registrar API, optional.** If you set an API key, the registrar confirms that each
50
+ free-looking name can really be bought, flags premium names and gives the exact price.
51
+
52
+ Having no DNS never proves a domain is free: a bought but unused domain has no DNS either.
53
+ Without a registrar API key, "not in the registry" is reported as `likely_available`. Premium
54
+ and registry-reserved names look the same there, so the registrar's page has the final price.
55
+
56
+ | Status | Meaning |
57
+ |---|---|
58
+ | `available` | A registrar API confirmed it can be registered; `premium` says whether it costs extra |
59
+ | `likely_available` | Not in the registry or DNS, not confirmed by a registrar |
60
+ | `taken` | Registered |
61
+ | `reserved` | Blocked by the registry, or the registrar cannot sell it |
62
+ | `unknown` | Could not verify right now (timeout, rate limit); try again |
63
+
64
+ Free domains come with buy links for Cloudflare, Porkbun, Namecheap, Spaceship, GoDaddy,
65
+ Name.com, Dynadot, NameSilo and Hover. Links are sorted by the cost of the first term plus one
66
+ renewal year, using public price lists (Cloudflare, Porkbun) or the exact price from your
67
+ registrar API.
68
+
69
+ ## Install
70
+
71
+ Node 20 or newer.
72
+
73
+ **Claude Code**
74
+
75
+ ```bash
76
+ claude mcp add domainscout -- npx -y @dakaio/domainscout-mcp
77
+ ```
78
+
79
+ **Claude Desktop / Cursor**: add to the MCP config:
80
+
81
+ ```json
82
+ {
83
+ "mcpServers": {
84
+ "domainscout": {
85
+ "command": "npx",
86
+ "args": ["-y", "@dakaio/domainscout-mcp"],
87
+ "env": { "DOMAINSCOUT_TLDS": "com,net,ai,io" }
88
+ }
89
+ }
90
+ }
91
+ ```
92
+
93
+ On Windows, if `npx` is not found, use `"command": "cmd"` with
94
+ `"args": ["/c", "npx", "-y", "@dakaio/domainscout-mcp"]`.
95
+
96
+ Then just ask: *"Brainstorm 200 names for my coffee subscription app and tell me which .com
97
+ domains are free."*
98
+
99
+ **From source:** `npm install && npm run build`, then run `node /path/to/domainscout-mcp/dist/index.js`
100
+ in place of `npx -y @dakaio/domainscout-mcp`.
101
+
102
+ ## Settings
103
+
104
+ Set these as environment variables in the MCP config. All are optional.
105
+
106
+ | Variable | Default | What it does |
107
+ |---|---|---|
108
+ | `DOMAINSCOUT_TLDS` | `com,net,ai` | TLDs tried for names given without one |
109
+ | `DOMAINSCOUT_REGISTRARS` | all | Registrars to show buy links for, e.g. `cloudflare,porkbun,namecheap` |
110
+ | `DOMAINSCOUT_MAX_CONCURRENCY` | `10` | Network requests in flight at once |
111
+ | `DOMAINSCOUT_PER_HOST_CONCURRENCY` | `2` | Requests in flight to one RDAP server |
112
+ | `DOMAINSCOUT_TIMEOUT_MS` | `10000` | Timeout of one request |
113
+ | `DOMAINSCOUT_MAX_DOMAINS` | `500` | Domains per call, after adding TLDs |
114
+ | `DOMAINSCOUT_CONFIRM_MAX` | `50` | Free domains per call confirmed through your registrar API |
115
+ | `DOMAINSCOUT_DNS` | `on` | DNS pre-check |
116
+ | `DOMAINSCOUT_DNS_SERVERS` | system | DNS servers to use, comma-separated |
117
+ | `DOMAINSCOUT_PRICES` | `on` | Public price lists |
118
+ | `DOMAINSCOUT_AFFILIATE` | `on` | Affiliate buy links (`off` gives plain links) |
119
+
120
+ Each RDAP server gets at most 5 requests per second. Each WHOIS server gets one query at a
121
+ time, at most one per second. A server that answers "too many requests" is left alone for a while.
122
+
123
+ ## Registrar API keys (optional)
124
+
125
+ With a key, free-looking names are confirmed: you get `available` or `reserved` instead of
126
+ `likely_available`, premium names are flagged, and the registrar's exact price is shown. One
127
+ key is enough. With several, the APIs are asked in this order, each name only until one answers.
128
+
129
+ | Registrar | Variables | Where to get it |
130
+ |---|---|---|
131
+ | Name.com | `DOMAINSCOUT_NAMECOM_USERNAME`, `DOMAINSCOUT_NAMECOM_TOKEN` | Account Settings → Security → API Tokens |
132
+ | Cloudflare | `DOMAINSCOUT_CLOUDFLARE_ACCOUNT_ID`, `DOMAINSCOUT_CLOUDFLARE_API_TOKEN` | API token with Registrar permission. The Registrar API is in beta |
133
+ | Spaceship | `DOMAINSCOUT_SPACESHIP_API_KEY`, `DOMAINSCOUT_SPACESHIP_API_SECRET` | API Manager; the key needs the `domains:read` scope |
134
+ | Porkbun | `DOMAINSCOUT_PORKBUN_API_KEY`, `DOMAINSCOUT_PORKBUN_SECRET_KEY` | porkbun.com/account/api |
135
+
136
+ Porkbun throttles an account to 50 names per 5 minutes for a day once it has checked about
137
+ 1,800 names in 3 hours while registering few of them. `DOMAINSCOUT_CONFIRM_MAX` keeps every
138
+ call well below that. A tool call can also pass `confirm: false` to skip confirmation for a
139
+ large list.
140
+
141
+ Each key is sent only to its own registrar's API, and it never appears in output, logs or error
142
+ messages. If a registrar rejects a key, the result says so in its notes, and the check still
143
+ works without that registrar.
144
+
145
+ ## Privacy
146
+
147
+ Your queries go straight from your machine to:
148
+
149
+ - your DNS resolver,
150
+ - the registries (RDAP/WHOIS),
151
+ - two public price lists: the Porkbun price API, and cfdomainpricing.com, a community mirror
152
+ of Cloudflare's prices,
153
+ - the APIs of registrars whose keys you set.
154
+
155
+ `DOMAINSCOUT_PRICES=off` turns the price lists off. Nothing goes to a server of this project,
156
+ because there is none.
157
+
158
+ ## Affiliate links
159
+
160
+ Some buy links may be affiliate links. When they are, the result says so, and buying through
161
+ them earns the project a commission at no extra cost to you. Links are always ordered by price
162
+ and never by commission. `DOMAINSCOUT_AFFILIATE=off` turns them off.
163
+
164
+ ## Command line
165
+
166
+ ```bash
167
+ npx @dakaio/domainscout-mcp check acme getacme.io --tlds com,dev
168
+ npx @dakaio/domainscout-mcp check acme --json --no-confirm
169
+ npx @dakaio/domainscout-mcp check $(cat names.txt) --tlds com
170
+ ```
171
+
172
+ ## Development
173
+
174
+ ```bash
175
+ npm run verify # typecheck + lint (Biome) + tests with coverage thresholds
176
+ npm run build
177
+ ```
178
+
179
+ | Path | What it is |
180
+ |---|---|
181
+ | `src/core/` | The checking funnel, input parsing, shared types, network limits and retries |
182
+ | `src/checkers/` | DNS, RDAP and WHOIS lookups |
183
+ | `src/confirm/` | Confirmation through registrar APIs, one class per registrar |
184
+ | `src/pricing/`, `src/registrars/` | Public price lists, buy links and affiliate templates |
185
+ | `src/report/`, `src/messages/` | The text report and every text shown to people or to the model |
186
+ | `src/scout/`, `src/server/`, `src/cli/` | The check as a whole, the MCP server and the command line |
187
+ | `test/` | Vitest tests; the network is faked, except for a local WHOIS server |
188
+ | `examples/` | Real output of the tool |
189
+ | `site/` | The landing page at domainscout.dakaio.com: static HTML, deployed as is |
190
+
191
+ ## License
192
+
193
+ MIT
@@ -0,0 +1,58 @@
1
+ import { randomBytes } from 'node:crypto';
2
+ import { NODATA, NOTFOUND } from 'node:dns';
3
+ import { Resolver } from 'node:dns/promises';
4
+ import { DNS_MAX_TIMEOUT_MS, DNS_TRIES, WILDCARD_PROBE_BYTES, WILDCARD_PROBE_ENCODING, } from '../constants.js';
5
+ import { joinLabels } from '../core/domain-name.js';
6
+ import { PromiseCache } from '../core/promise-cache.js';
7
+ import { DnsState } from '../core/types.js';
8
+ /** DNS error codes that are answers, not failures. */
9
+ const ANSWER_CODES = new Map()
10
+ // The name does not exist in the zone.
11
+ .set(NOTFOUND, DnsState.NxDomain)
12
+ // The name exists but has no NS records.
13
+ .set(NODATA, DnsState.NoData);
14
+ /** NS lookups through the system resolver, or the DNS servers the user configured. */
15
+ export class DnsChecker {
16
+ resolver;
17
+ /** Whether each TLD is a wildcard TLD; asked once per TLD. */
18
+ wildcards = new PromiseCache();
19
+ constructor(servers, timeoutMs) {
20
+ this.resolver = new Resolver({
21
+ timeout: Math.min(timeoutMs, DNS_MAX_TIMEOUT_MS),
22
+ tries: DNS_TRIES,
23
+ });
24
+ if (servers.length > 0)
25
+ this.resolver.setServers(servers);
26
+ }
27
+ /** Asks for the domain's nameservers: only a registered domain can be delegated in the TLD zone. */
28
+ async lookupNs(domain) {
29
+ const started = Date.now();
30
+ try {
31
+ const nameservers = await this.resolver.resolveNs(domain);
32
+ const state = nameservers.length > 0 ? DnsState.Delegated : DnsState.NoData;
33
+ return { state, nameservers, ms: Date.now() - started };
34
+ }
35
+ catch (error) {
36
+ return this.failedLookup(error, Date.now() - started);
37
+ }
38
+ }
39
+ /** A few TLDs answer for any name (wildcard); for them a DNS answer proves nothing. */
40
+ hasWildcard(tld) {
41
+ return this.wildcards.get(tld, () => this.probeWildcard(tld));
42
+ }
43
+ /** Looks up a random name that nobody has registered: a delegation means the TLD answers for anything. */
44
+ async probeWildcard(tld) {
45
+ const probe = randomBytes(WILDCARD_PROBE_BYTES).toString(WILDCARD_PROBE_ENCODING);
46
+ const lookup = await this.lookupNs(joinLabels(probe, tld));
47
+ return lookup.state === DnsState.Delegated;
48
+ }
49
+ /** "Does not exist" and "no NS records" are answers; anything else is a failure. */
50
+ failedLookup(error, ms) {
51
+ const code = error.code;
52
+ const state = code === undefined ? undefined : ANSWER_CODES.get(code);
53
+ if (state)
54
+ return { state, ms };
55
+ return { state: DnsState.Error, error: code ?? String(error), ms };
56
+ }
57
+ }
58
+ //# sourceMappingURL=dns.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dns.js","sourceRoot":"","sources":["../../src/checkers/dns.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,55 @@
1
+ import { HTTPS_PROTOCOL, LABEL_SEPARATOR, RDAP_BOOTSTRAP_TTL_MS, RDAP_BOOTSTRAP_URL, Retry, } from '../constants.js';
2
+ import { ensureOk } from '../core/http.js';
3
+ import { PromiseCache } from '../core/promise-cache.js';
4
+ import { withRetry } from '../core/retry.js';
5
+ /** Base URLs must end with a slash, so relative paths resolve below them. */
6
+ const PATH_SEPARATOR = '/';
7
+ /** IANA's list of RDAP servers (RFC 9224), loaded on first use and reused for a day. */
8
+ export class RdapBootstrap {
9
+ network;
10
+ /** The loaded list. Shared by all lookups, so it never uses a caller's cancel signal. */
11
+ list = new PromiseCache(RDAP_BOOTSTRAP_TTL_MS);
12
+ constructor(network) {
13
+ this.network = network;
14
+ }
15
+ /** The RDAP base URL for a TLD, longest match first: "co.uk" tries "co.uk", then "uk". Undefined: no RDAP. */
16
+ async serverFor(tld) {
17
+ const servers = await this.list.get(RDAP_BOOTSTRAP_URL, () => this.load());
18
+ const match = this.suffixes(tld).find((suffix) => servers.has(suffix));
19
+ return match === undefined ? undefined : servers.get(match);
20
+ }
21
+ /** "co.uk" -> ["co.uk", "uk"] */
22
+ suffixes(tld) {
23
+ const labels = tld.split(LABEL_SEPARATOR);
24
+ return labels.map((_label, start) => labels.slice(start).join(LABEL_SEPARATOR));
25
+ }
26
+ load() {
27
+ return withRetry(() => this.network.run(() => this.fetchList()), Retry.bootstrap);
28
+ }
29
+ async fetchList() {
30
+ const url = new URL(RDAP_BOOTSTRAP_URL);
31
+ const response = await this.network.request(url);
32
+ await ensureOk(response, url);
33
+ const file = (await response.json());
34
+ return this.toServerList(file);
35
+ }
36
+ toServerList(file) {
37
+ const servers = new Map();
38
+ for (const [tlds, urls] of file.services ?? []) {
39
+ const base = this.preferredUrl(urls);
40
+ if (!base)
41
+ continue;
42
+ for (const tld of tlds)
43
+ servers.set(tld.toLowerCase(), base);
44
+ }
45
+ return servers;
46
+ }
47
+ /** The first https URL (or the first URL), ending with a slash. */
48
+ preferredUrl(urls) {
49
+ const url = urls.find((candidate) => new URL(candidate).protocol === HTTPS_PROTOCOL) ?? urls[0];
50
+ if (!url)
51
+ return undefined;
52
+ return url.endsWith(PATH_SEPARATOR) ? url : `${url}${PATH_SEPARATOR}`;
53
+ }
54
+ }
55
+ //# sourceMappingURL=rdap-bootstrap.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rdap-bootstrap.js","sourceRoot":"","sources":["../../src/checkers/rdap-bootstrap.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,75 @@
1
+ import { Header, HttpStatus, RDAP_ACCEPT, RDAP_DOMAIN_PATH, RdapEvent, Retry, } from '../constants.js';
2
+ import { errorMessage } from '../core/errors.js';
3
+ import { discardBody, retryableError } from '../core/http.js';
4
+ import { isRetryableStatus } from '../core/http-status.js';
5
+ import { withRetry } from '../core/retry.js';
6
+ import { LookupState } from '../core/types.js';
7
+ import { becauseOf, ErrorText, httpStatusText } from '../messages/index.js';
8
+ import { RdapBootstrap } from './rdap-bootstrap.js';
9
+ /** Headers of every RDAP request. */
10
+ const RDAP_HEADERS = { [Header.Accept]: RDAP_ACCEPT };
11
+ /** The registry's own answer about a domain, over RDAP (RFC 9082/9083). */
12
+ export class RdapChecker {
13
+ network;
14
+ bootstrap;
15
+ constructor(network, bootstrap = new RdapBootstrap(network)) {
16
+ this.network = network;
17
+ this.bootstrap = bootstrap;
18
+ }
19
+ async lookup(domain, tld, signal) {
20
+ let base;
21
+ try {
22
+ base = await this.bootstrap.serverFor(tld);
23
+ }
24
+ catch (error) {
25
+ const message = becauseOf(ErrorText.RdapListUnavailable, errorMessage(error));
26
+ return { state: LookupState.Error, error: message, ms: 0 };
27
+ }
28
+ if (!base)
29
+ return { state: LookupState.Unsupported };
30
+ return this.ask(new URL(`${RDAP_DOMAIN_PATH}${domain}`, base), signal);
31
+ }
32
+ /** Asks the server within its limits; rate limits, server errors and timeouts are retried. */
33
+ async ask(url, signal) {
34
+ const started = Date.now();
35
+ const send = () => this.fetchOnce(url, started, signal);
36
+ try {
37
+ const queued = () => this.network.viaQueues(this.network.rdapHosts, url.host, send, signal);
38
+ return await withRetry(queued, { ...Retry.rdap, signal });
39
+ }
40
+ catch (error) {
41
+ const ms = Date.now() - started;
42
+ return { state: LookupState.Error, server: url.host, error: errorMessage(error), ms };
43
+ }
44
+ }
45
+ /** 404 means not registered, 200 registered; 429 and 5xx throw to be retried. */
46
+ async fetchOnce(url, started, signal) {
47
+ const response = await this.network.request(url, { headers: RDAP_HEADERS }, signal);
48
+ const server = url.host;
49
+ const ms = Date.now() - started;
50
+ if (!response.ok)
51
+ await discardBody(response);
52
+ if (response.status === HttpStatus.NotFound)
53
+ return { state: LookupState.NotFound, server, ms };
54
+ if (isRetryableStatus(response.status))
55
+ throw retryableError(response, url);
56
+ if (!response.ok) {
57
+ return { state: LookupState.Error, server, error: httpStatusText(response.status), ms };
58
+ }
59
+ return this.registered((await response.json()), server, ms);
60
+ }
61
+ registered(data, server, ms) {
62
+ return {
63
+ state: LookupState.Registered,
64
+ server,
65
+ statuses: data.status ?? [],
66
+ created: this.eventDate(data, RdapEvent.Registration),
67
+ expires: this.eventDate(data, RdapEvent.Expiration),
68
+ ms,
69
+ };
70
+ }
71
+ eventDate(data, action) {
72
+ return data.events?.find((event) => event.eventAction === action)?.eventDate;
73
+ }
74
+ }
75
+ //# sourceMappingURL=rdap.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rdap.js","sourceRoot":"","sources":["../../src/checkers/rdap.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,45 @@
1
+ import { createConnection } from 'node:net';
2
+ import { WHOIS_ENCODING, WHOIS_LINE_END, WHOIS_MAX_REPLY_BYTES, WHOIS_PORT } from '../constants.js';
3
+ import { errorMessage, RetryableError } from '../core/errors.js';
4
+ import { withTimeout } from '../core/signals.js';
5
+ import { atHost, ErrorText } from '../messages/index.js';
6
+ /**
7
+ * Sends one query to a WHOIS server over TCP (RFC 3912) and returns the whole reply.
8
+ * Timeouts and network errors become RetryableError; a cancel by the caller does not.
9
+ */
10
+ export async function whoisQuery(server, query, timeoutMs, signal, port = WHOIS_PORT) {
11
+ if (signal?.aborted)
12
+ throw new Error(ErrorText.Cancelled);
13
+ const deadline = withTimeout(timeoutMs, signal);
14
+ const socket = createConnection({ host: server, port, signal: deadline });
15
+ socket.write(`${query}${WHOIS_LINE_END}`);
16
+ try {
17
+ return await readReply(socket);
18
+ }
19
+ catch (error) {
20
+ throw queryError(error, server, deadline, signal);
21
+ }
22
+ finally {
23
+ socket.destroy();
24
+ }
25
+ }
26
+ /** Reads until the server closes the connection, keeping at most WHOIS_MAX_REPLY_BYTES. */
27
+ async function readReply(socket) {
28
+ const chunks = [];
29
+ let size = 0;
30
+ for await (const chunk of socket) {
31
+ size += chunk.length;
32
+ if (size > WHOIS_MAX_REPLY_BYTES)
33
+ break;
34
+ chunks.push(chunk);
35
+ }
36
+ return Buffer.concat(chunks).toString(WHOIS_ENCODING);
37
+ }
38
+ /** A cancel stays a plain error; a timeout or a network error is worth another try. */
39
+ function queryError(error, server, deadline, signal) {
40
+ if (signal?.aborted)
41
+ return new Error(ErrorText.Cancelled);
42
+ const reason = deadline.aborted ? ErrorText.TimedOut : errorMessage(error);
43
+ return new RetryableError(atHost(server, reason));
44
+ }
45
+ //# sourceMappingURL=whois-client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"whois-client.js","sourceRoot":"","sources":["../../src/checkers/whois-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,gBAAgB,EAAe,MAAM,UAAU,CAAC;AACzD,OAAO,EAAE,cAAc,EAAE,cAAc,EAAE,qBAAqB,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AACpG,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACjE,OAAO,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AACjD,OAAO,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAUzD;;;GAGG;AACH,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,MAAc,EACd,KAAa,EACb,SAAiB,EACjB,MAAoB,EACpB,IAAI,GAAW,UAAU;IAEzB,IAAI,MAAM,EAAE,OAAO;QAAE,MAAM,IAAI,KAAK,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;IAC1D,MAAM,QAAQ,GAAG,WAAW,CAAC,SAAS,EAAE,MAAM,CAAC,CAAC;IAChD,MAAM,MAAM,GAAG,gBAAgB,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,CAAC,CAAC;IAC1E,MAAM,CAAC,KAAK,CAAC,GAAG,KAAK,GAAG,cAAc,EAAE,CAAC,CAAC;IAC1C,IAAI,CAAC;QACH,OAAO,MAAM,SAAS,CAAC,MAAM,CAAC,CAAC;IACjC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,UAAU,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,CAAC,CAAC;IACpD,CAAC;YAAS,CAAC;QACT,MAAM,CAAC,OAAO,EAAE,CAAC;IACnB,CAAC;AACH,CAAC;AAED,2FAA2F;AAC3F,KAAK,UAAU,SAAS,CAAC,MAAc;IACrC,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,IAAI,IAAI,GAAG,CAAC,CAAC;IACb,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;QACjC,IAAI,IAAK,KAAgB,CAAC,MAAM,CAAC;QACjC,IAAI,IAAI,GAAG,qBAAqB;YAAE,MAAM;QACxC,MAAM,CAAC,IAAI,CAAC,KAAe,CAAC,CAAC;IAC/B,CAAC;IACD,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC;AACxD,CAAC;AAED,uFAAuF;AACvF,SAAS,UAAU,CACjB,KAAc,EACd,MAAc,EACd,QAAqB,EACrB,MAAoB;IAEpB,IAAI,MAAM,EAAE,OAAO;QAAE,OAAO,IAAI,KAAK,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC;IAC3D,MAAM,MAAM,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS,CAAC,QAAQ,CAAC,CAAC,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;IAC3E,OAAO,IAAI,cAAc,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;AACpD,CAAC"}
@@ -0,0 +1,109 @@
1
+ /** What a free-form WHOIS reply says. */
2
+ export const WhoisVerdict = {
3
+ Registered: 'registered',
4
+ NotFound: 'not_found',
5
+ Reserved: 'reserved',
6
+ /** The server only says its query limit was hit. */
7
+ RateLimited: 'rate_limited',
8
+ /** No line in the reply gives an answer. */
9
+ Unknown: 'unknown',
10
+ };
11
+ /** Line flags: `^` matches at every line start, and case does not matter. */
12
+ const LINE_FLAGS = 'im';
13
+ /**
14
+ * A registry states its answer at the start of a line ("Domain not found.", "Status: free"),
15
+ * optionally after a comment marker ("%", "#", ">"). Matching only there keeps the long
16
+ * terms-of-use paragraphs, which talk about throttling and missing data, from deciding anything.
17
+ */
18
+ function atLineStart(body) {
19
+ return new RegExp(String.raw `^[\s%#>*]*(?:${body})`, LINE_FLAGS);
20
+ }
21
+ /** The registry holds the name back. */
22
+ const RESERVED = [
23
+ // "Status: reserved", "Domain status: blocked"
24
+ atLineStart(String.raw `(domain )?status\s*:\s*(reserved|blocked|restricted|prohibited)\b`),
25
+ // "This domain name is reserved", "Name is blocked"
26
+ atLineStart(String.raw `(this )?(domain|name)( name)? (is )?(reserved|blocked|restricted)\b`),
27
+ // "... reserved by the registry", anywhere in a line
28
+ /\breserved (by|for) the registry\b/i,
29
+ // "... is not available for registration", anywhere in a line
30
+ /\bnot available for registration\b/i,
31
+ ];
32
+ /** A creation date: the clearest sign of a registration, so it is checked before "not found" lines. */
33
+ const CREATED = [
34
+ // "Creation Date: 2001-02-03", "created: …", "Registered on: …", "Registration Time: …"
35
+ atLineStart(String.raw `(creation date|created( on)?|registered( on)?|registration (date|time)|domain registered|registered date)\s*:\s*\S`),
36
+ ];
37
+ /** The registry does not know the name. */
38
+ const NOT_FOUND = [
39
+ // Verisign "No match for", JPRS "No match!!"
40
+ atLineStart(String.raw `no match\b`),
41
+ // "NOT FOUND", Identity Digital "Domain not found."
42
+ atLineStart(String.raw `(domain )?not found\b`),
43
+ // "No data found", "No entries found", "No matching record"
44
+ atLineStart(String.raw `no (data|entries|object|matching records?)( found)?\b`),
45
+ // "No such domain"
46
+ atLineStart(String.raw `no such domain\b`),
47
+ // "The queried object does not exist"
48
+ atLineStart(String.raw `(the queried )?(object|domain) does not exist\b`),
49
+ // DENIC, EURid, .it: "Status: free", "Status: AVAILABLE"
50
+ atLineStart(String.raw `(domain )?status\s*:\s*(free|available)\b`),
51
+ // SIDN: "example.nl is free"
52
+ atLineStart(String.raw `\S+ is free\b`),
53
+ // Nominet: "This domain name has not been registered."
54
+ atLineStart(String.raw `this domain( name)? has not been registered\b`),
55
+ // SWITCH: "We do not have an entry in our database matching your query."
56
+ atLineStart(String.raw `we do not have an entry in our database\b`),
57
+ // "No information available about domain name … in the Registry"
58
+ atLineStart(String.raw `no information available about domain name\b`),
59
+ // "Domain name not known"
60
+ atLineStart(String.raw `domain name not known\b`),
61
+ ];
62
+ /** Other signs of a registration. */
63
+ const REGISTERED = [
64
+ // "Registrar: Example Inc."
65
+ atLineStart(String.raw `(registrar|sponsoring registrar|registrar name)\s*:\s*\S`),
66
+ // "Name Server: ns1.example.com", "nserver: …"
67
+ atLineStart(String.raw `(name ?servers?|nserver)\s*:\s*\S`),
68
+ // "Status: active", "Domain Status: clientTransferProhibited", DENIC "Status: connect"
69
+ atLineStart(String.raw `(domain )?status\s*:\s*(active|ok|registered|connect|client|server)`),
70
+ // "Registry Expiry Date: …", "Expires on: …", "paid-till: …"
71
+ atLineStart(String.raw `(registry expiry date|expir(y|ation) date|expires( on)?|paid-till)\s*:\s*\S`),
72
+ ];
73
+ /** Phrases saying a limit was actually hit. Only consulted when the reply holds no answer at all. */
74
+ const RATE_LIMITED = [
75
+ // "Query limit exceeded", "quota has been reached"
76
+ /\b(limit|quota) (has been |was )?(exceeded|reached)\b/i,
77
+ // "You have exceeded the maximum allowable number of queries"
78
+ /\bexceeded (the |your )?(maximum|allowed|allowable|query|rate)\b/i,
79
+ // "Too many queries"
80
+ /\btoo many (queries|requests|connections)\b/i,
81
+ // "Queries exceeded"
82
+ /\b(queries|requests|lookups) (have been |were )?exceeded\b/i,
83
+ // "Please try again later", "try again in 60 seconds"
84
+ /\btry again (later|in \d)/i,
85
+ // "Temporarily blocked", "temporarily denied"
86
+ /\btemporarily (blocked|denied|unavailable)\b/i,
87
+ ];
88
+ /**
89
+ * The rules in the order they are tried; the first match decides. A definite answer line wins;
90
+ * rate-limit wording counts only when there is none, because terms of use often describe
91
+ * throttling. A rate-limit message can never turn into "not found": it carries no answer line,
92
+ * and a wrong "available" is the worst possible result. (A list, not a Map: "registered" comes
93
+ * twice, once before and once after "not found".)
94
+ */
95
+ const RULES = [
96
+ { verdict: WhoisVerdict.Reserved, patterns: RESERVED },
97
+ { verdict: WhoisVerdict.Registered, patterns: CREATED },
98
+ { verdict: WhoisVerdict.NotFound, patterns: NOT_FOUND },
99
+ { verdict: WhoisVerdict.Registered, patterns: REGISTERED },
100
+ { verdict: WhoisVerdict.RateLimited, patterns: RATE_LIMITED },
101
+ ];
102
+ /** Reads a free-form WHOIS reply. */
103
+ export function interpretWhois(reply) {
104
+ if (!reply.trim())
105
+ return WhoisVerdict.Unknown;
106
+ const rule = RULES.find(({ patterns }) => patterns.some((pattern) => pattern.test(reply)));
107
+ return rule?.verdict ?? WhoisVerdict.Unknown;
108
+ }
109
+ //# sourceMappingURL=whois-reply.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"whois-reply.js","sourceRoot":"","sources":["../../src/checkers/whois-reply.ts"],"names":[],"mappings":"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"}