config.rs (22330B)
1 use anyhow::{Context, Result, bail}; 2 use radroots_nostr::prelude::RadrootsNostrMetadata; 3 use radroots_runtime::RadrootsNostrServiceConfig; 4 use serde::{Deserialize, Serialize}; 5 use std::path::{Path, PathBuf}; 6 7 use super::paths::{ 8 RadrootsdRuntimePaths, default_publish_proxy_database_path, process_path_selection, 9 resolve_runtime_paths_with_resolver, 10 }; 11 12 fn default_rpc_addr() -> String { 13 "127.0.0.1:7070".to_string() 14 } 15 16 fn default_max_request_body_size() -> u32 { 17 10 * 1024 * 1024 18 } 19 20 fn default_max_response_body_size() -> u32 { 21 10 * 1024 * 1024 22 } 23 24 fn default_max_connections() -> u32 { 25 100 26 } 27 28 fn default_max_subscriptions_per_connection() -> u32 { 29 1024 30 } 31 32 fn default_message_buffer_capacity() -> u32 { 33 1024 34 } 35 36 fn default_rpc_batch_request_limit() -> Option<u32> { 37 Some(0) 38 } 39 40 fn default_nip46_session_ttl_secs() -> u64 { 41 900 42 } 43 44 fn default_nip46_perms() -> Vec<String> { 45 Vec::new() 46 } 47 48 fn default_nip46_public_jsonrpc_enabled() -> bool { 49 false 50 } 51 52 fn default_publish_proxy_enabled() -> bool { 53 true 54 } 55 56 fn default_publish_proxy_connect_timeout_secs() -> u64 { 57 10 58 } 59 60 fn default_publish_proxy_max_event_bytes() -> usize { 61 128 * 1024 62 } 63 64 fn default_publish_proxy_max_relays_per_request() -> usize { 65 20 66 } 67 68 fn default_publish_proxy_job_list_limit() -> usize { 69 100 70 } 71 72 fn default_publish_proxy_max_concurrent_publish_jobs() -> usize { 73 8 74 } 75 76 fn default_publish_proxy_relay_url_policy() -> PublishProxyRelayUrlPolicy { 77 PublishProxyRelayUrlPolicy::Public 78 } 79 80 #[derive(Debug, Deserialize, Clone, Default)] 81 struct RawServiceConfig { 82 #[serde(default)] 83 pub logs_dir: Option<String>, 84 #[serde(default)] 85 pub relays: Vec<String>, 86 #[serde(default)] 87 pub nip89_identifier: Option<String>, 88 #[serde(default)] 89 pub nip89_extra_tags: Vec<Vec<String>>, 90 } 91 92 impl RawServiceConfig { 93 fn into_service_config(self, paths: &RadrootsdRuntimePaths) -> RadrootsNostrServiceConfig { 94 RadrootsNostrServiceConfig { 95 logs_dir: self 96 .logs_dir 97 .unwrap_or_else(|| paths.logs_dir.display().to_string()), 98 relays: self.relays, 99 nip89_identifier: self.nip89_identifier, 100 nip89_extra_tags: self.nip89_extra_tags, 101 } 102 } 103 } 104 105 #[derive(Debug, Deserialize, Clone)] 106 struct RawPublishProxyConfig { 107 #[serde(default = "default_publish_proxy_enabled")] 108 pub enabled: bool, 109 #[serde(default = "default_publish_proxy_connect_timeout_secs")] 110 pub connect_timeout_secs: u64, 111 #[serde(default = "default_publish_proxy_max_event_bytes")] 112 pub max_event_bytes: usize, 113 #[serde(default = "default_publish_proxy_max_relays_per_request")] 114 pub max_relays_per_request: usize, 115 #[serde(default = "default_publish_proxy_job_list_limit")] 116 pub job_list_limit: usize, 117 #[serde(default = "default_publish_proxy_max_concurrent_publish_jobs")] 118 pub max_concurrent_publish_jobs: usize, 119 #[serde(default)] 120 pub database_path: Option<PathBuf>, 121 #[serde(default = "default_publish_proxy_relay_url_policy")] 122 pub relay_url_policy: PublishProxyRelayUrlPolicy, 123 #[serde(default)] 124 pub author_relay_discovery_relays: Vec<String>, 125 #[serde(default)] 126 pub daemon_default_publish_relays: Vec<String>, 127 } 128 129 impl Default for RawPublishProxyConfig { 130 fn default() -> Self { 131 Self { 132 enabled: default_publish_proxy_enabled(), 133 connect_timeout_secs: default_publish_proxy_connect_timeout_secs(), 134 max_event_bytes: default_publish_proxy_max_event_bytes(), 135 max_relays_per_request: default_publish_proxy_max_relays_per_request(), 136 job_list_limit: default_publish_proxy_job_list_limit(), 137 max_concurrent_publish_jobs: default_publish_proxy_max_concurrent_publish_jobs(), 138 database_path: None, 139 relay_url_policy: default_publish_proxy_relay_url_policy(), 140 author_relay_discovery_relays: Vec::new(), 141 daemon_default_publish_relays: Vec::new(), 142 } 143 } 144 } 145 146 impl RawPublishProxyConfig { 147 fn into_publish_proxy_config(self, paths: &RadrootsdRuntimePaths) -> PublishProxyConfig { 148 PublishProxyConfig { 149 enabled: self.enabled, 150 connect_timeout_secs: self.connect_timeout_secs, 151 max_event_bytes: self.max_event_bytes, 152 max_relays_per_request: self.max_relays_per_request, 153 job_list_limit: self.job_list_limit, 154 max_concurrent_publish_jobs: self.max_concurrent_publish_jobs, 155 database_path: self 156 .database_path 157 .unwrap_or_else(|| paths.publish_proxy_database_path.clone()), 158 relay_url_policy: self.relay_url_policy, 159 author_relay_discovery_relays: self.author_relay_discovery_relays, 160 daemon_default_publish_relays: self.daemon_default_publish_relays, 161 } 162 } 163 } 164 165 #[derive(Debug, Deserialize, Clone)] 166 struct RawConfiguration { 167 #[serde(flatten)] 168 pub service: RawServiceConfig, 169 #[serde(default)] 170 pub rpc: RpcConfig, 171 #[serde(default)] 172 pub rpc_addr: Option<String>, 173 #[serde(default)] 174 pub nip46: Nip46Config, 175 #[serde(default)] 176 pub publish_proxy: RawPublishProxyConfig, 177 #[serde(default, rename = "bridge")] 178 pub obsolete_publish_bridge_config: Option<serde::de::IgnoredAny>, 179 } 180 181 #[derive(Debug, Deserialize, Clone)] 182 struct RawSettings { 183 pub metadata: RadrootsNostrMetadata, 184 pub config: RawConfiguration, 185 } 186 187 impl RawSettings { 188 fn into_settings(self, paths: &RadrootsdRuntimePaths) -> Settings { 189 Settings { 190 metadata: self.metadata, 191 config: Configuration { 192 service: self.config.service.into_service_config(paths), 193 rpc: self.config.rpc, 194 rpc_addr: self.config.rpc_addr, 195 nip46: self.config.nip46, 196 publish_proxy: self.config.publish_proxy.into_publish_proxy_config(paths), 197 obsolete_bridge_config_present: self 198 .config 199 .obsolete_publish_bridge_config 200 .is_some(), 201 }, 202 } 203 } 204 } 205 206 fn load_settings_from_path_with_resolver( 207 path: &Path, 208 resolver: &radroots_runtime_paths::RadrootsPathResolver, 209 profile: radroots_runtime_paths::RadrootsPathProfile, 210 repo_local_root: Option<&Path>, 211 ) -> Result<Settings> { 212 let raw: RawSettings = radroots_runtime::load_required_file(path) 213 .with_context(|| format!("load configuration from {}", path.display()))?; 214 let paths = resolve_runtime_paths_with_resolver(resolver, profile, repo_local_root)?; 215 let settings = raw.into_settings(&paths); 216 settings.validate()?; 217 Ok(settings) 218 } 219 220 pub fn load_settings_from_path(path: impl AsRef<Path>) -> Result<Settings> { 221 let path = path.as_ref(); 222 let (profile, repo_local_root) = process_path_selection()?; 223 load_settings_from_path_with_resolver( 224 path, 225 &radroots_runtime_paths::RadrootsPathResolver::current(), 226 profile, 227 repo_local_root.as_deref(), 228 ) 229 } 230 231 #[derive(Debug, Serialize, Deserialize, Clone)] 232 pub struct Nip46Config { 233 #[serde(default = "default_nip46_session_ttl_secs")] 234 pub session_ttl_secs: u64, 235 #[serde(default = "default_nip46_perms")] 236 pub perms: Vec<String>, 237 #[serde(default = "default_nip46_public_jsonrpc_enabled")] 238 pub public_jsonrpc_enabled: bool, 239 #[serde(default)] 240 pub nostrconnect_url: Option<String>, 241 } 242 243 impl Default for Nip46Config { 244 fn default() -> Self { 245 Self { 246 session_ttl_secs: default_nip46_session_ttl_secs(), 247 perms: default_nip46_perms(), 248 public_jsonrpc_enabled: default_nip46_public_jsonrpc_enabled(), 249 nostrconnect_url: None, 250 } 251 } 252 } 253 254 #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] 255 #[serde(rename_all = "snake_case")] 256 pub enum PublishProxyRelayUrlPolicy { 257 Public, 258 Localhost, 259 } 260 261 #[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)] 262 pub struct PublishProxyConfig { 263 #[serde(default = "default_publish_proxy_enabled")] 264 pub enabled: bool, 265 #[serde(default = "default_publish_proxy_connect_timeout_secs")] 266 pub connect_timeout_secs: u64, 267 #[serde(default = "default_publish_proxy_max_event_bytes")] 268 pub max_event_bytes: usize, 269 #[serde(default = "default_publish_proxy_max_relays_per_request")] 270 pub max_relays_per_request: usize, 271 #[serde(default = "default_publish_proxy_job_list_limit")] 272 pub job_list_limit: usize, 273 #[serde(default = "default_publish_proxy_max_concurrent_publish_jobs")] 274 pub max_concurrent_publish_jobs: usize, 275 #[serde(default = "default_publish_proxy_database_path")] 276 pub database_path: PathBuf, 277 #[serde(default = "default_publish_proxy_relay_url_policy")] 278 pub relay_url_policy: PublishProxyRelayUrlPolicy, 279 #[serde(default)] 280 pub author_relay_discovery_relays: Vec<String>, 281 #[serde(default)] 282 pub daemon_default_publish_relays: Vec<String>, 283 } 284 285 impl Default for PublishProxyConfig { 286 fn default() -> Self { 287 Self { 288 enabled: default_publish_proxy_enabled(), 289 connect_timeout_secs: default_publish_proxy_connect_timeout_secs(), 290 max_event_bytes: default_publish_proxy_max_event_bytes(), 291 max_relays_per_request: default_publish_proxy_max_relays_per_request(), 292 job_list_limit: default_publish_proxy_job_list_limit(), 293 max_concurrent_publish_jobs: default_publish_proxy_max_concurrent_publish_jobs(), 294 database_path: default_publish_proxy_database_path(), 295 relay_url_policy: default_publish_proxy_relay_url_policy(), 296 author_relay_discovery_relays: Vec::new(), 297 daemon_default_publish_relays: Vec::new(), 298 } 299 } 300 } 301 302 impl PublishProxyConfig { 303 pub fn validate(&self) -> Result<()> { 304 if self.max_event_bytes == 0 { 305 bail!("publish_proxy max_event_bytes must be greater than zero"); 306 } 307 if self.max_relays_per_request == 0 { 308 bail!("publish_proxy max_relays_per_request must be greater than zero"); 309 } 310 if self.job_list_limit == 0 { 311 bail!("publish_proxy job_list_limit must be greater than zero"); 312 } 313 if self.max_concurrent_publish_jobs == 0 { 314 bail!("publish_proxy max_concurrent_publish_jobs must be greater than zero"); 315 } 316 if self.connect_timeout_secs == 0 { 317 bail!("publish_proxy connect_timeout_secs must be greater than zero"); 318 } 319 Ok(()) 320 } 321 } 322 323 #[derive(Debug, Serialize, Deserialize, Clone)] 324 pub struct RpcConfig { 325 #[serde(default = "default_rpc_addr")] 326 pub addr: String, 327 #[serde(default = "default_max_request_body_size")] 328 pub max_request_body_size: u32, 329 #[serde(default = "default_max_response_body_size")] 330 pub max_response_body_size: u32, 331 #[serde(default = "default_max_connections")] 332 pub max_connections: u32, 333 #[serde(default = "default_max_subscriptions_per_connection")] 334 pub max_subscriptions_per_connection: u32, 335 #[serde(default = "default_message_buffer_capacity")] 336 pub message_buffer_capacity: u32, 337 #[serde(default = "default_rpc_batch_request_limit")] 338 pub batch_request_limit: Option<u32>, 339 } 340 341 impl Default for RpcConfig { 342 fn default() -> Self { 343 Self { 344 addr: default_rpc_addr(), 345 max_request_body_size: default_max_request_body_size(), 346 max_response_body_size: default_max_response_body_size(), 347 max_connections: default_max_connections(), 348 max_subscriptions_per_connection: default_max_subscriptions_per_connection(), 349 message_buffer_capacity: default_message_buffer_capacity(), 350 batch_request_limit: default_rpc_batch_request_limit(), 351 } 352 } 353 } 354 355 #[derive(Debug, Serialize, Deserialize, Clone)] 356 pub struct Configuration { 357 #[serde(flatten)] 358 pub service: RadrootsNostrServiceConfig, 359 #[serde(default)] 360 pub rpc: RpcConfig, 361 #[serde(default)] 362 pub rpc_addr: Option<String>, 363 #[serde(default)] 364 pub nip46: Nip46Config, 365 #[serde(default)] 366 pub publish_proxy: PublishProxyConfig, 367 #[serde(default, skip_serializing)] 368 pub(crate) obsolete_bridge_config_present: bool, 369 } 370 371 impl Configuration { 372 pub fn rpc_addr(&self) -> &str { 373 self.rpc_addr.as_deref().unwrap_or(self.rpc.addr.as_str()) 374 } 375 376 pub fn validate(&self) -> Result<()> { 377 if self.obsolete_bridge_config_present { 378 bail!("config.bridge is obsolete; use config.publish_proxy"); 379 } 380 self.publish_proxy.validate()?; 381 Ok(()) 382 } 383 } 384 385 #[derive(Debug, Clone, Serialize, Deserialize)] 386 pub struct Settings { 387 pub metadata: RadrootsNostrMetadata, 388 pub config: Configuration, 389 } 390 391 impl Settings { 392 pub fn validate(&self) -> Result<()> { 393 self.config.validate() 394 } 395 } 396 397 #[cfg(test)] 398 mod tests { 399 use std::path::PathBuf; 400 401 use super::{ 402 Configuration, Nip46Config, PublishProxyConfig, PublishProxyRelayUrlPolicy, RpcConfig, 403 load_settings_from_path_with_resolver, 404 }; 405 use crate::app::paths::{ 406 default_runtime_paths_for_process, resolve_runtime_paths_with_resolver, 407 runtime_contract_with_resolver, 408 }; 409 use radroots_runtime::RadrootsNostrServiceConfig; 410 use radroots_runtime_paths::{ 411 RadrootsHostEnvironment, RadrootsPathProfile, RadrootsPathResolver, RadrootsPlatform, 412 }; 413 414 fn linux_resolver(home: &str) -> RadrootsPathResolver { 415 RadrootsPathResolver::new( 416 RadrootsPlatform::Linux, 417 RadrootsHostEnvironment { 418 home_dir: Some(PathBuf::from(home)), 419 ..RadrootsHostEnvironment::default() 420 }, 421 ) 422 } 423 424 fn service_config() -> RadrootsNostrServiceConfig { 425 let paths = resolve_runtime_paths_with_resolver( 426 &linux_resolver("/home/treesap"), 427 RadrootsPathProfile::InteractiveUser, 428 None, 429 ) 430 .expect("resolve interactive-user paths"); 431 RadrootsNostrServiceConfig { 432 logs_dir: paths.logs_dir.display().to_string(), 433 relays: Vec::new(), 434 nip89_identifier: Some("radrootsd".to_string()), 435 nip89_extra_tags: Vec::new(), 436 } 437 } 438 439 #[test] 440 fn nip46_defaults_are_expected() { 441 let cfg = Nip46Config::default(); 442 assert_eq!(cfg.session_ttl_secs, 900); 443 assert!(cfg.perms.is_empty()); 444 assert!(!cfg.public_jsonrpc_enabled); 445 assert!(cfg.nostrconnect_url.is_none()); 446 } 447 448 #[test] 449 fn rpc_defaults_disable_batches() { 450 let cfg = RpcConfig::default(); 451 assert_eq!(cfg.addr, "127.0.0.1:7070"); 452 assert_eq!(cfg.batch_request_limit, Some(0)); 453 } 454 455 #[test] 456 fn publish_proxy_defaults_are_expected() { 457 let paths = default_runtime_paths_for_process().expect("resolve process runtime paths"); 458 let cfg = PublishProxyConfig::default(); 459 assert!(cfg.enabled); 460 assert_eq!(cfg.connect_timeout_secs, 10); 461 assert_eq!(cfg.max_event_bytes, 128 * 1024); 462 assert_eq!(cfg.max_relays_per_request, 20); 463 assert_eq!(cfg.job_list_limit, 100); 464 assert_eq!(cfg.max_concurrent_publish_jobs, 8); 465 assert_eq!(cfg.database_path, paths.publish_proxy_database_path); 466 assert_eq!(cfg.relay_url_policy, PublishProxyRelayUrlPolicy::Public); 467 assert!(cfg.author_relay_discovery_relays.is_empty()); 468 assert!(cfg.daemon_default_publish_relays.is_empty()); 469 } 470 471 #[test] 472 fn rpc_addr_prefers_override() { 473 let mut cfg = Configuration { 474 service: service_config(), 475 rpc: RpcConfig { 476 addr: "127.0.0.1:1111".to_string(), 477 ..RpcConfig::default() 478 }, 479 rpc_addr: None, 480 nip46: Nip46Config::default(), 481 publish_proxy: PublishProxyConfig::default(), 482 obsolete_bridge_config_present: false, 483 }; 484 assert_eq!(cfg.rpc_addr(), "127.0.0.1:1111"); 485 cfg.rpc_addr = Some("127.0.0.1:2222".to_string()); 486 assert_eq!(cfg.rpc_addr(), "127.0.0.1:2222"); 487 } 488 489 #[test] 490 fn publish_proxy_validation_rejects_zero_limits() { 491 let mut cfg = PublishProxyConfig::default(); 492 cfg.max_event_bytes = 0; 493 assert!(cfg.validate().is_err()); 494 let mut cfg = PublishProxyConfig::default(); 495 cfg.max_relays_per_request = 0; 496 assert!(cfg.validate().is_err()); 497 let mut cfg = PublishProxyConfig::default(); 498 cfg.job_list_limit = 0; 499 assert!(cfg.validate().is_err()); 500 let mut cfg = PublishProxyConfig::default(); 501 cfg.max_concurrent_publish_jobs = 0; 502 assert!(cfg.validate().is_err()); 503 let mut cfg = PublishProxyConfig::default(); 504 cfg.connect_timeout_secs = 0; 505 assert!(cfg.validate().is_err()); 506 } 507 508 #[test] 509 fn runtime_paths_follow_interactive_user_contract() { 510 let paths = resolve_runtime_paths_with_resolver( 511 &linux_resolver("/home/treesap"), 512 RadrootsPathProfile::InteractiveUser, 513 None, 514 ) 515 .expect("resolve interactive-user paths"); 516 517 assert_eq!( 518 paths.config_path, 519 PathBuf::from("/home/treesap/.radroots/config/services/radrootsd/config.toml") 520 ); 521 assert_eq!( 522 paths.logs_dir, 523 PathBuf::from("/home/treesap/.radroots/logs/services/radrootsd") 524 ); 525 assert_eq!( 526 paths.identity_path, 527 PathBuf::from( 528 "/home/treesap/.radroots/secrets/services/radrootsd/identity.secret.json" 529 ) 530 ); 531 assert_eq!( 532 paths.publish_proxy_database_path, 533 PathBuf::from("/home/treesap/.radroots/data/services/radrootsd/publish_proxy.sqlite") 534 ); 535 } 536 537 #[test] 538 fn runtime_paths_follow_service_host_contract() { 539 let paths = resolve_runtime_paths_with_resolver( 540 &linux_resolver("/home/treesap"), 541 RadrootsPathProfile::ServiceHost, 542 None, 543 ) 544 .expect("resolve service-host paths"); 545 546 assert_eq!( 547 paths.config_path, 548 PathBuf::from("/etc/radroots/services/radrootsd/config.toml") 549 ); 550 assert_eq!( 551 paths.logs_dir, 552 PathBuf::from("/var/log/radroots/services/radrootsd") 553 ); 554 assert_eq!( 555 paths.identity_path, 556 PathBuf::from("/etc/radroots/secrets/services/radrootsd/identity.secret.json") 557 ); 558 assert_eq!( 559 paths.publish_proxy_database_path, 560 PathBuf::from("/var/lib/radroots/services/radrootsd/publish_proxy.sqlite") 561 ); 562 } 563 564 #[test] 565 fn runtime_paths_follow_repo_local_contract() { 566 let repo_local_root = PathBuf::from("/repo/.local/radroots/dev/radrootsd"); 567 let paths = resolve_runtime_paths_with_resolver( 568 &linux_resolver("/home/treesap"), 569 RadrootsPathProfile::RepoLocal, 570 Some(repo_local_root.as_path()), 571 ) 572 .expect("resolve repo-local paths"); 573 574 assert_eq!( 575 paths.config_path, 576 repo_local_root.join("config/services/radrootsd/config.toml") 577 ); 578 assert_eq!( 579 paths.logs_dir, 580 repo_local_root.join("logs/services/radrootsd") 581 ); 582 assert_eq!( 583 paths.identity_path, 584 repo_local_root.join("secrets/services/radrootsd/identity.secret.json") 585 ); 586 assert_eq!( 587 paths.publish_proxy_database_path, 588 repo_local_root.join("data/services/radrootsd/publish_proxy.sqlite") 589 ); 590 } 591 592 #[test] 593 fn load_settings_materializes_profile_defaults_when_paths_are_omitted() { 594 let temp = tempfile::tempdir().expect("tempdir"); 595 let config_path = temp.path().join("radrootsd.toml"); 596 std::fs::write( 597 &config_path, 598 r#" 599 [metadata] 600 name = "radrootsd-test" 601 602 [config] 603 relays = ["ws://127.0.0.1:8080"] 604 605 [config.rpc] 606 addr = "127.0.0.1:7070" 607 "#, 608 ) 609 .expect("write config"); 610 611 let settings = load_settings_from_path_with_resolver( 612 &config_path, 613 &linux_resolver("/home/treesap"), 614 RadrootsPathProfile::InteractiveUser, 615 None, 616 ) 617 .expect("load settings"); 618 619 assert_eq!( 620 settings.config.service.logs_dir, 621 "/home/treesap/.radroots/logs/services/radrootsd" 622 ); 623 assert_eq!( 624 settings.config.publish_proxy.database_path, 625 PathBuf::from("/home/treesap/.radroots/data/services/radrootsd/publish_proxy.sqlite") 626 ); 627 } 628 629 #[test] 630 fn obsolete_config_is_rejected() { 631 let temp = tempfile::tempdir().expect("tempdir"); 632 let config_path = temp.path().join("radrootsd.toml"); 633 std::fs::write( 634 &config_path, 635 r#" 636 [metadata] 637 name = "radrootsd-test" 638 639 [config] 640 relays = [] 641 642 [config.bridge] 643 enabled = true 644 "#, 645 ) 646 .expect("write config"); 647 648 let err = load_settings_from_path_with_resolver( 649 &config_path, 650 &linux_resolver("/home/treesap"), 651 RadrootsPathProfile::InteractiveUser, 652 None, 653 ) 654 .expect_err("obsolete config should fail"); 655 assert!(err.to_string().contains("config.bridge")); 656 } 657 658 #[test] 659 fn runtime_contract_output_matches_interactive_user_contract() { 660 let contract = runtime_contract_with_resolver( 661 &linux_resolver("/home/treesap"), 662 RadrootsPathProfile::InteractiveUser, 663 None, 664 ) 665 .expect("interactive-user contract"); 666 667 assert_eq!(contract.active_profile, "interactive_user"); 668 assert_eq!( 669 contract.path_overrides.subordinate_path_override_keys, 670 vec![ 671 "config.service.logs_dir".to_owned(), 672 "config.publish_proxy.database_path".to_owned(), 673 ] 674 ); 675 assert_eq!( 676 contract.canonical_publish_proxy_database_path, 677 PathBuf::from("/home/treesap/.radroots/data/services/radrootsd/publish_proxy.sqlite") 678 ); 679 } 680 }