upleb.uk

Public git repos — served from a NIP-34 GRASP relay at git.upleb.uk

summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorDanConwayDev <DanConwayDev@protonmail.com>2026-01-12 21:20:00 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2026-01-12 21:21:53 +0000
commit1948312d40f34fca868d1ef6d6d94e165c09738c (patch)
treef25f930785145023be6fe33e52904a5d8383a62d /src
parent82b56c37b26a2fac1a294873e539b19b9325dca6 (diff)
refactor(config): validate eagerly at startup and remove Result from runtime config methods
Refactors configuration validation to fail fast on fatal errors at startup while gracefully handling recoverable issues (e.g., malformed whitelist entries). Changes: - Add Config::validate() for eager validation called immediately after load - Remove Result<> from archive_config() and repository_config() methods - WhitelistEntry::parse_whitelist() skips invalid entries with warnings - Validate relay_owner_nsec format in Config::validate() - Update all call sites to remove Result handling from config getters Benefits: - Fatal config errors (incompatible settings) fail at startup, not runtime - Recoverable errors (bad whitelist entries) logged as warnings and skipped - No Result handling scattered throughout runtime code after validation - Config methods safe to call without error handling after validate() Testing: - Add 7 new tests for validation edge cases and error handling - Total config tests: 40 (up from 33) - All 320 library tests passing Breaking change: Config users must call config.validate() after Config::load() to ensure configuration is valid. This is enforced in main.rs.
Diffstat (limited to 'src')
-rw-r--r--src/config.rs261
-rw-r--r--src/http/nip11.rs33
-rw-r--r--src/main.rs9
-rw-r--r--src/nostr/builder.rs34
-rw-r--r--src/nostr/events.rs12
5 files changed, 230 insertions, 119 deletions
diff --git a/src/config.rs b/src/config.rs
index 8a9eb4d..37b1c1e 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -85,16 +85,25 @@ impl WhitelistEntry {
85 } 85 }
86 86
87 /// Parse whitelist from comma-separated string 87 /// Parse whitelist from comma-separated string
88 pub fn parse_whitelist(input: &str) -> Result<Vec<Self>> { 88 ///
89 /// Skips invalid entries with warnings instead of failing.
90 /// This allows the config to load even if some whitelist entries are malformed.
91 pub fn parse_whitelist(input: &str) -> Vec<Self> {
89 if input.trim().is_empty() { 92 if input.trim().is_empty() {
90 return Ok(Vec::new()); 93 return Vec::new();
91 } 94 }
92 95
93 input 96 input
94 .split(',') 97 .split(',')
95 .map(|s| s.trim()) 98 .map(|s| s.trim())
96 .filter(|s| !s.is_empty()) 99 .filter(|s| !s.is_empty())
97 .map(Self::parse) 100 .filter_map(|s| match Self::parse(s) {
101 Ok(entry) => Some(entry),
102 Err(e) => {
103 tracing::warn!("Skipping invalid whitelist entry '{}': {}", s, e);
104 None
105 }
106 })
98 .collect() 107 .collect()
99 } 108 }
100} 109}
@@ -464,23 +473,60 @@ impl Config {
464 } 473 }
465 } 474 }
466 475
476 /// Validate configuration and return fatal errors
477 ///
478 /// This should be called immediately after Config::load() to fail fast on config errors.
479 /// Recoverable issues (e.g., malformed whitelist entries) are logged as warnings and skipped.
480 pub fn validate(&self) -> Result<()> {
481 // Validate relay owner nsec (should always be set by Config::load())
482 let nsec = self
483 .relay_owner_nsec
484 .as_ref()
485 .context("relay_owner_nsec not set (should be set by Config::load())")?;
486 Keys::parse(nsec).context("Invalid relay_owner_nsec format")?;
487
488 // Validate archive configuration
489 let archive_whitelist = WhitelistEntry::parse_whitelist(&self.archive_whitelist);
490 let archive_enabled = self.archive_all || !archive_whitelist.is_empty();
491
492 // Fatal error: archive_read_only=true without archive mode enabled
493 if let Some(true) = self.archive_read_only {
494 if !archive_enabled {
495 return Err(anyhow!(
496 "NGIT_ARCHIVE_READ_ONLY=true requires either NGIT_ARCHIVE_ALL=true or NGIT_ARCHIVE_WHITELIST to be set"
497 ));
498 }
499 }
500
501 // Validate repository whitelist configuration
502 let repository_whitelist = WhitelistEntry::parse_whitelist(&self.repository_whitelist);
503
504 // Fatal error: repository_whitelist with archive_read_only=true (incompatible)
505 if !repository_whitelist.is_empty() {
506 let read_only = self.archive_read_only.unwrap_or(archive_enabled);
507 if read_only {
508 return Err(anyhow!(
509 "NGIT_REPOSITORY_WHITELIST cannot be used with NGIT_ARCHIVE_READ_ONLY=true. \
510 Archive read-only mode rejects announcements that don't match the archive whitelist, \
511 regardless of service listing. Either set NGIT_ARCHIVE_READ_ONLY=false or use \
512 NGIT_ARCHIVE_WHITELIST instead of NGIT_REPOSITORY_WHITELIST."
513 ));
514 }
515 }
516
517 Ok(())
518 }
519
467 /// Get parsed archive configuration with computed read-only mode 520 /// Get parsed archive configuration with computed read-only mode
468 /// 521 ///
469 /// Read-only mode defaults to true if archive mode is enabled, false otherwise. 522 /// Read-only mode defaults to true if archive mode is enabled, false otherwise.
470 /// Throws error if explicitly set to true without archive mode enabled. 523 /// This method assumes config has been validated - call Config::validate() first!
471 pub fn archive_config(&self) -> Result<ArchiveConfig> { 524 pub fn archive_config(&self) -> ArchiveConfig {
472 let whitelist = WhitelistEntry::parse_whitelist(&self.archive_whitelist)?; 525 let whitelist = WhitelistEntry::parse_whitelist(&self.archive_whitelist);
473 let archive_enabled = self.archive_all || !whitelist.is_empty(); 526 let archive_enabled = self.archive_all || !whitelist.is_empty();
474 527
475 let read_only = match self.archive_read_only { 528 let read_only = match self.archive_read_only {
476 Some(true) => { 529 Some(true) => true, // Already validated in validate()
477 if !archive_enabled {
478 return Err(anyhow!(
479 "NGIT_ARCHIVE_READ_ONLY=true requires either NGIT_ARCHIVE_ALL=true or NGIT_ARCHIVE_WHITELIST to be set"
480 ));
481 }
482 true
483 }
484 Some(false) => false, 530 Some(false) => false,
485 None => { 531 None => {
486 // Default: true if archive mode enabled, false otherwise 532 // Default: true if archive mode enabled, false otherwise
@@ -488,33 +534,19 @@ impl Config {
488 } 534 }
489 }; 535 };
490 536
491 Ok(ArchiveConfig { 537 ArchiveConfig {
492 archive_all: self.archive_all, 538 archive_all: self.archive_all,
493 whitelist, 539 whitelist,
494 read_only, 540 read_only,
495 }) 541 }
496 } 542 }
497 543
498 /// Get parsed repository whitelist configuration 544 /// Get parsed repository whitelist configuration
499 /// 545 ///
500 /// Throws error if repository_whitelist is set together with archive_read_only=true 546 /// This method assumes config has been validated - call Config::validate() first!
501 pub fn repository_config(&self) -> Result<RepositoryConfig> { 547 pub fn repository_config(&self) -> RepositoryConfig {
502 let whitelist = WhitelistEntry::parse_whitelist(&self.repository_whitelist)?; 548 let whitelist = WhitelistEntry::parse_whitelist(&self.repository_whitelist);
503 549 RepositoryConfig { whitelist }
504 // Validate incompatible configurations
505 if !whitelist.is_empty() {
506 let archive_config = self.archive_config()?;
507 if archive_config.read_only {
508 return Err(anyhow!(
509 "NGIT_REPOSITORY_WHITELIST cannot be used with NGIT_ARCHIVE_READ_ONLY=true. \
510 Archive read-only mode rejects announcements that don't match the archive whitelist, \
511 regardless of service listing. Either set NGIT_ARCHIVE_READ_ONLY=false or use \
512 NGIT_ARCHIVE_WHITELIST instead of NGIT_REPOSITORY_WHITELIST."
513 ));
514 }
515 }
516
517 Ok(RepositoryConfig { whitelist })
518 } 550 }
519 551
520 /// Create config for testing 552 /// Create config for testing
@@ -807,10 +839,10 @@ mod tests {
807 839
808 #[test] 840 #[test]
809 fn test_parse_whitelist_empty() { 841 fn test_parse_whitelist_empty() {
810 let whitelist = WhitelistEntry::parse_whitelist("").unwrap(); 842 let whitelist = WhitelistEntry::parse_whitelist("");
811 assert!(whitelist.is_empty()); 843 assert!(whitelist.is_empty());
812 844
813 let whitelist = WhitelistEntry::parse_whitelist(" ").unwrap(); 845 let whitelist = WhitelistEntry::parse_whitelist(" ");
814 assert!(whitelist.is_empty()); 846 assert!(whitelist.is_empty());
815 } 847 }
816 848
@@ -823,12 +855,22 @@ mod tests {
823 let whitelist = WhitelistEntry::parse_whitelist(&format!( 855 let whitelist = WhitelistEntry::parse_whitelist(&format!(
824 "{},bitcoin-core,{}/linux", 856 "{},bitcoin-core,{}/linux",
825 test_npub1, test_npub2 857 test_npub1, test_npub2
826 )) 858 ));
827 .unwrap();
828 assert_eq!(whitelist.len(), 3); 859 assert_eq!(whitelist.len(), 3);
829 } 860 }
830 861
831 #[test] 862 #[test]
863 fn test_parse_whitelist_invalid_npub_skipped() {
864 // Invalid entries should be skipped with warnings, not fail
865 let whitelist = WhitelistEntry::parse_whitelist("npub1invalid,bitcoin-core");
866 assert_eq!(whitelist.len(), 1); // Only bitcoin-core should be parsed
867 assert!(matches!(
868 &whitelist[0],
869 WhitelistEntry::Identifier(id) if id == "bitcoin-core"
870 ));
871 }
872
873 #[test]
832 fn test_archive_config_parsing() { 874 fn test_archive_config_parsing() {
833 let keys = Keys::generate(); 875 let keys = Keys::generate();
834 let test_npub = keys.public_key().to_bech32().unwrap(); 876 let test_npub = keys.public_key().to_bech32().unwrap();
@@ -836,31 +878,22 @@ mod tests {
836 archive_whitelist: format!("{},bitcoin-core", test_npub), 878 archive_whitelist: format!("{},bitcoin-core", test_npub),
837 ..Config::for_testing() 879 ..Config::for_testing()
838 }; 880 };
839 let archive_config = config.archive_config().unwrap(); 881 let archive_config = config.archive_config();
840 assert_eq!(archive_config.whitelist.len(), 2); 882 assert_eq!(archive_config.whitelist.len(), 2);
841 } 883 }
842 884
843 #[test] 885 #[test]
844 fn test_archive_config_invalid_npub() {
845 let config = Config {
846 archive_whitelist: "npub1invalid".to_string(),
847 ..Config::for_testing()
848 };
849 assert!(config.archive_config().is_err());
850 }
851
852 #[test]
853 fn test_archive_read_only_defaults() { 886 fn test_archive_read_only_defaults() {
854 // Default: false when no archive mode 887 // Default: false when no archive mode
855 let config = Config::for_testing(); 888 let config = Config::for_testing();
856 assert_eq!(config.archive_config().unwrap().read_only, false); 889 assert_eq!(config.archive_config().read_only, false);
857 890
858 // Default: true when archive_all is set 891 // Default: true when archive_all is set
859 let config = Config { 892 let config = Config {
860 archive_all: true, 893 archive_all: true,
861 ..Config::for_testing() 894 ..Config::for_testing()
862 }; 895 };
863 assert_eq!(config.archive_config().unwrap().read_only, true); 896 assert_eq!(config.archive_config().read_only, true);
864 897
865 // Default: true when archive_whitelist is set 898 // Default: true when archive_whitelist is set
866 let keys = Keys::generate(); 899 let keys = Keys::generate();
@@ -869,7 +902,7 @@ mod tests {
869 archive_whitelist: test_npub, 902 archive_whitelist: test_npub,
870 ..Config::for_testing() 903 ..Config::for_testing()
871 }; 904 };
872 assert_eq!(config.archive_config().unwrap().read_only, true); 905 assert_eq!(config.archive_config().read_only, true);
873 } 906 }
874 907
875 #[test] 908 #[test]
@@ -880,7 +913,7 @@ mod tests {
880 archive_read_only: Some(true), 913 archive_read_only: Some(true),
881 ..Config::for_testing() 914 ..Config::for_testing()
882 }; 915 };
883 assert_eq!(config.archive_config().unwrap().read_only, true); 916 assert_eq!(config.archive_config().read_only, true);
884 917
885 // Explicit false with archive_all (unusual but allowed) 918 // Explicit false with archive_all (unusual but allowed)
886 let config = Config { 919 let config = Config {
@@ -888,29 +921,26 @@ mod tests {
888 archive_read_only: Some(false), 921 archive_read_only: Some(false),
889 ..Config::for_testing() 922 ..Config::for_testing()
890 }; 923 };
891 assert_eq!(config.archive_config().unwrap().read_only, false); 924 assert_eq!(config.archive_config().read_only, false);
892 925
893 // Explicit false without archive mode 926 // Explicit false without archive mode
894 let config = Config { 927 let config = Config {
895 archive_read_only: Some(false), 928 archive_read_only: Some(false),
896 ..Config::for_testing() 929 ..Config::for_testing()
897 }; 930 };
898 assert_eq!(config.archive_config().unwrap().read_only, false); 931 assert_eq!(config.archive_config().read_only, false);
899 } 932 }
900 933
901 #[test] 934 #[test]
902 fn test_archive_read_only_error() { 935 fn test_archive_read_only_validation_error() {
903 // Error: true without archive mode 936 // Error: true without archive mode should fail validation
904 let config = Config { 937 let config = Config {
905 archive_read_only: Some(true), 938 archive_read_only: Some(true),
906 ..Config::for_testing() 939 ..Config::for_testing()
907 }; 940 };
908 assert!(config.archive_config().is_err()); 941 let result = config.validate();
909 assert!(config 942 assert!(result.is_err());
910 .archive_config() 943 assert!(result.unwrap_err().to_string().contains("requires either"));
911 .unwrap_err()
912 .to_string()
913 .contains("requires either"));
914 } 944 }
915 945
916 #[test] 946 #[test]
@@ -921,7 +951,7 @@ mod tests {
921 repository_whitelist: format!("{},bitcoin-core", test_npub), 951 repository_whitelist: format!("{},bitcoin-core", test_npub),
922 ..Config::for_testing() 952 ..Config::for_testing()
923 }; 953 };
924 let repo_config = config.repository_config().unwrap(); 954 let repo_config = config.repository_config();
925 assert_eq!(repo_config.whitelist.len(), 2); 955 assert_eq!(repo_config.whitelist.len(), 2);
926 assert!(repo_config.enabled()); 956 assert!(repo_config.enabled());
927 } 957 }
@@ -929,13 +959,13 @@ mod tests {
929 #[test] 959 #[test]
930 fn test_repository_whitelist_empty() { 960 fn test_repository_whitelist_empty() {
931 let config = Config::for_testing(); 961 let config = Config::for_testing();
932 let repo_config = config.repository_config().unwrap(); 962 let repo_config = config.repository_config();
933 assert!(repo_config.whitelist.is_empty()); 963 assert!(repo_config.whitelist.is_empty());
934 assert!(!repo_config.enabled()); 964 assert!(!repo_config.enabled());
935 } 965 }
936 966
937 #[test] 967 #[test]
938 fn test_repository_whitelist_incompatible_with_archive_read_only() { 968 fn test_repository_whitelist_validation_incompatible_with_archive_read_only() {
939 let keys = Keys::generate(); 969 let keys = Keys::generate();
940 let test_npub = keys.public_key().to_bech32().unwrap(); 970 let test_npub = keys.public_key().to_bech32().unwrap();
941 let config = Config { 971 let config = Config {
@@ -944,7 +974,7 @@ mod tests {
944 repository_whitelist: test_npub, 974 repository_whitelist: test_npub,
945 ..Config::for_testing() 975 ..Config::for_testing()
946 }; 976 };
947 let result = config.repository_config(); 977 let result = config.validate();
948 assert!(result.is_err()); 978 assert!(result.is_err());
949 let err = result.unwrap_err().to_string(); 979 let err = result.unwrap_err().to_string();
950 assert!(err.contains("cannot be used with")); 980 assert!(err.contains("cannot be used with"));
@@ -961,8 +991,8 @@ mod tests {
961 repository_whitelist: test_npub, 991 repository_whitelist: test_npub,
962 ..Config::for_testing() 992 ..Config::for_testing()
963 }; 993 };
964 // Should not error 994 // Should not error on validation
965 assert!(config.repository_config().is_ok()); 995 assert!(config.validate().is_ok());
966 } 996 }
967 997
968 #[test] 998 #[test]
@@ -980,4 +1010,99 @@ mod tests {
980 assert!(config.matches("npub1bob", "bitcoin-core")); 1010 assert!(config.matches("npub1bob", "bitcoin-core"));
981 assert!(!config.matches("npub1bob", "other-repo")); 1011 assert!(!config.matches("npub1bob", "other-repo"));
982 } 1012 }
1013
1014 #[test]
1015 fn test_validate_success_with_valid_config() {
1016 // Valid config should pass validation
1017 let keys = Keys::generate();
1018 let test_npub = keys.public_key().to_bech32().unwrap();
1019 let config = Config {
1020 archive_whitelist: format!("{},bitcoin-core", test_npub),
1021 archive_read_only: Some(false),
1022 repository_whitelist: "rust".to_string(),
1023 ..Config::for_testing()
1024 };
1025 assert!(config.validate().is_ok());
1026 }
1027
1028 #[test]
1029 fn test_validate_with_all_invalid_whitelist_entries() {
1030 // All invalid entries should be skipped with warnings, but validation should succeed
1031 let config = Config {
1032 archive_whitelist: "npub1invalid,npub1bad,npub1wrong".to_string(),
1033 ..Config::for_testing()
1034 };
1035 assert!(config.validate().is_ok());
1036 // All entries should be skipped
1037 let archive_config = config.archive_config();
1038 assert_eq!(archive_config.whitelist.len(), 0);
1039 assert!(!archive_config.enabled());
1040 }
1041
1042 #[test]
1043 fn test_validate_with_mixed_valid_invalid_entries() {
1044 let keys = Keys::generate();
1045 let test_npub = keys.public_key().to_bech32().unwrap();
1046 // Mixed valid and invalid entries - should keep valid ones
1047 let config = Config {
1048 repository_whitelist: format!("npub1invalid,{},bitcoin-core,npub1bad", test_npub),
1049 ..Config::for_testing()
1050 };
1051 assert!(config.validate().is_ok());
1052 let repo_config = config.repository_config();
1053 // Should have 2 valid entries: the test_npub and bitcoin-core
1054 assert_eq!(repo_config.whitelist.len(), 2);
1055 }
1056
1057 #[test]
1058 fn test_whitelist_entry_with_extra_whitespace() {
1059 let keys = Keys::generate();
1060 let test_npub = keys.public_key().to_bech32().unwrap();
1061 // Whitespace should be trimmed
1062 let whitelist =
1063 WhitelistEntry::parse_whitelist(&format!(" {} , bitcoin-core , rust ", test_npub));
1064 assert_eq!(whitelist.len(), 3);
1065 }
1066
1067 #[test]
1068 fn test_archive_config_with_all_invalid_entries_not_enabled() {
1069 // If all whitelist entries are invalid, archive mode should not be enabled
1070 let config = Config {
1071 archive_whitelist: "npub1invalid,npub1bad".to_string(),
1072 ..Config::for_testing()
1073 };
1074 let archive_config = config.archive_config();
1075 assert!(!archive_config.enabled());
1076 assert_eq!(archive_config.whitelist.len(), 0);
1077 }
1078
1079 #[test]
1080 fn test_validate_detects_invalid_relay_owner_nsec() {
1081 // Invalid nsec should fail validation
1082 let config = Config {
1083 relay_owner_nsec: Some("nsec1invalid".to_string()),
1084 ..Config::for_testing()
1085 };
1086 let result = config.validate();
1087 assert!(result.is_err());
1088 assert!(result
1089 .unwrap_err()
1090 .to_string()
1091 .contains("Invalid relay_owner_nsec"));
1092 }
1093
1094 #[test]
1095 fn test_validate_requires_relay_owner_nsec() {
1096 // Missing nsec should fail validation
1097 let config = Config {
1098 relay_owner_nsec: None,
1099 ..Config::for_testing()
1100 };
1101 let result = config.validate();
1102 assert!(result.is_err());
1103 assert!(result
1104 .unwrap_err()
1105 .to_string()
1106 .contains("relay_owner_nsec not set"));
1107 }
983} 1108}
diff --git a/src/http/nip11.rs b/src/http/nip11.rs
index ff7b8df..7c58175 100644
--- a/src/http/nip11.rs
+++ b/src/http/nip11.rs
@@ -56,16 +56,10 @@ pub struct RelayInformationDocument {
56impl RelayInformationDocument { 56impl RelayInformationDocument {
57 /// Create NIP-11 relay information document from configuration 57 /// Create NIP-11 relay information document from configuration
58 pub fn from_config(config: &Config) -> Self { 58 pub fn from_config(config: &Config) -> Self {
59 // Determine if archive mode is enabled 59 // Get validated configuration (config.validate() must be called at startup)
60 let archive_config = config.archive_config().ok(); 60 let archive_config = config.archive_config();
61 let archive_enabled = archive_config 61 let archive_enabled = archive_config.enabled();
62 .as_ref() 62 let archive_read_only = archive_config.read_only;
63 .map(|ac| ac.enabled())
64 .unwrap_or(false);
65 let archive_read_only = archive_config
66 .as_ref()
67 .map(|ac| ac.read_only)
68 .unwrap_or(false);
69 63
70 // Build supported_grasps list 64 // Build supported_grasps list
71 let mut supported_grasps = vec!["GRASP-01".to_string()]; 65 let mut supported_grasps = vec!["GRASP-01".to_string()];
@@ -75,22 +69,15 @@ impl RelayInformationDocument {
75 supported_grasps.push("GRASP-02".to_string()); 69 supported_grasps.push("GRASP-02".to_string());
76 70
77 // Build curation field for archive read-only mode or repository whitelist 71 // Build curation field for archive read-only mode or repository whitelist
78 let repository_config = config.repository_config().ok(); 72 let repository_config = config.repository_config();
79 let repository_whitelist_enabled = repository_config 73 let repository_whitelist_enabled = repository_config.enabled();
80 .as_ref()
81 .map(|rc| rc.enabled())
82 .unwrap_or(false);
83 74
84 let curation = if archive_read_only { 75 let curation = if archive_read_only {
85 // Archive read-only mode (GRASP-05 only) 76 // Archive read-only mode (GRASP-05 only)
86 if let Some(ref ac) = archive_config { 77 if archive_config.archive_all {
87 if ac.archive_all { 78 Some("Read-only sync of all repositories found on network".to_string())
88 Some("Read-only sync of all repositories found on network".to_string()) 79 } else if !archive_config.whitelist.is_empty() {
89 } else if !ac.whitelist.is_empty() { 80 Some("Read-only sync of whitelisted repositories and maintainers".to_string())
90 Some("Read-only sync of whitelisted repositories and maintainers".to_string())
91 } else {
92 None
93 }
94 } else { 81 } else {
95 None 82 None
96 } 83 }
diff --git a/src/main.rs b/src/main.rs
index 8b959a6..a6f1d9d 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -28,7 +28,14 @@ async fn main() -> Result<()> {
28 // Load configuration (priority: CLI flags > env vars > .env file > defaults) 28 // Load configuration (priority: CLI flags > env vars > .env file > defaults)
29 let config = Config::load()?; 29 let config = Config::load()?;
30 30
31 info!("Configuration loaded: {}", config.bind_address); 31 // Validate configuration and fail fast on fatal errors
32 // Recoverable issues (e.g., malformed whitelist entries) are logged as warnings
33 config.validate()?;
34
35 info!(
36 "Configuration loaded and validated: {}",
37 config.bind_address
38 );
32 info!("Domain: {}", config.domain); 39 info!("Domain: {}", config.domain);
33 info!("Relay name: {}", config.relay_name()); 40 info!("Relay name: {}", config.relay_name());
34 info!("Git data directory: {}", config.effective_git_data_path()); 41 info!("Git data directory: {}", config.effective_git_data_path());
diff --git a/src/nostr/builder.rs b/src/nostr/builder.rs
index 10f7648..9819e37 100644
--- a/src/nostr/builder.rs
+++ b/src/nostr/builder.rs
@@ -567,26 +567,24 @@ pub async fn create_relay(
567 // Clone Arc for the write policy so both relay and policy can access the database 567 // Clone Arc for the write policy so both relay and policy can access the database
568 let git_data_path = config.effective_git_data_path(); 568 let git_data_path = config.effective_git_data_path();
569 569
570 // Parse and log archive configuration 570 // Log archive configuration (config.validate() must be called at startup)
571 if let Ok(archive_config) = config.archive_config() { 571 let archive_config = config.archive_config();
572 if archive_config.enabled() { 572 if archive_config.enabled() {
573 tracing::info!( 573 tracing::info!(
574 "GRASP-05 archive mode enabled: archive_all={}, whitelist_entries={}, read_only={}", 574 "GRASP-05 archive mode enabled: archive_all={}, whitelist_entries={}, read_only={}",
575 archive_config.archive_all, 575 archive_config.archive_all,
576 archive_config.whitelist.len(), 576 archive_config.whitelist.len(),
577 archive_config.read_only 577 archive_config.read_only
578 ); 578 );
579 }
580 } 579 }
581 580
582 // Parse and log repository configuration 581 // Log repository configuration
583 if let Ok(repository_config) = config.repository_config() { 582 let repository_config = config.repository_config();
584 if repository_config.enabled() { 583 if repository_config.enabled() {
585 tracing::info!( 584 tracing::info!(
586 "Repository whitelist enabled: whitelist_entries={}", 585 "Repository whitelist enabled: whitelist_entries={}",
587 repository_config.whitelist.len() 586 repository_config.whitelist.len()
588 ); 587 );
589 }
590 } 588 }
591 589
592 // Create write policy with purgatory integration 590 // Create write policy with purgatory integration
diff --git a/src/nostr/events.rs b/src/nostr/events.rs
index 3ec075d..3b4ef25 100644
--- a/src/nostr/events.rs
+++ b/src/nostr/events.rs
@@ -400,15 +400,9 @@ pub fn validate_announcement(
400 Err(e) => return AnnouncementResult::Reject(format!("Invalid announcement: {}", e)), 400 Err(e) => return AnnouncementResult::Reject(format!("Invalid announcement: {}", e)),
401 }; 401 };
402 402
403 // Get archive and repository configs (fail-secure: reject on config errors) 403 // Get validated configs (config.validate() must be called at startup)
404 let archive_config = match config.archive_config() { 404 let archive_config = config.archive_config();
405 Ok(c) => c, 405 let repository_config = config.repository_config();
406 Err(e) => return AnnouncementResult::Reject(format!("Config error: {}", e)),
407 };
408 let repository_config = match config.repository_config() {
409 Ok(c) => c,
410 Err(e) => return AnnouncementResult::Reject(format!("Config error: {}", e)),
411 };
412 406
413 let npub = announcement.owner_npub(); 407 let npub = announcement.owner_npub();
414 let lists_service = announcement.lists_service(&config.domain); 408 let lists_service = announcement.lists_service(&config.domain);