upleb.uk

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

summaryrefslogtreecommitdiff
path: root/src/config.rs
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/config.rs
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/config.rs')
-rw-r--r--src/config.rs107
1 files changed, 106 insertions, 1 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}