use anyhow::{Context, Result};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
#[derive(Debug, Serialize, Deserialize, Clone)]
pub struct AgentSession {
pub instance: String,
pub base_url: String,
pub node_id: String,
pub public_key: String,
pub secret_key_hex: String,
pub mnemonic: String,
pub token: String,
pub refresh_token: String,
pub onboarded_at: DateTime<Utc>,
pub last_login_at: DateTime<Utc>,
}
impl AgentSession {
pub fn file_path(dev_dir: &Path, instance: &str) -> PathBuf {
let name = if instance.is_empty() {
"default-agent-session.json".to_string()
} else {
format!("{instance}-agent-session.json")
};
dev_dir.join(name)
}
pub fn load(dev_dir: &Path, instance: &str) -> Result<Option<Self>> {
let path = Self::file_path(dev_dir, instance);
if !path.exists() {
return Ok(None);
}
let text = std::fs::read_to_string(&path)
.with_context(|| format!("read {}", path.display()))?;
let session: Self = serde_json::from_str(&text)
.with_context(|| format!("parse {}", path.display()))?;
Ok(Some(session))
}
pub fn save(&self, dev_dir: &Path) -> Result<PathBuf> {
std::fs::create_dir_all(dev_dir)
.with_context(|| format!("mkdir -p {}", dev_dir.display()))?;
let final_path = Self::file_path(dev_dir, &self.instance);
let tmp_path = final_path.with_extension("json.tmp");
let text = serde_json::to_string_pretty(self).context("serialize agent session")?;
std::fs::write(&tmp_path, text)
.with_context(|| format!("write {}", tmp_path.display()))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perms = std::fs::Permissions::from_mode(0o600);
std::fs::set_permissions(&tmp_path, perms)
.with_context(|| format!("chmod 0600 {}", tmp_path.display()))?;
}
std::fs::rename(&tmp_path, &final_path).with_context(|| {
format!("rename {} → {}", tmp_path.display(), final_path.display())
})?;
Ok(final_path)
}
}
pub fn redact_token(token: &str) -> String {
let prefix: String = token.chars().take(12).collect();
format!("{prefix}…")
}