dig_rpc/lib.rs
1//! # dig-rpc
2//!
3//! An axum-based JSON-RPC **server framework** for a DIG node. It serves the
4//! canonical [`dig-rpc-protocol`](dig_rpc_protocol) interface over the three DIG
5//! transport surfaces and owns everything transport-shaped so a node doesn't
6//! have to:
7//!
8//! - **mTLS** peer surface, **HTTPS** public-read surface, **loopback** control
9//! surface — one [`RpcServer`] per surface, over the same handler;
10//! - the JSON-RPC 2.0 envelope + the uniform error envelope;
11//! - the surface/tier **allowlist boundary** ([`dispatch`](mod@dispatch)) — a
12//! control method is unreachable off loopback, a non-allowlisted method is
13//! unreachable over the peer surface;
14//! - `rpc.discover` served from the generated OpenRPC document;
15//! - per-(peer, tier) **rate limiting**;
16//! - **graceful shutdown** driven by any future.
17//!
18//! The node supplies the *semantics* through one small trait,
19//! [`RpcHandler`] — so this crate depends ONLY on [`dig_rpc_protocol`], never on a
20//! node or service crate. (The previous design's `dig-service` dependency is
21//! gone: the shutdown signal is a plain future and dispatch is a trait, not an
22//! external `RpcApi`.)
23//!
24//! ## Architecture
25//!
26//! ```text
27//! POST / (one surface: Loopback | PublicRead | Peer)
28//! │
29//! ▼ axum handler
30//! ┌────────────────────────────────────────────────┐
31//! │ rate limit — per (peer, tier) token bucket │
32//! │ dispatch — resolve method (dig-rpc-protocol) │
33//! │ — surface/tier allowlist boundary │
34//! │ — rpc.discover from OpenRPC generator │
35//! │ — RpcHandler::handle (node semantics) │
36//! │ envelope — JsonRpcResponse { result | error } │
37//! └────────────────────────────────────────────────┘
38//! ```
39//!
40//! ## Minimal handler
41//!
42//! ```
43//! use dig_rpc::{RpcHandler};
44//! use dig_rpc_protocol::{Method, RpcError, ErrorCode};
45//! use serde_json::{json, Value};
46//!
47//! struct MyNode;
48//!
49//! #[async_trait::async_trait]
50//! impl RpcHandler for MyNode {
51//! async fn handle(&self, method: Method, _params: Value) -> Result<Value, RpcError> {
52//! match method {
53//! Method::Health => Ok(json!({ "status": "ok" })),
54//! other => Err(RpcError::of(
55//! ErrorCode::MethodNotFound,
56//! format!("{} not served", other.name()),
57//! )),
58//! }
59//! }
60//! }
61//! ```
62
63#![forbid(unsafe_code)]
64#![warn(missing_docs)]
65
66pub mod dispatch;
67pub mod error;
68pub mod handler;
69pub mod middleware;
70pub mod server;
71pub mod tls;
72
73pub use dispatch::{dispatch, parse_error_response, SharedHandler, Surface};
74pub use error::RpcServerError;
75pub use handler::RpcHandler;
76pub use middleware::{RateLimitConfig, RateLimitState};
77pub use server::{RpcServer, RpcServerMode};
78pub use tls::{InternalCertPaths, PublicCertPaths, TlsConfig};
79
80// Re-export the wire contract for ergonomic downstream use.
81pub use dig_rpc_protocol::{
82 self, envelope, ErrorCode, ErrorOrigin, JsonRpcRequest, JsonRpcResponse, Method, RpcError, Tier,
83};
84
85#[cfg(test)]
86mod conformance {
87 //! Guards the single-source-of-truth boundary: this crate must never grow
88 //! a local shadow of the wire contract. If a re-export here stops
89 //! resolving to `dig_rpc_protocol`'s type, downstream consumers (dig-node,
90 //! the JS binding's Rust-side name-agreement test) silently diverge from
91 //! the canonical crate — so pin the identity at compile time.
92 use crate::{ErrorCode, ErrorOrigin, JsonRpcRequest, JsonRpcResponse, Method, RpcError, Tier};
93
94 fn assert_same_type<T>(_: T) {}
95
96 #[test]
97 fn reexports_are_the_canonical_dig_rpc_protocol_types() {
98 assert_same_type::<dig_rpc_protocol::Method>(Method::Health);
99 assert_same_type::<dig_rpc_protocol::Tier>(Tier::PublicRead);
100 assert_same_type::<dig_rpc_protocol::ErrorCode>(ErrorCode::MethodNotFound);
101 assert_same_type::<dig_rpc_protocol::ErrorOrigin>(ErrorOrigin::Node);
102 assert_same_type::<dig_rpc_protocol::RpcError>(RpcError::of(
103 ErrorCode::MethodNotFound,
104 "conformance",
105 ));
106 let req: JsonRpcRequest = JsonRpcRequest::new(1, "health", serde_json::Value::Null);
107 assert_same_type::<dig_rpc_protocol::JsonRpcRequest>(req);
108 let _: fn(JsonRpcResponse) = |_| {};
109 }
110}