providers_kit/config/credentials.rs
1use std::fmt;
2
3use reqwest::header::HeaderValue;
4use secrecy::{ExposeSecret, SecretString};
5
6use crate::KitError;
7use crate::error::ValidationViolation;
8
9/// Validated API key used by provider configuration.
10///
11/// The key is a nonempty, syntax-valid secret; construction neither authenticates
12/// it nor proves provider permissions. Empty values,
13/// whitespace-only values, and values with leading or trailing whitespace are
14/// rejected at construction. Values must also be valid HTTP header values;
15/// embedded control characters such as newlines are rejected.
16///
17/// # Examples
18/// ```rust
19/// let key = providers_kit::config::ApiKey::new("sk-test")?;
20/// assert!(!format!("{key:?}").contains("sk-test"));
21///
22/// # Ok::<(), providers_kit::KitError>(())
23/// ```
24#[derive(Clone)]
25pub struct ApiKey {
26 secret: SecretString,
27}
28
29impl ApiKey {
30 /// Validates and stores an API key.
31 ///
32 /// # Errors
33 /// Returns a configuration error when the secret is empty, whitespace-only,
34 /// contains leading or trailing whitespace, or is not a valid HTTP header value.
35 ///
36 /// # Examples
37 /// ```rust
38 /// let key = providers_kit::config::ApiKey::new("sk-test")?;
39 /// assert!(!format!("{key:?}").contains("sk-test"));
40 /// assert!(providers_kit::config::ApiKey::new(" padded ").is_err());
41 /// assert!(providers_kit::config::ApiKey::new("invalid\nheader").is_err());
42 ///
43 /// # Ok::<(), providers_kit::KitError>(())
44 /// ```
45 pub fn new(secret: impl Into<String>) -> Result<Self, KitError> {
46 let secret = SecretString::new(secret.into().into());
47 let exposed = secret.expose_secret();
48 if exposed.trim().is_empty() {
49 return Err(KitError::config(
50 ValidationViolation::Empty {
51 field: "credentials.api_key",
52 },
53 "API key must not be empty",
54 ));
55 }
56 if exposed != exposed.trim() {
57 return Err(KitError::config(
58 ValidationViolation::InvalidFormat {
59 field: "credentials.api_key",
60 rule: "must be unpadded",
61 },
62 "API key must not contain leading or trailing whitespace",
63 ));
64 }
65 if HeaderValue::try_from(exposed).is_err() {
66 return Err(KitError::config(
67 ValidationViolation::InvalidFormat {
68 field: "credentials.api_key",
69 rule: "valid HTTP header value",
70 },
71 "API key must be a valid HTTP header value",
72 ));
73 }
74 Ok(Self { secret })
75 }
76
77 pub(crate) fn as_secret(&self) -> &SecretString {
78 &self.secret
79 }
80}
81
82impl fmt::Debug for ApiKey {
83 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
84 f.debug_struct("ApiKey")
85 .field("secret", &"<redacted>")
86 .finish()
87 }
88}