Files
OmniRoute/open-sse/services/adobeFireflyCredentials.ts
MumuTW f3d9279b44 refactor(quality): split adobeFireflyClient.ts (2,952 lines) into nine leaf modules (#11728)
Mechanical split of the 2952-line adobeFireflyClient.ts into nine leaf modules, each under the
1000-line cap — follows the existing Adobe Firefly family decomposition pattern
(adobeFireflySecurity.ts, ModelSnapshot, References, Upscale, Models, Session, BrowserLogin).
No behavior change. Thanks for the cleanup!
2026-08-28 09:30:57 -03:00

206 lines
7.8 KiB
TypeScript

/**
* Adobe Firefly — credential shapes: IMS token / cookie-blob recognition and extraction.
*
* Split out of adobeFireflyClient.ts.
*/
import { resolvePublicCred } from "../utils/publicCreds.ts";
import {
decodeAdobeJwtPayload,
findAllAdobeJwts,
isExactAdobeJwt,
stripAdobeJwts,
} from "./adobeFireflySecurity.ts";
export class AdobeFireflyError extends Error {
status: number;
code?: string;
constructor(message: string, status = 502, code?: string) {
super(message);
this.name = "AdobeFireflyError";
this.status = status;
this.code = code;
}
}
/** Public x-api-key + primary IMS client_id for firefly.adobe.com (`clio-playground-web`). */
export function adobeFireflyApiKey(): string {
return resolvePublicCred("adobe_firefly_api_key", "ADOBE_FIREFLY_API_KEY");
}
/** Express IMS client_id fallback for cookie exchange (`projectx_webapp`). */
export function adobeFireflyExpressClientId(): string {
return resolvePublicCred("adobe_firefly_express_client_id", "ADOBE_FIREFLY_EXPRESS_CLIENT_ID");
}
/** Public x-api-key for GET firefly.adobe.io/v1/credits/balance (`SunbreakWebUI1`). */
export function adobeFireflyBalanceApiKey(): string {
return resolvePublicCred("adobe_firefly_balance_api_key", "ADOBE_FIREFLY_BALANCE_API_KEY");
}
/** Decode IMS JWT payload (no signature verification — client-side claim read only). */
/** AdobeID subject for x-account-id on balance / account_cluster calls. */
export function extractAdobeAccountIdFromToken(token: string): string {
const payload = decodeAdobeJwtPayload(token);
if (!payload) return "";
const candidates = [payload.user_id, payload.aa_id, payload.sub, payload.id];
for (const c of candidates) {
if (typeof c === "string" && c.includes("@")) return c.trim();
}
for (const c of candidates) {
if (typeof c === "string" && c.trim()) return c.trim();
}
return "";
}
export function looksLikeAdobeJwt(value: string): boolean {
const raw = value.trim();
if (!raw) return false;
// Avoid treating cookie blobs that happen to have two dots as JWT.
if (raw.includes(";") || (raw.includes("=") && !raw.startsWith("eyJ"))) return false;
// Allow a single space after optional Bearer prefix (stripped earlier).
if (/\s/.test(raw) && !/^bearer\s+/i.test(raw)) return false;
const token = raw.replace(/^bearer\s+/i, "").trim();
const parts = token.split(".");
if (parts.length !== 3) return false;
// Adobe IMS access tokens are sizable; reject tiny accidental 3-segment strings.
if (token.length < 80) return false;
return parts.every((p) => p.length > 0 && /^[A-Za-z0-9_-]+$/.test(p));
}
/**
* True when IMS issued a guest token (no signed-in AdobeID).
* Live repro: firefly.adobe.com page cookies alone → account_type=guest → generate 401 /
* balance 403 ErrMismatchOauthToken.
*/
export function isAdobeGuestAccessToken(token: string): boolean {
const payload = decodeAdobeJwtPayload(token);
if (!payload) return false;
const userId = typeof payload.user_id === "string" ? payload.user_id : "";
const aaId = typeof payload.aa_id === "string" ? payload.aa_id : "";
const type = typeof payload.type === "string" ? payload.type.toLowerCase() : "";
// Authenticated Firefly tokens always carry an @AdobeID (or similar) subject.
if (userId.includes("@AdobeID") || aaId.includes("@AdobeID")) return false;
if (userId.includes("@GuestID") || aaId.includes("@GuestID")) return true;
if (type === "guest" || type.includes("guest")) return true;
// Guest tokens from ims/check often omit type/user_id entirely.
if (!userId && !aaId) return true;
return false;
}
export function isAdobeUserAccessToken(token: string): boolean {
return looksLikeAdobeJwt(token) && !isAdobeGuestAccessToken(token);
}
/**
* Pull an IMS JWT out of free-form paste: raw JWT, Bearer …, access_token=…,
* IMS sessionStorage JSON (`tokenValue`), multi-line Network/HAR dumps.
* Prefer the longest user (non-guest) eyJ… JWT found.
*/
export function extractAdobeCredentialToken(raw: string): string {
const value = String(raw || "").trim();
if (!value) return "";
if (/^bearer\s+/i.test(value)) {
const bare =
value
.replace(/^bearer\s+/i, "")
.trim()
.split(/\s+/)[0] || "";
if (looksLikeAdobeJwt(bare)) return bare;
}
// access_token=... in cookie-ish or form paste
const accessMatch = value.match(/(?:^|[;\s&])access_token=([^;\s&]+)/i);
if (accessMatch?.[1]) {
const t = decodeURIComponent(accessMatch[1].trim());
if (looksLikeAdobeJwt(t)) return t;
}
// IMS sessionStorage / localStorage JSON: "tokenValue":"eyJ..."
const tokenValueMatch = value.match(/"tokenValue"\s*:\s*"(eyJ[^"]+)"/i);
if (tokenValueMatch?.[1] && looksLikeAdobeJwt(tokenValueMatch[1])) {
return tokenValueMatch[1];
}
// Authorization: Bearer eyJ...
const authMatch = value.match(
/Authorization\s*:\s*Bearer\s+([A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)/i
);
if (authMatch?.[1] && looksLikeAdobeJwt(authMatch[1])) return authMatch[1];
// Any eyJ… JWT in the blob (HAR / multi-line). Prefer user AdobeID tokens.
const jwtMatches = findAllAdobeJwts(value);
if (jwtMatches && jwtMatches.length > 0) {
const sorted = [...jwtMatches].sort((a, b) => b.length - a.length);
const user = sorted.find((t) => looksLikeAdobeJwt(t) && isAdobeUserAccessToken(t));
if (user) return user;
const best = sorted[0];
if (looksLikeAdobeJwt(best)) return best;
}
// Pure JWT
if (looksLikeAdobeJwt(value)) return value.replace(/^bearer\s+/i, "").trim();
// Cookie / other blob unchanged for IMS exchange
return value;
}
/**
* True when the paste still looks like a Cookie header (not a bare JWT).
* Used to attach Cookie + sherlockToken → x-arp-session-id on generate.
*/
export function looksLikeAdobeCookieBlob(value: string): boolean {
const raw = String(value || "").trim();
if (!raw || looksLikeAdobeJwt(raw)) return false;
if (raw.includes(";") && raw.includes("=")) return true;
if (/(?:^|[;\s])(?:aux_sid|ff_session|sherlockToken|forterToken|arkose)=/i.test(raw)) {
return true;
}
return false;
}
/**
* Strip JWTs / Authorization lines from a mixed paste so only Cookie pairs remain.
* Undici Headers.append rejects multi-line Cookie values (throws Headers.append: "eyJ…").
*/
export function extractAdobeCookieHeader(raw: string): string {
const value = String(raw || "").trim();
if (!value) return "";
if (looksLikeAdobeJwt(value)) return "";
// Drop pure JWT lines and Authorization: Bearer lines
const cleaned = value
.split(/[\r\n]+/)
.map((line) => line.trim())
.filter((line) => {
if (!line) return false;
if (/^authorization\s*:/i.test(line)) return false;
if (/^bearer\s+/i.test(line)) return false;
if (looksLikeAdobeJwt(line)) return false;
// Drop standalone eyJ… segments
if (isExactAdobeJwt(line)) return false;
return true;
})
.join("; ");
// Also strip inline eyJ JWT tokens that may sit inside a cookie string
const noJwt = stripAdobeJwts(cleaned)
.replace(/;\s*;/g, ";")
.replace(/^;\s*|\s*;$/g, "")
.trim();
if (!noJwt || !looksLikeAdobeCookieBlob(noJwt)) return "";
// Final safety: Cookie header must be single-line
return noJwt.replace(/[\r\n]+/g, "; ").trim();
}
export const GUEST_COOKIE_HELP =
"Firefly page cookies alone only mint a GUEST IMS token (no AdobeID) — generate returns 401 and Limits 403. " +
"Fix: open firefly.adobe.com signed-in → F12 → Network → click a request to firefly-3p.ff.adobe.io " +
"(generate-async or models/discovery) → Request Headers → Authorization → copy the token AFTER 'Bearer ' " +
"(starts with eyJ…). Paste that JWT as the credential. " +
"Cookie-only works only if you also export IMS session cookies from adobelogin.com / adobeid-na1 " +
"(Cookie-Editor → export all Adobe domains); firefly.adobe.com cookies by themselves are not enough.";