huskies: merge 609_story_extract_oauth_service

This commit is contained in:
dave
2026-04-24 16:15:10 +00:00
parent 2dc2513fac
commit 60a9c87794
6 changed files with 758 additions and 283 deletions
+124
View File
@@ -0,0 +1,124 @@
//! 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);
}
}