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}