use std::sync::Arc;
use std::time::Duration;
use affinidi_messaging_core::{Inbound, InboundKind, Protocol};
use affinidi_messaging_delivery::{MessagingService, OutboxStore};
use affinidi_tdk::common::TDKSharedState;
use affinidi_tdk::common::config::TDKConfig;
use affinidi_tdk::didcomm::Message;
use affinidi_tdk::messaging::config::ATMConfig;
use affinidi_tdk::messaging::messages::compat::UnpackMetadata;
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 tokio_util::sync::CancellationToken;
use tracing::{info, warn};
use crate::configs::{DidcommConfig, ProfileConfig};
use crate::didcomm::error::DIDCommError;
use crate::didcomm::listener::MessageHandler;
use crate::didcomm::listener::mediator_functions::set_mediator_acl_mode;
use crate::messaging::kv::{KvKeyspace, MessagingStore};
use crate::messaging::outbox_store::FjallOutboxStore;
use crate::messaging::relationship_store::{FjallRelationshipKv, RegistryRelationshipStore};
use crate::messaging::tsp_inbound::{ControlDecision, decide_control};
use crate::trust_tasks::TaskHandler;
fn messaging_store_path(alias: &str) -> std::path::PathBuf {
let base = std::env::var("TR_MESSAGING_STORE_PATH")
.map(std::path::PathBuf::from)
.unwrap_or_else(|_| {
std::env::var("TR_SECRETS_DATA_DIR")
.map(std::path::PathBuf::from)
.unwrap_or_else(|_| std::path::PathBuf::from("./.trust-registry"))
.join("messaging")
});
let safe: String = alias
.chars()
.map(|c| if c.is_ascii_alphanumeric() { c } else { '_' })
.collect();
base.join(safe)
}
pub async fn start_managed_delivery<H: MessageHandler>(
profile_config: ProfileConfig,
config: Arc<DidcommConfig>,
handler: Arc<H>,
tasks: TaskHandler,
shutdown: CancellationToken,
) -> Result<(), DIDCommError> {
let path = messaging_store_path(&profile_config.alias);
let path_str = path.to_str().ok_or_else(|| {
DIDCommError::Messaging(format!("non-UTF-8 messaging store path: {path:?}"))
})?;
let store = MessagingStore::open(path_str).map_err(DIDCommError::Messaging)?;
let messaging = build_messaging(
profile_config.secrets,
&profile_config.did,
&profile_config.alias,
&config.mediator_did,
store.relationships.clone(),
store.outbox.clone(),
)
.await
.map_err(DIDCommError::Messaging)?;
if let Err(e) =
set_mediator_acl_mode(&messaging.atm, &messaging.profile, config.acl_mode.clone()).await
{
warn!("Failed to set ACL mode for Trust Registry DID. Error: {e}");
}
tokio::spawn(crate::messaging::relationship_store::maintenance_loop(
messaging.relationship_store.clone(),
));
info!(
"[profile = {}] TSP relationship management active; driving inbound off the delivery layer",
&profile_config.alias
);
run_inbound_loop(
messaging.service.clone(),
messaging.atm.clone(),
messaging.profile.clone(),
handler,
tasks,
shutdown,
)
.await;
drop(store);
Ok(())
}
pub struct RegistryMessaging {
pub service: Arc<MessagingService>,
pub atm: Arc<ATM>,
pub profile: Arc<ATMProfile>,
pub relationship_store: Arc<RegistryRelationshipStore>,
}
pub async fn build_messaging(
secrets: Vec<Secret>,
did: &str,
alias: &str,
mediator_did: &str,
relationships_ks: KvKeyspace,
outbox_ks: KvKeyspace,
) -> Result<RegistryMessaging, String> {
let tdk_config = TDKConfig::builder()
.with_load_environment(false)
.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 relationship_store = Arc::new(RegistryRelationshipStore::new(FjallRelationshipKv::new(
relationships_ks,
)));
let atm_config = ATMConfig::builder()
.with_relationship_store(relationship_store.clone())
.build()
.map_err(|e| format!("build ATM config: {e}"))?;
let atm = Arc::new(
ATM::new(atm_config, Arc::new(tdk))
.await
.map_err(|e| format!("create ATM: {e}"))?,
);
let profile = ATMProfile::new(
&atm,
Some(alias.to_string()),
did.to_string(),
Some(mediator_did.to_string()),
)
.await
.map_err(|e| format!("create ATM profile: {e}"))?;
let profile = atm
.profile_add(&profile, false)
.await
.map_err(|e| format!("register ATM profile: {e}"))?;
let transport = match connect_transport(&atm, &profile).await {
Ok(transport) => transport,
Err(e) => {
atm.graceful_shutdown().await;
return Err(e);
}
};
let outbox: Arc<dyn OutboxStore> = Arc::new(FjallOutboxStore::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(RegistryMessaging {
service,
atm,
profile,
relationship_store,
})
}
async fn connect_transport(
atm: &Arc<ATM>,
profile: &Arc<ATMProfile>,
) -> Result<Arc<dyn affinidi_messaging_core::MessageTransport>, String> {
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(),
);
}
}
Ok(Arc::new(
DidCommTransport::new((**atm).clone(), profile.clone())
.await
.map_err(|e| format!("bind DidComm transport: {e}"))?,
))
}
pub async fn run_inbound_loop<H: MessageHandler>(
service: Arc<MessagingService>,
atm: Arc<ATM>,
profile: Arc<ATMProfile>,
handler: Arc<H>,
tasks: TaskHandler,
shutdown: CancellationToken,
) {
use futures::StreamExt;
let mut stream = service.subscribe();
info!("registry 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!("registry 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 atm = atm.clone();
let profile = profile.clone();
let handler = handler.clone();
let tasks = tasks.clone();
tokio::spawn(async move {
let _permit = permit;
handle_inbound(inbound, &atm, &profile, &handler, &tasks).await;
});
}
_ = shutdown.cancelled() => {
info!("registry messaging stopping (shutdown signalled)");
break;
}
}
}
info!("registry messaging stopped");
}
async fn handle_inbound<H: MessageHandler>(
inbound: Inbound,
atm: &Arc<ATM>,
profile: &Arc<ATMProfile>,
handler: &Arc<H>,
tasks: &TaskHandler,
) {
match inbound.message.protocol {
Protocol::DIDComm => handle_didcomm(inbound, atm, profile, handler).await,
Protocol::TSP => handle_tsp(inbound, atm, profile, tasks).await,
Protocol::DIDCommV1 => {
warn!("received an inbound DIDComm v1 frame; the registry speaks v2.1 only — dropping");
}
_ => warn!("received an inbound frame of an unknown protocol — dropping"),
}
}
async fn handle_didcomm<H: MessageHandler>(
inbound: Inbound,
atm: &Arc<ATM>,
profile: &Arc<ATMProfile>,
handler: &Arc<H>,
) {
let mut message: Message = match serde_json::from_slice(&inbound.message.payload) {
Ok(m) => m,
Err(e) => {
warn!(error = %e, "dropping an inbound DIDComm frame that did not rehydrate");
return;
}
};
if let Some(sender) = &inbound.message.sender {
message.from = Some(sender.clone());
}
let mut meta = UnpackMetadata::default();
meta.authenticated = inbound.message.verified;
meta.anonymous_sender = inbound.message.sender.is_none();
meta.encrypted = inbound.message.encrypted;
if let Err(e) = handler.handle(atm, profile, message, meta).await {
warn!(error = %e, "registry DIDComm handler returned an error");
}
}
async fn handle_tsp(
inbound: Inbound,
atm: &Arc<ATM>,
profile: &Arc<ATMProfile>,
tasks: &TaskHandler,
) {
let Some(sender_vid) = inbound.message.sender.clone() else {
warn!("inbound TSP frame has no authenticated sender VID — dropping");
return;
};
if let InboundKind::RelationshipControl {
request,
thread_digest,
reply_expected,
..
} = inbound.kind
{
handle_tsp_control(
atm,
profile,
&sender_vid,
request,
thread_digest,
reply_expected,
)
.await;
return;
}
crate::tsp::dispatch_tsp_application(
atm,
profile,
tasks,
&inbound.message.payload,
&sender_vid,
)
.await;
}
async fn handle_tsp_control(
atm: &Arc<ATM>,
profile: &Arc<ATMProfile>,
sender_vid: &str,
request: affinidi_messaging_core::RelationshipRequest,
thread_digest: [u8; 32],
reply_expected: bool,
) {
match decide_control(request, reply_expected) {
ControlDecision::Accept => {
match atm
.tsp()
.accept_relationship(profile, sender_vid, thread_digest)
.await
{
Ok(state) => info!(
sender = %sender_vid, ?request, ?state,
"accepted an inbound TSP relationship request",
),
Err(e) => warn!(
sender = %sender_vid, error = %e,
"could not send a TSP relationship accept; the relationship stays recorded, \
so traffic still flows, but the peer sees no answer",
),
}
}
ControlDecision::Cancel(why) => {
match atm
.tsp()
.answer_cancellation(profile, sender_vid, thread_digest)
.await
{
Ok(_) => info!(
sender = %sender_vid, ?request, reason = %why,
"answered an inbound TSP relationship request with a cancellation",
),
Err(e) => warn!(
sender = %sender_vid, reason = %why, error = %e,
"could not send a TSP relationship cancellation",
),
}
}
ControlDecision::Nothing => info!(
sender = %sender_vid, ?request,
"recorded an inbound TSP relationship request; no answer is due",
),
}
}