llmtxt_core/
native_signed_url.rs1use crate::compute_signature_with_length;
2
3#[derive(Debug)]
5pub struct SignedUrlParams {
6 pub slug: String,
8 pub agent_id: String,
10 pub conversation_id: String,
12 pub expires_at: u64,
14}
15
16#[derive(Debug)]
18pub enum VerifyError {
19 MissingParams,
21 Expired,
23 InvalidSignature,
25}
26
27pub struct SignedUrlBuildRequest<'a> {
29 pub base_url: &'a str,
31 pub path_prefix: &'a str,
33 pub slug: &'a str,
35 pub agent_id: &'a str,
37 pub conversation_id: &'a str,
39 pub expires_at: u64,
41 pub secret: &'a str,
43 pub sig_length: usize,
45}
46
47pub fn generate_signed_url(request: &SignedUrlBuildRequest<'_>) -> Result<String, String> {
55 let mut url =
56 url::Url::parse(request.base_url).map_err(|e| format!("invalid base url: {e}"))?;
57 let normalized_prefix = request.path_prefix.trim_matches('/');
58 let path = if normalized_prefix.is_empty() {
59 format!("/{}", request.slug)
60 } else {
61 format!("/{normalized_prefix}/{}", request.slug)
62 };
63 url.set_path(&path);
64
65 let signature = compute_signature_with_length(
66 request.slug,
67 request.agent_id,
68 request.conversation_id,
69 request.expires_at as f64,
70 request.secret,
71 request.sig_length,
72 );
73
74 url.query_pairs_mut()
75 .append_pair("agent", request.agent_id)
76 .append_pair("conv", request.conversation_id)
77 .append_pair("exp", &request.expires_at.to_string())
78 .append_pair("sig", &signature);
79
80 Ok(url.into())
81}
82
83pub fn verify_signed_url(input: &str, secret: &str) -> Result<SignedUrlParams, VerifyError> {
88 let parsed = url::Url::parse(input).map_err(|_| VerifyError::MissingParams)?;
89
90 let slug = parsed
91 .path_segments()
92 .and_then(|mut segments| segments.rfind(|segment| !segment.is_empty()))
93 .map(str::to_string)
94 .ok_or(VerifyError::MissingParams)?;
95
96 let get_param = |name: &str| -> Result<String, VerifyError> {
97 parsed
98 .query_pairs()
99 .find(|(k, _)| k == name)
100 .map(|(_, v)| v.to_string())
101 .ok_or(VerifyError::MissingParams)
102 };
103
104 let agent = get_param("agent")?;
105 let conv = get_param("conv")?;
106 let exp_str = get_param("exp")?;
107 let sig = get_param("sig")?;
108
109 let expires_at: u64 = exp_str.parse().map_err(|_| VerifyError::MissingParams)?;
110
111 let now = std::time::SystemTime::now()
112 .duration_since(std::time::UNIX_EPOCH)
113 .map(|d| d.as_millis() as u64)
114 .unwrap_or(0);
115 if expires_at != 0 && now > expires_at {
116 return Err(VerifyError::Expired);
117 }
118
119 let expected =
120 compute_signature_with_length(&slug, &agent, &conv, expires_at as f64, secret, sig.len());
121 if sig != expected {
122 return Err(VerifyError::InvalidSignature);
123 }
124
125 Ok(SignedUrlParams {
126 slug,
127 agent_id: agent,
128 conversation_id: conv,
129 expires_at,
130 })
131}