mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-28 06:39:35 +00:00
fix: merge concurrent oauth refresh writes
This commit is contained in:
@@ -20,6 +20,7 @@ import {
|
||||
clearRuntimeAuthProfileStoreSnapshots,
|
||||
ensureAuthProfileStore,
|
||||
ensureAuthProfileStoreWithoutExternalProfiles,
|
||||
loadAuthProfileStoreWithoutExternalProfiles,
|
||||
saveAuthProfileStore,
|
||||
} from "./store.js";
|
||||
import type { AuthProfileStore, OAuthCredential } from "./types.js";
|
||||
@@ -660,4 +661,98 @@ describe("createOAuthManager", () => {
|
||||
expect(surfacedCauseMessage).not.toContain("external-attempt-id-token");
|
||||
}
|
||||
});
|
||||
|
||||
it("merges concurrent refresh writes for different profiles in the same store", async () => {
|
||||
const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "oauth-manager-refresh-merge-"));
|
||||
tempDirs.push(tempRoot);
|
||||
process.env.OPENCLAW_STATE_DIR = tempRoot;
|
||||
const agentDir = path.join(tempRoot, "agents", "main", "agent");
|
||||
process.env.OPENCLAW_AGENT_DIR = agentDir;
|
||||
process.env.PI_CODING_AGENT_DIR = agentDir;
|
||||
await fs.mkdir(agentDir, { recursive: true });
|
||||
|
||||
const firstProfileId = "openai-codex:first";
|
||||
const secondProfileId = "openai-codex:second";
|
||||
const firstCredential = createCredential({
|
||||
access: "first-old-access",
|
||||
refresh: "first-old-refresh",
|
||||
expires: Date.now() - 60_000,
|
||||
});
|
||||
const secondCredential = createCredential({
|
||||
access: "second-old-access",
|
||||
refresh: "second-old-refresh",
|
||||
expires: Date.now() - 60_000,
|
||||
});
|
||||
saveAuthProfileStore(
|
||||
{
|
||||
version: 1,
|
||||
profiles: {
|
||||
[firstProfileId]: firstCredential,
|
||||
[secondProfileId]: secondCredential,
|
||||
},
|
||||
},
|
||||
agentDir,
|
||||
{ filterExternalAuthProfiles: false },
|
||||
);
|
||||
|
||||
let resolveBothStarted!: () => void;
|
||||
const bothStarted = new Promise<void>((resolve) => {
|
||||
resolveBothStarted = resolve;
|
||||
});
|
||||
let releaseRefreshes!: () => void;
|
||||
const refreshGate = new Promise<void>((resolve) => {
|
||||
releaseRefreshes = resolve;
|
||||
});
|
||||
const refreshInputs: string[] = [];
|
||||
const manager = createOAuthManager({
|
||||
buildApiKey: async (_provider, credential) => credential.access,
|
||||
refreshCredential: vi.fn(async (credential) => {
|
||||
refreshInputs.push(credential.access);
|
||||
if (refreshInputs.length === 2) {
|
||||
resolveBothStarted();
|
||||
}
|
||||
await refreshGate;
|
||||
return {
|
||||
access: `${credential.access}-rotated`,
|
||||
refresh: `${credential.refresh}-rotated`,
|
||||
expires: Date.now() + 60_000,
|
||||
};
|
||||
}),
|
||||
readBootstrapCredential: () => null,
|
||||
isRefreshTokenReusedError: () => false,
|
||||
});
|
||||
|
||||
const firstRefresh = manager.resolveOAuthAccess({
|
||||
store: ensureAuthProfileStoreWithoutExternalProfiles(agentDir, {
|
||||
allowKeychainPrompt: false,
|
||||
}),
|
||||
profileId: firstProfileId,
|
||||
credential: firstCredential,
|
||||
agentDir,
|
||||
forceRefresh: true,
|
||||
});
|
||||
const secondRefresh = manager.resolveOAuthAccess({
|
||||
store: ensureAuthProfileStoreWithoutExternalProfiles(agentDir, {
|
||||
allowKeychainPrompt: false,
|
||||
}),
|
||||
profileId: secondProfileId,
|
||||
credential: secondCredential,
|
||||
agentDir,
|
||||
forceRefresh: true,
|
||||
});
|
||||
|
||||
await bothStarted;
|
||||
releaseRefreshes();
|
||||
await Promise.all([firstRefresh, secondRefresh]);
|
||||
|
||||
const saved = loadAuthProfileStoreWithoutExternalProfiles(agentDir);
|
||||
expect(saved.profiles[firstProfileId]).toMatchObject({
|
||||
access: "first-old-access-rotated",
|
||||
refresh: "first-old-refresh-rotated",
|
||||
});
|
||||
expect(saved.profiles[secondProfileId]).toMatchObject({
|
||||
access: "second-old-access-rotated",
|
||||
refresh: "second-old-refresh-rotated",
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -30,7 +30,6 @@ import {
|
||||
import {
|
||||
ensureAuthProfileStoreWithoutExternalProfiles,
|
||||
loadAuthProfileStoreWithoutExternalProfiles,
|
||||
saveAuthProfileStore,
|
||||
resolvePersistedAuthProfileOwnerAgentDir,
|
||||
updateAuthProfileStoreWithLock,
|
||||
} from "./store.js";
|
||||
@@ -405,6 +404,23 @@ export function createOAuthManager(adapter: OAuthManagerAdapter) {
|
||||
}
|
||||
}
|
||||
|
||||
async function saveOAuthCredentialIntoStore(params: {
|
||||
agentDir?: string;
|
||||
profileId: string;
|
||||
credential: OAuthCredential;
|
||||
}): Promise<void> {
|
||||
const updated = await updateAuthProfileStoreWithLock({
|
||||
agentDir: params.agentDir,
|
||||
updater: (store) => {
|
||||
store.profiles[params.profileId] = { ...params.credential };
|
||||
return true;
|
||||
},
|
||||
});
|
||||
if (!updated) {
|
||||
throw new Error("Failed to save refreshed OAuth credential.");
|
||||
}
|
||||
}
|
||||
|
||||
async function doRefreshOAuthTokenWithLock(params: {
|
||||
profileId: string;
|
||||
provider: string;
|
||||
@@ -508,8 +524,11 @@ export function createOAuthManager(adapter: OAuthManagerAdapter) {
|
||||
shouldReplaceStoredOAuthCredential(cred, externallyManaged) &&
|
||||
!areOAuthCredentialsEquivalent(cred, externallyManaged)
|
||||
) {
|
||||
store.profiles[params.profileId] = { ...externallyManaged };
|
||||
saveAuthProfileStore(store, ownerAgentDir);
|
||||
await saveOAuthCredentialIntoStore({
|
||||
agentDir: ownerAgentDir,
|
||||
profileId: params.profileId,
|
||||
credential: externallyManaged,
|
||||
});
|
||||
}
|
||||
credentialToRefresh = externallyManaged;
|
||||
if (!params.forceRefresh && hasUsableOAuthCredential(externallyManaged)) {
|
||||
@@ -545,8 +564,11 @@ export function createOAuthManager(adapter: OAuthManagerAdapter) {
|
||||
if (!refreshedCredentials) {
|
||||
return null;
|
||||
}
|
||||
store.profiles[params.profileId] = refreshedCredentials;
|
||||
saveAuthProfileStore(store, ownerAgentDir);
|
||||
await saveOAuthCredentialIntoStore({
|
||||
agentDir: ownerAgentDir,
|
||||
profileId: params.profileId,
|
||||
credential: refreshedCredentials,
|
||||
});
|
||||
if (ownerAgentDir) {
|
||||
const mainStoreKey = resolveAuthProfileStoreKey(undefined);
|
||||
if (mainStoreKey !== ownerStoreKey) {
|
||||
|
||||
Reference in New Issue
Block a user