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::error::{Error, Result};
10use crate::message::agent::TapParticipant;
11use crate::message::tap_message_trait::{TapMessage as TapMessageTrait, TapMessageBody};
12use crate::message::{Agent, Party};
13use crate::TapMessage;
14
15/// Agent structure specific to Connect messages.
16/// Unlike regular agents, Connect agents don't require a "for" field
17/// because the principal is specified separately in the Connect message.
18#[derive(Debug, Clone, Serialize, Deserialize)]
19pub struct ConnectAgent {
20    /// DID of the agent.
21    #[serde(rename = "@id")]
22    pub id: String,
23
24    /// Name of the agent (optional).
25    #[serde(skip_serializing_if = "Option::is_none")]
26    pub name: Option<String>,
27
28    /// Type of the agent (optional).
29    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
30    pub agent_type: Option<String>,
31
32    /// Service URL for the agent (optional).
33    #[serde(rename = "serviceUrl", skip_serializing_if = "Option::is_none")]
34    pub service_url: Option<String>,
35
36    /// Additional metadata.
37    #[serde(flatten)]
38    pub metadata: HashMap<String, serde_json::Value>,
39}
40
41impl TapParticipant for ConnectAgent {
42    fn id(&self) -> &str {
43        &self.id
44    }
45}
46
47impl ConnectAgent {
48    /// Create a new ConnectAgent with just an ID.
49    pub fn new(id: &str) -> Self {
50        Self {
51            id: id.to_string(),
52            name: None,
53            agent_type: None,
54            service_url: None,
55            metadata: HashMap::new(),
56        }
57    }
58
59    /// Convert to a regular Agent by adding a for_party.
60    pub fn to_agent(&self, for_party: &str) -> Agent {
61        let mut agent = Agent::new_without_role(&self.id, for_party);
62
63        // Copy metadata fields
64        if let Some(name) = &self.name {
65            agent
66                .metadata
67                .insert("name".to_string(), serde_json::Value::String(name.clone()));
68        }
69        if let Some(agent_type) = &self.agent_type {
70            agent.metadata.insert(
71                "type".to_string(),
72                serde_json::Value::String(agent_type.clone()),
73            );
74        }
75        if let Some(service_url) = &self.service_url {
76            agent.metadata.insert(
77                "serviceUrl".to_string(),
78                serde_json::Value::String(service_url.clone()),
79            );
80        }
81
82        // Copy any additional metadata
83        for (k, v) in &self.metadata {
84            agent.metadata.insert(k.clone(), v.clone());
85        }
86
87        agent
88    }
89}
90
91/// Transaction limits for connection constraints.
92#[derive(Debug, Clone, Serialize, Deserialize)]
93pub struct TransactionLimits {
94    /// Maximum amount per transaction.
95    #[serde(skip_serializing_if = "Option::is_none")]
96    pub per_transaction: Option<String>,
97
98    /// Maximum daily amount.
99    #[serde(skip_serializing_if = "Option::is_none")]
100    pub daily: Option<String>,
101
102    /// Currency for the limits.
103    #[serde(skip_serializing_if = "Option::is_none")]
104    pub currency: Option<String>,
105}
106
107/// Connection constraints for the Connect message.
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct ConnectionConstraints {
110    /// Allowed purposes.
111    #[serde(skip_serializing_if = "Option::is_none")]
112    pub purposes: Option<Vec<String>>,
113
114    /// Allowed category purposes.
115    #[serde(rename = "categoryPurposes", skip_serializing_if = "Option::is_none")]
116    pub category_purposes: Option<Vec<String>>,
117
118    /// Transaction limits.
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub limits: Option<TransactionLimits>,
121}
122
123/// Connect message body (TAIP-2).
124#[derive(Debug, Clone, Serialize, Deserialize, TapMessage)]
125#[tap(
126    message_type = "https://tap.rsvp/schema/1.0#Connect",
127    initiator,
128    authorizable
129)]
130pub struct Connect {
131    /// Transaction ID (only available after creation).
132    #[serde(skip)]
133    #[tap(transaction_id)]
134    pub transaction_id: Option<String>,
135
136    /// Agent DID (kept for backward compatibility).
137    #[serde(skip_serializing_if = "Option::is_none")]
138    pub agent_id: Option<String>,
139
140    /// Agent object containing agent details.
141    #[serde(skip_serializing_if = "Option::is_none")]
142    #[tap(participant)]
143    pub agent: Option<ConnectAgent>,
144
145    /// Principal party this connection is for.
146    #[serde(skip_serializing_if = "Option::is_none")]
147    #[tap(participant)]
148    pub principal: Option<Party>,
149
150    /// The entity this connection is for (kept for backward compatibility).
151    #[serde(rename = "for", skip_serializing_if = "Option::is_none", default)]
152    pub for_: Option<String>,
153
154    /// The role of the agent (optional).
155    #[serde(skip_serializing_if = "Option::is_none")]
156    pub role: Option<String>,
157
158    /// Connection constraints (optional).
159    #[serde(skip_serializing_if = "Option::is_none")]
160    pub constraints: Option<ConnectionConstraints>,
161}
162
163impl Connect {
164    /// Create a new Connect message (backward compatible).
165    pub fn new(transaction_id: &str, agent_id: &str, for_id: &str, role: Option<&str>) -> Self {
166        Self {
167            transaction_id: Some(transaction_id.to_string()),
168            agent_id: Some(agent_id.to_string()),
169            agent: None,
170            principal: None,
171            for_: Some(for_id.to_string()),
172            role: role.map(|s| s.to_string()),
173            constraints: None,
174        }
175    }
176
177    /// Create a new Connect message with Agent and Principal.
178    pub fn new_with_agent_and_principal(
179        transaction_id: &str,
180        agent: ConnectAgent,
181        principal: Party,
182    ) -> Self {
183        Self {
184            transaction_id: Some(transaction_id.to_string()),
185            agent_id: None,
186            agent: Some(agent),
187            principal: Some(principal),
188            for_: None,
189            role: None,
190            constraints: None,
191        }
192    }
193
194    /// Add constraints to the Connect message.
195    pub fn with_constraints(mut self, constraints: ConnectionConstraints) -> Self {
196        self.constraints = Some(constraints);
197        self
198    }
199}
200
201impl Connect {
202    /// Custom validation for Connect messages
203    pub fn validate_connect(&self) -> Result<()> {
204        // transaction_id is optional for initiator messages
205        // It will be set when creating the DIDComm message
206
207        // Either agent_id or agent must be present
208        if self.agent_id.is_none() && self.agent.is_none() {
209            return Err(Error::Validation(
210                "either agent_id or agent is required".to_string(),
211            ));
212        }
213
214        // Either for_ or principal must be present and non-empty
215        let for_empty = self.for_.as_ref().is_none_or(|s| s.is_empty());
216        if for_empty && self.principal.is_none() {
217            return Err(Error::Validation(
218                "either for or principal is required".to_string(),
219            ));
220        }
221
222        // Constraints are required for Connect messages
223        if self.constraints.is_none() {
224            return Err(Error::Validation(
225                "Connection request must include constraints".to_string(),
226            ));
227        }
228
229        Ok(())
230    }
231
232    /// Validation method that will be called by TapMessageBody trait
233    pub fn validate(&self) -> Result<()> {
234        self.validate_connect()
235    }
236}
237
238/// Out of Band invitation for TAP connections.
239#[derive(Debug, Clone, Serialize, Deserialize, TapMessage)]
240#[tap(message_type = "https://tap.rsvp/schema/1.0#OutOfBand")]
241pub struct OutOfBand {
242    /// The goal code for this invitation.
243    pub goal_code: String,
244
245    /// The goal for this invitation.
246    pub goal: String,
247
248    /// The public DID or endpoint URL for the inviter.
249    pub service: String,
250
251    /// Accept media types.
252    #[serde(skip_serializing_if = "Option::is_none")]
253    pub accept: Option<Vec<String>>,
254
255    /// Handshake protocols supported.
256    #[serde(skip_serializing_if = "Option::is_none")]
257    pub handshake_protocols: Option<Vec<String>>,
258
259    /// Additional metadata.
260    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
261    pub metadata: HashMap<String, serde_json::Value>,
262}
263
264impl OutOfBand {
265    /// Create a new OutOfBand message.
266    pub fn new(goal_code: String, goal: String, service: String) -> Self {
267        Self {
268            goal_code,
269            goal,
270            service,
271            accept: None,
272            handshake_protocols: None,
273            metadata: HashMap::new(),
274        }
275    }
276}
277
278/// Authorization Required message body.
279#[derive(Debug, Clone, Serialize, Deserialize, TapMessage)]
280#[tap(message_type = "https://tap.rsvp/schema/1.0#AuthorizationRequired")]
281pub struct AuthorizationRequired {
282    /// Authorization URL.
283    #[serde(rename = "authorization_url")]
284    pub url: String,
285
286    /// Agent ID.
287    #[serde(skip_serializing_if = "Option::is_none")]
288    pub agent_id: Option<String>,
289
290    /// Expiry date/time.
291    #[serde(skip_serializing_if = "Option::is_none")]
292    pub expires: Option<String>,
293
294    /// Additional metadata.
295    #[serde(default, skip_serializing_if = "HashMap::is_empty")]
296    pub metadata: HashMap<String, serde_json::Value>,
297}
298
299impl AuthorizationRequired {
300    /// Create a new AuthorizationRequired message.
301    pub fn new(url: String, expires: String) -> Self {
302        Self {
303            url,
304            agent_id: None,
305            expires: Some(expires),
306            metadata: HashMap::new(),
307        }
308    }
309
310    /// Add metadata to the message.
311    pub fn add_metadata(mut self, key: &str, value: serde_json::Value) -> Self {
312        self.metadata.insert(key.to_string(), value);
313        self
314    }
315}
316
317impl OutOfBand {
318    /// Custom validation for OutOfBand messages
319    pub fn validate_out_of_band(&self) -> Result<()> {
320        if self.goal_code.is_empty() {
321            return Err(Error::Validation("Goal code is required".to_string()));
322        }
323
324        if self.service.is_empty() {
325            return Err(Error::Validation("Service is required".to_string()));
326        }
327
328        Ok(())
329    }
330
331    /// Validation method that will be called by TapMessageBody trait
332    pub fn validate(&self) -> Result<()> {
333        self.validate_out_of_band()
334    }
335}
336
337impl AuthorizationRequired {
338    /// Custom validation for AuthorizationRequired messages
339    pub fn validate_authorization_required(&self) -> Result<()> {
340        if self.url.is_empty() {
341            return Err(Error::Validation(
342                "Authorization URL is required".to_string(),
343            ));
344        }
345
346        // Validate expiry date if present
347        if let Some(expires) = &self.expires {
348            // Simple format check
349            if !expires.contains('T') || !expires.contains(':') {
350                return Err(Error::Validation(
351                    "Invalid expiry date format. Expected ISO8601/RFC3339 format".to_string(),
352                ));
353            }
354        }
355
356        Ok(())
357    }
358
359    /// Validation method that will be called by TapMessageBody trait
360    pub fn validate(&self) -> Result<()> {
361        self.validate_authorization_required()
362    }
363}