atagen

vineyard

public
Star0
Watch0
ssh://brewery@pls.lobotomise.me:33/atagen/vineyard.git

Blame: src/config.rs

25d14e49ab68atagen2026-09-24 11:07 UTC1
use std::collections::HashSet;
25d14e49ab68atagen2026-09-24 11:07 UTC2
use std::fs::read_to_string;
25d14e49ab68atagen2026-09-24 11:07 UTC3
use std::net::SocketAddr;
25d14e49ab68atagen2026-09-24 11:07 UTC4
use std::path::{Path, PathBuf};
25d14e49ab68atagen2026-09-24 11:07 UTC5
25d14e49ab68atagen2026-09-24 11:07 UTC6
use nostr::key::PublicKey;
25d14e49ab68atagen2026-09-24 11:07 UTC7
use serde::Deserialize;
25d14e49ab68atagen2026-09-24 11:07 UTC8
use tokio::sync::Semaphore;
25d14e49ab68atagen2026-09-24 11:07 UTC9
25d14e49ab68atagen2026-09-24 11:07 UTC10
fn default_listen() -> SocketAddr {
25d14e49ab68atagen2026-09-24 11:07 UTC11
    SocketAddr::from(([127, 0, 0, 1], 7777))
25d14e49ab68atagen2026-09-24 11:07 UTC12
}
25d14e49ab68atagen2026-09-24 11:07 UTC13
25d14e49ab68atagen2026-09-24 11:07 UTC14
fn default_database() -> PathBuf {
25d14e49ab68atagen2026-09-24 11:07 UTC15
    PathBuf::from("vineyard.sqlite")
25d14e49ab68atagen2026-09-24 11:07 UTC16
}
25d14e49ab68atagen2026-09-24 11:07 UTC17
25d14e49ab68atagen2026-09-24 11:07 UTC18
const fn default_max_connections() -> usize {
25d14e49ab68atagen2026-09-24 11:07 UTC19
    32
25d14e49ab68atagen2026-09-24 11:07 UTC20
}
25d14e49ab68atagen2026-09-24 11:07 UTC21
25d14e49ab68atagen2026-09-24 11:07 UTC22
const fn default_max_connections_per_ip() -> usize {
25d14e49ab68atagen2026-09-24 11:07 UTC23
    8
25d14e49ab68atagen2026-09-24 11:07 UTC24
}
25d14e49ab68atagen2026-09-24 11:07 UTC25
25d14e49ab68atagen2026-09-24 11:07 UTC26
const fn default_max_subscriptions() -> usize {
25d14e49ab68atagen2026-09-24 11:07 UTC27
    16
25d14e49ab68atagen2026-09-24 11:07 UTC28
}
25d14e49ab68atagen2026-09-24 11:07 UTC29
25d14e49ab68atagen2026-09-24 11:07 UTC30
const fn default_max_event_bytes() -> usize {
25d14e49ab68atagen2026-09-24 11:07 UTC31
    2_096_896
25d14e49ab68atagen2026-09-24 11:07 UTC32
}
25d14e49ab68atagen2026-09-24 11:07 UTC33
25d14e49ab68atagen2026-09-24 11:07 UTC34
const fn default_max_message_bytes() -> usize {
25d14e49ab68atagen2026-09-24 11:07 UTC35
    2_097_152
25d14e49ab68atagen2026-09-24 11:07 UTC36
}
25d14e49ab68atagen2026-09-24 11:07 UTC37
25d14e49ab68atagen2026-09-24 11:07 UTC38
const fn default_max_tags() -> usize {
25d14e49ab68atagen2026-09-24 11:07 UTC39
    128
25d14e49ab68atagen2026-09-24 11:07 UTC40
}
25d14e49ab68atagen2026-09-24 11:07 UTC41
25d14e49ab68atagen2026-09-24 11:07 UTC42
const fn default_max_filters() -> usize {
25d14e49ab68atagen2026-09-24 11:07 UTC43
    8
25d14e49ab68atagen2026-09-24 11:07 UTC44
}
25d14e49ab68atagen2026-09-24 11:07 UTC45
25d14e49ab68atagen2026-09-24 11:07 UTC46
const fn default_max_filter_bytes() -> usize {
25d14e49ab68atagen2026-09-24 11:07 UTC47
    32_768
25d14e49ab68atagen2026-09-24 11:07 UTC48
}
25d14e49ab68atagen2026-09-24 11:07 UTC49
25d14e49ab68atagen2026-09-24 11:07 UTC50
const fn default_max_query_results() -> usize {
25d14e49ab68atagen2026-09-24 11:07 UTC51
    500
25d14e49ab68atagen2026-09-24 11:07 UTC52
}
25d14e49ab68atagen2026-09-24 11:07 UTC53
25d14e49ab68atagen2026-09-24 11:07 UTC54
const fn default_max_repositories() -> usize {
25d14e49ab68atagen2026-09-24 11:07 UTC55
    4096
25d14e49ab68atagen2026-09-24 11:07 UTC56
}
25d14e49ab68atagen2026-09-24 11:07 UTC57
25d14e49ab68atagen2026-09-24 11:07 UTC58
const fn default_max_discovered_peers() -> usize {
25d14e49ab68atagen2026-09-24 11:07 UTC59
    32
25d14e49ab68atagen2026-09-24 11:07 UTC60
}
25d14e49ab68atagen2026-09-24 11:07 UTC61
25d14e49ab68atagen2026-09-24 11:07 UTC62
const fn default_max_query_bytes() -> usize {
25d14e49ab68atagen2026-09-24 11:07 UTC63
    16_777_216
25d14e49ab68atagen2026-09-24 11:07 UTC64
}
25d14e49ab68atagen2026-09-24 11:07 UTC65
25d14e49ab68atagen2026-09-24 11:07 UTC66
const fn default_events_per_minute() -> u32 {
25d14e49ab68atagen2026-09-24 11:07 UTC67
    120
25d14e49ab68atagen2026-09-24 11:07 UTC68
}
25d14e49ab68atagen2026-09-24 11:07 UTC69
25d14e49ab68atagen2026-09-24 11:07 UTC70
const fn default_queries_per_minute() -> u32 {
25d14e49ab68atagen2026-09-24 11:07 UTC71
    120
25d14e49ab68atagen2026-09-24 11:07 UTC72
}
25d14e49ab68atagen2026-09-24 11:07 UTC73
25d14e49ab68atagen2026-09-24 11:07 UTC74
const fn default_max_database_bytes() -> u64 {
25d14e49ab68atagen2026-09-24 11:07 UTC75
    1_073_741_824
25d14e49ab68atagen2026-09-24 11:07 UTC76
}
25d14e49ab68atagen2026-09-24 11:07 UTC77
25d14e49ab68atagen2026-09-24 11:07 UTC78
const fn default_min_free_bytes() -> u64 {
25d14e49ab68atagen2026-09-24 11:07 UTC79
    67_108_864
25d14e49ab68atagen2026-09-24 11:07 UTC80
}
25d14e49ab68atagen2026-09-24 11:07 UTC81
25d14e49ab68atagen2026-09-24 11:07 UTC82
const fn default_write_timeout_seconds() -> u64 {
25d14e49ab68atagen2026-09-24 11:07 UTC83
    10
25d14e49ab68atagen2026-09-24 11:07 UTC84
}
25d14e49ab68atagen2026-09-24 11:07 UTC85
25d14e49ab68atagen2026-09-24 11:07 UTC86
#[derive(Debug, Clone, Deserialize)]
25d14e49ab68atagen2026-09-24 11:07 UTC87
#[serde(deny_unknown_fields)]
25d14e49ab68atagen2026-09-24 11:07 UTC88
pub struct Config {
25d14e49ab68atagen2026-09-24 11:07 UTC89
    #[serde(default = "default_listen")]
25d14e49ab68atagen2026-09-24 11:07 UTC90
    pub listen: SocketAddr,
25d14e49ab68atagen2026-09-24 11:07 UTC91
    pub public_url: String,
25d14e49ab68atagen2026-09-24 11:07 UTC92
    #[serde(default = "default_database")]
25d14e49ab68atagen2026-09-24 11:07 UTC93
    pub database: PathBuf,
25d14e49ab68atagen2026-09-24 11:07 UTC94
    pub forge_pubkeys: Vec<String>,
25d14e49ab68atagen2026-09-24 11:07 UTC95
    #[serde(default)]
25d14e49ab68atagen2026-09-24 11:07 UTC96
    pub peers: Vec<Peer>,
25d14e49ab68atagen2026-09-24 11:07 UTC97
    #[serde(default)]
25d14e49ab68atagen2026-09-24 11:07 UTC98
    pub allow_private_discovery: bool,
25d14e49ab68atagen2026-09-24 11:07 UTC99
    #[serde(default)]
25d14e49ab68atagen2026-09-24 11:07 UTC100
    pub limits: Limits,
25d14e49ab68atagen2026-09-24 11:07 UTC101
}
25d14e49ab68atagen2026-09-24 11:07 UTC102
25d14e49ab68atagen2026-09-24 11:07 UTC103
#[derive(Debug, Clone, Deserialize)]
25d14e49ab68atagen2026-09-24 11:07 UTC104
#[serde(deny_unknown_fields)]
25d14e49ab68atagen2026-09-24 11:07 UTC105
pub struct Peer {
25d14e49ab68atagen2026-09-24 11:07 UTC106
    pub url: String,
25d14e49ab68atagen2026-09-24 11:07 UTC107
    #[serde(default = "default_max_query_results")]
25d14e49ab68atagen2026-09-24 11:07 UTC108
    pub query_limit: usize,
25d14e49ab68atagen2026-09-24 11:07 UTC109
    pub rescan_from: Option<u64>,
25d14e49ab68atagen2026-09-24 11:07 UTC110
}
25d14e49ab68atagen2026-09-24 11:07 UTC111
25d14e49ab68atagen2026-09-24 11:07 UTC112
#[derive(Debug, Clone, Deserialize)]
25d14e49ab68atagen2026-09-24 11:07 UTC113
#[serde(deny_unknown_fields)]
25d14e49ab68atagen2026-09-24 11:07 UTC114
pub struct Limits {
25d14e49ab68atagen2026-09-24 11:07 UTC115
    #[serde(default = "default_max_repositories")]
25d14e49ab68atagen2026-09-24 11:07 UTC116
    pub max_repositories: usize,
25d14e49ab68atagen2026-09-24 11:07 UTC117
    #[serde(default = "default_max_discovered_peers")]
25d14e49ab68atagen2026-09-24 11:07 UTC118
    pub max_discovered_peers: usize,
25d14e49ab68atagen2026-09-24 11:07 UTC119
    #[serde(default = "default_max_connections")]
25d14e49ab68atagen2026-09-24 11:07 UTC120
    pub max_connections: usize,
25d14e49ab68atagen2026-09-24 11:07 UTC121
    #[serde(default = "default_max_connections_per_ip")]
25d14e49ab68atagen2026-09-24 11:07 UTC122
    pub max_connections_per_ip: usize,
25d14e49ab68atagen2026-09-24 11:07 UTC123
    #[serde(default = "default_max_subscriptions")]
25d14e49ab68atagen2026-09-24 11:07 UTC124
    pub max_subscriptions: usize,
25d14e49ab68atagen2026-09-24 11:07 UTC125
    #[serde(default = "default_max_event_bytes")]
25d14e49ab68atagen2026-09-24 11:07 UTC126
    pub max_event_bytes: usize,
25d14e49ab68atagen2026-09-24 11:07 UTC127
    #[serde(default = "default_max_message_bytes")]
25d14e49ab68atagen2026-09-24 11:07 UTC128
    pub max_message_bytes: usize,
25d14e49ab68atagen2026-09-24 11:07 UTC129
    #[serde(default = "default_max_tags")]
25d14e49ab68atagen2026-09-24 11:07 UTC130
    pub max_tags: usize,
25d14e49ab68atagen2026-09-24 11:07 UTC131
    #[serde(default = "default_max_filters")]
25d14e49ab68atagen2026-09-24 11:07 UTC132
    pub max_filters: usize,
25d14e49ab68atagen2026-09-24 11:07 UTC133
    #[serde(default = "default_max_filter_bytes")]
25d14e49ab68atagen2026-09-24 11:07 UTC134
    pub max_filter_bytes: usize,
25d14e49ab68atagen2026-09-24 11:07 UTC135
    #[serde(default = "default_max_query_results")]
25d14e49ab68atagen2026-09-24 11:07 UTC136
    pub max_query_results: usize,
25d14e49ab68atagen2026-09-24 11:07 UTC137
    #[serde(default = "default_max_query_bytes")]
25d14e49ab68atagen2026-09-24 11:07 UTC138
    pub max_query_bytes: usize,
25d14e49ab68atagen2026-09-24 11:07 UTC139
    #[serde(default = "default_events_per_minute")]
25d14e49ab68atagen2026-09-24 11:07 UTC140
    pub events_per_minute: u32,
25d14e49ab68atagen2026-09-24 11:07 UTC141
    #[serde(default = "default_queries_per_minute")]
25d14e49ab68atagen2026-09-24 11:07 UTC142
    pub queries_per_minute: u32,
25d14e49ab68atagen2026-09-24 11:07 UTC143
    #[serde(default = "default_max_database_bytes")]
25d14e49ab68atagen2026-09-24 11:07 UTC144
    pub max_database_bytes: u64,
25d14e49ab68atagen2026-09-24 11:07 UTC145
    #[serde(default = "default_min_free_bytes")]
25d14e49ab68atagen2026-09-24 11:07 UTC146
    pub min_free_bytes: u64,
25d14e49ab68atagen2026-09-24 11:07 UTC147
    #[serde(default = "default_write_timeout_seconds")]
25d14e49ab68atagen2026-09-24 11:07 UTC148
    pub write_timeout_seconds: u64,
25d14e49ab68atagen2026-09-24 11:07 UTC149
}
25d14e49ab68atagen2026-09-24 11:07 UTC150
25d14e49ab68atagen2026-09-24 11:07 UTC151
impl Default for Limits {
25d14e49ab68atagen2026-09-24 11:07 UTC152
    fn default() -> Self {
25d14e49ab68atagen2026-09-24 11:07 UTC153
        Self {
25d14e49ab68atagen2026-09-24 11:07 UTC154
            max_repositories: default_max_repositories(),
25d14e49ab68atagen2026-09-24 11:07 UTC155
            max_discovered_peers: default_max_discovered_peers(),
25d14e49ab68atagen2026-09-24 11:07 UTC156
            max_connections: default_max_connections(),
25d14e49ab68atagen2026-09-24 11:07 UTC157
            max_connections_per_ip: default_max_connections_per_ip(),
25d14e49ab68atagen2026-09-24 11:07 UTC158
            max_subscriptions: default_max_subscriptions(),
25d14e49ab68atagen2026-09-24 11:07 UTC159
            max_event_bytes: default_max_event_bytes(),
25d14e49ab68atagen2026-09-24 11:07 UTC160
            max_message_bytes: default_max_message_bytes(),
25d14e49ab68atagen2026-09-24 11:07 UTC161
            max_tags: default_max_tags(),
25d14e49ab68atagen2026-09-24 11:07 UTC162
            max_filters: default_max_filters(),
25d14e49ab68atagen2026-09-24 11:07 UTC163
            max_filter_bytes: default_max_filter_bytes(),
25d14e49ab68atagen2026-09-24 11:07 UTC164
            max_query_results: default_max_query_results(),
25d14e49ab68atagen2026-09-24 11:07 UTC165
            max_query_bytes: default_max_query_bytes(),
25d14e49ab68atagen2026-09-24 11:07 UTC166
            events_per_minute: default_events_per_minute(),
25d14e49ab68atagen2026-09-24 11:07 UTC167
            queries_per_minute: default_queries_per_minute(),
25d14e49ab68atagen2026-09-24 11:07 UTC168
            max_database_bytes: default_max_database_bytes(),
25d14e49ab68atagen2026-09-24 11:07 UTC169
            min_free_bytes: default_min_free_bytes(),
25d14e49ab68atagen2026-09-24 11:07 UTC170
            write_timeout_seconds: default_write_timeout_seconds(),
25d14e49ab68atagen2026-09-24 11:07 UTC171
        }
25d14e49ab68atagen2026-09-24 11:07 UTC172
    }
25d14e49ab68atagen2026-09-24 11:07 UTC173
}
25d14e49ab68atagen2026-09-24 11:07 UTC174
25d14e49ab68atagen2026-09-24 11:07 UTC175
impl Config {
25d14e49ab68atagen2026-09-24 11:07 UTC176
    pub fn load(path: &Path) -> anyhow::Result<Self> {
25d14e49ab68atagen2026-09-24 11:07 UTC177
        let text = read_to_string(path).map_err(|error| {
25d14e49ab68atagen2026-09-24 11:07 UTC178
            anyhow::anyhow!("failed to read config {}: {error}", path.display())
25d14e49ab68atagen2026-09-24 11:07 UTC179
        })?;
25d14e49ab68atagen2026-09-24 11:07 UTC180
        let mut cfg: Self = toml::from_str(&text).map_err(|error| {
25d14e49ab68atagen2026-09-24 11:07 UTC181
            anyhow::anyhow!("failed to parse config {}: {error}", path.display())
25d14e49ab68atagen2026-09-24 11:07 UTC182
        })?;
25d14e49ab68atagen2026-09-24 11:07 UTC183
        if cfg.database.is_relative() {
25d14e49ab68atagen2026-09-24 11:07 UTC184
            if let Some(parent) = path.parent() {
25d14e49ab68atagen2026-09-24 11:07 UTC185
                if !parent.as_os_str().is_empty() {
25d14e49ab68atagen2026-09-24 11:07 UTC186
                    cfg.database = parent.join(&cfg.database);
25d14e49ab68atagen2026-09-24 11:07 UTC187
                }
25d14e49ab68atagen2026-09-24 11:07 UTC188
            }
25d14e49ab68atagen2026-09-24 11:07 UTC189
        }
25d14e49ab68atagen2026-09-24 11:07 UTC190
        cfg.validate()?;
25d14e49ab68atagen2026-09-24 11:07 UTC191
        Ok(cfg)
25d14e49ab68atagen2026-09-24 11:07 UTC192
    }
25d14e49ab68atagen2026-09-24 11:07 UTC193
25d14e49ab68atagen2026-09-24 11:07 UTC194
    pub fn validate(&self) -> anyhow::Result<()> {
25d14e49ab68atagen2026-09-24 11:07 UTC195
        let url = relay_url(&self.public_url)
25d14e49ab68atagen2026-09-24 11:07 UTC196
            .ok_or_else(|| anyhow::anyhow!("invalid public_url {}", self.public_url))?;
25d14e49ab68atagen2026-09-24 11:07 UTC197
        self.forge_keys()?;
25d14e49ab68atagen2026-09-24 11:07 UTC198
        if self.peers.len() > 16 {
25d14e49ab68atagen2026-09-24 11:07 UTC199
            anyhow::bail!("at most 16 peer relays are supported");
25d14e49ab68atagen2026-09-24 11:07 UTC200
        }
25d14e49ab68atagen2026-09-24 11:07 UTC201
        let mut peers = HashSet::new();
25d14e49ab68atagen2026-09-24 11:07 UTC202
        for peer in &self.peers {
25d14e49ab68atagen2026-09-24 11:07 UTC203
            let peer_url = relay_url(&peer.url).ok_or_else(|| {
25d14e49ab68atagen2026-09-24 11:07 UTC204
                anyhow::anyhow!(
25d14e49ab68atagen2026-09-24 11:07 UTC205
                    "peer urls must be ws or wss without credentials, queries or fragments"
25d14e49ab68atagen2026-09-24 11:07 UTC206
                )
25d14e49ab68atagen2026-09-24 11:07 UTC207
            })?;
25d14e49ab68atagen2026-09-24 11:07 UTC208
            if peer_url == url || !peers.insert(peer_url) {
25d14e49ab68atagen2026-09-24 11:07 UTC209
                anyhow::bail!("peer urls must be unique and distinct from public_url");
25d14e49ab68atagen2026-09-24 11:07 UTC210
            }
25d14e49ab68atagen2026-09-24 11:07 UTC211
            if !(2..10000).contains(&peer.query_limit) {
25d14e49ab68atagen2026-09-24 11:07 UTC212
                anyhow::bail!("peer query_limit must be between 2 and 9999");
25d14e49ab68atagen2026-09-24 11:07 UTC213
            }
25d14e49ab68atagen2026-09-24 11:07 UTC214
        }
25d14e49ab68atagen2026-09-24 11:07 UTC215
        if self.limits.max_connections == 0
25d14e49ab68atagen2026-09-24 11:07 UTC216
            || self.limits.max_connections_per_ip == 0
25d14e49ab68atagen2026-09-24 11:07 UTC217
            || self.limits.max_subscriptions == 0
25d14e49ab68atagen2026-09-24 11:07 UTC218
            || self.limits.max_event_bytes == 0
25d14e49ab68atagen2026-09-24 11:07 UTC219
            || self.limits.max_message_bytes == 0
25d14e49ab68atagen2026-09-24 11:07 UTC220
            || self.limits.max_tags == 0
25d14e49ab68atagen2026-09-24 11:07 UTC221
            || self.limits.max_filters == 0
25d14e49ab68atagen2026-09-24 11:07 UTC222
            || self.limits.max_filter_bytes == 0
25d14e49ab68atagen2026-09-24 11:07 UTC223
            || self.limits.max_query_results == 0
25d14e49ab68atagen2026-09-24 11:07 UTC224
            || self.limits.max_query_bytes == 0
25d14e49ab68atagen2026-09-24 11:07 UTC225
            || self.limits.events_per_minute == 0
25d14e49ab68atagen2026-09-24 11:07 UTC226
            || self.limits.queries_per_minute == 0
25d14e49ab68atagen2026-09-24 11:07 UTC227
            || self.limits.max_database_bytes == 0
25d14e49ab68atagen2026-09-24 11:07 UTC228
            || self.limits.min_free_bytes == 0
25d14e49ab68atagen2026-09-24 11:07 UTC229
            || self.limits.write_timeout_seconds == 0
25d14e49ab68atagen2026-09-24 11:07 UTC230
        {
25d14e49ab68atagen2026-09-24 11:07 UTC231
            anyhow::bail!("limits must all be positive");
25d14e49ab68atagen2026-09-24 11:07 UTC232
        }
25d14e49ab68atagen2026-09-24 11:07 UTC233
        if self.limits.max_connections_per_ip > self.limits.max_connections {
25d14e49ab68atagen2026-09-24 11:07 UTC234
            anyhow::bail!("max_connections_per_ip must not exceed max_connections");
25d14e49ab68atagen2026-09-24 11:07 UTC235
        }
25d14e49ab68atagen2026-09-24 11:07 UTC236
        let min_message = self
25d14e49ab68atagen2026-09-24 11:07 UTC237
            .limits
25d14e49ab68atagen2026-09-24 11:07 UTC238
            .max_event_bytes
25d14e49ab68atagen2026-09-24 11:07 UTC239
            .checked_add(256)
25d14e49ab68atagen2026-09-24 11:07 UTC240
            .ok_or_else(|| anyhow::anyhow!("max_event_bytes too large"))?;
25d14e49ab68atagen2026-09-24 11:07 UTC241
        if min_message > self.limits.max_message_bytes {
25d14e49ab68atagen2026-09-24 11:07 UTC242
            anyhow::bail!("max_message_bytes must fit max_event_bytes plus 256 bytes of overhead");
25d14e49ab68atagen2026-09-24 11:07 UTC243
        }
25d14e49ab68atagen2026-09-24 11:07 UTC244
        if self.limits.max_event_bytes > self.limits.max_query_bytes {
25d14e49ab68atagen2026-09-24 11:07 UTC245
            anyhow::bail!("max_query_bytes must fit one maximum size event");
25d14e49ab68atagen2026-09-24 11:07 UTC246
        }
25d14e49ab68atagen2026-09-24 11:07 UTC247
        if self.limits.max_repositories == 0 || self.limits.max_repositories >= 10000 {
25d14e49ab68atagen2026-09-24 11:07 UTC248
            anyhow::bail!("max_repositories must be between 1 and 9999");
25d14e49ab68atagen2026-09-24 11:07 UTC249
        }
25d14e49ab68atagen2026-09-24 11:07 UTC250
        if self.limits.max_discovered_peers == 0 || self.limits.max_discovered_peers > 256 {
25d14e49ab68atagen2026-09-24 11:07 UTC251
            anyhow::bail!("max_discovered_peers must be between 1 and 256");
25d14e49ab68atagen2026-09-24 11:07 UTC252
        }
25d14e49ab68atagen2026-09-24 11:07 UTC253
        if self.limits.max_query_results >= 10000 {
25d14e49ab68atagen2026-09-24 11:07 UTC254
            anyhow::bail!("max_query_results must be below 10000");
25d14e49ab68atagen2026-09-24 11:07 UTC255
        }
25d14e49ab68atagen2026-09-24 11:07 UTC256
        if self.limits.max_connections > Semaphore::MAX_PERMITS {
25d14e49ab68atagen2026-09-24 11:07 UTC257
            anyhow::bail!("max_connections exceeds the semaphore limit");
25d14e49ab68atagen2026-09-24 11:07 UTC258
        }
25d14e49ab68atagen2026-09-24 11:07 UTC259
        self.limits
25d14e49ab68atagen2026-09-24 11:07 UTC260
            .max_filter_bytes
25d14e49ab68atagen2026-09-24 11:07 UTC261
            .checked_mul(self.limits.max_subscriptions)
25d14e49ab68atagen2026-09-24 11:07 UTC262
            .ok_or_else(|| anyhow::anyhow!("subscription byte budget is too large"))?;
25d14e49ab68atagen2026-09-24 11:07 UTC263
        if self.websocket_path()? == "/healthz" {
25d14e49ab68atagen2026-09-24 11:07 UTC264
            anyhow::bail!("public_url must not use the health endpoint");
25d14e49ab68atagen2026-09-24 11:07 UTC265
        }
25d14e49ab68atagen2026-09-24 11:07 UTC266
        Ok(())
25d14e49ab68atagen2026-09-24 11:07 UTC267
    }
25d14e49ab68atagen2026-09-24 11:07 UTC268
25d14e49ab68atagen2026-09-24 11:07 UTC269
    pub fn forge_keys(&self) -> anyhow::Result<HashSet<PublicKey>> {
25d14e49ab68atagen2026-09-24 11:07 UTC270
        if self.forge_pubkeys.is_empty() {
25d14e49ab68atagen2026-09-24 11:07 UTC271
            anyhow::bail!("forge_pubkeys must not be empty");
25d14e49ab68atagen2026-09-24 11:07 UTC272
        }
25d14e49ab68atagen2026-09-24 11:07 UTC273
        let mut set = HashSet::new();
25d14e49ab68atagen2026-09-24 11:07 UTC274
        for raw in &self.forge_pubkeys {
25d14e49ab68atagen2026-09-24 11:07 UTC275
            let trimmed = raw.trim();
25d14e49ab68atagen2026-09-24 11:07 UTC276
            if trimmed.len() != 64 || !trimmed.bytes().all(|byte| byte.is_ascii_hexdigit()) {
25d14e49ab68atagen2026-09-24 11:07 UTC277
                anyhow::bail!("invalid forge public key: {raw}");
25d14e49ab68atagen2026-09-24 11:07 UTC278
            }
25d14e49ab68atagen2026-09-24 11:07 UTC279
            let key: PublicKey = trimmed
25d14e49ab68atagen2026-09-24 11:07 UTC280
                .parse()
25d14e49ab68atagen2026-09-24 11:07 UTC281
                .map_err(|error| anyhow::anyhow!("invalid forge public key {raw}: {error}"))?;
25d14e49ab68atagen2026-09-24 11:07 UTC282
            if !set.insert(key) {
25d14e49ab68atagen2026-09-24 11:07 UTC283
                anyhow::bail!("duplicate forge public key: {raw}");
25d14e49ab68atagen2026-09-24 11:07 UTC284
            }
25d14e49ab68atagen2026-09-24 11:07 UTC285
        }
25d14e49ab68atagen2026-09-24 11:07 UTC286
        Ok(set)
25d14e49ab68atagen2026-09-24 11:07 UTC287
    }
25d14e49ab68atagen2026-09-24 11:07 UTC288
25d14e49ab68atagen2026-09-24 11:07 UTC289
    pub fn websocket_path(&self) -> anyhow::Result<String> {
25d14e49ab68atagen2026-09-24 11:07 UTC290
        let url = url::Url::parse(&self.public_url)
25d14e49ab68atagen2026-09-24 11:07 UTC291
            .map_err(|error| anyhow::anyhow!("invalid public_url {}: {error}", self.public_url))?;
25d14e49ab68atagen2026-09-24 11:07 UTC292
        let path = url.path();
25d14e49ab68atagen2026-09-24 11:07 UTC293
        if path.is_empty() {
25d14e49ab68atagen2026-09-24 11:07 UTC294
            Ok("/".to_owned())
25d14e49ab68atagen2026-09-24 11:07 UTC295
        } else if path.starts_with('/') {
25d14e49ab68atagen2026-09-24 11:07 UTC296
            Ok(path.to_owned())
25d14e49ab68atagen2026-09-24 11:07 UTC297
        } else {
25d14e49ab68atagen2026-09-24 11:07 UTC298
            Ok(format!("/{path}"))
25d14e49ab68atagen2026-09-24 11:07 UTC299
        }
25d14e49ab68atagen2026-09-24 11:07 UTC300
    }
25d14e49ab68atagen2026-09-24 11:07 UTC301
}
25d14e49ab68atagen2026-09-24 11:07 UTC302
25d14e49ab68atagen2026-09-24 11:07 UTC303
pub(crate) fn relay_url(raw: &str) -> Option<url::Url> {
25d14e49ab68atagen2026-09-24 11:07 UTC304
    let url = url::Url::parse(raw).ok()?;
25d14e49ab68atagen2026-09-24 11:07 UTC305
    if !matches!(url.scheme(), "ws" | "wss")
25d14e49ab68atagen2026-09-24 11:07 UTC306
        || url.host_str().is_none()
25d14e49ab68atagen2026-09-24 11:07 UTC307
        || !url.username().is_empty()
25d14e49ab68atagen2026-09-24 11:07 UTC308
        || url.password().is_some()
25d14e49ab68atagen2026-09-24 11:07 UTC309
        || url.query().is_some()
25d14e49ab68atagen2026-09-24 11:07 UTC310
        || url.fragment().is_some()
25d14e49ab68atagen2026-09-24 11:07 UTC311
    {
25d14e49ab68atagen2026-09-24 11:07 UTC312
        return None;
25d14e49ab68atagen2026-09-24 11:07 UTC313
    }
25d14e49ab68atagen2026-09-24 11:07 UTC314
    Some(url)
25d14e49ab68atagen2026-09-24 11:07 UTC315
}