sentinel expiry

This commit is contained in:
Brandon Thomas
2025-10-16 17:45:00 -04:00
parent 844891014e
commit 2a454cd314
5 changed files with 100 additions and 23 deletions
@@ -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<Utc>,
/// 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<DateTime<Utc>>,
/// 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<u32>,
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
}
@@ -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<Utc>,
pub (super) expires_in_seconds: u32,
}
pub (super) fn extract_expiry(generated_at: DateTime<Utc>, 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,
}
}
@@ -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<SoraSentinelToken, SoraError> {
let (_request, base64_request) = GenerateSentinelRefreshRequest::new().with_fourth_and_tenth();
@@ -77,9 +78,9 @@ pub async fn generate_sentinel_token_2() -> Result<SoraSentinelToken, SoraError>
Ok(sentinel_token)
}
fn into_sora_sentinel_token(request: SentinelRequest, response: SentinelResponse, raw_body: &str) -> Result<SoraSentinelToken, SoraSpecificApiError> {
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)
@@ -1,3 +1,4 @@
pub (super) mod extract_expiry;
pub (super) mod request;
pub (super) mod response;
pub mod generate_sentinel_token_2;
@@ -1,10 +1,18 @@
use chrono::{DateTime, Utc};
use serde_derive::Deserialize;
#[derive(Debug, Deserialize)]
pub (crate) struct SentinelResponse {
pub (crate) token: Option<String>,
pub (crate) turnstile: Option<SentinelTurnstile>,
// NB: appears to be integer seconds.
// Not required for the sentinel token flow, but helps with renew.
pub (crate) expire_after: Option<u32>,
// NB: appears to be Unix timestamp in seconds.
// Not required for the sentinel token flow, but helps with renew.
pub (crate) expire_at: Option<u64>,
}
#[derive(Debug, Deserialize)]