1use std::collections::HashMap;
2use std::sync::Arc;
3
4use pipedash_plugin_api::{
5 Plugin as PluginTrait,
6 PluginRegistry,
7};
8use tokio::sync::{
9 Mutex,
10 RwLock,
11};
12
13use crate::domain::{
14 DomainError,
15 DomainResult,
16 FetchStatus,
17 Provider,
18 ProviderConfig,
19 ProviderSummary,
20};
21use crate::event::{
22 CoreEvent,
23 EventBus,
24};
25use crate::infrastructure::config::token_ref::TokenReference;
26use crate::infrastructure::database::Repository;
27use crate::infrastructure::providers::PluginAdapter;
28use crate::plugins;
29
30pub struct ProviderService {
31 repository: Arc<Repository>,
32 http_client_manager: Arc<crate::infrastructure::HttpClientManager>,
33 providers: Arc<RwLock<HashMap<i64, Arc<dyn Provider>>>>,
34 plugin_registry: Arc<PluginRegistry>,
35 parameter_fetches: Arc<Mutex<HashMap<String, Arc<Mutex<()>>>>>,
36 event_bus: Arc<dyn EventBus>,
37}
38
39impl ProviderService {
40 pub fn new(
41 repository: Arc<Repository>,
42 http_client_manager: Arc<crate::infrastructure::HttpClientManager>,
43 event_bus: Arc<dyn EventBus>,
44 ) -> Self {
45 let plugin_registry = plugins::create_plugin_registry();
46
47 Self {
48 repository,
49 http_client_manager,
50 providers: Arc::new(RwLock::new(HashMap::new())),
51 plugin_registry: Arc::new(plugin_registry),
52 parameter_fetches: Arc::new(Mutex::new(HashMap::new())),
53 event_bus,
54 }
55 }
56
57 pub fn repository(&self) -> &Arc<Repository> {
58 &self.repository
59 }
60
61 pub fn list_available_plugins(&self) -> Vec<pipedash_plugin_api::PluginMetadata> {
62 let mut metadata_list = Vec::new();
63 for provider_type in self.plugin_registry.provider_types() {
64 if let Some(plugin) = self.plugin_registry.get(&provider_type) {
65 metadata_list.push(plugin.metadata().clone());
66 }
67 }
68 metadata_list
69 }
70
71 pub fn create_uninitialized_plugin(
72 &self, provider_type: &str,
73 ) -> DomainResult<Box<dyn PluginTrait>> {
74 plugins::create_plugin(provider_type).ok_or_else(|| {
75 DomainError::InvalidProviderType(format!("Unknown provider type: {}", provider_type))
76 })
77 }
78
79 pub async fn add_provider(&self, config: ProviderConfig) -> DomainResult<i64> {
80 let mut plugin = self.create_uninitialized_plugin(&config.provider_type)?;
81
82 let mut plugin_config = config.config.clone();
83 plugin_config.insert("token".to_string(), config.token.clone());
84
85 let http_client = if let Some(base_url) = plugin_config.get("base_url") {
86 self.http_client_manager.client_for_url(base_url)?
87 } else {
88 self.http_client_manager.default_client()
89 };
90
91 plugin
92 .initialize(0, plugin_config.clone(), Some(http_client))
93 .map_err(|e| DomainError::InvalidConfig(format!("Failed to initialize plugin: {e}")))?;
94
95 let (validation_result, permission_status) =
96 tokio::join!(plugin.validate_credentials(), plugin.check_permissions());
97
98 validation_result.map_err(|e| {
99 DomainError::InvalidConfig(format!("Credential validation failed: {e}"))
100 })?;
101
102 let provider_type = plugin.provider_type();
103 if provider_type != config.provider_type {
104 return Err(DomainError::InvalidConfig(format!(
105 "Provider type mismatch: expected '{}', got '{}'",
106 config.provider_type, provider_type
107 )));
108 }
109
110 let permission_status = permission_status.ok();
111
112 let id = self.repository.add_provider(&config).await?;
113
114 if let Some(ref status) = permission_status {
115 if let Err(e) = self.repository.store_provider_permissions(id, status).await {
116 tracing::warn!(provider_id = id, error = %e, "Failed to store permissions");
117 }
118 }
119
120 let mut config_with_id = config.clone();
121 config_with_id.id = Some(id);
122 let provider = self.create_provider(&config_with_id)?;
123
124 let mut providers = self.providers.write().await;
125 providers.insert(id, provider);
126 drop(providers);
127
128 let fresh_config = self.repository.get_provider(id).await?;
129 let cached_pipelines = self.repository.get_cached_pipelines(Some(id)).await?;
130 let pipeline_count = cached_pipelines.len();
131 let last_updated = cached_pipelines.iter().map(|p| p.last_updated).max();
132
133 let icon = self
134 .plugin_registry
135 .get(&fresh_config.provider_type)
136 .and_then(|plugin| plugin.metadata().icon.clone());
137
138 let configured_repositories = fresh_config
139 .config
140 .get("selected_items")
141 .map(|items| {
142 if fresh_config.provider_type == "argocd" {
143 return Vec::new();
144 }
145
146 items
147 .split(',')
148 .map(|s| s.trim().to_string())
149 .filter(|s| !s.is_empty())
150 .map(|item| {
151 if fresh_config.provider_type == "tekton" && item.contains("__") {
152 item.replace("__", "/")
153 } else if fresh_config.provider_type == "jenkins" && !item.contains('/') {
154 format!("(root)/{}", item)
155 } else {
156 item
157 }
158 })
159 .collect()
160 })
161 .unwrap_or_default();
162
163 const FETCH_STATUS_SUCCESS: &str = "success";
164 const FETCH_STATUS_ERROR: &str = "error";
165 const FETCH_STATUS_NEVER: &str = "never";
166
167 let (fetch_status_str, last_fetch_error, last_fetch_at) = self
168 .repository
169 .get_provider_fetch_status(id)
170 .await
171 .unwrap_or_else(|_| (FETCH_STATUS_NEVER.to_string(), None, None));
172
173 let fetch_status_enum = match fetch_status_str.as_str() {
174 FETCH_STATUS_SUCCESS => FetchStatus::Success,
175 FETCH_STATUS_ERROR => FetchStatus::Error,
176 _ => FetchStatus::Never,
177 };
178
179 let last_fetch_at_parsed = last_fetch_at.and_then(|s| {
180 chrono::DateTime::parse_from_rfc3339(&s)
181 .ok()
182 .map(|dt| dt.with_timezone(&chrono::Utc))
183 });
184
185 let provider_summary = ProviderSummary {
186 id,
187 name: fresh_config.display_name().to_string(),
188 provider_type: fresh_config.provider_type,
189 icon,
190 pipeline_count,
191 last_updated,
192 refresh_interval: fresh_config.refresh_interval,
193 configured_repositories,
194 last_fetch_status: fetch_status_enum,
195 last_fetch_error,
196 last_fetch_at: last_fetch_at_parsed,
197 version: fresh_config.version.unwrap_or(1),
198 };
199
200 let timestamp = std::time::SystemTime::now()
201 .duration_since(std::time::UNIX_EPOCH)
202 .unwrap()
203 .as_millis() as i64;
204
205 self.event_bus
206 .emit(CoreEvent::ProviderAdded {
207 provider: provider_summary,
208 timestamp,
209 })
210 .await;
211
212 Ok(id)
213 }
214
215 pub async fn get_provider_config(&self, id: i64) -> DomainResult<ProviderConfig> {
216 self.repository.get_provider(id).await
217 }
218
219 pub async fn list_providers(&self) -> DomainResult<Vec<ProviderSummary>> {
220 let configs = self.repository.list_providers().await?;
221 let mut summaries = Vec::new();
222
223 for config in configs {
224 let cached_pipelines = self.repository.get_cached_pipelines(config.id).await?;
225 let pipeline_count = cached_pipelines.len();
226 let last_updated = cached_pipelines.iter().map(|p| p.last_updated).max();
227
228 let icon = self
229 .plugin_registry
230 .get(&config.provider_type)
231 .and_then(|plugin| plugin.metadata().icon.clone());
232
233 let configured_repositories = config
234 .config
235 .get("selected_items")
236 .map(|items| {
237 if config.provider_type == "argocd" {
238 return Vec::new();
239 }
240
241 items
242 .split(',')
243 .map(|s| s.trim().to_string())
244 .filter(|s| !s.is_empty())
245 .map(|item| {
246 if config.provider_type == "tekton" && item.contains("__") {
247 item.replace("__", "/")
248 } else if config.provider_type == "jenkins" && !item.contains('/') {
249 format!("(root)/{}", item)
250 } else {
251 item
252 }
253 })
254 .collect()
255 })
256 .unwrap_or_default();
257
258 let provider_id = config.id.unwrap();
259 const FETCH_STATUS_SUCCESS: &str = "success";
260 const FETCH_STATUS_ERROR: &str = "error";
261 const FETCH_STATUS_NEVER: &str = "never";
262
263 let (fetch_status, last_fetch_error, last_fetch_at) = self
264 .repository
265 .get_provider_fetch_status(provider_id)
266 .await
267 .unwrap_or_else(|_| (FETCH_STATUS_NEVER.to_string(), None, None));
268
269 let fetch_status_enum = match fetch_status.as_str() {
270 FETCH_STATUS_SUCCESS => FetchStatus::Success,
271 FETCH_STATUS_ERROR => FetchStatus::Error,
272 _ => FetchStatus::Never,
273 };
274
275 let last_fetch_at_parsed = last_fetch_at.and_then(|s| {
276 chrono::DateTime::parse_from_rfc3339(&s)
277 .ok()
278 .map(|dt| dt.with_timezone(&chrono::Utc))
279 });
280
281 summaries.push(ProviderSummary {
282 id: provider_id,
283 name: config.display_name().to_string(),
284 provider_type: config.provider_type,
285 icon,
286 pipeline_count,
287 last_updated,
288 refresh_interval: config.refresh_interval,
289 configured_repositories,
290 last_fetch_status: fetch_status_enum,
291 last_fetch_error,
292 last_fetch_at: last_fetch_at_parsed,
293 version: config.version.unwrap_or(1),
294 });
295 }
296
297 Ok(summaries)
298 }
299
300 pub async fn update_provider(&self, id: i64, config: ProviderConfig) -> DomainResult<()> {
301 let mut plugin = self.create_uninitialized_plugin(&config.provider_type)?;
302
303 let mut plugin_config = config.config.clone();
304 plugin_config.insert("token".to_string(), config.token.clone());
305
306 let http_client = if let Some(base_url) = plugin_config.get("base_url") {
307 self.http_client_manager.client_for_url(base_url)?
308 } else {
309 self.http_client_manager.default_client()
310 };
311
312 plugin
313 .initialize(id, plugin_config.clone(), Some(http_client))
314 .map_err(|e| DomainError::InvalidConfig(format!("Failed to initialize plugin: {e}")))?;
315
316 plugin.validate_credentials().await.map_err(|e| {
317 DomainError::InvalidConfig(format!("Credential validation failed: {e}"))
318 })?;
319
320 let permission_status = plugin.check_permissions().await.ok();
321
322 let current = self.repository.get_provider(id).await?;
323 let current_version = current.version.unwrap_or(1);
324
325 let success = self
326 .repository
327 .update_provider_with_version(id, &config, current_version)
328 .await?;
329
330 if !success {
331 return Err(DomainError::ConcurrentModification(format!(
332 "Provider {} was modified by another client",
333 id
334 )));
335 }
336
337 if let Some(ref status) = permission_status {
338 if let Err(e) = self.repository.store_provider_permissions(id, status).await {
339 tracing::warn!(provider_id = id, error = %e, "Failed to store permissions");
340 }
341 }
342
343 let mut config_with_id = config.clone();
344 config_with_id.id = Some(id);
345 let new_provider = self.create_provider(&config_with_id)?;
346
347 let providers = Arc::clone(&self.providers);
348 let old_provider = {
349 let mut map = providers.write().await;
350 map.insert(id, new_provider)
351 };
352
353 if old_provider.is_some() {
354 tokio::spawn(async move {
355 drop(old_provider);
356 tracing::debug!(
357 provider_id = id,
358 "Old provider cleanup completed after update"
359 );
360 });
361 }
362
363 let fresh_config = self.repository.get_provider(id).await?;
364 let cached_pipelines = self.repository.get_cached_pipelines(Some(id)).await?;
365 let pipeline_count = cached_pipelines.len();
366 let last_updated = cached_pipelines.iter().map(|p| p.last_updated).max();
367
368 let icon = self
369 .plugin_registry
370 .get(&fresh_config.provider_type)
371 .and_then(|plugin| plugin.metadata().icon.clone());
372
373 let configured_repositories = fresh_config
374 .config
375 .get("selected_items")
376 .map(|items| {
377 if fresh_config.provider_type == "argocd" {
378 return Vec::new();
379 }
380
381 items
382 .split(',')
383 .map(|s| s.trim().to_string())
384 .filter(|s| !s.is_empty())
385 .map(|item| {
386 if fresh_config.provider_type == "tekton" && item.contains("__") {
387 item.replace("__", "/")
388 } else if fresh_config.provider_type == "jenkins" && !item.contains('/') {
389 format!("(root)/{}", item)
390 } else {
391 item
392 }
393 })
394 .collect()
395 })
396 .unwrap_or_default();
397
398 const FETCH_STATUS_SUCCESS: &str = "success";
399 const FETCH_STATUS_ERROR: &str = "error";
400 const FETCH_STATUS_NEVER: &str = "never";
401
402 let (fetch_status_str, last_fetch_error, last_fetch_at) = self
403 .repository
404 .get_provider_fetch_status(id)
405 .await
406 .unwrap_or_else(|_| (FETCH_STATUS_NEVER.to_string(), None, None));
407
408 let fetch_status_enum = match fetch_status_str.as_str() {
409 FETCH_STATUS_SUCCESS => FetchStatus::Success,
410 FETCH_STATUS_ERROR => FetchStatus::Error,
411 _ => FetchStatus::Never,
412 };
413
414 let last_fetch_at_parsed = last_fetch_at.and_then(|s| {
415 chrono::DateTime::parse_from_rfc3339(&s)
416 .ok()
417 .map(|dt| dt.with_timezone(&chrono::Utc))
418 });
419
420 let provider_summary = ProviderSummary {
421 id,
422 name: fresh_config.display_name().to_string(),
423 provider_type: fresh_config.provider_type,
424 icon,
425 pipeline_count,
426 last_updated,
427 refresh_interval: fresh_config.refresh_interval,
428 configured_repositories,
429 last_fetch_status: fetch_status_enum,
430 last_fetch_error,
431 last_fetch_at: last_fetch_at_parsed,
432 version: fresh_config.version.unwrap_or(1),
433 };
434
435 let timestamp = std::time::SystemTime::now()
436 .duration_since(std::time::UNIX_EPOCH)
437 .unwrap()
438 .as_millis() as i64;
439
440 self.event_bus
441 .emit(CoreEvent::ProviderUpdated {
442 provider: provider_summary,
443 timestamp,
444 })
445 .await;
446
447 Ok(())
448 }
449
450 pub async fn update_provider_refresh_interval(
451 &self, id: i64, refresh_interval: i64,
452 ) -> DomainResult<()> {
453 let mut config = self.repository.get_provider(id).await?;
454 config.refresh_interval = refresh_interval;
455 self.repository.update_provider(id, &config).await?;
456 Ok(())
457 }
458
459 pub async fn remove_provider(&self, id: i64) -> DomainResult<()> {
460 let fresh_config = self.repository.get_provider(id).await?;
461 let cached_pipelines = self.repository.get_cached_pipelines(Some(id)).await?;
462 let pipeline_count = cached_pipelines.len();
463 let last_updated = cached_pipelines.iter().map(|p| p.last_updated).max();
464
465 let icon = self
466 .plugin_registry
467 .get(&fresh_config.provider_type)
468 .and_then(|plugin| plugin.metadata().icon.clone());
469
470 let configured_repositories = fresh_config
471 .config
472 .get("selected_items")
473 .map(|items| {
474 if fresh_config.provider_type == "argocd" {
475 return Vec::new();
476 }
477
478 items
479 .split(',')
480 .map(|s| s.trim().to_string())
481 .filter(|s| !s.is_empty())
482 .map(|item| {
483 if fresh_config.provider_type == "tekton" && item.contains("__") {
484 item.replace("__", "/")
485 } else if fresh_config.provider_type == "jenkins" && !item.contains('/') {
486 format!("(root)/{}", item)
487 } else {
488 item
489 }
490 })
491 .collect()
492 })
493 .unwrap_or_default();
494
495 const FETCH_STATUS_SUCCESS: &str = "success";
496 const FETCH_STATUS_ERROR: &str = "error";
497 const FETCH_STATUS_NEVER: &str = "never";
498
499 let (fetch_status_str, last_fetch_error, last_fetch_at) = self
500 .repository
501 .get_provider_fetch_status(id)
502 .await
503 .unwrap_or_else(|_| (FETCH_STATUS_NEVER.to_string(), None, None));
504
505 let fetch_status_enum = match fetch_status_str.as_str() {
506 FETCH_STATUS_SUCCESS => FetchStatus::Success,
507 FETCH_STATUS_ERROR => FetchStatus::Error,
508 _ => FetchStatus::Never,
509 };
510
511 let last_fetch_at_parsed = last_fetch_at.and_then(|s| {
512 chrono::DateTime::parse_from_rfc3339(&s)
513 .ok()
514 .map(|dt| dt.with_timezone(&chrono::Utc))
515 });
516
517 let provider_summary = ProviderSummary {
518 id,
519 name: fresh_config.display_name().to_string(),
520 provider_type: fresh_config.provider_type,
521 icon,
522 pipeline_count,
523 last_updated,
524 refresh_interval: fresh_config.refresh_interval,
525 configured_repositories,
526 last_fetch_status: fetch_status_enum,
527 last_fetch_error,
528 last_fetch_at: last_fetch_at_parsed,
529 version: fresh_config.version.unwrap_or(1),
530 };
531
532 let pipelines = cached_pipelines;
533
534 self.repository.remove_provider(id).await?;
535
536 let providers = Arc::clone(&self.providers);
537 tokio::spawn(async move {
538 let mut map = providers.write().await;
539 map.remove(&id);
540 tracing::debug!(provider_id = id, "Provider cleanup completed");
541 });
542
543 let mut fetches = self.parameter_fetches.lock().await;
544 for pipeline in pipelines {
545 fetches.remove(&pipeline.id);
546 }
547
548 let timestamp = std::time::SystemTime::now()
549 .duration_since(std::time::UNIX_EPOCH)
550 .unwrap()
551 .as_millis() as i64;
552
553 self.event_bus
554 .emit(CoreEvent::ProviderRemoved {
555 provider: provider_summary,
556 timestamp,
557 })
558 .await;
559
560 Ok(())
561 }
562
563 pub async fn get_provider(&self, id: i64) -> DomainResult<Arc<dyn Provider>> {
564 let providers = self.providers.read().await;
565
566 if let Some(provider) = providers.get(&id) {
567 return Ok(Arc::clone(provider));
568 }
569
570 drop(providers);
571
572 let config = self.repository.get_provider(id).await?;
573 let provider = self.create_provider(&config)?;
574
575 let mut providers = self.providers.write().await;
576 providers.insert(id, Arc::clone(&provider));
577
578 Ok(provider)
579 }
580
581 pub async fn get_provider_instance(&self, id: i64) -> DomainResult<Arc<dyn Provider>> {
582 self.get_provider(id).await
583 }
584
585 pub async fn load_all_providers(&self) -> DomainResult<()> {
586 let configs = self.repository.list_providers().await?;
587 let mut old_providers_to_cleanup = Vec::new();
588
589 for config in configs {
590 if let Some(id) = config.id {
591 if config.token.is_empty() {
592 tracing::debug!(
593 provider_id = id,
594 provider_type = %config.provider_type,
595 "Skipping provider load - token not available yet (vault initializing in background)"
596 );
597 continue;
598 }
599
600 match self.create_provider(&config) {
601 Ok(provider) => {
602 let mut providers = self.providers.write().await;
603 if let Some(old) = providers.insert(id, provider) {
604 old_providers_to_cleanup.push((id, old));
605 }
606 }
607 Err(e) => {
608 tracing::warn!(provider_id = id, error = %e, "Failed to load provider");
609 }
610 }
611 }
612 }
613
614 if !old_providers_to_cleanup.is_empty() {
615 tokio::spawn(async move {
616 for (id, provider) in old_providers_to_cleanup {
617 drop(provider);
618 tracing::debug!(
619 provider_id = id,
620 "Old provider cleanup completed after reload"
621 );
622 }
623 });
624 }
625
626 Ok(())
627 }
628
629 fn create_provider(&self, config: &ProviderConfig) -> DomainResult<Arc<dyn Provider>> {
630 if !self.plugin_registry.is_registered(&config.provider_type) {
631 return Err(DomainError::InvalidProviderType(format!(
632 "Plugin not found for provider type: {}",
633 config.provider_type
634 )));
635 }
636
637 let mut plugin = self.create_uninitialized_plugin(&config.provider_type)?;
638
639 let provider_id = config.id.unwrap_or(0);
640 let mut plugin_config = config.config.clone();
641
642 let resolved_token = if config.token.is_empty() {
643 String::new()
644 } else {
645 match TokenReference::parse(&config.token) {
646 Ok(TokenReference::EnvVar(var_name)) => {
647 std::env::var(&var_name).unwrap_or_else(|_| {
648 tracing::warn!(
649 provider_id = provider_id,
650 var_name = %var_name,
651 "Environment variable not found, using empty token"
652 );
653 String::new()
654 })
655 }
656 Ok(TokenReference::None) => String::new(),
657 Ok(_) => config.token.clone(),
658 Err(_) => config.token.clone(),
659 }
660 };
661 plugin_config.insert("token".to_string(), resolved_token);
662
663 let http_client = if let Some(base_url) = plugin_config.get("base_url") {
664 self.http_client_manager.client_for_url(base_url)?
665 } else {
666 self.http_client_manager.default_client()
667 };
668
669 plugin
670 .initialize(provider_id, plugin_config, Some(http_client))
671 .map_err(|e| DomainError::InvalidConfig(format!("Failed to initialize plugin: {e}")))?;
672
673 let adapter = PluginAdapter::new(plugin, config.provider_type.clone(), provider_id);
674 Ok(Arc::new(adapter))
675 }
676
677 pub async fn get_workflow_parameters(
678 &self, provider_id: i64, workflow_id: &str,
679 ) -> DomainResult<Vec<pipedash_plugin_api::WorkflowParameter>> {
680 if let Ok(Some(cached)) = self
681 .repository
682 .get_cached_workflow_parameters(workflow_id)
683 .await
684 {
685 return Ok(cached);
686 }
687
688 let fetch_lock = {
689 let mut fetches = self.parameter_fetches.lock().await;
690 fetches
691 .entry(workflow_id.to_string())
692 .or_insert_with(|| Arc::new(Mutex::new(())))
693 .clone()
694 };
695
696 let _guard = fetch_lock.lock().await;
697
698 if let Ok(Some(cached)) = self
699 .repository
700 .get_cached_workflow_parameters(workflow_id)
701 .await
702 {
703 return Ok(cached);
704 }
705
706 let providers = self.providers.read().await;
707 let provider = providers.get(&provider_id).ok_or_else(|| {
708 DomainError::ProviderNotFound(format!("Provider {provider_id} not found"))
709 })?;
710
711 let parameters = provider
712 .get_workflow_parameters(workflow_id)
713 .await
714 .map_err(|e| {
715 DomainError::InvalidConfig(format!("Failed to get workflow parameters: {e}"))
716 })?;
717
718 let _ = self
719 .repository
720 .cache_workflow_parameters(workflow_id, ¶meters)
721 .await;
722
723 {
724 let mut fetches = self.parameter_fetches.lock().await;
725 fetches.remove(workflow_id);
726 }
727
728 Ok(parameters)
729 }
730
731 pub async fn get_provider_permissions(
732 &self, provider_id: i64,
733 ) -> DomainResult<Option<pipedash_plugin_api::PermissionStatus>> {
734 self.repository.get_provider_permissions(provider_id).await
735 }
736
737 pub async fn recheck_provider_permissions(
738 &self, provider_id: i64,
739 ) -> DomainResult<pipedash_plugin_api::PermissionStatus> {
740 let config = self.repository.get_provider(provider_id).await?;
741 let mut plugin = self.create_uninitialized_plugin(&config.provider_type)?;
742
743 let mut plugin_config = config.config.clone();
744 plugin_config.insert("token".to_string(), config.token.clone());
745
746 let http_client = if let Some(base_url) = plugin_config.get("base_url") {
747 self.http_client_manager.client_for_url(base_url)?
748 } else {
749 self.http_client_manager.default_client()
750 };
751
752 plugin
753 .initialize(provider_id, plugin_config, Some(http_client))
754 .map_err(|e| DomainError::InvalidConfig(format!("Failed to initialize plugin: {e}")))?;
755
756 let status = plugin
757 .check_permissions()
758 .await
759 .map_err(|e| DomainError::InvalidConfig(format!("Failed to check permissions: {e}")))?;
760
761 self.repository
762 .store_provider_permissions(provider_id, &status)
763 .await?;
764
765 Ok(status)
766 }
767}