forked from boos/BoosAPI
fix(proxy): honor account proxy during oauth discovery
This commit is contained in:
@@ -7,6 +7,18 @@ import { eq } from 'drizzle-orm';
|
|||||||
const getApiTokenMock = vi.fn();
|
const getApiTokenMock = vi.fn();
|
||||||
const getModelsMock = vi.fn();
|
const getModelsMock = vi.fn();
|
||||||
const undiciFetchMock = vi.fn();
|
const undiciFetchMock = vi.fn();
|
||||||
|
const proxyAgentCtorMock = vi.fn();
|
||||||
|
|
||||||
|
class MockProxyAgent {
|
||||||
|
readonly proxyUrl: string;
|
||||||
|
|
||||||
|
constructor(proxyUrl: string) {
|
||||||
|
this.proxyUrl = proxyUrl;
|
||||||
|
proxyAgentCtorMock(proxyUrl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class MockAgent {}
|
||||||
|
|
||||||
vi.mock('./platforms/index.js', () => ({
|
vi.mock('./platforms/index.js', () => ({
|
||||||
getAdapter: () => ({
|
getAdapter: () => ({
|
||||||
@@ -17,6 +29,8 @@ vi.mock('./platforms/index.js', () => ({
|
|||||||
|
|
||||||
vi.mock('undici', () => ({
|
vi.mock('undici', () => ({
|
||||||
fetch: (...args: unknown[]) => undiciFetchMock(...args),
|
fetch: (...args: unknown[]) => undiciFetchMock(...args),
|
||||||
|
ProxyAgent: MockProxyAgent,
|
||||||
|
Agent: MockAgent,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
type DbModule = typeof import('../db/index.js');
|
type DbModule = typeof import('../db/index.js');
|
||||||
@@ -47,6 +61,7 @@ describe('refreshModelsForAccount credential discovery', () => {
|
|||||||
getApiTokenMock.mockReset();
|
getApiTokenMock.mockReset();
|
||||||
getModelsMock.mockReset();
|
getModelsMock.mockReset();
|
||||||
undiciFetchMock.mockReset();
|
undiciFetchMock.mockReset();
|
||||||
|
proxyAgentCtorMock.mockReset();
|
||||||
|
|
||||||
await db.delete(schema.routeChannels).run();
|
await db.delete(schema.routeChannels).run();
|
||||||
await db.delete(schema.tokenRoutes).run();
|
await db.delete(schema.tokenRoutes).run();
|
||||||
@@ -595,6 +610,115 @@ describe('refreshModelsForAccount credential discovery', () => {
|
|||||||
expect(parsed.runtimeHealth?.state).toBe('unhealthy');
|
expect(parsed.runtimeHealth?.state).toBe('unhealthy');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('applies account proxy override to codex oauth cloud discovery requests', async () => {
|
||||||
|
getApiTokenMock.mockResolvedValue(null);
|
||||||
|
getModelsMock.mockRejectedValue(new Error('codex oauth discovery should not call adapter.getModels'));
|
||||||
|
undiciFetchMock.mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: async () => ({
|
||||||
|
models: [
|
||||||
|
{ id: 'gpt-5.4' },
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
text: async () => JSON.stringify({ ok: true }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const site = await db.insert(schema.sites).values({
|
||||||
|
name: 'codex-account-proxy-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-proxy-user@example.com',
|
||||||
|
accessToken: 'oauth-access-token',
|
||||||
|
apiToken: null,
|
||||||
|
status: 'active',
|
||||||
|
extraConfig: JSON.stringify({
|
||||||
|
credentialMode: 'session',
|
||||||
|
proxyUrl: 'http://127.0.0.1:7890',
|
||||||
|
oauth: {
|
||||||
|
provider: 'codex',
|
||||||
|
accountId: 'chatgpt-account-proxy',
|
||||||
|
email: 'codex-proxy-user@example.com',
|
||||||
|
planType: 'plus',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}).returning().get();
|
||||||
|
|
||||||
|
const result = await refreshModelsForAccount(account.id);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
accountId: account.id,
|
||||||
|
refreshed: true,
|
||||||
|
status: 'success',
|
||||||
|
modelCount: 1,
|
||||||
|
modelsPreview: ['gpt-5.4'],
|
||||||
|
});
|
||||||
|
expect(undiciFetchMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(proxyAgentCtorMock).toHaveBeenCalledWith('http://127.0.0.1:7890');
|
||||||
|
expect(undiciFetchMock.mock.calls[0]?.[1]).toMatchObject({
|
||||||
|
method: 'GET',
|
||||||
|
dispatcher: expect.any(MockProxyAgent),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('applies account proxy override to gemini oauth validation requests', async () => {
|
||||||
|
getApiTokenMock.mockResolvedValue(null);
|
||||||
|
getModelsMock.mockRejectedValue(new Error('gemini oauth validation should not call adapter.getModels'));
|
||||||
|
undiciFetchMock.mockResolvedValue({
|
||||||
|
ok: true,
|
||||||
|
status: 200,
|
||||||
|
json: async () => ({
|
||||||
|
state: 'ENABLED',
|
||||||
|
}),
|
||||||
|
text: async () => JSON.stringify({ state: 'ENABLED' }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const site = await db.insert(schema.sites).values({
|
||||||
|
name: 'gemini-account-proxy-site',
|
||||||
|
url: 'https://gemini.example.com',
|
||||||
|
platform: 'gemini',
|
||||||
|
status: 'active',
|
||||||
|
}).returning().get();
|
||||||
|
|
||||||
|
const account = await db.insert(schema.accounts).values({
|
||||||
|
siteId: site.id,
|
||||||
|
username: 'gemini-proxy-user@example.com',
|
||||||
|
accessToken: 'gemini-access-token',
|
||||||
|
apiToken: null,
|
||||||
|
status: 'active',
|
||||||
|
extraConfig: JSON.stringify({
|
||||||
|
credentialMode: 'session',
|
||||||
|
proxyUrl: 'http://127.0.0.1:1080',
|
||||||
|
oauth: {
|
||||||
|
provider: 'gemini-cli',
|
||||||
|
projectId: 'project-proxy-demo',
|
||||||
|
email: 'gemini-proxy-user@example.com',
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
}).returning().get();
|
||||||
|
|
||||||
|
const result = await refreshModelsForAccount(account.id);
|
||||||
|
|
||||||
|
expect(result).toMatchObject({
|
||||||
|
accountId: account.id,
|
||||||
|
refreshed: true,
|
||||||
|
status: 'success',
|
||||||
|
modelCount: expect.any(Number),
|
||||||
|
});
|
||||||
|
expect(undiciFetchMock).toHaveBeenCalledTimes(1);
|
||||||
|
expect(proxyAgentCtorMock).toHaveBeenCalledWith('http://127.0.0.1:1080');
|
||||||
|
expect(String(undiciFetchMock.mock.calls[0]?.[0] || '')).toContain('/projects/project-proxy-demo/services/');
|
||||||
|
expect(undiciFetchMock.mock.calls[0]?.[1]).toMatchObject({
|
||||||
|
method: 'GET',
|
||||||
|
dispatcher: expect.any(MockProxyAgent),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('discovers antigravity oauth models via fetchAvailableModels fallback using the oauth project id', async () => {
|
it('discovers antigravity oauth models via fetchAvailableModels fallback using the oauth project id', async () => {
|
||||||
getApiTokenMock.mockResolvedValue(null);
|
getApiTokenMock.mockResolvedValue(null);
|
||||||
getModelsMock.mockRejectedValue(new Error('antigravity oauth discovery should not call adapter.getModels'));
|
getModelsMock.mockRejectedValue(new Error('antigravity oauth discovery should not call adapter.getModels'));
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
import { invalidateTokenRouterCache } from './tokenRouter.js';
|
import { invalidateTokenRouterCache } from './tokenRouter.js';
|
||||||
import { setAccountRuntimeHealth } from './accountHealthService.js';
|
import { setAccountRuntimeHealth } from './accountHealthService.js';
|
||||||
import { clearAllRouteDecisionSnapshots } from './routeDecisionSnapshotStore.js';
|
import { clearAllRouteDecisionSnapshots } from './routeDecisionSnapshotStore.js';
|
||||||
import { withAccountProxyOverride, withSiteRecordProxyRequestInit } from './siteProxy.js';
|
import { withAccountProxyOverride, withExplicitProxyRequestInit, withSiteRecordProxyRequestInit } from './siteProxy.js';
|
||||||
import { getCodexOauthInfoFromExtraConfig, isCodexPlatform } from './oauth/codexAccount.js';
|
import { getCodexOauthInfoFromExtraConfig, isCodexPlatform } from './oauth/codexAccount.js';
|
||||||
import { buildOauthInfo, getOauthInfoFromExtraConfig } from './oauth/oauthAccount.js';
|
import { buildOauthInfo, getOauthInfoFromExtraConfig } from './oauth/oauthAccount.js';
|
||||||
import { CLAUDE_DEFAULT_ANTHROPIC_VERSION } from './oauth/claudeProvider.js';
|
import { CLAUDE_DEFAULT_ANTHROPIC_VERSION } from './oauth/claudeProvider.js';
|
||||||
@@ -323,7 +323,7 @@ async function validateGeminiCliOauthConnection(input: {
|
|||||||
}
|
}
|
||||||
const response = await fetch(
|
const response = await fetch(
|
||||||
`https://serviceusage.googleapis.com/v1/projects/${encodeURIComponent(projectId)}/services/${encodeURIComponent(GEMINI_CLI_REQUIRED_SERVICE)}`,
|
`https://serviceusage.googleapis.com/v1/projects/${encodeURIComponent(projectId)}/services/${encodeURIComponent(GEMINI_CLI_REQUIRED_SERVICE)}`,
|
||||||
{
|
withExplicitProxyRequestInit(getProxyUrlFromExtraConfig(input.account.extraConfig), {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
headers: {
|
headers: {
|
||||||
Authorization: `Bearer ${accessToken}`,
|
Authorization: `Bearer ${accessToken}`,
|
||||||
@@ -331,7 +331,7 @@ async function validateGeminiCliOauthConnection(input: {
|
|||||||
'User-Agent': GEMINI_CLI_USER_AGENT,
|
'User-Agent': GEMINI_CLI_USER_AGENT,
|
||||||
'X-Goog-Api-Client': GEMINI_CLI_GOOGLE_API_CLIENT,
|
'X-Goog-Api-Client': GEMINI_CLI_GOOGLE_API_CLIENT,
|
||||||
},
|
},
|
||||||
},
|
}, true),
|
||||||
);
|
);
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
const text = await response.text().catch(() => '');
|
const text = await response.text().catch(() => '');
|
||||||
|
|||||||
@@ -469,7 +469,7 @@ export function withSiteRecordProxyRequestInit(
|
|||||||
if (mergedHeaders) {
|
if (mergedHeaders) {
|
||||||
nextOptions.headers = mergedHeaders;
|
nextOptions.headers = mergedHeaders;
|
||||||
}
|
}
|
||||||
const accountNormalized = normalizeSiteProxyUrl(accountProxyUrl);
|
const accountNormalized = normalizeSiteProxyUrl(accountProxyUrl) ?? accountProxyOverride.getStore();
|
||||||
const siteProxyUrl = resolveProxyUrlForSite(site);
|
const siteProxyUrl = resolveProxyUrlForSite(site);
|
||||||
const proxyUrl = accountNormalized || siteProxyUrl;
|
const proxyUrl = accountNormalized || siteProxyUrl;
|
||||||
const isAccountOverride = !!accountNormalized && accountNormalized !== siteProxyUrl;
|
const isAccountOverride = !!accountNormalized && accountNormalized !== siteProxyUrl;
|
||||||
|
|||||||
Reference in New Issue
Block a user