mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-09 20:14:30 +00:00
refactor(auth): share oauth profile config checks
This commit is contained in:
106
src/agents/auth-profiles/oauth.test.ts
Normal file
106
src/agents/auth-profiles/oauth.test.ts
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
import type { OpenClawConfig } from "../../config/config.js";
|
||||||
|
import { resolveApiKeyForProfile } from "./oauth.js";
|
||||||
|
import type { AuthProfileStore } from "./types.js";
|
||||||
|
|
||||||
|
function cfgFor(profileId: string, provider: string, mode: "api_key" | "token" | "oauth") {
|
||||||
|
return {
|
||||||
|
auth: {
|
||||||
|
profiles: {
|
||||||
|
[profileId]: { provider, mode },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies OpenClawConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("resolveApiKeyForProfile config compatibility", () => {
|
||||||
|
it("accepts token credentials when config mode is oauth", async () => {
|
||||||
|
const profileId = "anthropic:token";
|
||||||
|
const store: AuthProfileStore = {
|
||||||
|
version: 1,
|
||||||
|
profiles: {
|
||||||
|
[profileId]: {
|
||||||
|
type: "token",
|
||||||
|
provider: "anthropic",
|
||||||
|
token: "tok-123",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await resolveApiKeyForProfile({
|
||||||
|
cfg: cfgFor(profileId, "anthropic", "oauth"),
|
||||||
|
store,
|
||||||
|
profileId,
|
||||||
|
});
|
||||||
|
expect(result).toEqual({
|
||||||
|
apiKey: "tok-123",
|
||||||
|
provider: "anthropic",
|
||||||
|
email: undefined,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects token credentials when config mode is api_key", async () => {
|
||||||
|
const profileId = "anthropic:token";
|
||||||
|
const store: AuthProfileStore = {
|
||||||
|
version: 1,
|
||||||
|
profiles: {
|
||||||
|
[profileId]: {
|
||||||
|
type: "token",
|
||||||
|
provider: "anthropic",
|
||||||
|
token: "tok-123",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await resolveApiKeyForProfile({
|
||||||
|
cfg: cfgFor(profileId, "anthropic", "api_key"),
|
||||||
|
store,
|
||||||
|
profileId,
|
||||||
|
});
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects oauth credentials when config mode is token", async () => {
|
||||||
|
const profileId = "anthropic:oauth";
|
||||||
|
const store: AuthProfileStore = {
|
||||||
|
version: 1,
|
||||||
|
profiles: {
|
||||||
|
[profileId]: {
|
||||||
|
type: "oauth",
|
||||||
|
provider: "anthropic",
|
||||||
|
access: "access-123",
|
||||||
|
refresh: "refresh-123",
|
||||||
|
expires: Date.now() + 60_000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await resolveApiKeyForProfile({
|
||||||
|
cfg: cfgFor(profileId, "anthropic", "token"),
|
||||||
|
store,
|
||||||
|
profileId,
|
||||||
|
});
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("rejects credentials when provider does not match config", async () => {
|
||||||
|
const profileId = "anthropic:token";
|
||||||
|
const store: AuthProfileStore = {
|
||||||
|
version: 1,
|
||||||
|
profiles: {
|
||||||
|
[profileId]: {
|
||||||
|
type: "token",
|
||||||
|
provider: "anthropic",
|
||||||
|
token: "tok-123",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const result = await resolveApiKeyForProfile({
|
||||||
|
cfg: cfgFor(profileId, "openai", "token"),
|
||||||
|
store,
|
||||||
|
profileId,
|
||||||
|
});
|
||||||
|
expect(result).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -23,6 +23,31 @@ const isOAuthProvider = (provider: string): provider is OAuthProvider =>
|
|||||||
const resolveOAuthProvider = (provider: string): OAuthProvider | null =>
|
const resolveOAuthProvider = (provider: string): OAuthProvider | null =>
|
||||||
isOAuthProvider(provider) ? provider : null;
|
isOAuthProvider(provider) ? provider : null;
|
||||||
|
|
||||||
|
function isProfileConfigCompatible(params: {
|
||||||
|
cfg?: OpenClawConfig;
|
||||||
|
profileId: string;
|
||||||
|
provider: string;
|
||||||
|
mode: "api_key" | "token" | "oauth";
|
||||||
|
allowOAuthTokenCompatibility?: boolean;
|
||||||
|
}): boolean {
|
||||||
|
const profileConfig = params.cfg?.auth?.profiles?.[params.profileId];
|
||||||
|
if (profileConfig && profileConfig.provider !== params.provider) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (profileConfig && profileConfig.mode !== params.mode) {
|
||||||
|
if (
|
||||||
|
!(
|
||||||
|
params.allowOAuthTokenCompatibility &&
|
||||||
|
profileConfig.mode === "oauth" &&
|
||||||
|
params.mode === "token"
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
function buildOAuthApiKey(provider: string, credentials: OAuthCredentials): string {
|
function buildOAuthApiKey(provider: string, credentials: OAuthCredentials): string {
|
||||||
const needsProjectId = provider === "google-gemini-cli" || provider === "google-antigravity";
|
const needsProjectId = provider === "google-gemini-cli" || provider === "google-antigravity";
|
||||||
return needsProjectId
|
return needsProjectId
|
||||||
@@ -33,6 +58,14 @@ function buildOAuthApiKey(provider: string, credentials: OAuthCredentials): stri
|
|||||||
: credentials.access;
|
: credentials.access;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function buildApiKeyProfileResult(params: { apiKey: string; provider: string; email?: string }) {
|
||||||
|
return {
|
||||||
|
apiKey: params.apiKey,
|
||||||
|
provider: params.provider,
|
||||||
|
email: params.email,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function refreshOAuthTokenWithLock(params: {
|
async function refreshOAuthTokenWithLock(params: {
|
||||||
profileId: string;
|
profileId: string;
|
||||||
agentDir?: string;
|
agentDir?: string;
|
||||||
@@ -103,20 +136,23 @@ async function tryResolveOAuthProfile(params: {
|
|||||||
if (!cred || cred.type !== "oauth") {
|
if (!cred || cred.type !== "oauth") {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const profileConfig = cfg?.auth?.profiles?.[profileId];
|
if (
|
||||||
if (profileConfig && profileConfig.provider !== cred.provider) {
|
!isProfileConfigCompatible({
|
||||||
return null;
|
cfg,
|
||||||
}
|
profileId,
|
||||||
if (profileConfig && profileConfig.mode !== cred.type) {
|
provider: cred.provider,
|
||||||
|
mode: cred.type,
|
||||||
|
})
|
||||||
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Date.now() < cred.expires) {
|
if (Date.now() < cred.expires) {
|
||||||
return {
|
return buildApiKeyProfileResult({
|
||||||
apiKey: buildOAuthApiKey(cred.provider, cred),
|
apiKey: buildOAuthApiKey(cred.provider, cred),
|
||||||
provider: cred.provider,
|
provider: cred.provider,
|
||||||
email: cred.email,
|
email: cred.email,
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const refreshed = await refreshOAuthTokenWithLock({
|
const refreshed = await refreshOAuthTokenWithLock({
|
||||||
@@ -126,11 +162,11 @@ async function tryResolveOAuthProfile(params: {
|
|||||||
if (!refreshed) {
|
if (!refreshed) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return {
|
return buildApiKeyProfileResult({
|
||||||
apiKey: refreshed.apiKey,
|
apiKey: refreshed.apiKey,
|
||||||
provider: cred.provider,
|
provider: cred.provider,
|
||||||
email: cred.email,
|
email: cred.email,
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function resolveApiKeyForProfile(params: {
|
export async function resolveApiKeyForProfile(params: {
|
||||||
@@ -144,23 +180,25 @@ export async function resolveApiKeyForProfile(params: {
|
|||||||
if (!cred) {
|
if (!cred) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const profileConfig = cfg?.auth?.profiles?.[profileId];
|
if (
|
||||||
if (profileConfig && profileConfig.provider !== cred.provider) {
|
!isProfileConfigCompatible({
|
||||||
|
cfg,
|
||||||
|
profileId,
|
||||||
|
provider: cred.provider,
|
||||||
|
mode: cred.type,
|
||||||
|
// Compatibility: treat "oauth" config as compatible with stored token profiles.
|
||||||
|
allowOAuthTokenCompatibility: true,
|
||||||
|
})
|
||||||
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (profileConfig && profileConfig.mode !== cred.type) {
|
|
||||||
// Compatibility: treat "oauth" config as compatible with stored token profiles.
|
|
||||||
if (!(profileConfig.mode === "oauth" && cred.type === "token")) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (cred.type === "api_key") {
|
if (cred.type === "api_key") {
|
||||||
const key = cred.key?.trim();
|
const key = cred.key?.trim();
|
||||||
if (!key) {
|
if (!key) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return { apiKey: key, provider: cred.provider, email: cred.email };
|
return buildApiKeyProfileResult({ apiKey: key, provider: cred.provider, email: cred.email });
|
||||||
}
|
}
|
||||||
if (cred.type === "token") {
|
if (cred.type === "token") {
|
||||||
const token = cred.token?.trim();
|
const token = cred.token?.trim();
|
||||||
@@ -175,14 +213,14 @@ export async function resolveApiKeyForProfile(params: {
|
|||||||
) {
|
) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return { apiKey: token, provider: cred.provider, email: cred.email };
|
return buildApiKeyProfileResult({ apiKey: token, provider: cred.provider, email: cred.email });
|
||||||
}
|
}
|
||||||
if (Date.now() < cred.expires) {
|
if (Date.now() < cred.expires) {
|
||||||
return {
|
return buildApiKeyProfileResult({
|
||||||
apiKey: buildOAuthApiKey(cred.provider, cred),
|
apiKey: buildOAuthApiKey(cred.provider, cred),
|
||||||
provider: cred.provider,
|
provider: cred.provider,
|
||||||
email: cred.email,
|
email: cred.email,
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -193,20 +231,20 @@ export async function resolveApiKeyForProfile(params: {
|
|||||||
if (!result) {
|
if (!result) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return {
|
return buildApiKeyProfileResult({
|
||||||
apiKey: result.apiKey,
|
apiKey: result.apiKey,
|
||||||
provider: cred.provider,
|
provider: cred.provider,
|
||||||
email: cred.email,
|
email: cred.email,
|
||||||
};
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const refreshedStore = ensureAuthProfileStore(params.agentDir);
|
const refreshedStore = ensureAuthProfileStore(params.agentDir);
|
||||||
const refreshed = refreshedStore.profiles[profileId];
|
const refreshed = refreshedStore.profiles[profileId];
|
||||||
if (refreshed?.type === "oauth" && Date.now() < refreshed.expires) {
|
if (refreshed?.type === "oauth" && Date.now() < refreshed.expires) {
|
||||||
return {
|
return buildApiKeyProfileResult({
|
||||||
apiKey: buildOAuthApiKey(refreshed.provider, refreshed),
|
apiKey: buildOAuthApiKey(refreshed.provider, refreshed),
|
||||||
provider: refreshed.provider,
|
provider: refreshed.provider,
|
||||||
email: refreshed.email ?? cred.email,
|
email: refreshed.email ?? cred.email,
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
const fallbackProfileId = suggestOAuthProfileIdForLegacyDefault({
|
const fallbackProfileId = suggestOAuthProfileIdForLegacyDefault({
|
||||||
cfg,
|
cfg,
|
||||||
@@ -244,11 +282,11 @@ export async function resolveApiKeyForProfile(params: {
|
|||||||
agentDir: params.agentDir,
|
agentDir: params.agentDir,
|
||||||
expires: new Date(mainCred.expires).toISOString(),
|
expires: new Date(mainCred.expires).toISOString(),
|
||||||
});
|
});
|
||||||
return {
|
return buildApiKeyProfileResult({
|
||||||
apiKey: buildOAuthApiKey(mainCred.provider, mainCred),
|
apiKey: buildOAuthApiKey(mainCred.provider, mainCred),
|
||||||
provider: mainCred.provider,
|
provider: mainCred.provider,
|
||||||
email: mainCred.email,
|
email: mainCred.email,
|
||||||
};
|
});
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
// keep original error if main agent fallback also fails
|
// keep original error if main agent fallback also fails
|
||||||
|
|||||||
Reference in New Issue
Block a user