mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-03 13:52:09 +03:00
* feat(media): Adobe Firefly image + video generation provider
Add unofficial adobe-firefly media provider with full OpenAI-compatible
image and video generation: Nano Banana / GPT Image families, Sora 2,
Veo 3.1 (standard/fast/reference), and Kling 3.0.
Supports browser session cookies (auto IMS token exchange) or direct
IMS access tokens, async submit-and-poll against Firefly 3P endpoints,
aspect-ratio and resolution controls, and multi-account web-session UX.
Chat completions are intentionally rejected (media-only surface).
Includes unit coverage for registry wiring, payload builders, auth
resolution, and mocked generate happy-paths.
* fix(adobe-firefly): clio auth, discovery fallback, credits balance
Root-cause 401 invalid token against live firefly.adobe.com captures:
generate/discovery use x-api-key + IMS client_id clio-playground-web
(not projectx_webapp). Align headers/origin, dual cookie to IMS exchange
(clio first, Express fallback), BKS poll rewrite for /jobs/result.
Models: parse POST /v2/models/discovery + static fallback catalog from
adobe/get_models.txt; expand image/video registries.
Limits: GET firefly.adobe.io/v1/credits/balance (SunbreakWebUI1) with
total/remaining + free/plan detail quotas. Clarify cookie vs JWT UX.
Unit tests: 27/27 pass.
* fix(adobe-firefly): reject guest tokens from page-only cookies
Live repro with firefly.adobe.com Cookie export: IMS check with
guest_allowed=true returns account_type=guest (no AdobeID). That token
fails generate (401 invalid token) and credits/balance (403
ErrMismatchOauthToken). guest_allowed=false needs adobelogin.com IMS
session cookies which are not present in a page-only Cookie paste.
- Detect/reject guest JWTs; clear error tells user to paste Bearer JWT
- Prefer user JWT from HAR/mixed paste; improve credential extraction
- Update web-cookie + credential UX to recommend Authorization Bearer
Unit tests 29/29.
* fix(adobe-firefly): production auth, Limits, and 408 load handling
Live validation against firefly.adobe.com + packaged VibeProxy:
Auth / credentials
- Prefer IMS user JWT (Bearer from firefly-3p); reject guest tokens from
page-only cookies with an actionable error
- Extract JWT from Bearer, access_token=, IMS sessionStorage tokenValue,
and mixed HAR pastes; prefer non-guest tokens
- Strip JWT from Cookie header (undici Headers.append crash on mixed paste)
- Keep sherlockToken → x-arp-session-id + sanitized Cookie for generate
Limits
- credits/balance → Record quotas (firefly_total / free / plan) so
providerLimits caches them (arrays were ignored)
- Allowlist adobe-firefly + firefly in USAGE_SUPPORTED + APIKEY limits
- Live: 10000 plan credits parsed end-to-end after refresh
Generate
- Browser-shaped gpt-image body (size auto, no extra top-level size)
- Exponential 408 "system under load" retries (8 attempts) with clear
client message that 408 is Adobe capacity, not invalid token
- Live: generate returns proper 408 under load; balance/models stay 200
Tests: adobe-firefly unit suite 33/33 pass.
* fix(adobe-firefly): match live capture headers; add gpt-image-2
- Do not send firefly.adobe.com Cookie to firefly-3p (wrong-origin; soft 408)
- Lift sherlockToken only into x-arp-session-id
- Poll headers match status_check.txt (Bearer + accept, no x-api-key)
- Catalog gpt-image-2 alias → upstream modelVersion "2" (GPT Image 2)
- Shorter 408 retry budget so clients fail fast with clear message
- Unit suite 34/34
* fix(adobe-firefly): always send x-arp-session-id on generate (fixes 408)
Root cause of Bearer JWT → HTTP 408 colligo "system under load":
submit only set x-arp-session-id when sherlockToken was present in a
cookie paste. JWT-only credentials never sent the header, and Adobe
soft-blocks those requests with instant 408 (x-colligo-timeout:0.0).
A/B against a real user IMS token:
- det nonce + synthetic ARP → 200
- random nonce + synthetic ARP → 200
- det nonce without ARP → 408
Match adobe2api / GPT2Image-Pro:
- buildAdobeSubmitNonce = sha256(user_id + prompt[:256])
- buildAdobeArpSessionId = base64({sid, ftr}) synthetic session
- buildAdobeSubmitHeaders always sets both headers
Live adobeFireflyGenerateImage end-to-end: submit + poll → S3 presigned URL.
* fix(adobe-firefly): drop literal cred fallbacks + type-clean tests
Addresses pre-merge review feedback on #8006:
- Removes the `|| "literal"` fallback after resolvePublicCred() in
adobeFireflyApiKey()/adobeFireflyExpressClientId()/adobeFireflyBalanceApiKey()
(open-sse/services/adobeFireflyClient.ts). resolvePublicCred() already
always returns the decoded embedded default, so the literal fallback
was dead code that reproduced the exact env-or-literal anti-pattern
docs/security/PUBLIC_CREDS.md documents as BAD (Hard Rule #11).
- Replaces the 11 `@typescript-eslint/no-explicit-any` casts in
tests/unit/adobe-firefly.test.ts with concrete types
(Record<string, unknown>, Headers, Error-narrowing on the
assert.rejects predicate), matching the pattern already used
elsewhere in this suite. `no-explicit-any` is a hard ESLint error
under tests/ in this repo.
- Freezes file-size baseline entries for the new
open-sse/services/adobeFireflyClient.ts (1958 LOC, new-file cap 800,
mirrors the qoderCli.ts precedent for a legitimately large new
provider client), open-sse/config/imageRegistry.ts (800->821, new
adobe-firefly registry entry) and the +3 LOC growth in
src/lib/usage/providerLimits.ts (1000->1003).
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
---------
Co-authored-by: diegosouzapw <8016841+diegosouzapw@users.noreply.github.com>
Co-authored-by: Diego Rodrigues de Sa e Souza <diegosouza.pw@gmail.com>
Co-authored-by: artickc <artickc@users.noreply.github.com>
374 lines
15 KiB
TypeScript
374 lines
15 KiB
TypeScript
import { WEB_COOKIE_PROVIDERS } from "@/shared/constants/providers";
|
|
|
|
export type WebSessionCredentialRequirement =
|
|
| {
|
|
kind: "cookie" | "token";
|
|
credentialName: string;
|
|
placeholder: string;
|
|
acceptsFullCookieHeader: boolean;
|
|
storageKeys: readonly string[];
|
|
/**
|
|
* #5465 — Optional i18n key for a provider-specific credential hint that
|
|
* REPLACES the generic "Required cookie: {credential}…" copy. Use when the
|
|
* generic template is confusing (e.g. t3.chat needs a localStorage value
|
|
* AND the Cookie header, so the one-line cookie hint reads circular).
|
|
*/
|
|
hintKey?: string;
|
|
hintFallback?: string;
|
|
}
|
|
| {
|
|
kind: "none";
|
|
credentialName: "";
|
|
placeholder: "";
|
|
acceptsFullCookieHeader: false;
|
|
storageKeys: readonly [];
|
|
};
|
|
|
|
export const WEB_SESSION_CREDENTIAL_REQUIREMENTS = {
|
|
"zenmux-free": {
|
|
kind: "cookie",
|
|
credentialName: "Cookie header (full)",
|
|
placeholder: "paste the full Cookie header from zenmux.ai",
|
|
acceptsFullCookieHeader: true,
|
|
storageKeys: ["cookie"],
|
|
},
|
|
"chatgpt-web": {
|
|
kind: "cookie",
|
|
credentialName: "__Secure-next-auth.session-token",
|
|
placeholder: "__Secure-next-auth.session-token=...",
|
|
acceptsFullCookieHeader: true,
|
|
storageKeys: ["cookie", "sessionToken", "session-token", "__Secure-next-auth.session-token"],
|
|
},
|
|
"grok-web": {
|
|
kind: "cookie",
|
|
credentialName: "sso + sso-rw",
|
|
placeholder: "sso=...; sso-rw=...",
|
|
acceptsFullCookieHeader: true,
|
|
storageKeys: ["cookie", "sso", "sso-rw"],
|
|
// #7567 — grok.com's cf_clearance cookie is pinned to IP + User-Agent + TLS
|
|
// fingerprint of the browser that earned it, so pasting it from a different
|
|
// machine/IP causes a 403 that is actually correct Cloudflare behavior. Point
|
|
// users at the Custom User-Agent field under Advanced Settings + same IP/proxy,
|
|
// instead of the generic (and here misleading) single-cookie hint.
|
|
hintKey: "grokWebCookieHint",
|
|
hintFallback:
|
|
"grok.com's cf_clearance cookie is pinned to the IP, User-Agent, and TLS fingerprint of the browser where you copied it — pasting it from a different machine/IP causes a 403. Paste sso and sso-rw here, then open Advanced Settings and fill Custom User-Agent with the EXACT User-Agent string of that same browser, and use the same IP/proxy for this connection.",
|
|
},
|
|
"gemini-web": {
|
|
kind: "cookie",
|
|
credentialName: "__Secure-1PSID (optional: __Secure-1PSIDTS)",
|
|
placeholder: "__Secure-1PSID=...; __Secure-1PSIDTS=...",
|
|
acceptsFullCookieHeader: true,
|
|
storageKeys: ["cookie", "__Secure-1PSID", "__Secure-1PSIDTS"],
|
|
},
|
|
"notion-web": {
|
|
kind: "cookie",
|
|
credentialName: "token_v2 (optional: space_id, notion_browser_id)",
|
|
placeholder: "token_v2=...; space_id=...; notion_browser_id=...",
|
|
acceptsFullCookieHeader: true,
|
|
storageKeys: ["cookie", "token_v2", "space_id", "notion_browser_id"],
|
|
},
|
|
"gemini-business": {
|
|
kind: "cookie",
|
|
credentialName: "__Secure-1PSID (optional: __Secure-1PSIDTS)",
|
|
placeholder: "__Secure-1PSID=...; __Secure-1PSIDTS=... (from business.gemini.google)",
|
|
acceptsFullCookieHeader: true,
|
|
storageKeys: ["cookie", "__Secure-1PSID", "__Secure-1PSIDTS"],
|
|
},
|
|
"perplexity-web": {
|
|
kind: "cookie",
|
|
credentialName: "__Secure-next-auth.session-token",
|
|
placeholder: "__Secure-next-auth.session-token=...",
|
|
acceptsFullCookieHeader: true,
|
|
storageKeys: ["cookie", "sessionToken", "session-token", "__Secure-next-auth.session-token"],
|
|
},
|
|
hyperagent: {
|
|
kind: "cookie",
|
|
credentialName: "Session Cookie",
|
|
placeholder: "Paste full Cookie header from hyperagent.com",
|
|
acceptsFullCookieHeader: true,
|
|
storageKeys: ["cookie", "sessionCookie", "authCookie"],
|
|
},
|
|
"blackbox-web": {
|
|
kind: "cookie",
|
|
credentialName: "__Secure-authjs.session-token",
|
|
placeholder: "__Secure-authjs.session-token=...; other=value",
|
|
acceptsFullCookieHeader: true,
|
|
storageKeys: ["cookie", "sessionToken", "__Secure-authjs.session-token"],
|
|
},
|
|
"muse-spark-web": {
|
|
kind: "cookie",
|
|
credentialName: "abra_sess",
|
|
placeholder: "abra_sess=...; other=value",
|
|
acceptsFullCookieHeader: true,
|
|
storageKeys: ["cookie", "abra_sess"],
|
|
},
|
|
"hailuo-web": {
|
|
kind: "token",
|
|
credentialName: "_token",
|
|
placeholder: '_token=... (hailuo.ai → DevTools → Local Storage → "_token")',
|
|
acceptsFullCookieHeader: false,
|
|
storageKeys: ["token", "_token"],
|
|
},
|
|
"claude-web": {
|
|
kind: "cookie",
|
|
credentialName: "sessionKey",
|
|
placeholder: "sessionKey=... or full Cookie header from claude.ai",
|
|
acceptsFullCookieHeader: true,
|
|
storageKeys: ["cookie", "sessionKey"],
|
|
},
|
|
"deepseek-web": {
|
|
kind: "token",
|
|
credentialName: "userToken",
|
|
placeholder: "userToken=... or paste raw userToken",
|
|
acceptsFullCookieHeader: false,
|
|
storageKeys: ["token", "userToken"],
|
|
},
|
|
"copilot-web": {
|
|
kind: "token",
|
|
credentialName: "access_token",
|
|
placeholder: "access_token=... or a DevTools HAR export",
|
|
acceptsFullCookieHeader: false,
|
|
storageKeys: ["token", "access_token", "accessToken"],
|
|
},
|
|
"microsoft-designer-web": {
|
|
kind: "token",
|
|
credentialName: "access_token",
|
|
placeholder: "access_token=... (Authorization: Bearer header from the DallE.ashx request)",
|
|
acceptsFullCookieHeader: false,
|
|
storageKeys: ["token", "access_token", "accessToken"],
|
|
},
|
|
"copilot-m365-web": {
|
|
kind: "token",
|
|
credentialName: "access_token + chathubPath",
|
|
placeholder: "access_token=...; chathubPath=redacted",
|
|
acceptsFullCookieHeader: false,
|
|
storageKeys: ["token", "access_token", "accessToken", "chathubPath", "userTenant"],
|
|
},
|
|
"t3-web": {
|
|
kind: "cookie",
|
|
credentialName: "convex-session-id + Cookie header",
|
|
placeholder: "convex-session-id=abc123...; Cookie: ...",
|
|
acceptsFullCookieHeader: true,
|
|
storageKeys: ["cookie", "convex-session-id", "convexSessionId"],
|
|
// #5465 — the generic cookie hint reads circular for t3.chat (needs a
|
|
// localStorage value AND the Cookie header); use the step-by-step DevTools
|
|
// copy that already ships translated in every locale.
|
|
hintKey: "t3ChatWebCookieHint",
|
|
},
|
|
"adapta-web": {
|
|
kind: "cookie",
|
|
credentialName: "__client",
|
|
placeholder: "__client=... or full Cookie header from agent.adapta.one",
|
|
acceptsFullCookieHeader: true,
|
|
storageKeys: ["cookie", "__client"],
|
|
},
|
|
"inner-ai": {
|
|
kind: "cookie",
|
|
credentialName: "token + email",
|
|
placeholder: "token_value user@example.com",
|
|
acceptsFullCookieHeader: false,
|
|
storageKeys: ["token", "cookie", "email"],
|
|
},
|
|
huggingchat: {
|
|
kind: "cookie",
|
|
credentialName: "full Cookie header (hf-chat + token)",
|
|
placeholder:
|
|
"hf-chat=...; token=...; aws-waf-token=... (full Cookie header from huggingface.co)",
|
|
acceptsFullCookieHeader: true,
|
|
storageKeys: ["cookie", "hf-chat"],
|
|
},
|
|
"yuanbao-web": {
|
|
kind: "cookie",
|
|
credentialName: "full Cookie header (hy_user + hy_token)",
|
|
placeholder: "hy_user=...; hy_token=... (full Cookie header from yuanbao.tencent.com)",
|
|
acceptsFullCookieHeader: true,
|
|
storageKeys: ["cookie", "hy_user", "hy_token"],
|
|
},
|
|
"poe-web": {
|
|
kind: "cookie",
|
|
credentialName: "p-b",
|
|
placeholder: "p-b=... or full Cookie header from poe.com",
|
|
acceptsFullCookieHeader: true,
|
|
storageKeys: ["cookie", "p-b"],
|
|
},
|
|
"venice-web": {
|
|
kind: "cookie",
|
|
credentialName: "session",
|
|
placeholder: "session=... or full Cookie header from venice.ai",
|
|
acceptsFullCookieHeader: true,
|
|
storageKeys: ["cookie", "session"],
|
|
},
|
|
"v0-vercel-web": {
|
|
kind: "cookie",
|
|
credentialName: "__vercel_session",
|
|
placeholder: "__vercel_session=... or full Cookie header from v0.dev",
|
|
acceptsFullCookieHeader: true,
|
|
storageKeys: ["cookie", "__vercel_session"],
|
|
},
|
|
"kimi-web": {
|
|
kind: "token",
|
|
credentialName: "access_token",
|
|
placeholder: "access_token from www.kimi.com localStorage",
|
|
acceptsFullCookieHeader: true,
|
|
storageKeys: ["token", "access_token", "accessToken", "cookie", "kimi-auth"],
|
|
},
|
|
"doubao-web": {
|
|
kind: "cookie",
|
|
credentialName: "full Cookie header (sessionid + ttwid + s_v_web_id)",
|
|
placeholder:
|
|
"sessionid=...; ttwid=...; s_v_web_id=... (or fp=verify_... fallback from www.dola.com)",
|
|
acceptsFullCookieHeader: true,
|
|
storageKeys: ["cookie", "sessionid", "ttwid", "s_v_web_id", "fp"],
|
|
},
|
|
"qwen-web": {
|
|
kind: "cookie",
|
|
credentialName: "full Cookie header (must include cna, ssxmod_itna, token)",
|
|
placeholder:
|
|
"cna=...; token=...; ssxmod_itna=...; ssxmod_itna2=... (full Cookie header from chat.qwen.ai)",
|
|
acceptsFullCookieHeader: true,
|
|
storageKeys: ["cookie", "token", "ssxmod_itna", "ssxmod_itna2", "cna", "tongyi_sso_ticket"],
|
|
},
|
|
"duckduckgo-web": {
|
|
kind: "cookie",
|
|
credentialName: "duckai",
|
|
placeholder: "duckai=... or full Cookie header from duckduckgo.com",
|
|
acceptsFullCookieHeader: true,
|
|
storageKeys: ["cookie", "duckai"],
|
|
},
|
|
"t3-chat-web": {
|
|
kind: "token",
|
|
credentialName: "token",
|
|
placeholder: "Paste your T3 Chat token from t3.chat (Local Storage → token)",
|
|
acceptsFullCookieHeader: false,
|
|
storageKeys: ["token"],
|
|
},
|
|
"chatglm-web": {
|
|
kind: "cookie",
|
|
credentialName: "chatglm_session",
|
|
placeholder: "chatglm_session=... or full Cookie header from chatglm.cn",
|
|
acceptsFullCookieHeader: true,
|
|
storageKeys: ["cookie", "chatglm_session"],
|
|
},
|
|
"xiaomimimo-web": {
|
|
kind: "cookie",
|
|
credentialName: "session",
|
|
placeholder: "session=... or full Cookie header from aistudio.xiaomimimo.com",
|
|
acceptsFullCookieHeader: true,
|
|
storageKeys: ["cookie", "session"],
|
|
},
|
|
"manus-web": {
|
|
kind: "cookie",
|
|
credentialName: "manus_session",
|
|
placeholder: "manus_session=... or full Cookie header from manus.im",
|
|
acceptsFullCookieHeader: true,
|
|
storageKeys: ["cookie", "manus_session"],
|
|
},
|
|
"zai-web": {
|
|
kind: "cookie",
|
|
credentialName: "token",
|
|
placeholder: "token=... or full Cookie header from chat.z.ai",
|
|
acceptsFullCookieHeader: true,
|
|
storageKeys: ["cookie", "token"],
|
|
},
|
|
lmarena: {
|
|
kind: "cookie",
|
|
// arena.ai's auth cookie is `arena-auth-prod-v1` (the legacy hint said `session`,
|
|
// which never matched the real cookie name and confused users). #3810
|
|
//
|
|
// #4271: LMArena migrated to Supabase SSR chunked cookies — the single
|
|
// `arena-auth-prod-v1` cookie is now empty and the session is split across
|
|
// `arena-auth-prod-v1.0`, `arena-auth-prod-v1.1`, … Users must paste the FULL
|
|
// Cookie header so the executor can reconstruct the single cookie from chunks.
|
|
credentialName: "full Cookie header (arena-auth-prod-v1.0 + arena-auth-prod-v1.1)",
|
|
placeholder:
|
|
"arena-auth-prod-v1.0=...; arena-auth-prod-v1.1=...; other=value (full Cookie header from arena.ai)",
|
|
acceptsFullCookieHeader: true,
|
|
storageKeys: [
|
|
"cookie",
|
|
"arena-auth-prod-v1",
|
|
"arena-auth-prod-v1.0",
|
|
"arena-auth-prod-v1.1",
|
|
"session",
|
|
],
|
|
hintKey: "lmarenaWebCookieHint",
|
|
hintFallback:
|
|
"Open arena.ai, sign in, then copy the full Cookie header from a Network request. Include arena-auth-prod-v1.0 and arena-auth-prod-v1.1 (and further chunks if present), preferably with cf_clearance. Do not paste only the empty arena-auth-prod-v1 cookie. Optional: providerSpecificData.recaptchaV3Token if create-evaluation still returns 403.",
|
|
},
|
|
"promptql": {
|
|
kind: "token",
|
|
credentialName: "Bearer JWT (optional: projectId, session Cookie)",
|
|
placeholder: "eyJ... (Authorization Bearer from prompt.ql.app)",
|
|
acceptsFullCookieHeader: false,
|
|
storageKeys: ["token", "jwt", "apiKey", "projectId", "project_id", "cookie"],
|
|
},
|
|
"adobe-firefly": {
|
|
// Prefer IMS access_token JWT (Bearer). Cookie from firefly.adobe.com alone
|
|
// only mints a guest IMS token. Kind stays "cookie" for multi-account UX;
|
|
// resolveAdobeAccessToken auto-detects JWT vs cookie and rejects guests.
|
|
kind: "cookie",
|
|
credentialName: "IMS access_token JWT (recommended) or multi-domain Cookie",
|
|
placeholder:
|
|
"Paste eyJ… JWT from Authorization: Bearer on firefly-3p generate request (not page Cookie alone)",
|
|
acceptsFullCookieHeader: true,
|
|
storageKeys: ["cookie", "token", "access_token", "accessToken"],
|
|
},
|
|
} satisfies Record<keyof typeof WEB_COOKIE_PROVIDERS, WebSessionCredentialRequirement>;
|
|
|
|
export function getWebSessionCredentialRequirement(
|
|
providerId: unknown
|
|
): WebSessionCredentialRequirement | null {
|
|
if (typeof providerId !== "string") return null;
|
|
return (
|
|
WEB_SESSION_CREDENTIAL_REQUIREMENTS[
|
|
providerId as keyof typeof WEB_SESSION_CREDENTIAL_REQUIREMENTS
|
|
] ?? null
|
|
);
|
|
}
|
|
|
|
export function requiresWebSessionCredential(providerId: unknown): boolean {
|
|
const requirement = getWebSessionCredentialRequirement(providerId);
|
|
return !!requirement && requirement.kind !== "none";
|
|
}
|
|
|
|
function hasNonEmptyString(value: unknown): value is string {
|
|
return typeof value === "string" && value.trim().length > 0;
|
|
}
|
|
|
|
export function hasUsableWebSessionCredential(
|
|
providerId: unknown,
|
|
providerSpecificData: unknown
|
|
): boolean {
|
|
const requirement = getWebSessionCredentialRequirement(providerId);
|
|
if (!requirement || requirement.kind === "none") return false;
|
|
if (!providerSpecificData || typeof providerSpecificData !== "object") return false;
|
|
|
|
const data = providerSpecificData as Record<string, unknown>;
|
|
return requirement.storageKeys.some((key) => hasNonEmptyString(data[key]));
|
|
}
|
|
|
|
/**
|
|
* Resolve the value that a web-session import must store in the connection's
|
|
* `apiKey` column.
|
|
*
|
|
* `token`-kind providers (deepseek-web, copilot-web, copilot-m365-web,
|
|
* t3-chat-web, …) are authenticated from `apiKey`: both the connection
|
|
* validator (`validateDeepSeekWebProvider({ apiKey })`) and the executor
|
|
* (`extractUserToken` → `credentials.apiKey`) read the token there — never from
|
|
* `providerSpecificData`. The bulk web-session import used to leave `apiKey`
|
|
* null and stash the token only in `providerSpecificData`, so imported token-kind
|
|
* connections were never recognized. Return the credential for token-kind so the
|
|
* import stores it where those readers look.
|
|
*
|
|
* `cookie`-kind providers keep `apiKey` null — their executors read the full
|
|
* cookie from `providerSpecificData.cookie`.
|
|
*/
|
|
export function resolveWebSessionImportApiKey(
|
|
requirement: WebSessionCredentialRequirement | null,
|
|
credential: string
|
|
): string | null {
|
|
if (!requirement || requirement.kind !== "token") return null;
|
|
const trimmed = typeof credential === "string" ? credential.trim() : "";
|
|
return trimmed.length > 0 ? trimmed : null;
|
|
}
|