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}