273 lines
8.3 KiB
TypeScript
273 lines
8.3 KiB
TypeScript
import {
|
|
getOAuthApiKey,
|
|
getOAuthProviders,
|
|
type OAuthCredentials,
|
|
type OAuthProvider,
|
|
} from "@mariozechner/pi-ai";
|
|
import type { OpenClawConfig } from "../../config/config.js";
|
|
import type { AuthProfileStore } from "./types.js";
|
|
import { withFileLock } from "../../infra/file-lock.js";
|
|
import { refreshQwenPortalCredentials } from "../../providers/qwen-portal-oauth.js";
|
|
import { refreshChutesTokens } from "../chutes-oauth.js";
|
|
import { AUTH_STORE_LOCK_OPTIONS, log } from "./constants.js";
|
|
import { formatAuthDoctorHint } from "./doctor.js";
|
|
import { ensureAuthStoreFile, resolveAuthStorePath } from "./paths.js";
|
|
import { suggestOAuthProfileIdForLegacyDefault } from "./repair.js";
|
|
import { ensureAuthProfileStore, saveAuthProfileStore } from "./store.js";
|
|
|
|
const OAUTH_PROVIDER_IDS = new Set<string>(getOAuthProviders().map((provider) => provider.id));
|
|
|
|
const isOAuthProvider = (provider: string): provider is OAuthProvider =>
|
|
OAUTH_PROVIDER_IDS.has(provider);
|
|
|
|
const resolveOAuthProvider = (provider: string): OAuthProvider | null =>
|
|
isOAuthProvider(provider) ? provider : null;
|
|
|
|
function buildOAuthApiKey(provider: string, credentials: OAuthCredentials): string {
|
|
const needsProjectId = provider === "google-gemini-cli" || provider === "google-antigravity";
|
|
return needsProjectId
|
|
? JSON.stringify({
|
|
token: credentials.access,
|
|
projectId: credentials.projectId,
|
|
})
|
|
: credentials.access;
|
|
}
|
|
|
|
async function refreshOAuthTokenWithLock(params: {
|
|
profileId: string;
|
|
agentDir?: string;
|
|
}): Promise<{ apiKey: string; newCredentials: OAuthCredentials } | null> {
|
|
const authPath = resolveAuthStorePath(params.agentDir);
|
|
ensureAuthStoreFile(authPath);
|
|
|
|
return await withFileLock(authPath, AUTH_STORE_LOCK_OPTIONS, async () => {
|
|
const store = ensureAuthProfileStore(params.agentDir);
|
|
const cred = store.profiles[params.profileId];
|
|
if (!cred || cred.type !== "oauth") {
|
|
return null;
|
|
}
|
|
|
|
if (Date.now() < cred.expires) {
|
|
return {
|
|
apiKey: buildOAuthApiKey(cred.provider, cred),
|
|
newCredentials: cred,
|
|
};
|
|
}
|
|
|
|
const oauthCreds: Record<string, OAuthCredentials> = {
|
|
[cred.provider]: cred,
|
|
};
|
|
|
|
const result =
|
|
String(cred.provider) === "chutes"
|
|
? await (async () => {
|
|
const newCredentials = await refreshChutesTokens({
|
|
credential: cred,
|
|
});
|
|
return { apiKey: newCredentials.access, newCredentials };
|
|
})()
|
|
: String(cred.provider) === "qwen-portal"
|
|
? await (async () => {
|
|
const newCredentials = await refreshQwenPortalCredentials(cred);
|
|
return { apiKey: newCredentials.access, newCredentials };
|
|
})()
|
|
: await (async () => {
|
|
const oauthProvider = resolveOAuthProvider(cred.provider);
|
|
if (!oauthProvider) {
|
|
return null;
|
|
}
|
|
return await getOAuthApiKey(oauthProvider, oauthCreds);
|
|
})();
|
|
if (!result) {
|
|
return null;
|
|
}
|
|
store.profiles[params.profileId] = {
|
|
...cred,
|
|
...result.newCredentials,
|
|
type: "oauth",
|
|
};
|
|
saveAuthProfileStore(store, params.agentDir);
|
|
|
|
return result;
|
|
});
|
|
}
|
|
|
|
async function tryResolveOAuthProfile(params: {
|
|
cfg?: OpenClawConfig;
|
|
store: AuthProfileStore;
|
|
profileId: string;
|
|
agentDir?: string;
|
|
}): Promise<{ apiKey: string; provider: string; email?: string } | null> {
|
|
const { cfg, store, profileId } = params;
|
|
const cred = store.profiles[profileId];
|
|
if (!cred || cred.type !== "oauth") {
|
|
return null;
|
|
}
|
|
const profileConfig = cfg?.auth?.profiles?.[profileId];
|
|
if (profileConfig && profileConfig.provider !== cred.provider) {
|
|
return null;
|
|
}
|
|
if (profileConfig && profileConfig.mode !== cred.type) {
|
|
return null;
|
|
}
|
|
|
|
if (Date.now() < cred.expires) {
|
|
return {
|
|
apiKey: buildOAuthApiKey(cred.provider, cred),
|
|
provider: cred.provider,
|
|
email: cred.email,
|
|
};
|
|
}
|
|
|
|
const refreshed = await refreshOAuthTokenWithLock({
|
|
profileId,
|
|
agentDir: params.agentDir,
|
|
});
|
|
if (!refreshed) {
|
|
return null;
|
|
}
|
|
return {
|
|
apiKey: refreshed.apiKey,
|
|
provider: cred.provider,
|
|
email: cred.email,
|
|
};
|
|
}
|
|
|
|
export async function resolveApiKeyForProfile(params: {
|
|
cfg?: OpenClawConfig;
|
|
store: AuthProfileStore;
|
|
profileId: string;
|
|
agentDir?: string;
|
|
}): Promise<{ apiKey: string; provider: string; email?: string } | null> {
|
|
const { cfg, store, profileId } = params;
|
|
const cred = store.profiles[profileId];
|
|
if (!cred) {
|
|
return null;
|
|
}
|
|
const profileConfig = cfg?.auth?.profiles?.[profileId];
|
|
if (profileConfig && profileConfig.provider !== cred.provider) {
|
|
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") {
|
|
const key = cred.key?.trim();
|
|
if (!key) {
|
|
return null;
|
|
}
|
|
return { apiKey: key, provider: cred.provider, email: cred.email };
|
|
}
|
|
if (cred.type === "token") {
|
|
const token = cred.token?.trim();
|
|
if (!token) {
|
|
return null;
|
|
}
|
|
if (
|
|
typeof cred.expires === "number" &&
|
|
Number.isFinite(cred.expires) &&
|
|
cred.expires > 0 &&
|
|
Date.now() >= cred.expires
|
|
) {
|
|
return null;
|
|
}
|
|
return { apiKey: token, provider: cred.provider, email: cred.email };
|
|
}
|
|
if (Date.now() < cred.expires) {
|
|
return {
|
|
apiKey: buildOAuthApiKey(cred.provider, cred),
|
|
provider: cred.provider,
|
|
email: cred.email,
|
|
};
|
|
}
|
|
|
|
try {
|
|
const result = await refreshOAuthTokenWithLock({
|
|
profileId,
|
|
agentDir: params.agentDir,
|
|
});
|
|
if (!result) {
|
|
return null;
|
|
}
|
|
return {
|
|
apiKey: result.apiKey,
|
|
provider: cred.provider,
|
|
email: cred.email,
|
|
};
|
|
} catch (error) {
|
|
const refreshedStore = ensureAuthProfileStore(params.agentDir);
|
|
const refreshed = refreshedStore.profiles[profileId];
|
|
if (refreshed?.type === "oauth" && Date.now() < refreshed.expires) {
|
|
return {
|
|
apiKey: buildOAuthApiKey(refreshed.provider, refreshed),
|
|
provider: refreshed.provider,
|
|
email: refreshed.email ?? cred.email,
|
|
};
|
|
}
|
|
const fallbackProfileId = suggestOAuthProfileIdForLegacyDefault({
|
|
cfg,
|
|
store: refreshedStore,
|
|
provider: cred.provider,
|
|
legacyProfileId: profileId,
|
|
});
|
|
if (fallbackProfileId && fallbackProfileId !== profileId) {
|
|
try {
|
|
const fallbackResolved = await tryResolveOAuthProfile({
|
|
cfg,
|
|
store: refreshedStore,
|
|
profileId: fallbackProfileId,
|
|
agentDir: params.agentDir,
|
|
});
|
|
if (fallbackResolved) {
|
|
return fallbackResolved;
|
|
}
|
|
} catch {
|
|
// keep original error
|
|
}
|
|
}
|
|
|
|
// Fallback: if this is a secondary agent, try using the main agent's credentials
|
|
if (params.agentDir) {
|
|
try {
|
|
const mainStore = ensureAuthProfileStore(undefined); // main agent (no agentDir)
|
|
const mainCred = mainStore.profiles[profileId];
|
|
if (mainCred?.type === "oauth" && Date.now() < mainCred.expires) {
|
|
// Main agent has fresh credentials - copy them to this agent and use them
|
|
refreshedStore.profiles[profileId] = { ...mainCred };
|
|
saveAuthProfileStore(refreshedStore, params.agentDir);
|
|
log.info("inherited fresh OAuth credentials from main agent", {
|
|
profileId,
|
|
agentDir: params.agentDir,
|
|
expires: new Date(mainCred.expires).toISOString(),
|
|
});
|
|
return {
|
|
apiKey: buildOAuthApiKey(mainCred.provider, mainCred),
|
|
provider: mainCred.provider,
|
|
email: mainCred.email,
|
|
};
|
|
}
|
|
} catch {
|
|
// keep original error if main agent fallback also fails
|
|
}
|
|
}
|
|
|
|
const message = error instanceof Error ? error.message : String(error);
|
|
const hint = formatAuthDoctorHint({
|
|
cfg,
|
|
store: refreshedStore,
|
|
provider: cred.provider,
|
|
profileId,
|
|
});
|
|
throw new Error(
|
|
`OAuth token refresh failed for ${cred.provider}: ${message}. ` +
|
|
"Please try again or re-authenticate." +
|
|
(hint ? `\n\n${hint}` : ""),
|
|
{ cause: error },
|
|
);
|
|
}
|
|
}
|