From ec4d1eff43a3ffde69ec4ab2009fdf7e01f09960 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Sat, 19 Sep 2026 00:41:22 -0300 Subject: [PATCH] fix(sse): pin DNS on the three public-only image fetch sites (#13883) (#14032) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Merged via /merge-batch (2026-09-19) on top of the current `release/v3.8.51` tip. **Reconciled before landing:** the train-3 ejection from 2026-09-18 (`tests/unit/image-generation-route.test.ts` → promptless topaz request came back 502 because `pinDns: true` bypassed the test's mocked `globalThis.fetch`) is fixed in `5fafc490` by registering the same `setPinnedFetchTestOverride()` seam the other four image tests already use, cleared in `resetStorage()`/`after`. Production keeps pinning for real. **Evidence on the merged tree:** `image-generation-route.test.ts` 25/25; all 18 image-path test files that mock `globalThis.fetch` (handler, upscale, fal, nanobanana, agnes, alibaba, bailian, kie, magnific, minimax, pollinations, qwen, edits-multipart, fetch-timeout, route-auth, pindns-toctou-13883) 183/183; `typecheck:core` clean; `check:open-sse-typecheck` 0 errors; file-size, changelog-integrity, complexity and cognitive-complexity gates OK. Closes #13883 --- changelog.d/fixes/13883-image-fetch-pindns.md | 1 + open-sse/handlers/imageGeneration.ts | 26 +++--- open-sse/handlers/imageUpscale/shared.ts | 18 ++--- src/shared/network/remoteImageFetch.ts | 11 +++ .../unit/fal-image-generation-default.test.ts | 9 ++- tests/unit/image-generation-handler.test.ts | 54 ++++++------- tests/unit/image-generation-route.test.ts | 10 ++- tests/unit/image-upscale.test.ts | 9 ++- tests/unit/nanobanana-image-handler.test.ts | 10 ++- tests/unit/pindns-toctou-13883.test.ts | 81 +++++++++++++++++++ 10 files changed, 173 insertions(+), 56 deletions(-) create mode 100644 changelog.d/fixes/13883-image-fetch-pindns.md create mode 100644 tests/unit/pindns-toctou-13883.test.ts diff --git a/changelog.d/fixes/13883-image-fetch-pindns.md b/changelog.d/fixes/13883-image-fetch-pindns.md new file mode 100644 index 0000000000..06f8c6d57e --- /dev/null +++ b/changelog.d/fixes/13883-image-fetch-pindns.md @@ -0,0 +1 @@ +- **security(images):** close a DNS-rebinding TOCTOU (#13883) at the three newer public-only image download sites — `resolveImageSource` and the NanoBanana result-URL conversion in `imageGeneration.ts`, and `resolveUpscaleImageSource` in `imageUpscale/shared.ts`. All three validated a caller-supplied URL's DNS answer as public but then let the download perform an independent, un-pinned second resolution at connect time, so a host that answered differently between the two lookups (public, then loopback/LAN) could reach an internal address; they now set `pinDns: true` (reusing the existing `createPinnedFetch` helper already used by embeddings and the vision/audio/video bridges), binding the connection to the exact validated address. diff --git a/open-sse/handlers/imageGeneration.ts b/open-sse/handlers/imageGeneration.ts index 7447fb125e..94c51576e9 100644 --- a/open-sse/handlers/imageGeneration.ts +++ b/open-sse/handlers/imageGeneration.ts @@ -2226,7 +2226,7 @@ function extractImageInputs(body) { }; } -async function resolveImageSource(source) { +export async function resolveImageSource(source) { if (typeof source !== "string" || source.trim().length === 0) { throw new Error("Invalid image source"); } @@ -2243,15 +2243,11 @@ async function resolveImageSource(source) { } if (isHttpUrl(trimmed)) { - // GHSA-34rg-3pqj-35g9: this URL is caller input (`image_url` / `mask_url` / message - // parts) — pin `public-only` explicitly (string check + DNS validation of every - // resolved answer). Never let it fall back to the operator outbound policy - // (`getProviderOutboundGuard()`), which is `block-metadata` on a local-first default - // install and would let a request body make the server fetch loopback/LAN URLs and - // forward the bytes upstream. `pinDns` stays off on purpose: this handler's only - // transport is `globalThis.fetch` (no `fetchImpl` seam) and connection pinning - // replaces it with a raw undici fetch — same shape as the AI Horde result download. - const remoteImage = await fetchRemoteImage(trimmed, { guard: "public-only" }); + // GHSA-34rg-3pqj-35g9 / #13883: caller-input URL — pin `public-only` (never the operator + // outbound policy, which would let a request body reach loopback/LAN) and `pinDns: true` + // to close the DNS-rebinding TOCTOU where a second, un-pinned resolution at connect time + // could answer differently than the validated lookup and bypass the guard. + const remoteImage = await fetchRemoteImage(trimmed, { guard: "public-only", pinDns: true }); return { buffer: remoteImage.buffer, base64: remoteImage.buffer.toString("base64"), @@ -3242,7 +3238,7 @@ function normalizeNanoBananaSyncPayload(data, prompt) { return { data: images.filter(Boolean) }; } -async function normalizeNanoBananaTaskResult(taskData, body, log) { +export async function normalizeNanoBananaTaskResult(taskData, body, log) { const response = taskData?.response || {}; const urlCandidates = [ @@ -3280,10 +3276,10 @@ async function normalizeNanoBananaTaskResult(taskData, body, log) { if (urlCandidates.length > 0) { const firstUrl = urlCandidates[0]; - // GHSA-34rg-3pqj-35g9: upstream-supplied result URL, not an OmniRoute-controlled - // host — pin `public-only` exactly like the AI Horde result download does, never - // the operator outbound policy (see `resolveImageSource` for why `pinDns` is off). - const remoteImage = await fetchRemoteImage(firstUrl, { guard: "public-only" }); + // GHSA-34rg-3pqj-35g9 / #13883: upstream-supplied result URL, not an OmniRoute- + // controlled host — pin `public-only`, never the operator outbound policy, and + // `pinDns: true` to close the DNS-rebinding TOCTOU (see `resolveImageSource`). + const remoteImage = await fetchRemoteImage(firstUrl, { guard: "public-only", pinDns: true }); const base64 = remoteImage.buffer.toString("base64"); return [{ b64_json: base64, revised_prompt: body.prompt }]; } diff --git a/open-sse/handlers/imageUpscale/shared.ts b/open-sse/handlers/imageUpscale/shared.ts index 557274b88b..7efba61065 100644 --- a/open-sse/handlers/imageUpscale/shared.ts +++ b/open-sse/handlers/imageUpscale/shared.ts @@ -163,15 +163,15 @@ export async function resolveUpscaleImageSource(source: string): Promise { }); const { handleImageGeneration } = await import("../../open-sse/handlers/imageGeneration.ts"); +const { setPinnedFetchTestOverride } = await import("../../src/shared/network/remoteImageFetch.ts"); test("handleImageGeneration returns Fal images as base64 when response_format is omitted", async () => { const originalFetch = globalThis.fetch; - globalThis.fetch = async (url) => { + const mockFetchImpl = async (url) => { const stringUrl = String(url); if (stringUrl === "https://fal.run/fal-ai/flux-2-flex") { return new Response( @@ -39,6 +40,11 @@ test("handleImageGeneration returns Fal images as base64 when response_format is } throw new Error(`Unexpected URL: ${stringUrl}`); }; + // #13883: resolveImageSource now sets `pinDns: true`, which pins the connection via a + // real undici socket and would bypass this mocked globalThis.fetch — route it through + // the test-only pinned-fetch override instead (src/shared/network/remoteImageFetch.ts). + globalThis.fetch = mockFetchImpl; + setPinnedFetchTestOverride(mockFetchImpl); try { const result = await handleImageGeneration({ @@ -51,5 +57,6 @@ test("handleImageGeneration returns Fal images as base64 when response_format is assert.equal(result.data.data[0].url, undefined); } finally { globalThis.fetch = originalFetch; + setPinnedFetchTestOverride(undefined); } }); diff --git a/tests/unit/image-generation-handler.test.ts b/tests/unit/image-generation-handler.test.ts index 4092f3e807..8749c91e90 100644 --- a/tests/unit/image-generation-handler.test.ts +++ b/tests/unit/image-generation-handler.test.ts @@ -7,16 +7,9 @@ import { join } from "node:path"; process.env.DATA_DIR = mkdtempSync(join(tmpdir(), "omniroute-images-")); -// Stub DNS for fetchRemoteImage's GHSA-cmhj-wh2f-9cgx DNS-rebinding guard -// (assertHostnameResolvesPublic in src/shared/network/remoteImageFetch.ts). -// Several image-handler tests (Fal AI URL->b64 normalization, BFL polling -// with base64 input images, NanoBanana polling with URL->b64 conversion) -// mock globalThis.fetch with example.com URLs that don't resolve in CI; the -// handler invokes fetchRemoteImage without exposing a `lookup` injection -// point, so we monkey-patch dns.promises.lookup to always return a public IP -// so the rebinding guard passes and the test exercises the mocked fetch -// behaviour as intended. Node --test runs each file in its own process, so -// this rebinding does not leak across files. +// Stub DNS for fetchRemoteImage's GHSA-cmhj-wh2f-9cgx guard so mocked example.com URLs +// resolve as public. #13883's `pinDns: true` pins the connection via undici, bypassing a +// mocked globalThis.fetch — `mockFetch()` also sets the `setPinnedFetchTestOverride()` seam. const originalDnsLookup = dns.promises.lookup; (dns.promises as { lookup: unknown }).lookup = (async ( _hostname: string, @@ -32,6 +25,11 @@ process.on("exit", () => { const { IMAGE_PROVIDERS, parseImageModel, getAllImageModels } = await import("../../open-sse/config/imageRegistry.ts"); const { handleImageGeneration } = await import("../../open-sse/handlers/imageGeneration.ts"); +const { setPinnedFetchTestOverride } = await import("../../src/shared/network/remoteImageFetch.ts"); +function mockFetch(impl) { + globalThis.fetch = impl; + setPinnedFetchTestOverride(impl); +} function immediateTimeout(callback, _ms, ...args) { if (typeof callback === "function") callback(...args); @@ -366,7 +364,7 @@ test("handleImageGeneration calls Fal AI with Key auth and normalizes URL result const originalFetch = globalThis.fetch; let requestCapture; - globalThis.fetch = async (url, options = {}) => { + mockFetch(async (url, options = {}) => { const stringUrl = String(url); if (stringUrl === "https://fal.run/fal-ai/flux-pro/v1.1-ultra") { requestCapture = { @@ -391,7 +389,7 @@ test("handleImageGeneration calls Fal AI with Key auth and normalizes URL result } throw new Error(`Unexpected URL: ${stringUrl}`); - }; + }); try { const result = await handleImageGeneration({ @@ -416,7 +414,7 @@ test("handleImageGeneration calls Fal AI with Key auth and normalizes URL result assert.equal(requestCapture.body.sync_mode, true); assert.equal(result.data.data[0].b64_json, "BQYH"); } finally { - globalThis.fetch = originalFetch; + mockFetch(originalFetch); } }); @@ -424,7 +422,7 @@ test("handleImageGeneration routes Stability AI edit models to native endpoints" const originalFetch = globalThis.fetch; let requestCapture; - globalThis.fetch = async (url, options = {}) => { + mockFetch(async (url, options = {}) => { const stringUrl = String(url); if (stringUrl === "https://example.com/stability-input.png") { return new Response(new Uint8Array([4, 5]), { @@ -447,7 +445,7 @@ test("handleImageGeneration routes Stability AI edit models to native endpoints" } throw new Error(`Unexpected URL: ${stringUrl}`); - }; + }); try { const result = await handleImageGeneration({ @@ -476,7 +474,7 @@ test("handleImageGeneration routes Stability AI edit models to native endpoints" assert.equal((requestCapture.body.get("mask") as Blob).size, 1); assert.equal(result.data.data[0].b64_json, "c3RhYmlsaXR5LWltYWdl"); } finally { - globalThis.fetch = originalFetch; + mockFetch(originalFetch); } }); @@ -537,7 +535,7 @@ test("handleImageGeneration polls Black Forest Labs results and sends base64 inp let pollCapture; globalThis.setTimeout = immediateTimeout; - globalThis.fetch = async (url, options = {}) => { + mockFetch(async (url, options = {}) => { const stringUrl = String(url); if (stringUrl === "https://example.com/bfl-input.png") { return new Response(new Uint8Array([1, 2]), { @@ -582,7 +580,7 @@ test("handleImageGeneration polls Black Forest Labs results and sends base64 inp } throw new Error(`Unexpected URL: ${stringUrl}`); - }; + }); try { const result = await handleImageGeneration({ @@ -605,7 +603,7 @@ test("handleImageGeneration polls Black Forest Labs results and sends base64 inp assert.equal(pollCapture.headers["x-key"], "bfl-key"); assert.equal(result.data.data[0].b64_json, "CQgH"); } finally { - globalThis.fetch = originalFetch; + mockFetch(originalFetch); globalThis.setTimeout = originalSetTimeout; } }); @@ -660,7 +658,7 @@ test("handleImageGeneration uploads source images to Topaz and returns base64 ou const originalFetch = globalThis.fetch; let requestCapture; - globalThis.fetch = async (url, options = {}) => { + mockFetch(async (url, options = {}) => { const stringUrl = String(url); if (stringUrl === "https://example.com/topaz-input.png") { return new Response(new Uint8Array([1, 2, 3]), { @@ -686,7 +684,7 @@ test("handleImageGeneration uploads source images to Topaz and returns base64 ou } throw new Error(`Unexpected URL: ${stringUrl}`); - }; + }); try { const result = await handleImageGeneration({ @@ -709,7 +707,7 @@ test("handleImageGeneration uploads source images to Topaz and returns base64 ou assert.ok(requestCapture.image instanceof File); assert.equal(result.data.data[0].b64_json, "BwcH"); } finally { - globalThis.fetch = originalFetch; + mockFetch(originalFetch); } }); @@ -1050,7 +1048,7 @@ test("handleImageGeneration polls NanoBanana task results and converts URLs to b const originalFetch = globalThis.fetch; const calls = []; - globalThis.fetch = async (url, options = {}) => { + mockFetch(async (url, options = {}) => { const stringUrl = String(url); calls.push(stringUrl); @@ -1078,7 +1076,7 @@ test("handleImageGeneration polls NanoBanana task results and converts URLs to b } throw new Error(`Unexpected URL: ${stringUrl}`); - }; + }); try { const result = await handleImageGeneration({ @@ -1099,7 +1097,7 @@ test("handleImageGeneration polls NanoBanana task results and converts URLs to b ]); assert.deepEqual(result.data.data, [{ b64_json: "AQIDBA==", revised_prompt: "banana async" }]); } finally { - globalThis.fetch = originalFetch; + mockFetch(originalFetch); } }); @@ -2193,7 +2191,7 @@ test("handleImageGeneration still downloads a public image_url whose DNS resolve const fetchedUrls = []; let requestCapture; - globalThis.fetch = async (url, options = {}) => { + mockFetch(async (url, options = {}) => { const stringUrl = String(url); fetchedUrls.push(stringUrl); if (stringUrl === "https://cdn.example.com/public-input.png") { @@ -2210,7 +2208,7 @@ test("handleImageGeneration still downloads a public image_url whose DNS resolve }); } throw new Error(`Unexpected URL: ${stringUrl}`); - }; + }); try { const result = await handleImageGeneration({ @@ -2229,6 +2227,6 @@ test("handleImageGeneration still downloads a public image_url whose DNS resolve assert.equal(fetchedUrls[0], "https://cdn.example.com/public-input.png"); assert.equal((requestCapture.body.get("image") as Blob).size, 3); } finally { - globalThis.fetch = originalFetch; + mockFetch(originalFetch); } }); diff --git a/tests/unit/image-generation-route.test.ts b/tests/unit/image-generation-route.test.ts index 11e3dbb53b..1cae8a7eae 100644 --- a/tests/unit/image-generation-route.test.ts +++ b/tests/unit/image-generation-route.test.ts @@ -19,6 +19,7 @@ const providerChatRoute = await import("../../src/app/api/v1/providers/[provider]/chat/completions/route.ts"); const imageEditRoute = await import("../../src/app/api/v1/images/edits/route.ts"); const v1ModelsCatalog = await import("../../src/app/api/v1/models/catalog.ts"); +const { setPinnedFetchTestOverride } = await import("../../src/shared/network/remoteImageFetch.ts"); const originalFetch = globalThis.fetch; @@ -72,6 +73,7 @@ function createCodexEditForm( async function resetStorage() { globalThis.fetch = originalFetch; + setPinnedFetchTestOverride(undefined); apiKeysDb.resetApiKeyState(); core.resetDbInstance(); fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); @@ -120,6 +122,7 @@ test.beforeEach(async () => { test.after(() => { globalThis.fetch = originalFetch; + setPinnedFetchTestOverride(undefined); apiKeysDb.resetApiKeyState(); core.resetDbInstance(); fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true, maxRetries: 5, retryDelay: 100 }); @@ -234,7 +237,7 @@ test("v1 image models GET exposes current Codex image models and hides inactive test("v1 image generation POST accepts promptless requests for image-only models", async () => { await seedConnection("topaz", { apiKey: "topaz-key" }); - globalThis.fetch = async (url, options: RequestInit = {}) => { + const mockFetchImpl = async (url, options: RequestInit = {}) => { const stringUrl = String(url); if (stringUrl === "https://example.com/topaz-input.png") { return new Response(new Uint8Array([1, 2, 3]), { @@ -254,6 +257,11 @@ test("v1 image generation POST accepts promptless requests for image-only models throw new Error(`Unexpected URL: ${stringUrl}`); }; + // #13883: resolveImageSource now sets `pinDns: true`, which pins the connection via a + // real undici socket and would bypass this mocked globalThis.fetch — route it through + // the test-only pinned-fetch override instead (src/shared/network/remoteImageFetch.ts). + globalThis.fetch = mockFetchImpl; + setPinnedFetchTestOverride(mockFetchImpl); const response = await imageRoute.POST( new Request("http://localhost/api/v1/images/generations", { diff --git a/tests/unit/image-upscale.test.ts b/tests/unit/image-upscale.test.ts index 851be6e0bc..2cbbf4038a 100644 --- a/tests/unit/image-upscale.test.ts +++ b/tests/unit/image-upscale.test.ts @@ -34,6 +34,7 @@ import { handleImageUpscale } from "../../open-sse/handlers/imageUpscale.ts"; import { handleStabilityImageUpscale } from "../../open-sse/handlers/imageUpscale/stability.ts"; import { handleTopazImageUpscale } from "../../open-sse/handlers/imageUpscale/topaz.ts"; import { IMAGE_PROVIDERS } from "../../open-sse/config/imageRegistry.ts"; +import { setPinnedFetchTestOverride } from "../../src/shared/network/remoteImageFetch.ts"; // ── Fixtures ─────────────────────────────────────────────────────────────── @@ -758,10 +759,15 @@ for (const privateUrl of ["http://127.0.0.1:1/x.png", "http://192.168.1.50/x.png test("resolveUpscaleImageSource still downloads a public URL whose DNS resolves to a public IP (GHSA-34rg-3pqj-35g9)", async () => { const originalFetch = globalThis.fetch; const fetchedUrls: string[] = []; - globalThis.fetch = (async (url: string | URL | Request) => { + const mockFetchImpl = (async (url: string | URL | Request) => { fetchedUrls.push(String(url)); return new Response(bytes(PNG_1X1), { status: 200, headers: { "content-type": "image/png" } }); }) as unknown as typeof fetch; + // #13883: resolveUpscaleImageSource now sets `pinDns: true`, which pins the connection + // via a real undici socket and would bypass this mocked globalThis.fetch — route it + // through the test-only pinned-fetch override instead (src/shared/network/remoteImageFetch.ts). + globalThis.fetch = mockFetchImpl; + setPinnedFetchTestOverride(mockFetchImpl); try { const source = await withPublicDns(() => @@ -772,5 +778,6 @@ test("resolveUpscaleImageSource still downloads a public URL whose DNS resolves assert.deepEqual(fetchedUrls, ["https://cdn.example.com/public.png"]); } finally { globalThis.fetch = originalFetch; + setPinnedFetchTestOverride(undefined); } }); diff --git a/tests/unit/nanobanana-image-handler.test.ts b/tests/unit/nanobanana-image-handler.test.ts index fc438dff46..034ba6ddd7 100644 --- a/tests/unit/nanobanana-image-handler.test.ts +++ b/tests/unit/nanobanana-image-handler.test.ts @@ -3,6 +3,7 @@ import assert from "node:assert/strict"; import dns from "node:dns"; import { handleImageGeneration } from "../../open-sse/handlers/imageGeneration.ts"; +import { setPinnedFetchTestOverride } from "../../src/shared/network/remoteImageFetch.ts"; // Stub DNS for fetchRemoteImage's GHSA-cmhj-wh2f-9cgx DNS-rebinding guard // (assertHostnameResolvesPublic in src/shared/network/remoteImageFetch.ts). @@ -93,7 +94,7 @@ test("handleImageGeneration(nanobanana): async submit+poll returns URL payload", test("handleImageGeneration(nanobanana): response_format=b64_json converts URL to b64", async () => { const originalFetch = globalThis.fetch; - globalThis.fetch = async (url) => { + const mockFetchImpl = async (url) => { const u = String(url); if (u.includes("/generate")) { @@ -123,6 +124,12 @@ test("handleImageGeneration(nanobanana): response_format=b64_json converts URL t throw new Error(`Unexpected URL: ${u}`); }; + // #13883: resolveImageSource (used for the URL result → base64 conversion) now sets + // `pinDns: true`, which pins the connection via a real undici socket and would bypass + // this mocked globalThis.fetch — route it through the test-only pinned-fetch override + // instead (src/shared/network/remoteImageFetch.ts). + globalThis.fetch = mockFetchImpl; + setPinnedFetchTestOverride(mockFetchImpl); try { const result = await handleImageGeneration({ @@ -140,6 +147,7 @@ test("handleImageGeneration(nanobanana): response_format=b64_json converts URL t assert.equal(result.data.data[0].b64_json, "iVBORw=="); } finally { globalThis.fetch = originalFetch; + setPinnedFetchTestOverride(undefined); } }); diff --git a/tests/unit/pindns-toctou-13883.test.ts b/tests/unit/pindns-toctou-13883.test.ts new file mode 100644 index 0000000000..194746d7dc --- /dev/null +++ b/tests/unit/pindns-toctou-13883.test.ts @@ -0,0 +1,81 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import dns from "node:dns"; +import { mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +process.env.DATA_DIR = mkdtempSync(join(tmpdir(), "omniroute-pindns-13883-")); + +// #13883 — security: pinDns off at the three new public-only image fetch sites let a +// DNS-rebinding hostname (public at validation time, private at real connect time) bypass +// the `guard: "public-only"` check, since an un-pinned fetch performs its own, independent +// DNS resolution. `resolveImageSource` / `normalizeNanoBananaTaskResult` (imageGeneration.ts) +// and `resolveUpscaleImageSource` (imageUpscale/shared.ts) now all set `pinDns: true`, which +// closes the gap by binding the connection to the single validated DNS answer instead of +// letting the transport re-resolve it — see `src/shared/network/dnsPinnedFetch.ts`. +// +// None of these three call sites expose a `lookup` injection point (they always use the +// real resolver), so this regression guard resolves a fake hostname to a public-looking, +// deliberately unreachable TEST-NET-3 address (RFC 5737 — never routed on the public +// internet) and asserts each site's request goes out through the real pinned undici socket +// (and therefore fails closed against that unreachable address) rather than through a +// mocked `globalThis.fetch`. If a future change dropped `pinDns: true` at any of these +// sites, the un-pinned `fetch()` call would hit the mock below instead — turning this red. + +function withPublicDns(run: () => Promise): Promise { + const original = dns.promises.lookup; + (dns.promises as { lookup: unknown }).lookup = (async ( + _hostname: string, + options?: { all?: boolean } + ) => { + const record = { address: "203.0.113.7", family: 4 }; // RFC 5737 TEST-NET-3: unreachable + return options && options.all ? [record] : record; + }) as typeof dns.promises.lookup; + return run().finally(() => { + (dns.promises as { lookup: unknown }).lookup = original; + }); +} + +const { resolveImageSource, normalizeNanoBananaTaskResult } = + await import("../../open-sse/handlers/imageGeneration.ts"); +const { resolveUpscaleImageSource } = + await import("../../open-sse/handlers/imageUpscale/shared.ts"); + +/** Runs `attempt` with a mocked `globalThis.fetch` that must never be reached when + * `pinDns: true` is wired correctly, and confirms the call still fails closed (the pinned + * connection targets an unreachable address instead of falling back to the mock). */ +async function assertPinnedNotMocked(attempt: () => Promise): Promise { + let mockCalled = false; + const originalFetch = globalThis.fetch; + globalThis.fetch = (async () => { + mockCalled = true; + throw new Error("globalThis.fetch must not be reached when pinDns is active"); + }) as typeof fetch; + + try { + await assert.rejects(() => withPublicDns(attempt)); + assert.equal(mockCalled, false, "pinDns must bypass globalThis.fetch, not call it"); + } finally { + globalThis.fetch = originalFetch; + } +} + +test("resolveImageSource (imageGeneration.ts) fetches through the real pinned socket, not a mocked fetch (#13883)", async () => { + await assertPinnedNotMocked(() => resolveImageSource("https://rebind-13883.example.com/x.png")); +}); + +test("normalizeNanoBananaTaskResult result-URL download fetches through the real pinned socket, not a mocked fetch (#13883)", async () => { + const taskData = { + response: { resultImageUrl: "https://rebind-13883.example.com/result.png" }, + }; + await assertPinnedNotMocked(() => + normalizeNanoBananaTaskResult(taskData, { response_format: "b64_json" }, null) + ); +}); + +test("resolveUpscaleImageSource (imageUpscale/shared.ts) fetches through the real pinned socket, not a mocked fetch (#13883)", async () => { + await assertPinnedNotMocked(() => + resolveUpscaleImageSource("https://rebind-13883.example.com/source.png") + ); +});