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:
diegosouzapw
2026-09-02 03:43:11 -03:00
parent abd720029b
commit d3cee495bd
4 changed files with 57 additions and 21 deletions

View File

@@ -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) {

View File

@@ -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

View File

@@ -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)
);
}

View File

@@ -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);
});