use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::time::{SystemTime, UNIX_EPOCH};
use base64::Engine;
use base64::engine::general_purpose::URL_SAFE_NO_PAD;
use serde::Deserialize;
use crate::providers::import::{ImportError, ImportSource, Imported, home_dir};
use bitrouter_ai::auth::credentials::OAuthToken;
const AUTH_FILENAME: &str = "auth.json";
const FALLBACK_TTL_SECS: u64 = 3600;
#[derive(Deserialize)]
struct AuthEntry {
#[serde(default)]
key: Option<String>,
#[serde(default)]
refresh_token: Option<String>,
}
pub fn import() -> Result<Option<Imported>, ImportError> {
let home = grok_home().ok_or(ImportError::NoHome)?;
from_file(&home.join(AUTH_FILENAME))
}
pub fn from_file(path: &Path) -> Result<Option<Imported>, ImportError> {
let bytes = match std::fs::read(path) {
Ok(b) => b,
Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(source) => {
return Err(ImportError::Io {
path: path.to_path_buf(),
source,
});
}
};
let blob = String::from_utf8_lossy(&bytes);
match parse_blob(blob.as_ref(), &path.display().to_string())? {
Some(token) => Ok(Some(Imported {
token,
source: ImportSource::File(path.to_path_buf()),
})),
None => Ok(None),
}
}
fn grok_home() -> Option<PathBuf> {
if let Some(dir) = std::env::var_os("GROK_HOME").filter(|v| !v.is_empty()) {
return Some(PathBuf::from(dir));
}
home_dir().map(|h| h.join(".grok"))
}
fn parse_blob(blob: &str, origin: &str) -> Result<Option<OAuthToken>, ImportError> {
let entries: BTreeMap<String, AuthEntry> =
serde_json::from_str(blob).map_err(|source| ImportError::Json {
origin: origin.to_string(),
source,
})?;
let Some((access_token, refresh_token)) = entries.into_values().find_map(|e| {
let key = e.key.filter(|s| !s.is_empty())?;
let refresh = e.refresh_token.filter(|s| !s.is_empty())?;
Some((key, refresh))
}) else {
return Ok(None);
};
let expires_at =
decode_jwt_exp(&access_token).unwrap_or_else(|| now_secs() + FALLBACK_TTL_SECS);
Ok(Some(OAuthToken {
access_token,
expires_at,
refresh_token: Some(refresh_token),
}))
}
fn decode_jwt_exp(token: &str) -> Option<u64> {
#[derive(Deserialize)]
struct Exp {
exp: Option<u64>,
}
let payload = token
.split('.')
.nth(1)
.filter(|_| token.split('.').count() == 3)?;
let bytes = URL_SAFE_NO_PAD.decode(payload).ok()?;
let parsed: Exp = serde_json::from_slice(&bytes).ok()?;
parsed.exp
}
fn now_secs() -> u64 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0)
}
#[cfg(test)]
mod tests {
use super::*;
use anyhow::Context;
fn make_jwt_exp(exp: u64) -> String {
let header = URL_SAFE_NO_PAD.encode("{}");
let payload =
URL_SAFE_NO_PAD.encode(format!(r#"{{"exp":{exp},"iss":"https://auth.x.ai"}}"#));
let sig = URL_SAFE_NO_PAD.encode("sig");
format!("{header}.{payload}.{sig}")
}
fn tmp_file(contents: &str) -> anyhow::Result<PathBuf> {
use std::sync::atomic::{AtomicU64, Ordering};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let id = COUNTER.fetch_add(1, Ordering::Relaxed);
let dir =
std::env::temp_dir().join(format!("bitrouter-import-grok-{}-{id}", std::process::id()));
std::fs::create_dir_all(&dir).context("required fixture value")?;
let path = dir.join("auth.json");
std::fs::write(&path, contents).context("required fixture value")?;
Ok(path)
}
fn auth_json(entry_key: &str, access: &str, refresh: &str, auth_mode: &str) -> String {
format!(
r#"{{"{entry_key}":{{"key":"{access}","refresh_token":"{refresh}","auth_mode":"{auth_mode}","expires_at":"2026-07-09T23:12:46Z"}}}}"#
)
}
#[test]
fn parses_oidc_entry_and_reads_jwt_exp() -> anyhow::Result<()> {
let jwt = make_jwt_exp(1_783_638_765);
let json = auth_json("https://auth.x.ai::b1a00492", &jwt, "rt-abc", "oidc");
let path = tmp_file(&json)?;
let imported = from_file(&path)
.context("required fixture value")?
.context("required fixture value")?;
assert_eq!(imported.token.access_token, jwt);
assert_eq!(imported.token.refresh_token.as_deref(), Some("rt-abc"));
assert_eq!(imported.token.expires_at, 1_783_638_765);
Ok(())
}
#[test]
fn non_jwt_access_token_falls_back_to_ttl() -> anyhow::Result<()> {
let json = auth_json("https://auth.x.ai::b1a00492", "not-a-jwt", "rt", "oidc");
let path = tmp_file(&json)?;
let imported = from_file(&path)
.context("required fixture value")?
.context("required fixture value")?;
assert!(imported.token.expires_at >= now_secs());
Ok(())
}
#[test]
fn api_key_only_entry_is_none() -> anyhow::Result<()> {
let path = tmp_file(r#"{"api":{"api_key":"xai-...","auth_mode":"key"}}"#)?;
assert!(
from_file(&path)
.context("required fixture value")?
.is_none()
);
Ok(())
}
#[test]
fn entry_missing_refresh_token_is_skipped() -> anyhow::Result<()> {
let jwt = make_jwt_exp(1_783_638_765);
let json = format!(r#"{{"e":{{"key":"{jwt}"}}}}"#);
let path = tmp_file(&json)?;
assert!(
from_file(&path)
.context("required fixture value")?
.is_none()
);
Ok(())
}
#[test]
fn absent_file_is_none() -> anyhow::Result<()> {
let missing = std::env::temp_dir().join("bitrouter-import-grok-absent/nope/auth.json");
assert!(
from_file(&missing)
.context("required fixture value")?
.is_none()
);
Ok(())
}
#[test]
fn selects_oidc_entry_when_api_key_entry_also_present() -> anyhow::Result<()> {
let jwt = make_jwt_exp(1_783_638_765);
let json = format!(
r#"{{"api":{{"api_key":"xai-x"}},"https://auth.x.ai::c":{{"key":"{jwt}","refresh_token":"rt"}}}}"#
);
let path = tmp_file(&json)?;
let imported = from_file(&path)
.context("required fixture value")?
.context("required fixture value")?;
assert_eq!(imported.token.access_token, jwt);
assert_eq!(imported.token.refresh_token.as_deref(), Some("rt"));
Ok(())
}
}