use base64::Engine;
use chrono::Utc;
use hmac::{Hmac, KeyInit, Mac};
use serde_json::json;
use sha2::Sha256;
use crate::{ZaiResult, client::error::ZaiError};
type HmacSha256 = Hmac<Sha256>;
const MAX_TTL_SECONDS: i64 = 7 * 24 * 60 * 60;
pub fn authorization_header(api_key: &str, jwt_seconds: Option<i64>) -> ZaiResult<String> {
crate::client::error::validate_api_key(api_key).map_err(|error| {
ZaiError::RealtimeAuthError(format!("invalid API key: {}", error.message()))
})?;
match jwt_seconds {
Some(ttl) => Ok(format!("Bearer {}", generate(api_key, ttl)?)),
None => Ok(format!("Bearer {api_key}")),
}
}
pub fn generate(api_key: &str, ttl_seconds: i64) -> ZaiResult<String> {
crate::client::error::validate_api_key(api_key).map_err(|error| {
ZaiError::RealtimeAuthError(format!("invalid API key: {}", error.message()))
})?;
let (id, secret) = api_key.split_once('.').ok_or_else(|| {
ZaiError::RealtimeAuthError("API key must be '<id>.<secret>'".to_string())
})?;
if ttl_seconds <= 0 || ttl_seconds > MAX_TTL_SECONDS {
return Err(ZaiError::RealtimeAuthError(format!(
"jwt ttl_seconds must be in 1..={MAX_TTL_SECONDS}, got {ttl_seconds}"
)));
}
let now = Utc::now();
let exp = now
.timestamp()
.checked_add(ttl_seconds)
.ok_or_else(|| ZaiError::RealtimeAuthError("jwt exp overflow".into()))?;
let timestamp_ms = now.timestamp_millis();
let header = json!({ "alg": "HS256", "sign_type": "SIGN" });
let payload = json!({
"api_key": id,
"exp": exp,
"timestamp": timestamp_ms,
});
let header_b64 = base64url(&serde_json::to_vec(&header)?);
let payload_b64 = base64url(&serde_json::to_vec(&payload)?);
let signing_input = format!("{header_b64}.{payload_b64}");
let mut mac = HmacSha256::new_from_slice(secret.as_bytes())
.map_err(|e| ZaiError::RealtimeAuthError(format!("HMAC key error: {e}")))?;
mac.update(signing_input.as_bytes());
let sig = mac.finalize().into_bytes();
let sig_b64 = base64url(sig.as_slice());
Ok(format!("{signing_input}.{sig_b64}"))
}
fn base64url(data: &[u8]) -> String {
base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(data)
}
#[cfg(test)]
mod tests {
use base64::Engine;
use super::*;
#[test]
fn jwt_has_three_urlsafe_segments() {
let token = generate("abcdefghij.0123456789abcdef", 600).unwrap();
let parts: Vec<&str> = token.split('.').collect();
assert_eq!(parts.len(), 3, "JWT must have header.payload.signature");
assert!(!token.contains('='));
assert!(!token.contains('+'));
assert!(!token.contains('/'));
}
#[test]
fn jwt_header_and_payload_decode_to_expected_fields() {
let token = generate("abcdefghij.0123456789abcdef", 600).unwrap();
let mut parts = token.split('.');
let header_json = String::from_utf8(
base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(parts.next().unwrap())
.unwrap(),
)
.unwrap();
let payload_json = String::from_utf8(
base64::engine::general_purpose::URL_SAFE_NO_PAD
.decode(parts.next().unwrap())
.unwrap(),
)
.unwrap();
assert!(header_json.contains("\"alg\":\"HS256\""));
assert!(header_json.contains("\"sign_type\":\"SIGN\""));
assert!(payload_json.contains("\"api_key\":\"abcdefghij\""));
assert!(payload_json.contains("\"exp\""));
assert!(payload_json.contains("\"timestamp\""));
}
#[test]
fn rejects_malformed_keys() {
assert!(generate("no-dot-here", 600).is_err());
assert!(generate(".secret", 600).is_err());
assert!(generate("id.", 600).is_err());
assert!(generate("id.secret.extra", 600).is_err());
}
#[test]
fn rejects_out_of_range_ttl() {
assert!(generate("abcdefghij.0123456789abcdef", 0).is_err());
assert!(generate("abcdefghij.0123456789abcdef", -1).is_err());
assert!(generate("abcdefghij.0123456789abcdef", i64::MAX).is_err());
assert!(generate("abcdefghij.0123456789abcdef", 7 * 24 * 3600).is_ok());
assert!(generate("abcdefghij.0123456789abcdef", 7 * 24 * 3600 + 1).is_err());
}
#[test]
fn authorization_header_bearer_jwt_and_raw() {
let h = authorization_header("abcdefghij.0123456789abcdef", Some(3600)).unwrap();
assert!(h.starts_with("Bearer "));
assert!(h[7..].split('.').count() == 3);
let h2 = authorization_header("abcdefghij.0123456789abcdef", None).unwrap();
assert_eq!(h2, "Bearer abcdefghij.0123456789abcdef");
assert!(authorization_header(" ", None).is_err());
assert!(authorization_header("abc\ndef", None).is_err());
}
}