Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions rust/src/providers/claude/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -968,6 +968,14 @@ impl ClaudeProvider {
return Ok(result);
}

// A session key in the environment is the sign-in of VibeTV's own
// Claude window (#273). It names the account the customer chose, so a
// signed-in Claude Code (another account) must not answer instead, and
// an expired window sign-in stays an error rather than falling back.
if ClaudeWebApiFetcher::resolve_session_key_from_env().is_some() {
return self.fetch_via_web(ctx).await;
}

// Upstream 0.50.1 #2516: track whether OAuth failed with a revocation.
let oauth_result = self.fetch_via_oauth(ctx).await;
let oauth_revoked = oauth_result
Expand Down
44 changes: 43 additions & 1 deletion rust/src/providers/claude/web_api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,7 @@ impl ClaudeWebApiFetcher {
headers
}

fn resolve_session_key_from_env() -> Option<String> {
pub(super) fn resolve_session_key_from_env() -> Option<String> {
for env_name in ["CLAUDE_AI_SESSION_KEY", "CLAUDE_WEB_SESSION_KEY"] {
let Ok(value) = std::env::var(env_name) else {
continue;
Expand Down Expand Up @@ -960,6 +960,48 @@ mod tests {
}
}

// A session key in the environment is the sign-in of VibeTV's own Claude
// window (#273). Auto must read claude.ai with it and nothing else, so a
// signed-in Claude Code (a second account) cannot answer instead.
#[test]
fn auto_with_a_window_session_returns_only_the_web_error() {
use crate::core::{FetchContext, Provider};

let _guard = env_lock().lock().expect("env lock");
// SAFETY: env_lock() held for this whole test; restored before release.
unsafe {
std::env::remove_var("CLAUDE_WEB_SESSION_KEY");
std::env::set_var("CLAUDE_AI_SESSION_KEY", "sk-ant-window-session");
}

let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.expect("runtime");
let result = runtime.block_on(crate::providers::ClaudeProvider::new().fetch_usage(
&FetchContext {
web_timeout: 5,
..FetchContext::default()
},
));

// SAFETY: same env_lock()-guarded mutation.
unsafe {
std::env::remove_var("CLAUDE_AI_SESSION_KEY");
}

// The fake key never works, so claude.ai (or the network) refuses it.
let message = result
.expect_err("a fake session key cannot read usage")
.to_string();
assert!(
!message.contains("all configured sources")
&& !message.contains("OAuth:")
&& !message.contains("CLI:"),
"{message}"
);
}

#[test]
fn build_headers_include_required_browser_context() {
let headers = ClaudeWebApiFetcher::build_headers("sessionKey=sk-ant-cookie-format");
Expand Down