From 2c80eccc2f29c33c0053fceec1908755214f0b36 Mon Sep 17 00:00:00 2001 From: artickc Date: Mon, 3 Aug 2026 02:17:35 +0300 Subject: [PATCH] chore(adobe-firefly): remove obsolete browser fallback --- config/quality/file-size-baseline.json | 6 +- .../services/adobeFireflyChromeRuntime.ts | 1179 ----------------- 2 files changed, 2 insertions(+), 1183 deletions(-) delete mode 100644 open-sse/services/adobeFireflyChromeRuntime.ts 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 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(); -}