maint: follow-up cherry-pick fix-in-place #9549 (conflict-resolved fallback) (#9881)

* fix(adobe-firefly): open browser sign-in and resolve provider slug in /login

POST /api/providers/[id]/login passed the connection DB id to
inAppLoginService.startLogin, but that service looks up the provider by
slug in TOKEN_EXTRACTION_CONFIGS. The lookup always missed and returned
"No extraction config" without launching a browser — so the VibeProxy
"Sign in" button for Adobe Firefly (and every other web-cookie provider)
never opened a browser.

Adobe Firefly additionally had no extraction config because its IMS JWT
is never in cookies/localStorage — it only rides on the Authorization:
Bearer header of firefly-3p.ff.adobe.io XHRs.

- Resolve the provider slug from the connection row and pass the slug
  (not the DB id) to inAppLoginService.startLogin.
- Add open-sse/services/adobeFireflyBrowserLogin.ts: a Playwright
  service that launches a visible browser at firefly.adobe.com and
  intercepts firefly-3p requests to capture the IMS JWT + sherlockToken
  cookie. Wire it into the /login route for the adobe-firefly slug.
- Fix latent bug: updateProviderConnection reads camelCase keys
  (apiKey, providerSpecificData), so the previous snake_case call never
  persisted extracted credentials.

* fix(adobe-firefly): open browser sign-in and resolve provider slug in /login

POST /api/providers/[id]/login passed the connection DB id to
inAppLoginService.startLogin, but TOKEN_EXTRACTION_CONFIGS is keyed by
provider slug — so browser login never launched for web-cookie providers.

Adobe Firefly also cannot use cookie extraction: the IMS JWT only appears
on Authorization headers to firefly-3p.ff.adobe.io. Add a dedicated
Playwright interceptor and persist credentials with camelCase keys that
updateProviderConnection actually reads.

* fix(adobe-firefly): use system Chrome/Edge CDP for browser sign-in

Playwright is not available inside the pkg-packaged VibeProxyServices.exe,
so import('playwright') always failed with 'Playwright not installed' and
never opened a window. Launch Chrome/Edge with --remote-debugging-port and
capture the firefly-3p Authorization Bearer via pure CDP WebSocket instead.

* fix(adobe-firefly): live x-arp-session-id / Arkose wire (stop 408 under load)

Browser generate-async requires x-arp-session-id as base64({sid,ark,ftr}) with a
real Arkose blob (sherlockToken). JWT alone frequently returns colligo HTTP 408
system under load while credits still work.

- Match live ftr magic __UDF43-m4_31ck + Arkose pk in synthetic ARP fallback
- Ranked extract of sherlockToken / x-arp from Cookie, HAR, fetch() paste, and
  space-joined JWT+ARP (PasswordBox newline collapse)
- Reuse one ARP for storage upload + generate-async
- Clearer 408 errors when browser ARP is missing vs stale
- Unit suite 42/42

* fix(adobe-firefly): durable session ARP rebuild and aux_sid false-positive

Rebuild x-arp-session-id from forterToken/arkose/ff_session_guid instead of
ranking long Cookie pairs (e.g. aux_sid=…) as opaque ARP, which caused colligo
HTTP 408. Cache IMS JWT + cookie sessions, rotate ARP on 408 retries, and keep
Playwright warm-up opt-in only (headless Forter is rejected).

Also expand synthetic ARP shape with bfp/fpjs to match live successful captures.

* fix(adobe-firefly): durable session, off-screen Chrome recovery, browser sign-in

Rebuild x-arp-session-id from Cookie pieces (sid/ark/forter) so aux_sid is never
sent as ARP. Sticky ARP + submit spacing reduce mid-batch colligo 408 thrash.

Add optional managed Chrome warm (off-screen headed by default; Forter rejects
headless) and POST /api/providers/{id}/login browser sign-in that returns JWT+Cookie
after a fresh SSO. Visible sign-in resets off-screen window placement and clears
prior Adobe session when adding another account.

* fix(adobe-firefly): renew sessions through durable CDP

* fix(adobe-firefly): isolate browser sessions per account

* fix(adobe-firefly): make account login fresh and deterministic

* chore(adobe-firefly): remove obsolete browser fallback

* docs(adobe-firefly): document renewal controls

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

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

