Skip to main content

urma_node/
service.rs

1//! Small authenticated loopback control surface. The archive engine remains
2//! usable directly; HTTP is not a prerequisite for embedded applications.
3use crate::{
4    Error,
5    engine::Engine,
6    model::{CacheReceipt, ServiceStatus},
7    peer_discovery::Discovery,
8    peer_sync::Scheduler,
9    recorded::{RecordedError, read, record},
10    retrieval::RecordSource,
11    source::{CommitSource, CommitSources},
12};
13
14mod overlay;
15use rand::RngCore;
16use serde_json::json;
17use std::{
18    fs::OpenOptions,
19    io::{Read, Write},
20    net::{SocketAddr, TcpListener, TcpStream},
21    path::{Path, PathBuf},
22    sync::{
23        Arc, Mutex,
24        atomic::{AtomicBool, Ordering},
25    },
26    time::Duration,
27};
28
29#[derive(Default, Clone)]
30struct CacheStatus {
31    receipt: Option<CacheReceipt>,
32    error: Option<RecordedError>,
33}
34
35type ErrorSlot = Arc<Mutex<Option<RecordedError>>>;
36
37/// Cooperative stop request shared with the owner of a running service.
38/// Requesting shutdown does not wait for completion or interrupt entered I/O
39/// or SQL. The owner must wait for `Service::run` to return before reopening
40/// its archive. Embedded services do not install process signal handlers.
41#[derive(Clone)]
42pub struct ShutdownHandle {
43    stop: Arc<AtomicBool>,
44}
45
46impl ShutdownHandle {
47    pub fn request(&self) {
48        self.stop.store(true, Ordering::Release);
49    }
50}
51
52pub struct Service {
53    listener: TcpListener,
54    token: String,
55    engine: Arc<Engine>,
56    stop: Arc<AtomicBool>,
57    sync_error: ErrorSlot,
58    names_error: ErrorSlot,
59    retention_error: ErrorSlot,
60    cache_status: Arc<Mutex<CacheStatus>>,
61    cache_bytes: u64,
62    retention: Option<(PathBuf, urma_runtime::multipart::RecoveryLimits)>,
63    sources: Vec<Arc<dyn RecordSource + Send + Sync>>,
64    commit_sources: Option<CommitSources>,
65    dependency_error: ErrorSlot,
66    peer_sync: Option<Scheduler>,
67    inventory_error: ErrorSlot,
68    discovery: Option<Discovery>,
69    discovery_error: ErrorSlot,
70}
71
72impl Service {
73    const CAPABILITIES: [&str; 9] = [
74        "status",
75        "header-sync",
76        "full-block-scan",
77        "record-read",
78        "object-status",
79        "object-pin",
80        "cache-prune",
81        "wire-index",
82        "names-index",
83    ];
84
85    pub fn bind(address: SocketAddr, token: String, engine: Arc<Engine>) -> Result<Self, Error> {
86        if !address.ip().is_loopback()
87            || token.len() != 64
88            || !token.bytes().all(|c| c.is_ascii_hexdigit())
89        {
90            return Err(Error::Invalid(
91                "service requires loopback and a 32-byte hexadecimal bearer token".into(),
92            ));
93        }
94        let listener = TcpListener::bind(address)?;
95        listener.set_nonblocking(true)?;
96        Ok(Self {
97            listener,
98            token,
99            engine,
100            stop: Arc::new(AtomicBool::new(false)),
101            sync_error: Arc::new(Mutex::new(None)),
102            names_error: Arc::new(Mutex::new(None)),
103            retention_error: Arc::new(Mutex::new(None)),
104            cache_status: Arc::new(Mutex::new(CacheStatus::default())),
105            cache_bytes: crate::cache::DEFAULT_CACHE_BYTES,
106            retention: None,
107            sources: Vec::new(),
108            commit_sources: None,
109            dependency_error: Arc::new(Mutex::new(None)),
110            peer_sync: None,
111            inventory_error: Arc::new(Mutex::new(None)),
112            discovery: None,
113            discovery_error: Arc::new(Mutex::new(None)),
114        })
115    }
116
117    pub fn address(&self) -> Result<SocketAddr, Error> {
118        Ok(self.listener.local_addr()?)
119    }
120
121    pub fn shutdown_handle(&self) -> ShutdownHandle {
122        ShutdownHandle {
123            stop: Arc::clone(&self.stop),
124        }
125    }
126
127    /// Explicit optional candidates for background object recovery. Merely
128    /// configuring handles does not dial, fetch, change headers or scan coverage.
129    pub fn with_record_sources(
130        mut self,
131        sources: Vec<Arc<dyn RecordSource + Send + Sync>>,
132    ) -> Result<Self, Error> {
133        if sources.len() > 1024 {
134            return Err(Error::Invalid("too many configured record sources".into()));
135        }
136        self.sources = sources;
137        Ok(self)
138    }
139
140    pub fn with_commit_sources(mut self, sources: CommitSources) -> Result<Self, Error> {
141        if sources.is_empty() {
142            return Err(Error::Invalid("commit source set is empty".into()));
143        }
144        let expected = self.engine.status()?.chain_genesis;
145        let mut chains = sources.source_chains();
146        chains.push(sources.chain());
147        for chain in chains {
148            let genesis = chain
149                .genesis()
150                .map_err(|error| Error::Invalid(error.to_string()))?
151                .0;
152            if genesis != expected {
153                return Err(Error::Invalid(
154                    "commit source belongs to another chain".into(),
155                ));
156            }
157        }
158        self.commit_sources = Some(sources);
159        Ok(self)
160    }
161
162    /// Operational soft target, not persisted archive identity or a disk quota.
163    /// Only run(true) schedules enforcement; run(false) remains worker-free.
164    /// Zero requests eventual drainage of unprotected cache over bounded steps.
165    pub fn with_cache_limit(mut self, bytes: u64) -> Result<Self, Error> {
166        if bytes > i64::MAX as u64 {
167            return Err(Error::Invalid("cache limit exceeds storage range".into()));
168        }
169        self.cache_bytes = bytes;
170        Ok(self)
171    }
172
173    /// Configure bounded background object acquisition. The caller supplies an
174    /// owned scratch directory and explicit limits. Only run(true) schedules it;
175    /// pruned archives perform no automatic content acquisition.
176    pub fn with_retention(
177        mut self,
178        scratch: PathBuf,
179        limits: urma_runtime::multipart::RecoveryLimits,
180    ) -> Result<Self, Error> {
181        if limits.max_payload_bytes == 0 || limits.max_nodes == 0 || limits.timeout.is_zero() {
182            return Err(Error::Invalid(
183                "object retention limits must be nonzero".into(),
184            ));
185        }
186        self.retention = Some((scratch, limits));
187        Ok(self)
188    }
189
190    /// Run foreground. Network acquisition is opt-in; status works offline.
191    pub fn run(self, synchronize: bool) -> Result<(), Error> {
192        std::thread::scope(|scope| {
193            if synchronize {
194                scope.spawn(|| {
195                    while !self.stop.load(Ordering::Acquire) {
196                        self.worker_round();
197                        for _ in 0..20 {
198                            if self.stop.load(Ordering::Acquire) {
199                                break;
200                            }
201                            std::thread::sleep(Duration::from_millis(100));
202                        }
203                    }
204                });
205            }
206            let outcome = self.accept_loop();
207            self.stop.store(true, Ordering::Release);
208            outcome
209        })
210    }
211
212    fn worker_round(&self) {
213        let headers = self.engine.synchronize_headers_until(&self.stop);
214        // Discovery keeps connections and the book fresh before inventory
215        // sync uses them; it is independent of this round's header result.
216        self.discover_peers();
217        // Peer inventory is fast data: it runs before the full-block scan and
218        // independently of this round's header result, with its own status.
219        self.sync_next_inventory();
220        let result = headers
221            .and_then(|_| self.engine.scan_next_until(&self.stop))
222            .and_then(|status| {
223                if !self.stop.load(Ordering::Acquire) {
224                    self.engine.resolve_cached_dependency()?;
225                }
226                Ok(status)
227            });
228        record(&self.sync_error, &result);
229        self.acquire_next_dependency();
230        // Registered projections can use already scanned data
231        // even if this round's acquisition failed. They never
232        // create registrations or acquire missing history.
233        if !self.stop.load(Ordering::Acquire) {
234            let result = self.engine.maintain_next_names(&self.stop);
235            record(&self.names_error, &result);
236        }
237        // Full-block audit always gets its opportunity before
238        // one object attempt. A sync failure does not suppress
239        // independent recovery from the last selected view;
240        // its error remains separately visible in status.
241        if !self.stop.load(Ordering::Acquire) {
242            if let Some((scratch, limits)) = &self.retention {
243                let sources: Vec<&dyn RecordSource> = self
244                    .sources
245                    .iter()
246                    .map(|source| -> &dyn RecordSource { source.as_ref() })
247                    .collect();
248                let result = self
249                    .engine
250                    .retain_next_object(&sources, *limits, scratch, &self.stop);
251                record(&self.retention_error, &result);
252            }
253        }
254        // Independent local maintenance also runs after failed
255        // acquisition/projection/retention. It never opens a source.
256        if !self.stop.load(Ordering::Acquire) {
257            let result = self.engine.maintain_cache(self.cache_bytes, &self.stop);
258            if let Ok(mut status) = self.cache_status.lock() {
259                match result {
260                    Ok(receipt) => {
261                        status.receipt = Some(receipt);
262                        status.error = None;
263                    }
264                    Err(cause) => {
265                        status.error = Some(RecordedError::now(cause));
266                    }
267                }
268            }
269        }
270    }
271
272    fn acquire_next_dependency(&self) {
273        let Some(sources) = &self.commit_sources else {
274            return;
275        };
276        if self.stop.load(Ordering::Acquire) {
277            return;
278        }
279        let result = self.engine.resolve_next_dependency(sources, &self.stop);
280        if self.stop.load(Ordering::Acquire) {
281            return;
282        }
283        record(&self.dependency_error, &result);
284    }
285
286    fn status(&self) -> Result<ServiceStatus, Error> {
287        let (last_sync_error, last_sync_error_at) =
288            RecordedError::split(read(&self.sync_error, "sync")?);
289        let (last_retention_error, last_retention_error_at) =
290            RecordedError::split(read(&self.retention_error, "retention")?);
291        let (last_names_error, last_names_error_at) =
292            RecordedError::split(read(&self.names_error, "Names")?);
293        let (last_dependency_error, last_dependency_error_at) =
294            RecordedError::split(read(&self.dependency_error, "dependency")?);
295        let (last_inventory_error, last_inventory_error_at) =
296            RecordedError::split(read(&self.inventory_error, "inventory")?);
297        let (last_discovery_error, last_discovery_error_at) =
298            RecordedError::split(read(&self.discovery_error, "discovery")?);
299        let cache = self
300            .cache_status
301            .lock()
302            .map_err(|_| Error::Invalid("cache status poisoned".into()))?
303            .clone();
304        let (last_cache_error, last_cache_error_at) = RecordedError::split(cache.error);
305        let overlay = self.overlay_status()?;
306        let mut commit_sources = Vec::new();
307        for sources in self.commit_sources.iter() {
308            commit_sources.extend(sources.status()?);
309        }
310        Ok(ServiceStatus {
311            archive: self.engine.status()?,
312            last_sync_error,
313            last_sync_error_at,
314            last_retention_error,
315            last_retention_error_at,
316            last_names_error,
317            last_names_error_at,
318            last_dependency_error,
319            last_dependency_error_at,
320            last_inventory_error,
321            last_inventory_error_at,
322            last_discovery_error,
323            last_discovery_error_at,
324            commit_sources,
325            cache_limit_bytes: self.cache_bytes,
326            last_cache_error,
327            last_cache_error_at,
328            last_cache_maintenance: cache.receipt,
329            overlay,
330            chain_peers: self.engine.chain_peers()?,
331            retention_configured: self.retention.is_some(),
332            record_sources_configured: u64::try_from(self.sources.len())
333                .map_err(|error| Error::Invalid(format!("record source count: {error}")))?,
334            capabilities: Self::CAPABILITIES.map(String::from).to_vec(),
335        })
336    }
337
338    fn accept_loop(&self) -> Result<(), Error> {
339        while !self.stop.load(Ordering::Acquire) {
340            match self.listener.accept() {
341                Ok((mut stream, peer)) => {
342                    if !peer.ip().is_loopback() {
343                        continue;
344                    }
345                    stream.set_read_timeout(Some(Duration::from_secs(2)))?;
346                    stream.set_write_timeout(Some(Duration::from_secs(2)))?;
347                    // A malformed or disconnected client never stops the service.
348                    let _ = self.request(&mut stream);
349                }
350                Err(e) if e.kind() == std::io::ErrorKind::WouldBlock => {
351                    std::thread::sleep(Duration::from_millis(50))
352                }
353                Err(e) => return Err(e.into()),
354            }
355        }
356        Ok(())
357    }
358
359    fn request(&self, stream: &mut TcpStream) -> Result<(), Error> {
360        let mut raw = Vec::new();
361        let began = std::time::Instant::now();
362        loop {
363            if raw.len() == 8192 || began.elapsed() > Duration::from_secs(3) {
364                return respond(stream, 400, json!({"error":"request header limit"}));
365            }
366            let mut byte = [0];
367            if stream.read(&mut byte)? == 0 {
368                return Ok(());
369            }
370            raw.push(byte[0]);
371            if began.elapsed() > Duration::from_secs(3) {
372                return respond(stream, 400, json!({"error":"request header deadline"}));
373            }
374            if raw.ends_with(b"\r\n\r\n") {
375                break;
376            }
377        }
378        let Ok(request) = std::str::from_utf8(&raw) else {
379            return respond(stream, 400, json!({"error":"invalid header encoding"}));
380        };
381        let mut lines = request.split("\r\n");
382        let first = lines.next().unwrap_or("");
383        let mut authorized = false;
384        let mut authorization_seen = false;
385        for line in lines.filter(|line| !line.is_empty()) {
386            let Some((name, value)) = line.split_once(':') else {
387                return respond(stream, 400, json!({"error":"malformed header"}));
388            };
389            if name.eq_ignore_ascii_case("authorization") {
390                if authorization_seen {
391                    return respond(stream, 400, json!({"error":"duplicate authorization"}));
392                }
393                authorization_seen = true;
394                authorized = value
395                    .trim()
396                    .strip_prefix("Bearer ")
397                    .is_some_and(|value| equal_token(value, &self.token));
398            }
399            if name.eq_ignore_ascii_case("transfer-encoding")
400                || (name.eq_ignore_ascii_case("content-length") && value.trim() != "0")
401            {
402                return respond(
403                    stream,
404                    400,
405                    json!({"error":"this control endpoint accepts no request body"}),
406                );
407            }
408        }
409        if !authorized {
410            return respond(stream, 401, json!({"error":"authentication required"}));
411        }
412        match first {
413            line if (line.starts_with("GET /v1/names/") || line.starts_with("POST /v1/names/"))
414                && line.ends_with(" HTTP/1.1") =>
415            {
416                let (method, path) = line[..line.len() - " HTTP/1.1".len()]
417                    .split_once(' ')
418                    .expect("matched HTTP method");
419                let request = match names_request(method, path) {
420                    Ok(Some(request)) => request,
421                    Ok(None) => {
422                        return respond(stream, 404, json!({"error":"unknown endpoint"}));
423                    }
424                    Err(_) => {
425                        return respond(stream, 400, json!({"error":"invalid Names request"}));
426                    }
427                };
428                // Reads never register or advance a registry. Mutations are
429                // explicit one-shot commands under the Engine archive mutex.
430                let result = match request {
431                    NamesRequest::Status(genesis) => match self.engine.names_status(genesis) {
432                        Ok(Some(status)) => serde_json::to_value(status).map_err(Error::from),
433                        Ok(None) => {
434                            return respond(
435                                stream,
436                                404,
437                                json!({"error":"Names registry is not registered locally"}),
438                            );
439                        }
440                        Err(error) => Err(error),
441                    },
442                    NamesRequest::Resolve(genesis, name) => self
443                        .engine
444                        .names_resolve(genesis, &name)
445                        .and_then(|value| serde_json::to_value(value).map_err(Error::from)),
446                    NamesRequest::Register(genesis, point) => self
447                        .engine
448                        .register_names(genesis, point)
449                        .and_then(|value| serde_json::to_value(value).map_err(Error::from)),
450                    NamesRequest::Step(genesis) => self
451                        .engine
452                        .names_step(genesis)
453                        .and_then(|value| serde_json::to_value(value).map_err(Error::from)),
454                    NamesRequest::View(genesis, limit, cursor) => self
455                        .engine
456                        .names_view(genesis, cursor.as_ref(), limit)
457                        .and_then(|value| serde_json::to_value(value).map_err(Error::from)),
458                };
459                match result {
460                    Ok(value) => respond(stream, 200, value),
461                    Err(error) => control_error(stream, error),
462                }
463            }
464            line if line.starts_with("GET /v1/wire?") && line.ends_with(" HTTP/1.1") => {
465                let query = &line["GET /v1/wire?".len()..line.len() - " HTTP/1.1".len()];
466                let (limit, cursor) = match wire_query(query) {
467                    Ok(value) => value,
468                    Err(_) => {
469                        return respond(stream, 400, json!({"error":"invalid Wire page query"}));
470                    }
471                };
472                match self.engine.wire_page(cursor.as_ref(), limit) {
473                    Ok(page) => respond(stream, 200, serde_json::to_value(page)?),
474                    Err(error) => control_error(stream, error),
475                }
476            }
477            "POST /v1/cache/prune HTTP/1.1" => match self.engine.prune_cache() {
478                Ok(records_removed) => respond(
479                    stream,
480                    200,
481                    serde_json::to_value(crate::model::PruneReceipt { records_removed })?,
482                ),
483                Err(error) => control_error(stream, error),
484            },
485            line if line.starts_with("POST /v1/objects/") && line.ends_with(" HTTP/1.1") => {
486                let path = &line["POST /v1/objects/".len()..line.len() - " HTTP/1.1".len()];
487                let (id, pin) = match path.rsplit_once('/') {
488                    Some((id, "pin")) => (id, true),
489                    Some((id, "unpin")) => (id, false),
490                    _ => return respond(stream, 404, json!({"error":"unknown endpoint"})),
491                };
492                if id.len() != 64 || !id.bytes().all(|b| b.is_ascii_hexdigit()) {
493                    return respond(stream, 400, json!({"error":"invalid root transaction ID"}));
494                }
495                let root = id
496                    .parse()
497                    .map_err(|e| Error::Invalid(format!("TXID: {e}")))?;
498                // Engine serializes validation and mutation with pruning/reorg.
499                // The acknowledgement describes this operation's commit point;
500                // a lost response must not be treated as proof of no mutation.
501                match self.engine.set_pin(root, pin) {
502                    Ok(()) => respond(
503                        stream,
504                        200,
505                        serde_json::to_value(crate::model::PinReceipt { root, pinned: pin })?,
506                    ),
507                    Err(error) => control_error(stream, error),
508                }
509            }
510            line if line.starts_with("GET /v1/objects/") && line.ends_with(" HTTP/1.1") => {
511                let id = &line["GET /v1/objects/".len()..line.len() - " HTTP/1.1".len()];
512                if id.len() != 64 || !id.bytes().all(|b| b.is_ascii_hexdigit()) {
513                    return respond(stream, 400, json!({"error":"invalid root transaction ID"}));
514                }
515                let root = id
516                    .parse()
517                    .map_err(|e| Error::Invalid(format!("TXID: {e}")))?;
518                match self.engine.object_status(root)? {
519                    Some(status) => respond(stream, 200, serde_json::to_value(status)?),
520                    None => respond(
521                        stream,
522                        404,
523                        json!({"error":"no completed object recorded locally"}),
524                    ),
525                }
526            }
527            line if line.starts_with("GET /v1/records/") && line.ends_with(" HTTP/1.1") => {
528                let id = &line["GET /v1/records/".len()..line.len() - " HTTP/1.1".len()];
529                if id.len() != 64 || !id.bytes().all(|b| b.is_ascii_hexdigit()) {
530                    return respond(stream, 400, json!({"error":"invalid transaction ID"}));
531                }
532                let txid = id
533                    .parse()
534                    .map_err(|e| Error::Invalid(format!("TXID: {e}")))?;
535                match self.engine.record(txid)? {
536                    Some(record) => respond(stream, 200, serde_json::to_value(record)?),
537                    None => respond(
538                        stream,
539                        404,
540                        json!({"error":"record is not locally available in the selected view"}),
541                    ),
542                }
543            }
544            "GET /v1/status HTTP/1.1" => {
545                respond(stream, 200, serde_json::to_value(self.status()?)?)
546            }
547            // Ascending by height; only the newest (highest) 1024 rows are returned.
548            "GET /v1/overlay/omissions HTTP/1.1" => match self.engine.peer_omissions_latest(1024) {
549                Ok(omissions) => respond(stream, 200, serde_json::to_value(omissions)?),
550                Err(error) => control_error(stream, error),
551            },
552            "GET /v1/overlay/book HTTP/1.1" => match self.overlay_book() {
553                Ok(Some(book)) => respond(stream, 200, serde_json::to_value(book)?),
554                Ok(None) => respond(stream, 404, json!({"error":"overlay disabled"})),
555                Err(error) => control_error(stream, error),
556            },
557            "POST /v1/shutdown HTTP/1.1" => {
558                respond(stream, 200, json!({"stopping":true}))?;
559                self.stop.store(true, Ordering::Release);
560                Ok(())
561            }
562            _ => respond(stream, 404, json!({"error":"unknown endpoint"})),
563        }
564    }
565}
566
567enum NamesRequest {
568    Status(bitcoin::Txid),
569    Resolve(bitcoin::Txid, urma_names::name::Name),
570    Register(bitcoin::Txid, crate::model::ChainPoint),
571    Step(bitcoin::Txid),
572    View(bitcoin::Txid, u32, Option<crate::names::NamesCursor>),
573}
574
575fn names_request(method: &str, path: &str) -> Result<Option<NamesRequest>, Error> {
576    let invalid = || Error::Invalid("invalid Names request".into());
577    let path = path.strip_prefix("/v1/names/").ok_or_else(invalid)?;
578    let (id, operation) = path.split_once('/').unwrap_or((path, ""));
579    if id.len() != 64 || !id.bytes().all(|b| b.is_ascii_hexdigit()) {
580        return Err(invalid());
581    }
582    let genesis = id.parse().map_err(|_| invalid())?;
583    let request = match (method, operation) {
584        ("GET", "") => NamesRequest::Status(genesis),
585        ("GET", operation) if operation.starts_with("view?") => {
586            let (limit, cursor) = names_view_query(&operation["view?".len()..])?;
587            if cursor.as_ref().is_some_and(|c| c.genesis != genesis) {
588                return Err(invalid());
589            }
590            NamesRequest::View(genesis, limit, cursor)
591        }
592        ("GET", operation) if operation.starts_with("resolve/") => {
593            // Canonical Names V0 uses only ASCII letters, digits and hyphens;
594            // do not normalize, percent-decode or accept alternate spellings.
595            let name = urma_names::name::Name::parse(&operation["resolve/".len()..])
596                .map_err(|_| invalid())?;
597            NamesRequest::Resolve(genesis, name)
598        }
599        ("POST", "step") => NamesRequest::Step(genesis),
600        ("POST", operation) if operation.starts_with("register?") => {
601            let mut height = None;
602            let mut hash = None;
603            for field in operation["register?".len()..].split('&') {
604                match field.split_once('=') {
605                    Some(("height", value)) if height.is_none() => {
606                        if value.is_empty() || !value.bytes().all(|b| b.is_ascii_digit()) {
607                            return Err(invalid());
608                        }
609                        let value: u64 = value.parse().map_err(|_| invalid())?;
610                        if value > i64::MAX as u64 {
611                            return Err(invalid());
612                        }
613                        height = Some(value);
614                    }
615                    Some(("hash", value)) if hash.is_none() => {
616                        if value.len() != 64 || !value.bytes().all(|b| b.is_ascii_hexdigit()) {
617                            return Err(invalid());
618                        }
619                        hash = Some(value.parse().map_err(|_| invalid())?);
620                    }
621                    _ => return Err(invalid()),
622                }
623            }
624            NamesRequest::Register(
625                genesis,
626                crate::model::ChainPoint {
627                    height: height.ok_or_else(invalid)?,
628                    hash: hash.ok_or_else(invalid)?,
629                },
630            )
631        }
632        _ => return Ok(None),
633    };
634    Ok(Some(request))
635}
636
637fn names_view_query(query: &str) -> Result<(u32, Option<crate::names::NamesCursor>), Error> {
638    let invalid = || Error::Invalid("invalid Names view query".into());
639    let mut limit = None;
640    let mut cursor = None;
641    for field in query.split('&') {
642        match field.split_once('=') {
643            Some(("limit", value)) if limit.is_none() => {
644                if value.is_empty() || !value.bytes().all(|b| b.is_ascii_digit()) {
645                    return Err(invalid());
646                }
647                let value: u32 = value.parse().map_err(|_| invalid())?;
648                if !(1..=crate::names::MAX_NAMES_PAGE).contains(&value) {
649                    return Err(invalid());
650                }
651                limit = Some(value);
652            }
653            Some(("cursor", value)) if cursor.is_none() => {
654                if value.len() > 1024 {
655                    return Err(invalid());
656                }
657                let bytes = hex::decode(value).map_err(|_| invalid())?;
658                let decoded: crate::names::NamesCursor = serde_json::from_slice(&bytes)?;
659                if decoded.snapshot.instance.len() != 32
660                    || !decoded
661                        .snapshot
662                        .instance
663                        .bytes()
664                        .all(|b| b.is_ascii_hexdigit())
665                {
666                    return Err(invalid());
667                }
668                cursor = Some(decoded);
669            }
670            _ => return Err(invalid()),
671        }
672    }
673    Ok((limit.ok_or_else(invalid)?, cursor))
674}
675
676fn wire_query(query: &str) -> Result<(u32, Option<crate::projection::WireCursor>), Error> {
677    let invalid = || Error::Invalid("invalid Wire page query".into());
678    let mut limit = None;
679    let mut cursor = None;
680    for field in query.split('&') {
681        match field.split_once('=') {
682            Some(("limit", value)) if limit.is_none() => {
683                if value.is_empty() || !value.bytes().all(|b| b.is_ascii_digit()) {
684                    return Err(invalid());
685                }
686                let value: u32 = value.parse().map_err(|_| invalid())?;
687                if !(1..=crate::projection::MAX_WIRE_PAGE).contains(&value) {
688                    return Err(invalid());
689                }
690                limit = Some(value);
691            }
692            Some(("cursor", value)) if cursor.is_none() => {
693                if value.len() > 1024 {
694                    return Err(invalid());
695                }
696                let bytes = hex::decode(value).map_err(|_| invalid())?;
697                let decoded: crate::projection::WireCursor = serde_json::from_slice(&bytes)?;
698                if decoded.snapshot.instance.len() != 32
699                    || !decoded
700                        .snapshot
701                        .instance
702                        .bytes()
703                        .all(|b| b.is_ascii_hexdigit())
704                    || decoded.height > i64::MAX as u64
705                {
706                    return Err(invalid());
707                }
708                cursor = Some(decoded);
709            }
710            _ => return Err(invalid()),
711        }
712    }
713    Ok((limit.ok_or_else(invalid)?, cursor))
714}
715
716fn control_error(stream: &mut TcpStream, error: Error) -> Result<(), Error> {
717    match error {
718        Error::Invalid(message) => respond(stream, 422, json!({"error":message})),
719        error @ (Error::Busy(_)
720        | Error::PeerBusy
721        | Error::Transport(_)
722        | Error::DialDenied(_)
723        | Error::Stale) => respond(stream, 409, json!({"error":error.to_string()})),
724        // Do not expose database internals or local filesystem paths over HTTP.
725        _ => respond(
726            stream,
727            500,
728            json!({"error":"local archive operation failed"}),
729        ),
730    }
731}
732
733fn equal_token(left: &str, right: &str) -> bool {
734    if left.len() != right.len() {
735        return false;
736    }
737    left.bytes()
738        .zip(right.bytes())
739        .fold(0u8, |different, (a, b)| different | (a ^ b))
740        == 0
741}
742
743fn respond(stream: &mut TcpStream, code: u16, body: serde_json::Value) -> Result<(), Error> {
744    let body = serde_json::to_vec(&body)?;
745    if body.len() > 32 * 1024 * 1024 {
746        return respond(
747            stream,
748            413,
749            json!({"error":"record exceeds local API response limit"}),
750        );
751    }
752    let reason = match code {
753        200 => "OK",
754        400 => "Bad Request",
755        401 => "Unauthorized",
756        409 => "Conflict",
757        413 => "Content Too Large",
758        422 => "Unprocessable Content",
759        500 => "Internal Server Error",
760        _ => "Not Found",
761    };
762    write!(
763        stream,
764        "HTTP/1.1 {code} {reason}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nCache-Control: no-store\r\nConnection: close\r\n\r\n",
765        body.len()
766    )?;
767    stream.write_all(&body)?;
768    Ok(())
769}
770
771/// New service token, never a wallet/signing/decryption key. Existing files
772/// are not overwritten. Platform-specific private ACL support is required.
773pub fn create_token(path: &Path) -> Result<(), Error> {
774    #[cfg(not(unix))]
775    return Err(Error::Invalid(
776        "private token ACL creation is not implemented for this platform".into(),
777    ));
778    #[cfg(unix)]
779    {
780        use std::os::unix::fs::OpenOptionsExt;
781        let mut bytes = [0u8; 32];
782        rand::rngs::OsRng.fill_bytes(&mut bytes);
783        let mut file = OpenOptions::new()
784            .write(true)
785            .create_new(true)
786            .mode(0o600)
787            .open(path)?;
788        file.write_all(hex::encode(bytes).as_bytes())?;
789        file.sync_all()?;
790        Ok(())
791    }
792}