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.
This commit is contained in:
DanConwayDev
2026-01-12 20:30:13 +00:00
parent c29191b1e1
commit a12927181c
9 changed files with 429 additions and 24 deletions
+14 -1
View File
@@ -189,4 +189,17 @@
# NGIT_ARCHIVE_WHITELIST=npub1alice...
# NGIT_ARCHIVE_WHITELIST=npub1alice...,npub1bob.../linux
# NGIT_ARCHIVE_WHITELIST=bitcoin-core,linux,rust
# NGIT_ARCHIVE_WHITELIST=
# NGIT_ARCHIVE_WHITELIST=
# Archive read-only mode (relay is read-only sync of archived repositories)
# When true:
# - NIP-11 includes GRASP-05 in supported_grasps
# - NIP-11 curation field describes archive scope
# - Repository announcements not listing this service are accepted per whitelist/archive-all
# When false:
# - Archive mode disabled (standard GRASP-01 operation)
#
# CLI: --archive-read-only
# Default: true if NGIT_ARCHIVE_ALL or NGIT_ARCHIVE_WHITELIST is set, false otherwise
# Note: Setting to true without archive config causes startup error
# NGIT_ARCHIVE_READ_ONLY=
+2 -2
View File
@@ -141,11 +141,11 @@ See [GRASP-02 Proactive Sync](docs/explanation/grasp-02-proactive-sync.md) for f
- ✅ Accept repositories not listing this instance via configurable whitelist
- ✅ Three whitelist formats: `<npub>`, `<npub>/<identifier>`, `<identifier>`
- ✅ Read-only mirroring with full GRASP-02 sync (git data + Nostr events)
- ✅ Read-only mirroring with full GRASP-02 sync (git data + Nostr events) - **default behavior**
- ✅ Archive-all mode for complete ecosystem mirrors
- ✅ Fail-fast npub validation at startup
**Archive mode enables backup/mirror operation** - accept repository announcements that don't list your relay, useful for creating archives of critical projects or running comprehensive mirrors. Archived repositories are read-only with full event and git data sync.
**Archive mode enables backup/mirror operation** - accept repository announcements that don't list your relay, useful for creating archives of critical projects or running comprehensive mirrors. Archived repositories are read-only by default (`NGIT_ARCHIVE_READ_ONLY=true`) with full event and git data sync.
**See**: [GRASP-05 Archive Mode](docs/explanation/grasp-05-archive.md)
+20 -5
View File
@@ -35,14 +35,17 @@ Archive mode relaxes the "must list service" requirement for whitelisted reposit
**Configuration:**
```bash
# Specific repos (safest)
# Specific repos (safest) - read-only by default
NGIT_ARCHIVE_WHITELIST=npub1torvalds.../linux,npub1satoshi.../bitcoin
# NGIT_ARCHIVE_READ_ONLY defaults to true
# All repos from trusted maintainers
NGIT_ARCHIVE_WHITELIST=npub1alice...,npub1bob...
# NGIT_ARCHIVE_READ_ONLY defaults to true
# Archive everything (⚠️ storage risk)
NGIT_ARCHIVE_ALL=true
# NGIT_ARCHIVE_READ_ONLY defaults to true
```
### Validation Priority
@@ -63,11 +66,21 @@ Archived repos use the same directory structure as hosted repos:
<git_data_path>/
npub1alice.../
hosted-repo.git/ # Lists your service (writable)
archived-repo.git/ # Whitelisted (read-only)
archived-repo.git/ # Whitelisted (read-only by default)
```
**No flags or metadata** - archive status determined dynamically from config + announcement contents.
### Read-Only Mode
By default, archive mode operates in read-only mode (`NGIT_ARCHIVE_READ_ONLY=true`):
- Repository announcements are accepted per whitelist/archive-all configuration
- The service is **not listed** in accepted announcements (passive sync only)
- NIP-11 document advertises `GRASP-05` support
- NIP-11 `curation` field indicates read-only sync scope:
- `"Read-only sync of all repositories found on network"` (if `NGIT_ARCHIVE_ALL=true`)
- `"Read-only sync of whitelisted repositories and maintainers"` (if whitelist configured)
### Full Sync
Archived repositories trigger complete GRASP-02 sync:
@@ -129,12 +142,14 @@ Watch for:
## Comparison: Hosted vs Archived
| Aspect | Hosted (GRASP-01) | Archived (GRASP-05) |
|--------|-------------------|---------------------|
| Aspect | Hosted (GRASP-01) | Archived (GRASP-05 Read-Only) |
|--------|-------------------|-------------------------------|
| Announcement must list you | ✅ Required | ❌ Whitelisted instead |
| Git pushes | ✅ Accepted | ❌ Rejected (read-only) |
| GRASP-02 sync | ✅ Full sync | ✅ Full sync |
| Relay discovery | ✅ Listed | ❌ Not listed |
| Relay discovery | ✅ Listed in announcements | ❌ Not listed (passive sync) |
| NIP-11 supported_grasps | `["GRASP-01", "GRASP-02"]` | `["GRASP-01", "GRASP-05", "GRASP-02"]` |
| NIP-11 curation field | `null` | Describes archive scope |
| Use case | Hosting workspace | Backup/mirror |
## Related Documentation
+71
View File
@@ -574,6 +574,77 @@ NGIT_ARCHIVE_WHITELIST=npub1alice23...,npub1bob23.../linux,bitcoin-core
---
#### `NGIT_ARCHIVE_READ_ONLY`
**Description:** Configure relay as read-only sync of archived repositories
**Type:** Boolean
**Default:** `true` if `NGIT_ARCHIVE_ALL` or `NGIT_ARCHIVE_WHITELIST` is set, `false` otherwise
**Required:** No
**Examples:**
```bash
# Explicitly enable (requires archive mode)
NGIT_ARCHIVE_READ_ONLY=true
# Explicitly disable (writable archive repos)
NGIT_ARCHIVE_READ_ONLY=false
# Automatic (default behavior)
# - If NGIT_ARCHIVE_ALL or NGIT_ARCHIVE_WHITELIST is set → true
# - Otherwise → false
# NGIT_ARCHIVE_READ_ONLY=
```
**Behavior:**
- When `true`:
- NIP-11 document includes `GRASP-05` in `supported_grasps`
- NIP-11 `curation` field describes the archive scope
- Repository announcements not listing this service are accepted per whitelist/archive-all
- When `false`:
- Archive mode disabled (standard GRASP-01 operation)
- When unset (default):
- Automatically `true` if archive mode configured
- Automatically `false` otherwise
**Error Conditions:**
```bash
# ERROR: Cannot set read-only without archive config
NGIT_ARCHIVE_READ_ONLY=true
NGIT_ARCHIVE_ALL=false
NGIT_ARCHIVE_WHITELIST=
# → Server fails to start: "NGIT_ARCHIVE_READ_ONLY=true requires either
# NGIT_ARCHIVE_ALL=true or NGIT_ARCHIVE_WHITELIST to be set"
```
**NIP-11 Impact:**
When `NGIT_ARCHIVE_READ_ONLY=true`:
- `supported_grasps`: includes `"GRASP-05"`
- `curation`: Set to one of:
- `"Read-only sync of all repositories found on network"` (if `NGIT_ARCHIVE_ALL=true`)
- `"Read-only sync of whitelisted repositories and maintainers"` (if `NGIT_ARCHIVE_WHITELIST` set)
**Use Cases:**
```bash
# Public archive of entire ecosystem
NGIT_ARCHIVE_ALL=true
NGIT_ARCHIVE_READ_ONLY=true # Default
# Selective backup of critical projects
NGIT_ARCHIVE_WHITELIST=npub1torvalds.../linux,npub1satoshi.../bitcoin
NGIT_ARCHIVE_READ_ONLY=true # Default
# Writable mirror (advanced, not typical)
NGIT_ARCHIVE_WHITELIST=npub1alice...
NGIT_ARCHIVE_READ_ONLY=false
```
---
### Logging Configuration
#### `RUST_LOG`
+16
View File
@@ -196,6 +196,20 @@ let
'';
};
archiveReadOnly = mkOption {
type = types.nullOr types.bool;
default = null;
description = ''
Archive read-only mode (relay is read-only sync of archived repositories).
When true:
- NIP-11 includes GRASP-05 in supported_grasps
- NIP-11 curation field describes archive scope
- Repository announcements not listing this service are accepted per whitelist/archive-all
Default: true if archiveAll or archiveWhitelist is set, false otherwise
Note: Setting to true without archive config causes startup error
'';
};
user = mkOption {
type = types.str;
default = "ngit-grasp-${name}";
@@ -241,6 +255,8 @@ let
RUST_LOG = cfg.logLevel;
} // optionalAttrs (cfg.relayName != null) {
NGIT_RELAY_NAME = cfg.relayName;
} // optionalAttrs (cfg.archiveReadOnly != null) {
NGIT_ARCHIVE_READ_ONLY = toString cfg.archiveReadOnly;
} // optionalAttrs cfg.metricsEnabled { NGIT_METRICS_ENABLED = "true"; }
// optionalAttrs (cfg.syncBootstrapRelayUrl != null) {
NGIT_SYNC_BOOTSTRAP_RELAY_URL = cfg.syncBootstrapRelayUrl;
+106 -1
View File
@@ -98,6 +98,13 @@ pub struct ArchiveConfig {
///
/// If empty and archive_all is false, GRASP-05 is disabled (GRASP-01 strict mode).
pub whitelist: Vec<ArchiveWhitelistEntry>,
/// Read-only archive mode: relay is a read-only sync of archived repositories
///
/// When true, the relay ONLY accepts announcements matching the archive whitelist/all.
/// Announcements listing the relay but not in the whitelist are rejected.
/// When false, the relay operates in GRASP-01 mode for unwhitelisted repos.
pub read_only: bool,
}
impl ArchiveConfig {
@@ -141,6 +148,7 @@ impl Default for ArchiveConfig {
Self {
archive_all: false,
whitelist: Vec::new(),
read_only: false,
}
}
}
@@ -311,6 +319,12 @@ pub struct Config {
/// Formats: "npub1...", "npub1.../identifier", "identifier"
#[arg(long, env = "NGIT_ARCHIVE_WHITELIST", default_value = "")]
pub archive_whitelist: String,
/// Archive read-only mode: relay is a read-only sync of archived repositories
/// Defaults to true if archive_all or archive_whitelist is set, false otherwise
/// Throws error if set to true without archive_all or archive_whitelist
#[arg(long, env = "NGIT_ARCHIVE_READ_ONLY")]
pub archive_read_only: Option<bool>,
}
impl Config {
@@ -411,12 +425,34 @@ impl Config {
}
}
/// Get parsed archive configuration
/// Get parsed archive configuration with computed read-only mode
///
/// Read-only mode defaults to true if archive mode is enabled, false otherwise.
/// Throws error if explicitly set to true without archive mode enabled.
pub fn archive_config(&self) -> Result<ArchiveConfig> {
let whitelist = ArchiveConfig::parse_whitelist(&self.archive_whitelist)?;
let archive_enabled = self.archive_all || !whitelist.is_empty();
let read_only = match self.archive_read_only {
Some(true) => {
if !archive_enabled {
return Err(anyhow!(
"NGIT_ARCHIVE_READ_ONLY=true requires either NGIT_ARCHIVE_ALL=true or NGIT_ARCHIVE_WHITELIST to be set"
));
}
true
}
Some(false) => false,
None => {
// Default: true if archive mode enabled, false otherwise
archive_enabled
}
};
Ok(ArchiveConfig {
archive_all: self.archive_all,
whitelist,
read_only,
})
}
@@ -452,6 +488,7 @@ impl Config {
naughty_list_expiration_hours: 12,
archive_all: false,
archive_whitelist: String::new(),
archive_read_only: None,
}
}
}
@@ -664,12 +701,14 @@ mod tests {
let config = ArchiveConfig {
archive_all: true,
whitelist: Vec::new(),
read_only: true,
};
assert!(config.enabled());
let config = ArchiveConfig {
archive_all: false,
whitelist: vec![ArchiveWhitelistEntry::Identifier("test".into())],
read_only: true,
};
assert!(config.enabled());
}
@@ -684,6 +723,7 @@ mod tests {
ArchiveWhitelistEntry::Pubkey(test_npub.clone()),
ArchiveWhitelistEntry::Identifier("bitcoin-core".into()),
],
read_only: false,
};
assert!(config.matches(&test_npub, "any-repo"));
@@ -696,6 +736,7 @@ mod tests {
let config = ArchiveConfig {
archive_all: true,
whitelist: Vec::new(),
read_only: true,
};
assert!(config.matches("npub1alice", "any-repo"));
@@ -745,4 +786,68 @@ mod tests {
};
assert!(config.archive_config().is_err());
}
#[test]
fn test_archive_read_only_defaults() {
// Default: false when no archive mode
let config = Config::for_testing();
assert_eq!(config.archive_config().unwrap().read_only, false);
// Default: true when archive_all is set
let config = Config {
archive_all: true,
..Config::for_testing()
};
assert_eq!(config.archive_config().unwrap().read_only, true);
// Default: true when archive_whitelist is set
let keys = Keys::generate();
let test_npub = keys.public_key().to_bech32().unwrap();
let config = Config {
archive_whitelist: test_npub,
..Config::for_testing()
};
assert_eq!(config.archive_config().unwrap().read_only, true);
}
#[test]
fn test_archive_read_only_explicit() {
// Explicit true with archive_all
let config = Config {
archive_all: true,
archive_read_only: Some(true),
..Config::for_testing()
};
assert_eq!(config.archive_config().unwrap().read_only, true);
// Explicit false with archive_all (unusual but allowed)
let config = Config {
archive_all: true,
archive_read_only: Some(false),
..Config::for_testing()
};
assert_eq!(config.archive_config().unwrap().read_only, false);
// Explicit false without archive mode
let config = Config {
archive_read_only: Some(false),
..Config::for_testing()
};
assert_eq!(config.archive_config().unwrap().read_only, false);
}
#[test]
fn test_archive_read_only_error() {
// Error: true without archive mode
let config = Config {
archive_read_only: Some(true),
..Config::for_testing()
};
assert!(config.archive_config().is_err());
assert!(config
.archive_config()
.unwrap_err()
.to_string()
.contains("requires either"));
}
}
+85 -2
View File
@@ -56,6 +56,41 @@ pub struct RelayInformationDocument {
impl RelayInformationDocument {
/// Create NIP-11 relay information document from configuration
pub fn from_config(config: &Config) -> Self {
// Determine if archive mode is enabled
let archive_config = config.archive_config().ok();
let archive_enabled = archive_config
.as_ref()
.map(|ac| ac.enabled())
.unwrap_or(false);
let archive_read_only = archive_config
.as_ref()
.map(|ac| ac.read_only)
.unwrap_or(false);
// Build supported_grasps list
let mut supported_grasps = vec!["GRASP-01".to_string()];
if archive_enabled {
supported_grasps.push("GRASP-05".to_string());
}
supported_grasps.push("GRASP-02".to_string());
// Build curation field for archive read-only mode
let curation = if archive_read_only {
if let Some(ref ac) = archive_config {
if ac.archive_all {
Some("Read-only sync of all repositories found on network".to_string())
} else if !ac.whitelist.is_empty() {
Some("Read-only sync of whitelisted repositories and maintainers".to_string())
} else {
None
}
} else {
None
}
} else {
None
};
Self {
name: config.relay_name(),
description: config.relay_description.clone(),
@@ -75,9 +110,9 @@ impl RelayInformationDocument {
icon: Some(format!("https://{}/icon.png", config.domain)),
// GRASP Extensions
supported_grasps: vec!["GRASP-01".to_string(), "GRASP-02".to_string()],
supported_grasps,
repo_acceptance_criteria: "None".to_string(),
curation: None, // Not a curated relay - only SPAM prevention via GRASP-01 policy
curation,
}
}
@@ -90,6 +125,7 @@ impl RelayInformationDocument {
#[cfg(test)]
mod tests {
use super::*;
use nostr_sdk::nips::nip19::ToBech32;
#[test]
fn test_relay_information_document_structure() {
@@ -112,6 +148,7 @@ mod tests {
assert!(doc.supported_nips.contains(&11));
assert!(doc.supported_nips.contains(&34));
assert!(doc.supported_nips.contains(&77));
// Without archive mode, only GRASP-01 and GRASP-02
assert_eq!(doc.supported_grasps, vec!["GRASP-01", "GRASP-02"]);
assert!(doc.repo_acceptance_criteria.contains("None"));
assert!(doc.curation.is_none());
@@ -147,4 +184,50 @@ mod tests {
assert_eq!(parsed["supported_grasps"][1], "GRASP-02");
assert_eq!(parsed["icon"], "https://relay.example.com/icon.png");
}
#[test]
fn test_nip11_with_archive_mode() {
let mut config = Config::for_testing();
config.domain = "relay.example.com".to_string();
config.relay_name_override = Some("Archive Relay".to_string());
config.archive_all = true;
config.archive_read_only = Some(true);
let doc = RelayInformationDocument::from_config(&config);
// Archive mode enabled: should include GRASP-05
assert_eq!(
doc.supported_grasps,
vec!["GRASP-01", "GRASP-05", "GRASP-02"]
);
// Archive read-only: should have curation field
assert!(doc.curation.is_some());
assert!(doc
.curation
.unwrap()
.contains("Read-only sync of all repositories"));
}
#[test]
fn test_nip11_with_whitelist_archive() {
let keys = nostr_sdk::Keys::generate();
let test_npub = keys.public_key().to_bech32().unwrap();
let mut config = Config::for_testing();
config.domain = "relay.example.com".to_string();
config.archive_whitelist = format!("{},bitcoin-core", test_npub);
let doc = RelayInformationDocument::from_config(&config);
// Archive whitelist enabled: should include GRASP-05
assert_eq!(
doc.supported_grasps,
vec!["GRASP-01", "GRASP-05", "GRASP-02"]
);
// Archive read-only defaults to true: should have curation field
assert!(doc.curation.is_some());
assert!(doc
.curation
.unwrap()
.contains("Read-only sync of whitelisted"));
}
}
+3 -2
View File
@@ -575,9 +575,10 @@ pub async fn create_relay(
if archive_config.enabled() {
tracing::info!(
"GRASP-05 archive mode enabled: archive_all={}, whitelist_entries={}",
"GRASP-05 archive mode enabled: archive_all={}, whitelist_entries={}, read_only={}",
archive_config.archive_all,
archive_config.whitelist.len()
archive_config.whitelist.len(),
archive_config.read_only
);
}
+112 -11
View File
@@ -362,10 +362,14 @@ impl RepositoryState {
/// Validate a repository announcement according to GRASP-01 and GRASP-05
///
/// Returns:
/// - Accept: Announcement lists our service (GRASP-01)
/// - Accept: Announcement lists our service (GRASP-01) - unless archive_read_only mode
/// - AcceptArchive: Announcement matches archive config (GRASP-05)
/// - Reject: Validation failed
///
/// When archive_read_only is true:
/// - ONLY accept announcements matching archive whitelist/all
/// - REJECT announcements listing our service but not in whitelist (read-only sync mode)
///
/// Note: AcceptMaintainer is NOT returned here (requires database access)
pub fn validate_announcement(
event: &Event,
@@ -394,23 +398,32 @@ pub fn validate_announcement(
Err(e) => return AnnouncementResult::Reject(format!("Invalid announcement: {}", e)),
};
// GRASP-01: Check if announcement lists our service
if announcement.lists_service(domain) {
// GRASP-01: Normal mode - accept if announcement lists our service
if announcement.lists_service(domain) && !archive_config.read_only {
return AnnouncementResult::Accept;
}
// GRASP-05: Check if announcement matches archive configuration
let npub = announcement.owner_npub();
// GRASP-05: Archive mode - accept if announcement matches whitelist
if archive_config.matches(&npub, &announcement.identifier) {
return AnnouncementResult::AcceptArchive;
}
// Reject: Doesn't list us and not whitelisted
AnnouncementResult::Reject(format!(
"Announcement must list service in both 'clone' and 'relays' tags, or match archive whitelist. \
Found clone URLs: {:?}, relays: {:?}",
announcement.clone_urls, announcement.relays
))
// Reject with appropriate error message
if archive_config.read_only {
AnnouncementResult::Reject(format!(
"Archive read-only mode: announcement must match archive whitelist. \
Repository {}/{} not in whitelist",
npub, announcement.identifier
))
} else {
AnnouncementResult::Reject(format!(
"Announcement must list service in both 'clone' and 'relays' tags, or match archive whitelist. \
Found clone URLs: {:?}, relays: {:?}",
announcement.clone_urls, announcement.relays
))
}
}
/// Validate a repository state announcement according to GRASP-01
@@ -969,6 +982,7 @@ mod tests {
let archive_config = ArchiveConfig {
archive_all: false,
whitelist: vec![ArchiveWhitelistEntry::Pubkey(npub)],
read_only: false,
};
let result = validate_announcement(&event, "gitnostr.com", &archive_config);
@@ -994,6 +1008,7 @@ mod tests {
let archive_config = ArchiveConfig {
archive_all: false,
whitelist: vec![ArchiveWhitelistEntry::Identifier("bitcoin-core".into())],
read_only: false,
};
let result = validate_announcement(&event, "gitnostr.com", &archive_config);
@@ -1023,6 +1038,7 @@ mod tests {
npub,
identifier: "linux".into(),
}],
read_only: false,
};
let result = validate_announcement(&event, "gitnostr.com", &archive_config);
@@ -1048,6 +1064,7 @@ mod tests {
let archive_config = ArchiveConfig {
archive_all: true,
whitelist: Vec::new(),
read_only: false,
};
let result = validate_announcement(&event, "gitnostr.com", &archive_config);
@@ -1073,6 +1090,7 @@ mod tests {
let archive_config = ArchiveConfig {
archive_all: false,
whitelist: vec![ArchiveWhitelistEntry::Identifier("bitcoin-core".into())],
read_only: false,
};
let result = validate_announcement(&event, "gitnostr.com", &archive_config);
@@ -1094,13 +1112,96 @@ mod tests {
vec!["wss://gitnostr.com"],
);
// Even with archive config, GRASP-01 Accept takes precedence
// With archive_read_only=false, GRASP-01 Accept takes precedence
let archive_config = ArchiveConfig {
archive_all: true,
whitelist: Vec::new(),
read_only: false,
};
let result = validate_announcement(&event, "gitnostr.com", &archive_config);
assert!(matches!(result, AnnouncementResult::Accept));
}
#[test]
fn test_archive_read_only_rejects_non_whitelisted() {
use crate::config::{ArchiveConfig, ArchiveWhitelistEntry};
use crate::nostr::policy::AnnouncementResult;
let keys = create_test_keys();
// Create announcement that DOES list our service
let event = create_announcement_event(
&keys,
"test-repo",
vec!["https://gitnostr.com/alice/test-repo.git"],
vec!["wss://gitnostr.com"],
);
// With archive_read_only=true and whitelist that doesn't include this repo,
// should reject even though it lists our service
let archive_config = ArchiveConfig {
archive_all: false,
whitelist: vec![ArchiveWhitelistEntry::Identifier("bitcoin-core".into())],
read_only: true,
};
let result = validate_announcement(&event, "gitnostr.com", &archive_config);
assert!(matches!(result, AnnouncementResult::Reject(_)));
}
#[test]
fn test_archive_read_only_accepts_whitelisted() {
use crate::config::{ArchiveConfig, ArchiveWhitelistEntry};
use crate::nostr::policy::AnnouncementResult;
let keys = create_test_keys();
let npub = keys.public_key().to_bech32().unwrap();
// Create announcement that lists our service
let event = create_announcement_event(
&keys,
"test-repo",
vec!["https://gitnostr.com/alice/test-repo.git"],
vec!["wss://gitnostr.com"],
);
// With archive_read_only=true and whitelist that DOES include this repo,
// should accept as AcceptArchive
let archive_config = ArchiveConfig {
archive_all: false,
whitelist: vec![ArchiveWhitelistEntry::Pubkey(npub)],
read_only: true,
};
let result = validate_announcement(&event, "gitnostr.com", &archive_config);
assert!(matches!(result, AnnouncementResult::AcceptArchive));
}
#[test]
fn test_archive_read_only_with_archive_all() {
use crate::config::ArchiveConfig;
use crate::nostr::policy::AnnouncementResult;
let keys = create_test_keys();
// Create announcement that lists our service
let event = create_announcement_event(
&keys,
"any-repo",
vec!["https://gitnostr.com/alice/any-repo.git"],
vec!["wss://gitnostr.com"],
);
// With archive_read_only=true and archive_all=true,
// should accept as AcceptArchive
let archive_config = ArchiveConfig {
archive_all: true,
whitelist: Vec::new(),
read_only: true,
};
let result = validate_announcement(&event, "gitnostr.com", &archive_config);
assert!(matches!(result, AnnouncementResult::AcceptArchive));
}
}