diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2026-01-12 20:30:13 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2026-01-12 20:30:13 +0000 |
| commit | a12927181c571fc1641772ad44dd4c6a4ab209d9 (patch) | |
| tree | d7cb99fa87606e9fb13d91305cda8a0f919e6528 /src/http | |
| parent | c29191b1e1239e931c575a926ec9480e594476d6 (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/http')
| -rw-r--r-- | src/http/nip11.rs | 87 |
1 files changed, 85 insertions, 2 deletions
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 { | |||
| 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 | ||
| 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)] |
| 91 | mod tests { | 126 | mod 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 | } |