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