mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-09-13 18:32:12 +03:00
fix(providers): route chatgpt-session through the shared browser-session credential lifecycle
Both /api/providers routes gated the raw-cookie -> verified-storage-state finalize step on a hardcoded chatgpt-web-codex check, so chatgpt-session connections never had their pasted cookie replaced, never released the temporary validation directory, and leaked validationId into persisted providerSpecificData. Introduce usesChatGptBrowserSessionCredentials() as the single source of truth for both provider ids and use it in place of the hardcoded equality checks. Also replace a brittle source-regex test with one that proves the validation dispatch actually resolves.
This commit is contained in:
@@ -35,6 +35,7 @@ import {
|
||||
encodeChatGptWebCodexSecrets,
|
||||
} from "@omniroute/open-sse/services/chatgptWebCodexAdmin.ts";
|
||||
import { rejectRetiredCommonChatGptWebProvider } from "@/lib/providers/chatgptWebRetirementResponse";
|
||||
import { usesChatGptBrowserSessionCredentials } from "@/shared/constants/chatgptWebCodex";
|
||||
|
||||
function normalizeCodexLimitPolicy(
|
||||
incoming: unknown,
|
||||
@@ -172,7 +173,7 @@ export async function PUT(request: Request, { params }: { params: Promise<{ id:
|
||||
if (defaultModel !== undefined) updateData.defaultModel = defaultModel;
|
||||
if (isActive !== undefined) updateData.isActive = isActive;
|
||||
if (apiKey && canUpdateProviderApiKey(existing.authType, existing.provider)) {
|
||||
if (existing.provider === "chatgpt-web-codex") {
|
||||
if (usesChatGptBrowserSessionCredentials(existing.provider)) {
|
||||
const validationId =
|
||||
incomingPsd && typeof incomingPsd.validationId === "string"
|
||||
? incomingPsd.validationId
|
||||
@@ -215,7 +216,8 @@ export async function PUT(request: Request, { params }: { params: Promise<{ id:
|
||||
// the override (connection follows the global default); 0-1440 = explicit
|
||||
// per-connection minutes (0 opts this connection out of the sweep).
|
||||
if (healthCheckInterval === null) updateData.healthCheckInterval = null;
|
||||
else if (healthCheckInterval !== undefined) updateData.healthCheckInterval = healthCheckInterval;
|
||||
else if (healthCheckInterval !== undefined)
|
||||
updateData.healthCheckInterval = healthCheckInterval;
|
||||
if (group !== undefined) updateData.group = group;
|
||||
if (maxConcurrent !== undefined) updateData.maxConcurrent = maxConcurrent;
|
||||
if (incomingWindowThresholds !== undefined) {
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
getModelSyncInternalBaseUrl,
|
||||
} from "@/shared/services/modelSyncScheduler";
|
||||
import { finalizeValidatedChatGptWebCodexSecrets } from "@omniroute/open-sse/services/chatgptWebCodexAdmin.ts";
|
||||
import { usesChatGptBrowserSessionCredentials } from "@/shared/constants/chatgptWebCodex";
|
||||
import { isAutoFetchModelsEnabled } from "@/lib/providerModels/modelDiscovery";
|
||||
import { testSingleConnection } from "./[id]/test/route";
|
||||
import { rejectRetiredCommonChatGptWebProvider } from "@/lib/providers/chatgptWebRetirementResponse";
|
||||
@@ -198,7 +199,7 @@ export async function POST(request: Request) {
|
||||
providerSpecificData = normalizeQoderPatProviderData(providerSpecificData || {});
|
||||
}
|
||||
|
||||
if (provider === "chatgpt-web-codex") {
|
||||
if (usesChatGptBrowserSessionCredentials(provider)) {
|
||||
const validationId =
|
||||
providerSpecificData && typeof providerSpecificData.validationId === "string"
|
||||
? providerSpecificData.validationId
|
||||
@@ -324,11 +325,16 @@ export async function POST(request: Request) {
|
||||
})
|
||||
.then((syncRes) => {
|
||||
if (!syncRes.ok) {
|
||||
console.log(`[providers] Auto-sync failed for ${newConnection.id}: ${syncRes.status}`);
|
||||
console.log(
|
||||
`[providers] Auto-sync failed for ${newConnection.id}: ${syncRes.status}`
|
||||
);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log(`[providers] Auto-sync error for ${newConnection.id}:`, err?.message || err);
|
||||
console.log(
|
||||
`[providers] Auto-sync error for ${newConnection.id}:`,
|
||||
err?.message || err
|
||||
);
|
||||
});
|
||||
} catch (syncSetupError) {
|
||||
// Defensive: if URL parsing or header construction itself throws, do
|
||||
|
||||
@@ -10,3 +10,21 @@ export function isChatGptWebCodexModel(model: unknown): boolean {
|
||||
// persisted account session is valid. Keep runtime turns aligned with the headed browser
|
||||
// used to verify that same storage state.
|
||||
export const CHATGPT_WEB_CODEX_RUNTIME_HEADED = true;
|
||||
|
||||
// Both "chatgpt-web-codex" and "chatgpt-session" store a verified Playwright storage
|
||||
// state produced from a pasted ChatGPT cookie header — the browser-session credential
|
||||
// lifecycle (decode -> ensure storage state -> inspect -> finalize) is shared between
|
||||
// them. The finalize step is what discards the raw pasted cookie in favor of the
|
||||
// verified storage state, so any dashboard route that gates on that lifecycle must
|
||||
// recognize both provider ids, not just the codex one.
|
||||
export const CHATGPT_BROWSER_SESSION_PROVIDER_IDS = [
|
||||
CHATGPT_WEB_CODEX_PROVIDER_ID,
|
||||
"chatgpt-session",
|
||||
] as const;
|
||||
|
||||
export function usesChatGptBrowserSessionCredentials(provider: unknown): boolean {
|
||||
return (
|
||||
typeof provider === "string" &&
|
||||
(CHATGPT_BROWSER_SESSION_PROVIDER_IDS as readonly string[]).includes(provider)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,11 +3,10 @@ import assert from "node:assert/strict";
|
||||
|
||||
import { WEB_COOKIE_PROVIDERS } from "../../src/shared/constants/providers/web-cookie.ts";
|
||||
import { WEB_SESSION_CREDENTIAL_REQUIREMENTS } from "../../src/shared/providers/webSessionCredentials.ts";
|
||||
import { usesChatGptBrowserSessionCredentials } from "../../src/shared/constants/chatgptWebCodex.ts";
|
||||
|
||||
test("the dashboard card describes the session provider", () => {
|
||||
const card = (WEB_COOKIE_PROVIDERS as Record<string, Record<string, unknown>>)[
|
||||
"chatgpt-session"
|
||||
];
|
||||
const card = (WEB_COOKIE_PROVIDERS as Record<string, Record<string, unknown>>)["chatgpt-session"];
|
||||
assert.ok(card, "chatgpt-session must have a web-cookie card");
|
||||
assert.equal(card.alias, "cgpt-session");
|
||||
assert.equal(card.website, "https://chatgpt.com");
|
||||
@@ -25,23 +24,34 @@ test("the credential requirement accepts a full cookie header", () => {
|
||||
assert.equal(requirement.acceptsFullCookieHeader, true);
|
||||
});
|
||||
|
||||
test("validation is registered for the provider id", async () => {
|
||||
const module = await import("../../src/lib/providers/validation.ts");
|
||||
const source = await import("node:fs").then((fs) =>
|
||||
fs.readFileSync(
|
||||
new URL("../../src/lib/providers/validation.ts", import.meta.url),
|
||||
"utf8"
|
||||
)
|
||||
);
|
||||
assert.match(source, /"chatgpt-session":\s*validateChatGptSessionProvider/);
|
||||
assert.ok(module);
|
||||
test("validateProviderApiKey dispatch resolves chatgpt-session to its own validator", async () => {
|
||||
const { validateProviderApiKey } = await import("../../src/lib/providers/validation.ts");
|
||||
// "cookie:" decodes to an empty cookie (decodeChatGptWebCodexSecrets strips the
|
||||
// "cookie:" prefix), so validateChatGptSessionProvider returns its own
|
||||
// cookie-required rejection immediately — before any Chrome/CDP detection or
|
||||
// browser launch. A non-empty credential is required here: an empty apiKey is
|
||||
// intercepted by validateProviderApiKey's own "Provider and API key required"
|
||||
// gate before dispatch ever reaches the SPECIALTY_VALIDATORS map, which would
|
||||
// prove nothing about chatgpt-session's own registration.
|
||||
const result = await validateProviderApiKey({
|
||||
provider: "chatgpt-session",
|
||||
apiKey: "cookie:",
|
||||
});
|
||||
assert.equal(result.valid, false);
|
||||
assert.equal(result.error, "A ChatGPT cookie header or a stored browser session is required.");
|
||||
});
|
||||
|
||||
test("validation rejects an empty credential without launching a browser", async () => {
|
||||
const { validateChatGptSessionProvider } = await import(
|
||||
"../../src/lib/providers/validation/chatgptSession.ts"
|
||||
);
|
||||
const { validateChatGptSessionProvider } =
|
||||
await import("../../src/lib/providers/validation/chatgptSession.ts");
|
||||
const result = await validateChatGptSessionProvider({ apiKey: "" });
|
||||
assert.equal(result.valid, false);
|
||||
assert.match(String(result.error), /cookie|credential/i);
|
||||
});
|
||||
|
||||
test("usesChatGptBrowserSessionCredentials recognizes both browser-session providers", () => {
|
||||
assert.equal(usesChatGptBrowserSessionCredentials("chatgpt-web-codex"), true);
|
||||
assert.equal(usesChatGptBrowserSessionCredentials("chatgpt-session"), true);
|
||||
assert.equal(usesChatGptBrowserSessionCredentials("openai"), false);
|
||||
assert.equal(usesChatGptBrowserSessionCredentials(undefined), false);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user