fix: harden codex runtime routing and oauth recovery
This commit is contained in:
@@ -91,11 +91,15 @@ function parseTokenResponsePayload(payload: unknown): CodexTokenExchangeResult {
|
||||
throw new Error('codex token exchange response missing required fields');
|
||||
}
|
||||
const claims = parseJwtClaims(idToken);
|
||||
const accountId = asTrimmedString(claims?.['https://api.openai.com/auth']?.chatgpt_account_id);
|
||||
if (!accountId) {
|
||||
throw new Error('codex token exchange response missing chatgpt_account_id');
|
||||
}
|
||||
return {
|
||||
accessToken,
|
||||
refreshToken,
|
||||
idToken,
|
||||
accountId: asTrimmedString(claims?.['https://api.openai.com/auth']?.chatgpt_account_id),
|
||||
accountId,
|
||||
email: asTrimmedString(claims?.email),
|
||||
planType: asTrimmedString(claims?.['https://api.openai.com/auth']?.chatgpt_plan_type),
|
||||
tokenExpiresAt: Date.now() + expiresIn * 1000,
|
||||
@@ -220,7 +224,7 @@ export const codexOauthProvider: OAuthProviderDefinition = {
|
||||
};
|
||||
},
|
||||
buildProxyHeaders: ({ oauth, downstreamHeaders }) => {
|
||||
const accountId = getHeaderValue(downstreamHeaders, 'chatgpt-account-id') || oauth.accountId || oauth.accountKey;
|
||||
const accountId = oauth.accountId || oauth.accountKey;
|
||||
const originator = getHeaderValue(downstreamHeaders, 'originator') || 'codex_cli_rs';
|
||||
const headers: Record<string, string> = {
|
||||
Originator: originator,
|
||||
|
||||
@@ -178,7 +178,7 @@ async function findExistingOauthAccount(input: {
|
||||
if (byKey) return byKey;
|
||||
}
|
||||
|
||||
if (!accountKey && email) {
|
||||
if (!accountKey && email && input.provider !== 'codex') {
|
||||
const byEmail = await db.select().from(schema.accounts).where(and(
|
||||
eq(schema.accounts.oauthProvider, input.provider),
|
||||
eq(schema.accounts.username, email),
|
||||
@@ -253,6 +253,7 @@ async function upsertOauthAccount(input: {
|
||||
account: await db.select().from(schema.accounts).where(eq(schema.accounts.id, existing.id)).get(),
|
||||
site,
|
||||
created: false,
|
||||
previousAccount: existing,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -270,7 +271,7 @@ async function upsertOauthAccount(input: {
|
||||
isPinned: false,
|
||||
sortOrder: await getNextAccountSortOrder(),
|
||||
}).returning().get();
|
||||
return { account: created, site, created: true };
|
||||
return { account: created, site, created: true, previousAccount: null };
|
||||
}
|
||||
|
||||
export function listOauthProviders() {
|
||||
@@ -365,7 +366,7 @@ export async function handleOauthCallback(input: {
|
||||
codeVerifier: session.codeVerifier,
|
||||
projectId: session.projectId,
|
||||
});
|
||||
const { account, site, created } = await upsertOauthAccount({
|
||||
const { account, site, created, previousAccount } = await upsertOauthAccount({
|
||||
definition,
|
||||
exchange,
|
||||
rebindAccountId: session.rebindAccountId,
|
||||
@@ -379,6 +380,20 @@ export async function handleOauthCallback(input: {
|
||||
if (refreshResult.status !== 'success') {
|
||||
if (created) {
|
||||
await db.delete(schema.accounts).where(eq(schema.accounts.id, account.id)).run();
|
||||
} else if (previousAccount) {
|
||||
await db.update(schema.accounts).set({
|
||||
siteId: previousAccount.siteId,
|
||||
username: previousAccount.username,
|
||||
accessToken: previousAccount.accessToken,
|
||||
apiToken: previousAccount.apiToken,
|
||||
checkinEnabled: previousAccount.checkinEnabled,
|
||||
status: previousAccount.status,
|
||||
oauthProvider: previousAccount.oauthProvider,
|
||||
oauthAccountKey: previousAccount.oauthAccountKey,
|
||||
oauthProjectId: previousAccount.oauthProjectId,
|
||||
extraConfig: previousAccount.extraConfig,
|
||||
updatedAt: previousAccount.updatedAt,
|
||||
}).where(eq(schema.accounts.id, previousAccount.id)).run();
|
||||
}
|
||||
await rebuildTokenRoutesFromAvailability();
|
||||
const errorMessage = refreshResult.errorMessage || `${input.provider} model discovery failed`;
|
||||
|
||||
@@ -164,4 +164,49 @@ describe('TokenRouter site status guard', () => {
|
||||
expect(selected).not.toBeNull();
|
||||
expect(selected?.tokenValue).toBe('oauth-access-token');
|
||||
});
|
||||
|
||||
it('prefers codex oauth access token over a stale legacy api token on oauth-managed rows', async () => {
|
||||
const site = await db.insert(schema.sites).values({
|
||||
name: 'codex-site',
|
||||
url: 'https://chatgpt.com/backend-api/codex',
|
||||
platform: 'codex',
|
||||
status: 'active',
|
||||
}).returning().get();
|
||||
|
||||
const account = await db.insert(schema.accounts).values({
|
||||
siteId: site.id,
|
||||
username: 'codex-user@example.com',
|
||||
accessToken: 'oauth-access-token',
|
||||
apiToken: 'stale-direct-token',
|
||||
status: 'active',
|
||||
extraConfig: JSON.stringify({
|
||||
credentialMode: 'session',
|
||||
oauth: {
|
||||
provider: 'codex',
|
||||
accountId: 'chatgpt-account-123',
|
||||
email: 'codex-user@example.com',
|
||||
planType: 'plus',
|
||||
},
|
||||
}),
|
||||
}).returning().get();
|
||||
|
||||
const route = await db.insert(schema.tokenRoutes).values({
|
||||
modelPattern: 'gpt-5.2-codex',
|
||||
enabled: true,
|
||||
}).returning().get();
|
||||
|
||||
await db.insert(schema.routeChannels).values({
|
||||
routeId: route.id,
|
||||
accountId: account.id,
|
||||
tokenId: null,
|
||||
priority: 0,
|
||||
weight: 10,
|
||||
enabled: true,
|
||||
manualOverride: false,
|
||||
}).run();
|
||||
|
||||
const selected = await new TokenRouter().selectChannel('gpt-5.2-codex');
|
||||
expect(selected).not.toBeNull();
|
||||
expect(selected?.tokenValue).toBe('oauth-access-token');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2037,14 +2037,15 @@ export class TokenRouter {
|
||||
return token ? token : null;
|
||||
}
|
||||
|
||||
const fallback = candidate.account.apiToken?.trim();
|
||||
if (fallback) return fallback;
|
||||
|
||||
if (getOauthInfoFromExtraConfig(candidate.account.extraConfig)) {
|
||||
const accessToken = candidate.account.accessToken?.trim();
|
||||
return accessToken || null;
|
||||
if (accessToken) return accessToken;
|
||||
return null;
|
||||
}
|
||||
|
||||
const fallback = candidate.account.apiToken?.trim();
|
||||
if (fallback) return fallback;
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user