use affinidi_did_resolver_cache_sdk::DIDCacheClient;
use affinidi_messaging_didcomm::Message;
use affinidi_messaging_didcomm_service::DIDCommService;
use serde_json::Value;
use tracing::{debug, warn};
use crate::server::didcomm_profile::{PeerTransport, TransportFallback, resolve_send_binding};
pub type SendError = Box<dyn std::error::Error + Send + Sync>;
pub async fn send_trust_task(
didcomm: &DIDCommService,
listener_id: &str,
from: &str,
to: &str,
doc: &trust_tasks_rs::TrustTask<Value>,
fallback: &TransportFallback,
did_resolver: Option<&DIDCacheClient>,
) -> Result<PeerTransport, SendError> {
match resolve_send_binding(to, fallback, did_resolver).await {
Some((PeerTransport::Tsp, _)) => {
let payload = serde_json::to_vec(doc)?;
match didcomm.send_tsp(listener_id, to, &payload).await {
Ok(()) => {
debug!(to, type_uri = %doc.type_uri, "trust task sent over TSP");
Ok(PeerTransport::Tsp)
}
Err(tsp_err) => {
warn!(
to,
type_uri = %doc.type_uri,
error = %tsp_err,
"trust task: TSP send failed — falling back to DIDComm"
);
send_over_didcomm(didcomm, listener_id, from, to, doc).await?;
Ok(PeerTransport::Didcomm)
}
}
}
Some((PeerTransport::Didcomm, _)) => {
send_over_didcomm(didcomm, listener_id, from, to, doc).await?;
Ok(PeerTransport::Didcomm)
}
None => Err(format!(
"no route to {to}: peer advertises no messaging transport and no mediator is configured"
)
.into()),
}
}
#[allow(clippy::too_many_arguments)]
pub async fn send_trust_task_with_retry(
didcomm: &DIDCommService,
listener_id: &str,
from: &str,
to: &str,
doc: &trust_tasks_rs::TrustTask<Value>,
fallback: &TransportFallback,
did_resolver: Option<&DIDCacheClient>,
retry: Retry,
) -> Result<PeerTransport, SendError> {
let attempts = retry.attempts.max(1);
let mut last: Option<SendError> = None;
for attempt in 1..=attempts {
match send_trust_task(didcomm, listener_id, from, to, doc, fallback, did_resolver).await {
Ok(t) => return Ok(t),
Err(e) => {
debug!(
to,
attempt,
attempts,
error = %e,
"trust task send failed — retrying"
);
last = Some(e);
if attempt < attempts {
tokio::time::sleep(retry.delay).await;
}
}
}
}
Err(last.unwrap_or_else(|| "send_trust_task_with_retry: no attempts made".into()))
}
#[derive(Debug, Clone, Copy)]
pub struct Retry {
pub attempts: u32,
pub delay: std::time::Duration,
}
async fn send_over_didcomm(
didcomm: &DIDCommService,
listener_id: &str,
from: &str,
to: &str,
doc: &trust_tasks_rs::TrustTask<Value>,
) -> Result<(), SendError> {
let body = serde_json::to_value(doc)?;
let msg = Message::build(
uuid::Uuid::new_v4().to_string(),
trust_tasks_didcomm::ENVELOPE_TYPE.to_string(),
body,
)
.from(from.to_string())
.to(to.to_string())
.created_time(crate::server::auth::session::now_epoch())
.finalize();
didcomm.send_message(listener_id, msg, to).await?;
debug!(to, type_uri = %doc.type_uri, "trust task sent over DIDComm envelope");
Ok(())
}
pub fn build_request(
type_uri: &str,
from: &str,
to: &str,
payload: Value,
) -> Result<trust_tasks_rs::TrustTask<Value>, SendError> {
let uri: trust_tasks_rs::TypeUri = type_uri.parse()?;
let mut doc = trust_tasks_rs::TrustTask::new(uuid::Uuid::new_v4().to_string(), uri, payload);
doc.issuer = Some(from.to_string());
doc.recipient = Some(to.to_string());
doc.issued_at = Some(chrono::Utc::now());
Ok(doc)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::didcomm_types::{
MSG_HEALTH_PING, MSG_HEALTH_PONG, MSG_SERVER_REGISTER, MSG_SERVER_REGISTER_ACK,
};
#[test]
fn msg_constants_are_request_response_type_uri_pairs() {
for (req, resp) in [
(MSG_HEALTH_PING, MSG_HEALTH_PONG),
(MSG_SERVER_REGISTER, MSG_SERVER_REGISTER_ACK),
] {
let doc = build_request(req, "did:example:a", "did:example:b", Value::Null)
.unwrap_or_else(|e| panic!("{req} must parse as a TypeUri: {e}"));
let reply = doc.respond_with("reply-id", Value::Null);
assert_eq!(
reply.type_uri.to_string(),
resp,
"respond_with({req}) must produce {resp}"
);
}
}
#[test]
fn response_swaps_parties_and_threads_to_request() {
let doc = build_request(
MSG_HEALTH_PING,
"did:example:control",
"did:example:server",
serde_json::json!({}),
)
.expect("build");
let reply = doc.respond_with("reply-id", serde_json::json!({ "status": "ok" }));
assert_eq!(reply.issuer.as_deref(), Some("did:example:server"));
assert_eq!(reply.recipient.as_deref(), Some("did:example:control"));
assert_eq!(reply.thread_id.as_deref(), Some(doc.id.as_str()));
}
#[test]
fn route_decorator_uris_are_not_valid_document_type_uris() {
let decorator = "https://trusttasks.org/did-hosting/server/health-ping/1.0";
assert!(
build_request(decorator, "did:example:a", "did:example:b", Value::Null).is_err(),
"route-decorator URI must not parse as a document TypeUri"
);
}
}