diff options
| author | DanConwayDev <DanConwayDev@protonmail.com> | 2026-01-12 21:06:39 +0000 |
|---|---|---|
| committer | DanConwayDev <DanConwayDev@protonmail.com> | 2026-01-12 21:21:52 +0000 |
| commit | 82b56c37b26a2fac1a294873e539b19b9325dca6 (patch) | |
| tree | 07800949230f13f91fec2eebbd94b8fbb00dd83f /src/config.rs | |
| parent | a12927181c571fc1641772ad44dd4c6a4ab209d9 (diff) | |
feat(config): add repository whitelist for curated GRASP-01 acceptance
Adds NGIT_REPOSITORY_WHITELIST option for curated relay operation that
accepts only whitelisted repositories while maintaining GRASP-01 compliance
(announcements must list the service). This differs from archive whitelist
which enables GRASP-05 mode and doesn't require service listing.
Key features:
- Supports three whitelist formats: npub, npub/identifier, identifier
- Enforces mutual exclusivity with archive read-only mode
- Updates NIP-11 curation field when whitelist is enabled
- Maintains GRASP-01 compliance (doesn't add GRASP-05 support)
Configuration synced across all four sources: src/config.rs, docs/reference/configuration.md,
nix/module.nix, and .env.example as required by AGENTS.md.
Diffstat (limited to 'src/config.rs')
| -rw-r--r-- | src/config.rs | 210 |
1 files changed, 170 insertions, 40 deletions
diff --git a/src/config.rs b/src/config.rs index d9917a3..8a9eb4d 100644 --- a/src/config.rs +++ b/src/config.rs | |||
| @@ -5,21 +5,21 @@ use serde::{Deserialize, Serialize}; | |||
| 5 | use std::fs; | 5 | use std::fs; |
| 6 | use std::path::PathBuf; | 6 | use std::path::PathBuf; |
| 7 | 7 | ||
| 8 | /// GRASP-05 Archive whitelist entry | 8 | /// Whitelist entry for repository/archive filtering |
| 9 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] | 9 | #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)] |
| 10 | #[serde(rename_all = "lowercase")] | 10 | #[serde(rename_all = "lowercase")] |
| 11 | pub enum ArchiveWhitelistEntry { | 11 | pub enum WhitelistEntry { |
| 12 | /// Archive all repos from this pubkey: "npub1..." | 12 | /// All repos from this pubkey: "npub1..." |
| 13 | Pubkey(String), | 13 | Pubkey(String), |
| 14 | 14 | ||
| 15 | /// Archive specific repo: "npub1.../identifier" | 15 | /// Specific repo: "npub1.../identifier" |
| 16 | Repository { npub: String, identifier: String }, | 16 | Repository { npub: String, identifier: String }, |
| 17 | 17 | ||
| 18 | /// Archive any repo with this identifier: "identifier" | 18 | /// Any repo with this identifier: "identifier" |
| 19 | Identifier(String), | 19 | Identifier(String), |
| 20 | } | 20 | } |
| 21 | 21 | ||
| 22 | impl ArchiveWhitelistEntry { | 22 | impl WhitelistEntry { |
| 23 | /// Parse a whitelist entry from string | 23 | /// Parse a whitelist entry from string |
| 24 | /// | 24 | /// |
| 25 | /// Formats: | 25 | /// Formats: |
| @@ -83,6 +83,20 @@ impl ArchiveWhitelistEntry { | |||
| 83 | Self::Identifier(i) => identifier == i, | 83 | Self::Identifier(i) => identifier == i, |
| 84 | } | 84 | } |
| 85 | } | 85 | } |
| 86 | |||
| 87 | /// Parse whitelist from comma-separated string | ||
| 88 | pub fn parse_whitelist(input: &str) -> Result<Vec<Self>> { | ||
| 89 | if input.trim().is_empty() { | ||
| 90 | return Ok(Vec::new()); | ||
| 91 | } | ||
| 92 | |||
| 93 | input | ||
| 94 | .split(',') | ||
| 95 | .map(|s| s.trim()) | ||
| 96 | .filter(|s| !s.is_empty()) | ||
| 97 | .map(Self::parse) | ||
| 98 | .collect() | ||
| 99 | } | ||
| 86 | } | 100 | } |
| 87 | 101 | ||
| 88 | /// GRASP-05 Archive mode configuration | 102 | /// GRASP-05 Archive mode configuration |
| @@ -97,7 +111,7 @@ pub struct ArchiveConfig { | |||
| 97 | /// Whitelist entries for selective archiving | 111 | /// Whitelist entries for selective archiving |
| 98 | /// | 112 | /// |
| 99 | /// If empty and archive_all is false, GRASP-05 is disabled (GRASP-01 strict mode). | 113 | /// If empty and archive_all is false, GRASP-05 is disabled (GRASP-01 strict mode). |
| 100 | pub whitelist: Vec<ArchiveWhitelistEntry>, | 114 | pub whitelist: Vec<WhitelistEntry>, |
| 101 | 115 | ||
| 102 | /// Read-only archive mode: relay is a read-only sync of archived repositories | 116 | /// Read-only archive mode: relay is a read-only sync of archived repositories |
| 103 | /// | 117 | /// |
| @@ -127,28 +141,47 @@ impl ArchiveConfig { | |||
| 127 | .iter() | 141 | .iter() |
| 128 | .any(|entry| entry.matches(npub, identifier)) | 142 | .any(|entry| entry.matches(npub, identifier)) |
| 129 | } | 143 | } |
| 144 | } | ||
| 130 | 145 | ||
| 131 | /// Parse archive whitelist from comma-separated string | 146 | impl Default for ArchiveConfig { |
| 132 | pub fn parse_whitelist(input: &str) -> Result<Vec<ArchiveWhitelistEntry>> { | 147 | fn default() -> Self { |
| 133 | if input.trim().is_empty() { | 148 | Self { |
| 134 | return Ok(Vec::new()); | 149 | archive_all: false, |
| 150 | whitelist: Vec::new(), | ||
| 151 | read_only: false, | ||
| 135 | } | 152 | } |
| 153 | } | ||
| 154 | } | ||
| 136 | 155 | ||
| 137 | input | 156 | /// Repository whitelist configuration |
| 138 | .split(',') | 157 | #[derive(Debug, Clone, Serialize, Deserialize)] |
| 139 | .map(|s| s.trim()) | 158 | pub struct RepositoryConfig { |
| 140 | .filter(|s| !s.is_empty()) | 159 | /// Whitelist entries for selective repository acceptance |
| 141 | .map(ArchiveWhitelistEntry::parse) | 160 | /// |
| 142 | .collect() | 161 | /// If empty, all repositories listing the service are accepted (GRASP-01 mode). |
| 162 | pub whitelist: Vec<WhitelistEntry>, | ||
| 163 | } | ||
| 164 | |||
| 165 | impl RepositoryConfig { | ||
| 166 | /// Check if repository whitelist is enabled (non-empty whitelist) | ||
| 167 | pub fn enabled(&self) -> bool { | ||
| 168 | !self.whitelist.is_empty() | ||
| 169 | } | ||
| 170 | |||
| 171 | /// Check if an announcement matches the repository whitelist | ||
| 172 | /// | ||
| 173 | /// Returns true if announcement matches any whitelist entry | ||
| 174 | pub fn matches(&self, npub: &str, identifier: &str) -> bool { | ||
| 175 | self.whitelist | ||
| 176 | .iter() | ||
| 177 | .any(|entry| entry.matches(npub, identifier)) | ||
| 143 | } | 178 | } |
| 144 | } | 179 | } |
| 145 | 180 | ||
| 146 | impl Default for ArchiveConfig { | 181 | impl Default for RepositoryConfig { |
| 147 | fn default() -> Self { | 182 | fn default() -> Self { |
| 148 | Self { | 183 | Self { |
| 149 | archive_all: false, | ||
| 150 | whitelist: Vec::new(), | 184 | whitelist: Vec::new(), |
| 151 | read_only: false, | ||
| 152 | } | 185 | } |
| 153 | } | 186 | } |
| 154 | } | 187 | } |
| @@ -325,6 +358,12 @@ pub struct Config { | |||
| 325 | /// Throws error if set to true without archive_all or archive_whitelist | 358 | /// Throws error if set to true without archive_all or archive_whitelist |
| 326 | #[arg(long, env = "NGIT_ARCHIVE_READ_ONLY")] | 359 | #[arg(long, env = "NGIT_ARCHIVE_READ_ONLY")] |
| 327 | pub archive_read_only: Option<bool>, | 360 | pub archive_read_only: Option<bool>, |
| 361 | |||
| 362 | /// Repository whitelist: comma-separated list of npub/identifier/npub/identifier entries | ||
| 363 | /// Formats: "npub1...", "npub1.../identifier", "identifier" | ||
| 364 | /// When set, only announcements matching the whitelist AND listing the service are accepted | ||
| 365 | #[arg(long, env = "NGIT_REPOSITORY_WHITELIST", default_value = "")] | ||
| 366 | pub repository_whitelist: String, | ||
| 328 | } | 367 | } |
| 329 | 368 | ||
| 330 | impl Config { | 369 | impl Config { |
| @@ -430,7 +469,7 @@ impl Config { | |||
| 430 | /// Read-only mode defaults to true if archive mode is enabled, false otherwise. | 469 | /// 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. | 470 | /// Throws error if explicitly set to true without archive mode enabled. |
| 432 | pub fn archive_config(&self) -> Result<ArchiveConfig> { | 471 | pub fn archive_config(&self) -> Result<ArchiveConfig> { |
| 433 | let whitelist = ArchiveConfig::parse_whitelist(&self.archive_whitelist)?; | 472 | let whitelist = WhitelistEntry::parse_whitelist(&self.archive_whitelist)?; |
| 434 | let archive_enabled = self.archive_all || !whitelist.is_empty(); | 473 | let archive_enabled = self.archive_all || !whitelist.is_empty(); |
| 435 | 474 | ||
| 436 | let read_only = match self.archive_read_only { | 475 | let read_only = match self.archive_read_only { |
| @@ -456,6 +495,28 @@ impl Config { | |||
| 456 | }) | 495 | }) |
| 457 | } | 496 | } |
| 458 | 497 | ||
| 498 | /// Get parsed repository whitelist configuration | ||
| 499 | /// | ||
| 500 | /// Throws error if repository_whitelist is set together with archive_read_only=true | ||
| 501 | pub fn repository_config(&self) -> Result<RepositoryConfig> { | ||
| 502 | let whitelist = WhitelistEntry::parse_whitelist(&self.repository_whitelist)?; | ||
| 503 | |||
| 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 | } | ||
| 519 | |||
| 459 | /// Create config for testing | 520 | /// Create config for testing |
| 460 | #[cfg(test)] | 521 | #[cfg(test)] |
| 461 | pub fn for_testing() -> Self { | 522 | pub fn for_testing() -> Self { |
| @@ -489,6 +550,7 @@ impl Config { | |||
| 489 | archive_all: false, | 550 | archive_all: false, |
| 490 | archive_whitelist: String::new(), | 551 | archive_whitelist: String::new(), |
| 491 | archive_read_only: None, | 552 | archive_read_only: None, |
| 553 | repository_whitelist: String::new(), | ||
| 492 | } | 554 | } |
| 493 | } | 555 | } |
| 494 | } | 556 | } |
| @@ -629,9 +691,9 @@ mod tests { | |||
| 629 | // Generate a valid test npub | 691 | // Generate a valid test npub |
| 630 | let keys = Keys::generate(); | 692 | let keys = Keys::generate(); |
| 631 | let test_npub = keys.public_key().to_bech32().unwrap(); | 693 | let test_npub = keys.public_key().to_bech32().unwrap(); |
| 632 | let entry = ArchiveWhitelistEntry::parse(&test_npub).unwrap(); | 694 | let entry = WhitelistEntry::parse(&test_npub).unwrap(); |
| 633 | assert!(matches!(entry, ArchiveWhitelistEntry::Pubkey(_))); | 695 | assert!(matches!(entry, WhitelistEntry::Pubkey(_))); |
| 634 | if let ArchiveWhitelistEntry::Pubkey(npub) = entry { | 696 | if let WhitelistEntry::Pubkey(npub) = entry { |
| 635 | assert_eq!(npub, test_npub); | 697 | assert_eq!(npub, test_npub); |
| 636 | } | 698 | } |
| 637 | } | 699 | } |
| @@ -640,9 +702,9 @@ mod tests { | |||
| 640 | fn test_parse_whitelist_entry_repository() { | 702 | fn test_parse_whitelist_entry_repository() { |
| 641 | let keys = Keys::generate(); | 703 | let keys = Keys::generate(); |
| 642 | let test_npub = keys.public_key().to_bech32().unwrap(); | 704 | let test_npub = keys.public_key().to_bech32().unwrap(); |
| 643 | let entry = ArchiveWhitelistEntry::parse(&format!("{}/linux", test_npub)).unwrap(); | 705 | let entry = WhitelistEntry::parse(&format!("{}/linux", test_npub)).unwrap(); |
| 644 | assert!(matches!(entry, ArchiveWhitelistEntry::Repository { .. })); | 706 | assert!(matches!(entry, WhitelistEntry::Repository { .. })); |
| 645 | if let ArchiveWhitelistEntry::Repository { npub, identifier } = entry { | 707 | if let WhitelistEntry::Repository { npub, identifier } = entry { |
| 646 | assert_eq!(npub, test_npub); | 708 | assert_eq!(npub, test_npub); |
| 647 | assert_eq!(identifier, "linux"); | 709 | assert_eq!(identifier, "linux"); |
| 648 | } | 710 | } |
| @@ -650,16 +712,16 @@ mod tests { | |||
| 650 | 712 | ||
| 651 | #[test] | 713 | #[test] |
| 652 | fn test_parse_whitelist_entry_identifier() { | 714 | fn test_parse_whitelist_entry_identifier() { |
| 653 | let entry = ArchiveWhitelistEntry::parse("bitcoin-core").unwrap(); | 715 | let entry = WhitelistEntry::parse("bitcoin-core").unwrap(); |
| 654 | assert!(matches!(entry, ArchiveWhitelistEntry::Identifier(_))); | 716 | assert!(matches!(entry, WhitelistEntry::Identifier(_))); |
| 655 | if let ArchiveWhitelistEntry::Identifier(id) = entry { | 717 | if let WhitelistEntry::Identifier(id) = entry { |
| 656 | assert_eq!(id, "bitcoin-core"); | 718 | assert_eq!(id, "bitcoin-core"); |
| 657 | } | 719 | } |
| 658 | } | 720 | } |
| 659 | 721 | ||
| 660 | #[test] | 722 | #[test] |
| 661 | fn test_parse_whitelist_entry_invalid_npub() { | 723 | fn test_parse_whitelist_entry_invalid_npub() { |
| 662 | let result = ArchiveWhitelistEntry::parse("npub1invalid"); | 724 | let result = WhitelistEntry::parse("npub1invalid"); |
| 663 | assert!(result.is_err()); | 725 | assert!(result.is_err()); |
| 664 | } | 726 | } |
| 665 | 727 | ||
| @@ -667,7 +729,7 @@ mod tests { | |||
| 667 | fn test_whitelist_entry_matches() { | 729 | fn test_whitelist_entry_matches() { |
| 668 | let keys = Keys::generate(); | 730 | let keys = Keys::generate(); |
| 669 | let test_npub = keys.public_key().to_bech32().unwrap(); | 731 | let test_npub = keys.public_key().to_bech32().unwrap(); |
| 670 | let entry = ArchiveWhitelistEntry::Pubkey(test_npub.clone()); | 732 | let entry = WhitelistEntry::Pubkey(test_npub.clone()); |
| 671 | assert!(entry.matches(&test_npub, "any-identifier")); | 733 | assert!(entry.matches(&test_npub, "any-identifier")); |
| 672 | assert!(!entry.matches("npub1different", "any-identifier")); | 734 | assert!(!entry.matches("npub1different", "any-identifier")); |
| 673 | } | 735 | } |
| @@ -676,7 +738,7 @@ mod tests { | |||
| 676 | fn test_whitelist_entry_matches_repository() { | 738 | fn test_whitelist_entry_matches_repository() { |
| 677 | let keys = Keys::generate(); | 739 | let keys = Keys::generate(); |
| 678 | let test_npub = keys.public_key().to_bech32().unwrap(); | 740 | let test_npub = keys.public_key().to_bech32().unwrap(); |
| 679 | let entry = ArchiveWhitelistEntry::Repository { | 741 | let entry = WhitelistEntry::Repository { |
| 680 | npub: test_npub.clone(), | 742 | npub: test_npub.clone(), |
| 681 | identifier: "linux".to_string(), | 743 | identifier: "linux".to_string(), |
| 682 | }; | 744 | }; |
| @@ -687,7 +749,7 @@ mod tests { | |||
| 687 | 749 | ||
| 688 | #[test] | 750 | #[test] |
| 689 | fn test_whitelist_entry_matches_identifier() { | 751 | fn test_whitelist_entry_matches_identifier() { |
| 690 | let entry = ArchiveWhitelistEntry::Identifier("bitcoin-core".to_string()); | 752 | let entry = WhitelistEntry::Identifier("bitcoin-core".to_string()); |
| 691 | assert!(entry.matches("npub1alice", "bitcoin-core")); | 753 | assert!(entry.matches("npub1alice", "bitcoin-core")); |
| 692 | assert!(entry.matches("npub1bob", "bitcoin-core")); | 754 | assert!(entry.matches("npub1bob", "bitcoin-core")); |
| 693 | assert!(!entry.matches("npub1alice", "other-repo")); | 755 | assert!(!entry.matches("npub1alice", "other-repo")); |
| @@ -707,7 +769,7 @@ mod tests { | |||
| 707 | 769 | ||
| 708 | let config = ArchiveConfig { | 770 | let config = ArchiveConfig { |
| 709 | archive_all: false, | 771 | archive_all: false, |
| 710 | whitelist: vec![ArchiveWhitelistEntry::Identifier("test".into())], | 772 | whitelist: vec![WhitelistEntry::Identifier("test".into())], |
| 711 | read_only: true, | 773 | read_only: true, |
| 712 | }; | 774 | }; |
| 713 | assert!(config.enabled()); | 775 | assert!(config.enabled()); |
| @@ -720,8 +782,8 @@ mod tests { | |||
| 720 | let config = ArchiveConfig { | 782 | let config = ArchiveConfig { |
| 721 | archive_all: false, | 783 | archive_all: false, |
| 722 | whitelist: vec![ | 784 | whitelist: vec![ |
| 723 | ArchiveWhitelistEntry::Pubkey(test_npub.clone()), | 785 | WhitelistEntry::Pubkey(test_npub.clone()), |
| 724 | ArchiveWhitelistEntry::Identifier("bitcoin-core".into()), | 786 | WhitelistEntry::Identifier("bitcoin-core".into()), |
| 725 | ], | 787 | ], |
| 726 | read_only: false, | 788 | read_only: false, |
| 727 | }; | 789 | }; |
| @@ -745,10 +807,10 @@ mod tests { | |||
| 745 | 807 | ||
| 746 | #[test] | 808 | #[test] |
| 747 | fn test_parse_whitelist_empty() { | 809 | fn test_parse_whitelist_empty() { |
| 748 | let whitelist = ArchiveConfig::parse_whitelist("").unwrap(); | 810 | let whitelist = WhitelistEntry::parse_whitelist("").unwrap(); |
| 749 | assert!(whitelist.is_empty()); | 811 | assert!(whitelist.is_empty()); |
| 750 | 812 | ||
| 751 | let whitelist = ArchiveConfig::parse_whitelist(" ").unwrap(); | 813 | let whitelist = WhitelistEntry::parse_whitelist(" ").unwrap(); |
| 752 | assert!(whitelist.is_empty()); | 814 | assert!(whitelist.is_empty()); |
| 753 | } | 815 | } |
| 754 | 816 | ||
| @@ -758,7 +820,7 @@ mod tests { | |||
| 758 | let keys2 = Keys::generate(); | 820 | let keys2 = Keys::generate(); |
| 759 | let test_npub1 = keys1.public_key().to_bech32().unwrap(); | 821 | let test_npub1 = keys1.public_key().to_bech32().unwrap(); |
| 760 | let test_npub2 = keys2.public_key().to_bech32().unwrap(); | 822 | let test_npub2 = keys2.public_key().to_bech32().unwrap(); |
| 761 | let whitelist = ArchiveConfig::parse_whitelist(&format!( | 823 | let whitelist = WhitelistEntry::parse_whitelist(&format!( |
| 762 | "{},bitcoin-core,{}/linux", | 824 | "{},bitcoin-core,{}/linux", |
| 763 | test_npub1, test_npub2 | 825 | test_npub1, test_npub2 |
| 764 | )) | 826 | )) |
| @@ -850,4 +912,72 @@ mod tests { | |||
| 850 | .to_string() | 912 | .to_string() |
| 851 | .contains("requires either")); | 913 | .contains("requires either")); |
| 852 | } | 914 | } |
| 915 | |||
| 916 | #[test] | ||
| 917 | fn test_repository_whitelist_parsing() { | ||
| 918 | let keys = Keys::generate(); | ||
| 919 | let test_npub = keys.public_key().to_bech32().unwrap(); | ||
| 920 | let config = Config { | ||
| 921 | repository_whitelist: format!("{},bitcoin-core", test_npub), | ||
| 922 | ..Config::for_testing() | ||
| 923 | }; | ||
| 924 | let repo_config = config.repository_config().unwrap(); | ||
| 925 | assert_eq!(repo_config.whitelist.len(), 2); | ||
| 926 | assert!(repo_config.enabled()); | ||
| 927 | } | ||
| 928 | |||
| 929 | #[test] | ||
| 930 | fn test_repository_whitelist_empty() { | ||
| 931 | let config = Config::for_testing(); | ||
| 932 | let repo_config = config.repository_config().unwrap(); | ||
| 933 | assert!(repo_config.whitelist.is_empty()); | ||
| 934 | assert!(!repo_config.enabled()); | ||
| 935 | } | ||
| 936 | |||
| 937 | #[test] | ||
| 938 | fn test_repository_whitelist_incompatible_with_archive_read_only() { | ||
| 939 | let keys = Keys::generate(); | ||
| 940 | let test_npub = keys.public_key().to_bech32().unwrap(); | ||
| 941 | let config = Config { | ||
| 942 | archive_all: true, | ||
| 943 | archive_read_only: Some(true), | ||
| 944 | repository_whitelist: test_npub, | ||
| 945 | ..Config::for_testing() | ||
| 946 | }; | ||
| 947 | let result = config.repository_config(); | ||
| 948 | assert!(result.is_err()); | ||
| 949 | let err = result.unwrap_err().to_string(); | ||
| 950 | assert!(err.contains("cannot be used with")); | ||
| 951 | assert!(err.contains("NGIT_ARCHIVE_READ_ONLY=true")); | ||
| 952 | } | ||
| 953 | |||
| 954 | #[test] | ||
| 955 | fn test_repository_whitelist_compatible_with_archive_read_only_false() { | ||
| 956 | let keys = Keys::generate(); | ||
| 957 | let test_npub = keys.public_key().to_bech32().unwrap(); | ||
| 958 | let config = Config { | ||
| 959 | archive_all: true, | ||
| 960 | archive_read_only: Some(false), | ||
| 961 | repository_whitelist: test_npub, | ||
| 962 | ..Config::for_testing() | ||
| 963 | }; | ||
| 964 | // Should not error | ||
| 965 | assert!(config.repository_config().is_ok()); | ||
| 966 | } | ||
| 967 | |||
| 968 | #[test] | ||
| 969 | fn test_repository_config_matches() { | ||
| 970 | let keys = Keys::generate(); | ||
| 971 | let test_npub = keys.public_key().to_bech32().unwrap(); | ||
| 972 | let config = RepositoryConfig { | ||
| 973 | whitelist: vec![ | ||
| 974 | WhitelistEntry::Pubkey(test_npub.clone()), | ||
| 975 | WhitelistEntry::Identifier("bitcoin-core".into()), | ||
| 976 | ], | ||
| 977 | }; | ||
| 978 | |||
| 979 | assert!(config.matches(&test_npub, "any-repo")); | ||
| 980 | assert!(config.matches("npub1bob", "bitcoin-core")); | ||
| 981 | assert!(!config.matches("npub1bob", "other-repo")); | ||
| 982 | } | ||
| 853 | } | 983 | } |