From d6c4fec2ee4cf10f2938381005803c0d9ce4ef38 Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza Date: Wed, 19 Aug 2026 11:08:17 -0300 Subject: [PATCH] fix: register a real Firefly auth probe for the firefly/adobe-firefly alias pair (#10522) (#10743) Co-authored-by: Markus Hartung --- ...22-firefly-cookie-validation-alias-miss.md | 1 + src/lib/providers/validation.ts | 16 +++- src/lib/providers/validation/adobeFirefly.ts | 38 ++++++++ .../firefly-cookie-validation-10522.test.ts | 90 +++++++++++++++++++ 4 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 changelog.d/fixes/10522-firefly-cookie-validation-alias-miss.md create mode 100644 src/lib/providers/validation/adobeFirefly.ts create mode 100644 tests/unit/firefly-cookie-validation-10522.test.ts diff --git a/changelog.d/fixes/10522-firefly-cookie-validation-alias-miss.md b/changelog.d/fixes/10522-firefly-cookie-validation-alias-miss.md new file mode 100644 index 0000000000..3545d77ebb --- /dev/null +++ b/changelog.d/fixes/10522-firefly-cookie-validation-alias-miss.md @@ -0,0 +1 @@ +- fix(providers): register a real Firefly auth probe under both the `firefly` alias and the `adobe-firefly` canonical id, and normalize the provider id before the generic web-cookie fallback, so a Firefly connection stops always reporting "Provider validation not supported" (#10522) diff --git a/src/lib/providers/validation.ts b/src/lib/providers/validation.ts index a17b2df516..d5986d2419 100644 --- a/src/lib/providers/validation.ts +++ b/src/lib/providers/validation.ts @@ -7,6 +7,7 @@ import { isOpenAICompatibleProvider, isSelfHostedChatProvider, providerAllowsOptionalApiKey, + resolveProviderId, WEB_COOKIE_PROVIDERS, } from "@/shared/constants/providers"; import { MODAL_DEFAULT_VALIDATION_MODEL_ID } from "@/shared/constants/modal"; @@ -107,6 +108,7 @@ import { validateBytezProvider, } from "./validation/webCookie"; import { validateAiHordeProvider } from "./validation/aihorde"; +import { validateAdobeFireflyProvider } from "./validation/adobeFirefly"; import { validateV0VercelProvider, validateAuggieProvider, @@ -184,6 +186,11 @@ export async function validateProviderApiKey({ provider, apiKey, providerSpecifi devin: validateDevinCloudAgentProvider, auggie: validateAuggieProvider, aihorde: validateAiHordeProvider, + // #10522: registered under both the canonical id and the short alias — Firefly + // connections are commonly stored as "firefly" (same prefix as firefly/ + // routing ids), not the canonical "adobe-firefly" WEB_COOKIE_PROVIDERS key. + "adobe-firefly": validateAdobeFireflyProvider, + firefly: validateAdobeFireflyProvider, qoder: validateQoderProvider, kiro: validateKiroProvider, "command-code": validateCommandCodeProvider, @@ -328,9 +335,14 @@ export async function validateProviderApiKey({ provider, apiKey, providerSpecifi // per-provider validator (grok-web, chatgpt-web, claude-web, …) are handled by // SPECIALTY_VALIDATORS first and must not be shadowed by this generic probe (issue: the // #4023 dispatch was placed too early and intercepted every web-cookie provider). - if (WEB_COOKIE_PROVIDERS[provider]) { + const canonicalProvider = resolveProviderId(provider); + if (WEB_COOKIE_PROVIDERS[canonicalProvider]) { try { - return await validateWebCookieProvider({ provider, apiKey, providerSpecificData }); + return await validateWebCookieProvider({ + provider: canonicalProvider, + apiKey, + providerSpecificData, + }); } catch (error: any) { return toValidationErrorResult(error); } diff --git a/src/lib/providers/validation/adobeFirefly.ts b/src/lib/providers/validation/adobeFirefly.ts new file mode 100644 index 0000000000..0d198b99b9 --- /dev/null +++ b/src/lib/providers/validation/adobeFirefly.ts @@ -0,0 +1,38 @@ +/** + * Adobe Firefly key/cookie validation (#10522). + * + * validateProviderApiKey() previously had no SPECIALTY_VALIDATORS entry for + * "firefly"/"adobe-firefly" and the generic WEB_COOKIE_PROVIDERS fallback used a raw, + * unaliased lookup — so a Firefly connection always fell through to the "Provider + * validation not supported" branch regardless of whether the pasted cookie/JWT was + * valid. This reuses the already-working credits/balance probe + * (getAdobeFireflyUsage, used today by the Limits/quota page) as a real auth check: + * a successful balance fetch means the token is valid, a `{ message }` result means + * it was rejected (expired/guest/invalid), and thrown transport errors are mapped + * through the shared toValidationErrorResult() helper. + */ +import { getAdobeFireflyUsage } from "@omniroute/open-sse/services/usage/adobeFirefly.ts"; +import { toValidationErrorResult } from "./transport"; + +export async function validateAdobeFireflyProvider({ + apiKey, + providerSpecificData, + fetchImpl = fetch, +}: { + apiKey?: unknown; + providerSpecificData?: Record | null; + fetchImpl?: typeof fetch; +}) { + try { + const key = typeof apiKey === "string" ? apiKey : undefined; + const result = await getAdobeFireflyUsage(key, undefined, providerSpecificData, fetchImpl); + + if ("message" in result) { + return { valid: false, error: result.message }; + } + + return { valid: true, error: null }; + } catch (error) { + return toValidationErrorResult(error); + } +} diff --git a/tests/unit/firefly-cookie-validation-10522.test.ts b/tests/unit/firefly-cookie-validation-10522.test.ts new file mode 100644 index 0000000000..edda73f141 --- /dev/null +++ b/tests/unit/firefly-cookie-validation-10522.test.ts @@ -0,0 +1,90 @@ +import { test } from "node:test"; +import assert from "node:assert/strict"; + +import { validateProviderApiKey } from "../../src/lib/providers/validation.ts"; +import { validateAdobeFireflyProvider } from "../../src/lib/providers/validation/adobeFirefly.ts"; +import { resolveProviderId } from "../../src/shared/constants/providers.ts"; +import { ADOBE_FIREFLY_CREDITS_BALANCE_URL } from "../../open-sse/services/adobeFireflyClient.ts"; + +// A well-formed Adobe IMS *user* access token (3-segment JWT, non-guest payload, long +// enough to satisfy looksLikeAdobeJwt). Only used as a routing/shape fixture — never a +// real credential. +const userJwt = + `eyJhbGciOiJSUzI1NiJ9.` + + Buffer.from( + JSON.stringify({ + user_id: "0EB@AdobeID", + type: "access_token", + client_id: "clio-playground-web", + }) + ).toString("base64url") + + `.` + + "x".repeat(60); + +function jsonResponse(status: number, body: unknown) { + return new Response(JSON.stringify(body), { + status, + headers: { "content-type": "application/json" }, + }); +} + +test("#10522: resolveProviderId('firefly') stays stable (SPECIALTY_VALIDATORS dual-key registration depends on it)", () => { + assert.equal(resolveProviderId("firefly"), "adobe-firefly"); +}); + +test("#10522: firefly alias dispatches to the Firefly validator, not the generic unsupported fallback", async () => { + const result = await validateProviderApiKey({ + provider: "firefly", + apiKey: "not-a-real-token", + providerSpecificData: {}, + }); + assert.notEqual(result.unsupported, true); +}); + +test("#10522: adobe-firefly canonical id dispatches to the Firefly validator, not the generic unsupported fallback", async () => { + const result = await validateProviderApiKey({ + provider: "adobe-firefly", + apiKey: "not-a-real-token", + providerSpecificData: {}, + }); + assert.notEqual(result.unsupported, true); +}); + +test("#10522: expired/invalid token reports valid:false with a real error message (not 'not supported')", async () => { + const fetchImpl = async (url: string | URL) => { + assert.equal(String(url), ADOBE_FIREFLY_CREDITS_BALANCE_URL); + return jsonResponse(401, { error: "invalid_token" }); + }; + + const result = await validateAdobeFireflyProvider({ + apiKey: userJwt, + providerSpecificData: {}, + fetchImpl: fetchImpl as typeof fetch, + }); + + assert.equal(result.valid, false); + assert.notEqual((result as { unsupported?: boolean }).unsupported, true); + assert.ok(result.error && result.error.length > 0); +}); + +test("#10522: a genuine credits balance payload reports valid:true", async () => { + const fetchImpl = async (url: string | URL) => { + assert.equal(String(url), ADOBE_FIREFLY_CREDITS_BALANCE_URL); + return jsonResponse(200, { + total: { quota: { total: 100, used: 10, available: 90 } }, + credits: { + firefly_free_credit: { quota: { total: 50, used: 5, available: 45 } }, + firefly_plan_credit: { quota: { total: 50, used: 5, available: 45 } }, + }, + }); + }; + + const result = await validateAdobeFireflyProvider({ + apiKey: userJwt, + providerSpecificData: {}, + fetchImpl: fetchImpl as typeof fetch, + }); + + assert.equal(result.valid, true); + assert.equal(result.error, null); +});