Skip to main content

claude_api/
auth.rs

1//! Authentication primitives and the [`RequestSigner`] hook.
2//!
3//! [`ApiKey`] wraps a secret with a redacting [`Debug`] impl. The
4//! [`RequestSigner`] trait is the extension point: every outbound request
5//! is handed to a signer just before transmission. Default behavior is
6//! [`ApiKeySigner`] (adds `x-api-key`); behind the `bedrock` feature,
7//! [`BedrockSigner`](crate::bedrock::BedrockSigner) signs requests with
8//! AWS sigv4. A custom signer can be installed via
9//! [`ClientBuilder::signer`](crate::ClientBuilder::signer).
10
11use std::fmt;
12
13/// Anthropic API key.
14///
15/// Wraps the underlying string and redacts it from [`Debug`] output to
16/// reduce the chance of leaking the key into logs or panic messages.
17///
18/// ```
19/// use claude_api::auth::ApiKey;
20/// let k = ApiKey::new("sk-ant-secretvalue");
21/// // Debug output never includes the secret bytes.
22/// let dbg = format!("{k:?}");
23/// assert!(!dbg.contains("secretvalue"));
24/// ```
25#[derive(Clone)]
26pub struct ApiKey(String);
27
28impl ApiKey {
29    /// Wrap an API key string.
30    pub fn new(key: impl Into<String>) -> Self {
31        Self(key.into())
32    }
33
34    /// Borrow the underlying key bytes. Crate-internal so the secret stays
35    /// inside this crate unless the caller explicitly opts out via
36    /// [`Self::expose`].
37    #[cfg(any(feature = "async", feature = "sync"))]
38    #[allow(dead_code)] // used by async client and the blocking submodule
39    pub(crate) fn as_str(&self) -> &str {
40        &self.0
41    }
42
43    /// Consume the wrapper and return the underlying string.
44    ///
45    /// Use sparingly; the wrapper exists to discourage casual leakage.
46    #[must_use]
47    pub fn expose(self) -> String {
48        self.0
49    }
50}
51
52impl fmt::Debug for ApiKey {
53    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54        write!(f, "ApiKey(<redacted, {} chars>)", self.0.len())
55    }
56}
57
58impl From<String> for ApiKey {
59    fn from(s: String) -> Self {
60        Self(s)
61    }
62}
63
64impl From<&str> for ApiKey {
65    fn from(s: &str) -> Self {
66        Self(s.to_owned())
67    }
68}
69
70/// Result alias for [`RequestSigner::sign`]. Boxed errors so signer
71/// implementations can use any error type that satisfies the bound.
72pub type SignerResult<T = ()> =
73    std::result::Result<T, Box<dyn std::error::Error + Send + Sync + 'static>>;
74
75/// Hook called for every outbound HTTP request just before transmission.
76///
77/// Implementations install request-level authentication: the default
78/// [`ApiKeySigner`] adds `x-api-key`, the optional
79/// [`BedrockSigner`](crate::bedrock::BedrockSigner) adds AWS sigv4
80/// signing headers. Install a custom signer via
81/// [`ClientBuilder::signer`](crate::ClientBuilder::signer).
82///
83/// Signers run *after* per-request beta headers are merged but *before*
84/// the request is sent, so the canonical body and headers are visible
85/// for hashing.
86#[cfg(feature = "async")]
87#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
88pub trait RequestSigner: fmt::Debug + Send + Sync + 'static {
89    /// Sign `request` in place. Return an error to abort the request
90    /// before it is sent; the error is wrapped in
91    /// [`Error::Signing`](crate::Error::Signing).
92    fn sign(&self, request: &mut reqwest::Request) -> SignerResult;
93}
94
95/// Default signer: adds the `x-api-key` header from the wrapped key.
96#[cfg(feature = "async")]
97#[cfg_attr(docsrs, doc(cfg(feature = "async")))]
98#[derive(Debug, Clone)]
99pub struct ApiKeySigner {
100    key: ApiKey,
101}
102
103#[cfg(feature = "async")]
104impl ApiKeySigner {
105    /// Wrap an [`ApiKey`].
106    #[must_use]
107    pub fn new(key: ApiKey) -> Self {
108        Self { key }
109    }
110}
111
112#[cfg(feature = "async")]
113impl RequestSigner for ApiKeySigner {
114    fn sign(&self, request: &mut reqwest::Request) -> SignerResult {
115        request
116            .headers_mut()
117            .insert("x-api-key", self.key.as_str().parse()?);
118        Ok(())
119    }
120}
121
122#[cfg(test)]
123mod tests {
124    use super::*;
125    use pretty_assertions::assert_eq;
126
127    #[test]
128    fn debug_redacts_the_secret() {
129        let k = ApiKey::new("sk-ant-very-secret-value-do-not-leak");
130        let dbg = format!("{k:?}");
131        assert!(!dbg.contains("secret"), "{dbg}");
132        assert!(!dbg.contains("very"), "{dbg}");
133        assert!(dbg.contains("redacted"), "{dbg}");
134        // Length shown for sanity, not for reconstruction.
135        assert!(dbg.contains(&k.0.len().to_string()), "{dbg}");
136    }
137
138    #[test]
139    fn expose_returns_underlying_string() {
140        let k = ApiKey::new("sk-ant-foo");
141        assert_eq!(k.expose(), "sk-ant-foo");
142    }
143
144    #[test]
145    fn from_string_and_str() {
146        let _: ApiKey = "sk-ant-x".into();
147        let _: ApiKey = String::from("sk-ant-y").into();
148    }
149}