Skip to main content

kagi_sdk/
auth.rs

1use std::fmt;
2
3use reqwest::header::{HeaderMap, HeaderValue, AUTHORIZATION, COOKIE};
4
5use crate::error::KagiError;
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
8pub enum CredentialKind {
9    BotToken,
10    SessionToken,
11}
12
13impl fmt::Display for CredentialKind {
14    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
15        match self {
16            Self::BotToken => formatter.write_str("BotToken"),
17            Self::SessionToken => formatter.write_str("SessionToken"),
18        }
19    }
20}
21
22#[derive(Clone, PartialEq, Eq, Hash)]
23pub struct BotToken(String);
24
25impl BotToken {
26    pub fn new(value: impl Into<String>) -> Result<Self, KagiError> {
27        let token = parse_token(value.into(), CredentialKind::BotToken)?;
28        Ok(Self(token))
29    }
30
31    pub(crate) fn as_secret(&self) -> &str {
32        &self.0
33    }
34}
35
36impl fmt::Debug for BotToken {
37    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
38        formatter.write_str("BotToken(REDACTED)")
39    }
40}
41
42#[derive(Clone, PartialEq, Eq, Hash)]
43pub struct SessionToken(String);
44
45impl SessionToken {
46    pub fn new(value: impl Into<String>) -> Result<Self, KagiError> {
47        let token = parse_token(value.into(), CredentialKind::SessionToken)?;
48        Ok(Self(token))
49    }
50
51    pub(crate) fn as_secret(&self) -> &str {
52        &self.0
53    }
54}
55
56impl fmt::Debug for SessionToken {
57    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
58        formatter.write_str("SessionToken(REDACTED)")
59    }
60}
61
62#[derive(Clone, Debug, PartialEq, Eq, Hash)]
63pub enum Credentials {
64    BotToken(BotToken),
65    SessionToken(SessionToken),
66}
67
68impl Credentials {
69    pub fn kind(&self) -> CredentialKind {
70        match self {
71            Self::BotToken(_) => CredentialKind::BotToken,
72            Self::SessionToken(_) => CredentialKind::SessionToken,
73        }
74    }
75
76    pub(crate) fn apply_to_headers(&self, headers: &mut HeaderMap) -> Result<(), KagiError> {
77        match self {
78            Self::BotToken(token) => {
79                let value = HeaderValue::from_str(&format!("Bot {}", token.as_secret())).map_err(
80                    |source| KagiError::InvalidCredential {
81                        kind: CredentialKind::BotToken,
82                        reason: format!("token could not be encoded as header: {source}"),
83                    },
84                )?;
85                headers.insert(AUTHORIZATION, value);
86            }
87            Self::SessionToken(token) => {
88                let value = HeaderValue::from_str(&format!("kagi_session={}", token.as_secret()))
89                    .map_err(|source| KagiError::InvalidCredential {
90                    kind: CredentialKind::SessionToken,
91                    reason: format!("token could not be encoded as cookie: {source}"),
92                })?;
93                headers.insert(COOKIE, value);
94            }
95        }
96
97        Ok(())
98    }
99}
100
101impl From<BotToken> for Credentials {
102    fn from(value: BotToken) -> Self {
103        Self::BotToken(value)
104    }
105}
106
107impl From<SessionToken> for Credentials {
108    fn from(value: SessionToken) -> Self {
109        Self::SessionToken(value)
110    }
111}
112
113fn parse_token(raw_token: String, kind: CredentialKind) -> Result<String, KagiError> {
114    let trimmed = raw_token.trim();
115    if trimmed.is_empty() {
116        return Err(KagiError::InvalidCredential {
117            kind,
118            reason: "token cannot be empty".to_string(),
119        });
120    }
121
122    if trimmed.chars().any(char::is_whitespace) {
123        return Err(KagiError::InvalidCredential {
124            kind,
125            reason: "token cannot contain whitespace".to_string(),
126        });
127    }
128
129    Ok(trimmed.to_string())
130}
131
132#[cfg(test)]
133mod tests {
134    use super::BotToken;
135
136    #[test]
137    fn token_debug_is_redacted() {
138        let token = BotToken::new("super-secret").expect("token should parse");
139        assert_eq!(format!("{token:?}"), "BotToken(REDACTED)");
140    }
141}