Skip to main content

codex_protocol/
auth.rs

1use serde::Deserialize;
2use serde::Serialize;
3use strum_macros::Display;
4use thiserror::Error;
5
6/// Authentication mode for OpenAI-backed providers.
7#[derive(Debug, Clone, Copy, PartialEq, Eq, Display, Serialize, Deserialize)]
8#[serde(rename_all = "lowercase")]
9pub enum AuthMode {
10    /// OpenAI API key provided by the caller and stored by Codex.
11    ApiKey,
12    /// ChatGPT OAuth managed by Codex (tokens persisted and refreshed by Codex).
13    Chatgpt,
14    /// ChatGPT auth tokens supplied by an external host application.
15    #[serde(rename = "chatgptAuthTokens")]
16    #[strum(serialize = "chatgptAuthTokens")]
17    ChatgptAuthTokens,
18    /// Codex backend auth supplied as request headers.
19    #[serde(rename = "headers")]
20    #[strum(serialize = "headers")]
21    Headers,
22    /// Programmatic Codex auth backed by a registered Agent Identity.
23    #[serde(rename = "agentIdentity")]
24    #[strum(serialize = "agentIdentity")]
25    AgentIdentity,
26    /// Programmatic Codex auth backed by a personal access token.
27    #[serde(rename = "personalAccessToken")]
28    #[strum(serialize = "personalAccessToken")]
29    PersonalAccessToken,
30    /// Amazon Bedrock bearer token managed by Codex.
31    #[serde(rename = "bedrockApiKey")]
32    #[strum(serialize = "bedrockApiKey")]
33    BedrockApiKey,
34}
35
36impl AuthMode {
37    /// Returns whether this mode represents an authenticated human ChatGPT account.
38    pub fn has_chatgpt_account(self) -> bool {
39        match self {
40            Self::Chatgpt | Self::ChatgptAuthTokens | Self::PersonalAccessToken => true,
41            Self::ApiKey | Self::Headers | Self::AgentIdentity | Self::BedrockApiKey => false,
42        }
43    }
44
45    /// Returns whether this mode is backed by Codex services rather than a direct model API.
46    pub fn uses_codex_backend(self) -> bool {
47        match self {
48            Self::Chatgpt
49            | Self::ChatgptAuthTokens
50            | Self::Headers
51            | Self::AgentIdentity
52            | Self::PersonalAccessToken => true,
53            Self::ApiKey | Self::BedrockApiKey => false,
54        }
55    }
56}
57
58#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
59#[serde(untagged)]
60pub enum PlanType {
61    Known(KnownPlan),
62    Unknown(String),
63}
64
65impl PlanType {
66    pub fn from_raw_value(raw: &str) -> Self {
67        match raw.to_ascii_lowercase().as_str() {
68            "free" => Self::Known(KnownPlan::Free),
69            "go" => Self::Known(KnownPlan::Go),
70            "plus" => Self::Known(KnownPlan::Plus),
71            "pro" => Self::Known(KnownPlan::Pro),
72            "prolite" => Self::Known(KnownPlan::ProLite),
73            "team" => Self::Known(KnownPlan::Team),
74            "self_serve_business_usage_based" => {
75                Self::Known(KnownPlan::SelfServeBusinessUsageBased)
76            }
77            "business" => Self::Known(KnownPlan::Business),
78            "enterprise_cbp_usage_based" => Self::Known(KnownPlan::EnterpriseCbpUsageBased),
79            "enterprise" | "hc" => Self::Known(KnownPlan::Enterprise),
80            "education" | "edu" => Self::Known(KnownPlan::Edu),
81            _ => Self::Unknown(raw.to_string()),
82        }
83    }
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
87#[serde(rename_all = "lowercase")]
88pub enum KnownPlan {
89    Free,
90    Go,
91    Plus,
92    Pro,
93    ProLite,
94    Team,
95    #[serde(rename = "self_serve_business_usage_based")]
96    SelfServeBusinessUsageBased,
97    Business,
98    #[serde(rename = "enterprise_cbp_usage_based")]
99    EnterpriseCbpUsageBased,
100    #[serde(alias = "hc")]
101    Enterprise,
102    #[serde(alias = "education")]
103    Edu,
104}
105
106impl KnownPlan {
107    pub fn display_name(self) -> &'static str {
108        match self {
109            Self::Free => "Free",
110            Self::Go => "Go",
111            Self::Plus => "Plus",
112            Self::Pro => "Pro",
113            Self::ProLite => "Pro Lite",
114            Self::Team => "Team",
115            Self::SelfServeBusinessUsageBased => "Self Serve Business Usage Based",
116            Self::Business => "Business",
117            Self::EnterpriseCbpUsageBased => "Enterprise CBP Usage Based",
118            Self::Enterprise => "Enterprise",
119            Self::Edu => "Edu",
120        }
121    }
122
123    pub fn raw_value(self) -> &'static str {
124        match self {
125            Self::Free => "free",
126            Self::Go => "go",
127            Self::Plus => "plus",
128            Self::Pro => "pro",
129            Self::ProLite => "prolite",
130            Self::Team => "team",
131            Self::SelfServeBusinessUsageBased => "self_serve_business_usage_based",
132            Self::Business => "business",
133            Self::EnterpriseCbpUsageBased => "enterprise_cbp_usage_based",
134            Self::Enterprise => "enterprise",
135            Self::Edu => "edu",
136        }
137    }
138
139    pub fn is_workspace_account(self) -> bool {
140        matches!(
141            self,
142            Self::Team
143                | Self::SelfServeBusinessUsageBased
144                | Self::Business
145                | Self::EnterpriseCbpUsageBased
146                | Self::Enterprise
147                | Self::Edu
148        )
149    }
150}
151
152#[derive(Debug, Clone, PartialEq, Eq, Error)]
153#[error("{message}")]
154pub struct RefreshTokenFailedError {
155    pub reason: RefreshTokenFailedReason,
156    pub message: String,
157}
158
159impl RefreshTokenFailedError {
160    pub fn new(reason: RefreshTokenFailedReason, message: impl Into<String>) -> Self {
161        Self {
162            reason,
163            message: message.into(),
164        }
165    }
166}
167
168#[derive(Debug, Clone, Copy, PartialEq, Eq)]
169pub enum RefreshTokenFailedReason {
170    Expired,
171    Exhausted,
172    Revoked,
173    Other,
174}
175
176#[cfg(test)]
177mod tests {
178    use super::KnownPlan;
179    use super::PlanType;
180    use pretty_assertions::assert_eq;
181
182    #[test]
183    fn plan_type_deserializes_raw_aliases() {
184        assert_eq!(
185            serde_json::from_str::<PlanType>("\"hc\"").expect("hc should deserialize"),
186            PlanType::Known(KnownPlan::Enterprise)
187        );
188        assert_eq!(
189            serde_json::from_str::<PlanType>("\"education\"")
190                .expect("education should deserialize"),
191            PlanType::Known(KnownPlan::Edu)
192        );
193    }
194}