claude_code_toolkit/traits/
secrets.rs

1//! Secret management traits
2
3use super::{ Credentials, Secret, SyncResult, Target };
4use crate::error::Result;
5use async_trait::async_trait;
6use std::collections::HashMap;
7
8/// Trait for secret providers (GitHub, GitLab, Azure DevOps, etc.)
9#[async_trait]
10pub trait SecretProvider: Send + Sync {
11  /// Get provider name
12  fn provider_name(&self) -> &str;
13
14  /// Sync secrets to targets
15  async fn sync_secrets(&self, secrets: &[Secret], targets: &[Target]) -> Result<SyncResult>;
16
17  /// Validate access to targets
18  async fn validate_access(&self, targets: &[Target]) -> Result<HashMap<String, bool>>;
19
20  /// List available targets (organizations, repositories, etc.)
21  async fn list_targets(&self, target_type: &str) -> Result<Vec<String>>;
22
23  /// Check if provider is properly configured
24  async fn is_configured(&self) -> Result<bool>;
25}
26
27/// High-level secret management interface
28#[async_trait]
29pub trait SecretManager: Send + Sync {
30  /// Register a secret provider
31  fn register_provider(&mut self, provider: Box<dyn SecretProvider>);
32
33  /// Get provider by name
34  fn get_provider(&self, name: &str) -> Option<&dyn SecretProvider>;
35
36  /// Sync credentials using specified mapping
37  async fn sync_credentials(
38    &self,
39    credentials: &Credentials,
40    mapping: &SecretMapping
41  ) -> Result<SyncResult>;
42
43  /// Sync credentials to specific targets
44  async fn sync_credentials_to_targets(
45    &self,
46    credentials: &Credentials,
47    mapping: &SecretMapping,
48    targets: &[Target]
49  ) -> Result<SyncResult>;
50
51  /// Validate all configured targets
52  async fn validate_targets(&self) -> Result<HashMap<String, bool>>;
53
54  /// List all available providers
55  fn list_providers(&self) -> Vec<&str>;
56}
57
58/// Mapping between credential fields and secret names
59#[derive(Debug, Clone)]
60pub struct SecretMapping {
61  pub schema_name: String,
62  pub mappings: HashMap<String, String>,
63  pub templates: HashMap<String, String>,
64}
65
66impl SecretMapping {
67  pub fn new(schema_name: &str) -> Self {
68    Self {
69      schema_name: schema_name.to_string(),
70      mappings: HashMap::new(),
71      templates: HashMap::new(),
72    }
73  }
74
75  pub fn add_mapping(&mut self, field: &str, secret_name: &str) -> &mut Self {
76    self.mappings.insert(field.to_string(), secret_name.to_string());
77    self
78  }
79
80  pub fn get_secret_name(&self, field: &str) -> Option<&String> {
81    self.mappings.get(field)
82  }
83
84  pub fn to_secrets(&self, credentials: &Credentials) -> Vec<Secret> {
85    let mut secrets = Vec::new();
86
87    // Debug the mappings
88    tracing::debug!("SecretMapping has {} mappings: {:?}", self.mappings.len(), self.mappings);
89
90    // Try both camelCase and snake_case field names for flexibility
91    let access_token_name = self
92      .get_secret_name("accessToken")
93      .or_else(|| self.get_secret_name("access_token"));
94    if let Some(name) = access_token_name {
95      tracing::debug!("Found mapping for access token: {}", name);
96      secrets.push(Secret {
97        name: name.clone(),
98        value: credentials.access_token.clone(),
99        description: Some("Claude AI access token".to_string()),
100      });
101    } else {
102      tracing::debug!("No mapping found for access token");
103    }
104
105    let refresh_token_name = self
106      .get_secret_name("refreshToken")
107      .or_else(|| self.get_secret_name("refresh_token"));
108    if let (Some(token), Some(name)) = (&credentials.refresh_token, refresh_token_name) {
109      tracing::debug!("Found mapping for refresh token: {}", name);
110      secrets.push(Secret {
111        name: name.clone(),
112        value: token.clone(),
113        description: Some("Claude AI refresh token".to_string()),
114      });
115    } else {
116      tracing::debug!("No mapping found for refresh token or token is None");
117    }
118
119    let expires_at_name = self
120      .get_secret_name("expiresAt")
121      .or_else(|| self.get_secret_name("expires_at"));
122    if let (Some(expires), Some(name)) = (credentials.expires_at, expires_at_name) {
123      tracing::debug!("Found mapping for expires at: {}", name);
124      secrets.push(Secret {
125        name: name.clone(),
126        value: expires.to_string(),
127        description: Some("Claude AI token expiry timestamp".to_string()),
128      });
129    } else {
130      tracing::debug!("No mapping found for expires at or expires_at is None");
131    }
132
133    tracing::debug!("Generated {} secrets from credentials", secrets.len());
134    secrets
135  }
136}