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}