1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
/*!
* The TSP send leg for VTC-facing ceremonies (#185 item 2f).
*
* ## Carriage
*
* A Trust Task goes out in the TSP binding envelope
* (`{"type": ".../binding/tsp/0.1/envelope", "document": …}`), applied by
* `vta_sdk::tsp_binding::wrap_envelope` — the same function `vta-service` and
* the VTI SDK's own sessions use, so there is one implementation of the
* binding rather than one per sender. See [`send_trust_task`].
*
* ## Why this is send-only
*
* Inbound TSP is **already arriving**. `DidCommTransport` owns the one mediator
* websocket and surfaces both protocols off it, unpacking each TSP frame with
* `atm.tsp().unpack` — which authenticates the sender VID — and tagging it
* `Protocol::TSP`. So receiving needed no new transport, only routing: see
* `didcomm::tsp_frame_to_message`, where those frames used to be dropped.
*
* What was missing is the other direction. `ATM::forward_and_send_message` — the
* send half of [`crate::pack_and_send`] — builds a DIDComm `routing/2.0` forward
* unconditionally, so it cannot carry a Trust Task document as TSP. This module
* is that one missing call and nothing more.
*
* ## The peer is not on our mediator
*
* A community is addressed by the mediator *it* advertises, which is only our
* own in a single-mediator deployment. Everywhere else the send is a federated
* one: we post to our mediator, it forwards to theirs, theirs delivers. That is
* carried entirely by the hop list — see `hops` (not an intra-doc link: it is
* private) — and it is the part this module got wrong for as long as every
* deployment shared one mediator.
*
* Send-only is also why there is **no second websocket**. The mediator permits
* one channel per DID and evicts a second as `duplicate-channel`; a TSP send is
* an HTTP post through the mediator, holding no socket of its own, so it cannot
* duel with the DIDComm one. Same shape as the VTA leg
* (`enable_tsp_trust_tasks`) and for the same reason.
*
* ## Why this is not a `MessageTransport`
*
* The delivery layer would take one — `MessagingService` holds any number of
* transports over a single outbox, which is what `OutboxEntry::via` exists for —
* and an earlier draft of this change registered a TSP leg that way.
*
* It was the wrong fit *here*. The ceremony sends do not go through the delivery
* layer at all: [`crate::pack_and_send`] posts straight through the ATM, so a
* TSP leg on the outbox would have given TSP joins durability that DIDComm joins
* do not have, and would have meant threading a `Messaging` handle down into
* `join_flow.rs`. Matching the transport this change is *about* — same call
* shape, same failure semantics, only the wire differs — keeps the swap
* reviewable and the two paths comparable.
*
* Putting the ceremony sends behind the outbox is worth doing, but for **both**
* transports at once and as its own change; doing it here would have hidden a
* durability change inside a transport change.
*/
use Arc;
use RelationshipState;
use ATM;
use ATMProfile;
use Value;
use crateOpenVTCError;
/// Build the TSP hop list for a send from `our_mediator` to `to_did`, whose
/// advertised TSP mediator is `their_mediator`.
///
/// `route[0]` must be **our own** mediator. `send_routed` posts the frame to the
/// profile's mediator `/inbound`, and that mediator has to be the routing
/// layer's receiver in order to hold the key that unwraps it. A hop list opening
/// with the peer's mediator arrives at ours as an envelope addressed to someone
/// else, which it can only take as an opaque message for one of its own
/// accounts — so a cross-mediator send died at the *first* hop with
/// `404 e.p.direct_delivery.recipient.unknown`, "TSP recipient is not local to
/// this mediator (remote forwarding not yet enabled)". Nothing was wrong with
/// the peer's mediator; ours was never asked to forward.
///
/// Naming ours first is what turns the send into a forward: our mediator unwraps
/// its layer, sees a next hop that is not one of its accounts, resolves that
/// hop's `TSPTransport` endpoint and POSTs onward. The peer's mediator is then
/// the routing-layer receiver of the frame it actually gets, and delivers the
/// end-to-end-sealed inner to `to_did` locally.
///
/// When the two mediators coincide — every single-mediator deployment, which is
/// why this held up for so long — the list stays two hops: ours already *is* the
/// peer's, and naming it twice would ask it to forward to itself
/// (`protocol.forwarding.loop_detected`).
/// Seal a Trust Task `document` to `to_did` and route it through that peer's
/// advertised TSP mediator.
///
/// The TSP counterpart of [`crate::pack_and_send`], and deliberately the same
/// signature shape so the two are directly comparable at a call site.
///
/// `tsp_mediator_did` is the peer's **advertised** `#tsp` mediator — the hop that
/// hands the document to the peer — not ours. That is the addressing information
/// the peer published for exactly this purpose (`discover_tsp_mediator`). It is
/// the *last* mediator on the route rather than the first: the private `hops`
/// explains why the route has to open with our own mediator, and what it cost
/// when it did not.
///
/// Unlike the DIDComm path the caller does **not** pack: `send_routed` seals the
/// payload end-to-end to `route.last()` and wraps that in a routing layer sealed
/// to `route[0]`, so the document goes in as plaintext JSON.
///
/// # Errors
///
/// Returns [`OpenVTCError`] if the profile cannot name the mediator the route is
/// built from, if the document will not serialise, or if the mediator did not
/// accept the frame. The message names the peer, the routing hop, **and the
/// mediator the frame was actually posted to** (R6.4), so an operator can tell a
/// wrong advertised mediator from a refused send from an unreachable hop.
///
/// Naming all three is not belt-and-braces. `send_routed` posts to the
/// *profile's* mediator — the onward hops are sealed into the frame, not dialled
/// — so a rejection quoting only the advertised mediator sends an operator to
/// inspect a host that never received the request. That happened twice: a
/// mediator built without its `tsp` feature answered `400
/// w.m.message.deserialize` (it fed the CESR frame to the DIDComm JSON parser),
/// and our own mediator answered `404 direct_delivery.recipient.unknown` for a
/// hop list that did not start with it — both under an error naming the peer's
/// perfectly healthy TSP mediator.
pub async