use std::io::{Read, Write};
use std::net::{IpAddr, Ipv4Addr, SocketAddr, TcpListener};
use std::time::Duration;
use wiremux_auth::{
AnyTokenProvider, AuthScheme, LoadOptions, Login, OauthPack, ProfileError, ResolvedProfile,
TokenProvider, Wire, list_profiles, load_profile_from_cli, persist_login_tokens,
provider_from_profile, redact_secret_looking, redact_url_origin, sanitize_oauth_error_text,
};
pub const EXIT_OK: i32 = 0;
pub const EXIT_ERROR: i32 = 1;
pub const EXIT_NOT_READY: i32 = 2;
pub fn load_cli_profile(profile_arg: &str) -> Result<ResolvedProfile, ProfileError> {
load_profile_from_cli(profile_arg, &LoadOptions::default())
}
pub fn pick_auth_profile(
flag: Option<String>,
positional: Option<String>,
) -> Result<String, String> {
match (flag, positional) {
(Some(flag), Some(positional)) if flag != positional => {
Err("pass a profile id or --profile, not both (values differ)".into())
}
(Some(profile), _) | (None, Some(profile)) => Ok(profile),
(None, None) => Err("profile id required (positional or --profile)".into()),
}
}
pub fn list_cli_profiles() -> Result<Vec<String>, ProfileError> {
list_profiles(&LoadOptions::default())
}
pub fn validate_report(profile: &ResolvedProfile) -> String {
let mut lines = Vec::new();
lines.push(format!("id: {}", profile.id));
lines.push(format!("schema_version: {}", profile.schema_version));
if let Some(wire) = profile.dialect.wire {
lines.push(format!("wire: {}", wire_name(wire)));
}
push_url(&mut lines, "base_url", profile.http.base_url.as_deref());
push_url(&mut lines, "chat_path", profile.http.chat_path.as_deref());
if let Some(oauth) = &profile.oauth {
push_url(
&mut lines,
"oauth.token_url",
Some(oauth.token_url.as_str()),
);
push_url(
&mut lines,
"oauth.token_url_fallback",
oauth.token_url_fallback.as_deref(),
);
push_url(
&mut lines,
"oauth.authorize_url",
oauth.authorize_url.as_deref(),
);
push_url(
&mut lines,
"oauth.device_auth_url",
oauth.device_auth_url.as_deref(),
);
push_url(
&mut lines,
"oauth.redirect_uri",
oauth.redirect_uri.as_deref(),
);
}
lines.join("\n")
}
fn push_url(lines: &mut Vec<String>, field: &str, value: Option<&str>) {
let Some(value) = value.filter(|s| !s.is_empty()) else {
return;
};
lines.push(format!("{field}: {}", redact_printed_url(value)));
}
fn redact_printed_url(raw: &str) -> String {
if raw.contains("://") {
redact_secret_looking(&redact_url_origin(raw))
} else {
redact_secret_looking(raw)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LoginPlan {
SetupToken {
hint: String,
},
NotReady {
reason: String,
},
Pkce,
Device,
}
pub fn login_plan(profile: &ResolvedProfile) -> LoginPlan {
let Some(oauth) = profile.oauth.as_ref() else {
return LoginPlan::NotReady {
reason: format!(
"auth login needs an [oauth] table on profile `{}` ({})",
profile.id,
wiremux_auth::credential_options(profile)
),
};
};
let client_id = oauth.client_id.as_deref().unwrap_or("");
let login = oauth.login.unwrap_or(Login::None);
match login {
Login::SetupToken => LoginPlan::SetupToken {
hint: redact_secret_looking(
&oauth
.setup_token_hint
.clone()
.filter(|s| !s.trim().is_empty())
.unwrap_or_else(|| "run the vendor setup-token command".into()),
),
},
Login::None => {
let mut reason = String::from(
"oauth.login is none (set pkce or device; shipped openai-codex-oauth stays none until an overlay sets login)",
);
if client_id.is_empty() {
reason.push_str(" and client_id is empty (set a wiremux client id)");
}
LoginPlan::NotReady { reason }
}
Login::Pkce | Login::Device => {
if client_id.is_empty() {
LoginPlan::NotReady {
reason:
"oauth.client_id is empty after subst (set it in the profile or overlay)"
.into(),
}
} else if matches!(login, Login::Device) {
LoginPlan::Device
} else {
LoginPlan::Pkce
}
}
}
}
pub async fn run_login(profile: &ResolvedProfile) -> i32 {
match login_plan(profile) {
LoginPlan::SetupToken { hint } => {
println!("{hint}");
EXIT_NOT_READY
}
LoginPlan::NotReady { reason } => {
eprintln!("{reason}");
EXIT_NOT_READY
}
LoginPlan::Pkce => match run_pkce_or_device(profile).await {
Ok(()) => EXIT_OK,
Err(err) => {
eprintln!("{err}");
EXIT_ERROR
}
},
LoginPlan::Device => {
if let Some(note) = copilot_tos_note(profile) {
println!("{note}");
}
match run_device(profile).await {
Ok(()) => EXIT_OK,
Err(err) => {
eprintln!("{err}");
EXIT_ERROR
}
}
}
}
}
async fn run_pkce_or_device(profile: &ResolvedProfile) -> Result<(), String> {
let oauth = profile.oauth.as_ref().ok_or("missing [oauth]")?;
let redirect = oauth
.redirect_uri
.as_deref()
.ok_or("oauth.redirect_uri is required for PKCE")?;
match bind_redirect(redirect) {
Ok(listener) => run_pkce(oauth, listener).await,
Err(bind_err) => {
if oauth.device_auth_url.is_some() {
eprintln!("loopback bind failed ({bind_err}); falling back to device flow");
run_device(profile).await
} else {
Err(format!(
"loopback bind failed on {redirect} ({bind_err}); device_auth_url not set"
))
}
}
}
}
fn bind_redirect(redirect_uri: &str) -> Result<TcpListener, String> {
let addr = redirect_bind_addr(redirect_uri)?;
TcpListener::bind(addr).map_err(|e| e.to_string())
}
pub fn redirect_bind_addr(redirect_uri: &str) -> Result<SocketAddr, String> {
let rest = redirect_uri
.strip_prefix("http://")
.or_else(|| redirect_uri.strip_prefix("https://"))
.ok_or_else(|| format!("redirect_uri must be http(s): {redirect_uri}"))?;
let hostport = rest.split(['/', '?', '#']).next().unwrap_or(rest);
let (host, port) = match hostport.rsplit_once(':') {
Some((h, p)) => (h, p),
None => (hostport, "80"),
};
let host = host.trim_start_matches('[').trim_end_matches(']');
if host != "127.0.0.1" && !host.eq_ignore_ascii_case("localhost") {
return Err(format!(
"PKCE redirect_uri host must be 127.0.0.1 or localhost, got {host}"
));
}
let port: u16 = port
.parse()
.map_err(|_| format!("invalid redirect_uri port: {port}"))?;
Ok(SocketAddr::from((Ipv4Addr::LOCALHOST, port)))
}
async fn run_pkce(oauth: &OauthPack, listener: TcpListener) -> Result<(), String> {
use wiremux_auth::pkce::{build_auth_url_from_oauth, exchange_auth_code, generate_pkce};
let pkce = generate_pkce().map_err(|e| e.to_string())?;
let url = build_auth_url_from_oauth(oauth, &pkce).map_err(|e| e.to_string())?;
println!("Open this URL to authorize:\n{url}");
let local = listener.local_addr().map_err(|e| e.to_string())?;
println!("Waiting for loopback callback on {local}");
let _ = std::io::stdout().flush();
listener.set_nonblocking(false).map_err(|e| e.to_string())?;
let (mut stream, _) = listener.accept().map_err(|e| e.to_string())?;
stream
.set_read_timeout(Some(Duration::from_secs(5 * 60)))
.ok();
let mut buf = [0u8; 8192];
let n = stream.read(&mut buf).map_err(|e| e.to_string())?;
let req = String::from_utf8_lossy(&buf[..n]);
let first = req.lines().next().unwrap_or("");
let target = first.split_whitespace().nth(1).unwrap_or("");
let query = target.split_once('?').map(|(_, q)| q).unwrap_or("");
let code = reply_to_pkce_callback(&mut stream, query, &pkce.state)?;
let saved = async {
let tokens = exchange_auth_code(oauth, &code, &pkce.code_verifier)
.await
.map_err(|e| e.to_string())?;
persist_login_tokens(oauth, &tokens)
.await
.map_err(|e| e.to_string())
}
.await;
let (ok, body) = pkce_browser_after_save(saved.map(|_| ()));
write_pkce_browser(&mut stream, ok, &body);
if !ok {
return Err(body);
}
println!("login saved");
Ok(())
}
fn copilot_tos_note(profile: &ResolvedProfile) -> Option<String> {
let oauth = profile.oauth.as_ref()?;
if oauth.creds_format != Some(wiremux_auth::CredsFormat::CopilotHosts) {
return None;
}
Some("GitHub Copilot product terms apply to tokens obtained with this profile.".into())
}
async fn run_device(profile: &ResolvedProfile) -> Result<(), String> {
use wiremux_auth::device_flow::{poll_device_token, start_device_flow_from_oauth};
let oauth = profile.oauth.as_ref().ok_or("missing [oauth]")?;
let started = start_device_flow_from_oauth(oauth)
.await
.map_err(|e| e.to_string())?;
println!(
"Visit {} and enter {}",
started.verification_uri, started.user_code
);
if let Some(complete) = started.verification_uri_complete.as_deref() {
println!("or open {complete}");
}
let _ = std::io::stdout().flush();
let tokens = poll_device_token(
oauth,
&started.device_code,
started.interval,
started.expires_in,
)
.await
.map_err(|e| e.to_string())?;
persist_login_tokens(oauth, &tokens)
.await
.map_err(|e| e.to_string())?;
println!("login saved");
Ok(())
}
fn reply_to_pkce_callback(
stream: &mut impl Write,
query: &str,
expected_state: &str,
) -> Result<String, String> {
match pkce_callback_code(query, expected_state) {
Ok(code) => Ok(code),
Err(err) => {
write_pkce_browser(stream, false, &err);
Err(err)
}
}
}
pub(crate) fn pkce_callback_code(query: &str, expected_state: &str) -> Result<String, String> {
let (code, state) = pkce_callback_from_query(query)?;
if state != expected_state {
return Err("callback state mismatch".into());
}
Ok(code)
}
fn pkce_browser_after_save(saved: Result<(), String>) -> (bool, String) {
match saved {
Ok(()) => (true, "ok, you can close this tab".into()),
Err(err) => (false, redact_secret_looking(&err)),
}
}
fn pkce_browser_response(ok: bool, body: &str) -> Vec<u8> {
let status = if ok { "200 OK" } else { "400 Bad Request" };
format!("HTTP/1.1 {status}\r\nContent-Type: text/plain\r\nConnection: close\r\n\r\n{body}\n")
.into_bytes()
}
fn write_pkce_browser(stream: &mut impl Write, ok: bool, body: &str) {
let _ = stream.write_all(&pkce_browser_response(ok, body));
}
pub(crate) fn pkce_callback_from_query(query: &str) -> Result<(String, String), String> {
let mut code = None;
let mut state = None;
let mut error = None;
let mut error_description = None;
for pair in query.split('&') {
if pair.is_empty() {
continue;
}
if let Some((k, v)) = pair.split_once('=') {
match k {
"code" => code = Some(url_decode(v)),
"state" => state = Some(url_decode(v)),
"error" => error = Some(url_decode(v)),
"error_description" => error_description = Some(url_decode(v)),
_ => {}
}
}
}
let code_ok = code.as_ref().is_some_and(|c| !c.is_empty());
let state_ok = state.as_ref().is_some_and(|s| !s.is_empty());
if code_ok && state_ok {
return Ok((code.unwrap(), state.unwrap()));
}
if error.is_some() || error_description.is_some() {
return Err(format_pkce_vendor_error(
error.as_deref(),
error_description.as_deref(),
));
}
if !code_ok {
return Err("callback missing code".into());
}
Err("callback missing state".into())
}
fn format_pkce_vendor_error(error: Option<&str>, description: Option<&str>) -> String {
let err = error.filter(|s| !s.is_empty()).unwrap_or("error");
let raw = match description.filter(|s| !s.is_empty()) {
Some(desc) => format!("{err}: {desc}"),
None => err.to_owned(),
};
let summary = sanitize_oauth_error_text(&raw);
if summary.is_empty() {
"callback error".into()
} else {
format!("callback error {summary}")
}
}
pub(crate) fn url_decode(s: &str) -> String {
let bytes = s.as_bytes();
let mut out = Vec::with_capacity(bytes.len());
let mut i = 0;
while i < bytes.len() {
match bytes[i] {
b'+' => {
out.push(b' ');
i += 1;
}
b'%' if i + 2 < bytes.len() => {
let hex = &bytes[i + 1..i + 3];
if let Ok(hex) = std::str::from_utf8(hex)
&& let Ok(v) = u8::from_str_radix(hex, 16)
{
out.push(v);
i += 3;
} else {
out.push(b'%');
i += 1;
}
}
c => {
out.push(c);
i += 1;
}
}
}
String::from_utf8_lossy(&out).into_owned()
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TokenStatus {
pub id: String,
pub available: bool,
pub detail: String,
}
pub async fn token_status(profile: &ResolvedProfile) -> TokenStatus {
let aws = profile
.http
.aws_service
.as_deref()
.is_some_and(|service| !service.is_empty());
if matches!(profile.http.auth_scheme, Some(AuthScheme::None)) && profile.oauth.is_none() && aws
{
return aws_chain_status(profile).await;
}
if profile
.http
.headers
.iter()
.any(|(k, v)| is_profile_auth_header(profile, k) && !v.trim().is_empty())
{
return TokenStatus {
id: profile.id.clone(),
available: true,
detail: "header credential present".into(),
};
}
if matches!(profile.http.auth_scheme, Some(AuthScheme::None)) && profile.oauth.is_none() {
return TokenStatus {
id: profile.id.clone(),
available: true,
detail: "auth_scheme is none".into(),
};
}
let gcp = profile
.http
.gcp_key_env
.as_deref()
.is_some_and(|name| !name.is_empty());
if profile.oauth.is_some() || !profile.access_env.is_empty() || gcp {
match provider_from_profile(profile) {
Ok(provider) => {
if let Some(reason) = provider.offline_block().await {
return TokenStatus {
id: profile.id.clone(),
available: false,
detail: redact_secret_looking(&reason),
};
}
return TokenStatus {
id: profile.id.clone(),
available: true,
detail: "credentials loaded".into(),
};
}
Err(err) if !aws => {
return TokenStatus {
id: profile.id.clone(),
available: false,
detail: redact_secret_looking(&err.to_string()),
};
}
Err(_) => {}
}
}
if aws {
return aws_chain_status(profile).await;
}
TokenStatus {
id: profile.id.clone(),
available: false,
detail: format!(
"no credentials (set {})",
wiremux_auth::credential_options(profile)
),
}
}
#[cfg(any(feature = "proxy", feature = "client"))]
async fn aws_chain_status(profile: &ResolvedProfile) -> TokenStatus {
let service = profile.http.aws_service.as_deref().unwrap_or("bedrock");
match crate::aws_creds::resolve_aws_credentials().await {
Ok(Some(_)) => TokenStatus {
id: profile.id.clone(),
available: true,
detail: format!("aws sigv4 ({service})"),
},
Ok(None) => TokenStatus {
id: profile.id.clone(),
available: false,
detail: crate::aws_creds::missing_creds_message(profile),
},
Err(err) => TokenStatus {
id: profile.id.clone(),
available: false,
detail: redact_secret_looking(&err.to_string()),
},
}
}
#[cfg(not(any(feature = "proxy", feature = "client")))]
async fn aws_chain_status(profile: &ResolvedProfile) -> TokenStatus {
let service = profile.http.aws_service.as_deref().unwrap_or("bedrock");
TokenStatus {
id: profile.id.clone(),
available: false,
detail: format!(
"profile `{}` aws_service={service} needs a bearer token or IAM credentials",
profile.id
),
}
}
fn is_profile_auth_header(profile: &ResolvedProfile, name: &str) -> bool {
crate::profile_auth::is_profile_auth_header(profile, name)
}
pub fn format_status(status: &TokenStatus) -> String {
if status.available && status.detail == "auth_scheme is none" {
return format!(
"profile: {}\ntoken: not required\n{}",
status.id, status.detail
);
}
if status.available {
format!("profile: {}\ntoken: available", status.id)
} else {
format!(
"profile: {}\ntoken: unavailable\n{}",
status.id, status.detail
)
}
}
pub fn parse_listen(s: &str) -> Result<SocketAddr, String> {
let candidate = match s.rsplit_once(':') {
Some((host, port)) if host.eq_ignore_ascii_case("localhost") => {
format!("127.0.0.1:{port}")
}
_ => s.to_string(),
};
let addr: SocketAddr = candidate.parse().map_err(|_| {
format!("proxy listen address must be 127.0.0.1:<port> or localhost:<port> ({s})")
})?;
match addr.ip() {
IpAddr::V4(ip) if ip == Ipv4Addr::LOCALHOST => Ok(addr),
_ => Err(format!("proxy listen address must be 127.0.0.1 ({s})")),
}
}
pub fn parse_wire(s: &str) -> Result<Wire, String> {
match s {
"responses" => Ok(Wire::Responses),
"messages" => Ok(Wire::Messages),
"chat-completions" | "chat" => Ok(Wire::ChatCompletions),
"gemini" => Ok(Wire::Gemini),
"converse" => Ok(Wire::Converse),
other => {
let listed = "responses|messages|chat-completions|gemini|converse";
let mut msg = format!("unknown --from `{other}` ({listed})");
if let Some(suggest) = suggest_from(other) {
msg.push_str(&format!("; did you mean `{suggest}`"));
}
Err(msg)
}
}
}
fn suggest_from(s: &str) -> Option<&'static str> {
if let Some(name) = Wire::suggest(s) {
return Some(name);
}
let folded: String = s
.chars()
.filter(|c| *c != '_' && *c != '-')
.flat_map(char::to_lowercase)
.collect();
if folded.starts_with("chat") {
Some("chat")
} else {
None
}
}
pub fn wire_name(wire: Wire) -> &'static str {
match wire {
Wire::ChatCompletions => "chat-completions",
Wire::Messages => "messages",
Wire::Responses => "responses",
Wire::Gemini => "gemini",
Wire::Converse => "converse",
_ => wire.as_str(),
}
}
#[derive(Debug)]
pub enum ProxyTokenError {
Unauthorized(String),
Gateway(String),
}
impl std::fmt::Display for ProxyTokenError {
fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Unauthorized(detail) | Self::Gateway(detail) => formatter.write_str(detail),
}
}
}
fn proxy_token_error_from_auth(err: wiremux_auth::AuthError) -> ProxyTokenError {
let detail = err.to_string();
if wiremux_auth::is_oauth_transport_error(&err) {
ProxyTokenError::Gateway(detail)
} else {
ProxyTokenError::Unauthorized(detail)
}
}
pub async fn proxy_token(
profile: &ResolvedProfile,
provider: &AnyTokenProvider,
) -> Result<Option<String>, ProxyTokenError> {
if matches!(profile.http.auth_scheme, Some(AuthScheme::None)) && profile.oauth.is_none() {
return Ok(None);
}
if let Some(value) = crate::profile_auth::profile_header_credential(profile) {
let token = value
.strip_prefix("Bearer ")
.or_else(|| value.strip_prefix("bearer "))
.unwrap_or(value)
.trim()
.to_string();
if token.is_empty() {
return Err(ProxyTokenError::Unauthorized(
"empty token is not auth_scheme none".into(),
));
}
return Ok(Some(token));
}
if profile.oauth.is_some() || !profile.access_env.is_empty() || provider_has_token(provider) {
let token = provider
.get_token()
.await
.map_err(proxy_token_error_from_auth)?;
if token.trim().is_empty() {
return Err(ProxyTokenError::Unauthorized(
"empty token is not auth_scheme none".into(),
));
}
return Ok(Some(token));
}
Err(ProxyTokenError::Unauthorized(format!(
"no credentials (set {})",
wiremux_auth::credential_options(profile)
)))
}
fn provider_has_token(provider: &AnyTokenProvider) -> bool {
!matches!(provider, AnyTokenProvider::Static(_))
}
pub use crate::upstream::upstream_url_for_model;
pub fn upstream_url(profile: &ResolvedProfile) -> Result<String, String> {
upstream_url_for_model(profile, None, false)
}
#[cfg(test)]
mod tests {
use super::*;
use wiremux_auth::{
AnyTokenProvider, IsolatedHome, PlantCredentials, StaticToken, parse_profile_str,
};
#[test]
fn redact_printed_url_keeps_origin_only() {
let redacted = redact_printed_url(
"https://user:s3cret@api.example.invalid/v1?api_key=supersecret#frag",
);
assert_eq!(redacted, "https://api.example.invalid");
assert!(!redacted.contains("s3cret"));
assert!(!redacted.contains("supersecret"));
assert!(!redacted.contains("/v1"));
}
fn shipped(id: &str) -> ResolvedProfile {
wiremux_auth::load_profile(
id,
&LoadOptions {
include_user_config: false,
..LoadOptions::default()
},
)
.expect("shipped")
}
#[test]
fn login_plan_anthropic_is_setup_token() {
let profile = shipped("anthropic-oauth");
match login_plan(&profile) {
LoginPlan::SetupToken { hint } => {
assert!(hint.contains("claude setup-token"));
}
other => panic!("expected setup-token, got {other:?}"),
}
}
#[test]
fn login_plan_copilot_gist_device_needs_client_id() {
let path = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../wiremux-auth/tests/gists/github-copilot-device.toml");
let profile =
parse_profile_str(&std::fs::read_to_string(&path).expect("gist")).expect("parse");
match login_plan(&profile) {
LoginPlan::NotReady { reason } => {
assert!(
reason.contains("oauth.client_id"),
"device/pkce empty client_id must name oauth.client_id, got {reason}"
);
assert!(
reason.contains("empty after subst"),
"must say empty after subst, got {reason}"
);
assert!(
reason.contains("profile") && reason.contains("overlay"),
"must say set it in the profile or overlay, got {reason}"
);
}
other => panic!("empty client_id must be not-ready, got {other:?}"),
}
assert_eq!(
profile.oauth.as_ref().and_then(|o| o.creds_format),
Some(wiremux_auth::CredsFormat::CopilotHosts)
);
assert!(
copilot_tos_note(&profile)
.unwrap()
.contains("Copilot product terms")
);
}
#[test]
fn login_plan_openai_is_not_ready() {
let profile = shipped("openai-codex-oauth");
match login_plan(&profile) {
LoginPlan::NotReady { reason } => {
assert_login_none_names_field_and_values(&reason);
assert!(
reason.contains("client_id"),
"openai login plan must keep the empty-client_id hint, got {reason}"
);
}
other => panic!("expected not-ready, got {other:?}"),
}
}
#[test]
fn login_plan_none_with_client_id_names_oauth_login() {
let profile = parse_profile_str(
r#"
schema_version = 1
id = "x"
[oauth]
token_url = "https://auth.example.invalid/token"
client_id = "already-set"
login = "none"
"#,
)
.expect("parse");
match login_plan(&profile) {
LoginPlan::NotReady { reason } => {
assert_login_none_names_field_and_values(&reason);
assert!(
!reason.contains("client_id is empty"),
"set client_id must not keep the empty-client_id hint, got {reason}"
);
}
other => panic!("expected not-ready, got {other:?}"),
}
}
fn assert_login_none_names_field_and_values(reason: &str) {
assert!(
reason.contains("oauth.login"),
"login=none must name oauth.login, got {reason}"
);
assert!(
reason.contains("pkce") && reason.contains("device"),
"login=none must list pkce and device, got {reason}"
);
assert!(
reason.contains("openai-codex-oauth"),
"must say shipped openai-codex-oauth stays none, got {reason}"
);
assert!(
reason.contains("overlay"),
"must say overlay sets login, got {reason}"
);
}
#[test]
fn pick_auth_profile_accepts_positional_or_flag() {
assert_eq!(
pick_auth_profile(None, Some("anthropic-oauth".into())).unwrap(),
"anthropic-oauth"
);
assert_eq!(
pick_auth_profile(Some("openai-codex".into()), None).unwrap(),
"openai-codex"
);
assert_eq!(
pick_auth_profile(Some("xai-oauth".into()), Some("xai-oauth".into())).unwrap(),
"xai-oauth"
);
let err = pick_auth_profile(None, None).expect_err("neither");
assert!(
err.contains("positional") && err.contains("--profile"),
"{err}"
);
let err = pick_auth_profile(Some("a".into()), Some("b".into())).expect_err("both");
assert!(err.contains("not both"), "{err}");
}
#[test]
fn parse_wire_accepts_gemini() {
assert_eq!(parse_wire("gemini").unwrap(), Wire::Gemini);
assert_eq!(wire_name(Wire::Gemini), "gemini");
}
#[test]
fn parse_wire_typo_suggests_close_matches() {
assert_eq!(parse_wire("chat").unwrap(), Wire::ChatCompletions);
for (input, want) in [
("chat_completions", "chat-completions"),
("ChatCompletions", "chat-completions"),
("response", "responses"),
("chatt", "chat"),
] {
let err = parse_wire(input).expect_err(input);
assert!(
err.contains(&format!("unknown --from `{input}`")),
"{input} must echo the unknown value, got {err}"
);
assert!(
err.contains("responses")
&& err.contains("messages")
&& err.contains("chat-completions")
&& err.contains("gemini"),
"{input} must list legal --from values, got {err}"
);
assert!(
err.to_ascii_lowercase().contains("did you mean")
&& err.contains(&format!("`{want}`")),
"{input} should suggest {want}, got {err}"
);
}
}
#[test]
fn gemini_upstream_url_substitutes_model() {
let profile = parse_profile_str(
r#"
schema_version = 1
id = "g"
wire = "gemini"
base_url = "https://generativelanguage.googleapis.com"
"#,
)
.expect("parse");
let url = upstream_url_for_model(&profile, Some("gemini-2.5-flash"), false).expect("url");
assert!(
url.ends_with("/v1beta/models/gemini-2.5-flash:generateContent"),
"{url}"
);
let stream = upstream_url_for_model(&profile, Some("gemini-2.5-flash"), true).expect("url");
assert!(
stream.ends_with("/v1beta/models/gemini-2.5-flash:streamGenerateContent?alt=sse"),
"{stream}"
);
let embedded =
upstream_url_for_model(&profile, Some("gemini-2.5-flash:generateContent"), true)
.expect("url");
assert!(
embedded.ends_with(
"/v1beta/models/gemini-2.5-flash:generateContent:streamGenerateContent?alt=sse"
),
"{embedded}"
);
assert!(
!embedded.contains(":streamGenerateContent?alt=sse:generateContent"),
"{embedded}"
);
}
#[test]
fn stream_rewrite_keeps_query_and_fragment() {
let gemini = parse_profile_str(
r#"
schema_version = 1
id = "g"
wire = "gemini"
base_url = "https://generativelanguage.googleapis.com"
chat_path = "/v1beta/models/{model}:generateContent?key=test"
"#,
)
.expect("gemini");
let url = upstream_url_for_model(&gemini, Some("gemini-2.5-flash"), true).expect("query");
assert!(
url.ends_with(":streamGenerateContent?key=test&alt=sse"),
"{url}"
);
assert!(!url.contains("?key=test?"), "{url}");
let fragment = parse_profile_str(
r#"
schema_version = 1
id = "g"
wire = "gemini"
base_url = "https://generativelanguage.googleapis.com"
chat_path = "/v1beta/models/{model}:generateContent#section"
"#,
)
.expect("fragment");
let url =
upstream_url_for_model(&fragment, Some("gemini-2.5-flash"), true).expect("fragment");
assert!(
url.ends_with(":streamGenerateContent?alt=sse#section"),
"{url}"
);
let already = parse_profile_str(
r#"
schema_version = 1
id = "g"
wire = "gemini"
base_url = "https://generativelanguage.googleapis.com"
chat_path = "/v1beta/models/{model}:streamGenerateContent?key=test"
"#,
)
.expect("already streaming");
let url = upstream_url_for_model(&already, Some("gemini-2.5-flash"), true).expect("stay");
assert!(url.ends_with(":streamGenerateContent?key=test"), "{url}");
assert!(!url.contains("alt=sse"), "{url}");
let vertex = parse_profile_str(
r#"
schema_version = 1
id = "v"
wire = "messages"
base_url = "https://us-central1-aiplatform.googleapis.com"
chat_path = "/v1/projects/p/locations/us-central1/publishers/anthropic/models/{model}:rawPredict?foo=1"
"#,
)
.expect("vertex");
let url = upstream_url_for_model(&vertex, Some("claude-sonnet-4"), true).expect("raw");
assert!(url.ends_with(":streamRawPredict?foo=1"), "{url}");
let converse = parse_profile_str(
r#"
schema_version = 1
id = "c"
wire = "converse"
base_url = "https://bedrock-runtime.us-east-1.amazonaws.com"
chat_path = "/model/{model}/converse?foo=1"
"#,
)
.expect("converse");
let url =
upstream_url_for_model(&converse, Some("amazon.nova-lite-v1:0"), true).expect("stream");
assert!(url.ends_with("/converse-stream?foo=1"), "{url}");
let staying = parse_profile_str(
r#"
schema_version = 1
id = "c"
wire = "converse"
base_url = "https://bedrock-runtime.us-east-1.amazonaws.com"
chat_path = "/model/{model}/converse-stream?foo=1"
"#,
)
.expect("already stream");
let url =
upstream_url_for_model(&staying, Some("amazon.nova-lite-v1:0"), true).expect("stay");
assert!(url.ends_with("/converse-stream?foo=1"), "{url}");
assert!(!url.contains("/converse-stream-stream"), "{url}");
}
#[test]
fn missing_model_does_not_call_the_placeholder() {
let profile = parse_profile_str(
r#"
schema_version = 1
id = "g"
wire = "gemini"
base_url = "https://generativelanguage.googleapis.com"
"#,
)
.expect("parse");
for model in [None, Some("")] {
let err = upstream_url_for_model(&profile, model, false)
.expect_err("placeholder must not be a url");
assert!(
err.contains("{model}"),
"missing model must name the placeholder, got {err}"
);
}
let fixed = parse_profile_str(
r#"
schema_version = 1
id = "c"
wire = "chat-completions"
base_url = "https://example.invalid"
chat_path = "/v1/chat/completions"
"#,
)
.expect("parse fixed");
let url = upstream_url_for_model(&fixed, None, false).expect("fixed path");
assert!(url.ends_with("/v1/chat/completions"), "{url}");
assert!(!url.contains("{model}"), "{url}");
let absolute = parse_profile_str(
r#"
schema_version = 1
id = "c"
wire = "chat-completions"
base_url = "https://example.invalid"
chat_path = "HTTPS://api.example/v1/chat/completions"
"#,
)
.expect("uppercase scheme parses");
let url = upstream_url_for_model(&absolute, None, false).expect("absolute");
assert_eq!(
url, "HTTPS://api.example/v1/chat/completions",
"an absolute chat_path must not be joined onto base_url, got {url}"
);
}
#[test]
fn model_segment_encodes_slash_query_and_fragment() {
let profile = parse_profile_str(
r#"
schema_version = 1
id = "g"
wire = "gemini"
base_url = "https://generativelanguage.googleapis.com"
"#,
)
.expect("parse");
let traversed = upstream_url_for_model(&profile, Some("../x"), false).expect("url");
assert!(
traversed.contains("/v1beta/models/..%2Fx:generateContent"),
"slash in model must stay one segment, got {traversed}"
);
assert!(
!traversed.contains("/models/../x"),
"unencoded traversal must not appear, got {traversed}"
);
let query = upstream_url_for_model(&profile, Some("a?b"), false).expect("url");
assert!(
query.contains("/v1beta/models/a%3Fb:generateContent"),
"query delimiter must encode, got {query}"
);
assert!(!query.contains("?b:generateContent"), "{query}");
let frag = upstream_url_for_model(&profile, Some("a#b"), false).expect("url");
assert!(
frag.contains("/v1beta/models/a%23b:generateContent"),
"fragment delimiter must encode, got {frag}"
);
let percent = upstream_url_for_model(&profile, Some("a%b"), false).expect("url");
assert!(
percent.contains("/v1beta/models/a%25b:generateContent"),
"percent must encode, got {percent}"
);
let backslash = upstream_url_for_model(&profile, Some("a\\b"), false).expect("url");
assert!(
backslash.contains("/v1beta/models/a%5Cb:generateContent"),
"backslash must encode, got {backslash}"
);
let control = upstream_url_for_model(&profile, Some("a\nb"), false).expect("url");
assert!(
control.contains("/v1beta/models/a%0Ab:generateContent"),
"control must encode, got {control}"
);
let dot = upstream_url_for_model(&profile, Some("."), false).expect_err("dot segment");
assert!(
dot.contains("dot segment"),
"a lone dot must not collapse the path, got {dot}"
);
let dotdot = upstream_url_for_model(&profile, Some(".."), false).expect_err("dotdot");
assert!(
dotdot.contains("dot segment"),
"a lone dotdot must not collapse the path, got {dotdot}"
);
let dotted = upstream_url_for_model(&profile, Some("gemini-2.5-flash"), false)
.expect("dotted model id");
assert!(
dotted.contains("/v1beta/models/gemini-2.5-flash:generateContent"),
"dots inside a model id stay, got {dotted}"
);
}
#[test]
fn bedrock_arn_model_encodes_slash_as_one_segment() {
let profile = parse_profile_str(
r#"
schema_version = 1
id = "amazon-bedrock"
wire = "converse"
base_url = "https://bedrock-runtime.us-east-1.amazonaws.com"
chat_path = "/model/{model}/converse"
"#,
)
.expect("parse");
let arn = "arn:aws:bedrock:us-east-1:123:inference-profile/us.anthropic.claude";
let url = upstream_url_for_model(&profile, Some(arn), false).expect("url");
assert!(
url.ends_with(
"/model/arn:aws:bedrock:us-east-1:123:inference-profile%2Fus.anthropic.claude/converse"
),
"{url}"
);
}
#[test]
fn converse_stream_url_uses_converse_stream() {
let profile = parse_profile_str(
r#"
schema_version = 1
id = "amazon-bedrock"
wire = "converse"
base_url = "https://bedrock-runtime.us-east-1.amazonaws.com"
chat_path = "/model/{model}/converse"
"#,
)
.expect("parse");
let url =
upstream_url_for_model(&profile, Some("amazon.nova-lite-v1:0"), false).expect("url");
assert!(
url.ends_with("/model/amazon.nova-lite-v1:0/converse"),
"{url}"
);
let stream =
upstream_url_for_model(&profile, Some("amazon.nova-lite-v1:0"), true).expect("stream");
assert!(
stream.ends_with("/model/amazon.nova-lite-v1:0/converse-stream"),
"{stream}"
);
}
#[test]
fn vertex_global_host_rewrites_to_aiplatform() {
let profile = parse_profile_str(
r#"
schema_version = 1
id = "vertex-gemini-global"
wire = "gemini"
base_url = "https://global-aiplatform.googleapis.com"
chat_path = "/v1/projects/p/locations/global/publishers/google/models/{model}:generateContent"
"#,
)
.expect("parse");
let url = upstream_url_for_model(&profile, Some("gemini-2.5-flash"), false).expect("url");
assert!(
url.starts_with("https://aiplatform.googleapis.com/"),
"global host must drop the global- prefix, got {url}"
);
assert!(
!url.contains("global-aiplatform.googleapis.com"),
"joined URL must not keep the invalid global host, got {url}"
);
assert!(
url.contains("/locations/global/"),
"path must keep /locations/global/, got {url}"
);
assert!(
url.ends_with("/v1/projects/p/locations/global/publishers/google/models/gemini-2.5-flash:generateContent"),
"{url}"
);
}
#[test]
fn vertex_regional_host_stays_prefixed() {
let profile = parse_profile_str(
r#"
schema_version = 1
id = "vertex-gemini-regional"
wire = "gemini"
base_url = "https://us-central1-aiplatform.googleapis.com"
chat_path = "/v1/projects/p/locations/us-central1/publishers/google/models/{model}:generateContent"
"#,
)
.expect("parse");
let url = upstream_url_for_model(&profile, Some("gemini-2.5-flash"), false).expect("url");
assert!(
url.starts_with("https://us-central1-aiplatform.googleapis.com/"),
"regional host must stay prefixed, got {url}"
);
assert!(url.contains("/locations/us-central1/"), "{url}");
}
#[test]
fn vertex_messages_stream_rewrites_raw_predict() {
let profile = parse_profile_str(
r#"
schema_version = 1
id = "vertex-anthropic"
wire = "messages"
base_url = "https://us-central1-aiplatform.googleapis.com"
chat_path = "/v1/projects/p/locations/us-central1/publishers/anthropic/models/{model}:rawPredict"
"#,
)
.expect("parse");
let unary = upstream_url_for_model(&profile, Some("claude-sonnet-4"), false).expect("url");
assert!(
unary.ends_with(":rawPredict"),
"unary must stay :rawPredict, got {unary}"
);
assert!(
!unary.contains(":streamRawPredict"),
"unary must not rewrite to stream, got {unary}"
);
let stream =
upstream_url_for_model(&profile, Some("claude-sonnet-4"), true).expect("stream");
assert!(
stream.ends_with(":streamRawPredict"),
"stream=true must rewrite :rawPredict to :streamRawPredict, got {stream}"
);
assert!(
!stream.contains("?alt=sse"),
"Messages Vertex stream must not add ?alt=sse, got {stream}"
);
let embedded = upstream_url_for_model(&profile, Some("claude-sonnet-4:rawPredict"), true)
.expect("stream");
assert!(
embedded.ends_with(":rawPredict:streamRawPredict"),
"model text that contains :rawPredict must stay in the model segment, got {embedded}"
);
assert!(
!embedded.contains(":streamRawPredict:rawPredict"),
"{embedded}"
);
}
#[test]
fn parse_listen_rejects_wildcard() {
assert!(parse_listen("0.0.0.0:0").is_err());
let addr = parse_listen("127.0.0.1:0").expect("loopback ephemeral listen");
assert_eq!(addr, "127.0.0.1:0".parse().expect("socket addr"));
}
#[test]
fn parse_listen_accepts_localhost() {
let addr = parse_listen("localhost:8787").expect("localhost");
assert_eq!(addr, "127.0.0.1:8787".parse().expect("socket addr"));
let upper = parse_listen("LOCALHOST:9").expect("LOCALHOST");
assert_eq!(upper, "127.0.0.1:9".parse().expect("socket addr"));
let err = parse_listen("example.com:8787").expect_err("public name");
assert!(
err.contains("example.com:8787"),
"error should name the input, got {err}"
);
assert!(parse_listen("[::1]:8787").is_err());
}
#[test]
fn parse_listen_missing_port_asks_for_port() {
for input in ["127.0.0.1", "localhost", "127.0.0.1:99999"] {
let err = parse_listen(input).expect_err(input);
let host_only = format!("proxy listen address must be 127.0.0.1 ({input})");
assert!(
err.contains("<port>"),
"unparsed listen address must ask for a port, got {err}"
);
assert!(err.contains(input), "error must name the input, got {err}");
assert_ne!(
err, host_only,
"must not claim the host is forbidden, got {err}"
);
}
}
#[test]
fn parse_listen_parsed_non_loopback_keeps_host_error() {
let err = parse_listen("0.0.0.0:1").expect_err("wildcard");
assert_eq!(err, "proxy listen address must be 127.0.0.1 (0.0.0.0:1)");
assert!(
!err.contains("<port>"),
"a parsed port must not use the missing-port wording, got {err}"
);
let v6 = parse_listen("[::1]:8787").expect_err("ipv6 loopback");
assert!(
!v6.contains("<port>"),
"parsed ipv6 must keep the host error, got {v6}"
);
assert!(v6.contains("[::1]:8787"), "{v6}");
}
#[tokio::test]
async fn token_status_access_env_is_available() {
let _home = IsolatedHome::new();
_home.set_env("XAI_API_KEY", "xai-status-must-not-print");
let profile = parse_profile_str(
r#"
schema_version = 1
id = "xai"
wire = "chat-completions"
auth_scheme = "bearer"
access_env = ["XAI_API_KEY", "GROK_API_KEY"]
base_url = "https://api.x.ai"
"#,
)
.expect("parse");
let status = token_status(&profile).await;
assert!(status.available, "{}", status.detail);
assert!(
!status.detail.contains("xai-status-must-not-print"),
"status leaked the token: {}",
status.detail
);
}
#[tokio::test]
async fn token_status_expired_without_refresh_is_unavailable() {
let home = IsolatedHome::new();
let creds = home.plant_credentials(PlantCredentials::JsonPointer {
relative_path: ".config/wiremux/expired-status.json",
document: serde_json::json!({
"tokens": { "access": "stale-status-token", "expiry_unix": 1 }
}),
});
let creds = creds.to_string_lossy().replace('\\', "/");
let profile = parse_profile_str(&format!(
r#"
schema_version = 1
id = "expired-oauth"
wire = "chat-completions"
base_url = "https://example.invalid"
[oauth]
token_url = "https://example.invalid/oauth/token"
client_id = "example-public-client"
creds_format = "json-pointer"
creds_path = "{creds}"
access_token_ptr = "/tokens/access"
refresh_token_ptr = "/tokens/refresh"
expires_ptr = "/tokens/expiry_unix"
expires_unit = "s"
login = "none"
"#
))
.expect("parse");
let status = token_status(&profile).await;
assert!(!status.available, "{}", status.detail);
assert!(
status.detail.contains("expired") && status.detail.contains("refresh"),
"{}",
status.detail
);
assert!(
!status.detail.contains("stale-status-token"),
"status leaked the token: {}",
status.detail
);
let shown = format_status(&status);
assert!(shown.contains("token: unavailable"), "{shown}");
}
#[tokio::test]
async fn token_status_no_credentials_names_what_to_set() {
let profile = parse_profile_str(
r#"
schema_version = 1
id = "no-creds"
wire = "chat-completions"
base_url = "http://127.0.0.1:9"
"#,
)
.expect("parse");
let status = token_status(&profile).await;
assert!(!status.available);
let detail = &status.detail;
assert!(detail.contains("no credentials"), "{detail}");
assert!(
detail.contains("[headers]")
&& detail.contains("Authorization")
&& detail.contains("x-api-key"),
"must name [headers] Authorization or x-api-key, got {detail}"
);
assert!(
detail.contains("[oauth]"),
"must name [oauth], got {detail}"
);
assert!(
detail.contains("auth_scheme") && detail.contains("none"),
"must name auth_scheme = none, got {detail}"
);
let text = format_status(&status);
assert!(text.contains("unavailable"), "{text}");
assert!(text.contains("[headers]"), "{text}");
}
#[tokio::test]
async fn goog_api_key_header_is_the_credential() {
let profile = parse_profile_str(
r#"
schema_version = 1
id = "goog"
wire = "gemini"
base_url = "http://127.0.0.1"
auth_scheme = "header:x-goog-api-key"
access_env = ["GEMINI_API_KEY"]
[headers]
x-goog-api-key = "goog-secret"
"#,
)
.expect("parse");
let status = token_status(&profile).await;
assert!(status.available, "{}", status.detail);
assert_eq!(status.detail, "header credential present");
assert!(!format_status(&status).contains("goog-secret"));
let provider = AnyTokenProvider::from(StaticToken::new("env-secret"));
let token = proxy_token(&profile, &provider).await.expect("token");
assert_eq!(token.as_deref(), Some("goog-secret"));
}
#[tokio::test]
async fn token_status_none_auth_is_available() {
let profile = parse_profile_str(
r#"
schema_version = 1
id = "none-auth"
wire = "chat-completions"
auth_scheme = "none"
base_url = "http://127.0.0.1:9"
"#,
)
.expect("parse");
let status = token_status(&profile).await;
assert!(status.available);
let text = format_status(&status);
assert!(text.contains("token: not required"), "{text}");
assert!(text.contains("auth_scheme is none"), "{text}");
assert!(!text.contains("token: available"), "{text}");
assert!(!text.contains("http://"));
let provider = AnyTokenProvider::from(StaticToken::new(""));
let token = proxy_token(&profile, &provider).await.expect("none");
assert!(token.is_none(), "{token:?}");
}
#[tokio::test]
async fn empty_bearer_is_not_sent() {
let profile = parse_profile_str(
r#"
schema_version = 1
id = "bearer"
wire = "chat-completions"
auth_scheme = "bearer"
access_env = ["WIREMUX_EMPTY_BEARER"]
base_url = "http://127.0.0.1"
"#,
)
.expect("parse");
let provider = AnyTokenProvider::from(StaticToken::new(" "));
let err = proxy_token(&profile, &provider)
.await
.expect_err("empty bearer")
.to_string();
assert!(err.contains("empty token"), "{err}");
let bare = parse_profile_str(
r#"
schema_version = 1
id = "bare"
wire = "chat-completions"
base_url = "http://127.0.0.1"
"#,
)
.expect("parse");
let err = proxy_token(&bare, &provider)
.await
.expect_err("no credentials")
.to_string();
assert!(err.contains("auth_scheme"), "{err}");
assert!(err.contains("[headers]"), "{err}");
let header = parse_profile_str(
r#"
schema_version = 1
id = "blank-header"
wire = "chat-completions"
auth_scheme = "bearer"
base_url = "http://127.0.0.1"
[headers]
Authorization = "Bearer "
"#,
)
.expect("parse");
let err = proxy_token(&header, &provider)
.await
.expect_err("blank bearer header")
.to_string();
assert!(err.contains("empty token"), "{err}");
}
#[test]
fn login_plan_without_oauth_names_header_credentials() {
let profile = parse_profile_str(
r#"
schema_version = 1
id = "header-only"
wire = "responses"
auth_scheme = "bearer"
"#,
)
.expect("parse");
match login_plan(&profile) {
LoginPlan::NotReady { reason } => {
assert!(
reason.contains("auth login needs an [oauth] table"),
"login plan must say it needs oauth"
);
assert!(reason.contains("[headers]"), "login plan must name headers");
assert!(
reason.contains("access_env"),
"login plan must name access_env"
);
assert!(
reason.contains("auth_scheme"),
"login plan must name auth_scheme"
);
assert!(
reason.contains("header-only"),
"login plan must name the profile"
);
}
other => panic!("expected not-ready, got {other:?}"),
}
}
#[cfg(any(feature = "proxy", feature = "client"))]
#[tokio::test]
async fn token_status_auth_none_with_aws_keys_is_sigv4() {
let home = IsolatedHome::new();
crate::aws_creds::clear_aws_credential_cache();
home.set_env("AWS_ACCESS_KEY_ID", "status-probe-key");
home.set_env("AWS_SECRET_ACCESS_KEY", "status-probe-secret");
home.set_env("AWS_EC2_METADATA_DISABLED", "true");
let profile = parse_profile_str(
r#"
schema_version = 1
id = "bedrock-iam"
wire = "converse"
auth_scheme = "none"
aws_service = "bedrock"
aws_region = "us-east-1"
base_url = "https://bedrock-runtime.us-east-1.amazonaws.com"
"#,
)
.expect("parse");
let status = token_status(&profile).await;
assert!(status.available, "{}", status.detail);
assert!(
status.detail.contains("aws sigv4"),
"expected sigv4, got {}",
status.detail
);
let text = format_status(&status);
assert!(!text.contains("not required"), "{text}");
assert!(
!status.detail.contains("status-probe-key"),
"{}",
status.detail
);
let _ = home;
}
#[cfg(any(feature = "proxy", feature = "client"))]
#[tokio::test]
async fn token_status_auth_none_without_aws_keys_names_chain() {
let home = IsolatedHome::new();
crate::aws_creds::clear_aws_credential_cache();
home.set_env("AWS_EC2_METADATA_DISABLED", "true");
let profile = parse_profile_str(
r#"
schema_version = 1
id = "bedrock-iam"
wire = "converse"
auth_scheme = "none"
aws_service = "bedrock"
aws_region = "us-east-1"
base_url = "https://bedrock-runtime.us-east-1.amazonaws.com"
"#,
)
.expect("parse");
let status = token_status(&profile).await;
assert!(!status.available, "{}", status.detail);
assert_eq!(
status.detail,
crate::aws_creds::missing_creds_message(&profile)
);
assert!(!status.detail.contains("not required"), "{}", status.detail);
assert!(
!status.detail.contains("auth_scheme is none"),
"{}",
status.detail
);
let _ = home;
}
#[cfg(any(feature = "proxy", feature = "client"))]
#[tokio::test]
async fn token_status_bedrock_bearer_falls_through_to_sigv4() {
let home = IsolatedHome::new();
crate::aws_creds::clear_aws_credential_cache();
home.set_env("AWS_ACCESS_KEY_ID", "status-probe-key");
home.set_env("AWS_SECRET_ACCESS_KEY", "status-probe-secret");
home.set_env("AWS_EC2_METADATA_DISABLED", "true");
let profile = parse_profile_str(
r#"
schema_version = 1
id = "amazon-bedrock"
wire = "converse"
auth_scheme = "bearer"
access_env = ["AWS_BEARER_TOKEN_BEDROCK"]
aws_service = "bedrock"
aws_region = "us-east-1"
base_url = "https://bedrock-runtime.us-east-1.amazonaws.com"
"#,
)
.expect("parse");
let status = token_status(&profile).await;
assert!(status.available, "{}", status.detail);
assert!(status.detail.contains("aws sigv4"), "{}", status.detail);
assert!(
!status.detail.contains("AWS_BEARER_TOKEN_BEDROCK"),
"{}",
status.detail
);
let _ = home;
}
#[cfg(any(feature = "proxy", feature = "client"))]
#[tokio::test]
async fn token_status_bedrock_bearer_wins_over_sigv4() {
let home = IsolatedHome::new();
crate::aws_creds::clear_aws_credential_cache();
home.set_env("AWS_BEARER_TOKEN_BEDROCK", "bearer-must-not-print");
home.set_env("AWS_ACCESS_KEY_ID", "status-probe-key");
home.set_env("AWS_SECRET_ACCESS_KEY", "status-probe-secret");
home.set_env("AWS_EC2_METADATA_DISABLED", "true");
let profile = parse_profile_str(
r#"
schema_version = 1
id = "amazon-bedrock"
wire = "converse"
auth_scheme = "bearer"
access_env = ["AWS_BEARER_TOKEN_BEDROCK"]
aws_service = "bedrock"
aws_region = "us-east-1"
base_url = "https://bedrock-runtime.us-east-1.amazonaws.com"
"#,
)
.expect("parse");
let status = token_status(&profile).await;
assert!(status.available, "{}", status.detail);
assert_eq!(status.detail, "credentials loaded");
assert!(!status.detail.contains("bearer-must-not-print"));
assert!(!status.detail.contains("status-probe-key"));
let _ = home;
}
#[cfg(any(feature = "proxy", feature = "client"))]
#[tokio::test]
async fn token_status_auth_none_aws_ignores_header() {
let home = IsolatedHome::new();
crate::aws_creds::clear_aws_credential_cache();
home.set_env("AWS_ACCESS_KEY_ID", "status-probe-key");
home.set_env("AWS_SECRET_ACCESS_KEY", "status-probe-secret");
home.set_env("AWS_EC2_METADATA_DISABLED", "true");
let profile = parse_profile_str(
r#"
schema_version = 1
id = "bedrock-iam"
wire = "converse"
auth_scheme = "none"
aws_service = "bedrock"
aws_region = "us-east-1"
base_url = "https://bedrock-runtime.us-east-1.amazonaws.com"
[headers]
Authorization = "Bearer header-secret"
"#,
)
.expect("parse");
let status = token_status(&profile).await;
assert!(status.available, "{}", status.detail);
assert!(status.detail.contains("aws sigv4"), "{}", status.detail);
assert_ne!(status.detail, "header credential present");
assert!(!status.detail.contains("header-secret"));
let _ = home;
}
#[tokio::test]
async fn token_status_none_auth_header_is_present() {
let profile = parse_profile_str(
r#"
schema_version = 1
id = "header-none"
wire = "chat-completions"
auth_scheme = "none"
base_url = "http://127.0.0.1:9"
[headers]
Authorization = "Bearer header-secret"
"#,
)
.expect("parse");
let status = token_status(&profile).await;
assert!(status.available, "{}", status.detail);
assert_eq!(status.detail, "header credential present");
let text = format_status(&status);
assert!(!text.contains("not required"), "{text}");
assert!(!text.contains("header-secret"), "{text}");
}
#[tokio::test]
async fn token_status_gcp_key_file_is_loaded() {
let home = IsolatedHome::new();
let path = home.path().join("adc.json");
std::fs::write(
&path,
r#"{"type":"authorized_user","client_id":"123.apps.googleusercontent.com","client_secret":"user-secret","refresh_token":"1//refresh-me"}"#,
)
.expect("write adc");
home.set_env(
"GOOGLE_APPLICATION_CREDENTIALS",
path.to_str().expect("utf8"),
);
let profile = parse_profile_str(
r#"
schema_version = 1
id = "vertex-adc"
wire = "gemini"
gcp_key_env = "GOOGLE_APPLICATION_CREDENTIALS"
base_url = "https://example.googleapis.com"
"#,
)
.expect("parse");
let status = token_status(&profile).await;
assert!(status.available, "{}", status.detail);
assert_eq!(status.detail, "credentials loaded");
assert!(!status.detail.contains("user-secret"), "{}", status.detail);
assert!(!status.detail.contains("refresh-me"), "{}", status.detail);
let _ = home;
}
#[test]
fn format_pkce_vendor_error_redacts_secret_looking() {
let sk = format_pkce_vendor_error(
Some("invalid_request"),
Some("rejected token sk-ant-oat01-LEAKED"),
);
assert!(!sk.contains("sk-ant-oat01-LEAKED"), "API key leaked: {sk}");
assert!(sk.contains("invalid_request"), "{sk}");
let jwt = format_pkce_vendor_error(
Some("invalid_request"),
Some("bad jwt eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxIn0.sig"),
);
assert!(
!jwt.contains("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9"),
"{jwt}"
);
assert!(!jwt.contains("eyJzdWIiOiIxIn0"), "{jwt}");
}
#[test]
fn pkce_callback_surfaces_vendor_error_and_description() {
let err = pkce_callback_from_query("error=access_denied&error_description=user+denied")
.expect_err("vendor error query");
assert!(
err.contains("access_denied"),
"callback should surface vendor error, got {err}"
);
assert!(
err.contains("user denied"),
"callback should surface error_description, got {err}"
);
}
#[test]
fn pkce_callback_accepts_code_and_state() {
let (code, state) = pkce_callback_from_query("code=abc&state=xyz").expect("code and state");
assert_eq!(code, "abc");
assert_eq!(state, "xyz");
}
#[test]
fn pkce_vendor_error_is_http_400_not_the_success_page() {
let mut body = Vec::new();
let err = reply_to_pkce_callback(
&mut body,
"error=access_denied&error_description=user+denied",
"xyz",
)
.expect_err("vendor error");
let text = String::from_utf8(body).expect("utf8");
assert!(text.starts_with("HTTP/1.1 400 "), "{text}");
assert!(text.contains("access_denied"), "{text}");
assert!(
!text.contains("ok, you can close this tab"),
"failed login must not tell the browser to close, got {text}"
);
assert!(err.contains("access_denied"), "{err}");
}
#[test]
fn pkce_state_mismatch_is_http_400() {
let mut body = Vec::new();
let err =
reply_to_pkce_callback(&mut body, "code=abc&state=other", "xyz").expect_err("state");
assert_eq!(err, "callback state mismatch");
let text = String::from_utf8(body).expect("utf8");
assert!(text.starts_with("HTTP/1.1 400 "), "{text}");
assert!(text.contains("callback state mismatch"), "{text}");
assert!(!text.contains("ok, you can close this tab"), "{text}");
}
#[test]
fn pkce_matching_callback_does_not_write_the_success_page() {
let mut body = Vec::new();
let code = reply_to_pkce_callback(&mut body, "code=abc&state=xyz", "xyz").expect("code");
assert_eq!(code, "abc");
assert!(
body.is_empty(),
"success page waits until the token is saved, wrote {body:?}"
);
}
#[test]
fn pkce_success_page_follows_a_saved_token() {
let (ok, body) = pkce_browser_after_save(Ok(()));
assert!(ok);
assert_eq!(body, "ok, you can close this tab");
let text = String::from_utf8(pkce_browser_response(ok, &body)).expect("utf8");
assert!(text.starts_with("HTTP/1.1 200 "), "{text}");
}
#[test]
fn pkce_save_failure_page_redacts_and_is_not_success() {
let (ok, body) = pkce_browser_after_save(Err("token sk-live-secret".into()));
assert!(!ok);
assert!(!body.contains("sk-live-secret"), "{body}");
assert!(body.contains("[redacted]"), "{body}");
let text = String::from_utf8(pkce_browser_response(ok, &body)).expect("utf8");
assert!(text.starts_with("HTTP/1.1 400 "), "{text}");
assert!(!text.contains("ok, you can close this tab"), "{text}");
assert!(!text.contains("sk-live-secret"), "{text}");
}
#[test]
fn url_decode_percent_then_multibyte_utf8_does_not_panic() {
let decoded = url_decode("%完");
assert_eq!(decoded, "%完");
let callback = pkce_callback_from_query("code=%完&state=xyz");
assert!(
callback.is_ok(),
"hostile query must not abort: {callback:?}"
);
assert_eq!(url_decode("%FF"), "\u{FFFD}");
assert_eq!(url_decode("a+b%20c"), "a b c");
assert_eq!(url_decode("%E2%82%AC"), "€");
}
}