tap_msg/message/
connection.rs

1//! Connection types for TAP messages.
2//!
3//! This module defines the structure of connection messages and related types
4//! used in the Transaction Authorization Protocol (TAP).
5
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8
9use crate::didcomm::PlainMessage;
10use crate::error::{Error, Result};
11use crate::impl_tap_message;
12use crate::message::tap_message_trait::{Connectable, TapMessageBody};
13use chrono::Utc;
14
15/// Transaction limits for connection constraints.
16#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct TransactionLimits {
18    /// Maximum amount for a transaction.
19    pub max_amount: Option<String>,
20
21    /// Maximum total amount for all transactions.
22    pub max_total_amount: Option<String>,
23
24    /// Maximum number of transactions allowed.
25    pub max_transactions: Option<u64>,
26}
27
28/// Connection constraints for the Connect message.
29#[derive(Debug, Clone, Serialize, Deserialize)]
30pub struct ConnectionConstraints {
31    /// Limit on transaction amount.
32    pub transaction_limits: Option<TransactionLimits>,
33}
34
35/// Connect message body (TAIP-2).
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct Connect {
38    /// Transaction ID.
39    pub transaction_id: String,
40
41    /// Agent DID.
42    pub agent_id: String,
43
44    /// The entity this connection is for.
45    #[serde(rename = "for")]
46    pub for_: String,
47
48    /// The role of the agent (optional).
49    #[serde(skip_serializing_if = "Option::is_none")]
50    pub role: Option<String>,
51
52    /// Connection constraints (optional).
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub constraints: Option<ConnectionConstraints>,
55}
56
57impl Connect {
58    /// Create a new Connect message.
59    pub fn new(transaction_id: &str, agent_id: &str, for_id: &str, role: Option<&str>) -> Self {
60        Self {
61            transaction_id: transaction_id.to_string(),
62            agent_id: agent_id.to_string(),
63            for_: for_id.to_string(),
64            role: role.map(|s| s.to_string()),
65            constraints: None,
66        }
67    }
68
69    /// Add constraints to the Connect message.
70    pub fn with_constraints(mut self, constraints: ConnectionConstraints) -> Self {
71        self.constraints = Some(constraints);
72        self
73    }
74}
75
76impl Connectable for Connect {
77    fn with_connection(&mut self, _connect_id: &str) -> &mut Self {
78        // Connect messages don't have a connection ID
79        self
80    }
81
82    fn has_connection(&self) -> bool {
83        false
84    }
85
86    fn connection_id(&self) -> Option<&str> {
87        None
88    }
89}
90
91impl TapMessageBody for Connect {
92    fn message_type() -> &'static str {
93        "https://tap.rsvp/schema/1.0#connect"
94    }
95
96    fn validate(&self) -> Result<()> {
97        if self.transaction_id.is_empty() {
98            return Err(Error::Validation("transaction_id is required".to_string()));
99        }
100        if self.agent_id.is_empty() {
101            return Err(Error::Validation("agent_id is required".to_string()));
102        }
103        if self.for_.is_empty() {
104            return Err(Error::Validation("for is required".to_string()));
105        }
106        Ok(())
107    }
108
109    fn to_didcomm(&self, from_did: &str) -> Result<PlainMessage> {
110        // 1. Serialize self to JSON value
111        let mut body_json =
112            serde_json::to_value(self).map_err(|e| Error::SerializationError(e.to_string()))?;
113
114        // 2. Add/ensure '@type' field
115        if let Some(body_obj) = body_json.as_object_mut() {
116            body_obj.insert(
117                "@type".to_string(),
118                serde_json::Value::String(Self::message_type().to_string()),
119            );
120            // Note: serde handles #[serde(rename = "for")] automatically during serialization
121        }
122
123        // 3. Generate ID and timestamp
124        let id = uuid::Uuid::new_v4().to_string(); // Use new_v4 as per workspace UUID settings
125        let created_time = Utc::now().timestamp_millis() as u64;
126
127        // 4. Explicitly set the recipient using agent_id
128        let to = vec![self.agent_id.clone()];
129
130        // 5. Create the Message struct
131        let message = PlainMessage {
132            id,
133            typ: "application/didcomm-plain+json".to_string(), // Standard type
134            type_: Self::message_type().to_string(),
135            from: from_did.to_string(),
136            to, // Use the explicitly set 'to' field
137            thid: Some(self.transaction_id.clone()),
138            pthid: None, // Parent Thread ID usually set later
139            created_time: Some(created_time),
140            expires_time: None,
141            extra_headers: std::collections::HashMap::new(),
142            from_prior: None,
143            body: body_json,
144            attachments: None,
145        };
146
147        Ok(message)
148    }
149
150    fn from_didcomm(message: &PlainMessage) -> Result<Self> {
151        let body = message
152            .body
153            .as_object()
154            .ok_or_else(|| Error::Validation("Message body is not a JSON object".to_string()))?;
155
156        let transfer_id = body
157            .get("transaction_id")
158            .and_then(|v| v.as_str())
159            .ok_or_else(|| Error::Validation("Missing or invalid transaction_id".to_string()))?;
160
161        let agent_id = body
162            .get("agent_id")
163            .and_then(|v| v.as_str())
164            .ok_or_else(|| Error::Validation("Missing or invalid agent_id".to_string()))?;
165
166        let for_id = body
167            .get("for")
168            .and_then(|v| v.as_str())
169            .ok_or_else(|| Error::Validation("Missing or invalid for".to_string()))?;
170
171        let role = body
172            .get("role")
173            .and_then(|v| v.as_str())
174            .map(ToString::to_string);
175
176        let constraints = if let Some(constraints_value) = body.get("constraints") {
177            if constraints_value.is_null() {
178                None
179            } else {
180                // Parse constraints
181                let constraints_json = serde_json::to_value(constraints_value).map_err(|e| {
182                    Error::SerializationError(format!("Invalid constraints: {}", e))
183                })?;
184
185                Some(serde_json::from_value(constraints_json).map_err(|e| {
186                    Error::SerializationError(format!("Invalid constraints format: {}", e))
187                })?)
188            }
189        } else {
190            None
191        };
192
193        Ok(Connect {
194            transaction_id: transfer_id.to_string(),
195            agent_id: agent_id.to_string(),
196            for_: for_id.to_string(),
197            role,
198            constraints,
199        })
200    }
201}
202
203impl_tap_message!(Connect);
204
205impl TapMessageBody for AuthorizationRequired {
206    fn message_type() -> &'static str {
207        "https://tap.rsvp/schema/1.0#authorizationrequired"
208    }
209
210    fn validate(&self) -> Result<()> {
211        if self.url.is_empty() {
212            return Err(Error::Validation(
213                "Authorization URL is required".to_string(),
214            ));
215        }
216
217        // Validate expiry date if present
218        if let Some(expires) = self.metadata.get("expires") {
219            if let Some(expires_str) = expires.as_str() {
220                // Simple format check
221                if !expires_str.contains('T') || !expires_str.contains(':') {
222                    return Err(Error::Validation(
223                        "Invalid expiry date format. Expected ISO8601/RFC3339 format".to_string(),
224                    ));
225                }
226            }
227        }
228
229        Ok(())
230    }
231
232    fn to_didcomm(&self, from: &str) -> Result<PlainMessage> {
233        // Serialize to JSON
234        let mut body_json =
235            serde_json::to_value(self).map_err(|e| Error::SerializationError(e.to_string()))?;
236
237        // Ensure the @type field is correctly set in the body
238        if let Some(body_obj) = body_json.as_object_mut() {
239            body_obj.insert(
240                "@type".to_string(),
241                serde_json::Value::String(Self::message_type().to_string()),
242            );
243        }
244
245        let now = Utc::now().timestamp() as u64;
246
247        // Create a new Message with required fields
248        let message = PlainMessage {
249            id: uuid::Uuid::new_v4().to_string(),
250            typ: "application/didcomm-plain+json".to_string(),
251            type_: Self::message_type().to_string(),
252            body: body_json,
253            from: from.to_string(),
254            to: Vec::new(), // Recipients will be set separately
255            thid: None,
256            pthid: None,
257            created_time: Some(now),
258            expires_time: None,
259            extra_headers: std::collections::HashMap::new(),
260            from_prior: None,
261            attachments: None,
262        };
263
264        Ok(message)
265    }
266
267    fn from_didcomm(message: &PlainMessage) -> Result<Self> {
268        // Validate message type
269        if message.type_ != Self::message_type() {
270            return Err(Error::InvalidMessageType(format!(
271                "Expected {} but got {}",
272                Self::message_type(),
273                message.type_
274            )));
275        }
276
277        // Extract fields from message body
278        let auth_req: AuthorizationRequired = serde_json::from_value(message.body.clone())
279            .map_err(|e| Error::SerializationError(e.to_string()))?;
280
281        Ok(auth_req)
282    }
283}
284
285/// Out of Band invitation for TAP connections.
286#[derive(Debug, Clone, Serialize, Deserialize)]
287pub struct OutOfBand {
288    /// The goal code for this invitation.
289    pub goal_code: String,
290
291    /// The goal for this invitation.
292    pub goal: String,
293
294    /// The public DID or endpoint URL for the inviter.
295    pub service: String,
296
297    /// Accept media types.
298    #[serde(skip_serializing_if = "Option::is_none")]
299    pub accept: Option<Vec<String>>,
300
301    /// Handshake protocols supported.
302    #[serde(skip_serializing_if = "Option::is_none")]
303    pub handshake_protocols: Option<Vec<String>>,
304
305    /// Additional metadata.
306    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
307    pub metadata: HashMap<String, serde_json::Value>,
308}
309
310impl OutOfBand {
311    /// Create a new OutOfBand message.
312    pub fn new(goal_code: String, goal: String, service: String) -> Self {
313        Self {
314            goal_code,
315            goal,
316            service,
317            accept: None,
318            handshake_protocols: None,
319            metadata: HashMap::new(),
320        }
321    }
322}
323
324impl TapMessageBody for OutOfBand {
325    fn message_type() -> &'static str {
326        "https://tap.rsvp/schema/1.0#outofband"
327    }
328
329    fn validate(&self) -> Result<()> {
330        if self.goal_code.is_empty() {
331            return Err(Error::Validation("Goal code is required".to_string()));
332        }
333
334        if self.service.is_empty() {
335            return Err(Error::Validation("Service is required".to_string()));
336        }
337
338        Ok(())
339    }
340
341    fn to_didcomm(&self, from: &str) -> Result<PlainMessage> {
342        // Serialize to JSON
343        let mut body_json =
344            serde_json::to_value(self).map_err(|e| Error::SerializationError(e.to_string()))?;
345
346        // Ensure the @type field is correctly set in the body
347        if let Some(body_obj) = body_json.as_object_mut() {
348            body_obj.insert(
349                "@type".to_string(),
350                serde_json::Value::String(Self::message_type().to_string()),
351            );
352        }
353
354        let now = Utc::now().timestamp() as u64;
355
356        // Create a new Message with required fields
357        let message = PlainMessage {
358            id: uuid::Uuid::new_v4().to_string(),
359            typ: "application/didcomm-plain+json".to_string(),
360            type_: Self::message_type().to_string(),
361            body: body_json,
362            from: from.to_string(),
363            to: Vec::new(), // Recipients will be set separately
364            thid: None,
365            pthid: None,
366            created_time: Some(now),
367            expires_time: None,
368            extra_headers: std::collections::HashMap::new(),
369            from_prior: None,
370            attachments: None,
371        };
372
373        Ok(message)
374    }
375
376    fn from_didcomm(message: &PlainMessage) -> Result<Self> {
377        // Validate message type
378        if message.type_ != Self::message_type() {
379            return Err(Error::InvalidMessageType(format!(
380                "Expected {} but got {}",
381                Self::message_type(),
382                message.type_
383            )));
384        }
385
386        // Extract fields from message body
387        let oob: OutOfBand = serde_json::from_value(message.body.clone())
388            .map_err(|e| Error::SerializationError(e.to_string()))?;
389
390        Ok(oob)
391    }
392}
393
394/// Authorization Required message body.
395#[derive(Debug, Clone, Serialize, Deserialize)]
396pub struct AuthorizationRequired {
397    /// Authorization URL.
398    pub url: String,
399
400    /// Additional metadata.
401    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
402    pub metadata: HashMap<String, serde_json::Value>,
403}
404
405impl AuthorizationRequired {
406    /// Create a new AuthorizationRequired message.
407    pub fn new(url: String, expires: String) -> Self {
408        let mut metadata = HashMap::new();
409        metadata.insert("expires".to_string(), serde_json::Value::String(expires));
410
411        Self { url, metadata }
412    }
413
414    /// Add metadata to the message.
415    pub fn add_metadata(mut self, key: &str, value: serde_json::Value) -> Self {
416        self.metadata.insert(key.to_string(), value);
417        self
418    }
419}