@dakaio/domainscout-mcp 0.0.0-stage → 1.1.2

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 +261 -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 +318 -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 +78 -0
  94. package/dist/messages/report.js.map +1 -0
  95. package/dist/messages/tool.js +54 -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 +205 -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 +72 -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,262 @@
1
- # Temporary Holding Version
1
+ # DomainScout
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
+ **Domain research for AI agents.** Every good .com is taken. Find the ones that aren't.
4
+
5
+ [![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/install-mcp?name=domainscout&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsIkBkYWthaW8vZG9tYWluc2NvdXQtbWNwIl19)
6
+ [![Install in VS Code](https://img.shields.io/badge/VS_Code-Install_DomainScout-0098FF?logo=visualstudiocode&logoColor=white)](https://vscode.dev/redirect/mcp/install?name=domainscout&config=%7B%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22%40dakaio%2Fdomainscout-mcp%22%5D%7D)
7
+ [![Add to Claude Desktop](https://img.shields.io/badge/Claude_Desktop-Add_DomainScout-D97757?logo=claude&logoColor=white)](https://github.com/JohnDaka/domainscout-mcp/releases/latest/download/domainscout.mcpb)
8
+ [![npm](https://img.shields.io/npm/v/@dakaio/domainscout-mcp)](https://www.npmjs.com/package/@dakaio/domainscout-mcp)
9
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
10
+
11
+ ![DomainScout in an AI assistant: 500 brainstormed names checked in one call, the free .com domains with their prices](assets/domainscout-bulk.png)
12
+
13
+ DomainScout is an MCP server for bulk domain search. Your AI assistant brainstorms hundreds of
14
+ names, and DomainScout checks all of them in one call: which are free, what they cost and where
15
+ to buy them. It runs on your own machine. You need no account and no server of ours, and API keys
16
+ are optional.
17
+
18
+ [Website](https://domainscout.dakaio.com) ·
19
+ [npm](https://www.npmjs.com/package/@dakaio/domainscout-mcp) ·
20
+ [MCP Registry](https://registry.modelcontextprotocol.io/v0.1/servers?search=com.dakaio/domainscout) ·
21
+ [Examples](examples/README.md)
22
+
23
+ ## What it does
24
+
25
+ - **Finds the free names in a long list.** Hundreds of names in one call; 500 .com names take
26
+ about 20 seconds.
27
+ - **Checks for real.** DNS first, then the registry itself over RDAP, or WHOIS where a TLD has no
28
+ RDAP. A bought but unused domain is still reported as taken.
29
+ - **Compares prices at nine registrars:** Cloudflare, Porkbun, Namecheap, Spaceship, GoDaddy,
30
+ Name.com, Dynadot, NameSilo and Hover. The first year, the renewal and any minimum term, cheapest
31
+ to own first.
32
+ - **Spots the traps.** Premium and reserved names (with a registrar API key), and taken names that
33
+ are being deleted and may become available soon.
34
+ - **Any TLD.** `.com`, `.net` and `.ai` by default; ask for any other.
35
+
36
+ DomainScout does not invent names: your assistant does that, and DomainScout tells it which of
37
+ them are really free.
38
+
39
+ ## Install
40
+
41
+ Node 20 or newer.
42
+
43
+ **Claude Desktop:** one click: [download the extension](https://github.com/JohnDaka/domainscout-mcp/releases/latest/download/domainscout.mcpb)
44
+ and open it. Claude Desktop asks to install it, and lets you set the options below.
45
+
46
+ **Claude Code**
47
+
48
+ ```bash
49
+ claude mcp add domainscout -- npx -y @dakaio/domainscout-mcp
50
+ ```
51
+
52
+ **Cursor and VS Code:** use the buttons at the top, or add the server to the MCP config.
53
+
54
+ **Any MCP client:** add to the MCP config:
55
+
56
+ ```json
57
+ {
58
+ "mcpServers": {
59
+ "domainscout": {
60
+ "command": "npx",
61
+ "args": ["-y", "@dakaio/domainscout-mcp"],
62
+ "env": { "DOMAINSCOUT_TLDS": "com,net,ai,io" }
63
+ }
64
+ }
65
+ }
66
+ ```
67
+
68
+ On Windows, if `npx` is not found, use `"command": "cmd"` with
69
+ `"args": ["/c", "npx", "-y", "@dakaio/domainscout-mcp"]`.
70
+
71
+ Then just ask: *"Brainstorm 200 names for my coffee subscription app and tell me which .com
72
+ domains are free."*
73
+
74
+ **From source:** `npm install && npm run build`, then run `node /path/to/domainscout-mcp/dist/index.js`
75
+ in place of `npx -y @dakaio/domainscout-mcp`.
76
+
77
+ ## Tools
78
+
79
+ ### `check_domains`
80
+
81
+ Checks whether domain names are free to register, and shows prices and where to buy the free
82
+ ones. Read-only: it never registers or changes anything.
83
+
84
+ | Parameter | Type | Default | What it is |
85
+ |---|---|---|---|
86
+ | `domains` | string[] (1 to 1,000 entries) | required | Names or domains: `acme`, `acme.io`, `https://acme.dev`. A name without a TLD is tried in every TLD from `tlds` |
87
+ | `tlds` | string[] | `DOMAINSCOUT_TLDS` (`com,net,ai`) | TLDs for names given without one, e.g. `["com"]` for a long list |
88
+ | `confirm` | boolean | `true` | Confirm free-looking names with registrar APIs, when you set keys |
89
+ | `details` | boolean | `false` | Include the evidence from every source (DNS, RDAP, WHOIS, registrar API) |
90
+
91
+ It returns a short text report for the model and the same data as structured content: a summary
92
+ with counts per status, one result per domain (status, note, registration dates, buy links with
93
+ prices), skipped entries and warnings.
94
+
95
+ | Status | Meaning |
96
+ |---|---|
97
+ | `available` | A registrar API confirmed it can be registered; `premium` says whether it costs extra |
98
+ | `likely_available` | Not in the registry or DNS, not confirmed by a registrar |
99
+ | `taken` | Registered |
100
+ | `reserved` | Blocked by the registry, or the registrar cannot sell it |
101
+ | `unknown` | Could not verify right now (timeout, rate limit); try again |
102
+
103
+ ## Example
104
+
105
+ You ask your assistant *"I'm naming a note-taking app. Brainstorm 500 names and tell me which .com
106
+ domains are free."* The assistant calls `check_domains` once with all 500 names and gets back:
107
+
108
+ ```text
109
+ Checked 500 domain(s) in 23.0s: 27 available, 473 taken.
110
+
111
+ AVAILABLE (cheapest known offer for each):
112
+ - quillloom.com: $10.46/yr at Cloudflare https://www.cloudflare.com/domains/search/?q=quillloom.com
113
+ - scribeloom.com: $10.46/yr at Cloudflare https://www.cloudflare.com/domains/search/?q=scribeloom.com
114
+ - scribebloom.com: $10.46/yr at Cloudflare https://www.cloudflare.com/domains/search/?q=scribebloom.com
115
+ ...
116
+ TAKEN: quillwise.com, quillnest.com, quillflow.com, quillhub.com, quillpad.com, quillbox.com,
117
+ quillbase.com, quillkit.com, ...
118
+ ```
119
+
120
+ Ask about a few favourites afterwards and each one gets every registrar's link and price. More
121
+ real output, including the command line and JSON: [examples](examples/README.md).
122
+
123
+ ## How it checks
124
+
125
+ For each domain, in parallel and within polite rate limits:
126
+
127
+ 1. **DNS.** If the TLD zone delegates the name, it is taken. This step is free and saves the
128
+ registries' rate limits.
129
+ 2. **The registry over RDAP**, the authoritative source. If the domain is found, it is taken;
130
+ the result includes expiry, and a note when the domain is being deleted and may become
131
+ available soon. A 404 means it is likely available.
132
+ 3. **WHOIS**, only for TLDs without RDAP (for example `.io` and `.de`) or when RDAP fails.
133
+ 4. **Your registrar API, optional.** If you set an API key, the registrar confirms that each
134
+ free-looking name can really be bought, flags premium names and gives the exact price.
135
+
136
+ Having no DNS never proves a domain is free: a bought but unused domain has no DNS either.
137
+ Without a registrar API key, "not in the registry" is reported as `likely_available`. Premium
138
+ and registry-reserved names look the same there, so the registrar's page has the final price.
139
+
140
+ ## Authentication
141
+
142
+ None. DomainScout works without an account or API keys. Registrar API keys are optional: with
143
+ one, free-looking names are confirmed by the registrar, premium names are flagged and the exact
144
+ price is shown. One key is enough. With several, the APIs are asked in this order, each name only
145
+ until one answers.
146
+
147
+ | Registrar | Variables | Where to get it |
148
+ |---|---|---|
149
+ | Name.com | `DOMAINSCOUT_NAMECOM_USERNAME`, `DOMAINSCOUT_NAMECOM_TOKEN` | Account Settings → Security → API Tokens |
150
+ | Cloudflare | `DOMAINSCOUT_CLOUDFLARE_ACCOUNT_ID`, `DOMAINSCOUT_CLOUDFLARE_API_TOKEN` | API token with Registrar permission. The Registrar API is in beta |
151
+ | Spaceship | `DOMAINSCOUT_SPACESHIP_API_KEY`, `DOMAINSCOUT_SPACESHIP_API_SECRET` | API Manager; the key needs the `domains:read` scope |
152
+ | Porkbun | `DOMAINSCOUT_PORKBUN_API_KEY`, `DOMAINSCOUT_PORKBUN_SECRET_KEY` | porkbun.com/account/api |
153
+
154
+ Each key is sent only to its own registrar's API, and it never appears in output, logs or error
155
+ messages. If a registrar rejects a key, the result says so in its notes, and the check still
156
+ works without that registrar.
157
+
158
+ ## Settings
159
+
160
+ Set these as environment variables in the MCP config. All are optional.
161
+
162
+ | Variable | Default | What it does |
163
+ |---|---|---|
164
+ | `DOMAINSCOUT_TLDS` | `com,net,ai` | TLDs tried for names given without one |
165
+ | `DOMAINSCOUT_REGISTRARS` | all | Registrars to show buy links for, e.g. `cloudflare,porkbun,namecheap` |
166
+ | `DOMAINSCOUT_MAX_CONCURRENCY` | `10` | Network requests in flight at once |
167
+ | `DOMAINSCOUT_PER_HOST_CONCURRENCY` | `2` | Requests in flight to one RDAP server |
168
+ | `DOMAINSCOUT_TIMEOUT_MS` | `10000` | Timeout of one request |
169
+ | `DOMAINSCOUT_MAX_DOMAINS` | `500` | Domains per call, after adding TLDs |
170
+ | `DOMAINSCOUT_CONFIRM_MAX` | `50` | Free domains per call confirmed through your registrar API |
171
+ | `DOMAINSCOUT_DNS` | `on` | DNS pre-check |
172
+ | `DOMAINSCOUT_DNS_SERVERS` | system | DNS servers to use, comma-separated |
173
+ | `DOMAINSCOUT_PRICES` | `on` | Public price lists |
174
+ | `DOMAINSCOUT_AFFILIATE` | `on` | Affiliate buy links (`off` gives plain links) |
175
+
176
+ ## Rate limits
177
+
178
+ - **Per call:** up to 500 domains after adding TLDs (`DOMAINSCOUT_MAX_DOMAINS`, at most 10,000)
179
+ and 1,000 input entries.
180
+ - **Registries:** each RDAP server gets at most 2 requests in flight and 5 per second; each WHOIS
181
+ server one query at a time, at most one per second. A server that answers "too many requests"
182
+ is left alone for as long as it asks, and the request is retried.
183
+ - **Registrar APIs:** every API gets its documented limit (Name.com 10 requests per second,
184
+ Cloudflare 4, Spaceship 30 per 30 seconds, Porkbun 8 batches per minute), and at most
185
+ `DOMAINSCOUT_CONFIRM_MAX` names per call are confirmed. Porkbun throttles an account to 50 names
186
+ per 5 minutes for a day once it has checked about 1,800 names in 3 hours while registering few
187
+ of them; the limit keeps every call well below that. A tool call can pass `confirm: false` to
188
+ skip confirmation for a large list.
189
+
190
+ ## Pricing
191
+
192
+ DomainScout is free and open source under the MIT license. There is no account, subscription or
193
+ usage fee. The prices it shows are the registrars' own standard prices, from public price lists,
194
+ or the exact price from your registrar API.
195
+
196
+ ## Privacy
197
+
198
+ Your queries go straight from your machine to:
199
+
200
+ - your DNS resolver,
201
+ - the registries (RDAP/WHOIS),
202
+ - two public price lists: the Porkbun price API, and cfdomainpricing.com, a community mirror
203
+ of Cloudflare's prices,
204
+ - the APIs of registrars whose keys you set.
205
+
206
+ `DOMAINSCOUT_PRICES=off` turns the price lists off. Nothing goes to a server of this project,
207
+ because there is none.
208
+
209
+ ## Affiliate links
210
+
211
+ Some buy links may be affiliate links. When they are, the result says so, and buying through
212
+ them earns the project a commission at no extra cost to you. Links are always ordered by price
213
+ and never by commission. `DOMAINSCOUT_AFFILIATE=off` turns them off.
214
+
215
+ ## Security
216
+
217
+ The tool is read-only, runs on your machine, and sends each API key only to its own registrar.
218
+ To report a vulnerability, see [SECURITY.md](SECURITY.md).
219
+
220
+ ## Support
221
+
222
+ - Questions, bugs and ideas: [GitHub issues](https://github.com/JohnDaka/domainscout-mcp/issues)
223
+ - Website: [domainscout.dakaio.com](https://domainscout.dakaio.com)
224
+
225
+ ## Command line
226
+
227
+ ```bash
228
+ npx @dakaio/domainscout-mcp check acme getacme.io --tlds com,dev
229
+ npx @dakaio/domainscout-mcp check acme --json --no-confirm
230
+ npx @dakaio/domainscout-mcp check $(cat names.txt) --tlds com
231
+ ```
232
+
233
+ ## Development
234
+
235
+ ```bash
236
+ npm run verify # typecheck + lint (Biome) + tests with coverage thresholds
237
+ npm run build
238
+ ```
239
+
240
+ | Path | What it is |
241
+ |---|---|
242
+ | `src/core/` | The checking funnel, input parsing, shared types, network limits and retries |
243
+ | `src/checkers/` | DNS, RDAP and WHOIS lookups |
244
+ | `src/confirm/` | Confirmation through registrar APIs, one class per registrar |
245
+ | `src/pricing/`, `src/registrars/` | Public price lists, buy links and affiliate templates |
246
+ | `src/report/`, `src/messages/` | The text report and every text shown to people or to the model |
247
+ | `src/scout/`, `src/server/`, `src/cli/` | The check as a whole, the MCP server and the command line |
248
+ | `test/` | Vitest tests; the network is faked, except for a local WHOIS server |
249
+ | `examples/` | Real output of the tool |
250
+ | `site/` | The landing page at domainscout.dakaio.com: static HTML, released by a `landing-v<version>` tag (`npm --prefix site run release`) |
251
+ | `server.json`, `manifest.json`, `glama.json` | Listings: the MCP Registry, the Claude Desktop extension, Glama |
252
+
253
+ Releases: raise the version in a pull request with `npm run bump patch` (or `minor`, or an exact
254
+ version such as `npm run bump 1.2.0`); it updates `package.json`, `package-lock.json`,
255
+ `manifest.json` and `server.json` together. Then run `npm run release` on the merged `main`. It pushes the tag `v<version>`, and
256
+ `.github/workflows/publish.yml` publishes to npm (trusted publishing), creates a GitHub Release
257
+ with the Claude Desktop extension and publishes to the MCP Registry. The landing page goes out the
258
+ same way: `npm run bump:site patch` in a pull request, then `npm --prefix site run release`.
259
+
260
+ ## License
261
+
262
+ MIT © [dakaio.com](https://dakaio.com)
@@ -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":"AAAA,OAAO,EACL,cAAc,EACd,eAAe,EACf,qBAAqB,EACrB,kBAAkB,EAClB,KAAK,GACN,MAAM,iBAAiB,CAAC;AACzB,OAAO,EAAE,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAE3C,OAAO,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC;AACxD,OAAO,EAAE,SAAS,EAAE,MAAM,kBAAkB,CAAC;AAU7C,6EAA6E;AAC7E,MAAM,cAAc,GAAG,GAAG,CAAC;AAE3B,wFAAwF;AACxF,MAAM,OAAO,aAAa;IAIK,OAAO;IAHpC,yFAAyF;IACxE,IAAI,GAAG,IAAI,YAAY,CAAqB,qBAAqB,CAAC,CAAC;IAEpF,YAA6B,OAAgB;uBAAhB,OAAO;IAAY,CAAC;IAEjD,8GAA8G;IAC9G,KAAK,CAAC,SAAS,CAAC,GAAW;QACzB,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,kBAAkB,EAAE,GAAG,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC;QAC3E,MAAM,KAAK,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,OAAO,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC;QACvE,OAAO,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,OAAO,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAC9D,CAAC;IAED,iCAAiC;IACzB,QAAQ,CAAC,GAAW;QAC1B,MAAM,MAAM,GAAG,GAAG,CAAC,KAAK,CAAC,eAAe,CAAC,CAAC;QAC1C,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,CAAC,CAAC;IAClF,CAAC;IAEO,IAAI;QACV,OAAO,SAAS,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC,CAAC;IACpF,CAAC;IAEO,KAAK,CAAC,SAAS;QACrB,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,kBAAkB,CAAC,CAAC;QACxC,MAAM,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;QACjD,MAAM,QAAQ,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;QAC9B,MAAM,IAAI,GAAG,CAAC,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAkB,CAAC;QACtD,OAAO,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;IACjC,CAAC;IAEO,YAAY,CAAC,IAAmB;QACtC,MAAM,OAAO,GAAG,IAAI,GAAG,EAAkB,CAAC;QAC1C,KAAK,MAAM,CAAC,IAAI,EAAE,IAAI,CAAC,IAAI,IAAI,CAAC,QAAQ,IAAI,EAAE,EAAE,CAAC;YAC/C,MAAM,IAAI,GAAG,IAAI,CAAC,YAAY,CAAC,IAAI,CAAC,CAAC;YACrC,IAAI,CAAC,IAAI;gBAAE,SAAS;YACpB,KAAK,MAAM,GAAG,IAAI,IAAI;gBAAE,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,EAAE,IAAI,CAAC,CAAC;QAC/D,CAAC;QACD,OAAO,OAAO,CAAC;IACjB,CAAC;IAED,mEAAmE;IAC3D,YAAY,CAAC,IAAuB;QAC1C,MAAM,GAAG,GAAG,IAAI,CAAC,IAAI,CAAC,CAAC,SAAS,EAAE,EAAE,CAAC,IAAI,GAAG,CAAC,SAAS,CAAC,CAAC,QAAQ,KAAK,cAAc,CAAC,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC;QAChG,IAAI,CAAC,GAAG;YAAE,OAAO,SAAS,CAAC;QAC3B,OAAO,GAAG,CAAC,QAAQ,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,GAAG,GAAG,cAAc,EAAE,CAAC;IACxE,CAAC;CACF"}
@@ -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"}