use affinidi_messaging_core::RelationshipRequest;
use tracing::info;
use crate::messaging::auth::auth_for_trust_task_envelope;
use crate::server::AppState;
use vta_sdk::tsp_binding::{open_envelope, wrap_envelope};
pub async fn dispatch_one(app_state: &AppState, payload: &[u8], sender_vid: &str) -> Vec<u8> {
app_state.tsp_reach.record(sender_vid);
tracing::debug!(sender = %sender_vid, "recorded TSP reachability (learn-from-inbound)");
let document = match open_envelope(payload) {
Ok(d) => d,
Err(reason) => {
info!(sender = %sender_vid, %reason, "refused a TSP frame that is not a binding envelope");
return wrap_envelope(&crate::trust_tasks::malformed_request_response(reason).body);
}
};
let payload = document.as_slice();
let outcome = match auth_for_trust_task_envelope(app_state, sender_vid, payload).await {
Ok(auth) => {
crate::trust_tasks::dispatch_trust_task_core(
app_state,
&auth,
payload,
crate::trust_tasks::transport::TransportConfidentiality::EndToEnd,
)
.await
}
Err(e) => crate::trust_tasks::reject_trust_task(
payload,
trust_tasks_rs::RejectReason::PermissionDenied {
reason: e.to_string(),
},
),
};
info!(
sender = %sender_vid,
status = %outcome.status,
"TSP trust-task dispatched"
);
wrap_envelope(&outcome.body)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ControlDecision {
Accept,
Cancel(&'static str),
Nothing,
}
pub fn decide_control(request: RelationshipRequest, reply_expected: bool) -> ControlDecision {
match request {
RelationshipRequest::Invite => ControlDecision::Accept,
RelationshipRequest::Accept => ControlDecision::Nothing,
RelationshipRequest::Cancel => {
if reply_expected {
ControlDecision::Cancel("the peer cancelled a mutual relationship (§7.3)")
} else {
ControlDecision::Nothing
}
}
_ => ControlDecision::Nothing,
}
}
#[cfg(test)]
mod tests {
use super::{ControlDecision, decide_control};
use affinidi_messaging_core::RelationshipRequest;
#[test]
fn an_invite_is_accepted_and_the_acl_gate_stays_at_the_task_layer() {
assert_eq!(
decide_control(RelationshipRequest::Invite, false),
ControlDecision::Accept,
"refusing here drops the peer's first task and answers it with nothing"
);
}
#[test]
fn an_accept_is_not_answered() {
assert_eq!(
decide_control(RelationshipRequest::Accept, false),
ControlDecision::Nothing
);
}
#[test]
fn a_cancellation_is_answered_only_when_the_relationship_was_mutual() {
assert_eq!(
decide_control(RelationshipRequest::Cancel, false),
ControlDecision::Nothing
);
assert!(matches!(
decide_control(RelationshipRequest::Cancel, true),
ControlDecision::Cancel(_)
));
}
use super::*;
use crate::acl::{AclEntry, Role, store_acl_entry};
use crate::test_support::build_signing_test_app_state;
fn framed(document: &str) -> Vec<u8> {
wrap_envelope(document.as_bytes())
}
fn document_of(reply: &[u8]) -> serde_json::Value {
let envelope: serde_json::Value = serde_json::from_slice(reply).expect("reply is JSON");
assert_eq!(
envelope.get("type").and_then(|t| t.as_str()),
Some(trust_tasks_tsp::ENVELOPE_TYPE),
"a reply must be sealed in the binding envelope it arrived in: {envelope}"
);
envelope
.get("document")
.cloned()
.expect("the envelope carries a document")
}
#[tokio::test]
async fn dispatch_one_unknown_sender_replies_with_error_envelope() {
let (app_state, _dir) = build_signing_test_app_state().await;
let body = dispatch_one(&app_state, &framed("{}"), "did:key:zUnauthorizedTspSender").await;
assert!(
!body.is_empty(),
"unauthorized sender must get a reply envelope"
);
let doc = document_of(&body);
assert!(
doc.get("type").is_some() && doc.get("payload").is_some(),
"reply should be a trust-task error document, got: {doc}"
);
}
#[tokio::test]
async fn dispatch_one_authorized_sender_returns_reply_envelope() {
let (app_state, _dir) = build_signing_test_app_state().await;
let did = "did:key:zAuthorizedTspSender";
store_acl_entry(&app_state.acl_ks, &AclEntry::new(did, Role::Admin, "test"))
.await
.unwrap();
let body = dispatch_one(&app_state, &framed("{}"), did).await;
assert!(
!body.is_empty(),
"authorized sender must get a reply envelope"
);
document_of(&body);
}
#[tokio::test]
async fn dispatch_one_records_sender_as_tsp_reachable() {
let (app_state, _dir) = build_signing_test_app_state().await;
let did = "did:key:zTspDevice";
assert!(
!app_state.tsp_reach.fresh(did),
"a DID we've never seen over TSP is not reachable"
);
let _ = dispatch_one(&app_state, &framed("{}"), did).await;
assert!(
app_state.tsp_reach.fresh(did),
"an inbound TSP frame must mark its proven sender TSP-reachable"
);
}
#[tokio::test]
async fn a_bare_document_is_refused_as_carriage() {
let (app_state, _dir) = build_signing_test_app_state().await;
let bare = br#"{"id":"urn:uuid:1","type":"https://example.org/t","issuedAt":"2026-01-01T00:00:00Z","payload":{}}"#;
let reply = dispatch_one(&app_state, bare, "did:key:zLegacySender").await;
let doc = document_of(&reply);
assert_eq!(doc["payload"]["code"], "malformedRequest");
}
#[tokio::test]
async fn the_refusal_names_the_carriage_not_the_document() {
let (app_state, _dir) = build_signing_test_app_state().await;
let bare = br#"{"id":"urn:uuid:1","type":"https://example.org/t","issuedAt":"2026-01-01T00:00:00Z","payload":{}}"#;
let reply = dispatch_one(&app_state, bare, "did:key:zLegacySender").await;
let message = document_of(&reply)["payload"]["message"]
.as_str()
.unwrap_or_default()
.to_string();
assert!(
message.contains("envelope"),
"the refusal must name the envelope, not the document: {message}"
);
assert!(
!message.contains("did not parse as a Trust Task document"),
"this points the sender at a document that is perfectly well formed: {message}"
);
}
#[tokio::test]
async fn an_envelope_of_the_wrong_type_is_refused() {
let (app_state, _dir) = build_signing_test_app_state().await;
let wrong =
br#"{"type":"https://trusttasks.org/binding/didcomm/0.1/envelope","document":{}}"#;
let reply = dispatch_one(&app_state, wrong, "did:key:zConfusedSender").await;
let message = document_of(&reply)["payload"]["message"]
.as_str()
.unwrap_or_default()
.to_string();
assert!(
message.contains("binding/didcomm"),
"the refusal must name what arrived, so a misconfigured peer can see it: {message}"
);
}
}