Skip to main content

hyphae_server/
config.rs

1// SPDX-License-Identifier: Apache-2.0
2
3use std::{
4    fmt,
5    net::{IpAddr, Ipv4Addr, SocketAddr},
6    path::{Path, PathBuf},
7    time::Duration,
8};
9
10use hyphae_contracts::v1::ApiLimitsV1;
11use hyphae_engine::{
12    MAX_DOCUMENT_BYTES, MAX_DOCUMENT_DEPTH, MAX_DOCUMENT_NODES, MAX_RESULT_PROOF_BYTES,
13};
14use hyphae_query::ExecutionLimits;
15use hyphae_storage::MAX_KEY_BYTES;
16use subtle::ConstantTimeEq;
17use thiserror::Error;
18
19const MIN_BEARER_TOKEN_BYTES: usize = 32;
20const MAX_BEARER_TOKEN_BYTES: usize = 4_096;
21
22/// Default loopback-only address used by `hyphae serve`.
23pub const DEFAULT_PORT: u16 = 8_787;
24
25/// One bearer credential retained only as a BLAKE3 digest.
26#[derive(Clone)]
27pub struct BearerToken {
28    digest: [u8; 32],
29}
30
31impl BearerToken {
32    /// Hashes a sufficiently strong opaque bearer secret for later
33    /// constant-time verification.
34    ///
35    /// # Errors
36    ///
37    /// Returns an error when the token is shorter than 32 bytes or longer
38    /// than 4096 bytes.
39    pub fn new(secret: impl AsRef<[u8]>) -> Result<Self, ServerConfigError> {
40        let secret = secret.as_ref();
41        if !(MIN_BEARER_TOKEN_BYTES..=MAX_BEARER_TOKEN_BYTES).contains(&secret.len()) {
42            return Err(ServerConfigError::InvalidBearerTokenLength {
43                minimum: MIN_BEARER_TOKEN_BYTES,
44                maximum: MAX_BEARER_TOKEN_BYTES,
45                actual: secret.len(),
46            });
47        }
48        if !secret.iter().all(|byte| (0x21..=0x7e).contains(byte)) {
49            return Err(ServerConfigError::InvalidBearerTokenCharacter);
50        }
51        Ok(Self {
52            digest: *blake3::hash(secret).as_bytes(),
53        })
54    }
55
56    pub(crate) fn verifies(&self, candidate: &[u8]) -> bool {
57        if !(MIN_BEARER_TOKEN_BYTES..=MAX_BEARER_TOKEN_BYTES).contains(&candidate.len()) {
58            return false;
59        }
60        let candidate = *blake3::hash(candidate).as_bytes();
61        bool::from(self.digest.ct_eq(&candidate))
62    }
63}
64
65impl fmt::Debug for BearerToken {
66    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
67        formatter.write_str("BearerToken([REDACTED])")
68    }
69}
70
71/// Complete resource policy enforced by one HTTP server process.
72#[derive(Clone, Debug, Eq, PartialEq)]
73pub struct ServerLimits {
74    /// Maximum complete JSON request bytes.
75    pub request_body_bytes: usize,
76    /// Maximum JSON nesting depth.
77    pub json_depth: usize,
78    /// Maximum JSON scalar, array, and object nodes.
79    pub json_nodes: usize,
80    /// Maximum time allowed to receive one complete JSON request body.
81    pub request_body_timeout: Duration,
82    /// Maximum records or keys in one atomic mutation batch.
83    pub batch_items: usize,
84    /// Maximum admitted concurrent data operations.
85    pub concurrent_operations: usize,
86    /// Maximum serialized JSON response bytes.
87    pub response_bytes: usize,
88    /// Maximum canonical proof bytes before base64 transport.
89    pub proof_bytes: usize,
90    /// Maximum downloadable snapshot witness bytes.
91    pub witness_bytes: u64,
92    /// Deterministic structured-query work, shape, result, and timeout limits.
93    pub query: ExecutionLimits,
94}
95
96impl Default for ServerLimits {
97    fn default() -> Self {
98        Self {
99            request_body_bytes: 4 * 1024 * 1024,
100            json_depth: MAX_DOCUMENT_DEPTH,
101            json_nodes: 100_000,
102            request_body_timeout: Duration::from_secs(10),
103            batch_items: 1_000,
104            concurrent_operations: 16,
105            response_bytes: 32 * 1024 * 1024,
106            proof_bytes: 16 * 1024 * 1024,
107            witness_bytes: 512 * 1024 * 1024,
108            query: ExecutionLimits::default(),
109        }
110    }
111}
112
113impl ServerLimits {
114    pub(crate) fn validate(&self) -> Result<(), ServerConfigError> {
115        let scalar_limits = [
116            self.request_body_bytes,
117            self.json_depth,
118            self.json_nodes,
119            self.batch_items,
120            self.concurrent_operations,
121            self.response_bytes,
122            self.proof_bytes,
123            self.query.max_returned_records,
124            self.query.max_groups,
125            self.query.max_filter_nodes,
126            self.query.max_filter_depth,
127            self.query.max_sort_fields,
128            self.query.max_group_fields,
129            self.query.max_metrics,
130        ];
131        if scalar_limits.contains(&0)
132            || self.witness_bytes == 0
133            || self.query.max_scanned_records == 0
134            || self.query.max_matched_records == 0
135            || self.request_body_timeout.is_zero()
136            || self.query.timeout.is_zero()
137        {
138            return Err(ServerConfigError::ZeroLimit);
139        }
140        if self.json_depth > MAX_DOCUMENT_DEPTH {
141            return Err(ServerConfigError::JsonDepthTooLarge {
142                maximum: MAX_DOCUMENT_DEPTH,
143                actual: self.json_depth,
144            });
145        }
146        if self.json_nodes > MAX_DOCUMENT_NODES {
147            return Err(ServerConfigError::JsonNodesTooLarge {
148                maximum: MAX_DOCUMENT_NODES,
149                actual: self.json_nodes,
150            });
151        }
152        if u64::try_from(self.proof_bytes).unwrap_or(u64::MAX) > MAX_RESULT_PROOF_BYTES {
153            return Err(ServerConfigError::ProofLimitTooLarge {
154                maximum: MAX_RESULT_PROOF_BYTES,
155                actual: u64::try_from(self.proof_bytes).unwrap_or(u64::MAX),
156            });
157        }
158        Ok(())
159    }
160
161    pub(crate) fn as_contract(&self) -> ApiLimitsV1 {
162        ApiLimitsV1 {
163            key_bytes: usize_to_u64(MAX_KEY_BYTES),
164            document_bytes: usize_to_u64(MAX_DOCUMENT_BYTES),
165            request_body_bytes: usize_to_u64(self.request_body_bytes),
166            json_depth: usize_to_u64(self.json_depth),
167            json_nodes: usize_to_u64(self.json_nodes),
168            request_body_timeout_ms: duration_millis(self.request_body_timeout),
169            batch_items: usize_to_u64(self.batch_items),
170            scanned_records: self.query.max_scanned_records,
171            matched_records: self.query.max_matched_records,
172            result_rows: usize_to_u64(self.query.max_returned_records),
173            aggregation_groups: usize_to_u64(self.query.max_groups),
174            filter_nodes: usize_to_u64(self.query.max_filter_nodes),
175            filter_depth: usize_to_u64(self.query.max_filter_depth),
176            sort_fields: usize_to_u64(self.query.max_sort_fields),
177            group_fields: usize_to_u64(self.query.max_group_fields),
178            metrics: usize_to_u64(self.query.max_metrics),
179            concurrent_operations: usize_to_u64(self.concurrent_operations),
180            query_timeout_ms: duration_millis(self.query.timeout),
181            proof_bytes: usize_to_u64(self.proof_bytes),
182            witness_bytes: self.witness_bytes,
183            response_bytes: usize_to_u64(self.response_bytes),
184        }
185    }
186}
187
188/// Validated input for one owned loopback-first Hyphae server.
189#[derive(Clone, Debug)]
190pub struct ServerConfig {
191    /// Exclusively owned Hyphae data directory.
192    pub data_dir: PathBuf,
193    /// Listener address; defaults to `127.0.0.1:8787`.
194    pub bind: SocketAddr,
195    /// Optional bearer credential. Mandatory for non-loopback binds.
196    pub bearer_token: Option<BearerToken>,
197    /// Effective bounded-resource policy.
198    pub limits: ServerLimits,
199}
200
201impl ServerConfig {
202    /// Creates the secure loopback default for one data directory.
203    pub fn new(data_dir: impl Into<PathBuf>) -> Self {
204        Self {
205            data_dir: data_dir.into(),
206            bind: SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), DEFAULT_PORT),
207            bearer_token: None,
208            limits: ServerLimits::default(),
209        }
210    }
211
212    pub(crate) fn validate(&self) -> Result<(), ServerConfigError> {
213        if self.data_dir.as_os_str().is_empty() {
214            return Err(ServerConfigError::EmptyDataDirectory);
215        }
216        if !self.bind.ip().is_loopback() && self.bearer_token.is_none() {
217            return Err(ServerConfigError::RemoteBindRequiresAuthentication { bind: self.bind });
218        }
219        self.limits.validate()
220    }
221
222    pub(crate) fn data_dir(&self) -> &Path {
223        &self.data_dir
224    }
225}
226
227/// Invalid secure-server configuration rejected before socket bind.
228#[derive(Clone, Debug, Error, Eq, PartialEq)]
229pub enum ServerConfigError {
230    /// The data-directory path is empty.
231    #[error("server data-directory path must not be empty")]
232    EmptyDataDirectory,
233    /// A non-loopback listener was requested without bearer authentication.
234    #[error("non-loopback bind {bind} requires a bearer token")]
235    RemoteBindRequiresAuthentication {
236        /// Rejected listener address.
237        bind: SocketAddr,
238    },
239    /// Bearer token length does not meet the local security policy.
240    #[error("bearer token is {actual} bytes; required range is {minimum}..={maximum}")]
241    InvalidBearerTokenLength {
242        /// Minimum accepted bytes.
243        minimum: usize,
244        /// Maximum accepted bytes.
245        maximum: usize,
246        /// Observed bytes.
247        actual: usize,
248    },
249    /// Bearer tokens must be representable safely in one HTTP header.
250    #[error("bearer token must contain only visible ASCII without whitespace")]
251    InvalidBearerTokenCharacter,
252    /// Every configured budget must be positive.
253    #[error("server resource limits must be nonzero")]
254    ZeroLimit,
255    /// JSON depth policy cannot exceed the canonical document limit.
256    #[error("JSON depth limit {actual} exceeds canonical maximum {maximum}")]
257    JsonDepthTooLarge {
258        /// Canonical maximum.
259        maximum: usize,
260        /// Requested limit.
261        actual: usize,
262    },
263    /// JSON node policy cannot exceed the canonical document limit.
264    #[error("JSON node limit {actual} exceeds canonical maximum {maximum}")]
265    JsonNodesTooLarge {
266        /// Canonical maximum.
267        maximum: usize,
268        /// Requested limit.
269        actual: usize,
270    },
271    /// Proof policy cannot exceed the canonical proof codec hard bound.
272    #[error("proof limit {actual} exceeds canonical maximum {maximum}")]
273    ProofLimitTooLarge {
274        /// Canonical maximum.
275        maximum: u64,
276        /// Requested limit.
277        actual: u64,
278    },
279}
280
281fn usize_to_u64(value: usize) -> u64 {
282    u64::try_from(value).unwrap_or(u64::MAX)
283}
284
285fn duration_millis(value: Duration) -> u64 {
286    u64::try_from(value.as_millis()).unwrap_or(u64::MAX)
287}