did_hosting_server/tsp.rs
1//! TSP receive path for `did-hosting-server`.
2//!
3//! The messaging-service framework unpacks each TSP frame off the shared
4//! mediator socket, authenticates the sender VID, and hands us the cleartext
5//! payload. **Two payload shapes arrive here**, and we sniff between them:
6//!
7//! 1. A **trust-task document** (`TrustTask<Value>`) — health ping, register
8//! ack. Dispatched through [`crate::trust_tasks_infra`], the same entry the
9//! DIDComm envelope route uses. The response is *returned*, so the framework
10//! seals it back to the sender over TSP: a ping delivered here is ponged
11//! here.
12//! 2. A serialised DIDComm [`Message`] — the control plane's outbox sends
13//! sync/domain pushes this way (`control/src/outbox.rs`). Routed to the same
14//! `do_*` cores the DIDComm listener uses via
15//! [`crate::messaging::dispatch_tsp_message`]. Fire-and-forget: the outbox
16//! treats a successful send as delivery, so no ack is routed back.
17//!
18//! Sniffing rather than switching conventions is deliberate. The outbox on a
19//! *deployed* control plane already ships DIDComm `Message` bytes over TSP; a
20//! server that stopped accepting them would break every rolling upgrade.
21//!
22//! The two shapes are unambiguous, but **not** for the obvious reason. A
23//! `Message` also carries top-level `id` and `type`, and its `type` (e.g.
24//! `MSG_SYNC_UPDATE`) is itself a canonical Type URI, so both fields parse.
25//! What separates them is `payload`: `TrustTask` requires it and has no serde
26//! default, while a `Message` carries `body` instead. So a `Message` can never
27//! deserialise as a `TrustTask`, and trust tasks may safely be tried first.
28//! `didcomm_message_never_parses_as_a_trust_task` pins that — if it ever
29//! stopped holding, every sync/domain push over TSP would be silently swallowed
30//! by the `owns()` gate below.
31
32use affinidi_messaging_didcomm::Message;
33use affinidi_messaging_didcomm_service::{
34 DIDCommServiceError, HandlerContext, TspHandler, TspResponse,
35};
36use async_trait::async_trait;
37use serde_json::Value;
38use tracing::{debug, warn};
39
40use crate::messaging::dispatch_tsp_message;
41use crate::server::AppState;
42
43/// messaging-service [`TspHandler`] that applies inbound sync/domain
44/// messages delivered over TSP.
45pub struct ServerTspHandler {
46 state: AppState,
47}
48
49impl ServerTspHandler {
50 pub fn new(state: AppState) -> Self {
51 Self { state }
52 }
53}
54
55#[async_trait]
56impl TspHandler for ServerTspHandler {
57 async fn handle(
58 &self,
59 _ctx: HandlerContext,
60 payload: Vec<u8>,
61 sender_vid: String,
62 ) -> Result<Option<TspResponse>, DIDCommServiceError> {
63 // Shape 1: a trust-task document. Tried first — see the module note.
64 if let Ok(doc) = serde_json::from_slice::<trust_tasks_rs::TrustTask<Value>>(&payload) {
65 let type_uri = doc.type_uri.to_string();
66 if crate::trust_tasks_infra::owns(&type_uri) {
67 // Infra trust tasks are the periodic health ping (~every 60s)
68 // and the register ack — routine liveness traffic, not events.
69 // Keep the receipt at debug; the meaningful outcomes (server
70 // registered, health status changes) log at info elsewhere.
71 debug!(sender = %sender_vid, %type_uri, "inbound TSP: trust task");
72 return Ok(
73 match crate::trust_tasks_infra::dispatch(&self.state, &sender_vid, doc).await {
74 Some(resp) => Some(TspResponse::new(
75 serde_json::to_vec(&resp)
76 .map_err(|e| DIDCommServiceError::Internal(e.to_string()))?,
77 )),
78 None => None,
79 },
80 );
81 }
82 warn!(
83 sender = %sender_vid,
84 %type_uri,
85 "inbound TSP: trust task of a type this server does not implement"
86 );
87 return Ok(None);
88 }
89
90 // Shape 2: a serialised DIDComm Message from the control plane's outbox.
91 let msg: Message = match serde_json::from_slice(&payload) {
92 Ok(m) => m,
93 Err(e) => {
94 warn!(
95 sender = %sender_vid,
96 error = %e,
97 "TSP: payload is neither a trust task nor a DIDComm Message"
98 );
99 return Ok(None);
100 }
101 };
102 // Transport receipt — one per synced DID. The applied-update outcome
103 // logs at info in `control_register::apply_single_update`; keep this at
104 // debug so a bulk sync doesn't spam.
105 debug!(sender = %sender_vid, msg_type = %msg.typ, "inbound TSP: server sync/domain message");
106 // Apply via the shared `do_*` cores (which authorise the sender as
107 // the control plane). Fire-and-forget: the ack is dropped, mirroring
108 // the outbox's send-success-is-delivery model.
109 let _ = dispatch_tsp_message(&self.state, &sender_vid, &msg).await;
110 Ok(None)
111 }
112}
113
114#[cfg(test)]
115mod tests {
116 use super::*;
117 use did_hosting_common::didcomm_types::{MSG_HEALTH_PING, MSG_SYNC_UPDATE};
118 use serde_json::json;
119
120 /// The load-bearing assumption of the payload sniff in `handle`.
121 ///
122 /// A DIDComm `Message` carries `id` and `type` just like a trust task, and
123 /// `MSG_SYNC_UPDATE` is a canonical Type URI — so neither field
124 /// discriminates. Only `payload` does: `TrustTask` requires it, `Message`
125 /// has `body` instead.
126 ///
127 /// If this ever passed, the `owns()` gate would reject the misparsed sync
128 /// message and `handle` would return `Ok(None)` — silently discarding every
129 /// DID sync and domain push the control plane sends over TSP.
130 #[test]
131 fn didcomm_message_never_parses_as_a_trust_task() {
132 let msg = Message::build(
133 "msg-1".to_string(),
134 MSG_SYNC_UPDATE.to_string(),
135 json!({ "mnemonic": "alice", "log_content": "..." }),
136 )
137 .finalize();
138 let bytes = serde_json::to_vec(&msg).expect("message serialises");
139
140 let parsed = serde_json::from_slice::<trust_tasks_rs::TrustTask<Value>>(&bytes);
141 assert!(
142 parsed.is_err(),
143 "a DIDComm Message must not deserialise as a TrustTask — the TSP \
144 sniff depends on it; got {parsed:?}"
145 );
146 }
147
148 /// And the converse, so the fall-through can't misfire either.
149 #[test]
150 fn trust_task_never_parses_as_a_didcomm_message() {
151 let doc = did_hosting_common::server::trust_tasks::send::build_request(
152 MSG_HEALTH_PING,
153 "did:example:control",
154 "did:example:server",
155 json!({}),
156 )
157 .expect("build request");
158 let bytes = serde_json::to_vec(&doc).expect("doc serialises");
159
160 assert!(
161 serde_json::from_slice::<Message>(&bytes).is_err(),
162 "a TrustTask must not deserialise as a DIDComm Message"
163 );
164 }
165
166 /// The infra dispatcher owns exactly the two ops the server implements —
167 /// and must not claim the sync/domain types, which travel as `Message`s.
168 #[test]
169 fn infra_owns_only_health_ping_and_register_ack() {
170 use crate::trust_tasks_infra::owns;
171 use did_hosting_common::didcomm_types::{MSG_DOMAIN_ASSIGN, MSG_SERVER_REGISTER_ACK};
172
173 assert!(owns(MSG_HEALTH_PING));
174 assert!(owns(MSG_SERVER_REGISTER_ACK));
175 assert!(!owns(MSG_SYNC_UPDATE));
176 assert!(!owns(MSG_DOMAIN_ASSIGN));
177 }
178}