cedarling_wasm/lib.rs
1// This software is available under the Apache-2.0 license.
2// See https://www.apache.org/licenses/LICENSE-2.0.txt for full text.
3//
4// Copyright (c) 2024, Gluu, Inc.
5
6use cedarling::bindings::cedar_policy;
7use cedarling::{
8 AuthorizeMultiIssuerRequest, BatchAuthorizeMultiIssuerRequest,
9 BatchAuthorizeResponse as CedarBatchAuthorizeResponse, BatchAuthorizeUnsignedRequest,
10 BatchItemError as CedarBatchItemError, BootstrapConfig, BootstrapConfigRaw, DataApi,
11 DataEntry as CedarDataEntry, DataStoreStats as CedarDataStoreStats, LogStorage,
12 MetricsSnapshot as CedarMetricsSnapshot, PolicyId, RequestUnsigned, TrustedIssuerLoadingInfo,
13};
14use serde::ser::{Serialize, SerializeStruct, Serializer};
15use serde_json::json;
16use serde_wasm_bindgen::Error;
17use std::collections::HashMap;
18use std::rc::Rc;
19use std::time::Duration;
20use wasm_bindgen::prelude::*;
21use wasm_bindgen_futures::js_sys::{self, Array, Map, Object, Reflect};
22
23#[cfg(test)]
24mod tests;
25
26/// The instance of the Cedarling application.
27#[wasm_bindgen]
28#[derive(Clone)]
29pub struct Cedarling {
30 instance: cedarling::Cedarling,
31}
32
33/// A WASM wrapper for the Rust `cedarling::MultiIssuerAuthorizeResult` struct.
34/// Represents the result of a multi-issuer authorization request.
35#[wasm_bindgen]
36#[derive(Clone, Debug, serde::Serialize)]
37pub struct MultiIssuerAuthorizeResult {
38 /// Result of Cedar policy authorization
39 #[wasm_bindgen(getter_with_clone)]
40 pub response: AuthorizeResultResponse,
41
42 /// Result of authorization
43 /// true means `ALLOW`
44 /// false means `Deny`
45 pub decision: bool,
46
47 /// Request ID of the authorization request
48 #[wasm_bindgen(getter_with_clone)]
49 pub request_id: String,
50}
51
52#[wasm_bindgen]
53impl MultiIssuerAuthorizeResult {
54 /// Convert `MultiIssuerAuthorizeResult` to json string value
55 #[wasm_bindgen(js_name = jsonString)]
56 pub fn json_string(&self) -> String {
57 json!(self).to_string()
58 }
59}
60
61impl From<cedarling::MultiIssuerAuthorizeResult> for MultiIssuerAuthorizeResult {
62 fn from(value: cedarling::MultiIssuerAuthorizeResult) -> Self {
63 Self {
64 response: AuthorizeResultResponse {
65 inner: Rc::new(value.response),
66 },
67 decision: value.decision,
68 request_id: value.request_id,
69 }
70 }
71}
72
73/// Creates a Cedarling application from bootstrap properties.
74///
75/// # Arguments
76///
77/// * `config` - A JavaScript `Map` or plain object of Cedarling
78/// bootstrap-property names and values.
79///
80/// # Example
81///
82/// ```javascript
83/// await initWasm();
84/// const cedarling = await init({
85/// CEDARLING_APPLICATION_NAME: "task-api",
86/// CEDARLING_POLICY_STORE_URI: "https://example.com/policy-store.cjar",
87/// CEDARLING_LOG_TYPE: "memory",
88/// CEDARLING_LOG_TTL: 120,
89/// });
90/// ```
91#[wasm_bindgen]
92pub async fn init(config: JsValue) -> Result<Cedarling, Error> {
93 if config.is_instance_of::<Map>() {
94 // convert to map
95 let config_map: Map = config.unchecked_into();
96 Cedarling::new_from_map(config_map).await
97 } else if let Some(config_object) = Object::try_from(&config) {
98 Cedarling::new(config_object).await
99 } else {
100 Err(Error::new("config should be Map or Object"))
101 }
102}
103
104/// Create a new instance of the Cedarling application from archive bytes.
105///
106/// This function allows loading a policy store from a Cedar Archive (.cjar)
107/// that was fetched with custom logic (e.g., with authentication headers).
108///
109/// # Arguments
110/// * `config` - Bootstrap configuration (Map or Object) without a policy-store
111/// source property. The archive bytes provide the policy store.
112/// * `archive_bytes` - The .cjar archive bytes (Uint8Array)
113///
114/// # Example
115/// ```javascript
116/// await initWasm();
117/// const config = {
118/// CEDARLING_APPLICATION_NAME: "task-api",
119/// CEDARLING_LOG_TYPE: "memory",
120/// CEDARLING_LOG_TTL: 120,
121/// };
122/// const response = await fetch("https://example.com/policy-store.cjar", {
123/// headers: { Authorization: "Bearer <token>" },
124/// });
125/// if (!response.ok) throw new Error("Unable to fetch policy store");
126/// const archiveBytes = new Uint8Array(await response.arrayBuffer());
127/// const cedarling = await initFromArchiveBytes(config, archiveBytes);
128/// ```
129#[wasm_bindgen(js_name = initFromArchiveBytes)]
130pub async fn init_from_archive_bytes(
131 config: JsValue,
132 archive_bytes: js_sys::Uint8Array,
133) -> Result<Cedarling, Error> {
134 use cedarling::PolicyStoreSource;
135
136 // Convert Uint8Array to Vec<u8>
137 let bytes: Vec<u8> = archive_bytes.to_vec();
138
139 // Parse the config
140 let config_object = if config.is_instance_of::<Map>() {
141 let config_map: Map = config.unchecked_into();
142 Object::from_entries(&config_map.unchecked_into())?
143 } else if let Some(obj) = Object::try_from(&config) {
144 obj.clone()
145 } else {
146 return Err(Error::new("config should be Map or Object"));
147 };
148
149 let mut raw_config: BootstrapConfigRaw = serde_wasm_bindgen::from_value(config_object.into())?;
150
151 // Clear any existing policy store sources to avoid conflicts
152 // We'll set a dummy source temporarily to satisfy validation, then override with ArchiveBytes
153 raw_config.local_policy_store = None;
154 raw_config.policy_store_uri = None;
155 // Set a dummy .cjar file path to satisfy validation (will be overridden below)
156 raw_config.policy_store_local_fn = Some("dummy.cjar".to_string());
157
158 let mut bootstrap_config = BootstrapConfig::from_raw_config(&raw_config).map_err(Error::new)?;
159
160 // Override the policy store source with the archive bytes
161 bootstrap_config.policy_store_config.source = PolicyStoreSource::ArchiveBytes(bytes);
162
163 cedarling::Cedarling::new(&bootstrap_config)
164 .await
165 .map(|instance| Cedarling { instance })
166 .map_err(Error::new)
167}
168
169#[wasm_bindgen]
170impl Cedarling {
171 /// Creates a Cedarling application from bootstrap properties.
172 ///
173 /// # Arguments
174 ///
175 /// * `config` - A plain object of Cedarling bootstrap-property names and values.
176 ///
177 /// # Example
178 ///
179 /// ```javascript
180 /// await initWasm();
181 /// const cedarling = await Cedarling.new({
182 /// CEDARLING_APPLICATION_NAME: "task-api",
183 /// CEDARLING_POLICY_STORE_URI: "https://example.com/policy-store.cjar",
184 /// CEDARLING_LOG_TYPE: "memory",
185 /// CEDARLING_LOG_TTL: 120,
186 /// });
187 /// ```
188 pub async fn new(config: &Object) -> Result<Cedarling, Error> {
189 let config: BootstrapConfigRaw = serde_wasm_bindgen::from_value(config.into())?;
190
191 let config = BootstrapConfig::from_raw_config(&config).map_err(Error::new)?;
192
193 cedarling::Cedarling::new(&config)
194 .await
195 .map(|instance| Cedarling { instance })
196 .map_err(Error::new)
197 }
198
199 /// Creates a new Cedarling application from a JavaScript `Map`.
200 ///
201 /// # Arguments
202 ///
203 /// * `config` - A `Map` of Cedarling bootstrap-property names and values.
204 ///
205 /// # Example
206 ///
207 /// ```javascript
208 /// await initWasm();
209 /// const cedarling = await Cedarling.newFromMap(new Map([
210 /// ["CEDARLING_APPLICATION_NAME", "task-api"],
211 /// ["CEDARLING_POLICY_STORE_URI", "https://example.com/policy-store.cjar"],
212 /// ["CEDARLING_LOG_TYPE", "memory"],
213 /// ["CEDARLING_LOG_TTL", 120],
214 /// ]));
215 /// ```
216 #[wasm_bindgen(js_name = newFromMap)]
217 pub async fn new_from_map(config: Map) -> Result<Cedarling, Error> {
218 let conf_js_val = config.unchecked_into();
219
220 let conf_object = Object::from_entries(&conf_js_val)?;
221 Self::new(&conf_object).await
222 }
223
224 /// Authorize an unsigned request carrying an optional single principal.
225 /// Makes an authorization decision based on the [`RequestUnsigned`].
226 ///
227 /// When `principal` is omitted / `null` on the JS side the core uses Cedar
228 /// partial evaluation; residual-dependent requests fail closed with
229 /// `Decision::Deny` and surface residual policy ids in
230 /// `response.diagnostics.reason`.
231 ///
232 /// # Arguments
233 ///
234 /// * `request` - JSON string representation of [`RequestUnsigned`].
235 ///
236 /// # Example
237 ///
238 /// ```javascript
239 /// const result = await cedarling.authorizeUnsigned(JSON.stringify(request));
240 /// ```
241 #[wasm_bindgen(js_name = authorizeUnsigned)]
242 pub async fn authorize_unsigned(&self, request: &str) -> Result<AuthorizeResult, Error> {
243 let cedar_request: RequestUnsigned = serde_json::from_str(request)
244 .map_err(|e| Error::new(format!("invalid request JSON: {e}")))?;
245 let result = self
246 .instance
247 .authorize_unsigned(cedar_request)
248 .await
249 .map_err(Error::new)?;
250 Ok(result.into())
251 }
252
253 /// Authorize multi-issuer request.
254 /// Makes authorization decision based on multiple JWT tokens from different issuers.
255 ///
256 /// # Arguments
257 ///
258 /// * `request` - JSON string representation of [`AuthorizeMultiIssuerRequest`].
259 ///
260 /// # Example
261 ///
262 /// ```javascript
263 /// const result = await cedarling.authorizeMultiIssuer(JSON.stringify(request));
264 /// ```
265 #[wasm_bindgen(js_name = authorizeMultiIssuer)]
266 pub async fn authorize_multi_issuer(
267 &self,
268 request: &str,
269 ) -> Result<MultiIssuerAuthorizeResult, Error> {
270 let cedar_request: AuthorizeMultiIssuerRequest = serde_json::from_str(request)
271 .map_err(|e| Error::new(format!("invalid request JSON: {e}")))?;
272 let result = self
273 .instance
274 .authorize_multi_issuer(cedar_request)
275 .await
276 .map_err(Error::new)?;
277 Ok(result.into())
278 }
279
280 /// Authorize a batch of unsigned requests against one shared principal.
281 ///
282 /// Setup work (principal build + pushed-data snapshot) runs once and each
283 /// item is evaluated in input order. Results are returned inside a
284 /// [`BatchAuthorizeUnsignedResponse`] carrying the shared `batch_id`.
285 /// Batch-level failures (validation, principal parse) reject the whole
286 /// call; per-item failures are returned as `BatchItemError` results and
287 /// exposed by WASM with `is_ok=false` and `error`, while genuine Cedar
288 /// denials remain `AuthorizeResult` values with `decision=false`.
289 /// # Arguments
290 ///
291 /// * `request` - JSON string representation of [`BatchAuthorizeUnsignedRequest`].
292 ///
293 /// # Example
294 ///
295 /// ```javascript
296 /// const result = await cedarling.authorizeUnsignedBatch(JSON.stringify(batchRequest));
297 /// ```
298 #[wasm_bindgen(js_name = authorizeUnsignedBatch)]
299 pub async fn authorize_unsigned_batch(
300 &self,
301 request: &str,
302 ) -> Result<BatchAuthorizeUnsignedResponse, Error> {
303 let cedar_request: BatchAuthorizeUnsignedRequest = serde_json::from_str(request)
304 .map_err(|e| Error::new(format!("invalid request JSON: {e}")))?;
305 let response = self
306 .instance
307 .authorize_unsigned_batch(cedar_request)
308 .await
309 .map_err(Error::new)?;
310 Ok(response.into())
311 }
312
313 /// Authorize a batch of multi-issuer requests against one shared token set.
314 ///
315 /// Tokens are validated and token/issuer entities are built once, then
316 /// each item is evaluated in input order. Batch-level failures (validation,
317 /// JWT verification, status-list refresh) reject the whole call; per-item
318 /// failures are returned as `BatchItemError` results and exposed by WASM
319 /// with `is_ok=false` and `error`, while genuine Cedar denials remain
320 /// `AuthorizeResult` values with `decision=false`.
321 ///
322 /// # Arguments
323 ///
324 /// * `request` - JSON string representation of [`BatchAuthorizeMultiIssuerRequest`].
325 ///
326 /// # Example
327 ///
328 /// ```javascript
329 /// const result = await cedarling.authorizeMultiIssuerBatch(JSON.stringify(batchRequest));
330 /// ```
331 #[wasm_bindgen(js_name = authorizeMultiIssuerBatch)]
332 pub async fn authorize_multi_issuer_batch(
333 &self,
334 request: &str,
335 ) -> Result<BatchAuthorizeMultiIssuerResponse, Error> {
336 let cedar_request: BatchAuthorizeMultiIssuerRequest = serde_json::from_str(request)
337 .map_err(|e| Error::new(format!("invalid request JSON: {e}")))?;
338 let response = self
339 .instance
340 .authorize_multi_issuer_batch(cedar_request)
341 .await
342 .map_err(Error::new)?;
343 Ok(response.into())
344 }
345
346 /// Merge the annotations (`@key("value")`) of the given policies into a single object.
347 ///
348 /// Intended for resolving the determining policies of an authorization decision:
349 /// pass `result.response.diagnostics.reason`.
350 ///
351 /// Lossy: if the same annotation key appears on several policies, one value wins
352 /// arbitrarily. Use `annotationValues` / `annotationsByPolicy` when duplicates
353 /// matter. Unknown policy IDs are silently skipped.
354 ///
355 /// # Arguments
356 ///
357 /// * `policy_ids` - List of policy IDs whose annotations should be merged into
358 /// a single object. Typically `result.response.diagnostics.reason` from an
359 /// authorization result.
360 ///
361 /// # Example
362 ///
363 /// ```javascript
364 /// const annotations = cedarling.annotationsMap(result.response.diagnostics.reason);
365 /// // { redirect: "/upgrade", tier: "premium" }
366 /// ```
367 #[wasm_bindgen(js_name = annotationsMap)]
368 pub fn annotations_map(&self, policy_ids: Vec<String>) -> Result<JsValue, Error> {
369 let ids: Vec<PolicyId> = policy_ids.iter().map(PolicyId::new).collect();
370 let annotations = self.instance.annotations_map(ids.iter());
371 to_object_recursive(serde_wasm_bindgen::to_value(&annotations)?)
372 }
373
374 /// Collect every value of the annotation `key` across the given policies,
375 /// preserving duplicates. Unknown policy IDs are silently skipped.
376 ///
377 /// # Arguments
378 ///
379 /// * `policy_ids` - List of policy IDs to search. Typically
380 /// `result.response.diagnostics.reason` from an authorization result.
381 /// * `key` - The annotation key to collect values for (e.g. `"redirect"`).
382 ///
383 /// # Example
384 ///
385 /// ```javascript
386 /// const redirects = cedarling.annotationValues(
387 /// result.response.diagnostics.reason,
388 /// "redirect",
389 /// );
390 /// // ["/upgrade"]
391 /// ```
392 #[wasm_bindgen(js_name = annotationValues)]
393 pub fn annotation_values(&self, policy_ids: Vec<String>, key: &str) -> Vec<String> {
394 let ids: Vec<PolicyId> = policy_ids.iter().map(PolicyId::new).collect();
395 self.instance.annotation_values(ids.iter(), key)
396 }
397
398 /// Return the annotations of each given policy, grouped by policy ID. It is
399 /// the loss-free companion to `annotationsMap`. Unknown policy IDs are
400 /// silently skipped.
401 ///
402 /// # Arguments
403 ///
404 /// * `policy_ids` - List of policy IDs whose annotations should be returned
405 /// grouped by policy ID. Typically `result.response.diagnostics.reason` from
406 /// an authorization result.
407 ///
408 /// # Example
409 ///
410 /// ```javascript
411 /// const byPolicy = cedarling.annotationsByPolicy(result.response.diagnostics.reason);
412 /// // { "5": { redirect: "/upgrade", tier: "premium" } }
413 /// ```
414 #[wasm_bindgen(js_name = annotationsByPolicy)]
415 pub fn annotations_by_policy(&self, policy_ids: Vec<String>) -> Result<JsValue, Error> {
416 let ids: Vec<PolicyId> = policy_ids.iter().map(PolicyId::new).collect();
417 let by_policy = self.instance.annotations_by_policy(ids.iter());
418 to_object_recursive(serde_wasm_bindgen::to_value(&by_policy)?)
419 }
420
421 /// Returns all retained memory logs and removes them from storage.
422 /// Other log configurations return an empty array.
423 ///
424 /// # Arguments
425 ///
426 /// None.
427 ///
428 /// # Example
429 ///
430 /// ```javascript
431 /// const logs = cedarling.popLogs();
432 /// ```
433 #[wasm_bindgen(js_name = popLogs)]
434 pub fn pop_logs(&self) -> Result<Array, Error> {
435 let result = Array::new();
436 for log in self.instance.pop_logs() {
437 let js_log = convert_json_to_object(&log)?;
438 result.push(&js_log);
439 }
440 Ok(result)
441 }
442
443 /// Returns one retained memory log by ID, or `null` when it is not retained.
444 ///
445 /// # Arguments
446 ///
447 /// * `id` - The retained log identifier.
448 ///
449 /// # Example
450 ///
451 /// ```javascript
452 /// const log = cedarling.getLogById("request-id");
453 /// ```
454 #[wasm_bindgen(js_name = getLogById)]
455 pub fn get_log_by_id(&self, id: &str) -> Result<JsValue, Error> {
456 let result = if let Some(log_json_value) = self.instance.get_log_by_id(id) {
457 convert_json_to_object(&log_json_value)?
458 } else {
459 JsValue::NULL
460 };
461 Ok(result)
462 }
463
464 /// Returns identifiers for all retained memory logs.
465 ///
466 /// # Arguments
467 ///
468 /// None.
469 ///
470 /// # Example
471 ///
472 /// ```javascript
473 /// const ids = cedarling.getLogIds();
474 /// ```
475 #[wasm_bindgen(js_name = getLogIds)]
476 pub fn get_log_ids(&self) -> Array {
477 let result = Array::new();
478 for log_id in self.instance.get_log_ids() {
479 let js_id = log_id.into();
480 result.push(&js_id);
481 }
482 result
483 }
484
485 /// Returns retained memory logs matching an indexed value.
486 ///
487 /// # Arguments
488 ///
489 /// * `tag` - A log kind (`"System"`, `"Decision"`, or `"Metric"`) or a
490 /// system-log level such as `"DEBUG"`.
491 ///
492 /// # Example
493 ///
494 /// ```javascript
495 /// const logs = cedarling.getLogsByTag("System");
496 /// ```
497 #[wasm_bindgen(js_name = getLogsByTag)]
498 pub fn get_logs_by_tag(&self, tag: &str) -> Result<Vec<JsValue>, Error> {
499 self.instance
500 .get_logs_by_tag(tag)
501 .iter()
502 .map(convert_json_to_object)
503 .collect()
504 }
505
506 /// Returns retained memory logs for one request ID.
507 ///
508 /// # Arguments
509 ///
510 /// * `requestId` - The request identifier to match.
511 ///
512 /// # Example
513 ///
514 /// ```javascript
515 /// const logs = cedarling.getLogsByRequestId("request-id");
516 /// ```
517 #[wasm_bindgen(js_name = getLogsByRequestId)]
518 pub fn get_logs_by_request_id(&self, request_id: &str) -> Result<Vec<JsValue>, Error> {
519 self.instance
520 .get_logs_by_request_id(request_id)
521 .iter()
522 .map(convert_json_to_object)
523 .collect()
524 }
525
526 /// Returns retained memory logs matching one request ID and indexed value.
527 ///
528 /// # Arguments
529 ///
530 /// * `requestId` - The request identifier to match.
531 /// * `tag` - A log kind (`"System"`, `"Decision"`, or `"Metric"`) or a
532 /// system-log level such as `"DEBUG"`.
533 ///
534 /// # Example
535 ///
536 /// ```javascript
537 /// const logs = cedarling.getLogsByRequestIdAndTag("request-id", "System");
538 /// ```
539 #[wasm_bindgen(js_name = getLogsByRequestIdAndTag)]
540 pub fn get_logs_by_request_id_and_tag(
541 &self,
542 request_id: &str,
543 tag: &str,
544 ) -> Result<Vec<JsValue>, Error> {
545 self.instance
546 .get_logs_by_request_id_and_tag(request_id, tag)
547 .iter()
548 .map(convert_json_to_object)
549 .collect()
550 }
551
552 /// Closes Lock Server connections and pushes all available logs.
553 ///
554 /// # Arguments
555 ///
556 /// None.
557 ///
558 /// # Example
559 ///
560 /// ```javascript
561 /// await cedarling.shutDown();
562 /// ```
563 #[wasm_bindgen(js_name = shutDown)]
564 pub async fn shut_down(&self) {
565 self.instance.shut_down().await;
566 }
567
568 /// Push a value into the data store with an optional TTL.
569 /// If the key already exists, the value will be replaced.
570 /// If TTL is not provided, the default TTL from configuration is used.
571 ///
572 /// # Arguments
573 ///
574 /// * `key` - A string key for the data entry (must not be empty)
575 /// * `value` - The value to store (any JSON-serializable JavaScript value: object, array, string, number, boolean)
576 /// * `ttl_secs` - Optional TTL in seconds (undefined/null uses default from config)
577 ///
578 /// # Example
579 ///
580 /// ```javascript
581 /// cedarling.pushDataCtx("user:123", { name: "John", age: 30 }, 3600);
582 /// cedarling.pushDataCtx("config", { setting: "value" }); // Uses default TTL
583 /// ```
584 #[wasm_bindgen(js_name = pushDataCtx)]
585 pub fn push_data_ctx(
586 &self,
587 key: &str,
588 value: JsValue,
589 ttl_secs: Option<u64>,
590 ) -> Result<(), Error> {
591 let json_value: serde_json::Value = serde_wasm_bindgen::from_value(value)?;
592
593 // Reject null values on write
594 if json_value.is_null() {
595 return Err(Error::new("null values are not allowed in data ctx"));
596 }
597
598 let ttl = ttl_secs.map(Duration::from_secs);
599 self.instance
600 .push_data_ctx(key, json_value, ttl)
601 .map_err(Error::new)
602 }
603
604 /// Get a value from the data store by key.
605 /// Returns null if the key doesn't exist or the entry has expired.
606 ///
607 /// # Arguments
608 ///
609 /// * `key` - A string key for the data entry to retrieve
610 ///
611 /// # Example
612 ///
613 /// ```javascript
614 /// const value = cedarling.getDataCtx("user:123");
615 /// if (value !== null) {
616 /// console.log(value.name); // "John"
617 /// }
618 /// ```
619 #[wasm_bindgen(js_name = getDataCtx)]
620 pub fn get_data_ctx(&self, key: &str) -> Result<JsValue, Error> {
621 match self.instance.get_data_ctx(key).map_err(Error::new)? {
622 Some(value) => {
623 let js_value = serde_wasm_bindgen::to_value(&value)?;
624 Ok(to_object_recursive(js_value)?)
625 },
626 None => Ok(JsValue::NULL),
627 }
628 }
629
630 /// Get a data entry with full metadata by key.
631 /// Returns undefined if the key doesn't exist or the entry has expired.
632 ///
633 /// # Arguments
634 ///
635 /// * `key` - A string key for the data entry to retrieve
636 ///
637 /// # Example
638 ///
639 /// ```javascript
640 /// const entry = cedarling.getDataEntryCtx("user:123");
641 /// if (entry !== undefined) {
642 /// console.log(entry.key); // "user:123"
643 /// console.log(entry.value()); // { name: "John", age: 30 }
644 /// console.log(entry.data_type); // "Record"
645 /// console.log(entry.created_at); // "2024-01-01T12:00:00Z"
646 /// console.log(entry.access_count); // 5
647 /// }
648 /// ```
649 #[wasm_bindgen(js_name = getDataEntryCtx)]
650 pub fn get_data_entry_ctx(&self, key: &str) -> Result<Option<DataEntry>, Error> {
651 match self.instance.get_data_entry_ctx(key).map_err(Error::new)? {
652 Some(entry) => {
653 let wasm_entry = DataEntry::from(entry);
654 Ok(Some(wasm_entry))
655 },
656 None => Ok(None),
657 }
658 }
659
660 /// Remove a value from the data store by key.
661 /// Returns true if the key existed and was removed, false otherwise.
662 ///
663 /// # Arguments
664 ///
665 /// * `key` - A string key for the data entry to remove
666 ///
667 /// # Example
668 ///
669 /// ```javascript
670 /// const removed = cedarling.removeDataCtx("user:123");
671 /// if (removed) {
672 /// console.log("Entry was successfully removed");
673 /// }
674 /// ```
675 #[wasm_bindgen(js_name = removeDataCtx)]
676 pub fn remove_data_ctx(&self, key: &str) -> Result<bool, Error> {
677 self.instance.remove_data_ctx(key).map_err(Error::new)
678 }
679
680 /// Clear all entries from the data store.
681 ///
682 /// # Arguments
683 ///
684 /// None.
685 ///
686 /// # Example
687 ///
688 /// ```javascript
689 /// cedarling.clearDataCtx();
690 /// console.log("All data entries cleared");
691 /// ```
692 #[wasm_bindgen(js_name = clearDataCtx)]
693 pub fn clear_data_ctx(&self) -> Result<(), Error> {
694 self.instance.clear_data_ctx().map_err(Error::new)
695 }
696
697 /// List all entries with their metadata.
698 /// Returns an array of DataEntry objects.
699 ///
700 /// # Arguments
701 ///
702 /// None.
703 ///
704 /// # Example
705 ///
706 /// ```javascript
707 /// const entries = cedarling.listDataCtx();
708 /// entries.forEach(entry => {
709 /// console.log(`${entry.key}: ${entry.data_type} (accessed ${entry.access_count} times)`);
710 /// });
711 /// ```
712 #[wasm_bindgen(js_name = listDataCtx)]
713 pub fn list_data_ctx(&self) -> Result<Array, Error> {
714 let entries = self.instance.list_data_ctx().map_err(Error::new)?;
715 let result = Array::new();
716 for entry in entries {
717 let wasm_entry = DataEntry::from(entry);
718 result.push(&wasm_bindgen::JsValue::from(wasm_entry));
719 }
720 Ok(result)
721 }
722
723 /// Get statistics about the data store.
724 ///
725 /// # Arguments
726 ///
727 /// None.
728 ///
729 /// # Example
730 ///
731 /// ```javascript
732 /// const stats = cedarling.getStatsCtx();
733 /// console.log(`Entries: ${stats.entry_count}/${stats.max_entries || 'unlimited'}`);
734 /// console.log(`Capacity: ${stats.capacity_usage_percent.toFixed(2)}%`);
735 /// console.log(`Total size: ${stats.total_size_bytes} bytes`);
736 /// ```
737 #[wasm_bindgen(js_name = getStatsCtx)]
738 pub fn get_stats_ctx(&self) -> Result<DataStoreStats, Error> {
739 self.instance
740 .get_stats_ctx()
741 .map(|stats| stats.into())
742 .map_err(Error::new)
743 }
744
745 /// Destructive read: returns the telemetry metrics snapshot and resets
746 /// the counters for the next interval.
747 ///
748 /// Fails when `CEDARLING_METRICS_COLLECTION` is disabled or whenever
749 /// `CEDARLING_LOCK_TELEMETRY_INTERVAL` is set (even if the Lock server
750 /// has no telemetry endpoint). `interval_secs` is fractional seconds,
751 /// so sub-second intervals are reported exactly.
752 ///
753 /// # Example
754 ///
755 /// ```javascript
756 /// const snapshot = cedarling.drainMetrics();
757 /// console.log(`Requests: ${snapshot.operational_stats.get("authz.requests_total")}`);
758 /// ```
759 #[wasm_bindgen(js_name = drainMetrics)]
760 pub fn drain_metrics(&self) -> Result<MetricsSnapshot, Error> {
761 self.instance
762 .drain_metrics()
763 .map(Into::into)
764 .map_err(Error::new)
765 }
766
767 /// Get the ID of the currently published policy store, if it carries one.
768 ///
769 /// `undefined` means the store carries no ID. The value is an opaque,
770 /// source-dependent string; it may change after a background refresh.
771 ///
772 /// # Arguments
773 ///
774 /// None.
775 ///
776 /// # Example
777 ///
778 /// ```javascript
779 /// const id = cedarling.policyStoreId();
780 /// ```
781 #[wasm_bindgen(js_name = policyStoreId)]
782 pub fn policy_store_id(&self) -> Option<String> {
783 self.instance.policy_store_id()
784 }
785
786 /// Check whether a trusted issuer was loaded by issuer identifier.
787 ///
788 /// # Arguments
789 ///
790 /// * `issuer_id` - Trusted issuer identifier to check.
791 ///
792 /// # Example
793 ///
794 /// ```javascript
795 /// const ok = cedarling.isTrustedIssuerLoadedByName("issuer_id");
796 /// ```
797 #[wasm_bindgen(js_name = isTrustedIssuerLoadedByName)]
798 pub fn is_trusted_issuer_loaded_by_name(&self, issuer_id: &str) -> bool {
799 self.instance.is_trusted_issuer_loaded_by_name(issuer_id)
800 }
801
802 /// Check whether a trusted issuer was loaded by `iss` claim.
803 ///
804 /// # Arguments
805 ///
806 /// * `iss_claim` - Issuer `iss` claim value to check.
807 ///
808 /// # Example
809 ///
810 /// ```javascript
811 /// const ok = cedarling.isTrustedIssuerLoadedByIss("https://issuer.example.org");
812 /// ```
813 #[wasm_bindgen(js_name = isTrustedIssuerLoadedByIss)]
814 pub fn is_trusted_issuer_loaded_by_iss(&self, iss_claim: &str) -> bool {
815 self.instance.is_trusted_issuer_loaded_by_iss(iss_claim)
816 }
817
818 /// Get the total number of trusted issuer entries discovered.
819 ///
820 /// # Arguments
821 ///
822 /// None.
823 ///
824 /// # Example
825 ///
826 /// ```javascript
827 /// const total = cedarling.totalIssuers();
828 /// ```
829 #[wasm_bindgen(js_name = totalIssuers)]
830 pub fn total_issuers(&self) -> usize {
831 self.instance.total_issuers()
832 }
833
834 /// Get the number of trusted issuers loaded successfully.
835 ///
836 /// # Arguments
837 ///
838 /// None.
839 ///
840 /// # Example
841 ///
842 /// ```javascript
843 /// const loadedCount = cedarling.loadedTrustedIssuersCount();
844 /// ```
845 #[wasm_bindgen(js_name = loadedTrustedIssuersCount)]
846 pub fn loaded_trusted_issuers_count(&self) -> usize {
847 self.instance.loaded_trusted_issuers_count()
848 }
849
850 /// Get trusted issuer identifiers loaded successfully.
851 ///
852 /// # Arguments
853 ///
854 /// None.
855 ///
856 /// # Example
857 ///
858 /// ```javascript
859 /// const ids = cedarling.loadedTrustedIssuerIds();
860 /// ```
861 #[wasm_bindgen(js_name = loadedTrustedIssuerIds)]
862 pub fn loaded_trusted_issuer_ids(&self) -> Array {
863 let result = Array::new();
864 for id in self.instance.loaded_trusted_issuer_ids() {
865 result.push(&id.into());
866 }
867 result
868 }
869
870 /// Get trusted issuer identifiers that failed to load.
871 ///
872 /// # Arguments
873 ///
874 /// None.
875 ///
876 /// # Example
877 ///
878 /// ```javascript
879 /// const ids = cedarling.failedTrustedIssuerIds();
880 /// ```
881 #[wasm_bindgen(js_name = failedTrustedIssuerIds)]
882 pub fn failed_trusted_issuer_ids(&self) -> Array {
883 let result = Array::new();
884 for id in self.instance.failed_trusted_issuer_ids() {
885 result.push(&id.into());
886 }
887 result
888 }
889}
890
891/// convert json to js object
892fn convert_json_to_object(json_value: &serde_json::Value) -> Result<JsValue, Error> {
893 let js_map_value = serde_wasm_bindgen::to_value(json_value)?;
894 to_object_recursive(js_map_value)
895}
896
897/// recurcive convert [`Map`] to object
898fn to_object_recursive(value: JsValue) -> Result<JsValue, Error> {
899 if value.is_instance_of::<Map>() {
900 // Convert the Map into an Object where keys and values are recursively processed
901 let map = Map::from(value);
902 let obj = Object::new();
903 for entry in map.entries().into_iter() {
904 let entry = Array::unchecked_from_js(entry?);
905 let key = entry.get(0);
906 let val = to_object_recursive(entry.get(1))?;
907 Reflect::set(&obj, &key, &val)?;
908 }
909 Ok(obj.into())
910 } else if value.is_instance_of::<Array>() {
911 // Recursively process arrays
912 let array = Array::unchecked_from_js(value);
913 let serialized_array = Array::new();
914 for item in array.iter() {
915 serialized_array.push(&to_object_recursive(item)?);
916 }
917 Ok(serialized_array.into())
918 } else if value.is_object() {
919 // Recursively process plain objects
920 let obj = Object::from(value);
921 let keys = Object::keys(&obj);
922 let serialized_obj = Object::new();
923 for key in keys.iter() {
924 let val = Reflect::get(&obj, &key)?;
925 Reflect::set(&serialized_obj, &key, &to_object_recursive(val)?)?;
926 }
927 Ok(serialized_obj.into())
928 } else {
929 // Return primitive values as-is
930 Ok(value)
931 }
932}
933
934/// A WASM wrapper for the Rust `cedarling::AuthorizeResult` struct.
935/// Represents the result of an authorization request.
936#[wasm_bindgen]
937#[derive(Clone, Debug, serde::Serialize)]
938pub struct AuthorizeResult {
939 /// Cedar authorization response for the request.
940 #[wasm_bindgen(getter_with_clone)]
941 pub response: AuthorizeResultResponse,
942
943 /// Result of authorization
944 /// true means `ALLOW`
945 /// false means `Deny`
946 ///
947 /// this field is [`bool`] type to be compatible with [authzen Access Evaluation Decision](https://openid.github.io/authzen/#section-6.2.1).
948 pub decision: bool,
949
950 /// Request ID of the authorization request
951 #[wasm_bindgen(getter_with_clone)]
952 pub request_id: String,
953}
954
955#[wasm_bindgen]
956impl AuthorizeResult {
957 /// Convert `AuthorizeResult` to json string value
958 #[wasm_bindgen(js_name = jsonString)]
959 pub fn json_string(&self) -> String {
960 json!(self).to_string()
961 }
962}
963
964impl From<cedarling::AuthorizeResult> for AuthorizeResult {
965 fn from(value: cedarling::AuthorizeResult) -> Self {
966 Self {
967 response: AuthorizeResultResponse {
968 inner: Rc::new(value.response),
969 },
970 decision: value.decision,
971 request_id: value.request_id,
972 }
973 }
974}
975
976/// Per-item build failure surfaced inside a batch response at `results[i]`
977/// when Cedar couldn't be reached for that item.
978#[wasm_bindgen]
979#[derive(Clone, Debug)]
980pub struct BatchItemError {
981 inner: CedarBatchItemError,
982}
983
984#[wasm_bindgen]
985impl BatchItemError {
986 /// Stable variant slug — `action_parse`, `resource_build`, `context_build`,
987 /// `principal_build`, `schema_validation`, `multi_issuer_entity`,
988 /// `request_validation`.
989 #[wasm_bindgen(getter)]
990 pub fn category(&self) -> String {
991 self.inner.category().to_string()
992 }
993
994 /// Position of the failing item in the original `items` vector.
995 #[wasm_bindgen(getter)]
996 pub fn item_index(&self) -> usize {
997 self.inner.item_index()
998 }
999
1000 /// Human-readable diagnostic. Safe to log.
1001 #[wasm_bindgen(getter)]
1002 pub fn message(&self) -> String {
1003 self.inner.to_string()
1004 }
1005}
1006
1007impl From<CedarBatchItemError> for BatchItemError {
1008 fn from(inner: CedarBatchItemError) -> Self {
1009 Self { inner }
1010 }
1011}
1012
1013/// One slot in a batch response's `results` array. Callers switch on
1014/// `is_ok()` — on `true`, read `unwrap()`; on `false`, read `error()`.
1015#[wasm_bindgen]
1016#[derive(Clone, Debug)]
1017pub struct BatchItemUnsignedResult {
1018 inner: Result<AuthorizeResult, BatchItemError>,
1019}
1020
1021#[wasm_bindgen]
1022impl BatchItemUnsignedResult {
1023 /// `true` when Cedar evaluated this item (Allow or Deny); `false` when it
1024 /// failed to build.
1025 #[wasm_bindgen(getter)]
1026 pub fn is_ok(&self) -> bool {
1027 self.inner.is_ok()
1028 }
1029
1030 /// The Cedar decision if `is_ok()`; throws otherwise.
1031 pub fn unwrap(&self) -> Result<AuthorizeResult, Error> {
1032 self.inner
1033 .as_ref()
1034 .cloned()
1035 .map_err(|_| Error::new("BatchItemUnsignedResult is Err"))
1036 }
1037
1038 /// The per-item error if `!is_ok()`; `undefined` otherwise.
1039 #[wasm_bindgen(getter)]
1040 pub fn error(&self) -> Option<BatchItemError> {
1041 self.inner.as_ref().err().cloned()
1042 }
1043}
1044
1045impl From<Result<cedarling::AuthorizeResult, CedarBatchItemError>> for BatchItemUnsignedResult {
1046 fn from(r: Result<cedarling::AuthorizeResult, CedarBatchItemError>) -> Self {
1047 Self {
1048 inner: r.map(Into::into).map_err(Into::into),
1049 }
1050 }
1051}
1052
1053/// Multi-issuer analog of [`BatchItemUnsignedResult`].
1054#[wasm_bindgen]
1055#[derive(Clone, Debug)]
1056pub struct BatchItemMultiIssuerResult {
1057 inner: Result<MultiIssuerAuthorizeResult, BatchItemError>,
1058}
1059
1060#[wasm_bindgen]
1061impl BatchItemMultiIssuerResult {
1062 /// `true` when Cedar evaluated this item.
1063 #[wasm_bindgen(getter)]
1064 pub fn is_ok(&self) -> bool {
1065 self.inner.is_ok()
1066 }
1067
1068 /// The multi-issuer decision if `is_ok()`; throws otherwise.
1069 pub fn unwrap(&self) -> Result<MultiIssuerAuthorizeResult, Error> {
1070 self.inner
1071 .as_ref()
1072 .cloned()
1073 .map_err(|_| Error::new("BatchItemMultiIssuerResult is Err"))
1074 }
1075
1076 /// The per-item error if `!is_ok()`; `undefined` otherwise.
1077 #[wasm_bindgen(getter)]
1078 pub fn error(&self) -> Option<BatchItemError> {
1079 self.inner.as_ref().err().cloned()
1080 }
1081}
1082
1083impl From<Result<cedarling::MultiIssuerAuthorizeResult, CedarBatchItemError>>
1084 for BatchItemMultiIssuerResult
1085{
1086 fn from(r: Result<cedarling::MultiIssuerAuthorizeResult, CedarBatchItemError>) -> Self {
1087 Self {
1088 inner: r.map(Into::into).map_err(Into::into),
1089 }
1090 }
1091}
1092
1093/// WASM wrapper for `cedarling::BatchAuthorizeResponse<Result<AuthorizeResult, BatchItemError>>`.
1094///
1095/// Carries a shared `batch_id` (UUIDv7) alongside per-item results. Each result
1096/// is a [`BatchItemUnsignedResult`] — Cedar decision on `is_ok()`, per-item
1097/// build failure on `error`. `results[i]` corresponds to `items[i]`.
1098#[wasm_bindgen]
1099#[derive(Debug)]
1100pub struct BatchAuthorizeUnsignedResponse {
1101 inner_batch_id: String,
1102 inner_results: Vec<BatchItemUnsignedResult>,
1103}
1104
1105#[wasm_bindgen]
1106impl BatchAuthorizeUnsignedResponse {
1107 /// Shared correlation id stamped on every per-item decision log entry.
1108 #[wasm_bindgen(getter)]
1109 pub fn batch_id(&self) -> String {
1110 self.inner_batch_id.clone()
1111 }
1112
1113 /// Per-item results in input order — each slot is a
1114 /// [`BatchItemUnsignedResult`].
1115 #[wasm_bindgen(getter)]
1116 pub fn results(&self) -> Vec<BatchItemUnsignedResult> {
1117 self.inner_results.clone()
1118 }
1119}
1120
1121impl From<CedarBatchAuthorizeResponse<Result<cedarling::AuthorizeResult, CedarBatchItemError>>>
1122 for BatchAuthorizeUnsignedResponse
1123{
1124 fn from(
1125 value: CedarBatchAuthorizeResponse<Result<cedarling::AuthorizeResult, CedarBatchItemError>>,
1126 ) -> Self {
1127 Self {
1128 inner_batch_id: value.batch_id.to_string(),
1129 inner_results: value.results.into_iter().map(Into::into).collect(),
1130 }
1131 }
1132}
1133
1134/// WASM wrapper for
1135/// `cedarling::BatchAuthorizeResponse<Result<MultiIssuerAuthorizeResult, BatchItemError>>`.
1136/// Same shape as [`BatchAuthorizeUnsignedResponse`] with multi-issuer results.
1137#[wasm_bindgen]
1138#[derive(Debug)]
1139pub struct BatchAuthorizeMultiIssuerResponse {
1140 inner_batch_id: String,
1141 inner_results: Vec<BatchItemMultiIssuerResult>,
1142}
1143
1144#[wasm_bindgen]
1145impl BatchAuthorizeMultiIssuerResponse {
1146 /// Shared correlation id stamped on every per-item decision log entry.
1147 #[wasm_bindgen(getter)]
1148 pub fn batch_id(&self) -> String {
1149 self.inner_batch_id.clone()
1150 }
1151
1152 /// Per-item results in input order — each slot is a
1153 /// [`BatchItemMultiIssuerResult`].
1154 #[wasm_bindgen(getter)]
1155 pub fn results(&self) -> Vec<BatchItemMultiIssuerResult> {
1156 self.inner_results.clone()
1157 }
1158}
1159
1160impl
1161 From<
1162 CedarBatchAuthorizeResponse<
1163 Result<cedarling::MultiIssuerAuthorizeResult, CedarBatchItemError>,
1164 >,
1165 > for BatchAuthorizeMultiIssuerResponse
1166{
1167 fn from(
1168 value: CedarBatchAuthorizeResponse<
1169 Result<cedarling::MultiIssuerAuthorizeResult, CedarBatchItemError>,
1170 >,
1171 ) -> Self {
1172 Self {
1173 inner_batch_id: value.batch_id.to_string(),
1174 inner_results: value.results.into_iter().map(Into::into).collect(),
1175 }
1176 }
1177}
1178
1179/// A WASM wrapper for the Rust `cedar_policy::Response` struct.
1180/// Represents the result of an authorization request.
1181#[wasm_bindgen]
1182#[derive(Clone, Debug)]
1183pub struct AuthorizeResultResponse {
1184 // It can be premature optimization, but RC allows avoiding clone actual structure
1185 inner: Rc<cedar_policy::Response>,
1186}
1187
1188#[wasm_bindgen]
1189impl AuthorizeResultResponse {
1190 /// Authorization decision
1191 #[wasm_bindgen(getter)]
1192 pub fn decision(&self) -> bool {
1193 self.inner.decision() == cedar_policy::Decision::Allow
1194 }
1195
1196 /// Diagnostics providing more information on how this decision was reached
1197 #[wasm_bindgen(getter)]
1198 pub fn diagnostics(&self) -> Diagnostics {
1199 Diagnostics {
1200 inner: self.inner.diagnostics().clone(),
1201 }
1202 }
1203}
1204
1205impl Serialize for AuthorizeResultResponse {
1206 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1207 where
1208 S: Serializer,
1209 {
1210 let mut state = serializer.serialize_struct("Diagnostics", 2)?;
1211 state.serialize_field("decision", &self.decision())?;
1212 state.serialize_field("diagnostics", &self.diagnostics())?;
1213 state.end()
1214 }
1215}
1216
1217/// Diagnostics
1218/// ===========
1219///
1220/// Provides detailed information about how a policy decision was made, including policies that contributed to the decision and any errors encountered during evaluation.
1221#[wasm_bindgen]
1222pub struct Diagnostics {
1223 inner: cedar_policy::Diagnostics,
1224}
1225
1226#[wasm_bindgen]
1227impl Diagnostics {
1228 /// `PolicyId`s of the policies that contributed to the decision.
1229 /// If no policies applied to the request, this set will be empty.
1230 ///
1231 /// The ids should be treated as unordered,
1232 #[wasm_bindgen(getter)]
1233 pub fn reason(&self) -> Vec<String> {
1234 self.inner.reason().map(|v| v.to_string()).collect()
1235 }
1236
1237 /// Errors that occurred during authorization. The errors should be
1238 /// treated as unordered, since policies may be evaluated in any order.
1239 #[wasm_bindgen(getter)]
1240 pub fn errors(&self) -> Vec<PolicyEvaluationError> {
1241 self.inner
1242 .errors()
1243 .map(|err| {
1244 let mapped_error: cedarling::bindings::PolicyEvaluationError = err.into();
1245 PolicyEvaluationError {
1246 inner: mapped_error,
1247 }
1248 })
1249 .collect()
1250 }
1251}
1252
1253impl Serialize for Diagnostics {
1254 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1255 where
1256 S: Serializer,
1257 {
1258 let mut state = serializer.serialize_struct("Diagnostics", 2)?;
1259 state.serialize_field("reason", &self.reason())?;
1260 state.serialize_field("errors", &self.errors())?;
1261 state.end()
1262 }
1263}
1264
1265/// PolicyEvaluationError
1266/// =====================
1267///
1268/// Represents an error that occurred when evaluating a Cedar policy.
1269#[wasm_bindgen]
1270pub struct PolicyEvaluationError {
1271 inner: cedarling::bindings::PolicyEvaluationError,
1272}
1273
1274#[wasm_bindgen]
1275impl PolicyEvaluationError {
1276 /// Id of the policy with an error
1277 #[wasm_bindgen(getter)]
1278 pub fn id(&self) -> String {
1279 self.inner.id.clone()
1280 }
1281
1282 /// Underlying evaluation error string representation
1283 #[wasm_bindgen(getter)]
1284 pub fn error(&self) -> String {
1285 self.inner.error.clone()
1286 }
1287}
1288
1289impl Serialize for PolicyEvaluationError {
1290 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
1291 where
1292 S: Serializer,
1293 {
1294 let mut state = serializer.serialize_struct("PolicyEvaluationError", 2)?;
1295 state.serialize_field("id", &self.id())?;
1296 state.serialize_field("error", &self.error())?;
1297 state.end()
1298 }
1299}
1300
1301/// A WASM wrapper for the Rust `cedarling::DataEntry` struct.
1302/// Represents a data entry in the DataStore with value and metadata.
1303#[wasm_bindgen]
1304#[derive(serde::Serialize)]
1305pub struct DataEntry {
1306 /// The key for this entry
1307 #[wasm_bindgen(getter_with_clone)]
1308 pub key: String,
1309 /// The actual value stored (as JSON)
1310 #[wasm_bindgen(skip)]
1311 pub value: serde_json::Value,
1312 /// The inferred Cedar type of the value
1313 #[wasm_bindgen(getter_with_clone)]
1314 pub data_type: String,
1315 /// Timestamp when this entry was created (RFC 3339 format)
1316 #[wasm_bindgen(getter_with_clone)]
1317 pub created_at: String,
1318 /// Timestamp when this entry expires (RFC 3339 format). The getter returns undefined if no TTL.
1319 #[wasm_bindgen(getter_with_clone)]
1320 pub expires_at: Option<String>,
1321 /// Number of times this entry has been accessed
1322 pub access_count: u64,
1323}
1324
1325#[wasm_bindgen]
1326impl DataEntry {
1327 /// Get the value stored in this entry as a JavaScript object
1328 pub fn value(&self) -> Result<JsValue, Error> {
1329 let js_value = serde_wasm_bindgen::to_value(&self.value)?;
1330 to_object_recursive(js_value)
1331 }
1332
1333 /// Convert `DataEntry` to json string value
1334 #[wasm_bindgen(js_name = jsonString)]
1335 pub fn json_string(&self) -> String {
1336 json!(self).to_string()
1337 }
1338}
1339
1340impl From<CedarDataEntry> for DataEntry {
1341 fn from(value: CedarDataEntry) -> Self {
1342 Self {
1343 key: value.key,
1344 value: value.value,
1345 data_type: serde_json::to_string(&value.data_type)
1346 .unwrap_or_else(|_| "unknown".to_string())
1347 .trim_matches('"')
1348 .to_string(),
1349 created_at: value.created_at.to_rfc3339(),
1350 expires_at: value.expires_at.map(|dt| dt.to_rfc3339()),
1351 access_count: value.access_count,
1352 }
1353 }
1354}
1355
1356/// A WASM wrapper for the Rust `cedarling::DataStoreStats` struct.
1357/// Statistics about the DataStore.
1358#[wasm_bindgen]
1359#[derive(serde::Serialize)]
1360pub struct DataStoreStats {
1361 /// Number of entries currently stored
1362 pub entry_count: usize,
1363 /// Maximum number of entries allowed (0 = unlimited)
1364 pub max_entries: usize,
1365 /// Maximum size per entry in bytes (0 = unlimited)
1366 pub max_entry_size: usize,
1367 /// Whether metrics tracking is enabled
1368 pub metrics_enabled: bool,
1369 /// Total size of all entries in bytes (approximate, based on JSON serialization)
1370 pub total_size_bytes: usize,
1371 /// Average size per entry in bytes (0 if no entries)
1372 pub avg_entry_size_bytes: usize,
1373 /// Percentage of capacity used (0.0-100.0, based on entry count)
1374 pub capacity_usage_percent: f64,
1375 /// Memory usage threshold percentage (from config)
1376 pub memory_alert_threshold: f64,
1377 /// Whether memory usage exceeds the alert threshold
1378 pub memory_alert_triggered: bool,
1379}
1380
1381#[wasm_bindgen]
1382impl DataStoreStats {
1383 /// Convert `DataStoreStats` to json string value
1384 #[wasm_bindgen(js_name = jsonString)]
1385 pub fn json_string(&self) -> String {
1386 json!(self).to_string()
1387 }
1388}
1389
1390impl From<CedarDataStoreStats> for DataStoreStats {
1391 fn from(value: CedarDataStoreStats) -> Self {
1392 Self {
1393 entry_count: value.entry_count,
1394 max_entries: value.max_entries,
1395 max_entry_size: value.max_entry_size,
1396 metrics_enabled: value.metrics_enabled,
1397 total_size_bytes: value.total_size_bytes,
1398 avg_entry_size_bytes: value.avg_entry_size_bytes,
1399 capacity_usage_percent: value.capacity_usage_percent,
1400 memory_alert_threshold: value.memory_alert_threshold,
1401 memory_alert_triggered: value.memory_alert_triggered,
1402 }
1403 }
1404}
1405
1406/// A WASM wrapper for the Rust `cedarling::MetricsSnapshot` struct.
1407/// Represents a snapshot of the collected telemetry metrics for the current
1408/// interval. Destructive read: taking a snapshot resets the collected
1409/// metrics, so use a single consumer.
1410#[wasm_bindgen]
1411#[derive(Clone, Debug)]
1412pub struct MetricsSnapshot {
1413 /// Per-policy evaluation counts (`policy_id`, `policy_id.allow`, `policy_id.deny`).
1414 #[wasm_bindgen(getter_with_clone)]
1415 pub policy_stats: Map,
1416 /// Classified error counters keyed by error metric key.
1417 #[wasm_bindgen(getter_with_clone)]
1418 pub error_counters: Map,
1419 /// Operational counters and gauges (authorization, cache, JWT, data, lock).
1420 #[wasm_bindgen(getter_with_clone)]
1421 pub operational_stats: Map,
1422 /// Duration of the snapshot interval in fractional seconds.
1423 /// Exposed as `Number` (not `BigInt`) so plain JS arithmetic works.
1424 pub interval_secs: f64,
1425}
1426
1427#[wasm_bindgen]
1428impl MetricsSnapshot {
1429 /// Convert `MetricsSnapshot` to json string value.
1430 ///
1431 /// `policy_stats`, `error_counters` and `operational_stats` are converted
1432 /// from `Map` to plain objects so `JSON.stringify` emits their entries.
1433 #[wasm_bindgen(js_name = jsonString)]
1434 pub fn json_string(&self) -> Result<String, Error> {
1435 let obj = Object::new();
1436 Reflect::set(
1437 &obj,
1438 &"policy_stats".into(),
1439 &Object::from_entries(&self.policy_stats)?.into(),
1440 )?;
1441 Reflect::set(
1442 &obj,
1443 &"error_counters".into(),
1444 &Object::from_entries(&self.error_counters)?.into(),
1445 )?;
1446 Reflect::set(
1447 &obj,
1448 &"operational_stats".into(),
1449 &Object::from_entries(&self.operational_stats)?.into(),
1450 )?;
1451 Reflect::set(
1452 &obj,
1453 &"interval_secs".into(),
1454 &JsValue::from_f64(self.interval_secs),
1455 )?;
1456 Ok(String::from(js_sys::JSON::stringify(&obj)?))
1457 }
1458}
1459
1460impl From<CedarMetricsSnapshot> for MetricsSnapshot {
1461 fn from(value: CedarMetricsSnapshot) -> Self {
1462 fn to_map(counters: HashMap<String, i64>) -> Map {
1463 let map = Map::new();
1464 for (key, counter) in counters {
1465 map.set(&JsValue::from(key), &JsValue::from_f64(counter as f64));
1466 }
1467 map
1468 }
1469 Self {
1470 policy_stats: to_map(value.policy_stats),
1471 error_counters: to_map(value.error_counters),
1472 operational_stats: to_map(value.operational_stats),
1473 interval_secs: value.interval.as_secs_f64(),
1474 }
1475 }
1476}