use std::sync::Arc;
use std::time::Duration;
use affinidi_did_resolver_cache_sdk::DIDCacheClient;
use affinidi_did_resolver_cache_sdk::config::DIDCacheConfigBuilder;
use affinidi_messaging_core::{Inbound, MessageTransport, Protocol, ReceivedMessage};
use affinidi_messaging_delivery::{Delivery, MessagingService, OutboxStore};
use affinidi_messaging_didcomm::Message;
use affinidi_tdk::common::TDKSharedState;
use affinidi_tdk::common::config::TDKConfig;
use affinidi_tdk::messaging::config::ATMConfig;
use affinidi_tdk::messaging::profiles::ATMProfile;
use affinidi_tdk::messaging::{ATM, DidCommTransport};
use affinidi_tdk::secrets_resolver::SecretsResolver;
use affinidi_tdk::secrets_resolver::secrets::Secret;
use futures_util::StreamExt;
use tokio_util::sync::CancellationToken;
use tracing::{info, warn};
use vti_common::outbox_store::VtiOutboxStore;
use crate::messaging::router::{self, VtaState};
use crate::messaging::shim::{DIDCommResponse, ProblemReport, ServiceProblemReport};
use crate::server::AppState;
use crate::store::KeyspaceHandle;
pub struct VtaMessaging {
pub service: Arc<MessagingService>,
pub atm: Arc<ATM>,
pub profile: Arc<ATMProfile>,
}
pub async fn build_messaging(
secrets: Vec<Secret>,
vta_did: &str,
mediator_did: &str,
outbox_ks: KeyspaceHandle,
did_resolver: Option<&DIDCacheClient>,
resolver_url: Option<&str>,
) -> Result<VtaMessaging, String> {
let mut builder = TDKConfig::builder().with_load_environment(false);
if let Some(dr) = did_resolver {
builder = builder.with_did_resolver(dr.clone());
} else if let Some(url) = resolver_url {
let resolver_config = DIDCacheConfigBuilder::default()
.with_network_mode(url)
.build();
builder = builder.with_did_resolver_config(resolver_config);
}
let tdk_config = builder
.build()
.map_err(|e| format!("build TDK config: {e}"))?;
let tdk = TDKSharedState::new(tdk_config)
.await
.map_err(|e| format!("create TDK shared state: {e}"))?;
for secret in secrets {
tdk.secrets_resolver().insert(secret).await;
}
let atm = ATM::new(
ATMConfig::builder()
.build()
.map_err(|e| format!("build ATM config: {e}"))?,
Arc::new(tdk),
)
.await
.map_err(|e| format!("create ATM: {e}"))?;
let profile = Arc::new(
ATMProfile::new(
&atm,
None,
vta_did.to_string(),
Some(mediator_did.to_string()),
)
.await
.map_err(|e| format!("create ATM profile: {e}"))?,
);
match tokio::time::timeout(
Duration::from_secs(30),
atm.profile_enable_websocket(&profile),
)
.await
{
Ok(res) => res.map_err(|e| format!("enable websocket: {e}"))?,
Err(_) => {
return Err(
"timeout enabling websocket to mediator after 30s — mediator may be unreachable"
.to_string(),
);
}
}
let atm = Arc::new(atm);
let transport: Arc<dyn MessageTransport> = Arc::new(
DidCommTransport::new((*atm).clone(), profile.clone())
.await
.map_err(|e| format!("bind DidComm transport: {e}"))?,
);
let outbox: Arc<dyn OutboxStore> = Arc::new(VtiOutboxStore::new(outbox_ks));
let service = Arc::new(MessagingService::new(transport.clone(), outbox.clone()));
tokio::spawn(affinidi_messaging_delivery::drain_loop(
outbox.clone(),
service.primary_handle(),
Duration::from_secs(2),
));
tokio::spawn(affinidi_messaging_delivery::outbox_drain_loop(
service.primary_handle(),
outbox.clone(),
Duration::from_secs(10),
));
tokio::spawn(affinidi_messaging_delivery::confirmation_loop(
outbox.clone(),
Duration::from_secs(30),
));
Ok(VtaMessaging {
service,
atm,
profile,
})
}
pub async fn run_inbound_loop(
messaging: Arc<VtaMessaging>,
app_state: AppState,
vta_did: String,
mediator_did: String,
shutdown: CancellationToken,
) {
let vta_state = Arc::new(VtaState::from(&app_state));
let mut stream = messaging.service.subscribe();
info!("VTA messaging connected to mediator — inbound messages will be processed");
const MAX_INFLIGHT_INBOUND: usize = 32;
let inflight = Arc::new(tokio::sync::Semaphore::new(MAX_INFLIGHT_INBOUND));
loop {
tokio::select! {
maybe = stream.next() => {
let Some(inbound) = maybe else {
warn!("VTA inbound stream ended — messaging dispatcher stopping");
break;
};
let permit = match Arc::clone(&inflight).acquire_owned().await {
Ok(p) => p,
Err(_) => {
warn!("inbound concurrency semaphore closed — stopping");
break;
}
};
let messaging = Arc::clone(&messaging);
let app_state = app_state.clone();
let vta_state = Arc::clone(&vta_state);
let vta_did = vta_did.clone();
let mediator_did = mediator_did.clone();
tokio::spawn(async move {
let _permit = permit;
handle_inbound(
inbound,
&messaging,
&app_state,
&vta_state,
&vta_did,
&mediator_did,
)
.await;
});
}
_ = shutdown.cancelled() => {
info!("VTA messaging stopping (shutdown signalled)");
break;
}
}
}
info!("VTA messaging stopped");
}
async fn handle_inbound(
inbound: Inbound,
messaging: &Arc<VtaMessaging>,
app_state: &AppState,
vta_state: &Arc<VtaState>,
vta_did: &str,
mediator_did: &str,
) {
match inbound.message.protocol {
Protocol::DIDComm => {
let _ = mediator_did;
handle_didcomm(inbound, messaging, app_state, vta_state, vta_did).await;
}
#[cfg(feature = "tsp")]
Protocol::TSP => {
handle_tsp(inbound, messaging, app_state, mediator_did).await;
}
#[cfg(not(feature = "tsp"))]
Protocol::TSP => {
let _ = mediator_did;
warn!("received an inbound TSP frame but the `tsp` feature is disabled — dropping");
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum InboundGate {
NotEncrypted,
Unauthenticated,
Authenticated(String),
}
impl InboundGate {
fn authenticated_sender(&self) -> Option<&str> {
match self {
InboundGate::Authenticated(did) => Some(did.as_str()),
_ => None,
}
}
}
fn inbound_gate(message: &ReceivedMessage) -> InboundGate {
if !message.encrypted {
return InboundGate::NotEncrypted;
}
match message.sender.as_deref() {
Some(did) if message.verified => InboundGate::Authenticated(did.to_string()),
_ => InboundGate::Unauthenticated,
}
}
async fn handle_didcomm(
inbound: Inbound,
messaging: &Arc<VtaMessaging>,
app_state: &AppState,
vta_state: &Arc<VtaState>,
vta_did: &str,
) {
let mut msg: Message = match serde_json::from_slice(&inbound.message.payload) {
Ok(m) => m,
Err(e) => {
warn!(error = %e, "failed to parse inbound DIDComm message — dropping");
return;
}
};
let plaintext_from = msg.from.clone();
let gate = inbound_gate(&inbound.message);
let auth_sender = gate.authenticated_sender().map(str::to_string);
msg.from = auth_sender.clone();
let reply_to = auth_sender.clone().or(plaintext_from);
let msg_id = msg.id.clone();
let message_type = msg.typ.clone();
let start = std::time::Instant::now();
let reply = match &gate {
InboundGate::NotEncrypted => Some(DIDCommResponse::problem_report(
ProblemReport::bad_request("DIDComm message must be encrypted"),
)),
InboundGate::Unauthenticated => Some(DIDCommResponse::problem_report(
ProblemReport::unauthorized(
"DIDComm message must be authenticated (authcrypt) with a non-anonymous sender",
),
)),
InboundGate::Authenticated(_) => {
let ctx = crate::messaging::shim::HandlerContext {
sender_did: auth_sender.clone(),
};
router::dispatch(msg, ctx, vta_state.clone(), app_state.clone()).await
}
};
info!(
target: "didcomm_server::request",
message_type = %message_type,
sender = %auth_sender.as_deref().unwrap_or("<anon>"),
status = if reply.is_some() { "ok(response)" } else { "ok(empty)" },
latency = ?start.elapsed(),
"Request processed"
);
let Some(reply) = reply else {
return;
};
let Some(to) = reply_to else {
warn!(
reply_type = %reply.type_,
"computed a DIDComm reply but the inbound message had no sender/from to reply to — dropping"
);
return;
};
let reply_id = uuid::Uuid::new_v4().to_string();
let thid = reply.thid.unwrap_or(msg_id);
let reply_msg = Message::build(reply_id, reply.type_, reply.body)
.from(vta_did.to_string())
.to(to.clone())
.thid(thid)
.finalize();
match messaging
.atm
.pack_encrypted(&reply_msg, &to, Some(vta_did), Some(vta_did))
.await
{
Ok((packed, _)) => {
if let Err(e) = messaging
.service
.send(&to, packed.into_bytes(), Delivery::BestEffort)
.await
{
warn!(recipient = %to, error = %e, "failed to send DIDComm reply");
}
}
Err(e) => warn!(recipient = %to, error = %e, "failed to pack DIDComm reply"),
}
}
#[cfg(feature = "tsp")]
async fn handle_tsp(
inbound: Inbound,
messaging: &Arc<VtaMessaging>,
app_state: &AppState,
mediator_did: &str,
) {
let Some(sender_vid) = inbound.message.sender.clone() else {
warn!("inbound TSP frame has no authenticated sender VID — dropping");
return;
};
let reply = crate::messaging::tsp_inbound::dispatch_one(
app_state,
&inbound.message.payload,
&sender_vid,
)
.await;
if reply.is_empty() {
return;
}
let route = vec![mediator_did.to_string(), sender_vid.clone()];
if let Err(e) = messaging
.atm
.tsp()
.send_routed(&messaging.profile, &route, &reply)
.await
{
warn!(recipient = %sender_vid, error = %e, "failed to send TSP reply");
}
}
#[cfg(test)]
mod tests {
use super::{InboundGate, inbound_gate};
use affinidi_messaging_core::{Protocol, ReceivedMessage};
const SENDER: &str = "did:key:z6MkSenderUnderTest";
fn received(encrypted: bool, sender: Option<&str>, verified: bool) -> ReceivedMessage {
ReceivedMessage {
id: "urn:uuid:test".to_string(),
sender: sender.map(str::to_string),
recipient: "did:key:z6MkRecipient".to_string(),
payload: Vec::new(),
protocol: Protocol::DIDComm,
verified,
encrypted,
}
}
#[test]
fn encrypted_and_verified_sender_is_authenticated() {
assert_eq!(
inbound_gate(&received(true, Some(SENDER), true)),
InboundGate::Authenticated(SENDER.to_string()),
);
}
#[test]
fn unverified_sender_is_rejected() {
assert_eq!(
inbound_gate(&received(true, Some(SENDER), false)),
InboundGate::Unauthenticated,
);
assert_eq!(
inbound_gate(&received(true, Some(SENDER), false)).authenticated_sender(),
None,
);
}
#[test]
fn anonymous_encrypted_sender_is_rejected() {
assert_eq!(
inbound_gate(&received(true, None, false)),
InboundGate::Unauthenticated,
);
assert_eq!(
inbound_gate(&received(true, None, true)),
InboundGate::Unauthenticated,
);
}
#[test]
fn plaintext_frame_is_rejected() {
assert_eq!(
inbound_gate(&received(false, Some(SENDER), false)),
InboundGate::NotEncrypted,
);
assert_eq!(
inbound_gate(&received(false, Some(SENDER), true)),
InboundGate::NotEncrypted,
);
assert_eq!(
inbound_gate(&received(false, Some(SENDER), true)).authenticated_sender(),
None,
);
}
}