use std::collections::HashMap;
use std::path::Path;
use base64::Engine;
use serde_json::Value;
use time::OffsetDateTime;
use uuid::Uuid;
use crate::error::{HaiError, Result};
use crate::types::{
DocSearchResults, DocVerificationResult, MigrateAgentResult, RotationResult, SignedDocument,
SignedPayload, StorageCapabilities, UpdateAgentResult,
};
pub trait JacsProvider: Send + Sync {
fn jacs_id(&self) -> &str;
fn sign_string(&self, message: &str) -> Result<String>;
fn sign_bytes(&self, data: &[u8]) -> Result<Vec<u8>>;
fn key_id(&self) -> &str;
fn algorithm(&self) -> &str;
fn canonical_json(&self, value: &Value) -> Result<String>;
fn sign_response(&self, payload: &Value) -> Result<SignedPayload>;
fn verify_a2a_artifact(&self, wrapped_json: &str) -> Result<String> {
let wrapped: Value = serde_json::from_str(wrapped_json)?;
let signature = wrapped
.get("jacsSignature")
.and_then(|s| s.get("signature"))
.and_then(|s| s.as_str())
.unwrap_or("");
let signer_id = wrapped
.get("jacsSignature")
.and_then(|s| s.get("agentID"))
.and_then(|s| s.as_str())
.unwrap_or("");
let mut clone = wrapped.clone();
if let Some(obj) = clone.as_object_mut() {
obj.remove("jacsSignature");
}
let canonical = self.canonical_json(&clone)?;
let expected = self.sign_string(&canonical)?;
let valid = signature == expected;
let result = serde_json::json!({
"valid": valid,
"status": if valid { "verified" } else { "invalid" },
"signerId": signer_id,
"artifactType": wrapped.get("jacsType").and_then(|v| v.as_str()).unwrap_or(""),
"timestamp": wrapped.get("jacsVersionDate").and_then(|v| v.as_str()).unwrap_or(""),
"originalArtifact": wrapped.get("a2aArtifact").cloned().unwrap_or(Value::Null),
});
Ok(serde_json::to_string(&result)?)
}
fn sign_email_locally(&self, raw_email: &[u8]) -> Result<Vec<u8>> {
let _ = raw_email;
Err(HaiError::Provider(
"local email signing not supported by this provider; use LocalJacsProvider".to_string(),
))
}
fn rotate(&self) -> Result<RotationResult> {
Err(HaiError::Provider(
"key rotation not supported by this provider; use LocalJacsProvider".to_string(),
))
}
fn export_agent_json(&self) -> Result<String> {
Err(HaiError::Provider(
"export_agent_json not supported by this provider; use LocalJacsProvider".to_string(),
))
}
fn update_agent(&self, new_agent_data: &str) -> Result<UpdateAgentResult> {
let _ = new_agent_data;
Err(HaiError::Provider(
"update_agent not supported by this provider; use LocalJacsProvider".to_string(),
))
}
}
pub trait JacsAgentLifecycle: JacsProvider {
fn lifecycle_rotate(&self) -> Result<RotationResult>;
fn lifecycle_migrate(config_path: Option<&Path>) -> Result<MigrateAgentResult>
where
Self: Sized;
fn lifecycle_update_agent(&self, new_data: &str) -> Result<UpdateAgentResult>;
fn lifecycle_export_agent_json(&self) -> Result<String>;
fn diagnostics(&self) -> Result<Value>;
fn verify_self(&self) -> Result<DocVerificationResult>;
fn quickstart(
name: &str,
domain: &str,
description: Option<&str>,
algorithm: Option<&str>,
config_path: Option<&str>,
) -> Result<Value>
where
Self: Sized;
fn reencrypt_key(&self, old_password: &str, new_password: &str) -> Result<()>;
fn get_setup_instructions(&self, domain: &str, ttl: u32) -> Result<Value>;
}
pub trait JacsDocumentProvider: JacsProvider {
fn sign_document(&self, data: &Value) -> Result<String>;
fn store_document(&self, signed_json: &str) -> Result<String>;
fn sign_and_store(&self, data: &Value) -> Result<SignedDocument>;
fn sign_file(&self, path: &str, embed: bool) -> Result<SignedDocument>;
fn get_document(&self, key: &str) -> Result<String>;
fn list_documents(&self, jacs_type: Option<&str>) -> Result<Vec<String>>;
fn get_document_versions(&self, doc_id: &str) -> Result<Vec<String>>;
fn get_latest_document(&self, doc_id: &str) -> Result<String>;
fn remove_document(&self, key: &str) -> Result<()>;
fn update_document(&self, doc_id: &str, data: &str) -> Result<SignedDocument>;
fn search_documents(
&self,
query: &str,
limit: usize,
offset: usize,
) -> Result<DocSearchResults>;
fn query_by_type(
&self,
doc_type: &str,
limit: usize,
offset: usize,
) -> Result<Vec<String>>;
fn query_by_field(
&self,
field: &str,
value: &str,
limit: usize,
offset: usize,
) -> Result<Vec<String>>;
fn query_by_agent(
&self,
agent_id: &str,
limit: usize,
offset: usize,
) -> Result<Vec<String>>;
fn storage_capabilities(&self) -> Result<StorageCapabilities>;
}
pub trait JacsBatchProvider: JacsProvider {
fn sign_messages(&self, messages: &[&Value]) -> Result<Vec<SignedDocument>>;
fn verify_batch(&self, documents: &[&str]) -> Vec<DocVerificationResult>;
}
pub trait JacsVerificationProvider: JacsProvider {
fn verify_document(&self, document: &str) -> Result<DocVerificationResult>;
fn verify_with_key(&self, document: &str, key: Vec<u8>) -> Result<DocVerificationResult>;
fn verify_by_id(&self, doc_id: &str) -> Result<DocVerificationResult>;
fn verify_dns(&self, domain: &str) -> Result<()>;
fn build_auth_header_jacs(&self) -> Result<String>;
fn unwrap_signed_event(
&self,
event: &Value,
server_public_keys: &HashMap<String, Vec<u8>>,
) -> Result<(Value, bool)>;
}
pub trait JacsEmailProvider: JacsProvider {
fn sign_email(&self, raw: &[u8]) -> Result<Vec<u8>>;
fn verify_email(&self, raw: &[u8], key: Vec<u8>) -> Result<Value>;
fn add_jacs_attachment(&self, email: &[u8], doc: &[u8]) -> Result<Vec<u8>>;
fn get_jacs_attachment(&self, email: &[u8]) -> Result<Vec<u8>>;
fn remove_jacs_attachment(&self, email: &[u8]) -> Result<Vec<u8>>;
fn extract_email_parts(&self, raw: &[u8]) -> Result<Value>;
}
#[cfg(feature = "agreements")]
pub trait JacsAgreementProvider: JacsProvider {
fn create_agreement(
&self,
doc: &str,
agent_ids: &[String],
quorum: Option<&str>,
) -> Result<SignedDocument>;
fn sign_agreement(&self, document: &str) -> Result<SignedDocument>;
fn check_agreement(&self, document: &str) -> Result<Value>;
}
#[cfg(feature = "attestation")]
pub trait JacsAttestationProvider: JacsProvider {
fn create_attestation(&self, subject: &Value, claims: &[Value]) -> Result<String>;
fn verify_attestation(&self, doc_key: &str) -> Result<Value>;
}
#[derive(Debug, Clone)]
pub struct NoopJacsProvider {
jacs_id: String,
}
impl NoopJacsProvider {
pub fn new(jacs_id: impl Into<String>) -> Self {
Self {
jacs_id: jacs_id.into(),
}
}
}
impl JacsProvider for NoopJacsProvider {
fn jacs_id(&self) -> &str {
&self.jacs_id
}
fn sign_string(&self, _message: &str) -> Result<String> {
Err(HaiError::Provider(
"no JACS signer configured; provide a real JacsProvider".to_string(),
))
}
fn sign_bytes(&self, _data: &[u8]) -> Result<Vec<u8>> {
Err(HaiError::Provider(
"no JACS signer configured; provide a real JacsProvider".to_string(),
))
}
fn key_id(&self) -> &str {
&self.jacs_id
}
fn algorithm(&self) -> &str {
"none"
}
fn canonical_json(&self, value: &Value) -> Result<String> {
Ok(canonicalize_json_rfc8785(value))
}
fn sign_response(&self, _payload: &Value) -> Result<SignedPayload> {
Err(HaiError::Provider(
"no JACS response signer configured; provide a real JacsProvider".to_string(),
))
}
}
#[derive(Debug, Clone)]
pub struct StaticJacsProvider {
jacs_id: String,
algorithm: String,
}
impl StaticJacsProvider {
pub fn new(jacs_id: impl Into<String>) -> Self {
Self {
jacs_id: jacs_id.into(),
algorithm: "ed25519".to_string(),
}
}
pub fn with_algorithm(jacs_id: impl Into<String>, algorithm: impl Into<String>) -> Self {
Self {
jacs_id: jacs_id.into(),
algorithm: algorithm.into(),
}
}
}
impl JacsProvider for StaticJacsProvider {
fn jacs_id(&self) -> &str {
&self.jacs_id
}
fn sign_string(&self, message: &str) -> Result<String> {
let raw = format!("sig:{}", message);
Ok(base64::engine::general_purpose::STANDARD.encode(raw))
}
fn sign_bytes(&self, data: &[u8]) -> Result<Vec<u8>> {
let mut result = b"sig:".to_vec();
result.extend_from_slice(data);
Ok(result)
}
fn key_id(&self) -> &str {
&self.jacs_id
}
fn algorithm(&self) -> &str {
&self.algorithm
}
fn canonical_json(&self, value: &Value) -> Result<String> {
Ok(canonicalize_json_rfc8785(value))
}
fn sign_response(&self, payload: &Value) -> Result<SignedPayload> {
let canonical_payload = canonicalize_json_rfc8785(payload);
let data = serde_json::from_str::<Value>(&canonical_payload)?;
let now = OffsetDateTime::now_utc()
.format(&time::format_description::well_known::Rfc3339)
.map_err(|e| HaiError::Provider(format!("failed to format timestamp: {e}")))?;
let doc = serde_json::json!({
"version": "1.0.0",
"document_type": "job_response",
"data": data,
"metadata": {
"issuer": self.jacs_id,
"document_id": Uuid::new_v4().to_string(),
"created_at": now,
"hash": "",
},
"jacsSignature": {
"agentID": self.jacs_id,
"date": now,
"signature": self.sign_string(&canonical_payload)?,
},
});
Ok(SignedPayload {
signed_document: serde_json::to_string(&doc)?,
agent_jacs_id: self.jacs_id.clone(),
})
}
}
#[cfg(feature = "jacs-crate")]
pub fn canonicalize_json_rfc8785(value: &Value) -> String {
jacs::protocol::canonicalize_json(value)
}
#[cfg(all(not(feature = "jacs-crate"), feature = "serde_json_canonicalizer"))]
pub fn canonicalize_json_rfc8785(value: &Value) -> String {
serde_json_canonicalizer::to_string(value).unwrap_or_else(|_| "null".to_string())
}
#[cfg(all(not(feature = "jacs-crate"), not(feature = "serde_json_canonicalizer")))]
compile_error!(
"Either `jacs-crate` or `serde_json_canonicalizer` feature must be enabled for JSON canonicalization"
);