codex_cli/auth/
use_secret.rs1use 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(×tamp_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}