1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
mod private
{
use crate::error::{ XaiError, Result };
use secrecy::{ SecretString, ExposeSecret };
use workspace_tools::workspace;
use std::sync::atomic::{ AtomicUsize, Ordering };
/// Global counter tracking the number of times secrets have been exposed.
///
/// Used for security auditing and monitoring. Each call to `expose_secret()`
/// increments this counter.
static EXPOSURE_COUNTER : AtomicUsize = AtomicUsize::new( 0 );
/// Secure wrapper for XAI API key.
///
/// This type wraps a `SecretString` from the `secrecy` crate to prevent
/// accidental exposure of the API key in logs or debug output.
///
/// # Security Features
///
/// - **Automatic Validation**: Enforces XAI API key format requirements
/// - **Secure Storage**: Uses `SecretString` to prevent accidental leaks
/// - **Exposure Auditing**: Tracks calls to `expose_secret()` via atomic counter
/// - **Multi-tier Loading**: Fallback chain from env → workspace → config files
///
/// # API Key Format
///
/// XAI API keys must:
/// - Start with `xai-` prefix
/// - Be at least 10 characters long
///
/// # Examples
///
/// ```no_run
/// use api_xai::Secret;
///
/// // Load from environment variable
/// let secret = Secret::load_from_env( "XAI_API_KEY" )?;
///
/// // Load with automatic fallback chain
/// let secret = Secret::load_with_fallbacks( "XAI_API_KEY" )?;
///
/// // Create from string
/// let secret = Secret::new( "xai-your-key-here".to_string() )?;
/// # Ok::<(), Box< dyn std::error::Error > >(())
/// ```
#[ derive( Debug, Clone ) ]
pub struct Secret( SecretString );
impl Secret
{
/// Creates a new `Secret` from a string, validating the format.
///
/// # Validation
///
/// - Must start with `xai-` prefix
/// - Must be at least 10 characters long
///
/// # Errors
///
/// Returns `XaiError::InvalidApiKey` if validation fails.
///
/// # Examples
///
/// ```no_run
/// use api_xai::Secret;
///
/// let secret = Secret::new( "xai-1234567890".to_string() )?;
/// # Ok::<(), Box< dyn std::error::Error > >(())
/// ```
pub fn new( key : String ) -> Result< Self >
{
Self::validate_format( &key )?;
Ok( Self( SecretString::new( key.into_boxed_str() ) ) )
}
/// Loads the API key from an environment variable.
///
/// # Arguments
///
/// * `env_var` - Name of the environment variable (e.g., `"XAI_API_KEY"`)
///
/// # Errors
///
/// Returns `XaiError::Environment` if the environment variable is not set,
/// or `XaiError::InvalidApiKey` if validation fails.
///
/// # Examples
///
/// ```no_run
/// use api_xai::Secret;
///
/// let secret = Secret::load_from_env( "XAI_API_KEY" )?;
/// # Ok::<(), Box< dyn std::error::Error > >(())
/// ```
pub fn load_from_env( env_var : &str ) -> Result< Self >
{
let key = std::env::var( env_var )
.map_err( |_| XaiError::Environment(
format!( "Environment variable {env_var} not set" )
) )?;
Self::new( key )
}
/// Loads the API key from workspace secrets directory.
///
/// Uses the `workspace_tools` crate to locate and read secrets from
/// the workspace secrets directory (typically `./-secrets/`).
///
/// # Arguments
///
/// * `key_name` - Logical name of the key (e.g., `XAI_API_KEY`)
/// * `filename` - Filename in secrets directory (e.g., "-secrets.sh")
///
/// # Errors
///
/// Returns `XaiError::Environment` if the secret cannot be loaded,
/// or `XaiError::InvalidApiKey` if validation fails.
///
/// # Examples
///
/// ```no_run
/// use api_xai::Secret;
///
/// let secret = Secret::load_from_workspace( "XAI_API_KEY", "-secrets.sh" )?;
/// # Ok::<(), Box< dyn std::error::Error > >(())
/// ```
pub fn load_from_workspace( key_name : &str, filename : &str ) -> Result< Self >
{
let ws = workspace()
.map_err( |e| XaiError::Environment(
format!( "Failed to access workspace : {e}" )
) )?;
let key = ws.load_secret_key( key_name, filename )
.map_err( |e| XaiError::Environment(
format!( "Failed to load from workspace secrets : {e}" )
) )?;
Self::new( key )
}
/// Loads the API key with automatic fallback chain.
///
/// Attempts to load the API key from multiple sources in priority order:
///
/// 1. Workspace secrets file (`-secrets.sh`) - primary workspace pattern
/// 2. Alternative workspace files (`secrets.sh`, `.env`)
/// 3. Environment variable - fallback for CI/deployment
///
/// This is the **recommended** loading method for most use cases.
/// Follows wTools ecosystem conventions by prioritizing `workspace_tools`.
///
/// # Arguments
///
/// * `key_name` - Name of the key (e.g., `XAI_API_KEY`)
///
/// # Errors
///
/// Returns `XaiError::Environment` if the key cannot be loaded from any source,
/// or `XaiError::InvalidApiKey` if validation fails.
///
/// # Examples
///
/// ```no_run
/// use api_xai::Secret;
///
/// // Recommended : tries all sources automatically (workspace-first)
/// let secret = Secret::load_with_fallbacks( "XAI_API_KEY" )?;
/// # Ok::<(), Box< dyn std::error::Error > >(())
/// ```
pub fn load_with_fallbacks( key_name : &str ) -> Result< Self >
{
// Priority 1: Workspace secrets (-secrets.sh) - primary workspace pattern
if let Ok( secret ) = Self::load_from_workspace( key_name, "-secrets.sh" )
{
return Ok( secret );
}
// Priority 2: Alternative workspace file (secrets.sh)
if let Ok( secret ) = Self::load_from_workspace( key_name, "secrets.sh" )
{
return Ok( secret );
}
// Priority 3: .env file
if let Ok( secret ) = Self::load_from_workspace( key_name, ".env" )
{
return Ok( secret );
}
// Priority 4: Environment variable (fallback for CI/deployment)
if let Ok( secret ) = Self::load_from_env( key_name )
{
return Ok( secret );
}
Err( XaiError::Environment(
format!(
"Failed to load {key_name} from any source (-secrets.sh, secrets.sh, .env, env)"
)
).into() )
}
/// Validates the API key format.
///
/// # Validation Rules
///
/// - Must start with `xai-` prefix
/// - Must be at least 10 characters long
///
/// # Errors
///
/// Returns `XaiError::InvalidApiKey` if validation fails.
fn validate_format( key : &str ) -> Result< () >
{
if !key.starts_with( "xai-" )
{
return Err( XaiError::InvalidApiKey(
"XAI API key must start with 'xai-' prefix".to_string()
).into() );
}
if key.len() < 10
{
return Err( XaiError::InvalidApiKey(
"XAI API key too short (minimum 10 characters)".to_string()
).into() );
}
Ok( () )
}
/// Exposes the secret value for use in API requests.
///
/// **WARNING**: This method exposes the secret in plain text. Use sparingly
/// and only when necessary for API authentication.
///
/// Each call increments the global exposure counter for security auditing.
///
/// # Returns
///
/// The plain text API key as a string slice.
///
/// # Examples
///
/// ```no_run
/// use api_xai::Secret;
///
/// let secret = Secret::load_with_fallbacks( "XAI_API_KEY" )?;
///
/// // Use only when necessary (e.g., setting Authorization header)
/// let api_key = secret.expose_secret();
/// # Ok::<(), Box< dyn std::error::Error > >(())
/// ```
pub fn expose_secret( &self ) -> &str
{
EXPOSURE_COUNTER.fetch_add( 1, Ordering::Relaxed );
self.0.expose_secret()
}
/// Returns the number of times secrets have been exposed.
///
/// Used for security auditing and monitoring. Tracks all calls to
/// `expose_secret()` across all `Secret` instances.
///
/// # Examples
///
/// ```no_run
/// use api_xai::Secret;
///
/// let secret = Secret::load_with_fallbacks( "XAI_API_KEY" )?;
/// let initial_count = Secret::exposure_count();
///
/// let _ = secret.expose_secret();
/// assert_eq!( Secret::exposure_count(), initial_count + 1 );
/// # Ok::<(), Box< dyn std::error::Error > >(())
/// ```
pub fn exposure_count() -> usize
{
EXPOSURE_COUNTER.load( Ordering::Relaxed )
}
}
}
crate::mod_interface!
{
exposed use
{
Secret,
};
}