-
Notifications
You must be signed in to change notification settings - Fork 48
feat: support MOSTRO_NSEC_PRIVKEY env var for Nostr private key #713
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
984b733
dd9e3e3
b9565bf
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -12,6 +12,30 @@ use std::io::IsTerminal; | |
| use std::path::PathBuf; | ||
|
|
||
| const DB_FILENAME: &str = "mostro.db"; | ||
| const ENV_FILENAME: &str = ".env"; | ||
| const NSEC_ENV_VAR: &str = "MOSTRO_NSEC_PRIVKEY"; | ||
|
|
||
| /// Loads the optional `<settings_dir>/.env` file so that values placed there | ||
| /// become available through `std::env::var`. Variables already set in the | ||
| /// process environment take precedence and are never overwritten. | ||
| fn load_env_file(settings_dir: &std::path::Path) { | ||
| let env_file = settings_dir.join(ENV_FILENAME); | ||
| if env_file.exists() { | ||
| let _ = dotenvy::from_path(&env_file); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This ignores any error from Useful? React with 👍 / 👎. |
||
| } | ||
| } | ||
|
|
||
| /// If the `MOSTRO_NSEC_PRIVKEY` environment variable is set to a non-empty | ||
| /// value, override the nsec loaded from `settings.toml`. Whitespace is | ||
| /// trimmed; blank values are ignored so the TOML stays the fallback. | ||
| fn apply_nsec_env_override(settings: &mut Settings) { | ||
| if let Ok(nsec_from_env) = std::env::var(NSEC_ENV_VAR) { | ||
| let trimmed = nsec_from_env.trim(); | ||
| if !trimmed.is_empty() { | ||
| settings.nostr.nsec_privkey = trimmed.to_string(); | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /// Validates Mostro settings on startup | ||
| fn validate_mostro_settings(settings: &Settings) -> Result<(), MostroError> { | ||
|
|
@@ -56,10 +80,15 @@ pub fn init_configuration_file(config_path: Option<String>) -> Result<(), Mostro | |
| std::fs::create_dir_all(&settings_dir) | ||
| .map_err(|e| MostroInternalErr(ServiceError::IOError(e.to_string())))?; | ||
| } | ||
|
|
||
| // Load `<settings_dir>/.env` so MOSTRO_NSEC_PRIVKEY (and any future env | ||
| // overrides) can be read from it. Real env vars keep precedence. | ||
| load_env_file(&settings_dir); | ||
|
|
||
| let config_file_path = settings_dir.join("settings.toml"); | ||
|
|
||
| if !config_file_path.exists() { | ||
| let settings = if std::io::stdin().is_terminal() { | ||
| let mut settings = if std::io::stdin().is_terminal() { | ||
| // Interactive: show setup menu (wizard or manual template) | ||
| wizard::run_setup_menu(&settings_dir, &config_file_path)? | ||
| } else { | ||
|
|
@@ -73,6 +102,7 @@ pub fn init_configuration_file(config_path: Option<String>) -> Result<(), Mostro | |
| std::process::exit(0); | ||
| }; | ||
|
|
||
| apply_nsec_env_override(&mut settings); | ||
| validate_mostro_settings(&settings)?; | ||
| init_mostro_settings(settings); | ||
| tracing::info!("Settings correctly loaded!"); | ||
|
|
@@ -87,6 +117,10 @@ pub fn init_configuration_file(config_path: Option<String>) -> Result<(), Mostro | |
| let mut settings: Settings = toml::from_str(&contents) | ||
| .map_err(|e| MostroInternalErr(ServiceError::IOError(e.to_string())))?; | ||
|
|
||
| // Apply MOSTRO_NSEC_PRIVKEY override before validation so an empty TOML | ||
| // value is fine when the env var is set. | ||
| apply_nsec_env_override(&mut settings); | ||
|
|
||
| // Validate settings before initializing | ||
| validate_mostro_settings(&settings)?; | ||
|
|
||
|
|
@@ -100,3 +134,128 @@ pub fn init_configuration_file(config_path: Option<String>) -> Result<(), Mostro | |
|
|
||
| Ok(()) | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use super::*; | ||
| use crate::config::types::{ | ||
| DatabaseSettings, LightningSettings, MostroSettings, NostrSettings, RpcSettings, | ||
| }; | ||
| use std::sync::Mutex; | ||
|
|
||
| // Tests that read/write MOSTRO_NSEC_PRIVKEY must run serially because the | ||
| // process environment is shared across threads. | ||
| static ENV_LOCK: Mutex<()> = Mutex::new(()); | ||
|
|
||
| /// RAII guard that saves the current value of an env var and restores it | ||
| /// on drop, so tests don't leak state into each other. | ||
| struct EnvVarGuard { | ||
| key: &'static str, | ||
| previous: Option<String>, | ||
| } | ||
|
|
||
| impl EnvVarGuard { | ||
| fn new(key: &'static str) -> Self { | ||
| let previous = std::env::var(key).ok(); | ||
| std::env::remove_var(key); | ||
| Self { key, previous } | ||
| } | ||
|
|
||
| fn set(&self, value: &str) { | ||
| std::env::set_var(self.key, value); | ||
| } | ||
| } | ||
|
|
||
| impl Drop for EnvVarGuard { | ||
| fn drop(&mut self) { | ||
| match &self.previous { | ||
| Some(val) => std::env::set_var(self.key, val), | ||
| None => std::env::remove_var(self.key), | ||
| } | ||
| } | ||
| } | ||
|
|
||
| fn make_settings(nsec: &str) -> Settings { | ||
| Settings { | ||
| database: DatabaseSettings::default(), | ||
| lightning: LightningSettings::default(), | ||
| nostr: NostrSettings { | ||
| nsec_privkey: nsec.to_string(), | ||
| relays: vec!["wss://relay.test".to_string()], | ||
| }, | ||
| mostro: MostroSettings::default(), | ||
| rpc: RpcSettings::default(), | ||
| expiration: None, | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn env_var_overrides_toml_nsec() { | ||
| let _lock = ENV_LOCK.lock().unwrap(); | ||
| let guard = EnvVarGuard::new(NSEC_ENV_VAR); | ||
| guard.set("nsec_from_env"); | ||
|
|
||
| let mut settings = make_settings("nsec_from_toml"); | ||
| apply_nsec_env_override(&mut settings); | ||
|
|
||
| assert_eq!(settings.nostr.nsec_privkey, "nsec_from_env"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn empty_env_var_falls_back_to_toml() { | ||
| let _lock = ENV_LOCK.lock().unwrap(); | ||
| let guard = EnvVarGuard::new(NSEC_ENV_VAR); | ||
| guard.set(""); | ||
|
|
||
| let mut settings = make_settings("nsec_from_toml"); | ||
| apply_nsec_env_override(&mut settings); | ||
|
|
||
| assert_eq!(settings.nostr.nsec_privkey, "nsec_from_toml"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn no_env_var_keeps_toml() { | ||
| let _lock = ENV_LOCK.lock().unwrap(); | ||
| let _guard = EnvVarGuard::new(NSEC_ENV_VAR); | ||
|
|
||
| let mut settings = make_settings("nsec_from_toml"); | ||
| apply_nsec_env_override(&mut settings); | ||
|
|
||
| assert_eq!(settings.nostr.nsec_privkey, "nsec_from_toml"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn whitespace_only_env_is_ignored() { | ||
| let _lock = ENV_LOCK.lock().unwrap(); | ||
| let guard = EnvVarGuard::new(NSEC_ENV_VAR); | ||
| guard.set(" \t "); | ||
|
|
||
| let mut settings = make_settings("nsec_from_toml"); | ||
| apply_nsec_env_override(&mut settings); | ||
|
|
||
| assert_eq!(settings.nostr.nsec_privkey, "nsec_from_toml"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn env_var_value_is_trimmed() { | ||
| let _lock = ENV_LOCK.lock().unwrap(); | ||
| let guard = EnvVarGuard::new(NSEC_ENV_VAR); | ||
| guard.set(" nsec_from_env "); | ||
|
|
||
| let mut settings = make_settings("nsec_from_toml"); | ||
| apply_nsec_env_override(&mut settings); | ||
|
|
||
| assert_eq!(settings.nostr.nsec_privkey, "nsec_from_env"); | ||
| } | ||
|
|
||
| #[test] | ||
| fn toml_parses_without_nsec_privkey_field() { | ||
| // Operators who rely exclusively on MOSTRO_NSEC_PRIVKEY should be able | ||
| // to omit nsec_privkey from settings.toml entirely. | ||
| let toml_without_nsec = r#"relays = ["wss://relay.test"]"#; | ||
| let nostr: NostrSettings = | ||
| toml::from_str(toml_without_nsec).expect("nsec_privkey should be optional in TOML"); | ||
| assert_eq!(nostr.nsec_privkey, ""); | ||
| assert_eq!(nostr.relays, vec!["wss://relay.test"]); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Add a language specifier to the new fenced code block (MD040).
The
.envexample opens with a bare triple-backtick, which violates markdownlint MD040 (also flagged by static analysis on line 524).📝 Proposed fix
As per coding guidelines: "Add a language specifier to every fenced code block in documentation to comply with markdownlint MD040".
🧰 Tools
🪛 markdownlint-cli2 (0.22.1)
[warning] 524-524: Fenced code blocks should have a language specified
(MD040, fenced-code-language)
🤖 Prompt for AI Agents