diff --git a/crates/api_clients/openai_sora_client/src/creds/sora_sentinel_token.rs b/crates/api_clients/openai_sora_client/src/creds/sora_sentinel_token.rs index 0c2fdc2e9d..d056503aa1 100644 --- a/crates/api_clients/openai_sora_client/src/creds/sora_sentinel_token.rs +++ b/crates/api_clients/openai_sora_client/src/creds/sora_sentinel_token.rs @@ -1,5 +1,6 @@ use crate::error::sora_client_error::SoraClientError; use chrono::{DateTime, TimeDelta, Utc}; +use log::{error, warn}; use serde_derive::{Deserialize, Serialize}; #[derive(Clone, Debug, Serialize, Deserialize)] @@ -14,13 +15,9 @@ pub struct SoraSentinelToken { /// Clientside time when it was generated (client-side). pub generated_at: DateTime, - /// Expiration time (reported by the server) + /// Seconds of validity (reported by the server, or our best guess). /// If the server doesn't tell us, we'll fill it in with our best guess. - pub expires_at: Option>, - - /// Seconds of validity (reported by the server) - /// If the server doesn't tell us, we'll fill it in with our best guess. - pub expires_in_seconds: Option, + pub expires_in_seconds: u32, } /// This is what is sent over the wire in requests. @@ -50,22 +47,30 @@ pub struct RawSoraSentinelToken { } impl SoraSentinelToken { + /// Check to see if the sentinel token is expired and in need of a refresh. pub fn is_expired(&self) -> bool { - // NB(1): We're not trusting the server timestamp for now. - // NB(2): We're not self-expiring for now. let now = Utc::now(); - if let Some(expires_in_seconds) = self.expires_in_seconds { - let maybe_expires_at = now - .checked_add_signed(TimeDelta::seconds(expires_in_seconds as i64)); + let maybe_expires_at = self.generated_at + .checked_add_signed(TimeDelta::seconds(self.expires_in_seconds as i64)); - if let Some(expires_at) = maybe_expires_at { - if now >= expires_at { - return true; - } + let expires_at = match maybe_expires_at { + Some(expires_at) => expires_at, + None => { + error!("Could not compute expires_at for sentinel token!"); + return false; } + }; + + if now >= expires_at { + return true; } + if expires_at.signed_duration_since(now).num_seconds() < 30 { + warn!("Less than 30 seconds remaining on sentinel token, treating as expired."); + return true; + } + false } diff --git a/crates/api_clients/openai_sora_client/src/requests/auth_sentinel_2/extract_expiry.rs b/crates/api_clients/openai_sora_client/src/requests/auth_sentinel_2/extract_expiry.rs new file mode 100644 index 0000000000..fa62c931e6 --- /dev/null +++ b/crates/api_clients/openai_sora_client/src/requests/auth_sentinel_2/extract_expiry.rs @@ -0,0 +1,62 @@ +use crate::requests::auth_sentinel_2::response::SentinelResponse; +use chrono::{DateTime, TimeDelta, Utc}; +use log::{info, warn}; +use std::f32::consts::E; + +// Sora's servers typically tell us to expire in nine minutes (540 seconds). +const DEFAULT_EXPIRE_SECONDS : u32 = 60 * 3; // 3 minutes. + +pub (super) struct ExtractedExpiry { + pub (super) generated_at: DateTime, + pub (super) expires_in_seconds: u32, +} + +pub (super) fn extract_expiry(generated_at: DateTime, response: &SentinelResponse) -> ExtractedExpiry { + info!("Sentinel expires at: {:?} (in {:?} seconds)", response.expire_at, response.expire_after); + + const LOWER_BOUNDS_SECONDS: u32 = 60; + const UPPER_BOUNDS_SECONDS: u32 = 60 * 60; + + // Trust the `expire_at` timestamp first. + let mut maybe_expires_at = response.expire_at + .map(|timestamp| DateTime::from_timestamp(timestamp as i64, 0)) + .flatten(); + + if let Some(expires_at) = maybe_expires_at { + let seconds = expires_at.signed_duration_since(generated_at).num_seconds(); + + const LOWER_BOUNDS_SECONDS_I64: i64 = LOWER_BOUNDS_SECONDS as i64; + const UPPER_BOUNDS_SECONDS_I64: i64 = UPPER_BOUNDS_SECONDS as i64; + + if seconds.is_positive() + && seconds > LOWER_BOUNDS_SECONDS_I64 + && seconds < UPPER_BOUNDS_SECONDS_I64 + { + info!("Using sentinel expire_at timestamp, expires in {} seconds (at {})", seconds, expires_at); + + return ExtractedExpiry { + generated_at, + expires_in_seconds: seconds as u32, + } + } + } + + let mut expires_in_seconds = response.expire_after + .unwrap_or_else(|| { + warn!("No default expire_after from sentinel response, defaulting to {} seconds", DEFAULT_EXPIRE_SECONDS); + DEFAULT_EXPIRE_SECONDS + }); + + if expires_in_seconds < LOWER_BOUNDS_SECONDS { + warn!("Warning: sentinel token expires very soon: {} seconds, defaulting to {} seconds", expires_in_seconds, DEFAULT_EXPIRE_SECONDS); + expires_in_seconds = DEFAULT_EXPIRE_SECONDS; + } else if expires_in_seconds > UPPER_BOUNDS_SECONDS { + warn!("Warning: sentinel token expires very late: {} seconds, defaulting to {} seconds", expires_in_seconds, DEFAULT_EXPIRE_SECONDS); + expires_in_seconds = DEFAULT_EXPIRE_SECONDS; + } + + ExtractedExpiry { + generated_at, + expires_in_seconds, + } +} diff --git a/crates/api_clients/openai_sora_client/src/requests/auth_sentinel_2/generate_sentinel_token_2.rs b/crates/api_clients/openai_sora_client/src/requests/auth_sentinel_2/generate_sentinel_token_2.rs index a676bdd567..bb7d296062 100644 --- a/crates/api_clients/openai_sora_client/src/requests/auth_sentinel_2/generate_sentinel_token_2.rs +++ b/crates/api_clients/openai_sora_client/src/requests/auth_sentinel_2/generate_sentinel_token_2.rs @@ -4,12 +4,14 @@ use crate::error::sora_error::SoraError; use crate::error::sora_generic_api_error::SoraGenericApiError; use crate::error::sora_specific_api_error::SoraSpecificApiError; use crate::requests::auth_sentinel::request::GenerateSentinelRefreshRequest; +use crate::requests::auth_sentinel_2::extract_expiry::extract_expiry; use crate::requests::auth_sentinel_2::request::SentinelRequest; use crate::requests::auth_sentinel_2::response::SentinelResponse; use crate::utils_internal::classify_general_http_error::classify_general_http_error; +use chrono::{DateTime, NaiveDateTime, TimeDelta, TimeZone, Utc}; use errors::AnyhowResult; use idempotency::uuid::generate_random_uuid; -use log::{debug, error}; +use log::{debug, error, info}; use serde_derive::{Deserialize, Serialize}; use std::io::Write; use thiserror::Error; @@ -19,7 +21,6 @@ use wreq::Client; const SORA_SENTINEL_ENDPOINT: &str = "https://chatgpt.com/backend-api/sentinel/req"; - /// This generates a Sentinel Token that works with Sora 1.0 and Sora 2.0 consumer products. pub async fn generate_sentinel_token_2() -> Result { let (_request, base64_request) = GenerateSentinelRefreshRequest::new().with_fourth_and_tenth(); @@ -77,9 +78,9 @@ pub async fn generate_sentinel_token_2() -> Result Ok(sentinel_token) } - - fn into_sora_sentinel_token(request: SentinelRequest, response: SentinelResponse, raw_body: &str) -> Result { + let expiry = extract_expiry(Utc::now(), &response); + let maybe_token = response.token; let maybe_turnstile = response.turnstile @@ -126,12 +127,12 @@ fn into_sora_sentinel_token(request: SentinelRequest, response: SentinelResponse c: token, }; + let sentinel = SoraSentinelToken { token: sentinel_raw, browser_user_agent: CLIENT_USER_AGENT.to_string(), - generated_at: Default::default(), // TODO - expires_at: None, // TODO - expires_in_seconds: None, // TODO + generated_at: expiry.generated_at, + expires_in_seconds: expiry.expires_in_seconds, }; Ok(sentinel) diff --git a/crates/api_clients/openai_sora_client/src/requests/auth_sentinel_2/mod.rs b/crates/api_clients/openai_sora_client/src/requests/auth_sentinel_2/mod.rs index 79b8d561c5..3647ad408d 100644 --- a/crates/api_clients/openai_sora_client/src/requests/auth_sentinel_2/mod.rs +++ b/crates/api_clients/openai_sora_client/src/requests/auth_sentinel_2/mod.rs @@ -1,3 +1,4 @@ +pub (super) mod extract_expiry; pub (super) mod request; pub (super) mod response; pub mod generate_sentinel_token_2; \ No newline at end of file diff --git a/crates/api_clients/openai_sora_client/src/requests/auth_sentinel_2/response.rs b/crates/api_clients/openai_sora_client/src/requests/auth_sentinel_2/response.rs index 756c0710b7..0f8a32fff5 100644 --- a/crates/api_clients/openai_sora_client/src/requests/auth_sentinel_2/response.rs +++ b/crates/api_clients/openai_sora_client/src/requests/auth_sentinel_2/response.rs @@ -1,10 +1,18 @@ +use chrono::{DateTime, Utc}; use serde_derive::Deserialize; #[derive(Debug, Deserialize)] pub (crate) struct SentinelResponse { pub (crate) token: Option, pub (crate) turnstile: Option, - + + // NB: appears to be integer seconds. + // Not required for the sentinel token flow, but helps with renew. + pub (crate) expire_after: Option, + + // NB: appears to be Unix timestamp in seconds. + // Not required for the sentinel token flow, but helps with renew. + pub (crate) expire_at: Option, } #[derive(Debug, Deserialize)]