1use wasm_bindgen::prelude::*;
25
26#[cfg(feature = "console_error_panic_hook")]
28pub fn set_panic_hook() {
29 console_error_panic_hook::set_once();
30}
31
32#[wasm_bindgen(js_name = "ashInit")]
37pub fn ash_init() {
38 #[cfg(feature = "console_error_panic_hook")]
39 set_panic_hook();
40}
41
42#[wasm_bindgen(js_name = "ashCanonicalizeJson")]
54pub fn ash_canonicalize_json(input: &str) -> Result<String, JsValue> {
55 ash_core::canonicalize_json(input).map_err(|e| JsValue::from_str(&e.to_string()))
56}
57
58#[wasm_bindgen(js_name = "ashCanonicalizeUrlencoded")]
69pub fn ash_canonicalize_urlencoded(input: &str) -> Result<String, JsValue> {
70 ash_core::canonicalize_urlencoded(input).map_err(|e| JsValue::from_str(&e.to_string()))
71}
72
73#[wasm_bindgen(js_name = "ashBuildProof")]
86pub fn ash_build_proof(
87 mode: &str,
88 binding: &str,
89 context_id: &str,
90 nonce: Option<String>,
91 canonical_payload: &str,
92) -> Result<String, JsValue> {
93 let ash_mode: ash_core::AshMode = mode
94 .parse()
95 .map_err(|e: ash_core::AshError| JsValue::from_str(&e.to_string()))?;
96
97 ash_core::build_proof(
98 ash_mode,
99 binding,
100 context_id,
101 nonce.as_deref(),
102 canonical_payload,
103 )
104 .map_err(|e| JsValue::from_str(&e.to_string()))
105}
106
107#[wasm_bindgen(js_name = "ashVerifyProof")]
116pub fn ash_verify_proof(expected: &str, actual: &str) -> bool {
117 ash_core::timing_safe_equal(expected.as_bytes(), actual.as_bytes())
118}
119
120#[wasm_bindgen(js_name = "ashCanonicalizeQuery")]
132pub fn ash_canonicalize_query(query: &str) -> Result<String, JsValue> {
133 ash_core::canonicalize_query(query).map_err(|e| JsValue::from_str(&e.to_string()))
134}
135
136#[wasm_bindgen(js_name = "ashNormalizeBinding")]
154pub fn ash_normalize_binding(method: &str, path: &str, query: &str) -> Result<String, JsValue> {
155 ash_core::normalize_binding(method, path, query).map_err(|e| JsValue::from_str(&e.to_string()))
156}
157
158#[wasm_bindgen(js_name = "ashNormalizeBindingFromUrl")]
167pub fn ash_normalize_binding_from_url(method: &str, full_path: &str) -> Result<String, JsValue> {
168 ash_core::normalize_binding_from_url(method, full_path)
169 .map_err(|e| JsValue::from_str(&e.to_string()))
170}
171
172#[wasm_bindgen(js_name = "ashTimingSafeEqual")]
180pub fn ash_timing_safe_equal(a: &str, b: &str) -> bool {
181 ash_core::timing_safe_equal(a.as_bytes(), b.as_bytes())
182}
183
184#[wasm_bindgen(js_name = "ashVersion")]
188pub fn ash_version() -> String {
189 "ASHv2.1".to_string()
190}
191
192#[wasm_bindgen(js_name = "ashLibraryVersion")]
196pub fn ash_library_version() -> String {
197 env!("CARGO_PKG_VERSION").to_string()
198}
199
200#[wasm_bindgen(js_name = "canonicalizeJson")]
204pub fn canonicalize_json(input: &str) -> Result<String, JsValue> {
205 ash_canonicalize_json(input)
206}
207
208#[wasm_bindgen(js_name = "canonicalizeUrlencoded")]
209pub fn canonicalize_urlencoded(input: &str) -> Result<String, JsValue> {
210 ash_canonicalize_urlencoded(input)
211}
212
213#[wasm_bindgen(js_name = "buildProof")]
214pub fn build_proof(
215 mode: &str,
216 binding: &str,
217 context_id: &str,
218 nonce: Option<String>,
219 canonical_payload: &str,
220) -> Result<String, JsValue> {
221 ash_build_proof(mode, binding, context_id, nonce, canonical_payload)
222}
223
224#[wasm_bindgen(js_name = "verifyProof")]
225pub fn verify_proof(expected: &str, actual: &str) -> bool {
226 ash_verify_proof(expected, actual)
227}
228
229#[wasm_bindgen(js_name = "normalizeBinding")]
230pub fn normalize_binding(method: &str, path: &str, query: &str) -> Result<String, JsValue> {
231 ash_normalize_binding(method, path, query)
232}
233
234#[wasm_bindgen(js_name = "canonicalizeQuery")]
235pub fn canonicalize_query(query: &str) -> Result<String, JsValue> {
236 ash_canonicalize_query(query)
237}
238
239#[wasm_bindgen(js_name = "normalizeBindingFromUrl")]
240pub fn normalize_binding_from_url(method: &str, full_path: &str) -> Result<String, JsValue> {
241 ash_normalize_binding_from_url(method, full_path)
242}
243
244#[cfg(test)]
245mod tests {
246 use super::*;
247
248 #[test]
249 fn test_canonicalize_json() {
250 let result = ash_canonicalize_json(r#"{"z":1,"a":2}"#).unwrap();
251 assert_eq!(result, r#"{"a":2,"z":1}"#);
252 }
253
254 #[test]
255 fn test_canonicalize_urlencoded() {
256 let result = ash_canonicalize_urlencoded("z=1&a=2").unwrap();
257 assert_eq!(result, "a=2&z=1");
258 }
259
260 #[test]
261 fn test_build_and_verify_proof() {
262 let proof1 =
263 ash_build_proof("balanced", "POST /api/test", "ctx123", None, r#"{"a":1}"#).unwrap();
264
265 let proof2 =
266 ash_build_proof("balanced", "POST /api/test", "ctx123", None, r#"{"a":1}"#).unwrap();
267
268 assert!(ash_verify_proof(&proof1, &proof2));
269 }
270
271 #[test]
272 fn test_normalize_binding() {
273 let result = ash_normalize_binding("post", "/api//test/", "").unwrap();
274 assert_eq!(result, "POST|/api/test|");
275 }
276
277 #[test]
278 fn test_normalize_binding_with_query() {
279 let result = ash_normalize_binding("GET", "/api/users", "page=1&sort=name").unwrap();
280 assert_eq!(result, "GET|/api/users|page=1&sort=name");
281 }
282
283 #[test]
284 fn test_normalize_binding_from_url() {
285 let result = ash_normalize_binding_from_url("GET", "/api/search?z=3&a=1").unwrap();
286 assert_eq!(result, "GET|/api/search|a=1&z=3");
287 }
288
289 #[test]
290 fn test_canonicalize_query() {
291 let result = ash_canonicalize_query("z=3&a=1&b=2").unwrap();
292 assert_eq!(result, "a=1&b=2&z=3");
293 }
294
295 #[test]
296 fn test_version() {
297 assert_eq!(ash_version(), "ASHv2.1");
298 }
299}
300
301#[wasm_bindgen(js_name = "ashGenerateNonce")]
309pub fn ash_generate_nonce(bytes: Option<usize>) -> String {
310 ash_core::generate_nonce(bytes.unwrap_or(32))
311}
312
313#[wasm_bindgen(js_name = "ashGenerateContextId")]
315pub fn ash_generate_context_id() -> String {
316 ash_core::generate_context_id()
317}
318
319#[wasm_bindgen(js_name = "ashDeriveClientSecret")]
325pub fn ash_derive_client_secret(nonce: &str, context_id: &str, binding: &str) -> String {
326 ash_core::derive_client_secret(nonce, context_id, binding)
327}
328
329#[wasm_bindgen(js_name = "ashBuildProofV21")]
336pub fn ash_build_proof_v21(
337 client_secret: &str,
338 timestamp: &str,
339 binding: &str,
340 body_hash: &str,
341) -> String {
342 ash_core::build_proof_v21(client_secret, timestamp, binding, body_hash)
343}
344
345#[wasm_bindgen(js_name = "ashVerifyProofV21")]
354pub fn ash_verify_proof_v21(
355 nonce: &str,
356 context_id: &str,
357 binding: &str,
358 timestamp: &str,
359 body_hash: &str,
360 client_proof: &str,
361) -> bool {
362 ash_core::verify_proof_v21(
363 nonce,
364 context_id,
365 binding,
366 timestamp,
367 body_hash,
368 client_proof,
369 )
370}
371
372#[wasm_bindgen(js_name = "ashHashBody")]
376pub fn ash_hash_body(canonical_body: &str) -> String {
377 ash_core::hash_body(canonical_body)
378}
379
380#[wasm_bindgen(js_name = "ashBuildProofScoped")]
392pub fn ash_build_proof_scoped(
393 client_secret: &str,
394 timestamp: &str,
395 binding: &str,
396 payload: &str,
397 scope: &str,
398) -> Result<JsValue, JsValue> {
399 let scope_vec: Vec<&str> = if scope.is_empty() {
400 vec![]
401 } else {
402 scope.split(',').collect()
403 };
404
405 let (proof, scope_hash) =
406 ash_core::build_proof_v21_scoped(client_secret, timestamp, binding, payload, &scope_vec)
407 .map_err(|e| JsValue::from_str(&e.to_string()))?;
408
409 let result = serde_json::json!({
410 "proof": proof,
411 "scopeHash": scope_hash
412 });
413
414 Ok(JsValue::from_str(&result.to_string()))
415}
416
417#[allow(clippy::too_many_arguments)]
428#[wasm_bindgen(js_name = "ashVerifyProofScoped")]
429pub fn ash_verify_proof_scoped(
430 nonce: &str,
431 context_id: &str,
432 binding: &str,
433 timestamp: &str,
434 payload: &str,
435 scope: &str,
436 scope_hash: &str,
437 client_proof: &str,
438) -> Result<bool, JsValue> {
439 let scope_vec: Vec<&str> = if scope.is_empty() {
440 vec![]
441 } else {
442 scope.split(',').collect()
443 };
444
445 ash_core::verify_proof_v21_scoped(
446 nonce,
447 context_id,
448 binding,
449 timestamp,
450 payload,
451 &scope_vec,
452 scope_hash,
453 client_proof,
454 )
455 .map_err(|e| JsValue::from_str(&e.to_string()))
456}
457
458#[wasm_bindgen(js_name = "ashHashScopedBody")]
463pub fn ash_hash_scoped_body(payload: &str, scope: &str) -> Result<String, JsValue> {
464 let scope_vec: Vec<&str> = if scope.is_empty() {
465 vec![]
466 } else {
467 scope.split(',').collect()
468 };
469
470 ash_core::hash_scoped_body(payload, &scope_vec).map_err(|e| JsValue::from_str(&e.to_string()))
471}
472
473#[wasm_bindgen(js_name = "ashHashProof")]
481pub fn ash_hash_proof(proof: &str) -> String {
482 ash_core::hash_proof(proof)
483}
484
485#[wasm_bindgen(js_name = "ashBuildProofUnified")]
494pub fn ash_build_proof_unified(
495 client_secret: &str,
496 timestamp: &str,
497 binding: &str,
498 payload: &str,
499 scope: &str,
500 previous_proof: Option<String>,
501) -> Result<JsValue, JsValue> {
502 let scope_vec: Vec<&str> = if scope.is_empty() {
503 vec![]
504 } else {
505 scope.split(',').collect()
506 };
507
508 let prev_proof = previous_proof.as_deref().filter(|s| !s.is_empty());
509
510 let result = ash_core::build_proof_v21_unified(
511 client_secret,
512 timestamp,
513 binding,
514 payload,
515 &scope_vec,
516 prev_proof,
517 )
518 .map_err(|e| JsValue::from_str(&e.to_string()))?;
519
520 let json_result = serde_json::json!({
521 "proof": result.proof,
522 "scopeHash": result.scope_hash,
523 "chainHash": result.chain_hash
524 });
525
526 Ok(JsValue::from_str(&json_result.to_string()))
527}
528
529#[allow(clippy::too_many_arguments)]
542#[wasm_bindgen(js_name = "ashVerifyProofUnified")]
543pub fn ash_verify_proof_unified(
544 nonce: &str,
545 context_id: &str,
546 binding: &str,
547 timestamp: &str,
548 payload: &str,
549 client_proof: &str,
550 scope: &str,
551 scope_hash: &str,
552 previous_proof: Option<String>,
553 chain_hash: &str,
554) -> Result<bool, JsValue> {
555 let scope_vec: Vec<&str> = if scope.is_empty() {
556 vec![]
557 } else {
558 scope.split(',').collect()
559 };
560
561 let prev_proof = previous_proof.as_deref().filter(|s| !s.is_empty());
562
563 ash_core::verify_proof_v21_unified(
564 nonce,
565 context_id,
566 binding,
567 timestamp,
568 payload,
569 client_proof,
570 &scope_vec,
571 scope_hash,
572 prev_proof,
573 chain_hash,
574 )
575 .map_err(|e| JsValue::from_str(&e.to_string()))
576}