[codex] inherit site proxy settings for oauth (#296)

* test: define oauth proxy inheritance behavior

* feat: apply site proxy settings to oauth requests

* feat: backfill oauth provider sites at startup

* test: remove oauth site registry temp dir
This commit is contained in:
Cita
2026-03-28 16:38:39 +08:00
committed by GitHub
parent 40b8eddef8
commit 5ae452ab45
11 changed files with 326 additions and 85 deletions
+85
View File
@@ -6,9 +6,21 @@ import { tmpdir } from 'node:os';
import { join } from 'node:path';
const fetchMock = vi.fn();
const undiciAgentCtorMock = vi.fn();
const undiciProxyAgentCtorMock = vi.fn();
vi.mock('undici', () => ({
fetch: (...args: unknown[]) => fetchMock(...args),
Agent: class MockUndiciAgent {
constructor(...args: unknown[]) {
undiciAgentCtorMock(...args);
}
},
ProxyAgent: class MockUndiciProxyAgent {
constructor(...args: unknown[]) {
undiciProxyAgentCtorMock(...args);
}
},
}));
type DbModule = typeof import('../../db/index.js');
@@ -51,13 +63,18 @@ describe('oauth routes', { timeout: 15_000 }, () => {
beforeEach(async () => {
fetchMock.mockReset();
undiciAgentCtorMock.mockReset();
undiciProxyAgentCtorMock.mockReset();
await db.delete(schema.routeChannels).run();
await db.delete(schema.tokenRoutes).run();
await db.delete(schema.tokenModelAvailability).run();
await db.delete(schema.modelAvailability).run();
await db.delete(schema.accountTokens).run();
await db.delete(schema.accounts).run();
await db.delete(schema.settings).run();
await db.delete(schema.sites).run();
const { invalidateSiteProxyCache } = await import('../../services/siteProxy.js');
invalidateSiteProxyCache();
});
afterAll(async () => {
@@ -434,6 +451,74 @@ describe('oauth routes', { timeout: 15_000 }, () => {
expect(modelNames.sort()).toEqual(['gpt-5', 'gpt-5.2-codex', 'gpt-5.4']);
});
it('includes dispatcher for codex token exchange when oauth site enables the system proxy', async () => {
const jwt = buildJwt({
email: 'codex-proxy@example.com',
'https://api.openai.com/auth': {
chatgpt_account_id: 'chatgpt-account-proxy',
chatgpt_plan_type: 'plus',
},
});
await db.insert(schema.settings).values({
key: 'system_proxy_url',
value: JSON.stringify('http://127.0.0.1:7890'),
}).run();
await db.insert(schema.sites).values({
name: 'ChatGPT Codex OAuth',
url: 'https://chatgpt.com/backend-api/codex',
platform: 'codex',
status: 'active',
useSystemProxy: true,
}).run();
fetchMock
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
access_token: 'oauth-access-token',
refresh_token: 'oauth-refresh-token',
id_token: jwt,
expires_in: 3600,
token_type: 'Bearer',
}),
text: async () => JSON.stringify({ ok: true }),
})
.mockResolvedValueOnce({
ok: true,
status: 200,
json: async () => ({
models: [{ id: 'gpt-5.4' }],
}),
text: async () => JSON.stringify({ ok: true }),
});
const startResponse = await app.inject({
method: 'POST',
url: '/api/oauth/providers/codex/start',
headers: {
host: 'metapi.example',
'x-forwarded-proto': 'https',
},
});
expect(startResponse.statusCode).toBe(200);
const startBody = startResponse.json() as { state: string };
const callbackResponse = await app.inject({
method: 'POST',
url: `/api/oauth/sessions/${encodeURIComponent(startBody.state)}/manual-callback`,
payload: {
callbackUrl: `http://localhost:1455/auth/callback?state=${encodeURIComponent(startBody.state)}&code=oauth-code-proxy`,
},
});
expect(callbackResponse.statusCode).toBe(200);
const codexTokenCall = fetchMock.mock.calls.find((call) => String(call[0] || '') === 'https://auth.openai.com/oauth/token');
const codexTokenFetchInit = codexTokenCall?.[1] as Record<string, unknown> | undefined;
expect(codexTokenFetchInit).toEqual(expect.objectContaining({
dispatcher: expect.anything(),
}));
});
it('marks oauth session as error and avoids creating a connection when manual codex callback model discovery fails', async () => {
const jwt = buildJwt({
email: 'codex-fail@example.com',