import { test } from "node:test"; import assert from "node:assert"; import { resolvePublicCred } from "../../open-sse/utils/publicCreds.ts"; import { ADOBE_FIREFLY_IMAGE_MODELS, ADOBE_FIREFLY_VIDEO_MODELS, adobeFireflyApiKey, adobeFireflyBalanceApiKey, buildAdobeImagePayload, buildAdobePollHeaders, buildAdobeSubmitHeaders, buildAdobeUploadHeaders, buildAdobeVideoPayload, extractAdobeAccountIdFromToken, extractAdobeCredentialToken, extractAdobeMediaUrl, extractAdobeResultLink, extractAdobeSourceImageSources, looksLikeAdobeJwt, normalizeAdobeAspectRatio, normalizeAdobeOutputResolution, normalizeAdobePollUrl, parseAdobeCreditsBalance, parseAdobeModelsDiscovery, parseAdobeImageSourceBytes, parseAdobeStorageUploadResponse, resolveAdobeImageModel, resolveAdobeVideoModel, resolveAdobeSourceImageIds, adobeFireflyGenerateImage, adobeFireflyGenerateVideo, exchangeAdobeCookieForAccessToken, isAdobeGuestAccessToken, isAdobeUserAccessToken, isAdobeTransientSubmitError, generateAdobeNonce, extractAdobeArpSessionId, resolveAdobeAccessToken, ADOBE_FIREFLY_IMAGE_UPLOAD_URL, } from "../../open-sse/services/adobeFireflyClient.ts"; import { ADOBE_FIREFLY_FALLBACK_MODELS, getAdobeFireflyFallbackCatalog, mapDiscoveredToCatalog, } from "../../open-sse/services/adobeFireflyModels.ts"; import { buildAdobeFireflyCreditsQuota, buildAdobeFireflyQuotasRecord, } from "../../open-sse/services/usage/adobeFirefly.ts"; import { USAGE_SUPPORTED_PROVIDERS } from "../../src/shared/constants/providers.ts"; import { handleAdobeFireflyImageGeneration } from "../../open-sse/handlers/imageGeneration/providers/adobeFirefly.ts"; import { handleAdobeFireflyVideoGeneration } from "../../open-sse/handlers/videoGeneration/adobeFireflyHandler.ts"; import { WEB_COOKIE_PROVIDERS } from "../../src/shared/constants/providers/web-cookie.ts"; import { IMAGE_PROVIDERS } from "../../open-sse/config/imageRegistry.ts"; import { VIDEO_PROVIDERS } from "../../open-sse/config/videoRegistry.ts"; import { getExecutor } from "../../open-sse/executors/index.ts"; // --- Registry -------------------------------------------------------------- test("adobe-firefly is registered in WEB_COOKIE_PROVIDERS with a webCookie risk notice", () => { const entry = (WEB_COOKIE_PROVIDERS as Record)["adobe-firefly"]; assert.ok(entry, "adobe-firefly must exist in WEB_COOKIE_PROVIDERS"); assert.equal(entry.id, "adobe-firefly"); assert.equal(entry.alias, "firefly"); assert.equal(entry.subscriptionRisk, true); assert.equal(entry.riskNoticeVariant, "webCookie"); assert.match(entry.website, /firefly\.adobe\.com/); }); test("adobe-firefly is registered in IMAGE_PROVIDERS with adobe-firefly-image format", () => { const entry = (IMAGE_PROVIDERS as Record)["adobe-firefly"]; assert.ok(entry); assert.equal(entry.format, "adobe-firefly-image"); assert.match(entry.baseUrl, /firefly-3p\.ff\.adobe\.io/); assert.ok(Array.isArray(entry.models) && entry.models.length >= 4); }); test("adobe-firefly is registered in VIDEO_PROVIDERS with adobe-firefly-video format", () => { const entry = (VIDEO_PROVIDERS as Record)["adobe-firefly"]; assert.ok(entry); assert.equal(entry.format, "adobe-firefly-video"); assert.match(entry.baseUrl, /3p-videos/); assert.ok(Array.isArray(entry.models) && entry.models.length >= 5); }); test("getExecutor(adobe-firefly) rejects chat completions", async () => { const executor = getExecutor("adobe-firefly"); assert.ok(executor); const result = await executor.execute({ model: "adobe-firefly/nano-banana-pro", body: { model: "adobe-firefly/nano-banana-pro", messages: [{ role: "user", content: "hi" }], }, stream: false, credentials: { apiKey: "tok" }, }); assert.ok(result.response, "executor must return a Response wrapper"); assert.equal(result.response.status, 400); const bodyText = await result.response.text(); assert.match(bodyText, /images\/generations|videos\/generations|media-generation/i); }); // --- Public credential ----------------------------------------------------- test("adobe_firefly_api_key embedded default decodes to clio-playground-web", () => { assert.equal(resolvePublicCred("adobe_firefly_api_key"), "clio-playground-web"); assert.equal(adobeFireflyApiKey(), "clio-playground-web"); assert.equal(adobeFireflyBalanceApiKey(), "SunbreakWebUI1"); }); // --- Pure helpers ---------------------------------------------------------- test("looksLikeAdobeJwt detects JWT shape and rejects cookie blobs", () => { const longJwt = `eyJhbGciOiJSUzI1NiJ9.${"a".repeat(40)}.${"b".repeat(40)}`; assert.equal(looksLikeAdobeJwt(longJwt), true); assert.equal(looksLikeAdobeJwt("aaa.bbb.ccc"), false); // too short assert.equal(looksLikeAdobeJwt("s_ecid=foo; session=bar"), false); assert.equal(looksLikeAdobeJwt("not-a-jwt"), false); }); test("extractAdobeCredentialToken strips Bearer and access_token=", () => { const longJwt = `eyJhbGciOiJSUzI1NiJ9.${"c".repeat(40)}.${"d".repeat(40)}`; assert.equal(extractAdobeCredentialToken(`Bearer ${longJwt}`), longJwt); assert.equal(extractAdobeCredentialToken(`access_token=${longJwt}; other=1`), longJwt); assert.equal(extractAdobeCredentialToken(" rawcookie "), "rawcookie"); // IMS sessionStorage shape (firefly.adobe.com) const sessionJson = JSON.stringify({ valid: true, client_id: "clio-playground-web", tokenValue: longJwt, }); assert.equal(extractAdobeCredentialToken(sessionJson), longJwt); }); test("normalizeAdobeAspectRatio maps sizes and ratios", () => { assert.equal(normalizeAdobeAspectRatio("16:9"), "16:9"); assert.equal(normalizeAdobeAspectRatio("16x9"), "16:9"); assert.equal(normalizeAdobeAspectRatio("1024x1024"), "1:1"); assert.equal(normalizeAdobeAspectRatio("1792x1024"), "16:9"); assert.equal(normalizeAdobeAspectRatio("1024x1792"), "9:16"); assert.equal(normalizeAdobeAspectRatio("auto"), "1:1"); assert.equal(normalizeAdobeAspectRatio(undefined), "1:1"); }); test("normalizeAdobeOutputResolution maps quality tiers", () => { assert.equal(normalizeAdobeOutputResolution("4k", null), "4K"); assert.equal(normalizeAdobeOutputResolution("high", null), "4K"); assert.equal(normalizeAdobeOutputResolution("2k", null), "2K"); assert.equal(normalizeAdobeOutputResolution("low", null), "1K"); assert.equal(normalizeAdobeOutputResolution(undefined, "4096x4096"), "4K"); assert.equal(normalizeAdobeOutputResolution(undefined, undefined), "2K"); }); test("resolveAdobeImageModel maps catalog and long model ids", () => { assert.equal(resolveAdobeImageModel("nano-banana-pro").id, "nano-banana-pro"); assert.equal(resolveAdobeImageModel("adobe-firefly/nano-banana-2").id, "nano-banana-2"); assert.equal(resolveAdobeImageModel("firefly-nano-banana-pro-2k-16x9").id, "nano-banana-pro"); assert.equal(resolveAdobeImageModel("gpt-image").id, "gpt-image"); assert.ok(ADOBE_FIREFLY_IMAGE_MODELS["nano-banana-pro"].upstreamModelVersion); }); test("resolveAdobeVideoModel maps sora/veo/kling families", () => { assert.equal(resolveAdobeVideoModel("sora-2").id, "sora-2"); assert.equal(resolveAdobeVideoModel("firefly-sora2-pro-8s-16x9").id, "sora-2-pro"); assert.equal(resolveAdobeVideoModel("veo-3.1-fast").id, "veo-3.1-fast"); assert.equal(resolveAdobeVideoModel("kling-3").id, "kling-3"); assert.ok(ADOBE_FIREFLY_VIDEO_MODELS["sora-2"].defaultDuration > 0); }); test("buildAdobeImagePayload produces nano and gpt-image shapes", () => { const nano = buildAdobeImagePayload({ prompt: "a cat", aspectRatio: "16:9", outputResolution: "2K", modelSpec: ADOBE_FIREFLY_IMAGE_MODELS["nano-banana-pro"], }); assert.equal(nano.modelId, "gemini-flash"); assert.equal(nano.modelVersion, "nano-banana-2"); assert.deepEqual(nano.size, { width: 2752, height: 1536 }); assert.equal((nano.modelSpecificPayload as Record).aspectRatio, "16:9"); const gpt = buildAdobeImagePayload({ prompt: "a dog", aspectRatio: "1:1", outputResolution: "1K", modelSpec: ADOBE_FIREFLY_IMAGE_MODELS["gpt-image"], quality: "high", }); assert.equal(gpt.modelId, "gpt-image"); assert.equal((gpt.generationSettings as Record).detailLevel, 5); // Live browser body uses size:"auto" and no top-level size/outputResolution assert.equal((gpt.modelSpecificPayload as Record).size, "auto"); assert.equal(gpt.size, undefined); assert.equal(gpt.outputResolution, undefined); }); test("buildAdobeImagePayload attaches referenceBlobs like live adobe_atach_images capture", () => { // Live: referenceBlobs usage "general", module stays text2image for nano const nano = buildAdobeImagePayload({ prompt: "teest", aspectRatio: "1:1", outputResolution: "1K", modelSpec: ADOBE_FIREFLY_IMAGE_MODELS["nano-banana"], sourceImageIds: [ "2a4f1025-e0dc-4671-a11a-7dfd3c07bd94", "84c11d1a-e798-4300-a63e-c06504ca2068", ], }); assert.deepEqual(nano.referenceBlobs, [ { id: "2a4f1025-e0dc-4671-a11a-7dfd3c07bd94", usage: "general" }, { id: "84c11d1a-e798-4300-a63e-c06504ca2068", usage: "general" }, ]); assert.equal((nano.generationMetadata as Record).module, "text2image"); const gpt = buildAdobeImagePayload({ prompt: "edit me", aspectRatio: "1:1", outputResolution: "1K", modelSpec: ADOBE_FIREFLY_IMAGE_MODELS["gpt-image"], sourceImageIds: ["aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee"], }); assert.deepEqual(gpt.referenceBlobs, [ { id: "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee", usage: "subject" }, ]); assert.equal((gpt.generationMetadata as Record).module, "image2image"); }); test("extractAdobeSourceImageSources reads Media page image fields", () => { const tinyPng = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="; const sources = extractAdobeSourceImageSources({ prompt: "x", image_url: tinyPng, image_urls: [tinyPng, "https://cdn.example/b.png"], provider_options: { images: ["https://cdn.example/c.png"] }, }); assert.ok(sources.includes(tinyPng)); assert.ok(sources.includes("https://cdn.example/b.png")); assert.ok(sources.includes("https://cdn.example/c.png")); assert.equal(sources.length, 3); // tinyPng deduped from image_url + image_urls[0] }); test("parseAdobeImageSourceBytes + parseAdobeStorageUploadResponse", () => { const tinyPng = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="; const { buffer, contentType } = parseAdobeImageSourceBytes(tinyPng); assert.ok(buffer.length > 10); assert.equal(contentType, "image/png"); assert.equal( parseAdobeStorageUploadResponse({ images: [{ id: "2a4f1025-e0dc-4671-a11a-7dfd3c07bd94" }], }), "2a4f1025-e0dc-4671-a11a-7dfd3c07bd94" ); assert.equal(parseAdobeStorageUploadResponse({}), ""); }); test("buildAdobeUploadHeaders uses image content-type not json", () => { const h = buildAdobeUploadHeaders("tok", "image/png", { arpSessionId: "arp", }); assert.equal(h["content-type"], "image/png"); assert.equal(h.Authorization, "Bearer tok"); assert.equal(h["x-api-key"], "clio-playground-web"); assert.equal(h["x-arp-session-id"], "arp"); assert.ok(h["x-nonce"]); }); test("resolveAdobeSourceImageIds uploads data URLs then returns blob ids", async () => { const tinyPng = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="; let uploadCalls = 0; const fetchImpl = async (url: string | URL, init?: RequestInit) => { const u = String(url); if (u.includes("/v2/storage/image")) { uploadCalls += 1; assert.equal(init?.method, "POST"); const headers = init?.headers as Record; assert.match(String(headers["content-type"] || headers["Content-Type"] || ""), /image\//); assert.ok(init?.body); return new Response(JSON.stringify({ images: [{ id: `blob-${uploadCalls}` }] }), { status: 200, headers: { "content-type": "application/json" }, }); } throw new Error(`unexpected fetch ${u}`); }; const ids = await resolveAdobeSourceImageIds({ accessToken: "tok", body: { image_url: tinyPng, image_urls: [tinyPng] }, max: 4, prompt: "ref", fetchImpl: fetchImpl as typeof fetch, }); // Same data URL deduped → one upload assert.deepEqual(ids, ["blob-1"]); assert.equal(uploadCalls, 1); assert.equal(ADOBE_FIREFLY_IMAGE_UPLOAD_URL.includes("storage/image"), true); }); test("buildAdobeVideoPayload produces sora and veo shapes", () => { const sora = buildAdobeVideoPayload({ prompt: "ocean waves", aspectRatio: "16:9", duration: 8, modelSpec: ADOBE_FIREFLY_VIDEO_MODELS["sora-2"], }); assert.equal(sora.modelId, "sora"); assert.equal(sora.duration, 8); const veo = buildAdobeVideoPayload({ prompt: "city flyover", aspectRatio: "9:16", duration: 6, modelSpec: ADOBE_FIREFLY_VIDEO_MODELS["veo-3.1"], }); assert.equal(veo.modelId, "veo"); assert.equal(veo.modelVersion, "3.1-generate"); assert.equal( (veo.modelSpecificPayload as Record>).parameters .durationSeconds, 6 ); assert.equal(veo.generateAudio, true); }); test("extractAdobeResultLink prefers x-override-status-link then links.result", () => { const headers = new Headers({ "x-override-status-link": "https://poll.example/job/1", }); assert.equal(extractAdobeResultLink(headers, {}), "https://poll.example/job/1"); const headers2 = new Headers(); assert.equal( extractAdobeResultLink(headers2, { links: { result: { href: "https://poll.example/job/2" } }, }), "https://poll.example/job/2" ); }); test("extractAdobeMediaUrl reads outputs[].image/video.presignedUrl", () => { assert.equal( extractAdobeMediaUrl( { outputs: [{ image: { presignedUrl: "https://cdn.example/a.png" } }] }, "image" ), "https://cdn.example/a.png" ); assert.equal( extractAdobeMediaUrl( { outputs: [{ video: { presignedUrl: "https://cdn.example/a.mp4" } }] }, "video" ), "https://cdn.example/a.mp4" ); }); test("buildAdobeSubmitHeaders sets Bearer + clio-playground-web x-api-key", () => { const headers = buildAdobeSubmitHeaders("tok-1", { arpSessionId: "arp-1" }); assert.equal(headers.Authorization, "Bearer tok-1"); assert.equal(headers["x-api-key"], "clio-playground-web"); assert.equal(headers.origin, "https://firefly.adobe.com"); assert.equal(headers["content-type"], "application/json"); assert.equal(headers["x-arp-session-id"], "arp-1"); // Always has x-nonce (random when no prompt/user_id) assert.ok(headers["x-nonce"] && headers["x-nonce"].length === 64); // Never attach firefly.adobe.com page Cookie to firefly-3p (soft 408 risk) assert.equal(headers.cookie, undefined); const poll = buildAdobePollHeaders("tok-1"); assert.equal(poll.Authorization, "Bearer tok-1"); assert.equal(poll.accept, "*/*"); // status_check.txt: poll is Bearer-only (no x-api-key) assert.equal(poll["x-api-key"], undefined); }); test("buildAdobeSubmitNonce is sha256(user_id + prompt[:256])", async () => { const { buildAdobeSubmitNonce, buildAdobeArpSessionId, buildAdobeSubmitHeaders, extractAdobeAccountIdFromToken, } = await import("../../open-sse/services/adobeFireflyClient.ts"); // Minimal fake IMS JWT with AdobeID subject const payload = Buffer.from( JSON.stringify({ user_id: "0EB681AF6A5FF6C10A495FF2@AdobeID", type: "access_token", client_id: "clio-playground-web", }) ).toString("base64url"); const header = Buffer.from(JSON.stringify({ alg: "none" })).toString("base64url"); const token = `${header}.${payload}.${"x".repeat(40)}`; // Pad token length for looksLikeAdobeJwt (>=80) assert.ok(token.length >= 80 || true); const prompt = "a red fox in snow"; const nonce = buildAdobeSubmitNonce(token, prompt); assert.equal(nonce.length, 64); const { createHash } = await import("node:crypto"); const expected = createHash("sha256") .update(`0EB681AF6A5FF6C10A495FF2@AdobeID-${prompt}`, "utf8") .digest("hex"); assert.equal(nonce, expected); // Same inputs → same nonce; different prompt → different nonce assert.equal(buildAdobeSubmitNonce(token, prompt), nonce); assert.notEqual(buildAdobeSubmitNonce(token, prompt + "!"), nonce); assert.equal(extractAdobeAccountIdFromToken(token), "0EB681AF6A5FF6C10A495FF2@AdobeID"); const { isValidAdobeArpSessionId, resolveAdobeArpSessionId, extractAdobeArpSessionId, ADOBE_FIREFLY_FTR_MAGIC, } = await import("../../open-sse/services/adobeFireflyClient.ts"); const arp = buildAdobeArpSessionId(); assert.ok(arp.length > 20); assert.equal(isValidAdobeArpSessionId(arp), true); const decoded = JSON.parse(Buffer.from(arp, "base64").toString("utf8")); assert.ok(decoded.sid); // Live SPA shape (2026-07): sid + ark (Arkose) + ftr + bfp + fpjs assert.ok(decoded.ark, "synthetic ARP must include ark field"); assert.match(String(decoded.ark), /pk=BBCC314C-4937-4CCD-B0A3-FDF0F0F7603C/); assert.match(String(decoded.ftr), new RegExp(ADOBE_FIREFLY_FTR_MAGIC)); assert.match(String(decoded.ftr), /-v2_tt$/); assert.ok(decoded.bfp, "synthetic ARP must include bfp (browser fingerprint)"); assert.ok(decoded.fpjs, "synthetic ARP must include fpjs"); // Headers: deterministic nonce + always ARP (synthetic when none provided) const h = buildAdobeSubmitHeaders(token, { prompt }); assert.equal(h["x-nonce"], nonce); assert.ok(h["x-arp-session-id"]); assert.equal(h.cookie, undefined); // Prefer real sherlockToken / x-arp-session-id from paste over synthetic const realArp = Buffer.from( JSON.stringify({ sid: "11111111-2222-3333-4444-555555555555", ark: "sess.123|r=eu-west-1|pk=BBCC314C-4937-4CCD-B0A3-FDF0F0F7603C", ftr: "aa_1" + ADOBE_FIREFLY_FTR_MAGIC + "_x=-1-v2_tt", }), "utf8" ).toString("base64"); assert.equal(extractAdobeArpSessionId(`a=1; sherlockToken=${realArp}; b=2`), realArp); assert.equal( extractAdobeArpSessionId(`x-arp-session-id: ${realArp}\nAuthorization: Bearer x`), realArp ); assert.equal(resolveAdobeArpSessionId(`sherlockToken=${realArp}`), realArp); }); test("normalizeAdobePollUrl rewrites firefly-epo jobs/result to BKS", () => { const raw = "https://firefly-epo855232.adobe.io/jobs/result/4ae9fd2a-0864-46dd-9834-cfc16e91faa6"; const out = normalizeAdobePollUrl(raw); assert.match(out, /^https:\/\/bks-epo8552\.adobe\.io\/v2\/jobs\/result\/4ae9fd2a/); assert.match(out, /host=firefly-epo855232\.adobe\.io/); }); test("parseAdobeCreditsBalance maps total + free/plan buckets", () => { const bal = parseAdobeCreditsBalance({ total: { quota: { total: 10010, used: 10, available: 10000 }, availableUntil: "2026-07-28T22:48:31.576Z", }, credits: { firefly_free_credit: { quota: { total: 10, used: 0, available: 10 } }, firefly_plan_credit: { quota: { total: 10000, used: 10, available: 9990 }, }, }, }); assert.equal(bal.total, 10010); assert.equal(bal.used, 10); assert.equal(bal.remaining, 10000); assert.equal(bal.freeTotal, 10); assert.equal(bal.planTotal, 10000); const quota = buildAdobeFireflyCreditsQuota(bal); assert.equal(quota.total, 10010); assert.equal(quota.remaining, 10000); assert.equal(quota.displayName, "Firefly credits"); // providerLimits requires quotas as a Record, not an array const rec = buildAdobeFireflyQuotasRecord(bal); assert.ok(rec.firefly_total); assert.equal(rec.firefly_total.total, 10010); assert.equal(rec.firefly_total.remaining, 10000); assert.ok(rec.firefly_free); assert.ok(rec.firefly_plan); }); test("adobe-firefly is in USAGE_SUPPORTED_PROVIDERS for Limits", () => { assert.ok(USAGE_SUPPORTED_PROVIDERS.includes("adobe-firefly")); assert.ok(USAGE_SUPPORTED_PROVIDERS.includes("firefly")); }); test("parseAdobeModelsDiscovery extracts image/video versions", () => { const rows = parseAdobeModelsDiscovery({ models: [ { modelId: "gemini-flash", modelVersions: { "nano-banana-2": { enabled: true, outputModality: ["image"], modelDisplayName: "Gemini 3.0 (Nano Banana Pro)", healthStatus: "HEALTHY", requestSchema: { properties: { prompt: { type: "string" } } }, }, }, }, { modelId: "sora", modelVersions: { "sora-2": { enabled: true, outputModality: ["video"], modelDisplayName: "Sora 2", requestSchema: { properties: { prompt: { type: "string" } } }, }, }, }, ], }); assert.equal(rows.length, 2); assert.equal(rows[0].modality, "image"); assert.equal(rows[1].modality, "video"); const catalog = mapDiscoveredToCatalog(rows); assert.ok(catalog.some((m) => m.id === "gemini-flash-nano-banana-2")); assert.ok(catalog.some((m) => m.id === "sora-sora-2")); }); test("fallback catalog has image and video entries from get_models capture", () => { assert.ok(ADOBE_FIREFLY_FALLBACK_MODELS.length >= 10); assert.ok(getAdobeFireflyFallbackCatalog("image").length >= 4); assert.ok(getAdobeFireflyFallbackCatalog("video").length >= 4); }); test("extractAdobeAccountIdFromToken reads user_id claim", () => { // {"user_id":"0EB@AdobeID"} base64url const payload = Buffer.from( JSON.stringify({ user_id: "0EB@AdobeID", type: "access_token" }) ).toString("base64url"); const jwt = `eyJhbGciOiJub25lIn0.${payload}.sig`; assert.equal(extractAdobeAccountIdFromToken(jwt), "0EB@AdobeID"); }); // --- Handlers (mocked fetch) ---------------------------------------------- function jsonResponse(status: number, body: unknown, headerMap: Record = {}) { return { ok: status >= 200 && status < 300, status, headers: { get: (name: string) => { const key = Object.keys(headerMap).find((k) => k.toLowerCase() === name.toLowerCase()); return key ? headerMap[key] : null; }, }, json: async () => body, text: async () => JSON.stringify(body), } as unknown as Response; } test("handleAdobeFireflyImageGeneration returns 400 when prompt is missing", async () => { const result = await handleAdobeFireflyImageGeneration({ model: "nano-banana-pro", provider: "adobe-firefly", body: {}, credentials: { apiKey: "aaa.bbb.ccc" }, }); assert.equal(result.success, false); assert.equal(result.status, 400); }); function userImsJwt(userId = "0EB@AdobeID"): string { return ( `eyJhbGciOiJSUzI1NiJ9.` + Buffer.from( JSON.stringify({ user_id: userId, type: "access_token", client_id: "clio-playground-web", }) ).toString("base64url") + `.` + "sig".padEnd(40, "x") ); } test("handleAdobeFireflyImageGeneration submit+poll happy path (mocked)", async () => { let calls = 0; const fetchImpl = async (url: string, init?: RequestInit) => { calls += 1; const u = String(url); if (u.includes("generate-async")) { return jsonResponse( 200, { links: { result: "https://poll.example/job/img1" } }, { "x-override-status-link": "https://poll.example/job/img1" } ); } if (u.includes("poll.example")) { return jsonResponse(200, { status: "COMPLETED", outputs: [{ image: { presignedUrl: "https://cdn.example/out.png" } }], }); } throw new Error(`unexpected fetch ${u}`); }; const result = await handleAdobeFireflyImageGeneration({ model: "nano-banana-pro", provider: "adobe-firefly", body: { prompt: "sunset mountains", size: "16:9", quality: "2k" }, credentials: { apiKey: userImsJwt() }, fetchImpl: fetchImpl as typeof fetch, }); assert.equal(result.success, true); assert.ok(result.data?.data?.[0]?.url?.includes("cdn.example/out.png")); assert.ok(calls >= 2); }); test("handleAdobeFireflyImageGeneration uploads refs and submits referenceBlobs", async () => { const tinyPng = "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="; let sawGenerateBody: Record | null = null; let uploadCalls = 0; const fetchImpl = async (url: string | URL, init?: RequestInit) => { const u = String(url); if (u.includes("/v2/storage/image")) { uploadCalls += 1; return jsonResponse(200, { images: [{ id: "ref-blob-1" }] }); } if (u.includes("generate-async")) { sawGenerateBody = JSON.parse(String(init?.body || "{}")); return jsonResponse( 200, { links: { result: { href: "https://poll.example/j1" } } }, { "x-override-status-link": "https://poll.example/j1" } ); } if (u.includes("poll.example")) { return jsonResponse(200, { status: "COMPLETED", outputs: [{ image: { presignedUrl: "https://cdn.example/out.png" } }], }); } throw new Error(`unexpected fetch ${u}`); }; const result = await handleAdobeFireflyImageGeneration({ model: "nano-banana", provider: "adobe-firefly", body: { prompt: "teest", image_url: tinyPng }, credentials: { apiKey: userImsJwt() }, fetchImpl: fetchImpl as typeof fetch, }); assert.equal(result.success, true); assert.equal(uploadCalls, 1); assert.ok(sawGenerateBody); const refs = sawGenerateBody!.referenceBlobs as Array<{ id: string; usage: string; }>; assert.deepEqual(refs, [{ id: "ref-blob-1", usage: "general" }]); }); test("adobeFireflyGenerateVideo submit+poll happy path (mocked)", async () => { const fetchImpl = async (url: string) => { const u = String(url); if (u.includes("3p-videos")) { return jsonResponse( 200, { links: { result: { href: "https://poll.example/job/vid1" } } }, {} ); } if (u.includes("poll.example")) { return jsonResponse(200, { status: "COMPLETED", outputs: [{ video: { presignedUrl: "https://cdn.example/out.mp4" } }], }); } throw new Error(`unexpected fetch ${u}`); }; const result = await adobeFireflyGenerateVideo({ accessToken: "tok", prompt: "drone over forest", model: "sora-2", duration: 4, aspectRatio: "16:9", fetchImpl: fetchImpl as typeof fetch, }); assert.equal(result.format, "mp4"); assert.match(result.url, /out\.mp4/); }); test("handleAdobeFireflyVideoGeneration returns 400 without prompt", async () => { const result = await handleAdobeFireflyVideoGeneration({ model: "sora-2", provider: "adobe-firefly", body: {}, credentials: { apiKey: "aaa.bbb.ccc" }, }); assert.equal(result.success, false); assert.equal(result.status, 400); }); test("handleAdobeFireflyImageGeneration maps quota exhausted", async () => { const fetchImpl = async () => jsonResponse(403, { error: "nope" }, { "x-access-error": "taste_exhausted" }); const result = await handleAdobeFireflyImageGeneration({ model: "nano-banana-pro", provider: "adobe-firefly", body: { prompt: "test" }, credentials: { apiKey: userImsJwt() }, fetchImpl: fetchImpl as typeof fetch, }); assert.equal(result.success, false); assert.equal(result.status, 429); assert.match(String(result.error), /quota/i); }); test("guest JWT without AdobeID is detected", () => { // Minimal JWT payload {} — no user_id → guest const emptyPayload = Buffer.from("{}").toString("base64url"); const guestJwt = `eyJhbGciOiJub25lIn0.${emptyPayload}.sig`; // Pad to lookLikeAdobeJwt length if needed const longGuest = `eyJhbGciOiJSUzI1NiJ9.${Buffer.from(JSON.stringify({ client_id: "clio-playground-web" })).toString("base64url")}.` + "x".repeat(40); assert.equal(isAdobeGuestAccessToken(longGuest), true); const userJwt = `eyJhbGciOiJSUzI1NiJ9.` + Buffer.from( JSON.stringify({ user_id: "0EB@AdobeID", type: "access_token", client_id: "clio-playground-web", }) ).toString("base64url") + `.` + "y".repeat(40); assert.equal(isAdobeGuestAccessToken(userJwt), false); assert.equal(isAdobeUserAccessToken(userJwt), true); }); test("cookie exchange rejects guest IMS tokens", async () => { const fetchImpl = async (url: string, init?: RequestInit) => { if (String(url).includes("ims/check")) { const body = String(init?.body || ""); if (body.includes("guest_allowed=false")) { return jsonResponse(400, { error: "invalid_credentials", error_description: "All session cookies are empty", }); } // guest_allowed=true → guest token (no user_id) const guest = `eyJhbGciOiJSUzI1NiJ9.` + Buffer.from(JSON.stringify({ client_id: "clio-playground-web" })).toString("base64url") + `.` + "z".repeat(40); return jsonResponse(200, { access_token: guest, account_type: "guest", guestId: "1@GuestID", }); } throw new Error(`unexpected ${url}`); }; await assert.rejects( () => exchangeAdobeCookieForAccessToken( "ff_session_guid=abc; aux_sid=xyz", fetchImpl as typeof fetch ), (err: unknown) => { const message = err instanceof Error ? err.message : String(err); assert.match(message, /GUEST|guest|Bearer/i); return true; } ); }); test("extractAdobeArpSessionId recovers JWT+ARP joined by space (PasswordBox mangling)", async () => { const { extractAdobeArpSessionId, hasBrowserAdobeArpSession, formatAdobeSystemUnderLoadError } = await import("../../open-sse/services/adobeFireflyClient.ts"); const realArp = Buffer.from( JSON.stringify({ sid: "bdf37b8a-117f-467d-a737-7792932d98b4", ark: "60818c561473ddb23.0684402805|r=eu-west-1|pk=BBCC314C-4937-4CCD-B0A3-FDF0F0F7603C", ftr: "aab9dc9eb48f4ee1916428649f908f7d_1__UDF43-m4_31ck_x=-1-v2_tt", }), "utf8" ).toString("base64"); // Fake 3-segment JWT shape long enough for looksLikeAdobeJwt const fakeJwt = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9." + "eyJ1c2VyX2lkIjoiMEVCNjgxQUY2QTVGRjZDMTBBNDk1RkYyQEFkb2JlSUQifQ." + "sigABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnop"; const joined = `${fakeJwt} ${realArp}`; assert.equal(extractAdobeArpSessionId(joined), realArp); assert.equal(hasBrowserAdobeArpSession(joined), true); assert.equal(hasBrowserAdobeArpSession(fakeJwt), false); assert.match( formatAdobeSystemUnderLoadError("image", 2, { hadBrowserArp: false }), /missing a browser x-arp-session-id/ ); assert.match( formatAdobeSystemUnderLoadError("image", 2, { hadBrowserArp: true }), /auto-rebuilds x-arp-session-id|Cookie once|Forter\/Arkose/i ); }); test("extractAdobeArpSessionId does not pick aux_sid over sherlockToken", async () => { const { extractAdobeArpSessionId, isValidAdobeArpSessionId } = await import("../../open-sse/services/adobeFireflyClient.ts"); const { ADOBE_FIREFLY_FTR_MAGIC } = await import("../../open-sse/services/adobeFireflyClient.ts"); const realArp = Buffer.from( JSON.stringify({ sid: "bdf37b8a-117f-467d-a737-7792932d98b4", ark: "10618c58b8d3cd588.3119555905|r=eu-west-1|pk=BBCC314C-4937-4CCD-B0A3-FDF0F0F7603C", ftr: `aa_${Date.now()}${ADOBE_FIREFLY_FTR_MAGIC}_x=-1-v2_tt`, }), "utf8" ).toString("base64"); // Long aux_sid must NOT win ranking (this was the live 408 root cause) const aux = "A" + "x".repeat(780); const cookie = `ff_session_guid=bdf37b8a-117f-467d-a737-7792932d98b4; sherlockToken=${realArp}; aux_sid=${aux}; forterToken=x`; const got = extractAdobeArpSessionId(cookie); assert.equal(got, realArp); assert.equal(isValidAdobeArpSessionId(`aux_sid=${aux}`), false); assert.equal(isValidAdobeArpSessionId(realArp), true); }); test("rebuild ARP from cookie components (forter+arkose+sid)", async () => { const { buildAdobeArpSessionIdFromCookies, canRebuildAdobeArpFromCookies, mergeAdobeCookieHeaders, resolveAdobeArpSessionIdSmart, serializeAdobeFireflyCredential, normalizeAdobeForterToken, } = await import("../../open-sse/services/adobeFireflySession.ts"); const { ADOBE_FIREFLY_FTR_MAGIC } = await import("../../open-sse/services/adobeFireflyClient.ts"); const ftr = `aab9dc9eb48f4ee1916428649f908f7d_${Date.now()}${ADOBE_FIREFLY_FTR_MAGIC}_x=-1092-v2_tt`; const ark = "87818c58b11662a57.5347274705|r=eu-west-1|meta=3|pk=BBCC314C-4937-4CCD-B0A3-FDF0F0F7603C|at=40"; const cookie = `ff_session_guid=bdf37b8a-117f-467d-a737-7792932d98b4; arkose=${ark}; ` + `forterToken=${encodeURIComponent(ftr)}; bfp=58ef2899-b1c4-42e4-9625-ae265e1b4994; ` + `fpjs=${encodeURIComponent(JSON.stringify({ requestId: "1.x", visitorId: "v" }))}`; assert.equal(canRebuildAdobeArpFromCookies(cookie), true); const arp = buildAdobeArpSessionIdFromCookies(cookie); assert.ok(arp.length > 40); const decoded = JSON.parse(Buffer.from(arp, "base64").toString("utf8")); assert.equal(decoded.sid, "bdf37b8a-117f-467d-a737-7792932d98b4"); assert.equal(decoded.ark, ark); assert.ok(String(decoded.ftr).includes(ADOBE_FIREFLY_FTR_MAGIC)); assert.equal(decoded.bfp, "58ef2899-b1c4-42e4-9625-ae265e1b4994"); assert.ok(decoded.fpjs); // forter without _tt suffix gets normalized assert.match(normalizeAdobeForterToken("abc_1__UDF43-m4_31ck_x=-1-v2"), /-v2_tt$/); // localStorage comma form is rejected assert.equal(normalizeAdobeForterToken("aab9dc9eb48f4ee1916428649f908f7d,1784986682306"), ""); const merged = mergeAdobeCookieHeaders( "ff_session_guid=old; arkose=a1", "arkose=a2; forterToken=newftr" ); assert.match(merged, /arkose=a2/); assert.match(merged, /forterToken=newftr/); assert.match(merged, /ff_session_guid=old/); // Smart resolve prefers rebuild when cookie pieces present const smart = resolveAdobeArpSessionIdSmart(cookie); assert.equal(smart, arp); const ser = serializeAdobeFireflyCredential({ accessToken: "eyJ.token.sig", cookie, arpSessionId: arp, }); assert.match(ser, /eyJ\.token\.sig/); assert.match(ser, /ff_session_guid=/); }); test("isAdobeTransientSubmitError detects 408 system under load", () => { assert.equal( isAdobeTransientSubmitError( 408, '{"error_code":"timeout_error","message":"system under load"}' ), true ); assert.equal(isAdobeTransientSubmitError(429, "rate"), true); assert.equal(isAdobeTransientSubmitError(400, "bad request"), false); assert.ok(generateAdobeNonce().length === 64); assert.equal( extractAdobeArpSessionId("a=1; sherlockToken=eyJzaWQiOiJ4In0=; b=2"), "eyJzaWQiOiJ4In0=" ); }); test("extractAdobeCookieHeader strips JWT from mixed paste", async () => { const { extractAdobeCookieHeader, buildAdobeSubmitHeaders, extractAdobeArpSessionId } = await import("../../open-sse/services/adobeFireflyClient.ts"); const longJwt = `eyJhbGciOiJSUzI1NiJ9.${"e".repeat(40)}.${"f".repeat(40)}`; const cookie = "ff_session_guid=abc; sherlockToken=tok123; aux_sid=xyz"; const mixed = `${longJwt}\n${cookie}`; assert.equal(extractAdobeCookieHeader(longJwt), ""); assert.match(extractAdobeCookieHeader(mixed), /ff_session_guid=abc/); assert.doesNotMatch(extractAdobeCookieHeader(mixed), /eyJhbGci/); // Submit must not attach Cookie; arp id may still be lifted from the blob. const arp = extractAdobeArpSessionId(mixed); assert.equal(arp, "tok123"); const headers = buildAdobeSubmitHeaders("access-tok", { cookie: mixed, arpSessionId: arp, }); assert.equal(headers.cookie, undefined); assert.equal(headers["x-arp-session-id"], "tok123"); assert.equal(headers.Authorization, "Bearer access-tok"); }); test("resolveAdobeImageModel maps gpt-image-2 alias", async () => { const { resolveAdobeImageModel } = await import("../../open-sse/services/adobeFireflyClient.ts"); assert.equal(resolveAdobeImageModel("gpt-image-2").spec.upstreamModelVersion, "2"); assert.equal(resolveAdobeImageModel("adobe-firefly/gpt-image").spec.upstreamModelVersion, "2"); assert.equal(resolveAdobeImageModel("gpt-image-1.5").spec.upstreamModelVersion, "1.5"); }); test("image submit retries on 408 then succeeds", async () => { const { __resetAdobeFireflySessionCacheForTests } = await import("../../open-sse/services/adobeFireflySession.ts"); __resetAdobeFireflySessionCacheForTests(); let submits = 0; const userTok = userImsJwt(); const fetchImpl = async (url: string) => { const u = String(url); if (u.includes("generate-async")) { submits += 1; if (submits < 3) { return jsonResponse(408, { error_code: "timeout_error", message: "system under load", }); } return jsonResponse(200, { links: { result: { href: "https://poll.example/job/r1" } } }, {}); } if (u.includes("poll.example")) { return jsonResponse(200, { status: "COMPLETED", outputs: [{ image: { presignedUrl: "https://cdn.example/retry.png" } }], }); } throw new Error(`unexpected ${u}`); }; const result = await adobeFireflyGenerateImage({ accessToken: userTok, prompt: "retry me", model: "gpt-image", fetchImpl: fetchImpl as typeof fetch, }); assert.equal(submits, 3); assert.match(result.url, /retry\.png/); }); test("sticky ARP: successful submit is reused by ensure on next call", async () => { const { __resetAdobeFireflySessionCacheForTests, markAdobeFireflyArpSuccess, ensureAdobeFireflySession, fingerprintAdobeCredential, } = await import("../../open-sse/services/adobeFireflySession.ts"); const { ADOBE_FIREFLY_FTR_MAGIC } = await import("../../open-sse/services/adobeFireflyClient.ts"); __resetAdobeFireflySessionCacheForTests(); const userTok = userImsJwt(); const ftr = `aab9dc9eb48f4ee1916428649f908f7d_${Date.now()}${ADOBE_FIREFLY_FTR_MAGIC}_x=-1-v2_tt`; const ark = "87818c58b11662a57.5347274705|r=eu-west-1|meta=3|pk=BBCC314C-4937-4CCD-B0A3-FDF0F0F7603C|at=40"; const cookie = `ff_session_guid=bdf37b8a-117f-467d-a737-7792932d98b4; arkose=${ark}; ` + `forterToken=${encodeURIComponent(ftr)}`; const cred = `${userTok}\n${cookie}`; const fp = fingerprintAdobeCredential(cred); const stickyArp = Buffer.from( JSON.stringify({ sid: "sticky-sid", ark: "sticky-ark", ftr: "sticky-ftr" }), "utf8" ).toString("base64"); markAdobeFireflyArpSuccess(fp, stickyArp); const session = await ensureAdobeFireflySession({ credentials: { apiKey: cred }, allowBrowserRefresh: false, fetchImpl: (async () => { throw new Error("no network expected"); }) as typeof fetch, }); assert.equal(session.arpSessionId, stickyArp, "ensure must stick to last successful ARP"); assert.equal(session.fingerprint, fp); }); test("rotateAdobeFireflySessionOnError: attempt1-2 reuse sticky; attempt3 keeps ARP without browser", async () => { const { __resetAdobeFireflySessionCacheForTests, rotateAdobeFireflySessionOnError, markAdobeFireflyArpSuccess, buildAdobeArpSessionIdFromCookies, } = await import("../../open-sse/services/adobeFireflySession.ts"); const { ADOBE_FIREFLY_FTR_MAGIC } = await import("../../open-sse/services/adobeFireflyClient.ts"); __resetAdobeFireflySessionCacheForTests(); const ftr = `aa_${Date.now()}${ADOBE_FIREFLY_FTR_MAGIC}_x=-1-v2_tt`; const cookie = `ff_session_guid=sid-1; arkose=ark-1; forterToken=${encodeURIComponent(ftr)}`; const rebuilt = buildAdobeArpSessionIdFromCookies(cookie); assert.ok(rebuilt); const base = { accessToken: userImsJwt(), cookie, arpSessionId: rebuilt!, tokenExpiresAt: Date.now() + 3600_000, updatedAt: Date.now(), fingerprint: "fp-rotate-test", source: "paste" as const, }; markAdobeFireflyArpSuccess(base.fingerprint, rebuilt!); const a1 = await rotateAdobeFireflySessionOnError(base, { attempt: 1, tryBrowser: false, }); assert.equal(a1.arpSessionId, rebuilt, "attempt 1 reuses ARP (rate-limit quiet)"); const a2 = await rotateAdobeFireflySessionOnError( { ...base, arpSessionId: rebuilt! }, { attempt: 2, tryBrowser: false } ); assert.equal(a2.arpSessionId, rebuilt, "attempt 2 still reuses ARP (mid-batch quiet)"); const a3 = await rotateAdobeFireflySessionOnError( { ...base, arpSessionId: rebuilt! }, { attempt: 3, tryBrowser: false } ); // attempt 3 without browser: cookie rebuild (identical forter → same ARP) assert.ok(a3.arpSessionId, "attempt 3 still yields an ARP"); assert.equal( a3.arpSessionId, rebuilt, "identical cookie rebuild keeps ARP (no synthetic thrash)" ); const auth = await rotateAdobeFireflySessionOnError( { ...base, arpSessionId: rebuilt!, tokenExpiresAt: 0 }, { attempt: 1, authFailure: true, tryBrowser: false } ); assert.equal(auth.source, "rebuild", "auth failures must bypass quiet sticky reuse"); assert.ok(auth.tokenExpiresAt > Date.now(), "retry must repair a missing token expiry"); }); test("adobeFireflyGenerateImage cookie path exchanges IMS token first", async () => { const { __resetAdobeFireflySessionCacheForTests } = await import("../../open-sse/services/adobeFireflySession.ts"); __resetAdobeFireflySessionCacheForTests(); const userTok = `eyJhbGciOiJSUzI1NiJ9.` + Buffer.from( JSON.stringify({ user_id: "0EB@AdobeID", type: "access_token", client_id: "clio-playground-web", }) ).toString("base64url") + `.` + "s".repeat(40); const urls: string[] = []; const fetchImpl = async (url: string, init?: RequestInit) => { urls.push(String(url)); if (String(url).includes("ims/check")) { assert.equal(init?.method, "POST"); // Authenticated exchange (guest_allowed=false) return jsonResponse(200, { access_token: userTok, account_type: "type1", }); } if (String(url).includes("generate-async")) { const auth = (init?.headers as Record | undefined)?.Authorization || (init?.headers as Headers)?.get?.("Authorization"); // headers object from buildAdobeSubmitHeaders const headerAuth = typeof init?.headers === "object" && init.headers && !("get" in (init.headers as object)) ? (init.headers as Record).Authorization : auth; assert.equal(headerAuth, `Bearer ${userTok}`); return jsonResponse(200, {}, { "x-override-status-link": "https://poll.example/job/c1" }); } if (String(url).includes("poll.example")) { return jsonResponse(200, { outputs: [{ image: { presignedUrl: "https://cdn.example/cookie.png" } }], }); } throw new Error(`unexpected ${url}`); }; // Use the image handler which resolves credentials (cookie → IMS). const result = await handleAdobeFireflyImageGeneration({ model: "nano-banana-pro", provider: "adobe-firefly", body: { prompt: "cookie path" }, credentials: { apiKey: "s_ecid=abc; sessionToken=xyz; other=1" }, fetchImpl: fetchImpl as typeof fetch, }); assert.equal(result.success, true); assert.ok(urls.some((u) => u.includes("ims/check"))); assert.ok(urls.some((u) => u.includes("generate-async"))); });