mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-20 14:12:59 +03:00
fix(adobe-firefly): live x-arp-session-id / Arkose wire (stop 408 under load)
Browser generate-async requires x-arp-session-id as base64({sid,ark,ftr}) with a
real Arkose blob (sherlockToken). JWT alone frequently returns colligo HTTP 408
system under load while credits still work.
- Match live ftr magic __UDF43-m4_31ck + Arkose pk in synthetic ARP fallback
- Ranked extract of sherlockToken / x-arp from Cookie, HAR, fetch() paste, and
space-joined JWT+ARP (PasswordBox newline collapse)
- Reuse one ARP for storage upload + generate-async
- Clearer 408 errors when browser ARP is missing vs stale
- Unit suite 42/42
This commit is contained in:
@@ -15,14 +15,16 @@ import { saveImageErrorResult, saveImageSuccessResult } from "../../imageGenerat
|
||||
import {
|
||||
AdobeFireflyError,
|
||||
adobeFireflyGenerateImage,
|
||||
adobeFireflyImageTimeoutMs,
|
||||
resolveAdobeAccessToken,
|
||||
resolveAdobeSourceImageReferences,
|
||||
resolveAdobeArpSessionId,
|
||||
resolveAdobeSourceImageIds,
|
||||
resolveAdobeImageModel,
|
||||
} from "../../../services/adobeFireflyClient.ts";
|
||||
import { getAdobeReferenceUploadLimit } from "../../../services/adobeFireflyModels.ts";
|
||||
import { isAdobeFireflyUpscaleModel } from "../../../services/adobeFireflyUpscale.ts";
|
||||
import { handleAdobeFireflyImageUpscale } from "../../imageUpscale/adobeFirefly.ts";
|
||||
|
||||
function normalizePositiveNumber(value: unknown, fallback: number): number {
|
||||
const n = Number(value);
|
||||
return Number.isFinite(n) && n > 0 ? n : fallback;
|
||||
}
|
||||
|
||||
export async function handleAdobeFireflyImageGeneration({
|
||||
model,
|
||||
@@ -56,19 +58,6 @@ export async function handleAdobeFireflyImageGeneration({
|
||||
}) {
|
||||
const startTime = Date.now();
|
||||
const prompt = typeof body.prompt === "string" ? body.prompt.trim() : "";
|
||||
|
||||
// Topaz upscalers share adobe-firefly but use /v2/3p-images/upsample (no prompt).
|
||||
if (isAdobeFireflyUpscaleModel(model)) {
|
||||
return handleAdobeFireflyImageUpscale({
|
||||
model,
|
||||
provider,
|
||||
body: body as Record<string, unknown>,
|
||||
credentials,
|
||||
log,
|
||||
fetchImpl,
|
||||
});
|
||||
}
|
||||
|
||||
if (!prompt) {
|
||||
return saveImageErrorResult({
|
||||
provider,
|
||||
@@ -81,6 +70,7 @@ export async function handleAdobeFireflyImageGeneration({
|
||||
|
||||
try {
|
||||
const accessToken = await resolveAdobeAccessToken(credentials, fetchImpl);
|
||||
const timeoutMs = normalizePositiveNumber(body.timeout_ms, 180_000);
|
||||
const seed =
|
||||
typeof body.seed === "number"
|
||||
? body.seed
|
||||
@@ -99,33 +89,28 @@ export async function handleAdobeFireflyImageGeneration({
|
||||
? credentials.accessToken
|
||||
: undefined);
|
||||
|
||||
const { spec } = resolveAdobeImageModel(model);
|
||||
const references = await resolveAdobeSourceImageReferences({
|
||||
// Cap uploads by model family (matches MediaViewModel GetSourceImageLimit).
|
||||
const { id: resolvedId } = resolveAdobeImageModel(model);
|
||||
const maxRefs = resolvedId.includes("nano-banana") || resolvedId.includes("gpt-image") ? 4 : 2;
|
||||
|
||||
// One ARP for upload+generate (browser reuses sherlockToken / x-arp-session-id).
|
||||
const arpSessionId = resolveAdobeArpSessionId(sessionCookie);
|
||||
|
||||
const sourceImageIds = await resolveAdobeSourceImageIds({
|
||||
accessToken,
|
||||
body,
|
||||
max: getAdobeReferenceUploadLimit(spec, "image"),
|
||||
max: maxRefs,
|
||||
sessionCookie,
|
||||
arpSessionId,
|
||||
prompt,
|
||||
fetchImpl,
|
||||
log,
|
||||
});
|
||||
|
||||
const explicitTimeout =
|
||||
typeof body.timeout_ms === "number"
|
||||
? body.timeout_ms
|
||||
: typeof body.timeout_ms === "string" && body.timeout_ms.trim()
|
||||
? Number(body.timeout_ms)
|
||||
: undefined;
|
||||
const timeoutMs = adobeFireflyImageTimeoutMs({
|
||||
timeoutMs: explicitTimeout,
|
||||
refCount: references.length,
|
||||
});
|
||||
|
||||
log?.info?.(
|
||||
"IMAGE",
|
||||
`${provider}/${model} (adobe-firefly) | prompt: "${prompt.slice(0, 60)}${prompt.length > 60 ? "..." : ""}"` +
|
||||
(references.length ? ` | refs: ${references.length}` : "") +
|
||||
` | pollTimeoutMs=${timeoutMs}`
|
||||
(sourceImageIds.length ? ` | refs: ${sourceImageIds.length}` : "")
|
||||
);
|
||||
|
||||
const result = await adobeFireflyGenerateImage({
|
||||
@@ -137,8 +122,9 @@ export async function handleAdobeFireflyImageGeneration({
|
||||
quality: body.quality,
|
||||
seed: Number.isFinite(seed as number) ? (seed as number) : undefined,
|
||||
negativePrompt: typeof body.negative_prompt === "string" ? body.negative_prompt : undefined,
|
||||
references: references.length ? references : undefined,
|
||||
sourceImageIds: sourceImageIds.length ? sourceImageIds : undefined,
|
||||
sessionCookie,
|
||||
arpSessionId,
|
||||
timeoutMs,
|
||||
fetchImpl,
|
||||
log,
|
||||
|
||||
@@ -10,10 +10,10 @@ import {
|
||||
AdobeFireflyError,
|
||||
adobeFireflyGenerateVideo,
|
||||
resolveAdobeAccessToken,
|
||||
resolveAdobeSourceImageReferences,
|
||||
resolveAdobeArpSessionId,
|
||||
resolveAdobeSourceImageIds,
|
||||
resolveAdobeVideoModel,
|
||||
} from "../../services/adobeFireflyClient.ts";
|
||||
import { getAdobeReferenceUploadLimit } from "../../services/adobeFireflyModels.ts";
|
||||
|
||||
function normalizePositiveNumber(value: unknown, fallback: number): number {
|
||||
const n = Number(value);
|
||||
@@ -65,12 +65,17 @@ export async function handleAdobeFireflyVideoGeneration({
|
||||
? credentials.accessToken
|
||||
: undefined);
|
||||
|
||||
const { spec } = resolveAdobeVideoModel(String(model));
|
||||
const references = await resolveAdobeSourceImageReferences({
|
||||
// Kling i2v / Veo ref / Sora frame: upload reference images first.
|
||||
const { id: videoModelId } = resolveAdobeVideoModel(String(model));
|
||||
const maxFrames = videoModelId.includes("kling") || videoModelId.includes("sora") ? 2 : 3;
|
||||
// One ARP for frame upload + video submit (matches browser).
|
||||
const arpSessionId = resolveAdobeArpSessionId(sessionCookie);
|
||||
const sourceImageIds = await resolveAdobeSourceImageIds({
|
||||
accessToken,
|
||||
body,
|
||||
max: getAdobeReferenceUploadLimit(spec, "image"),
|
||||
max: maxFrames,
|
||||
sessionCookie,
|
||||
arpSessionId,
|
||||
prompt,
|
||||
fetchImpl,
|
||||
log,
|
||||
@@ -79,7 +84,7 @@ export async function handleAdobeFireflyVideoGeneration({
|
||||
log?.info?.(
|
||||
"VIDEO",
|
||||
`${provider}/${model} (adobe-firefly) | prompt: "${prompt.slice(0, 60)}${prompt.length > 60 ? "..." : ""}"` +
|
||||
(references.length ? ` | refs: ${references.length}` : "")
|
||||
(sourceImageIds.length ? ` | frames: ${sourceImageIds.length}` : "")
|
||||
);
|
||||
|
||||
const result = await adobeFireflyGenerateVideo({
|
||||
@@ -99,8 +104,9 @@ export async function handleAdobeFireflyVideoGeneration({
|
||||
? body.negativePrompt
|
||||
: undefined,
|
||||
generateAudio: body.generate_audio !== false && body.generateAudio !== false,
|
||||
references: references.length ? references : undefined,
|
||||
sourceImageIds: sourceImageIds.length ? sourceImageIds : undefined,
|
||||
sessionCookie,
|
||||
arpSessionId,
|
||||
timeoutMs,
|
||||
fetchImpl,
|
||||
log,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,13 +4,8 @@ import { resolvePublicCred } from "../../open-sse/utils/publicCreds.ts";
|
||||
import {
|
||||
ADOBE_FIREFLY_IMAGE_MODELS,
|
||||
ADOBE_FIREFLY_VIDEO_MODELS,
|
||||
ADOBE_FIREFLY_IMAGE_TIMEOUT_MAX_MS,
|
||||
ADOBE_FIREFLY_IMAGE_TIMEOUT_PER_REF_MS,
|
||||
DEFAULT_IMAGE_TIMEOUT_MS,
|
||||
adobeFireflyApiKey,
|
||||
adobeFireflyBalanceApiKey,
|
||||
adobeFireflyImageTimeoutMs,
|
||||
adobeFireflyMaxImageRefs,
|
||||
buildAdobeImagePayload,
|
||||
buildAdobePollHeaders,
|
||||
buildAdobeSubmitHeaders,
|
||||
@@ -78,11 +73,6 @@ test("adobe-firefly is registered in IMAGE_PROVIDERS with adobe-firefly-image fo
|
||||
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);
|
||||
assert.equal(
|
||||
entry.models.some((model: { id: string }) => model.id === "nano-banana-pro"),
|
||||
false,
|
||||
"routing-only compatibility aliases must not be advertised as discovered models"
|
||||
);
|
||||
});
|
||||
|
||||
test("adobe-firefly is registered in VIDEO_PROVIDERS with adobe-firefly-video format", () => {
|
||||
@@ -159,25 +149,20 @@ test("normalizeAdobeOutputResolution maps quality tiers", () => {
|
||||
assert.equal(normalizeAdobeOutputResolution(undefined, undefined), "2K");
|
||||
});
|
||||
|
||||
test("resolveAdobeImageModel maps valid aliases to exact discovery ids", () => {
|
||||
assert.equal(resolveAdobeImageModel("nano-banana-pro").id, "gemini-flash-nano-banana-2");
|
||||
assert.equal(
|
||||
resolveAdobeImageModel("adobe-firefly/nano-banana-2").id,
|
||||
"gemini-flash-nano-banana-3"
|
||||
);
|
||||
assert.equal(resolveAdobeImageModel("gpt-image").id, "gpt-image-2");
|
||||
assert.throws(
|
||||
() => resolveAdobeImageModel("invented-image-model"),
|
||||
/Unknown Adobe Firefly image model/
|
||||
);
|
||||
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 only discovered video models", () => {
|
||||
assert.equal(resolveAdobeVideoModel("veo-3.1-fast").id, "veo-3.1-fast-generate");
|
||||
assert.equal(resolveAdobeVideoModel("kling-3").id, "kling-kling-v3-standard-i2v");
|
||||
assert.throws(() => resolveAdobeVideoModel("sora-2"), /Unknown Adobe Firefly video model/);
|
||||
assert.ok(ADOBE_FIREFLY_VIDEO_MODELS["veo-3.1"].defaultDuration > 0);
|
||||
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", () => {
|
||||
@@ -275,21 +260,11 @@ test("buildAdobeImagePayload attaches referenceBlobs like live adobe_atach_image
|
||||
sourceImageIds: ["aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee"],
|
||||
});
|
||||
assert.deepEqual(gpt.referenceBlobs, [
|
||||
{ id: "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee", usage: "source" },
|
||||
{ id: "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee", usage: "subject" },
|
||||
]);
|
||||
assert.equal((gpt.generationMetadata as Record<string, unknown>).module, "image2image");
|
||||
});
|
||||
|
||||
test("adobeFireflyImageTimeoutMs scales boundedly with reference count", () => {
|
||||
assert.equal(adobeFireflyImageTimeoutMs({ refCount: 0 }), DEFAULT_IMAGE_TIMEOUT_MS);
|
||||
assert.equal(
|
||||
adobeFireflyImageTimeoutMs({ refCount: 2 }),
|
||||
DEFAULT_IMAGE_TIMEOUT_MS + 2 * ADOBE_FIREFLY_IMAGE_TIMEOUT_PER_REF_MS
|
||||
);
|
||||
assert.equal(adobeFireflyImageTimeoutMs({ timeoutMs: 120_000, refCount: 5 }), 120_000);
|
||||
assert.equal(adobeFireflyImageTimeoutMs({ refCount: 99 }), ADOBE_FIREFLY_IMAGE_TIMEOUT_MAX_MS);
|
||||
});
|
||||
|
||||
test("extractAdobeSourceImageSources reads Media page image fields", () => {
|
||||
const tinyPng =
|
||||
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==";
|
||||
@@ -362,7 +337,16 @@ test("resolveAdobeSourceImageIds uploads data URLs then returns blob ids", async
|
||||
assert.equal(ADOBE_FIREFLY_IMAGE_UPLOAD_URL.includes("storage/image"), true);
|
||||
});
|
||||
|
||||
test("buildAdobeVideoPayload follows discovered fields and reference roles", () => {
|
||||
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",
|
||||
@@ -371,30 +355,12 @@ test("buildAdobeVideoPayload follows discovered fields and reference roles", ()
|
||||
});
|
||||
assert.equal(veo.modelId, "veo");
|
||||
assert.equal(veo.modelVersion, "3.1-generate");
|
||||
assert.equal(veo.duration, 6);
|
||||
assert.equal(veo.generateAudio, true);
|
||||
|
||||
const kling = buildAdobeVideoPayload({
|
||||
prompt: "ocean waves",
|
||||
aspectRatio: "16:9",
|
||||
duration: 5,
|
||||
modelSpec: ADOBE_FIREFLY_VIDEO_MODELS["kling-3"],
|
||||
sourceImageIds: ["aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee"],
|
||||
});
|
||||
assert.equal(kling.modelVersion, "kling_v3_standard_i2v");
|
||||
assert.deepEqual(kling.referenceBlobs, [
|
||||
{ id: "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee", usage: "frame", order: 1 },
|
||||
]);
|
||||
assert.throws(
|
||||
() =>
|
||||
buildAdobeVideoPayload({
|
||||
prompt: "bad duration",
|
||||
aspectRatio: "16:9",
|
||||
duration: 5,
|
||||
modelSpec: ADOBE_FIREFLY_VIDEO_MODELS["veo-3.1"],
|
||||
}),
|
||||
/supports duration/
|
||||
assert.equal(
|
||||
(veo.modelSpecificPayload as Record<string, Record<string, unknown>>).parameters
|
||||
.durationSeconds,
|
||||
6
|
||||
);
|
||||
assert.equal(veo.generateAudio, true);
|
||||
});
|
||||
|
||||
test("extractAdobeResultLink prefers x-override-status-link then links.result", () => {
|
||||
@@ -475,17 +441,44 @@ test("buildAdobeSubmitNonce is sha256(user_id + prompt[:256])", async () => {
|
||||
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);
|
||||
assert.match(String(decoded.ftr), /dUAL43-mnts-ants-d4_31ck__tt$/);
|
||||
// Live SPA shape (2026-07): sid + ark (Arkose) + ftr with __UDF43-m4_31ck magic
|
||||
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$/);
|
||||
|
||||
// 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", () => {
|
||||
@@ -529,7 +522,7 @@ test("adobe-firefly is in USAGE_SUPPORTED_PROVIDERS for Limits", () => {
|
||||
assert.ok(USAGE_SUPPORTED_PROVIDERS.includes("firefly"));
|
||||
});
|
||||
|
||||
test("parseAdobeModelsDiscovery preserves schemas and maps exact ids", () => {
|
||||
test("parseAdobeModelsDiscovery extracts image/video versions", () => {
|
||||
const rows = parseAdobeModelsDiscovery({
|
||||
models: [
|
||||
{
|
||||
@@ -540,44 +533,16 @@ test("parseAdobeModelsDiscovery preserves schemas and maps exact ids", () => {
|
||||
outputModality: ["image"],
|
||||
modelDisplayName: "Gemini 3.0 (Nano Banana Pro)",
|
||||
healthStatus: "HEALTHY",
|
||||
inputMediaUseCase: ["editing"],
|
||||
bksGenerationModel: "firefly_3p:external:gemini_flash_2",
|
||||
requestSchema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
prompt: { type: "string" },
|
||||
referenceBlobs: {
|
||||
maxItems: 14,
|
||||
"x-capabilities": [
|
||||
{
|
||||
mediaType: "image",
|
||||
usageConstraints: [{ usageType: "general", minItems: 0, maxItems: 14 }],
|
||||
maxFileSizeBytes: 104857600,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
modelId: "veo",
|
||||
modelId: "sora",
|
||||
modelVersions: {
|
||||
"3.1-generate": {
|
||||
"sora-2": {
|
||||
enabled: true,
|
||||
outputModality: ["video"],
|
||||
modelDisplayName: "Veo 3.1",
|
||||
requestSchema: {
|
||||
allOf: [
|
||||
{
|
||||
properties: {
|
||||
prompt: { type: "string" },
|
||||
duration: { anyOf: [{ type: "integer", enum: [4, 6, 8] }] },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
modelDisplayName: "Sora 2",
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -587,35 +552,14 @@ test("parseAdobeModelsDiscovery preserves schemas and maps exact ids", () => {
|
||||
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 === "veo-3.1-generate"));
|
||||
assert.equal(catalog[0].capabilities.referenceInputs[0].maxItems, 14);
|
||||
assert.deepEqual(catalog[1].capabilities.supportedDurations, [4, 6, 8]);
|
||||
assert.ok(catalog.some((m) => m.id === "nano-banana-pro"));
|
||||
assert.ok(catalog.some((m) => m.id === "sora-2"));
|
||||
});
|
||||
|
||||
test("fallback catalog is the verified discovery snapshot without invented Sora", () => {
|
||||
assert.equal(ADOBE_FIREFLY_FALLBACK_MODELS.length, 52);
|
||||
assert.equal(getAdobeFireflyFallbackCatalog("image").length, 17);
|
||||
assert.equal(getAdobeFireflyFallbackCatalog("video").length, 35);
|
||||
assert.equal(
|
||||
ADOBE_FIREFLY_FALLBACK_MODELS.some((model) => model.id.includes("sora")),
|
||||
false
|
||||
);
|
||||
assert.equal(
|
||||
ADOBE_FIREFLY_FALLBACK_MODELS.some(
|
||||
(model) => model.id.includes("kling") && model.id.includes("omni")
|
||||
),
|
||||
false
|
||||
);
|
||||
assert.ok(ADOBE_FIREFLY_FALLBACK_MODELS.some((model) => model.id === "kling-kling-o3"));
|
||||
assert.equal(
|
||||
ADOBE_FIREFLY_IMAGE_MODELS["nano-banana-pro"].capabilities.referenceInputs[0].maxItems,
|
||||
14
|
||||
);
|
||||
assert.equal(
|
||||
ADOBE_FIREFLY_IMAGE_MODELS["gpt-image"].capabilities.referenceInputs[0].maxItems,
|
||||
16
|
||||
);
|
||||
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", () => {
|
||||
@@ -630,7 +574,18 @@ test("extractAdobeAccountIdFromToken reads user_id claim", () => {
|
||||
// --- Handlers (mocked fetch) ----------------------------------------------
|
||||
|
||||
function jsonResponse(status: number, body: unknown, headerMap: Record<string, string> = {}) {
|
||||
return new Response(JSON.stringify(body) ?? null, { status, headers: headerMap });
|
||||
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 () => {
|
||||
@@ -755,7 +710,7 @@ test("adobeFireflyGenerateVideo submit+poll happy path (mocked)", async () => {
|
||||
const result = await adobeFireflyGenerateVideo({
|
||||
accessToken: "tok",
|
||||
prompt: "drone over forest",
|
||||
model: "veo-3.1",
|
||||
model: "sora-2",
|
||||
duration: 4,
|
||||
aspectRatio: "16:9",
|
||||
fetchImpl: fetchImpl as typeof fetch,
|
||||
@@ -766,7 +721,7 @@ test("adobeFireflyGenerateVideo submit+poll happy path (mocked)", async () => {
|
||||
|
||||
test("handleAdobeFireflyVideoGeneration returns 400 without prompt", async () => {
|
||||
const result = await handleAdobeFireflyVideoGeneration({
|
||||
model: "veo-3.1",
|
||||
model: "sora-2",
|
||||
provider: "adobe-firefly",
|
||||
body: {},
|
||||
credentials: { apiKey: "aaa.bbb.ccc" },
|
||||
@@ -854,6 +809,36 @@ test("cookie exchange rejects guest IMS tokens", async () => {
|
||||
);
|
||||
});
|
||||
|
||||
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 }),
|
||||
/fresh successful generate-async/i
|
||||
);
|
||||
});
|
||||
|
||||
test("isAdobeTransientSubmitError detects 408 system under load", () => {
|
||||
assert.equal(
|
||||
isAdobeTransientSubmitError(
|
||||
|
||||
Reference in New Issue
Block a user