Skip to main content

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}