The connect-time SSRF pin returned only the first vetted address and
attached an undici@8 Agent to Node's built-in fetch. Two failures:
- A host whose first DNS record is an unreachable IPv6 (no IPv6 route on
the container / LAN / Tailscale) failed the connect instantly, because
the single-address pin discarded the working IPv4 record behind it and
Happy Eyeballs never ran.
- Node's global fetch is backed by its own pinned undici copy; handing it
an Agent built from the project's undici throws UND_ERR_INVALID_ARG on
a dispatch-handler interface mismatch.
Hand undici the full vetted survivor set and enable autoSelectFamily so
it runs RFC 8305 Happy Eyeballs across the already-vetted IPs (rebinding
stays closed: every dialable IP passed isPublicIp in the same lookup).
Drive the pinned path through undici's own fetch so the dispatcher and
the fetch engine share one undici version.
Add an end-to-end regression that drives a real localhost-bound server
through the pinned path (not a mocked fetch, which hid the coupling),
covering the IPv6-to-IPv4 fallback, streaming, timeout, and abort.
Closes issue 217. The isPublicUrl input-time guard catches every
RFC1918 / CGNAT / loopback / link-local alt-notation, but a low-TTL
DNS record can flip between accept and dispatch. The new
pinnedPublicDispatcher resolves the hostname literally inside
undici.Agent's connect hook, refuses any address isPublicIp would
reject, and pins the connection to the first valid public address.
The five outbound paths that accept a user-supplied host route
through the dispatcher via safeFetch's requirePublicHost flag —
moodlog sync, moodlog reverse-push, the moodlog credentials probe,
the local AI client (already gated in commit 1), and the ntfy
sender. The flag now buys both the input-time check and the
connect-time pin in one call.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes issue 218. Flips the convention from "remember to opt in to
redirect: manual" to "remember to opt out". Every outbound fetch that
doesn't pin a hard-coded host now routes through safeFetch and inherits
AbortSignal.timeout(15_000) so a tar-pit upstream can no longer pin a
worker indefinitely. The local AI client also gates on
requirePublicHost: true since its host is user-supplied; operators
running self-hosted Ollama / LM Studio on an RFC1918 address keep their
escape hatch via ALLOW_LOCAL_AI_PRIVATE_HOSTS=true (also enforced at
write-time in /api/user/ai-provider).
14 outbound call sites migrated. The four that already pinned manual
redirect (the historic templates) keep their semantics; the wrapper
preserves an explicit init.redirect. Adds undici as an explicit
dependency so the connect-time pin landing alongside this can rely on
the documented API rather than a transitive hop.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>