From 7e7ca43a797534c6ec7c171bc7e4d1cea6a456b7 Mon Sep 17 00:00:00 2001 From: lbo728 Date: Thu, 26 Feb 2026 08:27:59 +0900 Subject: [PATCH] fix(auth-profiles): accept mode/apiKey aliases to prevent silent credential loss MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Users following openclaw.json auth.profiles examples (which use 'mode' for the credential type) would write their auth-profiles.json entries with: { provider: "anthropic", mode: "api_key", apiKey: "sk-ant-..." } The actual auth-profiles.json schema uses: { provider: "anthropic", type: "api_key", key: "sk-ant-..." } coerceAuthStore() and coerceLegacyStore() validated entries strictly on typed.type, silently skipping any entry that used the mode/apiKey spelling. The user would get 'No API key found for provider anthropic' with no hint about the field name mismatch. Add normalizeRawCredentialEntry() which, before validation: - coerces mode → type when type is absent - coerces apiKey → key when key is absent Both functions now call the normalizer before the type guard so mode/apiKey entries are loaded and resolved correctly. Fixes #26916 --- ...th-profiles.ensureauthprofilestore.test.ts | 32 +++++++++++++++++++ src/agents/auth-profiles/store.ts | 26 +++++++++++++-- 2 files changed, 56 insertions(+), 2 deletions(-) diff --git a/src/agents/auth-profiles.ensureauthprofilestore.test.ts b/src/agents/auth-profiles.ensureauthprofilestore.test.ts index e106a2391e7..72cf9e8f9b6 100644 --- a/src/agents/auth-profiles.ensureauthprofilestore.test.ts +++ b/src/agents/auth-profiles.ensureauthprofilestore.test.ts @@ -122,4 +122,36 @@ describe("ensureAuthProfileStore", () => { fs.rmSync(root, { recursive: true, force: true }); } }); + + it("accepts mode/apiKey aliases so users who follow openclaw.json format are not silently broken", () => { + // A common mistake: users write auth-profiles.json using the same field names + // as openclaw.json auth.profiles ("mode" + "apiKey") instead of the canonical + // auth-profiles.json fields ("type" + "key"). The parser now normalises both. + const agentDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-auth-alias-")); + try { + const storeWithAliases = { + version: AUTH_STORE_VERSION, + profiles: { + "anthropic:work": { + provider: "anthropic", + mode: "api_key", // alias for "type" + apiKey: "sk-ant-alias-test", // alias for "key" + }, + }, + }; + fs.writeFileSync( + path.join(agentDir, "auth-profiles.json"), + `${JSON.stringify(storeWithAliases, null, 2)}\n`, + "utf8", + ); + + const store = ensureAuthProfileStore(agentDir); + const profile = store.profiles["anthropic:work"]; + expect(profile).toBeDefined(); + expect(profile?.type).toBe("api_key"); + expect((profile as { key?: string }).key).toBe("sk-ant-alias-test"); + } finally { + fs.rmSync(agentDir, { recursive: true, force: true }); + } + }); }); diff --git a/src/agents/auth-profiles/store.ts b/src/agents/auth-profiles/store.ts index 4e6b1f91bf6..b0418647299 100644 --- a/src/agents/auth-profiles/store.ts +++ b/src/agents/auth-profiles/store.ts @@ -39,6 +39,28 @@ export async function updateAuthProfileStoreWithLock(params: { } } +/** + * Normalise a raw auth-profiles.json credential entry. + * + * The official format uses `type` and (for api_key credentials) `key`. + * A common mistake — caused by the similarity with the `openclaw.json` + * `auth.profiles` section which uses `mode` — is to write `mode` instead of + * `type` and `apiKey` instead of `key`. Accept both spellings so users don't + * silently lose their credentials. + */ +function normalizeRawCredentialEntry(raw: Record): Partial { + const entry = { ...raw } as Record; + // mode → type alias (openclaw.json uses "mode"; auth-profiles.json uses "type") + if (!("type" in entry) && typeof entry["mode"] === "string") { + entry["type"] = entry["mode"]; + } + // apiKey → key alias for ApiKeyCredential + if (!("key" in entry) && typeof entry["apiKey"] === "string") { + entry["key"] = entry["apiKey"]; + } + return entry as Partial; +} + function coerceLegacyStore(raw: unknown): LegacyAuthStore | null { if (!raw || typeof raw !== "object") { return null; @@ -52,7 +74,7 @@ function coerceLegacyStore(raw: unknown): LegacyAuthStore | null { if (!value || typeof value !== "object") { continue; } - const typed = value as Partial; + const typed = normalizeRawCredentialEntry(value as Record); if (typed.type !== "api_key" && typed.type !== "oauth" && typed.type !== "token") { continue; } @@ -78,7 +100,7 @@ function coerceAuthStore(raw: unknown): AuthProfileStore | null { if (!value || typeof value !== "object") { continue; } - const typed = value as Partial; + const typed = normalizeRawCredentialEntry(value as Record); if (typed.type !== "api_key" && typed.type !== "oauth" && typed.type !== "token") { continue; }