diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/config.rs | 261 | ||||
| -rw-r--r-- | src/http/nip11.rs | 33 | ||||
| -rw-r--r-- | src/main.rs | 9 | ||||
| -rw-r--r-- | src/nostr/builder.rs | 34 | ||||
| -rw-r--r-- | src/nostr/events.rs | 12 |
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 { | |||
| 56 | impl RelayInformationDocument { | 56 | impl 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); |