fix: keep oauth refresh on persisted auth stores

This commit is contained in:
Shakker
2026-05-02 04:26:02 +01:00
parent 15db5ff7ce
commit 34b17c82da
2 changed files with 101 additions and 8 deletions

View File

@@ -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);

View File

@@ -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" &&