doctor: report ChatGPT token health via /api/auth/session error field
ChatGPT session tokens are JWEs whose `exp` is encrypted and unreadable client-side, so the old JWT-decode path could never yield an expiry, and the `/api/auth/session` `expires` is a misleading rolling window (it advances on every call even for a dead token). The honest signal is the `error` field: `RefreshAccessTokenError` means the session token is dead while `expires` and a stale `accessToken` are still echoed (verified live). - Add ChatGPTProvider.session_health() and a "ChatGPT token active" doctor check based on it; drop the dead JWE/exp decode path in doctor and auth. - Fix _fetch_access_token to fail fast on a set `error` instead of returning the stale accessToken (which produced a confusing downstream 401). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
4a7ee5773f
commit
ef603cf659
@@ -207,19 +207,51 @@ class ChatGPTProvider(BaseProvider):
|
||||
len(self._project_ids),
|
||||
)
|
||||
|
||||
def _get_session_payload(self) -> dict:
|
||||
"""GET /api/auth/session and return the parsed NextAuth JSON.
|
||||
|
||||
Health signal (verified live 2026-06-28): a healthy session returns
|
||||
``accessToken`` with ``error`` absent/null. A *dead* session-token
|
||||
returns HTTP 200 with ``error == "RefreshAccessTokenError"`` and a
|
||||
STALE ``accessToken`` — so ``error``, not token presence, is the
|
||||
reliable validity signal. ``expires`` is a rolling window (it advances
|
||||
on every call even for a dead token) and must NOT be used to judge
|
||||
validity.
|
||||
"""
|
||||
resp = self._session.get(AUTH_SESSION_URL, timeout=REQUEST_TIMEOUT)
|
||||
resp.raise_for_status()
|
||||
return resp.json()
|
||||
|
||||
def session_health(self) -> tuple[bool, str]:
|
||||
"""Return ``(ok, detail)`` describing session-token validity.
|
||||
|
||||
Used by ``doctor`` for a cheap, honest token-health check. Returns
|
||||
``ok=False`` with a refresh hint when NextAuth reports an auth
|
||||
``error`` or omits the access token.
|
||||
"""
|
||||
try:
|
||||
data = self._get_session_payload()
|
||||
except Exception as e: # network error / non-JSON / HTTP 4xx-5xx
|
||||
return False, f"/api/auth/session unreachable: {str(e)[:60]}"
|
||||
error = data.get("error")
|
||||
if error:
|
||||
return False, (
|
||||
f"Session expired (error={error}) — refresh "
|
||||
"__Secure-next-auth.session-token via DevTools / 'auth'"
|
||||
)
|
||||
if not data.get("accessToken"):
|
||||
return False, "No accessToken — refresh token via DevTools / 'auth'"
|
||||
return True, "Session active"
|
||||
|
||||
def _fetch_access_token(self) -> str:
|
||||
"""Exchange the session cookie for a Bearer access token.
|
||||
|
||||
Calls GET /api/auth/session — the cookie jar already contains the
|
||||
session token, so no manual Cookie header is needed.
|
||||
|
||||
Returns {"accessToken": "...", "user": {...}}.
|
||||
"""
|
||||
logger.debug("[chatgpt] Fetching access token from %s", AUTH_SESSION_URL)
|
||||
try:
|
||||
resp = self._session.get(AUTH_SESSION_URL, timeout=REQUEST_TIMEOUT)
|
||||
resp.raise_for_status()
|
||||
data = resp.json()
|
||||
data = self._get_session_payload()
|
||||
except Exception as e:
|
||||
raise ProviderError(
|
||||
self.provider_name,
|
||||
@@ -230,6 +262,21 @@ class ChatGPTProvider(BaseProvider):
|
||||
),
|
||||
) from e
|
||||
|
||||
# A dead session-token returns HTTP 200 with error=RefreshAccessTokenError
|
||||
# and a stale accessToken. Fail fast here rather than letting the stale
|
||||
# token produce a confusing downstream 401.
|
||||
error = data.get("error")
|
||||
if error:
|
||||
raise ProviderError(
|
||||
self.provider_name,
|
||||
"fetch_access_token",
|
||||
RuntimeError(
|
||||
f"Session token expired (/api/auth/session error={error!r}). "
|
||||
"Refresh __Secure-next-auth.session-token via DevTools, then run "
|
||||
"'ai-chat-exporter auth' or update CHATGPT_SESSION_TOKEN in .env."
|
||||
),
|
||||
)
|
||||
|
||||
access_token = data.get("accessToken")
|
||||
if not access_token:
|
||||
raise ProviderError(
|
||||
|
||||
Reference in New Issue
Block a user