//! Minimal JWT helpers used for local token inspection. //! //! cdxs only decodes the payload to extract account metadata and expiry. It //! does not verify signatures because validation is performed by OpenAI when //! the token is used against the network APIs. use anyhow::{anyhow, Context, Result}; use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; use serde::Deserialize; #[derive(Debug, Clone, Deserialize)] pub struct JwtPayload { #[serde(default)] pub email: Option, #[serde(default)] pub sub: Option, #[serde(default)] pub exp: Option, #[serde(rename = "https://api.openai.com/auth", default)] pub auth: Option, } #[derive(Debug, Clone, Deserialize)] pub struct OpenAiAuth { #[serde(default)] pub chatgpt_plan_type: Option, #[serde(default)] pub account_id: Option, #[serde(default)] pub organization_id: Option, } pub fn decode_payload(token: &str) -> Result { // JWT shape is header.payload.signature; only the base64url payload is // needed for cdxs metadata. let payload = token .split('.') .nth(1) .ok_or_else(|| anyhow!("JWT 格式无效"))?; let bytes = URL_SAFE_NO_PAD .decode(payload) .context("JWT payload base64 解码失败")?; serde_json::from_slice(&bytes).context("JWT payload JSON 解析失败") } pub fn token_expired(token: &str, skew_seconds: i64) -> bool { // Treat malformed tokens as expired so callers attempt refresh or reauth. let Ok(payload) = decode_payload(token) else { return true; }; match payload.exp { Some(exp) => chrono::Utc::now().timestamp() + skew_seconds >= exp, None => false, } }