dig-rpc-types 0.2.1

Canonical DIG-node JSON-RPC interface: request/response types, the method enum + tier classification, the error-code taxonomy, and an OpenRPC 1.2.6 document generator. The single source of truth both DIG node implementations depend on. Pure types — no I/O, no async, no server logic.
Documentation
//! The canonical DIG-node RPC method catalogue.
//!
//! [`Method`] enumerates every JSON-RPC method the DIG node surface exposes, its
//! stable wire name, its primary [`Tier`], and whether it is reachable over the
//! mTLS peer surface. This is the single method table the OpenRPC generator,
//! both node dispatchers, and the server's allow-list all read — so the name set,
//! the tier map, and the peer allowlist can never drift from one another.
//!
//! Frame families that are shape-dispatched rather than carried in a JSON-RPC
//! `method` field — the DHT (`find_node` / `find_providers` / `add_provider` /
//! `ping`) and PEX (`pex_handshake` / `pex_snapshot` / `pex_delta` /
//! `pex_error`) wires — are documented in [`crate::frames`] and are not
//! `Method` variants.

use crate::tier::Tier;

/// A DIG-node RPC method.
///
/// `#[non_exhaustive]` so adding a method in a minor release is additive.
/// Convert to/from the wire name with [`Method::name`] / [`Method::from_name`].
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Method {
    // ---- PublicRead ----
    /// `dig.getContent` — a verified resource-window read.
    GetContent,
    /// `dig.getCapsule` — the whole `.dig` module for `(store, root)`.
    GetCapsule,
    /// `dig.getModule` — alias of [`GetCapsule`](Method::GetCapsule).
    GetModule,
    /// `dig.getManifest` — the public discovery manifest resource.
    GetManifest,
    /// `dig.getMetadata` — the plaintext metadata manifest (never encrypted).
    GetMetadata,
    /// `dig.listCapsules` — the confirmed capsule list (discovery metadata).
    ListCapsules,
    /// `dig.getProof` — the real inclusion proof + execution-proof status.
    GetProof,
    /// `dig.getProofStatus` — poll a real execution-proof job by id.
    GetProofStatus,
    /// `dig.getAnchoredRoot` — resolve a store's chain-anchored tip root.
    /// (Also peer-reachable.)
    GetAnchoredRoot,
    /// `dig.getCollection` — collection-level facts for a set of NFT launchers.
    /// (Also peer-reachable.)
    GetCollection,
    /// `dig.listCollectionItems` — a page of resolved collection items.
    /// (Also peer-reachable.)
    ListCollectionItems,
    /// `dig.health` — liveness + capability summary.
    Health,
    /// `dig.methods` — the implemented method names (agent self-describe).
    Methods,

    // ---- Peer ----
    /// `dig.getNetworkInfo` — this node's peer-network posture.
    GetNetworkInfo,
    /// `dig.getPeers` — the peers this node knows (peer exchange).
    GetPeers,
    /// `dig.announce` — accept a peer announcement.
    Announce,
    /// `dig.getAvailability` — batch presence check across stores/roots/capsules.
    GetAvailability,
    /// `dig.listInventory` — enumerate what this node serves.
    ListInventory,
    /// `dig.fetchRange` — a single verified range frame of a resource.
    FetchRange,

    // ---- Control (loopback / in-process only) ----
    /// `dig.stage` — compile a local folder into a capsule `.dig` in-process.
    Stage,
    /// `cache.getConfig` — the local-cache config.
    CacheGetConfig,
    /// `cache.setCapBytes` — set the cache size cap.
    CacheSetCapBytes,
    /// `cache.clear` — clear the cache.
    CacheClear,
    /// `cache.listCached` — the durable module inventory.
    CacheListCached,
    /// `cache.removeCached` — remove one cached capsule.
    CacheRemoveCached,
    /// `cache.fetchAndCache` — fetch + cache one capsule.
    CacheFetchAndCache,
    /// `control.peerStatus` — the peer-network status snapshot.
    ControlPeerStatus,
    /// `rpc.discover` — the OpenRPC self-describe document.
    RpcDiscover,
}

