1use secrecy::SecretString;
2use serde::{Deserialize, Serialize};
3use std::collections::HashMap;
4
5#[derive(Debug, Clone, Serialize, Deserialize)]
7pub enum GitLabScope {
8 Project {
10 id: String,
12 },
13 Group {
15 id: String,
17 },
18 Global,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24pub enum GitHubScope {
25 Repository {
27 owner: String,
29 repo: String,
30 },
31 Organization {
33 name: String,
35 },
36 Global,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
42pub enum ClickUpScope {
43 List {
45 id: String,
46 team_id: Option<String>,
48 },
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
53pub enum JiraScope {
54 Project {
56 key: String,
58 },
59 MultiProject { keys: Vec<String> },
61}
62
63#[derive(Debug, Clone, Serialize, Deserialize)]
65pub enum ConfluenceScope {
66 Space {
68 key: Option<String>,
70 },
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75pub enum SlackScope {
76 Workspace { team_id: Option<String> },
78}
79
80#[derive(Debug, Clone)]
86pub enum ConfluenceAuthConfig {
87 BearerToken {
88 token: SecretString,
89 },
90 Basic {
91 username: String,
92 password: SecretString,
93 },
94}
95
96#[derive(Debug, Clone)]
107pub enum ProviderConfig {
108 GitLab {
109 base_url: String,
110 access_token: SecretString,
111 scope: GitLabScope,
112 extra: HashMap<String, serde_json::Value>,
113 },
114 GitHub {
115 base_url: String,
116 access_token: SecretString,
117 scope: GitHubScope,
118 extra: HashMap<String, serde_json::Value>,
119 },
120 ClickUp {
121 access_token: SecretString,
122 scope: ClickUpScope,
123 extra: HashMap<String, serde_json::Value>,
124 },
125 Jira {
126 base_url: String,
127 access_token: SecretString,
128 email: String,
129 scope: JiraScope,
130 flavor: Option<devboy_jira::JiraFlavor>,
133 extra: HashMap<String, serde_json::Value>,
134 },
135 Confluence {
136 base_url: String,
137 auth: ConfluenceAuthConfig,
138 scope: ConfluenceScope,
139 api_version: Option<String>,
140 extra: HashMap<String, serde_json::Value>,
141 },
142 Fireflies {
144 api_key: SecretString,
145 extra: HashMap<String, serde_json::Value>,
146 },
147 Slack {
149 base_url: String,
150 access_token: SecretString,
151 scope: SlackScope,
152 required_scopes: Vec<String>,
153 extra: HashMap<String, serde_json::Value>,
154 },
155 Custom {
157 name: String,
158 config: HashMap<String, serde_json::Value>,
159 },
160}
161
162impl ProviderConfig {
163 pub fn provider_name(&self) -> &str {
165 match self {
166 Self::GitLab { .. } => "gitlab",
167 Self::GitHub { .. } => "github",
168 Self::ClickUp { .. } => "clickup",
169 Self::Jira { .. } => "jira",
170 Self::Confluence { .. } => "confluence",
171 Self::Fireflies { .. } => "fireflies",
172 Self::Slack { .. } => "slack",
173 Self::Custom { name, .. } => name,
174 }
175 }
176}
177
178#[derive(Debug, Clone, Serialize, Deserialize)]
184pub struct ProxyConfig {
185 pub url: String,
186 #[serde(default)]
187 pub headers: HashMap<String, String>,
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize)]
199pub struct ProviderMetadata {
200 pub data: serde_json::Value,
202}
203
204impl ProviderMetadata {
205 pub fn new(data: serde_json::Value) -> Self {
206 Self { data }
207 }
208}
209
210#[derive(Debug, Clone)]
221pub struct AdditionalContext {
222 pub provider: ProviderConfig,
223 pub proxy: Option<ProxyConfig>,
224 pub metadata: Option<ProviderMetadata>,
225 pub extra: HashMap<String, serde_json::Value>,
226}
227
228#[cfg(test)]
229mod tests {
230 use super::*;
231 use secrecy::ExposeSecret;
232
233 fn token(s: &str) -> SecretString {
234 SecretString::from(s.to_string())
235 }
236
237 #[test]
238 fn test_provider_config_gitlab_project_scope() {
239 let config = ProviderConfig::GitLab {
240 base_url: "https://gitlab.com".into(),
241 access_token: token("glpat-xxx"),
242 scope: GitLabScope::Project { id: "12345".into() },
243 extra: HashMap::new(),
244 };
245 assert_eq!(config.provider_name(), "gitlab");
246 }
247
248 #[test]
249 fn test_provider_config_github_repo_scope() {
250 let config = ProviderConfig::GitHub {
251 base_url: "https://api.github.com".into(),
252 access_token: token("ghp_xxx"),
253 scope: GitHubScope::Repository {
254 owner: "meteora-pro".into(),
255 repo: "devboy-tools".into(),
256 },
257 extra: HashMap::new(),
258 };
259 assert_eq!(config.provider_name(), "github");
260 }
261
262 #[test]
263 fn test_provider_config_custom() {
264 let config = ProviderConfig::Custom {
265 name: "my-provider".into(),
266 config: HashMap::new(),
267 };
268 assert_eq!(config.provider_name(), "my-provider");
269 }
270
271 #[test]
272 fn test_provider_config_confluence_scope() {
273 let config = ProviderConfig::Confluence {
274 base_url: "https://wiki.example.com".into(),
275 auth: ConfluenceAuthConfig::BearerToken {
276 token: token("pat-token"),
277 },
278 scope: ConfluenceScope::Space {
279 key: Some("ENG".into()),
280 },
281 api_version: Some("v1".into()),
282 extra: HashMap::new(),
283 };
284 assert_eq!(config.provider_name(), "confluence");
285 }
286
287 #[test]
288 fn test_provider_name_clickup() {
289 let config = ProviderConfig::ClickUp {
290 access_token: token("pk_test"),
291 scope: ClickUpScope::List {
292 id: "list1".into(),
293 team_id: None,
294 },
295 extra: HashMap::new(),
296 };
297 assert_eq!(config.provider_name(), "clickup");
298 }
299
300 #[test]
301 fn test_provider_name_jira() {
302 let config = ProviderConfig::Jira {
303 base_url: "https://jira.example.com".into(),
304 access_token: token("tok"),
305 email: "a@b.com".into(),
306 scope: JiraScope::Project { key: "X".into() },
307 flavor: None,
308 extra: HashMap::new(),
309 };
310 assert_eq!(config.provider_name(), "jira");
311 }
312
313 #[test]
314 fn test_provider_metadata_new() {
315 let data = serde_json::json!({"statuses": [{"name": "Done"}]});
316 let meta = ProviderMetadata::new(data.clone());
317 assert_eq!(meta.data, data);
318 }
319
320 #[test]
321 fn test_proxy_config_serialize_deserialize() {
322 let mut headers = HashMap::new();
325 headers.insert("X-Routing".into(), "internal".into());
326 let proxy = ProxyConfig {
327 url: "https://proxy.internal/jira".into(),
328 headers,
329 };
330 let json = serde_json::to_string(&proxy).unwrap();
331 let deserialized: ProxyConfig = serde_json::from_str(&json).unwrap();
332 assert_eq!(deserialized.url, "https://proxy.internal/jira");
333 assert_eq!(deserialized.headers["X-Routing"], "internal");
334 }
335
336 #[test]
337 fn test_provider_config_debug_redacts_access_token() {
338 let config = ProviderConfig::GitLab {
339 base_url: "https://gitlab.com".into(),
340 access_token: token("super-secret-glpat"),
341 scope: GitLabScope::Project { id: "12345".into() },
342 extra: HashMap::new(),
343 };
344 let dbg = format!("{:?}", config);
345 assert!(
346 !dbg.contains("super-secret-glpat"),
347 "Debug must redact access_token, got: {dbg}"
348 );
349 }
350
351 #[test]
352 fn test_confluence_auth_basic_password_redacted() {
353 let auth = ConfluenceAuthConfig::Basic {
354 username: "dev@example.com".into(),
355 password: token("super-secret-password"),
356 };
357 let dbg = format!("{:?}", auth);
358 assert!(
359 !dbg.contains("super-secret-password"),
360 "Basic password must not appear in Debug: {dbg}"
361 );
362
363 if let ConfluenceAuthConfig::Basic { password, .. } = &auth {
365 assert_eq!(password.expose_secret(), "super-secret-password");
366 }
367 }
368}