From 20c2005df965e34f68af36734008ee043857db28 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Wed, 29 Apr 2026 18:13:15 -0400 Subject: [PATCH] fix(cliproxy): clear stale cleanup timer and fix session/tunnel edge cases - Stop cleanup interval when all auth sessions are removed (unregister, cancel, cancelAll) so the Node event loop isn't held alive. - getActiveSessionForProvider now returns the most recent session by startedAt instead of the first map entry. - HTTPS tunnel includes port in Host header when non-443. --- src/cliproxy/auth/auth-session-manager.ts | 26 +++++++++++++++-------- src/cliproxy/proxy/https-tunnel-proxy.ts | 8 +++++-- 2 files changed, 23 insertions(+), 11 deletions(-) diff --git a/src/cliproxy/auth/auth-session-manager.ts b/src/cliproxy/auth/auth-session-manager.ts index 0ad0dd7c..56a3a5f5 100644 --- a/src/cliproxy/auth/auth-session-manager.ts +++ b/src/cliproxy/auth/auth-session-manager.ts @@ -39,14 +39,18 @@ function startCleanupInterval(): void { authSessionEvents.emit('session:expired', sessionId); } } - // Stop interval if no active sessions - if (activeSessions.size === 0 && cleanupInterval) { - clearInterval(cleanupInterval); - cleanupInterval = null; - } + stopCleanupIfEmpty(); }, 60000); // Check every minute } +/** Clear the cleanup interval when no sessions remain. */ +function stopCleanupIfEmpty(): void { + if (activeSessions.size === 0 && cleanupInterval) { + clearInterval(cleanupInterval); + cleanupInterval = null; + } +} + /** * Register an active OAuth session */ @@ -82,6 +86,7 @@ export function attachProcessToSession(sessionId: string, process: ChildProcess) export function unregisterAuthSession(sessionId: string): void { activeSessions.delete(sessionId); authSessionEvents.emit('session:ended', sessionId); + stopCleanupIfEmpty(); } /** @@ -101,6 +106,7 @@ export function cancelAuthSession(sessionId: string): boolean { activeSessions.delete(sessionId); authSessionEvents.emit('session:cancelled', sessionId); + stopCleanupIfEmpty(); return true; } @@ -112,15 +118,16 @@ export function getActiveSession(sessionId: string): ActiveAuthSession | null { } /** - * Get active session for a provider (most recent) + * Get active session for a provider (most recent by startedAt) */ export function getActiveSessionForProvider(provider: string): ActiveAuthSession | null { + let latest: ActiveAuthSession | null = null; for (const session of activeSessions.values()) { - if (session.provider === provider) { - return session; + if (session.provider === provider && (!latest || session.startedAt > latest.startedAt)) { + latest = session; } } - return null; + return latest; } /** @@ -145,5 +152,6 @@ export function cancelAllSessionsForProvider(provider: string): number { count++; } } + stopCleanupIfEmpty(); return count; } diff --git a/src/cliproxy/proxy/https-tunnel-proxy.ts b/src/cliproxy/proxy/https-tunnel-proxy.ts index 81e17867..64bd003c 100644 --- a/src/cliproxy/proxy/https-tunnel-proxy.ts +++ b/src/cliproxy/proxy/https-tunnel-proxy.ts @@ -162,8 +162,12 @@ export class HttpsTunnelProxy { headers[key] = value; } - // Set correct host header for remote - headers['Host'] = this.config.remoteHost; + // Set correct host header for remote (include port when non-default) + if (this.config.remotePort === 443) { + headers['Host'] = this.config.remoteHost; + } else { + headers['Host'] = `${this.config.remoteHost}:${this.config.remotePort}`; + } // Inject Authorization header if not present but authToken is configured // This is a fallback - normally the client (CodexReasoningProxy) forwards the header