Skip to main content

jacs_binding_core/
simple_wrapper.rs

1//! `SimpleAgentWrapper` — thin FFI adapter over the narrow `SimpleAgent` contract.
2//!
3//! This module contains zero business logic. Every method delegates to
4//! `jacs::simple::SimpleAgent` and marshals the result to FFI-safe types
5//! (String in/out, base64 for bytes, JSON for structured data).
6
7use crate::{BindingCoreError, BindingResult};
8use jacs::simple::SimpleAgent;
9use std::sync::Arc;
10
11/// Thread-safe, Clone-able FFI wrapper around the narrow [`SimpleAgent`] contract.
12///
13/// All methods return `BindingResult<String>` (or simple scalars) so that
14/// language bindings (Python/PyO3, Node/NAPI, Go/CGo) never touch Rust-only types.
15#[derive(Clone)]
16pub struct SimpleAgentWrapper {
17    inner: Arc<SimpleAgent>,
18}
19
20// Compile-time proof of thread safety.
21const _: () = {
22    fn _assert<T: Send + Sync>() {}
23    let _ = _assert::<SimpleAgentWrapper>;
24};
25
26impl SimpleAgentWrapper {
27    // WARNING: If you add or remove a public method here, update BOTH:
28    //   1. binding-core/tests/fixtures/method_parity.json  (canonical method list)
29    //   2. binding-core/tests/method_parity.rs::known_methods()  (compile-time anchor)
30    // All language bindings (Python, Node, Go) have parity tests against that fixture.
31
32    // =========================================================================
33    // Constructors
34    // =========================================================================
35
36    /// Create a new agent with persistent identity.
37    ///
38    /// Returns `(wrapper, info_json)` where `info_json` is a serialized
39    /// [`jacs::simple::AgentInfo`].
40    pub fn create(
41        name: &str,
42        purpose: Option<&str>,
43        key_algorithm: Option<&str>,
44    ) -> BindingResult<(Self, String)> {
45        let (agent, info) = SimpleAgent::create(name, purpose, key_algorithm)
46            .map_err(|e| BindingCoreError::agent_load(format!("Failed to create agent: {}", e)))?;
47        let info_json = crate::serialize_agent_info(&info)?;
48
49        Ok((
50            Self {
51                inner: Arc::new(agent),
52            },
53            info_json,
54        ))
55    }
56
57    /// Load an existing agent from a config file.
58    pub fn load(config_path: Option<&str>, strict: Option<bool>) -> BindingResult<Self> {
59        let (wrapper, _info_json) = Self::load_with_info(config_path, strict)?;
60        Ok(wrapper)
61    }
62
63    /// Load an existing agent from a config file and return canonical metadata.
64    pub fn load_with_info(
65        config_path: Option<&str>,
66        strict: Option<bool>,
67    ) -> BindingResult<(Self, String)> {
68        let requested_path = config_path.unwrap_or("./jacs.config.json");
69        let resolved_config_path = crate::resolve_existing_config_path(requested_path)?;
70        let agent = SimpleAgent::load(Some(&resolved_config_path), strict)
71            .map_err(|e| BindingCoreError::agent_load(format!("Failed to load agent: {}", e)))?;
72        let info = agent
73            .loaded_info()
74            .map_err(|e| BindingCoreError::agent_load(format!("Failed to load agent: {}", e)))?;
75        let info_json = crate::serialize_agent_info(&info)?;
76
77        Ok((
78            Self {
79                inner: Arc::new(agent),
80            },
81            info_json,
82        ))
83    }
84
85    /// Create an ephemeral (in-memory, throwaway) agent.
86    ///
87    /// Returns `(wrapper, info_json)`.
88    pub fn ephemeral(algorithm: Option<&str>) -> BindingResult<(Self, String)> {
89        let (agent, info) = SimpleAgent::ephemeral(algorithm).map_err(|e| {
90            BindingCoreError::agent_load(format!("Failed to create ephemeral agent: {}", e))
91        })?;
92        let info_json = crate::serialize_agent_info(&info)?;
93
94        Ok((
95            Self {
96                inner: Arc::new(agent),
97            },
98            info_json,
99        ))
100    }
101
102    /// Create an agent with full programmatic control via JSON parameters.
103    ///
104    /// `params_json` is a JSON string of [`CreateAgentParams`] fields.
105    /// The `storage` field is skipped during deserialization (use builder for that).
106    /// Returns `(wrapper, info_json)` where `info_json` is a serialized
107    /// [`jacs::simple::AgentInfo`].
108    pub fn create_with_params(params_json: &str) -> BindingResult<(Self, String)> {
109        let params: jacs::simple::CreateAgentParams =
110            serde_json::from_str(params_json).map_err(|e| {
111                BindingCoreError::invalid_argument(format!("Invalid CreateAgentParams JSON: {}", e))
112            })?;
113
114        let (agent, info) = SimpleAgent::create_with_params(params).map_err(|e| {
115            BindingCoreError::agent_load(format!("Failed to create agent with params: {}", e))
116        })?;
117        let info_json = crate::serialize_agent_info(&info)?;
118
119        Ok((
120            Self {
121                inner: Arc::new(agent),
122            },
123            info_json,
124        ))
125    }
126
127    /// Wrap an existing `SimpleAgent` in a `SimpleAgentWrapper`.
128    pub fn from_agent(agent: SimpleAgent) -> Self {
129        Self {
130            inner: Arc::new(agent),
131        }
132    }
133
134    /// Get a reference to the inner `SimpleAgent`.
135    ///
136    /// This is intended for advanced operations (attestation, reencrypt, etc.)
137    /// that need direct access to the underlying agent. Language bindings
138    /// should prefer the wrapper methods for the narrow contract.
139    pub fn inner_ref(&self) -> &SimpleAgent {
140        &self.inner
141    }
142
143    // =========================================================================
144    // Identity / Introspection
145    // =========================================================================
146
147    /// Get the agent's unique ID.
148    pub fn get_agent_id(&self) -> BindingResult<String> {
149        self.inner
150            .get_agent_id()
151            .map_err(|e| BindingCoreError::generic(format!("Failed to get agent ID: {}", e)))
152    }
153
154    /// Get the JACS key ID (signing key identifier).
155    pub fn key_id(&self) -> BindingResult<String> {
156        self.inner
157            .key_id()
158            .map_err(|e| BindingCoreError::generic(format!("Failed to get key ID: {}", e)))
159    }
160
161    /// Whether the agent is in strict mode.
162    pub fn is_strict(&self) -> bool {
163        self.inner.is_strict()
164    }
165
166    /// Config file path, if loaded from disk.
167    pub fn config_path(&self) -> Option<String> {
168        self.inner.config_path().map(|s| s.to_string())
169    }
170
171    /// Export the agent's identity JSON for P2P exchange.
172    pub fn export_agent(&self) -> BindingResult<String> {
173        self.inner
174            .export_agent()
175            .map_err(|e| BindingCoreError::generic(format!("Failed to export agent: {}", e)))
176    }
177
178    /// Get the public key as a PEM string.
179    pub fn get_public_key_pem(&self) -> BindingResult<String> {
180        self.inner.get_public_key_pem().map_err(|e| {
181            BindingCoreError::key_not_found(format!("Failed to get public key PEM: {}", e))
182        })
183    }
184
185    /// Get the public key as base64-encoded raw bytes (FFI-safe).
186    pub fn get_public_key_base64(&self) -> BindingResult<String> {
187        use base64::Engine;
188        let bytes = self.inner.get_public_key().map_err(|e| {
189            BindingCoreError::key_not_found(format!("Failed to get public key: {}", e))
190        })?;
191        Ok(base64::engine::general_purpose::STANDARD.encode(&bytes))
192    }
193
194    /// Runtime diagnostic info as a JSON string.
195    pub fn diagnostics(&self) -> String {
196        self.inner.diagnostics().to_string()
197    }
198
199    // =========================================================================
200    // Verification
201    // =========================================================================
202
203    /// Verify the agent's own document signature. Returns JSON `VerificationResult`.
204    pub fn verify_self(&self) -> BindingResult<String> {
205        let result = self.inner.verify_self().map_err(|e| {
206            BindingCoreError::verification_failed(format!("Verify self failed: {}", e))
207        })?;
208        serde_json::to_string(&result).map_err(|e| {
209            BindingCoreError::serialization_failed(format!(
210                "Failed to serialize VerificationResult: {}",
211                e
212            ))
213        })
214    }
215
216    /// Verify a signed document JSON string. Returns JSON `VerificationResult`.
217    pub fn verify_json(&self, signed_document: &str) -> BindingResult<String> {
218        let result = self.inner.verify(signed_document).map_err(|e| {
219            BindingCoreError::verification_failed(format!("Verification failed: {}", e))
220        })?;
221        serde_json::to_string(&result).map_err(|e| {
222            BindingCoreError::serialization_failed(format!(
223                "Failed to serialize VerificationResult: {}",
224                e
225            ))
226        })
227    }
228
229    /// Verify a signed document with an explicit public key (base64-encoded).
230    /// Returns JSON `VerificationResult`.
231    pub fn verify_with_key_json(
232        &self,
233        signed_document: &str,
234        public_key_base64: &str,
235    ) -> BindingResult<String> {
236        use base64::Engine;
237        let key_bytes = base64::engine::general_purpose::STANDARD
238            .decode(public_key_base64)
239            .map_err(|e| {
240                BindingCoreError::invalid_argument(format!("Invalid base64 public key: {}", e))
241            })?;
242
243        let result = self
244            .inner
245            .verify_with_key(signed_document, key_bytes)
246            .map_err(|e| {
247                BindingCoreError::verification_failed(format!(
248                    "Verification with key failed: {}",
249                    e
250                ))
251            })?;
252        serde_json::to_string(&result).map_err(|e| {
253            BindingCoreError::serialization_failed(format!(
254                "Failed to serialize VerificationResult: {}",
255                e
256            ))
257        })
258    }
259
260    /// Verify a stored document by its ID (e.g., "uuid:version").
261    /// Returns JSON `VerificationResult`.
262    pub fn verify_by_id_json(&self, document_id: &str) -> BindingResult<String> {
263        let result = self.inner.verify_by_id(document_id).map_err(|e| {
264            BindingCoreError::verification_failed(format!("Verify by ID failed: {}", e))
265        })?;
266        serde_json::to_string(&result).map_err(|e| {
267            BindingCoreError::serialization_failed(format!(
268                "Failed to serialize VerificationResult: {}",
269                e
270            ))
271        })
272    }
273
274    // =========================================================================
275    // Signing
276    // =========================================================================
277
278    /// Sign a JSON message string. Returns the signed JACS document JSON.
279    pub fn sign_message_json(&self, data_json: &str) -> BindingResult<String> {
280        let value: serde_json::Value = serde_json::from_str(data_json).map_err(|e| {
281            BindingCoreError::invalid_argument(format!("Invalid JSON input: {}", e))
282        })?;
283
284        let signed = self
285            .inner
286            .sign_message(&value)
287            .map_err(|e| BindingCoreError::signing_failed(format!("Sign message failed: {}", e)))?;
288
289        Ok(signed.raw)
290    }
291
292    /// Sign raw bytes and return the signature as base64 (FFI-safe).
293    pub fn sign_raw_bytes_base64(&self, data: &[u8]) -> BindingResult<String> {
294        use base64::Engine;
295        let sig_bytes = self.inner.sign_raw_bytes(data).map_err(|e| {
296            BindingCoreError::signing_failed(format!("Sign raw bytes failed: {}", e))
297        })?;
298        Ok(base64::engine::general_purpose::STANDARD.encode(&sig_bytes))
299    }
300
301    /// Sign a file with optional content embedding.
302    /// Returns the signed JACS document JSON.
303    pub fn sign_file_json(&self, file_path: &str, embed: bool) -> BindingResult<String> {
304        let signed = self
305            .inner
306            .sign_file(file_path, embed)
307            .map_err(|e| BindingCoreError::signing_failed(format!("Sign file failed: {}", e)))?;
308        Ok(signed.raw)
309    }
310
311    // =========================================================================
312    // Format Conversion (stateless -- no agent lock needed)
313    // =========================================================================
314
315    /// Convert a JSON string to YAML.
316    pub fn to_yaml(&self, json_str: &str) -> BindingResult<String> {
317        jacs::convert::jacs_to_yaml(json_str).map_err(|e| {
318            BindingCoreError::new(
319                crate::ErrorKind::SerializationFailed,
320                format!("to_yaml failed: {}", e),
321            )
322        })
323    }
324
325    /// Convert a YAML string to pretty-printed JSON.
326    pub fn from_yaml(&self, yaml_str: &str) -> BindingResult<String> {
327        jacs::convert::yaml_to_jacs(yaml_str).map_err(|e| {
328            BindingCoreError::new(
329                crate::ErrorKind::SerializationFailed,
330                format!("from_yaml failed: {}", e),
331            )
332        })
333    }
334
335    /// Convert a JSON string to a self-contained HTML document.
336    pub fn to_html(&self, json_str: &str) -> BindingResult<String> {
337        jacs::convert::jacs_to_html(json_str).map_err(|e| {
338            BindingCoreError::new(
339                crate::ErrorKind::SerializationFailed,
340                format!("to_html failed: {}", e),
341            )
342        })
343    }
344
345    /// Extract JSON from an HTML document produced by `to_html`.
346    pub fn from_html(&self, html_str: &str) -> BindingResult<String> {
347        jacs::convert::html_to_jacs(html_str).map_err(|e| {
348            BindingCoreError::new(
349                crate::ErrorKind::SerializationFailed,
350                format!("from_html failed: {}", e),
351            )
352        })
353    }
354}
355
356// =============================================================================
357// Free functions for Go FFI (C-style calling convention friendly)
358// =============================================================================
359
360/// Sign a JSON message — free function for Go FFI.
361pub fn sign_message_json(wrapper: &SimpleAgentWrapper, data_json: &str) -> BindingResult<String> {
362    wrapper.sign_message_json(data_json)
363}
364
365/// Verify a signed document — free function for Go FFI.
366pub fn verify_json(wrapper: &SimpleAgentWrapper, signed_document: &str) -> BindingResult<String> {
367    wrapper.verify_json(signed_document)
368}
369
370#[cfg(test)]
371mod tests {
372    use super::*;
373
374    /// Create a wrapper for conversion tests. Conversion methods are stateless
375    /// so we only need a default wrapper (no agent loaded).
376    fn test_wrapper() -> SimpleAgentWrapper {
377        let (wrapper, _info) =
378            SimpleAgentWrapper::ephemeral(Some("ed25519")).expect("ephemeral agent");
379        wrapper
380    }
381
382    #[test]
383    fn to_yaml_valid_json_succeeds() {
384        let wrapper = test_wrapper();
385        let result = wrapper.to_yaml(r#"{"key": "value"}"#);
386        assert!(result.is_ok(), "to_yaml should succeed for valid JSON");
387        let yaml = result.unwrap();
388        assert!(yaml.contains("key"), "YAML should contain 'key'");
389        assert!(yaml.contains("value"), "YAML should contain 'value'");
390    }
391
392    #[test]
393    fn from_yaml_valid_yaml_succeeds() {
394        let wrapper = test_wrapper();
395        let result = wrapper.from_yaml("key: value\n");
396        assert!(result.is_ok(), "from_yaml should succeed for valid YAML");
397        let json = result.unwrap();
398        assert!(json.contains("\"key\""), "JSON should contain key");
399        assert!(json.contains("\"value\""), "JSON should contain value");
400    }
401
402    #[test]
403    fn to_html_valid_json_succeeds() {
404        let wrapper = test_wrapper();
405        let result = wrapper.to_html(r#"{"key": "value"}"#);
406        assert!(result.is_ok(), "to_html should succeed for valid JSON");
407        let html = result.unwrap();
408        assert!(html.contains("<!DOCTYPE html>"), "HTML should have DOCTYPE");
409        assert!(
410            html.contains(r#"id="jacs-data">"#),
411            "HTML should have jacs-data script tag"
412        );
413    }
414
415    #[test]
416    fn from_html_valid_html_succeeds() {
417        let wrapper = test_wrapper();
418        let json = r#"{"key": "value"}"#;
419        let html = wrapper.to_html(json).unwrap();
420        let result = wrapper.from_html(&html);
421        assert!(result.is_ok(), "from_html should succeed for valid HTML");
422        assert_eq!(result.unwrap(), json, "Extracted JSON should match input");
423    }
424
425    #[test]
426    fn yaml_round_trip_preserves_content() {
427        let wrapper = test_wrapper();
428        let json = r#"{"hello": "world", "count": 42}"#;
429        let yaml = wrapper.to_yaml(json).unwrap();
430        let back = wrapper.from_yaml(&yaml).unwrap();
431        let original: serde_json::Value = serde_json::from_str(json).unwrap();
432        let reconstituted: serde_json::Value = serde_json::from_str(&back).unwrap();
433        assert_eq!(
434            original, reconstituted,
435            "YAML round-trip should preserve content"
436        );
437    }
438
439    #[test]
440    fn html_round_trip_preserves_content() {
441        let wrapper = test_wrapper();
442        let json = r#"{"hello": "world", "count": 42}"#;
443        let html = wrapper.to_html(json).unwrap();
444        let back = wrapper.from_html(&html).unwrap();
445        assert_eq!(back, json, "HTML round-trip should preserve exact JSON");
446    }
447
448    #[test]
449    fn to_yaml_invalid_json_returns_serialization_failed() {
450        let wrapper = test_wrapper();
451        let result = wrapper.to_yaml("{not valid json}");
452        assert!(result.is_err(), "to_yaml should fail for invalid JSON");
453        let err = result.unwrap_err();
454        assert_eq!(
455            err.kind,
456            crate::ErrorKind::SerializationFailed,
457            "Error should be SerializationFailed, got: {:?}",
458            err.kind
459        );
460    }
461
462    #[test]
463    fn from_yaml_invalid_yaml_returns_serialization_failed() {
464        let wrapper = test_wrapper();
465        let result = wrapper.from_yaml("{{{{ not yaml ::::");
466        assert!(result.is_err(), "from_yaml should fail for invalid YAML");
467        let err = result.unwrap_err();
468        assert_eq!(
469            err.kind,
470            crate::ErrorKind::SerializationFailed,
471            "Error should be SerializationFailed, got: {:?}",
472            err.kind
473        );
474    }
475
476    #[test]
477    fn from_html_no_script_tag_returns_serialization_failed() {
478        let wrapper = test_wrapper();
479        let result = wrapper.from_html("<html><body>No jacs data here</body></html>");
480        assert!(
481            result.is_err(),
482            "from_html should fail without jacs-data tag"
483        );
484        let err = result.unwrap_err();
485        assert_eq!(
486            err.kind,
487            crate::ErrorKind::SerializationFailed,
488            "Error should be SerializationFailed, got: {:?}",
489            err.kind
490        );
491    }
492}