Skip to main content

solid_pod_rs_forge/
config.rs

1//! Forge configuration and the chain/mainnet policy guard.
2//!
3//! Field defaults mirror the JSS `forge` plugin config surface
4//! (README §Configuration) — cited by name only. All bounds that keep
5//! the read-time body fetch cheap and DoS-resistant live here.
6
7/// All tunables for a [`crate::ForgeService`].
8#[derive(Debug, Clone)]
9pub struct ForgeConfig {
10    /// URL prefix the forge is mounted under (default `/forge`). No
11    /// trailing slash.
12    pub prefix: String,
13    /// Time-to-live for a minted forge push token, in seconds.
14    pub token_ttl_secs: u64,
15    /// Per-body byte cap when re-fetching pod-hosted issue/PR bodies
16    /// (default 64 KiB).
17    pub max_body_bytes: usize,
18    /// Parallelism for the read-time body fetch (default 8).
19    pub fetch_concurrency: usize,
20    /// Per-body fetch timeout, seconds (default 8).
21    pub fetch_timeout_secs: u64,
22    /// Hard cap on thread pointers materialised in one read (default 500).
23    pub thread_cap: usize,
24    /// Bitcoin chain the mark trail targets. `"tbtc4"` (testnet4) by
25    /// default; mainnet (`"btc"`/`"mainnet"`) is refused unless
26    /// [`ForgeConfig::allow_mainnet`] is set.
27    pub default_chain: String,
28    /// Fail-closed mainnet guard. When `false`, [`chain_allowed`] rejects
29    /// any mainnet chain string. Mark custody here is testnet-grade.
30    pub allow_mainnet: bool,
31    /// Opt-in relays the NIP-34 announcement is published to. Empty =
32    /// build/sign only, never publish.
33    pub announce_relays: Vec<String>,
34}
35
36impl Default for ForgeConfig {
37    fn default() -> Self {
38        Self {
39            prefix: "/forge".to_string(),
40            token_ttl_secs: 3600,
41            max_body_bytes: 64 * 1024,
42            fetch_concurrency: 8,
43            fetch_timeout_secs: 8,
44            thread_cap: 500,
45            default_chain: "tbtc4".to_string(),
46            allow_mainnet: false,
47            announce_relays: Vec::new(),
48        }
49    }
50}
51
52/// Chain strings this build treats as Bitcoin mainnet (custody refused
53/// unless explicitly allowed).
54const MAINNET_ALIASES: &[&str] = &["btc", "bitcoin", "mainnet", "main"];
55
56impl ForgeConfig {
57    /// Normalise the configured prefix: ensure a single leading slash and
58    /// no trailing slash. `""` and `"/"` both normalise to `""` (root
59    /// mount).
60    #[must_use]
61    pub fn normalized_prefix(&self) -> String {
62        let trimmed = self.prefix.trim_matches('/');
63        if trimmed.is_empty() {
64            String::new()
65        } else {
66            format!("/{trimmed}")
67        }
68    }
69
70    /// Is `chain` permitted under the current mainnet policy? Mainnet
71    /// aliases are refused unless [`allow_mainnet`](Self::allow_mainnet).
72    #[must_use]
73    pub fn chain_allowed(&self, chain: &str) -> bool {
74        let lc = chain.to_ascii_lowercase();
75        if MAINNET_ALIASES.contains(&lc.as_str()) {
76            self.allow_mainnet
77        } else {
78            true
79        }
80    }
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn defaults_are_testnet_and_bounded() {
89        let c = ForgeConfig::default();
90        assert_eq!(c.prefix, "/forge");
91        assert_eq!(c.default_chain, "tbtc4");
92        assert!(!c.allow_mainnet);
93        assert_eq!(c.max_body_bytes, 64 * 1024);
94        assert_eq!(c.thread_cap, 500);
95        assert_eq!(c.fetch_concurrency, 8);
96    }
97
98    #[test]
99    fn mainnet_guard_fails_closed() {
100        let c = ForgeConfig::default();
101        for m in ["btc", "BTC", "Bitcoin", "mainnet", "main"] {
102            assert!(!c.chain_allowed(m), "{m} must be refused by default");
103        }
104        assert!(c.chain_allowed("tbtc4"));
105        assert!(c.chain_allowed("testnet"));
106        assert!(c.chain_allowed("signet"));
107    }
108
109    #[test]
110    fn mainnet_guard_opens_when_allowed() {
111        let c = ForgeConfig {
112            allow_mainnet: true,
113            ..Default::default()
114        };
115        assert!(c.chain_allowed("btc"));
116        assert!(c.chain_allowed("mainnet"));
117    }
118
119    #[test]
120    fn prefix_normalisation() {
121        let mk = |p: &str| ForgeConfig {
122            prefix: p.to_string(),
123            ..Default::default()
124        };
125        assert_eq!(mk("/forge").normalized_prefix(), "/forge");
126        assert_eq!(mk("forge").normalized_prefix(), "/forge");
127        assert_eq!(mk("/forge/").normalized_prefix(), "/forge");
128        assert_eq!(mk("/git/forge/").normalized_prefix(), "/git/forge");
129        assert_eq!(mk("/").normalized_prefix(), "");
130        assert_eq!(mk("").normalized_prefix(), "");
131    }
132}