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.
This commit is contained in:
Tam Nhu Tran
2026-04-29 18:13:15 -04:00
parent 6633bf456b
commit 20c2005df9
2 changed files with 23 additions and 11 deletions
+17 -9
View File
@@ -39,14 +39,18 @@ function startCleanupInterval(): void {
authSessionEvents.emit('session:expired', sessionId); authSessionEvents.emit('session:expired', sessionId);
} }
} }
// Stop interval if no active sessions stopCleanupIfEmpty();
if (activeSessions.size === 0 && cleanupInterval) {
clearInterval(cleanupInterval);
cleanupInterval = null;
}
}, 60000); // Check every minute }, 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 * Register an active OAuth session
*/ */
@@ -82,6 +86,7 @@ export function attachProcessToSession(sessionId: string, process: ChildProcess)
export function unregisterAuthSession(sessionId: string): void { export function unregisterAuthSession(sessionId: string): void {
activeSessions.delete(sessionId); activeSessions.delete(sessionId);
authSessionEvents.emit('session:ended', sessionId); authSessionEvents.emit('session:ended', sessionId);
stopCleanupIfEmpty();
} }
/** /**
@@ -101,6 +106,7 @@ export function cancelAuthSession(sessionId: string): boolean {
activeSessions.delete(sessionId); activeSessions.delete(sessionId);
authSessionEvents.emit('session:cancelled', sessionId); authSessionEvents.emit('session:cancelled', sessionId);
stopCleanupIfEmpty();
return true; 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 { export function getActiveSessionForProvider(provider: string): ActiveAuthSession | null {
let latest: ActiveAuthSession | null = null;
for (const session of activeSessions.values()) { for (const session of activeSessions.values()) {
if (session.provider === provider) { if (session.provider === provider && (!latest || session.startedAt > latest.startedAt)) {
return session; latest = session;
} }
} }
return null; return latest;
} }
/** /**
@@ -145,5 +152,6 @@ export function cancelAllSessionsForProvider(provider: string): number {
count++; count++;
} }
} }
stopCleanupIfEmpty();
return count; return count;
} }
+6 -2
View File
@@ -162,8 +162,12 @@ export class HttpsTunnelProxy {
headers[key] = value; headers[key] = value;
} }
// Set correct host header for remote // Set correct host header for remote (include port when non-default)
headers['Host'] = this.config.remoteHost; 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 // Inject Authorization header if not present but authToken is configured
// This is a fallback - normally the client (CodexReasoningProxy) forwards the header // This is a fallback - normally the client (CodexReasoningProxy) forwards the header