From 26b9239c8a835b0cfe99292e576101fa59b9a35e Mon Sep 17 00:00:00 2001 From: ciregenz Date: Sat, 23 May 2026 07:05:25 -0700 Subject: [PATCH] [eric] cycles: move OAuth flow state into a leaf module --- backend/apps/nine_router/oauth.py | 57 ++++++++++++++----------------- backend/apps/oauth_state.py | 18 ++++++++++ backend/main.py | 24 ++++--------- 3 files changed, 49 insertions(+), 50 deletions(-) create mode 100644 backend/apps/oauth_state.py diff --git a/backend/apps/nine_router/oauth.py b/backend/apps/nine_router/oauth.py index 93bcce8b..55b7ba85 100644 --- a/backend/apps/nine_router/oauth.py +++ b/backend/apps/nine_router/oauth.py @@ -11,6 +11,7 @@ import os import httpx from .process import NINE_ROUTER_API, NINE_ROUTER_PORT, NINE_ROUTER_V1 +from backend.apps.oauth_state import _pending_oauth, _mark_oauth_completed logger = logging.getLogger(__name__) @@ -108,38 +109,30 @@ async def _start_codex_callback_listener(timeout: float = 300.0) -> asyncio.base code = (q.get("code") or [""])[0] state = (q.get("state") or [""])[0] if code and state: - try: - from backend.main import _pending_oauth, _mark_oauth_completed - except Exception: - _pending_oauth = None - _mark_oauth_completed = None - - if _pending_oauth is not None: - pending = _pending_oauth.pop(state, None) - if pending: - try: - await exchange_oauth( - pending["provider"], - code, - pending["redirect_uri"], - pending["code_verifier"], - state, - ) - if _mark_oauth_completed is not None: - _mark_oauth_completed(state) - logger.info( - f"Codex callback: server-side exchange succeeded for state {state[:8]}..." - ) - except Exception as e: - # Put the pending entry back so the - # frontend's msgHandler retry via - # /agents/subscriptions/exchange still - # has a shot. Safe because we only popped - # it a moment ago. - _pending_oauth[state] = pending - logger.debug( - f"Codex callback: server-side exchange failed ({e}); leaving for frontend retry" - ) + pending = _pending_oauth.pop(state, None) + if pending: + try: + await exchange_oauth( + pending["provider"], + code, + pending["redirect_uri"], + pending["code_verifier"], + state, + ) + _mark_oauth_completed(state) + logger.info( + f"Codex callback: server-side exchange succeeded for state {state[:8]}..." + ) + except Exception as e: + # Put the pending entry back so the + # frontend's msgHandler retry via + # /agents/subscriptions/exchange still + # has a shot. Safe because we only popped + # it a moment ago. + _pending_oauth[state] = pending + logger.debug( + f"Codex callback: server-side exchange failed ({e}); leaving for frontend retry" + ) except Exception as e: logger.debug(f"Codex callback listener pre-exchange error: {e}") diff --git a/backend/apps/oauth_state.py b/backend/apps/oauth_state.py new file mode 100644 index 00000000..26e3dca2 --- /dev/null +++ b/backend/apps/oauth_state.py @@ -0,0 +1,18 @@ +# In-memory store for pending OAuth flows (state -> {provider, code_verifier, redirect_uri}) +_pending_oauth: dict[str, dict] = {} +# Recently-completed OAuth states so the /api/subscriptions/callback handler +# can distinguish a legitimate duplicate callback (browser prefetch, refresh, +# or Google redirect retry after a slow first response) from a truly stale +# request. Bounded FIFO, drops the oldest entries once it grows past +# _MAX_COMPLETED_OAUTH so it can't leak memory. +_completed_oauth: list[str] = [] +_MAX_COMPLETED_OAUTH = 64 + + +def _mark_oauth_completed(state: str) -> None: + if state in _completed_oauth: + return + _completed_oauth.append(state) + # Trim head if we've outgrown the bound + while len(_completed_oauth) > _MAX_COMPLETED_OAUTH: + _completed_oauth.pop(0) diff --git a/backend/main.py b/backend/main.py index b73270b8..6b286c76 100644 --- a/backend/main.py +++ b/backend/main.py @@ -9,24 +9,12 @@ logger = logging.getLogger(__name__) from fastapi.responses import JSONResponse, HTMLResponse from fastapi import Request -# In-memory store for pending OAuth flows (state -> {provider, code_verifier, redirect_uri}) -_pending_oauth: dict[str, dict] = {} -# Recently-completed OAuth states so the /api/subscriptions/callback handler -# can distinguish a legitimate duplicate callback (browser prefetch, refresh, -# or Google redirect retry after a slow first response) from a truly stale -# request. Bounded FIFO, drops the oldest entries once it grows past -# _MAX_COMPLETED_OAUTH so it can't leak memory. -_completed_oauth: list[str] = [] -_MAX_COMPLETED_OAUTH = 64 - - -def _mark_oauth_completed(state: str) -> None: - if state in _completed_oauth: - return - _completed_oauth.append(state) - # Trim head if we've outgrown the bound - while len(_completed_oauth) > _MAX_COMPLETED_OAUTH: - _completed_oauth.pop(0) +from backend.apps.oauth_state import ( + _pending_oauth, + _completed_oauth, + _MAX_COMPLETED_OAUTH, + _mark_oauth_completed, +) from backend.config.Apps import MainApp from backend.apps.health.health import health from backend.apps.agents.agents import agents