Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion config/quality/file-size-baseline.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
{
"_comment": "Catraca de tamanho (check-file-size.mjs). frozen so pode encolher; arquivos novos <= cap. --update ratcheta.",
"_rebaseline_2026_06_20_4023_web_cookie_noauth_validation": "PR #4023 (oyi77) own growth: src/lib/providers/validation.ts 4450->4518 (+68 = a new validateWebCookieProvider that probes the provider's /models endpoint — 401/403 => AUTH_007 SESSION_EXPIRED, any other status => valid session, empty cookie => invalid, provider-not-in-registry => unsupported — plus a local STANDARD_USER_AGENT const for the probe). Cohesive validator at the validateProviderApiKey dispatch; not extractable. Covered by tests/unit/provider-validation-web-cookie-auth007.test.ts. Heavily curated on merge — the PR's branch was badly stale-based (squash-base-stale), so its tree was DESTRUCTIVE: providers/index.ts deleted live providers openadapter/dit/tokenrouter (added by #4313) and the executor/base.ts edits reverted release fixes (#4037 duckduckgo host, theoldllm gpt5 models, base.ts fetch-start-timeout). Only the purely-additive validation feature was kept (validation.ts validateWebCookieProvider + errorCodes AUTH_007 + the test). Dropped: 5 malformed new registry entries (used non-RegistryEntry fields defaultModel/auth + referenced non-existent executors -> tsc TS2353), the destructive providers/index.ts + executor reverts, the unrelated pr-*.sh automation scripts, and evals/types.ts (belongs to the deferred evals modularization #4422). Also removed the PR's fragile 'Phase 2' executor probe (ran a live upstream chat during validation + classified any 'auth'-containing error as SESSION_EXPIRED) and rewrote the test to install its fetch mock before module load (the original mocked too late and silently hit live chatgpt.com).",
"_rebaseline_2026_06_20_3368_pool_tools": "PR #3368 own growth: open-sse/mcp-server/server.ts 1468->1509 (+41 = register the web-session pool observability tools — poolStatus/poolSessions/poolWarm — at the existing createMcpServer registration chokepoint, with their read:health/write:resilience scope wiring). Reconciled late: the server.ts bump was a separate commit that did not ride along when the feature commit was cherry-picked onto release/v3.8.32 (squash-base-stale rebuild). Thin cohesive registration at the single tool-registration boundary; not extractable. Covered by tests/unit/mcp-pool-tools-3368.test.ts.",
"_rebaseline_2026_06_20_4401_webfetch_validators": "PR #4401 own growth: src/lib/providers/validation.ts 4428->4450 (+22 = two new API-key validators — Firecrawl + Jina Reader — each a cohesive provider validator branch mirroring the existing ones; 401/403->invalid else->valid). Not extractable (it IS the per-provider validator list). Covered by tests/unit/provider-validation-webfetch-4401.test.ts.",
"_rebaseline_2026_06_20_4380_parse_once": "PR #4380 own growth: src/sse/handlers/chat.ts 1486->1491 (+5 = thread the once-parsed request body from the route guard into handleChat, replacing the duplicate re-parse). The reusable body accessor lives in the new src/sse/handlers/requestBody.ts (<cap). Thin wiring at the single entry chokepoint; not extractable. Covered by tests/unit/chat-request-body-parse-once-4380.test.ts.",
Expand Down Expand Up @@ -171,7 +172,7 @@
"src/lib/evals/evalRunner.ts": 961,
"src/lib/memory/retrieval.ts": 1171,
"src/lib/modelsDevSync.ts": 934,
"src/lib/providers/validation.ts": 4450,
"src/lib/providers/validation.ts": 4518,
"src/lib/tailscaleTunnel.ts": 1202,
"src/lib/usage/callLogs.ts": 975,
"src/lib/usage/providerLimits.ts": 949,
Expand Down
68 changes: 68 additions & 0 deletions src/lib/providers/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import {
isOpenAICompatibleProvider,
isSelfHostedChatProvider,
providerAllowsOptionalApiKey,
WEB_COOKIE_PROVIDERS,
} from "@/shared/constants/providers";
import {
SAFE_OUTBOUND_FETCH_PRESETS,
Expand Down Expand Up @@ -190,6 +191,10 @@ function normalizeGigachatChatUrl(baseUrl: string) {
return `${normalized}/chat/completions`;
}

// Standardized desktop Chrome UA for web-cookie/no-auth session probes (minimizes anti-bot detection).
const STANDARD_USER_AGENT =
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36";

function getCustomUserAgent(providerSpecificData: any = {}) {
if (typeof providerSpecificData?.customUserAgent !== "string") return null;
const customUserAgent = providerSpecificData.customUserAgent.trim();
Expand Down Expand Up @@ -3932,6 +3937,60 @@ async function validateInnerAiProvider({ apiKey, providerSpecificData = {} }: an
return toValidationErrorResult(error);
}
}
/**
* Validates web-cookie providers by performing a ping request to check if the session is still valid.
* Returns SESSION_EXPIRED error code if the upstream returns 401/403.
*/
export async function validateWebCookieProvider({
provider,
apiKey,
providerSpecificData = {},
}: any) {
try {
const entry = getRegistryEntry(provider);
if (!entry) {
return { valid: false, error: "Provider not found in registry", unsupported: true };
}

// For web-cookie providers, apiKey contains the cookie string
const cookie = (apiKey || "").trim();
if (!cookie) {
return { valid: false, error: "Cookie required for web-cookie provider", unsupported: false };
}

// Attempt a minimal request to check if the session is valid
// Use /models endpoint or a minimal completion request depending on the provider
const baseUrl = entry.baseUrl || "";
const testUrl = `${baseUrl}/models`;

const res = await directHttpsRequest(
testUrl,
{
method: "GET",
headers: {
"User-Agent": STANDARD_USER_AGENT,
},
},
10_000
);

if (res.status === 401 || res.status === 403) {
return {
valid: false,
error: "SESSION_EXPIRED",
errorCode: "AUTH_007",
unsupported: false,
};
}

// Any other response (200, 404, 405, 429, ...) means the cookie was accepted —
// a 401/403 from the /models probe is the only definitive "session expired" signal
// for web-cookie auth, so a non-auth status is treated as a valid session.
return { valid: true, error: null, unsupported: false };
} catch (error: any) {
return toValidationErrorResult(error);
}
}

export async function validateProviderApiKey({ provider, apiKey, providerSpecificData = {} }: any) {
const requiresApiKey = !providerAllowsOptionalApiKey(provider);
Expand All @@ -3941,6 +4000,15 @@ export async function validateProviderApiKey({ provider, apiKey, providerSpecifi
return { valid: false, error: "Provider and API key required", unsupported: false };
}

// Web-cookie providers (session-based authentication)
if (WEB_COOKIE_PROVIDERS[provider]) {
try {
return await validateWebCookieProvider({ provider, apiKey, providerSpecificData });
} catch (error: any) {
return toValidationErrorResult(error);
}
}

if (isOpenAICompatibleProvider(provider)) {
try {
return await validateOpenAICompatibleProvider({ apiKey, providerSpecificData });
Expand Down
6 changes: 6 additions & 0 deletions src/shared/constants/errorCodes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,12 @@ export const ERROR_CODES: Record<string, ErrorCodeDef> = {
httpStatus: 400,
category: "AUTH",
},
AUTH_007: {
code: "AUTH_007",
message: "Session expired (re-login required)",
httpStatus: 401,
category: "AUTH",
},

// ── Proxy ──
PROXY_001: {
Expand Down
102 changes: 102 additions & 0 deletions tests/unit/provider-validation-web-cookie-auth007.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
import test from "node:test";
import assert from "node:assert/strict";

// The validator probes the provider's /models endpoint via safeOutboundFetch →
// fetchWithTimeout, which binds globalThis.fetch at MODULE LOAD time. The mock MUST be
// installed BEFORE importing validation.ts — a late reassignment (inside a test) is
// ignored and the validator hits the real network instead. (That made the 401/403
// assertions pass only by coincidence — live chatgpt.com returns 401/403 — while the
// 200 case failed.) A mutable `nextResponse` lets each test vary the probe result, and
// `fetchCalls` proves the mocked probe ran rather than the live network.
let nextResponse: { status: number; body: string } = { status: 200, body: "{}" };
let fetchCalls = 0;
globalThis.fetch = (async () => {
fetchCalls++;
return new Response(nextResponse.body, {
status: nextResponse.status,
headers: { "content-type": "application/json" },
});
}) as typeof fetch;

const { validateWebCookieProvider } = await import("../../src/lib/providers/validation.ts");

function mockFetch(status: number, body: string) {
nextResponse = { status, body };
fetchCalls = 0;
}

test("should_return_AUTH_007_when_models_endpoint_returns_401", async () => {
mockFetch(401, JSON.stringify({ error: "unauthorized" }));

const result = await validateWebCookieProvider({
provider: "chatgpt-web",
apiKey: "expired_cookie=session=abc123",
providerSpecificData: {},
});

assert.equal(fetchCalls, 1, "the /models probe must be the mocked fetch, not the live network");
assert.equal(result.valid, false);
assert.equal(result.errorCode, "AUTH_007");
assert.equal(result.error, "SESSION_EXPIRED");
});

test("should_return_AUTH_007_when_models_endpoint_returns_403", async () => {
mockFetch(403, JSON.stringify({ error: "forbidden" }));

const result = await validateWebCookieProvider({
provider: "chatgpt-web",
apiKey: "expired_cookie=session=abc123",
providerSpecificData: {},
});

assert.equal(fetchCalls, 1, "the /models probe must be the mocked fetch, not the live network");
assert.equal(result.valid, false);
assert.equal(result.errorCode, "AUTH_007");
assert.equal(result.error, "SESSION_EXPIRED");
});

test("should_return_valid_when_models_endpoint_returns_200", async () => {
// A non-401/403 status from the /models probe means the cookie was accepted,
// so the session is treated as valid.
mockFetch(200, JSON.stringify({ ok: true, data: [] }));

const result = await validateWebCookieProvider({
provider: "chatgpt-web",
apiKey: "valid_cookie=session=abc123",
providerSpecificData: {},
});

assert.equal(fetchCalls, 1, "the /models probe must be the mocked fetch, not the live network");
assert.equal(result.valid, true);
});

test("should_return_unsupported_when_provider_not_in_registry", async () => {
mockFetch(200, "{}");

const result = await validateWebCookieProvider({
provider: "nonexistent-provider",
apiKey: "some_cookie",
providerSpecificData: {},
});

// Unknown provider short-circuits before any network probe.
assert.equal(fetchCalls, 0);
assert.equal(result.valid, false);
assert.equal(result.unsupported, true);
});

test("should_return_error_when_cookie_is_empty", async () => {
mockFetch(200, "{}");

const result = await validateWebCookieProvider({
provider: "chatgpt-web",
apiKey: "",
providerSpecificData: {},
});

// Empty cookie short-circuits before any network probe.
assert.equal(fetchCalls, 0);
assert.equal(result.valid, false);
assert.equal(result.unsupported, false);
assert.match(result.error, /cookie/i);
});
Loading