Skip to main content

composio_sdk/models/
connected_accounts.rs

1//! Connected accounts management
2//!
3//! This module provides functionality to manage connected accounts,
4//! which represent user connections to external services through Composio.
5//!
6//! # Overview
7//!
8//! Connected accounts are used to authenticate with third-party services.
9//! They can be created through OAuth flows, API keys, or other authentication methods.
10//!
11//! # Connection Flow
12//!
13//! 1. Initiate a connection request
14//! 2. User authenticates via redirect URL
15//! 3. Wait for connection to become ACTIVE
16//! 4. Use the connected account for tool execution
17
18use serde::{Deserialize, Serialize};
19use std::time::{Duration, SystemTime};
20
21/// Default timeout for waiting for connection (60 seconds)
22pub const DEFAULT_WAIT_TIMEOUT: Duration = Duration::from_secs(60);
23
24/// Connection request representing an in-progress authentication
25#[derive(Debug, Clone)]
26pub struct ConnectionRequest {
27    /// Unique identifier for the connection
28    pub id: String,
29
30    /// Current status of the connection
31    pub status: ConnectionStatus,
32
33    /// Redirect URL for OAuth flows
34    pub redirect_url: Option<String>,
35}
36
37/// Connection status enumeration
38#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
39#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
40pub enum ConnectionStatus {
41    /// Connection is being initialized
42    Initializing,
43
44    /// OAuth flow has been initiated
45    Initiated,
46
47    /// Connection is active and ready to use
48    Active,
49
50    /// Connection credentials have expired
51    Expired,
52
53    /// Connection failed
54    Failed,
55
56    /// Connection has been manually disabled
57    Inactive,
58}
59
60impl ConnectionRequest {
61    /// Create a new connection request
62    pub fn new(id: String, status: ConnectionStatus, redirect_url: Option<String>) -> Self {
63        Self {
64            id,
65            status,
66            redirect_url,
67        }
68    }
69
70    /// Wait for the connection to become active
71    ///
72    /// This method polls the connection status until it becomes ACTIVE or the timeout is reached.
73    ///
74    /// # Arguments
75    ///
76    /// * `timeout` - Optional timeout duration (defaults to 60 seconds)
77    ///
78    /// # Returns
79    ///
80    /// Returns `Ok(())` when connection is active, or `Err` on timeout or failure
81    ///
82    /// # Example
83    ///
84    /// ```rust,no_run
85    /// # use composio_sdk::models::connected_accounts::ConnectionRequest;
86    /// # use std::time::Duration;
87    /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
88    /// let mut connection_request = ConnectionRequest::new(
89    ///     "ca_abc123".to_string(),
90    ///     composio_sdk::models::connected_accounts::ConnectionStatus::Initiated,
91    ///     Some("https://auth.example.com".to_string()),
92    /// );
93    ///
94    /// // Wait up to 60 seconds for connection
95    /// connection_request.wait_for_connection(None).await?;
96    /// # Ok(())
97    /// # }
98    /// ```
99    pub async fn wait_for_connection(
100        &mut self,
101        timeout: Option<Duration>,
102    ) -> Result<(), ConnectionError> {
103        let timeout = timeout.unwrap_or(DEFAULT_WAIT_TIMEOUT);
104        let deadline = SystemTime::now() + timeout;
105
106        while SystemTime::now() < deadline {
107            // In a real implementation, this would poll the API
108            // For now, we just check the status
109            if self.status == ConnectionStatus::Active {
110                return Ok(());
111            }
112
113            if self.status == ConnectionStatus::Failed {
114                return Err(ConnectionError::Failed(format!(
115                    "Connection {} failed",
116                    self.id
117                )));
118            }
119
120            // Sleep for 1 second before next poll
121            tokio::time::sleep(Duration::from_secs(1)).await;
122        }
123
124        Err(ConnectionError::Timeout(format!(
125            "Timeout while waiting for connection {} to be active",
126            self.id
127        )))
128    }
129}
130
131/// Connection error types
132#[derive(Debug, thiserror::Error)]
133pub enum ConnectionError {
134    /// Connection timed out
135    #[error("Connection timeout: {0}")]
136    Timeout(String),
137
138    /// Connection failed
139    #[error("Connection failed: {0}")]
140    Failed(String),
141
142    /// Multiple connections found when only one expected
143    #[error("Multiple connected accounts found: {0}")]
144    MultipleAccounts(String),
145
146    /// API error
147    #[error("API error: {0}")]
148    ApiError(String),
149}
150
151/// Authentication scheme enumeration
152#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
153#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
154pub enum AuthScheme {
155    /// OAuth 1.0 authentication
156    Oauth1,
157
158    /// OAuth 2.0 authentication
159    Oauth2,
160
161    /// Composio Connect Link
162    ComposioLink,
163
164    /// API Key authentication
165    ApiKey,
166
167    /// Basic authentication
168    Basic,
169
170    /// Bearer token authentication
171    BearerToken,
172
173    /// Google Service Account
174    GoogleServiceAccount,
175
176    /// No authentication required
177    NoAuth,
178
179    /// Cal.com authentication
180    CalcomAuth,
181
182    /// Bill.com authentication
183    BillcomAuth,
184
185    /// Basic authentication with JWT
186    BasicWithJwt,
187}
188
189/// Connection state for creating connected accounts
190#[derive(Debug, Clone, Serialize, Deserialize)]
191pub struct ConnectionState {
192    /// Authentication scheme
193    pub auth_scheme: AuthScheme,
194
195    /// Connection status
196    pub status: ConnectionStatus,
197
198    /// Additional configuration (varies by auth scheme)
199    #[serde(flatten)]
200    pub config: serde_json::Value,
201}
202
203/// Helper functions for creating connection states with different auth schemes
204pub struct AuthSchemeHelper;
205
206impl AuthSchemeHelper {
207    /// Create OAuth 1.0 connection state
208    pub fn oauth1(config: serde_json::Value) -> ConnectionState {
209        ConnectionState {
210            auth_scheme: AuthScheme::Oauth1,
211            status: ConnectionStatus::Initializing,
212            config,
213        }
214    }
215
216    /// Create OAuth 2.0 connection state
217    pub fn oauth2(config: serde_json::Value) -> ConnectionState {
218        ConnectionState {
219            auth_scheme: AuthScheme::Oauth2,
220            status: ConnectionStatus::Initializing,
221            config,
222        }
223    }
224
225    /// Create Composio Link connection state
226    pub fn composio_link(config: serde_json::Value) -> ConnectionState {
227        ConnectionState {
228            auth_scheme: AuthScheme::ComposioLink,
229            status: ConnectionStatus::Initializing,
230            config,
231        }
232    }
233
234    /// Create API Key connection state
235    pub fn api_key(config: serde_json::Value) -> ConnectionState {
236        ConnectionState {
237            auth_scheme: AuthScheme::ApiKey,
238            status: ConnectionStatus::Active,
239            config,
240        }
241    }
242
243    /// Create Basic auth connection state
244    pub fn basic(config: serde_json::Value) -> ConnectionState {
245        ConnectionState {
246            auth_scheme: AuthScheme::Basic,
247            status: ConnectionStatus::Active,
248            config,
249        }
250    }
251
252    /// Create Bearer token connection state
253    pub fn bearer_token(config: serde_json::Value) -> ConnectionState {
254        ConnectionState {
255            auth_scheme: AuthScheme::BearerToken,
256            status: ConnectionStatus::Active,
257            config,
258        }
259    }
260
261    /// Create Google Service Account connection state
262    pub fn google_service_account(config: serde_json::Value) -> ConnectionState {
263        ConnectionState {
264            auth_scheme: AuthScheme::GoogleServiceAccount,
265            status: ConnectionStatus::Active,
266            config,
267        }
268    }
269
270    /// Create No Auth connection state
271    pub fn no_auth(config: serde_json::Value) -> ConnectionState {
272        ConnectionState {
273            auth_scheme: AuthScheme::NoAuth,
274            status: ConnectionStatus::Active,
275            config,
276        }
277    }
278
279    /// Create Cal.com auth connection state
280    pub fn calcom_auth(config: serde_json::Value) -> ConnectionState {
281        ConnectionState {
282            auth_scheme: AuthScheme::CalcomAuth,
283            status: ConnectionStatus::Active,
284            config,
285        }
286    }
287
288    /// Create Bill.com auth connection state
289    pub fn billcom_auth(config: serde_json::Value) -> ConnectionState {
290        ConnectionState {
291            auth_scheme: AuthScheme::BillcomAuth,
292            status: ConnectionStatus::Active,
293            config,
294        }
295    }
296
297    /// Create Basic with JWT connection state
298    pub fn basic_with_jwt(config: serde_json::Value) -> ConnectionState {
299        ConnectionState {
300            auth_scheme: AuthScheme::BasicWithJwt,
301            status: ConnectionStatus::Active,
302            config,
303        }
304    }
305}
306
307/// Global auth scheme helper instance
308pub static AUTH_SCHEME: AuthSchemeHelper = AuthSchemeHelper;
309
310/// Parameters for initiating a connection
311#[derive(Debug, Clone, Serialize, Deserialize)]
312pub struct InitiateConnectionParams {
313    /// User ID to create the connection for
314    pub user_id: String,
315
316    /// Auth config ID to use
317    pub auth_config_id: String,
318
319    /// Optional callback URL for OAuth flows
320    #[serde(skip_serializing_if = "Option::is_none")]
321    pub callback_url: Option<String>,
322
323    /// Whether to allow multiple connections for the same user and auth config
324    #[serde(skip_serializing_if = "Option::is_none")]
325    pub allow_multiple: Option<bool>,
326
327    /// Optional connection state configuration
328    #[serde(skip_serializing_if = "Option::is_none")]
329    pub config: Option<ConnectionState>,
330}
331
332/// Parameters for creating a connection link
333#[derive(Debug, Clone, Serialize, Deserialize)]
334pub struct LinkConnectionParams {
335    /// User ID to create the connection for
336    pub user_id: String,
337
338    /// Auth config ID to use
339    pub auth_config_id: String,
340
341    /// Optional callback URL
342    #[serde(skip_serializing_if = "Option::is_none")]
343    pub callback_url: Option<String>,
344}
345
346/// Connected account list parameters
347#[derive(Debug, Clone, Default, Serialize, Deserialize)]
348pub struct ConnectedAccountListParams {
349    /// Filter by user IDs
350    #[serde(skip_serializing_if = "Option::is_none")]
351    pub user_ids: Option<Vec<String>>,
352
353    /// Filter by auth config IDs
354    #[serde(skip_serializing_if = "Option::is_none")]
355    pub auth_config_ids: Option<Vec<String>>,
356
357    /// Filter by connection statuses
358    #[serde(skip_serializing_if = "Option::is_none")]
359    pub statuses: Option<Vec<ConnectionStatus>>,
360
361    /// Filter by toolkit slugs
362    #[serde(skip_serializing_if = "Option::is_none")]
363    pub toolkit_slugs: Option<Vec<String>>,
364
365    /// Filter by connected account IDs
366    #[serde(skip_serializing_if = "Option::is_none")]
367    pub connected_account_ids: Option<Vec<String>>,
368
369    /// Show disabled accounts
370    #[serde(skip_serializing_if = "Option::is_none")]
371    pub show_disabled: Option<bool>,
372
373    /// Maximum number of results
374    #[serde(skip_serializing_if = "Option::is_none")]
375    pub limit: Option<u32>,
376
377    /// Pagination cursor
378    #[serde(skip_serializing_if = "Option::is_none")]
379    pub cursor: Option<String>,
380
381    /// Order by field
382    #[serde(skip_serializing_if = "Option::is_none")]
383    pub order_by: Option<String>,
384
385    /// Order direction (asc/desc)
386    #[serde(skip_serializing_if = "Option::is_none")]
387    pub order_direction: Option<String>,
388}
389
390/// Connected account information
391#[derive(Debug, Clone, Serialize, Deserialize)]
392pub struct ConnectedAccountInfo {
393    /// Unique identifier
394    pub id: String,
395
396    /// User ID
397    pub user_id: String,
398
399    /// Auth config ID
400    pub auth_config_id: String,
401
402    /// Toolkit slug
403    pub toolkit: String,
404
405    /// Connection status
406    pub status: ConnectionStatus,
407
408    /// Status reason (if failed or expired)
409    #[serde(skip_serializing_if = "Option::is_none")]
410    pub status_reason: Option<String>,
411
412    /// Whether the account is disabled
413    #[serde(skip_serializing_if = "Option::is_none")]
414    pub is_disabled: Option<bool>,
415
416    /// Creation timestamp
417    pub created_at: String,
418
419    /// Last update timestamp
420    pub updated_at: String,
421
422    /// Connection state data
423    #[serde(skip_serializing_if = "Option::is_none")]
424    pub state: Option<serde_json::Value>,
425
426    /// Connection data
427    #[serde(skip_serializing_if = "Option::is_none")]
428    pub data: Option<serde_json::Value>,
429}
430
431/// Connected account list response
432#[derive(Debug, Clone, Serialize, Deserialize)]
433pub struct ConnectedAccountListResponse {
434    /// List of connected accounts
435    pub items: Vec<ConnectedAccountInfo>,
436
437    /// Next cursor for pagination
438    #[serde(skip_serializing_if = "Option::is_none")]
439    pub next_cursor: Option<String>,
440
441    /// Total number of pages
442    #[serde(skip_serializing_if = "Option::is_none")]
443    pub total_pages: Option<u32>,
444
445    /// Current page number
446    #[serde(skip_serializing_if = "Option::is_none")]
447    pub current_page: Option<u32>,
448
449    /// Total number of items
450    #[serde(skip_serializing_if = "Option::is_none")]
451    pub total_items: Option<u32>,
452}
453
454/// Parameters for refreshing a connected account
455#[derive(Debug, Clone, Default, Serialize, Deserialize)]
456pub struct ConnectedAccountRefreshParams {
457    /// Optional redirect URL for auth flows
458    #[serde(skip_serializing_if = "Option::is_none")]
459    pub redirect_url: Option<String>,
460
461    /// [EXPERIMENTAL] Validate provided credentials for API key flows
462    #[serde(skip_serializing_if = "Option::is_none")]
463    pub validate_credentials: Option<bool>,
464}
465
466/// Response from refreshing a connected account
467#[derive(Debug, Clone, Serialize, Deserialize)]
468pub struct ConnectedAccountRefreshResponse {
469    /// Connected account ID
470    pub id: String,
471
472    /// Current status
473    pub status: ConnectionStatus,
474
475    /// Redirect URL for completing auth, if needed
476    #[serde(skip_serializing_if = "Option::is_none")]
477    pub redirect_url: Option<String>,
478}
479
480/// Parameters for updating connected account status
481#[derive(Debug, Clone, Serialize, Deserialize)]
482pub struct ConnectedAccountUpdateStatusParams {
483    /// Set to true to enable account or false to disable it
484    pub enabled: bool,
485}
486
487/// Response from deleting a connected account
488#[derive(Debug, Clone, Serialize, Deserialize)]
489pub struct ConnectedAccountDeleteResponse {
490    /// Indicates whether delete operation succeeded
491    pub success: bool,
492}
493
494/// Connected account update status response
495#[derive(Debug, Clone, Serialize, Deserialize)]
496pub struct ConnectedAccountUpdateStatusResponse {
497    /// Indicates whether status update operation succeeded
498    pub success: bool,
499}
500
501// Note: The ConnectedAccounts resource implementation is pending full HTTP client integration.
502// The data structures above are ready for use once the client supports generic HTTP methods.
503
504#[cfg(test)]
505mod tests {
506    use super::*;
507
508    #[test]
509    fn test_connection_status_serialization() {
510        let status = ConnectionStatus::Active;
511        let json = serde_json::to_string(&status).unwrap();
512        assert_eq!(json, "\"ACTIVE\"");
513    }
514
515    #[test]
516    fn test_connection_status_deserialization() {
517        let json = "\"ACTIVE\"";
518        let status: ConnectionStatus = serde_json::from_str(json).unwrap();
519        assert_eq!(status, ConnectionStatus::Active);
520    }
521
522    #[test]
523    fn test_auth_scheme_serialization() {
524        let scheme = AuthScheme::Oauth2;
525        let json = serde_json::to_string(&scheme).unwrap();
526        assert_eq!(json, "\"OAUTH2\"");
527    }
528
529    #[test]
530    fn test_auth_scheme_helper_oauth2() {
531        let config = serde_json::json!({"client_id": "test"});
532        let state = AuthSchemeHelper::oauth2(config);
533        assert_eq!(state.auth_scheme, AuthScheme::Oauth2);
534        assert_eq!(state.status, ConnectionStatus::Initializing);
535    }
536
537    #[test]
538    fn test_auth_scheme_helper_api_key() {
539        let config = serde_json::json!({"api_key": "test_key"});
540        let state = AuthSchemeHelper::api_key(config);
541        assert_eq!(state.auth_scheme, AuthScheme::ApiKey);
542        assert_eq!(state.status, ConnectionStatus::Active);
543    }
544
545    #[test]
546    fn test_connection_request_new() {
547        let request = ConnectionRequest::new(
548            "ca_test123".to_string(),
549            ConnectionStatus::Initiated,
550            Some("https://auth.example.com".to_string()),
551        );
552        assert_eq!(request.id, "ca_test123");
553        assert_eq!(request.status, ConnectionStatus::Initiated);
554        assert!(request.redirect_url.is_some());
555    }
556
557    #[test]
558    fn test_connected_account_list_params_default() {
559        let params = ConnectedAccountListParams::default();
560        assert!(params.user_ids.is_none());
561        assert!(params.auth_config_ids.is_none());
562        assert!(params.statuses.is_none());
563    }
564
565    #[test]
566    fn test_initiate_connection_params_serialization() {
567        let params = InitiateConnectionParams {
568            user_id: "user_123".to_string(),
569            auth_config_id: "ac_abc".to_string(),
570            callback_url: Some("https://callback.example.com".to_string()),
571            allow_multiple: Some(false),
572            config: None,
573        };
574
575        let json = serde_json::to_string(&params).unwrap();
576        assert!(json.contains("user_123"));
577        assert!(json.contains("ac_abc"));
578        assert!(json.contains("callback"));
579    }
580}