domainfree: find the domains you can actually register - #8
Merged
Conversation
Ported from the bash version in profullstack/scripts to this repo's conventions, and exposed to moshcode as a `domain` plugin alongside `blog`. Availability is read from RDAP, never inferred from DNS, because DNS cannot tell registration apart from configuration: - a parked domain resolves fine and is taken - a domain registered with no nameservers returns NXDOMAIN, exactly like a name nobody owns Over 8,513 generated candidates the DNS shortcut (`dig NAME | grep "ANSWER: 0"`) called 20 registered domains free and missed none that were genuinely free. oubliette.com is the one to remember: registered 1996, paid through 2034, three nameservers, no A record, so dig reports ANSWER: 0 and it reads as available. Fine as a cheap prefilter, wrong as a buy signal. An indeterminate response — 429, 5xx, timeout — is retried once and then reported as ERR:<code>, never as available, and the exit status is 2. A name wrongly reported free is the only failure here that costs real time. Layout follows the repo: logic in src/domain-free.ts with an injectable fetcher, a thin bin/ entry guarded by isMain, args through the shared parseArgs, and vitest tests that touch no network. New plugin `domain` exposes /domain:free and /domain:lookup, the latter wrapping the existing domainjson so the plugin covers both directions — one verdict across thousands of names, or everything about one. One thing worth recording: the first version of these tests used real setTimeout delays, and the added wall-clock load made blog.test.ts's concurrent-createPost race fail — it passed alone and failed in the full suite. That test is timing-sensitive and this change happened to expose it. Rather than touch it, checkMany's retry pause is now injectable (retryDelayMs), the tests pass 0, and no test in this file uses a real timer. Full suite is green across four consecutive runs. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
ThreatCrush Security Scan3 finding(s) MEDIUM: 1 | LOW: 2
Snippets are redacted; ThreatCrush never prints matched credential material. |
Merged
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Adds
domainfree— find the domains you can actually register — and adomainplugin so it's available in moshcode.Ported from the bash version in
profullstack/scriptsto this repo's conventions.Why RDAP and not
digDNS cannot tell registration apart from configuration:
NXDOMAIN— exactly what a name nobody owns returnsOver 8,513 generated candidates,
dig NAME | grep "ANSWER: 0"reported 20 registered domains as free, and missed none that were genuinely free.oubliette.comis the one to remember: registered in 1996, paid through 2034, three nameservers, noArecord — sodigreturnsANSWER: 0and it reads as available.Fine as a cheap prefilter. Wrong as a buy signal.
Usage
Only available names go to stdout; the summary goes to stderr, so
domainfree -f in.txt | wc -lcounts what you can buy. ~8,500 names in ~45s at the default concurrency.An indeterminate response is never reported as available. A 429, 5xx or timeout is retried once, then reported as
ERR:<code>, and the exit status is2. A name wrongly reported free is the only failure mode here that costs real time.Plugin
New
domainplugin, registered in the marketplace next toblog:/domain:free— filter a list down to registerable names/domain:lookup— everything about one name, wrapping the existingdomainjsonLayout
Follows the repo: logic in
src/domain-free.tswith an injectable fetcher, a thinbin/entry guarded byisMain, args through the sharedparseArgs,.tsimport specifiers, vitest tests that touch no network.One thing worth flagging
My first version of these tests used real
setTimeoutdelays, and that added wall-clock load madeblog.test.ts's concurrent-createPostrace test fail — it passed alone but failed in the full suite. That test is timing-sensitive and this change happened to expose it.I didn't touch it. Instead
checkMany's retry pause is now injectable (retryDelayMs), the tests pass0, and no test in this file uses a real timer. Full suite is green across four consecutive runs (82/82).Worth knowing separately:
blog.test.tsasserts that two concurrent creates produce exactly one success and oneEEXIST. Under different scheduling both can succeed with different numbers, so it may resurface as other test files are added.Verified
pnpm test— 82/82, four consecutive runspnpm typecheck— clean--file,--all, non-.comrouting, case/whitespace normalisation, junk filtering,--help/bad-flag/invalid-value exit codesisMainguard confirmed: importingbin/domainfree.tsdoes not run the toolNote on installation
install-links.mjs --dry-runcurrently skips all seven commands, including this one, because~/.local/bin/*still points at~/scripts/bin. That's the repo's existing ported-but-not-switched-over state, not something this PR changes.--forcetakes them over when you want to switch.There is now a bash
domainfreeinprofullstack/scripts(PR #7) and this TypeScript one — same situation asdomainjson. Happy to close the scripts PR if you'd rather this be the only home.🤖 Generated with Claude Code