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,
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}