mirror of https://github.com/openclaw/openclaw.git
fix(auth): persist codex oauth refresh tokens
This commit is contained in:
parent
af5f4f6716
commit
00218ac8a4
|
|
@ -60,6 +60,7 @@ Docs: https://docs.openclaw.ai
|
|||
- Auth profiles/store: coerce misplaced SecretRef objects out of plaintext `key` and `token` fields during store load so agents without ACP runtime stop crashing on `.trim()` after upgrade. (#58923) Thanks @openperf.
|
||||
- ACPX/runtime: repair `queue owner unavailable` session recovery by replacing dead named sessions and resuming the backend session when ACPX exposes a stable session id, so the first ACP prompt no longer inherits a dead handle. (#58669) Thanks @neeravmakwana
|
||||
- ACPX/runtime: retry dead-session queue-owner repair without `--resume-session` when the reported ACPX session id is stale, so recovery still creates a fresh named session instead of failing session init. Thanks @obviyus.
|
||||
- Auth/OpenAI Codex: persist plugin-refreshed OAuth credentials to `auth-profiles.json` before returning them, so rotated Codex refresh tokens survive restart and stop falling into `refresh_token_reused` loops. (#53082)
|
||||
|
||||
## 2026.3.31
|
||||
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import {
|
|||
ensureAuthProfileStore,
|
||||
saveAuthProfileStore,
|
||||
} from "./store.js";
|
||||
import type { AuthProfileStore } from "./types.js";
|
||||
import type { AuthProfileStore, OAuthCredential } from "./types.js";
|
||||
let resolveApiKeyForProfile: typeof import("./oauth.js").resolveApiKeyForProfile;
|
||||
|
||||
const { getOAuthApiKeyMock } = vi.hoisted(() => ({
|
||||
|
|
@ -24,7 +24,7 @@ const {
|
|||
buildProviderAuthDoctorHintWithPluginMock,
|
||||
} = vi.hoisted(() => ({
|
||||
refreshProviderOAuthCredentialWithPluginMock: vi.fn(
|
||||
async (_params?: { context?: unknown }) => undefined,
|
||||
async (_params?: { context?: unknown }): Promise<OAuthCredential | undefined> => undefined,
|
||||
),
|
||||
formatProviderAuthProfileApiKeyWithPluginMock: vi.fn(() => undefined),
|
||||
buildProviderAuthDoctorHintWithPluginMock: vi.fn(async () => undefined),
|
||||
|
|
@ -61,6 +61,12 @@ async function loadFreshOAuthModuleForTest() {
|
|||
({ resolveApiKeyForProfile } = await import("./oauth.js"));
|
||||
}
|
||||
|
||||
async function readPersistedStore(agentDir: string): Promise<AuthProfileStore> {
|
||||
return JSON.parse(
|
||||
await fs.readFile(path.join(agentDir, "auth-profiles.json"), "utf8"),
|
||||
) as AuthProfileStore;
|
||||
}
|
||||
|
||||
function createExpiredOauthStore(params: {
|
||||
profileId: string;
|
||||
provider: string;
|
||||
|
|
@ -142,6 +148,47 @@ describe("resolveApiKeyForProfile openai-codex refresh fallback", () => {
|
|||
expect(refreshProviderOAuthCredentialWithPluginMock).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("persists plugin-refreshed openai-codex credentials before returning", async () => {
|
||||
const profileId = "openai-codex:default";
|
||||
saveAuthProfileStore(
|
||||
createExpiredOauthStore({
|
||||
profileId,
|
||||
provider: "openai-codex",
|
||||
access: "stale-access-token",
|
||||
}),
|
||||
agentDir,
|
||||
);
|
||||
refreshProviderOAuthCredentialWithPluginMock.mockResolvedValueOnce({
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "rotated-access-token",
|
||||
refresh: "rotated-refresh-token",
|
||||
expires: Date.now() + 86_400_000,
|
||||
accountId: "acct-rotated",
|
||||
});
|
||||
|
||||
const result = await resolveApiKeyForProfile({
|
||||
store: ensureAuthProfileStore(agentDir),
|
||||
profileId,
|
||||
agentDir,
|
||||
});
|
||||
|
||||
expect(result).toEqual({
|
||||
apiKey: "rotated-access-token",
|
||||
provider: "openai-codex",
|
||||
email: undefined,
|
||||
});
|
||||
|
||||
const persisted = await readPersistedStore(agentDir);
|
||||
expect(persisted.profiles[profileId]).toMatchObject({
|
||||
type: "oauth",
|
||||
provider: "openai-codex",
|
||||
access: "rotated-access-token",
|
||||
refresh: "rotated-refresh-token",
|
||||
accountId: "acct-rotated",
|
||||
});
|
||||
});
|
||||
|
||||
it("keeps throwing for non-codex providers on the same refresh error", async () => {
|
||||
const profileId = "anthropic:default";
|
||||
saveAuthProfileStore(
|
||||
|
|
|
|||
|
|
@ -185,9 +185,16 @@ async function refreshOAuthTokenWithLock(params: {
|
|||
context: cred,
|
||||
});
|
||||
if (pluginRefreshed) {
|
||||
const refreshedCredentials: OAuthCredential = {
|
||||
...cred,
|
||||
...pluginRefreshed,
|
||||
type: "oauth",
|
||||
};
|
||||
store.profiles[params.profileId] = refreshedCredentials;
|
||||
saveAuthProfileStore(store, params.agentDir);
|
||||
return {
|
||||
apiKey: await buildOAuthApiKey(cred.provider, pluginRefreshed),
|
||||
newCredentials: pluginRefreshed,
|
||||
apiKey: await buildOAuthApiKey(cred.provider, refreshedCredentials),
|
||||
newCredentials: refreshedCredentials,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue