upleb.uk

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

summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-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);