use affinidi_messaging_core::RelationshipRequest;
use tracing::info;
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 = crate::trust_tasks::accept_from_proven_sender(
app_state,
sender_vid,
payload,
crate::trust_tasks::transport::TransportConfidentiality::EndToEnd,
)
.await;
info!(
sender = %sender_vid,
status = %outcome.status,
"TSP trust-task dispatched"
);
if outcome.body.is_empty() {
return Vec::new();
}
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 a_reply_reaches_its_waiter_without_the_sender_needing_acl_standing() {
let (app_state, _dir) = build_signing_test_app_state().await;
const THREAD: &str = "urn:uuid:11111111-1111-1111-1111-111111111111";
let mut waiting = app_state.pending_replies.register(THREAD);
let response = serde_json::json!({
"id": "urn:uuid:22222222-2222-2222-2222-222222222222",
"threadId": THREAD,
"type": "https://trusttasks.org/spec/did-management/did/problem-report/0.1",
"payload": {},
})
.to_string();
let body = dispatch_one(
&app_state,
&framed(&response),
"did:webvh:zHostingServerWithNoAclEntry",
)
.await;
assert!(
body.is_empty(),
"a reply is delivered to its waiter, never answered — got: {}",
String::from_utf8_lossy(&body)
);
assert!(
waiting.try_recv().is_ok(),
"the waiting request must receive the answer it asked for"
);
}
#[tokio::test]
async fn a_threaded_document_with_no_waiter_is_still_dispatched() {
let (app_state, _dir) = build_signing_test_app_state().await;
let threaded = serde_json::json!({
"id": "urn:uuid:44444444-4444-4444-4444-444444444444",
"threadId": "urn:uuid:nobody-is-waiting-on-this",
"type": "https://trusttasks.org/spec/keys/create/0.1",
"payload": {},
})
.to_string();
let body = dispatch_one(&app_state, &framed(&threaded), "did:key:zSomeApprover").await;
assert!(
!body.is_empty(),
"a threaded document nobody is waiting for is an ordinary request \
and must still be answered"
);
}
#[tokio::test]
async fn an_inbound_error_is_terminal_and_is_never_answered() {
let (app_state, _dir) = build_signing_test_app_state().await;
let error_doc = serde_json::json!({
"id": "urn:uuid:33333333-3333-3333-3333-333333333333",
"threadId": "urn:uuid:11111111-1111-1111-1111-111111111111",
"type": "https://trusttasks.org/spec/trust-task-error/0.5",
"payload": {"code": "e.p.did.validation-error", "message": "nope"},
})
.to_string();
let body = dispatch_one(&app_state, &framed(&error_doc), "did:key:zAnyPeer").await;
assert!(
body.is_empty(),
"answering an error is the loop — got: {}",
String::from_utf8_lossy(&body)
);
}
#[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}"
);
}
}