diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json
index 997c081d2f..cfa8a5a3c2 100644
--- a/config/quality/file-size-baseline.json
+++ b/config/quality/file-size-baseline.json
@@ -426,9 +426,6 @@
"_rebaseline_2026_07_28_8863_firefly_detail_level": "PR #8863 (fix/adobe-firefly-gpt-detail-level-max) own growth: adobeFireflyClient.ts 2317->2322 (+5 = gpt-image detailLevel defaulting to maximal at the existing payload-build site). Covered by tests/unit/adobe-firefly.test.ts.",
"_rebaseline_2026_07_29_8281_home_quickstart_prefetch": "Release v3.8.49 base-red fix (no PR — captain sweep): src/app/(dashboard)/dashboard/HomePageClient.tsx 1377->1381 (+4). #8292 added prefetch={false} to the sidebar but left /home's five quick-start Links prefetching, so first paint still fired 12 speculative RSC requests — caught by navigation.spec.ts only after the e2e helper bug (APP_ROUTE_PATTERN missing /home) was repaired in the same cycle. Growth is the five prefetch attributes; it was offset first by extracting the repeated className literals (INLINE_LINK x4, DOCS_LINK x1), which collapsed five wrapped blocks back to one line each — a naive fix measured 1391. Guard: tests/unit/sidebar-prefetch-policy-8281.test.ts.",
"_rebaseline_2026_08_02_v3850_agentrouter_responses": "Release v3.8.50 AgentRouter/Codex compatibility reconciliation. open-sse/executors/base.ts 1562->1578: #9190 wires AgentRouter's selected Claude/OpenAI/Responses protocol through the existing executor URL, auth, identity-header and fingerprint chokepoints; the reusable alternate resolver remains outside base.ts. open-sse/utils/stream.ts 2887->2889: #9213 evaluates Responses ID and usage normalization independently so response.completed always receives finite usage.total_tokens instead of short-circuiting after an ID rewrite. tests/unit/chatcore-translation-paths.test.ts 2769->2776: #9191 updates the existing Claude-Code bridge assertions for the dynamic AgentRouter wire image. PR #9224 offsets its own chatCore growth by extracting the AgentRouter protocol decisions into chatCore/agentRouterProtocol.ts, leaving chatCore below its frozen ceiling. Covered by agentrouter executor/chatCore protocol tests, chatcore translation-path tests, and responses-commentary-passthrough tests.",
- "_rebaseline_2026_08_08_v3850_base_drift_batch_9757": "Base drift on release/v3.8.50, not own growth: the 08-06..08-08 merge batches grew 12 already-frozen (or newly-landed) files without carrying their rebaselines — the dedicated rebaseline PR #9616 was closed as 'superseded' but its file-size entries never actually reached the base, and later merges (#8894 combos page, #9539 EditConnectionModal, #8895 models route, #9294/#9293 catalog, #9541 db/core, #8970 tokenHealthCheck, #8925 mcp schemas+server, #8890 accountFallback, #9467 chat.ts, #8931 openai-to-kiro, ProxyRegistryManager) kept growing them. All 12 values re-measured on THIS branch's tree (= pure tip + this PR's 1-line chat.ts fix, which adds zero lines). This PR's own source changes (chat.ts identifier restore, stream.ts format carve-out) do not grow any frozen file past these values.",
- "_rebaseline_2026_08_08_migration_135_collision": "fix(db): resolve migration version 135 numbering collision — #9449's 135_connection_runtime_state.sql and #8908's 135_migrate_model_capability_max_token.sql both claimed version 135 (#9449 branched before #8908 merged and never got renumbered before landing on release/v3.8.50), which threw 'Migration version collision detected' the moment ANY code touched the database — a fresh install/deploy from this tip cannot even boot. Renumbered the later-landing file to 140 (next free slot) and added the matching isSchemaAlreadyApplied('140') retroactive guard, matching the established pattern already used for the prior 135/136 -> 137/138 renumber in the same file. Own growth: src/lib/db/migrationRunner.ts 1084->1094 (+10, the new case block) — irreducible, matches the existing per-case guard pattern exactly. Covered by tests/unit/migration-135-numbering-collision.test.ts (2/2), confirmed failing (reproducing the exact live crash) against the pre-fix colliding filenames, passing after.",
- "_rebaseline_2026_08_02_9259_rolling_rpm": "PR #9259 (issue #8733) own growth: open-sse/services/rateLimitManager.ts baseline 1060->1167 (+107; final source 1153). The existing withRateLimit chokepoint now composes process-local rolling RPM leases with Bottleneck admission, releases pre-dispatch leases on queue timeout/abort/connection disable, preserves caller abort reasons, and wires 429/header state into the extracted rollingRpmGate.ts. The remaining growth is irreducible lifecycle wiring at the dispatch boundary plus the real watchdog test hooks needed to verify queued-wedge recovery; moving it further would obscure lease ownership and Bottleneck cleanup. Covered by the focused rate-limit manager/sliding-window suite (33/33); distributed multi-instance coordination remains explicitly out of scope.",
"_rebaseline_2026_07_25_dario_upstream_proxy_selector": "PR #8523 (Dario embedded service): upstream-proxy mode selector replaces the binary CLIProxyAPI toggle with Native/CLIProxyAPI/Dario/Fallback + a fallback-backend picker. ProviderDetailPageClient.tsx 798->804 (+6, new hook fields threaded through to ConnectionsListPanel), ConnectionRow.tsx 942->958 (+16, the mode + conditional fallback-backend replacing a single pill button), useProviderConnections.ts 954->986 (+32, upstreamProxyMode/upstreamProxyFallbackBackend state + handleSetUpstreamProxyMode, handleToggleCliproxyapiMode kept as a thin backward-compat wrapper for the existing hook-shape test). All additive UI/state for the new modes — no unrelated refactor.",
"_rebaseline_2026_08_02_9242_token_health_transient": "PR #9242 (fix/refresh-circuit-transient): src/lib/tokenHealthCheck.ts 1021 (new file, above cap 1000). The file consolidates token-refresh health checking logic that was previously scattered across auth.ts and tokenRefresh.ts. Cohesive single-responsibility module for refresh circuit state management; not extractable without splitting the refresh state machine. Covered by tests/unit/tokenHealthCheck-transient.test.ts.",
"_rebaseline_2026_07_28_8870_firefly_ref_cap_timeout": "PR #8870 (fix/adobe-firefly-gpt-ref-cap-timeout) own growth: adobeFireflyClient.ts 2322->2385 (+63 = gpt-image subject-ref hard cap at 2 + adaptive poll timeout budget (base 300s + 60s/ref, max 600s) + defensive .slice on referenceBlobs for gpt/nano/generic families). Fixes live 504s on multi-screenshot listing jobs (Featured Promo / Box Art) where 3–4+ subject refs stall colligo until the old 180s poll budget expires. Helpers adobeFireflyMaxImageRefs/adobeFireflyImageTimeoutMs live next to the existing payload/poll chokepoint (not extractable without splitting the wire recipe mid-PR). Covered by tests/unit/adobe-firefly.test.ts (ref-cap + timeout cases). Structural shrink tracked in #3501.",
@@ -567,5 +564,6 @@
"open-sse/utils/sseHeartbeat.ts": "142",
"_rebaseline_2026_08_04_9305_sse_comments": "#9305 fix: broadened sseCommentsEnabled()",
"_rebaseline_2026_08_09_v3850_release_close": "Release v3.8.50 close reconciliation on e0ce95c592: src/sse/handlers/chat.ts 1904->1918 is the irreducible request-pipeline wiring from #9759 that invokes the Modality Bridge guardrail without moving its implementation into the handler; covered by the 17 Vision Bridge canaries plus the PR-1 focused suite. open-sse/translator/response/openai-responses.ts 1204->1215 is #9168's Responses tool-call argument delta buffering/normalization at the existing translator state-machine chokepoint; covered by its dedicated translator regression tests. Both values are measured by check:file-size (split-newline semantics), and the gate remains frozen at the new exact sizes.",
- "_rebaseline_2026_08_08_toolcall_message_index_collision": "fix(responses-api): tool call after a text message collided on the same output_index. own growth: open-sse/translator/response/openai-responses.ts 1204->1224 (+20, extracted toolCallOutputIndexBase() shared helper so emitToolCall/closeToolCall can no longer compute a tool call's output_index independently and collide with a text message emitted in the same turn). Live incident (2026-08-08, OpenClaw agent): a client that tracks response items by output_index saw the tool call's added/delta/done events land on an index it had already marked complete (the just-closed text message), and silently dropped them — the agent spoke its preamble and never executed the tool call, even though OmniRoute's own recorded responseBody had a complete, valid tool_calls entry. Covered by the new regression test in tests/unit/translator-resp-openai-responses.test.ts reproducing the exact live scenario."
+ "_rebaseline_2026_08_08_toolcall_message_index_collision": "fix(responses-api): tool call after a text message collided on the same output_index. own growth: open-sse/translator/response/openai-responses.ts 1204->1224 (+20, extracted toolCallOutputIndexBase() shared helper so emitToolCall/closeToolCall can no longer compute a tool call's output_index independently and collide with a text message emitted in the same turn). Live incident (2026-08-08, OpenClaw agent): a client that tracks response items by output_index saw the tool call's added/delta/done events land on an index it had already marked complete (the just-closed text message), and silently dropped them — the agent spoke its preamble and never executed the tool call, even though OmniRoute's own recorded responseBody had a complete, valid tool_calls entry. Covered by the new regression test in tests/unit/translator-resp-openai-responses.test.ts reproducing the exact live scenario.",
+ "_rebaseline_2026_08_03_9255_adobe_firefly_durable_sessions": "PR #9255 own cohesive growth: open-sse/services/adobeFireflyClient.ts 2322->2894 adds authenticated-vs-guest IMS classification, browser-risk ARP validation/rebuild, bounded 408 retry/recovery, sticky accepted-session handling, and matching image/video submit recovery at the existing Adobe upstream client chokepoints. This client was already explicitly frozen as a single self-contained upstream integration by #8006/#8510; splitting only the retry/auth helpers now would scatter one request state machine while structural shrink remains tracked in #3501. tests/unit/adobe-firefly.test.ts 871->1136 adds direct regression coverage for guest-token rejection, cookie/ARP rebuilding, 408 retries, sticky accepted ARP reuse, forced auth recovery, and cookie-to-IMS exchange. The obsolete 1179-line managed-Chrome fallback module was deleted rather than rebaselined after the packaged-safe pure-CDP path became authoritative. Focused Adobe suite: 61/61."
}
diff --git a/open-sse/services/adobeFireflyChromeRuntime.ts b/open-sse/services/adobeFireflyChromeRuntime.ts
deleted file mode 100644
index 2b727852ed..0000000000
--- a/open-sse/services/adobeFireflyChromeRuntime.ts
+++ /dev/null
@@ -1,1179 +0,0 @@
-/**
- * Adobe Firefly optional Chrome (CDP) session runtime.
- *
- * Default product path is the same as other OmniRoute web-cookie providers
- * (notion-web, perplexity-web, …): pure HTTP with the pasted Cookie/JWT — NO browser.
- *
- * Browser warm is OPT-IN for proactive use (`ADOBE_FIREFLY_BROWSER_REFRESH=1`) and may
- * also run mid-batch 408 recovery via `allowWithoutEnvOptIn`.
- *
- * **Mode (critical for colligo):** Forter risk scores reject Chrome `--headless=new`.
- * Live verification: headless warm → still 408; off-screen **headed** Chrome → 200.
- * Default is therefore **off-screen headed** (window parked at -32000,-32000 — not visible
- * on a normal desktop). Opt into true headless only with `ADOBE_FIREFLY_CHROME_HEADLESS=1`
- * (known-broken for generate). Debug on-screen: `ADOBE_FIREFLY_CHROME_VISIBLE=1`.
- */
-
-import { spawn, type ChildProcess } from "node:child_process";
-import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
-import { join } from "node:path";
-import {
- buildAdobeArpSessionIdFromCookies,
- extractAdobeForterTimestampMs,
- mergeAdobeCookieHeaders,
- type AdobeFireflySession,
-} from "./adobeFireflySession.ts";
-import {
- extractAdobeCookieHeader,
- isAdobeUserAccessToken,
- looksLikeAdobeJwt,
- decodeAdobeJwtPayload,
-} from "./adobeFireflyClient.ts";
-
-const DEFAULT_CDP_PORT = Number(process.env.ADOBE_FIREFLY_CHROME_CDP_PORT || 9334);
-const PROFILE_DIR_NAME = "adobe-chrome-profile";
-
-type Log = { info?: (...a: unknown[]) => void; warn?: (...a: unknown[]) => void };
-
-type RuntimeState = {
- port: number;
- profileDir: string;
- chromeProc: ChildProcess | null;
- browser: import("playwright").Browser | null;
- context: import("playwright").BrowserContext | null;
- page: import("playwright").Page | null;
- lastWarmAt: number;
- lastCookieSeed: string;
- /** "offscreen" | "visible" | "headless" */
- mode: string;
-};
-
-let runtime: RuntimeState | null = null;
-let warmChain: Promise = Promise.resolve();
-let startingChrome: Promise | null = null;
-/** Temporary mode override (e.g. force a visible window for interactive sign-in). */
-let modeOverride: "offscreen" | "visible" | "headless" | null = null;
-
-/** Prefer off-screen headed (Forter works). Headless is opt-in and usually rejected. */
-function resolveChromeMode(): "offscreen" | "visible" | "headless" {
- if (modeOverride) return modeOverride;
- if (process.env.ADOBE_FIREFLY_CHROME_HEADLESS === "1") return "headless";
- // Legacy alias: ADOBE_FIREFLY_CHROME_HEADED=1 meant "show window"
- if (
- process.env.ADOBE_FIREFLY_CHROME_VISIBLE === "1" ||
- process.env.ADOBE_FIREFLY_CHROME_HEADED === "1"
- ) {
- return "visible";
- }
- return "offscreen";
-}
-
-async function safePageWait(page: import("playwright").Page, ms: number): Promise {
- try {
- if (page.isClosed()) return;
- await page.waitForTimeout(ms);
- } catch {
- /* page closed / target destroyed — caller will re-acquire */
- }
-}
-
-async function ensureLivePage(
- context: import("playwright").BrowserContext,
- preferred: import("playwright").Page | null
-): Promise {
- if (preferred && !preferred.isClosed()) {
- try {
- // Touch the page; if target is dead this throws
- void preferred.url();
- return preferred;
- } catch {
- /* fall through */
- }
- }
- const existing =
- context.pages().find((p) => !p.isClosed() && /firefly\.adobe\.com/i.test(p.url())) ||
- context.pages().find((p) => !p.isClosed());
- if (existing) return existing;
- return context.newPage();
-}
-
-function dataDir(): string {
- return (
- String(process.env.DATA_DIR || process.env.OMNIROUTE_DATA_DIR || "").trim() ||
- join(process.cwd(), ".data")
- );
-}
-
-function profileDir(): string {
- // Prefer LOCALAPPDATA when present so the managed Chrome profile survives restarts.
- const local =
- process.env.LOCALAPPDATA ||
- process.env.HOME ||
- process.env.USERPROFILE ||
- "";
- if (local) {
- const p = join(local, "OmniRoute", PROFILE_DIR_NAME);
- try {
- mkdirSync(p, { recursive: true });
- } catch {
- /* ignore */
- }
- return p;
- }
- const p = join(dataDir(), PROFILE_DIR_NAME);
- try {
- mkdirSync(p, { recursive: true });
- } catch {
- /* ignore */
- }
- return p;
-}
-
-function findChromeExecutable(): string | null {
- if (process.env.CHROME_PATH && existsSync(process.env.CHROME_PATH)) {
- return process.env.CHROME_PATH;
- }
- const candidates = [
- "C:\\\\Program Files\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe",
- "C:\\\\Program Files (x86)\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe",
- join(process.env.LOCALAPPDATA || "", "Google", "Chrome", "Application", "chrome.exe"),
- "/usr/bin/google-chrome",
- "/usr/bin/chromium-browser",
- "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome",
- ];
- for (const c of candidates) {
- if (c && existsSync(c)) return c;
- }
- return null;
-}
-
-async function waitForCdp(port: number, timeoutMs: number): Promise {
- const start = Date.now();
- while (Date.now() - start < timeoutMs) {
- try {
- const r = await fetch(`http://127.0.0.1:${port}/json/version`);
- if (r.ok) return;
- } catch {
- /* retry */
- }
- await new Promise((r) => setTimeout(r, 350));
- }
- throw new Error(`Chrome CDP not ready on port ${port}`);
-}
-
-async function killPortOwner(port: number): Promise {
- if (process.platform !== "win32") return;
- try {
- const { execSync } = await import("node:child_process");
- execSync(
- `powershell -NoProfile -Command "Get-NetTCPConnection -LocalPort ${port} -ErrorAction SilentlyContinue | ForEach-Object { Stop-Process -Id $_.OwningProcess -Force -ErrorAction SilentlyContinue }"`,
- { stdio: "ignore", timeout: 8000 }
- );
- } catch {
- /* ignore */
- }
-}
-
-function parseCookieHeader(cookieHeader: string): Array<{ name: string; value: string }> {
- const out: Array<{ name: string; value: string }> = [];
- for (const part of String(cookieHeader || "").split(";")) {
- const idx = part.indexOf("=");
- if (idx <= 0) continue;
- let name = part.slice(0, idx).trim();
- let value = part.slice(idx + 1).trim();
- try {
- name = decodeURIComponent(name);
- } catch {
- /* keep */
- }
- if (
- (value.startsWith('"') && value.endsWith('"')) ||
- (value.startsWith("'") && value.endsWith("'"))
- ) {
- value = value.slice(1, -1);
- }
- if (!name || /[\r\n\0]/.test(value)) continue;
- out.push({ name, value });
- }
- return out;
-}
-
-/** Detect whether the process listening on `port` was started with --headless. */
-async function isPortChromeHeadless(port: number): Promise {
- if (process.platform !== "win32") return null;
- try {
- const { execSync } = await import("node:child_process");
- const out = execSync(
- `powershell -NoProfile -Command "$c=Get-NetTCPConnection -LocalPort ${port} -State Listen -ErrorAction SilentlyContinue | Select-Object -First 1; if(-not $c){exit 2}; $p=Get-CimInstance Win32_Process -Filter (\\"ProcessId=$($c.OwningProcess)\\"); if($p.CommandLine -match 'headless'){Write-Output 'headless'}else{Write-Output 'headed'}"`,
- { encoding: "utf8", timeout: 8000, stdio: ["ignore", "pipe", "ignore"] }
- ).trim();
- if (out === "headless") return true;
- if (out === "headed") return false;
- return null;
- } catch {
- return null;
- }
-}
-
-async function tryConnectExistingCdp(
- chromium: typeof import("playwright").chromium,
- port: number,
- dir: string,
- desiredMode: string,
- log?: Log
-): Promise {
- try {
- const r = await fetch(`http://127.0.0.1:${port}/json/version`);
- if (!r.ok) return null;
-
- // Never reuse a headless Chrome for recovery — colligo rejects its Forter tokens.
- // Desired "headless" may reuse headless; offscreen/visible must get a headed process.
- if (desiredMode !== "headless") {
- const headless = await isPortChromeHeadless(port);
- if (headless === true) {
- log?.warn?.(
- "ADOBE-FIREFLY",
- `existing CDP on ${port} is headless — killing and restarting as ${desiredMode}`
- );
- await killPortOwner(port);
- return null;
- }
- }
-
- const browser = await chromium.connectOverCDP(`http://127.0.0.1:${port}`);
- const context = browser.contexts()[0] || (await browser.newContext());
- const page = await ensureLivePage(context, null);
- log?.info?.(
- "ADOBE-FIREFLY",
- `reused existing Chrome CDP port=${port} desiredMode=${desiredMode} pages=${context.pages().length}`
- );
- return {
- port,
- profileDir: dir,
- chromeProc: null,
- browser,
- context,
- page,
- lastWarmAt: 0,
- lastCookieSeed: "",
- mode: desiredMode,
- };
- } catch {
- return null;
- }
-}
-
-/**
- * Chrome remembers last window bounds in the profile. Off-screen warms park the window at
- * ~(-32000,-32000) / secondary-monitor coords — a later "visible" sign-in then opens Firefly
- * off-screen and the user sees nothing. Reset placement on disk before a visible spawn.
- */
-function resetChromeWindowPlacementOnDisk(dir: string, log?: Log): void {
- const candidates = [
- join(dir, "Default", "Preferences"),
- join(dir, "Preferences"),
- ];
- const onScreen = {
- bottom: 960,
- left: 80,
- maximized: false,
- right: 1360,
- top: 60,
- work_area_bottom: 1080,
- work_area_left: 0,
- work_area_right: 1920,
- work_area_top: 0,
- };
- for (const path of candidates) {
- if (!existsSync(path)) continue;
- try {
- const raw = readFileSync(path, "utf8");
- const obj = JSON.parse(raw) as Record;
- const browser = (obj.browser && typeof obj.browser === "object"
- ? (obj.browser as Record)
- : {}) as Record;
- browser.window_placement = onScreen;
- browser.window_placement_popup = onScreen;
- obj.browser = browser;
- // Avoid session restore putting us back off-screen.
- if (obj.profile && typeof obj.profile === "object") {
- (obj.profile as Record).exit_type = "Normal";
- (obj.profile as Record).exited_cleanly = true;
- }
- writeFileSync(path, JSON.stringify(obj), "utf8");
- log?.info?.("ADOBE-FIREFLY", `reset Chrome window_placement on disk (${path})`);
- } catch (err) {
- log?.warn?.(
- "ADOBE-FIREFLY",
- `could not reset window_placement: ${err instanceof Error ? err.message : String(err)}`
- );
- }
- }
-}
-
-/** After CDP connect, force the browser window onto the primary work area (visible sign-in). */
-async function forceChromeWindowOnScreen(
- browser: import("playwright").Browser,
- page: import("playwright").Page,
- log?: Log
-): Promise {
- try {
- const cdp = await page.context().newCDPSession(page);
- const { windowId } = (await cdp.send("Browser.getWindowForTarget" as "Browser.getWindowForTarget")) as {
- windowId: number;
- };
- await cdp.send("Browser.setWindowBounds" as "Browser.setWindowBounds", {
- windowId,
- bounds: {
- left: 80,
- top: 60,
- width: 1280,
- height: 900,
- windowState: "normal",
- },
- });
- await page.bringToFront().catch(() => {});
- // Best-effort Windows focus (Chrome can open behind the host app).
- if (process.platform === "win32") {
- try {
- const { execSync } = await import("node:child_process");
- execSync(
- `powershell -NoProfile -Command "$p=Get-Process chrome -ErrorAction SilentlyContinue | Where-Object { $_.MainWindowTitle -match 'Firefly|Adobe|Chrome' } | Select-Object -First 1; if($p){ Add-Type -Name W -Namespace N -MemberDefinition '[DllImport(\\\"user32.dll\\\")] public static extern bool SetForegroundWindow(IntPtr h); [DllImport(\\\"user32.dll\\\")] public static extern bool ShowWindow(IntPtr h,int n);'; [N.W]::ShowWindow($p.MainWindowHandle,9) | Out-Null; [N.W]::SetForegroundWindow($p.MainWindowHandle) | Out-Null }"`,
- { stdio: "ignore", timeout: 5000 }
- );
- } catch {
- /* ignore */
- }
- }
- log?.info?.("ADOBE-FIREFLY", "forced Chrome window on-screen (80,60 1280x900)");
- } catch (err) {
- log?.warn?.(
- "ADOBE-FIREFLY",
- `forceChromeWindowOnScreen failed: ${err instanceof Error ? err.message : String(err)}`
- );
- }
-}
-
-async function ensureChromeStarted(
- log?: Log,
- opts?: { forceRestart?: boolean }
-): Promise {
- const mode = resolveChromeMode();
-
- // Always kill the CDP port on forceRestart (even if in-memory runtime is null — leftover
- // off-screen Chrome from a prior warm is the usual "browser didn't appear" case).
- if (opts?.forceRestart) {
- try {
- await runtime?.browser?.close();
- } catch {
- /* ignore */
- }
- runtime = null;
- await killPortOwner(DEFAULT_CDP_PORT);
- }
-
- if (runtime?.browser && runtime.context) {
- // If cached runtime mode is headless but we need headed Forter, restart.
- if (runtime.mode === "headless" && mode !== "headless") {
- log?.warn?.("ADOBE-FIREFLY", "cached Chrome is headless — restarting headed for Forter");
- try {
- await runtime.browser?.close();
- } catch {
- /* ignore */
- }
- runtime = null;
- await killPortOwner(DEFAULT_CDP_PORT);
- } else {
- try {
- await fetch(`http://127.0.0.1:${runtime.port}/json/version`);
- // Double-check process still headed when we need it
- if (mode !== "headless") {
- const hl = await isPortChromeHeadless(runtime.port);
- if (hl === true) {
- log?.warn?.("ADOBE-FIREFLY", "live CDP became headless — restarting");
- try {
- await runtime.browser?.close();
- } catch {
- /* ignore */
- }
- runtime = null;
- await killPortOwner(DEFAULT_CDP_PORT);
- } else {
- runtime.page = await ensureLivePage(runtime.context, runtime.page);
- return runtime;
- }
- } else {
- runtime.page = await ensureLivePage(runtime.context, runtime.page);
- return runtime;
- }
- } catch {
- try {
- await runtime.browser?.close();
- } catch {
- /* ignore */
- }
- runtime = null;
- }
- }
- }
-
- if (startingChrome) return startingChrome;
-
- if (process.env.ADOBE_FIREFLY_BROWSER_REFRESH === "0") {
- throw new Error("ADOBE_FIREFLY_BROWSER_REFRESH=0");
- }
-
- startingChrome = (async () => {
- const chromePath = findChromeExecutable();
- if (!chromePath) throw new Error("Google Chrome not found (set CHROME_PATH)");
-
- let chromium: typeof import("playwright").chromium;
- try {
- chromium = (await import("playwright")).chromium;
- } catch {
- throw new Error("playwright package not available for CDP connect");
- }
-
- const port = DEFAULT_CDP_PORT;
- const dir = profileDir();
-
- // Prefer reusing a healthy headed CDP (do NOT kill mid-warm — was causing "page closed").
- // Never reuse headless when desired mode is offscreen/visible.
- if (!opts?.forceRestart) {
- const existing = await tryConnectExistingCdp(chromium, port, dir, mode, log);
- if (existing) {
- runtime = existing;
- return existing;
- }
- }
-
- // Kill stale listener before spawn (headless leftover / force restart).
- await killPortOwner(port);
-
- // Visible sign-in: wipe off-screen bounds left by prior off-screen warms.
- if (mode === "visible") {
- resetChromeWindowPlacementOnDisk(dir, log);
- }
-
- // Off-screen headed is the default: Forter accepts real Chrome; headless=new is rejected.
- const args = [
- `--remote-debugging-port=${port}`,
- `--user-data-dir=${dir}`,
- "--no-first-run",
- "--no-default-browser-check",
- "--disable-blink-features=AutomationControlled",
- "--disable-features=TranslateUI",
- "--disable-session-crashed-bubble",
- "--hide-crash-restore-bubble",
- ...(mode === "headless"
- ? ["--headless=new", "--disable-gpu", "--window-size=1280,900"]
- : mode === "offscreen"
- ? [
- "--window-position=-32000,-32000",
- "--window-size=1280,900",
- // Start minimized as extra belt-and-suspenders (Windows may still create a taskbar entry).
- "--start-minimized",
- ]
- : [
- // Explicit on-screen position — profile restore alone is not enough.
- "--window-position=80,60",
- "--window-size=1280,900",
- "--start-maximized",
- ]),
- mode === "visible" ? "https://firefly.adobe.com/" : "https://firefly.adobe.com/generate/image",
- ];
-
- log?.info?.(
- "ADOBE-FIREFLY",
- `starting Chrome CDP profile=${dir} port=${port} mode=${mode} (offscreen=headed parked off-display; visible=on-screen sign-in)`
- );
- const chromeProc = spawn(chromePath, args, {
- stdio: "ignore",
- detached: true,
- // Hide the spawn console only; Chrome UI must remain visible for sign-in.
- windowsHide: mode !== "visible",
- });
- chromeProc.unref();
-
- await waitForCdp(port, 45_000);
- const browser = await chromium.connectOverCDP(`http://127.0.0.1:${port}`);
- const context = browser.contexts()[0] || (await browser.newContext());
- const page = await ensureLivePage(context, null);
-
- if (mode === "visible") {
- await forceChromeWindowOnScreen(browser, page, log);
- }
-
- runtime = {
- port,
- profileDir: dir,
- chromeProc,
- browser,
- context,
- page,
- lastWarmAt: 0,
- lastCookieSeed: "",
- mode,
- };
- return runtime;
- })();
-
- try {
- return await startingChrome;
- } finally {
- startingChrome = null;
- }
-}
-
-async function seedCookies(
- context: import("playwright").BrowserContext,
- cookieHeader: string
-): Promise {
- const pairs = parseCookieHeader(cookieHeader);
- let n = 0;
- for (const { name, value } of pairs) {
- for (const domain of [".adobe.com", "firefly.adobe.com", ".firefly.adobe.com"]) {
- try {
- await context.addCookies([
- { name, value, domain, path: "/", secure: true, sameSite: "Lax" },
- ]);
- n++;
- break;
- } catch {
- /* try next domain */
- }
- }
- }
- return n;
-}
-
-function extractUserJwtFromStorageRaw(raw: string): string {
- const matches = String(raw || "").match(/eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+/g) || [];
- for (const tok of matches) {
- if (looksLikeAdobeJwt(tok) && isAdobeUserAccessToken(tok)) return tok;
- }
- return "";
-}
-
-async function readSpaUserJwt(page: import("playwright").Page): Promise {
- const tokens = await page.evaluate(() => {
- const out: string[] = [];
- for (const key of Object.keys(sessionStorage)) {
- if (!/adobeid_ims_access_token|clio-playground/i.test(key)) continue;
- out.push(sessionStorage.getItem(key) || "");
- }
- return out;
- });
- for (const raw of tokens) {
- const tok = extractUserJwtFromStorageRaw(raw);
- if (tok) return tok;
- }
- // broader scan
- const all = await page.evaluate(() => {
- const out: string[] = [];
- for (const key of Object.keys(sessionStorage)) out.push(sessionStorage.getItem(key) || "");
- return out;
- });
- for (const raw of all) {
- const tok = extractUserJwtFromStorageRaw(raw);
- if (tok) return tok;
- }
- return "";
-}
-
-async function injectUserJwt(page: import("playwright").Page, token: string): Promise {
- if (!token) return;
- await page
- .evaluate((t) => {
- for (const key of Object.keys(sessionStorage)) {
- if (!key.includes("adobeid_ims_access_token")) continue;
- try {
- const obj = JSON.parse(sessionStorage.getItem(key) || "{}") as Record;
- obj.tokenValue = t;
- obj.access_token = t;
- obj.valid = true;
- obj.expire = Date.now() + 20 * 3600 * 1000;
- obj.expires_in = 86400000;
- obj.client_id = "clio-playground-web";
- sessionStorage.setItem(key, JSON.stringify(obj));
- } catch {
- /* skip */
- }
- }
- }, token)
- .catch(() => {});
-}
-
-async function humanize(page: import("playwright").Page): Promise {
- try {
- if (page.isClosed()) return;
- for (let i = 0; i < 16; i++) {
- if (page.isClosed()) return;
- await page.mouse.move(100 + i * 45, 160 + (i % 5) * 35, { steps: 4 });
- await safePageWait(page, 80);
- }
- // Light scroll nudges Forter / passive listeners on real headed Chrome.
- await page.mouse.wheel(0, 240).catch(() => {});
- await safePageWait(page, 200);
- await page.mouse.wheel(0, -120).catch(() => {});
- } catch {
- /* ignore */
- }
-}
-
-/** Poll jar until forterToken timestamp advances past `minTs`, or timeout. */
-async function waitForFresherForter(
- context: import("playwright").BrowserContext,
- minTs: number,
- timeoutMs: number,
- log?: Log
-): Promise {
- const start = Date.now();
- let best = 0;
- while (Date.now() - start < timeoutMs) {
- const cookie = await jarCookieHeader(context);
- const ts = extractAdobeForterTimestampMs(cookie);
- if (ts > best) best = ts;
- if (ts > minTs) {
- log?.info?.(
- "ADOBE-FIREFLY",
- `Chrome forter refreshed (ts=${ts}, deltaMs=${ts - minTs})`
- );
- return ts;
- }
- await new Promise((r) => setTimeout(r, 1500));
- }
- log?.warn?.(
- "ADOBE-FIREFLY",
- `Chrome forter did not advance past ${minTs} within ${timeoutMs}ms (best=${best})`
- );
- return best;
-}
-
-async function jarCookieHeader(context: import("playwright").BrowserContext): Promise {
- const jar = await context.cookies();
- // Prefer firefly-relevant cookies; keep full jar for rebuild pieces
- return jar.map((c) => `${c.name}=${c.value}`).join("; ");
-}
-
-async function buildArpFromContext(
- context: import("playwright").BrowserContext,
- page: import("playwright").Page
-): Promise<{ arp: string; cookie: string }> {
- const cookie = await jarCookieHeader(context);
- const ls = await page
- .evaluate(() => ({
- bfp: localStorage.getItem("bfp") || "",
- fpjs: localStorage.getItem("fpjs") || "",
- }))
- .catch(() => ({ bfp: "", fpjs: "" }));
- let blob = cookie;
- if (ls.bfp && !/(?:^|;\s*)bfp=/.test(blob)) blob = mergeAdobeCookieHeaders(blob, `bfp=${ls.bfp}`);
- if (ls.fpjs && !/(?:^|;\s*)fpjs=/.test(blob)) {
- blob = mergeAdobeCookieHeaders(blob, `fpjs=${encodeURIComponent(ls.fpjs)}`);
- }
- const arp =
- buildAdobeArpSessionIdFromCookies(blob, {
- bfp: ls.bfp || undefined,
- fpjs: ls.fpjs || undefined,
- }) || "";
- return { arp, cookie: extractAdobeCookieHeader(blob) || blob };
-}
-
-/**
- * Warm (or create) the durable Chrome Firefly session.
- * Returns accessToken + cookie + arpSessionId ready for generate-async.
- */
-export async function warmAdobeFireflyViaChrome(opts: {
- cookie: string;
- accessToken?: string;
- log?: Log;
- /** Wait for interactive login if only guest JWT is present (ms, 0 = don't wait). */
- waitForLoginMs?: number;
- /**
- * Mid-batch 408 recovery: allow warm without ADOBE_FIREFLY_BROWSER_REFRESH=1.
- * Uses off-screen headed Chrome by default (Forter-safe; no normal-desktop window).
- */
- allowWithoutEnvOptIn?: boolean;
- /** When true (or ADOBE_FIREFLY_CHROME_PING=1), prove ARP with in-page generate-async. */
- proveWithPing?: boolean;
-}): Promise {
- // Kill switch
- if (process.env.ADOBE_FIREFLY_BROWSER_REFRESH === "0") return null;
- // Default OFF for proactive use; recovery may pass allowWithoutEnvOptIn.
- if (!opts.allowWithoutEnvOptIn && process.env.ADOBE_FIREFLY_BROWSER_REFRESH !== "1") return null;
- if (process.env.NODE_ENV === "test" || process.env.VITEST || process.env.NODE_TEST_CONTEXT) {
- return null;
- }
-
- const run = warmChain.then(async () => {
- const log = opts.log;
- const cookieIn = extractAdobeCookieHeader(opts.cookie) || opts.cookie;
- if (!cookieIn?.trim() && !opts.accessToken) return null;
-
- const forterBefore = extractAdobeForterTimestampMs(cookieIn);
- // Recovery path always prefers a fresh headed Chrome (stale headless CDP is poison).
- const rt = await ensureChromeStarted(log, {
- forceRestart: Boolean(opts.allowWithoutEnvOptIn) || process.env.ADOBE_FIREFLY_CHROME_FORCE_RESTART === "1",
- });
- const context = rt.context!;
- let page = await ensureLivePage(context, rt.page);
-
- if (cookieIn && cookieIn !== rt.lastCookieSeed) {
- const n = await seedCookies(context, cookieIn);
- rt.lastCookieSeed = cookieIn;
- log?.info?.("ADOBE-FIREFLY", `Chrome seeded ${n} cookie entries`);
- }
-
- // Navigate / reload with page-closed recovery (prior flaky "Target page closed").
- const gotoFirefly = async () => {
- page = await ensureLivePage(context, page);
- if (!/firefly\.adobe\.com/i.test(page.url())) {
- await page.goto("https://firefly.adobe.com/generate/image", {
- waitUntil: "domcontentloaded",
- timeout: 90_000,
- });
- } else {
- await page.reload({ waitUntil: "domcontentloaded", timeout: 90_000 }).catch(async () => {
- page = await ensureLivePage(context, null);
- await page.goto("https://firefly.adobe.com/generate/image", {
- waitUntil: "domcontentloaded",
- timeout: 90_000,
- });
- });
- }
- };
-
- await gotoFirefly();
- await safePageWait(page, 8_000);
- await humanize(page);
-
- let jwt = await readSpaUserJwt(page).catch(() => "");
- if (!jwt && opts.accessToken && isAdobeUserAccessToken(opts.accessToken)) {
- page = await ensureLivePage(context, page);
- await injectUserJwt(page, opts.accessToken);
- await page.reload({ waitUntil: "domcontentloaded", timeout: 90_000 }).catch(() => {});
- await safePageWait(page, 6_000);
- await humanize(page);
- jwt = (await readSpaUserJwt(page).catch(() => "")) || opts.accessToken;
- log?.info?.("ADOBE-FIREFLY", "Chrome injected cached user JWT into SPA sessionStorage");
- }
-
- // Wait for interactive login if still no user JWT (one-time profile SSO)
- const waitMs = opts.waitForLoginMs ?? Number(process.env.ADOBE_FIREFLY_LOGIN_WAIT_MS || 0);
- if (!jwt && waitMs > 0) {
- log?.warn?.(
- "ADOBE-FIREFLY",
- `No user JWT yet — sign in to Firefly in the Chrome window (wait ${Math.round(waitMs / 1000)}s)`
- );
- const start = Date.now();
- while (Date.now() - start < waitMs) {
- await safePageWait(page, 2000);
- page = await ensureLivePage(context, page);
- jwt = await readSpaUserJwt(page).catch(() => "");
- if (jwt) break;
- }
- }
-
- if (!jwt && opts.accessToken && isAdobeUserAccessToken(opts.accessToken)) {
- jwt = opts.accessToken;
- }
- if (!jwt || !isAdobeUserAccessToken(jwt)) {
- log?.warn?.("ADOBE-FIREFLY", "Chrome warm: still no AdobeID user JWT (cookie-only guest)");
- // Still return ARP if possible — caller may already have JWT
- if (!opts.accessToken) return null;
- jwt = opts.accessToken;
- }
-
- // Give Forter SDK time to mint a NEW forterToken (stale paste is the usual 408 root cause).
- const forterWaitMs = Number(process.env.ADOBE_FIREFLY_FORTER_WAIT_MS || 45_000);
- await waitForFresherForter(context, forterBefore, forterWaitMs, log);
-
- // Second humanize + short settle after token land
- page = await ensureLivePage(context, page);
- await humanize(page);
- await safePageWait(page, 2_000);
-
- let { arp, cookie } = await buildArpFromContext(context, page);
- if (!arp) {
- log?.warn?.("ADOBE-FIREFLY", "Chrome warm: could not rebuild ARP from jar — one more reload");
- await gotoFirefly();
- await safePageWait(page, 8_000);
- await humanize(page);
- await waitForFresherForter(context, forterBefore, 20_000, log);
- ({ arp, cookie } = await buildArpFromContext(context, page));
- }
- if (!arp) {
- log?.warn?.("ADOBE-FIREFLY", "Chrome warm: could not rebuild ARP from jar");
- return null;
- }
-
- // Prove colligo accepts this ARP. Default ON for recovery path; env can force either way.
- const shouldPing =
- opts.proveWithPing === true ||
- process.env.ADOBE_FIREFLY_CHROME_PING === "1" ||
- (opts.allowWithoutEnvOptIn && process.env.ADOBE_FIREFLY_CHROME_PING !== "0");
- if (shouldPing) {
- page = await ensureLivePage(context, page);
- const ok = await pingGenerateInPage(page, jwt, arp, log);
- if (!ok) {
- log?.warn?.(
- "ADOBE-FIREFLY",
- "Chrome ping generate failed — waiting for forter once more and rebuilding ARP"
- );
- await waitForFresherForter(context, extractAdobeForterTimestampMs(cookie), 20_000, log);
- ({ arp, cookie } = await buildArpFromContext(context, page));
- if (arp) {
- page = await ensureLivePage(context, page);
- const ok2 = await pingGenerateInPage(page, jwt, arp, log);
- if (!ok2) {
- log?.warn?.("ADOBE-FIREFLY", "Chrome ping still failed — returning ARP for node retry");
- }
- }
- }
- }
-
- rt.page = page;
- rt.lastWarmAt = Date.now();
- const ftrTs = extractAdobeForterTimestampMs(cookie);
- log?.info?.(
- "ADOBE-FIREFLY",
- `Chrome warm OK (mode=${rt.mode}, arpLen=${arp.length}, forterTs=${ftrTs || 0}, forterDeltaMs=${ftrTs && forterBefore ? ftrTs - forterBefore : "n/a"}, user=${String(decodeAdobeJwtPayload(jwt)?.user_id || "").slice(0, 20)})`
- );
-
- return {
- accessToken: jwt,
- cookie,
- arpSessionId: arp,
- tokenExpiresAt: (() => {
- const p = decodeAdobeJwtPayload(jwt);
- const created = Number(p?.created_at || 0);
- const exp = Number(p?.expires_in || 0);
- return created && exp ? created + exp : Date.now() + 20 * 3600_000;
- })(),
- updatedAt: Date.now(),
- fingerprint: "chrome",
- source: "browser" as const,
- };
- });
-
- // Serialize warms
- warmChain = run.then(
- () => undefined,
- () => undefined
- );
- try {
- return await run;
- } catch (err) {
- opts.log?.warn?.(
- "ADOBE-FIREFLY",
- `Chrome warm failed: ${err instanceof Error ? err.message : String(err)}`
- );
- // Soft-reset page/browser handle but do not kill Chrome process — reuse next warm.
- if (runtime) {
- runtime.page = null;
- try {
- await runtime.browser?.close();
- } catch {
- /* ignore */
- }
- runtime.browser = null;
- runtime.context = null;
- }
- runtime = null;
- return null;
- }
-}
-
-/**
- * Wipe Adobe SSO from the managed profile so "Add Account" can log into a *new* identity
- * instead of silently reusing the previous Adobe session.
- */
-async function clearAdobeBrowserSession(
- context: import("playwright").BrowserContext,
- page: import("playwright").Page,
- log?: Log
-): Promise {
- try {
- await context.clearCookies();
- } catch {
- /* ignore */
- }
- try {
- await page.goto("https://firefly.adobe.com/", { waitUntil: "domcontentloaded", timeout: 60_000 });
- await page
- .evaluate(() => {
- try {
- sessionStorage.clear();
- } catch {
- /* ignore */
- }
- try {
- localStorage.clear();
- } catch {
- /* ignore */
- }
- })
- .catch(() => {});
- } catch {
- /* ignore */
- }
- // Best-effort IMS logout so the next load shows the sign-in UI.
- try {
- await page.goto(
- "https://auth.services.adobe.com/en_US/index.html?callback=https%3A%2F%2Ffirefly.adobe.com%2F",
- {
- waitUntil: "domcontentloaded",
- timeout: 45_000,
- }
- );
- await safePageWait(page, 1500);
- } catch {
- /* ignore */
- }
- log?.info?.("ADOBE-FIREFLY", "sign-in: cleared prior Adobe session for a fresh login");
-}
-
-/**
- * Interactive one-time sign-in for the "browser session" credential model.
- * Opens a VISIBLE managed Chrome (persistent profile), navigates to Firefly, and waits for the
- * user to log in. Returns the IMS JWT + cookie jar so generate works immediately without
- * depending on sessionStorage surviving a browser close.
- * Never throws — returns { success:false } on timeout / unavailable.
- */
-export async function loginAdobeFireflyViaChrome(opts: {
- cookie?: string;
- /** Max time to wait for the user to complete login (ms). Default 5 min. */
- waitForLoginMs?: number;
- /**
- * When true (default for "Add Account"), wipe the prior Adobe SSO so a *new* account can be
- * signed in instead of reopening the previous logged-in profile.
- */
- freshSession?: boolean;
- log?: Log;
-}): Promise<{
- success: boolean;
- account?: string;
- accessToken?: string;
- cookie?: string;
- arpSessionId?: string;
-}> {
- if (process.env.ADOBE_FIREFLY_BROWSER_REFRESH === "0") {
- return { success: false };
- }
- const log = opts.log;
- const prev = modeOverride;
- modeOverride = "visible";
- const fresh = opts.freshSession !== false; // default true for multi-account Add Account
- try {
- // Fresh visible window (a cached off-screen CDP would be parked off-display for login).
- // forceRestart ALWAYS kills port 9334 + restarts with on-screen bounds.
- const rt = await ensureChromeStarted(log, { forceRestart: true });
- const context = rt.context!;
- let page = await ensureLivePage(context, rt.page);
-
- // Re-assert on-screen + foreground (profile may re-apply bad bounds after first paint).
- await forceChromeWindowOnScreen(rt.browser!, page, log);
-
- if (fresh) {
- await clearAdobeBrowserSession(context, page, log);
- page = await ensureLivePage(context, null);
- rt.lastCookieSeed = "";
- } else {
- const cookieIn = opts.cookie ? extractAdobeCookieHeader(opts.cookie) || opts.cookie : "";
- if (cookieIn) {
- const n = await seedCookies(context, cookieIn);
- rt.lastCookieSeed = cookieIn;
- log?.info?.("ADOBE-FIREFLY", `sign-in: seeded ${n} cookie entries as a hint`);
- }
- }
-
- await page
- .goto("https://firefly.adobe.com/", { waitUntil: "domcontentloaded", timeout: 90_000 })
- .catch(() => {});
- page = await ensureLivePage(context, page);
- await forceChromeWindowOnScreen(rt.browser!, page, log);
- log?.info?.(
- "ADOBE-FIREFLY",
- `sign-in: Chrome window open ON-SCREEN (fresh=${fresh}) — waiting for Adobe login…`
- );
-
- const waitMs = opts.waitForLoginMs ?? Number(process.env.ADOBE_FIREFLY_LOGIN_WAIT_MS || 300_000);
- const start = Date.now();
- let jwt = "";
- while (Date.now() - start < waitMs) {
- await safePageWait(page, 2500);
- page = await ensureLivePage(context, page);
- jwt = await readSpaUserJwt(page).catch(() => "");
- if (jwt && isAdobeUserAccessToken(jwt)) break;
- }
- const ok = Boolean(jwt && isAdobeUserAccessToken(jwt));
- const account = ok ? String(decodeAdobeJwtPayload(jwt)?.user_id || "") : undefined;
-
- // Capture durable credentials BEFORE closing the window (sessionStorage JWT dies with the tab).
- let cookie = "";
- let arpSessionId = "";
- if (ok) {
- try {
- const built = await buildArpFromContext(context, page);
- cookie = extractAdobeCookieHeader(built.cookie) || built.cookie || "";
- arpSessionId = built.arp || "";
- } catch {
- cookie = (await jarCookieHeader(context).catch(() => "")) || "";
- }
- }
-
- log?.info?.(
- "ADOBE-FIREFLY",
- ok
- ? `sign-in OK (account=${account?.slice(0, 24)}, cookieLen=${cookie.length}, arpLen=${arpSessionId.length})`
- : "sign-in timed out — no AdobeID session"
- );
-
- // Close the visible window; the persistent profile keeps the SSO for off-screen warms.
- try {
- await rt.browser?.close();
- } catch {
- /* ignore */
- }
- runtime = null;
- return {
- success: ok,
- account,
- accessToken: ok ? jwt : undefined,
- cookie: ok ? cookie : undefined,
- arpSessionId: ok ? arpSessionId : undefined,
- };
- } catch (err) {
- log?.warn?.(
- "ADOBE-FIREFLY",
- `sign-in failed: ${err instanceof Error ? err.message : String(err)}`
- );
- try {
- await runtime?.browser?.close();
- } catch {
- /* ignore */
- }
- runtime = null;
- return { success: false };
- } finally {
- modeOverride = prev;
- }
-}
-
-async function pingGenerateInPage(
- page: import("playwright").Page,
- token: string,
- arp: string,
- log?: Log
-): Promise {
- try {
- const res = await page.evaluate(
- async ({ token, arp }) => {
- const claims = JSON.parse(
- atob(token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/"))
- ) as { user_id?: string };
- const prompt = "ping";
- const data = new TextEncoder().encode(String(claims.user_id || "") + "-" + prompt);
- const hash = await crypto.subtle.digest("SHA-256", data);
- const nonce = [...new Uint8Array(hash)].map((b) => b.toString(16).padStart(2, "0")).join("");
- const r = await fetch("https://firefly-3p.ff.adobe.io/v2/3p-images/generate-async", {
- method: "POST",
- headers: {
- Authorization: "Bearer " + token,
- "x-api-key": "clio-playground-web",
- "content-type": "application/json",
- accept: "*/*",
- "x-nonce": nonce,
- "x-arp-session-id": arp,
- },
- credentials: "include",
- body: JSON.stringify({
- n: 1,
- seeds: [1],
- output: { storeInputs: true },
- prompt,
- referenceBlobs: [],
- modelSpecificPayload: { size: "auto" },
- modelId: "gpt-image",
- modelVersion: "2",
- generationMetadata: { module: "text2image", submodule: "ff-image-generate" },
- generationSettings: { detailLevel: 1 },
- }),
- });
- return { status: r.status, body: (await r.text()).slice(0, 120) };
- },
- { token, arp }
- );
- log?.info?.("ADOBE-FIREFLY", `Chrome ping generate status=${res.status}`);
- return res.status === 200 || res.status === 202;
- } catch (e) {
- log?.warn?.("ADOBE-FIREFLY", `Chrome ping error: ${e instanceof Error ? e.message : String(e)}`);
- return false;
- }
-}
-
-/**
- * Submit generate-async inside the warmed Chrome page (same TLS/cookie jar as SPA).
- * Falls back to null so caller can use node fetch with the warmed ARP.
- */
-export async function adobeFireflyGenerateInChrome(opts: {
- accessToken: string;
- arpSessionId: string;
- payload: Record;
- prompt: string;
- log?: Log;
-}): Promise<{ status: number; body: string; headers: Record } | null> {
- if (!runtime?.page) return null;
- try {
- const res = await runtime.page.evaluate(
- async ({ token, arp, payload, prompt }) => {
- const claims = JSON.parse(
- atob(token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/"))
- ) as { user_id?: string };
- const data = new TextEncoder().encode(
- String(claims.user_id || "") + "-" + String(prompt || "").slice(0, 256)
- );
- const hash = await crypto.subtle.digest("SHA-256", data);
- const nonce = [...new Uint8Array(hash)].map((b) => b.toString(16).padStart(2, "0")).join("");
- const r = await fetch("https://firefly-3p.ff.adobe.io/v2/3p-images/generate-async", {
- method: "POST",
- headers: {
- Authorization: "Bearer " + token,
- "x-api-key": "clio-playground-web",
- "content-type": "application/json",
- accept: "*/*",
- "x-nonce": nonce,
- "x-arp-session-id": arp,
- },
- credentials: "include",
- body: JSON.stringify(payload),
- });
- const headers: Record = {};
- r.headers.forEach((v, k) => {
- headers[k] = v;
- });
- return { status: r.status, body: await r.text(), headers };
- },
- {
- token: opts.accessToken,
- arp: opts.arpSessionId,
- payload: opts.payload,
- prompt: opts.prompt,
- }
- );
- return res;
- } catch (e) {
- opts.log?.warn?.(
- "ADOBE-FIREFLY",
- `in-Chrome generate failed: ${e instanceof Error ? e.message : String(e)}`
- );
- return null;
- }
-}
-
-/** Test helper */
-export function __resetAdobeFireflyChromeRuntimeForTests(): void {
- runtime = null;
- warmChain = Promise.resolve();
-}