Add codex login --api-key (#1759)
Allow setting the API key via `codex login --api-key`
This commit is contained in:
1
codex-rs/Cargo.lock
generated
1
codex-rs/Cargo.lock
generated
@@ -793,6 +793,7 @@ dependencies = [
|
|||||||
"reqwest",
|
"reqwest",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
"tempfile",
|
||||||
"tokio",
|
"tokio",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ pub fn set_chatgpt_token_data(value: TokenData) {
|
|||||||
|
|
||||||
/// Initialize the ChatGPT token from auth.json file
|
/// Initialize the ChatGPT token from auth.json file
|
||||||
pub async fn init_chatgpt_token_from_auth(codex_home: &Path) -> std::io::Result<()> {
|
pub async fn init_chatgpt_token_from_auth(codex_home: &Path) -> std::io::Result<()> {
|
||||||
let auth = codex_login::load_auth(codex_home)?;
|
let auth = codex_login::load_auth(codex_home, true)?;
|
||||||
if let Some(auth) = auth {
|
if let Some(auth) = auth {
|
||||||
let token_data = auth.get_token_data().await?;
|
let token_data = auth.get_token_data().await?;
|
||||||
set_chatgpt_token_data(token_data);
|
set_chatgpt_token_data(token_data);
|
||||||
|
|||||||
@@ -1,8 +1,12 @@
|
|||||||
|
use std::env;
|
||||||
|
|
||||||
use codex_common::CliConfigOverrides;
|
use codex_common::CliConfigOverrides;
|
||||||
use codex_core::config::Config;
|
use codex_core::config::Config;
|
||||||
use codex_core::config::ConfigOverrides;
|
use codex_core::config::ConfigOverrides;
|
||||||
use codex_login::AuthMode;
|
use codex_login::AuthMode;
|
||||||
|
use codex_login::OPENAI_API_KEY_ENV_VAR;
|
||||||
use codex_login::load_auth;
|
use codex_login::load_auth;
|
||||||
|
use codex_login::login_with_api_key;
|
||||||
use codex_login::login_with_chatgpt;
|
use codex_login::login_with_chatgpt;
|
||||||
|
|
||||||
pub async fn run_login_with_chatgpt(cli_config_overrides: CliConfigOverrides) -> ! {
|
pub async fn run_login_with_chatgpt(cli_config_overrides: CliConfigOverrides) -> ! {
|
||||||
@@ -21,14 +25,40 @@ pub async fn run_login_with_chatgpt(cli_config_overrides: CliConfigOverrides) ->
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn run_login_with_api_key(
|
||||||
|
cli_config_overrides: CliConfigOverrides,
|
||||||
|
api_key: String,
|
||||||
|
) -> ! {
|
||||||
|
let config = load_config_or_exit(cli_config_overrides);
|
||||||
|
|
||||||
|
match login_with_api_key(&config.codex_home, &api_key) {
|
||||||
|
Ok(_) => {
|
||||||
|
eprintln!("Successfully logged in");
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
Err(e) => {
|
||||||
|
eprintln!("Error logging in: {e}");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn run_login_status(cli_config_overrides: CliConfigOverrides) -> ! {
|
pub async fn run_login_status(cli_config_overrides: CliConfigOverrides) -> ! {
|
||||||
let config = load_config_or_exit(cli_config_overrides);
|
let config = load_config_or_exit(cli_config_overrides);
|
||||||
|
|
||||||
match load_auth(&config.codex_home) {
|
match load_auth(&config.codex_home, true) {
|
||||||
Ok(Some(auth)) => match auth.mode {
|
Ok(Some(auth)) => match auth.mode {
|
||||||
AuthMode::ApiKey => {
|
AuthMode::ApiKey => {
|
||||||
if let Some(api_key) = auth.api_key.as_deref() {
|
if let Some(api_key) = auth.api_key.as_deref() {
|
||||||
eprintln!("Logged in using an API key - {}", safe_format_key(api_key));
|
eprintln!("Logged in using an API key - {}", safe_format_key(api_key));
|
||||||
|
|
||||||
|
if let Ok(env_api_key) = env::var(OPENAI_API_KEY_ENV_VAR) {
|
||||||
|
if env_api_key == api_key {
|
||||||
|
eprintln!(
|
||||||
|
" API loaded from OPENAI_API_KEY environment variable or .env file"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
eprintln!("Logged in using an API key");
|
eprintln!("Logged in using an API key");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ use codex_chatgpt::apply_command::run_apply_command;
|
|||||||
use codex_cli::LandlockCommand;
|
use codex_cli::LandlockCommand;
|
||||||
use codex_cli::SeatbeltCommand;
|
use codex_cli::SeatbeltCommand;
|
||||||
use codex_cli::login::run_login_status;
|
use codex_cli::login::run_login_status;
|
||||||
|
use codex_cli::login::run_login_with_api_key;
|
||||||
use codex_cli::login::run_login_with_chatgpt;
|
use codex_cli::login::run_login_with_chatgpt;
|
||||||
use codex_cli::proto;
|
use codex_cli::proto;
|
||||||
use codex_common::CliConfigOverrides;
|
use codex_common::CliConfigOverrides;
|
||||||
@@ -92,6 +93,9 @@ struct LoginCommand {
|
|||||||
#[clap(skip)]
|
#[clap(skip)]
|
||||||
config_overrides: CliConfigOverrides,
|
config_overrides: CliConfigOverrides,
|
||||||
|
|
||||||
|
#[arg(long = "api-key", value_name = "API_KEY")]
|
||||||
|
api_key: Option<String>,
|
||||||
|
|
||||||
#[command(subcommand)]
|
#[command(subcommand)]
|
||||||
action: Option<LoginSubcommand>,
|
action: Option<LoginSubcommand>,
|
||||||
}
|
}
|
||||||
@@ -133,7 +137,11 @@ async fn cli_main(codex_linux_sandbox_exe: Option<PathBuf>) -> anyhow::Result<()
|
|||||||
run_login_status(login_cli.config_overrides).await;
|
run_login_status(login_cli.config_overrides).await;
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
run_login_with_chatgpt(login_cli.config_overrides).await;
|
if let Some(api_key) = login_cli.api_key {
|
||||||
|
run_login_with_api_key(login_cli.config_overrides, api_key).await;
|
||||||
|
} else {
|
||||||
|
run_login_with_chatgpt(login_cli.config_overrides).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ pub async fn run_main(opts: ProtoCli) -> anyhow::Result<()> {
|
|||||||
.map_err(anyhow::Error::msg)?;
|
.map_err(anyhow::Error::msg)?;
|
||||||
|
|
||||||
let config = Config::load_with_cli_overrides(overrides_vec, ConfigOverrides::default())?;
|
let config = Config::load_with_cli_overrides(overrides_vec, ConfigOverrides::default())?;
|
||||||
let auth = load_auth(&config.codex_home)?;
|
let auth = load_auth(&config.codex_home, true)?;
|
||||||
let ctrl_c = notify_on_sigint();
|
let ctrl_c = notify_on_sigint();
|
||||||
let CodexSpawnOk { codex, .. } = Codex::spawn(config, auth, ctrl_c.clone()).await?;
|
let CodexSpawnOk { codex, .. } = Codex::spawn(config, auth, ctrl_c.clone()).await?;
|
||||||
let codex = Arc::new(codex);
|
let codex = Arc::new(codex);
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ pub struct CodexConversation {
|
|||||||
/// that callers can surface the information to the UI.
|
/// that callers can surface the information to the UI.
|
||||||
pub async fn init_codex(config: Config) -> anyhow::Result<CodexConversation> {
|
pub async fn init_codex(config: Config) -> anyhow::Result<CodexConversation> {
|
||||||
let ctrl_c = notify_on_sigint();
|
let ctrl_c = notify_on_sigint();
|
||||||
let auth = load_auth(&config.codex_home)?;
|
let auth = load_auth(&config.codex_home, true)?;
|
||||||
let CodexSpawnOk {
|
let CodexSpawnOk {
|
||||||
codex,
|
codex,
|
||||||
init_id,
|
init_id,
|
||||||
|
|||||||
@@ -327,14 +327,14 @@ fn auth_from_token(id_token: String) -> CodexAuth {
|
|||||||
AuthMode::ChatGPT,
|
AuthMode::ChatGPT,
|
||||||
PathBuf::new(),
|
PathBuf::new(),
|
||||||
Some(AuthDotJson {
|
Some(AuthDotJson {
|
||||||
tokens: TokenData {
|
openai_api_key: None,
|
||||||
|
tokens: Some(TokenData {
|
||||||
id_token,
|
id_token,
|
||||||
access_token: "Access Token".to_string(),
|
access_token: "Access Token".to_string(),
|
||||||
refresh_token: "test".to_string(),
|
refresh_token: "test".to_string(),
|
||||||
account_id: None,
|
account_id: None,
|
||||||
},
|
}),
|
||||||
last_refresh: Utc::now(),
|
last_refresh: Some(Utc::now()),
|
||||||
openai_api_key: None,
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,3 +18,6 @@ tokio = { version = "1", features = [
|
|||||||
"rt-multi-thread",
|
"rt-multi-thread",
|
||||||
"signal",
|
"signal",
|
||||||
] }
|
] }
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
tempfile = "3"
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ use tokio::process::Command;
|
|||||||
const SOURCE_FOR_PYTHON_SERVER: &str = include_str!("./login_with_chatgpt.py");
|
const SOURCE_FOR_PYTHON_SERVER: &str = include_str!("./login_with_chatgpt.py");
|
||||||
|
|
||||||
const CLIENT_ID: &str = "app_EMoamEEZ73f0CkXaXp7hrann";
|
const CLIENT_ID: &str = "app_EMoamEEZ73f0CkXaXp7hrann";
|
||||||
const OPENAI_API_KEY_ENV_VAR: &str = "OPENAI_API_KEY";
|
pub const OPENAI_API_KEY_ENV_VAR: &str = "OPENAI_API_KEY";
|
||||||
|
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub enum AuthMode {
|
pub enum AuthMode {
|
||||||
@@ -70,13 +70,16 @@ impl CodexAuth {
|
|||||||
pub async fn get_token_data(&self) -> Result<TokenData, std::io::Error> {
|
pub async fn get_token_data(&self) -> Result<TokenData, std::io::Error> {
|
||||||
#[expect(clippy::unwrap_used)]
|
#[expect(clippy::unwrap_used)]
|
||||||
let auth_dot_json = self.auth_dot_json.lock().unwrap().clone();
|
let auth_dot_json = self.auth_dot_json.lock().unwrap().clone();
|
||||||
|
|
||||||
match auth_dot_json {
|
match auth_dot_json {
|
||||||
Some(auth_dot_json) => {
|
Some(AuthDotJson {
|
||||||
if auth_dot_json.last_refresh < Utc::now() - chrono::Duration::days(28) {
|
tokens: Some(mut tokens),
|
||||||
|
last_refresh: Some(last_refresh),
|
||||||
|
..
|
||||||
|
}) => {
|
||||||
|
if last_refresh < Utc::now() - chrono::Duration::days(28) {
|
||||||
let refresh_response = tokio::time::timeout(
|
let refresh_response = tokio::time::timeout(
|
||||||
Duration::from_secs(60),
|
Duration::from_secs(60),
|
||||||
try_refresh_token(auth_dot_json.tokens.refresh_token.clone()),
|
try_refresh_token(tokens.refresh_token.clone()),
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.map_err(|_| {
|
.map_err(|_| {
|
||||||
@@ -92,13 +95,21 @@ impl CodexAuth {
|
|||||||
)
|
)
|
||||||
.await?;
|
.await?;
|
||||||
|
|
||||||
|
tokens = updated_auth_dot_json
|
||||||
|
.tokens
|
||||||
|
.clone()
|
||||||
|
.ok_or(std::io::Error::other(
|
||||||
|
"Token data is not available after refresh.",
|
||||||
|
))?;
|
||||||
|
|
||||||
#[expect(clippy::unwrap_used)]
|
#[expect(clippy::unwrap_used)]
|
||||||
let mut auth_dot_json = self.auth_dot_json.lock().unwrap();
|
let mut auth_lock = self.auth_dot_json.lock().unwrap();
|
||||||
*auth_dot_json = Some(updated_auth_dot_json);
|
*auth_lock = Some(updated_auth_dot_json);
|
||||||
}
|
}
|
||||||
Ok(auth_dot_json.tokens.clone())
|
|
||||||
|
Ok(tokens)
|
||||||
}
|
}
|
||||||
None => Err(std::io::Error::other("Token data is not available.")),
|
_ => Err(std::io::Error::other("Token data is not available.")),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -115,8 +126,8 @@ impl CodexAuth {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Loads the available auth information from the auth.json or OPENAI_API_KEY environment variable.
|
// Loads the available auth information from the auth.json or OPENAI_API_KEY environment variable.
|
||||||
pub fn load_auth(codex_home: &Path) -> std::io::Result<Option<CodexAuth>> {
|
pub fn load_auth(codex_home: &Path, include_env_var: bool) -> std::io::Result<Option<CodexAuth>> {
|
||||||
let auth_file = codex_home.join("auth.json");
|
let auth_file = get_auth_file(codex_home);
|
||||||
|
|
||||||
let auth_dot_json = try_read_auth_json(&auth_file).ok();
|
let auth_dot_json = try_read_auth_json(&auth_file).ok();
|
||||||
|
|
||||||
@@ -125,12 +136,21 @@ pub fn load_auth(codex_home: &Path) -> std::io::Result<Option<CodexAuth>> {
|
|||||||
.and_then(|a| a.openai_api_key.clone())
|
.and_then(|a| a.openai_api_key.clone())
|
||||||
.filter(|s| !s.is_empty());
|
.filter(|s| !s.is_empty());
|
||||||
|
|
||||||
let openai_api_key = env::var(OPENAI_API_KEY_ENV_VAR)
|
let openai_api_key = if include_env_var {
|
||||||
.ok()
|
env::var(OPENAI_API_KEY_ENV_VAR)
|
||||||
.filter(|s| !s.is_empty())
|
.ok()
|
||||||
.or(auth_json_api_key);
|
.filter(|s| !s.is_empty())
|
||||||
|
.or(auth_json_api_key)
|
||||||
|
} else {
|
||||||
|
auth_json_api_key
|
||||||
|
};
|
||||||
|
|
||||||
if openai_api_key.is_none() && auth_dot_json.is_none() {
|
let has_tokens = auth_dot_json
|
||||||
|
.as_ref()
|
||||||
|
.and_then(|a| a.tokens.as_ref())
|
||||||
|
.is_some();
|
||||||
|
|
||||||
|
if openai_api_key.is_none() && !has_tokens {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,6 +168,10 @@ pub fn load_auth(codex_home: &Path) -> std::io::Result<Option<CodexAuth>> {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn get_auth_file(codex_home: &Path) -> PathBuf {
|
||||||
|
codex_home.join("auth.json")
|
||||||
|
}
|
||||||
|
|
||||||
/// Run `python3 -c {{SOURCE_FOR_PYTHON_SERVER}}` with the CODEX_HOME
|
/// Run `python3 -c {{SOURCE_FOR_PYTHON_SERVER}}` with the CODEX_HOME
|
||||||
/// environment variable set to the provided `codex_home` path. If the
|
/// environment variable set to the provided `codex_home` path. If the
|
||||||
/// subprocess exits 0, read the OPENAI_API_KEY property out of
|
/// subprocess exits 0, read the OPENAI_API_KEY property out of
|
||||||
@@ -187,6 +211,15 @@ pub async fn login_with_chatgpt(codex_home: &Path, capture_output: bool) -> std:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn login_with_api_key(codex_home: &Path, api_key: &str) -> std::io::Result<()> {
|
||||||
|
let auth_dot_json = AuthDotJson {
|
||||||
|
openai_api_key: Some(api_key.to_string()),
|
||||||
|
tokens: None,
|
||||||
|
last_refresh: None,
|
||||||
|
};
|
||||||
|
write_auth_json(&get_auth_file(codex_home), &auth_dot_json)
|
||||||
|
}
|
||||||
|
|
||||||
/// Attempt to read and refresh the `auth.json` file in the given `CODEX_HOME` directory.
|
/// Attempt to read and refresh the `auth.json` file in the given `CODEX_HOME` directory.
|
||||||
/// Returns the full AuthDotJson structure after refreshing if necessary.
|
/// Returns the full AuthDotJson structure after refreshing if necessary.
|
||||||
pub fn try_read_auth_json(auth_file: &Path) -> std::io::Result<AuthDotJson> {
|
pub fn try_read_auth_json(auth_file: &Path) -> std::io::Result<AuthDotJson> {
|
||||||
@@ -198,35 +231,38 @@ pub fn try_read_auth_json(auth_file: &Path) -> std::io::Result<AuthDotJson> {
|
|||||||
Ok(auth_dot_json)
|
Ok(auth_dot_json)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn update_tokens(
|
fn write_auth_json(auth_file: &Path, auth_dot_json: &AuthDotJson) -> std::io::Result<()> {
|
||||||
auth_file: &Path,
|
let json_data = serde_json::to_string_pretty(auth_dot_json)?;
|
||||||
id_token: String,
|
|
||||||
access_token: Option<String>,
|
|
||||||
refresh_token: Option<String>,
|
|
||||||
) -> std::io::Result<AuthDotJson> {
|
|
||||||
let mut options = OpenOptions::new();
|
let mut options = OpenOptions::new();
|
||||||
options.truncate(true).write(true).create(true);
|
options.truncate(true).write(true).create(true);
|
||||||
#[cfg(unix)]
|
#[cfg(unix)]
|
||||||
{
|
{
|
||||||
options.mode(0o600);
|
options.mode(0o600);
|
||||||
}
|
}
|
||||||
|
let mut file = options.open(auth_file)?;
|
||||||
|
file.write_all(json_data.as_bytes())?;
|
||||||
|
file.flush()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn update_tokens(
|
||||||
|
auth_file: &Path,
|
||||||
|
id_token: String,
|
||||||
|
access_token: Option<String>,
|
||||||
|
refresh_token: Option<String>,
|
||||||
|
) -> std::io::Result<AuthDotJson> {
|
||||||
let mut auth_dot_json = try_read_auth_json(auth_file)?;
|
let mut auth_dot_json = try_read_auth_json(auth_file)?;
|
||||||
|
|
||||||
auth_dot_json.tokens.id_token = id_token.to_string();
|
let tokens = auth_dot_json.tokens.get_or_insert_with(TokenData::default);
|
||||||
|
tokens.id_token = id_token.to_string();
|
||||||
if let Some(access_token) = access_token {
|
if let Some(access_token) = access_token {
|
||||||
auth_dot_json.tokens.access_token = access_token.to_string();
|
tokens.access_token = access_token.to_string();
|
||||||
}
|
}
|
||||||
if let Some(refresh_token) = refresh_token {
|
if let Some(refresh_token) = refresh_token {
|
||||||
auth_dot_json.tokens.refresh_token = refresh_token.to_string();
|
tokens.refresh_token = refresh_token.to_string();
|
||||||
}
|
|
||||||
auth_dot_json.last_refresh = Utc::now();
|
|
||||||
|
|
||||||
let json_data = serde_json::to_string_pretty(&auth_dot_json)?;
|
|
||||||
{
|
|
||||||
let mut file = options.open(auth_file)?;
|
|
||||||
file.write_all(json_data.as_bytes())?;
|
|
||||||
file.flush()?;
|
|
||||||
}
|
}
|
||||||
|
auth_dot_json.last_refresh = Some(Utc::now());
|
||||||
|
write_auth_json(auth_file, &auth_dot_json)?;
|
||||||
Ok(auth_dot_json)
|
Ok(auth_dot_json)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,12 +318,14 @@ pub struct AuthDotJson {
|
|||||||
#[serde(rename = "OPENAI_API_KEY")]
|
#[serde(rename = "OPENAI_API_KEY")]
|
||||||
pub openai_api_key: Option<String>,
|
pub openai_api_key: Option<String>,
|
||||||
|
|
||||||
pub tokens: TokenData,
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub tokens: Option<TokenData>,
|
||||||
|
|
||||||
pub last_refresh: DateTime<Utc>,
|
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||||
|
pub last_refresh: Option<DateTime<Utc>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq)]
|
#[derive(Deserialize, Serialize, Clone, Debug, PartialEq, Default)]
|
||||||
pub struct TokenData {
|
pub struct TokenData {
|
||||||
/// This is a JWT.
|
/// This is a JWT.
|
||||||
pub id_token: String,
|
pub id_token: String,
|
||||||
@@ -299,3 +337,95 @@ pub struct TokenData {
|
|||||||
|
|
||||||
pub account_id: Option<String>,
|
pub account_id: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use tempfile::tempdir;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[expect(clippy::unwrap_used)]
|
||||||
|
fn writes_api_key_and_loads_auth() {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
login_with_api_key(dir.path(), "sk-test-key").unwrap();
|
||||||
|
let auth = load_auth(dir.path(), false).unwrap().unwrap();
|
||||||
|
assert_eq!(auth.mode, AuthMode::ApiKey);
|
||||||
|
assert_eq!(auth.api_key.as_deref(), Some("sk-test-key"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
#[expect(clippy::unwrap_used)]
|
||||||
|
fn loads_from_env_var_if_env_var_exists() {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
|
||||||
|
let env_var = std::env::var(OPENAI_API_KEY_ENV_VAR);
|
||||||
|
|
||||||
|
if let Ok(env_var) = env_var {
|
||||||
|
let auth = load_auth(dir.path(), true).unwrap().unwrap();
|
||||||
|
assert_eq!(auth.mode, AuthMode::ApiKey);
|
||||||
|
assert_eq!(auth.api_key, Some(env_var));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[expect(clippy::unwrap_used)]
|
||||||
|
async fn loads_token_data_from_auth_json() {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let auth_file = dir.path().join("auth.json");
|
||||||
|
std::fs::write(
|
||||||
|
auth_file,
|
||||||
|
format!(
|
||||||
|
r#"
|
||||||
|
{{
|
||||||
|
"OPENAI_API_KEY": null,
|
||||||
|
"tokens": {{
|
||||||
|
"id_token": "test-id-token",
|
||||||
|
"access_token": "test-access-token",
|
||||||
|
"refresh_token": "test-refresh-token"
|
||||||
|
}},
|
||||||
|
"last_refresh": "{}"
|
||||||
|
}}
|
||||||
|
"#,
|
||||||
|
Utc::now().to_rfc3339()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let auth = load_auth(dir.path(), false).unwrap().unwrap();
|
||||||
|
assert_eq!(auth.mode, AuthMode::ChatGPT);
|
||||||
|
assert_eq!(auth.api_key, None);
|
||||||
|
assert_eq!(
|
||||||
|
auth.get_token_data().await.unwrap(),
|
||||||
|
TokenData {
|
||||||
|
id_token: "test-id-token".to_string(),
|
||||||
|
access_token: "test-access-token".to_string(),
|
||||||
|
refresh_token: "test-refresh-token".to_string(),
|
||||||
|
account_id: None,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
#[expect(clippy::unwrap_used)]
|
||||||
|
async fn loads_api_key_from_auth_json() {
|
||||||
|
let dir = tempdir().unwrap();
|
||||||
|
let auth_file = dir.path().join("auth.json");
|
||||||
|
std::fs::write(
|
||||||
|
auth_file,
|
||||||
|
r#"
|
||||||
|
{
|
||||||
|
"OPENAI_API_KEY": "sk-test-key",
|
||||||
|
"tokens": null,
|
||||||
|
"last_refresh": null
|
||||||
|
}
|
||||||
|
"#,
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
let auth = load_auth(dir.path(), false).unwrap().unwrap();
|
||||||
|
assert_eq!(auth.mode, AuthMode::ApiKey);
|
||||||
|
assert_eq!(auth.api_key, Some("sk-test-key".to_string()));
|
||||||
|
|
||||||
|
assert!(auth.get_token_data().await.is_err());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -226,7 +226,7 @@ fn should_show_login_screen(config: &Config) -> bool {
|
|||||||
// Reading the OpenAI API key is an async operation because it may need
|
// Reading the OpenAI API key is an async operation because it may need
|
||||||
// to refresh the token. Block on it.
|
// to refresh the token. Block on it.
|
||||||
let codex_home = config.codex_home.clone();
|
let codex_home = config.codex_home.clone();
|
||||||
match load_auth(&codex_home) {
|
match load_auth(&codex_home, true) {
|
||||||
Ok(Some(_)) => false,
|
Ok(Some(_)) => false,
|
||||||
Ok(None) => true,
|
Ok(None) => true,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
|
|||||||
Reference in New Issue
Block a user