- CDP warm: clear Firefly origin storage + risk cookies (keep SSO); require
  forter age under 10 minutes on loop and timeout paths; dual CDP queues;
  await Runtime.runIfWaitingForDebugger; profile-lock launch retries
- Session: connectionId fingerprint; write-back JWT+Cookie; warm-fail
  cooldown; fail closed risk_session_stale when forter is known-stale
- Client: submit gate around generate-async; max 2 attempts when forter
  known-stale; poll 401 one refresh; pass sessionBrowserKey through handlers
- Login route: pure system Chrome/Edge CDP only; camelCase credential persist
- Unit: browser-login + firefly suites green (60)

---------

Co-authored-by: artickc <artur1992123@mail.ru>
This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-08-09 10:07:17 -03:00
committed by GitHub
parent 5eba045175
commit 57fb90d734
12 changed files with 5143 additions and 929 deletions

View File

@@ -20,7 +20,142 @@ function resolveProviderSlug(connection: Record<string, unknown> | null): string
return "";
}
// ─── POST: Start login flow ────────────────────────────────────────────────
function isAdobeFireflyProvider(
connection: { provider?: unknown } | null,
providerSlug: string
): boolean {
const raw = String(connection?.provider || "").trim();
return ADOBE_FIREFLY_SLUGS.has(raw) || ADOBE_FIREFLY_SLUGS.has(providerSlug);
}
/**
* Persist JWT + Cookie the way desktop clients (and generate) expect:
* multi-line api_key, plus camelCase providerSpecificData for updateProviderConnection.
*/
async function persistAdobeFireflyCredentials(
connectionId: string,
opts: {
accessToken?: string;
cookie?: string;
account?: string;
arpSessionId?: string;
}
): Promise<{
accessToken: string;
cookie: string;
credential: string;
account: string;
}> {
const accessToken = String(opts.accessToken || "").trim();
const cookie = String(opts.cookie || "").trim();
const account = String(opts.account || "").trim();
const credential =
accessToken && cookie
? `${accessToken}\n${cookie}`
: accessToken ||
cookie ||
JSON.stringify({
mode: "browser-profile",
account,
signedInAt: Date.now(),
});
const marker = {
mode: "browser-profile",
account,
signedInAt: Date.now(),
arpSessionId: String(opts.arpSessionId || ""),
};
try {
// camelCase only — updateProviderConnection / encryptConnectionFields read apiKey +
// providerSpecificData (snake_case keys are silently ignored and never persisted).
await updateProviderConnection(connectionId, {
apiKey: credential,
providerSpecificData: {
...marker,
cookie: cookie || credential,
access_token: accessToken || undefined,
},
});
} catch {
/* non-fatal — return credentials to the host app either way */
}
return { accessToken, cookie, credential, account };
}
function adobeFireflySuccessResponse(data: {
accessToken: string;
cookie: string;
credential: string;
account: string;
arpSessionId?: string;
via: "pure-cdp";
}): NextResponse {
return NextResponse.json({
success: true,
account: data.account || undefined,
accessToken: data.accessToken || undefined,
cookie: data.cookie || undefined,
arpSessionId: data.arpSessionId || undefined,
credential: data.credential,
credentials: {
access_token: data.accessToken || undefined,
cookie: data.cookie || undefined,
},
via: data.via,
persisted: true,
});
}
/**
* Adobe Firefly browser sign-in:
* pure system Chrome/Edge CDP only (packaged-safe, no Playwright/browser bundle).
*/
async function loginAdobeFirefly(
connectionId: string,
body: { timeout?: unknown; freshSession?: unknown }
): Promise<NextResponse> {
const timeout = typeof body.timeout === "number" ? body.timeout : undefined;
const freshSession = typeof body.freshSession === "boolean" ? body.freshSession : true;
// Pure system-browser CDP is the packaged-safe implementation. Do not open a second browser
// after failure: it creates ambiguous success/error races and the packaged runtime has no
// reliable Playwright browser bundle.
// startAdobeFireflyBrowserLogin always kills its Chrome tree in `finally` (no orphans).
try {
const { startAdobeFireflyBrowserLogin } =
await import("@omniroute/open-sse/services/adobeFireflyBrowserLogin.ts");
const pure = await startAdobeFireflyBrowserLogin(timeout, {
sessionKey: connectionId,
freshSession,
});
if (pure.success && pure.credentials?.accessToken) {
const persisted = await persistAdobeFireflyCredentials(connectionId, {
accessToken: pure.credentials.accessToken,
cookie: pure.credentials.cookie,
account: pure.account,
});
return adobeFireflySuccessResponse({
...persisted,
via: "pure-cdp",
});
}
return NextResponse.json(
{
success: false,
error: pure.error || "Adobe Firefly sign-in did not capture an authenticated IMS JWT.",
},
{ status: 400 }
);
} catch (err) {
const msg = sanitizeErrorMessage(err instanceof Error ? err.message : err);
return NextResponse.json({ success: false, error: msg }, { status: 400 });
}
}
// --- POST: Start login flow -------------------------------------------------
export async function POST(
req: NextRequest,
@@ -35,69 +170,35 @@ export async function POST(
return NextResponse.json({ success: false, error: "Provider not found" }, { status: 404 });
}
const body = await req.json().catch(() => ({}));
const timeout = typeof body.timeout === "number" ? body.timeout : undefined;
const body = (await req.json().catch(() => ({}))) as {
timeout?: unknown;
freshSession?: unknown;
};
const providerSlug = resolveProviderSlug(provider as Record<string, unknown>);
try {
// Adobe Firefly is special: the IMS JWT is only ever in the Authorization
// header of firefly-3p.ff.adobe.io XHRs (never cookies/localStorage), so
// the generic cookie-extraction service cannot capture it. Use a dedicated
// Playwright service that intercepts that request instead.
if (ADOBE_FIREFLY_SLUGS.has(providerSlug)) {
const { startAdobeFireflyBrowserLogin } =
await import("@omniroute/open-sse/services/adobeFireflyBrowserLogin.ts");
const fireflyResult = await startAdobeFireflyBrowserLogin(timeout);
if (fireflyResult.success && fireflyResult.credentials) {
const credentials = fireflyResult.credentials;
try {
// Store the JWT in apiKey (where resolveAdobeAccessToken looks first)
// and the cookie + access_token in providerSpecificData (camelCase —
// updateProviderConnection ignores snake_case keys).
const providerSpecificData: Record<string, string> = {};
if (credentials.accessToken) {
providerSpecificData.access_token = credentials.accessToken;
}
if (credentials.cookie) {
providerSpecificData.cookie = credentials.cookie;
}
await updateProviderConnection(id, {
apiKey: credentials.accessToken || "",
providerSpecificData,
});
return NextResponse.json({
success: true,
accessToken: credentials.accessToken || "",
cookie: credentials.cookie || "",
account: fireflyResult.account || "",
credentials: providerSpecificData,
persisted: true,
});
} catch (err) {
const msg = sanitizeErrorMessage(err instanceof Error ? err.message : err);
return NextResponse.json(
{ success: false, error: `Extracted but failed to persist: ${msg}` },
{ status: 500 }
);
}
}
// Adobe Firefly: dedicated JWT capture (never cookies/localStorage alone).
if (isAdobeFireflyProvider(provider as { provider?: unknown }, providerSlug)) {
try {
return await loginAdobeFirefly(id, body);
} catch (err) {
const msg = sanitizeErrorMessage(err instanceof Error ? err.message : err);
return NextResponse.json(
{ success: false, error: fireflyResult.error || "Adobe Firefly sign-in failed" },
{ status: 400 }
{ success: false, error: `Adobe Firefly sign-in error: ${msg}` },
{ status: 500 }
);
}
}
try {
// Generic web-cookie path: pass the provider SLUG (not the DB id) so
// TOKEN_EXTRACTION_CONFIGS can find the extraction config.
// Bug: the previous code passed `id` (connection UUID), so the lookup always
// missed and returned "No extraction config" without launching a browser.
const { inAppLoginService } = await import("@omniroute/open-sse/services/inAppLoginService.ts");
const result = await inAppLoginService.startLogin(providerSlug || id, { timeout });
const result = await inAppLoginService.startLogin(providerSlug || id, {
timeout: typeof body.timeout === "number" ? body.timeout : undefined,
});
// Persist credentials if extraction succeeded
if (result.success && result.credentials) {