Skip to main content

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}