use core::time::Duration;
use std::time::{ SystemTime, UNIX_EPOCH };
use claude_common::ClaudePaths;
pub const WARNING_THRESHOLD_SECS : u64 = 3600;
#[ derive( Debug, Clone, PartialEq, Eq ) ]
pub enum TokenStatus
{
Valid
{
expires_in : Duration,
},
ExpiringSoon
{
expires_in : Duration,
},
Expired,
}
#[ inline ]
pub fn status() -> Result< TokenStatus, std::io::Error >
{
status_with_threshold( WARNING_THRESHOLD_SECS )
}
#[ inline ]
pub fn status_with_threshold( warning_secs : u64 ) -> Result< TokenStatus, std::io::Error >
{
let paths = ClaudePaths::new()
.ok_or_else( || std::io::Error::new(
std::io::ErrorKind::NotFound,
"HOME environment variable not set",
) )?;
let content = std::fs::read_to_string( paths.credentials_file() )
.map_err( | e | std::io::Error::new(
e.kind(),
format!( "failed to read credentials file: {e}" ),
) )?;
let expires_at_ms = parse_expires_at( &content )
.ok_or_else( || std::io::Error::new(
std::io::ErrorKind::InvalidData,
"credentials file missing or unparseable 'expiresAt' field",
) )?;
let now_ms = u64::try_from(
SystemTime::now()
.duration_since( UNIX_EPOCH )
.unwrap_or_default()
.as_millis()
).unwrap_or( u64::MAX );
if now_ms >= expires_at_ms
{
return Ok( TokenStatus::Expired );
}
let remaining = Duration::from_millis( expires_at_ms - now_ms );
if remaining.as_secs() <= warning_secs
{
Ok( TokenStatus::ExpiringSoon { expires_in : remaining } )
}
else
{
Ok( TokenStatus::Valid { expires_in : remaining } )
}
}
#[ doc( hidden ) ]
#[ must_use ]
#[ inline ]
pub fn parse_expires_at( json : &str ) -> Option< u64 >
{
let key = "\"expiresAt\":";
let colon_end = json.find( key )? + key.len();
let rest = json[ colon_end.. ].trim_start();
let end = rest
.find( | c : char | !c.is_ascii_digit() )
.unwrap_or( rest.len() );
if end == 0 { return None; }
rest[ ..end ].parse().ok()
}