fix(oauth): extend loopback listener lifetime 60s -> 300s (#3145)

Co-authored-by: Cyrus Gray <cyrus@tinyhumans.ai>
This commit is contained in:
sanil-23
2026-06-01 19:53:59 +05:30
committed by GitHub
co-authored by Cyrus Gray
parent a40cd7e64d
commit e9c69a42fb
4 changed files with 24 additions and 7 deletions
@@ -25,8 +25,11 @@ interface OAuthProviderButtonProps {
// Reset the loading state if the OAuth round-trip never completes — covers
// the case where the user cancels in the system browser, or the backend
// redirect fails so the `openhuman://` deep link never fires.
const OAUTH_LOADING_TIMEOUT_MS = 90_000;
// redirect fails so the `openhuman://` deep link never fires. Kept >= the
// loopback listener lifetime (`DEFAULT_TIMEOUT_SECS`, 300s) so the button
// never re-enables while the loopback server is still legitimately waiting
// for a slow (2FA / consent) sign-in to redirect back.
const OAUTH_LOADING_TIMEOUT_MS = 300_000;
// Pre-flight budget for `/health` before we open the system browser. Kept
// short so a healthy backend adds barely any perceptible click→browser delay,
@@ -166,7 +166,7 @@ describe('OAuthProviderButton', () => {
expect(screen.getByRole('button', { name: 'Google' })).toBeEnabled();
});
it('resets isLoading after the 90s safety timeout', async () => {
it('resets isLoading after the 300s safety timeout', async () => {
render(<OAuthProviderButton provider={stubProvider} />);
fireEvent.click(screen.getByRole('button', { name: 'Google' }));
@@ -178,9 +178,16 @@ describe('OAuthProviderButton', () => {
expect(screen.getByText('Connecting...')).toBeInTheDocument();
// Loading must persist past the old 90s mark — the loopback listener now
// legitimately waits up to 300s for a slow (2FA / consent) sign-in.
await act(async () => {
vi.advanceTimersByTime(90_000);
});
expect(screen.getByText('Connecting...')).toBeInTheDocument();
await act(async () => {
vi.advanceTimersByTime(210_000);
});
expect(screen.queryByText('Connecting...')).not.toBeInTheDocument();
expect(screen.getByRole('button', { name: 'Google' })).toBeEnabled();
@@ -367,7 +374,7 @@ describe('OAuthProviderButton', () => {
expect(screen.queryByRole('alert')).not.toBeInTheDocument();
});
it('shows the unavailable banner if the 90s timeout fires while the backend is down', async () => {
it('shows the unavailable banner if the 300s timeout fires while the backend is down', async () => {
vi.mocked(checkBackendHealthy)
.mockResolvedValueOnce(healthyResult)
.mockResolvedValueOnce({ healthy: false, reason: 'timeout', latencyMs: 6000 });
@@ -380,7 +387,7 @@ describe('OAuthProviderButton', () => {
expect(openUrl).toHaveBeenCalledTimes(1);
await act(async () => {
vi.advanceTimersByTime(90_000);
vi.advanceTimersByTime(300_000);
// After the safety timer fires we kick off probeBackendOnReturn().
// Drain enough microtasks for that async probe to resolve and its
// .then() to flush the alert into the DOM.
@@ -28,7 +28,7 @@ describe('startLoopbackOauthListener', () => {
expect(handle).toBeNull();
expect(mockInvoke).toHaveBeenCalledWith('start_loopback_oauth_listener', {
port: 53824,
timeoutSecs: 60,
timeoutSecs: 300,
});
});
+8 -1
View File
@@ -20,7 +20,14 @@ import { isTauri } from './tauriCommands/common';
*/
const DEFAULT_PORT = 53824;
const DEFAULT_TIMEOUT_SECS = 60;
// The listener lifetime starts at the button click — before the system browser
// even opens — and the Rust shell hard-kills the loopback server when it
// elapses (`loopback_oauth.rs` `timeout(lifetime, run)`). A real GitHub/Google
// sign-in (account chooser, password manager, 2FA/OTP, first-time consent)
// routinely exceeds 60s; if the server dies first, the post-auth redirect lands
// on a dead port and the browser shows "127.0.0.1:<port> not reached" even
// though OAuth succeeded. 5 minutes comfortably covers an interactive sign-in.
const DEFAULT_TIMEOUT_SECS = 300;
const CALLBACK_EVENT = 'loopback-oauth-callback';
export interface LoopbackHandle {