Skip to main content

codex_cli/auth/
use_secret.rs

1use anyhow::Result;
2use serde_json::json;
3use std::path::{Path, PathBuf};
4
5use crate::auth;
6use crate::auth::output::{self, AuthUseResult};
7use crate::paths;
8use nils_common::fs;
9use nils_common::provider_runtime::auth::{SecretFileResolution, resolve_secret_file_by_email};
10
11pub fn run(target: &str) -> Result<i32> {
12    run_with_json(target, false)
13}
14
15pub fn run_with_json(target: &str, output_json: bool) -> Result<i32> {
16    if target.is_empty() {
17        if output_json {
18            output::emit_error(
19                "auth use",
20                "invalid-usage",
21                "codex-use: usage: codex-use <name|name.json|email>",
22                None,
23            )?;
24        } else {
25            eprintln!("codex-use: usage: codex-use <name|name.json|email>");
26        }
27        return Ok(64);
28    }
29
30    if auth::is_invalid_secret_target(target) {
31        if output_json {
32            output::emit_error(
33                "auth use",
34                "invalid-secret-name",
35                format!("codex-use: invalid secret name: {target}"),
36                Some(json!({
37                    "target": target,
38                })),
39            )?;
40        } else {
41            eprintln!("codex-use: invalid secret name: {target}");
42        }
43        return Ok(64);
44    }
45
46    let secret_dir = match paths::resolve_secret_dir() {
47        Some(dir) => dir,
48        None => {
49            if output_json {
50                output::emit_error(
51                    "auth use",
52                    "secret-not-found",
53                    format!("codex-use: secret not found: {target}"),
54                    Some(json!({
55                        "target": target,
56                    })),
57                )?;
58            } else {
59                eprintln!("codex-use: secret not found: {target}");
60            }
61            return Ok(1);
62        }
63    };
64
65    let is_email = target.contains('@');
66    let secret_name = if is_email {
67        target.to_string()
68    } else {
69        auth::normalize_secret_file_name(target)
70    };
71
72    if secret_dir.join(&secret_name).is_file() {
73        let (code, auth_file) = apply_secret(&secret_dir, &secret_name, output_json)?;
74        if output_json && code == 0 {
75            output::emit_result(
76                "auth use",
77                AuthUseResult {
78                    target: target.to_string(),
79                    matched_secret: Some(secret_name),
80                    applied: true,
81                    auth_file: auth_file.unwrap_or_default(),
82                },
83            )?;
84        }
85        return Ok(code);
86    }
87
88    match resolve_secret_file_by_email(&secret_dir, target) {
89        SecretFileResolution::Exact(name) => {
90            let (code, auth_file) = apply_secret(&secret_dir, &name, output_json)?;
91            if output_json && code == 0 {
92                output::emit_result(
93                    "auth use",
94                    AuthUseResult {
95                        target: target.to_string(),
96                        matched_secret: Some(name),
97                        applied: true,
98                        auth_file: auth_file.unwrap_or_default(),
99                    },
100                )?;
101            }
102            Ok(code)
103        }
104        SecretFileResolution::Ambiguous { candidates } => {
105            if output_json {
106                output::emit_error(
107                    "auth use",
108                    "ambiguous-secret",
109                    format!("codex-use: identifier matches multiple secrets: {target}"),
110                    Some(json!({
111                        "target": target,
112                        "candidates": candidates,
113                    })),
114                )?;
115            } else {
116                eprintln!("codex-use: identifier matches multiple secrets: {target}");
117                eprintln!("codex-use: candidates: {}", candidates.join(", "));
118            }
119            Ok(2)
120        }
121        SecretFileResolution::NotFound => {
122            if output_json {
123                output::emit_error(
124                    "auth use",
125                    "secret-not-found",
126                    format!("codex-use: secret not found: {target}"),
127                    Some(json!({
128                        "target": target,
129                    })),
130                )?;
131            } else {
132                eprintln!("codex-use: secret not found: {target}");
133            }
134            Ok(1)
135        }
136    }
137}
138
139fn apply_secret(
140    secret_dir: &Path,
141    secret_name: &str,
142    output_json: bool,
143) -> Result<(i32, Option<String>)> {
144    let source_file = secret_dir.join(secret_name);
145    if !source_file.is_file() {
146        if !output_json {
147            eprintln!("codex: requested secret file not found");
148        }
149        return Ok((1, None));
150    }
151
152    let auth_file = match paths::resolve_auth_file() {
153        Some(path) => path,
154        None => return Ok((1, None)),
155    };
156
157    if auth_file.is_file() {
158        let sync_result = crate::auth::sync::run_with_json(false)?;
159        if sync_result != 0 {
160            if !output_json {
161                eprintln!("codex: failed to sync current auth before switching secrets");
162            }
163            return Ok((1, None));
164        }
165    }
166
167    let contents = std::fs::read(&source_file)?;
168    fs::write_atomic(&auth_file, &contents, fs::SECRET_FILE_MODE)?;
169
170    let iso = auth::last_refresh_from_auth_file(&auth_file).unwrap_or(None);
171    let timestamp_path = secret_timestamp_path(&auth_file)?;
172    fs::write_timestamp(&timestamp_path, iso.as_deref())?;
173
174    if !output_json {
175        println!("codex: applied stored secret to {}", auth_file.display());
176    }
177    Ok((0, Some(auth_file.display().to_string())))
178}
179
180fn secret_timestamp_path(target_file: &Path) -> Result<PathBuf> {
181    paths::resolve_secret_timestamp_path(target_file)
182        .ok_or_else(|| anyhow::anyhow!("CODEX_SECRET_CACHE_DIR not resolved"))
183}
184
185#[cfg(test)]
186mod tests {
187    use super::secret_timestamp_path;
188    use nils_test_support::{EnvGuard, GlobalStateLock};
189    use pretty_assertions::assert_eq;
190    use std::path::Path;
191
192    #[test]
193    fn secret_timestamp_path_uses_cache_dir_and_default_file_name() {
194        let lock = GlobalStateLock::new();
195        let dir = tempfile::TempDir::new().expect("tempdir");
196        let cache = dir.path().join("cache");
197        std::fs::create_dir_all(&cache).expect("cache");
198        let cache_value = cache.to_string_lossy().to_string();
199        let _guard = EnvGuard::set(&lock, "CODEX_SECRET_CACHE_DIR", &cache_value);
200
201        let with_name =
202            secret_timestamp_path(Path::new("/tmp/demo-auth.json")).expect("timestamp path");
203        assert_eq!(with_name, cache.join("demo-auth.json.timestamp"));
204
205        let without_name = secret_timestamp_path(Path::new("")).expect("timestamp path");
206        assert_eq!(without_name, cache.join("auth.json.timestamp"));
207    }
208}