commit f8ad4cf3e380cdffba39b5e8f3bf59fa850578b3
parent ecef6d2c169ce679a243db2f6f1be2185cf9a509
Author: triesap <tyson@radroots.org>
Date: Wed, 8 Apr 2026 15:54:34 +0000
runtime: tighten shared bootstrap helpers
Diffstat:
6 files changed, 60 insertions(+), 157 deletions(-)
diff --git a/crates/net-core/src/error.rs b/crates/net-core/src/error.rs
@@ -31,6 +31,9 @@ pub enum NetError {
#[error("overwrite denied")]
OverwriteDenied,
+ #[error("persistence path required")]
+ PersistencePathRequired,
+
#[error("persistence unsupported")]
PersistenceUnsupported,
@@ -59,6 +62,7 @@ impl Clone for NetError {
NetError::InvalidKeyFile => NetError::InvalidKeyFile,
NetError::KeyIo => NetError::KeyIo,
NetError::OverwriteDenied => NetError::OverwriteDenied,
+ NetError::PersistencePathRequired => NetError::PersistencePathRequired,
NetError::PersistenceUnsupported => NetError::PersistenceUnsupported,
NetError::LoggingInit(s) => NetError::LoggingInit(s),
}
diff --git a/crates/net-core/src/keys.rs b/crates/net-core/src/keys.rs
@@ -7,10 +7,6 @@ use radroots_nostr::prelude::{
RadrootsNostrKeys, RadrootsNostrSecp256k1SecretKey, RadrootsNostrSecretKey,
RadrootsNostrToBech32,
};
-#[cfg(all(feature = "nostr-client", feature = "fs-persistence"))]
-use radroots_runtime_paths::{
- RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver, default_shared_identity_path,
-};
#[cfg(feature = "nostr-client")]
use serde::Deserialize;
#[cfg(feature = "nostr-client")]
@@ -267,43 +263,8 @@ impl KeysManager {
}
#[cfg(feature = "fs-persistence")]
- pub fn default_key_path() -> Option<PathBuf> {
- default_key_path_for(
- &RadrootsPathResolver::current(),
- RadrootsPathProfile::InteractiveUser,
- &RadrootsPathOverrides::default(),
- )
- }
-
- #[cfg(feature = "fs-persistence")]
- pub fn persist_best_practice(&self) -> Result<PathBuf> {
- let path = Self::default_key_path().ok_or(NetError::PersistenceUnsupported)?;
- if path.exists() {
- return Err(NetError::OverwriteDenied);
- }
- self.save_to_path_with_format(&path, KeyFormat::Json, true)?;
- Ok(path)
- }
-
- #[cfg(not(feature = "fs-persistence"))]
- pub fn persist_best_practice(&self) -> Result<PathBuf> {
- Err(NetError::PersistenceUnsupported)
- }
-
- #[cfg(feature = "fs-persistence")]
pub fn persist_with_config(&self, cfg: &KeyPersistenceConfig) -> Result<PathBuf> {
- let path = if let Some(p) = &cfg.path {
- p.clone()
- } else {
- #[cfg(feature = "fs-persistence")]
- {
- Self::default_key_path().ok_or(NetError::PersistenceUnsupported)?
- }
- #[cfg(not(feature = "fs-persistence"))]
- {
- return Err(NetError::PersistenceUnsupported);
- }
- };
+ let path = cfg.path.clone().ok_or(NetError::PersistencePathRequired)?;
self.save_to_path_with_format(&path, cfg.format, cfg.no_overwrite)?;
Ok(path)
}
@@ -314,15 +275,6 @@ impl KeysManager {
}
}
-#[cfg(all(feature = "nostr-client", feature = "fs-persistence"))]
-fn default_key_path_for(
- resolver: &RadrootsPathResolver,
- profile: RadrootsPathProfile,
- overrides: &RadrootsPathOverrides,
-) -> Option<PathBuf> {
- default_shared_identity_path(resolver, profile, overrides).ok()
-}
-
#[cfg(feature = "nostr-client")]
fn write_secret_atomically_noclobber(path: &Path, data: &[u8]) -> crate::error::Result<()> {
use std::io::Write;
@@ -355,41 +307,49 @@ fn write_secret_atomically_noclobber(path: &Path, data: &[u8]) -> crate::error::
#[cfg(all(test, feature = "nostr-client", feature = "fs-persistence"))]
mod tests {
- use std::path::PathBuf;
+ use tempfile::tempdir;
- use radroots_identity::RadrootsIdentity;
- use radroots_runtime_paths::{
- RadrootsHostEnvironment, RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver,
- RadrootsPlatform,
+ use crate::{
+ config::{KeyFormat, KeyPersistenceConfig},
+ error::NetError,
};
- use super::default_key_path_for;
+ use super::KeysManager;
+
+ #[test]
+ fn persist_with_config_requires_explicit_path() {
+ let mut manager = KeysManager::new();
+ let _ = manager.generate_in_memory();
+ let cfg = KeyPersistenceConfig {
+ path: None,
+ format: KeyFormat::Json,
+ no_overwrite: true,
+ };
+
+ let err = manager
+ .persist_with_config(&cfg)
+ .expect_err("implicit default persistence path should be rejected");
+
+ assert!(matches!(err, NetError::PersistencePathRequired));
+ }
#[test]
- fn default_key_path_matches_identity_default_path() {
- let resolver = RadrootsPathResolver::new(
- RadrootsPlatform::Linux,
- RadrootsHostEnvironment {
- home_dir: Some(PathBuf::from("/home/treesap")),
- ..RadrootsHostEnvironment::default()
- },
- );
- let overrides = RadrootsPathOverrides::default();
-
- let net_core_path =
- default_key_path_for(&resolver, RadrootsPathProfile::InteractiveUser, &overrides)
- .expect("net-core default key path should resolve");
- let identity_path = RadrootsIdentity::default_path_for(
- &resolver,
- RadrootsPathProfile::InteractiveUser,
- &overrides,
- )
- .expect("identity default path should resolve");
-
- assert_eq!(net_core_path, identity_path);
- assert_eq!(
- net_core_path,
- PathBuf::from("/home/treesap/.radroots/secrets/shared/identities/default.json")
- );
+ fn persist_with_config_writes_only_to_explicit_path() {
+ let dir = tempdir().expect("tempdir");
+ let path = dir.path().join("keys.json");
+ let mut manager = KeysManager::new();
+ let _ = manager.generate_in_memory();
+ let cfg = KeyPersistenceConfig {
+ path: Some(path.clone()),
+ format: KeyFormat::Json,
+ no_overwrite: true,
+ };
+
+ let written = manager
+ .persist_with_config(&cfg)
+ .expect("explicit persistence path should succeed");
+
+ assert_eq!(written, path);
+ assert!(written.exists());
}
}
diff --git a/crates/runtime/src/cli.rs b/crates/runtime/src/cli.rs
@@ -52,7 +52,7 @@ where
FL: Fn(&C) -> &str,
{
let (args, cfg) = parse_and_load_path::<Args, C, FP>(path_of)?;
- crate::tracing::init_with(logs_dir_of(&cfg), default_level)?;
+ crate::tracing::init_with_logs_dir(Path::new(logs_dir_of(&cfg)), default_level)?;
Ok((args, cfg))
}
@@ -75,7 +75,7 @@ where
env_prefix,
overrides_of,
)?;
- crate::tracing::init_with(logs_dir_of(&cfg), default_level)?;
+ crate::tracing::init_with_logs_dir(Path::new(logs_dir_of(&cfg)), default_level)?;
Ok((args, cfg))
}
diff --git a/crates/runtime/src/lib.rs b/crates/runtime/src/lib.rs
@@ -30,11 +30,8 @@ pub use secret_file::{local_wrapping_key_path, open_local_secret_file, seal_loca
pub use service::RadrootsNostrServiceConfig;
#[cfg(feature = "cli")]
pub use service::RadrootsServiceCliArgs;
-pub use service::{
- DEFAULT_SERVICE_IDENTITY_PATH, default_service_bootstrap_paths, default_service_config_path,
- default_service_identity_path, default_service_logs_dir, service_bootstrap_paths_for,
-};
+pub use service::{DEFAULT_SERVICE_IDENTITY_PATH, service_bootstrap_paths_for};
pub use signals::shutdown_signal;
pub use tracing::{
- default_shared_runtime_logs_dir, default_shared_runtime_logs_dir_for, init, init_with,
+ default_shared_runtime_logs_dir, default_shared_runtime_logs_dir_for, init, init_with_logs_dir,
};
diff --git a/crates/runtime/src/service.rs b/crates/runtime/src/service.rs
@@ -1,4 +1,5 @@
use serde::{Deserialize, Serialize};
+#[cfg(any(feature = "cli", test))]
use std::path::PathBuf;
#[cfg(feature = "cli")]
@@ -27,31 +28,6 @@ pub fn service_bootstrap_paths_for(
)
}
-pub fn default_service_bootstrap_paths(
- runtime_id: &str,
-) -> Result<RadrootsBootstrapPaths, RadrootsRuntimePathsError> {
- service_bootstrap_paths_for(
- &RadrootsPathResolver::current(),
- RadrootsPathProfile::InteractiveUser,
- &RadrootsPathOverrides::default(),
- runtime_id,
- )
-}
-
-pub fn default_service_config_path(runtime_id: &str) -> Result<PathBuf, RadrootsRuntimePathsError> {
- Ok(default_service_bootstrap_paths(runtime_id)?.config_path)
-}
-
-pub fn default_service_logs_dir(runtime_id: &str) -> Result<PathBuf, RadrootsRuntimePathsError> {
- Ok(default_service_bootstrap_paths(runtime_id)?.logs_dir)
-}
-
-pub fn default_service_identity_path(
- runtime_id: &str,
-) -> Result<PathBuf, RadrootsRuntimePathsError> {
- Ok(default_service_bootstrap_paths(runtime_id)?.identity_path)
-}
-
#[cfg(feature = "cli")]
#[derive(Args, Debug, Clone)]
pub struct RadrootsServiceCliArgs {
diff --git a/crates/runtime/src/tracing.rs b/crates/runtime/src/tracing.rs
@@ -9,7 +9,7 @@ use crate::error::RuntimeTracingError;
pub fn init() -> Result<(), RuntimeTracingError> {
let logs_dir = default_shared_runtime_logs_dir()?;
- init_with(logs_dir, None)
+ init_with_logs_dir(logs_dir, None)
}
pub fn default_shared_runtime_logs_dir() -> Result<PathBuf, RadrootsRuntimePathsError> {
@@ -28,17 +28,15 @@ pub fn default_shared_runtime_logs_dir_for(
resolve_shared_runtime_logs_dir(resolver, profile, overrides)
}
-pub fn init_with(
+pub fn init_with_logs_dir(
logs_dir: impl AsRef<Path>,
default_level: Option<&str>,
) -> Result<(), RuntimeTracingError> {
let logs_dir = logs_dir.as_ref();
- let env_dir = env_path("LOG_DIR").or_else(|| env_path("RADROOTS_LOG_DIR"));
let env_file = env_value("LOG_FILE").or_else(|| env_value("RADROOTS_LOG_FILE"));
let env_level = env_value("LOG_LEVEL").or_else(|| env_value("RUST_LOG"));
- let dir = resolve_log_dir(logs_dir, env_dir);
let opts = LoggingOptions {
- dir,
+ dir: Some(logs_dir.to_path_buf()),
file_name: env_file.unwrap_or_else(default_log_file_name),
stdout: true,
default_level: resolve_default_level(env_level, default_level),
@@ -100,10 +98,6 @@ fn env_value(key: &str) -> Option<String> {
normalize_env_value(&value)
}
-fn env_path(key: &str) -> Option<PathBuf> {
- env_value(key).map(PathBuf::from)
-}
-
fn normalize_env_value(value: &str) -> Option<String> {
let trimmed = value.trim();
if trimmed.is_empty() {
@@ -113,16 +107,6 @@ fn normalize_env_value(value: &str) -> Option<String> {
}
}
-fn resolve_log_dir(logs_dir: &Path, env_dir: Option<PathBuf>) -> Option<PathBuf> {
- env_dir.or_else(|| {
- if logs_dir.as_os_str().is_empty() {
- None
- } else {
- Some(logs_dir.to_path_buf())
- }
- })
-}
-
fn resolve_default_level(env_level: Option<String>, default_level: Option<&str>) -> Option<String> {
if let Some(level) = env_level {
Some(level)
@@ -135,15 +119,14 @@ fn resolve_default_level(env_level: Option<String>, default_level: Option<&str>)
mod tests {
use super::{
default_log_file_name, default_log_file_name_from_exe_name,
- default_shared_runtime_logs_dir_for, env_path, env_value, init_with, log_name_from_path,
- log_name_from_stem, normalize_env_value, resolve_default_level, resolve_log_dir,
- test_hooks,
+ default_shared_runtime_logs_dir_for, env_value, init_with_logs_dir, log_name_from_path,
+ log_name_from_stem, normalize_env_value, resolve_default_level, test_hooks,
};
use radroots_runtime_paths::{
RadrootsHostEnvironment, RadrootsPathOverrides, RadrootsPathProfile, RadrootsPathResolver,
RadrootsPlatform,
};
- use std::path::{Path, PathBuf};
+ use std::path::PathBuf;
use tempfile::tempdir;
#[test]
@@ -158,8 +141,6 @@ mod tests {
assert_eq!(env_value("RADROOTS_RUNTIME_TEST_MISSING_KEY"), None);
let home = env_value("HOME").expect("home env");
assert!(!home.is_empty());
- let home_path = env_path("HOME").expect("home path");
- assert_eq!(home_path, PathBuf::from(home));
}
#[test]
@@ -195,19 +176,6 @@ mod tests {
}
#[test]
- fn resolve_log_dir_prefers_env_and_handles_empty_logs_dir() {
- let fallback = resolve_log_dir(Path::new("logs"), None);
- assert_eq!(fallback, Some(PathBuf::from("logs")));
-
- let empty = resolve_log_dir(Path::new(""), None);
- assert_eq!(empty, None);
-
- let env_dir = PathBuf::from("env-logs");
- let resolved = resolve_log_dir(Path::new("logs"), Some(env_dir.clone()));
- assert_eq!(resolved, Some(env_dir));
- }
-
- #[test]
fn resolve_default_level_prefers_env_then_fallback() {
let env_value = resolve_default_level(Some("warn".to_string()), Some("info"));
assert_eq!(env_value, Some("warn".to_string()));
@@ -246,18 +214,16 @@ mod tests {
test_hooks::set_ignore_env(true);
let invalid = dir.path().join("not-a-dir");
std::fs::write(&invalid, "file").expect("write invalid path");
- let err_path = init_with(invalid.as_path(), Some("info"));
+ let err_path = init_with_logs_dir(invalid.as_path(), Some("info"));
assert!(err_path.is_err());
let invalid_str = invalid.to_string_lossy().to_string();
- let err_str = init_with(invalid_str.as_str(), Some("info"));
+ let err_str = init_with_logs_dir(invalid_str.as_str(), Some("info"));
assert!(err_str.is_err());
- test_hooks::set_ignore_env(false);
- let first = init_with(dir.path(), Some("info"));
+ let first = init_with_logs_dir(dir.path(), Some("info"));
assert!(first.is_ok());
let owned_path = dir.path().to_path_buf();
- let third = init_with(owned_path.as_path(), None);
+ let third = init_with_logs_dir(owned_path.as_path(), None);
assert!(third.is_ok());
- let fourth = init_with("logs", Some("debug"));
- assert!(fourth.is_ok());
+ test_hooks::set_ignore_env(false);
}
}