//! OAuth flow state types and pure decision logic. //! //! All functions here are pure — no I/O, no network, no clocks. //! Side-effectful operations live exclusively in `io.rs`. use crate::llm::oauth::CredentialsFile; /// A pending PKCE flow waiting for an OAuth callback. pub struct PendingFlow { /// The PKCE code verifier generated at flow initiation. pub code_verifier: String, /// The redirect URI sent to the authorization endpoint. pub redirect_uri: String, } /// Current OAuth credential status, computed without I/O from already-loaded credentials. #[derive(Debug, Clone)] pub struct FlowStatus { /// Whether valid credentials were found on disk. pub authenticated: bool, /// Whether the access token is past its expiry timestamp. pub expired: bool, /// The Unix-epoch millisecond expiry timestamp (0 when unauthenticated). pub expires_at: u64, /// Whether a non-empty refresh token is present. pub has_refresh_token: bool, } /// Determine whether `expires_at` (Unix epoch ms) has passed, given `now_ms`. /// /// Returns `true` when `now_ms > expires_at`. pub fn is_token_expired(expires_at: u64, now_ms: u64) -> bool { now_ms > expires_at } /// Build a `FlowStatus` from loaded credentials and the current time. pub fn build_flow_status(creds: &CredentialsFile, now_ms: u64) -> FlowStatus { let expires_at = creds.claude_ai_oauth.expires_at; FlowStatus { authenticated: true, expired: is_token_expired(expires_at, now_ms), expires_at, has_refresh_token: !creds.claude_ai_oauth.refresh_token.is_empty(), } } /// Return the unauthenticated `FlowStatus` (no credentials on disk). pub fn unauthenticated_status() -> FlowStatus { FlowStatus { authenticated: false, expired: false, expires_at: 0, has_refresh_token: false, } } #[cfg(test)] mod tests { use super::*; #[test] fn is_token_expired_when_past_expiry() { assert!(is_token_expired(1000, 2000)); } #[test] fn is_token_not_expired_when_before_expiry() { assert!(!is_token_expired(2000, 1000)); } #[test] fn is_token_not_expired_at_exact_boundary() { // expires_at == now_ms → not expired assert!(!is_token_expired(1000, 1000)); } #[test] fn unauthenticated_status_is_not_authenticated() { let s = unauthenticated_status(); assert!(!s.authenticated); assert!(!s.expired); assert_eq!(s.expires_at, 0); assert!(!s.has_refresh_token); } #[test] fn build_flow_status_authenticated_not_expired() { use crate::llm::oauth::{CredentialsFile, OAuthCredentials}; let creds = CredentialsFile { claude_ai_oauth: OAuthCredentials { access_token: "tok".to_string(), refresh_token: "ref".to_string(), expires_at: 5000, scopes: vec![], subscription_type: None, rate_limit_tier: None, }, }; let status = build_flow_status(&creds, 1000); assert!(status.authenticated); assert!(!status.expired); assert_eq!(status.expires_at, 5000); assert!(status.has_refresh_token); } #[test] fn build_flow_status_authenticated_expired() { use crate::llm::oauth::{CredentialsFile, OAuthCredentials}; let creds = CredentialsFile { claude_ai_oauth: OAuthCredentials { access_token: "tok".to_string(), refresh_token: String::new(), expires_at: 1000, scopes: vec![], subscription_type: None, rate_limit_tier: None, }, }; let status = build_flow_status(&creds, 9999); assert!(status.authenticated); assert!(status.expired); assert!(!status.has_refresh_token); } }