domainhack 0.2.0__py3-none-any.whl

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 (67) hide show
  1. domainhack/__init__.py +5 -0
  2. domainhack/__main__.py +5 -0
  3. domainhack/adapters/__init__.py +0 -0
  4. domainhack/adapters/_circuit.py +105 -0
  5. domainhack/adapters/_http.py +22 -0
  6. domainhack/adapters/_registration.py +128 -0
  7. domainhack/adapters/_stderr.py +16 -0
  8. domainhack/adapters/_text_sink.py +33 -0
  9. domainhack/adapters/_throttle.py +150 -0
  10. domainhack/adapters/brand_data.py +49 -0
  11. domainhack/adapters/brand_report_render.py +222 -0
  12. domainhack/adapters/cached_registrar.py +340 -0
  13. domainhack/adapters/composite_writer.py +27 -0
  14. domainhack/adapters/console_writer.py +77 -0
  15. domainhack/adapters/csv_writer.py +79 -0
  16. domainhack/adapters/dns_confirming_writer.py +72 -0
  17. domainhack/adapters/dns_resolver.py +120 -0
  18. domainhack/adapters/file_word_source.py +51 -0
  19. domainhack/adapters/iana_tlds.py +110 -0
  20. domainhack/adapters/json_writer.py +51 -0
  21. domainhack/adapters/pacing.py +46 -0
  22. domainhack/adapters/rdap_bootstrap.py +223 -0
  23. domainhack/adapters/rdap_registrar.py +328 -0
  24. domainhack/adapters/registrar_catalog.py +120 -0
  25. domainhack/adapters/registrar_router.py +72 -0
  26. domainhack/adapters/registry_sources.py +192 -0
  27. domainhack/adapters/tqdm_progress.py +101 -0
  28. domainhack/adapters/whois_registrar.py +386 -0
  29. domainhack/cli/__init__.py +0 -0
  30. domainhack/cli/app.py +800 -0
  31. domainhack/cli/name_cmd.py +222 -0
  32. domainhack/cli/tlds_cmd.py +96 -0
  33. domainhack/data/hack_words.json +20 -0
  34. domainhack/data/iana_tlds.txt +1438 -0
  35. domainhack/data/rdap_dns.json +1 -0
  36. domainhack/data/registry_sources.json +2367 -0
  37. domainhack/data/second_level.json +28 -0
  38. domainhack/data/tld_presets.json +7 -0
  39. domainhack/domain/__init__.py +0 -0
  40. domainhack/domain/entities.py +287 -0
  41. domainhack/domain/label_rules.py +96 -0
  42. domainhack/domain/parking.py +81 -0
  43. domainhack/ports/__init__.py +0 -0
  44. domainhack/ports/dns_lookup.py +11 -0
  45. domainhack/ports/known_tlds.py +21 -0
  46. domainhack/ports/progress.py +32 -0
  47. domainhack/ports/registrar.py +27 -0
  48. domainhack/ports/result_cache.py +19 -0
  49. domainhack/ports/result_writer.py +13 -0
  50. domainhack/ports/word_source.py +9 -0
  51. domainhack/py.typed +0 -0
  52. domainhack/usecases/__init__.py +0 -0
  53. domainhack/usecases/brand_candidates.py +195 -0
  54. domainhack/usecases/brand_report.py +271 -0
  55. domainhack/usecases/check_domains.py +287 -0
  56. domainhack/usecases/confirm_dns.py +64 -0
  57. domainhack/usecases/estimate_run.py +80 -0
  58. domainhack/usecases/filter_words.py +37 -0
  59. domainhack/usecases/generate_range.py +107 -0
  60. domainhack/usecases/lanes.py +193 -0
  61. domainhack/usecases/rank_candidates.py +106 -0
  62. domainhack-0.2.0.dist-info/METADATA +501 -0
  63. domainhack-0.2.0.dist-info/RECORD +67 -0
  64. domainhack-0.2.0.dist-info/WHEEL +5 -0
  65. domainhack-0.2.0.dist-info/entry_points.txt +2 -0
  66. domainhack-0.2.0.dist-info/licenses/LICENSE +21 -0
  67. domainhack-0.2.0.dist-info/top_level.txt +1 -0
