Skip to main content

codex_cli/auth/
sync.rs

1use anyhow::Result;
2use serde_json::json;
3use std::path::{Path, PathBuf};
4
5use crate::auth;
6use crate::auth::output::{self, AuthSyncResult};
7use crate::fs;
8use crate::paths;
9
10pub fn run() -> Result<i32> {
11    run_with_json(false)
12}
13
14pub fn run_with_json(output_json: bool) -> Result<i32> {
15    let auth_file = match paths::resolve_auth_file() {
16        Some(path) => path,
17        None => {
18            if output_json {
19                output::emit_result(
20                    "auth sync",
21                    AuthSyncResult {
22                        auth_file: String::new(),
23                        synced: 0,
24                        skipped: 0,
25                        failed: 0,
26                        updated_files: Vec::new(),
27                    },
28                )?;
29            }
30            return Ok(0);
31        }
32    };
33
34    if !auth_file.is_file() {
35        if output_json {
36            output::emit_result(
37                "auth sync",
38                AuthSyncResult {
39                    auth_file: auth_file.display().to_string(),
40                    synced: 0,
41                    skipped: 1,
42                    failed: 0,
43                    updated_files: Vec::new(),
44                },
45            )?;
46        }
47        return Ok(0);
48    }
49
50    let auth_key = match auth::identity_key_from_auth_file(&auth_file) {
51        Ok(Some(key)) => key,
52        _ => {
53            if output_json {
54                output::emit_result(
55                    "auth sync",
56                    AuthSyncResult {
57                        auth_file: auth_file.display().to_string(),
58                        synced: 0,
59                        skipped: 1,
60                        failed: 0,
61                        updated_files: Vec::new(),
62                    },
63                )?;
64            }
65            return Ok(0);
66        }
67    };
68
69    let auth_last_refresh = auth::last_refresh_from_auth_file(&auth_file).unwrap_or(None);
70    let auth_hash = match fs::sha256_file(&auth_file) {
71        Ok(hash) => hash,
72        Err(_) => {
73            if output_json {
74                output::emit_error(
75                    "auth sync",
76                    "hash-failed",
77                    format!("failed to hash {}", auth_file.display()),
78                    Some(json!({
79                        "path": auth_file.display().to_string(),
80                    })),
81                )?;
82            } else {
83                eprintln!("codex: failed to hash {}", auth_file.display());
84            }
85            return Ok(1);
86        }
87    };
88
89    let mut synced = 0usize;
90    let mut skipped = 0usize;
91    let failed = 0usize;
92    let mut updated_files: Vec<String> = Vec::new();
93
94    let secret_dir = paths::resolve_secret_dir();
95    if let Some(secret_dir) = secret_dir
96        && let Ok(entries) = std::fs::read_dir(&secret_dir)
97    {
98        for entry in entries.flatten() {
99            let path = entry.path();
100            if path.extension().and_then(|s| s.to_str()) != Some("json") {
101                continue;
102            }
103            let candidate_key = match auth::identity_key_from_auth_file(&path) {
104                Ok(Some(key)) => key,
105                _ => {
106                    skipped += 1;
107                    continue;
108                }
109            };
110            if candidate_key != auth_key {
111                skipped += 1;
112                continue;
113            }
114
115            let secret_hash = match fs::sha256_file(&path) {
116                Ok(hash) => hash,
117                Err(_) => {
118                    if output_json {
119                        output::emit_error(
120                            "auth sync",
121                            "hash-failed",
122                            format!("failed to hash {}", path.display()),
123                            Some(json!({
124                                "path": path.display().to_string(),
125                            })),
126                        )?;
127                    } else {
128                        eprintln!("codex: failed to hash {}", path.display());
129                    }
130                    return Ok(1);
131                }
132            };
133            if secret_hash == auth_hash {
134                skipped += 1;
135                continue;
136            }
137
138            let contents = std::fs::read(&auth_file)?;
139            fs::write_atomic(&path, &contents, fs::SECRET_FILE_MODE)?;
140
141            let timestamp_path = secret_timestamp_path(&path)?;
142            fs::write_timestamp(&timestamp_path, auth_last_refresh.as_deref())?;
143            synced += 1;
144            updated_files.push(path.display().to_string());
145        }
146    }
147
148    let auth_timestamp = secret_timestamp_path(&auth_file)?;
149    fs::write_timestamp(&auth_timestamp, auth_last_refresh.as_deref())?;
150
151    if output_json {
152        output::emit_result(
153            "auth sync",
154            AuthSyncResult {
155                auth_file: auth_file.display().to_string(),
156                synced,
157                skipped,
158                failed,
159                updated_files,
160            },
161        )?;
162    }
163
164    Ok(0)
165}
166
167fn secret_timestamp_path(target_file: &Path) -> Result<PathBuf> {
168    let cache_dir = paths::resolve_secret_cache_dir()
169        .ok_or_else(|| anyhow::anyhow!("CODEX_SECRET_CACHE_DIR not resolved"))?;
170    let name = target_file
171        .file_name()
172        .and_then(|name| name.to_str())
173        .unwrap_or("auth.json");
174    Ok(cache_dir.join(format!("{name}.timestamp")))
175}