From 908ff43be3208cbb0b547532ad6cae4ebcc1ca97 Mon Sep 17 00:00:00 2001 From: Brandon Thomas Date: Wed, 7 May 2025 03:33:53 -0400 Subject: [PATCH] fix sora / chatgpt login, enhance login flow (#586) --- .../sora/check_sora_session_command.rs | 85 +++++++++ .../desktop/artcraft/src/commands/sora/mod.rs | 1 + .../commands/sora/open_sora_login_command.rs | 61 ++---- crates/desktop/artcraft/src/lib.rs | 13 +- .../persist_storyteller_cookies_task.rs | 1 - crates/desktop/artcraft/src/threads/mod.rs | 1 - .../src/threads/sora_session_login_thread.rs | 178 ------------------ .../desktop/artcraft/src/utils/cookies/mod.rs | 1 - .../src/utils/cookies/sora_webview_cookies.rs | 26 --- crates/desktop/artcraft/src/utils/mod.rs | 1 - crates/desktop/artcraft/src/windows/mod.rs | 1 + .../clear_browsing_on_test_domain.rs | 20 ++ .../extract_sora_webview_cookies.rs | 28 +++ .../keep_on_login_domains.rs | 28 +++ .../src/windows/sora_login_window/mod.rs | 5 + .../open_sora_login_window.rs | 84 +++++++++ .../sora_login_window/sora_login_thread.rs | 150 +++++++++++++++ .../recipes/maybe_upgrade_or_renew_session.rs | 3 +- .../src/requests/bearer/generate.rs | 140 +++++++++----- .../src/requests/list_media/list_media.rs | 102 ++++++++++ .../src/requests/list_media/mod.rs | 1 + .../openai_sora_client/src/requests/mod.rs | 1 + .../openai_sora_client/src/sora_error.rs | 14 ++ .../src/utils/classify_general_http_error.rs | 26 +++ .../src/utils/has_session_cookie.rs | 7 +- .../src/utils/lightweight_sora_jwt_parse.rs | 53 ++++-- .../login-modal/src/lib/login-modal.tsx | 16 +- frontend/libs/tauri-api/src/index.ts | 1 + .../src/lib/sora/CheckSoraSession.ts | 18 ++ 29 files changed, 731 insertions(+), 335 deletions(-) create mode 100644 crates/desktop/artcraft/src/commands/sora/check_sora_session_command.rs delete mode 100644 crates/desktop/artcraft/src/threads/sora_session_login_thread.rs delete mode 100644 crates/desktop/artcraft/src/utils/cookies/mod.rs delete mode 100644 crates/desktop/artcraft/src/utils/cookies/sora_webview_cookies.rs create mode 100644 crates/desktop/artcraft/src/windows/mod.rs create mode 100644 crates/desktop/artcraft/src/windows/sora_login_window/clear_browsing_on_test_domain.rs create mode 100644 crates/desktop/artcraft/src/windows/sora_login_window/extract_sora_webview_cookies.rs create mode 100644 crates/desktop/artcraft/src/windows/sora_login_window/keep_on_login_domains.rs create mode 100644 crates/desktop/artcraft/src/windows/sora_login_window/mod.rs create mode 100644 crates/desktop/artcraft/src/windows/sora_login_window/open_sora_login_window.rs create mode 100644 crates/desktop/artcraft/src/windows/sora_login_window/sora_login_thread.rs create mode 100644 crates/lib/api_clients/openai_sora_client/src/requests/list_media/list_media.rs create mode 100644 crates/lib/api_clients/openai_sora_client/src/requests/list_media/mod.rs create mode 100644 frontend/libs/tauri-api/src/lib/sora/CheckSoraSession.ts diff --git a/crates/desktop/artcraft/src/commands/sora/check_sora_session_command.rs b/crates/desktop/artcraft/src/commands/sora/check_sora_session_command.rs new file mode 100644 index 0000000000..8c00c91aef --- /dev/null +++ b/crates/desktop/artcraft/src/commands/sora/check_sora_session_command.rs @@ -0,0 +1,85 @@ +use crate::commands::app_preferences::get_app_preferences_command::AppPreferencesPayload; +use crate::state::app_preferences::app_preferences_manager::AppPreferencesManager; +use crate::state::data_dir::app_data_root::AppDataRoot; +use crate::state::sora::sora_credential_manager::SoraCredentialManager; +use crate::windows::sora_login_window::open_sora_login_window::open_sora_login_window; +use errors::AnyhowResult; +use log::{error, info}; +use once_cell::sync::Lazy; +use openai_sora_client::creds::sora_credential_set::SoraCredentialSet; +use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session; +use openai_sora_client::requests::list_media::list_media::list_media; +use serde_derive::Serialize; +use tauri::{AppHandle, State}; + +#[derive(Serialize)] +#[serde(rename_all = "snake_case")] +pub struct CheckSoraSessionCommand { + pub state: SoraSessionState, + pub maybe_account_email: Option, +} + +#[derive(Serialize)] +#[serde(rename_all = "snake_case")] +pub enum SoraSessionState{ + #[serde(rename = "not_set_up")] + NotSetUp, + #[serde(rename = "expired_or_error")] + ExpiredOrError, + #[serde(rename = "valid")] + Valid, +} + + +#[tauri::command] +pub async fn check_sora_session_command( + app: AppHandle, + sora_creds_manager: State<'_, SoraCredentialManager>, +) -> Result { + info!("check_sora_session_command called"); + + match do_check(&sora_creds_manager).await { + Ok(state) => Ok(state), + Err(err) => { + error!("Error checking session state: {:?}", err); + Err(CheckSoraSessionCommand { + state: SoraSessionState::ExpiredOrError, + maybe_account_email: None, + }) + } + } +} + +async fn do_check( + sora_creds_manager: &SoraCredentialManager, +) -> AnyhowResult { + + let mut creds = match sora_creds_manager.get_credentials()? { + Some(creds) => creds, + None => { + return Ok(CheckSoraSessionCommand { + state: SoraSessionState::NotSetUp, + maybe_account_email: None, + }); + } + }; + + let upgraded = maybe_upgrade_or_renew_session(&mut creds).await?; + + list_media(&creds).await?; + + if upgraded { + info!("Saving upgraded session to manager and disk..."); + sora_creds_manager.set_credentials(&creds)?; + sora_creds_manager.persist_all_to_disk()?; + } + + let maybe_account_email = creds + .jwt_bearer_token + .map(|token| token.jwt_claims().email.clone()); + + Ok(CheckSoraSessionCommand { + state: SoraSessionState::Valid, + maybe_account_email, + }) +} diff --git a/crates/desktop/artcraft/src/commands/sora/mod.rs b/crates/desktop/artcraft/src/commands/sora/mod.rs index 0c197b2d85..3a85a45beb 100644 --- a/crates/desktop/artcraft/src/commands/sora/mod.rs +++ b/crates/desktop/artcraft/src/commands/sora/mod.rs @@ -1,3 +1,4 @@ +pub mod check_sora_session_command; pub mod open_sora_login_command; pub mod sora_image_generation_command; pub mod sora_image_remix_command; diff --git a/crates/desktop/artcraft/src/commands/sora/open_sora_login_command.rs b/crates/desktop/artcraft/src/commands/sora/open_sora_login_command.rs index 6a330816cf..d60ea4192b 100644 --- a/crates/desktop/artcraft/src/commands/sora/open_sora_login_command.rs +++ b/crates/desktop/artcraft/src/commands/sora/open_sora_login_command.rs @@ -1,31 +1,21 @@ -use crate::threads::sora_session_login_thread::{LOGIN_WINDOW_NAME, SORA_LOGIN_URL}; -use crate::utils::clear_all_webview_cookies::clear_all_webview_cookies; +use crate::state::app_preferences::app_preferences_manager::AppPreferencesManager; +use crate::state::data_dir::app_data_root::AppDataRoot; +use crate::state::sora::sora_credential_manager::SoraCredentialManager; +use crate::windows::sora_login_window::open_sora_login_window::open_sora_login_window; use errors::AnyhowResult; use log::{error, info}; use once_cell::sync::Lazy; -use std::time::Duration; -use tauri::{AppHandle, Manager, Url, WebviewUrl, WebviewWindowBuilder, WindowBuilder}; -use tokio::time::sleep_until; - -pub const START_URL_STR: &str = "https://storyteller.ai/"; - -pub static START_URL: Lazy = Lazy::new(|| { - Url::parse(START_URL_STR).expect("URL should parse") -}); - -pub const SORA_HOMEPAGE_URL_STR: &str = "https://sora.com/"; - -pub static SORA_HOMEPAGE_URL : Lazy = Lazy::new(|| { - Url::parse(SORA_HOMEPAGE_URL_STR).expect("URL should parse") -}); +use tauri::{AppHandle, State}; #[tauri::command] pub async fn open_sora_login_command( app: AppHandle, + app_data_root: State<'_, AppDataRoot>, + sora_creds_manager: State<'_, SoraCredentialManager>, ) -> Result { info!("open_login_command called"); - do_open_login(&app) + do_open_login(&app, &app_data_root, &sora_creds_manager) .await .map_err(|err| { error!("Error opening login: {:?}", err); @@ -35,37 +25,14 @@ pub async fn open_sora_login_command( Ok("result".to_string()) } -async fn do_open_login(app: &AppHandle) -> AnyhowResult<()> { +async fn do_open_login( + app: &AppHandle, + app_data_root: &AppDataRoot, + sora_creds_manager: &SoraCredentialManager, +) -> AnyhowResult<()> { info!("Building login window..."); - let url = WebviewUrl::External(START_URL.clone()); - - let window = WebviewWindowBuilder::new(app, LOGIN_WINDOW_NAME, url) - //.user_agent(openai_sora_client::credentials::USER_AGENT) - .always_on_top(true) - .title("Login to OpenAI") - .center() - .resizable(true) - .visible(true) - .closable(true) - .focused(true) - .devtools(true) - .build()?; - - for (name, webview) in window.webviews() { - if name != LOGIN_WINDOW_NAME { - continue; - } - - clear_all_webview_cookies(&webview)?; - - // NB: We're starting to get Cloudflare protection screens. Let's try to avoid. - webview.navigate(SORA_HOMEPAGE_URL.clone())?; - tokio::time::sleep(Duration::from_millis(100)).await; - - webview.navigate(SORA_LOGIN_URL.clone())?; - break; - } + open_sora_login_window(app, app_data_root, sora_creds_manager).await?; info!("Done."); Ok(()) diff --git a/crates/desktop/artcraft/src/lib.rs b/crates/desktop/artcraft/src/lib.rs index 65f9c29b06..a1de6336d1 100644 --- a/crates/desktop/artcraft/src/lib.rs +++ b/crates/desktop/artcraft/src/lib.rs @@ -3,16 +3,19 @@ pub mod events; pub mod state; pub mod threads; pub mod utils; +pub mod windows; use tauri::Manager; -use crate::commands::flip_image::flip_image; use crate::commands::app_preferences::get_app_preferences_command::get_app_preferences_command; use crate::commands::app_preferences::update_app_preference_command::update_app_preferences_command; +use crate::commands::flip_image::flip_image; use crate::commands::platform_info_command::platform_info_command; +use crate::commands::sora::check_sora_session_command::check_sora_session_command; use crate::commands::sora::open_sora_login_command::open_sora_login_command; use crate::commands::sora::sora_image_generation_command::sora_image_generation_command; use crate::commands::sora::sora_image_remix_command::sora_image_remix_command; +use crate::state::app_preferences::app_preferences_manager::load_app_preferences_or_default; use crate::state::data_dir::app_data_root::AppDataRoot; use crate::state::main_window_size::MainWindowSize; use crate::state::sora::sora_credential_manager::SoraCredentialManager; @@ -20,13 +23,11 @@ use crate::state::sora::sora_task_queue::SoraTaskQueue; use crate::state::storyteller::storyteller_credential_manager::StorytellerCredentialManager; use crate::threads::discord_presence_thread::discord_presence_thread; use crate::threads::main_window_thread::main_window_thread::main_window_thread; -use crate::threads::sora_session_login_thread::sora_session_login_thread; use crate::threads::sora_task_polling_thread::sora_task_polling_thread; use crate::utils::webview_unsafe::webview_unsafe_for_app; -use crate::state::app_preferences::app_preferences_manager::load_app_preferences_or_default; -use tauri_plugin_http; use tauri_plugin_dialog; +use tauri_plugin_http; use tauri_plugin_log::Target; use tauri_plugin_log::TargetKind; @@ -108,7 +109,6 @@ pub fn run() { } } - tauri::async_runtime::spawn(sora_session_login_thread(app_2, app_data_root_2, sora_creds_manager_2)); tauri::async_runtime::spawn(main_window_thread(app_3, app_data_root_3, storyteller_creds_manager_2)); tauri::async_runtime::spawn(sora_task_polling_thread(app_4, app_data_root_4, sora_creds_manager_3, storyteller_creds_manager_3, sora_task_queue_2)); tauri::async_runtime::spawn(discord_presence_thread()); @@ -120,13 +120,14 @@ pub fn run() { .manage(sora_creds_manager) .manage(sora_task_queue) .invoke_handler(tauri::generate_handler![ + check_sora_session_command, flip_image, get_app_preferences_command, - update_app_preferences_command, open_sora_login_command, platform_info_command, sora_image_generation_command, sora_image_remix_command, + update_app_preferences_command, ]) .run(tauri::generate_context!("tauri.conf.json")) .expect("error while running tauri application"); diff --git a/crates/desktop/artcraft/src/threads/main_window_thread/persist_storyteller_cookies_task.rs b/crates/desktop/artcraft/src/threads/main_window_thread/persist_storyteller_cookies_task.rs index 066a1f294c..49c5c4bdf0 100644 --- a/crates/desktop/artcraft/src/threads/main_window_thread/persist_storyteller_cookies_task.rs +++ b/crates/desktop/artcraft/src/threads/main_window_thread/persist_storyteller_cookies_task.rs @@ -1,7 +1,6 @@ use crate::state::app_startup_time::AppStartupTime; use crate::state::data_dir::app_data_root::AppDataRoot; use crate::state::storyteller::storyteller_credential_manager::StorytellerCredentialManager; -use crate::threads::sora_session_login_thread::LOGIN_WINDOW_NAME; use anyhow::anyhow; use chrono::TimeDelta; use errors::AnyhowResult; diff --git a/crates/desktop/artcraft/src/threads/mod.rs b/crates/desktop/artcraft/src/threads/mod.rs index 1be2c3b811..eaae9fdf2e 100644 --- a/crates/desktop/artcraft/src/threads/mod.rs +++ b/crates/desktop/artcraft/src/threads/mod.rs @@ -1,4 +1,3 @@ pub mod discord_presence_thread; pub mod main_window_thread; -pub mod sora_session_login_thread; pub mod sora_task_polling_thread; \ No newline at end of file diff --git a/crates/desktop/artcraft/src/threads/sora_session_login_thread.rs b/crates/desktop/artcraft/src/threads/sora_session_login_thread.rs deleted file mode 100644 index ca8562d549..0000000000 --- a/crates/desktop/artcraft/src/threads/sora_session_login_thread.rs +++ /dev/null @@ -1,178 +0,0 @@ -use crate::state::data_dir::app_data_root::AppDataRoot; -use crate::state::sora::sora_credential_holder::SoraCredentialHolder; -use crate::state::sora::sora_credential_manager::SoraCredentialManager; -use crate::utils::cookies::sora_webview_cookies::get_all_sora_cookies_as_string; -use crate::utils::sora::initialize_sora_jwt_bearer_token::initialize_sora_jwt_bearer_token; -use anyhow::anyhow; -use chrono::{DateTime, NaiveDateTime, TimeDelta}; -use errors::AnyhowResult; -use log::{error, info, warn}; -use once_cell::sync::Lazy; -use openai_sora_client::creds::sora_credential_set::SoraCredentialSet; -use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session; -use openai_sora_client::utils::has_session_cookie::{has_session_cookie, SessionCookiePresence}; -use reqwest::Url; -use std::fs; -use std::fs::{read_to_string, OpenOptions}; -use std::io::Write; -use std::ops::Sub; -use tauri::{AppHandle, Manager, Webview}; - -pub const LOGIN_WINDOW_NAME: &str = "login_window"; - -pub const SORA_LOGIN_URL_STR: &str = "https://sora.com/auth/login?callback_path=/"; - -pub static SORA_LOGIN_URL: Lazy = Lazy::new(|| { - Url::parse(SORA_LOGIN_URL_STR).expect("URL should parse") -}); - -pub const SORA_ROOT_URL_STR: &str = "https://sora.com/"; - -pub static SORA_ROOT_URL: Lazy = Lazy::new(|| { - Url::parse(SORA_ROOT_URL_STR).expect("URL should parse") -}); - -pub async fn sora_session_login_thread( - app: AppHandle, - app_data_root: AppDataRoot, - sora_creds_manager: SoraCredentialManager -) -> ! { - loop { - for (window_name, webview) in app.webviews() { - if window_name == LOGIN_WINDOW_NAME { - let result = check_login_window( - &webview, - &app_data_root, - &sora_creds_manager, - ).await; - if let Err(err) = result { - error!("Error checking login window: {:?}", err); - } - break; - } - } - tokio::time::sleep(std::time::Duration::from_millis(2_000)).await; - } -} - -async fn check_login_window( - webview: &Webview, - app_data_root: &AppDataRoot, - sora_credential_manager: &SoraCredentialManager, -) -> AnyhowResult<()> { - - - - - - - clear_browsing_data_on_test_domain(webview)?; - //keep_on_task(webview)?; - extract_cookies_to_file(webview, app_data_root, sora_credential_manager).await?; - //initialize_sora_jwt_bearer_token(app_data_root).await?; // TODO: This only runs once. We need better management. - Ok(()) -} - -fn keep_on_task(webview: &Webview) -> AnyhowResult<()> { - let url = webview.url()?; - let hostname= url.host() - .ok_or(anyhow!("no host in url"))? - .to_string(); - match hostname.as_str() { - "auth.openai.com" => {}, - "openai.com" => {}, - "sora.com" => {}, - // Third party SSO - "accounts.google.com" => {}, - "accounts.youtube.com" => {}, - "login.live.com" => {}, - "appleid.apple.com" => {}, - _ => { - info!("Non login hostname: {}", hostname); - webview.navigate(SORA_LOGIN_URL.clone())?; - } - } - Ok(()) -} - -/// This is just so we have a way to clear browsing data. -fn clear_browsing_data_on_test_domain(webview: &Webview) -> AnyhowResult<()> { - let url = webview.url()?; - let hostname= url.host() - .ok_or(anyhow!("no host in url"))? - .to_string(); - match hostname.as_str() { - "storyteller.ai" => { - info!("Clearing all browsing data..."); - webview.clear_all_browsing_data()?; - } - _ => {} - } - Ok(()) -} - -// TODO(bt,2025-04-07): Heuristic to detect when logged in. Only write when logged in. -async fn extract_cookies_to_file( - webview: &Webview, - app_data_root: &AppDataRoot, - sora_credential_manager: &SoraCredentialManager, -) -> AnyhowResult<()> { - let new_cookies = get_all_sora_cookies_as_string(webview)?.trim().to_string(); - - if new_cookies.is_empty() { - return Ok(()); - } - - let session_cookie_presence = has_session_cookie(&new_cookies) - .unwrap_or_else(|err| { - error!("Failed to check for session cookie: {:?}", err); - SessionCookiePresence::MaybePresent - }); - - info!("Session cookies are: {:?}", session_cookie_presence); - - let (should_write_cookies, should_upgrade_session) = match session_cookie_presence { - SessionCookiePresence::Present => (true, true), - SessionCookiePresence::MaybePresent => (true, false), - SessionCookiePresence::Absent => (false, false), - }; - - if !should_write_cookies { - return Ok(()); - } - - // TODO(bt): Race conditions ahead. - - sora_credential_manager.clear_credentials()?; - - let _r = fs::remove_file(app_data_root.get_sora_cookie_file_path()); - let _r = fs::remove_file(app_data_root.get_sora_bearer_token_file_path()); - let _r = fs::remove_file(app_data_root.get_sora_sentinel_file_path()); - - let mut new_credentials = - SoraCredentialSet::initialize_with_just_cookies_str(&new_cookies); - - if should_upgrade_session { - let _upgraded = maybe_upgrade_or_renew_session(&mut new_credentials).await?; - } - - sora_credential_manager.set_credentials(&new_credentials)?; - sora_credential_manager.persist_all_to_disk()?; - - Ok(()) -} - -pub fn read_sora_cookies_from_disk(app_data_root: &AppDataRoot) -> Option { - let cookie_file = app_data_root.get_sora_cookie_file_path(); - if !cookie_file.exists() { - return None; - } - - match read_to_string(cookie_file) { - Ok(contents) => Some(contents.trim().to_string()), - Err(err) => { - warn!("Failed to read cookie file: {:?}", err); - None - } - } -} diff --git a/crates/desktop/artcraft/src/utils/cookies/mod.rs b/crates/desktop/artcraft/src/utils/cookies/mod.rs deleted file mode 100644 index fc9a63ecd8..0000000000 --- a/crates/desktop/artcraft/src/utils/cookies/mod.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod sora_webview_cookies; \ No newline at end of file diff --git a/crates/desktop/artcraft/src/utils/cookies/sora_webview_cookies.rs b/crates/desktop/artcraft/src/utils/cookies/sora_webview_cookies.rs deleted file mode 100644 index e85c887a81..0000000000 --- a/crates/desktop/artcraft/src/utils/cookies/sora_webview_cookies.rs +++ /dev/null @@ -1,26 +0,0 @@ -use once_cell::sync::Lazy; -use reqwest::Url; -use tauri::Webview; -use tauri::webview::Cookie; -use errors::AnyhowResult; - -const SORA_ROOT_COOKIE_URL_STR: &str = "https://sora.com/"; - -static SORA_ROOT_COOKIE_URL: Lazy = Lazy::new(|| { - Url::parse(SORA_ROOT_COOKIE_URL_STR).expect("URL should parse") -}); - -pub fn get_all_sora_cookies(webview: &Webview) -> AnyhowResult> { - let cookies = webview.cookies_for_url(SORA_ROOT_COOKIE_URL.clone())?; - Ok(cookies) -} - -pub fn get_all_sora_cookies_as_string(webview: &Webview) -> AnyhowResult { - let cookies = get_all_sora_cookies(webview)?; - let cookie_string = cookies - .iter() - .map(|cookie| format!("{}={}", cookie.name(), cookie.value())) - .collect::>() - .join("; "); - Ok(cookie_string) -} diff --git a/crates/desktop/artcraft/src/utils/mod.rs b/crates/desktop/artcraft/src/utils/mod.rs index 02dbf73e50..c98cf6ab71 100644 --- a/crates/desktop/artcraft/src/utils/mod.rs +++ b/crates/desktop/artcraft/src/utils/mod.rs @@ -1,5 +1,4 @@ pub mod clear_all_webview_cookies; -pub mod cookies; pub mod get_url_file_extension; pub mod image; pub mod large_file_transfer; diff --git a/crates/desktop/artcraft/src/windows/mod.rs b/crates/desktop/artcraft/src/windows/mod.rs new file mode 100644 index 0000000000..3843c67df6 --- /dev/null +++ b/crates/desktop/artcraft/src/windows/mod.rs @@ -0,0 +1 @@ +pub mod sora_login_window; \ No newline at end of file diff --git a/crates/desktop/artcraft/src/windows/sora_login_window/clear_browsing_on_test_domain.rs b/crates/desktop/artcraft/src/windows/sora_login_window/clear_browsing_on_test_domain.rs new file mode 100644 index 0000000000..54cc5403d8 --- /dev/null +++ b/crates/desktop/artcraft/src/windows/sora_login_window/clear_browsing_on_test_domain.rs @@ -0,0 +1,20 @@ +use anyhow::anyhow; +use log::info; +use tauri::Webview; +use errors::AnyhowResult; + +// This is just so we have a way to clear browsing data while debugging / building. +pub fn clear_browsing_data_on_test_domain(webview: &Webview) -> AnyhowResult<()> { + let url = webview.url()?; + let hostname= url.host() + .ok_or(anyhow!("no host in url"))? + .to_string(); + match hostname.as_str() { + "storyteller.ai" => { + info!("Clearing all browsing data..."); + webview.clear_all_browsing_data()?; + } + _ => {} + } + Ok(()) +} diff --git a/crates/desktop/artcraft/src/windows/sora_login_window/extract_sora_webview_cookies.rs b/crates/desktop/artcraft/src/windows/sora_login_window/extract_sora_webview_cookies.rs new file mode 100644 index 0000000000..03696c522a --- /dev/null +++ b/crates/desktop/artcraft/src/windows/sora_login_window/extract_sora_webview_cookies.rs @@ -0,0 +1,28 @@ +use errors::AnyhowResult; +use once_cell::sync::Lazy; +use reqwest::Url; +use tauri::webview::Cookie; +use tauri::WebviewWindow; + +const CHAT_GPT_ROOT_COOKIE_URL_STR: &str = "https://chatgpt.com"; + +static CHAT_GPT_ROOT_COOKIE_URL: Lazy = Lazy::new(|| { + Url::parse(CHAT_GPT_ROOT_COOKIE_URL_STR).expect("URL should parse") +}); + +pub fn extract_sora_webview_cookies(webview: &WebviewWindow) -> AnyhowResult { + // NB(bt): Cookies were originally on sora.com, but now they're located on + // the sora.chatgpt.com / *.chatgpt.com domains. + let cookies = get_all_chatgpt_cookies(webview)?; + let cookie_string = cookies + .iter() + .map(|cookie| format!("{}={}", cookie.name(), cookie.value())) + .collect::>() + .join("; "); + Ok(cookie_string) +} + +fn get_all_chatgpt_cookies(webview: &WebviewWindow) -> AnyhowResult> { + let cookies = webview.cookies_for_url(CHAT_GPT_ROOT_COOKIE_URL.clone())?; + Ok(cookies) +} diff --git a/crates/desktop/artcraft/src/windows/sora_login_window/keep_on_login_domains.rs b/crates/desktop/artcraft/src/windows/sora_login_window/keep_on_login_domains.rs new file mode 100644 index 0000000000..9c485ae030 --- /dev/null +++ b/crates/desktop/artcraft/src/windows/sora_login_window/keep_on_login_domains.rs @@ -0,0 +1,28 @@ +use crate::windows::sora_login_window::open_sora_login_window::SORA_LOGIN_URL; +use anyhow::anyhow; +use errors::AnyhowResult; +use log::info; +use tauri::Webview; + +// NB: Probably best not to use this. +pub fn keep_on_login_domains(webview: &Webview) -> AnyhowResult<()> { + let url = webview.url()?; + let hostname= url.host() + .ok_or(anyhow!("no host in url"))? + .to_string(); + match hostname.as_str() { + "auth.openai.com" => {}, + "openai.com" => {}, + "sora.com" => {}, + // Third party SSO + "accounts.google.com" => {}, + "accounts.youtube.com" => {}, + "login.live.com" => {}, + "appleid.apple.com" => {}, + _ => { + info!("Non login hostname: {}", hostname); + webview.navigate(SORA_LOGIN_URL.clone())?; + } + } + Ok(()) +} diff --git a/crates/desktop/artcraft/src/windows/sora_login_window/mod.rs b/crates/desktop/artcraft/src/windows/sora_login_window/mod.rs new file mode 100644 index 0000000000..0ff8648e3b --- /dev/null +++ b/crates/desktop/artcraft/src/windows/sora_login_window/mod.rs @@ -0,0 +1,5 @@ +pub mod clear_browsing_on_test_domain; +pub mod extract_sora_webview_cookies; +pub mod keep_on_login_domains; +pub mod open_sora_login_window; +pub mod sora_login_thread; diff --git a/crates/desktop/artcraft/src/windows/sora_login_window/open_sora_login_window.rs b/crates/desktop/artcraft/src/windows/sora_login_window/open_sora_login_window.rs new file mode 100644 index 0000000000..d017bdd2a2 --- /dev/null +++ b/crates/desktop/artcraft/src/windows/sora_login_window/open_sora_login_window.rs @@ -0,0 +1,84 @@ +use crate::state::data_dir::app_data_root::AppDataRoot; +use crate::state::sora::sora_credential_manager::SoraCredentialManager; +use crate::utils::clear_all_webview_cookies::clear_all_webview_cookies; +use crate::windows::sora_login_window::sora_login_thread::sora_login_thread; +use anyhow::anyhow; +use errors::AnyhowResult; +use once_cell::sync::Lazy; +use reqwest::Url; +use std::time::Duration; +use tauri::{AppHandle, Manager, WebviewUrl, WebviewWindowBuilder}; + +/// Name of the window +pub const LOGIN_WINDOW_NAME: &str = "login_window"; + +pub const START_URL_STR: &str = "https://google.com"; + +pub static START_URL: Lazy = Lazy::new(|| { + Url::parse(START_URL_STR).expect("URL should parse") +}); + +pub const SORA_HOMEPAGE_URL_STR: &str = "https://sora.com/"; + +pub static SORA_HOMEPAGE_URL : Lazy = Lazy::new(|| { + Url::parse(SORA_HOMEPAGE_URL_STR).expect("URL should parse") +}); + +pub const SORA_LOGIN_URL_STR: &str = "https://chatgpt.com/auth/login?next=%2Fsora%2F"; + +pub static SORA_LOGIN_URL: Lazy = Lazy::new(|| { + Url::parse(SORA_LOGIN_URL_STR).expect("URL should parse") +}); + +//pub const SORA_ROOT_URL_STR: &str = "https://sora.com/"; +// +//pub static SORA_ROOT_URL: Lazy = Lazy::new(|| { +// Url::parse(SORA_ROOT_URL_STR).expect("URL should parse") +//}); + +pub async fn open_sora_login_window( + app: &AppHandle, + app_data_root: &AppDataRoot, + sora_creds_manager: &SoraCredentialManager, +) -> AnyhowResult<()> { + if app.get_window(LOGIN_WINDOW_NAME).is_some() { + return Err(anyhow!("Login window already open")); + } + + let url = WebviewUrl::External(START_URL.clone()); + + let window = WebviewWindowBuilder::new(app, LOGIN_WINDOW_NAME, url) + //.user_agent(openai_sora_client::credentials::USER_AGENT) + .always_on_top(true) + .title("Login to OpenAI's Sora") + .center() + .resizable(true) + .visible(true) + .closable(true) + .min_inner_size(200.0, 800.0) + .focused(true) + .devtools(true) + .build()?; + + let webview = window.get_webview(LOGIN_WINDOW_NAME) + .ok_or_else(|| anyhow!("no webview found"))?; + + clear_all_webview_cookies(&webview)?; + + webview.navigate(SORA_HOMEPAGE_URL.clone())?; + + // NB: We're starting to get Cloudflare protection screens. Let's try to avoid. + tokio::time::sleep(Duration::from_millis(100)).await; + + webview.navigate(SORA_LOGIN_URL.clone())?; + + let app_handle = app.clone(); + let app_data_root = app_data_root.clone(); + let sora_creds_manager = sora_creds_manager.clone(); + + let _ = tauri::async_runtime::spawn(async move { + sora_login_thread(app_handle, app_data_root, sora_creds_manager).await; + }); + + Ok(()) +} diff --git a/crates/desktop/artcraft/src/windows/sora_login_window/sora_login_thread.rs b/crates/desktop/artcraft/src/windows/sora_login_window/sora_login_thread.rs new file mode 100644 index 0000000000..26c2a55e20 --- /dev/null +++ b/crates/desktop/artcraft/src/windows/sora_login_window/sora_login_thread.rs @@ -0,0 +1,150 @@ +use crate::state::data_dir::app_data_root::AppDataRoot; +use crate::state::sora::sora_credential_holder::SoraCredentialHolder; +use crate::state::sora::sora_credential_manager::SoraCredentialManager; +use crate::utils::sora::initialize_sora_jwt_bearer_token::initialize_sora_jwt_bearer_token; +use crate::windows::sora_login_window::extract_sora_webview_cookies::extract_sora_webview_cookies; +use crate::windows::sora_login_window::open_sora_login_window::LOGIN_WINDOW_NAME; +use anyhow::anyhow; +use chrono::{DateTime, NaiveDateTime, TimeDelta}; +use errors::AnyhowResult; +use log::{error, info, warn}; +use once_cell::sync::Lazy; +use openai_sora_client::creds::sora_credential_set::SoraCredentialSet; +use openai_sora_client::recipes::maybe_upgrade_or_renew_session::maybe_upgrade_or_renew_session; +use openai_sora_client::utils::has_session_cookie::{has_session_cookie, SessionCookiePresence}; +use reqwest::Url; +use std::fs; +use std::fs::{read_to_string, OpenOptions}; +use std::io::Write; +use std::ops::Sub; +use tauri::{AppHandle, Manager, Webview, WebviewWindow}; + +pub async fn sora_login_thread( + app: AppHandle, + app_data_root: AppDataRoot, + sora_creds_manager: SoraCredentialManager +) { + loop { + let login_webview_window = match app.get_webview_window(LOGIN_WINDOW_NAME) { + Some(webview) => webview, + None => { + info!("Exit sora login thread."); + return; // NB: Only exit if we don't have the webview. + } + }; + + let result = check_login_window( + &login_webview_window, + &app_data_root, + &sora_creds_manager, + ).await; + + match result { + Err(err) => { + error!("Error checking login window: {:?}", err); + } + Ok(false) => {} // Continue iteration and try again... + Ok(true) => { + info!("Successfully saved cookies from login window. Closing."); + if let Err(err) = login_webview_window.close() { + error!("Error closing login window: {:?}", err); + } + return; + } + } + + tokio::time::sleep(std::time::Duration::from_millis(2_000)).await; + } +} + +/// Returns true if we can exit. +async fn check_login_window( + webview_window: &WebviewWindow, + app_data_root: &AppDataRoot, + sora_credential_manager: &SoraCredentialManager, +) -> AnyhowResult { + + /* Login flow looks like this: + + 1. Start: https://sora.chatgpt.com/ + 2. Login Start: https://chatgpt.com/auth/login?next=%2Fsora%2F [this is where we start] + 3. Login Continue: https://auth.openai.com/log-in + 4. SSO / Google Login (etc): https://accounts.google.com/v3/signin/challenge/pwd?[query] + 5. Done / Landing: https://sora.chatgpt.com/explore + */ + + /* + - Do not use credential manager until the end (we don't load old cookies!) + - Check if we're on the correct domain, if not exit? (or inverse, that we're not in login flow) + */ + + let hostname = get_hostname(webview_window)?; + + let mut maybe_at_destination = false; + + match hostname.as_str() { + "sora.com" | // Old destination + "sora.chatgpt.com" // New destination + => { + maybe_at_destination = true; + } + // chatgpt.com/auth is also an auth domain + "auth.openai.com" | + "accounts.google.com" | + "accounts.youtube.com" | + "login.live.com" | + "appleid.apple.com" + => { + // NB: We're in auth flow. + info!("Sora login webview is in auth flow; hostname `{}`.", hostname); + return Ok(false) + } + _ => {}, // We just don't know... + } + + let webview_cookies = extract_sora_webview_cookies(webview_window)?.trim().to_string(); + + let session_cookie_presence = has_session_cookie(&webview_cookies) + .unwrap_or_else(|err| { + error!("Failed to check for session cookie: {:?}", err); + SessionCookiePresence::MaybePresent + }); + + info!("Sora login webview is at hostname `{}`; cookie status: {:?}.", hostname, session_cookie_presence); + + match session_cookie_presence { + SessionCookiePresence::Absent => { + info!("Session cookies are absent."); + return Ok(false); + } + _ => {}, + } + + let mut new_credentials = + SoraCredentialSet::initialize_with_just_cookies_str(&webview_cookies); + + let _upgraded = maybe_upgrade_or_renew_session(&mut new_credentials).await?; + + // TODO(bt): Race conditions ahead. + + sora_credential_manager.clear_credentials()?; + + let creds_dir = app_data_root.credentials_dir(); + + let _r = fs::remove_file(creds_dir.get_sora_cookie_file_path()); + let _r = fs::remove_file(creds_dir.get_sora_bearer_token_file_path()); + let _r = fs::remove_file(creds_dir.get_sora_sentinel_file_path()); + + sora_credential_manager.set_credentials(&new_credentials)?; + sora_credential_manager.persist_all_to_disk()?; + + Ok(true) +} + +fn get_hostname(webview: &WebviewWindow) -> AnyhowResult { + let url = webview.url()?; + let url_hostname= url.host() + .ok_or(anyhow!("no host in url"))? + .to_string(); + Ok(url_hostname) +} diff --git a/crates/lib/api_clients/openai_sora_client/src/recipes/maybe_upgrade_or_renew_session.rs b/crates/lib/api_clients/openai_sora_client/src/recipes/maybe_upgrade_or_renew_session.rs index 98b2f284f0..ea9fec866e 100644 --- a/crates/lib/api_clients/openai_sora_client/src/recipes/maybe_upgrade_or_renew_session.rs +++ b/crates/lib/api_clients/openai_sora_client/src/recipes/maybe_upgrade_or_renew_session.rs @@ -7,13 +7,14 @@ use chrono::{DateTime, TimeDelta, Utc}; use errors::AnyhowResult; use log::info; use std::ops::Sub; +use crate::sora_error::SoraError; const EXPIRATION_DEADLINE : TimeDelta = TimeDelta::hours(12); /// Call this at session startup to generate the tokens for the first time. /// Call this periodically to refresh the JWT as it nears its expiration date. /// This will not refresh the sentinel token once it's been generated - we can use other flows to accomplish that. -pub async fn maybe_upgrade_or_renew_session(sora_credentials: &mut SoraCredentialSet) -> AnyhowResult { +pub async fn maybe_upgrade_or_renew_session(sora_credentials: &mut SoraCredentialSet) -> Result { let mut refresh_jwt = !sora_credentials.jwt_bearer_token.is_some(); let refresh_sentinel = !sora_credentials.sora_sentinel.is_some(); diff --git a/crates/lib/api_clients/openai_sora_client/src/requests/bearer/generate.rs b/crates/lib/api_clients/openai_sora_client/src/requests/bearer/generate.rs index 81a9005f87..1db22e7bb8 100644 --- a/crates/lib/api_clients/openai_sora_client/src/requests/bearer/generate.rs +++ b/crates/lib/api_clients/openai_sora_client/src/requests/bearer/generate.rs @@ -1,69 +1,119 @@ use crate::sora_error::SoraError; use crate::utils::classify_general_http_error::classify_general_http_error; use errors::AnyhowResult; -use log::error; +use log::{debug, error, info}; use reqwest::Client; use serde_derive::Deserialize; -const SORA_BEARER_GENERATE_URL: &str = "https://sora.com/api/auth/session"; +const SORA_BEARER_GENERATE_URL: &str = "https://sora.chatgpt.com/api/auth/session"; #[derive(Debug, Deserialize)] pub struct SoraAuthResponse { - pub user: SoraUser, - pub expires: String, - #[serde(rename = "accessToken")] - pub access_token: String, - #[serde(rename = "internalApiBase")] - pub internal_api_base: Option, + pub user: SoraUser, + pub expires: String, + #[serde(rename = "accessToken")] + pub access_token: String, + #[serde(rename = "internalApiBase")] + pub internal_api_base: Option, } #[derive(Debug, Deserialize)] pub struct SoraUser { - pub id: String, - pub name: Option, - pub email: Option, - pub image: Option, - pub picture: Option, - pub provider: Option, - #[serde(rename = "lastAuthorizationCheck")] - pub last_authorization_check: Option, + pub id: String, + pub name: Option, + pub email: Option, + pub image: Option, + pub picture: Option, + pub provider: Option, + #[serde(rename = "lastAuthorizationCheck")] + pub last_authorization_check: Option, } pub async fn generate_bearer_with_cookie(cookie: &str) -> Result { - let client = Client::builder() - .gzip(true) - .build()?; + let client = Client::builder() + .gzip(true) + .build()?; - let response = client.get(SORA_BEARER_GENERATE_URL) - .header("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:137.0) Gecko/20100101 Firefox/137.0") - .header("Accept", "*/*") - .header("Accept-Encoding", "gzip, deflate, br") - .header("Accept-Language", "en-US,en;q=0.5") - .header("Cookie", cookie) - .send() - .await?; + let response = client.get(SORA_BEARER_GENERATE_URL) + .header("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:137.0) Gecko/20100101 Firefox/137.0") + .header("Accept", "*/*") + .header("Accept-Encoding", "gzip, deflate, br") + .header("Accept-Language", "en-US,en;q=0.5") + .header("Cookie", cookie) + .send() + .await?; - if !response.status().is_success() { - error!("Failed to generate bearer token: {}", response.status()); - let error = classify_general_http_error(response).await; - return Err(error); - } - - let response_body = &response.text().await?; - let auth_response: SoraAuthResponse = serde_json::from_str(&response_body)?; - Ok(auth_response.access_token) + if !response.status().is_success() { + error!("Failed to generate bearer token: {}", response.status()); + let error = classify_general_http_error(response).await; + return Err(error); + } + + debug!("Bearer token generation response was 200."); + + let response_body = response.text().await?; + + debug!("Auth Response: {}", response_body); + + if &response_body == "null" { + error!("Failed to generate bearer token. Response was `null`."); + return Err(SoraError::FailedToGenerateBearer) + } + + let auth_response: SoraAuthResponse = serde_json::from_str(&response_body)?; + + Ok(auth_response.access_token) } #[cfg(test)] mod tests { - use super::*; + use super::*; - #[tokio::test] - #[ignore] // Don't run in CI. Requires valid cookie - async fn test_generate_bearer_with_cookie() { - let cookie = "cf_clearance=nelyAZB.T1J.3ToyOuc_NozgPVqEmOoZXZgVqICRVLQ-1745296855-1.2.1.1-R..9Ca0ZCZjSoqDaWLD62phe1hUu.43nPpJV2zqxDibkFPCLJ2R2Am6ygUqJTB1_b86ctFGktBZ4YIPrHHow3jqPk5Mfu_VubL2A5r0R6R_1ILrdvY168CLSpAFRsJKIupMUPBFDePmQ738P7afr8W0YK6dO_m0qjYDYquZyCu5dC3u9CRUGaM8SLDZTTZkxoLzuxDzB1Skuns5rqdjTft4DoNUI_7EJvsYxKoklWylvL2FqcETULoyVq5FyElbZenz8VblXU4HLgspRTz3q0_SNh4_LAIxHygfXaCOBmmii1KY6mFHE83z4JYWgx7MM9jqxKKz1pHgVd8JR72aYgTo7WwE6ZpKmvuxhTtgobsA; oai-did=8ae351f6-bc99-45c9-9fff-bb8dd6eb128b; __Secure-authjs.session-token=eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIiwia2lkIjoiYWNBYVdTWHBfaDZnNkxtUEs1S0RvTjR3NHFxTEsxVlpSTHA4VWdiaFdWNjctMG5pbDdVVjBCN2dSUU9Bem84ZXBjMVFjb1ozNGxwa0c1OEUyU2Q4cUEifQ..n6nMP5MA40XKktVazIps6w.tsgjkdkBaHt2jpO7GjIBzgammeanUH5W-3SQMPUDIG_cMrGcgmW_uaMwQY-LJ7qfWGfSqwZyGND6LQD_e-8vYdWbhE_gl1O8AQVJaLfCxJbpauCaXQTsSD0PD_XzGYsW0GZjWIbb-J8YFc-XKi8Y2AhjPhf2El5LJrvkQ4MbtlIJAhDOBNAp7UiBR4SWvWgtD60WqqnW3BhUjqXSigqhopUIkcsXAx_NHkfQpwFyJfkI8WK7JJoW4SbD6hdOX0BCKPjXKn7bTpYG---QkfjJ5f5wsRl0QYRVIQ4lzz4iak0A7oyV5jyX_aXYzK4iroVtEjUBAt77wTDOZ8ID3idkPOWQHPWnaugDzX9ZxZ8sWOxsttqbIMWWpEIm9G9ODc3-F29XHPLzciiLuRlRp2tn2ky12vXBdpiFINL7i5-OEryQ1xs1yEsSnAkzrGcXboXDgPSh-wPpmOhWLwqsydmflSahxQH-k0fUQAnjCq2EocdXnfIL84kqwnOxD36YGsKcX0DOir_3gL2PnAcE25Dql1OTwo9SBSLEf2c8dejDVfFT63UHmCkEqvHgdtTnZRApeqJj57A_2aFvy4d8mhflSaSD5qYmYUIdRo5I-uJumWOuHeDOwXOrlnCzxpgvcl1c5ng560I_oYgNUAhIGUnL5ezMQ3MD6jKwVqk-0kKZUPr2ebE1_w7jh4Rau6esLYiMcJI-Jfm_0TfSxlAXMKC3CKIII50PcXXOrvxy0yiBU_DwU79L6fYPAq7NFJ26P6821iOQGP-0mkaHPZHxEJGaPVKKjg2Y9ZQJ9u1gnm0GWwssY4uhFet6CxURnpqzIXKf78kevXOwkEsgFa-2hhMnr_OWBzKSH4UfVIymulxqoAoOh7Em51NUF-HJ0I0rr_3m7wBlhNvVVnmaxSYkuArZoboixX9ka-JVDopTPXUvoEykm45LRRvPdGiPB5hvCrglzNrp_vep4n-YbcLp4Uzn9ljcMjhSFZfXAzqquaiO7H5ys50_-mBquTq0Hoag2pHbVr5uJL8I1V-b8F1XWOnRtRIlj5pHoLX6KyxlT7pNTTSRgMo9PwAWxmLXpRba1h4ceKQTDputfrD1-itFJfZ3FZgkAXVQGrYyXEQIj79ERqq9xw18qGoBi9-GQkqBmCg1TxeS35SBnc62L7gh-UsZ_LSchizC4l4KODSeKdyNGHRONzK0cNHAGWqAJpLu5vBh_VxiljeaQxc4-p0SRiMXPYQf06uWDaRPdglCcavnlPKOcbL1GLku8CzMMCTAeqN3TJBV8BOHXsW81jw73oaBv18peTpgaJS6-Og9jMQVDb8pwlCi7SZnf1uafeGnBb6jJN4qCBN5EjZcoQikHxv0SOYmjs2XN7xE7EyczkovGcU2K3v03cnbbe01U_1mBAXgiJrW8gb4u3x8kngtphuWjEbStqHivLCtP5dyD22LTEMUE4ixvDmKwuLoB2NukM4QcB7PwrMnjI30SfCx0l5OVToz-TmLkLa-Ous-2cckw-D5pXD4RsgLv5NsxGQ9rtaL3qWeA8KnOByvYH1thLFtBUw6rjHtwLnQUGPomJkXIV8OuEuIWWnbsD1jW7hIpbYynhizTyrLChTnxuD8Witu01NtRa0dqSEhy3H5Y0qDXqFhaziydmW_kIdDxQBCWvPBNVBMAlXldqXgazM05eK3DFP86Div8b8k0pjXNoEtmEcwGLR06taiwQ3Fjbyc3y4HSLnQ-_yYz2jKLbDzpVJdaS6tlWT9rOrgniCkfvxJ5LE6KMF_2i6L_yFprOQ1DCYniM39CFehsD-7XVpS2Ui9t-Kx1g7eebK229kdQzB6Dm_HBEHTOxyXMiR4C0n24WYxEHukfCSiAvChiXHhfHOldmzusKY0r-MB_knKgM69elcXN_VAiukuUo1EdrsaqBprHqwmbkEWYfg-TmYefXJnf-58Xtx0lC2T7Uq8JJDdQL4f4PTnuyU_JX5NOwg9CwDUZoPE4gvYTicshEKxw4TABawLB6LXzr5vxjr1PK9lP1H7iwSyutBUGHEyK_R7fM0OzFQSfMEENwKtaGmGT5GnMTkvF4iGTyEtmZS1RhWqYyIagSFkf66ZRIwt5uohQJhZ8kpU_EOfihCkPC6c8he18W2TPtvGWxe0ljNfykMl-w4564IuHIYO3sESs9RzOdHkul9YqpjZC1C831zzEyM3vgM3hYWKo2VmovC5uMYZO0roqQyuy9RNbGPbXnbzMCE5tTPNeHMd1FVYNGy76t-BLj69MfJN0C-U9RgaLRe2Nz34EZBBfK3v9NNCLYhqm04WhM7XwX3CMrG9_j8mlMKpb_MxYqvQUodtttrBoe3bS4WId7OSz8k3KiswcZD86yopGq5X_a910-C3VzVfxo9jCLlRVP4moY2iohjc9asuQrKjqkUgEXp0bLy-gBB67UxV69rXMBEtz2pLYqfNKEUFFL7cVF1d407QUuSRpANitXUFbmtWo5M-0mJQM7LA0ktMDZUl3_-hPxNHFt9Sba4xDEiSyjPdMZDd--iNpCkJAGvManh-cNYI7HIBQM3kLE4GwD3ihStlW3IT5MfITEXfaaz0vWSvYdMXXmcVjVzgVn5JchynFilRsx7QK4JT7jPb_LQ1vAENBCfIuEERty8dgrKOvS4k21Br6IXckd9hl6l_5GoZUpbQzEY4BWGA8Ly8xKGbwwF4L-Mncc4TQt-2y09qYqU0TMTTXKBGgFfjVn4r0yIvryieCRAHSWuethGyYVX_Qhk_nZB3jqFkp42vMc1mkTjBCR-jlUCJWMbMcmL0tW2_SXDOk5JrACsq-T1djWeUCPgGYXCqYIygtSEE0f025feOpQJAynV78UM2w6fbgGBvsODpVr8Q2hjiI9Ydwh6MIJW1owo1QDDnuEOvTmdwdsQT9wX1H-SyN0cusSITAzgNMEp-NCj8NDW60kqTCbi7kMLF4OtPOK8THCBBkZ5c_SasAvSIxXo36fnMEKGPOLZs9GzLKMtKupym5PZ4H6H2zl_Y6iOIBniyQ3HBs5edLBBGQ3qqeeI_j7Xpwsr06cTCuQGYFAsNhu9dSs3LQTKlVfVeTiqBjRkxAiaQJgPUvFpOCvhIe2t-5n-MNj4.0dhenwDdC-iJ2W4uNnbx9YQcfEgnYSJzo1vXNawiifA; __Host-authjs.csrf-token=61aa1209933a1c967b7af6a787481f4e249a2d6b1d535924540addd4bd4ba196%7C510cca933fb90ce231290471f3885cdee2fdaf4f8f6994d96cfef9c63227e5f7; __Secure-authjs.callback-url=https%3A%2F%2Fsora.com; _cfuvid=XjJAS2zDerYymewq8Gl3e572OMiJ3cyyMqsmFiuGMn4-1745275079858-0.0.1.1-604800000; __cf_bm=hVGZ2X.67OhOZrqzUiW9Xg4IYGdCnss73d7GDU75YHk-1745296490-1.0.1.1-ppPUYZX7zAWYWXknUVzYPLjI7HDdBfmQz9DDMT5pMUNvqKBMhbUrJCp30PzxmBjMYwQL3injMe2IammgQNE5cj7JO7o6Alljt.MN1Pp8s8k; __cflb=0H28vtyuPgbsDEUozcoHSWoAnvpSqAh9friCA2BEchB; _dd_s=rum=2&id=b52d0910-2332-43c2-9fa0-da1c33d06227&created=1745296855240&expire=1745297781201"; - let result = generate_bearer_with_cookie(cookie).await; - assert!(result.is_ok()); - println!("Bearer token: {}", result.unwrap()); - } + // NB: Logged out 2025-05-06 + const LOGGED_OUT_COOKIE :&str = "__Host-next-auth.csrf-token=ef2192abb44aad120d2bd0c67ffa156661b3a971274bebd5822bdfb45d414c38%7C05f5966f7a8c00637388b2c135eded2502ff7af04f986513354123f2fac31a24; __Secure-next-auth.callback-url=https%3A%2F%2Fsora.com; _cfuvid=oD_JCwm5zaQGmsQIy4XjzEoGM276yori_BR0EOBEEFc-1746571084534-0.0.1.1-604800000; cf_clearance=jHTsbECz4hIp7a7acOilpM5iKqbRCOcCMIiy664dbPo-1746580754-1.2.1.1-xNvHlmDlSKGCbed79EyntnpCU7U.dpI9qyVYYEW0HuXT5YKQoWEepQVbB8wMEWB0MtXemxts5SfVHYhY67uESdyN7yIrEP2LPiQ0L1tTdsjFoOvsJGURGdqRAiUF8sjqCdJ1Gfx8FpGfbmJUUYh1I.wV3c5e4zcw2M7Xs2iW6wSA4kvmuwZDXj7H4oPgInwhp_XVXa.jKPHPnNIMRoGR7dirbYhY6wcywUk53Uw4j418ml2Bb_93LWlhzVO6WVPfjnKvWc7vxw1_cutXLy.IqClatx17HXzbCISOVgOcDeGITvvVgbl5OTmXbBlGXtmaJlxtfjsFbxHJFgX93zFYq7pt9znzbM4DPNSO57A0eP0; oai-did=feeb4ee9-615d-451b-91d6-35c9b9ca9826; oai-sc=0gAAAAABoGrUiAr4TfGAcKlXSjhT1h5_5UYzixku3R9aaTwvYNKk4lKP_Yk1OzPWjJL0J1CwOzC2Jb17t3aRDM3z8kxH1WOAZhIdnTU_u2NBgkfQNUQhzMBR_Qb5xHkfJ4PFUpwO9n7YNDtSoN8QVP7U2hbi0GJfMA06k0kcaceUmTvbEK6Uceq5R13QVhTPfZC9JdEjcgXorTcq7iQW9mQPJYfVhUwKgWbtdXgwQROAM51Zq4ETutVYZklPhaXy0hHs1LZdM0GRd; _cfuvid=tYpYCaVU3akRC7xqJ9HhgKb45yUMSDkSkk6zoY7wxbA-1746571091829-0.0.1.1-604800000; cf_clearance=oAW.3k9.ail0y7zwjDtsUlJIC0OAd5NllKMUq7r6T7o-1746571092-1.2.1.1-N0zV9i8bumZPfGDsB3TBpE5dIoU51SUzUvjsycWn8GmGHo6v_M6EeuCIEg9OIfAJEeJWNvE6mf6v0wOZ4x2FrUPBEc06NfnQgVb9fkIE5vzUsukkPX7jK1pigl7vtSiqDmFKyvcg4ojUUUEHuHPZeXDmiDE8UdHzLPSrMaANI6V7k5cflmlhnho8vgnFGpkiHfOArlfABVTkHWmTQo1pJAuQxhdSM3hMyxpA_6VHeQC_fUFX8Rk.dSwQzx7dCV_TCY5PKbOvfOd6X93sPSaQWneGo9hrDOTlZxeVEGLfpd7WngdKPS_fbjf6nDmwHnMtmKPMZsd_BLS.b3TfkmqngKPG_oZxWxi730a5NJv7O6w; __Secure-next-auth.session-token=eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0..UtJZGQCBDAEdkNRL.h7_eoTetqxFXGt6RmB8fr5JYaJPXH6vvcSMKG05JDGq5Nb3TSNXHfOIYdY5g_E-7xUgsPJnoE9RHmUZRuS1YCA0UW23cYmwHigGU89KKcdtJqg7rpcgosIQ2UTLY7aXe1WZBR8-3ACI-auo51JzMrjD7vnDL41k6UgGr7tdGoieyVxqlB58ughkcfIyneBm9k7VdSTahgD75BQRqWEtruPoAMCir5qtbu5bnDyAiCjT4S02gK-kVfgGlP5_tUTT_VoSQ-YMGE0Mw3Iae85l4X5PjuBj_3uhRlKEL6aABfs3KKr8gvsb6xGP_cypWVaTcPMpeFidLicJV4GTbj4x5ncGdJ4E9WK98j_fQRB6tQnF8op7UBuh1zOdKUFvmrKof4DgJm4ChPeff-kR_z64jRkZfiDdu7hTwEx1VIoIcXiVjBLg4MJAG_79WhETxiz61-HMrsUpNgRD3QSH6H6j4X3HpPs3hgiBZnd0L0WDOQU_mTcKBkvpm3Uubcatv5Gss797TelMs2cM7J64DDz88tMzChyMj1mBPguONIWlrj7G5pYkC9sLY5gyydZoK4cvfcxkAVgWa-UZJAtNwpf7XdYw9FutjW0Y5hucMqch3J2O51wVx6KltKjycLXGIJfFydq5qO53Ngu02WEz2AbBfHsdBqR2S-46t16LEqjCfWzJRuUOe0V_Ao4_lqVxgjc4Nd97pjaJhuHIFjWVY8GLmh5rqVUDAiDexEax7TLF8Htp6BBBdOt5-j772SbrvjCD8u06OyksGLIXElY66k4RMGsyzJLDxYoNa-c7E_38WtQJsPU2QH8kYRkwP8H_QYdYnOpukHVY-2Uv5AhAwaP2q9zqYAwPGyJ39mWQq0NrZ_rGqotRz9n_3W37Sbu1eX_5Ae0Vn33BuKG3fzxMU7akSVmA7_4DE_4dQC8PpmQ9C4UoLYhxg0aKCs4KVOWjpSTkLRoehiB-15R-SNHwNGKUMGoFg9B741jb8SHjgYsAVJnvj8VPPDhFm4r4rSqv-IIN--iebrj3ExC6YqBJmwWp-ZAvioSElJfvqZasoYkRKm0CjLoXuVawCcHVNngK0U0PjEFMKkveD9E4-MW2KO2uhud2OShZvXs4ge1Fvmk1yFwttmVM1f7MvSEoAolivtFj623iqlT-Sw0zTSg1Dw4-fDIfPk24r_eoaZvooP_r1sJ89NDDmPJDmZm_oavGdkP_2CCEEPaVNq8m72Mu2K2Le1geyvm8vepT9OR_5q3MDypRqs4KLruzrSqhfDr80Z0FMJHQGv-uyugY9yroX8GFtTBiXdIbRBiGquLgkOvjuw92JDTCT3HtMm5liFR4tGXbcsEKqp_HtK4cd1krKIphq71LKEhklkSXC-JvNzWS4qYEANdBrNfEaSa2e8-wtzW2qIuMfOZT4Cocv1H49cW7SIB1_5-DG3dy--1s0Wf6JniMn0Cprq9h01A1hraFxpSax3gAeOehYCLk3soPu1r0VlbXzdxTFg0S9AxeuXwj-IKL39C9gpQSkkeIsVeSL3VFlzYlpdupZcNbFLlp8JMoIlOnNaABBFPji12ln9dkeyEEkFq0JL4UyZLZ1SegADd0l7s_cn4dtYYfuIuAAJFC16vHDJ7UMVfVMhBeKA96U1w4ak-uKs8wk-l5EhGragNpPogCh_ccHyQf9zV3AnHbJyGsiVc1ztjNu6mduFB41FO3kQDBH2uoa2PgdgWtaO-zLSrTDns3aGffp6VWYddxuPvA3T5VQDVjVLz6eJ2_d2N0B9sPmevKkIYEaI1-OClY_StB4TG3VdNUsfcaqymweg43scS4DZBa9DPGz7ZmC-HG_z1oVfaoZGfa5-XD-u0Oyuu91xYM-QOuccvsHco2GWUTsf52qH51mLvkpc5AkaO9tUzhN0rQLabOGSUBjJZyxIFrRsLLlDWDwPSr3k7BbB1fBUvsUiPShqI_CWKJjfqsEiHjrt3MbfcbLc12XqYuvrCHwtpi0JRP67TwnUKADP7SEP0FEdXz34prhXpJSvK49hmpUkKYCbsdntJFz4J6vB7BKLcb9B6MsWNH5N14yhn35UJZc7_IToLLnjG93KcOhSWNvXtP9vTXRVmxIJtYfnZOOI6wsTxZ3WQcAypxYA1PLzJzlVrZC2TkT9Jhcph_Y2uHCKm0CFka2nr8DHF2VyrEo1n4x_bjXve_BZigWgGgFE0Vn4NEUdLislf0jTXcVhTwVCCxtYGPkKE3NTpu-Djl3HjPUGRYFC3dXa10HWJiazfOITTUO85LEpVRb9j2lhQaX9dKe6toPipSpGwzsdkOaw1liuADV9J8ssMSkhs0W_CLKd2HaJioPKUVUJ0v2B5Z39DiLyJ_NDx6vZoGzO8bRUp2Atm_z3ccrsaKuO-brZBmG_gApoXLQXdBPwovyS36TXx0fcdqcFlH5yqADtK1Yn9Zjkp5gIzbPz-k8nUtBtu4_JOayzFJpWGheTrkoj-nBIvuOfQB1yJan9BVZPF-KzALb1JdTC28k0kK60Exf7r2lulwA_3_-Ydg8inZ1HHGUYouAb6Na1r4Pk70g4waTzLItIbDPOeATPfvV9xRJ7LT8Yq-pbfIg1rHKRjrCcQ8IeonjziGxfNnFiPy4ANLtg2F4Z_dwi0BPE9MFo0yO7gKELGGpGoOB4rcd8mCK_wnm6MyBbdqC5oftova8ymTqjqNEH24L9DwCk8F_jx8DJhPvgrOnMU0Gc691Ym5uQeF_aMwtn6AaauBVr2Qf8VBE89wz1-sxk2vI4nJQasW9Q8wanqw_pSxwMoc1XzG8MY2sNRRREt1Ex1nmR4HjVdDSYJmbnqYgREPiJ6zUTcJkNJDPQjo70R1TEVjV8v0a97Ca_Fijp8CG2ZcD4VIAM8kOF90RJlUeG74pCIscB6AGi7tmgcO9a5VdHfeeqomRlFKVN9Ipi255SZHpI8JiUaUGwpP71RE-JHo4baZ-g7Z6i_UJsh6-76LKwL3HWblb2shAazc5cqC-ySHohWfY6_zeFD02zeoVN5vr-SaoQjzywVfef9V4AS5cs8BqlIJYP6Lrzh2AC9GwtSpEw22NVT6rR4VAIbER1HuGbwnNmnCDwHaOoyzFwkVGU8uohlOG8QYPUnLLfl8LzKv2O6SLlwcdZA1Fxiftv_ziMA4qo9Q3NQ-bblDycx-oKMWzSAvbx4LrjYBOxVn-XErq7B7K_i2kkK1C9Ttf63zbemz0aPcdhXWiEnKArv-q3kj5y21FKlmI4TwZuTKV5xlu-mOfXXWmTZZysOT6my6xZXsNOa1N-LiigdU_NV6gYmzV1Pvk1dM4lyFjr2SM8jB6hA_Je4gyhMfwqdDlFNKRJGG5SYGylYGgrqoF9Q8P5ZPxy-rzmliE5nIYICZDRw4GixtjlXElu1-voa1qlO-jB64-Q1pjfpY6FYNBdtVjFYUiMoSdaH2qscS0x9y_TXH-qvHmrn0Rbr6HTcaUQWuFwB05o-hrqpqIXnhsYaFRFo9mIKqerSdYcXWV7vjt66yOBOJXGGMdeUKEiOhoEBRzMONTaGzyqnyREpKAGKGnnAoBxiDJAPJiU7Un0A.N3e3RwsHSx7RIyRpkArjhg; __cf_bm=.tgjY3xPUqb8neZ82V8alW6BUjaUjhl9AKSAVrWvCok-1746580754-1.0.1.1-O_ArZyOJreXvGtcaHIt1cvKmgvmfWHl09pqbT9ThwMTzN9eioOcPFJty1P.ubiZc.mnPMaJD3dRXzkhbHNYVBPuRgzmujyF5kvUs0hE6LJQ; __cflb=0H28vBjUqcdJN5F5i8D3tzftjvH7q1fnq8Q2Zh98qhf; _dd_s=rum=2&id=a8178f5f-ae08-4826-bb1d-587e16634b15&created=1746580754683&expire=1746581708921; __cf_bm=.17Mcz2Tr68P5cveYebWZwneOqCeN0Mu3Ad_vzuw5hk-1746580754-1.0.1.1-go.48gAjI1dOIK3MfpLAfmz43EA.5_Wh.fxObP8M58fbQPeP_72H0dqO20Ei0At3Z5RqbNqS8GED9zvJ1utsNqmWHJLtgG58fLvIwcXkgTI"; + + const OLD_COOKIE : &str = "__Host-authjs.csrf-token=aae0efad45e177963b2ec92343e42fb5edd2e61600c8d5f986caa1aae5a18a45%7C88e6407c124636cc2a5de4fc557c6fbf4cb77e6876cdc798427fe739a689570b; __Secure-authjs.callback-url=https%3A%2F%2Fsora.com%2F; __cf_bm=URhSdu4GaYtMgZXeQSJUHHPrp7ncob5oLSBZVYJqUBc-1743622054-1.0.1.1-ucpMZjhlQwqfwqRcYdDCrTrfN6O7qai6OU1Fr4iBg.j6asF2FoF5Ih0f6JETLsLZEkNmugCqLVAsh7tERScqIDckcN1NT9.ARPjozqgsjw.MEe7SwynVbQCw5TCWzSIj; __cflb=0H28vtyuPgbsDEUozcoHSWoAnvpSqAh9Qx421HSZDQR; _cfuvid=3fRsqhtWJn06LJpJsT5mBtjFDIy4Hu_IkUh.xGZWqhQ-1743621009366-0.0.1.1-604800000; cf_clearance=eVjck2N7zn0.mF9XP94m.x1GyyVSYgp_F.UKgaVO20c-1743621009-1.2.1.1-ivofMJGm9YOYnPABiwCxRNOwepOeiksOUI0pBj4rGGV94RDDUE5x1b_bDSKKtkW5L7PuTxRXOK5_yjZkXfjGhMfrXDeJhiXzIJGGN3p_4OAWj2Fp0fNhvV1rtALM7gd8QgTs9iBiWwA9GmUkZIdZ9._QhWx.v7cfuVKjFv2U239z3CzjvQ7RmrTDrWS2B1RCShW_m4RD6DY4QfJZSdp0NKQqcnzrc2nzBHf19SicEo13KU7OJ0htu2MyV9XY72WFXfXy0rUJGByRgB9ebpoFUA7is7ms1.5gZmUGHJ3xfdxwhACR16r4PV4eUBmUDpLNbsw4zrc7YADDJPcujclnyM13KAo426yPsA.N1NeK5TQ; oai-did=30ce2582-2117-4941-a891-c417e3ff316d; __Secure-authjs.session-token=eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2Q0JDLUhTNTEyIiwia2lkIjoiYWNBYVdTWHBfaDZnNkxtUEs1S0RvTjR3NHFxTEsxVlpSTHA4VWdiaFdWNjctMG5pbDdVVjBCN2dSUU9Bem84ZXBjMVFjb1ozNGxwa0c1OEUyU2Q4cUEifQ..HCOVM3lniGpAz6nESjnKfA.nUde2F0QzNHHKj1pozvMKXvAd-YBSWrGW7k1TGGmyPYqDivgKxSZEcCYH2dvtPgBJ1M2dkc2RToDHdXHG_H1GL7QU-JPoRbf9EyfXfNf9KFTI8ymYT9sBvInIuyxzXCNp8fgWT96BjFjnWG-HK035XQhfsGp7ZvUlLdzTDibxo17xzALBYOKOwHGROGFXFZFxrA33m4_D14GAfxQ_YikvKHbnwEd6I2LeJhgrlrftn7XWTOvyzQwpoWQjSmVtvHW6el_wrvUG5ixZR_0IhMOK2lstQLQrqAv9VhC7W4v9OybqfHaSWZ_uwjjpkcP9QrOyPAeflcNKvIz_DtD_2VkoZM-ik82EZvycueEUZG0bBr6SX0T3AU71vQHQdqVrS7vL8As1rkJPAyBBLws5Os6xelij8BaG120lUmVeg0mRLyHWiD_hJk6cfKsvvpcV9wsqwaWs1PDB4HIIhYM5JMGg-LjtbMY6esFYIGibwGaQa1Ngcvp4JPW4vGIU1qzZnaYSPSdnDMPwDSYS4bZwWnYYi_7WptDbUTJ_wzG9jnUQe6fdUTZjFYBAOWjZo6ip3GeuSEZPEimlYWtCtwIsx42AkMneu7sIv2n40UnK4L5cPF-Hss9jvgm7qeuk6A9_kRS1x7owJ6H-byZiD3mIQ4UtTpOHHk97NK4lSpDyvAFIzh54xQzQiSXHcsRBC0VQ6UnLYm3CUv5d_ozv_A8o7klMUec7bG9LNIavqhEdvwxm-RuZ6VmiiDaOFQVjD4Nyid0WVjUOGjQqNJE_5nFhujw-Ob_HSGYZW0Zp1tZ6qlFV1QR3xUyo8jg7KJpng-qcNfNDTBr7AzwABZ7-3Iknw-dJIN0NaHhLYBBN36ZTNjwnySOpKsHssFWutYuqoUGiyW19deoEIBqfJ_d9SnAefTSbhKSsuxolvtdGO7wqFeq7pZdljmRnzC0dpoOLe8DPSYprnL84mDmZnCvanVObSRJW7R3F0Pf5hsWom5J8udvK8dNJhPwtrmPngnZv6Bd0-pEJhQw47qcX6j6emSTLEkAivVn2HknTMpwAoyMpairnOY4EZ3T9ETfGagvtRU7RcdMMwWH1NZ853lzkulHtMGF_ut2r83yKNi0M-ogcQVZTEU9F8QCkEWXHXO-x8aMMlG-axzkkZmpmrT9X_1YRJfQRQA18UyxdSIOPM1h3Ol6qMddA5SHF26p1u4BxE82-ZD1kDlilGNzr3El1ds4jQryQsWWBV8b6wKz7332CSTE3lQJLcoGTvLhBjFgSQv4KgT6bVEFzzBXgP8B7YNzkXTZL-wTHKi9S8SjkTDxASLNmjigCSlf15O8hEM9VlGkO-vF3D3E7uTl_ZSevyF5SJdjgYRL-Q3ll4lYBDQU9bi-tpsm9RgJ8CnH_jAMTiwNdzHvokqGoiNqQX6e5s2KSGMS45j41iXZ-A3MwahWyjWgzAdszv-uETxPGqndWXVoAWcj6NXQkPPufPolFkkDHJwL_Xu6Ol5AURoNE7p0qy3dCDtk621uruLOJMfV56kRR6cS5LZ02mc75Dnjv3zCao8qWt1rmPdHlAiRW7JiPbJV__Whkm1bBxJl2zkhJquKshu4ViUQCkkt0OEMnzp-ZRCSYhZczXlGGczD1iA3BzLeAx4OO0baPWwYxb_7_B0sN4hL4L2zOqBHu8aUyyV1C8xJJIVAefWxbppUy8sGPvL9mVZjDolJ8MZjzCJMM2Zr8si8TF9nfrL9AT3wlFUU04GhknBAbGppBYKnRC7-uu5Z5MLZSD19xxNCOrwcPwPw85_qCz8h40NJSlkicTqxQ7yjkV0x_qDjIUgqfUISFt1W5Dmk2J6zxWLpL2cgFRD8LzDgSnNPtvgCXia0rOTu28aIstp3QyPtIUplwgbyHZxAw6M_HBjoS6V16l0qMdSTVjhbIBMI_piKVXDrm_8Pcx0PGa2Z7NSGZITXPddQLukShgraxsSUQtOG8nLmULnTqBKVLUgK3_VYGOEEMkfixPnmiSEuWmEg66P4_2wi1MceFPlhzxmUWthRUygZ0u9iRzAVUvnsFJ5t892_4QwhYlMlGQTESkCyeHNNIUhEHWl1eGDTRsOAk0cL5EnZFAR_jZHxap37zvn4UV-M0XyKcWluYLWGl9A6mXBpFujt8x5VfWaX5MJugeOIlvBL-AwDaqhZ1ekIWJ6AWDKVCJoP-Y8tatndqaSRYxPs8KMmdEkkMtLgSkeF4GHe6KxGqCYk9gCDdoUD-9Qrte3p71lC8qtb28FlomS4cq0zTt7YYor7DSvA1RIUCcKo90s3rmfyIs7riAviDaBFMGtwKhrkMY0LR5AX0LFFyNncGHgWNhPDHl7KDcKvFfSF3Zs9VcSEdfPOZdoGV0Bm28-Md_gtNEtEDdI52-y2cQmpWcaIi0CNM-ebnNvRXbeq-KjZeCPgDwyAbqEjBbbj27o69sQcSzHF-GWw12s5Qc6gx9y-tZ97Xxc1JadcHNy2MXqFX8LcWMKdgQJJJZzch1FXqZFgqOBQbTPvMnFZZDJ-vIAHvAavn_SE-pj98FUghAeF_Mu8xBW6aBvkz1iJrQkP1RREF_oIq0hy_2fVIY1LSrlwre8lCnwgjSqgPIpwAKpl2FiG0YeU.JTV0M3rYnJJEs3ETueQc5lbJu6yfercLCVcsFpuhTQo; _dd_s=rum=2&id=e4297d4e-7494-44e7-b1bc-44c9c4472d8f&created=1743621072209&expire=1743623584230"; + + #[tokio::test] + #[ignore] // Don't run in CI. Requires valid cookie + async fn test_generate_bearer_with_cookie() { + let cookie = "oai-did=bb449d6b-e6d1-40c8-b5eb-972b110a0918; __cf_bm=JqXYK3aRe_0KZb8vsSijTm2YJvpDV33oyEQpfcKKKKI-1746570472-1.0.1.1-eMi1cn_XDBxJ2u_UqGql0BS5lC5FXwSxZCYjq2FR_AL38LBllAuKbWS93THhE5wjn3YwGhifW2fa0pAFEHIV3eZIZuH5oFSb.aWOqmtYEsw; _cfuvid=KbIJVncxt0WZo2fG_tci8JjgnxJBfcL3SxlLeP9OHE0-1746570472074-0.0.1.1-604800000; cf_clearance=mlWui.52Enn_gRDTISC4Lx8j.UcsRlKo2dfFd15bzKw-1746570472-1.2.1.1-MblVM7BHYxZoTmR15yY_dEyQhQJQgA4boTjDv25pG7MqKAOKh2QwH_8QHG2er0mTxrOfzOkKFwYpKD.DPV7fPp0fUIGVmoi.WwDVChrwdsH8kgnMP3g6w19TT5qmgHc9gMmpjsoop0mRjPbmPEY0lrKvobTvXAclTdPU87dBkQgaMQbFb5M4qDSi3XN2r.0aYhbTLdN0ypR3mx9Jl3Oil5HThUGNSy8rhqDis3TwtaoWu7GoeaXd16ubHX6vmEMnP1UZJUupE70WcfxSSGDMRcpkjePOjsHpbTuxDGziEdHHwODhQlrriXCp_LTZuCwiOPWNyiJSHEbZSFieW5s02S3J4smBp6qpSH_MMQgAh.0; __Secure-next-auth.session-token=eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0..DhbnG188knzLnuC5.yyYzvDaawyv5penH9KFwivcPeEuVX1xkaIAKGhT0aeW-e2B1lqVK8uiHmOpba6RNsBuJSrJ3zGljBrCwGeeD_vCLye53SH_1wqUHg3BrsPbGFozslPHr6HBDM2RF4fMYb-wNxuHoCb3QcRfZWGQbaxwoOlL3MlUt_ZZ90kH-_Kut3tGhWM82d7x2YSJiV0Bgci9AjkNPNqKQtod9q1_MCmW7E0Hos-09CR__sGp6GUBsdMHR64V8zDbEpg6nFU0--6p7kFoJ3FidbXGyiMlqeMi_rrr3Od03S2tbUJSTUguxA0y8SnwpPZ9oliQ1Gort-EbRRnUYG5VvyxMptiJXmhnz0AJ6Ey8d4BY9WtnMo9HcOm1wUF_7tZ14oTZVdtn6oFIQvo9xSa4aoV19XlvMCifYyYnqtOS5CYq5xzmu3BUJvfzsBIafKhAaRlQg8uyI9zaYZ3JpXx0Aa_8MGuUcOuDPksoaFRHYiy_UoPPzDDJBCGd9ymiBS9CE5TsXbBPE01IkvF32nWAaC3_HDUp_qEpksP5WNA29akLUFBjYL1WXU2aeGmDHSeDqwwI_2kzzga6TPibj1HDRXxfuJPf58cYtZ0jIjLYAA77LgQ59QGInRQRr6rN2TlbEf89Onkv0NWRBqXzZMu3qr0cj17m1Smq7HaLCgcRJSGdGR2LVKZpcZl2V0EuktLQumVgmZLSvcKqpyCk-zZkGRtLBqMpnsSGa4uC8wonoejGt8y6TQSdjPoTTWDynjEH48bFF_aXLIxqF4W_DzE5M7ymQ31PdxKQ1Rkxq9A2v_igOAAb1uN5oE8TGcFGM1aWpuqX6fEKsNMS-hUitTq6aase64uSEXUWWTMFKwSNhJCGOIo1p3zsfznjAxSYsEUYy0LiVPv7SpiDCcYsIsFxZ0QksS9i3CcuFKCqWPRNkSR_F0B2juQEiAVTHBD7ugbg0j8D2y68xGNMR-HrNBHld3k1LrA3pkz1m28dcllhL-NhZ9Izm98kvY1Sj3uGP97d9_Q7QG0ce_93xXPDSKhrV_eTV_BKkUSNN4uUBHm2kYiHK3_SOfNUlFHEGa7ANiiNhOwWGzj_lAeAbN9Ab6FEPvgyPQS0BEXDRu5tey-c2KVPZJjOWReS_ZUBXEy7fNCRAH5zsO1EeD3cjlv2MIH-Gupp4YqeZWW1QJPvFIlHqTBLvturXx9lteYxweeWH1wWedr-jbJfjp1csSF5E46WuW6MjfNPWNBYCiBg_Al3876mrAvag62bRgptUFpZ2m_VWVdENA_rhyL5ruyyIoKF6war6_y0oGlpPY_AULFeK0-thWtt2zB_kGgNt8HblwnskOvta1k1xZt9oXTMG9dlnlVLn-155VEbWnMF8wtFYcT7By_Fpu_BfCVyjdg70qk8YoibUnk0Jv7L2GHfmlPejQlhfX_xZiZ9zjC80Gs-jnQyV5xG_7IAUqYZ6BrCzGXyk0ZVLKC-gGWAN_ly6N5Rs1G-X2bEGRmE-Kv8JcvSyK3ye3T02ymOzIml0qt37RGNzOLry0cPi0GVr1boeYAJmJdBpT9w5PvuoXmnoMF5yhIVfA3jIxnNjJ7vkUaLOsKT13KOKtpEA0nuUALyJ4UmIQLZtD0j8xXgMsBolDTnt5LRbS43PrvfhtKm9l0rnBJ2Ruja-gOhUmCogwitZlfmrwJGa-2hf225nMr2L0u6biBaETWFwOQ-EEseOENEhqTbygTmdDMwl7iMbFbn4bgtj3N9tpfO-mua_e0oNWInEYvrT93qIKl7UIZegoxqWhN9cMbjsHd2I5PJF-ES96tfuVDYgoo0wQ5ZY61ZbNOh04lqy7TDYjLJ-YfXy01rU5z8X3O0sS1lg636fJbpJymYhrrCMpzfNMaw3veRdTTUzcLnLfakAlB1Rv7ouLX3XJ76cUO1PezX2ukTNDaMz5ls5b8OTFh7XnOTYT08nKHZOXj7w3_udrgPvLr7YZAf17N6BosQ6FZu-AyBtIZtZwaZR24JrBypo0Ny0pc2erMjEn35q2UgB1vgyhLADfvV2qzZwNSw2ziwFyMKdw_XE5ZgapKW-x3kEwhQJw2-B1sJcJqg4uf4GZcDUE5odvDAMtbziTX6b43VM6nVUlCTRXNznwt-NQ-kWoCSEna90AtJNgBbBzZ0BXPVNyps9B7lwNF9iQukF063pVEzwReYBjlqkSMxTSU54yN1TLWdhYqgg_4BAQsXsCdZw9PwY5n8vk3j6JI4LSiFPHUI_Lok3hNHK0gV4wdOgdoLTing4iHWRyq12ZVyS3p0PeCKNKLP1ZMePG6pW90GlBo1QdL3AbukJOITy-n102UFa1udb0GHtOfjQVzDGQ3hhUmka04H0-LexyfEi1Ee0HijvQFbMaWF4-Uzpzlu4__u0PDD9Kzxhw66NGFMaDxKxAYXiXEcsmxH_ptQPHOz_i5A5mNeKCFoeSl898T0mxPg4DRwB-R7aRzHgnUiputdJ2x5TAsQsMWB4XqfAVJqEe0ORNXvzezRPKiL1gCif1tyQj6acm1Hull78zPYvzNC76p3srFW2Ohg7W3tM0ODuLLx7E5tnmzAhnIWy_fkdcJIV8gvommNkBzaGWut5c_h_f6sP4WgIAgBi6Z60YSjMaflD6bRQumHcZl1dMi8Gv7j5BDPo7-phJIqpLc1uYzxMZ-IoAcSCSvmhh8cwsqONDj9muvflfYAISA8UL2ZVlRnHI_l6E0I8RXiXWt59u_UeAv9FdRtcmoQ85wa5axtUaJ19RkpmPygiH-A3xNB0EhwfOgI6OOrJL8L7EWDVJhViSYYCGVW5ZVOGsYyT7wKHqIxbrM18TJkJ4XJuHC9rUowmWLkq2xkbfmRpUQIMrvyB8HBaWmkTA5rZnqkYJDr4Avm4SOltFX1g4Dv-ediVZRtCMPYz37olezdDfBqPchQfYWbp4Bnd5Uy_Z6Rs3ciF2-Z0HUpF2i6FxezCIR7r60QRoWah84PS_2-_7hfhQyWUoQEuqwntI9y2jp77bWULIaU81g0r66_Q8KSSdwnxmIFk0OmhQCUN4Bsh63UDyGQ0AKII29QXY7lQU8E0kAyfyL2kTthsIdo4r6ilMQU9aZVJUC2Om7wAfQjZBYzyWNHdphyboGmSb5J2uKxPhE-whc87ZSfJ2aiQ4FiWu112SpjZSQ1pmrM7NDUtsKYZeUvZ85LoSKmgwz7oy2PaW5q786s_nAKVDAHTy5JeZbtWN3pI8CGA9kJakz1axsTBis_W8aJ3kRUyxgUFAJZ4C0HukYxzr8kvL4e5gjKF0-EvzWmN7TIWV03kzbh1-vg7X_gUsijHL-SCDaZMKqOz146zVIIpS-xNjr-7lBfS-AxciI6JPlncTgLntTxfuoP2QavSBc4PZ_Fbn_Ko1UxvQQhJgWYkoBH2hUpIvObn_BaeT2PLbVcht4GhUgjwkDxpV14u1RLDqyqOyXRyVqTVO3wDtmSefrMRWze0Q2D-XW5JUAHOjvuU-AQ4MdIV9LiVlQAOjlIx5e56mVWXshjmcCNaG1vHo62yLZj5Iz0kf7SjQZ-WZEzDbpPXUeAEsCANQDB92l-sRUVTdg.DlCfPqTSppBQoodvYnNh7w; oai-sc=0gAAAAABoGoz4VyDlu57UUFNU7plsIHZekd0cT9h36pzM5uCs1hG7R9NaGclCm7hpLmaftSFPPd8EMNOHKFfjF2XMwpdhCCtYVv8N9FQUwCoC49u7I5rS6ozwDiBur0tlCduiWNhx8N2w4onMd3F1EOJpVLheW9XNY23elJ2-3ZcX-3WFvdPIrQdpgI0PRjXuEyPNcGF3IBOsELHuy5EqCww2SibjLF1yFDtua06RUtACm4J9ma1pHGTzCqVUo91FCQi4F8ktbkVQ; __Host-next-auth.csrf-token=96efb8da024d1edf44c144394fac947b9350a05dbbe1fb7c62e72a0e1198b078%7C8cdcb6b83d18d8c4b0bf75b19e9178303fd51b89bbc543a1bbc875ce7bf1cf89; __Secure-next-auth.callback-url=https%3A%2F%2Fsora.chatgpt.com%2F; __cflb=0H28vzvP5FJafnkHxj4Jor5o3JpmAUGCcWNAeV5HsmV; _dd_s=rum=0&expire=1746571374067&logs=1&id=3d0c4c67-870a-4724-9c41-3849e3df0bbc&created=1746570472537"; + let result = generate_bearer_with_cookie(cookie).await; + + assert!(result.is_ok()); + println!("Bearer token: {}", result.unwrap()); + } + + #[tokio::test] + #[ignore] // Don't run in CI. Requires valid cookie + async fn logged_out_cookie_failure() { + let result = generate_bearer_with_cookie(LOGGED_OUT_COOKIE).await; + + println!("Results: {:?}", result); + + assert!(result.is_ok()); + println!("Bearer token: {}", result.unwrap()); + } + + #[tokio::test] + #[ignore] // Don't run in CI. Requires valid cookie + async fn old_cookie_failure() { + let result = generate_bearer_with_cookie(LOGGED_OUT_COOKIE).await; + + println!("Results: {:?}", result); + + assert!(result.is_ok()); + println!("Bearer token: {}", result.unwrap()); + } + + #[tokio::test] + #[ignore] // Don't run in CI. Requires valid cookie + async fn invalid_cookie_failure() { + let result = generate_bearer_with_cookie("").await; + + println!("Results: {:?}", result); + + assert!(result.is_ok()); + println!("Bearer token: {}", result.unwrap()); + } } diff --git a/crates/lib/api_clients/openai_sora_client/src/requests/list_media/list_media.rs b/crates/lib/api_clients/openai_sora_client/src/requests/list_media/list_media.rs new file mode 100644 index 0000000000..00e490ff95 --- /dev/null +++ b/crates/lib/api_clients/openai_sora_client/src/requests/list_media/list_media.rs @@ -0,0 +1,102 @@ +use crate::creds::sora_credential_set::SoraCredentialSet; +use crate::requests::upload::upload_media_http_request::SoraMediaUploadResponse; +use crate::sora_error::SoraError; +use crate::utils::classify_general_http_error::classify_general_http_error; +use log::{error, info}; +use once_cell::sync::Lazy; +use reqwest::{Client, Url}; +use serde_derive::Deserialize; + +const SORA_MEDIA_LIST_URL: &str = "https://sora.chatgpt.com/backend/video_gen?limit=50"; + +#[derive(Debug, Deserialize)] +pub struct ListMediaResponse { + pub task_responses: Vec, + pub last_id: Option, + pub has_more: Option, +} + +#[derive(Debug, Deserialize)] +pub struct TaskResponse { + /// Task id (eg. task_{foo}) + pub id: String, + pub title: String, + pub user: String, + pub created_at: String, + pub status: String, + /// Text prompt that generated the image + pub prompt: String, + pub r#type: String, + pub height: u32, + pub width: u32, + pub operation: String, + + // There are many more fields [...] +} + +/// Note: this request only requires a valid bearer token, and it doesn't require a cookie payload at all! +pub async fn list_media(credentials: &SoraCredentialSet) -> Result { + let auth_header = credentials.jwt_bearer_token + .as_ref() + .map(|bearer| bearer.to_authorization_header_value()) + .ok_or_else(|| SoraError::NoBearerTokenAvailable)?; + + let cookie = credentials.cookies.to_string(); + + let client = Client::builder() + .gzip(true) + .build()?; + + let response = client.get(SORA_MEDIA_LIST_URL) + .header("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:137.0) Gecko/20100101 Firefox/137.0") + .header("Accept", "*/*") + .header("Accept-Encoding", "gzip, deflate, br") + .header("Accept-Language", "en-US,en;q=0.5") + .header("Cookie", &cookie) + .header("Authorization", &auth_header) + .send() + .await?; + + if !response.status().is_success() { + error!("Failed to fetch media list: {}", response.status()); + let error = classify_general_http_error(response).await; + return Err(error); + } + + info!("Successfully generated bearer token."); + + // Parse response + let typed_response = response.json::().await?; + Ok(typed_response) +} + + +#[cfg(test)] +mod tests { + use super::*; + use crate::creds::sora_jwt_bearer_token::SoraJwtBearerToken; + use errors::AnyhowResult; + + #[tokio::test] + #[ignore] // Don't run in CI. Requires valid cookie + async fn test_list_media() -> AnyhowResult<()> { + //let cookie = "__Host-next-auth.csrf-token=ef2192abb44aad120d2bd0c67ffa156661b3a971274bebd5822bdfb45d414c38%7C05f5966f7a8c00637388b2c135eded2502ff7af04f986513354123f2fac31a24; __Secure-next-auth.callback-url=https%3A%2F%2Fsora.com; _cfuvid=oD_JCwm5zaQGmsQIy4XjzEoGM276yori_BR0EOBEEFc-1746571084534-0.0.1.1-604800000; cf_clearance=jHTsbECz4hIp7a7acOilpM5iKqbRCOcCMIiy664dbPo-1746580754-1.2.1.1-xNvHlmDlSKGCbed79EyntnpCU7U.dpI9qyVYYEW0HuXT5YKQoWEepQVbB8wMEWB0MtXemxts5SfVHYhY67uESdyN7yIrEP2LPiQ0L1tTdsjFoOvsJGURGdqRAiUF8sjqCdJ1Gfx8FpGfbmJUUYh1I.wV3c5e4zcw2M7Xs2iW6wSA4kvmuwZDXj7H4oPgInwhp_XVXa.jKPHPnNIMRoGR7dirbYhY6wcywUk53Uw4j418ml2Bb_93LWlhzVO6WVPfjnKvWc7vxw1_cutXLy.IqClatx17HXzbCISOVgOcDeGITvvVgbl5OTmXbBlGXtmaJlxtfjsFbxHJFgX93zFYq7pt9znzbM4DPNSO57A0eP0; oai-did=feeb4ee9-615d-451b-91d6-35c9b9ca9826; oai-sc=0gAAAAABoGrUiAr4TfGAcKlXSjhT1h5_5UYzixku3R9aaTwvYNKk4lKP_Yk1OzPWjJL0J1CwOzC2Jb17t3aRDM3z8kxH1WOAZhIdnTU_u2NBgkfQNUQhzMBR_Qb5xHkfJ4PFUpwO9n7YNDtSoN8QVP7U2hbi0GJfMA06k0kcaceUmTvbEK6Uceq5R13QVhTPfZC9JdEjcgXorTcq7iQW9mQPJYfVhUwKgWbtdXgwQROAM51Zq4ETutVYZklPhaXy0hHs1LZdM0GRd; _cfuvid=tYpYCaVU3akRC7xqJ9HhgKb45yUMSDkSkk6zoY7wxbA-1746571091829-0.0.1.1-604800000; cf_clearance=oAW.3k9.ail0y7zwjDtsUlJIC0OAd5NllKMUq7r6T7o-1746571092-1.2.1.1-N0zV9i8bumZPfGDsB3TBpE5dIoU51SUzUvjsycWn8GmGHo6v_M6EeuCIEg9OIfAJEeJWNvE6mf6v0wOZ4x2FrUPBEc06NfnQgVb9fkIE5vzUsukkPX7jK1pigl7vtSiqDmFKyvcg4ojUUUEHuHPZeXDmiDE8UdHzLPSrMaANI6V7k5cflmlhnho8vgnFGpkiHfOArlfABVTkHWmTQo1pJAuQxhdSM3hMyxpA_6VHeQC_fUFX8Rk.dSwQzx7dCV_TCY5PKbOvfOd6X93sPSaQWneGo9hrDOTlZxeVEGLfpd7WngdKPS_fbjf6nDmwHnMtmKPMZsd_BLS.b3TfkmqngKPG_oZxWxi730a5NJv7O6w; __Secure-next-auth.session-token=eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0..UtJZGQCBDAEdkNRL.h7_eoTetqxFXGt6RmB8fr5JYaJPXH6vvcSMKG05JDGq5Nb3TSNXHfOIYdY5g_E-7xUgsPJnoE9RHmUZRuS1YCA0UW23cYmwHigGU89KKcdtJqg7rpcgosIQ2UTLY7aXe1WZBR8-3ACI-auo51JzMrjD7vnDL41k6UgGr7tdGoieyVxqlB58ughkcfIyneBm9k7VdSTahgD75BQRqWEtruPoAMCir5qtbu5bnDyAiCjT4S02gK-kVfgGlP5_tUTT_VoSQ-YMGE0Mw3Iae85l4X5PjuBj_3uhRlKEL6aABfs3KKr8gvsb6xGP_cypWVaTcPMpeFidLicJV4GTbj4x5ncGdJ4E9WK98j_fQRB6tQnF8op7UBuh1zOdKUFvmrKof4DgJm4ChPeff-kR_z64jRkZfiDdu7hTwEx1VIoIcXiVjBLg4MJAG_79WhETxiz61-HMrsUpNgRD3QSH6H6j4X3HpPs3hgiBZnd0L0WDOQU_mTcKBkvpm3Uubcatv5Gss797TelMs2cM7J64DDz88tMzChyMj1mBPguONIWlrj7G5pYkC9sLY5gyydZoK4cvfcxkAVgWa-UZJAtNwpf7XdYw9FutjW0Y5hucMqch3J2O51wVx6KltKjycLXGIJfFydq5qO53Ngu02WEz2AbBfHsdBqR2S-46t16LEqjCfWzJRuUOe0V_Ao4_lqVxgjc4Nd97pjaJhuHIFjWVY8GLmh5rqVUDAiDexEax7TLF8Htp6BBBdOt5-j772SbrvjCD8u06OyksGLIXElY66k4RMGsyzJLDxYoNa-c7E_38WtQJsPU2QH8kYRkwP8H_QYdYnOpukHVY-2Uv5AhAwaP2q9zqYAwPGyJ39mWQq0NrZ_rGqotRz9n_3W37Sbu1eX_5Ae0Vn33BuKG3fzxMU7akSVmA7_4DE_4dQC8PpmQ9C4UoLYhxg0aKCs4KVOWjpSTkLRoehiB-15R-SNHwNGKUMGoFg9B741jb8SHjgYsAVJnvj8VPPDhFm4r4rSqv-IIN--iebrj3ExC6YqBJmwWp-ZAvioSElJfvqZasoYkRKm0CjLoXuVawCcHVNngK0U0PjEFMKkveD9E4-MW2KO2uhud2OShZvXs4ge1Fvmk1yFwttmVM1f7MvSEoAolivtFj623iqlT-Sw0zTSg1Dw4-fDIfPk24r_eoaZvooP_r1sJ89NDDmPJDmZm_oavGdkP_2CCEEPaVNq8m72Mu2K2Le1geyvm8vepT9OR_5q3MDypRqs4KLruzrSqhfDr80Z0FMJHQGv-uyugY9yroX8GFtTBiXdIbRBiGquLgkOvjuw92JDTCT3HtMm5liFR4tGXbcsEKqp_HtK4cd1krKIphq71LKEhklkSXC-JvNzWS4qYEANdBrNfEaSa2e8-wtzW2qIuMfOZT4Cocv1H49cW7SIB1_5-DG3dy--1s0Wf6JniMn0Cprq9h01A1hraFxpSax3gAeOehYCLk3soPu1r0VlbXzdxTFg0S9AxeuXwj-IKL39C9gpQSkkeIsVeSL3VFlzYlpdupZcNbFLlp8JMoIlOnNaABBFPji12ln9dkeyEEkFq0JL4UyZLZ1SegADd0l7s_cn4dtYYfuIuAAJFC16vHDJ7UMVfVMhBeKA96U1w4ak-uKs8wk-l5EhGragNpPogCh_ccHyQf9zV3AnHbJyGsiVc1ztjNu6mduFB41FO3kQDBH2uoa2PgdgWtaO-zLSrTDns3aGffp6VWYddxuPvA3T5VQDVjVLz6eJ2_d2N0B9sPmevKkIYEaI1-OClY_StB4TG3VdNUsfcaqymweg43scS4DZBa9DPGz7ZmC-HG_z1oVfaoZGfa5-XD-u0Oyuu91xYM-QOuccvsHco2GWUTsf52qH51mLvkpc5AkaO9tUzhN0rQLabOGSUBjJZyxIFrRsLLlDWDwPSr3k7BbB1fBUvsUiPShqI_CWKJjfqsEiHjrt3MbfcbLc12XqYuvrCHwtpi0JRP67TwnUKADP7SEP0FEdXz34prhXpJSvK49hmpUkKYCbsdntJFz4J6vB7BKLcb9B6MsWNH5N14yhn35UJZc7_IToLLnjG93KcOhSWNvXtP9vTXRVmxIJtYfnZOOI6wsTxZ3WQcAypxYA1PLzJzlVrZC2TkT9Jhcph_Y2uHCKm0CFka2nr8DHF2VyrEo1n4x_bjXve_BZigWgGgFE0Vn4NEUdLislf0jTXcVhTwVCCxtYGPkKE3NTpu-Djl3HjPUGRYFC3dXa10HWJiazfOITTUO85LEpVRb9j2lhQaX9dKe6toPipSpGwzsdkOaw1liuADV9J8ssMSkhs0W_CLKd2HaJioPKUVUJ0v2B5Z39DiLyJ_NDx6vZoGzO8bRUp2Atm_z3ccrsaKuO-brZBmG_gApoXLQXdBPwovyS36TXx0fcdqcFlH5yqADtK1Yn9Zjkp5gIzbPz-k8nUtBtu4_JOayzFJpWGheTrkoj-nBIvuOfQB1yJan9BVZPF-KzALb1JdTC28k0kK60Exf7r2lulwA_3_-Ydg8inZ1HHGUYouAb6Na1r4Pk70g4waTzLItIbDPOeATPfvV9xRJ7LT8Yq-pbfIg1rHKRjrCcQ8IeonjziGxfNnFiPy4ANLtg2F4Z_dwi0BPE9MFo0yO7gKELGGpGoOB4rcd8mCK_wnm6MyBbdqC5oftova8ymTqjqNEH24L9DwCk8F_jx8DJhPvgrOnMU0Gc691Ym5uQeF_aMwtn6AaauBVr2Qf8VBE89wz1-sxk2vI4nJQasW9Q8wanqw_pSxwMoc1XzG8MY2sNRRREt1Ex1nmR4HjVdDSYJmbnqYgREPiJ6zUTcJkNJDPQjo70R1TEVjV8v0a97Ca_Fijp8CG2ZcD4VIAM8kOF90RJlUeG74pCIscB6AGi7tmgcO9a5VdHfeeqomRlFKVN9Ipi255SZHpI8JiUaUGwpP71RE-JHo4baZ-g7Z6i_UJsh6-76LKwL3HWblb2shAazc5cqC-ySHohWfY6_zeFD02zeoVN5vr-SaoQjzywVfef9V4AS5cs8BqlIJYP6Lrzh2AC9GwtSpEw22NVT6rR4VAIbER1HuGbwnNmnCDwHaOoyzFwkVGU8uohlOG8QYPUnLLfl8LzKv2O6SLlwcdZA1Fxiftv_ziMA4qo9Q3NQ-bblDycx-oKMWzSAvbx4LrjYBOxVn-XErq7B7K_i2kkK1C9Ttf63zbemz0aPcdhXWiEnKArv-q3kj5y21FKlmI4TwZuTKV5xlu-mOfXXWmTZZysOT6my6xZXsNOa1N-LiigdU_NV6gYmzV1Pvk1dM4lyFjr2SM8jB6hA_Je4gyhMfwqdDlFNKRJGG5SYGylYGgrqoF9Q8P5ZPxy-rzmliE5nIYICZDRw4GixtjlXElu1-voa1qlO-jB64-Q1pjfpY6FYNBdtVjFYUiMoSdaH2qscS0x9y_TXH-qvHmrn0Rbr6HTcaUQWuFwB05o-hrqpqIXnhsYaFRFo9mIKqerSdYcXWV7vjt66yOBOJXGGMdeUKEiOhoEBRzMONTaGzyqnyREpKAGKGnnAoBxiDJAPJiU7Un0A.N3e3RwsHSx7RIyRpkArjhg; __cf_bm=.tgjY3xPUqb8neZ82V8alW6BUjaUjhl9AKSAVrWvCok-1746580754-1.0.1.1-O_ArZyOJreXvGtcaHIt1cvKmgvmfWHl09pqbT9ThwMTzN9eioOcPFJty1P.ubiZc.mnPMaJD3dRXzkhbHNYVBPuRgzmujyF5kvUs0hE6LJQ; __cflb=0H28vBjUqcdJN5F5i8D3tzftjvH7q1fnq8Q2Zh98qhf; _dd_s=rum=2&id=a8178f5f-ae08-4826-bb1d-587e16634b15&created=1746580754683&expire=1746581708921; __cf_bm=.17Mcz2Tr68P5cveYebWZwneOqCeN0Mu3Ad_vzuw5hk-1746580754-1.0.1.1-go.48gAjI1dOIK3MfpLAfmz43EA.5_Wh.fxObP8M58fbQPeP_72H0dqO20Ei0At3Z5RqbNqS8GED9zvJ1utsNqmWHJLtgG58fLvIwcXkgTI"; + let cookie = ""; + + let mut creds = SoraCredentialSet::initialize_with_just_cookies_str(cookie); + + let bearer = "eyJhbGciOiJSUzI1NiIsImtpZCI6IjE5MzQ0ZTY1LWJiYzktNDRkMS1hOWQwLWY5NTdiMDc5YmQwZSIsInR5cCI6IkpXVCJ9.eyJhdWQiOlsiaHR0cHM6Ly9hcGkub3BlbmFpLmNvbS92MSJdLCJjbGllbnRfaWQiOiJhcHBfWDh6WTZ2VzJwUTl0UjNkRTduSzFqTDVnSCIsImV4cCI6MTc0NzQzOTIwMiwiaHR0cHM6Ly9hcGkub3BlbmFpLmNvbS9hdXRoIjp7InVzZXJfaWQiOiJ1c2VyLTZOeEpmbEFIb0VCREp6Wmw5aVhocWJERyJ9LCJodHRwczovL2FwaS5vcGVuYWkuY29tL3Byb2ZpbGUiOnsiZW1haWwiOiJ2b2NvZGVzMjAyMEBnbWFpbC5jb20iLCJlbWFpbF92ZXJpZmllZCI6dHJ1ZX0sImlhdCI6MTc0NjU3NTIwMiwiaXNzIjoiaHR0cHM6Ly9hdXRoLm9wZW5haS5jb20iLCJqdGkiOiJiYzM3NjgyYS1iZjJhLTQ1MmUtODFiMi1hNDRhMzY1ZGMzZTIiLCJuYmYiOjE3NDY1NzUyMDIsInB3ZF9hdXRoX3RpbWUiOjE3NDY1NzUyMDE2NjYsInNjcCI6WyJvcGVuaWQiLCJlbWFpbCIsInByb2ZpbGUiLCJvZmZsaW5lX2FjY2VzcyIsIm1vZGVsLnJlcXVlc3QiLCJtb2RlbC5yZWFkIiwib3JnYW5pemF0aW9uLnJlYWQiLCJvcmdhbml6YXRpb24ud3JpdGUiXSwic2Vzc2lvbl9pZCI6ImF1dGhzZXNzX2M0VXcyZWZUVE0xT0ZBZkFoTml0ck43dCIsInN1YiI6Imdvb2dsZS1vYXV0aDJ8MTEzMTAxOTY3NjEyMzk2NzkzNzc3In0.JPi0OSP8T27JM_hYffctiWN2MaVVNzE-kQxbhgdHFhrirjQvNdIAfYbP5764-_3UhtM-waNNsLPx3rjTwHuDxC9319axCpCDvNJg55JZMEW-4r-juUecGFoF9VS3w57Znt3mXAk3LyB0adoGZq3Hs6Za6bqrTVyyil24iIj1kEapK0uiDKf4jW_YKR5R3ZV4igZoOAMglKSEIiQ2oR2yGLEj0Omi6XF8awe8RQ1IlAkvv-67fvUIFHU9lBHvRFgv6ktEUJSIz9FOCUADgGTUKhqXyvlT1xVeMcjifi0xpks39gmLcGhygVGsOHLf2j7DRCuSuYDd8_xqtsg_Qc58iMHwWuEIRaoPiTN_PMkT4gncmou7PnoUgRQMlpojGwMH8fyRSYsgFcg0wC0Q7QWlZvZCEMt_w8jhXk1Hn8k0wIKWH0L5v2PN6i1oDVjfXPXDq9X0Mp1vqgMwal_LOwsKB93SNHdtvzUD0ZCq9Nhr-eW-6xOF1YT9g5j3-Dk7cm97lJjmYlDEYBlDGCC34D5IBlZJy2H3Ojl68Ff3DYGsWsDiAidYAmoj2rUXio3got06l81L45ddXayrqMA9BizwmXjReJcnZZm7sCGSYTOBY5PeAo_I0i-TZe7sXoFppsO78KozLVrtfoCa6hUPlX0psJJbnbaI_IJualCuByR8gxg"; + + creds.jwt_bearer_token = Some(SoraJwtBearerToken::new(bearer.to_string())?); + + //let _updated = maybe_upgrade_or_renew_session(&mut creds).await?; + + let results = list_media(&creds).await.expect("should work"); + + println!("{:?}", results); + + assert!(results.task_responses.len() > 0); + + Ok(()) + } +} diff --git a/crates/lib/api_clients/openai_sora_client/src/requests/list_media/mod.rs b/crates/lib/api_clients/openai_sora_client/src/requests/list_media/mod.rs new file mode 100644 index 0000000000..b366dc8e8a --- /dev/null +++ b/crates/lib/api_clients/openai_sora_client/src/requests/list_media/mod.rs @@ -0,0 +1 @@ +pub mod list_media; \ No newline at end of file diff --git a/crates/lib/api_clients/openai_sora_client/src/requests/mod.rs b/crates/lib/api_clients/openai_sora_client/src/requests/mod.rs index 29da77e446..0739b457fb 100644 --- a/crates/lib/api_clients/openai_sora_client/src/requests/mod.rs +++ b/crates/lib/api_clients/openai_sora_client/src/requests/mod.rs @@ -1,5 +1,6 @@ pub mod bearer; pub mod image_gen; pub mod job; +pub mod list_media; pub mod sentinel_refresh; pub mod upload; \ No newline at end of file diff --git a/crates/lib/api_clients/openai_sora_client/src/sora_error.rs b/crates/lib/api_clients/openai_sora_client/src/sora_error.rs index 1f785d374f..4188728e56 100644 --- a/crates/lib/api_clients/openai_sora_client/src/sora_error.rs +++ b/crates/lib/api_clients/openai_sora_client/src/sora_error.rs @@ -26,6 +26,14 @@ pub enum SoraError { /// } UnauthorizedCookieOrBearerExpired, + /// Failed to generate a bearer token despite the `200 OK` response. + /// This has been generated by sending old/expired session cookies or empty cookies. + FailedToGenerateBearer, + + /// Something is wrong with the JWT bearer token. + /// This error originates on our end as we try to parse the JWT. + LocalJwtClaimsParseError(String), + /// Sora is having issues. /// Typically served from Cloudflare /// We preserve the message for debugging. @@ -55,6 +63,12 @@ impl Display for SoraError { Self::NoBearerTokenAvailable => { write!(f, "Unauthorized: no bearer token set") } + Self::FailedToGenerateBearer => { + write!(f, "Failed to generate bearer. Probably invalid session.") + } + Self::LocalJwtClaimsParseError(message) => { + write!(f, "Local JWT claims parse error: {}", message) + } Self::TooManyConcurrentTasks => { write!(f, "Too many concurrent tasks. Please wait.") } diff --git a/crates/lib/api_clients/openai_sora_client/src/utils/classify_general_http_error.rs b/crates/lib/api_clients/openai_sora_client/src/utils/classify_general_http_error.rs index f76956dfb4..5f43badbe0 100644 --- a/crates/lib/api_clients/openai_sora_client/src/utils/classify_general_http_error.rs +++ b/crates/lib/api_clients/openai_sora_client/src/utils/classify_general_http_error.rs @@ -8,7 +8,33 @@ pub async fn classify_general_http_error(response: reqwest::Response) -> SoraErr Ok(text) => text, Err(err) => return SoraError::ReqwestError(err), }; + + /* + +Old cookie - +Message: { + "error": { + "message": "Your authentication token has expired. Please try signing in again.", + "type": "invalid_request_error", + "param": null, + "code": "token_expired" + } +} + +No cookie - + +Message: { + "error": { + "message": "Your authentication token has expired. Please try signing in again.", + "type": "invalid_request_error", + "param": null, + "code": "token_expired" + } +} + */ + + // TODO: I *think* this is just the bearer token, not the cookie. let cookie_expired = message.contains("Your authentication token has expired. Please try signing in again.") || message.contains("token_expired"); diff --git a/crates/lib/api_clients/openai_sora_client/src/utils/has_session_cookie.rs b/crates/lib/api_clients/openai_sora_client/src/utils/has_session_cookie.rs index f5a9c3ab2e..f3d0065a9c 100644 --- a/crates/lib/api_clients/openai_sora_client/src/utils/has_session_cookie.rs +++ b/crates/lib/api_clients/openai_sora_client/src/utils/has_session_cookie.rs @@ -4,7 +4,8 @@ use cookie::Cookie; use once_cell::sync::Lazy; use errors::AnyhowResult; -pub const SESSION_COOKIE_NAME : &str = "__Secure-authjs.session-token"; +pub const SESSION_COOKIE_NAME_OLD : &str = "__Secure-authjs.session-token"; +pub const SESSION_COOKIE_NAME_NEW : &str = "__Secure-next-auth.session-token"; /// Cookies Sora.com mints that are not the session cookie. We can ignore these. /// This list is very likely to change over time. @@ -51,7 +52,9 @@ pub fn has_session_cookie(cookies_header: &str) -> AnyhowResult AnyhowResult { +pub fn lightweight_sora_jwt_parse(token: &str) -> Result { let [_header_str, claims_str, _signature_str] = split_components(token)?; - let claims = decode_base64(claims_str)?; - let claims : serde_json::Map = serde_json::from_str(&claims)?; + let claims = decode_base64(claims_str) + .map_err(|err| SoraError::LocalJwtClaimsParseError( + format!("failure to parse claims base64: {:?}", err)))?; + + let claims : serde_json::Map = serde_json::from_str(&claims) + .map_err(|err| SoraError::LocalJwtClaimsParseError( + format!("failure to parse claims json: {:?}", err)))?; let iat = claims.get("iat") .map(|val| val.as_number()) .flatten() - .ok_or(anyhow!("no iat claim"))?; + .ok_or_else(|| jwt_error("no iat claim"))?; let iat = iat.as_i64() - .ok_or(anyhow!("iat is not a number"))?; + .ok_or_else(|| jwt_error("iat is not a number"))?; let iat = DateTime::from_timestamp(iat, 0) - .ok_or(anyhow!("iat is not a valid timestamp"))?; + .ok_or_else(|| jwt_error("iat is not a valid timestamp"))?; let exp = claims.get("exp") .map(|val| val.as_number()) .flatten() - .ok_or(anyhow!("no exp claim"))?; + .ok_or_else(|| jwt_error("no exp claim"))?; let exp = exp.as_i64() - .ok_or(anyhow!("exp is not a number"))?; + .ok_or_else(|| jwt_error("exp is not a number"))?; let exp = DateTime::from_timestamp(exp, 0) - .ok_or(anyhow!("iat is not a valid timestamp"))?; + .ok_or_else(|| jwt_error("iat is not a valid timestamp"))?; let user_id = claims.get("https://api.openai.com/auth") .map(|val| val.get("user_id")) .flatten() .map(|val| val.as_str()) .flatten() - .ok_or(anyhow!("no user_id claim"))?; + .ok_or_else(|| jwt_error("no user_id claim"))?; let profile = claims.get("https://api.openai.com/profile") - .ok_or(anyhow!("no user_id claim"))?; + .ok_or_else(|| jwt_error("no user_id claim"))?; let email = profile.get("email") .map(|val| val.as_str()) .flatten() - .ok_or(anyhow!("no email claim"))?; + .ok_or_else(|| jwt_error("no email claim"))?; let email_verified = profile.get("email_verified") .map(|val| val.as_bool()) @@ -79,14 +85,19 @@ pub fn lightweight_sora_jwt_parse(token: &str) -> AnyhowResult { }) } -fn split_components(token: &str) -> AnyhowResult<[&str; 3]> { +fn split_components(token: &str) -> Result<[&str; 3], SoraError> { let mut components = token.split(SEPARATOR); - let header = components.next().ok_or(anyhow!("no header component"))?; - let claims = components.next().ok_or(anyhow!("no claims component"))?; - let signature = components.next().ok_or(anyhow!("no signature component"))?; + let header = components.next() + .ok_or_else(|| jwt_error("no header component"))?; + + let claims = components.next() + .ok_or_else(|| jwt_error("no claims component"))?; + + let signature = components.next() + .ok_or_else(|| jwt_error("no signature component"))?; if components.next().is_some() { - return Err(anyhow!("too many components")); + return Err(jwt_error("too many components")); } Ok([header, claims, signature]) @@ -98,6 +109,10 @@ fn decode_base64(raw: &str) -> AnyhowResult { Ok(json_str) } +fn jwt_error(reason: &str) -> SoraError { + SoraError::LocalJwtClaimsParseError(reason.to_string()) +} + #[cfg(test)] mod tests { use crate::utils::lightweight_sora_jwt_parse::lightweight_sora_jwt_parse; diff --git a/frontend/libs/components/login-modal/src/lib/login-modal.tsx b/frontend/libs/components/login-modal/src/lib/login-modal.tsx index 7011547a64..c071dd7cf2 100644 --- a/frontend/libs/components/login-modal/src/lib/login-modal.tsx +++ b/frontend/libs/components/login-modal/src/lib/login-modal.tsx @@ -8,6 +8,7 @@ import { } from "@fortawesome/pro-solid-svg-icons"; import { faDiscord } from "@fortawesome/free-brands-svg-icons"; import { IsDesktopApp } from "@storyteller/tauri-utils"; +import { CheckSoraSession, SoraSessionState } from "@storyteller/tauri-api"; import { invoke } from "@tauri-apps/api/core"; interface LoginModalProps { @@ -29,15 +30,11 @@ export function LoginModal({ const [isOpen, setIsOpen] = useState(false); const totalSteps = 3; - const checkSession = async () => { - // Return the current session state - return hasSession; - }; - // Check session on component mount useEffect(() => { const initSession = async () => { - const sessionExists = await checkSession(); + const result = await CheckSoraSession(); + const sessionExists = result.state === SoraSessionState.Valid; if (!sessionExists) { setIsOpen(true); setStep(1); @@ -57,10 +54,12 @@ export function LoginModal({ try { if (IsDesktopApp()) { await invoke("open_sora_login_command"); + // Add 3 second delay before setting session to true await new Promise((resolve) => setTimeout(resolve, 3000)); setHasSession(true); - const sessionExists = await checkSession(); + const result = await CheckSoraSession(); + const sessionExists = result.state === SoraSessionState.Valid; if (sessionExists) { setStep(step + 1); } @@ -168,6 +167,9 @@ export function LoginModal({ alt="OpenAI Logo" className="w-72 h-72 mx-auto grow" /> + {/*
+ +
*/} ); case 3: diff --git a/frontend/libs/tauri-api/src/index.ts b/frontend/libs/tauri-api/src/index.ts index a858878488..1f690c6e9e 100644 --- a/frontend/libs/tauri-api/src/index.ts +++ b/frontend/libs/tauri-api/src/index.ts @@ -1,2 +1,3 @@ export * from './lib/settings/GetAppPreferences.js'; +export * from './lib/sora/CheckSoraSession.js'; diff --git a/frontend/libs/tauri-api/src/lib/sora/CheckSoraSession.ts b/frontend/libs/tauri-api/src/lib/sora/CheckSoraSession.ts new file mode 100644 index 0000000000..8d9b5831d8 --- /dev/null +++ b/frontend/libs/tauri-api/src/lib/sora/CheckSoraSession.ts @@ -0,0 +1,18 @@ +import { invoke } from "@tauri-apps/api/core"; + +export interface CheckSoraSessionResult { + state: SoraSessionState, + maybe_account_email?: string, +} + +export enum SoraSessionState { + NotSetUp = "not_set_up", + ExpiredOrError = "expired_or_error", + Valid = "valid", +} + +export const CheckSoraSession = async () : Promise => { + let result = await invoke("check_sora_session_command"); + console.log(">>> sora result", result); + return (result as CheckSoraSessionResult); +}