Skip to content

porkbun: add check and register - #29

Merged
ralyodio merged 1 commit into
masterfrom
worktree-porkbun-register
Aug 29, 2026
Merged

porkbun: add check and register#29
ralyodio merged 1 commit into
masterfrom
worktree-porkbun-register

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Buying a domain was the one thing still worth opening the Porkbun dashboard for. Their API can do it now, so the CLI can too.

porkbun check diskpush.com                      # available? at what price?
porkbun register diskpush.com --max-price 20    # buy it, after confirming

How it avoids spending money by accident

register resolves a single plan — the TLD's rules, then availability and price — prints it, and asks. The plan exists so the number in the prompt and the number sent to the registrar are the same number by construction rather than by two lookups agreeing. It also pins the availability check to exactly one call, which matters: Porkbun rate limits it to one per ten seconds, so re-checking to "confirm" just before buying earns an error instead of a second answer.

Guards on top of that:

  • --max-price refuses anything dearer. A premium name is priced in the hundreds and reads the same as a normal one at a glance.
  • A promotional first year is called out, because the price agreed to is then not the price paid next year.
  • WHOIS privacy is on by default; a TLD that cannot do privacy is refused rather than quietly publishing a home address.
  • A TLD that is dashboard-only, or that needs registry eligibility fields (.us nexus, .ca legal type), is refused up front instead of after a charge.
  • --dry-run prices it and stops.

The money bug that isn't there

priceCents is its own function with its own tests because the obvious conversion is wrong. Prices are quoted as dollar strings, /domain/create wants integer cents matching the quote exactly, and parseFloat('11.08') * 100 is 1107.9999999999998 — truncating to 1107, which buys nothing and fails as a price mismatch. The decimal is split as text and never becomes a float.

Testing

  • 24 new unit tests; full suite 499 passing, typecheck clean.
  • Exercised live against the real API: --dry-run priced diskpush.com at $11.08/yr correctly.
  • The real purchase was correctly refused by Porkbun with "Your account phone number and email address must be verified" — surfaced as a clean CLI error, exit 1, nothing charged and no domain created.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QLFd9jDQ4D5UpHCWFJi8YM

Buying a domain was the one thing still worth opening the dashboard for.
Porkbun's API can do it now, so the CLI can too.

`register` resolves a single plan — the TLD's rules, then availability and
price — prints it, and asks before spending. The plan exists so the number in
the prompt and the number sent to the registrar are the same number by
construction, and so the availability check happens exactly once: it is rate
limited to one call per ten seconds, and re-checking to "confirm" just before
buying earns an error rather than a second answer.

Two guards against a surprise charge. `--max-price` refuses anything dearer,
because a premium name is priced in the hundreds and reads the same at a glance.
And a promotional first year is called out, since the price agreed to is then not
the price paid next year. WHOIS privacy defaults on, and a TLD that cannot do
privacy is refused rather than quietly publishing a home address.

`priceCents` is its own function because the obvious conversion is wrong:
prices are quoted as dollar strings, `/domain/create` wants integer cents that
match the quote exactly, and `parseFloat('11.08') * 100` is 1107.9999999999998
— which truncates to 1107 and buys nothing. The decimal is split as text and
never becomes a float.

`check` is the read half, so a name can be priced without going near the
purchase path.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLFd9jDQ4D5UpHCWFJi8YM
Comment thread src/porkbun.ts
.trim()
.toLowerCase()
.replace(/\.$/, '');
if (!/^[a-z0-9-]+(\.[a-z0-9-]+)+$/.test(zone)) {
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

16 finding(s)

HIGH/CRITICAL: 5 | MEDIUM: 2 | LOW: 9

Severity Rule Location
HIGH sh-remote-script-execution root-ubuntu.sh:134
HIGH sh-remote-script-execution root-ubuntu.sh:2597
HIGH sh-remote-script-execution root-ubuntu.sh:2601
HIGH sh-remote-script-execution root-ubuntu.sh:2654
HIGH sh-remote-script-execution root-ubuntu.sh:3683
MEDIUM redos-nested-quantifier src/domain-free.ts:56
MEDIUM redos-nested-quantifier src/porkbun.ts:460
LOW secret-generic-credential src/credentials.ts:36
LOW insecure-temp-file test/blog.test.ts:73
LOW insecure-temp-file test/blog.test.ts:74
LOW insecure-temp-file test/credentials.test.ts:43
LOW insecure-temp-file test/credentials.test.ts:44
LOW secret-generic-api-key test/credentials.test.ts:208
LOW insecure-temp-file test/download.test.ts:99
LOW insecure-temp-file test/download.test.ts:100
LOW secret-generic-credential test/shorten.test.ts:36

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit 635a1a6 into master Aug 29, 2026
5 checks passed
ralyodio added a commit that referenced this pull request Aug 29, 2026
The free-names branch was cut when master was 0.12.0 and bumped to 0.13.0 in
its feature commit, the convention here. By the time it merged, `torrent` (#28)
and porkbun's `check`/`register` (#29) had taken master to 0.14.0, and the
squash replayed the older bump on top — so a release that added a command
published a lower version than the one before it.

0.15.0 rather than 0.14.1: free-names is a new command, and a new command is a
minor here.

Worth knowing for next time: a hand-maintained version in the feature commit
only works while one branch is in flight. A squash does not conflict on it when
the surrounding lines still match, so nothing catches the regression at merge
time — only reading the tag afterwards does.


Claude-Session: https://claude.ai/code/session_01XGe9mWC6FvUjT6p1HCVjba

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants