use anyhow::{Context, Result};
use rand::RngCore;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
#[derive(Serialize, Deserialize, Clone, Default)]
pub struct AuthConfig {
#[serde(default)]
pub base_url: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub token: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub session_cookie: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub install_id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub telemetry: Option<bool>,
#[serde(default, skip_serializing_if = "std::ops::Not::not")]
pub telemetry_notice_shown: bool,
}
fn config_dir() -> Result<PathBuf> {
let dir = dirs::config_dir()
.context("could not resolve user config directory")?
.join("wavekat");
fs::create_dir_all(&dir).with_context(|| format!("creating {}", dir.display()))?;
Ok(dir)
}
pub fn auth_path() -> Result<PathBuf> {
Ok(config_dir()?.join("auth.json"))
}
pub fn load() -> Result<AuthConfig> {
let path = auth_path()?;
let bytes = fs::read(&path).with_context(|| {
format!(
"not signed in — run `wk login` first (looked at {})",
path.display()
)
})?;
let cfg: AuthConfig =
serde_json::from_slice(&bytes).with_context(|| format!("parsing {}", path.display()))?;
Ok(cfg)
}
pub fn load_or_default() -> AuthConfig {
let Ok(path) = auth_path() else {
return AuthConfig::default();
};
let Ok(bytes) = fs::read(&path) else {
return AuthConfig::default();
};
serde_json::from_slice(&bytes).unwrap_or_default()
}
#[cfg_attr(not(feature = "telemetry"), allow(dead_code))]
pub fn new_install_id() -> String {
let mut bytes = [0u8; 16];
rand::thread_rng().fill_bytes(&mut bytes);
bytes[6] = (bytes[6] & 0x0f) | 0x40;
bytes[8] = (bytes[8] & 0x3f) | 0x80;
format!(
"{:02x}{:02x}{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}-{:02x}{:02x}{:02x}{:02x}{:02x}{:02x}",
bytes[0], bytes[1], bytes[2], bytes[3],
bytes[4], bytes[5],
bytes[6], bytes[7],
bytes[8], bytes[9],
bytes[10], bytes[11], bytes[12], bytes[13], bytes[14], bytes[15],
)
}
#[cfg_attr(not(feature = "telemetry"), allow(dead_code))]
pub fn ensure_install_id() -> Option<String> {
let mut cfg = load_or_default();
if let Some(id) = cfg.install_id.as_deref() {
if !id.is_empty() {
return Some(id.to_string());
}
}
let id = new_install_id();
cfg.install_id = Some(id.clone());
let _ = save(&cfg);
Some(id)
}
pub fn save(cfg: &AuthConfig) -> Result<()> {
let path = auth_path()?;
let bytes = serde_json::to_vec_pretty(cfg)?;
fs::write(&path, bytes).with_context(|| format!("writing {}", path.display()))?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
fs::set_permissions(&path, fs::Permissions::from_mode(0o600))
.with_context(|| format!("chmod 600 {}", path.display()))?;
}
Ok(())
}
pub fn clear() -> Result<bool> {
let path = auth_path()?;
if path.exists() {
fs::remove_file(&path).with_context(|| format!("removing {}", path.display()))?;
Ok(true)
} else {
Ok(false)
}
}