mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 10:57:40 +00:00
fix(auth-profiles): accept mode/apiKey aliases to prevent silent credential loss
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
This commit is contained in:
committed by
Peter Steinberger
parent
85b075d0cc
commit
7e7ca43a79
@@ -122,4 +122,36 @@ describe("ensureAuthProfileStore", () => {
|
|||||||
fs.rmSync(root, { recursive: true, force: true });
|
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 });
|
||||||
|
}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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<string, unknown>): Partial<AuthProfileCredential> {
|
||||||
|
const entry = { ...raw } as Record<string, unknown>;
|
||||||
|
// 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<AuthProfileCredential>;
|
||||||
|
}
|
||||||
|
|
||||||
function coerceLegacyStore(raw: unknown): LegacyAuthStore | null {
|
function coerceLegacyStore(raw: unknown): LegacyAuthStore | null {
|
||||||
if (!raw || typeof raw !== "object") {
|
if (!raw || typeof raw !== "object") {
|
||||||
return null;
|
return null;
|
||||||
@@ -52,7 +74,7 @@ function coerceLegacyStore(raw: unknown): LegacyAuthStore | null {
|
|||||||
if (!value || typeof value !== "object") {
|
if (!value || typeof value !== "object") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const typed = value as Partial<AuthProfileCredential>;
|
const typed = normalizeRawCredentialEntry(value as Record<string, unknown>);
|
||||||
if (typed.type !== "api_key" && typed.type !== "oauth" && typed.type !== "token") {
|
if (typed.type !== "api_key" && typed.type !== "oauth" && typed.type !== "token") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -78,7 +100,7 @@ function coerceAuthStore(raw: unknown): AuthProfileStore | null {
|
|||||||
if (!value || typeof value !== "object") {
|
if (!value || typeof value !== "object") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
const typed = value as Partial<AuthProfileCredential>;
|
const typed = normalizeRawCredentialEntry(value as Record<string, unknown>);
|
||||||
if (typed.type !== "api_key" && typed.type !== "oauth" && typed.type !== "token") {
|
if (typed.type !== "api_key" && typed.type !== "oauth" && typed.type !== "token") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user