fix: claude auth expiry warning — correct field name and smarter threshold
- Fix 'undefined' in auth banner: read access_token_hours_remaining (not hours_remaining) - Fix false-positive warning on fresh tokens: when refresh token present, only warn within 1 hour of expiry (not 24h) since the CLI should auto-rotate but sometimes misses - Emit claude_auth_expired SSE event on 401 so UI shows inline red banner immediately - app.js: handle claude_auth_expired SSE event with persistent top banner + dismiss button Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -4,6 +4,7 @@ import os
|
|||||||
import signal
|
import signal
|
||||||
import subprocess
|
import subprocess
|
||||||
from config import settings
|
from config import settings
|
||||||
|
import event_bus
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -48,7 +49,10 @@ async def complete(
|
|||||||
response = await _dispatch(primary, system_prompt, messages, model)
|
response = await _dispatch(primary, system_prompt, messages, model)
|
||||||
return response, primary
|
return response, primary
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
err_str = str(e)
|
||||||
logger.warning("%s failed (%s) — falling back to %s", primary, e, fallback)
|
logger.warning("%s failed (%s) — falling back to %s", primary, e, fallback)
|
||||||
|
if primary == "claude" and any(k in err_str for k in ("401", "authenticate", "expired", "OAuth")):
|
||||||
|
await event_bus.publish({"type": "claude_auth_expired"})
|
||||||
response = await _dispatch(fallback, system_prompt, messages, None)
|
response = await _dispatch(fallback, system_prompt, messages, None)
|
||||||
return response, fallback
|
return response, fallback
|
||||||
|
|
||||||
|
|||||||
@@ -20,7 +20,8 @@ router = APIRouter(prefix="/auth")
|
|||||||
CLAUDE_CREDS = Path.home() / ".claude" / ".credentials.json"
|
CLAUDE_CREDS = Path.home() / ".claude" / ".credentials.json"
|
||||||
GEMINI_CREDS = Path.home() / ".gemini" / "oauth_creds.json"
|
GEMINI_CREDS = Path.home() / ".gemini" / "oauth_creds.json"
|
||||||
GEMINI_ACCTS = Path.home() / ".gemini" / "google_accounts.json"
|
GEMINI_ACCTS = Path.home() / ".gemini" / "google_accounts.json"
|
||||||
WARN_HOURS = 24
|
WARN_HOURS = 24 # no refresh token — warn a day ahead
|
||||||
|
WARN_HOURS_REFRESH = 1 # refresh token present — only warn if CLI hasn't rotated in time
|
||||||
|
|
||||||
|
|
||||||
def _claude_status() -> dict:
|
def _claude_status() -> dict:
|
||||||
@@ -31,11 +32,13 @@ def _claude_status() -> dict:
|
|||||||
expires_dt = datetime.fromtimestamp(oauth["expiresAt"] / 1000, tz=timezone.utc)
|
expires_dt = datetime.fromtimestamp(oauth["expiresAt"] / 1000, tz=timezone.utc)
|
||||||
now = datetime.now(tz=timezone.utc)
|
now = datetime.now(tz=timezone.utc)
|
||||||
hours_remaining = (expires_dt - now).total_seconds() / 3600
|
hours_remaining = (expires_dt - now).total_seconds() / 3600
|
||||||
# If a refresh token is present the session is long-lived (~1 year).
|
# When a refresh token is present the CLI *should* auto-rotate the access
|
||||||
# expiresAt only reflects the current access token window (~8 h) and
|
# token, but sometimes it doesn't. Use a tight 1-hour window so a fresh
|
||||||
# rotates automatically — do not warn based on it when a refresh token exists.
|
# 8-hour token doesn't immediately trigger a warning, but a stale token
|
||||||
warning = not has_refresh and hours_remaining < WARN_HOURS
|
# that the CLI missed will still surface before it expires.
|
||||||
expired = hours_remaining <= 0 and not has_refresh
|
expired = hours_remaining <= 0
|
||||||
|
threshold = WARN_HOURS_REFRESH if has_refresh else WARN_HOURS
|
||||||
|
warning = expired or hours_remaining < threshold
|
||||||
return {
|
return {
|
||||||
"ok": True,
|
"ok": True,
|
||||||
"has_refresh_token": has_refresh,
|
"has_refresh_token": has_refresh,
|
||||||
|
|||||||
@@ -975,6 +975,20 @@
|
|||||||
let data;
|
let data;
|
||||||
try { data = JSON.parse(e.data); } catch { return; }
|
try { data = JSON.parse(e.data); } catch { return; }
|
||||||
if (data.type === 'keepalive') return;
|
if (data.type === 'keepalive') return;
|
||||||
|
|
||||||
|
if (data.type === 'claude_auth_expired') {
|
||||||
|
let banner = document.getElementById('claude-auth-banner');
|
||||||
|
if (!banner) {
|
||||||
|
banner = document.createElement('div');
|
||||||
|
banner.id = 'claude-auth-banner';
|
||||||
|
banner.style.cssText = 'position:fixed;top:0;left:0;right:0;z-index:9999;background:#7c2d12;color:#fef2f2;padding:0.6rem 1rem;font-size:0.85rem;display:flex;align-items:center;justify-content:space-between;gap:1rem;';
|
||||||
|
banner.innerHTML = '<span>⚠️ Claude authentication expired — run <code style="background:#991b1b;padding:0.1rem 0.3rem;border-radius:3px;">claude</code> in your terminal to re-authenticate, then reload.</span>'
|
||||||
|
+ '<button onclick="this.parentElement.remove()" style="background:none;border:none;color:#fef2f2;font-size:1.1rem;cursor:pointer;padding:0 0.3rem;">✕</button>';
|
||||||
|
document.body.prepend(banner);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (data.type !== 'nct_message' && data.type !== 'nct_response') return;
|
if (data.type !== 'nct_message' && data.type !== 'nct_response') return;
|
||||||
|
|
||||||
if (sessionId === data.session_id) {
|
if (sessionId === data.session_id) {
|
||||||
@@ -1188,7 +1202,7 @@
|
|||||||
warnings.push('✕ Claude CLI token has expired');
|
warnings.push('✕ Claude CLI token has expired');
|
||||||
anyExpired = true;
|
anyExpired = true;
|
||||||
} else {
|
} else {
|
||||||
warnings.push(`⚠ Claude CLI token expires in ${d.claude.hours_remaining}h`);
|
warnings.push(`⚠ Claude CLI token expires in ${d.claude.access_token_hours_remaining}h`);
|
||||||
}
|
}
|
||||||
fixes.push('<code>claude</code>');
|
fixes.push('<code>claude</code>');
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user