monovm-whois-ruby 1.0.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 (64) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +75 -0
  3. data/LICENSE +21 -0
  4. data/README.md +348 -0
  5. data/data/rdap_bootstrap.json +5337 -0
  6. data/data/whois_servers.json +1460 -0
  7. data/exe/monovm-whois +6 -0
  8. data/lib/monovm/whois/availability/analyzer.rb +91 -0
  9. data/lib/monovm/whois/availability/context.rb +137 -0
  10. data/lib/monovm/whois/availability/patterns.rb +415 -0
  11. data/lib/monovm/whois/availability/rule.rb +57 -0
  12. data/lib/monovm/whois/availability/rule_set.rb +137 -0
  13. data/lib/monovm/whois/availability/rules/availability_keywords.rb +32 -0
  14. data/lib/monovm/whois/availability/rules/explicit_unavailability.rb +43 -0
  15. data/lib/monovm/whois/availability/rules/no_match.rb +31 -0
  16. data/lib/monovm/whois/availability/rules/premium_name.rb +35 -0
  17. data/lib/monovm/whois/availability/rules/rdap_object.rb +94 -0
  18. data/lib/monovm/whois/availability/rules/recordless.rb +45 -0
  19. data/lib/monovm/whois/availability/rules/registration_fields.rb +37 -0
  20. data/lib/monovm/whois/availability/rules/registry_marker.rb +38 -0
  21. data/lib/monovm/whois/availability/rules/server_refusal.rb +46 -0
  22. data/lib/monovm/whois/availability/rules/status_field.rb +42 -0
  23. data/lib/monovm/whois/availability/rules/tld_specific.rb +38 -0
  24. data/lib/monovm/whois/availability/rules/wrong_registry.rb +48 -0
  25. data/lib/monovm/whois/availability/verdict.rb +100 -0
  26. data/lib/monovm/whois/checker.rb +165 -0
  27. data/lib/monovm/whois/cli.rb +250 -0
  28. data/lib/monovm/whois/client.rb +227 -0
  29. data/lib/monovm/whois/configuration.rb +160 -0
  30. data/lib/monovm/whois/domain_name.rb +168 -0
  31. data/lib/monovm/whois/endpoint.rb +131 -0
  32. data/lib/monovm/whois/errors.rb +63 -0
  33. data/lib/monovm/whois/parser/base.rb +126 -0
  34. data/lib/monovm/whois/parser/icann_rdd.rb +79 -0
  35. data/lib/monovm/whois/parser/key_value.rb +169 -0
  36. data/lib/monovm/whois/parser/rdap_json.rb +170 -0
  37. data/lib/monovm/whois/parser/record.rb +165 -0
  38. data/lib/monovm/whois/parser/selector.rb +74 -0
  39. data/lib/monovm/whois/paths.rb +31 -0
  40. data/lib/monovm/whois/punycode.rb +206 -0
  41. data/lib/monovm/whois/referral/follower.rb +90 -0
  42. data/lib/monovm/whois/registry/definition.rb +119 -0
  43. data/lib/monovm/whois/registry/resolution.rb +57 -0
  44. data/lib/monovm/whois/registry/server_registry.rb +164 -0
  45. data/lib/monovm/whois/registry/sources/base.rb +58 -0
  46. data/lib/monovm/whois/registry/sources/iana_bootstrap.rb +142 -0
  47. data/lib/monovm/whois/registry/sources/json_file.rb +137 -0
  48. data/lib/monovm/whois/response.rb +89 -0
  49. data/lib/monovm/whois/result.rb +114 -0
  50. data/lib/monovm/whois/transport/base.rb +51 -0
  51. data/lib/monovm/whois/transport/factory.rb +51 -0
  52. data/lib/monovm/whois/transport/middleware/base.rb +55 -0
  53. data/lib/monovm/whois/transport/middleware/cache.rb +92 -0
  54. data/lib/monovm/whois/transport/middleware/instrumentation.rb +63 -0
  55. data/lib/monovm/whois/transport/middleware/retry.rb +56 -0
  56. data/lib/monovm/whois/transport/middleware/throttle.rb +62 -0
  57. data/lib/monovm/whois/transport/rdap_http.rb +146 -0
  58. data/lib/monovm/whois/transport/whois_socket.rb +130 -0
  59. data/lib/monovm/whois/version.rb +7 -0
  60. data/lib/monovm/whois/whois_handler.rb +157 -0
  61. data/lib/monovm/whois.rb +142 -0
  62. data/lib/monovm-whois-ruby.rb +5 -0
  63. data/lib/monovm-whois.rb +5 -0
  64. metadata +114 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 5f8f480691e87bc25b8579f2392661362c8ad7a84d0931ca7c9140d032b4ca53
