mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-11 17:52:31 +03:00
fix(adobe-firefly): isolate browser sessions per account
This commit is contained in:
@@ -13,9 +13,11 @@
|
||||
* Protocol over WebSocket — zero Playwright dependency.
|
||||
*/
|
||||
import { spawn, type ChildProcess } from "node:child_process";
|
||||
import { existsSync, mkdirSync } from "node:fs";
|
||||
import { createHash } from "node:crypto";
|
||||
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
||||
import { createServer } from "node:net";
|
||||
import { join } from "node:path";
|
||||
import { decodeAdobeJwtPayload, isAdobeUserAccessToken } from "./adobeFireflyClient.ts";
|
||||
import { sanitizeErrorMessage } from "../utils/error.ts";
|
||||
|
||||
const FIREFLY_HOME_URL = "https://firefly.adobe.com/";
|
||||
@@ -34,7 +36,7 @@ export interface AdobeFireflyBrowserLoginResult {
|
||||
success: boolean;
|
||||
credentials?: { accessToken?: string; cookie?: string };
|
||||
arpSessionId?: string;
|
||||
/** Best-effort Adobe account label (email or user id) decoded from the JWT. */
|
||||
/** Human-readable Adobe account label resolved from IMS userinfo. */
|
||||
account?: string;
|
||||
error?: string;
|
||||
}
|
||||
@@ -52,17 +54,28 @@ type AdobeFireflyBrowserLog = {
|
||||
|
||||
let cdpBrowserChain: Promise<void> = Promise.resolve();
|
||||
|
||||
/**
|
||||
* A non-default profile is required by Chrome 136+ for remote debugging. Keep this profile
|
||||
* durable: Adobe IMS SSO lives here and is what lets a background warm mint the next user JWT.
|
||||
*/
|
||||
export function resolveAdobeFireflyBrowserProfileDir(): string {
|
||||
function resolveAdobeFireflyDataRoot(): string {
|
||||
const dataRoot =
|
||||
String(process.env.DATA_DIR || process.env.OMNIROUTE_DATA_DIR || "").trim() ||
|
||||
(process.env.LOCALAPPDATA
|
||||
? join(process.env.LOCALAPPDATA, "OmniRoute")
|
||||
: join(process.cwd(), ".data"));
|
||||
const profile = join(dataRoot, "adobe-chrome-profile");
|
||||
mkdirSync(dataRoot, { recursive: true });
|
||||
return dataRoot;
|
||||
}
|
||||
|
||||
export function adobeFireflyBrowserSessionKey(value: unknown): string {
|
||||
const raw = String(value || "legacy-default").trim() || "legacy-default";
|
||||
return createHash("sha256").update(raw).digest("hex").slice(0, 32);
|
||||
}
|
||||
|
||||
/** Chrome 136+ requires a non-default user-data-dir for remote debugging. */
|
||||
export function resolveAdobeFireflyBrowserProfileDir(sessionKey?: string): string {
|
||||
const profile = join(
|
||||
resolveAdobeFireflyDataRoot(),
|
||||
"adobe-chrome-profiles",
|
||||
adobeFireflyBrowserSessionKey(sessionKey)
|
||||
);
|
||||
mkdirSync(profile, { recursive: true });
|
||||
return profile;
|
||||
}
|
||||
@@ -105,23 +118,56 @@ export function buildAdobeFireflyCookieHeader(
|
||||
return parts.join("; ");
|
||||
}
|
||||
|
||||
/** Best-effort account label from an IMS JWT payload. Exported for unit tests. */
|
||||
function humanAdobeLabel(value: unknown): string {
|
||||
const label = typeof value === "string" ? value.trim() : "";
|
||||
if (!label || /@(Adobe|Guest)ID$/i.test(label)) return "";
|
||||
return label;
|
||||
}
|
||||
|
||||
/** Human-readable label claims only; opaque Adobe IDs are intentionally excluded. */
|
||||
export function accountLabelFromAdobeJwt(token: string): string {
|
||||
try {
|
||||
const part = String(token || "").split(".")[1];
|
||||
if (!part) return "";
|
||||
const json = Buffer.from(part.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8");
|
||||
const obj = JSON.parse(json) as Record<string, unknown>;
|
||||
for (const key of ["email", "preferred_username", "user_id", "sub"]) {
|
||||
const v = obj[key];
|
||||
if (typeof v === "string" && v.trim()) return v.trim();
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
const obj = decodeAdobeJwtPayload(token);
|
||||
if (!obj) return "";
|
||||
for (const key of ["email", "preferred_username", "name", "display_name"]) {
|
||||
const label = humanAdobeLabel(obj[key]);
|
||||
if (label) return label;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/** Resolve email/display name from Adobe IMS; never expose the opaque user_id as a label. */
|
||||
export async function resolveAdobeAccountLabel(
|
||||
token: string,
|
||||
fetchImpl: typeof fetch = fetch
|
||||
): Promise<string> {
|
||||
const claimLabel = accountLabelFromAdobeJwt(token);
|
||||
const payload = decodeAdobeJwtPayload(token);
|
||||
const clientId = humanAdobeLabel(payload?.client_id) || "clio-playground-web";
|
||||
try {
|
||||
const response = await fetchImpl(
|
||||
`https://ims-na1.adobelogin.com/ims/userinfo/v2?client_id=${encodeURIComponent(clientId)}`,
|
||||
{
|
||||
headers: { Authorization: `Bearer ${token}` },
|
||||
signal: AbortSignal.timeout(10_000),
|
||||
}
|
||||
);
|
||||
if (response.ok) {
|
||||
const user = (await response.json()) as Record<string, unknown>;
|
||||
for (const key of ["email", "preferred_username", "name", "display_name"]) {
|
||||
const label = humanAdobeLabel(user[key]);
|
||||
if (label) return label;
|
||||
}
|
||||
const given = humanAdobeLabel(user.given_name);
|
||||
const family = humanAdobeLabel(user.family_name);
|
||||
const full = [given, family].filter(Boolean).join(" ").trim();
|
||||
if (full) return full;
|
||||
}
|
||||
} catch {
|
||||
// JWT label or generic fallback below keeps login successful if userinfo is unavailable.
|
||||
}
|
||||
return claimLabel || "Adobe account";
|
||||
}
|
||||
|
||||
/** Resolve system Chrome/Edge executable. Exported for unit tests. */
|
||||
export function resolveSystemBrowserExecutable(): string | null {
|
||||
const configured = process.env.OMNIROUTE_LOGIN_BROWSER_PATH?.trim();
|
||||
@@ -195,7 +241,82 @@ async function waitForCdpReady(
|
||||
throw new Error(`Chrome DevTools did not become ready: ${lastError}`);
|
||||
}
|
||||
|
||||
type CdpCookie = { name: string; value: string; domain?: string };
|
||||
export type AdobeBrowserCookie = {
|
||||
name: string;
|
||||
value: string;
|
||||
domain?: string;
|
||||
path?: string;
|
||||
expires?: number;
|
||||
httpOnly?: boolean;
|
||||
secure?: boolean;
|
||||
sameSite?: "Strict" | "Lax" | "None";
|
||||
};
|
||||
|
||||
type CdpCookie = AdobeBrowserCookie;
|
||||
|
||||
function isAdobeCookieDomain(domain: string | undefined): boolean {
|
||||
const value = String(domain || "")
|
||||
.trim()
|
||||
.replace(/^\./, "")
|
||||
.toLowerCase();
|
||||
return (
|
||||
value === "adobe.com" ||
|
||||
value.endsWith(".adobe.com") ||
|
||||
value === "adobelogin.com" ||
|
||||
value.endsWith(".adobelogin.com") ||
|
||||
value === "adobe.io" ||
|
||||
value.endsWith(".adobe.io")
|
||||
);
|
||||
}
|
||||
|
||||
export function filterAdobeBrowserCookies(cookies: CdpCookie[]): AdobeBrowserCookie[] {
|
||||
return cookies
|
||||
.filter(
|
||||
(cookie) =>
|
||||
isAdobeCookieDomain(cookie.domain) &&
|
||||
Boolean(cookie.name && cookie.value) &&
|
||||
!/[\r\n\0]/.test(cookie.name + cookie.value)
|
||||
)
|
||||
.map((cookie) => ({
|
||||
name: cookie.name,
|
||||
value: cookie.value,
|
||||
...(cookie.domain ? { domain: cookie.domain } : {}),
|
||||
path: cookie.path || "/",
|
||||
...(typeof cookie.expires === "number" ? { expires: cookie.expires } : {}),
|
||||
...(typeof cookie.httpOnly === "boolean" ? { httpOnly: cookie.httpOnly } : {}),
|
||||
...(typeof cookie.secure === "boolean" ? { secure: cookie.secure } : {}),
|
||||
...(cookie.sameSite ? { sameSite: cookie.sameSite } : {}),
|
||||
}));
|
||||
}
|
||||
|
||||
function adobeBrowserCookieJarPath(sessionKey: string): string {
|
||||
const dir = join(resolveAdobeFireflyDataRoot(), "adobe-browser-sessions");
|
||||
mkdirSync(dir, { recursive: true });
|
||||
return join(dir, `${adobeFireflyBrowserSessionKey(sessionKey)}.json`);
|
||||
}
|
||||
|
||||
function loadAdobeBrowserCookies(sessionKey: string): AdobeBrowserCookie[] {
|
||||
try {
|
||||
const path = adobeBrowserCookieJarPath(sessionKey);
|
||||
if (!existsSync(path)) return [];
|
||||
const parsed = JSON.parse(readFileSync(path, "utf8"));
|
||||
return Array.isArray(parsed) ? filterAdobeBrowserCookies(parsed as CdpCookie[]) : [];
|
||||
} catch {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
function saveAdobeBrowserCookies(sessionKey: string, cookies: CdpCookie[]): void {
|
||||
try {
|
||||
writeFileSync(
|
||||
adobeBrowserCookieJarPath(sessionKey),
|
||||
JSON.stringify(filterAdobeBrowserCookies(cookies)),
|
||||
"utf8"
|
||||
);
|
||||
} catch {
|
||||
// Best-effort: login still returns the portable JWT + Firefly risk cookies.
|
||||
}
|
||||
}
|
||||
|
||||
function parseCookieHeader(cookieHeader: string): Array<{ name: string; value: string }> {
|
||||
const cookies: Array<{ name: string; value: string }> = [];
|
||||
@@ -304,6 +425,7 @@ async function captureViaCdp(opts: {
|
||||
timeoutMs: number;
|
||||
fallbackAccessToken?: string;
|
||||
seedCookie?: string;
|
||||
seedBrowserCookies?: AdobeBrowserCookie[];
|
||||
waitForRiskRefresh?: boolean;
|
||||
}): Promise<{
|
||||
accessToken: string;
|
||||
@@ -316,7 +438,7 @@ async function captureViaCdp(opts: {
|
||||
const pageSockets = new Map<string, CdpSocket>();
|
||||
let browserCdp: CdpSocket | null = null;
|
||||
const initialForter =
|
||||
parseCookieHeader(opts.seedCookie || "").find(
|
||||
[...(opts.seedBrowserCookies || []), ...parseCookieHeader(opts.seedCookie || "")].find(
|
||||
(cookie) => cookie.name.toLowerCase() === "fortertoken"
|
||||
)?.value || "";
|
||||
const startedAt = Date.now();
|
||||
@@ -329,7 +451,7 @@ async function captureViaCdp(opts: {
|
||||
const headers = request.headers || {};
|
||||
const auth = headers.Authorization || headers.authorization || headers.AUTHORIZATION || "";
|
||||
const token = extractAdobeBearerTokenFromAuthorization(auth);
|
||||
if (token) capturedAccessToken = token;
|
||||
if (token && isAdobeUserAccessToken(token)) capturedAccessToken = token;
|
||||
const arp =
|
||||
headers["x-arp-session-id"] ||
|
||||
headers["X-Arp-Session-Id"] ||
|
||||
@@ -348,16 +470,29 @@ async function captureViaCdp(opts: {
|
||||
try {
|
||||
const browserWs = await openCdp(opts.browserWsUrl);
|
||||
browserCdp = new CdpSocket(browserWs, onEvent);
|
||||
const seed = parseCookieHeader(opts.seedCookie || "");
|
||||
const seed: AdobeBrowserCookie[] = [
|
||||
...(opts.seedBrowserCookies || []),
|
||||
...parseCookieHeader(opts.seedCookie || "").map((cookie) => ({
|
||||
...cookie,
|
||||
domain: "firefly.adobe.com",
|
||||
path: "/",
|
||||
secure: true,
|
||||
})),
|
||||
];
|
||||
if (seed.length > 0) {
|
||||
await browserCdp
|
||||
.send("Storage.setCookies", {
|
||||
cookies: seed.map(({ name, value }) => ({
|
||||
name,
|
||||
value,
|
||||
url: FIREFLY_HOME_URL,
|
||||
path: "/",
|
||||
secure: true,
|
||||
cookies: seed.map((cookie) => ({
|
||||
name: cookie.name,
|
||||
value: cookie.value,
|
||||
...(cookie.domain ? { domain: cookie.domain } : { url: FIREFLY_HOME_URL }),
|
||||
path: cookie.path || "/",
|
||||
...(typeof cookie.expires === "number" && cookie.expires > 0
|
||||
? { expires: cookie.expires }
|
||||
: {}),
|
||||
...(typeof cookie.httpOnly === "boolean" ? { httpOnly: cookie.httpOnly } : {}),
|
||||
...(typeof cookie.sameSite === "string" ? { sameSite: cookie.sameSite } : {}),
|
||||
secure: cookie.secure !== false,
|
||||
})),
|
||||
})
|
||||
.catch(() => undefined);
|
||||
@@ -417,7 +552,9 @@ async function captureViaCdp(opts: {
|
||||
/* retry while Chrome is settling */
|
||||
}
|
||||
|
||||
const accessToken = capturedAccessToken || String(opts.fallbackAccessToken || "").trim();
|
||||
const fallbackToken = String(opts.fallbackAccessToken || "").trim();
|
||||
const accessToken =
|
||||
capturedAccessToken || (isAdobeUserAccessToken(fallbackToken) ? fallbackToken : "");
|
||||
if (accessToken) {
|
||||
if (!opts.waitForRiskRefresh) {
|
||||
return {
|
||||
@@ -446,7 +583,8 @@ async function captureViaCdp(opts: {
|
||||
await new Promise((r) => setTimeout(r, POLL_INTERVAL_MS));
|
||||
}
|
||||
|
||||
const fallback = String(opts.fallbackAccessToken || "").trim();
|
||||
const fallbackRaw = String(opts.fallbackAccessToken || "").trim();
|
||||
const fallback = isAdobeUserAccessToken(fallbackRaw) ? fallbackRaw : "";
|
||||
if (fallback && latestCookies.length > 0) {
|
||||
return {
|
||||
accessToken: capturedAccessToken || fallback,
|
||||
@@ -491,6 +629,24 @@ function killProcessTree(child: ChildProcess | null): void {
|
||||
}
|
||||
}
|
||||
|
||||
export function buildAdobeFireflyBrowserArgs(opts: {
|
||||
port: number;
|
||||
userDataDir: string;
|
||||
interactive: boolean;
|
||||
freshSession?: boolean;
|
||||
}): string[] {
|
||||
return [
|
||||
`--remote-debugging-port=${opts.port}`,
|
||||
`--user-data-dir=${opts.userDataDir}`,
|
||||
"--no-first-run",
|
||||
"--no-default-browser-check",
|
||||
...(opts.interactive && opts.freshSession !== false ? ["--incognito"] : []),
|
||||
...(opts.interactive ? [] : ["--window-position=-32000,-32000"]),
|
||||
"--window-size=1280,800",
|
||||
opts.interactive ? FIREFLY_HOME_URL : "about:blank",
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Launch system Chrome/Edge at firefly.adobe.com, intercept firefly-3p
|
||||
* Authorization Bearer via CDP, return JWT + useful cookies.
|
||||
@@ -498,6 +654,8 @@ function killProcessTree(child: ChildProcess | null): void {
|
||||
async function runAdobeFireflyCdpBrowser(opts: {
|
||||
timeoutMs: number;
|
||||
interactive: boolean;
|
||||
sessionKey: string;
|
||||
freshSession?: boolean;
|
||||
seedCookie?: string;
|
||||
accessToken?: string;
|
||||
log?: AdobeFireflyBrowserLog;
|
||||
@@ -515,18 +673,15 @@ async function runAdobeFireflyCdpBrowser(opts: {
|
||||
|
||||
let child: ChildProcess | null = null;
|
||||
try {
|
||||
const userDataDir = resolveAdobeFireflyBrowserProfileDir();
|
||||
const userDataDir = resolveAdobeFireflyBrowserProfileDir(opts.sessionKey);
|
||||
const port = await getFreeLoopbackPort();
|
||||
|
||||
const args = [
|
||||
`--remote-debugging-port=${port}`,
|
||||
`--user-data-dir=${userDataDir}`,
|
||||
"--no-first-run",
|
||||
"--no-default-browser-check",
|
||||
...(opts.interactive ? [] : ["--window-position=-32000,-32000"]),
|
||||
"--window-size=1280,800",
|
||||
FIREFLY_HOME_URL,
|
||||
];
|
||||
const args = buildAdobeFireflyBrowserArgs({
|
||||
port,
|
||||
userDataDir,
|
||||
interactive: opts.interactive,
|
||||
freshSession: opts.freshSession,
|
||||
});
|
||||
|
||||
child = spawn(browserPath, args, {
|
||||
stdio: "ignore",
|
||||
@@ -558,13 +713,18 @@ async function runAdobeFireflyCdpBrowser(opts: {
|
||||
timeoutMs: opts.timeoutMs,
|
||||
fallbackAccessToken: opts.accessToken,
|
||||
seedCookie: opts.seedCookie,
|
||||
seedBrowserCookies:
|
||||
opts.interactive && opts.freshSession !== false
|
||||
? []
|
||||
: loadAdobeBrowserCookies(opts.sessionKey),
|
||||
waitForRiskRefresh: !opts.interactive,
|
||||
}),
|
||||
earlyExit,
|
||||
]);
|
||||
|
||||
const cookie = buildAdobeFireflyCookieHeader(captured.cookies);
|
||||
const account = accountLabelFromAdobeJwt(captured.accessToken);
|
||||
saveAdobeBrowserCookies(opts.sessionKey, captured.cookies);
|
||||
const account = await resolveAdobeAccountLabel(captured.accessToken);
|
||||
opts.log?.info?.(
|
||||
"ADOBE-FIREFLY",
|
||||
`CDP ${opts.interactive ? "sign-in" : "refresh"} captured durable session ` +
|
||||
@@ -591,12 +751,15 @@ async function runAdobeFireflyCdpBrowser(opts: {
|
||||
}
|
||||
|
||||
export async function startAdobeFireflyBrowserLogin(
|
||||
requestedTimeout?: unknown
|
||||
requestedTimeout?: unknown,
|
||||
opts?: { sessionKey?: string; freshSession?: boolean }
|
||||
): Promise<AdobeFireflyBrowserLoginResult> {
|
||||
const run = cdpBrowserChain.then(() =>
|
||||
runAdobeFireflyCdpBrowser({
|
||||
timeoutMs: clampAdobeFireflyLoginTimeout(requestedTimeout),
|
||||
interactive: true,
|
||||
sessionKey: String(opts?.sessionKey || "legacy-default"),
|
||||
freshSession: opts?.freshSession !== false,
|
||||
})
|
||||
);
|
||||
cdpBrowserChain = run.then(
|
||||
@@ -612,11 +775,13 @@ export async function refreshAdobeFireflyViaCdp(opts: {
|
||||
accessToken?: string;
|
||||
timeoutMs?: number;
|
||||
log?: AdobeFireflyBrowserLog;
|
||||
sessionKey?: string;
|
||||
}): Promise<AdobeFireflyCdpRefreshResult | null> {
|
||||
const run = cdpBrowserChain.then(async () => {
|
||||
const result = await runAdobeFireflyCdpBrowser({
|
||||
timeoutMs: Math.max(15_000, Math.min(120_000, Number(opts.timeoutMs) || 75_000)),
|
||||
interactive: false,
|
||||
sessionKey: String(opts.sessionKey || "legacy-default"),
|
||||
seedCookie: opts.cookie,
|
||||
accessToken: opts.accessToken,
|
||||
log: opts.log,
|
||||
|
||||
@@ -38,6 +38,8 @@ export interface AdobeFireflySession {
|
||||
updatedAt: number;
|
||||
/** Hash of the original credential paste (cache key). */
|
||||
fingerprint: string;
|
||||
/** Stable provider connection id used to isolate browser SSO/cookie state per Adobe account. */
|
||||
browserSessionKey?: string;
|
||||
source: "paste" | "ims" | "browser" | "cache" | "rebuild";
|
||||
}
|
||||
|
||||
@@ -45,10 +47,12 @@ export interface AdobeFireflySessionResolveOpts {
|
||||
credentials?: {
|
||||
apiKey?: string;
|
||||
accessToken?: string;
|
||||
connectionId?: string;
|
||||
providerSpecificData?: {
|
||||
cookie?: unknown;
|
||||
access_token?: unknown;
|
||||
accessToken?: unknown;
|
||||
browserSessionKey?: unknown;
|
||||
} | null;
|
||||
} | null;
|
||||
/** Force browser / cookie ARP rebuild (e.g. after HTTP 408). */
|
||||
@@ -490,6 +494,7 @@ export async function refreshAdobeSessionViaBrowser(
|
||||
accessToken: session.accessToken,
|
||||
log,
|
||||
timeoutMs: force ? 90_000 : 75_000,
|
||||
sessionKey: session.browserSessionKey,
|
||||
});
|
||||
if (!warmed) return null;
|
||||
|
||||
@@ -551,6 +556,12 @@ export async function ensureAdobeFireflySession(
|
||||
|
||||
const joined = blobs.join("\n");
|
||||
const fingerprint = fingerprintAdobeCredential(joined);
|
||||
const browserSessionKey =
|
||||
String(
|
||||
opts.credentials?.connectionId ||
|
||||
opts.credentials?.providerSpecificData?.browserSessionKey ||
|
||||
fingerprint
|
||||
).trim() || fingerprint;
|
||||
|
||||
// forceRefresh / rotate always drop in-memory cache for this fingerprint
|
||||
if (opts.forceRefresh) sessionCache.delete(fingerprint);
|
||||
@@ -668,6 +679,7 @@ export async function ensureAdobeFireflySession(
|
||||
tokenExpiresAt: estimateAdobeTokenExpiry(accessToken),
|
||||
updatedAt: Date.now(),
|
||||
fingerprint,
|
||||
browserSessionKey,
|
||||
source: workingFresh ? "cache" : cached?.source || "paste",
|
||||
};
|
||||
|
||||
|
||||
@@ -5,10 +5,14 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
adobeFireflyBrowserSessionKey,
|
||||
accountLabelFromAdobeJwt,
|
||||
buildAdobeFireflyBrowserArgs,
|
||||
buildAdobeFireflyCookieHeader,
|
||||
clampAdobeFireflyLoginTimeout,
|
||||
extractAdobeBearerTokenFromAuthorization,
|
||||
filterAdobeBrowserCookies,
|
||||
resolveAdobeAccountLabel,
|
||||
resolveSystemBrowserExecutable,
|
||||
} from "../../open-sse/services/adobeFireflyBrowserLogin.ts";
|
||||
|
||||
@@ -57,6 +61,72 @@ test("accountLabelFromAdobeJwt prefers email", () => {
|
||||
assert.equal(accountLabelFromAdobeJwt("not-a-jwt"), "");
|
||||
});
|
||||
|
||||
test("accountLabelFromAdobeJwt never exposes opaque Adobe IDs", () => {
|
||||
const payload = Buffer.from(
|
||||
JSON.stringify({ user_id: "0123456789ABCDEF@AdobeID", sub: "opaque-subject" })
|
||||
).toString("base64url");
|
||||
assert.equal(accountLabelFromAdobeJwt(`eyJhbGciOiJIUzI1NiJ9.${payload}.sig`), "");
|
||||
});
|
||||
|
||||
test("resolveAdobeAccountLabel uses IMS display name and generic fallback", async () => {
|
||||
const payload = Buffer.from(
|
||||
JSON.stringify({ client_id: "clio-playground-web", user_id: "opaque@AdobeID" })
|
||||
).toString("base64url");
|
||||
const jwt = `eyJhbGciOiJIUzI1NiJ9.${payload}.sig`;
|
||||
const displayName = await resolveAdobeAccountLabel(
|
||||
jwt,
|
||||
(async () =>
|
||||
new Response(JSON.stringify({ name: "Friendly Name", sub: "opaque@AdobeID" }), {
|
||||
status: 200,
|
||||
})) as typeof fetch
|
||||
);
|
||||
assert.equal(displayName, "Friendly Name");
|
||||
|
||||
const fallback = await resolveAdobeAccountLabel(
|
||||
jwt,
|
||||
(async () => new Response("unavailable", { status: 503 })) as typeof fetch
|
||||
);
|
||||
assert.equal(fallback, "Adobe account");
|
||||
});
|
||||
|
||||
test("browser args isolate profiles and make fresh interactive login incognito", () => {
|
||||
const firstKey = adobeFireflyBrowserSessionKey("connection-a");
|
||||
const secondKey = adobeFireflyBrowserSessionKey("connection-b");
|
||||
assert.equal(firstKey, adobeFireflyBrowserSessionKey("connection-a"));
|
||||
assert.notEqual(firstKey, secondKey);
|
||||
|
||||
const interactive = buildAdobeFireflyBrowserArgs({
|
||||
port: 9222,
|
||||
userDataDir: `C:\\profiles\\${firstKey}`,
|
||||
interactive: true,
|
||||
freshSession: true,
|
||||
});
|
||||
assert.ok(interactive.includes("--incognito"));
|
||||
assert.ok(interactive.includes(`--user-data-dir=C:\\profiles\\${firstKey}`));
|
||||
assert.equal(interactive.at(-1), "https://firefly.adobe.com/");
|
||||
|
||||
const background = buildAdobeFireflyBrowserArgs({
|
||||
port: 9223,
|
||||
userDataDir: `C:\\profiles\\${secondKey}`,
|
||||
interactive: false,
|
||||
});
|
||||
assert.equal(background.includes("--incognito"), false);
|
||||
assert.equal(background.at(-1), "about:blank");
|
||||
});
|
||||
|
||||
test("filterAdobeBrowserCookies keeps Adobe SSO domains only", () => {
|
||||
assert.deepEqual(
|
||||
filterAdobeBrowserCookies([
|
||||
{ name: "ims", value: "one", domain: ".adobelogin.com", secure: true },
|
||||
{ name: "firefly", value: "two", domain: "firefly.adobe.com" },
|
||||
{ name: "service", value: "three", domain: "firefly-3p.ff.adobe.io" },
|
||||
{ name: "unrelated", value: "secret", domain: ".example.com" },
|
||||
{ name: "bad", value: "line\nbreak", domain: ".adobe.com" },
|
||||
]).map((cookie) => cookie.name),
|
||||
["ims", "firefly", "service"]
|
||||
);
|
||||
});
|
||||
|
||||
test("resolveSystemBrowserExecutable finds Chrome or Edge on this host (or honors env)", () => {
|
||||
const path = resolveSystemBrowserExecutable();
|
||||
// CI images may lack a browser — only assert type / env override behavior.
|
||||
|
||||
Reference in New Issue
Block a user