mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
This commit is contained in:
committed by
GitHub
parent
45698736e3
commit
c95a161709
1
changelog.d/fixes/7676-gemini-persist-cookies.md
Normal file
1
changelog.d/fixes/7676-gemini-persist-cookies.md
Normal file
@@ -0,0 +1 @@
|
||||
- fix(sse): persist rotated Gemini web-session cookies via onCredentialsRefreshed (#7676)
|
||||
@@ -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) {
|
||||
|
||||
93
tests/unit/gemini-web-cookie-rotation-7676.test.ts
Normal file
93
tests/unit/gemini-web-cookie-rotation-7676.test.ts
Normal 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;
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user