4
+ data.tar.gz: 7cf7b4eacb5a979835d74369c4b916f1222461c4fdfb0ac8b13bb0995b6f2d8c
5
+ SHA512:
6
+ metadata.gz: 91346d50c1234e0acb4a4c46dd1dec4486cf953560c43d3dd74e3ab7e097e5cd9f81df3673cc73dbd0e5aeab93877e72d5680edd23168c1d4db16c3538563754
7
+ data.tar.gz: b8543cd285fc1a074b6d56af85b9d01732f9b31f5614b69766e9965a0ee7443c8d4ab77f39a5b861cded6f70ea076b9561521af718642420e9eb92d1552bb340
data/CHANGELOG.md ADDED
@@ -0,0 +1,75 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here.
4
+
5
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this
6
+ project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [1.0.0] - 2026-08-05
9
+
10
+ First release.
11
+
12
+ ### Added
13
+
14
+ - **Four-state availability verdict.** `:available`, `:registered`, `:premium` and
15
+ `:unknown`, plus `:invalid` on `Result` for names that never reached a server.
16
+ `:unknown` is a real answer and is never promoted to `:available`.
17
+ - **RDAP support, preferred over WHOIS.** TLD → RDAP endpoint mapping from a bundled
18
+ snapshot of the IANA bootstrap registry (RFC 7484, ~1,200 TLDs). A registered domain
19
+ is read from `objectClassName`, an unregistered one from `errorCode` 404, so no
20
+ pattern matching is involved. Port 43 remains the fallback.
21
+ - **Availability detection as an extensible rule chain.** Twelve ordered rules, each an
22
+ object returning a verdict or deferring. `RuleSet` supports `prepend`, `append`,
23
+ `insert_before`, `insert_after`, `replace` and `remove`, so a registry's new wording
24
+ needs one small class rather than a fork.
25
+ - **Structured record parsing.** `Parser::Record` exposes registrar, registrant,
26
+ creation/update/expiry dates, nameservers, EPP statuses, DNSSEC and contacts, from
27
+ three parsers (RDAP JSON, ICANN RDD, generic key/value) selected per response. Raw
28
+ fields stay reachable. GDPR redaction placeholders are reported as `nil`.
29
+ - **Registrar referral following.** Thin registries are chased one hop to the
30
+ registrar's server for the fuller record. The referral enriches the record only; the
31
+ registry keeps authority over the verdict.
32
+ - **Concurrent bulk checks** with per-host throttling, so a large single-TLD batch does
33
+ not trigger a registry rate limit.
34
+ - **Transport middleware**: in-process response cache, per-host throttle, retry for
35
+ transient failures only, and an instrumentation hook.
36
+ - **Punycode / IDN support** via an RFC 3492 implementation with no dependencies,
37
+ verified against the RFC test vectors and the published IDN ccTLD ACE forms.
38
+ - **`explain`** on the client, the module and the handler, returning the deciding rule,
39
+ its evidence and the full trace of every rule consulted.
40
+ - **CLI** `monovm-whois`, with `--json`, `--details`, `--raw`, `--prefer`, `--timeout`,
41
+ `--concurrency`, `--tlds`, `--[no-]cache`, `--[no-]colour` and `--tld-count`. Exits
42
+ non-zero when any name came back inconclusive.
43
+ - **`WhoisHandler`**, a single-domain handler object, with camelCase method aliases
44
+ (`isAvailable`, `getWhoisMessage`, …) for code migrating from camelCase WHOIS APIs.
45
+ - **Definition overrides** through `MONOVM_WHOIS_DEFINITIONS`, so a stale registry
46
+ entry can be corrected without a gem release.
47
+ - Error taxonomy under `MonoVM::Whois::Error`: `InvalidDomainError`,
48
+ `UnsupportedTldError`, `DefinitionsError`, `ConnectionError`, `TimeoutError`,
49
+ `ServerRefusedError`, `EmptyResponseError`.
50
+
51
+ ### Fail-safe design decisions
52
+
53
+ Every choice points the same way — declining to guess where a permissive heuristic
54
+ would answer "available". See the table in the README for the full list. The main ones:
55
+
56
+ - Rate limits, blocked clients, retired port 43 endpoints, HTTP 4xx/5xx and empty
57
+ responses are `:unknown`, not `:available`.
58
+ - Reaching an address registry (RIPE, ARIN, APNIC) by a misconfigured TLD mapping is
59
+ `:unknown`, not `:available` for every name in that TLD.
60
+ - "Fewer than two registration fields" is no longer treated as availability. That
61
+ inference converts any non-answer into a free domain. It survives only as the
62
+ `recordless` rule, opt-in per TLD via `available_when_empty`.
63
+ - DENIC's `Status: invalid` is `:registered`, not `:available`.
64
+ - Premium and reserved names are `:premium`, not `:available`.
65
+ - Field matching tolerates dot-padded keys (`status.........: Registered`), which
66
+ several registries emit and a plain `status:` match misses.
67
+ - TLS certificates are verified by default.
68
+
69
+ ### Notes
70
+
71
+ - Requires Ruby 3.1 or newer. No runtime dependencies.
72
+ - The bundled RDAP bootstrap snapshot was taken 2026-07-23 and can be refreshed with
73
+ `rake data:refresh_rdap`.
74
+
75
+ [1.0.0]: https://github.com/monovm/whois-ruby/releases/tag/v1.0.0
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 MonoVM
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.
data/README.md ADDED
@@ -0,0 +1,348 @@
1
+ # monovm-whois-ruby
2
+
3
+ Domain WHOIS and RDAP lookups for Ruby, with availability detection that says
4
+ "I don't know" instead of guessing.
5
+
6
+ No runtime dependencies.
7
+
8
+ ```ruby
9
+ require "monovm-whois-ruby"
10
+
11
+ MonoVM::Whois.available?("monovm.com") # => false
12
+ MonoVM::Whois.lookup("monovm.com").record.expires_on
13
+ MonoVM::Whois.whois(%w[monovm google.com])
14
+ ```
15
+
16
+ ## Installation
17
+
18
+ ```ruby
19
+ gem "monovm-whois-ruby"
20
+ ```
21
+
22
+ or
23
+
24
+ ```sh
25
+ gem install monovm-whois-ruby
26
+ ```
27
+
28
+ Requires Ruby 3.1 or newer.
29
+
30
+ ## Why the availability status has four values
31
+
32
+ Most WHOIS libraries answer "is this domain available?" with a boolean. That collapses two very different situations —
33
+ *this domain is registered* and *I could not find out* — into the same `false`.
34
+
35
+ The expensive failure is the other direction. A WHOIS server that is rate-limiting
36
+ you, or that has retired port 43, or that answered with an HTML error page, sends back
37
+ text containing none of the words that mean "registered". A detector built from
38
+ heuristics falls through all of them and lands on its most permissive rule, which
39
+ reports the domain as free. For a registrar that means putting a registered domain in
40
+ a customer's shopping cart.
41
+
42
+ So a lookup here returns one of five statuses, and `available` means it was positively
43
+ established:
44
+
45
+ | Status | Meaning |
46
+ |---|---|
47
+ | `:available` | The registry said this name is not registered. |
48
+ | `:registered` | The name exists. |
49
+ | `:premium` | Unregistered, but reserved or premium-priced — not obtainable normally. |
50
+ | `:unknown` | No verdict. Rate limited, unreachable, unreadable. **Ask again later.** |
51
+ | `:invalid` | The input was not a usable domain, or its TLD has no known server. |
52
+
53
+ ```ruby
54
+ result = MonoVM::Whois.lookup("example.com")
55
+
56
+ result.status # => :registered
57
+ result.available? # => false
58
+ result.registered? # => true
59
+ result.unknown? # => false
60
+ result.conclusive? # => true
61
+ ```
62
+
63
+ `available?` is safe to branch on. Treating `!available?` as "registered" is not —
64
+ that is what `registered?` is for.
65
+
66
+ ## Usage
67
+
68
+ ### One domain
69
+
70
+ ```ruby
71
+ result = MonoVM::Whois.lookup("monovm.com")
72
+
73
+ result.name # => "monovm.com"
74
+ result.tld # => ".com"
75
+ result.sld # => "monovm"
76
+ result.status # => :registered
77
+ result.whois_message # the raw registry response, verbatim
78
+ result.record.registrar # => "Example Registrar, LLC"
79
+ ```
80
+
81
+ ### Many domains, and names without a TLD
82
+
83
+ ```ruby
84
+ MonoVM::Whois.whois("monovm.com")
85
+ # => {"monovm.com" => :registered}
86
+
87
+ MonoVM::Whois.whois(%w[monovm google.com])
88
+ # => {"monovm.com" => :registered,
89
+ # "monovm.net" => :registered,
90
+ # "monovm.org" => :available,
91
+ # "monovm.info" => :available,
92
+ # "google.com" => :registered}
93
+
94
+ MonoVM::Whois.whois("monovm", popular_tlds: %w[.io .dev])
95
+ # => {"monovm.io" => :registered, "monovm.dev" => :available}
96
+ ```
97
+
98
+ Bulk checks run concurrently (8 threads by default) and are throttled per host, so a
99
+ list of 500 `.com` names does not get your IP rate-limited by Verisign.
100
+
101
+ Duplicates, mixed case, URLs and trailing dots all collapse to one lookup:
102
+ `"EXAMPLE.COM"`, `"https://example.com/path"` and `"example.com."` are the same name.
103
+
104
+ ### The parsed record
105
+
106
+ ```ruby
107
+ record = MonoVM::Whois.lookup("example.com").record
108
+
109
+ record.registrar # => "Example Registrar, LLC"
110
+ record.created_on # => 1995-08-14 04:00:00 UTC
111
+ record.expires_on # => 2027-08-13 04:00:00 UTC
112
+ record.days_until_expiry # => 372
113
+ record.nameservers # => ["ns1.example.com", "ns2.example.com"]
114
+ record.statuses # => ["clientTransferProhibited"]
115
+ record.registrant # => nil when the registry redacts it
116
+ record.dnssec? # => false
117
+ record.transfer_prohibited? # => true
118
+ record.expiring? # => false
119
+ record.contacts[:admin] # => {name: ..., email: ...}
120
+ record["Registry Domain ID"] # any raw field, by its original key
121
+ ```
122
+
123
+ Post-GDPR placeholders (`REDACTED FOR PRIVACY`, `Data Protected`, …) are reported as
124
+ `nil` rather than as a registrant literally named "REDACTED FOR PRIVACY".
125
+
126
+ ### Why a verdict came out that way
127
+
128
+ ```ruby
129
+ MonoVM::Whois.explain("example.com")
130
+ # => {domain: "example.com",
131
+ # status: :registered,
132
+ # reason: "RDAP returned a domain object",
133
+ # trace: [{rule: "server_refusal", matched: false},
134
+ # {rule: "wrong_registry", matched: false},
135
+ # {rule: "rdap_object", matched: true, status: :registered,
136
+ # evidence: "example.com"}],
137
+ # endpoint: "https://rdap.verisign.com/com/v1/domain/"}
138
+ ```
139
+
140
+ Every rule that was consulted is in the trace, in order, with the one that decided and
141
+ the text it matched. This is the first thing to reach for when a classification looks
142
+ wrong.
143
+
144
+ ### Single-domain handler
145
+
146
+ `WhoisHandler` wraps one lookup in an object, with camelCase aliases for code
147
+ being migrated from camelCase WHOIS APIs:
148
+
149
+ ```ruby
150
+ handler = MonoVM::Whois::WhoisHandler.whois("monovm.com")
151
+
152
+ handler.available? # also handler.isAvailable
153
+ handler.valid? # also handler.isValid
154
+ handler.whois_message # also handler.getWhoisMessage
155
+ handler.tld # also handler.getTld
156
+ handler.availability_details # also handler.getAvailabilityDetails
157
+ ```
158
+
159
+ The four-state verdict described above applies here too: `handler.unknown?` is a
160
+ real answer, distinct from both `available?` and `registered?`.
161
+
162
+ ### Command line
163
+
164
+ ```sh
165
+ $ monovm-whois monovm.com google.com
166
+ monovm.com registered
167
+ google.com registered
168
+
169
+ $ monovm-whois monovm --tlds .io,.dev --details
170
+ $ monovm-whois example.com --json
171
+ $ monovm-whois example.com --prefer whois --timeout 5
172
+ $ monovm-whois --tld-count
173
+ 2043 TLDs supported
174
+ ```
175
+
176
+ Exit code is 0 when every name got a real answer and 1 when any came back `unknown` or
177
+ `invalid` — so a script can tell "definitely free" from "could not find out".
178
+
179
+ ### Configuration
180
+
181
+ ```ruby
182
+ MonoVM::Whois.configure do |config|
183
+ config.prefer = :whois # port 43 before RDAP (default: :rdap)
184
+ config.follow_referrals = true # chase thin registries to the registrar
185
+ config.concurrency = 8 # threads for bulk checks
186
+ config.throttle_interval = 0.5 # minimum seconds between queries to one host
187
+ config.cache_ttl = 300 # in-process response cache
188
+ config.retry_attempts = 2 # retries for timeouts, never for refusals
189
+ config.verify_ssl = true
190
+ config.socket_read_timeout = 15
191
+ config.popular_tlds = %w[.com .net .org .info]
192
+ config.instrumentation = ->(event) { Rails.logger.info(event) }
193
+ end
194
+ ```
195
+
196
+ ## Architecture
197
+
198
+ Five collaborators, each replaceable, wired together by `Client`:
199
+
200
+ ```
201
+ Client
202
+ ├── Registry::ServerRegistry which server serves this TLD, and where the name splits
203
+ ├── Transport::Factory how to talk to it (Strategy + Decorator)
204
+ ├── Availability::Analyzer what the answer means (Chain of Responsibility)
205
+ ├── Parser::Selector what the record says (Adapter)
206
+ └── Referral::Follower thin-registry second hop
207
+ ```
208
+
209
+ `Client` owns the sequence and none of the policy. Every collaborator arrives by
210
+ constructor injection, which is why the whole test suite runs offline against a
211
+ one-method fake transport.
212
+
213
+ ### Detection is a chain of rules
214
+
215
+ Each rule answers one question and either returns a `Verdict` or `nil` for "not mine,
216
+ ask the next one". Two invariants hold the order together:
217
+
218
+ 1. Rules that recognise a **non-answer** run first, so nothing reaching the permissive
219
+ rules could have been a refusal or a wrong-server reply.
220
+ 2. Every rule concluding `:registered` runs before every rule concluding `:available`,
221
+ so when signals conflict the safe one wins.
222
+
223
+ | # | Rule | Verdict |
224
+ |---|---|---|
225
+ | 1 | `server_refusal` — rate limit, blocked client, retired port 43, HTTP error | `:unknown` |
226
+ | 2 | `wrong_registry` — unsupported TLD, or an address registry's banner | `:unknown` |
227
+ | 3 | `rdap_object` — structured JSON: `objectClassName` vs `errorCode` 404 | `:registered` / `:available` |
228
+ | 4 | `premium_name` | `:premium` |
229
+ | 5 | `explicit_unavailability` — general and per-TLD "registered" wording | `:registered` |
230
+ | 6 | `registration_fields` — three or more record fields present | `:registered` |
231
+ | 7 | `registry_marker` — the TLD's configured not-found string | `:available` |
232
+ | 8 | `availability_keywords` — multilingual not-found phrases | `:available` |
233
+ | 9 | `no_match` — the whitespace-tolerant regexp forms | `:available` |
234
+ | 10 | `tld_specific` — per-TLD availability wording | `:available` |
235
+ | 11 | `status_field` — an explicit `status: available` | `:available` |
236
+ | 12 | `recordless` — no record and no refusal; opt-in per TLD | `:available` |
237
+ | — | nothing matched | `:unknown` |
238
+
239
+ Adding support for a registry that invents new wording means one small object:
240
+
241
+ ```ruby
242
+ class AcmeRule < MonoVM::Whois::Availability::Rule
243
+ def call(context)
244
+ return nil unless context.lower.include?("acme says this name is free")
245
+
246
+ available(reason: "ACME wording")
247
+ end
248
+ end
249
+
250
+ MonoVM::Whois.configure do |config|
251
+ config.rules.insert_before("registry_marker", AcmeRule.new)
252
+ end
253
+ ```
254
+
255
+ `RuleSet` also supports `prepend`, `append`, `insert_after`, `replace` and `remove`.
256
+
257
+ ### RDAP first
258
+
259
+ For any TLD with an RDAP endpoint the client queries RDAP before port 43. RDAP answers
260
+ in structured JSON — a registered domain is an object with an `objectClassName`, an
261
+ unregistered one is an error document with `errorCode` 404 — so availability is *read*
262
+ rather than inferred from prose. Port 43 is the fallback, used whenever a TLD has no
263
+ RDAP endpoint or its RDAP endpoint fails.
264
+
265
+ TLD coverage comes from two bundled data files:
266
+
267
+ - `data/whois_servers.json` — a curated port 43 server list, 872 TLDs, with the
268
+ registry's not-found marker per TLD.
269
+ - `data/rdap_bootstrap.json` — a snapshot of the IANA RDAP bootstrap registry
270
+ (RFC 7484), ~1,200 TLDs. Refresh with `rake data:refresh_rdap`.
271
+
272
+ Definitions merge, so a TLD can get its port 43 host from one file and its RDAP URL
273
+ from the other. A deployment can correct a stale entry without waiting for a release:
274
+
275
+ ```sh
276
+ export MONOVM_WHOIS_DEFINITIONS=/etc/monovm/whois-overrides.json
277
+ ```
278
+
279
+ ```json
280
+ [
281
+ {
282
+ "extensions": ".example,.test",
283
+ "uri": "socket://whois.example.test",
284
+ "available": "Domain not found",
285
+ "rdap": "https://rdap.example.test/domain/"
286
+ }
287
+ ]
288
+ ```
289
+
290
+ ### Referral following
291
+
292
+ Thin registries hold almost nothing. Query `.com` and Verisign returns a name, a
293
+ status, nameservers and a pointer: `Registrar WHOIS Server:`. The registrant and often
294
+ the accurate expiry date only exist on that second server, so the client follows the
295
+ pointer one hop.
296
+
297
+ A referral enriches the **record** and never the **verdict**. The registry is
298
+ authoritative about whether a name exists, and a registrar's server that is down must
299
+ not be able to turn a registered domain into an available one.
300
+
301
+ ### Punycode
302
+
303
+ Internationalised names are converted with an RFC 3492 implementation in
304
+ `MonoVM::Whois::Punycode`, verified against the RFC's own test vectors and the
305
+ published ACE forms of the IDN ccTLDs. It lives here rather than in a dependency to
306
+ keep the gem dependency-free.
307
+
308
+ Registries are queried in Punycode by default, because Verisign answers "No match" to
309
+ a UTF-8 query — which would read as availability. DENIC is the documented exception and
310
+ is sent the Unicode form over port 43 (`config.unicode_query_tlds`).
311
+
312
+ ## Fail-safe classifications
313
+
314
+ Situations that a permissive heuristic reads as "available", and what this gem
315
+ reports instead — every choice points the same way, refusing to guess:
316
+
317
+ | Situation | Permissive heuristic | This gem |
318
+ |---|---|---|
319
+ | Rate-limit notice | `available` | `:unknown` |
320
+ | Client blocked / port 43 retired | `available` | `:unknown` |
321
+ | HTTP 403/429/5xx from RDAP | `available` | `:unknown` |
322
+ | Empty response | `available` | `:unknown` |
323
+ | Address registry reached by mistake | `available` | `:unknown` |
324
+ | Fewer than 2 registration fields | `available` | `:unknown` unless the TLD opts in |
325
+ | DENIC `Status: invalid` | `available` | `:registered` |
326
+ | Premium/reserved name | `available` | `:premium` |
327
+ | Dot-padded keys (`status....: Registered`) | `available` | `:registered` |
328
+ | Registry restriction notice | `available` | `:registered` |
329
+
330
+ ## Development
331
+
332
+ ```sh
333
+ bundle install
334
+ bundle exec rspec # the offline suite
335
+ bundle exec rubocop
336
+ rake # both
337
+
338
+ rake spec_network # live registry lookups, opt-in
339
+ rake data:refresh_rdap # re-snapshot the IANA bootstrap
340
+ COVERAGE=1 bundle exec rspec
341
+ ```
342
+
343
+ The offline suite never touches the network: HTTP is blocked by WebMock, and the
344
+ socket transport is exercised against a loopback server the spec starts itself.
345
+
346
+ ## License
347
+
348
+ MIT. See [LICENSE](LICENSE).