impl Method {
    /// The stable JSON-RPC wire name.
    pub const fn name(self) -> &'static str {
        match self {
            Method::GetContent => "dig.getContent",
            Method::GetCapsule => "dig.getCapsule",
            Method::GetModule => "dig.getModule",
            Method::GetManifest => "dig.getManifest",
            Method::GetMetadata => "dig.getMetadata",
            Method::ListCapsules => "dig.listCapsules",
            Method::GetProof => "dig.getProof",
            Method::GetProofStatus => "dig.getProofStatus",
            Method::GetAnchoredRoot => "dig.getAnchoredRoot",
            Method::GetCollection => "dig.getCollection",
            Method::ListCollectionItems => "dig.listCollectionItems",
            Method::Health => "dig.health",
            Method::Methods => "dig.methods",
            Method::GetNetworkInfo => "dig.getNetworkInfo",
            Method::GetPeers => "dig.getPeers",
            Method::Announce => "dig.announce",
            Method::GetAvailability => "dig.getAvailability",
            Method::ListInventory => "dig.listInventory",
            Method::FetchRange => "dig.fetchRange",
            Method::Stage => "dig.stage",
            Method::CacheGetConfig => "cache.getConfig",
            Method::CacheSetCapBytes => "cache.setCapBytes",
            Method::CacheClear => "cache.clear",
            Method::CacheListCached => "cache.listCached",
            Method::CacheRemoveCached => "cache.removeCached",
            Method::CacheFetchAndCache => "cache.fetchAndCache",
            Method::ControlPeerStatus => "control.peerStatus",
            Method::RpcDiscover => "rpc.discover",
        }
    }

    /// Parse a wire name into a [`Method`], or `None` for an unknown method
    /// (the caller answers `-32601`).
    pub fn from_name(name: &str) -> Option<Method> {
        Method::ALL.iter().copied().find(|m| m.name() == name)
    }

    /// The method's PRIMARY access [`Tier`].
    ///
    /// Note: a `PublicRead` method may still be peer-reachable — see
    /// [`is_peer_reachable`](Method::is_peer_reachable). The tier is the
    /// method's canonical home, the allowlist is the security boundary.
    pub const fn tier(self) -> Tier {
        match self {
            Method::GetContent
            | Method::GetCapsule
            | Method::GetModule
            | Method::GetManifest
            | Method::GetMetadata
            | Method::ListCapsules
            | Method::GetProof
            | Method::GetProofStatus
            | Method::GetAnchoredRoot
            | Method::GetCollection
            | Method::ListCollectionItems
            | Method::Health
            | Method::Methods => Tier::PublicRead,

            Method::GetNetworkInfo
            | Method::GetPeers
            | Method::Announce
            | Method::GetAvailability
            | Method::ListInventory
            | Method::FetchRange => Tier::Peer,

            Method::Stage
            | Method::CacheGetConfig
            | Method::CacheSetCapBytes
            | Method::CacheClear
            | Method::CacheListCached
            | Method::CacheRemoveCached
            | Method::CacheFetchAndCache
            | Method::ControlPeerStatus
            | Method::RpcDiscover => Tier::Control,
        }
    }

    /// Whether this method is reachable over the mTLS **peer** surface.
    ///
    /// This mirrors the canonical node's `is_peer_reachable_method` byte-for-byte
    /// (digstore `dig-node` `peer.rs`). It is an ALLOWLIST: adding a method here
    /// is a deliberate security decision — it exposes the method to any remote
    /// peer (the peer verifier authenticates a `peer_id`, it does NOT authorize).
    /// Management/mutation methods (`cache.*`, `control.*`, `dig.stage`) are
    /// NEVER peer-reachable.
    pub const fn is_peer_reachable(self) -> bool {
        matches!(
            self,
            Method::GetContent
                | Method::GetNetworkInfo
                | Method::GetPeers
                | Method::Announce
                | Method::GetAvailability
                | Method::ListInventory
                | Method::FetchRange
                | Method::GetAnchoredRoot
                | Method::GetCollection
                | Method::ListCollectionItems
        )
    }

    /// A one-line human summary (drives the OpenRPC method `summary`).
    pub const fn summary(self) -> &'static str {
        match self {
            Method::GetContent => "Read a verified window of a resource's ciphertext.",
            Method::GetCapsule => "Fetch the whole .dig module for (store, root).",
            Method::GetModule => "Alias of dig.getCapsule.",
            Method::GetManifest => "Fetch the public discovery manifest resource.",
            Method::GetMetadata => "Fetch the plaintext metadata manifest.",
            Method::ListCapsules => "List a store's confirmed capsules.",
            Method::GetProof => "Get the real inclusion proof + execution-proof status.",
            Method::GetProofStatus => "Poll a real execution-proof job by id.",
            Method::GetAnchoredRoot => "Resolve a store's chain-anchored tip root.",
            Method::GetCollection => "Get collection-level facts for NFT launcher ids.",
            Method::ListCollectionItems => "List resolved NFT collection items (paginated).",
            Method::Health => "Liveness and capability summary.",
            Method::Methods => "List the method names this node implements.",
            Method::GetNetworkInfo => "This node's peer-network posture.",
            Method::GetPeers => "The peers this node currently knows.",
            Method::Announce => "Accept a peer announcement (peer_id + addresses).",
            Method::GetAvailability => "Batch-check whether this node holds items.",
            Method::ListInventory => "Enumerate the stores / roots this node serves.",
            Method::FetchRange => "Fetch a single verified range frame of a resource.",
            Method::Stage => "Compile a local folder into a capsule .dig in-process.",
            Method::CacheGetConfig => "Get the local-cache configuration.",
            Method::CacheSetCapBytes => "Set the local-cache size cap.",
            Method::CacheClear => "Clear the local cache.",
            Method::CacheListCached => "List the durable cached modules.",
            Method::CacheRemoveCached => "Remove one cached capsule.",
            Method::CacheFetchAndCache => "Fetch and cache one capsule.",
            Method::ControlPeerStatus => "Snapshot the node's peer network.",
            Method::RpcDiscover => "Return the OpenRPC self-describe document.",
        }
    }

    /// Every method, in catalogue order.
    pub const ALL: &'static [Method] = &[
        Method::GetContent,
        Method::GetCapsule,
        Method::GetModule,
        Method::GetManifest,
        Method::GetMetadata,
        Method::ListCapsules,
        Method::GetProof,
        Method::GetProofStatus,
        Method::GetAnchoredRoot,
        Method::GetCollection,
        Method::ListCollectionItems,
        Method::Health,
        Method::Methods,
        Method::GetNetworkInfo,
        Method::GetPeers,
        Method::Announce,
        Method::GetAvailability,
        Method::ListInventory,
        Method::FetchRange,
        Method::Stage,
        Method::CacheGetConfig,
        Method::CacheSetCapBytes,
        Method::CacheClear,
        Method::CacheListCached,
        Method::CacheRemoveCached,
        Method::CacheFetchAndCache,
        Method::ControlPeerStatus,
        Method::RpcDiscover,
    ];

    /// The method names reachable over the peer surface, in catalogue order —
    /// the exact allowlist a server hands its peer responder.
    pub fn peer_reachable_names() -> Vec<&'static str> {
        Method::ALL
            .iter()
            .copied()
            .filter(|m| m.is_peer_reachable())
            .map(Method::name)
            .collect()
    }
}

