commit 95bdf1753b46d172f983ee41d1773e283826ab55
parent 41803c15495a4663662ffa8c9a72be9f0534a599
Author: triesap <tyson@radroots.org>
Date: Sun, 22 Mar 2026 18:34:16 +0000
config: load runtime settings from env files
- replace the repo-root config contract with explicit .env parsing and validation
- add .env.example and update the cli, tests, and discovery docs to use --env-file
- remove the legacy toml example and dependency while tightening config parse errors
- validate with cargo metadata --format-version 1 --no-deps, cargo fmt --all --check, and the repo-root cargo test --locked lane on the reconciled tree
Diffstat:
10 files changed, 430 insertions(+), 239 deletions(-)
diff --git a/.env.example b/.env.example
@@ -0,0 +1,33 @@
+# explicit runtime contract for myc
+# copy to `.env` for local runs and replace paths/relays/domains with real values
+
+MYC_SERVICE_INSTANCE_NAME=myc
+MYC_LOGGING_FILTER=info,myc=info
+
+MYC_PATHS_STATE_DIR=var
+MYC_PATHS_SIGNER_IDENTITY_PATH=identity.json
+MYC_PATHS_USER_IDENTITY_PATH=user-identity.json
+
+MYC_AUDIT_DEFAULT_READ_LIMIT=200
+MYC_AUDIT_MAX_ACTIVE_FILE_BYTES=262144
+MYC_AUDIT_MAX_ARCHIVED_FILES=8
+
+MYC_DISCOVERY_ENABLED=true
+MYC_DISCOVERY_DOMAIN=signer.example.com
+MYC_DISCOVERY_HANDLER_IDENTIFIER=myc
+MYC_DISCOVERY_APP_IDENTITY_PATH=app-identity.json
+MYC_DISCOVERY_PUBLIC_RELAYS=wss://relay.example.com
+MYC_DISCOVERY_PUBLISH_RELAYS=wss://relay.example.com
+MYC_DISCOVERY_NOSTRCONNECT_URL_TEMPLATE=https://signer.example.com/connect?uri=<nostrconnect>
+MYC_DISCOVERY_NIP05_OUTPUT_PATH=public/.well-known/nostr.json
+MYC_DISCOVERY_METADATA_NAME=myc
+MYC_DISCOVERY_METADATA_DISPLAY_NAME=Mycorrhiza
+MYC_DISCOVERY_METADATA_ABOUT=NIP-46 signer
+MYC_DISCOVERY_METADATA_WEBSITE=https://signer.example.com
+MYC_DISCOVERY_METADATA_PICTURE=https://signer.example.com/logo.png
+
+MYC_POLICY_CONNECTION_APPROVAL=explicit_user
+
+MYC_TRANSPORT_ENABLED=true
+MYC_TRANSPORT_CONNECT_TIMEOUT_SECS=10
+MYC_TRANSPORT_RELAYS=wss://relay.example.com
diff --git a/.gitignore b/.gitignore
@@ -3,6 +3,7 @@
# Local environment files
.env
.env.*
+!.env.example
# OS and editor files
.DS_Store
diff --git a/Cargo.lock b/Cargo.lock
@@ -1044,7 +1044,6 @@ dependencies = [
"thiserror 2.0.18",
"tokio",
"tokio-tungstenite",
- "toml",
"tracing",
"tracing-subscriber",
"url",
diff --git a/Cargo.toml b/Cargo.toml
@@ -24,7 +24,6 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "2.0"
tokio = { version = "1.48", features = ["macros", "net", "rt-multi-thread", "sync", "time"] }
-toml = "0.8"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
url = "2.5"
diff --git a/config.example.toml b/config.example.toml
@@ -1,40 +0,0 @@
-[service]
-instance_name = "myc"
-
-[logging]
-filter = "info,myc=info"
-
-[paths]
-state_dir = "var"
-signer_identity_path = "identity.json"
-user_identity_path = "user-identity.json"
-
-[audit]
-default_read_limit = 200
-max_active_file_bytes = 262144
-max_archived_files = 8
-
-[discovery]
-enabled = true
-domain = "signer.example.com"
-handler_identifier = "myc"
-app_identity_path = "app-identity.json"
-public_relays = ["wss://relay.example.com"]
-publish_relays = ["wss://relay.example.com"]
-nostrconnect_url_template = "https://signer.example.com/connect?uri=<nostrconnect>"
-nip05_output_path = "public/.well-known/nostr.json"
-
-[discovery.metadata]
-name = "myc"
-display_name = "Mycorrhiza"
-about = "NIP-46 signer"
-website = "https://signer.example.com"
-picture = "https://signer.example.com/logo.png"
-
-[policy]
-connection_approval = "explicit_user"
-
-[transport]
-enabled = true
-connect_timeout_secs = 10
-relays = ["wss://relay.example.com"]
diff --git a/src/cli.rs b/src/cli.rs
@@ -11,7 +11,7 @@ use serde::Serialize;
use crate::app::MycRuntime;
use crate::audit::{MycOperationAuditKind, MycOperationAuditOutcome, MycOperationAuditRecord};
-use crate::config::{DEFAULT_CONFIG_PATH, MycConfig};
+use crate::config::{DEFAULT_ENV_PATH, MycConfig};
use crate::control::{accept_client_uri, authorize_auth_challenge, parse_permission_values};
use crate::discovery::{
MycDiscoveryContext, MycDiscoveryRepairSummary, diff_live_nip89, fetch_live_nip89,
@@ -24,8 +24,8 @@ use crate::logging;
#[command(name = "myc")]
#[command(about = "Mycorrhiza NIP-46 signer service")]
pub struct MycCli {
- #[arg(long, global = true)]
- config: Option<PathBuf>,
+ #[arg(long = "env-file", global = true)]
+ env_file: Option<PathBuf>,
#[command(subcommand)]
command: Option<MycCommand>,
}
@@ -257,7 +257,7 @@ pub enum MycDiscoveryRepairAttemptOutput {
pub async fn run_from_env() -> Result<(), MycError> {
let cli = MycCli::parse();
- let config = load_config(cli.config.as_deref())?;
+ let config = load_config(cli.env_file.as_deref())?;
match cli.command.unwrap_or(MycCommand::Run) {
MycCommand::Run => {
@@ -433,8 +433,8 @@ pub async fn run_from_env() -> Result<(), MycError> {
fn load_config(path: Option<&Path>) -> Result<MycConfig, MycError> {
match path {
- Some(path) => MycConfig::load_from_path_if_exists(path),
- None => MycConfig::load_from_path_if_exists(DEFAULT_CONFIG_PATH),
+ Some(path) => MycConfig::load_from_env_path(path),
+ None => MycConfig::load_from_env_path(DEFAULT_ENV_PATH),
}
}
diff --git a/src/config.rs b/src/config.rs
@@ -1,3 +1,4 @@
+use std::collections::BTreeSet;
use std::fs;
use std::path::{Path, PathBuf};
@@ -9,7 +10,7 @@ use tracing_subscriber::EnvFilter;
use crate::error::MycError;
-pub const DEFAULT_CONFIG_PATH: &str = "config.toml";
+pub const DEFAULT_ENV_PATH: &str = ".env";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, deny_unknown_fields)]
@@ -202,32 +203,21 @@ impl MycConnectionApproval {
}
impl MycConfig {
- pub fn load_from_default_path_if_exists() -> Result<Self, MycError> {
- Self::load_from_path_if_exists(DEFAULT_CONFIG_PATH)
+ pub fn load_from_default_env_path() -> Result<Self, MycError> {
+ Self::load_from_env_path(DEFAULT_ENV_PATH)
}
- pub fn load_from_path_if_exists(path: impl AsRef<Path>) -> Result<Self, MycError> {
- let path = path.as_ref();
- if !path.exists() {
- let config = Self::default();
- config.validate()?;
- return Ok(config);
- }
-
- Self::load_from_path(path)
- }
-
- pub fn load_from_path(path: impl AsRef<Path>) -> Result<Self, MycError> {
+ pub fn load_from_env_path(path: impl AsRef<Path>) -> Result<Self, MycError> {
let path = path.as_ref();
let value = fs::read_to_string(path).map_err(|source| MycError::ConfigIo {
path: path.to_path_buf(),
source,
})?;
- Self::from_toml_str_with_source(&value, path)
+ Self::from_env_str_with_source(&value, path)
}
- pub fn from_toml_str(value: &str) -> Result<Self, MycError> {
- Self::from_toml_str_with_source(value, Path::new("<inline>"))
+ pub fn from_env_str(value: &str) -> Result<Self, MycError> {
+ Self::from_env_str_with_source(value, Path::new("<inline>"))
}
pub fn validate(&self) -> Result<(), MycError> {
@@ -298,16 +288,261 @@ impl MycConfig {
Ok(())
}
- fn from_toml_str_with_source(value: &str, path: &Path) -> Result<Self, MycError> {
- let config: Self = toml::from_str(value).map_err(|source| MycError::ConfigParse {
- path: path.to_path_buf(),
- source,
- })?;
+ fn from_env_str_with_source(value: &str, path: &Path) -> Result<Self, MycError> {
+ let entries = parse_env_entries(value, path)?;
+ let mut config = Self::default();
+ for (key, value, line_number) in entries {
+ apply_env_entry(&mut config, key.as_str(), value.as_str(), path, line_number)?;
+ }
config.validate()?;
Ok(config)
}
}
+fn parse_env_entries(value: &str, path: &Path) -> Result<Vec<(String, String, usize)>, MycError> {
+ let mut seen = BTreeSet::new();
+ let mut entries = Vec::new();
+
+ for (index, raw_line) in value.lines().enumerate() {
+ let line_number = index + 1;
+ let line = raw_line.trim();
+ if line.is_empty() || line.starts_with('#') {
+ continue;
+ }
+
+ let Some((key_raw, value_raw)) = raw_line.split_once('=') else {
+ return Err(config_parse_error(
+ path,
+ line_number,
+ "expected KEY=VALUE assignment",
+ ));
+ };
+ let key = key_raw.trim();
+ if key.is_empty() {
+ return Err(config_parse_error(
+ path,
+ line_number,
+ "environment variable name must not be empty",
+ ));
+ }
+ if !key.chars().all(|character| {
+ character.is_ascii_uppercase() || character.is_ascii_digit() || character == '_'
+ }) {
+ return Err(config_parse_error(
+ path,
+ line_number,
+ format!("invalid environment variable name `{key}`"),
+ ));
+ }
+ if !seen.insert(key.to_owned()) {
+ return Err(config_parse_error(
+ path,
+ line_number,
+ format!("duplicate environment variable `{key}`"),
+ ));
+ }
+ entries.push((
+ key.to_owned(),
+ parse_env_value(value_raw.trim(), path, line_number)?,
+ line_number,
+ ));
+ }
+
+ Ok(entries)
+}
+
+fn parse_env_value(value: &str, path: &Path, line_number: usize) -> Result<String, MycError> {
+ if value.starts_with('"') || value.starts_with('\'') {
+ let quote = value.chars().next().expect("quoted env value prefix");
+ if !value.ends_with(quote) || value.len() < 2 {
+ return Err(config_parse_error(
+ path,
+ line_number,
+ "unterminated quoted environment value",
+ ));
+ }
+ return Ok(value[1..value.len() - 1].to_owned());
+ }
+ Ok(value.to_owned())
+}
+
+fn apply_env_entry(
+ config: &mut MycConfig,
+ key: &str,
+ value: &str,
+ path: &Path,
+ line_number: usize,
+) -> Result<(), MycError> {
+ match key {
+ "MYC_SERVICE_INSTANCE_NAME" => config.service.instance_name = value.to_owned(),
+ "MYC_LOGGING_FILTER" => config.logging.filter = value.to_owned(),
+ "MYC_PATHS_STATE_DIR" => config.paths.state_dir = PathBuf::from(value),
+ "MYC_PATHS_SIGNER_IDENTITY_PATH" => {
+ config.paths.signer_identity_path = PathBuf::from(value);
+ }
+ "MYC_PATHS_USER_IDENTITY_PATH" => {
+ config.paths.user_identity_path = PathBuf::from(value);
+ }
+ "MYC_AUDIT_DEFAULT_READ_LIMIT" => {
+ config.audit.default_read_limit = parse_usize_env(key, value, path, line_number)?;
+ }
+ "MYC_AUDIT_MAX_ACTIVE_FILE_BYTES" => {
+ config.audit.max_active_file_bytes = parse_u64_env(key, value, path, line_number)?;
+ }
+ "MYC_AUDIT_MAX_ARCHIVED_FILES" => {
+ config.audit.max_archived_files = parse_usize_env(key, value, path, line_number)?;
+ }
+ "MYC_DISCOVERY_ENABLED" => {
+ config.discovery.enabled = parse_bool_env(key, value, path, line_number)?;
+ }
+ "MYC_DISCOVERY_DOMAIN" => {
+ config.discovery.domain = parse_optional_string_env(value);
+ }
+ "MYC_DISCOVERY_HANDLER_IDENTIFIER" => {
+ config.discovery.handler_identifier = value.to_owned();
+ }
+ "MYC_DISCOVERY_APP_IDENTITY_PATH" => {
+ config.discovery.app_identity_path = parse_optional_path_env(value);
+ }
+ "MYC_DISCOVERY_PUBLIC_RELAYS" => {
+ config.discovery.public_relays = parse_string_list_env(value);
+ }
+ "MYC_DISCOVERY_PUBLISH_RELAYS" => {
+ config.discovery.publish_relays = parse_string_list_env(value);
+ }
+ "MYC_DISCOVERY_NOSTRCONNECT_URL_TEMPLATE" => {
+ config.discovery.nostrconnect_url_template = parse_optional_string_env(value);
+ }
+ "MYC_DISCOVERY_NIP05_OUTPUT_PATH" => {
+ config.discovery.nip05_output_path = parse_optional_path_env(value);
+ }
+ "MYC_DISCOVERY_METADATA_NAME" => {
+ config.discovery.metadata.name = parse_optional_string_env(value);
+ }
+ "MYC_DISCOVERY_METADATA_DISPLAY_NAME" => {
+ config.discovery.metadata.display_name = parse_optional_string_env(value);
+ }
+ "MYC_DISCOVERY_METADATA_ABOUT" => {
+ config.discovery.metadata.about = parse_optional_string_env(value);
+ }
+ "MYC_DISCOVERY_METADATA_WEBSITE" => {
+ config.discovery.metadata.website = parse_optional_string_env(value);
+ }
+ "MYC_DISCOVERY_METADATA_PICTURE" => {
+ config.discovery.metadata.picture = parse_optional_string_env(value);
+ }
+ "MYC_POLICY_CONNECTION_APPROVAL" => {
+ config.policy.connection_approval =
+ parse_connection_approval_env(key, value, path, line_number)?;
+ }
+ "MYC_TRANSPORT_ENABLED" => {
+ config.transport.enabled = parse_bool_env(key, value, path, line_number)?;
+ }
+ "MYC_TRANSPORT_CONNECT_TIMEOUT_SECS" => {
+ config.transport.connect_timeout_secs = parse_u64_env(key, value, path, line_number)?;
+ }
+ "MYC_TRANSPORT_RELAYS" => {
+ config.transport.relays = parse_string_list_env(value);
+ }
+ _ => {
+ return Err(config_parse_error(
+ path,
+ line_number,
+ format!("unknown environment variable `{key}`"),
+ ));
+ }
+ }
+
+ Ok(())
+}
+
+fn parse_bool_env(
+ key: &str,
+ value: &str,
+ path: &Path,
+ line_number: usize,
+) -> Result<bool, MycError> {
+ value.parse::<bool>().map_err(|_| {
+ config_parse_error(
+ path,
+ line_number,
+ format!("{key} must be `true` or `false`"),
+ )
+ })
+}
+
+fn parse_usize_env(
+ key: &str,
+ value: &str,
+ path: &Path,
+ line_number: usize,
+) -> Result<usize, MycError> {
+ value.parse::<usize>().map_err(|_| {
+ config_parse_error(
+ path,
+ line_number,
+ format!("{key} must be an unsigned integer"),
+ )
+ })
+}
+
+fn parse_u64_env(key: &str, value: &str, path: &Path, line_number: usize) -> Result<u64, MycError> {
+ value.parse::<u64>().map_err(|_| {
+ config_parse_error(
+ path,
+ line_number,
+ format!("{key} must be an unsigned integer"),
+ )
+ })
+}
+
+fn parse_connection_approval_env(
+ key: &str,
+ value: &str,
+ path: &Path,
+ line_number: usize,
+) -> Result<MycConnectionApproval, MycError> {
+ match value {
+ "not_required" => Ok(MycConnectionApproval::NotRequired),
+ "explicit_user" => Ok(MycConnectionApproval::ExplicitUser),
+ _ => Err(config_parse_error(
+ path,
+ line_number,
+ format!("{key} must be `not_required` or `explicit_user`"),
+ )),
+ }
+}
+
+fn parse_optional_string_env(value: &str) -> Option<String> {
+ let value = value.trim();
+ if value.is_empty() {
+ None
+ } else {
+ Some(value.to_owned())
+ }
+}
+
+fn parse_optional_path_env(value: &str) -> Option<PathBuf> {
+ parse_optional_string_env(value).map(PathBuf::from)
+}
+
+fn parse_string_list_env(value: &str) -> Vec<String> {
+ value
+ .split(',')
+ .map(str::trim)
+ .filter(|entry| !entry.is_empty())
+ .map(ToOwned::to_owned)
+ .collect()
+}
+
+fn config_parse_error(path: &Path, line_number: usize, message: impl Into<String>) -> MycError {
+ MycError::ConfigParse {
+ path: path.to_path_buf(),
+ line_number,
+ message: message.into(),
+ }
+}
+
impl MycTransportConfig {
pub fn parse_relays(&self) -> Result<Vec<RadrootsNostrRelayUrl>, MycError> {
self.relays
@@ -513,49 +748,34 @@ mod tests {
}
#[test]
- fn parse_config_from_toml_overrides_defaults() {
- let config = MycConfig::from_toml_str(
+ fn parse_config_from_env_overrides_defaults() {
+ let config = MycConfig::from_env_str(
r#"
- [service]
- instance_name = "myc-dev"
-
- [logging]
- filter = "debug,myc=trace"
-
- [paths]
- state_dir = "/tmp/myc"
- signer_identity_path = "/tmp/myc-identity.json"
- user_identity_path = "/tmp/myc-user.json"
-
- [audit]
- default_read_limit = 50
- max_active_file_bytes = 4096
- max_archived_files = 3
-
- [discovery]
- enabled = true
- domain = "myc.example.com"
- handler_identifier = "myc-main"
- app_identity_path = "/tmp/myc-app.json"
- public_relays = ["wss://relay.discovery.example.com"]
- publish_relays = ["wss://relay.publish.example.com"]
- nostrconnect_url_template = "https://myc.example.com/connect/<nostrconnect>"
- nip05_output_path = "/tmp/nostr.json"
-
- [discovery.metadata]
- name = "myc"
- display_name = "Mycorrhiza"
- about = "NIP-46 signer"
- website = "https://myc.example.com"
- picture = "https://myc.example.com/logo.png"
-
- [policy]
- connection_approval = "not_required"
-
- [transport]
- enabled = true
- connect_timeout_secs = 15
- relays = ["wss://relay.example.com", "wss://relay2.example.com"]
+MYC_SERVICE_INSTANCE_NAME=myc-dev
+MYC_LOGGING_FILTER=debug,myc=trace
+MYC_PATHS_STATE_DIR=/tmp/myc
+MYC_PATHS_SIGNER_IDENTITY_PATH=/tmp/myc-identity.json
+MYC_PATHS_USER_IDENTITY_PATH=/tmp/myc-user.json
+MYC_AUDIT_DEFAULT_READ_LIMIT=50
+MYC_AUDIT_MAX_ACTIVE_FILE_BYTES=4096
+MYC_AUDIT_MAX_ARCHIVED_FILES=3
+MYC_DISCOVERY_ENABLED=true
+MYC_DISCOVERY_DOMAIN=myc.example.com
+MYC_DISCOVERY_HANDLER_IDENTIFIER=myc-main
+MYC_DISCOVERY_APP_IDENTITY_PATH=/tmp/myc-app.json
+MYC_DISCOVERY_PUBLIC_RELAYS=wss://relay.discovery.example.com
+MYC_DISCOVERY_PUBLISH_RELAYS=wss://relay.publish.example.com
+MYC_DISCOVERY_NOSTRCONNECT_URL_TEMPLATE=https://myc.example.com/connect/<nostrconnect>
+MYC_DISCOVERY_NIP05_OUTPUT_PATH=/tmp/nostr.json
+MYC_DISCOVERY_METADATA_NAME=myc
+MYC_DISCOVERY_METADATA_DISPLAY_NAME=Mycorrhiza
+MYC_DISCOVERY_METADATA_ABOUT=NIP-46 signer
+MYC_DISCOVERY_METADATA_WEBSITE=https://myc.example.com
+MYC_DISCOVERY_METADATA_PICTURE=https://myc.example.com/logo.png
+MYC_POLICY_CONNECTION_APPROVAL=not_required
+MYC_TRANSPORT_ENABLED=true
+MYC_TRANSPORT_CONNECT_TIMEOUT_SECS=15
+MYC_TRANSPORT_RELAYS=wss://relay.example.com,wss://relay2.example.com
"#,
)
.expect("config");
@@ -618,24 +838,23 @@ mod tests {
}
#[test]
- fn load_from_missing_path_returns_default_config() {
+ fn load_from_missing_env_path_fails() {
let temp = tempfile::tempdir().expect("tempdir");
- let config = MycConfig::load_from_path_if_exists(temp.path().join("missing.toml"))
- .expect("missing path fallback");
+ let err = MycConfig::load_from_env_path(temp.path().join("missing.env"))
+ .expect_err("missing env");
- assert_eq!(config, MycConfig::default());
+ assert!(err.to_string().contains("config io error"));
}
#[test]
- fn parse_rejects_unknown_fields() {
- let err = MycConfig::from_toml_str(
+ fn parse_rejects_unknown_env_keys() {
+ let err = MycConfig::from_env_str(
r#"
- [service]
- instance_name = "myc-dev"
- extra = "nope"
+MYC_SERVICE_INSTANCE_NAME=myc-dev
+MYC_UNKNOWN=nope
"#,
)
- .expect_err("unknown field");
+ .expect_err("unknown key");
assert!(err.to_string().contains("config parse error"));
}
@@ -690,13 +909,12 @@ mod tests {
}
#[test]
- fn example_config_parses_and_validates() {
- let example = fs::read_to_string(
- PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("config.example.toml"),
- )
- .expect("read example config");
+ fn example_env_parses_and_validates() {
+ let example =
+ fs::read_to_string(PathBuf::from(env!("CARGO_MANIFEST_DIR")).join(".env.example"))
+ .expect("read example config");
- let config = MycConfig::from_toml_str(&example).expect("example config");
+ let config = MycConfig::from_env_str(&example).expect("example config");
assert_eq!(config.service.instance_name, "myc");
assert!(config.discovery.enabled);
diff --git a/src/error.rs b/src/error.rs
@@ -14,11 +14,11 @@ pub enum MycError {
#[source]
source: std::io::Error,
},
- #[error("config parse error at {path}: {source}")]
+ #[error("config parse error at {path}:{line_number}: {message}")]
ConfigParse {
path: PathBuf,
- #[source]
- source: toml::de::Error,
+ line_number: usize,
+ message: String,
},
#[error("invalid config: {0}")]
InvalidConfig(String),
diff --git a/src/lib.rs b/src/lib.rs
@@ -16,7 +16,7 @@ pub use audit::{
MycOperationAuditStore,
};
pub use config::{
- DEFAULT_CONFIG_PATH, MycAuditConfig, MycConfig, MycConnectionApproval, MycDiscoveryConfig,
+ DEFAULT_ENV_PATH, MycAuditConfig, MycConfig, MycConnectionApproval, MycDiscoveryConfig,
MycDiscoveryMetadataConfig, MycLoggingConfig, MycPathsConfig, MycPolicyConfig,
MycServiceConfig, MycTransportConfig,
};
@@ -36,7 +36,7 @@ pub use error::MycError;
pub use transport::{MycNostrTransport, MycRelayPublishResult, MycTransportSnapshot};
pub async fn run() -> Result<(), MycError> {
- let config = MycConfig::load_from_default_path_if_exists()?;
+ let config = MycConfig::load_from_default_env_path()?;
logging::init_logging(&config.logging)?;
MycApp::bootstrap(config)?.run().await
}
diff --git a/tests/discovery_cli.rs b/tests/discovery_cli.rs
@@ -308,7 +308,7 @@ fn write_identity(path: &Path, secret_key: &str) {
.expect("save identity");
}
-fn write_config(
+fn write_env_file(
path: &Path,
state_dir: &Path,
signer_identity_path: &Path,
@@ -316,52 +316,33 @@ fn write_config(
app_identity_path: &Path,
relay_urls: &[&str],
) {
- let relay_list = relay_urls
- .iter()
- .map(|relay| format!("\"{relay}\""))
- .collect::<Vec<_>>()
- .join(", ");
- let config = format!(
- r#"[service]
-instance_name = "myc"
-
-[logging]
-filter = "info,myc=info"
-
-[paths]
-state_dir = "{state_dir}"
-signer_identity_path = "{signer_identity_path}"
-user_identity_path = "{user_identity_path}"
-
-[audit]
-default_read_limit = 200
-max_active_file_bytes = 262144
-max_archived_files = 8
-
-[discovery]
-enabled = true
-domain = "signer.example.com"
-handler_identifier = "myc"
-app_identity_path = "{app_identity_path}"
-public_relays = [{relay_list}]
-publish_relays = [{relay_list}]
-nostrconnect_url_template = "https://signer.example.com/connect?uri=<nostrconnect>"
-nip05_output_path = "{nip05_output_path}"
-
-[discovery.metadata]
-name = "myc"
-display_name = "Mycorrhiza"
-about = "NIP-46 signer"
-website = "https://signer.example.com"
-picture = "https://signer.example.com/logo.png"
-
-[policy]
-connection_approval = "explicit_user"
-
-[transport]
-enabled = false
-connect_timeout_secs = 10
-relays = []
+ let relay_list = relay_urls.join(",");
+ let env_file = format!(
+ r#"MYC_SERVICE_INSTANCE_NAME=myc
+MYC_LOGGING_FILTER=info,myc=info
+MYC_PATHS_STATE_DIR={state_dir}
+MYC_PATHS_SIGNER_IDENTITY_PATH={signer_identity_path}
+MYC_PATHS_USER_IDENTITY_PATH={user_identity_path}
+MYC_AUDIT_DEFAULT_READ_LIMIT=200
+MYC_AUDIT_MAX_ACTIVE_FILE_BYTES=262144
+MYC_AUDIT_MAX_ARCHIVED_FILES=8
+MYC_DISCOVERY_ENABLED=true
+MYC_DISCOVERY_DOMAIN=signer.example.com
+MYC_DISCOVERY_HANDLER_IDENTIFIER=myc
+MYC_DISCOVERY_APP_IDENTITY_PATH={app_identity_path}
+MYC_DISCOVERY_PUBLIC_RELAYS={relay_list}
+MYC_DISCOVERY_PUBLISH_RELAYS={relay_list}
+MYC_DISCOVERY_NOSTRCONNECT_URL_TEMPLATE=https://signer.example.com/connect?uri=<nostrconnect>
+MYC_DISCOVERY_NIP05_OUTPUT_PATH={nip05_output_path}
+MYC_DISCOVERY_METADATA_NAME=myc
+MYC_DISCOVERY_METADATA_DISPLAY_NAME=Mycorrhiza
+MYC_DISCOVERY_METADATA_ABOUT=NIP-46 signer
+MYC_DISCOVERY_METADATA_WEBSITE=https://signer.example.com
+MYC_DISCOVERY_METADATA_PICTURE=https://signer.example.com/logo.png
+MYC_POLICY_CONNECTION_APPROVAL=explicit_user
+MYC_TRANSPORT_ENABLED=false
+MYC_TRANSPORT_CONNECT_TIMEOUT_SECS=10
+MYC_TRANSPORT_RELAYS=
"#,
state_dir = state_dir.display(),
signer_identity_path = signer_identity_path.display(),
@@ -370,13 +351,13 @@ relays = []
relay_list = relay_list,
nip05_output_path = state_dir.join("public/.well-known/nostr.json").display(),
);
- fs::write(path, config).expect("write config");
+ fs::write(path, env_file).expect("write env file");
}
-fn run_myc(config_path: &Path, args: &[&str]) -> TestResult<Output> {
+fn run_myc(env_path: &Path, args: &[&str]) -> TestResult<Output> {
Ok(Command::new(env!("CARGO_BIN_EXE_myc"))
- .arg("--config")
- .arg(config_path)
+ .arg("--env-file")
+ .arg(env_path)
.args(args)
.output()?)
}
@@ -425,7 +406,7 @@ async fn publish_handler_event(
#[test]
fn export_bundle_and_verify_bundle_work_through_the_cli() -> TestResult<()> {
let temp = tempfile::tempdir()?;
- let config_path = temp.path().join("config.toml");
+ let env_path = temp.path().join(".env");
let state_dir = temp.path().join("state");
let signer_identity_path = temp.path().join("signer.json");
let user_identity_path = temp.path().join("user.json");
@@ -444,8 +425,8 @@ fn export_bundle_and_verify_bundle_work_through_the_cli() -> TestResult<()> {
&app_identity_path,
"3333333333333333333333333333333333333333333333333333333333333333",
);
- write_config(
- &config_path,
+ write_env_file(
+ &env_path,
&state_dir,
&signer_identity_path,
&user_identity_path,
@@ -454,7 +435,7 @@ fn export_bundle_and_verify_bundle_work_through_the_cli() -> TestResult<()> {
);
let export = run_myc(
- &config_path,
+ &env_path,
&[
"discovery",
"export-bundle",
@@ -475,7 +456,7 @@ fn export_bundle_and_verify_bundle_work_through_the_cli() -> TestResult<()> {
assert!(bundle_dir.join("nip89-handler.json").exists());
let verify = run_myc(
- &config_path,
+ &env_path,
&[
"discovery",
"verify-bundle",
@@ -507,7 +488,7 @@ fn export_bundle_and_verify_bundle_work_through_the_cli() -> TestResult<()> {
async fn discovery_sync_commands_work_through_the_cli() -> TestResult<()> {
let relay = TestRelay::spawn().await?;
let temp = tempfile::tempdir()?;
- let config_path = temp.path().join("config.toml");
+ let env_path = temp.path().join(".env");
let state_dir = temp.path().join("state");
let signer_identity_path = temp.path().join("signer.json");
let user_identity_path = temp.path().join("user.json");
@@ -525,8 +506,8 @@ async fn discovery_sync_commands_work_through_the_cli() -> TestResult<()> {
"2222222222222222222222222222222222222222222222222222222222222222",
);
app_identity.save_json(&app_identity_path)?;
- write_config(
- &config_path,
+ write_env_file(
+ &env_path,
&state_dir,
&signer_identity_path,
&user_identity_path,
@@ -534,7 +515,7 @@ async fn discovery_sync_commands_work_through_the_cli() -> TestResult<()> {
&[relay.url()],
);
- let inspect_missing = run_myc(&config_path, &["discovery", "inspect-live-nip89"])?;
+ let inspect_missing = run_myc(&env_path, &["discovery", "inspect-live-nip89"])?;
assert!(
inspect_missing.status.success(),
"inspect-live-nip89 failed: {}",
@@ -556,7 +537,7 @@ async fn discovery_sync_commands_work_through_the_cli() -> TestResult<()> {
1
);
- let refresh = run_myc(&config_path, &["discovery", "refresh-nip89"])?;
+ let refresh = run_myc(&env_path, &["discovery", "refresh-nip89"])?;
assert!(
refresh.status.success(),
"refresh-nip89 failed: {}",
@@ -570,7 +551,7 @@ async fn discovery_sync_commands_work_through_the_cli() -> TestResult<()> {
.wait_for_published_events_by_author(app_identity.public_key(), 1)
.await?;
- let inspect_live = run_myc(&config_path, &["discovery", "inspect-live-nip89"])?;
+ let inspect_live = run_myc(&env_path, &["discovery", "inspect-live-nip89"])?;
assert!(
inspect_live.status.success(),
"inspect-live-nip89 after refresh failed: {}",
@@ -589,7 +570,7 @@ async fn discovery_sync_commands_work_through_the_cli() -> TestResult<()> {
1
);
- let diff = run_myc(&config_path, &["discovery", "diff-live-nip89"])?;
+ let diff = run_myc(&env_path, &["discovery", "diff-live-nip89"])?;
assert!(
diff.status.success(),
"diff-live-nip89 failed: {}",
@@ -621,7 +602,7 @@ async fn discovery_sync_commands_work_through_the_cli() -> TestResult<()> {
async fn conflicted_refresh_requires_force_through_the_cli() -> TestResult<()> {
let relay = TestRelay::spawn().await?;
let temp = tempfile::tempdir()?;
- let config_path = temp.path().join("config.toml");
+ let env_path = temp.path().join(".env");
let state_dir = temp.path().join("state");
let signer_identity_path = temp.path().join("signer.json");
let user_identity_path = temp.path().join("user.json");
@@ -639,8 +620,8 @@ async fn conflicted_refresh_requires_force_through_the_cli() -> TestResult<()> {
"2222222222222222222222222222222222222222222222222222222222222222",
);
app_identity.save_json(&app_identity_path)?;
- write_config(
- &config_path,
+ write_env_file(
+ &env_path,
&state_dir,
&signer_identity_path,
&user_identity_path,
@@ -665,7 +646,7 @@ async fn conflicted_refresh_requires_force_through_the_cli() -> TestResult<()> {
.wait_for_published_events_by_author(app_identity.public_key(), 2)
.await?;
- let diff = run_myc(&config_path, &["discovery", "diff-live-nip89"])?;
+ let diff = run_myc(&env_path, &["discovery", "diff-live-nip89"])?;
assert!(
diff.status.success(),
"diff-live-nip89 failed: {}",
@@ -688,7 +669,7 @@ async fn conflicted_refresh_requires_force_through_the_cli() -> TestResult<()> {
.is_empty()
);
- let refresh = run_myc(&config_path, &["discovery", "refresh-nip89"])?;
+ let refresh = run_myc(&env_path, &["discovery", "refresh-nip89"])?;
assert!(
!refresh.status.success(),
"refresh-nip89 unexpectedly succeeded: {}",
@@ -716,7 +697,7 @@ async fn conflicted_refresh_requires_force_through_the_cli() -> TestResult<()> {
])
);
let attempt = run_myc(
- &config_path,
+ &env_path,
&[
"audit",
"discovery-repair-attempt",
@@ -755,7 +736,7 @@ async fn conflicted_refresh_requires_force_through_the_cli() -> TestResult<()> {
Value::Array(vec![Value::String(relay.url().to_owned())])
);
- let forced_refresh = run_myc(&config_path, &["discovery", "refresh-nip89", "--force"])?;
+ let forced_refresh = run_myc(&env_path, &["discovery", "refresh-nip89", "--force"])?;
assert!(
forced_refresh.status.success(),
"refresh-nip89 --force failed: {}",
@@ -773,7 +754,7 @@ async fn refresh_reports_partial_repair_and_audit_summary_through_the_cli() -> T
let relay_a = TestRelay::spawn().await?;
let relay_b = TestRelay::spawn().await?;
let temp = tempfile::tempdir()?;
- let config_path = temp.path().join("config.toml");
+ let env_path = temp.path().join(".env");
let state_dir = temp.path().join("state");
let signer_identity_path = temp.path().join("signer.json");
let user_identity_path = temp.path().join("user.json");
@@ -791,8 +772,8 @@ async fn refresh_reports_partial_repair_and_audit_summary_through_the_cli() -> T
"2222222222222222222222222222222222222222222222222222222222222222",
);
app_identity.save_json(&app_identity_path)?;
- write_config(
- &config_path,
+ write_env_file(
+ &env_path,
&state_dir,
&signer_identity_path,
&user_identity_path,
@@ -807,7 +788,7 @@ async fn refresh_reports_partial_repair_and_audit_summary_through_the_cli() -> T
.queue_publish_outcomes(app_identity.public_key(), &[false])
.await;
- let refresh = run_myc(&config_path, &["discovery", "refresh-nip89"])?;
+ let refresh = run_myc(&env_path, &["discovery", "refresh-nip89"])?;
assert!(
refresh.status.success(),
"refresh-nip89 failed: {}",
@@ -839,7 +820,7 @@ async fn refresh_reports_partial_repair_and_audit_summary_through_the_cli() -> T
0
);
- let audit_summary = run_myc(&config_path, &["audit", "summary", "--scope", "operation"])?;
+ let audit_summary = run_myc(&env_path, &["audit", "summary", "--scope", "operation"])?;
assert!(
audit_summary.status.success(),
"audit summary failed: {}",
@@ -874,7 +855,7 @@ async fn refresh_reports_partial_repair_and_audit_summary_through_the_cli() -> T
async fn failed_refresh_publish_surfaces_attempt_id_and_exact_audit_lookup() -> TestResult<()> {
let relay = TestRelay::spawn().await?;
let temp = tempfile::tempdir()?;
- let config_path = temp.path().join("config.toml");
+ let env_path = temp.path().join(".env");
let state_dir = temp.path().join("state");
let signer_identity_path = temp.path().join("signer.json");
let user_identity_path = temp.path().join("user.json");
@@ -892,8 +873,8 @@ async fn failed_refresh_publish_surfaces_attempt_id_and_exact_audit_lookup() ->
"2222222222222222222222222222222222222222222222222222222222222222",
);
app_identity.save_json(&app_identity_path)?;
- write_config(
- &config_path,
+ write_env_file(
+ &env_path,
&state_dir,
&signer_identity_path,
&user_identity_path,
@@ -905,7 +886,7 @@ async fn failed_refresh_publish_surfaces_attempt_id_and_exact_audit_lookup() ->
.queue_publish_outcomes(app_identity.public_key(), &[false])
.await;
- let refresh = run_myc(&config_path, &["discovery", "refresh-nip89"])?;
+ let refresh = run_myc(&env_path, &["discovery", "refresh-nip89"])?;
assert!(
!refresh.status.success(),
"refresh-nip89 unexpectedly succeeded: {}",
@@ -924,7 +905,7 @@ async fn failed_refresh_publish_surfaces_attempt_id_and_exact_audit_lookup() ->
);
let attempt = run_myc(
- &config_path,
+ &env_path,
&[
"audit",
"discovery-repair-attempt",
@@ -973,7 +954,7 @@ async fn discovery_repair_attempt_commands_correlate_multiple_refresh_runs() ->
let relay_a = TestRelay::spawn().await?;
let relay_b = TestRelay::spawn().await?;
let temp = tempfile::tempdir()?;
- let config_path = temp.path().join("config.toml");
+ let env_path = temp.path().join(".env");
let state_dir = temp.path().join("state");
let signer_identity_path = temp.path().join("signer.json");
let user_identity_path = temp.path().join("user.json");
@@ -991,8 +972,8 @@ async fn discovery_repair_attempt_commands_correlate_multiple_refresh_runs() ->
"2222222222222222222222222222222222222222222222222222222222222222",
);
app_identity.save_json(&app_identity_path)?;
- write_config(
- &config_path,
+ write_env_file(
+ &env_path,
&state_dir,
&signer_identity_path,
&user_identity_path,
@@ -1007,7 +988,7 @@ async fn discovery_repair_attempt_commands_correlate_multiple_refresh_runs() ->
.queue_publish_outcomes(app_identity.public_key(), &[false, true])
.await;
- let first_refresh = run_myc(&config_path, &["discovery", "refresh-nip89"])?;
+ let first_refresh = run_myc(&env_path, &["discovery", "refresh-nip89"])?;
assert!(
first_refresh.status.success(),
"first refresh-nip89 failed: {}",
@@ -1029,7 +1010,7 @@ async fn discovery_repair_attempt_commands_correlate_multiple_refresh_runs() ->
.wait_for_published_events_by_author(app_identity.public_key(), 1)
.await?;
- let second_refresh = run_myc(&config_path, &["discovery", "refresh-nip89"])?;
+ let second_refresh = run_myc(&env_path, &["discovery", "refresh-nip89"])?;
assert!(
second_refresh.status.success(),
"second refresh-nip89 failed: {}",
@@ -1049,7 +1030,7 @@ async fn discovery_repair_attempt_commands_correlate_multiple_refresh_runs() ->
Value::Array(vec![])
);
- let latest_attempt = run_myc(&config_path, &["audit", "latest-discovery-repair"])?;
+ let latest_attempt = run_myc(&env_path, &["audit", "latest-discovery-repair"])?;
assert!(
latest_attempt.status.success(),
"latest-discovery-repair failed: {}",
@@ -1077,7 +1058,7 @@ async fn discovery_repair_attempt_commands_correlate_multiple_refresh_runs() ->
);
let first_attempt_summary = run_myc(
- &config_path,
+ &env_path,
&[
"audit",
"discovery-repair-attempt",
@@ -1115,7 +1096,7 @@ async fn discovery_repair_attempt_commands_correlate_multiple_refresh_runs() ->
);
let first_attempt_records = run_myc(
- &config_path,
+ &env_path,
&[
"audit",
"discovery-repair-attempt",
@@ -1153,7 +1134,7 @@ async fn discovery_diff_surfaces_relay_provenance_through_the_cli() -> TestResul
let relay_a = TestRelay::spawn().await?;
let relay_b = TestRelay::spawn().await?;
let temp = tempfile::tempdir()?;
- let config_path = temp.path().join("config.toml");
+ let env_path = temp.path().join(".env");
let state_dir = temp.path().join("state");
let signer_identity_path = temp.path().join("signer.json");
let user_identity_path = temp.path().join("user.json");
@@ -1174,8 +1155,8 @@ async fn discovery_diff_surfaces_relay_provenance_through_the_cli() -> TestResul
"2222222222222222222222222222222222222222222222222222222222222222",
);
app_identity.save_json(&app_identity_path)?;
- write_config(
- &config_path,
+ write_env_file(
+ &env_path,
&state_dir,
&signer_identity_path,
&user_identity_path,
@@ -1224,7 +1205,7 @@ async fn discovery_diff_surfaces_relay_provenance_through_the_cli() -> TestResul
.wait_for_published_events_by_author(app_identity.public_key(), 1)
.await?;
- let inspect = run_myc(&config_path, &["discovery", "inspect-live-nip89"])?;
+ let inspect = run_myc(&env_path, &["discovery", "inspect-live-nip89"])?;
assert!(
inspect.status.success(),
"inspect-live-nip89 failed: {}",
@@ -1257,7 +1238,7 @@ async fn discovery_diff_surfaces_relay_provenance_through_the_cli() -> TestResul
.any(|relays| relays == &vec![relay_b.url().to_owned()])
);
- let diff = run_myc(&config_path, &["discovery", "diff-live-nip89"])?;
+ let diff = run_myc(&env_path, &["discovery", "diff-live-nip89"])?;
assert!(
diff.status.success(),
"diff-live-nip89 failed: {}",
@@ -1295,7 +1276,7 @@ async fn refresh_requires_force_when_a_discovery_relay_is_unavailable_through_th
let relay = TestRelay::spawn().await?;
let unavailable_relay = unavailable_relay_url()?;
let temp = tempfile::tempdir()?;
- let config_path = temp.path().join("config.toml");
+ let env_path = temp.path().join(".env");
let state_dir = temp.path().join("state");
let signer_identity_path = temp.path().join("signer.json");
let user_identity_path = temp.path().join("user.json");
@@ -1313,8 +1294,8 @@ async fn refresh_requires_force_when_a_discovery_relay_is_unavailable_through_th
"2222222222222222222222222222222222222222222222222222222222222222",
);
app_identity.save_json(&app_identity_path)?;
- write_config(
- &config_path,
+ write_env_file(
+ &env_path,
&state_dir,
&signer_identity_path,
&user_identity_path,
@@ -1322,7 +1303,7 @@ async fn refresh_requires_force_when_a_discovery_relay_is_unavailable_through_th
&[relay.url(), unavailable_relay.as_str()],
);
- let inspect = run_myc(&config_path, &["discovery", "inspect-live-nip89"])?;
+ let inspect = run_myc(&env_path, &["discovery", "inspect-live-nip89"])?;
assert!(
inspect.status.success(),
"inspect-live-nip89 failed: {}",
@@ -1344,7 +1325,7 @@ async fn refresh_requires_force_when_a_discovery_relay_is_unavailable_through_th
})
);
- let refresh = run_myc(&config_path, &["discovery", "refresh-nip89"])?;
+ let refresh = run_myc(&env_path, &["discovery", "refresh-nip89"])?;
assert!(
!refresh.status.success(),
"refresh-nip89 unexpectedly succeeded: {}",
@@ -1363,7 +1344,7 @@ async fn refresh_requires_force_when_a_discovery_relay_is_unavailable_through_th
Value::String(attempt_id.to_owned())
);
let attempt = run_myc(
- &config_path,
+ &env_path,
&[
"audit",
"discovery-repair-attempt",
@@ -1402,7 +1383,7 @@ async fn refresh_requires_force_when_a_discovery_relay_is_unavailable_through_th
Value::Array(vec![Value::String(relay.url().to_owned())])
);
- let forced_refresh = run_myc(&config_path, &["discovery", "refresh-nip89", "--force"])?;
+ let forced_refresh = run_myc(&env_path, &["discovery", "refresh-nip89", "--force"])?;
assert!(
forced_refresh.status.success(),
"refresh-nip89 --force failed: {}",
@@ -1424,7 +1405,7 @@ async fn refresh_surfaces_blocked_summary_when_all_discovery_relays_are_unavaila
-> TestResult<()> {
let unavailable_relay = unavailable_relay_url()?;
let temp = tempfile::tempdir()?;
- let config_path = temp.path().join("config.toml");
+ let env_path = temp.path().join(".env");
let state_dir = temp.path().join("state");
let signer_identity_path = temp.path().join("signer.json");
let user_identity_path = temp.path().join("user.json");
@@ -1442,8 +1423,8 @@ async fn refresh_surfaces_blocked_summary_when_all_discovery_relays_are_unavaila
&app_identity_path,
"3333333333333333333333333333333333333333333333333333333333333333",
);
- write_config(
- &config_path,
+ write_env_file(
+ &env_path,
&state_dir,
&signer_identity_path,
&user_identity_path,
@@ -1451,7 +1432,7 @@ async fn refresh_surfaces_blocked_summary_when_all_discovery_relays_are_unavaila
&[unavailable_relay.as_str()],
);
- let refresh = run_myc(&config_path, &["discovery", "refresh-nip89"])?;
+ let refresh = run_myc(&env_path, &["discovery", "refresh-nip89"])?;
assert!(
!refresh.status.success(),
"refresh-nip89 unexpectedly succeeded: {}",
@@ -1470,7 +1451,7 @@ async fn refresh_surfaces_blocked_summary_when_all_discovery_relays_are_unavaila
);
let attempt = run_myc(
- &config_path,
+ &env_path,
&[
"audit",
"discovery-repair-attempt",