briefcase-wasm 2.4.1

WebAssembly bindings for Briefcase AI
Documentation
//! WASM bindings for the BriefcaseClient.
//!
//! WASM environments cannot make HTTP requests directly, so this module
//! provides a "pre-validated" client that is constructed from a validation
//! response obtained server-side.
//!
//! # Usage
//!
//! ```javascript
//! // 1. Validate on the server (via fetch)
//! const resp = await fetch('/api/v1/auth/validate', {
//!   method: 'POST',
//!   body: JSON.stringify({ api_key: 'sk-my-key' })
//! });
//! const data = await resp.json();
//!
//! // 2. Construct WASM client from response
//! const client = JsBriefcaseClient.fromValidationResponse(JSON.stringify(data));
//! console.log(client.clientId());
//! console.log(client.hasPermission("read"));
//! ```

use wasm_bindgen::prelude::*;

/// Authenticated client for the Briefcase AI platform (WASM, pre-validated).
///
/// Constructed from a server validation response since WASM cannot make
/// HTTP calls directly.
#[wasm_bindgen]
pub struct JsBriefcaseClient {
    client_id: String,
    permissions: Vec<String>,
    rate_limit_rps: Option<u32>,
}

#[wasm_bindgen]
impl JsBriefcaseClient {
    /// Create a client from explicit parameters.
    #[wasm_bindgen(constructor)]
    pub fn new(client_id: String, permissions_json: &str) -> Result<JsBriefcaseClient, JsError> {
        let permissions: Vec<String> = serde_json::from_str(permissions_json)
            .map_err(|e| JsError::new(&format!("Invalid permissions JSON: {}", e)))?;

        Ok(JsBriefcaseClient {
            client_id,
            permissions,
            rate_limit_rps: None,
        })
    }

    /// Create a client from a full validation response JSON.
    ///
    /// Expected shape:
    /// ```json
    /// {
    ///   "valid": true,
    ///   "client": {
    ///     "client_id": "acme",
    ///     "permissions": ["read", "write"],
    ///     "rate_limit_rps": 100
    ///   },
    ///   "expires_at": "..."
    /// }
    /// ```
    #[wasm_bindgen(js_name = "fromValidationResponse")]
    pub fn from_validation_response(json: &str) -> Result<JsBriefcaseClient, JsError> {
        let parsed: serde_json::Value = serde_json::from_str(json)
            .map_err(|e| JsError::new(&format!("Invalid JSON: {}", e)))?;

        let valid = parsed["valid"].as_bool().unwrap_or(false);
        if !valid {
            return Err(JsError::new("Validation response indicates invalid key"));
        }

        let client = &parsed["client"];
        let client_id = client["client_id"]
            .as_str()
            .ok_or_else(|| JsError::new("Missing client.client_id"))?
            .to_string();

        let permissions = client["permissions"]
            .as_array()
            .ok_or_else(|| JsError::new("Missing client.permissions"))?
            .iter()
            .filter_map(|v| v.as_str().map(String::from))
            .collect();

        let rate_limit_rps = client["rate_limit_rps"].as_u64().map(|v| v as u32);

        Ok(JsBriefcaseClient {
            client_id,
            permissions,
            rate_limit_rps,
        })
    }

    /// The authenticated client ID.
    #[wasm_bindgen(js_name = "clientId")]
    pub fn client_id(&self) -> String {
        self.client_id.clone()
    }

    /// Check whether this client has a specific permission.
    #[wasm_bindgen(js_name = "hasPermission")]
    pub fn has_permission(&self, permission: &str) -> bool {
        self.permissions.iter().any(|p| p == permission)
    }

    /// Get the list of granted permissions.
    #[wasm_bindgen(js_name = "getPermissions")]
    pub fn get_permissions(&self) -> Vec<String> {
        self.permissions.clone()
    }

    /// Get the rate limit (requests per second), if set.
    #[wasm_bindgen(js_name = "rateLimitRps")]
    pub fn rate_limit_rps(&self) -> Option<u32> {
        self.rate_limit_rps
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_constructor_valid() {
        let client = JsBriefcaseClient::new("acme".into(), r#"["read","write"]"#).unwrap();
        assert_eq!(client.client_id(), "acme");
        assert!(client.has_permission("read"));
        assert!(client.has_permission("write"));
        assert!(!client.has_permission("delete"));
    }

    #[test]
    fn test_constructor_empty_permissions() {
        let client = JsBriefcaseClient::new("acme".into(), "[]").unwrap();
        assert_eq!(client.client_id(), "acme");
        assert!(!client.has_permission("read"));
        assert_eq!(client.get_permissions().len(), 0);
    }

    #[test]
    #[cfg(target_arch = "wasm32")]
    fn test_constructor_invalid_json() {
        let result = JsBriefcaseClient::new("acme".into(), "not json");
        assert!(result.is_err());
    }

    #[test]
    fn test_from_validation_response_valid() {
        let json = r#"{
            "valid": true,
            "client": {
                "client_id": "beta",
                "permissions": ["read", "write", "replay"],
                "rate_limit_rps": 200
            },
            "expires_at": "2026-02-07T00:00:00Z"
        }"#;

        let client = JsBriefcaseClient::from_validation_response(json).unwrap();
        assert_eq!(client.client_id(), "beta");
        assert!(client.has_permission("read"));
        assert!(client.has_permission("replay"));
        assert!(!client.has_permission("admin"));
        assert_eq!(client.rate_limit_rps(), Some(200));
        assert_eq!(client.get_permissions().len(), 3);
    }

    #[test]
    #[cfg(target_arch = "wasm32")]
    fn test_from_validation_response_invalid_key() {
        let json = r#"{
            "valid": false,
            "error": "Invalid API key"
        }"#;

        let result = JsBriefcaseClient::from_validation_response(json);
        assert!(result.is_err());
    }

    #[test]
    #[cfg(target_arch = "wasm32")]
    fn test_from_validation_response_missing_client() {
        let json = r#"{"valid": true}"#;
        let result = JsBriefcaseClient::from_validation_response(json);
        assert!(result.is_err());
    }

    #[test]
    #[cfg(target_arch = "wasm32")]
    fn test_from_validation_response_invalid_json() {
        let result = JsBriefcaseClient::from_validation_response("{invalid");
        assert!(result.is_err());
    }

    #[test]
    fn test_from_validation_response_no_rate_limit() {
        let json = r#"{
            "valid": true,
            "client": {
                "client_id": "basic",
                "permissions": ["read"]
            },
            "expires_at": "2026-02-07T00:00:00Z"
        }"#;

        let client = JsBriefcaseClient::from_validation_response(json).unwrap();
        assert_eq!(client.rate_limit_rps(), None);
    }

    #[test]
    fn test_has_permission_case_sensitive() {
        let client = JsBriefcaseClient::new("acme".into(), r#"["Read"]"#).unwrap();
        assert!(client.has_permission("Read"));
        assert!(!client.has_permission("read"));
    }
}