fix(auth): harden delayed oauth token registration

This commit is contained in:
Tam Nhu Tran committed 2026-04-04 00:37:04 -04:00
1 parent e2e5d99465
commit 01313a5a8e
4 files changed
+457 -36

No files matched your search

+8 -1
View File
@@ -432,9 +432,16 @@ export function useCliproxyAuthFlow() {
return;
}
const success = data.success === true;
const waitingForLocalToken = data.status === 'wait';
const hasAccount = typeof data.account === 'object' && data.account !== null;
if (response.ok && success && hasAccount) {
if (response.ok && waitingForLocalToken) {
setState((prev) => ({
...prev,
isSubmittingCallback: false,
error: null,
}));
} else if (response.ok && success && hasAccount) {
stopPolling();
queryClient.invalidateQueries({ queryKey: ['cliproxy-auth'] });
queryClient.invalidateQueries({ queryKey: ['cliproxy-accounts'] });
@@ -151,6 +151,76 @@ describe('useCliproxyAuthFlow', () => {
expect(toast.error).toHaveBeenCalledWith('poll failed');
});
it('keeps polling through wait responses until a later ok response includes the account', async () => {
let pollCount = 0;
vi.stubGlobal(
'fetch',
vi.fn((input: RequestInfo | URL) => {
const url = String(input);
if (url.includes('/start-url')) {
return Promise.resolve(
createJsonResponse({
success: true,
authUrl: 'https://auth.example/wait-ok',
state: 'state-wait-ok',
})
);
}
if (url.includes('/status?state=state-wait-ok')) {
pollCount += 1;
if (pollCount === 1) {
return Promise.resolve(createJsonResponse({ status: 'wait' }));
}
return Promise.resolve(
createJsonResponse({
status: 'ok',
account: {
id: 'delayed@example.com',
email: 'delayed@example.com',
nickname: 'delayed',
provider: 'codex',
isDefault: true,
},
})
);
}
return Promise.reject(new Error(`Unexpected fetch: ${url}`));
})
);
const { result } = renderHook(() => useCliproxyAuthFlow(), { wrapper });
await act(async () => {
await result.current.startAuth('codex', { startEndpoint: 'start-url' });
});
await act(async () => {
vi.advanceTimersByTime(3000);
await Promise.resolve();
await Promise.resolve();
});
expect(result.current.isAuthenticating).toBe(true);
expect(result.current.oauthState).toBe('state-wait-ok');
expect(result.current.error).toBeNull();
expect(toast.success).not.toHaveBeenCalled();
await act(async () => {
vi.advanceTimersByTime(3000);
await Promise.resolve();
await Promise.resolve();
});
expect(result.current.isAuthenticating).toBe(false);
expect(result.current.oauthState).toBeNull();
expect(toast.success).toHaveBeenCalledWith('codex authentication successful');
});
it('treats callback responses without an account as failures', async () => {
vi.stubGlobal(
'fetch',
@@ -197,6 +267,71 @@ describe('useCliproxyAuthFlow', () => {
expect(toast.success).not.toHaveBeenCalled();
});
it('keeps auth active when callback submission returns wait and only errors on the terminal poll', async () => {
vi.stubGlobal(
'fetch',
vi.fn((input: RequestInfo | URL) => {
const url = String(input);
if (url.includes('/start-url')) {
return Promise.resolve(
createJsonResponse({
success: true,
authUrl: 'https://auth.example/callback-wait',
state: 'state-callback-wait',
})
);
}
if (url.includes('/submit-callback')) {
return Promise.resolve(createJsonResponse({ status: 'wait' }));
}
if (url.includes('/status?state=state-callback-wait')) {
return Promise.resolve(
createJsonResponse({
status: 'error',
error:
'Authentication completed upstream, but no new local token was saved for this account. Update CCS/CLIProxy and retry.',
})
);
}
return Promise.reject(new Error(`Unexpected fetch: ${url}`));
})
);
const { result } = renderHook(() => useCliproxyAuthFlow(), { wrapper });
await act(async () => {
await result.current.startAuth('codex', { startEndpoint: 'start-url' });
});
await act(async () => {
await result.current.submitCallback(
'http://localhost/callback?code=abc123&state=state-callback-wait'
);
});
expect(result.current.isSubmittingCallback).toBe(false);
expect(result.current.isAuthenticating).toBe(true);
expect(result.current.error).toBeNull();
await act(async () => {
vi.advanceTimersByTime(3000);
await Promise.resolve();
await Promise.resolve();
});
expect(result.current.isAuthenticating).toBe(false);
expect(result.current.error).toBe(
'Authentication completed upstream, but no new local token was saved for this account. Update CCS/CLIProxy and retry.'
);
expect(toast.error).toHaveBeenCalledWith(
'Authentication completed upstream, but no new local token was saved for this account. Update CCS/CLIProxy and retry.'
);
});
it('treats status ok responses without an account as failures', async () => {
vi.stubGlobal(
'fetch',