fix(sse): persist rotated Gemini web-session cookies via onCredentialsRefreshed (#7676) (#7751)

This commit is contained in:
Diego Rodrigues de Sa e Souza
2026-07-19 09:39:02 -03:00
committed by GitHub
parent 45698736e3
commit c95a161709
3 changed files with 161 additions and 1 deletions

View File

@@ -0,0 +1 @@
- fix(sse): persist rotated Gemini web-session cookies via onCredentialsRefreshed (#7676)

View File

@@ -169,6 +169,40 @@ function readProviderSpecificString(
return "";
}
/**
* Merge rotated __Secure-1PSID* cookies read back from the live Playwright
* cookie jar into the original cookie string. Only the three long-lived
* Gemini auth cookies are considered — pulling in the entire jar would risk
* treating short-lived Google analytics/consent cookies as credentials
* (#7676). Cookies the jar didn't return, or that are unchanged, are left
* untouched in the original string.
*/
export function mergeRotatedGeminiCookies(
originalCookie: string,
jarCookies: Array<{ name: string; value: string }>
): string {
const ROTATABLE_NAMES = ["__Secure-1PSID", "__Secure-1PSIDTS", "__Secure-1PSIDCC"];
const jarByName = new Map(jarCookies.map((c) => [c.name, c.value]));
const pairs = parseCookies(originalCookie);
const seen = new Set<string>();
const merged = pairs.map(({ name, value }) => {
seen.add(name);
if (ROTATABLE_NAMES.includes(name) && jarByName.has(name)) {
return { name, value: jarByName.get(name) as string };
}
return { name, value };
});
for (const name of ROTATABLE_NAMES) {
if (!seen.has(name) && jarByName.has(name)) {
merged.push({ name, value: jarByName.get(name) as string });
}
}
return merged.map(({ name, value }) => `${name}=${value}`).join("; ");
}
function normalizeGeminiCookieInput(raw: string, cookieName = "__Secure-1PSID"): string {
const trimmed = raw.trim();
if (!trimmed) return "";
@@ -202,8 +236,38 @@ export class GeminiWebExecutor extends BaseExecutor {
super("gemini-web", { id: "gemini-web", baseUrl: GEMINI_URL });
}
/**
* Read the live Playwright cookie jar back after a successful run and, if
* Google rotated any of the __Secure-1PSID* cookies, forward the merged
* cookie string through onCredentialsRefreshed so it gets persisted to the
* encrypted provider_connections.api_key field. Mirrors the rotate-and-
* persist pattern already shipped in chatgpt-web.ts. A persistence failure
* must never fail the user-facing response (#7676).
*/
private async persistRotatedCookies(
context: import("playwright").BrowserContext,
cookie: string,
credentials: ExecuteInput["credentials"],
onCredentialsRefreshed: ExecuteInput["onCredentialsRefreshed"],
log: ExecuteInput["log"]
): Promise<void> {
if (!onCredentialsRefreshed) return;
try {
const jarCookies = await context.cookies();
const mergedCookie = mergeRotatedGeminiCookies(cookie, jarCookies);
if (mergedCookie && mergedCookie !== cookie) {
await onCredentialsRefreshed({ ...credentials, apiKey: mergedCookie });
}
} catch (err) {
log?.warn?.(
"GEMINI-WEB",
`Failed to persist rotated cookie: ${err instanceof Error ? err.message : String(err)}`
);
}
}
async execute(input: ExecuteInput) {
const { model, body, stream, credentials, signal } = input;
const { model, body, stream, credentials, signal, log, onCredentialsRefreshed } = input;
const requestBody = body as GeminiRequestBody;
const cookie = resolveGeminiWebCookie(credentials);
@@ -314,6 +378,8 @@ export class GeminiWebExecutor extends BaseExecutor {
};
}
await this.persistRotatedCookies(context, cookie, credentials, onCredentialsRefreshed, log);
const modelId = model || "gemini-2.5-pro";
if (stream) {

View File

@@ -0,0 +1,93 @@
// Repro probe for issue #7676:
// gemini-web executor never reads back the live Playwright cookie jar after a
// successful run, so rotated __Secure-1PSIDTS / __Secure-1PSIDCC values are
// never persisted via onCredentialsRefreshed — unlike chatgpt-web.ts, which
// already forwards its rotated cookie through the same callback
// (open-sse/executors/chatgpt-web.ts:2843).
import test from "node:test";
import assert from "node:assert/strict";
const { GeminiWebExecutor } = await import("../../open-sse/executors/gemini-web.ts");
test("#7676: GeminiWebExecutor persists rotated __Secure-1PSIDTS/__Secure-1PSIDCC via onCredentialsRefreshed after a successful run", async () => {
const playwright = await import("playwright");
const originalLaunch = playwright.chromium.launch;
const staleCookie =
"__Secure-1PSID=abc123; __Secure-1PSIDTS=OLD_TS_VALUE; __Secure-1PSIDCC=OLD_CC_VALUE";
const rotatedJarCookies = [
{ name: "__Secure-1PSID", value: "abc123", domain: ".google.com", path: "/" },
{ name: "__Secure-1PSIDTS", value: "ROTATED_TS_VALUE", domain: ".google.com", path: "/" },
{ name: "__Secure-1PSIDCC", value: "ROTATED_CC_VALUE", domain: ".google.com", path: "/" },
];
playwright.chromium.launch = async () =>
({
newContext: async () => ({
addCookies: async () => {},
cookies: async () => rotatedJarCookies,
newPage: async () => ({
on: (event: string, handler: (resp: { url: () => string; text: () => Promise<string> }) => void) => {
if (event === "response") {
const body =
")]}'\n" +
"30\n" +
JSON.stringify([
[
"wrb.fr",
null,
JSON.stringify([null, null, null, null, [[null, ["hello back"]]]]),
],
]) +
"\n";
handler({
url: () =>
"https://gemini.google.com/_/BardChatUi/data/assistant.lamda.BardFrontendService/StreamGenerate",
text: async () => body,
});
}
},
goto: async () => {},
waitForTimeout: async () => {},
waitForSelector: async () => ({ click: async () => {} }),
keyboard: { type: async () => {}, press: async () => {} },
}),
}),
close: async () => {},
}) as unknown as typeof originalLaunch;
let persistedCredentials: Record<string, unknown> | null = null;
try {
const executor = new GeminiWebExecutor();
const result = await executor.execute({
model: "gemini-3.1-pro",
body: { messages: [{ role: "user", content: "hello" }], stream: false },
stream: false,
credentials: { apiKey: staleCookie },
signal: AbortSignal.timeout(5000),
log: null,
onCredentialsRefreshed: async (newCreds: Record<string, unknown>) => {
persistedCredentials = newCreds;
},
} as unknown as Parameters<InstanceType<typeof GeminiWebExecutor>["execute"]>[0]);
assert.equal(result.response.status, 200, "run should succeed with a Gemini response");
assert.ok(
persistedCredentials,
"onCredentialsRefreshed must be called so the rotated cookie jar is persisted to provider_connections (#7676)"
);
assert.ok(
typeof persistedCredentials.apiKey === "string" &&
persistedCredentials.apiKey.includes("ROTATED_TS_VALUE"),
`persisted apiKey must contain the rotated __Secure-1PSIDTS value, got: ${persistedCredentials?.apiKey}`
);
assert.ok(
persistedCredentials.apiKey.includes("ROTATED_CC_VALUE"),
`persisted apiKey must contain the rotated __Secure-1PSIDCC value, got: ${persistedCredentials?.apiKey}`
);
} finally {
playwright.chromium.launch = originalLaunch;
}
});