use affinidi_messaging_delivery::Delivery;
use affinidi_messaging_didcomm::Message;
use serde_json::Value;
use trust_tasks_rs::TrustTask;
use uuid::Uuid;
use vti_common::capability_client::TRUST_TASK_ENVELOPE_TYPE;
use crate::messaging::VtcMessaging;
#[derive(Debug)]
pub(crate) enum OutboundError {
Serialise(String),
Pack(String),
Send(String),
}
impl std::fmt::Display for OutboundError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Serialise(e) => write!(f, "serialise envelope body: {e}"),
Self::Pack(e) => write!(f, "pack failed: {e}"),
Self::Send(e) => write!(f, "send failed: {e}"),
}
}
}
pub(crate) async fn send_trust_task_didcomm(
messaging: &VtcMessaging,
recipient_did: &str,
doc: &TrustTask<Value>,
) -> Result<(), OutboundError> {
let body = serde_json::to_value(doc).map_err(|e| OutboundError::Serialise(e.to_string()))?;
let envelope = Message::build(
format!("urn:uuid:{}", Uuid::new_v4()),
TRUST_TASK_ENVELOPE_TYPE.to_string(),
body,
)
.from(messaging.vtc_did.clone())
.to(recipient_did.to_string())
.thid(doc.id.clone())
.finalize();
let (packed, _) = messaging
.atm
.pack_encrypted(
&envelope,
recipient_did,
Some(&messaging.vtc_did),
Some(&messaging.vtc_did),
)
.await
.map_err(|e| OutboundError::Pack(e.to_string()))?;
messaging
.service
.send(recipient_did, packed.into_bytes(), Delivery::BestEffort)
.await
.map_err(|e| OutboundError::Send(e.to_string()))?;
Ok(())
}
#[derive(Clone)]
pub(crate) struct PeerTasks {
pub(crate) didcomm: std::sync::Arc<tokio::sync::OnceCell<std::sync::Arc<VtcMessaging>>>,
pub(crate) signer: Option<std::sync::Arc<crate::credentials::LocalSigner>>,
pub(crate) replies: crate::hooks::PendingReplies,
pub(crate) resolver: Option<affinidi_did_resolver_cache_sdk::DIDCacheClient>,
}
#[derive(Debug, Clone)]
pub enum PeerError {
Transient(String),
NoMatchingProtocol(String),
}
impl std::fmt::Display for PeerError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Transient(m) | Self::NoMatchingProtocol(m) => f.write_str(m),
}
}
}
impl PeerTasks {
pub(crate) async fn select(
&self,
peer_did: &str,
) -> Result<vta_sdk::protocol::matching::ProtocolMatch, PeerError> {
use vta_sdk::protocol::matching::{ServiceCapabilities, select_protocol};
let resolver = self.resolver.as_ref().ok_or_else(|| {
PeerError::Transient(
"no DID resolver configured — cannot read the peer's advertised transports".into(),
)
})?;
let resolved = resolver
.resolve(peer_did)
.await
.map_err(|e| PeerError::Transient(format!("could not resolve {peer_did}: {e}")))?;
let doc = serde_json::to_value(&resolved.doc)
.map_err(|e| PeerError::Transient(format!("unreadable DID document: {e}")))?;
let theirs = ServiceCapabilities::from_did_document(&doc);
let up = self.didcomm.get().is_some();
let ours = ServiceCapabilities {
tsp: (up && cfg!(feature = "tsp")).then(|| peer_did.to_string()),
didcomm: up.then(|| peer_did.to_string()),
rest: None,
};
if ours.advertised().is_empty() {
return Err(PeerError::Transient(
"VTC messaging is not running yet".into(),
));
}
select_protocol(&ours, &theirs, peer_did)
.map_err(|e| PeerError::NoMatchingProtocol(e.to_string()))
}
pub(crate) async fn exchange(
&self,
peer_did: &str,
type_uri: &str,
payload: &Value,
timeout: std::time::Duration,
) -> Result<TrustTask<Value>, PeerError> {
use vta_sdk::protocol::matching::Protocol;
let messaging = self
.didcomm
.get()
.ok_or_else(|| PeerError::Transient("VTC messaging is not running yet".into()))?;
let signer = self
.signer
.as_ref()
.ok_or_else(|| PeerError::Transient("this VTC has no signing key yet".into()))?;
let matched = self.select(peer_did).await?;
let doc = vti_common::capability_client::build_document(
&messaging.vtc_did,
peer_did,
type_uri,
payload.clone(),
);
let mut value = serde_json::to_value(&doc)
.map_err(|e| PeerError::Transient(format!("serialise document: {e}")))?;
signer
.sign_operational_doc(&mut value)
.await
.map_err(|e| PeerError::Transient(format!("sign document: {e}")))?;
let doc: TrustTask<Value> = serde_json::from_value(value)
.map_err(|e| PeerError::Transient(format!("reparse signed document: {e}")))?;
let receiver = self.replies.register(&doc.id, peer_did);
let sent = match matched.protocol {
#[cfg(feature = "tsp")]
Protocol::Tsp => {
let body =
seal_trust_task_tsp(&doc).map_err(|e| PeerError::Transient(e.to_string()))?;
vti_common::tsp_route::send_reestablishing(
&messaging.atm,
&messaging.profile,
&messaging.mediator_did,
peer_did,
Some(matched.peer_endpoint.as_str()),
&body,
)
.await
.map(|_| ())
.map_err(|e| PeerError::Transient(format!("TSP send failed: {e}")))
}
Protocol::Didcomm => send_trust_task_didcomm(messaging, peer_did, &doc)
.await
.map_err(|e| PeerError::Transient(e.to_string())),
_ => Err(PeerError::NoMatchingProtocol(format!(
"{peer_did} can be reached by no transport this VTC speaks"
))),
};
if let Err(e) = sent {
self.replies.abandon(&doc.id);
return Err(e);
}
match tokio::time::timeout(timeout, receiver).await {
Ok(Ok(reply)) => Ok(reply),
Ok(Err(_)) => {
self.replies.abandon(&doc.id);
Err(PeerError::Transient("reply channel closed".into()))
}
Err(_) => {
self.replies.abandon(&doc.id);
Err(PeerError::Transient(format!(
"no reply from {peer_did} within {}s",
timeout.as_secs()
)))
}
}
}
}
#[cfg(feature = "tsp")]
pub(crate) fn seal_trust_task_tsp(doc: &TrustTask<Value>) -> Result<Vec<u8>, OutboundError> {
let document = serde_json::to_vec(doc).map_err(|e| OutboundError::Serialise(e.to_string()))?;
Ok(vta_sdk::tsp_binding::wrap_envelope(&document))
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(feature = "tsp")]
#[test]
fn the_tsp_seal_is_the_published_envelope() {
let doc: TrustTask<Value> = TrustTask::new(
"urn:uuid:1".to_string(),
"https://trusttasks.org/spec/registry/record/put/0.1"
.parse()
.expect("valid type URI"),
serde_json::json!({}),
);
let sealed = seal_trust_task_tsp(&doc).expect("seals");
let parsed: Value = serde_json::from_slice(&sealed).expect("is JSON");
assert_eq!(
parsed["type"],
vta_sdk::tsp_binding::ENVELOPE_TYPE,
"the wrapper must carry the binding's own type"
);
assert_eq!(
parsed["document"]["id"], "urn:uuid:1",
"and the document must survive the wrapping unchanged"
);
}
}