mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:50:43 +00:00
fix: keep oauth refresh on persisted auth stores
This commit is contained in:
@@ -14,6 +14,7 @@ import {
|
||||
import {
|
||||
clearRuntimeAuthProfileStoreSnapshots,
|
||||
ensureAuthProfileStore,
|
||||
ensureAuthProfileStoreWithoutExternalProfiles,
|
||||
saveAuthProfileStore,
|
||||
} from "./store.js";
|
||||
import type { AuthProfileStore, OAuthCredential } from "./types.js";
|
||||
@@ -145,6 +146,94 @@ describe("OAuthManagerRefreshError", () => {
|
||||
});
|
||||
|
||||
describe("createOAuthManager", () => {
|
||||
it("does not overlay external auth while checking main-store adoption", async () => {
|
||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "oauth-manager-main-adopt-"));
|
||||
tempDirs.push(tempRoot);
|
||||
process.env.OPENCLAW_STATE_DIR = tempRoot;
|
||||
const mainAgentDir = path.join(tempRoot, "agents", "main", "agent");
|
||||
const agentDir = path.join(tempRoot, "agents", "sub", "agent");
|
||||
process.env.OPENCLAW_AGENT_DIR = mainAgentDir;
|
||||
process.env.PI_CODING_AGENT_DIR = mainAgentDir;
|
||||
await fs.mkdir(agentDir, { recursive: true });
|
||||
await fs.mkdir(mainAgentDir, { recursive: true });
|
||||
|
||||
const profileId = "openai-codex:default";
|
||||
const subCredential = createCredential({
|
||||
access: "expired-sub-access",
|
||||
refresh: "sub-refresh",
|
||||
expires: Date.now() - 60_000,
|
||||
});
|
||||
const mainCredential = createCredential({
|
||||
access: "expired-main-access",
|
||||
refresh: "main-refresh",
|
||||
expires: Date.now() - 30_000,
|
||||
});
|
||||
saveAuthProfileStore(
|
||||
{
|
||||
version: 1,
|
||||
profiles: {
|
||||
[profileId]: subCredential,
|
||||
},
|
||||
},
|
||||
agentDir,
|
||||
{ filterExternalAuthProfiles: false },
|
||||
);
|
||||
saveAuthProfileStore(
|
||||
{
|
||||
version: 1,
|
||||
profiles: {
|
||||
[profileId]: mainCredential,
|
||||
},
|
||||
},
|
||||
mainAgentDir,
|
||||
{ filterExternalAuthProfiles: false },
|
||||
);
|
||||
externalAuthTesting.setResolveExternalAuthProfilesForTest(() => [
|
||||
{
|
||||
profileId,
|
||||
credential: createCredential({
|
||||
access: "external-fresh-access",
|
||||
refresh: "external-fresh-refresh",
|
||||
expires: Date.now() + 60_000,
|
||||
}),
|
||||
persistence: "runtime-only",
|
||||
},
|
||||
]);
|
||||
|
||||
const refreshCredential = vi.fn(async (credential: OAuthCredential) => {
|
||||
expect(credential.access).toBe("expired-main-access");
|
||||
return {
|
||||
access: "rotated-main-access",
|
||||
refresh: "rotated-main-refresh",
|
||||
expires: Date.now() + 60_000,
|
||||
};
|
||||
});
|
||||
const manager = createOAuthManager({
|
||||
buildApiKey: async (_provider, credential) => credential.access,
|
||||
refreshCredential,
|
||||
readBootstrapCredential: () => null,
|
||||
isRefreshTokenReusedError: () => false,
|
||||
});
|
||||
|
||||
const result = await manager.resolveOAuthAccess({
|
||||
store: ensureAuthProfileStoreWithoutExternalProfiles(agentDir, {
|
||||
allowKeychainPrompt: false,
|
||||
}),
|
||||
profileId,
|
||||
credential: subCredential,
|
||||
agentDir,
|
||||
});
|
||||
|
||||
expect(refreshCredential).toHaveBeenCalledTimes(1);
|
||||
expect(result).toEqual({
|
||||
apiKey: "rotated-main-access",
|
||||
credential: expect.objectContaining({
|
||||
access: "rotated-main-access",
|
||||
refresh: "rotated-main-refresh",
|
||||
}),
|
||||
});
|
||||
});
|
||||
|
||||
it("refreshes with the adopted external oauth credential", async () => {
|
||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "oauth-manager-refresh-"));
|
||||
tempDirs.push(tempRoot);
|
||||
|
||||
@@ -25,8 +25,8 @@ import {
|
||||
} from "./oauth-shared.js";
|
||||
import { ensureAuthStoreFile, resolveAuthStorePath, resolveOAuthRefreshLockPath } from "./paths.js";
|
||||
import {
|
||||
ensureAuthProfileStore,
|
||||
loadAuthProfileStoreForSecretsRuntime,
|
||||
ensureAuthProfileStoreWithoutExternalProfiles,
|
||||
loadAuthProfileStoreWithoutExternalProfiles,
|
||||
saveAuthProfileStore,
|
||||
resolvePersistedAuthProfileOwnerAgentDir,
|
||||
updateAuthProfileStoreWithLock,
|
||||
@@ -143,7 +143,7 @@ async function loadFreshStoredOAuthCredential(params: {
|
||||
previous?: Pick<OAuthCredential, "access" | "refresh" | "expires">;
|
||||
requireChange?: boolean;
|
||||
}): Promise<OAuthCredential | null> {
|
||||
const reloadedStore = loadAuthProfileStoreForSecretsRuntime(params.agentDir);
|
||||
const reloadedStore = loadAuthProfileStoreWithoutExternalProfiles(params.agentDir);
|
||||
const reloaded = reloadedStore.profiles[params.profileId];
|
||||
if (
|
||||
reloaded?.type !== "oauth" ||
|
||||
@@ -217,7 +217,9 @@ export function createOAuthManager(adapter: OAuthManagerAdapter) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const mainStore = ensureAuthProfileStore(undefined);
|
||||
const mainStore = ensureAuthProfileStoreWithoutExternalProfiles(undefined, {
|
||||
allowKeychainPrompt: false,
|
||||
});
|
||||
const mainCred = mainStore.profiles[params.profileId];
|
||||
if (
|
||||
mainCred?.type === "oauth" &&
|
||||
@@ -325,7 +327,7 @@ export function createOAuthManager(adapter: OAuthManagerAdapter) {
|
||||
try {
|
||||
return await withFileLock(globalRefreshLockPath, OAUTH_REFRESH_LOCK_OPTIONS, async () =>
|
||||
withFileLock(authPath, AUTH_STORE_LOCK_OPTIONS, async () => {
|
||||
const store = loadAuthProfileStoreForSecretsRuntime(ownerAgentDir);
|
||||
const store = loadAuthProfileStoreWithoutExternalProfiles(ownerAgentDir);
|
||||
const cred = store.profiles[params.profileId];
|
||||
if (!cred || cred.type !== "oauth") {
|
||||
return null;
|
||||
@@ -341,7 +343,7 @@ export function createOAuthManager(adapter: OAuthManagerAdapter) {
|
||||
|
||||
if (params.agentDir) {
|
||||
try {
|
||||
const mainStore = loadAuthProfileStoreForSecretsRuntime(undefined);
|
||||
const mainStore = loadAuthProfileStoreWithoutExternalProfiles(undefined);
|
||||
const mainCred = mainStore.profiles[params.profileId];
|
||||
if (
|
||||
mainCred?.type === "oauth" &&
|
||||
@@ -517,7 +519,7 @@ export function createOAuthManager(adapter: OAuthManagerAdapter) {
|
||||
});
|
||||
return refreshed;
|
||||
} catch (error) {
|
||||
const refreshedStore = loadAuthProfileStoreForSecretsRuntime(params.agentDir);
|
||||
const refreshedStore = loadAuthProfileStoreWithoutExternalProfiles(params.agentDir);
|
||||
const refreshed = refreshedStore.profiles[params.profileId];
|
||||
if (refreshed?.type === "oauth" && hasUsableOAuthCredential(refreshed)) {
|
||||
return {
|
||||
@@ -560,7 +562,9 @@ export function createOAuthManager(adapter: OAuthManagerAdapter) {
|
||||
}
|
||||
if (params.agentDir) {
|
||||
try {
|
||||
const mainStore = ensureAuthProfileStore(undefined);
|
||||
const mainStore = ensureAuthProfileStoreWithoutExternalProfiles(undefined, {
|
||||
allowKeychainPrompt: false,
|
||||
});
|
||||
const mainCred = mainStore.profiles[params.profileId];
|
||||
if (
|
||||
mainCred?.type === "oauth" &&
|
||||
|
||||
Reference in New Issue
Block a user