fix(adobe-firefly): harden CDP warm, risk session, and browser sign-in

Stop colligo 408 thrash from stale Forter and frozen Google login during
Sign in with browser:

- CDP warm: clear Firefly origin storage + risk cookies (keep SSO); require
  forter age under 10 minutes on loop and timeout paths; dual CDP queues;
  await Runtime.runIfWaitingForDebugger; profile-lock launch retries
- Session: connectionId fingerprint; write-back JWT+Cookie; warm-fail
  cooldown; fail closed risk_session_stale when forter is known-stale
- Client: submit gate around generate-async; max 2 attempts when forter
  known-stale; poll 401 one refresh; pass sessionBrowserKey through handlers
- Login route: pure system Chrome/Edge CDP only; camelCase credential persist
- Unit: browser-login + firefly suites green (60)
This commit is contained in:
artickc
2026-08-06 03:53:43 +03:00
committed by diegosouzapw
parent 6c02c0c97d
commit b1619959a5
8 changed files with 2621 additions and 447 deletions

View File

@@ -51,7 +51,17 @@ export async function handleAdobeFireflyImageGeneration({
images?: unknown;
[key: string]: unknown;
};
credentials: { apiKey?: string; accessToken?: string };
credentials: {
apiKey?: string;
accessToken?: string;
connectionId?: string;
providerSpecificData?: {
cookie?: unknown;
access_token?: unknown;
accessToken?: unknown;
browserSessionKey?: unknown;
} | null;
};
log?: { info?: (...args: unknown[]) => void; error?: (...args: unknown[]) => void };
fetchImpl?: typeof fetch;
}) {
@@ -88,10 +98,7 @@ export async function handleAdobeFireflyImageGeneration({
// 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;
const maxRefs = resolvedId.includes("nano-banana") || resolvedId.includes("gpt-image") ? 4 : 2;
const sourceImageIds = await resolveAdobeSourceImageIds({
accessToken,
@@ -119,12 +126,12 @@ export async function handleAdobeFireflyImageGeneration({
aspectRatio: body.aspect_ratio ?? body.aspectRatio ?? body.size,
quality: body.quality,
seed: Number.isFinite(seed as number) ? (seed as number) : undefined,
negativePrompt:
typeof body.negative_prompt === "string" ? body.negative_prompt : undefined,
negativePrompt: typeof body.negative_prompt === "string" ? body.negative_prompt : undefined,
sourceImageIds: sourceImageIds.length ? sourceImageIds : undefined,
sessionCookie,
arpSessionId,
sessionFingerprint: session.fingerprint,
sessionBrowserKey: session.browserSessionKey,
timeoutMs,
fetchImpl,
log,

View File

@@ -31,7 +31,17 @@ export async function handleAdobeFireflyVideoGeneration({
provider: string;
providerConfig?: { baseUrl?: string };
body: Record<string, unknown>;
credentials?: { apiKey?: string; accessToken?: string } | null;
credentials?: {
apiKey?: string;
accessToken?: string;
connectionId?: string;
providerSpecificData?: {
cookie?: unknown;
access_token?: unknown;
accessToken?: unknown;
browserSessionKey?: unknown;
} | null;
} | null;
log?: { info?: (...args: unknown[]) => void; error?: (...args: unknown[]) => void };
fetchImpl?: typeof fetch;
}) {
@@ -104,6 +114,7 @@ export async function handleAdobeFireflyVideoGeneration({
sessionCookie,
arpSessionId,
sessionFingerprint: session.fingerprint,
sessionBrowserKey: session.browserSessionKey,
timeoutMs,
fetchImpl,
log,

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1751,7 +1751,7 @@ export function formatAdobeSystemUnderLoadError(
`Adobe Firefly ${kind} generation failed (HTTP 408 "system under load", after ${attempts} attempt` +
`${attempts === 1 ? "" : "s"}). JWT was accepted for balance/discovery but colligo rejected the risk session ` +
`(Forter/Arkose stale or rate-limited). The app spaces submits, sticks to the last working x-arp-session-id, ` +
`and on 408 auto-warms a fresh Forter/ARP via off-screen headed Chrome (not headless — colligo rejects that). ` +
`and on 408 auto-warms Forter/ARP via off-screen Chrome CDP (true headless is rejected by colligo — set ADOBE_FIREFLY_CHROME_HEADLESS=1 only for debug). ` +
`Paste the full firefly.adobe.com Cookie once with the JWT so recovery can run. If it still fails after that, ` +
`open firefly.adobe.com, generate one image in-browser, then paste a FRESH multi-line credential (JWT + Cookie) once.`
);
@@ -2322,6 +2322,9 @@ async function pollAdobeJob(opts: {
kind: "image" | "video";
timeoutMs: number;
pollIntervalMs?: number;
/** Optional session cookie so a mid-poll 401 can renew JWT once via CDP. */
sessionCookie?: string;
sessionFingerprint?: string;
fetchImpl?: typeof fetch;
log?: {
info?: (...args: unknown[]) => void;
@@ -2334,12 +2337,14 @@ async function pollAdobeJob(opts: {
opts.pollIntervalMs && opts.pollIntervalMs > 0 ? opts.pollIntervalMs : DEFAULT_POLL_INTERVAL_MS;
let attempt = 0;
let latest: unknown = {};
let accessToken = opts.accessToken;
let authRefreshAttempted = false;
while (Date.now() < deadline) {
attempt += 1;
const pollResp = await fetchImpl(opts.pollUrl, {
method: "GET",
headers: buildAdobePollHeaders(opts.accessToken),
headers: buildAdobePollHeaders(accessToken),
});
if (pollResp.status === 401 || pollResp.status === 403) {
@@ -2351,6 +2356,44 @@ async function pollAdobeJob(opts: {
"quota_exhausted"
);
}
// One CDP JWT renewal mid-poll (long jobs can outlive a near-expiry IMS token).
if (!authRefreshAttempted && opts.sessionCookie) {
authRefreshAttempted = true;
try {
const {
rotateAdobeFireflySessionOnError,
fingerprintAdobeCredential,
estimateAdobeTokenExpiry,
} = await import("./adobeFireflySession.ts");
const fp =
String(opts.sessionFingerprint || "").trim() ||
fingerprintAdobeCredential(
[accessToken, opts.sessionCookie].filter(Boolean).join("\n")
);
const refreshed = await rotateAdobeFireflySessionOnError(
{
accessToken,
cookie: opts.sessionCookie,
arpSessionId: "",
tokenExpiresAt: estimateAdobeTokenExpiry(accessToken),
updatedAt: Date.now(),
fingerprint: fp,
source: "rebuild",
},
{ attempt: 3, authFailure: true, tryBrowser: true, log: opts.log }
);
if (refreshed?.accessToken && isAdobeUserAccessToken(refreshed.accessToken)) {
accessToken = refreshed.accessToken;
opts.log?.info?.(
"ADOBE-FIREFLY",
`poll auth ${pollResp.status}; retrying once with renewed JWT`
);
continue;
}
} catch {
/* fall through to auth error */
}
}
throw new AdobeFireflyError("Adobe Firefly token invalid or expired", 401, "auth");
}
@@ -2438,6 +2481,8 @@ export async function adobeFireflyGenerateImage(opts: {
arpSessionId?: string;
/** Session cache key from ensureAdobeFireflySession — sticky ARP across batch jobs. */
sessionFingerprint?: string;
/** Chrome profile key (provider connection id) for CDP warm/login isolation. */
sessionBrowserKey?: string;
timeoutMs?: number;
fetchImpl?: typeof fetch;
log?: {
@@ -2489,11 +2534,14 @@ export async function adobeFireflyGenerateImage(opts: {
const fingerprint =
String(opts.sessionFingerprint || "").trim() ||
fingerprintAdobeCredential([accessToken, activeCookie].filter(Boolean).join("\n"));
const browserSessionKey = String(opts.sessionBrowserKey || "").trim() || fingerprint;
// Gate serialize + min gap: mid-batch 408 is often rate-limit thrash, not "dead" ARP.
await withAdobeFireflySubmitGate(async () => {
for (let attempt = 1; attempt <= SUBMIT_MAX_ATTEMPTS; attempt++) {
const submitResp = await fetchImpl(ADOBE_FIREFLY_IMAGE_SUBMIT_URL, {
// Gate ONLY the actual generate-async HTTP call (min gap). CDP warm / backoff run
// outside so interactive browser login and other Firefly submits are not blocked for minutes.
let submitOk = false;
for (let attempt = 1; attempt <= SUBMIT_MAX_ATTEMPTS; attempt++) {
const submitResp = await withAdobeFireflySubmitGate(() =>
fetchImpl(ADOBE_FIREFLY_IMAGE_SUBMIT_URL, {
method: "POST",
headers: buildAdobeSubmitHeaders(accessToken, {
arpSessionId,
@@ -2501,124 +2549,151 @@ export async function adobeFireflyGenerateImage(opts: {
cookie: activeCookie || undefined,
}),
body: JSON.stringify(payload),
});
})
);
if (submitResp.status === 401 || submitResp.status === 403) {
noteAdobeFireflySubmitFailure();
const accessError = submitResp.headers.get("x-access-error") || "";
if (accessError === "taste_exhausted") {
throw new AdobeFireflyError(
"Adobe Firefly quota exhausted for this account",
429,
"quota_exhausted"
);
}
if (!authRefreshAttempted && attempt < SUBMIT_MAX_ATTEMPTS) {
authRefreshAttempted = true;
const refreshed = await rotateAdobeFireflySessionOnError(
{
accessToken,
cookie: activeCookie,
arpSessionId,
tokenExpiresAt: estimateAdobeTokenExpiry(accessToken),
updatedAt: Date.now(),
fingerprint,
source: "rebuild",
},
{ attempt, authFailure: true, tryBrowser: true, log: opts.log }
).catch(() => null);
if (refreshed?.accessToken && refreshed?.arpSessionId) {
accessToken = refreshed.accessToken;
activeCookie = refreshed.cookie || activeCookie;
arpSessionId = refreshed.arpSessionId;
opts.log?.info?.(
"ADOBE-FIREFLY",
`image submit auth ${submitResp.status}; retrying once with renewed CDP session`
);
continue;
}
}
if (submitResp.status === 401 || submitResp.status === 403) {
noteAdobeFireflySubmitFailure();
const accessError = submitResp.headers.get("x-access-error") || "";
if (accessError === "taste_exhausted") {
throw new AdobeFireflyError(
"Adobe Firefly session is no longer authenticated and automatic browser renewal failed. " +
"Sign in once through the Adobe Firefly browser login to restore durable renewal.",
401,
"auth"
"Adobe Firefly quota exhausted for this account",
429,
"quota_exhausted"
);
}
if (!submitResp.ok) {
const text = await submitResp.text().catch(() => "");
if (isAdobeTransientSubmitError(submitResp.status, text)) {
sawSystemUnderLoad = true;
}
lastSubmitError = `Adobe Firefly image submit failed (${submitResp.status}): ${sanitizeErrorMessage(text.slice(0, 300))}`;
if (isAdobeTransientSubmitError(submitResp.status, text) && attempt < SUBMIT_MAX_ATTEMPTS) {
try {
if (activeCookie) {
const rotated = await rotateAdobeFireflySessionOnError(
{
accessToken,
cookie: activeCookie,
arpSessionId,
tokenExpiresAt: estimateAdobeTokenExpiry(accessToken),
updatedAt: Date.now(),
fingerprint,
source: "rebuild",
},
{
// Stale forter warms immediately; fresh forter quiet-reuses on 12 then warms.
attempt,
tryBrowser: process.env.ADOBE_FIREFLY_BROWSER_REFRESH !== "0",
log: opts.log,
}
);
accessToken = rotated.accessToken || accessToken;
activeCookie = rotated.cookie || activeCookie;
arpSessionId = rotated.arpSessionId;
} else {
arpSessionId = resolveAdobeArpSessionIdSmart(sessionCookie, {
rotate: true,
});
}
} catch {
// Keep prior ARP — synthetic thrash rarely recovers colligo 408.
}
const base = submitBaseDelayMs();
const delay =
base <= 50
? base
: Math.min(90_000, base * Math.pow(2, attempt - 1)) +
Math.floor(Math.random() * 1500);
if (!authRefreshAttempted && attempt < SUBMIT_MAX_ATTEMPTS) {
authRefreshAttempted = true;
const refreshed = await rotateAdobeFireflySessionOnError(
{
accessToken,
cookie: activeCookie,
arpSessionId,
tokenExpiresAt: estimateAdobeTokenExpiry(accessToken),
updatedAt: Date.now(),
fingerprint,
browserSessionKey,
source: "rebuild",
},
{ attempt, authFailure: true, tryBrowser: true, log: opts.log }
).catch(() => null);
if (refreshed?.accessToken && refreshed?.arpSessionId) {
accessToken = refreshed.accessToken;
activeCookie = refreshed.cookie || activeCookie;
arpSessionId = refreshed.arpSessionId;
opts.log?.info?.(
"ADOBE-FIREFLY",
`image submit transient ${submitResp.status}, retry ${attempt}/${SUBMIT_MAX_ATTEMPTS} in ${delay}ms (recovery attempt=${attempt})`
`image submit auth ${submitResp.status}; retrying once with renewed CDP session`
);
await sleep(delay);
continue;
}
noteAdobeFireflySubmitFailure();
if (sawSystemUnderLoad && isAdobeTransientSubmitError(submitResp.status, text)) {
}
throw new AdobeFireflyError(
"Adobe Firefly session is no longer authenticated and automatic browser renewal failed. " +
"Sign in once through the Adobe Firefly browser login to restore durable renewal.",
401,
"auth"
);
}
if (!submitResp.ok) {
const text = await submitResp.text().catch(() => "");
if (isAdobeTransientSubmitError(submitResp.status, text)) {
sawSystemUnderLoad = true;
}
lastSubmitError = `Adobe Firefly image submit failed (${submitResp.status}): ${sanitizeErrorMessage(text.slice(0, 300))}`;
if (isAdobeTransientSubmitError(submitResp.status, text) && attempt < SUBMIT_MAX_ATTEMPTS) {
const { getAdobeForterAgeMs: forterAgeMsFn, extractAdobeForterTimestampMs: forterTsFn } =
await import("./adobeFireflySession.ts");
// Only treat as known-stale when the cookie embeds a parseable forter timestamp.
// Missing timestamp (tests / synthetic ARP) must keep the full retry ladder.
const forterTs = forterTsFn(activeCookie || "");
const forterAgeBefore = forterAgeMsFn(activeCookie || "");
const forterKnownStale =
forterTs > 0 && Number.isFinite(forterAgeBefore) && forterAgeBefore > 4 * 60_000;
// Stale risk session: at most 2 attempts (warm once + one retry). Avoid ~600s thrash.
if (forterKnownStale && attempt >= 2) {
noteAdobeFireflySubmitFailure();
throw new AdobeFireflyError(
formatAdobeSystemUnderLoadError("image", attempt, {
hadBrowserArp,
}),
formatAdobeSystemUnderLoadError("image", attempt, { hadBrowserArp }) +
" Risk session looks expired — open Providers → Adobe Firefly → Sign in with browser once.",
408,
"system_under_load"
);
}
try {
if (activeCookie) {
const rotated = await rotateAdobeFireflySessionOnError(
{
accessToken,
cookie: activeCookie,
arpSessionId,
tokenExpiresAt: estimateAdobeTokenExpiry(accessToken),
updatedAt: Date.now(),
fingerprint,
browserSessionKey,
source: "rebuild",
},
{
// Stale forter warms immediately; fresh forter quiet-reuses on 12 then warms.
attempt,
tryBrowser: process.env.ADOBE_FIREFLY_BROWSER_REFRESH !== "0",
log: opts.log,
}
);
accessToken = rotated.accessToken || accessToken;
activeCookie = rotated.cookie || activeCookie;
arpSessionId = rotated.arpSessionId;
} else {
arpSessionId = resolveAdobeArpSessionIdSmart(sessionCookie, {
rotate: true,
});
}
} catch {
// Keep prior ARP — synthetic thrash rarely recovers colligo 408.
}
const base = submitBaseDelayMs();
const delay =
base <= 50
? base
: Math.min(90_000, base * Math.pow(2, attempt - 1)) + Math.floor(Math.random() * 1500);
opts.log?.info?.(
"ADOBE-FIREFLY",
`image submit transient ${submitResp.status}, retry ${attempt}/${SUBMIT_MAX_ATTEMPTS} in ${delay}ms (recovery attempt=${attempt})`
);
await sleep(delay);
continue;
}
noteAdobeFireflySubmitFailure();
if (sawSystemUnderLoad && isAdobeTransientSubmitError(submitResp.status, text)) {
throw new AdobeFireflyError(
lastSubmitError,
submitResp.status >= 400 && submitResp.status < 500 ? submitResp.status : 502
formatAdobeSystemUnderLoadError("image", attempt, {
hadBrowserArp,
}),
408,
"system_under_load"
);
}
submitData = await submitResp.json().catch(() => ({}));
submitHeaders = submitResp.headers;
// Sticky: remember ARP that colligo accepted so the next batch image reuses it.
markAdobeFireflyArpSuccess(fingerprint, arpSessionId);
break;
throw new AdobeFireflyError(
lastSubmitError,
submitResp.status >= 400 && submitResp.status < 500 ? submitResp.status : 502
);
}
});
submitData = await submitResp.json().catch(() => ({}));
submitHeaders = submitResp.headers;
// Sticky: remember ARP that colligo accepted so the next batch image reuses it.
markAdobeFireflyArpSuccess(fingerprint, arpSessionId);
submitOk = true;
break;
}
if (!submitOk && !lastSubmitError) {
throw new AdobeFireflyError(
formatAdobeSystemUnderLoadError("image", SUBMIT_MAX_ATTEMPTS, { hadBrowserArp }),
408,
"system_under_load"
);
}
let pollUrl = extractAdobeResultLink(submitHeaders, submitData);
if (!pollUrl) {
@@ -2668,6 +2743,8 @@ export async function adobeFireflyGenerateVideo(opts: {
arpSessionId?: string;
/** Session cache key from ensureAdobeFireflySession — sticky ARP across batch jobs. */
sessionFingerprint?: string;
/** Chrome profile key (provider connection id) for CDP warm/login isolation. */
sessionBrowserKey?: string;
timeoutMs?: number;
fetchImpl?: typeof fetch;
log?: {
@@ -2734,10 +2811,12 @@ export async function adobeFireflyGenerateVideo(opts: {
const fingerprint =
String(opts.sessionFingerprint || "").trim() ||
fingerprintAdobeCredential([accessToken, activeCookie].filter(Boolean).join("\n"));
const browserSessionKey = String(opts.sessionBrowserKey || "").trim() || fingerprint;
await withAdobeFireflySubmitGate(async () => {
for (let attempt = 1; attempt <= SUBMIT_MAX_ATTEMPTS; attempt++) {
const submitResp = await fetchImpl(ADOBE_FIREFLY_VIDEO_SUBMIT_URL, {
let videoSubmitOk = false;
for (let attempt = 1; attempt <= SUBMIT_MAX_ATTEMPTS; attempt++) {
const submitResp = await withAdobeFireflySubmitGate(() =>
fetchImpl(ADOBE_FIREFLY_VIDEO_SUBMIT_URL, {
method: "POST",
headers: buildAdobeSubmitHeaders(accessToken, {
arpSessionId,
@@ -2745,122 +2824,146 @@ export async function adobeFireflyGenerateVideo(opts: {
cookie: activeCookie || undefined,
}),
body: JSON.stringify(payload),
});
})
);
if (submitResp.status === 401 || submitResp.status === 403) {
noteAdobeFireflySubmitFailure();
const accessError = submitResp.headers.get("x-access-error") || "";
if (accessError === "taste_exhausted") {
throw new AdobeFireflyError(
"Adobe Firefly quota exhausted for this account",
429,
"quota_exhausted"
);
}
if (!authRefreshAttempted && attempt < SUBMIT_MAX_ATTEMPTS) {
authRefreshAttempted = true;
const refreshed = await rotateAdobeFireflySessionOnError(
{
accessToken,
cookie: activeCookie,
arpSessionId,
tokenExpiresAt: estimateAdobeTokenExpiry(accessToken),
updatedAt: Date.now(),
fingerprint,
source: "rebuild",
},
{ attempt, authFailure: true, tryBrowser: true, log: opts.log }
).catch(() => null);
if (refreshed?.accessToken && refreshed?.arpSessionId) {
accessToken = refreshed.accessToken;
activeCookie = refreshed.cookie || activeCookie;
arpSessionId = refreshed.arpSessionId;
opts.log?.info?.(
"ADOBE-FIREFLY",
`video submit auth ${submitResp.status}; retrying once with renewed CDP session`
);
continue;
}
}
if (submitResp.status === 401 || submitResp.status === 403) {
noteAdobeFireflySubmitFailure();
const accessError = submitResp.headers.get("x-access-error") || "";
if (accessError === "taste_exhausted") {
throw new AdobeFireflyError(
"Adobe Firefly session is no longer authenticated and automatic browser renewal failed. " +
"Sign in once through the Adobe Firefly browser login to restore durable renewal.",
401,
"auth"
"Adobe Firefly quota exhausted for this account",
429,
"quota_exhausted"
);
}
if (!submitResp.ok) {
const text = await submitResp.text().catch(() => "");
if (isAdobeTransientSubmitError(submitResp.status, text)) {
sawSystemUnderLoad = true;
}
lastSubmitError = `Adobe Firefly video submit failed (${submitResp.status}): ${sanitizeErrorMessage(text.slice(0, 300))}`;
if (isAdobeTransientSubmitError(submitResp.status, text) && attempt < SUBMIT_MAX_ATTEMPTS) {
try {
if (activeCookie) {
const rotated = await rotateAdobeFireflySessionOnError(
{
accessToken,
cookie: activeCookie,
arpSessionId,
tokenExpiresAt: estimateAdobeTokenExpiry(accessToken),
updatedAt: Date.now(),
fingerprint,
source: "rebuild",
},
{
attempt,
tryBrowser: process.env.ADOBE_FIREFLY_BROWSER_REFRESH !== "0",
log: opts.log,
}
);
accessToken = rotated.accessToken || accessToken;
activeCookie = rotated.cookie || activeCookie;
arpSessionId = rotated.arpSessionId;
} else {
arpSessionId = resolveAdobeArpSessionIdSmart(sessionCookie, {
rotate: true,
});
}
} catch {
/* keep prior ARP */
}
const base = submitBaseDelayMs();
const delay =
base <= 50
? base
: Math.min(90_000, base * Math.pow(2, attempt - 1)) +
Math.floor(Math.random() * 1500);
if (!authRefreshAttempted && attempt < SUBMIT_MAX_ATTEMPTS) {
authRefreshAttempted = true;
const refreshed = await rotateAdobeFireflySessionOnError(
{
accessToken,
cookie: activeCookie,
arpSessionId,
tokenExpiresAt: estimateAdobeTokenExpiry(accessToken),
updatedAt: Date.now(),
fingerprint,
browserSessionKey,
source: "rebuild",
},
{ attempt, authFailure: true, tryBrowser: true, log: opts.log }
).catch(() => null);
if (refreshed?.accessToken && refreshed?.arpSessionId) {
accessToken = refreshed.accessToken;
activeCookie = refreshed.cookie || activeCookie;
arpSessionId = refreshed.arpSessionId;
opts.log?.info?.(
"ADOBE-FIREFLY",
`video submit transient ${submitResp.status}, retry ${attempt}/${SUBMIT_MAX_ATTEMPTS} in ${delay}ms (recovery attempt=${attempt})`
`video submit auth ${submitResp.status}; retrying once with renewed CDP session`
);
await sleep(delay);
continue;
}
noteAdobeFireflySubmitFailure();
if (sawSystemUnderLoad && isAdobeTransientSubmitError(submitResp.status, text)) {
}
throw new AdobeFireflyError(
"Adobe Firefly session is no longer authenticated and automatic browser renewal failed. " +
"Sign in once through the Adobe Firefly browser login to restore durable renewal.",
401,
"auth"
);
}
if (!submitResp.ok) {
const text = await submitResp.text().catch(() => "");
if (isAdobeTransientSubmitError(submitResp.status, text)) {
sawSystemUnderLoad = true;
}
lastSubmitError = `Adobe Firefly video submit failed (${submitResp.status}): ${sanitizeErrorMessage(text.slice(0, 300))}`;
if (isAdobeTransientSubmitError(submitResp.status, text) && attempt < SUBMIT_MAX_ATTEMPTS) {
const { getAdobeForterAgeMs: forterAgeMsFn, extractAdobeForterTimestampMs: forterTsFn } =
await import("./adobeFireflySession.ts");
const forterTs = forterTsFn(activeCookie || "");
const forterAgeBefore = forterAgeMsFn(activeCookie || "");
const forterKnownStale =
forterTs > 0 && Number.isFinite(forterAgeBefore) && forterAgeBefore > 4 * 60_000;
if (forterKnownStale && attempt >= 2) {
noteAdobeFireflySubmitFailure();
throw new AdobeFireflyError(
formatAdobeSystemUnderLoadError("video", attempt, {
hadBrowserArp,
}),
formatAdobeSystemUnderLoadError("video", attempt, { hadBrowserArp }) +
" Risk session looks expired — open Providers → Adobe Firefly → Sign in with browser once.",
408,
"system_under_load"
);
}
try {
if (activeCookie) {
const rotated = await rotateAdobeFireflySessionOnError(
{
accessToken,
cookie: activeCookie,
arpSessionId,
tokenExpiresAt: estimateAdobeTokenExpiry(accessToken),
updatedAt: Date.now(),
fingerprint,
browserSessionKey,
source: "rebuild",
},
{
attempt,
tryBrowser: process.env.ADOBE_FIREFLY_BROWSER_REFRESH !== "0",
log: opts.log,
}
);
accessToken = rotated.accessToken || accessToken;
activeCookie = rotated.cookie || activeCookie;
arpSessionId = rotated.arpSessionId;
} else {
arpSessionId = resolveAdobeArpSessionIdSmart(sessionCookie, {
rotate: true,
});
}
} catch {
/* keep prior ARP */
}
const base = submitBaseDelayMs();
const delay =
base <= 50
? base
: Math.min(90_000, base * Math.pow(2, attempt - 1)) + Math.floor(Math.random() * 1500);
opts.log?.info?.(
"ADOBE-FIREFLY",
`video submit transient ${submitResp.status}, retry ${attempt}/${SUBMIT_MAX_ATTEMPTS} in ${delay}ms (recovery attempt=${attempt})`
);
await sleep(delay);
continue;
}
noteAdobeFireflySubmitFailure();
if (sawSystemUnderLoad && isAdobeTransientSubmitError(submitResp.status, text)) {
throw new AdobeFireflyError(
lastSubmitError,
submitResp.status >= 400 && submitResp.status < 500 ? submitResp.status : 502
formatAdobeSystemUnderLoadError("video", attempt, {
hadBrowserArp,
}),
408,
"system_under_load"
);
}
submitData = await submitResp.json().catch(() => ({}));
submitHeaders = submitResp.headers;
markAdobeFireflyArpSuccess(fingerprint, arpSessionId);
break;
throw new AdobeFireflyError(
lastSubmitError,
submitResp.status >= 400 && submitResp.status < 500 ? submitResp.status : 502
);
}
});
submitData = await submitResp.json().catch(() => ({}));
submitHeaders = submitResp.headers;
markAdobeFireflyArpSuccess(fingerprint, arpSessionId);
videoSubmitOk = true;
break;
}
if (!videoSubmitOk && !lastSubmitError) {
throw new AdobeFireflyError(
formatAdobeSystemUnderLoadError("video", SUBMIT_MAX_ATTEMPTS, { hadBrowserArp }),
408,
"system_under_load"
);
}
let pollUrl = extractAdobeResultLink(submitHeaders, submitData);
if (!pollUrl) {
@@ -2885,6 +2988,8 @@ export async function adobeFireflyGenerateVideo(opts: {
accessToken,
kind: "video",
timeoutMs: opts.timeoutMs && opts.timeoutMs > 0 ? opts.timeoutMs : DEFAULT_VIDEO_TIMEOUT_MS,
sessionCookie: activeCookie || sessionCookie || undefined,
sessionFingerprint: fingerprint,
fetchImpl,
log: opts.log,
});

View File

@@ -72,6 +72,9 @@ const sessionCache = new Map<string, AdobeFireflySession>();
const browserRefreshInFlight = new Map<string, Promise<AdobeFireflySession | null>>();
/** Last ARP that produced HTTP 2xx on generate-async — prefer until colligo 408. */
const lastWorkingArpByFingerprint = new Map<string, { arp: string; at: number }>();
/** After a failed force-warm, skip re-launching Chrome for this fingerprint for a short window. */
const browserWarmFailureCooldown = new Map<string, number>();
const BROWSER_WARM_FAIL_COOLDOWN_MS = 90_000;
/** Serialize Firefly generate submits + enforce a quiet period (colligo rate-limits look like 408). */
let adobeSubmitChain: Promise<void> = Promise.resolve();
let lastAdobeSubmitAt = 0;
@@ -478,6 +481,46 @@ function collectCredentialBlobs(
* Uses the same persistent pure-CDP profile as interactive sign-in, including in pkg builds.
* Never throws — returns null when unavailable.
*/
/**
* Best-effort write refreshed JWT+Cookie back to provider_connections so restarts
* and WinUI sync do not keep serving a guest/stale paste after a successful warm.
*/
async function writeBackAdobeFireflyCredentials(
session: AdobeFireflySession,
log?: AdobeFireflySessionResolveOpts["log"]
): Promise<void> {
const connectionId = String(session.browserSessionKey || "").trim();
if (!connectionId || connectionId === "legacy-default") return;
if (!isAdobeUserAccessToken(session.accessToken)) return;
// Skip when connectionId looks like a credential fingerprint (32 hex) without a real UUID.
// Real OmniRoute connection ids are UUIDs; still attempt write-back for any non-empty key.
try {
const { updateProviderConnection } = await import("@/lib/db/providers");
const credential = serializeAdobeFireflyCredential(session);
await updateProviderConnection(connectionId, {
apiKey: credential,
providerSpecificData: {
mode: "browser-profile",
adobeFireflyMode: "browser-profile",
cookie: session.cookie || credential,
access_token: session.accessToken,
browserSessionKey: connectionId,
arpSessionId: session.arpSessionId || "",
refreshedAt: Date.now(),
},
});
log?.info?.(
"ADOBE-FIREFLY",
`wrote refreshed JWT+Cookie to connection ${connectionId.slice(0, 8)}`
);
} catch (err) {
log?.warn?.(
"ADOBE-FIREFLY",
`credential write-back skipped: ${err instanceof Error ? err.message : String(err)}`
);
}
}
export async function refreshAdobeSessionViaBrowser(
session: AdobeFireflySession,
log?: AdobeFireflySessionResolveOpts["log"],
@@ -487,48 +530,92 @@ export async function refreshAdobeSessionViaBrowser(
// Browser warm is the default engine now — only the explicit kill switch disables it.
if (!adobeFireflyBrowserEnabled()) return null;
const coolKey = String(session.browserSessionKey || session.fingerprint || "").trim();
const coolUntil = coolKey ? browserWarmFailureCooldown.get(coolKey) || 0 : 0;
if (force && coolUntil > Date.now()) {
log?.warn?.(
"ADOBE-FIREFLY",
`skip CDP warm (cooldown ${Math.ceil((coolUntil - Date.now()) / 1000)}s after recent failure)`
);
return null;
}
try {
const baseFtr = extractAdobeForterTimestampMs(session.cookie || "");
const { refreshAdobeFireflyViaCdp } = await import("./adobeFireflyBrowserLogin.ts");
const warmed = await refreshAdobeFireflyViaCdp({
cookie: session.cookie,
accessToken: session.accessToken,
log,
timeoutMs: force ? 90_000 : 75_000,
sessionKey: session.browserSessionKey,
sessionKey: session.browserSessionKey || session.fingerprint,
});
if (!warmed) return null;
if (!warmed) {
if (force && coolKey) {
browserWarmFailureCooldown.set(coolKey, Date.now() + BROWSER_WARM_FAIL_COOLDOWN_MS);
}
return null;
}
if (coolKey) browserWarmFailureCooldown.delete(coolKey);
// Prefer warm cookie as authority for risk pieces (do not re-merge stale forter over new).
// On force warm, prefer the warmed cookie as authority (do not re-merge hours-old forter
// from the previous session blob over a freshly minted jar).
const nextCookie = force
? warmed.cookie || session.cookie
: warmed.cookie
? mergeAdobeCookieHeaders(session.cookie || "", warmed.cookie)
: session.cookie;
const warmFtr = extractAdobeForterTimestampMs(nextCookie);
const warmAge = warmFtr > 0 ? Math.max(0, Date.now() - warmFtr) : Number.POSITIVE_INFINITY;
// Force path: require a parseable forter younger than FORTER_STALE (or strictly newer than base).
if (force) {
const advanced =
warmFtr > 0 && (baseFtr <= 0 || warmFtr > baseFtr || warmAge < FORTER_STALE_MS);
if (!advanced) {
log?.warn?.(
"ADOBE-FIREFLY",
`CDP warm rejected: forter not advanced (base=${baseFtr}, warm=${warmFtr || 0}, ageMs=${Number.isFinite(warmAge) ? warmAge : "inf"})`
);
return null;
}
}
const nextCookie = mergeAdobeCookieHeaders(session.cookie, warmed.cookie || "");
const nextArp =
warmed.arpSessionId ||
buildAdobeArpSessionIdFromCookies(nextCookie) ||
extractAdobeArpSessionId(nextCookie);
if (!nextArp) return null;
const nextToken =
(warmed.accessToken && isAdobeUserAccessToken(warmed.accessToken)
? warmed.accessToken
: "") || session.accessToken;
if (!isAdobeUserAccessToken(nextToken)) return null;
const next: AdobeFireflySession = {
...session,
accessToken: warmed.accessToken || session.accessToken,
accessToken: nextToken,
cookie: nextCookie,
arpSessionId: nextArp,
tokenExpiresAt: estimateAdobeTokenExpiry(warmed.accessToken || session.accessToken),
tokenExpiresAt: estimateAdobeTokenExpiry(nextToken),
updatedAt: Date.now(),
browserSessionKey: session.browserSessionKey || session.fingerprint,
source: "browser",
};
// When warm returns a fresher forter, prefer its cookie entirely for ARP rebuild pieces.
const warmFtr = extractAdobeForterTimestampMs(nextCookie);
const baseFtr = extractAdobeForterTimestampMs(session.cookie || "");
if (warmFtr > baseFtr && warmed.cookie) {
next.cookie = mergeAdobeCookieHeaders(session.cookie, warmed.cookie);
}
sessionCache.set(session.fingerprint, next);
saveDiskSession(next);
clearAdobeFireflyWorkingArp(session.fingerprint);
void writeBackAdobeFireflyCredentials(next, log);
log?.info?.(
"ADOBE-FIREFLY",
`durable CDP warm refreshed session (arpLen=${next.arpSessionId.length}, force=${force}, forterTs=${warmFtr || 0})`
`durable CDP warm refreshed session (arpLen=${next.arpSessionId.length}, force=${force}, forterTs=${warmFtr || 0}, forterDeltaMs=${warmFtr && baseFtr ? warmFtr - baseFtr : 0})`
);
return next;
} catch (err) {
if (force && coolKey) {
browserWarmFailureCooldown.set(coolKey, Date.now() + BROWSER_WARM_FAIL_COOLDOWN_MS);
}
log?.warn?.(
"ADOBE-FIREFLY",
`browser CDP session refresh failed: ${err instanceof Error ? err.message : String(err)}`
@@ -555,19 +642,36 @@ export async function ensureAdobeFireflySession(
}
const joined = blobs.join("\n");
const fingerprint = fingerprintAdobeCredential(joined);
const browserSessionKey =
String(
opts.credentials?.connectionId ||
opts.credentials?.providerSpecificData?.browserSessionKey ||
fingerprint
).trim() || fingerprint;
// Prefer stable connection-scoped fingerprint so JWT/cookie refresh does not orphan
// the session cache / sticky ARP map (paste hash changes every warm write-back).
const connectionId = String(
opts.credentials?.connectionId ||
opts.credentials?.providerSpecificData?.browserSessionKey ||
""
).trim();
const fingerprint = connectionId
? fingerprintAdobeCredential(`conn:${connectionId}`)
: fingerprintAdobeCredential(joined);
const browserSessionKey = connectionId || fingerprint;
// forceRefresh / rotate always drop in-memory cache for this fingerprint
if (opts.forceRefresh) sessionCache.delete(fingerprint);
const cached = sessionCache.get(fingerprint) || loadDiskSession(fingerprint);
if (cached && !opts.forceRefresh) sessionCache.set(fingerprint, cached);
// Also try legacy paste-hash session files (pre-connection-scoped fingerprints).
const legacyFingerprint = fingerprintAdobeCredential(joined);
const cached =
sessionCache.get(fingerprint) ||
loadDiskSession(fingerprint) ||
(legacyFingerprint !== fingerprint ? loadDiskSession(legacyFingerprint) : null);
if (cached && !opts.forceRefresh) {
// Re-key legacy disk session under the stable connection fingerprint.
const normalized = {
...cached,
fingerprint,
browserSessionKey: cached.browserSessionKey || browserSessionKey,
};
sessionCache.set(fingerprint, normalized);
}
const fetchImpl = opts.fetchImpl || fetch;
let accessToken = "";
@@ -676,12 +780,14 @@ export async function ensureAdobeFireflySession(
accessToken,
cookie: cookieForSession,
arpSessionId: String(arpSessionId || ""),
tokenExpiresAt: estimateAdobeTokenExpiry(accessToken),
tokenExpiresAt: estimateAdobeTokenExpiry(accessToken || cached?.accessToken || ""),
updatedAt: Date.now(),
fingerprint,
browserSessionKey,
source: workingFresh ? "cache" : cached?.source || "paste",
};
// Prefer connection-scoped browser profile always (never empty → legacy-default).
if (!session.browserSessionKey) session.browserSessionKey = browserSessionKey;
// Off-screen Chrome Forter-warm is now the DEFAULT engine (kill switch:
// ADOBE_FIREFLY_BROWSER_REFRESH=0). Warm proactively when we lack a usable session so the
@@ -755,8 +861,39 @@ export async function ensureAdobeFireflySession(
);
}
// Dead Forter risk session: colligo returns 408 for ~minutes/hours of retries. Fail closed
// with a re-login instruction instead of burning ~600s of generate-async attempts.
// Only when forter timestamp is parseable and old — missing timestamp is not treated as stale
// (JWT-only / synthetic ARP / unit fixtures).
const finalForterTs = extractAdobeForterTimestampMs(session.cookie);
const finalForterAge = getAdobeForterAgeMs(session.cookie);
const hasStickyWorking =
Boolean(workingFresh) &&
Date.now() - (lastWorkingArpByFingerprint.get(fingerprint)?.at || 0) < WORKING_ARP_STICKY_MS;
if (
finalForterTs > 0 &&
Number.isFinite(finalForterAge) &&
finalForterAge > FORTER_STALE_MS &&
!hasStickyWorking &&
opts.allowBrowserRefresh !== false
) {
throw new AdobeFireflyError(
"Adobe Firefly risk session expired (Forter/Arkose). Open Providers → Adobe Firefly → " +
"Add Account (OAuth) → Sign in with browser once. After sign-in the app stores a fresh " +
"JWT+Cookie and refreshes them automatically for later generates.",
401,
"risk_session_stale"
);
}
session.fingerprint = fingerprint;
session.browserSessionKey = session.browserSessionKey || browserSessionKey;
sessionCache.set(fingerprint, session);
saveDiskSession(session);
// Keep SQLite in sync when we have a real connection + user JWT (best-effort).
if (session.source === "browser" || session.source === "rebuild") {
void writeBackAdobeFireflyCredentials(session, opts.log);
}
return session;
}
@@ -810,8 +947,9 @@ export async function rotateAdobeFireflySessionOnError(
return same;
}
// Known-stale forter or attempt 3+: cookie rebuild is a no-op. Off-screen headed Chrome mints a
// fresh Forter/ARP (headless browsers are rejected by colligo, so CDP uses off-screen headed mode).
// Known-stale forter or attempt 3+: cookie rebuild is a no-op. CDP warm mints a fresh
// Forter/ARP via offscreen headed Chrome by default (colligo rejects true headless).
// ADOBE_FIREFLY_CHROME_HEADLESS=1 is debug-only and usually keeps returning 408.
clearAdobeFireflyWorkingArp(session.fingerprint);
noteAdobeFireflySubmitFailure();
@@ -857,6 +995,7 @@ export function __resetAdobeFireflySessionCacheForTests(): void {
sessionCache.clear();
browserRefreshInFlight.clear();
lastWorkingArpByFingerprint.clear();
browserWarmFailureCooldown.clear();
lastAdobeSubmitAt = 0;
consecutiveAdobeSubmitSuccesses = 0;
adobeSubmitChain = Promise.resolve();

View File

@@ -20,7 +20,142 @@ function resolveProviderSlug(connection: Record<string, unknown> | null): string
return "";
}
// тФАтФАтФА POST: Start login flow тФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФАтФА
function isAdobeFireflyProvider(
connection: { provider?: unknown } | null,
providerSlug: string
): boolean {
const raw = String(connection?.provider || "").trim();
return ADOBE_FIREFLY_SLUGS.has(raw) || ADOBE_FIREFLY_SLUGS.has(providerSlug);
}
/**
* Persist JWT + Cookie the way desktop clients (and generate) expect:
* multi-line api_key, plus camelCase providerSpecificData for updateProviderConnection.
*/
async function persistAdobeFireflyCredentials(
connectionId: string,
opts: {
accessToken?: string;
cookie?: string;
account?: string;
arpSessionId?: string;
}
): Promise<{
accessToken: string;
cookie: string;
credential: string;
account: string;
}> {
const accessToken = String(opts.accessToken || "").trim();
const cookie = String(opts.cookie || "").trim();
const account = String(opts.account || "").trim();
const credential =
accessToken && cookie
? `${accessToken}\n${cookie}`
: accessToken ||
cookie ||
JSON.stringify({
mode: "browser-profile",
account,
signedInAt: Date.now(),
});
const marker = {
mode: "browser-profile",
account,
signedInAt: Date.now(),
arpSessionId: String(opts.arpSessionId || ""),
};
try {
// camelCase only — updateProviderConnection / encryptConnectionFields read apiKey +
// providerSpecificData (snake_case keys are silently ignored and never persisted).
await updateProviderConnection(connectionId, {
apiKey: credential,
providerSpecificData: {
...marker,
cookie: cookie || credential,
access_token: accessToken || undefined,
},
});
} catch {
/* non-fatal — return credentials to the host app either way */
}
return { accessToken, cookie, credential, account };
}
function adobeFireflySuccessResponse(data: {
accessToken: string;
cookie: string;
credential: string;
account: string;
arpSessionId?: string;
via: "pure-cdp";
}): NextResponse {
return NextResponse.json({
success: true,
account: data.account || undefined,
accessToken: data.accessToken || undefined,
cookie: data.cookie || undefined,
arpSessionId: data.arpSessionId || undefined,
credential: data.credential,
credentials: {
access_token: data.accessToken || undefined,
cookie: data.cookie || undefined,
},
via: data.via,
persisted: true,
});
}
/**
* Adobe Firefly browser sign-in:
* pure system Chrome/Edge CDP only (packaged-safe, no Playwright/browser bundle).
*/
async function loginAdobeFirefly(
connectionId: string,
body: { timeout?: unknown; freshSession?: unknown }
): Promise<NextResponse> {
const timeout = typeof body.timeout === "number" ? body.timeout : undefined;
const freshSession = typeof body.freshSession === "boolean" ? body.freshSession : true;
// Pure system-browser CDP is the packaged-safe implementation. Do not open a second browser
// after failure: it creates ambiguous success/error races and the packaged runtime has no
// reliable Playwright browser bundle.
// startAdobeFireflyBrowserLogin always kills its Chrome tree in `finally` (no orphans).
try {
const { startAdobeFireflyBrowserLogin } =
await import("@omniroute/open-sse/services/adobeFireflyBrowserLogin.ts");
const pure = await startAdobeFireflyBrowserLogin(timeout, {
sessionKey: connectionId,
freshSession,
});
if (pure.success && pure.credentials?.accessToken) {
const persisted = await persistAdobeFireflyCredentials(connectionId, {
accessToken: pure.credentials.accessToken,
cookie: pure.credentials.cookie,
account: pure.account,
});
return adobeFireflySuccessResponse({
...persisted,
via: "pure-cdp",
});
}
return NextResponse.json(
{
success: false,
error: pure.error || "Adobe Firefly sign-in did not capture an authenticated IMS JWT.",
},
{ status: 400 }
);
} catch (err) {
const msg = sanitizeErrorMessage(err instanceof Error ? err.message : err);
return NextResponse.json({ success: false, error: msg }, { status: 400 });
}
}
// --- POST: Start login flow -------------------------------------------------
export async function POST(
req: NextRequest,
@@ -39,67 +174,18 @@ export async function POST(
timeout?: unknown;
freshSession?: unknown;
};
const timeout = typeof body.timeout === "number" ? body.timeout : undefined;
const providerSlug = resolveProviderSlug(provider as Record<string, unknown>);
// Firefly JWTs exist only on firefly-3p Authorization headers. Use one packaged-safe CDP
// flow, isolate state by connection, and never fall through to a second browser.
if (ADOBE_FIREFLY_SLUGS.has(providerSlug)) {
// Adobe Firefly: dedicated JWT capture (never cookies/localStorage alone).
if (isAdobeFireflyProvider(provider as { provider?: unknown }, providerSlug)) {
try {
const { startAdobeFireflyBrowserLogin } =
await import("@omniroute/open-sse/services/adobeFireflyBrowserLogin.ts");
const result = await startAdobeFireflyBrowserLogin(timeout, {
sessionKey: id,
freshSession: typeof body.freshSession === "boolean" ? body.freshSession : true,
});
const accessToken = String(result.credentials?.accessToken || "").trim();
const cookie = String(result.credentials?.cookie || "").trim();
if (result.success && accessToken) {
const credential =
accessToken && cookie ? `${accessToken}\n${cookie}` : accessToken || cookie;
const marker = {
mode: "browser-profile",
account: result.account || "",
signedInAt: Date.now(),
arpSessionId: result.arpSessionId || "",
};
try {
await updateProviderConnection(id, {
apiKey: credential,
providerSpecificData: {
...marker,
cookie: cookie || credential,
access_token: accessToken || undefined,
},
});
} catch {
/* non-fatal — return credentials to the host app either way */
}
return NextResponse.json({
success: true,
account: result.account,
accessToken: accessToken || undefined,
cookie: cookie || undefined,
arpSessionId: result.arpSessionId || undefined,
credential,
credentials: {
access_token: accessToken || undefined,
cookie: cookie || undefined,
},
via: "pure-cdp",
persisted: true,
});
}
return NextResponse.json(
{
success: false,
error: result.error || "Adobe Firefly sign-in did not capture an authenticated IMS JWT.",
},
{ status: 400 }
);
return await loginAdobeFirefly(id, body);
} catch (err) {
const msg = sanitizeErrorMessage(err instanceof Error ? err.message : err);
return NextResponse.json({ success: false, error: msg }, { status: 400 });
return NextResponse.json(
{ success: false, error: `Adobe Firefly sign-in error: ${msg}` },
{ status: 500 }
);
}
}
@@ -110,7 +196,9 @@ export async function POST(
// missed and returned "No extraction config" without launching a browser.
const { inAppLoginService } = await import("@omniroute/open-sse/services/inAppLoginService.ts");
const result = await inAppLoginService.startLogin(providerSlug || id, { timeout });
const result = await inAppLoginService.startLogin(providerSlug || id, {
timeout: typeof body.timeout === "number" ? body.timeout : undefined,
});
// Persist credentials if extraction succeeded
if (result.success && result.credentials) {

View File

@@ -5,13 +5,18 @@
import test from "node:test";
import assert from "node:assert/strict";
import {
adobeFireflyBackgroundUsesHeadlessChrome,
adobeFireflyBrowserSessionKey,
accountLabelFromAdobeJwt,
buildAdobeFireflyBrowserArgs,
buildAdobeFireflyCookieHeader,
clampAdobeFireflyLoginTimeout,
extractAdobeBearerTokenFromAuthorization,
extractAdobeForterTimestampFromValue,
extractUserJwtFromStorageRaw,
filterAdobeBrowserCookies,
filterSeedCookiesForWarm,
isAdobeRiskCookieName,
resolveAdobeAccountLabel,
resolveSystemBrowserExecutable,
} from "../../open-sse/services/adobeFireflyBrowserLogin.ts";
@@ -89,7 +94,7 @@ test("resolveAdobeAccountLabel uses IMS display name and generic fallback", asyn
assert.equal(fallback, "Adobe account");
});
test("browser args isolate profiles and make fresh interactive login incognito", () => {
test("browser args: interactive headed; background offscreen (Forter-safe), headless opt-in only", () => {
const firstKey = adobeFireflyBrowserSessionKey("connection-a");
const secondKey = adobeFireflyBrowserSessionKey("connection-b");
assert.equal(firstKey, adobeFireflyBrowserSessionKey("connection-a"));
@@ -101,17 +106,86 @@ test("browser args isolate profiles and make fresh interactive login incognito",
interactive: true,
freshSession: true,
});
assert.ok(interactive.includes("--incognito"));
// User-initiated Sign in with browser: real window, never headless.
assert.equal(interactive.includes("--headless=new"), false);
assert.ok(interactive.includes("--new-window"));
assert.ok(interactive.includes(`--user-data-dir=C:\\profiles\\${firstKey}`));
assert.equal(interactive.at(-1), "https://firefly.adobe.com/");
const background = buildAdobeFireflyBrowserArgs({
port: 9223,
userDataDir: `C:\\profiles\\${secondKey}`,
interactive: false,
});
assert.equal(background.includes("--incognito"), false);
assert.equal(background.at(-1), "about:blank");
const prevHeadless = process.env.ADOBE_FIREFLY_CHROME_HEADLESS;
delete process.env.ADOBE_FIREFLY_CHROME_HEADLESS;
try {
// Default background: offscreen headed (colligo accepts; true headless → 408).
assert.equal(adobeFireflyBackgroundUsesHeadlessChrome(), false);
const background = buildAdobeFireflyBrowserArgs({
port: 9223,
userDataDir: `C:\\profiles\\${secondKey}`,
interactive: false,
});
assert.equal(background.includes("--headless=new"), false);
assert.equal(background.includes("--new-window"), false);
assert.ok(background.includes("--window-position=-32000,-32000"));
assert.ok(background.includes("--start-minimized"));
assert.equal(background.at(-1), "https://firefly.adobe.com/");
process.env.ADOBE_FIREFLY_CHROME_HEADLESS = "1";
assert.equal(adobeFireflyBackgroundUsesHeadlessChrome(), true);
const headless = buildAdobeFireflyBrowserArgs({
port: 9224,
userDataDir: `C:\\profiles\\${secondKey}`,
interactive: false,
});
assert.ok(headless.includes("--headless=new"));
} finally {
if (prevHeadless === undefined) delete process.env.ADOBE_FIREFLY_CHROME_HEADLESS;
else process.env.ADOBE_FIREFLY_CHROME_HEADLESS = prevHeadless;
}
});
test("isAdobeRiskCookieName flags forter/arkose/sherlock", () => {
assert.equal(isAdobeRiskCookieName("forterToken"), true);
assert.equal(isAdobeRiskCookieName("arkose"), true);
assert.equal(isAdobeRiskCookieName("sherlockToken"), true);
assert.equal(isAdobeRiskCookieName("ff_session_guid"), false);
assert.equal(isAdobeRiskCookieName("aux_sid"), false);
});
test("filterSeedCookiesForWarm drops risk cookies on force warm", () => {
const filtered = filterSeedCookiesForWarm(
[
{ name: "forterToken", value: "stale" },
{ name: "arkose", value: "a" },
{ name: "ff_session_guid", value: "sid" },
{ name: "aux_sid", value: "aux" },
],
{ dropRiskCookies: true }
);
assert.deepEqual(filtered.map((c) => c.name).sort(), ["aux_sid", "ff_session_guid"]);
});
test("extractAdobeForterTimestampFromValue reads embedded ms", () => {
const ftr = "abc_1785777856265__UDF43-mnts-ants-x";
assert.equal(extractAdobeForterTimestampFromValue(ftr), 1785777856265);
assert.equal(extractAdobeForterTimestampFromValue(""), 0);
});
test("extractUserJwtFromStorageRaw prefers user AdobeID JWT", () => {
const guestPayload = Buffer.from(
JSON.stringify({ type: "guest", account_type: "guest", user_id: "x@GuestID" })
).toString("base64url");
const userPayload = Buffer.from(
JSON.stringify({
type: "access_token",
user_id: "0EB6@AdobeID",
client_id: "clio-playground-web",
created_at: Date.now(),
expires_in: 86400000,
})
).toString("base64url");
const guest = `eyJhbGciOiJIUzI1NiJ9.${guestPayload}.sig`;
const user = `eyJhbGciOiJSUzI1NiJ9.${userPayload}.usersig`;
const raw = JSON.stringify({ tokenValue: guest }) + "\n" + JSON.stringify({ access_token: user });
assert.equal(extractUserJwtFromStorageRaw(raw), user);
});
test("filterAdobeBrowserCookies keeps Adobe SSO domains only", () => {