use std::path::Path;
use serde_json::{Map, Value};
pub(crate) const OPENAI_ACCOUNT_ID_ALIASES: &[&str] = &[
"account_id",
"chatgpt_account_id",
"chatgptAccountId",
"chatgpt_account",
"accountId",
];
pub(crate) fn epoch_ms_is_expired(expires_at_ms: i64, now_ms: i64) -> bool {
now_ms >= expires_at_ms
}
pub(crate) fn extract_account_id(obj: &Value) -> Option<String> {
let map = obj.as_object()?;
OPENAI_ACCOUNT_ID_ALIASES.iter().find_map(|key| {
map.get(*key)
.and_then(Value::as_str)
.map(str::trim)
.filter(|value| !value.is_empty())
.map(str::to_string)
})
}
#[allow(dead_code)]
pub(crate) fn load_json(path: &Path) -> anyhow::Result<Option<Map<String, Value>>> {
let contents = match std::fs::read_to_string(path) {
Ok(contents) => contents,
Err(source) if source.kind() == std::io::ErrorKind::NotFound => return Ok(None),
Err(source) => return Err(source.into()),
};
let value: Value = serde_json::from_str(&contents)?;
match value {
Value::Object(document) => Ok(Some(document)),
_ => anyhow::bail!("top-level auth document must be a JSON object"),
}
}
pub(crate) fn save_json_0600(path: &Path, value: &Value) -> anyhow::Result<()> {
ensure_parent_dir(path)?;
let bytes = serde_json::to_vec_pretty(value)?;
prepare_existing_file_for_private_replace(path)?;
crate::fs_atomic::atomic_write_sync(path, &bytes)?;
restrict_file_permissions(path)?;
Ok(())
}
fn ensure_parent_dir(path: &Path) -> anyhow::Result<()> {
let Some(parent) = path.parent().filter(|path| !path.as_os_str().is_empty()) else {
return Ok(());
};
std::fs::create_dir_all(parent)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(parent, std::fs::Permissions::from_mode(0o700))?;
}
Ok(())
}
#[cfg(unix)]
fn prepare_existing_file_for_private_replace(path: &Path) -> anyhow::Result<()> {
match std::fs::metadata(path) {
Ok(_) => restrict_file_permissions(path),
Err(source) if source.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(source) => Err(source.into()),
}
}
#[cfg(not(unix))]
fn prepare_existing_file_for_private_replace(_path: &Path) -> anyhow::Result<()> {
Ok(())
}
#[cfg(unix)]
fn restrict_file_permissions(path: &Path) -> anyhow::Result<()> {
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(path, std::fs::Permissions::from_mode(0o600))?;
Ok(())
}
#[cfg(not(unix))]
fn restrict_file_permissions(_path: &Path) -> anyhow::Result<()> {
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use std::path::PathBuf;
struct TestDir(PathBuf);
impl TestDir {
fn new(tag: &str) -> Self {
let path = std::env::temp_dir().join(format!(
"dirge_file_store_{tag}_{}_{}",
std::process::id(),
uuid::Uuid::new_v4().simple()
));
std::fs::create_dir_all(&path).unwrap();
Self(path)
}
fn file(&self) -> PathBuf {
self.0.join("auth.json")
}
}
impl Drop for TestDir {
fn drop(&mut self) {
let _ = std::fs::remove_dir_all(&self.0);
}
}
#[test]
fn epoch_ms_is_expired_uses_inclusive_lower_bound() {
assert!(!epoch_ms_is_expired(1_000, 999));
assert!(epoch_ms_is_expired(1_000, 1_000));
assert!(epoch_ms_is_expired(1_000, 1_001));
}
#[test]
fn extract_account_id_prefers_canonical_then_aliases() {
assert_eq!(
extract_account_id(&json!({"account_id": "canon", "chatgpt_account_id": "alias"}))
.as_deref(),
Some("canon")
);
assert_eq!(
extract_account_id(&json!({"chatgptAccountId": " acct "})).as_deref(),
Some("acct")
);
assert_eq!(extract_account_id(&json!({"account_id": " "})), None);
assert_eq!(extract_account_id(&json!({})), None);
assert_eq!(extract_account_id(&json!("not-an-object")), None);
}
#[test]
fn load_json_returns_none_when_absent() {
let dir = TestDir::new("absent");
assert!(load_json(&dir.file()).unwrap().is_none());
}
#[test]
fn load_json_parses_object() {
let dir = TestDir::new("parse");
std::fs::write(dir.file(), r#"{"a": 1}"#).unwrap();
let map = load_json(&dir.file()).unwrap().unwrap();
assert_eq!(map.get("a"), Some(&json!(1)));
}
#[test]
fn save_json_0600_writes_private_file_that_round_trips() {
let dir = TestDir::new("save");
let value = json!({"hello": "world"});
save_json_0600(&dir.file(), &value).unwrap();
let loaded = load_json(&dir.file()).unwrap().unwrap();
assert_eq!(loaded.get("hello"), Some(&json!("world")));
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = std::fs::metadata(dir.file()).unwrap().permissions().mode() & 0o777;
assert_eq!(mode, 0o600);
}
}
}