use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::fmt;
fn redact(v: &Option<String>) -> Option<&'static str> {
v.as_ref().map(|_| "<redacted>")
}
#[derive(Debug, Clone, Deserialize)]
pub struct ClaudeUsageResponse {
#[serde(default)]
pub five_hour: Option<ClaudeUsageWindow>,
#[serde(default)]
pub seven_day: Option<ClaudeUsageWindow>,
#[serde(default, deserialize_with = "de_lenient_limits")]
pub limits: Vec<ClaudeLimit>,
#[serde(default)]
pub spend: Option<ClaudeSpend>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ClaudeUsageWindow {
#[serde(default, deserialize_with = "de_f64_or_zero")]
pub utilization: f64,
#[serde(default)]
pub resets_at: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ClaudeLimit {
#[serde(default)]
pub kind: Option<String>,
#[serde(default)]
pub percent: f64,
#[serde(default)]
pub severity: Option<String>,
#[serde(default)]
pub resets_at: Option<String>,
#[serde(default)]
pub scope: Option<ClaudeScope>,
#[serde(default, deserialize_with = "de_bool_or_false")]
pub is_active: bool,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ClaudeScope {
#[serde(default)]
pub model: Option<ClaudeScopeModel>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ClaudeScopeModel {
#[serde(default)]
pub display_name: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ClaudeSpend {
#[serde(default)]
pub used: Option<ClaudeMoney>,
#[serde(default)]
pub balance: Option<ClaudeMoney>,
#[serde(default)]
pub enabled: bool,
}
#[derive(Debug, Clone, Deserialize)]
pub struct ClaudeMoney {
#[serde(default)]
pub amount_minor: i64,
#[serde(default)]
pub currency: Option<String>,
#[serde(default)]
pub exponent: i32,
}
impl ClaudeMoney {
pub fn as_display(&self) -> String {
let value = self.amount_minor as f64 / 10f64.powi(self.exponent.max(0));
match self.currency.as_deref() {
Some("USD") | None => format!("${value:.2}"),
Some(cur) => format!("{value:.2} {cur}"),
}
}
}
#[derive(Clone, Deserialize)]
pub struct ClaudeCredentials {
#[serde(rename = "claudeAiOauth", default)]
pub claude_ai_oauth: Option<ClaudeOauth>,
}
impl fmt::Debug for ClaudeCredentials {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ClaudeCredentials")
.field("claude_ai_oauth", &self.claude_ai_oauth)
.finish()
}
}
#[derive(Clone, Deserialize)]
pub struct ClaudeOauth {
#[serde(rename = "accessToken", default)]
pub access_token: Option<String>,
#[serde(rename = "refreshToken", default)]
pub refresh_token: Option<String>,
#[serde(rename = "expiresAt", default)]
pub expires_at: Option<i64>,
#[serde(default)]
pub scopes: Vec<String>,
#[serde(rename = "rateLimitTier", default)]
pub rate_limit_tier: Option<String>,
#[serde(rename = "subscriptionType", default)]
pub subscription_type: Option<String>,
}
impl fmt::Debug for ClaudeOauth {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ClaudeOauth")
.field("access_token", &redact(&self.access_token))
.field("refresh_token", &redact(&self.refresh_token))
.field("expires_at", &self.expires_at)
.field("scopes", &self.scopes)
.field("rate_limit_tier", &self.rate_limit_tier)
.field("subscription_type", &self.subscription_type)
.finish()
}
}
#[derive(Clone, Deserialize)]
pub struct ClaudeRefreshResponse {
#[serde(default)]
pub access_token: Option<String>,
#[serde(default)]
pub refresh_token: Option<String>,
#[serde(default)]
pub expires_in: Option<i64>,
#[serde(default)]
pub scope: Option<String>,
}
impl fmt::Debug for ClaudeRefreshResponse {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ClaudeRefreshResponse")
.field("access_token", &redact(&self.access_token))
.field("refresh_token", &redact(&self.refresh_token))
.field("expires_in", &self.expires_in)
.field("scope", &self.scope)
.finish()
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ClaudeQuotaSnapshot {
#[serde(default)]
pub source: QuotaSource,
pub fetched_at: i64,
#[serde(default)]
pub plan_type: Option<String>,
pub five_hour: Option<QuotaWindow>,
pub seven_day: Option<QuotaWindow>,
#[serde(default)]
pub scoped_weekly: Option<QuotaWindow>,
#[serde(default)]
pub scoped_label: Option<String>,
#[serde(default)]
pub balance: Option<String>,
#[serde(default)]
pub spend_used: Option<String>,
#[serde(default)]
pub limit_reached: bool,
#[serde(default)]
pub needs_login: bool,
}
#[derive(Debug, Clone, Deserialize)]
pub struct WhamUsageResponse {
#[serde(default)]
pub plan_type: Option<String>,
#[serde(default)]
pub rate_limit: Option<WhamRateLimit>,
#[serde(default)]
pub credits: Option<WhamCredits>,
#[serde(default)]
pub rate_limit_reset_credits: Option<WhamResetCredits>,
#[serde(default)]
pub spend_control: Option<WhamSpendControl>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct WhamSpendControl {
#[serde(default)]
pub reached: Option<bool>,
#[serde(default)]
pub individual_limit: Option<f64>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct WhamRateLimit {
#[serde(default)]
pub limit_reached: Option<bool>,
#[serde(default)]
pub primary_window: Option<WhamWindow>,
#[serde(default)]
pub secondary_window: Option<WhamWindow>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct WhamWindow {
#[serde(default)]
pub used_percent: Option<f64>,
#[serde(default)]
pub limit_window_seconds: Option<i64>,
#[serde(default)]
pub reset_after_seconds: Option<i64>,
#[serde(default)]
pub reset_at: Option<i64>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct WhamCredits {
#[serde(default)]
pub has_credits: Option<bool>,
#[serde(default)]
pub unlimited: Option<bool>,
#[serde(default)]
pub overage_limit_reached: Option<bool>,
#[serde(default, deserialize_with = "de_string_or_number")]
pub balance: Option<String>,
#[serde(default)]
pub approx_local_messages: Option<Vec<i64>>,
#[serde(default)]
pub approx_cloud_messages: Option<Vec<i64>>,
}
fn de_string_or_number<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
where
D: serde::Deserializer<'de>,
{
Ok(match Option::<Value>::deserialize(deserializer)? {
Some(Value::String(s)) => Some(s),
Some(Value::Number(n)) => Some(n.to_string()),
_ => None,
})
}
fn de_f64_or_zero<'de, D>(deserializer: D) -> Result<f64, D::Error>
where
D: serde::Deserializer<'de>,
{
Ok(match Option::<Value>::deserialize(deserializer)? {
Some(Value::Number(n)) => n.as_f64().unwrap_or(0.0),
_ => 0.0,
})
}
fn de_bool_or_false<'de, D>(deserializer: D) -> Result<bool, D::Error>
where
D: serde::Deserializer<'de>,
{
Ok(matches!(
Option::<Value>::deserialize(deserializer)?,
Some(Value::Bool(true))
))
}
fn de_lenient_limits<'de, D>(deserializer: D) -> Result<Vec<ClaudeLimit>, D::Error>
where
D: serde::Deserializer<'de>,
{
Ok(match Value::deserialize(deserializer)? {
Value::Array(arr) => arr
.into_iter()
.filter_map(|e| serde_json::from_value(e).ok())
.collect(),
_ => Vec::new(),
})
}
#[derive(Debug, Clone, Deserialize)]
pub struct WhamResetCredits {
#[serde(default)]
pub available_count: Option<i64>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct WhamResetCreditsDetails {
pub credits: Vec<WhamResetCreditDetails>,
pub available_count: i64,
}
#[derive(Debug, Clone, Deserialize)]
pub struct WhamResetCreditDetails {
pub id: String,
pub reset_type: String,
pub status: String,
pub granted_at: String,
pub expires_at: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct CodexAuthJson {
#[serde(default)]
pub tokens: Option<CodexAuthTokens>,
}
#[derive(Clone, Deserialize)]
pub struct CodexAuthTokens {
#[serde(default)]
pub id_token: Option<String>,
#[serde(default)]
pub access_token: Option<String>,
#[serde(default)]
pub refresh_token: Option<String>,
#[serde(default)]
pub account_id: Option<String>,
}
impl fmt::Debug for CodexAuthTokens {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CodexAuthTokens")
.field("id_token", &redact(&self.id_token))
.field("access_token", &redact(&self.access_token))
.field("refresh_token", &redact(&self.refresh_token))
.field("account_id", &redact(&self.account_id))
.finish()
}
}
#[derive(Clone, Deserialize)]
pub struct CodexRefreshResponse {
#[serde(default)]
pub id_token: Option<String>,
#[serde(default)]
pub access_token: Option<String>,
#[serde(default)]
pub refresh_token: Option<String>,
}
impl fmt::Debug for CodexRefreshResponse {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("CodexRefreshResponse")
.field("id_token", &redact(&self.id_token))
.field("access_token", &redact(&self.access_token))
.field("refresh_token", &redact(&self.refresh_token))
.finish()
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct CodexSessionRateLimits {
#[serde(default)]
pub limit_id: Option<String>,
#[serde(default)]
pub plan_type: Option<String>,
#[serde(default)]
pub primary: Option<CodexSessionWindow>,
#[serde(default)]
pub secondary: Option<CodexSessionWindow>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct CodexSessionWindow {
#[serde(default)]
pub used_percent: Option<f64>,
#[serde(default)]
pub window_minutes: Option<i64>,
#[serde(default)]
pub resets_at: Option<i64>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum QuotaSource {
#[default]
None,
Api,
SessionFallback,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct QuotaWindow {
pub used_percent: f64,
pub resets_at_unix: Option<i64>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CodexQuotaSnapshot {
pub source: QuotaSource,
pub fetched_at: i64,
pub plan_type: Option<String>,
pub primary: Option<QuotaWindow>,
pub secondary: Option<QuotaWindow>,
pub credits_balance: Option<String>,
pub has_credits: Option<bool>,
pub unlimited: Option<bool>,
pub reset_credits_available: Option<i64>,
#[serde(default)]
pub reset_credit_expirations: Option<Vec<Option<i64>>>,
#[serde(default)]
pub approx_messages: Option<(i64, i64)>,
#[serde(default)]
pub spend_limit: Option<f64>,
pub limit_reached: Option<bool>,
#[serde(default)]
pub needs_login: bool,
}
#[derive(Debug, Clone, Deserialize)]
pub struct CopilotUserResponse {
#[serde(default)]
pub copilot_plan: Option<String>,
#[serde(default)]
pub quota_reset_date_utc: Option<String>,
#[serde(default)]
pub quota_reset_date: Option<String>,
#[serde(default)]
pub quota_snapshots: Option<CopilotQuotaSnapshots>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct CopilotQuotaSnapshots {
#[serde(default)]
pub premium_interactions: Option<CopilotQuotaEntry>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct CopilotQuotaEntry {
#[serde(default)]
pub percent_remaining: Option<f64>,
#[serde(default)]
pub remaining: Option<f64>,
#[serde(default)]
pub entitlement: Option<f64>,
#[serde(default)]
pub unlimited: Option<bool>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CursorUsageSummary {
#[serde(default)]
pub membership_type: Option<String>,
#[serde(default)]
pub is_unlimited: Option<bool>,
#[serde(default)]
pub billing_cycle_end: Option<String>,
#[serde(default)]
pub individual_usage: Option<CursorIndividualUsage>,
#[serde(default)]
pub team_usage: Option<CursorTeamUsage>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CursorTeamUsage {
#[serde(default)]
pub on_demand: Option<CursorOnDemand>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CursorIndividualUsage {
#[serde(default)]
pub plan: Option<CursorPlanUsage>,
#[serde(default)]
pub on_demand: Option<CursorOnDemand>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CursorPlanUsage {
#[serde(default)]
pub auto_percent_used: Option<f64>,
#[serde(default)]
pub api_percent_used: Option<f64>,
#[serde(default)]
pub total_percent_used: Option<f64>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CursorOnDemand {
#[serde(default)]
pub enabled: Option<bool>,
#[serde(default)]
pub used: Option<f64>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct GrokBillingResponse {
#[serde(default)]
pub config: Option<GrokCreditsConfig>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct GrokCreditsConfig {
#[serde(default, deserialize_with = "de_f64_or_zero")]
pub credit_usage_percent: f64,
#[serde(default)]
pub current_period: Option<GrokUsagePeriod>,
#[serde(default)]
pub on_demand_cap: Option<GrokMoney>,
#[serde(default)]
pub on_demand_used: Option<GrokMoney>,
#[serde(default)]
pub prepaid_balance: Option<GrokMoney>,
#[serde(default)]
pub billing_period_end: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct GrokUsagePeriod {
#[serde(default, rename = "type")]
pub period_type: Option<String>,
#[serde(default)]
pub end: Option<String>,
}
#[derive(Debug, Clone, Deserialize)]
pub struct GrokMoney {
#[serde(default, deserialize_with = "de_f64_or_zero")]
pub val: f64,
}
impl GrokMoney {
pub fn as_dollars(&self) -> f64 {
self.val / 100.0
}
}
#[derive(Debug, Clone, Deserialize)]
pub struct GrokSettingsResponse {
#[serde(default)]
pub subscription_tier_display: Option<String>,
}
#[derive(Clone, Default, Deserialize)]
pub struct GrokAuthEntry {
#[serde(default)]
pub key: Option<String>,
#[serde(default)]
pub refresh_token: Option<String>,
#[serde(default)]
pub expires_at: Option<String>,
#[serde(default)]
pub oidc_issuer: Option<String>,
#[serde(default)]
pub oidc_client_id: Option<String>,
#[serde(default)]
pub user_id: Option<String>,
#[serde(default)]
pub principal_type: Option<String>,
#[serde(default)]
pub principal_id: Option<String>,
}
impl fmt::Debug for GrokAuthEntry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("GrokAuthEntry")
.field("key", &redact(&self.key))
.field("refresh_token", &redact(&self.refresh_token))
.field("expires_at", &self.expires_at)
.field("oidc_issuer", &self.oidc_issuer)
.field("oidc_client_id", &self.oidc_client_id)
.field("user_id", &redact(&self.user_id))
.field("principal_type", &self.principal_type)
.field("principal_id", &redact(&self.principal_id))
.finish()
}
}
#[derive(Clone, Deserialize)]
pub struct GrokRefreshResponse {
#[serde(default)]
pub access_token: Option<String>,
#[serde(default)]
pub refresh_token: Option<String>,
#[serde(default)]
pub expires_in: Option<i64>,
}
impl fmt::Debug for GrokRefreshResponse {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("GrokRefreshResponse")
.field("access_token", &redact(&self.access_token))
.field("refresh_token", &redact(&self.refresh_token))
.field("expires_in", &self.expires_in)
.finish()
}
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CopilotQuotaSnapshot {
#[serde(default)]
pub source: QuotaSource,
pub fetched_at: i64,
#[serde(default)]
pub plan_type: Option<String>,
#[serde(default)]
pub premium: Option<QuotaWindow>,
#[serde(default)]
pub premium_remaining: Option<i64>,
#[serde(default)]
pub premium_entitlement: Option<i64>,
#[serde(default)]
pub premium_unlimited: bool,
#[serde(default)]
pub limit_reached: bool,
#[serde(default)]
pub needs_login: bool,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct CursorQuotaSnapshot {
#[serde(default)]
pub source: QuotaSource,
pub fetched_at: i64,
#[serde(default)]
pub plan_type: Option<String>,
#[serde(default)]
pub total: Option<QuotaWindow>,
#[serde(default)]
pub auto: Option<QuotaWindow>,
#[serde(default)]
pub api: Option<QuotaWindow>,
#[serde(default)]
pub on_demand_dollars: Option<f64>,
#[serde(default)]
pub limit_reached: bool,
#[serde(default)]
pub needs_login: bool,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct GrokQuotaSnapshot {
#[serde(default)]
pub source: QuotaSource,
pub fetched_at: i64,
#[serde(default)]
pub plan_type: Option<String>,
#[serde(default)]
pub included: Option<QuotaWindow>,
#[serde(default)]
pub period_label: Option<String>,
#[serde(default)]
pub on_demand_dollars: Option<f64>,
#[serde(default)]
pub on_demand_cap_dollars: Option<f64>,
#[serde(default)]
pub prepaid_balance_dollars: Option<f64>,
#[serde(default)]
pub limit_reached: bool,
#[serde(default)]
pub needs_login: bool,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn auth_tokens_debug_redacts_secrets() {
let tokens = CodexAuthTokens {
id_token: Some("jwt-header.payload.sig".into()),
access_token: Some("sk-super-secret-value".into()),
refresh_token: Some("rt-super-secret".into()),
account_id: Some("acct-1234567890".into()),
};
let direct = format!("{tokens:?}");
assert!(!direct.contains("sk-super-secret-value"));
assert!(!direct.contains("rt-super-secret"));
assert!(!direct.contains("acct-1234567890"));
assert!(direct.contains("<redacted>"));
let wrapped = format!(
"{:?}",
CodexAuthJson {
tokens: Some(tokens)
}
);
assert!(!wrapped.contains("sk-super-secret-value"));
assert!(!wrapped.contains("acct-1234567890"));
}
#[test]
fn claude_oauth_debug_redacts_secrets() {
let oauth = ClaudeOauth {
access_token: Some("claude-access-secret".into()),
refresh_token: Some("claude-refresh-secret".into()),
expires_at: Some(1783108188604),
scopes: vec!["user:inference".into()],
rate_limit_tier: Some("default_claude_max_20x".into()),
subscription_type: Some("max".into()),
};
let s = format!("{oauth:?}");
assert!(!s.contains("claude-access-secret"));
assert!(!s.contains("claude-refresh-secret"));
assert!(s.contains("<redacted>"));
assert!(s.contains("1783108188604"));
assert!(s.contains("user:inference"));
}
#[test]
fn refresh_responses_debug_redact_secrets() {
let c = ClaudeRefreshResponse {
access_token: Some("new-access".into()),
refresh_token: Some("new-refresh".into()),
expires_in: Some(28800),
scope: Some("user:inference".into()),
};
let cs = format!("{c:?}");
assert!(!cs.contains("new-access"));
assert!(!cs.contains("new-refresh"));
assert!(cs.contains("28800"));
let x = CodexRefreshResponse {
id_token: Some("id-secret".into()),
access_token: Some("acc-secret".into()),
refresh_token: Some("ref-secret".into()),
};
let xs = format!("{x:?}");
assert!(!xs.contains("id-secret"));
assert!(!xs.contains("acc-secret"));
assert!(!xs.contains("ref-secret"));
}
}