Skip to main content

codex_auth_manager/
identity.rs

1use std::{fmt, path::PathBuf, str::FromStr};
2
3use super::Error;
4
5/// A valid identity name.
6#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
7pub struct IdentityName(String);
8
9impl IdentityName {
10    /// Borrow the identity name as a string.
11    #[must_use]
12    pub fn as_str(&self) -> &str {
13        &self.0
14    }
15}
16
17impl TryFrom<&str> for IdentityName {
18    type Error = Error;
19
20    fn try_from(value: &str) -> Result<Self, Self::Error> {
21        validate_identity_name(value)?;
22        Ok(Self(value.to_owned()))
23    }
24}
25
26impl TryFrom<String> for IdentityName {
27    type Error = Error;
28
29    fn try_from(value: String) -> Result<Self, Self::Error> {
30        validate_identity_name(&value)?;
31        Ok(Self(value))
32    }
33}
34
35impl FromStr for IdentityName {
36    type Err = Error;
37
38    fn from_str(value: &str) -> Result<Self, Self::Err> {
39        Self::try_from(value)
40    }
41}
42
43impl AsRef<str> for IdentityName {
44    fn as_ref(&self) -> &str {
45        self.as_str()
46    }
47}
48
49impl fmt::Display for IdentityName {
50    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
51        f.write_str(&self.0)
52    }
53}
54
55/// An identity entry in the manager directory.
56#[derive(Debug, Clone, PartialEq, Eq)]
57pub struct Identity {
58    /// Identity name.
59    pub name: IdentityName,
60    /// Storage path.
61    pub path: PathBuf,
62    /// Whether this identity is selected by `auth.json`.
63    pub active: bool,
64    /// Whether this identity entry exists but is unusable.
65    pub broken: bool,
66}
67
68fn validate_identity_name(value: &str) -> Result<(), Error> {
69    let mut chars = value.chars();
70    let Some(first) = chars.next() else {
71        return Err(Error::InvalidIdentityName {
72            name: value.to_owned(),
73        });
74    };
75    if !first.is_ascii_alphanumeric()
76        || !chars.all(|char| char.is_ascii_alphanumeric() || matches!(char, '.' | '_' | '-'))
77    {
78        return Err(Error::InvalidIdentityName {
79            name: value.to_owned(),
80        });
81    }
82    Ok(())
83}
84
85#[cfg(test)]
86mod tests {
87    use crate::{Error, IdentityName};
88
89    #[test]
90    fn identity_name_validation_accepts_portable_names() {
91        for name in ["personal", "OpenAI-Work", "org.dev", "test_2", "work.json"] {
92            assert!(IdentityName::try_from(name).is_ok());
93        }
94    }
95
96    #[test]
97    fn identity_name_validation_rejects_paths_and_shellish_names() {
98        for name in ["", "-prod", "my work", "../auth", "work/main", "work\\main"] {
99            assert!(matches!(
100                IdentityName::try_from(name),
101                Err(Error::InvalidIdentityName { .. })
102            ));
103        }
104    }
105}