persist sentinel token in wrapper

This commit is contained in:
Brandon Thomas committed 2025-10-16 15:32:20 -04:00
1 parent c60770e881
commit 7842b3bd95
9 files changed
+234 -50

No files matched your search

@@ -2,4 +2,5 @@ pub mod sora_cookies;
pub mod sora_credential_builder;
pub mod sora_credential_set;
pub mod sora_jwt_bearer_token;
pub mod sora_sentinel;
pub mod sora_sentinel;
pub mod sora_sentinel_token;
@@ -2,13 +2,20 @@ use crate::creds::sora_cookies::SoraCookies;
use crate::creds::sora_credential_set::SoraCredentialSet;
use crate::creds::sora_jwt_bearer_token::SoraJwtBearerToken;
use crate::creds::sora_sentinel::SoraSentinel;
use crate::creds::sora_sentinel_token::SoraSentinelToken;
use crate::error::sora_client_error::SoraClientError;
use crate::error::sora_error::SoraError;
pub struct SoraCredentialBuilder {
cookies: Option<String>,
jwt_bearer_token: Option<String>,
#[deprecated(note="use sora_sentinel_token instead")]
sora_sentinel: Option<String>,
/// This is the newer sentinel token. Gradually phase out the older one.
sora_sentinel_token: Option<SoraSentinelToken>,
}
impl SoraCredentialBuilder {
@@ -17,6 +24,7 @@ impl SoraCredentialBuilder {
cookies: None,
jwt_bearer_token: None,
sora_sentinel: None,
sora_sentinel_token: None,
}
}
@@ -37,6 +45,11 @@ impl SoraCredentialBuilder {
self
}
pub fn with_sora_sentinel_token(mut self, sentinel_token: &SoraSentinelToken) -> Self {
self.sora_sentinel_token = Some(sentinel_token.clone());
self
}
// Mutable methods
pub fn set_cookies(&mut self, cookies: &str) {
@@ -50,6 +63,10 @@ impl SoraCredentialBuilder {
pub fn set_sora_sentinel(&mut self, sentinel: &str) {
self.sora_sentinel = Some(sentinel.to_string());
}
pub fn set_sora_sentinel_token(&mut self, sentinel_token: &SoraSentinelToken) {
self.sora_sentinel_token = Some(sentinel_token.clone());
}
pub fn build(self) -> Result<SoraCredentialSet, SoraError> {
let cookies = match self.cookies {
@@ -67,10 +84,16 @@ impl SoraCredentialBuilder {
None => None,
};
let sora_sentinel_token = match self.sora_sentinel_token {
Some(s) => Some(s),
None => None,
};
Ok(SoraCredentialSet {
cookies,
jwt_bearer_token,
sora_sentinel,
sora_sentinel_token,
})
}
}
@@ -2,24 +2,32 @@ use crate::creds::sora_cookies::SoraCookies;
use crate::creds::sora_credential_builder::SoraCredentialBuilder;
use crate::creds::sora_jwt_bearer_token::SoraJwtBearerToken;
use crate::creds::sora_sentinel::SoraSentinel;
use crate::creds::sora_sentinel_token::SoraSentinelToken;
#[derive(Clone)]
pub struct SoraCredentialSet {
pub cookies: SoraCookies,
pub jwt_bearer_token: Option<SoraJwtBearerToken>,
#[deprecated(note="use sora_sentinel_token instead")]
pub sora_sentinel: Option<SoraSentinel>,
// NB: This is the newer sentinel token. Gradually phase out the older one.
pub sora_sentinel_token: Option<SoraSentinelToken>,
}
impl SoraCredentialSet {
pub fn initialize(
cookies: SoraCookies,
bearer: Option<SoraJwtBearerToken>,
sentinel: Option<SoraSentinel>
sentinel: Option<SoraSentinel>,
sentinel_token: Option<SoraSentinelToken>,
) -> Self {
Self {
cookies,
jwt_bearer_token: bearer,
sora_sentinel: sentinel,
sora_sentinel_token: sentinel_token,
}
}
@@ -28,6 +36,7 @@ impl SoraCredentialSet {
cookies,
jwt_bearer_token: None,
sora_sentinel: None,
sora_sentinel_token: None,
}
}
@@ -46,6 +55,9 @@ impl SoraCredentialSet {
if let Some(sentinel) = &self.sora_sentinel {
builder = builder.with_sora_sentinel(sentinel.as_str());
}
if let Some(sentinel_token) = &self.sora_sentinel_token {
builder = builder.with_sora_sentinel_token(sentinel_token);
}
builder
}
}
@@ -1,6 +1,7 @@
/// Sora Sentinel tokens can be generated without cookies or JWTs, and are needed for some API calls.
#[derive(Clone)]
#[deprecated(note="use SoraSentinelToken instead")]
pub struct SoraSentinel {
sentinel: String,
@@ -13,14 +14,17 @@ impl SoraSentinel {
SoraSentinel { sentinel }
}
#[deprecated(note="use SoraSentinelToken instead")]
pub fn get_sentinel(&self) -> &str {
&self.sentinel
}
#[deprecated(note="use SoraSentinelToken instead")]
pub fn as_str(&self) -> &str {
&self.sentinel
}
#[deprecated(note="use SoraSentinelToken instead")]
pub fn as_bytes(&self) -> &[u8] {
self.sentinel.as_bytes()
}
@@ -0,0 +1,94 @@
use crate::error::sora_client_error::SoraClientError;
use chrono::{DateTime, TimeDelta, Utc};
use serde_derive::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct SoraSentinelToken {
/// The token sent over the wire.
/// (The other fields in this struct are metadata.)
pub token: RawSoraSentinelToken,
/// User agent used to get the token.
pub browser_user_agent: String,
/// Clientside time when it was generated (client-side).
pub generated_at: DateTime<Utc>,
/// Expiration time (reported by the server)
/// 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>,
}
/// This is what is sent over the wire in requests.
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RawSoraSentinelToken {
/// "Proof of Work" token (we generated this clientside)
#[serde(rename = "p")]
pub p: String,
/// Random UUID (we generated this clientside)
#[serde(rename = "id")]
pub id: String,
/// Frontend UI flow (this is the website flow used)
#[serde(rename = "flow")]
pub flow: String,
/// Turnstile dx from the response.
/// response["turnstile"]["dx"]
#[serde(rename = "t")]
pub t: String,
/// Token from the response.
/// response["token"]
#[serde(rename = "c")]
pub c: String,
}
impl SoraSentinelToken {
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));
if let Some(expires_at) = maybe_expires_at {
if now >= expires_at {
return true;
}
}
}
false
}
/// Convert to JSON, which is the raw over-the-wire format Sora consumes.
pub fn to_request_header_json(&self) -> Result<String, SoraClientError> {
self.token.to_request_header_json()
}
pub fn to_persistent_storage_json(&self) -> Result<String, SoraClientError> {
serde_json::to_string(self)
.map_err(|err| SoraClientError::CouldNotSerializeSentinelTokenStore(err))
}
pub fn from_persistent_storage_json(json: &str) -> Result<Self, SoraClientError> {
serde_json::from_str(json)
.map_err(|err| SoraClientError::CouldNotDeserializeSentinelTokenStore(err))
}
}
impl RawSoraSentinelToken {
/// Convert to JSON, which is the raw over-the-wire format Sora consumes.
pub fn to_request_header_json(&self) -> Result<String, SoraClientError> {
serde_json::to_string(self)
.map_err(|err| SoraClientError::CouldNotSerializeSentinelToken(err))
}
}
@@ -35,6 +35,12 @@ pub enum SoraClientError {
/// Error serializing the sentinel token to JSON
CouldNotSerializeSentinelToken(serde_json::Error),
/// Error serializing the sentinel store token to JSON (typically for persistent storage).
CouldNotSerializeSentinelTokenStore(serde_json::Error),
/// Error deserializing the sentinel token from JSON (typically for persistent storage).
CouldNotDeserializeSentinelTokenStore(serde_json::Error),
}
impl Error for SoraClientError {}
@@ -52,6 +58,8 @@ impl Display for SoraClientError {
Self::UrlParseError(err) => write!(f, "URL parse error: {}", err),
Self::WreqClientError(err) => write!(f, "Wreq client error (during client creation): {}", err),
Self::CouldNotSerializeSentinelToken(err) => write!(f, "Could not serialize sentinel token to JSON: {}", err),
Self::CouldNotSerializeSentinelTokenStore(err) => write!(f, "Could not serialize sentinel token store to JSON: {}", err),
Self::CouldNotDeserializeSentinelTokenStore(err) => write!(f, "Could not deserialize sentinel token store from JSON: {}", err),
}
}
}
@@ -1,11 +1,12 @@
use crate::constants::user_agent::CLIENT_USER_AGENT;
use crate::creds::sora_sentinel_token::{RawSoraSentinelToken, SoraSentinelToken};
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::request::SentinelRequest;
use crate::requests::auth_sentinel_2::response::SentinelResponse;
use crate::requests::auth_sentinel_2::sentinel_token::{SentinelResponsePieces, SentinelToken};
use crate::requests::auth_sentinel_2::sentinel_token::SentinelResponsePieces;
use crate::utils_internal::classify_general_http_error::classify_general_http_error;
use errors::AnyhowResult;
use idempotency::uuid::generate_random_uuid;
@@ -21,7 +22,7 @@ const SORA_SENTINEL_ENDPOINT: &str = "https://chatgpt.com/backend-api/sentinel/r
/// 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<SentinelToken, SoraError> {
pub async fn generate_sentinel_token_2() -> Result<SoraSentinelToken, SoraError> {
let (_request, base64_request) = GenerateSentinelRefreshRequest::new().with_fourth_and_tenth();
let request = SentinelRequest::new(base64_request);
@@ -72,49 +73,75 @@ pub async fn generate_sentinel_token_2() -> Result<SentinelToken, SoraError> {
SoraGenericApiError::SerdeResponseParseErrorWithBody(err, text_body.to_string())
})?;
let response_pieces = sentinel_response_into_pieces(response, text_body)?;
let sentinel_token = SentinelToken::from_server_request(&request, &response_pieces);
let sentinel_token = into_sora_sentinel_token(request, response, text_body)?;
Ok(sentinel_token)
}
fn sentinel_response_into_pieces(response: SentinelResponse, raw_body: &str) -> Result<SentinelResponsePieces, SoraSpecificApiError> {
fn into_sora_sentinel_token(request: SentinelRequest, response: SentinelResponse, raw_body: &str) -> Result<SoraSentinelToken, SoraSpecificApiError> {
let maybe_token = response.token;
let maybe_turnstile = response.turnstile
.map(|turn| turn.dx)
.flatten();
let mut missing_token = false;
let mut missing_turnstile = false;
let mut missing_turnstile = false;
match (response.token, maybe_turnstile) {
(None, None) => {
missing_token = true;
missing_turnstile = true;
let (token, turnstile) = {
let maybe_items = match (maybe_token, maybe_turnstile) {
(Some(token), Some(turnstile)) => {
Some((token, turnstile))
}
(None, None) => {
missing_token = true;
missing_turnstile = true;
None
}
(None, Some(_turnstile)) => {
missing_token = true;
None
}
(Some(_token), None) => {
missing_turnstile = true;
None
}
};
match maybe_items {
Some((token, turnstile)) => (token, turnstile),
_ => return Err(SoraSpecificApiError::SentinelResponseIsMissingFields {
missing_token,
missing_turnstile,
raw_response: raw_body.to_string(),
}),
}
(None, Some(_turnstile)) => {
missing_token = true;
}
(Some(_token), None) => {
missing_turnstile = true;
}
(Some(token), Some(turnstile)) => {
return Ok(SentinelResponsePieces {
token,
turnstile_dx: turnstile,
});
}
}
};
Err(SoraSpecificApiError::SentinelResponseIsMissingFields {
missing_token,
missing_turnstile,
raw_response: raw_body.to_string(),
})
let sentinel_raw = RawSoraSentinelToken {
p: request.p,
id: request.id,
flow: request.flow,
t: turnstile,
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
};
Ok(sentinel)
}
#[cfg(test)]
mod tests {
use std::fs::write;
use super::*;
#[tokio::test]
@@ -125,4 +152,19 @@ mod tests {
assert_eq!(1, 2);
Ok(())
}
#[tokio::test]
#[ignore] // Only manually trigger this
async fn write_token_to_disk() -> AnyhowResult<()> {
let filename = "/Users/bt/Artcraft/credentials/sora_sentinel_token_store.json";
let token = generate_sentinel_token_2().await?;
let json = token.to_persistent_storage_json()?;
write(filename, &json)?;
assert_eq!(1, 2);
Ok(())
}
}
@@ -3,6 +3,7 @@ use crate::creds::sora_credential_set::SoraCredentialSet;
use crate::error::sora_client_error::SoraClientError;
use crate::error::sora_error::SoraError;
use crate::error::sora_generic_api_error::SoraGenericApiError;
use crate::requests::auth_sentinel_2::generate_sentinel_token_2::generate_sentinel_token_2;
use crate::requests::common::task_id::TaskId;
use crate::requests::generate_sora2_video::http_request::{HttpCreateRequest, InpaintItem};
use crate::requests::generate_sora2_video::http_response::HttpCreateResponse;
@@ -13,7 +14,6 @@ use std::io::Write;
use std::time::Duration;
use wreq::header::{ACCEPT, ACCEPT_LANGUAGE, AUTHORIZATION, CONTENT_TYPE, COOKIE, ORIGIN, REFERER, USER_AGENT};
use wreq::Client;
use crate::requests::auth_sentinel_2::generate_sentinel_token_2::generate_sentinel_token_2;
const GENERATE_SORA_2_VIDEO_URL: &str = "https://sora.chatgpt.com/backend/nf/create";
@@ -47,21 +47,18 @@ pub (crate) async fn generate_sora2_video(
.ok_or(SoraClientError::NoBearerTokenForRequest)?
.to_authorization_header_value();
let sentinel_token = generate_sentinel_token_2().await?;
let sentinel_token = sentinel_token.to_json()?;
//let sentinel_token = args.credentials.sora_sentinel.as_ref()
// .map(|sentinel| sentinel.get_sentinel().to_string())
// .ok_or(SoraClientError::NoSentinelTokenForRequest)?;
let sentinel_token = args.credentials.sora_sentinel_token
.as_ref()
.ok_or(SoraClientError::NoSentinelTokenForRequest)?;
let sentinel_token = sentinel_token.to_request_header_json()?;
let cookie = args.credentials.cookies.to_string();
// TODO: Make the sec-* headers match the user agent and platform
// TODO: No sentinel?
// TODO: device id
//-H 'oai-device-id: 7c216860-5b73-4e63-983f-142dbcae1809' \
let mut http_request = client.post(GENERATE_SORA_2_VIDEO_URL)
let mut request_builder = client.post(GENERATE_SORA_2_VIDEO_URL)
.header(ACCEPT, "*/*")
.header("priority", "u=1, i")
.header(REFERER, "https://sora.chatgpt.com/explore")
@@ -80,7 +77,7 @@ pub (crate) async fn generate_sora2_video(
.header("sec-fetch-site", "same-origin");
if let Some(timeout) = args.request_timeout {
http_request = http_request.timeout(timeout);
request_builder = request_builder.timeout(timeout);
}
let orientation = match args.orientation {
@@ -119,15 +116,13 @@ pub (crate) async fn generate_sora2_video(
storyboard_id: None,
};
let http_request = http_request.json(&request_body).build()
let http_request = request_builder.json(&request_body)
.build()
.map_err(|err| {
error!("Error building Sora image generation HTTP request: {:?}", err);
SoraClientError::WreqClientError(err)
})?;
println!("send request...");
std::io::stdout().flush().unwrap();
let response = client.execute(http_request)
.await
.map_err(|err| {
@@ -135,8 +130,6 @@ pub (crate) async fn generate_sora2_video(
SoraClientError::WreqClientError(err)
})?;
println!("read status...");
std::io::stdout().flush().unwrap();
let status = response.status();
let response_body = &response.text().await
@@ -170,7 +163,7 @@ mod tests {
let creds = get_test_credentials()?;
let request = GenerateSora2VideoArgs {
//prompt: "A cute corgi wearing glasses, sitting on a picnic blanket and reading a book, digital art",
prompt: "A dog eating waffles in a Waffle House",
prompt: "A dog eating pancakes in an iHop",
credentials: &creds,
request_timeout: None,
orientation: Orientation::Landscape,
@@ -1,5 +1,6 @@
use crate::creds::sora_credential_builder::SoraCredentialBuilder;
use crate::creds::sora_credential_set::SoraCredentialSet;
use crate::creds::sora_sentinel_token::SoraSentinelToken;
use errors::AnyhowResult;
use std::fs::read_to_string;
@@ -8,7 +9,12 @@ pub fn get_test_credentials() -> AnyhowResult<SoraCredentialSet> {
// TODO: This should work on other folks' machines when they exist
// This is so brittle...
let sentinel = read_to_string("/Users/bt/Artcraft/credentials/sora_sentinel.txt")?;
let sentinel = sentinel.trim().to_string();
let sentinel_token = {
let sentinel_json = read_to_string("/Users/bt/Artcraft/credentials/sora_sentinel_token_store.json")?;
let sentinel_json = sentinel.trim().to_string();
SoraSentinelToken::from_persistent_storage_json(&sentinel_json)?
};
let cookie = read_to_string("/Users/bt/Artcraft/credentials/sora_cookies.txt")?;
let cookie = cookie.trim().to_string();
@@ -20,7 +26,8 @@ pub fn get_test_credentials() -> AnyhowResult<SoraCredentialSet> {
.with_cookies(&cookie)
.with_jwt_bearer_token(&bearer)
.with_sora_sentinel(&sentinel)
.with_sora_sentinel_token(&sentinel_token)
.build()?;
Ok(creds)
}
}