@@ -0,0 +1,501 @@
1
+ Metadata-Version: 2.4
2
+ Name: domainhack
3
+ Version: 0.2.0
4
+ Summary: Find domain hacks hiding in real words
5
+ Author: Pablo Alaniz
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/PabloAlaniz/find-amazing-domains
8
+ Project-URL: Repository, https://github.com/PabloAlaniz/find-amazing-domains
9
+ Project-URL: Issues, https://github.com/PabloAlaniz/find-amazing-domains/issues
10
+ Project-URL: Changelog, https://github.com/PabloAlaniz/find-amazing-domains/blob/main/CHANGELOG.md
11
+ Keywords: domain,domain-hack,tld,rdap,whois,availability,cli
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: End Users/Desktop
16
+ Classifier: Operating System :: OS Independent
17
+ Classifier: Programming Language :: Python :: 3
18
+ Classifier: Programming Language :: Python :: 3 :: Only
19
+ Classifier: Programming Language :: Python :: 3.10
20
+ Classifier: Programming Language :: Python :: 3.11
21
+ Classifier: Programming Language :: Python :: 3.12
22
+ Classifier: Programming Language :: Python :: 3.13
23
+ Classifier: Programming Language :: Python :: 3.14
24
+ Classifier: Topic :: Internet :: Name Service (DNS)
25
+ Classifier: Topic :: Utilities
26
+ Classifier: Typing :: Typed
27
+ Requires-Python: >=3.10
28
+ Description-Content-Type: text/markdown
29
+ License-File: LICENSE
30
+ Requires-Dist: dnspython<3,>=2.6
31
+ Requires-Dist: httpx>=0.27
32
+ Requires-Dist: idna>=3.7
33
+ Requires-Dist: tqdm>=4.66
34
+ Provides-Extra: dev
35
+ Requires-Dist: pytest>=8.0; extra == "dev"
36
+ Requires-Dist: pytest-cov>=6.0; extra == "dev"
37
+ Requires-Dist: coverage>=7.10; extra == "dev"
38
+ Requires-Dist: ruff~=0.16.10; extra == "dev"
39
+ Requires-Dist: mypy~=2.4.0; extra == "dev"
40
+ Requires-Dist: pre-commit>=4.0; extra == "dev"
41
+ Requires-Dist: types-tqdm>=4.66; extra == "dev"
42
+ Provides-Extra: docs
43
+ Requires-Dist: mkdocs-material>=9.5; extra == "docs"
44
+ Dynamic: license-file
45
+
46
+ # domainhack
47
+
48
+ [![PyPI](https://img.shields.io/pypi/v/domainhack)](https://pypi.org/project/domainhack/)
49
+ [![Python](https://img.shields.io/pypi/pyversions/domainhack)](https://pypi.org/project/domainhack/)
50
+ [![CI](https://github.com/PabloAlaniz/find-amazing-domains/actions/workflows/lint-typecheck-test.yml/badge.svg)](https://github.com/PabloAlaniz/find-amazing-domains/actions/workflows/lint-typecheck-test.yml)
51
+ [![Coverage](https://codecov.io/gh/PabloAlaniz/find-amazing-domains/graph/badge.svg)](https://codecov.io/gh/PabloAlaniz/find-amazing-domains)
52
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
53
+
54
+ **Find domain hacks hiding in real words, and check them live against the registries.**
55
+
56
+ Some words have a secret: they end in a top-level domain. The Spanish word *plato* (plate) becomes `pla.to`. *Abasto* (supply) becomes `abas.to`. *Grato* (pleasant) becomes `gra.to`. domainhack finds those words, asks the registry whether each domain is really free, and tells you what to register.
57
+
58
+ ![domainhack demo: filter a word list for .to hacks, then find a domain for a name](docs/demo.gif)
59
+
60
+ ## Install
61
+
62
+ ```bash
63
+ pipx install domainhack # or: pip install domainhack
64
+ uvx domainhack name myname # run without installing
65
+ ```
66
+
67
+ Python 3.10 or newer. No API keys, no accounts, no captchas: domainhack talks to the registries directly.
68
+
69
+ ## Why domainhack
70
+
71
+ - **It asks the registry, not a reseller.** Every generic TLD is checked over RDAP through the IANA bootstrap, and 170 of the 248 country-code TLDs over RDAP or port-43 WHOIS, each verified live with a registered and a random name. Requests are paced per host, back off on 429/503 as RFC 7480 asks, and a circuit breaker skips a host that stops answering.
72
+ - **It knows when "available" lies.** A public-DNS cross-check flags names the registry calls free but that already have nameservers. Taken names come with their registration date, expiry date, registrar, the parking or aftermarket service behind them, and whether they are about to drop (redemption or pending delete).
73
+ - **One command for a name.** `domainhack name acme` checks the exact name under a set of TLDs, finds hacks that spell it (`acme` + studio -> `acmestud.io`), optionally tries brand variants, and ends with a ranked recommendation.
74
+ - **It handles the hard cases.** Second-level suffixes (`com.ar`, `co.uk`), IDNs (`ñandú.de`), per-TLD label rules, a SQLite cache with TTLs that match how fast each kind of result goes stale, and CSV, JSON Lines, markdown and JSON output.
75
+
76
+ ## How it compares
77
+
78
+ | | domainhack | [domaingrep](https://github.com/ysm-dev/domaingrep) | [tldx](https://github.com/brandonyoungdev/tldx) | [DomainHacks.info](https://domainhacks.info/) |
79
+ |---|---|---|---|---|
80
+ | Finds domain hacks (word split at a TLD) | yes | yes | no: prefix/suffix permutations | yes |
81
+ | Availability source | registry RDAP + WHOIS | DNS | RDAP | registry lookups (about 900 of 1466 TLDs) |
82
+ | Cross-checks "available" against DNS | yes | DNS is the source | not documented | not documented |
83
+ | Taken-name details (expiry, registrar, parking, dropping) | yes | not documented | `_for-sale` TXT record | not documented |
84
+ | Second-level suffixes (`com.ar`, `co.uk`) and IDN | yes | not documented | not documented | not documented |
85
+ | Country-code TLDs you can check | 170 of 248, each verified live | "every TLD", list not published | not published | about 900 TLDs |
86
+ | Runs as | CLI, Python 3.10+ | CLI, Rust | CLI, Go | website |
87
+ | License | MIT | MIT | Apache-2.0 | proprietary |
88
+
89
+ "Not documented" means the project's README or site did not say so in October 2026; corrections are welcome in an issue.
90
+
91
+ ## Find a domain for a name
92
+
93
+ You have a name and want a domain for it. One command checks the exact name under a set of TLDs, looks for domain hacks that spell it, optionally tries brand variants, and recommends what to register:
94
+
95
+ ```bash
96
+ domainhack name sumanda
97
+ domainhack name sumanda --tlds startup,latam --variants --format markdown --output sumanda.csv
98
+ ```
99
+
100
+ ```
101
+ Domains for 'sumanda'
102
+ TLDs: com, app, io, ai, co, dev, so, xyz
103
+
104
+ Exact name
105
+ sumanda.com taken since 2015-11-12, expires 2026-11-12, registrar Nikko Reg LLP, parked: domainrecover
106
+ sumanda.app available DNS: not delegated
107
+ sumanda.io available DNS: not delegated
108
+ ...
109
+
110
+ Domain hacks
111
+ no TLD is a suffix of 'sumanda'
112
+ sumanda.studio available sumanda studio: a creative or production studio -- DNS: not delegated
113
+ sumandastud.io available sumanda studio: a creative or production studio -- DNS: not delegated
114
+ sumandarad.io available sumanda radio: a radio show or podcast -- DNS: not delegated
115
+ sumandagam.es unsupported sumanda games: games -- no registrar supports .es (restricted: ...)
116
+ ...
117
+
118
+ Recommendation
119
+ 1. sumanda.ai exact name, strong TLD .ai
120
+ ...
121
+ - sumanda.com is taken (expires 2026-11-12); parked at domainrecover: may be purchasable from the owner
122
+ ```
123
+
124
+ What it checks:
125
+
126
+ - **Exact name** under every TLD of `--tlds`. It takes presets and TLDs, mixed: `--tlds startup,com.ar,la`. The presets are `startup` (the default: com, app, io, ai, co, dev, so, xyz), `latam` (com.ar, ar, com.mx, mx, com.co, co, com.br, cl, pe), `classic` (com, net, org) and `all-supported` (every TLD the tool can check). A TLD the tool cannot check (e.g. `.es`, whose registry only serves accredited users) is reported as unsupported, with the reason.
127
+ - **Domain hacks**: the name split at a TLD it ends with (`plato` -> `pla.to`), and the name plus a curated word ending in a TLD (`sumanda` + studio -> `sumandastud.io`, or `sumanda.studio` where the word is itself a TLD). The words and their meanings live in `src/domainhack/data/hack_words.json`. When the name ends in no TLD, the report says so.
128
+ - **Brand variants** (`--variants`): `get`/`use`/`try`/`hola`/`my` + name and name + `hq`/`app`/`labs`/`studio`. They are checked under `.com` and the first 3 other requested TLDs only, so there are at most 36 of them.
129
+
130
+ The report lists the exact names by TLD, the hacks, the available variants and the taken names. Taken names come with their details (registration and expiry dates, parking service, dropping status). It also names everything it could not verify (registry error or unreachable, no registrar for the TLD) and every DNS conflict (the registry says available, but DNS has nameservers). `--format` picks `text` (default), `markdown` (to share) or `json`. `--output FILE` also saves the raw results as CSV or JSON Lines, like `check`.
131
+
132
+ The recommendation ranks available names only:
133
+
134
+ - an exact name under `.com`;
135
+ - then an exact name under `.app`/`.io`/`.ai`/`.co`;
136
+ - then an exact name under another TLD;
137
+ - then a `.com` variant;
138
+ - then a hack that spells the name;
139
+ - then other variants;
140
+ - then hacks with an extra word.
141
+
142
+ Shorter names win within a tier. Taken, dropping, DNS-conflict and unverifiable names are never recommended.
143
+
144
+ `--confirm-dns` / `--no-confirm-dns` turns the DNS cross-check on or off (on by default when a resolver is available). The registry check uses the same options as `check`: `--delay`, `--parallel`, `--no-cache`, `--cache-*`, `--contact` and `--no-progress`. The exit status follows `check`: it is 1 when some check ended in an error, and the report is still printed.
145
+
146
+ ## Examples
147
+
148
+ | Word | Language | Domain | Meaning |
149
+ |------|----------|--------|---------|
150
+ | plato | Spanish | `pla.to` | plate / dish |
151
+ | grato | Spanish | `gra.to` | pleasant |
152
+ | abasto | Spanish | `abas.to` | supply |
153
+ | veto | English | `ve.to` | veto |
154
+ | gusto | English | `gus.to` | gusto |
155
+ | pinto | English | `pin.to` | pinto |
156
+
157
+ ## How It Works
158
+
159
+ The tool has **two modes** to generate candidate domains, and then checks their availability against the registrar.
160
+
161
+ ### Mode 1: Word list -- find domains that are real words
162
+
163
+ Feed a dictionary file. The tool finds words ending in a TLD suffix, splits them, and checks the resulting domain.
164
+
165
+ ```
166
+ plato --> ends in "to" --> pla.to --> AVAILABLE / TAKEN
167
+ grato --> ends in "to" --> gra.to --> AVAILABLE / TAKEN
168
+ hello --> no TLD match --> (skipped)
169
+ ```
170
+
171
+ This mode produces **memorable, meaningful domains** because every candidate is a real word.
172
+
173
+ ### Mode 2: Brute-force -- try all letter combinations
174
+
175
+ Generate all combinations from `a` to `zzzzzz` (max length 6) and check each one as a domain. No dictionary needed.
176
+
177
+ ```
178
+ a.to, b.to, ..., z.to, aa.to, ab.to, ..., zz.to, aaa.to, ...
179
+ ```
180
+
181
+ This mode is useful for finding **short, available domains** regardless of whether they form real words.
182
+
183
+ ## Usage
184
+
185
+ ### Filter only (no availability check)
186
+
187
+ List which words from a file would form domain hacks, without querying the registrar. Useful to preview candidates before a long check run.
188
+
189
+ ```bash
190
+ # Spanish words ending in "to", at least 5 letters
191
+ domainhack --tld to filter data/words_es.txt --min-length 5
192
+
193
+ # English words ending in "in"
194
+ domainhack --tld in filter data/words_en.txt --min-length 4
195
+ ```
196
+
197
+ ### Check availability -- word list mode
198
+
199
+ Filter a word list **and** check each domain against the registrar in one step.
200
+
201
+ ```bash
202
+ # Check Spanish .to domains with 1s delay between requests
203
+ domainhack --tld to check --file data/words_es.txt --delay 1.0
204
+
205
+ # Preview which domains would be checked (no HTTP requests)
206
+ domainhack --tld to check --file data/words_es.txt --dry-run
207
+
208
+ # Also show taken domains in the output
209
+ domainhack --tld to check --file data/words_es.txt --show-taken
210
+
211
+ # Show taken domains that are about to drop (redemption or pending delete)
212
+ domainhack --tld to check --file data/words_es.txt --show-dropping
213
+
214
+ # Only the 50 best candidates per TLD (shortest names first)
215
+ domainhack --tld to,it check --file data/words_es.txt --limit 50
216
+
217
+ # Check in word-list order instead
218
+ domainhack --tld to check --file data/words_es.txt --order input
219
+ ```
220
+
221
+ Taken names in their registry's redemption period or pending delete are "dropping": they may become free soon. They print as `TAKEN (dropping: pending delete, expires 2026-11-02): x.to` with `--show-taken` or `--show-dropping`, and the summary counts them (`12 taken (1 dropping)`). Statuses and expiry dates come from RDAP; for WHOIS TLDs they are read only when the reply uses EPP status codes and ISO dates (e.g. `.it`).
222
+
223
+ Other taken names print with what the registry says about them, so you can tell an active site from a name parked for sale:
224
+
225
+ ```
226
+ TAKEN: sumanda.com (since 2015-11-12, expires 2026-11-12, parked: domainrecover)
227
+ ```
228
+
229
+ Only the known parts are shown. `parked:` names the parking or aftermarket service the nameservers point to (`domainrecover`, `sedo`, `parkingcrew`, `bodis`, `afternic`, `dan.com`, `hugedomains`, `godaddy-parked`, `namebright`, ...; the curated list is in `domain/parking.py`). RDAP gives the registration date, registrar and nameservers; WHOIS gives the creation date and nameservers when the reply has `Creation Date:`/`Created:` in ISO form and `Name Server:`/`nserver:` lines.
230
+
231
+ Registries rightly limit how many automated lookups a client may make, so the best candidates are checked first. `--order` picks the order:
232
+
233
+ - `score` (default): shortest SLD first, then shortest full word, then alphabetical. The order is deterministic.
234
+ - `alpha`: alphabetical by domain name.
235
+ - `input`: the order of the word list.
236
+
237
+ `--limit N` checks at most N domains per TLD, taken in that order. `--dry-run` lists exactly what a run with the same options would check, and the progress bar total accounts for the limit.
238
+
239
+ ### Check availability -- brute-force mode
240
+
241
+ Generate all letter combinations up to a given length (max 6) and check each one.
242
+
243
+ ```bash
244
+ # Try all 1-2 letter .to domains (26 + 676 = 702 combinations)
245
+ domainhack --tld to check --range-max 2
246
+
247
+ # Try up to 3 letters, but stop at "ba"
248
+ domainhack --tld to check --range-max 3 --range-end "ba"
249
+
250
+ # Preview combinations without checking
251
+ domainhack --tld to check --range-max 2 --dry-run
252
+
253
+ # Only the first 500 combinations per TLD
254
+ domainhack --tld to,io check --range-max 3 --limit 500
255
+ ```
256
+
257
+ Combinations are generated shortest first, lazily (`--range-max 6` is over 300 million names), so range mode is never sorted and `--order` only accepts `input` there.
258
+
259
+ Before a brute-force run, domainhack prints the estimated number of queries and the minimum run time. The estimate accounts for TLDs that share a host and for each host's pacing:
260
+
261
+ ```
262
+ Estimated 36,556 queries to 2 hosts (rdap.tonicregistry.to, rdap.identitydigital.services): at least 5 h 4 min at the current pacing.
263
+ ```
264
+
265
+ Runs over 10,000 queries are refused with exit status 2 unless you pass `--yes`. Narrow the run with `--range-end` or `--limit` instead where you can: registry terms of use forbid bulk querying. `--dry-run` never queries anything, so it is exempt.
266
+
267
+ ### Multiple TLDs
268
+
269
+ Pass a comma-separated list to `--tld`. Each TLD is routed to a registrar that supports it; unsupported TLDs are skipped with a warning.
270
+
271
+ ```bash
272
+ # Words that form .to, .io or .it hacks (prints "word -> sld.tld")
273
+ domainhack --tld to,io,it filter data/words_en.txt
274
+
275
+ # Check all of them
276
+ domainhack --tld to,io,it check --file data/words_en.txt
277
+ ```
278
+
279
+ Second-level suffixes work too: `--tld com.ar,com.mx,co.uk`. A word matches by plain concatenation, ignoring the suffix's dots (`fotocomar` -> `foto.com.ar`). Each one is checked against the registry of its top-level domain (`com.ar` -> `rdap.nic.ar`, `com.mx` -> `whois.mx`), with the whole name in the query.
280
+
281
+ #### DNS confirmation
282
+
283
+ ```bash
284
+ domainhack --tld io,com.ar check --file words.txt --confirm-dns
285
+ ```
286
+
287
+ `--confirm-dns` looks up every available or taken name in public DNS (NS, then A/AAAA, 3 s timeout, system resolver) as results come in. DNS is a second opinion, never the verdict. If the registry says a name is available but it has NS records, it prints as
288
+
289
+ ```
290
+ AVAILABLE? x.io -- registry says free but DNS has NS records
291
+ ```
292
+
293
+ and the summary counts these names. Treat them as taken until a registrar confirms otherwise. NXDOMAIN means "not in DNS". It is not an error, because many registered names are not delegated. Timeouts and SERVFAIL are recorded as DNS errors and never stop the run.
294
+
295
+ #### Unreachable registries
296
+
297
+ When a registry does not answer (timeouts, rate limits, an open circuit breaker), the summary names it, e.g. `warning: registry for .com.ar did not respond (3 errors); re-run later`. A TLD with only some failed checks gets `registry for .io failed 2 of 10 checks`. Errors are never cached, so the re-run checks only those names again.
298
+
299
+ #### Parallel checks
300
+
301
+ Different registry hosts are checked in parallel, each with one request in flight at a time. TLDs served by the same host share it: `.io`, `.sh`, `.ac` and `.me` (Identity Digital) are checked one after another, while `.to` and `.it` go ahead independently. One slow or unresponsive host (e.g. `whois.nic.it` at one query per 4 s) no longer holds up the others.
302
+
303
+ ```bash
304
+ # Up to 8 hosts at once (default: 4)
305
+ domainhack --tld to,io,it,co,de check --file data/words_en.txt --parallel 8
306
+
307
+ # Strictly one domain at a time, in check order (the old behaviour)
308
+ domainhack --tld to,io,it check --file data/words_en.txt --parallel 1
309
+
310
+ # Parallel, but print and save results in check order
311
+ domainhack --tld to,io,it check --file data/words_en.txt --keep-order
312
+ ```
313
+
314
+ - **Output order is completion order.** With `--parallel` above 1, results print (and are saved) as they come back, so domains from a fast host come out ahead of a slow one. Within one host they keep the check order. `--keep-order` holds results back to restore the check order (a slow host then delays the output, not the checks). `--parallel 1` checks in order, so output is in order too.
315
+ - **What gets checked does not change.** `--order` and `--limit` pick the domains before they are dispatched, and each host gets its share in that order.
316
+ - **Cache hits skip the queue.** Cached results are answered before dispatch and never wait for a host.
317
+ - **Memory stays bounded in brute-force mode.** Candidates are generated lazily. Each host has a queue of at most 16 waiting domains; when it is full, generation pauses until that host catches up. At most `hosts x 16` candidates (plus their results) are in memory, however large `--range-max` is.
318
+ - **Ctrl-C** stops dispatching at once, gives checks in flight up to a second to finish (their results are kept), and exits with status 130 as before.
319
+
320
+ ### Saving results
321
+
322
+ ```bash
323
+ # CSV (format inferred from the extension)
324
+ domainhack --tld to check --file data/words_es.txt --output results.csv
325
+
326
+ # JSON Lines
327
+ domainhack --tld to check --file data/words_es.txt --output results.jsonl
328
+ ```
329
+
330
+ Rows are written as they are checked, so an interrupted run (Ctrl-C) keeps everything checked so far. Columns: `fqdn` (ASCII, what was queried), `display` (as written, e.g. `ñandú.de`), `word`, `sld`, `tld`, `availability`, `error_message`, `statuses`, `expires_at`, `registered_at`, `registrar`, `nameservers`, `parked_hint`, `dns_nameservers`, `dns_conflict`. New columns are always appended, so existing positions stay stable.
331
+
332
+ - `statuses`: registry statuses of a taken name (RFC 9083 form, e.g. `client transfer prohibited`).
333
+ - `expires_at` / `registered_at`: expiration and registration dates in ISO 8601 UTC (`2026-11-30T07:38:29Z`).
334
+ - `registrar`: the sponsoring registrar's name (RDAP only).
335
+ - `nameservers`: NS hosts the registry lists, lowercase without the trailing dot.
336
+ - `parked_hint`: the parking/aftermarket service those nameservers belong to (e.g. `domainrecover`), empty if none is recognised.
337
+ - `dns_nameservers`: NS hosts public DNS returned, when DNS was consulted.
338
+ - `dns_conflict`: `true` when the registry said available but DNS shows a delegation (do not trust it as free), else `false`.
339
+
340
+ Lists are joined with `;` in CSV and are arrays in JSON; unknown values are empty in CSV and `null` (dates) or empty in JSON.
341
+
342
+ Only results go to stdout; warnings, errors and the final summary go to stderr, so stdout can be piped.
343
+
344
+ ### Name validation and international names
345
+
346
+ Candidates are validated before any query: letters, digits and hyphens only, 1-63 characters, plus per-TLD rules (e.g. `.it` requires at least 3 characters; `.com.ar` allows at most 50 and accepts `ñ` and accents, per NIC Argentina's rules). Names with accents or `ñ` are converted to their IDN form (`ñandú.de` -> `xn--and-6ma2c.de`) only for TLDs that accept IDNs; elsewhere they are skipped. Skipped candidates are counted on stderr.
347
+
348
+ Word lists are read as UTF-8; use `--encoding latin-1` for older lists.
349
+
350
+ ### Progress and caching
351
+
352
+ - A progress bar is shown on stderr when running in a terminal. Disable it with `--no-progress`.
353
+ - Results are cached in SQLite (`$XDG_CACHE_HOME/domainhack/results.sqlite3`), so re-runs skip domains already checked. How long a result is reused depends on what it says:
354
+
355
+ | Result | Reused for |
356
+ |--------|------------|
357
+ | Available | 24 hours (`--cache-ttl-available HOURS`): someone else can register it at any moment |
358
+ | Taken, dropping (redemption / pending delete) | 24 hours |
359
+ | Taken, expiration date known | until that date, at least 24 hours and at most 90 days |
360
+ | Taken, no expiration date | 30 days |
361
+ | Error | never cached |
362
+
363
+ `--cache-ttl HOURS` caps all of these (e.g. `--cache-ttl 1` rechecks anything older than an hour). Use `--no-cache` to skip the cache and `--cache-path PATH` to move it. Cache files from older versions are upgraded in place. The cache keeps registration details (statuses, dates, registrar, nameservers, parking hint) but never DNS evidence, which is looked up fresh.
364
+
365
+ ## Supported TLDs
366
+
367
+ Availability is checked against the registry directly, with no captchas or API keys. Ask the tool what it covers; it answers offline:
368
+
369
+ ```bash
370
+ domainhack tlds # summary + every country-code TLD
371
+ domainhack tlds cl es com.ar it # how these are checked
372
+ domainhack tlds --unsupported # what cannot be checked, and why
373
+ domainhack tlds --all --json # every TLD, machine-readable
374
+ ```
375
+
376
+ ```
377
+ 170 of 248 TLDs can be checked (102 rdap, 68 whois, 78 unsupported)
378
+ .cl whois whois.nic.cl 2026-10-07
379
+ .es unsupported restricted: the registry provides automated lookups to accredited users only; ...
380
+ .com.ar rdap rdap.nic.ar 2026-10-06
381
+ ```
382
+
383
+ Every generic TLD (com, app, dev...) is covered through the IANA RDAP bootstrap. Registries missing from it live in `data/registry_sources.json`: RDAP endpoints, WHOIS servers with their "not found" patterns and pacing, and the TLDs that cannot be checked with the reason why (no published lookup service, lookups for accredited users only, terms of use that do not cover automated lookups...), plus a link to check by hand where there is one. Each supported entry was verified live with a registered and a random name, and `tests/fixtures/whois/` keeps the real replies so a wrong pattern (which would mean false "available" results) fails the tests. A weekly CI job re-checks a rotating sample against the live registries.
384
+
385
+ A snapshot of the IANA RDAP bootstrap ships with the package, so the backend chosen for each TLD is the same online and offline.
386
+
387
+ The package also bundles IANA's list of every TLD (`data/iana_tlds.txt`, snapshot with its `# Version` line) and a curated list of common registrable second-level suffixes, Latin America first (`data/second_level.json`, checked against the [Public Suffix List](https://publicsuffix.org/)). `adapters/iana_tlds.IanaTldList` answers "is this a public suffix?" and "which suffixes does this name end with?" (`plato` -> `to`, `fotocomar` -> `com.ar`, `ar`). Both lists are snapshot-only, so results are deterministic offline; refresh them by replacing the files.
388
+
389
+ Note: "available" means *not registered*. Premium or reserved names may still show as available; confirm with a registrar before buying.
390
+
391
+ ### Being a good citizen
392
+
393
+ - Requests identify the tool: `User-Agent: domainhack/<version> (+repo URL)`. Add `--contact you@example.com` (or `DOMAINHACK_CONTACT`) to include a `From` header.
394
+ - Each registry host gets at most one request at a time from domainhack, however high `--parallel` is. Parallelism only goes across hosts (RFC 9112 §9.4 asks clients to limit simultaneous connections per server).
395
+ - Requests to each host are spaced (`--delay`, with stricter per-server minimums, e.g. 4 s for `whois.nic.it`). Each wait gets ±20% random jitter.
396
+ - The spacing adapts to each host (RFC 7480 §5.5). A 429, a 503, a timeout or WHOIS rate-limit text doubles that host's interval, up to 60 s. Each real answer shrinks it by 10%, back down to `--delay`.
397
+ - RDAP retries a 429, a 5xx, a timeout or a dropped connection at most twice. It waits for `Retry-After` when the server sends one (a 429 asking for more than 30 s is not retried, but the host is still held back, for up to 60 s). Otherwise it waits a "full jitter" exponential backoff, `random(0, min(30, 2 × 2^n))` seconds. WHOIS retries a timeout or a rate-limited reply only once.
398
+ - A check counts once, however many retries it takes: it slows its host down at most once and counts as at most one failure for the circuit breaker below.
399
+ - Checks go best-first, `--limit` caps them per TLD, and large brute-force runs need `--yes` (see above).
400
+ - If a host stops responding (3 consecutive failures), its domains are skipped for 60 s instead of waiting on timeouts; skipped checks are reported as errors and re-checked on the next run.
401
+
402
+ ### Exit codes
403
+
404
+ | Code | Meaning |
405
+ |------|---------|
406
+ | 0 | Completed, no errors |
407
+ | 1 | Completed with some failed checks, or a runtime error |
408
+ | 2 | Usage error |
409
+ | 130 | Interrupted (Ctrl-C) |
410
+
411
+ ## Word Lists
412
+
413
+ The sample files in `data/samples/` contain ~20 words each for quick testing. For serious domain hunting, you'll need full dictionaries:
414
+
415
+ - **Spanish**: Any comprehensive Spanish word list (600K+ words recommended)
416
+ - **English**: `/usr/share/dict/words` on macOS/Linux, or download from open word list repositories
417
+
418
+ Place them as `data/words_es.txt` and `data/words_en.txt` (these paths are gitignored).
419
+
420
+ ## Architecture
421
+
422
+ Built with **Clean Architecture** and **SOLID principles**:
423
+
424
+ ```
425
+ domain/ Pure entities: TLD, DomainHack, Availability
426
+ ports/ Abstract interfaces: WordSource, RegistrarClient, ResultWriter,
427
+ ResultCache, DnsLookup, KnownTlds
428
+ usecases/ Business logic: FilterWords, RangeCandidates, RankCandidates,
429
+ EstimateRun, CheckDomains (sequential, or parallel lanes
430
+ per registry host), ConfirmWithDns,
431
+ BrandCandidates + BrandReport (`name`)
432
+ adapters/ Implementations: FileWordSource, RDAP/WHOIS registrars,
433
+ RegistrarRouter, CachedRegistrar, Console/CSV/JSON writers, tqdm progress,
434
+ DnsPythonLookup (dnspython), IanaTldList
435
+ cli/ Composition root: argparse + dependency injection
436
+ ```
437
+
438
+ Adding support for a new TLD registrar or output format requires implementing a single interface -- zero changes to existing code.
439
+
440
+ ## Why .to?
441
+
442
+ `.to` is the country code top-level domain (ccTLD) for Tonga. It's popular for domain hacks because many Spanish words end in "-to" (a common suffix in verb conjugations and nouns). English has plenty too: veto, photo, gusto, motto.
443
+
444
+ The tool is TLD-agnostic: see [Supported TLDs](#supported-tlds), or add a new source by implementing the `RegistrarClient` interface and registering it in `adapters/registrar_catalog.py`.
445
+
446
+ ## Development
447
+
448
+ ```bash
449
+ pip install -e ".[dev]"
450
+ pre-commit install # enable git hooks
451
+ ```
452
+
453
+ ```bash
454
+ pytest # unit tests (hermetic, no network)
455
+ pytest -m integration # live registry tests (network)
456
+ ruff check src tests # linting
457
+ ruff format --check src tests # formatting
458
+ mypy # strict type checking
459
+ pre-commit run --all-files # exactly what CI runs for lint + types
460
+ ```
461
+
462
+ The ruff and mypy revs in `.pre-commit-config.yaml` are the source of truth. The `dev` extra pins matching ranges, so update both together.
463
+
464
+ CI (GitHub Actions) does the following:
465
+
466
+ - runs pre-commit;
467
+ - runs the unit tests on Python 3.10 to 3.14, with a 95% coverage floor;
468
+ - builds the wheel and smoke-tests it in a fresh venv.
469
+
470
+ A separate weekly workflow runs the live `integration` tests against real registries. It is non-blocking and skips the `.it` case, because whois.nic.it throttles hard. See [CHANGELOG.md](CHANGELOG.md) for release notes.
471
+
472
+ ## Contributing
473
+
474
+ Bug reports, new TLDs and WHOIS pattern fixes are welcome. [CONTRIBUTING.md](CONTRIBUTING.md) has the setup and a step-by-step guide to adding a TLD; [SECURITY.md](SECURITY.md) says how to report a vulnerability privately.
475
+
476
+ ## Roadmap
477
+
478
+ | Feature | Status |
479
+ |---------|--------|
480
+ | CSV/JSON ResultWriter | Done |
481
+ | Multi-TLD support (`--tld to,in,io`) | Done |
482
+ | Progress bar (tqdm) | Done |
483
+ | RDAP + WHOIS registrar adapters | Done |
484
+ | Result caching (decorator pattern) | Done |
485
+ | Best-first ordering (`--order`, `--limit`) | Done |
486
+ | Adaptive per-host backoff with jitter | Done |
487
+ | Parallel checks, one request per registry host (`--parallel`) | Done |
488
+ | DNS confirmation (`--confirm-dns`) | Done |
489
+ | Second-level suffixes (`com.ar`, `co.uk`) | Done |
490
+ | Registration details and parking detection for taken names | Done |
491
+ | `name` command: exact names, domain hacks, brand variants, recommendation | Done |
492
+ | `tlds` command and live-verified registry data (170 of 248 ccTLDs) | Done |
493
+ | Reference prices per TLD: a bundled, dated snapshot of public registrar price lists (registration and renewal), shown as a reference only; per-name premium pricing is out of scope | Planned |
494
+ | Re-verify registries that did not answer during verification (e.g. .pt, .uy, .dk, .do) and the .my RDAP service | Planned |
495
+ | Word-frequency scoring (`Scorer` hook) | Planned |
496
+ | Open zone files (.nu, .ch, .li) as an offline "registered" source | Planned |
497
+ | Async HTTP (`httpx.AsyncClient`) | Planned |
498
+
499
+ ## License
500
+
501
+ MIT
@@ -0,0 +1,67 @@
1
+ domainhack/__init__.py,sha256=Wz7SRMIVH-8jMdvzHNfhVjrbeX1hdmEDKkYlDh1Uvcs,231
2
+ domainhack/__main__.py,sha256=Y5xehKAWTDeD9xZ3PlpUequDvJirPUMpzohEaUMs8C8,95
3
+ domainhack/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ domainhack/adapters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ domainhack/adapters/_circuit.py,sha256=zLwuQldhvG3vPZALlyhU2knf5UWakjbXUG9BwlDhfb0,3852
6
+ domainhack/adapters/_http.py,sha256=7mP86htErkKnb8JlKZsMppzkeCbdTNBeBdg7fIxgL9E,834
7
+ domainhack/adapters/_registration.py,sha256=LYvaxvUcNka4TRFAO1BlEEE3GUH8NPpukQPFYgLC988,4604
8
+ domainhack/adapters/_stderr.py,sha256=nQBclA3DRwRFo1Uq1he4L7BtGY3EidKEKR9kQlo5urc,429
9
+ domainhack/adapters/_text_sink.py,sha256=cTJINq8Ssf9CvSMJMulajKo9s5aaJQjWfX2u9uNKgyM,985
10
+ domainhack/adapters/_throttle.py,sha256=Eefinjh-rC5NgT9DPvHisvtOsap_5LAl5GpNzvXPOvI,5909
11
+ domainhack/adapters/brand_data.py,sha256=DZNa0xgg-LPfXPf5vPrVZb0X55HoAjLgOEfT_J5nzG0,1654
12
+ domainhack/adapters/brand_report_render.py,sha256=NsPF6Hjr9gtWQNgVTDkEZKXTalxWfgNceBCew3qecrM,8629
13
+ domainhack/adapters/cached_registrar.py,sha256=iLJq5Yvjj85vGLZ6XUXy085qWLgu4ylsPBgGkZj4HWk,12661
14
+ domainhack/adapters/composite_writer.py,sha256=UweEL14m_RDn4cOG94Lobfalyg7Oiyp2EVKmgG6myZY,936
15
+ domainhack/adapters/console_writer.py,sha256=9ouCc3bMjD6UK7LqklsiH5m1mnAj_6NnvVzAbGvfrYw,3439
16
+ domainhack/adapters/csv_writer.py,sha256=oRNBDnMEZrMH9RzOF1Ir_fPzkyE1TrDaG9Um5SLqB7I,2850
17
+ domainhack/adapters/dns_confirming_writer.py,sha256=DU7ElXQq7NnHEtPpp_dEqyZBpbFGlNqjtTiJTvN4Rak,3031
18
+ domainhack/adapters/dns_resolver.py,sha256=COMhhfuSsDE97iZENAZQrthuMbypdjK2YVyvxRoqGUo,4391
19
+ domainhack/adapters/file_word_source.py,sha256=gtEHsk8hwnqZwI73JPuFUmw46W47pwVfd_QNQNlrLWw,1839
20
+ domainhack/adapters/iana_tlds.py,sha256=QXsorv8GHAqh1zBcQ59-C_jKi5eOb7t6l3YlnAPjLdI,4053
21
+ domainhack/adapters/json_writer.py,sha256=c-owoXpPWPGLuxhjOQc6vWh9gGsw01ydsnUCSkU2q0A,2272
22
+ domainhack/adapters/pacing.py,sha256=9PJlVp8M7kJqIK_60Lv456ap3KDt9BWiMti-sN3Bc3E,2052
23
+ domainhack/adapters/rdap_bootstrap.py,sha256=rDC3YuTcJWZX8w5b1EhkQW9SZ_dnmcIPBDt_LN6FcNc,8782
24
+ domainhack/adapters/rdap_registrar.py,sha256=p2AFeDeIemJTQdLQtvMRmuj1zx0727eifcBUjOmdjis,13209
25
+ domainhack/adapters/registrar_catalog.py,sha256=8_lHhqHODckLiL47h1dhxx4QdNJ_MsHUEAimOnyWKmk,4915
26
+ domainhack/adapters/registrar_router.py,sha256=sRBGBWipfwIvMLVE9QJBine3tBEUP2S6rrF4anEBBCI,2816
27
+ domainhack/adapters/registry_sources.py,sha256=1Nz7ZbK0CfO7AX4MAH7vNi7863WPQ2AoyFXBgLhES5c,7268
28
+ domainhack/adapters/tqdm_progress.py,sha256=Y-3rsuCwmqiGIWgPWtAkii9BYdg0rymlhCHFQSxZ2ME,3441
29
+ domainhack/adapters/whois_registrar.py,sha256=NDwoU3_qKD4dW4t5LByWDL-X45dw_YRdv4MeLHVIa1s,15816
30
+ domainhack/cli/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
31
+ domainhack/cli/app.py,sha256=VDgHr8lIFIc2ETjpx6Uuadf1I9EWU0kuJ4X5lSRJOxY,30128
32
+ domainhack/cli/name_cmd.py,sha256=JGoJD-PCz-xUBoJ-CUfhsFJIzmempPpW101v10nbR4U,8737
33
+ domainhack/cli/tlds_cmd.py,sha256=3tkFp_SaQiI0qodDoIT_8gmO694RSsgxy-rDN1WgK94,3698
34
+ domainhack/data/hack_words.json,sha256=i67n7HV67YTt7RiH6JMKK8VByJ2iRmnZdmJZloo5evY,791
35
+ domainhack/data/iana_tlds.txt,sha256=G6RUraMre5rmoSQPHRzogsFBBuxwWT0WeEigQUrhDr4,9528
36
+ domainhack/data/rdap_dns.json,sha256=Z2lFvp3FwNLf0fxTHt-3cXPvTUwg0x08FwXQKc8eoMM,32663
37
+ domainhack/data/registry_sources.json,sha256=bQenie0c87aEUMWigYVHPfAG-Aig4QhwIs3SHSoy8Bc,75439
38
+ domainhack/data/second_level.json,sha256=0VxTkZV3zetqiTOaMuNDB0VkvzcsPgWSiM7hzn5_RDs,1347
39
+ domainhack/data/tld_presets.json,sha256=Rz6ytrA_mCRCejL1KvJGUHFnM9Vi7-Jb1A0P-bzbrOo,562
40
+ domainhack/domain/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
41
+ domainhack/domain/entities.py,sha256=0MnlfEwj3Ix9UqpX0rwP2nmK9S0JSGNYKE-8DCe7D9Q,10795
42
+ domainhack/domain/label_rules.py,sha256=Cu0JM_qqxgNDUA-beGRG5zzd2uEhyhnCjY-HBHBE7qo,4830
43
+ domainhack/domain/parking.py,sha256=xdnalgA7UYp_Jf_CyJeGuSGGin0Cb3J-Bv1wuK1hLXE,3364
44
+ domainhack/ports/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
45
+ domainhack/ports/dns_lookup.py,sha256=4YtXWqYWEwkV6zUmEdVKUP30PgrLV2YuoV6h_4Ay1jM,349
46
+ domainhack/ports/known_tlds.py,sha256=ljNn_PsXPHbSv5-wRune7aAiMVMPRavleu2lm9iWMEM,713
47
+ domainhack/ports/progress.py,sha256=HDYqvZLJcLS_opEc69z7IiE4fqF7B3Sh55ki0TDAPd4,910
48
+ domainhack/ports/registrar.py,sha256=msd8ilmxvpnxOVN3Ccvtu_6bxxNkkfs0foC7fy6hd24,696
49
+ domainhack/ports/result_cache.py,sha256=BrYeQ30_opgw6FaI03Wsm24gEG_YacFykxoPrR6y4ZI,667
50
+ domainhack/ports/result_writer.py,sha256=GNc_8LdqD-UoxbNv18o0cOwah-gJo0t3RqfDBfR-ENA,356
51
+ domainhack/ports/word_source.py,sha256=u6v1Wy6rwK1c5iuosYNuVcK4yIG3Et16rshypJI3lV4,218
52
+ domainhack/usecases/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
53
+ domainhack/usecases/brand_candidates.py,sha256=1sKVP044diUFlpQ3DZT9VXDSfDcTNfD6o1ixzpE2dIs,7191
54
+ domainhack/usecases/brand_report.py,sha256=U1tzvat7GFdZOkL2xFvQvgHXf3L6DvF0LwBPlZW1n8Q,9962
55
+ domainhack/usecases/check_domains.py,sha256=CM6qxj6d22NNQ9DEW9PIDQPZGNmN2EApxfnC_iOcj6E,11277
56
+ domainhack/usecases/confirm_dns.py,sha256=4kQE8U1fjSPFVOs_eOzPQhiUOa1k5qr50yQ5lsLoSqk,2860
57
+ domainhack/usecases/estimate_run.py,sha256=ycVoSBw7MQfadlfVrXJkijGd3gCDZ4pyu6Qfgt9_OaE,2463
58
+ domainhack/usecases/filter_words.py,sha256=ZGrpo9Kyk3ly5QNENMRz7xCD5Bz-f7Lil3C-5TfOXSw,1477
59
+ domainhack/usecases/generate_range.py,sha256=QRYrK3wJUpcJHbxV6lBRwhV_INMvRODLrRt0WpmCr6w,4201
60
+ domainhack/usecases/lanes.py,sha256=SaCWGJJpV5pfST5KeO_TABXuwh25ybXPAiKi78ep-9E,7712
61
+ domainhack/usecases/rank_candidates.py,sha256=ccAJ7yHt16mtK8Fv-NHgtNjB2zJzxF4Hfi85XEYODAQ,4046
62
+ domainhack-0.2.0.dist-info/licenses/LICENSE,sha256=25xFe8kmZ3TZzs93SbzlDP_OHHkT5lsANRqQ_Ao2A0M,1069
63
+ domainhack-0.2.0.dist-info/METADATA,sha256=qSoxNq2AHHZJdQ2kZvhBwyu-pCXs4yFMP7qYsALEKTE,29827
64
+ domainhack-0.2.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
65
+ domainhack-0.2.0.dist-info/entry_points.txt,sha256=EJKgi9tS5I71wGB2HgdKkOqAi6ZmcLdBFqyThR7HAAI,55
66
+ domainhack-0.2.0.dist-info/top_level.txt,sha256=QGmlJHE7cQAh1gmF4sFWtsmr38EJ8Gc5iDsZqmiWOvU,11
67
+ domainhack-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.2.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ domainhack = domainhack.cli.app:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Pablo Alaniz
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.
@@ -0,0 +1 @@
1
+ domainhack