agent_first_pay/
config.rs1use crate::types::*;
2use agent_first_data::cli_parse_log_filters;
3use std::path::Path;
4
5pub const VERSION: &str = env!("CARGO_PKG_VERSION");
6
7pub fn enabled_features() -> Vec<&'static str> {
8 let features: &[Option<&str>] = &[
9 #[cfg(feature = "redb")]
10 Some("redb"),
11 #[cfg(feature = "postgres")]
12 Some("postgres"),
13 #[cfg(feature = "cashu")]
14 Some("cashu"),
15 #[cfg(feature = "ln-nwc")]
16 Some("ln-nwc"),
17 #[cfg(feature = "ln-phoenixd")]
18 Some("ln-phoenixd"),
19 #[cfg(feature = "ln-lnbits")]
20 Some("ln-lnbits"),
21 #[cfg(feature = "sol")]
22 Some("sol"),
23 #[cfg(feature = "evm")]
24 Some("evm"),
25 #[cfg(feature = "btc-esplora")]
26 Some("btc-esplora"),
27 #[cfg(feature = "btc-core")]
28 Some("btc-core"),
29 #[cfg(feature = "btc-electrum")]
30 Some("btc-electrum"),
31 #[cfg(feature = "interactive")]
32 Some("interactive"),
33 #[cfg(feature = "rest")]
34 Some("rest"),
35 ];
36 features.iter().copied().flatten().collect()
37}
38
39pub fn build_startup_log(
41 argv: Option<Vec<String>>,
42 config: Option<&RuntimeConfig>,
43 args: serde_json::Value,
44) -> Output {
45 Output::Log {
46 event: "startup".to_string(),
47 request_id: None,
48 version: Some(VERSION.to_string()),
49 argv: argv.map(sanitize_startup_argv),
50 config: config.map(|c| serde_json::to_value(c).unwrap_or(serde_json::Value::Null)),
51 args: Some(args),
52 env: Some(serde_json::json!({
53 "features": enabled_features(),
54 })),
55 trace: Trace::from_duration(0),
56 }
57}
58
59fn sanitize_startup_argv(argv: Vec<String>) -> Vec<String> {
60 const SECRET_FLAGS: &[&str] = &[
61 "--admin-key-secret",
62 "--btc-core-auth-secret",
63 "--mnemonic-secret",
64 "--nwc-uri-secret",
65 "--password-secret",
66 "--pg-url-secret",
67 "--rest-api-key",
68 "--rpc-secret",
69 ];
70
71 let mut redact_next = false;
72 argv.into_iter()
73 .map(|arg| {
74 if redact_next {
75 redact_next = false;
76 return "***".to_string();
77 }
78 if SECRET_FLAGS.iter().any(|flag| arg == *flag) {
79 redact_next = true;
80 return arg;
81 }
82 for flag in SECRET_FLAGS {
83 let prefix = format!("{flag}=");
84 if arg.starts_with(&prefix) {
85 return format!("{flag}=***");
86 }
87 }
88 arg
89 })
90 .collect()
91}
92
93pub fn should_emit_startup_log(log_filters: &[String], startup_requested: bool) -> bool {
96 startup_requested || !log_filters.is_empty()
97}
98
99pub fn maybe_startup_log(
101 log_filters: &[String],
102 startup_requested: bool,
103 argv: Option<Vec<String>>,
104 config: Option<&RuntimeConfig>,
105 args: serde_json::Value,
106) -> Option<Output> {
107 if !should_emit_startup_log(log_filters, startup_requested) {
108 return None;
109 }
110 Some(build_startup_log(argv, config, args))
111}
112
113impl RuntimeConfig {
114 pub fn load_from_dir(data_dir: &str) -> Result<Self, String> {
116 let path = Path::new(data_dir).join("config.toml");
117 if !path.exists() {
118 return Ok(Self {
119 data_dir: data_dir.to_string(),
120 ..Self::default()
121 });
122 }
123 let contents =
124 std::fs::read_to_string(&path).map_err(|e| format!("read {}: {e}", path.display()))?;
125 let mut cfg: Self =
126 toml::from_str(&contents).map_err(|e| format!("parse {}: {e}", path.display()))?;
127 cfg.data_dir = data_dir.to_string();
129 Ok(cfg)
130 }
131
132 #[allow(dead_code)]
133 pub fn apply_update(&mut self, patch: ConfigPatch) {
134 if let Some(v) = patch.data_dir {
135 self.data_dir = v;
136 }
137 if let Some(v) = patch.log {
138 self.log = cli_parse_log_filters(&v);
139 }
140 if let Some(rpc_nodes) = patch.afpay_rpc {
141 for (name, cfg) in rpc_nodes {
142 self.afpay_rpc.insert(name, cfg);
143 }
144 }
145 if let Some(providers) = patch.providers {
146 for (network, rpc_name) in providers {
147 self.providers.insert(network, rpc_name);
148 }
149 }
150 }
151}
152
153#[cfg(test)]
154mod tests {
155 use super::*;
156
157 #[test]
158 fn maybe_startup_log_disabled_without_filters_or_request() {
159 let out = maybe_startup_log(&[], false, None, None, serde_json::json!({"mode": "test"}));
160 assert!(out.is_none());
161 }
162
163 #[test]
164 fn maybe_startup_log_enabled_with_filters() {
165 let filters = vec!["cashu".to_string()];
166 let out = maybe_startup_log(
167 &filters,
168 false,
169 None,
170 None,
171 serde_json::json!({"mode": "test"}),
172 );
173 assert!(out.is_some());
174 }
175
176 #[test]
177 fn maybe_startup_log_enabled_with_explicit_request() {
178 let out = maybe_startup_log(&[], true, None, None, serde_json::json!({"mode": "test"}));
179 assert!(out.is_some());
180 }
181
182 #[test]
183 fn startup_log_redacts_secret_argv_values() -> Result<(), Box<dyn std::error::Error>> {
184 let out = build_startup_log(
185 Some(vec![
186 "afpay".to_string(),
187 "--rpc-secret".to_string(),
188 "rpc-secret-value".to_string(),
189 "--rest-api-key=rest-secret-value".to_string(),
190 "--data-dir".to_string(),
191 "/tmp/afpay".to_string(),
192 ]),
193 None,
194 serde_json::json!({"mode": "test"}),
195 );
196 let value = serde_json::to_value(out)?;
197 let rendered = value.to_string();
198 assert!(!rendered.contains("rpc-secret-value"));
199 assert!(!rendered.contains("rest-secret-value"));
200 assert!(rendered.contains("***"));
201 assert!(rendered.contains("/tmp/afpay"));
202 Ok(())
203 }
204}