mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-16 20:22:21 +03:00
fix(providers): validate Z.ai web auth semantics (#10329)
This commit is contained in:
1
changelog.d/fixes/10329-zai-web-auth-semantics.md
Normal file
1
changelog.d/fixes/10329-zai-web-auth-semantics.md
Normal file
@@ -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
|
||||
@@ -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,
|
||||
@@ -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({
|
||||
@@ -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,
|
||||
|
||||
52
src/lib/providers/validation/zaiWeb.ts
Normal file
52
src/lib/providers/validation/zaiWeb.ts
Normal file
@@ -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
tests/unit/zai-web-auth-semantics.test.ts
Normal file
108
tests/unit/zai-web-auth-semantics.test.ts
Normal file
@@ -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);
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user