fix(codex): durable OAuth refresh lifecycle

Add shared OAuth credential lifecycle manager with provider-aware refresh
decisions. Implement CodexExecutor.refreshCredentials so 401/403 retry
refresh works for Codex, track lastRefreshAt and refresh before the
upstream stale-token window, preserve omitted idToken, and add
per-connection single-flight refresh to avoid refresh-token rotation races.

Merged from PR #1664.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Kevin Le
2026-06-06 11:04:36 +07:00
committed by decolua
co-authored by Cursor
parent 38b73bfc6b
commit c233c7c8fc
15 changed files with 484 additions and 140 deletions
+40 -4
View File
@@ -1,5 +1,36 @@
import { getProviderConnections } from "@/lib/localDb.js";
import { getExecutor, refreshTokenByProvider } from "open-sse/index.js";
import { getProviderConnections, updateProviderConnection } from "@/lib/localDb.js";
import { getExecutor } from "open-sse/index.js";
async function persistRefreshedCredentials(connection, newCredentials) {
const updateData = {};
if (newCredentials.accessToken) updateData.accessToken = newCredentials.accessToken;
if (newCredentials.refreshToken) updateData.refreshToken = newCredentials.refreshToken;
if (newCredentials.idToken) updateData.idToken = newCredentials.idToken;
if (newCredentials.lastRefreshAt) updateData.lastRefreshAt = newCredentials.lastRefreshAt;
if (newCredentials.expiresIn) {
updateData.expiresIn = newCredentials.expiresIn;
updateData.expiresAt = new Date(Date.now() + newCredentials.expiresIn * 1000).toISOString();
} else if (newCredentials.expiresAt) {
updateData.expiresAt = newCredentials.expiresAt;
}
const providerSpecificUpdates = {
...(newCredentials.providerSpecificData || {}),
...(newCredentials.copilotToken ? { copilotToken: newCredentials.copilotToken } : {}),
...(newCredentials.copilotTokenExpiresAt ? { copilotTokenExpiresAt: newCredentials.copilotTokenExpiresAt } : {}),
};
if (Object.keys(providerSpecificUpdates).length > 0) {
updateData.providerSpecificData = {
...(connection.providerSpecificData || {}),
...providerSpecificUpdates,
};
}
if (Object.keys(updateData).length > 0) {
await updateProviderConnection(connection.id, updateData);
}
}
export async function POST(request) {
try {
@@ -19,7 +50,11 @@ export async function POST(request) {
apiKey: connection.apiKey,
accessToken: connection.accessToken,
refreshToken: connection.refreshToken,
copilotToken: connection.copilotToken,
idToken: connection.idToken,
lastRefreshAt: connection.lastRefreshAt,
connectionId: connection.id,
copilotToken: connection.providerSpecificData?.copilotToken,
copilotTokenExpiresAt: connection.providerSpecificData?.copilotTokenExpiresAt,
projectId: connection.projectId,
providerSpecificData: connection.providerSpecificData
};
@@ -31,9 +66,10 @@ export async function POST(request) {
// Auto-refresh token on 401/403 and retry (same as chatCore.js)
if (response.status === 401 || response.status === 403) {
const newCredentials = await refreshTokenByProvider(provider, credentials);
const newCredentials = await executor.refreshCredentials(credentials, console);
if (newCredentials?.accessToken || newCredentials?.copilotToken) {
Object.assign(credentials, newCredentials);
await persistRefreshedCredentials(connection, newCredentials);
({ response } = await executor.execute({ model, body, stream, credentials }));
}
}