diff --git a/src/agents/minimax-vlm.normalizes-api-key.test.ts b/src/agents/minimax-vlm.normalizes-api-key.test.ts new file mode 100644 index 00000000000..2d8fa0b0a20 --- /dev/null +++ b/src/agents/minimax-vlm.normalizes-api-key.test.ts @@ -0,0 +1,39 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +describe("minimaxUnderstandImage apiKey normalization", () => { + const priorFetch = global.fetch; + + afterEach(() => { + // @ts-expect-error restore + global.fetch = priorFetch; + vi.restoreAllMocks(); + }); + + it("strips embedded CR/LF before sending Authorization header", async () => { + const fetchSpy = vi.fn(async (_input: RequestInfo | URL, init?: RequestInit) => { + const auth = (init?.headers as Record | undefined)?.Authorization; + expect(auth).toBe("Bearer minimax-test-key"); + + return new Response( + JSON.stringify({ + base_resp: { status_code: 0, status_msg: "ok" }, + content: "ok", + }), + { status: 200, headers: { "Content-Type": "application/json" } }, + ); + }); + // @ts-expect-error mock fetch + global.fetch = fetchSpy; + + const { minimaxUnderstandImage } = await import("./minimax-vlm.js"); + const text = await minimaxUnderstandImage({ + apiKey: "minimax-test-\r\nkey", + prompt: "hi", + imageDataUrl: "data:image/png;base64,AAAA", + apiHost: "https://api.minimax.io", + }); + + expect(text).toBe("ok"); + expect(fetchSpy).toHaveBeenCalled(); + }); +}); diff --git a/src/agents/tools/web-fetch.firecrawl-api-key-normalization.test.ts b/src/agents/tools/web-fetch.firecrawl-api-key-normalization.test.ts new file mode 100644 index 00000000000..9e7fc694858 --- /dev/null +++ b/src/agents/tools/web-fetch.firecrawl-api-key-normalization.test.ts @@ -0,0 +1,62 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +vi.mock("../../infra/net/fetch-guard.js", () => { + return { + fetchWithSsrFGuard: vi.fn(async () => { + throw new Error("network down"); + }), + }; +}); + +describe("web_fetch firecrawl apiKey normalization", () => { + const priorFetch = global.fetch; + + afterEach(() => { + // @ts-expect-error restore + global.fetch = priorFetch; + vi.restoreAllMocks(); + }); + + it("strips embedded CR/LF before sending Authorization header", async () => { + const fetchSpy = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => { + const url = typeof input === "string" ? input : input instanceof URL ? input.toString() : ""; + expect(url).toContain("/v2/scrape"); + + const auth = (init?.headers as Record | undefined)?.Authorization; + expect(auth).toBe("Bearer firecrawl-test-key"); + + return new Response( + JSON.stringify({ + success: true, + data: { markdown: "ok", metadata: { title: "t" } }, + }), + { status: 200, headers: { "Content-Type": "application/json" } }, + ); + }); + + // @ts-expect-error mock fetch + global.fetch = fetchSpy; + + const { createWebFetchTool } = await import("./web-tools.js"); + const tool = createWebFetchTool({ + config: { + tools: { + web: { + fetch: { + cacheTtlMinutes: 0, + firecrawl: { apiKey: "firecrawl-test-\r\nkey" }, + readability: false, + }, + }, + }, + }, + }); + + const result = await tool?.execute?.("call", { + url: "https://example.com", + extractMode: "text", + }); + expect(result?.details).toMatchObject({ extractor: "firecrawl" }); + expect(fetchSpy).toHaveBeenCalled(); + }); +}); diff --git a/src/gateway/server-methods/skills.update.normalizes-api-key.test.ts b/src/gateway/server-methods/skills.update.normalizes-api-key.test.ts new file mode 100644 index 00000000000..45b9d719e7c --- /dev/null +++ b/src/gateway/server-methods/skills.update.normalizes-api-key.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it, vi } from "vitest"; + +let writtenConfig: unknown = null; + +vi.mock("../../config/config.js", () => { + return { + loadConfig: () => ({ + skills: { + entries: {}, + }, + }), + writeConfigFile: async (cfg: unknown) => { + writtenConfig = cfg; + }, + }; +}); + +describe("skills.update", () => { + it("strips embedded CR/LF from apiKey", async () => { + writtenConfig = null; + const { skillsHandlers } = await import("./skills.js"); + + let ok: boolean | null = null; + let error: unknown = null; + await skillsHandlers["skills.update"]({ + params: { + skillKey: "brave-search", + apiKey: "abc\r\ndef", + }, + respond: (success, _result, err) => { + ok = success; + error = err; + }, + }); + + expect(ok).toBe(true); + expect(error).toBeUndefined(); + expect(writtenConfig).toMatchObject({ + skills: { + entries: { + "brave-search": { + apiKey: "abcdef", + }, + }, + }, + }); + }); +}); diff --git a/src/infra/provider-usage.auth.normalizes-keys.test.ts b/src/infra/provider-usage.auth.normalizes-keys.test.ts new file mode 100644 index 00000000000..1b1edb579ae --- /dev/null +++ b/src/infra/provider-usage.auth.normalizes-keys.test.ts @@ -0,0 +1,73 @@ +import fs from "node:fs/promises"; +import path from "node:path"; +import { describe, expect, it, vi } from "vitest"; +import { withTempHome } from "../../test/helpers/temp-home.js"; + +describe("resolveProviderAuths key normalization", () => { + it("strips embedded CR/LF from env keys", async () => { + await withTempHome( + async () => { + vi.resetModules(); + const { resolveProviderAuths } = await import("./provider-usage.auth.js"); + + const auths = await resolveProviderAuths({ + providers: ["zai", "minimax", "xiaomi"], + }); + expect(auths).toEqual([ + { provider: "zai", token: "zai-key" }, + { provider: "minimax", token: "minimax-key" }, + { provider: "xiaomi", token: "xiaomi-key" }, + ]); + }, + { + env: { + ZAI_API_KEY: "zai-\r\nkey", + MINIMAX_API_KEY: "minimax-\r\nkey", + XIAOMI_API_KEY: "xiaomi-\r\nkey", + }, + }, + ); + }); + + it("strips embedded CR/LF from stored auth profiles (token + api_key)", async () => { + await withTempHome( + async (home) => { + const agentDir = path.join(home, ".openclaw", "agents", "main", "agent"); + await fs.mkdir(agentDir, { recursive: true }); + await fs.writeFile( + path.join(agentDir, "auth-profiles.json"), + `${JSON.stringify( + { + version: 1, + profiles: { + "minimax:default": { type: "token", provider: "minimax", token: "mini-\r\nmax" }, + "xiaomi:default": { type: "api_key", provider: "xiaomi", key: "xiao-\r\nmi" }, + }, + }, + null, + 2, + )}\n`, + "utf8", + ); + + vi.resetModules(); + const { resolveProviderAuths } = await import("./provider-usage.auth.js"); + + const auths = await resolveProviderAuths({ + providers: ["minimax", "xiaomi"], + }); + expect(auths).toEqual([ + { provider: "minimax", token: "mini-max" }, + { provider: "xiaomi", token: "xiao-mi" }, + ]); + }, + { + env: { + MINIMAX_API_KEY: undefined, + MINIMAX_CODE_PLAN_KEY: undefined, + XIAOMI_API_KEY: undefined, + }, + }, + ); + }); +});