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
+13
View File
@@ -0,0 +1,13 @@
import { describe, expect, it } from "vitest";
import { whoisQuery } from "./client";
describe("blockPrivateAddresses", () => {
it("refuses to connect when the host resolves to loopback", async () => {
await expect(
whoisQuery("localhost", "example.com", { timeoutMs: 2000 }, { blockPrivateAddresses: true }),
).rejects.toMatchObject({
code: "connect_failed",
message: expect.stringContaining("non-public"),
});
});
});