1use 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
25pub const DEFAULT_PORT: u16 = 8_787;
27
28#[derive(Clone)]
30pub struct BearerToken {
31 digest: [u8; 32],
32}
33
34impl BearerToken {
35 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#[derive(Clone, Debug, Eq, PartialEq)]
78pub struct ServerLimits {
79 pub request_body_bytes: usize,
81 pub json_depth: usize,
83 pub json_nodes: usize,
85 pub request_body_timeout: Duration,
87 pub batch_items: usize,
89 pub concurrent_operations: usize,
91 pub response_bytes: usize,
93 pub proof_bytes: usize,
95 pub witness_bytes: u64,
97 pub query: ExecutionLimits,
99 pub exact_retrieval: ExactRetrievalLimits,
101 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#[derive(Clone, Debug)]
222pub struct ServerConfig {
223 pub data_dir: PathBuf,
225 pub bind: SocketAddr,
227 pub bearer_token: Option<BearerToken>,
229 pub limits: ServerLimits,
231}
232
233impl ServerConfig {
234 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#[derive(Clone, Debug, Error, Eq, PartialEq)]
261pub enum ServerConfigError {
262 #[error("server data-directory path must not be empty")]
264 EmptyDataDirectory,
265 #[error("non-loopback bind {bind} requires a bearer token")]
267 RemoteBindRequiresAuthentication {
268 bind: SocketAddr,
270 },
271 #[error("bearer token is {actual} bytes; required range is {minimum}..={maximum}")]
273 InvalidBearerTokenLength {
274 minimum: usize,
276 maximum: usize,
278 actual: usize,
280 },
281 #[error("bearer token must contain only visible ASCII without whitespace")]
283 InvalidBearerTokenCharacter,
284 #[error("server resource limits must be nonzero")]
286 ZeroLimit,
287 #[error("JSON depth limit {actual} exceeds canonical maximum {maximum}")]
289 JsonDepthTooLarge {
290 maximum: usize,
292 actual: usize,
294 },
295 #[error("JSON node limit {actual} exceeds canonical maximum {maximum}")]
297 JsonNodesTooLarge {
298 maximum: usize,
300 actual: usize,
302 },
303 #[error("proof limit {actual} exceeds canonical maximum {maximum}")]
305 ProofLimitTooLarge {
306 maximum: u64,
308 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}