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
1 change: 1 addition & 0 deletions changelog.d/fixes/10329-zai-web-auth-semantics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- **fix(providers):** validate Z.ai web Local Storage sessions against the authenticated user-settings endpoint and preserve exact upstream status codes ([#10329](https://github.com/diegosouzapw/OmniRoute/pull/10329)) — thanks @Zartharas
4 changes: 3 additions & 1 deletion src/lib/providers/validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ import {
validatePoeProvider,
} from "./validation/audioMiscProviders";
import { validateChatGptWebCodexProvider } from "./validation/chatgptWebCodex";
import { validateZaiWebProvider } from "./validation/zaiWeb";
import { validateSearchProvider, SEARCH_VALIDATOR_CONFIGS } from "./validation/searchProviders";
import {
validateClarifaiProvider,
Expand Down Expand Up @@ -227,7 +228,7 @@ export async function validateProviderApiKey({ provider, apiKey, providerSpecifi
error:
"Modal requires a Base URL pointing to your OpenAI-compatible Modal app " +
"(e.g. https://<workspace>--<app>.modal.run/v1). " +
"Fill in the \"Base URL override\" field.",
'Fill in the "Base URL override" field.',
};
}
return validateOpenAILikeProvider({
Expand All @@ -249,6 +250,7 @@ export async function validateProviderApiKey({ provider, apiKey, providerSpecifi
snowflake: validateSnowflakeProvider,
gigachat: validateGigachatProvider,
"deepseek-web": validateDeepSeekWebProvider,
"zai-web": validateZaiWebProvider,
"grok-web": validateGrokWebProvider,
"qwen-web": validateQwenWebProvider,
"kimi-web": validateKimiWebProvider,
Expand Down
52 changes: 52 additions & 0 deletions src/lib/providers/validation/zaiWeb.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { extractZaiToken } from "@omniroute/open-sse/services/zaiWebCredentials.ts";
import { toValidationErrorResult, validationRead } from "./transport";

const ZAI_SESSION_PROBE_URL = "https://chat.z.ai/api/v1/users/user/settings";

export async function validateZaiWebProvider({ apiKey }: { apiKey?: string }) {
const token = extractZaiToken(String(apiKey || ""));

if (!token) {
return {
valid: false,
error:
'Invalid Z.ai web-session credential — copy the "token" value from chat.z.ai Local Storage.',
};
}

try {
const response = await validationRead(ZAI_SESSION_PROBE_URL, {
method: "GET",
headers: {
Accept: "application/json, text/plain, */*",
Authorization: `Bearer ${token}`,
Origin: "https://chat.z.ai",
Referer: "https://chat.z.ai/",
},
});

if (response.status >= 200 && response.status < 300) {
return {
valid: true,
error: null,
};
}

if (response.status === 401) {
return {
valid: false,
error:
'Invalid or expired Z.ai web-session credential — copy a fresh "token" value from chat.z.ai Local Storage.',
statusCode: 401,
};
}

return {
valid: false,
error: `Z.ai session validation returned HTTP ${response.status}`,
statusCode: response.status,
};
} catch (error: unknown) {
return toValidationErrorResult(error);
}
}
108 changes: 108 additions & 0 deletions tests/unit/zai-web-auth-semantics.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import test from "node:test";
import assert from "node:assert/strict";

const originalFetch = globalThis.fetch;

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

let nextStatus = 200;
let lastRequest: {
url: string;
method: string;
authorization: string;
cookie: string;
} | null = null;

let lastResponse: Response | null = null;

globalThis.fetch = (async (input: RequestInfo | URL, init?: RequestInit) => {
const headers = new Headers(init?.headers);

lastRequest = {
url: String(input),
method: String(init?.method || "GET"),
authorization: headers.get("authorization") || "",
cookie: headers.get("cookie") || "",
};

lastResponse = new Response(
JSON.stringify({
secret: "response-body-must-not-be-consumed",
}),
{
status: nextStatus,
headers: {
"content-type": "application/json",
},
}
);

return lastResponse;
}) as typeof fetch;

test.after(() => {
globalThis.fetch = originalFetch;
});

async function validate(status: number) {
nextStatus = status;
lastRequest = null;
lastResponse = null;

return validateProviderApiKey({
provider: "zai-web",
apiKey: "synthetic-zai-token",
providerSpecificData: {},
});
}

test("zai-web uses the token-only authenticated user-settings GET", async () => {
const result = await validate(200);

assert.equal(result.valid, true);

assert.equal(lastRequest?.url, "https://chat.z.ai/api/v1/users/user/settings");

assert.equal(lastRequest?.method, "GET");

assert.equal(lastRequest?.authorization, "Bearer synthetic-zai-token");

assert.equal(lastRequest?.cookie, "");

assert.equal(lastResponse?.bodyUsed, false);
});

test("zai-web preserves exact 401 as credential rejection", async () => {
const result = await validate(401);

assert.equal(result.valid, false);
assert.equal(result.statusCode, 401);

assert.match(result.error, /invalid or expired/i);

assert.equal(lastResponse?.bodyUsed, false);
});

test("zai-web preserves 403 without calling it expired", async () => {
const result = await validate(403);

assert.equal(result.valid, false);
assert.equal(result.statusCode, 403);

assert.doesNotMatch(result.error, /invalid or expired/i);

assert.equal(lastResponse?.bodyUsed, false);
});

test("zai-web preserves rate-limit and server statuses", async () => {
for (const status of [429, 503]) {
const result = await validate(status);

assert.equal(result.valid, false);
assert.equal(result.statusCode, status);

assert.doesNotMatch(result.error, /invalid or expired/i);

assert.equal(lastResponse?.bodyUsed, false);
}
});
Loading