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