From 000cff5c63ced7f501914df0c7dacc17af5804cc Mon Sep 17 00:00:00 2001 From: RCD <90105158+Finesssee@users.noreply.github.com> Date: Thu, 1 Oct 2026 12:23:08 +0700 Subject: [PATCH] Port upstream 0.68.0: Venice web session per-browser fall-through --- rust/src/browser/cookies.rs | 16 + rust/src/providers/mod.rs | 13 + rust/src/providers/venice/mod.rs | 479 ++++++++------------------- rust/src/providers/venice/tests.rs | 515 +++++++++++++++++++++++++++++ 4 files changed, 681 insertions(+), 342 deletions(-) create mode 100644 rust/src/providers/venice/tests.rs diff --git a/rust/src/browser/cookies.rs b/rust/src/browser/cookies.rs index 25be5f29dd..3233d702a5 100755 --- a/rust/src/browser/cookies.rs +++ b/rust/src/browser/cookies.rs @@ -770,6 +770,22 @@ pub fn get_cookie_headers_for_domain( Ok(headers) } +/// Get cookie candidates from every detected browser that has readable cookies +/// for a domain, preserving browser order and cookie metadata for provider-side +/// host filtering. Reports App-Bound Encryption when it is the only blocker. +pub fn get_cookies_by_browser_for_domain( + domain: &str, +) -> Result)>, CookieError> { + let scan = extract_domain_candidates(domain)?; + if scan.candidates.is_empty() && scan.abe_error_seen { + return Err(CookieError::AppBoundEncryption); + } + if scan.candidates.is_empty() { + return Err(CookieError::NotFound(domain.to_string())); + } + Ok(scan.candidates) +} + /// Get a cookie header string for a domain pub fn get_cookie_header(domain: &str) -> Result { let cookies = get_cookies_for_domain(domain)?; diff --git a/rust/src/providers/mod.rs b/rust/src/providers/mod.rs index de9d265f6e..cd901db94c 100755 --- a/rust/src/providers/mod.rs +++ b/rust/src/providers/mod.rs @@ -232,6 +232,19 @@ pub(crate) fn browser_cookie_headers_for_domain( .map_err(map_browser_cookie_error) } +pub(crate) fn browser_cookie_candidates_for_domain( + domain: &str, +) -> Result< + Vec<( + crate::browser::detection::BrowserType, + Vec, + )>, + crate::core::ProviderError, +> { + crate::browser::cookies::get_cookies_by_browser_for_domain(domain) + .map_err(map_browser_cookie_error) +} + /// All non-empty values for one cookie name in a `Cookie:` header, in order. /// Returns every value so callers can reject duplicates instead of silently /// picking the first. diff --git a/rust/src/providers/venice/mod.rs b/rust/src/providers/venice/mod.rs index 600a277f17..e8d4e8f48a 100644 --- a/rust/src/providers/venice/mod.rs +++ b/rust/src/providers/venice/mod.rs @@ -8,6 +8,7 @@ use reqwest::Client; use serde::Deserialize; use serde_json::Value; use std::collections::BTreeMap; +use std::future::Future; use crate::core::{ FetchContext, Provider, ProviderDisplayDetail, ProviderError, ProviderFetchResult, ProviderId, @@ -52,6 +53,31 @@ pub struct VeniceProvider { client: Client, } +#[derive(Debug)] +enum VeniceWebFailure { + InvalidSession, + Anonymous, + MissingQuota(ProviderError), + Other(ProviderError), +} + +impl VeniceWebFailure { + fn into_provider_error(self) -> ProviderError { + match self { + Self::InvalidSession => invalid_session_error(), + Self::Anonymous => ProviderError::AuthRequired, + Self::MissingQuota(error) | Self::Other(error) => error, + } + } + + fn is_unusable_session(&self) -> bool { + matches!( + self, + Self::InvalidSession | Self::Anonymous | Self::MissingQuota(_) + ) + } +} + impl VeniceProvider { pub fn new() -> Self { Self { @@ -111,43 +137,69 @@ impl VeniceProvider { &self, manual_cookie_header: Option<&str>, ) -> Result { - let credential = match manual_cookie_header { - Some(header) => session_credential_from_header(header), - None => match crate::providers::browser_cookies_for_domain(VENICE_COOKIE_DOMAIN) { - Ok(cookies) => session_credential_from_browser_cookies(&cookies), - Err(ProviderError::NoCookies) => None, - Err(error) => return Err(error), - }, + if let Some(header) = manual_cookie_header { + let credential = session_credential_from_header(header) + .ok_or_else(|| ProviderError::Other(VENICE_MISSING_CREDENTIALS_MESSAGE.into()))?; + return self + .fetch_web_session(&credential) + .await + .map_err(VeniceWebFailure::into_provider_error); } - .ok_or_else(|| ProviderError::Other(VENICE_MISSING_CREDENTIALS_MESSAGE.into()))?; + let candidates = + match crate::providers::browser_cookie_candidates_for_domain(VENICE_COOKIE_DOMAIN) { + Ok(candidates) => candidates, + Err(ProviderError::NoCookies) => { + return Err(ProviderError::Other( + VENICE_MISSING_CREDENTIALS_MESSAGE.into(), + )); + } + Err(error) => return Err(error), + }; + let candidates = browser_session_candidates(candidates); + fetch_web_sessions(candidates, |credential| async move { + self.fetch_web_session(&credential).await + }) + .await + } + + async fn fetch_web_session( + &self, + credential: &VeniceSessionCredential, + ) -> Result { let response = credential .apply(self.client.get(VENICE_SESSION_URL)) .header("Accept", "application/json") .send() - .await?; + .await + .map_err(|error| VeniceWebFailure::Other(error.into()))?; if response.status() == reqwest::StatusCode::UNAUTHORIZED || response.status() == reqwest::StatusCode::FORBIDDEN { - return Err(invalid_session_error()); + return Err(VeniceWebFailure::InvalidSession); } if !response.status().is_success() { - return Err(ProviderError::Other(format!( + return Err(VeniceWebFailure::Other(ProviderError::Other(format!( "Venice web session returned status {}", response.status() - ))); + )))); } let session: VeniceSessionResponse = response.json().await.map_err(|e| { - ProviderError::Parse(format!("Failed to parse Venice web session: {e}")) + VeniceWebFailure::Other(ProviderError::Parse(format!( + "Failed to parse Venice web session: {e}" + ))) })?; if session.token.trim().is_empty() { - return Err(invalid_session_error()); + return Err(VeniceWebFailure::InvalidSession); } let token = session.token.as_str(); - let claims = crate::codex_accounts::api::jwt_payload(token) - .ok_or_else(|| ProviderError::Parse("Venice session token is not a JWT".into()))?; + let claims = crate::codex_accounts::api::jwt_payload(token).ok_or_else(|| { + VeniceWebFailure::Other(ProviderError::Parse( + "Venice session token is not a JWT".into(), + )) + })?; snapshot_from_web_claims(&claims, Utc::now()) } } @@ -257,6 +309,57 @@ fn invalid_session_error() -> ProviderError { ProviderError::Other(VENICE_INVALID_SESSION_MESSAGE.into()) } +fn browser_session_candidates( + candidates: Vec<( + crate::browser::detection::BrowserType, + Vec, + )>, +) -> Vec<( + crate::browser::detection::BrowserType, + VeniceSessionCredential, +)> { + candidates + .into_iter() + .filter_map(|(browser, cookies)| { + session_credential_from_browser_cookies(&cookies) + .map(|credential| (browser, credential)) + }) + .collect() +} + +async fn fetch_web_sessions( + candidates: Vec<( + crate::browser::detection::BrowserType, + VeniceSessionCredential, + )>, + mut loader: F, +) -> Result +where + F: FnMut(VeniceSessionCredential) -> Fut, + Fut: Future>, +{ + let mut candidates = candidates.into_iter().peekable(); + let mut last_failure = None; + while let Some((browser, credential)) = candidates.next() { + match loader(credential).await { + Ok(result) => return Ok(result), + Err(failure) if failure.is_unusable_session() => { + if candidates.peek().is_some() { + tracing::debug!( + browser = %browser.display_name(), + "Venice session unusable; trying the next browser" + ); + } + last_failure = Some(failure.into_provider_error()); + } + Err(failure) => return Err(failure.into_provider_error()), + } + } + + Err(last_failure + .unwrap_or_else(|| ProviderError::Other(VENICE_MISSING_CREDENTIALS_MESSAGE.into()))) +} + /// Credential accepted by the Venice session endpoint. #[derive(Debug, PartialEq, Eq)] enum VeniceSessionCredential { @@ -378,11 +481,11 @@ fn session_credential<'a>( fn snapshot_from_web_claims( claims: &serde_json::Map, now: DateTime, -) -> Result { +) -> Result { let expiration = - epoch_value_to_datetime(claims.get("exp")).ok_or_else(invalid_session_error)?; + epoch_value_to_datetime(claims.get("exp")).ok_or(VeniceWebFailure::InvalidSession)?; if expiration < now - chrono::Duration::seconds(VENICE_EXPIRATION_SKEW_SECS) { - return Err(invalid_session_error()); + return Err(VeniceWebFailure::InvalidSession); } if claims @@ -390,19 +493,28 @@ fn snapshot_from_web_claims( .and_then(Value::as_str) .is_some_and(is_anonymous_user_type) { - return Err(ProviderError::AuthRequired); + return Err(VeniceWebFailure::Anonymous); } let usage = claims .get("bundledCreditsUsage") .and_then(Value::as_object) - .ok_or_else(|| ProviderError::Parse("Venice web session has no credits usage".into()))?; - let used_this_cycle = finite_non_negative(usage.get("usedThisCycle")) - .ok_or_else(|| ProviderError::Parse("Venice web session has invalid usage".into()))?; + .ok_or_else(|| { + VeniceWebFailure::MissingQuota(ProviderError::Parse( + "Venice web session has no credits usage".into(), + )) + })?; + let used_this_cycle = finite_non_negative(usage.get("usedThisCycle")).ok_or_else(|| { + VeniceWebFailure::MissingQuota(ProviderError::Parse( + "Venice web session has invalid usage".into(), + )) + })?; let monthly_refill_credits = finite_non_negative(usage.get("monthlyRefillCredits")) .filter(|value| *value > 0.0) .ok_or_else(|| { - ProviderError::Parse("Venice web session has invalid refill credits".into()) + VeniceWebFailure::MissingQuota(ProviderError::Parse( + "Venice web session has invalid refill credits".into(), + )) })?; let available_credits = finite_non_negative(usage.get("availableCredits")) @@ -527,321 +639,4 @@ fn format_credits(value: f64) -> String { } #[cfg(test)] -mod tests { - use super::*; - - fn web_claims() -> serde_json::Map { - serde_json::from_value(serde_json::json!({ - "exp": 1_900_000_000, - "userType": "paid", - "bundledCredits": 80, - "veniceCredits": 120, - "bundledCreditsUsage": { - "usedThisCycle": 12, - "monthlyRefillCredits": 100, - "availableCredits": 88, - "tierCap": 200, - "nextRefillAt": 1_900_000_000_000i64 - } - })) - .unwrap() - } - - #[test] - fn venice_snapshot_uses_diem_allocation() { - let snapshot = snapshot_from_balance(&VeniceBalanceResponse { - can_consume: true, - consumption_currency: Some("DIEM".into()), - balances: VeniceBalances { - diem: Some(25.0), - usd: None, - }, - diem_epoch_allocation: Some(100.0), - }); - assert_eq!(snapshot.primary.used_percent, 75.0); - } - - fn legacy(value: &str) -> Option { - Some(VeniceSessionCredential::Legacy(value.to_string())) - } - - fn clerk(value: &str) -> Option { - Some(VeniceSessionCredential::Clerk(value.to_string())) - } - - fn browser_cookie(name: &str, domain: &str) -> crate::browser::cookies::Cookie { - crate::browser::cookies::Cookie { - name: name.to_string(), - value: "synthetic-session".to_string(), - domain: domain.to_string(), - path: "/".to_string(), - expires: None, - is_secure: true, - is_http_only: false, - } - } - - #[test] - fn session_cookie_prefers_exact_and_reassembles_contiguous_chunks() { - assert_eq!( - session_credential_from_header( - "other=x; __venice-auth.session-token.0=ab; __venice-auth.session-token.1=cd" - ), - legacy("abcd") - ); - assert_eq!( - session_credential_from_header( - "__venice-auth.session-token.0=ab; __venice-auth.session-token.2=cd" - ), - None - ); - assert_eq!( - session_credential_from_header( - "__venice-auth.session-token=exact; __venice-auth.session-token.0=chunk" - ), - legacy("exact") - ); - assert_eq!( - session_credential_from_header("__venice-auth.session-token.0=a\nsecret"), - None - ); - assert_eq!( - session_credential_from_header( - "__venice-auth.session-token=one; __venice-auth.session-token=two" - ), - None - ); - assert_eq!( - session_credential_from_header("__venice-auth.session-token.not-a-chunk=value"), - None - ); - assert_eq!( - session_credential_from_header("Cookie: __session=pasted; other=x"), - clerk("pasted") - ); - assert_eq!( - session_credential_from_header("cookie: __venice-auth.session-token=legacy"), - legacy("legacy") - ); - let oversized = format!( - "__venice-auth.session-token={}", - "x".repeat(MAX_VENICE_COOKIE_VALUE_LEN + 1) - ); - assert_eq!(session_credential_from_header(&oversized), None); - } - - #[test] - fn clerk_session_family_is_accepted_and_sent_only_as_bearer() { - for name in ["__session", "__session_synthetic"] { - assert!(is_clerk_session_cookie_name(name)); - let raw = format!( - "__client_uat=123; {name}=synthetic-session; __client=private; clerk_active_synthetic=1" - ); - let credential = session_credential_from_header(&raw).unwrap(); - assert_eq!( - credential, - VeniceSessionCredential::Clerk("synthetic-session".into()) - ); - - let client = Client::new(); - let request = credential - .apply(client.get(VENICE_SESSION_URL)) - .header("Accept", "application/json") - .build() - .unwrap(); - assert_eq!(request.method(), reqwest::Method::GET); - assert_eq!(request.url().as_str(), VENICE_SESSION_URL); - assert_eq!( - request.headers().get("Authorization").unwrap(), - "Bearer synthetic-session" - ); - assert!(request.headers().get("Cookie").is_none()); - } - } - - #[test] - fn legacy_session_is_sent_as_cookie_without_authorization() { - let client = Client::new(); - let request = legacy("legacy") - .unwrap() - .apply(client.get(VENICE_SESSION_URL)) - .build() - .unwrap(); - assert_eq!( - request.headers().get("Cookie").unwrap(), - "__venice-auth.session-token=legacy" - ); - assert!(request.headers().get("Authorization").is_none()); - } - - #[test] - fn legacy_session_retains_priority_over_clerk_including_numbered_chunks() { - assert_eq!( - session_credential_from_header("__session=clerk; __venice-auth.session-token=legacy"), - legacy("legacy") - ); - assert_eq!( - session_credential_from_header( - "__session=clerk; __venice-auth.session-token.1=b; __venice-auth.session-token.0=a" - ), - legacy("ab") - ); - assert_eq!( - session_credential_from_header("__session_synthetic=secondary; __session=primary"), - clerk("primary") - ); - assert_eq!( - session_credential_from_header("__session=primary; __session_synthetic=secondary"), - clerk("primary") - ); - assert_eq!( - session_credential_from_header("__session_a=first; __session_b=second"), - clerk("first") - ); - // A repeated `__session` keeps the last value, as upstream does. - assert_eq!( - session_credential_from_header("__session=stale; __session=fresh"), - clerk("fresh") - ); - // A partial legacy chunk set cannot be reassembled, so Clerk is used. - assert_eq!( - session_credential_from_header("__venice-auth.session-token.1=b; __session=clerk"), - clerk("clerk") - ); - } - - #[test] - fn browser_session_cookies_are_restricted_to_the_exact_venice_site() { - for (domain, accepted) in [ - ("venice.ai", true), - (".venice.ai", true), - (".Venice.AI", true), - ("clerk.venice.ai", false), - (".clerk.venice.ai", false), - ("outerface.venice.ai", false), - ("notvenice.ai", false), - ] { - let cookies = [browser_cookie("__session", domain)]; - assert_eq!( - session_credential_from_browser_cookies(&cookies), - accepted.then(|| VeniceSessionCredential::Clerk("synthetic-session".into())), - "{domain}" - ); - } - let cookies = [ - browser_cookie("__client", "clerk.venice.ai"), - browser_cookie("__session", "clerk.venice.ai"), - browser_cookie("__venice-auth.session-token", "venice.ai"), - ]; - assert_eq!( - session_credential_from_browser_cookies(&cookies), - legacy("synthetic-session") - ); - } - - #[test] - fn non_session_clerk_and_authjs_cookies_cannot_authenticate() { - for name in [ - "__client", - "__client_uat", - "__client_uat_synthetic", - "clerk_active_synthetic", - "__session_", - "__sessionevil", - "__Host-authjs.csrf-token", - "__Secure-authjs.callback-url", - ] { - assert!(!is_clerk_session_cookie_name(name), "{name}"); - assert_eq!( - session_credential_from_header(&format!("{name}=synthetic")), - None, - "{name}" - ); - } - } - - #[test] - fn recovery_messages_explain_active_tab_and_missing_cookie_names() { - let missing = VENICE_MISSING_CREDENTIALS_MESSAGE; - assert!(missing.contains("__session")); - assert!(missing.contains("__venice-auth.session-token")); - assert!(VENICE_INVALID_SESSION_MESSAGE.contains("tab")); - - let provider = VeniceProvider::new(); - assert_eq!( - provider.error_state_kind(&ProviderError::Other(missing.into())), - crate::core::ProviderStateKind::NeedsAuthentication - ); - assert_eq!( - provider.error_state_kind(&invalid_session_error()), - crate::core::ProviderStateKind::ExpiredSession - ); - assert_eq!( - provider.error_state_kind(&ProviderError::Other("other".into())), - crate::core::ProviderStateKind::Unknown - ); - } - - #[test] - fn web_claims_produce_display_details_without_quota_math() { - let result = snapshot_from_web_claims( - &web_claims(), - DateTime::::from_timestamp(1_800_000_000, 0).unwrap(), - ) - .unwrap(); - - assert!(result.usage.primary.is_informational); - let details: Vec<_> = result.display_details().iter().collect(); - assert_eq!(details.len(), 6); - assert_eq!(details[0].value(), "88"); - assert_eq!( - details[2].progress().map(|progress| progress.total()), - Some(100.0) - ); - } - - #[test] - fn epoch_value_accepts_seconds_milliseconds_and_rejects_outliers() { - let seconds = serde_json::json!(1_900_000_000u64); - let millis = serde_json::json!(1_900_000_000_000i64); - assert_eq!( - epoch_value_to_datetime(Some(&seconds)), - DateTime::::from_timestamp(1_900_000_000, 0) - ); - assert_eq!( - epoch_value_to_datetime(Some(&millis)), - DateTime::::from_timestamp(1_900_000_000, 0) - ); - assert_eq!(epoch_value_to_datetime(None), None); - assert_eq!(epoch_value_to_datetime(Some(&serde_json::json!(42))), None); - assert_eq!( - epoch_value_to_datetime(Some(&serde_json::json!("1900000000"))), - DateTime::::from_timestamp(1_900_000_000, 0) - ); - } - - #[test] - fn web_claims_reject_expired_anonymous_and_missing_usage() { - let now = DateTime::::from_timestamp(1_900_000_000, 0).unwrap(); - let mut expired = web_claims(); - expired.insert("exp".into(), Value::from(1_800_000_000)); - assert!(matches!( - snapshot_from_web_claims(&expired, now), - Err(ProviderError::Other(message)) if message == VENICE_INVALID_SESSION_MESSAGE - )); - - let mut anonymous = web_claims(); - anonymous.insert("userType".into(), Value::from("anonymous")); - assert!(matches!( - snapshot_from_web_claims(&anonymous, now), - Err(ProviderError::AuthRequired) - )); - - let mut missing = web_claims(); - missing.remove("bundledCreditsUsage"); - assert!(matches!( - snapshot_from_web_claims(&missing, now), - Err(ProviderError::Parse(_)) - )); - } -} +mod tests; diff --git a/rust/src/providers/venice/tests.rs b/rust/src/providers/venice/tests.rs new file mode 100644 index 0000000000..c81468cbd1 --- /dev/null +++ b/rust/src/providers/venice/tests.rs @@ -0,0 +1,515 @@ +use super::*; + +fn web_claims() -> serde_json::Map { + serde_json::from_value(serde_json::json!({ + "exp": 1_900_000_000, + "userType": "paid", + "bundledCredits": 80, + "veniceCredits": 120, + "bundledCreditsUsage": { + "usedThisCycle": 12, + "monthlyRefillCredits": 100, + "availableCredits": 88, + "tierCap": 200, + "nextRefillAt": 1_900_000_000_000i64 + } + })) + .unwrap() +} + +#[test] +fn venice_snapshot_uses_diem_allocation() { + let snapshot = snapshot_from_balance(&VeniceBalanceResponse { + can_consume: true, + consumption_currency: Some("DIEM".into()), + balances: VeniceBalances { + diem: Some(25.0), + usd: None, + }, + diem_epoch_allocation: Some(100.0), + }); + assert_eq!(snapshot.primary.used_percent, 75.0); +} + +fn legacy(value: &str) -> Option { + Some(VeniceSessionCredential::Legacy(value.to_string())) +} + +fn clerk(value: &str) -> Option { + Some(VeniceSessionCredential::Clerk(value.to_string())) +} + +fn browser_cookie(name: &str, domain: &str) -> crate::browser::cookies::Cookie { + crate::browser::cookies::Cookie { + name: name.to_string(), + value: "synthetic-session".to_string(), + domain: domain.to_string(), + path: "/".to_string(), + expires: None, + is_secure: true, + is_http_only: false, + } +} + +#[test] +fn session_cookie_prefers_exact_and_reassembles_contiguous_chunks() { + assert_eq!( + session_credential_from_header( + "other=x; __venice-auth.session-token.0=ab; __venice-auth.session-token.1=cd" + ), + legacy("abcd") + ); + assert_eq!( + session_credential_from_header( + "__venice-auth.session-token.0=ab; __venice-auth.session-token.2=cd" + ), + None + ); + assert_eq!( + session_credential_from_header( + "__venice-auth.session-token=exact; __venice-auth.session-token.0=chunk" + ), + legacy("exact") + ); + assert_eq!( + session_credential_from_header("__venice-auth.session-token.0=a\nsecret"), + None + ); + assert_eq!( + session_credential_from_header( + "__venice-auth.session-token=one; __venice-auth.session-token=two" + ), + None + ); + assert_eq!( + session_credential_from_header("__venice-auth.session-token.not-a-chunk=value"), + None + ); + assert_eq!( + session_credential_from_header("Cookie: __session=pasted; other=x"), + clerk("pasted") + ); + assert_eq!( + session_credential_from_header("cookie: __venice-auth.session-token=legacy"), + legacy("legacy") + ); + let oversized = format!( + "__venice-auth.session-token={}", + "x".repeat(MAX_VENICE_COOKIE_VALUE_LEN + 1) + ); + assert_eq!(session_credential_from_header(&oversized), None); +} + +#[test] +fn clerk_session_family_is_accepted_and_sent_only_as_bearer() { + for name in ["__session", "__session_synthetic"] { + assert!(is_clerk_session_cookie_name(name)); + let raw = format!( + "__client_uat=123; {name}=synthetic-session; __client=private; clerk_active_synthetic=1" + ); + let credential = session_credential_from_header(&raw).unwrap(); + assert_eq!( + credential, + VeniceSessionCredential::Clerk("synthetic-session".into()) + ); + + let client = Client::new(); + let request = credential + .apply(client.get(VENICE_SESSION_URL)) + .header("Accept", "application/json") + .build() + .unwrap(); + assert_eq!(request.method(), reqwest::Method::GET); + assert_eq!(request.url().as_str(), VENICE_SESSION_URL); + assert_eq!( + request.headers().get("Authorization").unwrap(), + "Bearer synthetic-session" + ); + assert!(request.headers().get("Cookie").is_none()); + } +} + +#[test] +fn legacy_session_is_sent_as_cookie_without_authorization() { + let client = Client::new(); + let request = legacy("legacy") + .unwrap() + .apply(client.get(VENICE_SESSION_URL)) + .build() + .unwrap(); + assert_eq!( + request.headers().get("Cookie").unwrap(), + "__venice-auth.session-token=legacy" + ); + assert!(request.headers().get("Authorization").is_none()); +} + +#[test] +fn legacy_session_retains_priority_over_clerk_including_numbered_chunks() { + assert_eq!( + session_credential_from_header("__session=clerk; __venice-auth.session-token=legacy"), + legacy("legacy") + ); + assert_eq!( + session_credential_from_header( + "__session=clerk; __venice-auth.session-token.1=b; __venice-auth.session-token.0=a" + ), + legacy("ab") + ); + assert_eq!( + session_credential_from_header("__session_synthetic=secondary; __session=primary"), + clerk("primary") + ); + assert_eq!( + session_credential_from_header("__session=primary; __session_synthetic=secondary"), + clerk("primary") + ); + assert_eq!( + session_credential_from_header("__session_a=first; __session_b=second"), + clerk("first") + ); + // A repeated `__session` keeps the last value, as upstream does. + assert_eq!( + session_credential_from_header("__session=stale; __session=fresh"), + clerk("fresh") + ); + // A partial legacy chunk set cannot be reassembled, so Clerk is used. + assert_eq!( + session_credential_from_header("__venice-auth.session-token.1=b; __session=clerk"), + clerk("clerk") + ); +} + +#[test] +fn browser_session_cookies_are_restricted_to_the_exact_venice_site() { + for (domain, accepted) in [ + ("venice.ai", true), + (".venice.ai", true), + (".Venice.AI", true), + ("clerk.venice.ai", false), + (".clerk.venice.ai", false), + ("outerface.venice.ai", false), + ("notvenice.ai", false), + ] { + let cookies = [browser_cookie("__session", domain)]; + assert_eq!( + session_credential_from_browser_cookies(&cookies), + accepted.then(|| VeniceSessionCredential::Clerk("synthetic-session".into())), + "{domain}" + ); + } + let cookies = [ + browser_cookie("__client", "clerk.venice.ai"), + browser_cookie("__session", "clerk.venice.ai"), + browser_cookie("__venice-auth.session-token", "venice.ai"), + ]; + assert_eq!( + session_credential_from_browser_cookies(&cookies), + legacy("synthetic-session") + ); +} + +#[test] +fn non_session_clerk_and_authjs_cookies_cannot_authenticate() { + for name in [ + "__client", + "__client_uat", + "__client_uat_synthetic", + "clerk_active_synthetic", + "__session_", + "__sessionevil", + "__Host-authjs.csrf-token", + "__Secure-authjs.callback-url", + ] { + assert!(!is_clerk_session_cookie_name(name), "{name}"); + assert_eq!( + session_credential_from_header(&format!("{name}=synthetic")), + None, + "{name}" + ); + } +} + +#[test] +fn recovery_messages_explain_active_tab_and_missing_cookie_names() { + let missing = VENICE_MISSING_CREDENTIALS_MESSAGE; + assert!(missing.contains("__session")); + assert!(missing.contains("__venice-auth.session-token")); + assert!(VENICE_INVALID_SESSION_MESSAGE.contains("tab")); + + let provider = VeniceProvider::new(); + assert_eq!( + provider.error_state_kind(&ProviderError::Other(missing.into())), + crate::core::ProviderStateKind::NeedsAuthentication + ); + assert_eq!( + provider.error_state_kind(&invalid_session_error()), + crate::core::ProviderStateKind::ExpiredSession + ); + assert_eq!( + provider.error_state_kind(&ProviderError::Other("other".into())), + crate::core::ProviderStateKind::Unknown + ); +} + +#[test] +fn web_claims_produce_display_details_without_quota_math() { + let result = snapshot_from_web_claims( + &web_claims(), + DateTime::::from_timestamp(1_800_000_000, 0).unwrap(), + ) + .unwrap(); + + assert!(result.usage.primary.is_informational); + let details: Vec<_> = result.display_details().iter().collect(); + assert_eq!(details.len(), 6); + assert_eq!(details[0].value(), "88"); + assert_eq!( + details[2].progress().map(|progress| progress.total()), + Some(100.0) + ); +} + +#[test] +fn epoch_value_accepts_seconds_milliseconds_and_rejects_outliers() { + let seconds = serde_json::json!(1_900_000_000u64); + let millis = serde_json::json!(1_900_000_000_000i64); + assert_eq!( + epoch_value_to_datetime(Some(&seconds)), + DateTime::::from_timestamp(1_900_000_000, 0) + ); + assert_eq!( + epoch_value_to_datetime(Some(&millis)), + DateTime::::from_timestamp(1_900_000_000, 0) + ); + assert_eq!(epoch_value_to_datetime(None), None); + assert_eq!(epoch_value_to_datetime(Some(&serde_json::json!(42))), None); + assert_eq!( + epoch_value_to_datetime(Some(&serde_json::json!("1900000000"))), + DateTime::::from_timestamp(1_900_000_000, 0) + ); +} + +#[test] +fn web_claims_reject_expired_anonymous_and_missing_usage() { + let now = DateTime::::from_timestamp(1_900_000_000, 0).unwrap(); + let mut expired = web_claims(); + expired.insert("exp".into(), Value::from(1_800_000_000)); + let expired_error = snapshot_from_web_claims(&expired, now) + .unwrap_err() + .into_provider_error(); + assert!(matches!( + expired_error, + ProviderError::Other(message) if message == VENICE_INVALID_SESSION_MESSAGE + )); + + let mut anonymous = web_claims(); + anonymous.insert("userType".into(), Value::from("anonymous")); + let anonymous_error = snapshot_from_web_claims(&anonymous, now) + .unwrap_err() + .into_provider_error(); + assert!(matches!(anonymous_error, ProviderError::AuthRequired)); + + let mut missing = web_claims(); + missing.remove("bundledCreditsUsage"); + let missing_error = snapshot_from_web_claims(&missing, now) + .unwrap_err() + .into_provider_error(); + assert!(matches!(missing_error, ProviderError::Parse(_))); +} + +fn test_candidates() -> Vec<( + crate::browser::detection::BrowserType, + VeniceSessionCredential, +)> { + use crate::browser::detection::BrowserType; + + vec![ + ( + BrowserType::Chrome, + VeniceSessionCredential::Clerk("first".into()), + ), + ( + BrowserType::Edge, + VeniceSessionCredential::Legacy("second".into()), + ), + ] +} + +fn credential_value(credential: &VeniceSessionCredential) -> &str { + match credential { + VeniceSessionCredential::Legacy(value) | VeniceSessionCredential::Clerk(value) => value, + } +} + +fn test_result(source: &str) -> ProviderFetchResult { + ProviderFetchResult::new( + UsageSnapshot::new(RateWindow::informational("synthetic")), + source, + ) +} + +#[tokio::test] +async fn unusable_browser_sessions_fall_through_to_the_next_candidate() { + use std::cell::RefCell; + + let failures = [ + VeniceWebFailure::InvalidSession, + VeniceWebFailure::Anonymous, + VeniceWebFailure::MissingQuota(ProviderError::Parse("missing quota".into())), + ]; + for failure in failures { + let calls = RefCell::new(Vec::new()); + let first_failure = RefCell::new(Some(failure)); + let result = fetch_web_sessions(test_candidates(), |credential| { + let value = credential_value(&credential).to_string(); + calls.borrow_mut().push(value.clone()); + let outcome = if value == "first" { + Err(first_failure.borrow_mut().take().unwrap()) + } else { + Ok(test_result("second")) + }; + async move { outcome } + }) + .await; + + assert_eq!(*calls.borrow(), ["first", "second"]); + assert_eq!(result.unwrap().source_label, "second"); + } +} + +#[tokio::test] +async fn other_browser_failures_stop_without_trying_the_next_candidate() { + use std::cell::RefCell; + + let cases = [ + (false, "network failure"), + (true, "JWT parse failure"), + ( + false, + "Venice web session returned status 500 Internal Server Error", + ), + ]; + for (parse, message) in cases { + let calls = RefCell::new(Vec::new()); + let (failure, expected) = if parse { + ( + VeniceWebFailure::Other(ProviderError::Parse(message.into())), + ProviderError::Parse(message.into()), + ) + } else { + ( + VeniceWebFailure::Other(ProviderError::Other(message.into())), + ProviderError::Other(message.into()), + ) + }; + let actual = RefCell::new(Some(failure)); + let result = fetch_web_sessions(test_candidates(), |credential| { + calls + .borrow_mut() + .push(credential_value(&credential).to_string()); + let failure = actual.borrow_mut().take().unwrap(); + async move { Err(failure) } + }) + .await; + + assert_eq!(calls.borrow().as_slice(), ["first"]); + match (result.unwrap_err(), expected) { + (ProviderError::Other(actual), ProviderError::Other(expected)) + | (ProviderError::Parse(actual), ProviderError::Parse(expected)) => { + assert_eq!(actual, expected); + } + errors => panic!("unexpected errors: {errors:?}"), + } + } +} + +#[tokio::test] +async fn all_unusable_candidates_return_the_last_error() { + let result = fetch_web_sessions(test_candidates(), |credential| async move { + let failure = if credential_value(&credential) == "first" { + VeniceWebFailure::InvalidSession + } else { + VeniceWebFailure::MissingQuota(ProviderError::Parse("last quota error".into())) + }; + Err(failure) + }) + .await; + + assert!(matches!( + result, + Err(ProviderError::Parse(message)) if message == "last quota error" + )); +} + +#[tokio::test] +async fn empty_browser_candidates_return_missing_credentials() { + let result = fetch_web_sessions(Vec::new(), |_| async { + panic!("the loader must not run for an empty candidate list") + }) + .await; + + assert!(matches!( + result, + Err(ProviderError::Other(message)) if message == VENICE_MISSING_CREDENTIALS_MESSAGE + )); +} + +#[tokio::test] +async fn browser_candidates_without_session_credentials_are_skipped_in_order() { + use crate::browser::detection::BrowserType; + use std::cell::RefCell; + + let mut edge_cookie = browser_cookie("__session", "venice.ai"); + edge_cookie.value = "edge-session".into(); + let mut firefox_cookie = browser_cookie(VENICE_SESSION_COOKIE, "venice.ai"); + firefox_cookie.value = "firefox-session".into(); + let credentials = browser_session_candidates(vec![ + ( + BrowserType::Chrome, + vec![browser_cookie("__client", "venice.ai")], + ), + (BrowserType::Edge, vec![edge_cookie]), + (BrowserType::Firefox, vec![firefox_cookie]), + ]); + assert_eq!( + credentials + .iter() + .map(|(browser, _)| *browser) + .collect::>(), + [BrowserType::Edge, BrowserType::Firefox] + ); + assert_eq!( + credentials + .iter() + .map(|(_, credential)| credential_value(credential)) + .collect::>(), + ["edge-session", "firefox-session"] + ); + + let calls = RefCell::new(0); + let result = fetch_web_sessions(credentials, |_| { + *calls.borrow_mut() += 1; + async { Err(VeniceWebFailure::InvalidSession) } + }) + .await; + assert_eq!(*calls.borrow(), 2); + assert!( + matches!(result, Err(ProviderError::Other(message)) if message == VENICE_INVALID_SESSION_MESSAGE) + ); + + let no_credentials = browser_session_candidates(vec![( + BrowserType::Chrome, + vec![browser_cookie("__client", "venice.ai")], + )]); + let called = RefCell::new(false); + let result = fetch_web_sessions(no_credentials, |_| { + *called.borrow_mut() = true; + async { Err(VeniceWebFailure::InvalidSession) } + }) + .await; + assert!(!*called.borrow()); + assert!( + matches!(result, Err(ProviderError::Other(message)) if message == VENICE_MISSING_CREDENTIALS_MESSAGE) + ); +}