pub mod creds;
pub mod fetch;
pub mod types;
pub mod vendor;
use std::path::{Path, PathBuf};
use crate::config::GrokbotConfig;
use crate::error::{AppError, Result};
pub const APP_CONFIG_DIR: &str = "Grok Bot";
pub const SECRETS_FILE_NAME: &str = "sand-secrets.json";
pub fn secrets_path_in(cfg: &GrokbotConfig, home: &Path) -> PathBuf {
cfg.secrets_path.clone().unwrap_or_else(|| {
if cfg!(target_os = "macos") {
home.join("Library/Application Support")
.join(APP_CONFIG_DIR)
.join(SECRETS_FILE_NAME)
} else {
home.join(".config")
.join(APP_CONFIG_DIR)
.join(SECRETS_FILE_NAME)
}
})
}
pub fn windows_secrets_path_in(cfg: &GrokbotConfig, app_data: &Path) -> PathBuf {
cfg.secrets_path
.clone()
.unwrap_or_else(|| app_data.join(APP_CONFIG_DIR).join(SECRETS_FILE_NAME))
}
pub fn secrets_path(cfg: &GrokbotConfig) -> Result<PathBuf> {
if cfg!(windows) {
let base = directories::BaseDirs::new().ok_or_else(|| {
AppError::Other("could not resolve the platform config directory".into())
})?;
return Ok(windows_secrets_path_in(cfg, base.config_dir()));
}
Ok(secrets_path_in(cfg, &crate::cache::home_dir()?))
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
pub fn resolve_credentials(cfg: &GrokbotConfig) -> Result<creds::GrokbotCredentials> {
let path = secrets_path(cfg)?;
creds::read_at(&path, &creds::oscrypt_key()?)
}
#[cfg(windows)]
pub fn resolve_credentials(cfg: &GrokbotConfig) -> Result<creds::GrokbotCredentials> {
let path = secrets_path(cfg)?;
if !path.is_file() {
return Err(creds::missing_file_error(&path));
}
creds::read_at(&path, &creds::windows_oscrypt_key(&path)?)
}
#[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
pub fn resolve_credentials(_cfg: &GrokbotConfig) -> Result<creds::GrokbotCredentials> {
Err(AppError::Credentials(
"Grok Bot usage is supported on Linux, macOS and Windows — the desktop app's \
credential store is not read on this platform"
.into(),
))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
#[cfg(not(target_os = "macos"))]
fn the_default_path_lives_under_the_apps_xdg_config_dir() {
let cfg = GrokbotConfig::default();
let path = secrets_path_in(&cfg, Path::new("/home/u"));
assert_eq!(
path,
PathBuf::from("/home/u/.config/Grok Bot/sand-secrets.json")
);
}
#[test]
#[cfg(target_os = "macos")]
fn the_default_path_lives_under_application_support() {
let cfg = GrokbotConfig::default();
let path = secrets_path_in(&cfg, Path::new("/Users/u"));
assert_eq!(
path,
PathBuf::from("/Users/u/Library/Application Support/Grok Bot/sand-secrets.json")
);
}
#[test]
fn a_configured_secrets_path_wins() {
let cfg = GrokbotConfig {
enabled: true,
secrets_path: Some(PathBuf::from("/elsewhere/secrets.json")),
};
assert_eq!(
secrets_path_in(&cfg, Path::new("/home/u")),
PathBuf::from("/elsewhere/secrets.json")
);
}
#[test]
fn the_windows_default_path_lives_under_appdata() {
let cfg = GrokbotConfig::default();
assert_eq!(
windows_secrets_path_in(&cfg, Path::new("C:/Users/u/AppData/Roaming")),
Path::new("C:/Users/u/AppData/Roaming")
.join("Grok Bot")
.join("sand-secrets.json")
);
let configured = GrokbotConfig {
enabled: true,
secrets_path: Some(PathBuf::from("D:/elsewhere/secrets.json")),
};
assert_eq!(
windows_secrets_path_in(&configured, Path::new("C:/ignored")),
PathBuf::from("D:/elsewhere/secrets.json")
);
}
#[cfg(windows)]
#[test]
fn a_missing_windows_credential_file_names_the_file() {
let td = tempfile::TempDir::new().unwrap();
let cfg = GrokbotConfig {
enabled: true,
secrets_path: Some(td.path().join("sand-secrets.json")),
};
let err = resolve_credentials(&cfg).unwrap_err();
assert!(matches!(err, AppError::Credentials(_)), "{err:?}");
assert!(err.to_string().contains("sand-secrets.json"), "{err}");
}
#[cfg(not(any(target_os = "linux", target_os = "macos", windows)))]
#[test]
fn unsupported_platforms_fail_closed_with_a_credentials_error() {
let err = resolve_credentials(&GrokbotConfig::default()).unwrap_err();
assert!(matches!(err, AppError::Credentials(_)), "{err:?}");
assert!(
err.to_string().contains("not read on this platform"),
"{err}"
);
}
}