1use crate::{BindingCoreError, BindingResult, ErrorKind};
8use base64::{Engine as _, engine::general_purpose::STANDARD};
9use jacs::simple::SimpleAgent;
10use serde::Serialize;
11use std::sync::Arc;
12
13#[derive(Clone)]
18pub struct SimpleAgentWrapper {
19 inner: Arc<SimpleAgent>,
20}
21
22const _: () = {
24 fn _assert<T: Send + Sync>() {}
25 let _ = _assert::<SimpleAgentWrapper>;
26};
27
28fn serialize_json<T: Serialize>(value: &T, context: &str) -> BindingResult<String> {
29 serde_json::to_string(value).map_err(|e| {
30 BindingCoreError::serialization_failed(format!("Failed to serialize {}: {}", context, e))
31 })
32}
33
34fn encode_base64(bytes: &[u8]) -> String {
35 STANDARD.encode(bytes)
36}
37
38fn decode_base64(input: &str, label: &str) -> BindingResult<Vec<u8>> {
39 STANDARD
40 .decode(input)
41 .map_err(|e| BindingCoreError::invalid_argument(format!("Invalid base64 {}: {}", label, e)))
42}
43
44fn conversion_error(operation: &str, err: impl std::fmt::Display) -> BindingCoreError {
45 BindingCoreError::new(
46 ErrorKind::SerializationFailed,
47 format!("{} failed: {}", operation, err),
48 )
49}
50
51impl SimpleAgentWrapper {
52 pub fn create(
66 name: &str,
67 purpose: Option<&str>,
68 key_algorithm: Option<&str>,
69 ) -> BindingResult<(Self, String)> {
70 let (agent, info) = SimpleAgent::create(name, purpose, key_algorithm)
71 .map_err(|e| BindingCoreError::agent_load(format!("Failed to create agent: {}", e)))?;
72 let info_json = crate::serialize_agent_info(&info)?;
73
74 Ok((Self::from_agent(agent), info_json))
75 }
76
77 pub fn load(config_path: Option<&str>, strict: Option<bool>) -> BindingResult<Self> {
79 let (wrapper, _info_json) = Self::load_with_info(config_path, strict)?;
80 Ok(wrapper)
81 }
82
83 pub fn load_with_info(
85 config_path: Option<&str>,
86 strict: Option<bool>,
87 ) -> BindingResult<(Self, String)> {
88 let requested_path = config_path.unwrap_or("./jacs.config.json");
89 let resolved_config_path = crate::resolve_existing_config_path(requested_path)?;
90 let agent = SimpleAgent::load(Some(&resolved_config_path), strict)
91 .map_err(|e| BindingCoreError::agent_load(format!("Failed to load agent: {}", e)))?;
92 let info = agent
93 .loaded_info()
94 .map_err(|e| BindingCoreError::agent_load(format!("Failed to load agent: {}", e)))?;
95 let info_json = crate::serialize_agent_info(&info)?;
96
97 Ok((Self::from_agent(agent), info_json))
98 }
99
100 pub fn ephemeral(algorithm: Option<&str>) -> BindingResult<(Self, String)> {
104 let (agent, info) = SimpleAgent::ephemeral(algorithm).map_err(|e| {
105 BindingCoreError::agent_load(format!("Failed to create ephemeral agent: {}", e))
106 })?;
107 let info_json = crate::serialize_agent_info(&info)?;
108
109 Ok((Self::from_agent(agent), info_json))
110 }
111
112 pub fn create_with_params(params_json: &str) -> BindingResult<(Self, String)> {
119 let params: jacs::simple::CreateAgentParams =
120 serde_json::from_str(params_json).map_err(|e| {
121 BindingCoreError::invalid_argument(format!("Invalid CreateAgentParams JSON: {}", e))
122 })?;
123
124 let (agent, info) = SimpleAgent::create_with_params(params).map_err(|e| {
125 BindingCoreError::agent_load(format!("Failed to create agent with params: {}", e))
126 })?;
127 let info_json = crate::serialize_agent_info(&info)?;
128
129 Ok((Self::from_agent(agent), info_json))
130 }
131
132 pub fn from_agent(agent: SimpleAgent) -> Self {
134 Self {
135 inner: Arc::new(agent),
136 }
137 }
138
139 pub fn inner_ref(&self) -> &SimpleAgent {
145 &self.inner
146 }
147
148 pub fn get_agent_id(&self) -> BindingResult<String> {
154 self.inner
155 .get_agent_id()
156 .map_err(|e| BindingCoreError::generic(format!("Failed to get agent ID: {}", e)))
157 }
158
159 pub fn key_id(&self) -> BindingResult<String> {
161 self.inner
162 .key_id()
163 .map_err(|e| BindingCoreError::generic(format!("Failed to get key ID: {}", e)))
164 }
165
166 pub fn is_strict(&self) -> bool {
168 self.inner.is_strict()
169 }
170
171 pub fn config_path(&self) -> Option<String> {
173 self.inner.config_path().map(|s| s.to_string())
174 }
175
176 pub fn export_agent(&self) -> BindingResult<String> {
178 self.inner
179 .export_agent()
180 .map_err(|e| BindingCoreError::generic(format!("Failed to export agent: {}", e)))
181 }
182
183 pub fn get_public_key_pem(&self) -> BindingResult<String> {
185 self.inner.get_public_key_pem().map_err(|e| {
186 BindingCoreError::key_not_found(format!("Failed to get public key PEM: {}", e))
187 })
188 }
189
190 pub fn get_public_key_base64(&self) -> BindingResult<String> {
192 let bytes = self.inner.get_public_key().map_err(|e| {
193 BindingCoreError::key_not_found(format!("Failed to get public key: {}", e))
194 })?;
195 Ok(encode_base64(&bytes))
196 }
197
198 pub fn diagnostics(&self) -> String {
200 self.inner.diagnostics().to_string()
201 }
202
203 pub fn export_w3c_did(&self, origin: Option<&str>) -> BindingResult<String> {
209 jacs::simple::w3c::export_w3c_did_identifier_with_origin(&self.inner, origin)
210 .map_err(|e| BindingCoreError::generic(format!("Failed to export W3C DID: {}", e)))
211 }
212
213 pub fn export_w3c_did_document_json(&self, origin: Option<&str>) -> BindingResult<String> {
215 let document =
216 jacs::simple::w3c::export_w3c_did_document(&self.inner, origin).map_err(|e| {
217 BindingCoreError::generic(format!("Failed to export W3C DID document: {}", e))
218 })?;
219 serialize_json(&document, "W3C DID document")
220 }
221
222 pub fn export_w3c_agent_description_json(&self, origin: Option<&str>) -> BindingResult<String> {
224 let description = jacs::simple::w3c::export_w3c_agent_description(&self.inner, origin)
225 .map_err(|e| {
226 BindingCoreError::generic(format!("Failed to export W3C agent description: {}", e))
227 })?;
228 serialize_json(&description, "W3C agent description")
229 }
230
231 pub fn generate_w3c_well_known_json(&self, origin: Option<&str>) -> BindingResult<String> {
233 let documents =
234 jacs::simple::w3c::generate_w3c_well_known(&self.inner, origin).map_err(|e| {
235 BindingCoreError::generic(format!(
236 "Failed to generate W3C well-known documents: {}",
237 e
238 ))
239 })?;
240 let mut by_path = serde_json::Map::new();
241 for (path, value) in documents {
242 by_path.insert(path, value);
243 }
244 serialize_json(
245 &serde_json::Value::Object(by_path),
246 "W3C well-known documents",
247 )
248 }
249
250 pub fn sign_w3c_request_json(&self, params_json: &str) -> BindingResult<String> {
254 let params: jacs::w3c::W3cRequestProofParams =
255 serde_json::from_str(params_json).map_err(|e| {
256 BindingCoreError::invalid_argument(format!(
257 "Invalid W3C request proof params JSON: {}",
258 e
259 ))
260 })?;
261 let proof = jacs::simple::w3c::sign_w3c_request(&self.inner, params).map_err(|e| {
262 BindingCoreError::signing_failed(format!("Failed to sign W3C request proof: {}", e))
263 })?;
264 serialize_json(&proof, "W3C request proof")
265 }
266
267 pub fn verify_w3c_request_json(
269 &self,
270 proof_json: &str,
271 did_document_json: &str,
272 body: Option<&str>,
273 max_age_seconds: u64,
274 expected_method: Option<&str>,
275 expected_url: Option<&str>,
276 ) -> BindingResult<String> {
277 let result = jacs::simple::w3c::verify_w3c_request(
278 &self.inner,
279 proof_json,
280 did_document_json,
281 body,
282 max_age_seconds,
283 expected_method,
284 expected_url,
285 )
286 .map_err(|e| {
287 BindingCoreError::verification_failed(format!(
288 "Failed to verify W3C request proof: {}",
289 e
290 ))
291 })?;
292 serialize_json(&result, "W3C request proof verification")
293 }
294
295 pub fn verify_self(&self) -> BindingResult<String> {
301 let result = self.inner.verify_self().map_err(|e| {
302 BindingCoreError::verification_failed(format!("Verify self failed: {}", e))
303 })?;
304 serialize_json(&result, "VerificationResult")
305 }
306
307 pub fn verify_json(&self, signed_document: &str) -> BindingResult<String> {
309 let result = self.inner.verify(signed_document).map_err(|e| {
310 BindingCoreError::verification_failed(format!("Verification failed: {}", e))
311 })?;
312 serialize_json(&result, "VerificationResult")
313 }
314
315 pub fn verify_with_key_json(
318 &self,
319 signed_document: &str,
320 public_key_base64: &str,
321 ) -> BindingResult<String> {
322 let key_bytes = decode_base64(public_key_base64, "public key")?;
323
324 let result = self
325 .inner
326 .verify_with_key(signed_document, key_bytes)
327 .map_err(|e| {
328 BindingCoreError::verification_failed(format!(
329 "Verification with key failed: {}",
330 e
331 ))
332 })?;
333 serialize_json(&result, "VerificationResult")
334 }
335
336 pub fn verify_by_id_json(&self, document_id: &str) -> BindingResult<String> {
339 let result = self.inner.verify_by_id(document_id).map_err(|e| {
340 BindingCoreError::verification_failed(format!("Verify by ID failed: {}", e))
341 })?;
342 serialize_json(&result, "VerificationResult")
343 }
344
345 pub fn sign_message_json(&self, data_json: &str) -> BindingResult<String> {
351 let value: serde_json::Value = serde_json::from_str(data_json).map_err(|e| {
352 BindingCoreError::invalid_argument(format!("Invalid JSON input: {}", e))
353 })?;
354
355 let signed = self
356 .inner
357 .sign_message(&value)
358 .map_err(|e| BindingCoreError::signing_failed(format!("Sign message failed: {}", e)))?;
359
360 Ok(signed.raw)
361 }
362
363 pub fn sign_raw_bytes_base64(&self, data: &[u8]) -> BindingResult<String> {
365 let sig_bytes = self.inner.sign_raw_bytes(data).map_err(|e| {
366 BindingCoreError::signing_failed(format!("Sign raw bytes failed: {}", e))
367 })?;
368 Ok(encode_base64(&sig_bytes))
369 }
370
371 pub fn sign_file_json(&self, file_path: &str, embed: bool) -> BindingResult<String> {
374 let signed = self
375 .inner
376 .sign_file(file_path, embed)
377 .map_err(|e| BindingCoreError::signing_failed(format!("Sign file failed: {}", e)))?;
378 Ok(signed.raw)
379 }
380
381 #[cfg(feature = "agreements")]
387 pub fn create_agreement_v2_json(&self, input_json: &str) -> BindingResult<String> {
388 let input: jacs::agreements::v2::CreateAgreementV2 = serde_json::from_str(input_json)
389 .map_err(|e| {
390 BindingCoreError::validation(format!(
391 "{}: {}",
392 crate::agreement_v2::CTX_INVALID_CREATE_INPUT,
393 e
394 ))
395 })?;
396 let signed = jacs::agreements::v2::create(&self.inner, input).map_err(|e| {
397 BindingCoreError::agreement_failed(format!(
398 "{}: {}",
399 crate::agreement_v2::CTX_CREATE,
400 e
401 ))
402 })?;
403 Ok(signed.raw)
404 }
405
406 #[cfg(feature = "agreements")]
408 pub fn apply_agreement_v2_json(
409 &self,
410 document_json: &str,
411 mutation_json: &str,
412 ) -> BindingResult<String> {
413 let mutation: jacs::agreements::v2::AgreementV2Mutation =
414 serde_json::from_str(mutation_json).map_err(|e| {
415 BindingCoreError::validation(format!(
416 "{}: {}",
417 crate::agreement_v2::CTX_INVALID_MUTATION,
418 e
419 ))
420 })?;
421 let signed =
422 jacs::agreements::v2::apply(&self.inner, document_json, mutation).map_err(|e| {
423 BindingCoreError::agreement_failed(format!(
424 "{}: {}",
425 crate::agreement_v2::CTX_APPLY,
426 e
427 ))
428 })?;
429 Ok(signed.raw)
430 }
431
432 #[cfg(feature = "agreements")]
434 pub fn sign_agreement_v2_json(&self, document_json: &str, role: &str) -> BindingResult<String> {
435 let role = crate::agreement_v2::parse_agreement_v2_role(role)?;
436 let signed = jacs::agreements::v2::sign(&self.inner, document_json, role).map_err(|e| {
437 BindingCoreError::agreement_failed(format!("{}: {}", crate::agreement_v2::CTX_SIGN, e))
438 })?;
439 Ok(signed.raw)
440 }
441
442 #[cfg(feature = "agreements")]
444 pub fn verify_agreement_v2_json(&self, document_json: &str) -> BindingResult<String> {
445 let report = jacs::agreements::v2::verify(&self.inner, document_json).map_err(|e| {
446 BindingCoreError::verification_failed(format!(
447 "{}: {}",
448 crate::agreement_v2::CTX_VERIFY,
449 e
450 ))
451 })?;
452 serde_json::to_string(&report).map_err(|e| {
453 BindingCoreError::serialization_failed(format!(
454 "{}: {}",
455 crate::agreement_v2::CTX_SERIALIZE_VERIFY,
456 e
457 ))
458 })
459 }
460
461 #[cfg(feature = "agreements")]
463 pub fn detect_agreement_v2_branch_conflict_json(
464 &self,
465 base_document_json: &str,
466 left_document_json: &str,
467 right_document_json: &str,
468 ) -> BindingResult<String> {
469 let analysis = jacs::agreements::v2::detect_branch_conflict(
470 base_document_json,
471 left_document_json,
472 right_document_json,
473 )
474 .map_err(|e| {
475 BindingCoreError::agreement_failed(format!(
476 "{}: {}",
477 crate::agreement_v2::CTX_DETECT,
478 e
479 ))
480 })?;
481 serde_json::to_string(&analysis).map_err(|e| {
482 BindingCoreError::serialization_failed(format!(
483 "{}: {}",
484 crate::agreement_v2::CTX_SERIALIZE_BRANCH,
485 e
486 ))
487 })
488 }
489
490 #[cfg(feature = "agreements")]
492 pub fn merge_agreement_v2_transcript_branches_json(
493 &self,
494 base_document_json: &str,
495 left_document_json: &str,
496 right_document_json: &str,
497 ) -> BindingResult<String> {
498 let signed = jacs::agreements::v2::merge_transcript_branches(
499 &self.inner,
500 base_document_json,
501 left_document_json,
502 right_document_json,
503 )
504 .map_err(|e| {
505 BindingCoreError::agreement_failed(format!("{}: {}", crate::agreement_v2::CTX_MERGE, e))
506 })?;
507 Ok(signed.raw)
508 }
509
510 #[cfg(feature = "agreements")]
512 pub fn resolve_agreement_v2_branch_conflict_json(
513 &self,
514 base_document_json: &str,
515 previous_document_json: &str,
516 side_branch_document_json: &str,
517 mutation_json: &str,
518 ) -> BindingResult<String> {
519 let mutation: jacs::agreements::v2::AgreementV2Mutation =
520 serde_json::from_str(mutation_json).map_err(|e| {
521 BindingCoreError::validation(format!(
522 "{}: {}",
523 crate::agreement_v2::CTX_INVALID_RESOLUTION_MUTATION,
524 e
525 ))
526 })?;
527 let signed = jacs::agreements::v2::resolve_branch_conflict(
528 &self.inner,
529 base_document_json,
530 previous_document_json,
531 side_branch_document_json,
532 mutation,
533 )
534 .map_err(|e| {
535 BindingCoreError::agreement_failed(format!(
536 "{}: {}",
537 crate::agreement_v2::CTX_RESOLVE,
538 e
539 ))
540 })?;
541 Ok(signed.raw)
542 }
543
544 pub fn to_yaml(&self, json_str: &str) -> BindingResult<String> {
550 jacs::convert::jacs_to_yaml(json_str).map_err(|e| conversion_error("to_yaml", e))
551 }
552
553 pub fn from_yaml(&self, yaml_str: &str) -> BindingResult<String> {
555 jacs::convert::yaml_to_jacs(yaml_str).map_err(|e| conversion_error("from_yaml", e))
556 }
557
558 pub fn to_html(&self, json_str: &str) -> BindingResult<String> {
560 jacs::convert::jacs_to_html(json_str).map_err(|e| conversion_error("to_html", e))
561 }
562
563 pub fn from_html(&self, html_str: &str) -> BindingResult<String> {
565 jacs::convert::html_to_jacs(html_str).map_err(|e| conversion_error("from_html", e))
566 }
567
568 pub fn rotate_keys(&self, algorithm: Option<&str>) -> BindingResult<String> {
577 let result = jacs::simple::advanced::rotate(&self.inner, algorithm).map_err(|e| {
578 BindingCoreError::new(ErrorKind::Generic, format!("Key rotation failed: {}", e))
579 })?;
580 serialize_json(&result, "rotation result")
581 }
582
583 pub fn sign_text_file_json(&self, path: &str, opts_json: &str) -> BindingResult<String> {
596 let opts = parse_sign_text_options(opts_json)?;
597 let outcome = jacs::simple::advanced::sign_text_file(&self.inner, path, opts)
598 .map_err(|e| map_jacs_err(e, "sign_text_file"))?;
599 serialize_json(&outcome, "sign_text_file outcome")
600 }
601
602 pub fn verify_text_file_json(&self, path: &str, opts_json: &str) -> BindingResult<String> {
613 let opts = parse_verify_options(opts_json)?;
614 let strict = opts.strict;
615 match jacs::simple::advanced::verify_text_file(&self.inner, path, opts) {
616 Ok(result) => serialize_verify_text_result(&result),
617 Err(jacs::error::JacsError::MissingSignature(p)) if strict => Err(
618 BindingCoreError::missing_signature(format!("no JACS signature found in {}", p)),
619 ),
620 Err(e) => Err(map_jacs_err(e, "verify_text_file")),
624 }
625 }
626
627 pub fn sign_image_json(
635 &self,
636 in_path: &str,
637 out_path: &str,
638 opts_json: &str,
639 ) -> BindingResult<String> {
640 let opts = parse_sign_image_options(opts_json)?;
641 let outcome = jacs::simple::advanced::sign_image(&self.inner, in_path, out_path, opts)
642 .map_err(|e| map_jacs_err(e, "sign_image"))?;
643 serialize_json(&outcome, "sign_image outcome")
644 }
645
646 pub fn verify_image_json(&self, path: &str, opts_json: &str) -> BindingResult<String> {
653 let opts = parse_verify_image_options(opts_json)?;
654 let strict = opts.base.strict;
655 match jacs::simple::advanced::verify_image(&self.inner, path, opts) {
656 Ok(result) => serialize_json(&result, "verify_image result"),
657 Err(jacs::error::JacsError::MissingSignature(p)) if strict => Err(
658 BindingCoreError::missing_signature(format!("no JACS signature found in {}", p)),
659 ),
660 Err(e) => Err(map_jacs_err(e, "verify_image")),
662 }
663 }
664
665 pub fn extract_media_signature_json(
675 &self,
676 path: &str,
677 opts_json: &str,
678 ) -> BindingResult<String> {
679 let parsed = parse_extract_options(opts_json)?;
680 let opts = jacs::simple::types::ExtractMediaOptions {
681 scan_robust: parsed.scan_robust,
682 };
683 let result = if parsed.raw_payload {
684 jacs::simple::advanced::extract_media_signature_raw_with_options(path, opts)
685 } else {
686 jacs::simple::advanced::extract_media_signature_with_options(path, opts)
687 };
688 let payload = result.map_err(|e| map_jacs_err(e, "extract_media_signature"))?;
689 let envelope = serde_json::json!({
690 "present": payload.is_some(),
691 "payload": payload,
692 });
693 Ok(envelope.to_string())
694 }
695}
696
697fn map_jacs_err(e: jacs::error::JacsError, op: &str) -> BindingCoreError {
702 use jacs::error::JacsError;
703 match e {
704 JacsError::MissingSignature(p) => BindingCoreError::missing_signature(p),
705 JacsError::ValidationError(msg) => BindingCoreError::invalid_argument(msg),
706 JacsError::FileNotFound { path } => {
707 BindingCoreError::invalid_argument(format!("file not found: {}", path))
708 }
709 JacsError::FileReadFailed { path, reason } => {
710 BindingCoreError::invalid_argument(format!("read {} failed: {}", path, reason))
711 }
712 JacsError::FileWriteFailed { path, reason } => BindingCoreError::new(
713 ErrorKind::Generic,
714 format!("write {} failed: {}", path, reason),
715 ),
716 other => BindingCoreError::new(ErrorKind::Generic, format!("{}: {}", op, other)),
717 }
718}
719
720fn opts_is_default(s: &str) -> bool {
721 let t = s.trim();
722 t.is_empty() || t == "null" || t == "{}"
723}
724
725fn parse_sign_text_options(opts_json: &str) -> BindingResult<jacs::simple::types::SignTextOptions> {
726 if opts_is_default(opts_json) {
727 return Ok(jacs::simple::types::SignTextOptions::default());
728 }
729 let v: serde_json::Value = serde_json::from_str(opts_json)
730 .map_err(|e| BindingCoreError::invalid_argument(format!("sign_text_file opts: {}", e)))?;
731 let mut o = jacs::simple::types::SignTextOptions::default();
732 if let Some(b) = v.get("backup").and_then(|x| x.as_bool()) {
733 o.backup = b;
734 }
735 if let Some(b) = v.get("allow_duplicate").and_then(|x| x.as_bool()) {
736 o.allow_duplicate = b;
737 }
738 if let Some(b) = v.get("allowDuplicate").and_then(|x| x.as_bool()) {
739 o.allow_duplicate = b;
740 }
741 if let Some(n) = v
745 .get("unsafeBakMode")
746 .or_else(|| v.get("unsafe_bak_mode"))
747 .and_then(|x| x.as_u64())
748 {
749 o.unsafe_bak_mode = Some(n as u32);
750 }
751 Ok(o)
752}
753
754fn parse_verify_options(opts_json: &str) -> BindingResult<jacs::inline::VerifyOptions> {
755 if opts_is_default(opts_json) {
756 return Ok(jacs::inline::VerifyOptions::default());
757 }
758 let v: serde_json::Value = serde_json::from_str(opts_json)
759 .map_err(|e| BindingCoreError::invalid_argument(format!("verify opts: {}", e)))?;
760 let strict = v.get("strict").and_then(|x| x.as_bool()).unwrap_or(false);
761 let key_dir = v
762 .get("keyDir")
763 .or_else(|| v.get("key_dir"))
764 .and_then(|x| x.as_str())
765 .map(std::path::PathBuf::from);
766 Ok(jacs::inline::VerifyOptions { strict, key_dir })
767}
768
769fn parse_sign_image_options(
770 opts_json: &str,
771) -> BindingResult<jacs::simple::types::SignImageOptions> {
772 if opts_is_default(opts_json) {
773 return Ok(jacs::simple::types::SignImageOptions::default());
774 }
775 let v: serde_json::Value = serde_json::from_str(opts_json)
776 .map_err(|e| BindingCoreError::invalid_argument(format!("sign_image opts: {}", e)))?;
777 let mut o = jacs::simple::types::SignImageOptions::default();
778 if let Some(b) = v.get("robust").and_then(|x| x.as_bool()) {
779 o.robust = b;
780 }
781 if let Some(b) = v
782 .get("refuseOverwrite")
783 .or_else(|| v.get("refuse_overwrite"))
784 .and_then(|x| x.as_bool())
785 {
786 o.refuse_overwrite = b;
787 }
788 if let Some(b) = v.get("backup").and_then(|x| x.as_bool()) {
789 o.backup = b;
790 }
791 if let Some(n) = v
792 .get("unsafeBakMode")
793 .or_else(|| v.get("unsafe_bak_mode"))
794 .and_then(|x| x.as_u64())
795 {
796 o.unsafe_bak_mode = Some(n as u32);
797 }
798 if let Some(s) = v
799 .get("formatHint")
800 .or_else(|| v.get("format_hint"))
801 .and_then(|x| x.as_str())
802 {
803 o.format_hint = Some(s.to_string());
804 }
805 Ok(o)
806}
807
808fn parse_verify_image_options(
809 opts_json: &str,
810) -> BindingResult<jacs::simple::types::VerifyImageOptions> {
811 if opts_is_default(opts_json) {
812 return Ok(jacs::simple::types::VerifyImageOptions::default());
813 }
814 let v: serde_json::Value = serde_json::from_str(opts_json)
815 .map_err(|e| BindingCoreError::invalid_argument(format!("verify_image opts: {}", e)))?;
816 let strict = v.get("strict").and_then(|x| x.as_bool()).unwrap_or(false);
817 let key_dir = v
818 .get("keyDir")
819 .or_else(|| v.get("key_dir"))
820 .and_then(|x| x.as_str())
821 .map(std::path::PathBuf::from);
822 let scan_robust = v
823 .get("robust")
824 .or_else(|| v.get("scan_robust"))
825 .and_then(|x| x.as_bool())
826 .unwrap_or(false);
827 Ok(jacs::simple::types::VerifyImageOptions {
828 base: jacs::inline::VerifyOptions { strict, key_dir },
829 scan_robust,
830 })
831}
832
833#[derive(Debug, Clone, Copy, Default)]
836struct ParsedExtractOptions {
837 raw_payload: bool,
838 scan_robust: bool,
840}
841
842fn parse_extract_options(opts_json: &str) -> BindingResult<ParsedExtractOptions> {
843 if opts_is_default(opts_json) {
844 return Ok(ParsedExtractOptions::default());
845 }
846 let v: serde_json::Value = serde_json::from_str(opts_json).map_err(|e| {
847 BindingCoreError::invalid_argument(format!("extract_media_signature opts: {}", e))
848 })?;
849 let raw_payload = v
850 .get("rawPayload")
851 .or_else(|| v.get("raw_payload"))
852 .and_then(|x| x.as_bool())
853 .unwrap_or(false);
854 let scan_robust = v
855 .get("scanRobust")
856 .or_else(|| v.get("scan_robust"))
857 .or_else(|| v.get("robust"))
858 .and_then(|x| x.as_bool())
859 .unwrap_or(false);
860 Ok(ParsedExtractOptions {
861 raw_payload,
862 scan_robust,
863 })
864}
865
866fn serialize_verify_text_result(result: &jacs::inline::VerifyTextResult) -> BindingResult<String> {
867 use jacs::inline::{SignatureStatus, VerifyTextResult};
868 let v = match result {
869 VerifyTextResult::MissingSignature => {
870 serde_json::json!({"status": "missing_signature"})
871 }
872 VerifyTextResult::Malformed(detail) => {
873 serde_json::json!({"status": "malformed", "error": detail})
874 }
875 VerifyTextResult::Signed { signatures } => {
876 let entries: Vec<serde_json::Value> = signatures
877 .iter()
878 .map(|e| {
879 let (status_str, error) = match &e.status {
880 SignatureStatus::Valid => ("valid", None),
881 SignatureStatus::InvalidSignature => ("invalid_signature", None),
882 SignatureStatus::HashMismatch => ("hash_mismatch", None),
883 SignatureStatus::KeyNotFound => ("key_not_found", None),
884 SignatureStatus::UnsupportedAlgorithm => ("unsupported_algorithm", None),
885 SignatureStatus::Malformed(s) => ("malformed", Some(s.clone())),
886 };
887 let mut o = serde_json::json!({
888 "signer_id": e.signer_id,
889 "algorithm": e.algorithm,
890 "timestamp": e.timestamp,
891 "status": status_str,
892 });
893 if let Some(err) = error {
894 o["error"] = serde_json::Value::String(err);
895 }
896 o
897 })
898 .collect();
899 serde_json::json!({"status": "signed", "signatures": entries})
900 }
901 };
902 Ok(v.to_string())
903}
904
905pub fn sign_message_json(wrapper: &SimpleAgentWrapper, data_json: &str) -> BindingResult<String> {
911 wrapper.sign_message_json(data_json)
912}
913
914pub fn verify_json(wrapper: &SimpleAgentWrapper, signed_document: &str) -> BindingResult<String> {
916 wrapper.verify_json(signed_document)
917}
918
919#[cfg(test)]
920mod tests {
921 use super::*;
922
923 fn test_wrapper() -> SimpleAgentWrapper {
926 let (wrapper, _info) =
927 SimpleAgentWrapper::ephemeral(Some("ed25519")).expect("ephemeral agent");
928 wrapper
929 }
930
931 #[test]
932 fn to_yaml_valid_json_succeeds() {
933 let wrapper = test_wrapper();
934 let result = wrapper.to_yaml(r#"{"key": "value"}"#);
935 assert!(result.is_ok(), "to_yaml should succeed for valid JSON");
936 let yaml = result.unwrap();
937 assert!(yaml.contains("key"), "YAML should contain 'key'");
938 assert!(yaml.contains("value"), "YAML should contain 'value'");
939 }
940
941 #[test]
942 fn from_yaml_valid_yaml_succeeds() {
943 let wrapper = test_wrapper();
944 let result = wrapper.from_yaml("key: value\n");
945 assert!(result.is_ok(), "from_yaml should succeed for valid YAML");
946 let json = result.unwrap();
947 assert!(json.contains("\"key\""), "JSON should contain key");
948 assert!(json.contains("\"value\""), "JSON should contain value");
949 }
950
951 #[test]
952 fn to_html_valid_json_succeeds() {
953 let wrapper = test_wrapper();
954 let result = wrapper.to_html(r#"{"key": "value"}"#);
955 assert!(result.is_ok(), "to_html should succeed for valid JSON");
956 let html = result.unwrap();
957 assert!(html.contains("<!DOCTYPE html>"), "HTML should have DOCTYPE");
958 assert!(
959 html.contains(r#"id="jacs-data">"#),
960 "HTML should have jacs-data script tag"
961 );
962 }
963
964 #[test]
965 fn from_html_valid_html_succeeds() {
966 let wrapper = test_wrapper();
967 let json = r#"{"key": "value"}"#;
968 let html = wrapper.to_html(json).unwrap();
969 let result = wrapper.from_html(&html);
970 assert!(result.is_ok(), "from_html should succeed for valid HTML");
971 assert_eq!(result.unwrap(), json, "Extracted JSON should match input");
972 }
973
974 #[test]
975 fn yaml_round_trip_preserves_content() {
976 let wrapper = test_wrapper();
977 let json = r#"{"hello": "world", "count": 42}"#;
978 let yaml = wrapper.to_yaml(json).unwrap();
979 let back = wrapper.from_yaml(&yaml).unwrap();
980 let original: serde_json::Value = serde_json::from_str(json).unwrap();
981 let reconstituted: serde_json::Value = serde_json::from_str(&back).unwrap();
982 assert_eq!(
983 original, reconstituted,
984 "YAML round-trip should preserve content"
985 );
986 }
987
988 #[test]
989 fn html_round_trip_preserves_content() {
990 let wrapper = test_wrapper();
991 let json = r#"{"hello": "world", "count": 42}"#;
992 let html = wrapper.to_html(json).unwrap();
993 let back = wrapper.from_html(&html).unwrap();
994 assert_eq!(back, json, "HTML round-trip should preserve exact JSON");
995 }
996
997 #[test]
998 fn to_yaml_invalid_json_returns_serialization_failed() {
999 let wrapper = test_wrapper();
1000 let result = wrapper.to_yaml("{not valid json}");
1001 assert!(result.is_err(), "to_yaml should fail for invalid JSON");
1002 let err = result.unwrap_err();
1003 assert_eq!(
1004 err.kind,
1005 crate::ErrorKind::SerializationFailed,
1006 "Error should be SerializationFailed, got: {:?}",
1007 err.kind
1008 );
1009 }
1010
1011 #[test]
1012 fn from_yaml_invalid_yaml_returns_serialization_failed() {
1013 let wrapper = test_wrapper();
1014 let result = wrapper.from_yaml("{{{{ not yaml ::::");
1015 assert!(result.is_err(), "from_yaml should fail for invalid YAML");
1016 let err = result.unwrap_err();
1017 assert_eq!(
1018 err.kind,
1019 crate::ErrorKind::SerializationFailed,
1020 "Error should be SerializationFailed, got: {:?}",
1021 err.kind
1022 );
1023 }
1024
1025 #[test]
1026 fn from_html_no_script_tag_returns_serialization_failed() {
1027 let wrapper = test_wrapper();
1028 let result = wrapper.from_html("<html><body>No jacs data here</body></html>");
1029 assert!(
1030 result.is_err(),
1031 "from_html should fail without jacs-data tag"
1032 );
1033 let err = result.unwrap_err();
1034 assert_eq!(
1035 err.kind,
1036 crate::ErrorKind::SerializationFailed,
1037 "Error should be SerializationFailed, got: {:?}",
1038 err.kind
1039 );
1040 }
1041
1042 #[test]
1050 fn parse_sign_text_options_honours_unsafe_bak_mode_snake_case() {
1051 let opts =
1052 parse_sign_text_options(r#"{"unsafe_bak_mode": 420}"#).expect("parse should succeed");
1053 assert_eq!(
1054 opts.unsafe_bak_mode,
1055 Some(420),
1056 "snake_case unsafe_bak_mode must round-trip"
1057 );
1058 }
1059
1060 #[test]
1061 fn parse_sign_text_options_honours_unsafe_bak_mode_camel_case() {
1062 let opts =
1063 parse_sign_text_options(r#"{"unsafeBakMode": 420}"#).expect("parse should succeed");
1064 assert_eq!(
1065 opts.unsafe_bak_mode,
1066 Some(420),
1067 "camelCase unsafeBakMode must round-trip"
1068 );
1069 }
1070
1071 #[test]
1072 fn parse_sign_text_options_default_unsafe_bak_mode_is_none() {
1073 let opts = parse_sign_text_options(r#"{"backup": true}"#).expect("parse should succeed");
1074 assert_eq!(
1075 opts.unsafe_bak_mode, None,
1076 "absent unsafe_bak_mode must remain None (uses 0o600 default at write time)"
1077 );
1078 }
1079
1080 #[test]
1081 fn parse_sign_text_options_combines_with_other_fields() {
1082 let opts = parse_sign_text_options(
1083 r#"{"backup": false, "allowDuplicate": true, "unsafeBakMode": 384}"#,
1084 )
1085 .expect("parse should succeed");
1086 assert!(!opts.backup);
1087 assert!(opts.allow_duplicate);
1088 assert_eq!(opts.unsafe_bak_mode, Some(384));
1089 }
1090
1091 #[test]
1098 fn verify_text_file_json_non_existent_path_returns_invalid_argument() {
1099 let wrapper = test_wrapper();
1100 let result =
1101 wrapper.verify_text_file_json("/tmp/jacs-binding-core-r008-does-not-exist.md", "{}");
1102 assert!(result.is_err(), "verify on non-existent path should fail");
1103 let err = result.unwrap_err();
1104 assert_eq!(
1108 err.kind,
1109 crate::ErrorKind::InvalidArgument,
1110 "expected InvalidArgument for non-existent path, got: {:?}",
1111 err.kind
1112 );
1113 }
1114
1115 #[test]
1116 fn verify_image_json_non_existent_path_returns_invalid_argument() {
1117 let wrapper = test_wrapper();
1118 let result =
1119 wrapper.verify_image_json("/tmp/jacs-binding-core-r008-does-not-exist.png", "{}");
1120 assert!(result.is_err(), "verify on non-existent path should fail");
1121 let err = result.unwrap_err();
1122 assert_eq!(
1123 err.kind,
1124 crate::ErrorKind::InvalidArgument,
1125 "expected InvalidArgument for non-existent path, got: {:?}",
1126 err.kind
1127 );
1128 }
1129
1130 #[test]
1137 fn parse_extract_options_default_has_no_robust_scan_or_raw() {
1138 let parsed = parse_extract_options("{}").expect("ok");
1139 assert!(!parsed.raw_payload);
1140 assert!(!parsed.scan_robust);
1141 }
1142
1143 #[test]
1144 fn parse_extract_options_honours_scan_robust_camel() {
1145 let parsed = parse_extract_options(r#"{"scanRobust": true}"#).expect("ok");
1146 assert!(parsed.scan_robust);
1147 assert!(!parsed.raw_payload);
1148 }
1149
1150 #[test]
1151 fn parse_extract_options_honours_scan_robust_snake() {
1152 let parsed = parse_extract_options(r#"{"scan_robust": true}"#).expect("ok");
1153 assert!(parsed.scan_robust);
1154 }
1155
1156 #[test]
1157 fn parse_extract_options_honours_short_robust_alias() {
1158 let parsed = parse_extract_options(r#"{"robust": true}"#).expect("ok");
1159 assert!(parsed.scan_robust);
1160 }
1161
1162 #[test]
1163 fn parse_extract_options_combines_raw_payload_and_scan_robust() {
1164 let parsed =
1165 parse_extract_options(r#"{"rawPayload": true, "scanRobust": true}"#).expect("ok");
1166 assert!(parsed.raw_payload);
1167 assert!(parsed.scan_robust);
1168 }
1169
1170 #[test]
1183 #[cfg(unix)]
1184 fn sign_text_file_json_routes_unsafe_bak_mode_camel_to_disk() {
1185 use std::os::unix::fs::PermissionsExt;
1186 let wrapper = test_wrapper();
1187 let dir = tempfile::TempDir::new().expect("tempdir");
1188 let path = dir.path().join("doc.md");
1189 std::fs::write(&path, b"# Hello\n\nbody\n").expect("write fixture");
1190
1191 let outcome_json = wrapper
1192 .sign_text_file_json(
1193 path.to_str().unwrap(),
1194 r#"{"backup": true, "unsafeBakMode": 420}"#,
1195 )
1196 .expect("sign_text_file_json should succeed");
1197
1198 let outcome: serde_json::Value =
1200 serde_json::from_str(&outcome_json).expect("outcome is JSON");
1201 let bak_path = outcome
1202 .get("backup_path")
1203 .and_then(|v| v.as_str())
1204 .expect("backup_path present");
1205 let mode = std::fs::metadata(bak_path)
1206 .expect("bak exists")
1207 .permissions()
1208 .mode()
1209 & 0o777;
1210 assert_eq!(
1211 mode, 0o644,
1212 "JSON envelope unsafeBakMode=420 must reach the on-disk .bak; got {:o}",
1213 mode
1214 );
1215 }
1216
1217 #[test]
1218 #[cfg(unix)]
1219 fn sign_text_file_json_default_is_owner_only() {
1220 use std::os::unix::fs::PermissionsExt;
1221 let wrapper = test_wrapper();
1222 let dir = tempfile::TempDir::new().expect("tempdir");
1223 let path = dir.path().join("doc.md");
1224 std::fs::write(&path, b"# Hello\n\nbody\n").expect("write fixture");
1225
1226 let outcome_json = wrapper
1227 .sign_text_file_json(path.to_str().unwrap(), "{}")
1228 .expect("sign_text_file_json default opts should succeed");
1229 let outcome: serde_json::Value =
1230 serde_json::from_str(&outcome_json).expect("outcome is JSON");
1231 let bak_path = outcome
1232 .get("backup_path")
1233 .and_then(|v| v.as_str())
1234 .expect("backup_path present");
1235 let mode = std::fs::metadata(bak_path)
1236 .expect("bak exists")
1237 .permissions()
1238 .mode()
1239 & 0o777;
1240 assert_eq!(
1241 mode, 0o600,
1242 "default .bak mode through JSON envelope must be 0o600; got {:o}",
1243 mode
1244 );
1245 }
1246
1247 #[test]
1258 fn verify_text_file_json_malformed_block_strict_returns_invalid_argument() {
1259 let wrapper = test_wrapper();
1260 let dir = tempfile::TempDir::new().expect("tempdir");
1261 let path = dir.path().join("malformed.md");
1262 std::fs::write(
1266 &path,
1267 b"# Doc\n\n-----BEGIN JACS SIGNATURE-----\nsigner: x\n",
1268 )
1269 .expect("write fixture");
1270
1271 let result = wrapper.verify_text_file_json(path.to_str().unwrap(), r#"{"strict": true}"#);
1272 assert!(
1273 result.is_err(),
1274 "strict verify on malformed block should fail with Err"
1275 );
1276 let err = result.unwrap_err();
1277 assert_eq!(
1280 err.kind,
1281 crate::ErrorKind::InvalidArgument,
1282 "expected InvalidArgument for malformed-block, got: {:?} (msg: {})",
1283 err.kind,
1284 err.message
1285 );
1286 }
1287
1288 #[test]
1289 fn verify_text_file_json_malformed_block_permissive_returns_status() {
1290 let wrapper = test_wrapper();
1291 let dir = tempfile::TempDir::new().expect("tempdir");
1292 let path = dir.path().join("malformed_permissive.md");
1293 std::fs::write(
1297 &path,
1298 b"# Doc\n\n-----BEGIN JACS SIGNATURE-----\nsigner: x\n",
1299 )
1300 .expect("write fixture");
1301
1302 let result = wrapper
1303 .verify_text_file_json(path.to_str().unwrap(), "{}")
1304 .expect("permissive verify of malformed file must NOT error");
1305 let v: serde_json::Value = serde_json::from_str(&result).expect("result is JSON");
1306 let status = v.get("status").and_then(|s| s.as_str()).unwrap_or("");
1307 assert_eq!(
1308 status, "malformed",
1309 "permissive verify must report status=malformed; got JSON: {}",
1310 v
1311 );
1312 }
1313
1314 #[test]
1315 fn verify_text_file_json_unsigned_permissive_returns_ok_status() {
1316 let wrapper = test_wrapper();
1317 let dir = tempfile::TempDir::new().expect("tempdir");
1318 let path = dir.path().join("unsigned.md");
1319 std::fs::write(&path, b"# Plain\n\nno signatures here\n").expect("write fixture");
1320
1321 let result = wrapper
1325 .verify_text_file_json(path.to_str().unwrap(), "{}")
1326 .expect("permissive verify of unsigned file must NOT error");
1327 let v: serde_json::Value = serde_json::from_str(&result).expect("result is JSON");
1328 let status = v.get("status").and_then(|s| s.as_str()).unwrap_or("");
1329 assert_eq!(
1330 status, "missing_signature",
1331 "permissive verify of unsigned file must report status=missing_signature; got JSON: {}",
1332 v
1333 );
1334 }
1335
1336 #[test]
1337 fn verify_json_accepts_inline_signed_markdown_string() {
1338 let wrapper = test_wrapper();
1339 let dir = tempfile::TempDir::new().expect("tempdir");
1340 let path = dir.path().join("signed.md");
1341 std::fs::write(&path, b"# Plain\n\nsigned through the inline footer\n")
1342 .expect("write fixture");
1343
1344 wrapper
1345 .sign_text_file_json(path.to_str().unwrap(), r#"{"backup": false}"#)
1346 .expect("sign text");
1347 let signed_markdown = std::fs::read_to_string(&path).expect("read signed markdown");
1348 let result = wrapper
1349 .verify_json(&signed_markdown)
1350 .expect("verify_json must dispatch inline signed markdown");
1351 let v: serde_json::Value = serde_json::from_str(&result).expect("result is JSON");
1352
1353 assert_eq!(v["valid"], true);
1354 assert_eq!(v["data"]["verificationType"], "inline-text");
1355 assert_eq!(v["data"]["signatures"][0]["status"], "valid");
1356 }
1357}