diff --git a/src/cliproxy/remote-auth-fetcher.ts b/src/cliproxy/remote-auth-fetcher.ts index aaedf04e..8259582f 100644 --- a/src/cliproxy/remote-auth-fetcher.ts +++ b/src/cliproxy/remote-auth-fetcher.ts @@ -30,6 +30,7 @@ interface RemoteAuthFile { export interface RemoteAccountInfo { id: string; email: string; + provider: CLIProxyProvider; isDefault: boolean; status: 'active' | 'disabled' | 'unavailable'; } @@ -124,6 +125,8 @@ function transformRemoteAuthFiles(files: RemoteAuthFile[]): RemoteAuthStatus[] { const accounts: RemoteAccountInfo[] = providerFiles.map((f, idx) => ({ id: f.id, email: f.email || f.name || 'Unknown', + // Keep provider on each account so UI account rendering can infer capabilities safely. + provider, isDefault: idx === 0, status: f.status, })); diff --git a/src/web-server/routes/cliproxy-auth-routes.ts b/src/web-server/routes/cliproxy-auth-routes.ts index a940a117..b07da8e2 100644 --- a/src/web-server/routes/cliproxy-auth-routes.ts +++ b/src/web-server/routes/cliproxy-auth-routes.ts @@ -184,13 +184,7 @@ router.get('/accounts', async (_req: Request, res: Response): Promise => { const target = getProxyTarget(); if (target.isRemote) { const authStatus = await fetchRemoteAuthStatus(target); - // Transform RemoteAuthStatus[] to account summary format - const accounts = authStatus.flatMap((status) => - status.accounts.map((acc) => ({ - provider: status.provider, - ...acc, - })) - ); + const accounts = authStatus.flatMap((status) => status.accounts); res.json({ accounts, source: 'remote' }); return; } diff --git a/tests/unit/cliproxy/remote-auth-fetcher.test.ts b/tests/unit/cliproxy/remote-auth-fetcher.test.ts new file mode 100644 index 00000000..d7103d6c --- /dev/null +++ b/tests/unit/cliproxy/remote-auth-fetcher.test.ts @@ -0,0 +1,58 @@ +import { afterEach, beforeEach, describe, expect, it, mock } from 'bun:test'; +import { fetchRemoteAuthStatus } from '../../../src/cliproxy/remote-auth-fetcher'; +import type { ProxyTarget } from '../../../src/cliproxy/proxy-target-resolver'; + +describe('remote-auth-fetcher', () => { + let originalFetch: typeof fetch; + + const remoteTarget: ProxyTarget = { + host: 'remote.example.com', + port: 8317, + protocol: 'https', + authToken: 'token', + isRemote: true, + }; + + beforeEach(() => { + originalFetch = global.fetch; + }); + + afterEach(() => { + global.fetch = originalFetch; + }); + + it('includes provider on each remote account', async () => { + global.fetch = mock((url: string) => { + expect(url).toBe('https://remote.example.com:8317/v0/management/auth-files'); + return Promise.resolve( + new Response( + JSON.stringify({ + files: [ + { + id: 'acc-codex', + name: 'codex-main', + type: 'oauth', + provider: 'codex', + email: 'codex@example.com', + status: 'active', + source: 'file', + }, + ], + }), + { + status: 200, + headers: { 'Content-Type': 'application/json' }, + } + ) + ); + }) as typeof fetch; + + const result = await fetchRemoteAuthStatus(remoteTarget); + + expect(result).toHaveLength(1); + expect(result[0]?.provider).toBe('codex'); + expect(result[0]?.accounts).toHaveLength(1); + expect(result[0]?.accounts[0]?.provider).toBe('codex'); + expect(result[0]?.accounts[0]?.email).toBe('codex@example.com'); + }); +}); diff --git a/ui/src/components/cliproxy/provider-editor/index.tsx b/ui/src/components/cliproxy/provider-editor/index.tsx index 72a64f5d..5051538b 100644 --- a/ui/src/components/cliproxy/provider-editor/index.tsx +++ b/ui/src/components/cliproxy/provider-editor/index.tsx @@ -105,7 +105,16 @@ export function ProviderEditor({ missingRequiredFields, } = useProviderEditor(provider); - const accounts = authStatus.accounts || []; + // Defensive normalization: remote/legacy payloads may omit account.provider. + // Fallback to current editor provider to avoid runtime crashes in account UI. + const accounts = useMemo( + () => + (authStatus.accounts || []).map((account) => ({ + ...account, + provider: account.provider || baseProvider || provider, + })), + [authStatus.accounts, baseProvider, provider] + ); // Fetch effective API key for presets (uses configured value, not hardcoded) const { data: authTokens } = useQuery<{ apiKey: { value: string } }>({