125 lines
3.9 KiB
Rust
125 lines
3.9 KiB
Rust
|
|
//! 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);
|
||
|
|
}
|
||
|
|
}
|