fix(executor): fix urlSuffix and authHeader bugs causing auth failures (Issue #1846) (#1861)

Integrated into release/v3.7.8
This commit is contained in:
Paijo
2026-05-02 03:44:24 +07:00
committed by GitHub
parent 3d78cd6848
commit 95a43597c9
3 changed files with 92 additions and 11 deletions

View File

@@ -8,6 +8,7 @@ import {
joinClaudeCodeCompatibleUrl,
} from "../services/claudeCodeCompatible.ts";
import { getGigachatAccessToken } from "../services/gigachatAuth.ts";
import { getRegistryEntry } from "../config/providerRegistry.ts";
import { applyProviderRequestDefaults } from "../services/providerRequestDefaults.ts";
import {
getOpenAICompatibleType,
@@ -198,6 +199,11 @@ export class DefaultExecutor extends BaseExecutor {
const baseUrl = credentials?.providerSpecificData?.baseUrl || this.config.baseUrl;
return normalizeOpenAIChatUrl(baseUrl);
}
case "zai":
case "glm-coding-apikey": {
const zaiBaseUrl = credentials?.providerSpecificData?.baseUrl || this.config.baseUrl;
return `${zaiBaseUrl}?beta=true`;
}
case "claude":
case "glm":
case "glmt":
@@ -211,8 +217,11 @@ export class DefaultExecutor extends BaseExecutor {
const resourceUrl = credentials?.providerSpecificData?.resourceUrl;
return `https://${resourceUrl || "portal.qwen.ai"}/v1/chat/completions`;
}
default:
return this.config.baseUrl;
default: {
const url = this.config.baseUrl;
const entry = getRegistryEntry(this.provider);
return entry?.urlSuffix ? `${url}${entry.urlSuffix}` : url;
}
}
}
@@ -307,6 +316,8 @@ export class DefaultExecutor extends BaseExecutor {
case "kimi-coding":
case "bailian-coding-plan":
case "kimi-coding-apikey":
case "zai":
case "glm-coding-apikey":
headers["x-api-key"] = effectiveKey || credentials.accessToken;
break;
default:
@@ -327,9 +338,18 @@ export class DefaultExecutor extends BaseExecutor {
headers["anthropic-version"] = "2023-06-01";
}
} else {
const bearerToken = effectiveKey || credentials.accessToken;
if (bearerToken) {
headers["Authorization"] = `Bearer ${bearerToken}`;
// Use registry authHeader if available, otherwise default to bearer
const entry = getRegistryEntry(this.provider);
const authHeader = entry?.authHeader || "bearer";
const token = effectiveKey || credentials.accessToken;
if (token) {
if (authHeader === "x-api-key") {
headers["x-api-key"] = token;
} else if (authHeader === "x-goog-api-key") {
headers["x-goog-api-key"] = token;
} else {
headers["Authorization"] = `Bearer ${token}`;
}
}
}
}

View File

@@ -18,7 +18,7 @@
* 4. process.env (shell / Docker -e flags, highest priority)
*/
import { randomBytes, createDecipheriv } from "node:crypto";
import { randomBytes, createDecipheriv, scryptSync, createHash } from "node:crypto";
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { createRequire } from "node:module";
import { homedir } from "node:os";
@@ -246,14 +246,41 @@ export function bootstrapEnv({ dataDirOverride, quiet = false } = {}) {
const iv = Buffer.from(parts[2], "hex");
const ct = Buffer.from(parts[3], "hex");
const tag = Buffer.from(parts[4], "hex");
const key = Buffer.from(merged.STORAGE_ENCRYPTION_KEY, "hex");
const decipher = createDecipheriv("aes-256-gcm", key, iv);
decipher.setAuthTag(tag);
try {
// Try decrypting with both key derivation methods matching encryption.ts
const tryDecrypt = (derivedKey) => {
const decipher = createDecipheriv("aes-256-gcm", derivedKey, iv);
decipher.setAuthTag(tag);
decipher.update(ct);
decipher.final();
// Decrypt succeeded — key matches
};
// Dynamic salt (current): scryptSync(secret, sha256(secret).slice(0,16), 32)
const dynamicSalt = createHash("sha256")
.update(merged.STORAGE_ENCRYPTION_KEY)
.digest()
.slice(0, 16);
const dynamicKey = scryptSync(merged.STORAGE_ENCRYPTION_KEY, dynamicSalt, 32);
// Legacy salt (fallback): scryptSync(secret, "omniroute-field-encryption-v1", 32)
const legacySalt = "omniroute-field-encryption-v1";
const legacyKey = scryptSync(merged.STORAGE_ENCRYPTION_KEY, legacySalt, 32);
let keyMatched = false;
try {
tryDecrypt(dynamicKey);
keyMatched = true;
} catch {
// Try legacy key as fallback
try {
tryDecrypt(legacyKey);
keyMatched = true;
} catch {
// Both failed — key truly doesn't match
}
}
if (!keyMatched) {
log(
"⛔ STORAGE_ENCRYPTION_KEY does not match the key used to encrypt your stored credentials."
);

View File

@@ -268,6 +268,40 @@ test("DefaultExecutor.buildUrl falls back to OpenAI config for unknown providers
assert.equal(executor.buildUrl("gpt-4.1", true), PROVIDERS.openai.baseUrl);
});
test("DefaultExecutor.buildUrl applies urlSuffix for zai and glm-coding-apikey", () => {
const zai = new DefaultExecutor("zai");
const glmCodingApikey = new DefaultExecutor("glm-coding-apikey");
assert.equal(
zai.buildUrl("glm-5", true, 0, {
providerSpecificData: { baseUrl: "https://api.z.ai/api/anthropic/v1/messages" },
}),
"https://api.z.ai/api/anthropic/v1/messages?beta=true"
);
assert.equal(
glmCodingApikey.buildUrl("glm-4.7", true, 0, {
providerSpecificData: { baseUrl: "https://api.z.ai/api/anthropic/v1/messages" },
}),
"https://api.z.ai/api/anthropic/v1/messages?beta=true"
);
assert.equal(zai.buildUrl("glm-5", true), "https://api.z.ai/api/anthropic/v1/messages?beta=true");
});
test("DefaultExecutor.buildUrl applies urlSuffix from registry for unknown providers with suffix", () => {
const executor = new DefaultExecutor("unknown-provider");
assert.equal(executor.buildUrl("gpt-4.1", true), PROVIDERS.openai.baseUrl);
});
test("DefaultExecutor.buildHeaders uses x-api-key for zai and glm-coding-apikey", () => {
const zai = new DefaultExecutor("zai");
const glmCodingApikey = new DefaultExecutor("glm-coding-apikey");
const zaiHeaders = zai.buildHeaders({ apiKey: "zai-key" }, true);
const glmHeaders = glmCodingApikey.buildHeaders({ apiKey: "glm-key" }, true);
assert.equal(zaiHeaders["x-api-key"], "zai-key");
assert.equal(glmHeaders["x-api-key"], "glm-key");
assert.equal(zaiHeaders["Authorization"], undefined);
assert.equal(glmHeaders["Authorization"], undefined);
});
test("DefaultExecutor.buildHeaders handles Gemini and Claude auth modes", () => {
const gemini = new DefaultExecutor("gemini");
const claude = new DefaultExecutor("claude");