diff --git a/open-sse/executors/default.ts b/open-sse/executors/default.ts index 367ff163b4..1f12173a41 100644 --- a/open-sse/executors/default.ts +++ b/open-sse/executors/default.ts @@ -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}`; + } } } } diff --git a/scripts/bootstrap-env.mjs b/scripts/bootstrap-env.mjs index 91e650aa9e..c1945b6abb 100644 --- a/scripts/bootstrap-env.mjs +++ b/scripts/bootstrap-env.mjs @@ -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." ); diff --git a/tests/unit/executor-default-base.test.ts b/tests/unit/executor-default-base.test.ts index 0358fa87b8..055a35955a 100644 --- a/tests/unit/executor-default-base.test.ts +++ b/tests/unit/executor-default-base.test.ts @@ -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");