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 20:30:13 +0000
committerDanConwayDev <DanConwayDev@protonmail.com>2026-01-12 20:30:13 +0000
commita12927181c571fc1641772ad44dd4c6a4ab209d9 (patch)
treed7cb99fa87606e9fb13d91305cda8a0f919e6528 /src
parentc29191b1e1239e931c575a926ec9480e594476d6 (diff)
feat(grasp-05): add read-only mode with auto-enable for archive configs
Implements NGIT_ARCHIVE_READ_ONLY configuration option that defaults to true when archive mode is enabled, allowing relays to operate as read-only syncs of archived repositories. Key changes: - Add NGIT_ARCHIVE_READ_ONLY config option (defaults to true if archive enabled) - NIP-11 advertises GRASP-05 support and includes curation field when read-only - Validation logic rejects non-whitelisted repos in read-only mode - Comprehensive tests for read-only behavior and defaults - Full documentation in config reference, .env.example, and NixOS module Read-only mode enables passive mirroring without being listed in announcements, useful for backup/archive operations while preventing accidental write acceptance.
Diffstat (limited to 'src')
-rw-r--r--src/config.rs107
-rw-r--r--src/http/nip11.rs87
-rw-r--r--src/nostr/builder.rs5
-rw-r--r--src/nostr/events.rs123
4 files changed, 306 insertions, 16 deletions
diff --git a/src/config.rs b/src/config.rs
index b1ab43e..d9917a3 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -98,6 +98,13 @@ pub struct ArchiveConfig {
98 /// 98 ///
99 /// If empty and archive_all is false, GRASP-05 is disabled (GRASP-01 strict mode). 99 /// If empty and archive_all is false, GRASP-05 is disabled (GRASP-01 strict mode).
100 pub whitelist: Vec<ArchiveWhitelistEntry>, 100 pub whitelist: Vec<ArchiveWhitelistEntry>,
101
102 /// Read-only archive mode: relay is a read-only sync of archived repositories
103 ///
104 /// When true, the relay ONLY accepts announcements matching the archive whitelist/all.
105 /// Announcements listing the relay but not in the whitelist are rejected.
106 /// When false, the relay operates in GRASP-01 mode for unwhitelisted repos.
107 pub read_only: bool,
101} 108}
102 109
103impl ArchiveConfig { 110impl ArchiveConfig {
@@ -141,6 +148,7 @@ impl Default for ArchiveConfig {
141 Self { 148 Self {
142 archive_all: false, 149 archive_all: false,
143 whitelist: Vec::new(), 150 whitelist: Vec::new(),
151 read_only: false,
144 } 152 }
145 } 153 }
146} 154}
@@ -311,6 +319,12 @@ pub struct Config {
311 /// Formats: "npub1...", "npub1.../identifier", "identifier" 319 /// Formats: "npub1...", "npub1.../identifier", "identifier"
312 #[arg(long, env = "NGIT_ARCHIVE_WHITELIST", default_value = "")] 320 #[arg(long, env = "NGIT_ARCHIVE_WHITELIST", default_value = "")]
313 pub archive_whitelist: String, 321 pub archive_whitelist: String,
322
323 /// Archive read-only mode: relay is a read-only sync of archived repositories
324 /// Defaults to true if archive_all or archive_whitelist is set, false otherwise
325 /// Throws error if set to true without archive_all or archive_whitelist
326 #[arg(long, env = "NGIT_ARCHIVE_READ_ONLY")]
327 pub archive_read_only: Option<bool>,
314} 328}
315 329
316impl Config { 330impl Config {
@@ -411,12 +425,34 @@ impl Config {
411 } 425 }
412 } 426 }
413 427
414 /// Get parsed archive configuration 428 /// Get parsed archive configuration with computed read-only mode
429 ///
430 /// Read-only mode defaults to true if archive mode is enabled, false otherwise.
431 /// Throws error if explicitly set to true without archive mode enabled.
415 pub fn archive_config(&self) -> Result<ArchiveConfig> { 432 pub fn archive_config(&self) -> Result<ArchiveConfig> {
416 let whitelist = ArchiveConfig::parse_whitelist(&self.archive_whitelist)?; 433 let whitelist = ArchiveConfig::parse_whitelist(&self.archive_whitelist)?;
434 let archive_enabled = self.archive_all || !whitelist.is_empty();
435
436 let read_only = match self.archive_read_only {
437 Some(true) => {
438 if !archive_enabled {
439 return Err(anyhow!(
440 "NGIT_ARCHIVE_READ_ONLY=true requires either NGIT_ARCHIVE_ALL=true or NGIT_ARCHIVE_WHITELIST to be set"
441 ));
442 }
443 true
444 }
445 Some(false) => false,
446 None => {
447 // Default: true if archive mode enabled, false otherwise
448 archive_enabled
449 }
450 };
451
417 Ok(ArchiveConfig { 452 Ok(ArchiveConfig {
418 archive_all: self.archive_all, 453 archive_all: self.archive_all,
419 whitelist, 454 whitelist,
455 read_only,
420 }) 456 })
421 } 457 }
422 458
@@ -452,6 +488,7 @@ impl Config {
452 naughty_list_expiration_hours: 12, 488 naughty_list_expiration_hours: 12,
453 archive_all: false, 489 archive_all: false,
454 archive_whitelist: String::new(), 490 archive_whitelist: String::new(),
491 archive_read_only: None,
455 } 492 }
456 } 493 }
457} 494}
@@ -664,12 +701,14 @@ mod tests {
664 let config = ArchiveConfig { 701 let config = ArchiveConfig {
665 archive_all: true, 702 archive_all: true,
666 whitelist: Vec::new(), 703 whitelist: Vec::new(),
704 read_only: true,
667 }; 705 };
668 assert!(config.enabled()); 706 assert!(config.enabled());
669 707
670 let config = ArchiveConfig { 708 let config = ArchiveConfig {
671 archive_all: false, 709 archive_all: false,
672 whitelist: vec![ArchiveWhitelistEntry::Identifier("test".into())], 710 whitelist: vec![ArchiveWhitelistEntry::Identifier("test".into())],
711 read_only: true,
673 }; 712 };
674 assert!(config.enabled()); 713 assert!(config.enabled());
675 } 714 }
@@ -684,6 +723,7 @@ mod tests {
684 ArchiveWhitelistEntry::Pubkey(test_npub.clone()), 723 ArchiveWhitelistEntry::Pubkey(test_npub.clone()),
685 ArchiveWhitelistEntry::Identifier("bitcoin-core".into()), 724 ArchiveWhitelistEntry::Identifier("bitcoin-core".into()),
686 ], 725 ],
726 read_only: false,
687 }; 727 };
688 728
689 assert!(config.matches(&test_npub, "any-repo")); 729 assert!(config.matches(&test_npub, "any-repo"));
@@ -696,6 +736,7 @@ mod tests {
696 let config = ArchiveConfig { 736 let config = ArchiveConfig {
697 archive_all: true, 737 archive_all: true,
698 whitelist: Vec::new(), 738 whitelist: Vec::new(),
739 read_only: true,
699 }; 740 };
700 741
701 assert!(config.matches("npub1alice", "any-repo")); 742 assert!(config.matches("npub1alice", "any-repo"));
@@ -745,4 +786,68 @@ mod tests {
745 }; 786 };
746 assert!(config.archive_config().is_err()); 787 assert!(config.archive_config().is_err());
747 } 788 }
789
790 #[test]
791 fn test_archive_read_only_defaults() {
792 // Default: false when no archive mode
793 let config = Config::for_testing();
794 assert_eq!(config.archive_config().unwrap().read_only, false);
795
796 // Default: true when archive_all is set
797 let config = Config {
798 archive_all: true,
799 ..Config::for_testing()
800 };
801 assert_eq!(config.archive_config().unwrap().read_only, true);
802
803 // Default: true when archive_whitelist is set
804 let keys = Keys::generate();
805 let test_npub = keys.public_key().to_bech32().unwrap();
806 let config = Config {
807 archive_whitelist: test_npub,
808 ..Config::for_testing()
809 };
810 assert_eq!(config.archive_config().unwrap().read_only, true);
811 }
812
813 #[test]
814 fn test_archive_read_only_explicit() {
815 // Explicit true with archive_all
816 let config = Config {
817 archive_all: true,
818 archive_read_only: Some(true),
819 ..Config::for_testing()
820 };
821 assert_eq!(config.archive_config().unwrap().read_only, true);
822
823 // Explicit false with archive_all (unusual but allowed)
824 let config = Config {
825 archive_all: true,
826 archive_read_only: Some(false),
827 ..Config::for_testing()
828 };
829 assert_eq!(config.archive_config().unwrap().read_only, false);
830
831 // Explicit false without archive mode
832 let config = Config {
833 archive_read_only: Some(false),
834 ..Config::for_testing()
835 };
836 assert_eq!(config.archive_config().unwrap().read_only, false);
837 }
838
839 #[test]
840 fn test_archive_read_only_error() {
841 // Error: true without archive mode
842 let config = Config {
843 archive_read_only: Some(true),
844 ..Config::for_testing()
845 };
846 assert!(config.archive_config().is_err());
847 assert!(config
848 .archive_config()
849 .unwrap_err()
850 .to_string()
851 .contains("requires either"));
852 }
748} 853}
diff --git a/src/http/nip11.rs b/src/http/nip11.rs
index b756d9c..71cadb1 100644
--- a/src/http/nip11.rs
+++ b/src/http/nip11.rs
@@ -56,6 +56,41 @@ 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
60 let archive_config = config.archive_config().ok();
61 let archive_enabled = archive_config
62 .as_ref()
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
70 // Build supported_grasps list
71 let mut supported_grasps = vec!["GRASP-01".to_string()];
72 if archive_enabled {
73 supported_grasps.push("GRASP-05".to_string());
74 }
75 supported_grasps.push("GRASP-02".to_string());
76
77 // Build curation field for archive read-only mode
78 let curation = if archive_read_only {
79 if let Some(ref ac) = archive_config {
80 if ac.archive_all {
81 Some("Read-only sync of all repositories found on network".to_string())
82 } else if !ac.whitelist.is_empty() {
83 Some("Read-only sync of whitelisted repositories and maintainers".to_string())
84 } else {
85 None
86 }
87 } else {
88 None
89 }
90 } else {
91 None
92 };
93
59 Self { 94 Self {
60 name: config.relay_name(), 95 name: config.relay_name(),
61 description: config.relay_description.clone(), 96 description: config.relay_description.clone(),
@@ -75,9 +110,9 @@ impl RelayInformationDocument {
75 icon: Some(format!("https://{}/icon.png", config.domain)), 110 icon: Some(format!("https://{}/icon.png", config.domain)),
76 111
77 // GRASP Extensions 112 // GRASP Extensions
78 supported_grasps: vec!["GRASP-01".to_string(), "GRASP-02".to_string()], 113 supported_grasps,
79 repo_acceptance_criteria: "None".to_string(), 114 repo_acceptance_criteria: "None".to_string(),
80 curation: None, // Not a curated relay - only SPAM prevention via GRASP-01 policy 115 curation,
81 } 116 }
82 } 117 }
83 118
@@ -90,6 +125,7 @@ impl RelayInformationDocument {
90#[cfg(test)] 125#[cfg(test)]
91mod tests { 126mod tests {
92 use super::*; 127 use super::*;
128 use nostr_sdk::nips::nip19::ToBech32;
93 129
94 #[test] 130 #[test]
95 fn test_relay_information_document_structure() { 131 fn test_relay_information_document_structure() {
@@ -112,6 +148,7 @@ mod tests {
112 assert!(doc.supported_nips.contains(&11)); 148 assert!(doc.supported_nips.contains(&11));
113 assert!(doc.supported_nips.contains(&34)); 149 assert!(doc.supported_nips.contains(&34));
114 assert!(doc.supported_nips.contains(&77)); 150 assert!(doc.supported_nips.contains(&77));
151 // Without archive mode, only GRASP-01 and GRASP-02
115 assert_eq!(doc.supported_grasps, vec!["GRASP-01", "GRASP-02"]); 152 assert_eq!(doc.supported_grasps, vec!["GRASP-01", "GRASP-02"]);
116 assert!(doc.repo_acceptance_criteria.contains("None")); 153 assert!(doc.repo_acceptance_criteria.contains("None"));
117 assert!(doc.curation.is_none()); 154 assert!(doc.curation.is_none());
@@ -147,4 +184,50 @@ mod tests {
147 assert_eq!(parsed["supported_grasps"][1], "GRASP-02"); 184 assert_eq!(parsed["supported_grasps"][1], "GRASP-02");
148 assert_eq!(parsed["icon"], "https://relay.example.com/icon.png"); 185 assert_eq!(parsed["icon"], "https://relay.example.com/icon.png");
149 } 186 }
187
188 #[test]
189 fn test_nip11_with_archive_mode() {
190 let mut config = Config::for_testing();
191 config.domain = "relay.example.com".to_string();
192 config.relay_name_override = Some("Archive Relay".to_string());
193 config.archive_all = true;
194 config.archive_read_only = Some(true);
195
196 let doc = RelayInformationDocument::from_config(&config);
197
198 // Archive mode enabled: should include GRASP-05
199 assert_eq!(
200 doc.supported_grasps,
201 vec!["GRASP-01", "GRASP-05", "GRASP-02"]
202 );
203 // Archive read-only: should have curation field
204 assert!(doc.curation.is_some());
205 assert!(doc
206 .curation
207 .unwrap()
208 .contains("Read-only sync of all repositories"));
209 }
210
211 #[test]
212 fn test_nip11_with_whitelist_archive() {
213 let keys = nostr_sdk::Keys::generate();
214 let test_npub = keys.public_key().to_bech32().unwrap();
215 let mut config = Config::for_testing();
216 config.domain = "relay.example.com".to_string();
217 config.archive_whitelist = format!("{},bitcoin-core", test_npub);
218
219 let doc = RelayInformationDocument::from_config(&config);
220
221 // Archive whitelist enabled: should include GRASP-05
222 assert_eq!(
223 doc.supported_grasps,
224 vec!["GRASP-01", "GRASP-05", "GRASP-02"]
225 );
226 // Archive read-only defaults to true: should have curation field
227 assert!(doc.curation.is_some());
228 assert!(doc
229 .curation
230 .unwrap()
231 .contains("Read-only sync of whitelisted"));
232 }
150} 233}
diff --git a/src/nostr/builder.rs b/src/nostr/builder.rs
index deee641..33f2fe5 100644
--- a/src/nostr/builder.rs
+++ b/src/nostr/builder.rs
@@ -575,9 +575,10 @@ pub async fn create_relay(
575 575
576 if archive_config.enabled() { 576 if archive_config.enabled() {
577 tracing::info!( 577 tracing::info!(
578 "GRASP-05 archive mode enabled: archive_all={}, whitelist_entries={}", 578 "GRASP-05 archive mode enabled: archive_all={}, whitelist_entries={}, read_only={}",
579 archive_config.archive_all, 579 archive_config.archive_all,
580 archive_config.whitelist.len() 580 archive_config.whitelist.len(),
581 archive_config.read_only
581 ); 582 );
582 } 583 }
583 584
diff --git a/src/nostr/events.rs b/src/nostr/events.rs
index dabe5fe..f83e00c 100644
--- a/src/nostr/events.rs
+++ b/src/nostr/events.rs
@@ -362,10 +362,14 @@ impl RepositoryState {
362/// Validate a repository announcement according to GRASP-01 and GRASP-05 362/// Validate a repository announcement according to GRASP-01 and GRASP-05
363/// 363///
364/// Returns: 364/// Returns:
365/// - Accept: Announcement lists our service (GRASP-01) 365/// - Accept: Announcement lists our service (GRASP-01) - unless archive_read_only mode
366/// - AcceptArchive: Announcement matches archive config (GRASP-05) 366/// - AcceptArchive: Announcement matches archive config (GRASP-05)
367/// - Reject: Validation failed 367/// - Reject: Validation failed
368/// 368///
369/// When archive_read_only is true:
370/// - ONLY accept announcements matching archive whitelist/all
371/// - REJECT announcements listing our service but not in whitelist (read-only sync mode)
372///
369/// Note: AcceptMaintainer is NOT returned here (requires database access) 373/// Note: AcceptMaintainer is NOT returned here (requires database access)
370pub fn validate_announcement( 374pub fn validate_announcement(
371 event: &Event, 375 event: &Event,
@@ -394,23 +398,32 @@ pub fn validate_announcement(
394 Err(e) => return AnnouncementResult::Reject(format!("Invalid announcement: {}", e)), 398 Err(e) => return AnnouncementResult::Reject(format!("Invalid announcement: {}", e)),
395 }; 399 };
396 400
397 // GRASP-01: Check if announcement lists our service 401 // GRASP-01: Normal mode - accept if announcement lists our service
398 if announcement.lists_service(domain) { 402 if announcement.lists_service(domain) && !archive_config.read_only {
399 return AnnouncementResult::Accept; 403 return AnnouncementResult::Accept;
400 } 404 }
401 405
402 // GRASP-05: Check if announcement matches archive configuration
403 let npub = announcement.owner_npub(); 406 let npub = announcement.owner_npub();
407
408 // GRASP-05: Archive mode - accept if announcement matches whitelist
404 if archive_config.matches(&npub, &announcement.identifier) { 409 if archive_config.matches(&npub, &announcement.identifier) {
405 return AnnouncementResult::AcceptArchive; 410 return AnnouncementResult::AcceptArchive;
406 } 411 }
407 412
408 // Reject: Doesn't list us and not whitelisted 413 // Reject with appropriate error message
409 AnnouncementResult::Reject(format!( 414 if archive_config.read_only {
410 "Announcement must list service in both 'clone' and 'relays' tags, or match archive whitelist. \ 415 AnnouncementResult::Reject(format!(
411 Found clone URLs: {:?}, relays: {:?}", 416 "Archive read-only mode: announcement must match archive whitelist. \
412 announcement.clone_urls, announcement.relays 417 Repository {}/{} not in whitelist",
413 )) 418 npub, announcement.identifier
419 ))
420 } else {
421 AnnouncementResult::Reject(format!(
422 "Announcement must list service in both 'clone' and 'relays' tags, or match archive whitelist. \
423 Found clone URLs: {:?}, relays: {:?}",
424 announcement.clone_urls, announcement.relays
425 ))
426 }
414} 427}
415 428
416/// Validate a repository state announcement according to GRASP-01 429/// Validate a repository state announcement according to GRASP-01
@@ -969,6 +982,7 @@ mod tests {
969 let archive_config = ArchiveConfig { 982 let archive_config = ArchiveConfig {
970 archive_all: false, 983 archive_all: false,
971 whitelist: vec![ArchiveWhitelistEntry::Pubkey(npub)], 984 whitelist: vec![ArchiveWhitelistEntry::Pubkey(npub)],
985 read_only: false,
972 }; 986 };
973 987
974 let result = validate_announcement(&event, "gitnostr.com", &archive_config); 988 let result = validate_announcement(&event, "gitnostr.com", &archive_config);
@@ -994,6 +1008,7 @@ mod tests {
994 let archive_config = ArchiveConfig { 1008 let archive_config = ArchiveConfig {
995 archive_all: false, 1009 archive_all: false,
996 whitelist: vec![ArchiveWhitelistEntry::Identifier("bitcoin-core".into())], 1010 whitelist: vec![ArchiveWhitelistEntry::Identifier("bitcoin-core".into())],
1011 read_only: false,
997 }; 1012 };
998 1013
999 let result = validate_announcement(&event, "gitnostr.com", &archive_config); 1014 let result = validate_announcement(&event, "gitnostr.com", &archive_config);
@@ -1023,6 +1038,7 @@ mod tests {
1023 npub, 1038 npub,
1024 identifier: "linux".into(), 1039 identifier: "linux".into(),
1025 }], 1040 }],
1041 read_only: false,
1026 }; 1042 };
1027 1043
1028 let result = validate_announcement(&event, "gitnostr.com", &archive_config); 1044 let result = validate_announcement(&event, "gitnostr.com", &archive_config);
@@ -1048,6 +1064,7 @@ mod tests {
1048 let archive_config = ArchiveConfig { 1064 let archive_config = ArchiveConfig {
1049 archive_all: true, 1065 archive_all: true,
1050 whitelist: Vec::new(), 1066 whitelist: Vec::new(),
1067 read_only: false,
1051 }; 1068 };
1052 1069
1053 let result = validate_announcement(&event, "gitnostr.com", &archive_config); 1070 let result = validate_announcement(&event, "gitnostr.com", &archive_config);
@@ -1073,6 +1090,7 @@ mod tests {
1073 let archive_config = ArchiveConfig { 1090 let archive_config = ArchiveConfig {
1074 archive_all: false, 1091 archive_all: false,
1075 whitelist: vec![ArchiveWhitelistEntry::Identifier("bitcoin-core".into())], 1092 whitelist: vec![ArchiveWhitelistEntry::Identifier("bitcoin-core".into())],
1093 read_only: false,
1076 }; 1094 };
1077 1095
1078 let result = validate_announcement(&event, "gitnostr.com", &archive_config); 1096 let result = validate_announcement(&event, "gitnostr.com", &archive_config);
@@ -1094,13 +1112,96 @@ mod tests {
1094 vec!["wss://gitnostr.com"], 1112 vec!["wss://gitnostr.com"],
1095 ); 1113 );
1096 1114
1097 // Even with archive config, GRASP-01 Accept takes precedence 1115 // With archive_read_only=false, GRASP-01 Accept takes precedence
1098 let archive_config = ArchiveConfig { 1116 let archive_config = ArchiveConfig {
1099 archive_all: true, 1117 archive_all: true,
1100 whitelist: Vec::new(), 1118 whitelist: Vec::new(),
1119 read_only: false,
1101 }; 1120 };
1102 1121
1103 let result = validate_announcement(&event, "gitnostr.com", &archive_config); 1122 let result = validate_announcement(&event, "gitnostr.com", &archive_config);
1104 assert!(matches!(result, AnnouncementResult::Accept)); 1123 assert!(matches!(result, AnnouncementResult::Accept));
1105 } 1124 }
1125
1126 #[test]
1127 fn test_archive_read_only_rejects_non_whitelisted() {
1128 use crate::config::{ArchiveConfig, ArchiveWhitelistEntry};
1129 use crate::nostr::policy::AnnouncementResult;
1130
1131 let keys = create_test_keys();
1132
1133 // Create announcement that DOES list our service
1134 let event = create_announcement_event(
1135 &keys,
1136 "test-repo",
1137 vec!["https://gitnostr.com/alice/test-repo.git"],
1138 vec!["wss://gitnostr.com"],
1139 );
1140
1141 // With archive_read_only=true and whitelist that doesn't include this repo,
1142 // should reject even though it lists our service
1143 let archive_config = ArchiveConfig {
1144 archive_all: false,
1145 whitelist: vec![ArchiveWhitelistEntry::Identifier("bitcoin-core".into())],
1146 read_only: true,
1147 };
1148
1149 let result = validate_announcement(&event, "gitnostr.com", &archive_config);
1150 assert!(matches!(result, AnnouncementResult::Reject(_)));
1151 }
1152
1153 #[test]
1154 fn test_archive_read_only_accepts_whitelisted() {
1155 use crate::config::{ArchiveConfig, ArchiveWhitelistEntry};
1156 use crate::nostr::policy::AnnouncementResult;
1157
1158 let keys = create_test_keys();
1159 let npub = keys.public_key().to_bech32().unwrap();
1160
1161 // Create announcement that lists our service
1162 let event = create_announcement_event(
1163 &keys,
1164 "test-repo",
1165 vec!["https://gitnostr.com/alice/test-repo.git"],
1166 vec!["wss://gitnostr.com"],
1167 );
1168
1169 // With archive_read_only=true and whitelist that DOES include this repo,
1170 // should accept as AcceptArchive
1171 let archive_config = ArchiveConfig {
1172 archive_all: false,
1173 whitelist: vec![ArchiveWhitelistEntry::Pubkey(npub)],
1174 read_only: true,
1175 };
1176
1177 let result = validate_announcement(&event, "gitnostr.com", &archive_config);
1178 assert!(matches!(result, AnnouncementResult::AcceptArchive));
1179 }
1180
1181 #[test]
1182 fn test_archive_read_only_with_archive_all() {
1183 use crate::config::ArchiveConfig;
1184 use crate::nostr::policy::AnnouncementResult;
1185
1186 let keys = create_test_keys();
1187
1188 // Create announcement that lists our service
1189 let event = create_announcement_event(
1190 &keys,
1191 "any-repo",
1192 vec!["https://gitnostr.com/alice/any-repo.git"],
1193 vec!["wss://gitnostr.com"],
1194 );
1195
1196 // With archive_read_only=true and archive_all=true,
1197 // should accept as AcceptArchive
1198 let archive_config = ArchiveConfig {
1199 archive_all: true,
1200 whitelist: Vec::new(),
1201 read_only: true,
1202 };
1203
1204 let result = validate_announcement(&event, "gitnostr.com", &archive_config);
1205 assert!(matches!(result, AnnouncementResult::AcceptArchive));
1206 }
1106} 1207}