From 47032e7769af26a5eeca07054f0e7360f0582aab Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Wed, 16 Sep 2026 06:18:22 -0300 Subject: [PATCH] fix(providers): correct Magnific key validation probe path (#12927) (#13754) Merged in the 2026-09-16 sweep of the maintainer's own open PRs, at the owner's explicit instruction. No push was made to the PR branch: the merge took the head as the owning session left it (verified OPEN, non-draft and MERGEABLE against the release tip immediately before merging). --- ...-magnific-key-validation-false-negative.md | 1 + src/lib/providers/imageValidation.ts | 8 +- ...ue-12927-magnific-validation-probe.test.ts | 80 +++++++++++++++++++ .../provider-validation-image-only.test.ts | 6 +- 4 files changed, 90 insertions(+), 5 deletions(-) create mode 100644 changelog.d/fixes/12927-magnific-key-validation-false-negative.md create mode 100644 tests/unit/issue-12927-magnific-validation-probe.test.ts diff --git a/changelog.d/fixes/12927-magnific-key-validation-false-negative.md b/changelog.d/fixes/12927-magnific-key-validation-false-negative.md new file mode 100644 index 0000000000..a694de7a66 --- /dev/null +++ b/changelog.d/fixes/12927-magnific-key-validation-false-negative.md @@ -0,0 +1 @@ +- **fix(providers):** correct Magnific API key validation, which reported every valid key as invalid due to a GET probe against a POST-only endpoint (#12927) — thanks @hubo1989 diff --git a/src/lib/providers/imageValidation.ts b/src/lib/providers/imageValidation.ts index 4e190f95d3..bb1c24a221 100644 --- a/src/lib/providers/imageValidation.ts +++ b/src/lib/providers/imageValidation.ts @@ -30,9 +30,13 @@ const IMAGE_PROVIDER_VALIDATION_ENDPOINTS: Record< path: "/account/v1/credits/balance", }, magnific: { - // GET /v1/ai/mystic lists tasks and does not start a paid generation. + // GET /v1/ai/mystic is POST-only (task submission); once a key authenticates, + // routing to that unhandled GET 404s, reporting every valid key as invalid + // (#12927). GET /v1/ai/flows is a genuine read-only route that returns 200 for + // valid keys (team AND personal accounts) and 401 for invalid/missing keys, + // verified against a real Premium+ personal account by the issue reporter. baseUrl: "https://api.magnific.com", - path: "/v1/ai/mystic", + path: "/v1/ai/flows", }, }; diff --git a/tests/unit/issue-12927-magnific-validation-probe.test.ts b/tests/unit/issue-12927-magnific-validation-probe.test.ts new file mode 100644 index 0000000000..f9cfd8faf2 --- /dev/null +++ b/tests/unit/issue-12927-magnific-validation-probe.test.ts @@ -0,0 +1,80 @@ +// Repro for GitHub issue #12927: the Magnific image-provider key validation probe +// sends GET /v1/ai/mystic, which is a POST-only task-submission route. Once +// authentication passes, the API has no GET handler for that path and returns 404, +// so a VALID key is reported as invalid ("Validation failed: 404"). +// +// Run: DATA_DIR=$(mktemp -d) node --import tsx/esm --test --test-force-exit \ +// tests/unit/issue-12927-magnific-validation-probe.test.ts + +import assert from "node:assert/strict"; +import { test } from "node:test"; + +// Simulates the real Magnific API auth-before-routing behavior described by the +// reporter: a bad key gets 401 on every route; a valid key gets routed and the +// POST-only /v1/ai/mystic path answers 404 to a GET, while /v1/ai/flows answers 200. +function mockMagnificFetch(input: RequestInfo | URL, init?: RequestInit): Promise { + const url = String(input instanceof URL ? input.toString() : input); + const method = String(init?.method || "GET").toUpperCase(); + const headers = new Headers(init?.headers); + const apiKey = headers.get("x-magnific-api-key"); + + if (apiKey !== "valid-magnific-key") { + return Promise.resolve( + new Response(JSON.stringify({ error: "Unauthorized" }), { status: 401 }) + ); + } + + if (url === "https://api.magnific.com/v1/ai/mystic" && method === "GET") { + // Valid key, but this route is POST-only -> 404 once routed past auth. + return Promise.resolve(new Response(JSON.stringify({ error: "Not found" }), { status: 404 })); + } + + if (url === "https://api.magnific.com/v1/ai/flows" && method === "GET") { + // The reporter's verified working read-only probe for personal accounts. + return Promise.resolve(new Response(JSON.stringify({ flows: [] }), { status: 200 })); + } + + return Promise.resolve(new Response(JSON.stringify({ error: "unexpected" }), { status: 500 })); +} + +// NOTE: `open-sse/utils/proxyFetch.ts` unconditionally does +// `globalThis.fetch = patchedFetch` as a module-level side effect the first time it is +// imported (directly or transitively). imageValidation.ts pulls it in via +// safeOutboundFetch -> proxyFetch, so assigning our mock to globalThis.fetch BEFORE that +// first import gets silently clobbered. Import first, THEN install the mock so it is what +// `fetchWithTimeout` reads (`fetchFn || globalThis.fetch`, read at call time, not captured). +const { validateImageProviderApiKey } = await import("../../src/lib/providers/imageValidation.ts"); +(globalThis as unknown as { fetch: typeof mockMagnificFetch }).fetch = mockMagnificFetch; + +test("issue #12927: a genuinely valid Magnific key must validate as valid", async () => { + const result = await validateImageProviderApiKey({ + provider: "magnific", + apiKey: "valid-magnific-key", + providerSpecificData: {}, + }); + + // EXPECTED (post-fix): a valid key validates successfully. + // Pre-fix, IMAGE_PROVIDER_VALIDATION_ENDPOINTS.magnific pointed GET at + // /v1/ai/mystic, a POST-only task-submission route. Auth passed but routing 404s, + // so validateImageProviderApiKey() reported `{ valid: false, error: "Validation failed: 404" }` + // for a key that is genuinely valid — the false negative from the issue. + assert.equal( + result.valid, + true, + `expected a valid key to validate as valid, got: ${JSON.stringify(result)}` + ); +}); + +test("control: invalid Magnific key correctly fails with 401 -> Invalid API key", async () => { + const { validateImageProviderApiKey } = + await import("../../src/lib/providers/imageValidation.ts"); + + const result = await validateImageProviderApiKey({ + provider: "magnific", + apiKey: "totally-wrong-key", + providerSpecificData: {}, + }); + + assert.equal(result.valid, false); + assert.equal(result.error, "Invalid API key"); +}); diff --git a/tests/unit/provider-validation-image-only.test.ts b/tests/unit/provider-validation-image-only.test.ts index 11a37dee18..4d2a1736a1 100644 --- a/tests/unit/provider-validation-image-only.test.ts +++ b/tests/unit/provider-validation-image-only.test.ts @@ -36,7 +36,7 @@ const imageOnlyProviders = { value: "topaz-key", }, magnific: { - url: "https://api.magnific.com/v1/ai/mystic", + url: "https://api.magnific.com/v1/ai/flows", header: "x-magnific-api-key", value: "magnific-key", }, @@ -101,11 +101,11 @@ for (const provider of Object.keys(imageOnlyProviders)) { } } -test("freepik alias validates through the Magnific Mystic endpoint", async () => { +test("freepik alias validates through the Magnific Flows endpoint", async () => { let fetchCalled = false; globalThis.fetch = async (url, init = {}) => { fetchCalled = true; - assert.equal(String(url), "https://api.magnific.com/v1/ai/mystic"); + assert.equal(String(url), "https://api.magnific.com/v1/ai/flows"); assert.equal((init.headers as Record)["x-magnific-api-key"], "legacy-key"); return new Response(JSON.stringify({ data: [] }), { status: 200 }); };