Skip to main content

everruns_core/
connection_provider.rs

1// Connection Provider Plugin System
2//
3// Decision: Parallel to IntegrationPlugin, allows integration crates to register
4// connection providers via inventory::submit! without core knowing about them.
5// Decision: Form schema is backend-driven — providers define their own UI fields
6// and instructions, frontend renders generically.
7// Decision: Validation is async — providers can call external APIs to verify credentials.
8
9use async_trait::async_trait;
10use serde::Serialize;
11use std::collections::HashMap;
12use std::sync::Arc;
13
14// ============================================================================
15// Plugin Registration
16// ============================================================================
17
18/// Plugin registration point for connection provider crates.
19///
20/// Integration crates use `inventory::submit!` to register their connection
21/// providers. The server discovers them at runtime to serve form schemas
22/// and handle credential submission.
23///
24/// # Example
25///
26/// ```ignore
27/// inventory::submit! {
28///     ConnectionProviderPlugin {
29///         experimental_only: true,
30///         factory: || Box::new(DaytonaConnectionProvider),
31///     }
32/// }
33/// ```
34pub struct ConnectionProviderPlugin {
35    /// If true, only registered when experimental features are enabled.
36    pub experimental_only: bool,
37    /// Factory function that creates the provider instance.
38    pub factory: fn() -> Box<dyn ConnectionProvider>,
39}
40
41inventory::collect!(ConnectionProviderPlugin);
42
43// ============================================================================
44// ConnectionProvider Trait
45// ============================================================================
46
47/// How the user provides credentials for this connection.
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
49#[serde(rename_all = "snake_case")]
50pub enum ConnectionType {
51    /// OAuth flow (redirect-based, e.g. GitHub)
52    OAuth,
53    /// Direct API key entry (form-based, e.g. Daytona)
54    ApiKey,
55}
56
57/// A connection provider that can validate credentials and describe its UI form.
58#[async_trait]
59pub trait ConnectionProvider: Send + Sync {
60    /// Unique provider identifier (e.g. "daytona"). Must match `user_connections.provider`.
61    fn provider_id(&self) -> &str;
62
63    /// Human-readable name (e.g. "Daytona").
64    fn display_name(&self) -> &str;
65
66    /// Short description for the connections UI.
67    fn description(&self) -> &str;
68
69    /// Lucide icon name (e.g. "cloud", "github").
70    fn icon(&self) -> &str;
71
72    /// Whether this provider uses OAuth or direct API key entry.
73    fn connection_type(&self) -> ConnectionType;
74
75    /// Form schema for API key providers. OAuth providers return None.
76    fn form_schema(&self) -> Option<ConnectionFormSchema>;
77
78    /// Validate a credential before saving. Called for API key providers.
79    /// Returns Ok with optional metadata on success, Err with user-facing message on failure.
80    async fn validate(&self, credential: &str) -> Result<ConnectionValidation, String>;
81
82    /// Validate with all form fields. Default delegates to `validate()` using the `api_key` field.
83    /// Override this when the provider needs extra fields (e.g. org slug for personal tokens).
84    async fn validate_fields(
85        &self,
86        fields: &HashMap<String, String>,
87    ) -> Result<ConnectionValidation, String> {
88        let api_key = fields.get("api_key").map(|s| s.as_str()).unwrap_or("");
89        self.validate(api_key).await
90    }
91}
92
93// ============================================================================
94// ConnectionProvider Registry
95// ============================================================================
96
97/// Registry of connection providers available to a platform.
98///
99/// This is the explicit counterpart to `ConnectionProviderPlugin`. Inventory-based
100/// discovery remains useful for the default OSS platform, but embedders need a
101/// concrete registry they can edit before handing the platform to the server.
102#[derive(Clone, Default)]
103pub struct ConnectionProviderRegistry {
104    providers: HashMap<String, Arc<dyn ConnectionProvider>>,
105}
106
107impl ConnectionProviderRegistry {
108    /// Create an empty provider registry.
109    pub fn new() -> Self {
110        Self {
111            providers: HashMap::new(),
112        }
113    }
114
115    /// Register a connection provider.
116    ///
117    /// If a provider with the same `provider_id()` already exists, it is replaced.
118    pub fn register(&mut self, provider: impl ConnectionProvider + 'static) {
119        self.providers
120            .insert(provider.provider_id().to_string(), Arc::new(provider));
121    }
122
123    /// Register a boxed connection provider.
124    pub fn register_boxed(&mut self, provider: Box<dyn ConnectionProvider>) {
125        self.providers
126            .insert(provider.provider_id().to_string(), Arc::from(provider));
127    }
128
129    /// Register an `Arc`-wrapped connection provider.
130    pub fn register_arc(&mut self, provider: Arc<dyn ConnectionProvider>) {
131        self.providers
132            .insert(provider.provider_id().to_string(), provider);
133    }
134
135    /// Remove a provider by ID.
136    pub fn unregister(&mut self, provider_id: &str) -> Option<Arc<dyn ConnectionProvider>> {
137        self.providers.remove(provider_id)
138    }
139
140    /// Get a provider by ID.
141    pub fn get(&self, provider_id: &str) -> Option<&Arc<dyn ConnectionProvider>> {
142        self.providers.get(provider_id)
143    }
144
145    /// Check whether a provider is registered.
146    pub fn has(&self, provider_id: &str) -> bool {
147        self.providers.contains_key(provider_id)
148    }
149
150    /// List all registered providers.
151    pub fn list(&self) -> Vec<&Arc<dyn ConnectionProvider>> {
152        self.providers.values().collect()
153    }
154
155    /// Number of registered providers.
156    pub fn len(&self) -> usize {
157        self.providers.len()
158    }
159
160    /// Whether the registry is empty.
161    pub fn is_empty(&self) -> bool {
162        self.providers.is_empty()
163    }
164
165    /// Create a builder for fluent registration.
166    pub fn builder() -> ConnectionProviderRegistryBuilder {
167        ConnectionProviderRegistryBuilder::new()
168    }
169}
170
171impl std::fmt::Debug for ConnectionProviderRegistry {
172    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
173        let ids: Vec<_> = self.providers.keys().collect();
174        f.debug_struct("ConnectionProviderRegistry")
175            .field("providers", &ids)
176            .finish()
177    }
178}
179
180/// Builder for creating a `ConnectionProviderRegistry`.
181pub struct ConnectionProviderRegistryBuilder {
182    registry: ConnectionProviderRegistry,
183}
184
185impl ConnectionProviderRegistryBuilder {
186    /// Create a new empty builder.
187    pub fn new() -> Self {
188        Self {
189            registry: ConnectionProviderRegistry::new(),
190        }
191    }
192
193    /// Add a connection provider to the registry.
194    pub fn provider(mut self, provider: impl ConnectionProvider + 'static) -> Self {
195        self.registry.register(provider);
196        self
197    }
198
199    /// Build the registry.
200    pub fn build(self) -> ConnectionProviderRegistry {
201        self.registry
202    }
203}
204
205impl Default for ConnectionProviderRegistryBuilder {
206    fn default() -> Self {
207        Self::new()
208    }
209}
210
211// ============================================================================
212// Form Schema Types
213// ============================================================================
214
215/// Describes the form fields and instructions for an API key connection.
216#[derive(Debug, Clone, Serialize)]
217pub struct ConnectionFormSchema {
218    /// Input fields to render.
219    pub fields: Vec<FormField>,
220    /// Markdown instructions shown above the form (how to get the key, etc.).
221    pub instructions_markdown: String,
222}
223
224/// A single form field.
225#[derive(Debug, Clone, Serialize)]
226pub struct FormField {
227    /// Field name used as the key when submitting (e.g. "api_key").
228    pub name: String,
229    /// Label shown next to the input.
230    pub label: String,
231    /// Input type.
232    pub field_type: FieldType,
233    /// Whether the field is required.
234    pub required: bool,
235    /// Placeholder text inside the input.
236    #[serde(skip_serializing_if = "Option::is_none")]
237    pub placeholder: Option<String>,
238    /// Help text shown below the input.
239    #[serde(skip_serializing_if = "Option::is_none")]
240    pub help_text: Option<String>,
241}
242
243/// Input field type for rendering.
244#[derive(Debug, Clone, Copy, Serialize)]
245#[serde(rename_all = "snake_case")]
246pub enum FieldType {
247    /// Masked password/secret input.
248    Password,
249    /// Plain text input.
250    Text,
251    /// URL input.
252    Url,
253}
254
255/// Result of credential validation.
256#[derive(Debug, Clone)]
257pub struct ConnectionValidation {
258    /// Display name from the provider (e.g. organization name, username).
259    pub provider_username: Option<String>,
260    /// Provider-specific metadata to store alongside the connection (e.g. org slug).
261    /// Stored as JSONB in the database and returned via the connection resolver.
262    pub provider_metadata: Option<serde_json::Value>,
263}