impl std::fmt::Display for Method {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.write_str(self.name())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::collections::HashSet;

    /// **Proves:** every method name round-trips through `from_name`.
    /// **Catches:** a name typo or a variant left out of `ALL`.
    #[test]
    fn names_round_trip() {
        for m in Method::ALL {
            assert_eq!(Method::from_name(m.name()), Some(*m), "{}", m.name());
        }
        assert!(Method::from_name("dig.nope").is_none());
    }

    /// **Proves:** all wire names are unique.
    /// **Catches:** two variants accidentally sharing a name.
    #[test]
    fn names_unique() {
        let names: HashSet<&str> = Method::ALL.iter().map(|m| m.name()).collect();
        assert_eq!(names.len(), Method::ALL.len());
    }

    /// **Proves:** the peer allowlist is EXACTLY the ten methods the canonical
    /// node exposes over mTLS — and no `cache.*`/`control.*`/`dig.stage` leaks
    /// onto the peer surface.
    /// **Catches:** a drift from digstore `is_peer_reachable_method`, or a
    /// management method accidentally allowlisted (the audit #179 auth-bypass).
    #[test]
    fn peer_allowlist_matches_canonical() {
        let mut got = Method::peer_reachable_names();
        got.sort_unstable();
        let mut want = vec![
            "dig.getContent",
            "dig.getNetworkInfo",
            "dig.getPeers",
            "dig.announce",
            "dig.getAvailability",
            "dig.listInventory",
            "dig.fetchRange",
            "dig.getAnchoredRoot",
            "dig.getCollection",
            "dig.listCollectionItems",
        ];
        want.sort_unstable();
        assert_eq!(got, want);

        // No management/mutation method is peer-reachable.
        for m in [
            Method::Stage,
            Method::CacheGetConfig,
            Method::CacheSetCapBytes,
            Method::CacheClear,
            Method::CacheListCached,
            Method::CacheRemoveCached,
            Method::CacheFetchAndCache,
            Method::ControlPeerStatus,
            Method::RpcDiscover,
        ] {
            assert!(
                !m.is_peer_reachable(),
                "{} must NOT be peer-reachable",
                m.name()
            );
        }
    }

    /// **Proves:** the chain-anchored public reads are PublicRead yet ALSO
    /// peer-reachable (the design-brief tier decision §5).
    #[test]
    fn anchored_reads_public_but_peer_reachable() {
        for m in [
            Method::GetAnchoredRoot,
            Method::GetCollection,
            Method::ListCollectionItems,
        ] {
            assert_eq!(m.tier(), Tier::PublicRead);
            assert!(m.is_peer_reachable());
        }
    }

    /// **Proves:** every Control method is NOT peer-reachable (the boundary
    /// invariant).
    #[test]
    fn control_tier_never_peer_reachable() {
        for m in Method::ALL {
            if m.tier() == Tier::Control {
                assert!(
                    !m.is_peer_reachable(),
                    "{} is Control but peer-reachable",
                    m.name()
                );
            }
        }
    }
}