1mod canonicalize;
43mod compare;
44pub mod config;
45mod errors;
46mod proof;
47mod types;
48
49pub use canonicalize::{canonicalize_json, canonicalize_query, canonicalize_urlencoded};
50pub use compare::timing_safe_equal;
51pub use errors::{AshError, AshErrorCode};
52pub use proof::{
53 build_proof,
54 build_proof_v21,
55 build_proof_v21_scoped,
56 build_proof_v21_unified,
57 derive_client_secret,
58 extract_scoped_fields,
60 generate_context_id,
61 generate_nonce,
63 hash_body,
64 hash_proof,
65 hash_scoped_body,
66 verify_proof,
67 verify_proof_v21,
68 verify_proof_v21_scoped,
69 verify_proof_v21_unified,
70 UnifiedProofResult,
72 ASH_SDK_VERSION,
74 ASH_VERSION_PREFIX,
75 ASH_VERSION_PREFIX_V21,
76};
77pub use types::{AshMode, BuildProofInput, VerifyInput};
78
79pub fn normalize_binding(method: &str, path: &str, query: &str) -> Result<String, AshError> {
103 let method = method.trim().to_uppercase();
105 if method.is_empty() {
106 return Err(AshError::new(
107 AshErrorCode::MalformedRequest,
108 "Method cannot be empty",
109 ));
110 }
111
112 let path = path.trim();
114 if !path.starts_with('/') {
115 return Err(AshError::new(
116 AshErrorCode::MalformedRequest,
117 "Path must start with /",
118 ));
119 }
120
121 let path_only = path.split('?').next().unwrap_or(path);
123
124 let mut normalized_path = String::with_capacity(path_only.len());
126 let mut prev_slash = false;
127
128 for ch in path_only.chars() {
129 if ch == '/' {
130 if !prev_slash {
131 normalized_path.push(ch);
132 }
133 prev_slash = true;
134 } else {
135 normalized_path.push(ch);
136 prev_slash = false;
137 }
138 }
139
140 if normalized_path.len() > 1 && normalized_path.ends_with('/') {
142 normalized_path.pop();
143 }
144
145 let canonical_query = if query.is_empty() {
147 String::new()
148 } else {
149 canonicalize::canonicalize_query(query)?
150 };
151
152 Ok(format!(
154 "{}|{}|{}",
155 method, normalized_path, canonical_query
156 ))
157}
158
159pub fn normalize_binding_from_url(method: &str, full_path: &str) -> Result<String, AshError> {
172 let (path, query) = match full_path.find('?') {
173 Some(pos) => (&full_path[..pos], &full_path[pos + 1..]),
174 None => (full_path, ""),
175 };
176 normalize_binding(method, path, query)
177}
178
179#[cfg(test)]
180mod tests {
181 use super::*;
182
183 #[test]
186 fn test_normalize_binding_basic() {
187 assert_eq!(
188 normalize_binding("POST", "/api/users", "").unwrap(),
189 "POST|/api/users|"
190 );
191 }
192
193 #[test]
194 fn test_normalize_binding_lowercase_method() {
195 assert_eq!(
196 normalize_binding("post", "/api/users", "").unwrap(),
197 "POST|/api/users|"
198 );
199 }
200
201 #[test]
202 fn test_normalize_binding_duplicate_slashes() {
203 assert_eq!(
204 normalize_binding("GET", "/api//users///profile", "").unwrap(),
205 "GET|/api/users/profile|"
206 );
207 }
208
209 #[test]
210 fn test_normalize_binding_trailing_slash() {
211 assert_eq!(
212 normalize_binding("PUT", "/api/users/", "").unwrap(),
213 "PUT|/api/users|"
214 );
215 }
216
217 #[test]
218 fn test_normalize_binding_root() {
219 assert_eq!(normalize_binding("GET", "/", "").unwrap(), "GET|/|");
220 }
221
222 #[test]
223 fn test_normalize_binding_with_query() {
224 assert_eq!(
225 normalize_binding("GET", "/api/users", "page=1&sort=name").unwrap(),
226 "GET|/api/users|page=1&sort=name"
227 );
228 }
229
230 #[test]
231 fn test_normalize_binding_query_sorted() {
232 assert_eq!(
233 normalize_binding("GET", "/api/users", "z=3&a=1&b=2").unwrap(),
234 "GET|/api/users|a=1&b=2&z=3"
235 );
236 }
237
238 #[test]
239 fn test_normalize_binding_from_url_basic() {
240 assert_eq!(
241 normalize_binding_from_url("GET", "/api/users?page=1&sort=name").unwrap(),
242 "GET|/api/users|page=1&sort=name"
243 );
244 }
245
246 #[test]
247 fn test_normalize_binding_from_url_no_query() {
248 assert_eq!(
249 normalize_binding_from_url("POST", "/api/users").unwrap(),
250 "POST|/api/users|"
251 );
252 }
253
254 #[test]
255 fn test_normalize_binding_from_url_query_sorted() {
256 assert_eq!(
257 normalize_binding_from_url("GET", "/api/search?z=last&a=first").unwrap(),
258 "GET|/api/search|a=first&z=last"
259 );
260 }
261
262 #[test]
263 fn test_normalize_binding_empty_method() {
264 assert!(normalize_binding("", "/api", "").is_err());
265 }
266
267 #[test]
268 fn test_normalize_binding_no_leading_slash() {
269 assert!(normalize_binding("GET", "api/users", "").is_err());
270 }
271
272 #[test]
275 fn test_version_constants() {
276 use crate::{ASH_SDK_VERSION, ASH_VERSION_PREFIX, ASH_VERSION_PREFIX_V21};
277
278 assert_eq!(ASH_SDK_VERSION, "2.3.1");
279 assert_eq!(ASH_VERSION_PREFIX, "ASHv1");
280 assert_eq!(ASH_VERSION_PREFIX_V21, "ASHv2.1");
281 }
282
283 #[test]
286 fn test_normalize_binding_strips_fragment() {
287 assert_eq!(
289 normalize_binding("GET", "/api/search", "q=test#section").unwrap(),
290 "GET|/api/search|q=test"
291 );
292 }
293
294 #[test]
295 fn test_normalize_binding_plus_literal() {
296 assert_eq!(
298 normalize_binding("GET", "/api/search", "q=a+b").unwrap(),
299 "GET|/api/search|q=a%2Bb"
300 );
301 }
302}