feat: surface retryAfterMs, add blocked error code, and harden private-address guard

- Parse `Retry-After` headers (delay-seconds or HTTP-date) into `retryAfterMs` via a new `parseRetryAfterMs` helper and expose the value on both `LookupAttempt` and `LookupResult` so callers can honour server-requested back-off without parsing the error string
- Propagate `retryAfterMs` through `classifyError`, `traced`, `failure`, and the `rdap_unavailable` path so the value surfaces on the top-level result
- Add `blocked` to `LookupErrorCode` for WHOIS servers that permanently refuse a client (distinct from `rate_limited`, which is a temporary throttle that may succeed on retry)
- Add `blockPrivateAddresses` to `WhoisTransportOptions`; when set, a custom `dns.lookup` shim rejects the connection before it opens if any resolved address is non-public, covering DNS rebinding and hostnames that resolve to private ranges
- Expand `isPrivateIp` / `isSafeWhoisReferralHost` to reject IPv4-mapped (`::ffff:7f00:1`), NAT64 (`64:ff9b::`), 6to4 (`2002:7f00::`), Teredo (`2001:0:`), deprecated site-local (`fec0::`), and bracketed IPv6 literals
This commit is contained in:
2026-09-19 12:25:30 -04:00
parent 208fd6c69f
commit 35959b208b
14 changed files with 269 additions and 112 deletions
+1 -12
View File
@@ -17,20 +17,9 @@ vi.mock("./client.js", () => ({
}),
}));
import { collectWhoisReferralChain, followWhoisReferrals } from "./referral";
import { collectWhoisReferralChain } from "./referral";
describe("WHOIS referral contradiction handling", () => {
it("keeps TLD WHOIS when registrar claims availability", async () => {
const res = await followWhoisReferrals("whois.nic.io", "raindrop.io", {
followWhoisReferral: true,
maxWhoisReferralHops: 2,
});
expect(res.serverQueried).toBe("whois.nic.io");
// ensure we didn't adopt the registrar response
expect(res.text.toLowerCase().includes("creation date")).toBe(true);
expect(res.text.toLowerCase().includes("no match")).toBe(false);
});
it("collects chain and does not append contradictory registrar", async () => {
const { results: chain } = await collectWhoisReferralChain("whois.nic.io", "raindrop.io", {
followWhoisReferral: true,