use crate::compute_signature_with_length;
#[derive(Debug)]
pub struct SignedUrlParams {
pub slug: String,
pub agent_id: String,
pub conversation_id: String,
pub expires_at: u64,
}
#[derive(Debug)]
pub enum VerifyError {
MissingParams,
Expired,
InvalidSignature,
}
pub struct SignedUrlBuildRequest<'a> {
pub base_url: &'a str,
pub path_prefix: &'a str,
pub slug: &'a str,
pub agent_id: &'a str,
pub conversation_id: &'a str,
pub expires_at: u64,
pub secret: &'a str,
pub sig_length: usize,
}
pub fn generate_signed_url(request: &SignedUrlBuildRequest<'_>) -> Result<String, String> {
let mut url =
url::Url::parse(request.base_url).map_err(|e| format!("invalid base url: {e}"))?;
let normalized_prefix = request.path_prefix.trim_matches('/');
let path = if normalized_prefix.is_empty() {
format!("/{}", request.slug)
} else {
format!("/{normalized_prefix}/{}", request.slug)
};
url.set_path(&path);
let signature = compute_signature_with_length(
request.slug,
request.agent_id,
request.conversation_id,
request.expires_at as f64,
request.secret,
request.sig_length,
);
url.query_pairs_mut()
.append_pair("agent", request.agent_id)
.append_pair("conv", request.conversation_id)
.append_pair("exp", &request.expires_at.to_string())
.append_pair("sig", &signature);
Ok(url.into())
}
pub fn verify_signed_url(input: &str, secret: &str) -> Result<SignedUrlParams, VerifyError> {
let parsed = url::Url::parse(input).map_err(|_| VerifyError::MissingParams)?;
let slug = parsed
.path_segments()
.and_then(|mut segments| segments.rfind(|segment| !segment.is_empty()))
.map(str::to_string)
.ok_or(VerifyError::MissingParams)?;
let get_param = |name: &str| -> Result<String, VerifyError> {
parsed
.query_pairs()
.find(|(k, _)| k == name)
.map(|(_, v)| v.to_string())
.ok_or(VerifyError::MissingParams)
};
let agent = get_param("agent")?;
let conv = get_param("conv")?;
let exp_str = get_param("exp")?;
let sig = get_param("sig")?;
let expires_at: u64 = exp_str.parse().map_err(|_| VerifyError::MissingParams)?;
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_millis() as u64)
.unwrap_or(0);
if expires_at != 0 && now > expires_at {
return Err(VerifyError::Expired);
}
let expected =
compute_signature_with_length(&slug, &agent, &conv, expires_at as f64, secret, sig.len());
if sig != expected {
return Err(VerifyError::InvalidSignature);
}
Ok(SignedUrlParams {
slug,
agent_id: agent,
conversation_id: conv,
expires_at,
})
}