25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 1 | use std::borrow::Cow; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 2 | use std::collections::{HashMap, HashSet}; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 3 | use std::net::{IpAddr, SocketAddr}; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 4 | use std::sync::{Arc, Mutex, PoisonError}; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 5 | use std::time::{Duration, Instant}; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 6 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 7 | use nostr::event::{Event, EventId, Kind}; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 8 | use nostr::filter::Filter; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 9 | use nostr::key::PublicKey; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 10 | use nostr::message::MachineReadablePrefix; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 11 | use nostr::nips::nip65::extract_relay_list; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 12 | use nostr::types::Timestamp; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 13 | use nostr_database::{DatabaseEventStatus, NostrDatabase, error::Error}; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 14 | use serde::Deserialize; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 15 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 16 | use crate::config::{Config, Limits}; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 17 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 18 | #[derive(Debug)] |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 19 | struct Usage { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 20 | window: Instant, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 21 | events: u32, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 22 | queries: u32, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 23 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 24 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 25 | #[derive(Debug, Clone)] |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 26 | pub struct Policy { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 27 | database: Arc<dyn NostrDatabase>, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 28 | limits: Limits, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 29 | forge_keys: HashSet<PublicKey>, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 30 | peers: Arc<Mutex<HashMap<IpAddr, Usage>>>, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 31 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 32 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 33 | #[derive(Deserialize)] |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 34 | #[serde(deny_unknown_fields)] |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 35 | struct Binding { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 36 | version: u64, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 37 | action: String, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 38 | fingerprint: String, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 39 | certificate: Option<String>, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 40 | signature: Option<String>, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 41 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 42 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 43 | pub(crate) enum WritePolicyResult { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 44 | Accept, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 45 | Respond { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 46 | prefix: MachineReadablePrefix, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 47 | message: Cow<'static, str>, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 48 | status: bool, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 49 | }, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 50 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 51 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 52 | impl WritePolicyResult { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 53 | const fn accepted(prefix: MachineReadablePrefix, message: &'static str) -> Self { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 54 | Self::Respond { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 55 | prefix, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 56 | message: Cow::Borrowed(message), |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 57 | status: true, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 58 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 59 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 60 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 61 | const fn reject(prefix: MachineReadablePrefix, message: &'static str) -> Self { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 62 | Self::Respond { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 63 | prefix, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 64 | message: Cow::Borrowed(message), |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 65 | status: false, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 66 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 67 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 68 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 69 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 70 | pub(crate) enum QueryPolicyResult { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 71 | Accept, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 72 | Reject { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 73 | prefix: MachineReadablePrefix, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 74 | message: Cow<'static, str>, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 75 | }, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 76 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 77 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 78 | impl QueryPolicyResult { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 79 | const fn reject(prefix: MachineReadablePrefix, message: &'static str) -> Self { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 80 | Self::Reject { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 81 | prefix, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 82 | message: Cow::Borrowed(message), |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 83 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 84 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 85 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 86 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 87 | impl Policy { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 88 | pub fn new(database: Arc<dyn NostrDatabase>, config: &Config) -> anyhow::Result<Self> { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 89 | config.validate()?; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 90 | Ok(Self { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 91 | database, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 92 | limits: config.limits.clone(), |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 93 | forge_keys: config.forge_keys()?, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 94 | peers: Arc::new(Mutex::new(HashMap::new())), |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 95 | }) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 96 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 97 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 98 | fn charge(&self, ip: IpAddr, publish: bool) -> bool { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 99 | let mut peers = self.peers.lock().unwrap_or_else(PoisonError::into_inner); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 100 | peers.retain(|_, usage| usage.window.elapsed() < Duration::from_secs(60)); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 101 | if peers.len() >= 4096 && !peers.contains_key(&ip) { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 102 | return false; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 103 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 104 | let usage = peers.entry(ip).or_insert_with(|| Usage { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 105 | window: Instant::now(), |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 106 | events: 0, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 107 | queries: 0, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 108 | }); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 109 | let (count, limit) = if publish { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 110 | (&mut usage.events, self.limits.events_per_minute) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 111 | } else { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 112 | (&mut usage.queries, self.limits.queries_per_minute) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 113 | }; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 114 | if *count >= limit { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 115 | return false; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 116 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 117 | *count += 1; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 118 | drop(peers); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 119 | true |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 120 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 121 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 122 | pub(crate) async fn admit_event( |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 123 | &self, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 124 | event: &Event, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 125 | address: &SocketAddr, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 126 | ) -> WritePolicyResult { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 127 | if !self.charge(address.ip(), true) { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 128 | return WritePolicyResult::reject( |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 129 | MachineReadablePrefix::RateLimited, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 130 | "publish rate exceeded", |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 131 | ); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 132 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 133 | self.admit(event).await.unwrap_or_else(|error| { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 134 | tracing::error!(%error, "event admission database failure"); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 135 | WritePolicyResult::reject(MachineReadablePrefix::Error, "database unavailable") |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 136 | }) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 137 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 138 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 139 | pub(crate) async fn admit_peer(&self, event: &Event) -> WritePolicyResult { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 140 | match self.admit(event).await { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 141 | Ok(result) => result, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 142 | Err(error) => { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 143 | tracing::error!(%error, "event admission database failure"); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 144 | WritePolicyResult::reject(MachineReadablePrefix::Error, "database unavailable") |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 145 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 146 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 147 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 148 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 149 | pub(crate) async fn admit_query( |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 150 | &self, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 151 | filter: &Filter, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 152 | address: &SocketAddr, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 153 | ) -> QueryPolicyResult { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 154 | if !self.charge(address.ip(), false) { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 155 | return QueryPolicyResult::reject( |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 156 | MachineReadablePrefix::RateLimited, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 157 | "query rate exceeded", |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 158 | ); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 159 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 160 | if filter.as_json().len() > self.limits.max_filter_bytes { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 161 | return QueryPolicyResult::reject( |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 162 | MachineReadablePrefix::RateLimited, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 163 | "filter byte limit exceeded", |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 164 | ); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 165 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 166 | if filter.search.is_some() { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 167 | return QueryPolicyResult::reject( |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 168 | MachineReadablePrefix::Unsupported, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 169 | "search is not supported", |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 170 | ); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 171 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 172 | if let Some(limit) = filter.limit { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 173 | return if limit > self.limits.max_query_results { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 174 | QueryPolicyResult::reject( |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 175 | MachineReadablePrefix::RateLimited, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 176 | "query result limit exceeded", |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 177 | ) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 178 | } else { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 179 | QueryPolicyResult::Accept |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 180 | }; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 181 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 182 | match self |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 183 | .database |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 184 | .count(filter.clone().limit(self.limits.max_query_results + 1)) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 185 | .await |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 186 | { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 187 | Ok(count) if count <= self.limits.max_query_results => QueryPolicyResult::Accept, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 188 | Ok(_) => QueryPolicyResult::reject( |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 189 | MachineReadablePrefix::RateLimited, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 190 | "history result limit exceeded", |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 191 | ), |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 192 | Err(error) => { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 193 | tracing::error!(%error, "query admission database failure"); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 194 | QueryPolicyResult::reject(MachineReadablePrefix::Error, "database unavailable") |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 195 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 196 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 197 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 198 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 199 | async fn admit(&self, event: &Event) -> Result<WritePolicyResult, Error> { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 200 | if let Some(result) = self.check_event(event) { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 201 | return Ok(result); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 202 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 203 | match self.database.check_id(&event.id).await? { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 204 | DatabaseEventStatus::Saved => { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 205 | return Ok(WritePolicyResult::accepted( |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 206 | MachineReadablePrefix::Duplicate, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 207 | "already stored", |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 208 | )); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 209 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 210 | DatabaseEventStatus::Deleted => return Ok(blocked("event is deleted")), |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 211 | DatabaseEventStatus::NotExistent => {} |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 212 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 213 | self.admit_kind(event).await |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 214 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 215 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 216 | fn check_event(&self, event: &Event) -> Option<WritePolicyResult> { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 217 | if event.as_json().len() > self.limits.max_event_bytes |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 218 | || event.tags.len() > self.limits.max_tags |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 219 | { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 220 | return Some(blocked("event exceeds size or tag limit")); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 221 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 222 | if event.created_at.as_secs() == 0 |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 223 | || event.created_at.as_secs() > Timestamp::now().as_secs().saturating_add(900) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 224 | { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 225 | return Some(blocked("event timestamp is outside the supported range")); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 226 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 227 | if event.is_expired() || event.is_protected() { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 228 | return Some(blocked("expired or protected events are not accepted")); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 229 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 230 | None |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 231 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 232 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 233 | async fn admit_kind(&self, event: &Event) -> Result<WritePolicyResult, Error> { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 234 | match event.kind.as_u16() { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 235 | 10002 => { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 236 | if !self.forge_keys.contains(&event.pubkey) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 237 | && !self.registered_forge(event.pubkey).await? |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 238 | { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 239 | return Ok(blocked("relay list signer has no registered repository")); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 240 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 241 | let relay_tags = event |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 242 | .tags |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 243 | .iter() |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 244 | .filter(|tag| tag.as_slice().first().is_some_and(|name| name == "r")) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 245 | .count(); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 246 | if relay_tags > 16 || extract_relay_list(event).count() != relay_tags { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 247 | return Ok(blocked("relay list is invalid or too large")); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 248 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 249 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 250 | 30617 => { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 251 | let Some(identifier) = identifier(event) else { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 252 | return Ok(blocked("one valid repository identifier is required")); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 253 | }; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 254 | if !self.registered(event.pubkey, identifier).await? { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 255 | let count = self |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 256 | .database |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 257 | .query( |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 258 | Filter::new() |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 259 | .kind(Kind::from(30617)) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 260 | .limit(self.limits.max_repositories + 1), |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 261 | ) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 262 | .await? |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 263 | .len(); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 264 | if count >= self.limits.max_repositories { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 265 | return Ok(WritePolicyResult::reject( |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 266 | MachineReadablePrefix::RateLimited, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 267 | "repository directory is full", |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 268 | )); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 269 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 270 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 271 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 272 | 30618 => { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 273 | let Some(identifier) = identifier(event) else { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 274 | return Ok(blocked("one valid repository identifier is required")); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 275 | }; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 276 | if !self.registered(event.pubkey, identifier).await? { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 277 | return Ok(blocked("repository is not registered")); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 278 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 279 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 280 | 1617 | 1621 | 1630..=1632 => { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 281 | let mut addresses = 0_usize; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 282 | for tag in event |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 283 | .tags |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 284 | .iter() |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 285 | .filter(|tag| tag.as_slice().first().is_some_and(|name| name == "a")) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 286 | { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 287 | let Some((kind, author, identifier)) = |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 288 | tag.as_slice().get(1).and_then(|value| coordinate(value)) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 289 | else { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 290 | return Ok(blocked("invalid repository address")); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 291 | }; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 292 | if kind != 30617 || !self.registered(author, identifier).await? { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 293 | return Ok(blocked("repository is not registered")); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 294 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 295 | if matches!(event.kind.as_u16(), 1630..=1632) && author != event.pubkey { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 296 | return Ok(blocked("status signer does not own the repository")); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 297 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 298 | addresses += 1; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 299 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 300 | if addresses != 1 { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 301 | return Ok(blocked("exactly one repository address is required")); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 302 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 303 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 304 | 30382 => { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 305 | if !valid_binding(event) { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 306 | return Ok(blocked("invalid openpgp binding envelope")); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 307 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 308 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 309 | 5 => return self.deletion(event).await, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 310 | _ => { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 311 | return Ok(WritePolicyResult::reject( |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 312 | MachineReadablePrefix::Unsupported, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 313 | "event kind is not supported", |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 314 | )); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 315 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 316 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 317 | Ok(WritePolicyResult::Accept) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 318 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 319 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 320 | async fn registered(&self, author: PublicKey, identifier: &str) -> Result<bool, Error> { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 321 | Ok(self |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 322 | .database |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 323 | .query( |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 324 | Filter::new() |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 325 | .kind(Kind::from(30617)) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 326 | .author(author) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 327 | .identifier(identifier) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 328 | .limit(1), |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 329 | ) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 330 | .await? |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 331 | .iter() |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 332 | .any(|event| !event.is_expired())) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 333 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 334 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 335 | async fn registered_forge(&self, author: PublicKey) -> Result<bool, Error> { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 336 | Ok(self |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 337 | .database |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 338 | .query( |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 339 | Filter::new() |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 340 | .kind(Kind::from(30617)) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 341 | .author(author) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 342 | .limit(1), |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 343 | ) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 344 | .await? |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 345 | .iter() |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 346 | .any(|event| !event.is_expired())) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 347 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 348 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 349 | async fn deletion(&self, event: &Event) -> Result<WritePolicyResult, Error> { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 350 | let mut targets = 0_usize; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 351 | for tag in event.tags.iter() { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 352 | let values = tag.as_slice(); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 353 | match values.first().map(String::as_str) { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 354 | Some("e") => { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 355 | let Some(id) = values |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 356 | .get(1) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 357 | .filter(|id| id.len() == 64) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 358 | .and_then(|id| id.parse::<EventId>().ok()) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 359 | else { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 360 | return Ok(blocked("invalid deletion target")); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 361 | }; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 362 | if let Some(target) = self.database.event_by_id(&id).await? { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 363 | if target.pubkey != event.pubkey || target.kind == Kind::EventDeletion { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 364 | return Ok(blocked("deletion target is not removable by this author")); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 365 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 366 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 367 | targets += 1; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 368 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 369 | Some("a") => { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 370 | let Some((kind, author, _)) = values.get(1).and_then(|value| coordinate(value)) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 371 | else { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 372 | return Ok(blocked("invalid deletion address")); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 373 | }; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 374 | if author != event.pubkey || !matches!(kind, 30617 | 30618 | 30382) { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 375 | return Ok(blocked("deletion address is not owned by this author")); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 376 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 377 | targets += 1; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 378 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 379 | _ => {} |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 380 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 381 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 382 | Ok(if targets == 0 { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 383 | blocked("deletion target is required") |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 384 | } else { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 385 | WritePolicyResult::Accept |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 386 | }) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 387 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 388 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 389 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 390 | const fn blocked(message: &'static str) -> WritePolicyResult { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 391 | WritePolicyResult::reject(MachineReadablePrefix::Blocked, message) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 392 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 393 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 394 | fn valid_identifier(identifier: &str) -> bool { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 395 | !identifier.is_empty() && identifier.len() <= 512 && !identifier.chars().any(char::is_control) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 396 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 397 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 398 | fn identifier(event: &Event) -> Option<&str> { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 399 | let mut tags = event |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 400 | .tags |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 401 | .iter() |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 402 | .filter(|tag| tag.as_slice().first().is_some_and(|name| name == "d")); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 403 | let tag = tags.next()?.as_slice(); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 404 | if tags.next().is_some() || tag.len() != 2 || !valid_identifier(&tag[1]) { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 405 | return None; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 406 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 407 | Some(&tag[1]) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 408 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 409 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 410 | pub(crate) fn coordinate(value: &str) -> Option<(u16, PublicKey, &str)> { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 411 | let mut parts = value.splitn(3, ':'); |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 412 | let kind = parts.next()?.parse().ok()?; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 413 | let author_hex = parts.next()?; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 414 | if author_hex.len() != 64 || !author_hex.bytes().all(|byte| byte.is_ascii_hexdigit()) { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 415 | return None; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 416 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 417 | let author = author_hex.parse().ok()?; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 418 | let identifier = parts.next()?; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 419 | valid_identifier(identifier).then_some((kind, author, identifier)) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 420 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 421 | |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 422 | fn valid_binding(event: &Event) -> bool { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 423 | if event.content.len() > 192 * 1024 { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 424 | return false; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 425 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 426 | let Ok(body) = serde_json::from_str::<Binding>(&event.content) else { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 427 | return false; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 428 | }; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 429 | if body.version != 1 |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 430 | || !matches!(body.fingerprint.len(), 40 | 64) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 431 | || !body |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 432 | .fingerprint |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 433 | .bytes() |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 434 | .all(|byte| byte.is_ascii_hexdigit()) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 435 | || identifier(event) != Some(body.fingerprint.as_str()) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 436 | { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 437 | return false; |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 438 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 439 | match body.action.as_str() { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 440 | "bind" => { |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 441 | body.certificate |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 442 | .is_some_and(|certificate| !certificate.trim().is_empty()) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 443 | && body |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 444 | .signature |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 445 | .is_some_and(|signature| !signature.trim().is_empty()) |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 446 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 447 | "revoke" => body.certificate.is_none() && body.signature.is_none(), |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 448 | _ => false, |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 449 | } |
25d14e49ab68 | atagen | 2026-09-24 11:07 UTC | 450 | } |