Skip to main content

composio_sdk/models/
webhook_events.rs

1//! Webhook event types for typed event handling.
2//!
3//! This module provides strongly-typed definitions for specific webhook event types,
4//! enabling type-safe handling of events like connection expiration.
5//!
6//! # Overview
7//!
8//! Webhook events are delivered via HTTP POST to your configured webhook URL.
9//! This module provides Rust types that match the webhook payload structure.
10//!
11//! # Event Types
12//!
13//! - `ConnectionExpiredEvent`: Emitted when a connected account expires
14//! - `TriggerEvent`: Emitted when a trigger fires (defined in triggers.rs)
15//!
16//! # Example
17//!
18//! ```rust
19//! use composio::models::webhook_events::{WebhookEvent, is_connection_expired_event};
20//!
21//! fn handle_webhook(payload: serde_json::Value) {
22//!     if is_connection_expired_event(&payload) {
23//!         // Handle connection expiration
24//!         if let Ok(event) = serde_json::from_value::<ConnectionExpiredEvent>(payload) {
25//!             println!("Connection {} expired for user {}", 
26//!                 event.data.id, event.data.user_id);
27//!         }
28//!     }
29//! }
30//! ```
31
32use serde::{Deserialize, Serialize};
33use std::collections::HashMap;
34
35/// Known webhook event types
36#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
37#[serde(rename_all = "snake_case")]
38pub enum WebhookEventType {
39    /// Connection expired event
40    #[serde(rename = "composio.connected_account.expired")]
41    ConnectionExpired,
42    
43    /// Trigger message event
44    #[serde(rename = "composio.trigger.message")]
45    TriggerMessage,
46}
47
48/// Connection status values
49#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
51pub enum ConnectionStatus {
52    /// Connection is being initialized
53    Initializing,
54    
55    /// OAuth flow has been initiated
56    Initiated,
57    
58    /// Connection is active and working
59    Active,
60    
61    /// Connection failed
62    Failed,
63    
64    /// Connection has expired
65    Expired,
66    
67    /// Connection is manually disabled
68    Inactive,
69}
70
71// =============================================================================
72// CONNECTED ACCOUNT DATA (matches GET /api/v3/connected_accounts/{id})
73// =============================================================================
74
75/// Toolkit information in connected account response
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct ConnectedAccountToolkit {
78    /// Toolkit slug (e.g., "github", "gmail")
79    pub slug: String,
80}
81
82/// Deprecated auth config fields
83#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct ConnectedAccountAuthConfigDeprecated {
85    /// Legacy UUID identifier
86    #[serde(skip_serializing_if = "Option::is_none")]
87    pub uuid: Option<String>,
88}
89
90/// Auth config details in connected account response
91#[derive(Debug, Clone, Serialize, Deserialize)]
92pub struct ConnectedAccountAuthConfig {
93    /// Auth config ID (nano ID format)
94    pub id: String,
95    
96    /// Authentication scheme (OAUTH2, API_KEY, etc.)
97    pub auth_scheme: String,
98    
99    /// Whether this uses Composio's managed authentication
100    pub is_composio_managed: bool,
101    
102    /// Whether this auth config is disabled
103    pub is_disabled: bool,
104    
105    /// Deprecated fields
106    #[serde(skip_serializing_if = "Option::is_none")]
107    pub deprecated: Option<ConnectedAccountAuthConfigDeprecated>,
108}
109
110/// Connection state value - varies by auth scheme
111#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct ConnectionStateVal {
113    /// Connection status
114    #[serde(skip_serializing_if = "Option::is_none")]
115    pub status: Option<String>,
116    
117    // OAuth2 fields
118    /// OAuth2 access token
119    #[serde(skip_serializing_if = "Option::is_none")]
120    pub access_token: Option<String>,
121    
122    /// OAuth2 refresh token
123    #[serde(skip_serializing_if = "Option::is_none")]
124    pub refresh_token: Option<String>,
125    
126    /// Token type (usually "Bearer")
127    #[serde(skip_serializing_if = "Option::is_none")]
128    pub token_type: Option<String>,
129    
130    /// Token expiration time in seconds
131    #[serde(skip_serializing_if = "Option::is_none")]
132    pub expires_in: Option<serde_json::Value>, // Can be int, string, or null
133    
134    /// OAuth2 scopes
135    #[serde(skip_serializing_if = "Option::is_none")]
136    pub scope: Option<serde_json::Value>, // Can be string or array
137    
138    /// OpenID Connect ID token
139    #[serde(skip_serializing_if = "Option::is_none")]
140    pub id_token: Option<String>,
141    
142    /// PKCE code verifier
143    #[serde(skip_serializing_if = "Option::is_none")]
144    pub code_verifier: Option<String>,
145    
146    /// OAuth callback URL
147    #[serde(skip_serializing_if = "Option::is_none")]
148    pub callback_url: Option<String>,
149    
150    // OAuth1 fields
151    /// OAuth1 token
152    #[serde(skip_serializing_if = "Option::is_none")]
153    pub oauth_token: Option<String>,
154    
155    /// OAuth1 token secret
156    #[serde(skip_serializing_if = "Option::is_none")]
157    pub oauth_token_secret: Option<String>,
158    
159    // API Key fields
160    /// API key
161    #[serde(skip_serializing_if = "Option::is_none")]
162    pub api_key: Option<String>,
163    
164    /// Generic API key
165    #[serde(skip_serializing_if = "Option::is_none")]
166    pub generic_api_key: Option<String>,
167    
168    // Bearer Token fields
169    /// Bearer token
170    #[serde(skip_serializing_if = "Option::is_none")]
171    pub token: Option<String>,
172    
173    // Basic auth fields
174    /// Username for basic auth
175    #[serde(skip_serializing_if = "Option::is_none")]
176    pub username: Option<String>,
177    
178    /// Password for basic auth
179    #[serde(skip_serializing_if = "Option::is_none")]
180    pub password: Option<String>,
181}
182
183/// Connection state data discriminated by auth scheme
184#[derive(Debug, Clone, Serialize, Deserialize)]
185pub struct ConnectionState {
186    /// Authentication scheme identifier
187    #[serde(rename = "authScheme")]
188    pub auth_scheme: String,
189    
190    /// Connection state value (varies by auth scheme)
191    pub val: ConnectionStateVal,
192}
193
194/// Deprecated connected account fields
195#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct ConnectedAccountDeprecated {
197    /// Legacy labels
198    #[serde(skip_serializing_if = "Option::is_none")]
199    pub labels: Option<Vec<String>>,
200    
201    /// Legacy UUID identifier
202    #[serde(skip_serializing_if = "Option::is_none")]
203    pub uuid: Option<String>,
204}
205
206/// Connected account data matching GET /api/v3/connected_accounts/{id} response.
207///
208/// This is used in webhook payloads for connection lifecycle events.
209/// It intentionally does NOT reuse the SDK client's response types because
210/// webhook payloads arrive in raw snake_case format, while the SDK client
211/// may transform responses to a different shape. This struct validates the
212/// raw webhook payload directly without any transformation layer.
213#[derive(Debug, Clone, Serialize, Deserialize)]
214pub struct SingleConnectedAccountDetailedResponse {
215    /// Toolkit information
216    pub toolkit: ConnectedAccountToolkit,
217    
218    /// Auth config details
219    pub auth_config: ConnectedAccountAuthConfig,
220    
221    /// Connected account ID (nano ID format)
222    pub id: String,
223    
224    /// User ID this connection belongs to
225    pub user_id: String,
226    
227    /// Connection status
228    pub status: String, // ConnectionStatus value as string
229    
230    /// Creation timestamp (ISO-8601)
231    pub created_at: String,
232    
233    /// Last update timestamp (ISO-8601)
234    pub updated_at: String,
235    
236    /// Connection state with credentials
237    pub state: ConnectionState,
238    
239    /// Additional connection data
240    pub data: HashMap<String, serde_json::Value>,
241    
242    /// Connection parameters
243    pub params: HashMap<String, serde_json::Value>,
244    
245    /// Reason for current status (if applicable)
246    pub status_reason: Option<String>,
247    
248    /// Whether this connection is disabled
249    pub is_disabled: bool,
250    
251    /// Test request endpoint
252    #[serde(skip_serializing_if = "Option::is_none")]
253    pub test_request_endpoint: Option<String>,
254    
255    /// Deprecated fields
256    #[serde(skip_serializing_if = "Option::is_none")]
257    pub deprecated: Option<ConnectedAccountDeprecated>,
258}
259
260// =============================================================================
261// CONNECTION EXPIRED WEBHOOK EVENT
262// =============================================================================
263
264/// Webhook metadata for connection events
265#[derive(Debug, Clone, Serialize, Deserialize)]
266pub struct WebhookConnectionMetadata {
267    /// Project ID
268    pub project_id: String,
269    
270    /// Organization ID
271    pub org_id: String,
272}
273
274/// Connection expired webhook event payload.
275///
276/// Emitted when a connected account expires due to authentication refresh failure.
277///
278/// # Example
279///
280/// ```rust
281/// use composio::models::webhook_events::{ConnectionExpiredEvent, is_connection_expired_event};
282///
283/// fn handle_webhook(payload: serde_json::Value) {
284///     if is_connection_expired_event(&payload) {
285///         if let Ok(event) = serde_json::from_value::<ConnectionExpiredEvent>(payload) {
286///             println!("Connection {} expired", event.data.id);
287///             println!("Toolkit: {}", event.data.toolkit.slug);
288///             println!("User: {}", event.data.user_id);
289///         }
290///     }
291/// }
292/// ```
293#[derive(Debug, Clone, Serialize, Deserialize)]
294pub struct ConnectionExpiredEvent {
295    /// Unique message ID (e.g., "msg_847cdfcd-d219-4f18-a6dd-91acd42ca94a")
296    pub id: String,
297    
298    /// ISO-8601 timestamp
299    pub timestamp: String,
300    
301    /// Event type (always "composio.connected_account.expired")
302    #[serde(rename = "type")]
303    pub event_type: String,
304    
305    /// Connected account details
306    pub data: SingleConnectedAccountDetailedResponse,
307    
308    /// Event metadata
309    pub metadata: WebhookConnectionMetadata,
310}
311
312/// Type alias for non-trigger webhook events with specific typed schemas.
313///
314/// Trigger events (composio.trigger.message) are handled through the
315/// TriggerEvent type in triggers.rs via the trigger subscription system.
316#[derive(Debug, Clone, Serialize, Deserialize)]
317#[serde(untagged)]
318pub enum WebhookEvent {
319    /// Connection expired event
320    ConnectionExpired(ConnectionExpiredEvent),
321}
322
323/// Check if a webhook payload is a connection expired event.
324///
325/// This function performs type narrowing to determine if the payload
326/// represents a connection expiration event.
327///
328/// # Arguments
329///
330/// * `payload` - The webhook payload to check
331///
332/// # Returns
333///
334/// `true` if this is a connection expired event
335///
336/// # Example
337///
338/// ```rust
339/// use composio::models::webhook_events::is_connection_expired_event;
340/// use serde_json::json;
341///
342/// let payload = json!({
343///     "type": "composio.connected_account.expired",
344///     "id": "msg_123",
345///     "timestamp": "2024-01-01T00:00:00Z",
346///     "data": {},
347///     "metadata": {}
348/// });
349///
350/// if is_connection_expired_event(&payload) {
351///     // Handle connection expired event
352/// }
353/// ```
354pub fn is_connection_expired_event(payload: &serde_json::Value) -> bool {
355    payload
356        .get("type")
357        .and_then(|t| t.as_str())
358        .map(|t| t == "composio.connected_account.expired")
359        .unwrap_or(false)
360}
361
362#[cfg(test)]
363mod tests {
364    use super::*;
365    use serde_json::json;
366
367    #[test]
368    fn test_webhook_event_type_serialization() {
369        let event_type = WebhookEventType::ConnectionExpired;
370        let json = serde_json::to_string(&event_type).unwrap();
371        assert_eq!(json, "\"composio.connected_account.expired\"");
372
373        let event_type = WebhookEventType::TriggerMessage;
374        let json = serde_json::to_string(&event_type).unwrap();
375        assert_eq!(json, "\"composio.trigger.message\"");
376    }
377
378    #[test]
379    fn test_connection_status_serialization() {
380        let status = ConnectionStatus::Active;
381        let json = serde_json::to_string(&status).unwrap();
382        assert_eq!(json, "\"ACTIVE\"");
383
384        let status = ConnectionStatus::Expired;
385        let json = serde_json::to_string(&status).unwrap();
386        assert_eq!(json, "\"EXPIRED\"");
387    }
388
389    #[test]
390    fn test_is_connection_expired_event() {
391        let payload = json!({
392            "type": "composio.connected_account.expired",
393            "id": "msg_123",
394            "timestamp": "2024-01-01T00:00:00Z"
395        });
396
397        assert!(is_connection_expired_event(&payload));
398
399        let payload = json!({
400            "type": "composio.trigger.message",
401            "id": "msg_456"
402        });
403
404        assert!(!is_connection_expired_event(&payload));
405    }
406
407    #[test]
408    fn test_connection_expired_event_deserialization() {
409        let json = json!({
410            "id": "msg_847cdfcd-d219-4f18-a6dd-91acd42ca94a",
411            "timestamp": "2024-01-01T12:00:00Z",
412            "type": "composio.connected_account.expired",
413            "data": {
414                "toolkit": {
415                    "slug": "github"
416                },
417                "auth_config": {
418                    "id": "ac_123",
419                    "auth_scheme": "OAUTH2",
420                    "is_composio_managed": true,
421                    "is_disabled": false
422                },
423                "id": "ca_456",
424                "user_id": "user_789",
425                "status": "EXPIRED",
426                "created_at": "2024-01-01T00:00:00Z",
427                "updated_at": "2024-01-01T12:00:00Z",
428                "state": {
429                    "authScheme": "OAUTH2",
430                    "val": {
431                        "status": "expired"
432                    }
433                },
434                "data": {},
435                "params": {},
436                "status_reason": "Refresh token expired",
437                "is_disabled": false
438            },
439            "metadata": {
440                "project_id": "proj_123",
441                "org_id": "org_456"
442            }
443        });
444
445        let event: ConnectionExpiredEvent = serde_json::from_value(json).unwrap();
446        assert_eq!(event.id, "msg_847cdfcd-d219-4f18-a6dd-91acd42ca94a");
447        assert_eq!(event.event_type, "composio.connected_account.expired");
448        assert_eq!(event.data.toolkit.slug, "github");
449        assert_eq!(event.data.user_id, "user_789");
450        assert_eq!(event.data.status, "EXPIRED");
451        assert_eq!(event.metadata.project_id, "proj_123");
452    }
453
454    #[test]
455    fn test_connection_state_val_oauth2() {
456        let json = json!({
457            "status": "active",
458            "access_token": "token_123",
459            "refresh_token": "refresh_456",
460            "token_type": "Bearer",
461            "expires_in": 3600,
462            "scope": "repo user"
463        });
464
465        let state_val: ConnectionStateVal = serde_json::from_value(json).unwrap();
466        assert_eq!(state_val.status, Some("active".to_string()));
467        assert_eq!(state_val.access_token, Some("token_123".to_string()));
468        assert_eq!(state_val.token_type, Some("Bearer".to_string()));
469    }
470
471    #[test]
472    fn test_connection_state_val_api_key() {
473        let json = json!({
474            "status": "active",
475            "api_key": "sk_test_123"
476        });
477
478        let state_val: ConnectionStateVal = serde_json::from_value(json).unwrap();
479        assert_eq!(state_val.api_key, Some("sk_test_123".to_string()));
480    }
481}