Skip to content

porkbun: use Porkbun's own pre-flight, and keep structured refusals - #32

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

porkbun: use Porkbun's own pre-flight, and keep structured refusals#32
ralyodio merged 1 commit into
masterfrom
porkbun-preflight

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

Follow-up to #29. Three things learned from actually trying to register a domain with it.

The refusal was hiding its own answer

Porkbun refused the purchase with one sentence — "Your account phone number and email address must be verified." That reads like a transient, so it got retried six times instead of acted on.

It isn't a transient. Porkbun sends a code, a next_action.hint, a url and a retryable verdict beside the prose, and unwrap was throwing all of it away:

{ "code": "VERIFICATION_REQUIRED",
  "next_action": { "hint": "Verify your account email and phone number, then retry.",
                   "retryable": false, "url": "https://porkbun.com/account" } }

unwrap now keeps them, and PorkbunError carries code and retryable. The same failure prints:

porkbun: /domain/create/diskpush.com: Your account phone number and email address must be verified.
  Verify your account email and phone number, then retry.
  https://porkbun.com/account
  retrying will not help

--dry-run was mine when Porkbun has a real one

/domain/create accepts dryRun: true, which runs every pre-flight — availability, price match, eligibility, funds, spend cap, account verification — charging and creating nothing, and without consuming the create rate-limit budget.

This is strictly better than checking locally, and not by a little: funds, the monthly API spend cap and verification status are account-level gates that no read endpoint reports at all. My local --dry-run passed happily on a purchase that could never have succeeded. register now runs the real pre-flight before prompting, reports the balance, and --dry-run stops there.

The purchase wasn't idempotent

A create that times out has very likely registered the domain anyway, and the obvious response — run it again — buys and bills a second year. The create now carries an Idempotency-Key, so a retry within 24h replays the first result. Reads don't send one.

Testing

  • 531 tests passing, 10 new here (structured refusals, the idempotency header including its absence on reads, and the preview's null-vs-false handling for an unset spend cap).
  • Verified live against the API: the error output above is real, from this branch.

Unrelated, but worth knowing

pnpm typecheck is already red on master — two exactOptionalPropertyTypes errors in src/free-names.ts:80 and :89, from #30/#31. That file is byte-identical here, so it is not from this branch and I have left it alone. Happy to fix it separately.

🤖 Generated with Claude Code

https://claude.ai/code/session_01QLFd9jDQ4D5UpHCWFJi8YM

Three things learned from trying to register a real domain with this.

The refusal came back as one sentence — "Your account phone number and email
address must be verified." — and that reads exactly like a transient, so it got
retried six times instead of acted on. Porkbun actually sends a `code`, a
`next_action.hint`, a `url` and a `retryable` verdict alongside it; `unwrap` was
throwing all of that away. It now keeps them, and the same failure prints the
fix, where to do it, and "retrying will not help".

`/domain/create` takes `dryRun: true`, which runs every pre-flight — availability,
price match, eligibility, funds, spend cap, account verification — while charging
and creating nothing, and without consuming the create rate-limit budget. That is
strictly better than checking locally, because funds, the monthly API spend cap
and verification status are account-level gates that no read endpoint reports at
all. `register` now runs it before asking, and `--dry-run` stops there.

The real create carries an `Idempotency-Key`. A create that times out has very
likely registered the domain anyway, and the obvious response — run it again —
buys and bills a second year; keyed, a retry within 24h replays the first result.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01QLFd9jDQ4D5UpHCWFJi8YM
@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:506
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 aafb504 into master Aug 29, 2026
5 checks passed
@ralyodio
ralyodio deleted the porkbun-preflight branch August 29, 2026 17:17
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.

1 participant