Skip to main content

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