use std::collections::VecDeque;
use std::sync::{Arc, Mutex};
use std::time::Duration;
use affinidi_messaging_core::{InboundKind, MessageTransport, Protocol, RelationshipRequest};
use affinidi_messaging_delivery::{Delivery, InMemoryOutboxStore, MessagingService, OutboxStore};
use affinidi_messaging_sdk::protocols::tsp::{SendReadiness, invite_refusal_is_benign};
use affinidi_tdk::common::TDKSharedState;
use affinidi_tdk::common::config::TDKConfig;
use affinidi_tdk::didcomm::Message;
use affinidi_tdk::messaging::ATM;
use affinidi_tdk::messaging::DidCommTransport;
use affinidi_tdk::messaging::config::ATMConfig;
use affinidi_tdk::messaging::profiles::ATMProfile;
use affinidi_tdk::secrets_resolver::SecretsResolver;
use anyhow::{Context, Result, anyhow};
use async_trait::async_trait;
use futures_util::StreamExt;
use futures_util::stream::BoxStream;
use serde_json::Value;
use tokio::sync::RwLock;
use vta_sdk::protocol::matching::ServiceCapabilities;
use crate::identity::BridgeIdentity;
use crate::wire::{ENVELOPE_TYPE, new_id};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Via {
Tsp,
Didcomm,
}
impl std::fmt::Display for Via {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Via::Tsp => "TSP",
Via::Didcomm => "DIDComm",
})
}
}
pub fn preferred_via(caps: &ServiceCapabilities) -> Via {
if caps.tsp.is_some() {
Via::Tsp
} else {
Via::Didcomm
}
}
#[async_trait]
pub trait VtcLink: Send + Sync {
async fn send(&self, to: &str, doc: &Value) -> Result<()>;
async fn send_via(&self, to: &str, doc: &Value, via: Via) -> Result<()>;
}
#[derive(Debug, Clone)]
pub struct InboundDoc {
pub doc: Value,
pub authenticated_sender: Option<String>,
pub via: Via,
}
const MEDIATOR_TIMEOUT: Duration = Duration::from_secs(30);
const ARRIVAL_ID_MAX: usize = 256;
const ARRIVALS_KEPT: usize = 1024;
#[derive(Default)]
struct Arrivals(Mutex<VecDeque<(String, Via)>>);
impl Arrivals {
fn record(&self, id: &str, via: Via) {
if id.len() > ARRIVAL_ID_MAX {
return;
}
let mut q = self.0.lock().unwrap_or_else(|p| p.into_inner());
if q.len() >= ARRIVALS_KEPT {
q.pop_front();
}
q.push_back((id.to_string(), via));
}
fn of(&self, id: &str) -> Option<Via> {
let q = self.0.lock().unwrap_or_else(|p| p.into_inner());
q.iter().rev().find(|(i, _)| i == id).map(|(_, v)| *v)
}
}
pub struct MediatorLink {
atm: Arc<ATM>,
tdk: Arc<TDKSharedState>,
profile: Arc<ATMProfile>,
service: Arc<MessagingService>,
did: String,
mediator_did: String,
arrivals: Arc<Arrivals>,
}
struct InboundCtx {
atm: Arc<ATM>,
profile: Arc<ATMProfile>,
vtc_did: String,
peers: Vec<String>,
arrivals: Arc<Arrivals>,
}
impl InboundCtx {
fn is_peer(&self, sender: &str) -> bool {
self.peers.iter().any(|p| p == sender)
}
}
fn did_part(did: &str) -> &str {
did.split_once('#').map_or(did, |(d, _)| d)
}
impl MediatorLink {
pub async fn connect(
identity: &BridgeIdentity,
mediator_did: &str,
vtc_did: &str,
mut peers: Vec<String>,
) -> Result<(Self, BoxStream<'static, InboundDoc>)> {
peers.push(vtc_did.to_string());
let tdk = Arc::new(
TDKSharedState::new(TDKConfig::builder().build()?)
.await
.map_err(|e| anyhow!("TDK: {e}"))?,
);
for secret in identity.messaging_secrets() {
tdk.secrets_resolver().insert(secret).await;
}
let atm = Arc::new(
ATM::new(ATMConfig::builder().build()?, Arc::clone(&tdk))
.await
.map_err(|e| anyhow!("messaging: {e}"))?,
);
match Self::attach(&atm, identity.did(), mediator_did).await {
Ok((profile, service)) => {
let arrivals = Arc::new(Arrivals::default());
let ctx = Arc::new(InboundCtx {
atm: Arc::clone(&atm),
profile: Arc::clone(&profile),
vtc_did: vtc_did.to_string(),
peers,
arrivals: Arc::clone(&arrivals),
});
let stream = inbound_stream(service.subscribe(), ctx);
Ok((
MediatorLink {
atm,
tdk,
profile,
service,
did: identity.did().to_string(),
mediator_did: mediator_did.to_string(),
arrivals,
},
stream,
))
}
Err(e) => {
atm.graceful_shutdown().await;
Err(e)
}
}
}
async fn attach(
atm: &Arc<ATM>,
did: &str,
mediator_did: &str,
) -> Result<(Arc<ATMProfile>, Arc<MessagingService>)> {
let profile = ATMProfile::new(atm, None, did.to_string(), Some(mediator_did.to_string()))
.await
.map_err(|e| anyhow!("building the messaging profile: {e}"))?;
let profile = atm
.profile_add(&profile, false)
.await
.map_err(|e| anyhow!("registering the messaging profile: {e}"))?;
tokio::time::timeout(MEDIATOR_TIMEOUT, atm.profile_enable_websocket(&profile))
.await
.context("the mediator did not answer in time")?
.map_err(|e| anyhow!("mediator websocket: {e}"))?;
let transport: Arc<dyn MessageTransport> = Arc::new(
DidCommTransport::new((**atm).clone(), profile.clone())
.await
.map_err(|e| anyhow!("messaging transport: {e}"))?,
);
let outbox: Arc<dyn OutboxStore> = Arc::new(InMemoryOutboxStore::new());
Ok((profile, Arc::new(MessagingService::new(transport, outbox))))
}
pub async fn preferred(&self, peer: &str) -> Result<Via> {
let resolved = self
.tdk
.did_resolver()
.resolve(peer)
.await
.map_err(|e| anyhow!("resolving `{peer}`: {e}"))?;
let doc = serde_json::to_value(&resolved.doc)
.map_err(|e| anyhow!("reading `{peer}`'s DID document: {e}"))?;
Ok(preferred_via(&ServiceCapabilities::from_did_document(&doc)))
}
pub async fn relate(&self, peer: &str) -> Result<()> {
if self.preferred(peer).await? != Via::Tsp {
return Ok(());
}
let tsp = self.atm.tsp();
if tsp
.send_readiness(&self.profile, peer)
.await
.map_err(|e| anyhow!("TSP relationship with `{peer}`: {e}"))?
== SendReadiness::Reestablish
{
tsp.form_relationship_routed(&self.profile, peer)
.await
.map_err(|e| anyhow!("TSP relationship invite to `{peer}`: {e}"))?;
}
Ok(())
}
pub async fn has_relationship(&self, peer: &str) -> Result<bool> {
Ok(self
.atm
.tsp()
.send_readiness(&self.profile, peer)
.await
.map_err(|e| anyhow!("TSP relationship with `{peer}`: {e}"))?
!= SendReadiness::Reestablish)
}
pub async fn shutdown(&self) {
let _ = self.atm.profile_remove(&self.did).await;
self.atm.graceful_shutdown().await;
}
async fn send_didcomm(&self, to: &str, doc: &Value) -> Result<()> {
let thread = doc
.get("threadId")
.and_then(Value::as_str)
.map(str::to_string);
let mut msg = Message::build(new_id(), ENVELOPE_TYPE.to_string(), doc.clone())
.from(self.did.clone())
.to(to.to_string());
if let Some(t) = thread {
msg = msg.thid(t);
}
let msg = msg.finalize();
let (packed, _) = self
.atm
.pack_encrypted(&msg, to, Some(&self.did), Some(&self.did))
.await
.map_err(|e| anyhow!("packing for `{to}`: {e}"))?;
self.service
.send(to, packed.into_bytes(), Delivery::BestEffort)
.await
.map_err(|e| anyhow!("sending to `{to}`: {e}"))?;
Ok(())
}
async fn send_tsp(&self, to: &str, doc: &Value) -> Result<()> {
let document =
serde_json::to_vec(doc).map_err(|e| anyhow!("serialising the document: {e}"))?;
let body = vta_sdk::tsp_binding::wrap_envelope(&document);
let tsp = self.atm.tsp();
let peer_mediator = tsp
.peer_mediator(&self.profile, to)
.await
.map_err(|e| anyhow!("finding `{to}`'s TSP mediator: {e}"))?
.filter(|m| *m != self.mediator_did);
let sent = match peer_mediator {
None => {
tsp.send_reestablishing(
&self.profile,
to,
&[self.mediator_did.clone(), to.to_string()],
&body,
)
.await
}
Some(peer_mediator) => {
let invite = async {
if tsp.send_readiness(&self.profile, to).await? == SendReadiness::Reestablish
&& let Err(e) = tsp.form_relationship_routed(&self.profile, to).await
&& !invite_refusal_is_benign(tsp.send_readiness(&self.profile, to).await?)
{
return Err(e);
}
Ok(())
};
match invite.await {
Ok(()) => {
tsp.send_nested_routed(
&self.profile,
&[self.mediator_did.clone(), peer_mediator],
to,
&body,
)
.await
}
Err(e) => Err(e),
}
}
};
sent.map_err(|e| anyhow!("sending to `{to}` over TSP: {e}"))
}
}
#[async_trait]
impl VtcLink for MediatorLink {
async fn send(&self, to: &str, doc: &Value) -> Result<()> {
let answered = doc
.get("threadId")
.and_then(Value::as_str)
.and_then(|t| self.arrivals.of(t));
let via = match answered {
Some(via) => via,
None => self.preferred(to).await?,
};
self.send_via(to, doc, via).await
}
async fn send_via(&self, to: &str, doc: &Value, via: Via) -> Result<()> {
match via {
Via::Tsp => self.send_tsp(to, doc).await,
Via::Didcomm => self.send_didcomm(to, doc).await,
}
}
}
fn inbound_stream(
stream: BoxStream<'static, affinidi_messaging_core::Inbound>,
ctx: Arc<InboundCtx>,
) -> BoxStream<'static, InboundDoc> {
stream
.filter_map(move |inbound| {
let ctx = Arc::clone(&ctx);
async move {
let doc = match inbound.message.protocol {
Protocol::DIDComm => didcomm_doc(&inbound),
Protocol::TSP => tsp_doc(&ctx, inbound).await,
other => {
tracing::warn!(protocol = ?other, "dropping an inbound message in a protocol this bridge does not speak");
None
}
}?;
let from_vtc = doc
.authenticated_sender
.as_deref()
.is_some_and(|s| did_part(s) == ctx.vtc_did);
if from_vtc && let Some(id) = doc.doc.get("id").and_then(Value::as_str) {
ctx.arrivals.record(id, doc.via);
}
Some(doc)
}
})
.boxed()
}
fn didcomm_doc(inbound: &affinidi_messaging_core::Inbound) -> Option<InboundDoc> {
let m = &inbound.message;
let sender = m.sender.clone().filter(|_| m.verified);
let msg: Message = match serde_json::from_slice(&m.payload) {
Ok(msg) => msg,
Err(e) => {
tracing::warn!(error = %e, "dropping an inbound message that is not DIDComm");
return None;
}
};
if msg.typ != ENVELOPE_TYPE {
tracing::debug!(r#type = %msg.typ, "ignoring a DIDComm message that is not a Trust Task");
return None;
}
Some(InboundDoc {
doc: msg.body,
authenticated_sender: sender,
via: Via::Didcomm,
})
}
fn tsp_sender(m: &affinidi_messaging_core::ReceivedMessage) -> Option<String> {
m.sender.clone().filter(|_| m.verified)
}
async fn tsp_doc(
ctx: &InboundCtx,
inbound: affinidi_messaging_core::Inbound,
) -> Option<InboundDoc> {
let Some(sender) = tsp_sender(&inbound.message) else {
tracing::warn!("dropping a TSP frame with no verified sender");
return None;
};
match inbound.kind {
InboundKind::Application => {}
InboundKind::RelationshipControl {
request,
thread_digest,
reply_expected,
..
} => {
answer_control(ctx, &sender, request, thread_digest, reply_expected).await;
return None;
}
_ => {
tracing::debug!(%sender, "ignoring a TSP frame that is not application data");
return None;
}
}
if !ctx.is_peer(&sender) {
tracing::warn!(%sender, "dropping a TSP message from a DID this bridge does not serve");
return None;
}
let document = match vta_sdk::tsp_binding::open_envelope(&inbound.message.payload) {
Ok(d) => d,
Err(e) => {
tracing::warn!(%sender, error = %e, "dropping a TSP message outside the Trust Task binding");
return None;
}
};
let doc: Value = match serde_json::from_slice(&document) {
Ok(d) => d,
Err(e) => {
tracing::warn!(%sender, error = %e, "dropping a TSP Trust Task that is not JSON");
return None;
}
};
Some(InboundDoc {
doc,
authenticated_sender: Some(sender),
via: Via::Tsp,
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ControlDecision {
Accept,
AnswerCancel,
Nothing,
}
fn decide_control(
request: RelationshipRequest,
reply_expected: bool,
from_peer: bool,
) -> ControlDecision {
match request {
RelationshipRequest::Invite if from_peer => ControlDecision::Accept,
RelationshipRequest::Cancel if reply_expected => ControlDecision::AnswerCancel,
_ => ControlDecision::Nothing,
}
}
async fn answer_control(
ctx: &InboundCtx,
sender: &str,
request: RelationshipRequest,
thread_digest: [u8; 32],
reply_expected: bool,
) {
let from_peer = ctx.is_peer(sender);
let tsp = ctx.atm.tsp();
match decide_control(request, reply_expected, from_peer) {
ControlDecision::Accept => {
match tsp
.accept_relationship(&ctx.profile, sender, thread_digest)
.await
{
Ok(_) => tracing::info!(%sender, "accepted a TSP relationship"),
Err(e) => {
tracing::warn!(%sender, error = %e, "could not accept a TSP relationship")
}
}
}
ControlDecision::AnswerCancel => {
if let Err(e) = tsp
.answer_cancellation(&ctx.profile, sender, thread_digest)
.await
{
tracing::warn!(%sender, error = %e, "could not answer a TSP relationship cancellation");
}
}
ControlDecision::Nothing => {
if request == RelationshipRequest::Invite && !from_peer {
tracing::warn!(%sender, "refusing a TSP relationship invite from a DID this bridge does not serve");
if let Err(e) = tsp.reset_relationship(&ctx.profile, sender).await {
tracing::warn!(%sender, error = %e, "could not forget a stranger's TSP relationship invite");
}
}
}
}
}
#[derive(Default)]
pub struct SupervisedLink {
current: RwLock<Option<Arc<dyn VtcLink>>>,
}
impl SupervisedLink {
pub fn new() -> Self {
SupervisedLink::default()
}
pub async fn set(&self, link: Option<Arc<dyn VtcLink>>) {
*self.current.write().await = link;
}
}
#[async_trait]
impl VtcLink for SupervisedLink {
async fn send(&self, to: &str, doc: &Value) -> Result<()> {
let link = self.current.read().await.clone();
match link {
Some(l) => l.send(to, doc).await,
None => Err(anyhow!("not connected to the mediator")),
}
}
async fn send_via(&self, to: &str, doc: &Value, via: Via) -> Result<()> {
let link = self.current.read().await.clone();
match link {
Some(l) => l.send_via(to, doc, via).await,
None => Err(anyhow!("not connected to the mediator")),
}
}
}
pub mod memory {
use super::*;
use tokio::sync::mpsc;
pub struct ChannelLink {
tx: mpsc::UnboundedSender<(String, Value)>,
pinned: std::sync::Mutex<Vec<Via>>,
}
impl ChannelLink {
pub fn new() -> (Self, mpsc::UnboundedReceiver<(String, Value)>) {
let (tx, rx) = mpsc::unbounded_channel();
(
ChannelLink {
tx,
pinned: Default::default(),
},
rx,
)
}
pub fn pinned(&self) -> Vec<Via> {
self.pinned
.lock()
.unwrap_or_else(|p| p.into_inner())
.clone()
}
}
#[async_trait]
impl VtcLink for ChannelLink {
async fn send(&self, to: &str, doc: &Value) -> Result<()> {
self.tx
.send((to.to_string(), doc.clone()))
.map_err(|_| anyhow!("the peer is gone"))
}
async fn send_via(&self, to: &str, doc: &Value, via: Via) -> Result<()> {
self.pinned
.lock()
.unwrap_or_else(|p| p.into_inner())
.push(via);
self.send(to, doc).await
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn caps(tsp: bool, didcomm: bool) -> ServiceCapabilities {
ServiceCapabilities {
tsp: tsp.then(|| "did:web:mediator.example".to_string()),
didcomm: didcomm.then(|| "did:web:mediator.example".to_string()),
rest: None,
}
}
#[test]
fn tsp_is_preferred_over_didcomm() {
assert_eq!(preferred_via(&caps(true, true)), Via::Tsp);
assert_eq!(preferred_via(&caps(true, false)), Via::Tsp);
assert_eq!(preferred_via(&caps(false, true)), Via::Didcomm);
assert_eq!(preferred_via(&caps(false, false)), Via::Didcomm);
}
#[test]
fn only_a_served_peer_gets_a_relationship() {
assert_eq!(
decide_control(RelationshipRequest::Invite, false, true),
ControlDecision::Accept
);
assert_eq!(
decide_control(RelationshipRequest::Invite, false, false),
ControlDecision::Nothing
);
assert_eq!(
decide_control(RelationshipRequest::Cancel, true, false),
ControlDecision::AnswerCancel
);
assert_eq!(
decide_control(RelationshipRequest::Cancel, false, true),
ControlDecision::Nothing
);
assert_eq!(
decide_control(RelationshipRequest::Accept, false, true),
ControlDecision::Nothing
);
}
#[test]
fn an_unverified_tsp_sender_is_no_sender() {
let mut m = affinidi_messaging_core::ReceivedMessage {
id: "x".into(),
sender: Some("did:example:vtc".into()),
recipient: "did:example:bridge".into(),
payload: vec![],
protocol: Protocol::TSP,
verified: false,
encrypted: true,
};
assert_eq!(tsp_sender(&m), None);
m.verified = true;
assert_eq!(tsp_sender(&m).as_deref(), Some("did:example:vtc"));
m.sender = None;
assert_eq!(tsp_sender(&m), None);
}
#[test]
fn a_response_goes_back_the_way_its_request_came() {
let a = Arrivals::default();
a.record("urn:uuid:job", Via::Tsp);
a.record("urn:uuid:other", Via::Didcomm);
assert_eq!(a.of("urn:uuid:job"), Some(Via::Tsp));
assert_eq!(a.of("urn:uuid:other"), Some(Via::Didcomm));
assert_eq!(a.of("urn:uuid:unknown"), None);
let long = format!("urn:uuid:{}", "x".repeat(ARRIVAL_ID_MAX));
a.record(&long, Via::Tsp);
assert_eq!(a.of(&long), None, "an over-long id is not remembered");
for i in 0..ARRIVALS_KEPT {
a.record(&format!("urn:uuid:{i}"), Via::Didcomm);
}
assert_eq!(a.of("urn:uuid:job"), None, "the oldest are forgotten");
}
}