Skip to main content

dig_node_control_interface/
method.rs

1//! The canonical control-method catalog.
2//!
3//! [`ControlMethod`] enumerates every method a client can send to a running dig-node's CONTROL
4//! plane, its stable wire name, whether it requires the local control token, whether it is a
5//! pairing-administration method (which requires the MASTER token specifically), and how the node
6//! routes it (owned by the service shell, delegated to the embedded node engine, or an open
7//! pairing-bootstrap method reachable without a token).
8//!
9//! This is the SINGLE source of truth for "what can be controlled". The node dispatchers, the
10//! client SDKs (CLI `dign`, the extension, dig-app, hub), the OpenRPC/discovery surface, and the
11//! conformance KATs all read this one table, so the method set can never drift between them.
12//!
13//! Mirrors the live dig-node surface: the shell-owned methods in
14//! `dig-node-service/src/control.rs` (`CONTROL_METHODS`) plus the peer/subscription methods
15//! delegated to `dig-node-core` (`control.peerStatus` / `control.peers.*` / `control.subscribe`
16//! / `control.unsubscribe` / `control.listSubscriptions`), and the two OPEN pairing-bootstrap
17//! methods (`pairing.request` / `pairing.poll`) a token-less MV3 extension uses to obtain a
18//! scoped token after local operator approval.
19
20/// How the node resolves a control method — the routing source of truth.
21#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22pub enum Routing {
23    /// Answered by the dig-node service shell itself (config/status/cache/pins/sync/updater/pairing-admin).
24    Owned,
25    /// Delegated to the embedded dig-node engine's own control surface (peers + subscriptions).
26    Delegated,
27    /// An OPEN bootstrap method reachable WITHOUT the control token (pairing handshake).
28    OpenBootstrap,
29}
30
31/// The functional area a control method belongs to — for grouping in UIs and docs.
32#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
33pub enum Category {
34    /// Node status snapshot.
35    Status,
36    /// Node configuration (upstream override).
37    Config,
38    /// Live log-level control.
39    Log,
40    /// On-disk content cache.
41    Cache,
42    /// Hosted/pinned stores.
43    HostedStores,
44    /// §21 authenticated whole-store sync.
45    Sync,
46    /// The DIG auto-update beacon proxy.
47    Updater,
48    /// Control-token pairing lifecycle.
49    Pairing,
50    /// The L7 peer network.
51    Peers,
52    /// The node's subscribed-store set.
53    Subscriptions,
54}
55
56/// A dig-node CONTROL method.
57///
58/// `#[non_exhaustive]` so adding a method in a minor release is additive; downstream matches must
59/// carry a `_ => …` arm. Convert to/from the wire name with [`ControlMethod::name`] /
60/// [`ControlMethod::from_name`].
61#[non_exhaustive]
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
63pub enum ControlMethod {
64    // ---- Status / config / log (shell-owned) ----
65    /// `control.status` — a rich node status snapshot.
66    Status,
67    /// `control.config.get` — the node's effective configuration.
68    ConfigGet,
69    /// `control.config.setUpstream` — persist an upstream-RPC override (effective on restart).
70    ConfigSetUpstream,
71    /// `control.log.setLevel` — live-swap the running node's tracing level filter.
72    LogSetLevel,
73
74    // ---- Cache (shell-owned) ----
75    /// `control.cache.get` — the on-disk cache view (cap/used/dir/shared).
76    CacheGet,
77    /// `control.cache.setCap` — set the cache size cap (floored at 64 MiB).
78    CacheSetCap,
79    /// `control.cache.clear` — delete all locally cached content.
80    CacheClear,
81
82    // ---- Hosted stores (shell-owned) ----
83    /// `control.hostedStores.list` — every held/pinned store with its cached capsules.
84    HostedStoresList,
85    /// `control.hostedStores.pin` — pin a store (and pre-fetch when a root is given).
86    HostedStoresPin,
87    /// `control.hostedStores.unpin` — unpin a store and evict its cached capsules.
88    HostedStoresUnpin,
89    /// `control.hostedStores.status` — per-store pinned flag + cached capsules.
90    HostedStoresStatus,
91
92    // ---- §21 sync (shell-owned) ----
93    /// `control.sync.status` — whether authenticated whole-store sync is available + pin coverage.
94    SyncStatus,
95    /// `control.sync.trigger` — trigger a §21 sync for one capsule (storeId + root).
96    SyncTrigger,
97
98    // ---- Updater beacon proxy (shell-owned) ----
99    /// `control.updater.status` — the DIG auto-update beacon's current status.
100    UpdaterStatus,
101    /// `control.updater.setChannel` — set the beacon's update channel.
102    UpdaterSetChannel,
103    /// `control.updater.pause` — suspend auto-updates (optionally until a unix time).
104    UpdaterPause,
105    /// `control.updater.resume` — resume auto-updates.
106    UpdaterResume,
107    /// `control.updater.checkNow` — force an immediate update check.
108    UpdaterCheckNow,
109
110    // ---- Pairing administration (shell-owned, MASTER-token only) ----
111    /// `control.pairing.list` — list pending pairing requests + issued paired tokens.
112    PairingList,
113    /// `control.pairing.approve` — approve a pending pairing, minting a scoped token.
114    PairingApprove,
115    /// `control.pairing.revoke` — revoke an issued paired token.
116    PairingRevoke,
117
118    // ---- Peers (delegated to the engine) ----
119    /// `control.peerStatus` — live peer-pool + relay-reservation snapshot.
120    PeerStatus,
121    /// `control.peers.connect` — dial a peer by address / resolve a connected peer_id.
122    PeersConnect,
123    /// `control.peers.disconnect` — drop a pooled peer by peer_id.
124    PeersDisconnect,
125
126    // ---- Subscriptions (delegated to the engine) ----
127    /// `control.subscribe` — subscribe the node to a store (watch + gap-fill).
128    Subscribe,
129    /// `control.unsubscribe` — stop watching a store.
130    Unsubscribe,
131    /// `control.listSubscriptions` — the node's persisted subscription set.
132    ListSubscriptions,
133
134    // ---- Pairing bootstrap (OPEN — no token) ----
135    /// `pairing.request` — request a control-token pairing (returns a code to compare).
136    PairingRequest,
137    /// `pairing.poll` — poll a pairing; once the operator approves, returns the scoped token once.
138    PairingPoll,
139}
140
141impl ControlMethod {
142    /// The stable JSON-RPC wire name. Never derived from anything else — the published contract.
143    pub const fn name(self) -> &'static str {
144        match self {
145            ControlMethod::Status => "control.status",
146            ControlMethod::ConfigGet => "control.config.get",
147            ControlMethod::ConfigSetUpstream => "control.config.setUpstream",
148            ControlMethod::LogSetLevel => "control.log.setLevel",
149            ControlMethod::CacheGet => "control.cache.get",
150            ControlMethod::CacheSetCap => "control.cache.setCap",
151            ControlMethod::CacheClear => "control.cache.clear",
152            ControlMethod::HostedStoresList => "control.hostedStores.list",
153            ControlMethod::HostedStoresPin => "control.hostedStores.pin",
154            ControlMethod::HostedStoresUnpin => "control.hostedStores.unpin",
155            ControlMethod::HostedStoresStatus => "control.hostedStores.status",
156            ControlMethod::SyncStatus => "control.sync.status",
157            ControlMethod::SyncTrigger => "control.sync.trigger",
158            ControlMethod::UpdaterStatus => "control.updater.status",
159            ControlMethod::UpdaterSetChannel => "control.updater.setChannel",
160            ControlMethod::UpdaterPause => "control.updater.pause",
161            ControlMethod::UpdaterResume => "control.updater.resume",
162            ControlMethod::UpdaterCheckNow => "control.updater.checkNow",
163            ControlMethod::PairingList => "control.pairing.list",
164            ControlMethod::PairingApprove => "control.pairing.approve",
165            ControlMethod::PairingRevoke => "control.pairing.revoke",
166            ControlMethod::PeerStatus => "control.peerStatus",
167            ControlMethod::PeersConnect => "control.peers.connect",
168            ControlMethod::PeersDisconnect => "control.peers.disconnect",
169            ControlMethod::Subscribe => "control.subscribe",
170            ControlMethod::Unsubscribe => "control.unsubscribe",
171            ControlMethod::ListSubscriptions => "control.listSubscriptions",
172            ControlMethod::PairingRequest => "pairing.request",
173            ControlMethod::PairingPoll => "pairing.poll",
174        }
175    }
176
177    /// Resolve a wire name back to its [`ControlMethod`], or `None` for an unknown name.
178    pub fn from_name(name: &str) -> Option<ControlMethod> {
179        ControlMethod::ALL
180            .iter()
181            .copied()
182            .find(|m| m.name() == name)
183    }
184
185    /// Does calling this method require the local control token?
186    ///
187    /// Every `control.*` method is token-gated; the two OPEN pairing-bootstrap methods
188    /// (`pairing.request` / `pairing.poll`) are not, so a token-less client can obtain a token.
189    pub const fn requires_auth(self) -> bool {
190        !matches!(
191            self,
192            ControlMethod::PairingRequest | ControlMethod::PairingPoll
193        )
194    }
195
196    /// Is this a PAIRING-ADMINISTRATION method that requires the MASTER control token specifically?
197    ///
198    /// A paired (scoped) token can drive ordinary `control.*` mutations but MUST NOT mint more
199    /// tokens or revoke itself — so listing/approving/revoking pairings requires the master token
200    /// (a local file read), never a paired token.
201    pub const fn is_pairing_admin(self) -> bool {
202        matches!(
203            self,
204            ControlMethod::PairingList
205                | ControlMethod::PairingApprove
206                | ControlMethod::PairingRevoke
207        )
208    }
209
210    /// How the node routes this method (shell-owned, engine-delegated, or open bootstrap).
211    pub const fn routing(self) -> Routing {
212        match self {
213            ControlMethod::PeerStatus
214            | ControlMethod::PeersConnect
215            | ControlMethod::PeersDisconnect
216            | ControlMethod::Subscribe
217            | ControlMethod::Unsubscribe
218            | ControlMethod::ListSubscriptions => Routing::Delegated,
219            ControlMethod::PairingRequest | ControlMethod::PairingPoll => Routing::OpenBootstrap,
220            _ => Routing::Owned,
221        }
222    }
223
224    /// The functional area this method belongs to.
225    pub const fn category(self) -> Category {
226        match self {
227            ControlMethod::Status => Category::Status,
228            ControlMethod::ConfigGet | ControlMethod::ConfigSetUpstream => Category::Config,
229            ControlMethod::LogSetLevel => Category::Log,
230            ControlMethod::CacheGet | ControlMethod::CacheSetCap | ControlMethod::CacheClear => {
231                Category::Cache
232            }
233            ControlMethod::HostedStoresList
234            | ControlMethod::HostedStoresPin
235            | ControlMethod::HostedStoresUnpin
236            | ControlMethod::HostedStoresStatus => Category::HostedStores,
237            ControlMethod::SyncStatus | ControlMethod::SyncTrigger => Category::Sync,
238            ControlMethod::UpdaterStatus
239            | ControlMethod::UpdaterSetChannel
240            | ControlMethod::UpdaterPause
241            | ControlMethod::UpdaterResume
242            | ControlMethod::UpdaterCheckNow => Category::Updater,
243            ControlMethod::PairingList
244            | ControlMethod::PairingApprove
245            | ControlMethod::PairingRevoke
246            | ControlMethod::PairingRequest
247            | ControlMethod::PairingPoll => Category::Pairing,
248            ControlMethod::PeerStatus
249            | ControlMethod::PeersConnect
250            | ControlMethod::PeersDisconnect => Category::Peers,
251            ControlMethod::Subscribe
252            | ControlMethod::Unsubscribe
253            | ControlMethod::ListSubscriptions => Category::Subscriptions,
254        }
255    }
256
257    /// A one-line human/agent description for the discovery catalogue.
258    pub const fn summary(self) -> &'static str {
259        match self {
260            ControlMethod::Status => "A rich node status snapshot (version, uptime, addr, cache, hosted/pinned counts, sync availability).",
261            ControlMethod::ConfigGet => "The node's effective configuration (addr/port, upstream + override, cache dir/shared, config path, sync availability).",
262            ControlMethod::ConfigSetUpstream => "Persist an upstream-RPC override; takes effect on next node start (requires_restart).",
263            ControlMethod::LogSetLevel => "Live-swap the running node's tracing EnvFilter directive (not persisted).",
264            ControlMethod::CacheGet => "The on-disk content-cache view: cap_bytes, used_bytes, dir, shared.",
265            ControlMethod::CacheSetCap => "Set the on-disk cache size cap in bytes (floored at 64 MiB).",
266            ControlMethod::CacheClear => "Delete all locally cached DIG content.",
267            ControlMethod::HostedStoresList => "Every held/pinned store, merged, with each store's cached capsules and a pinned flag.",
268            ControlMethod::HostedStoresPin => "Pin a store (storeId[:rootHash]); pre-fetches the capsule when a root is given and §21 sync is available.",
269            ControlMethod::HostedStoresUnpin => "Unpin a store and evict its cached capsules.",
270            ControlMethod::HostedStoresStatus => "Per-store status: pinned flag, cached capsules, total bytes.",
271            ControlMethod::SyncStatus => "Whether authenticated §21 whole-store sync is available, plus pinned-store cache coverage.",
272            ControlMethod::SyncTrigger => "Trigger a §21 sync for one capsule (storeId + root).",
273            ControlMethod::UpdaterStatus => "The DIG auto-update beacon's current status (proxied from dig-updater).",
274            ControlMethod::UpdaterSetChannel => "Set the beacon's update channel (\"nightly\" | \"stable\").",
275            ControlMethod::UpdaterPause => "Suspend the beacon's auto-updates (optionally until a unix time).",
276            ControlMethod::UpdaterResume => "Resume the beacon's auto-updates.",
277            ControlMethod::UpdaterCheckNow => "Force an immediate beacon update check.",
278            ControlMethod::PairingList => "List pending pairing requests and issued paired tokens (MASTER token only).",
279            ControlMethod::PairingApprove => "Approve a pending pairing, minting a scoped token (MASTER token only).",
280            ControlMethod::PairingRevoke => "Revoke an issued paired token by token_id (MASTER token only).",
281            ControlMethod::PeerStatus => "Live peer-pool + relay-reservation snapshot, including the per-peer connected array.",
282            ControlMethod::PeersConnect => "Dial a peer by address, or resolve an already-connected peer_id, via the live gossip pool.",
283            ControlMethod::PeersDisconnect => "Drop a pooled peer by peer_id, closing its mTLS link (idempotent).",
284            ControlMethod::Subscribe => "Subscribe the node to a store it actively watches and gap-fills.",
285            ControlMethod::Unsubscribe => "Stop watching a store.",
286            ControlMethod::ListSubscriptions => "The node's persisted subscription set + count.",
287            ControlMethod::PairingRequest => "OPEN: request a control-token pairing; returns a pairing_id + pairing_code to compare.",
288            ControlMethod::PairingPoll => "OPEN: poll a pairing by id; once the operator approves, returns the scoped token once.",
289        }
290    }
291
292    /// Every catalogued method, in a stable order — the enumeration a machine reads to discover the
293    /// full control surface, and the anchor the conformance KATs pin against.
294    pub const ALL: &'static [ControlMethod] = &[
295        ControlMethod::Status,
296        ControlMethod::ConfigGet,
297        ControlMethod::ConfigSetUpstream,
298        ControlMethod::LogSetLevel,
299        ControlMethod::CacheGet,
300        ControlMethod::CacheSetCap,
301        ControlMethod::CacheClear,
302        ControlMethod::HostedStoresList,
303        ControlMethod::HostedStoresPin,
304        ControlMethod::HostedStoresUnpin,
305        ControlMethod::HostedStoresStatus,
306        ControlMethod::SyncStatus,
307        ControlMethod::SyncTrigger,
308        ControlMethod::UpdaterStatus,
309        ControlMethod::UpdaterSetChannel,
310        ControlMethod::UpdaterPause,
311        ControlMethod::UpdaterResume,
312        ControlMethod::UpdaterCheckNow,
313        ControlMethod::PairingList,
314        ControlMethod::PairingApprove,
315        ControlMethod::PairingRevoke,
316        ControlMethod::PeerStatus,
317        ControlMethod::PeersConnect,
318        ControlMethod::PeersDisconnect,
319        ControlMethod::Subscribe,
320        ControlMethod::Unsubscribe,
321        ControlMethod::ListSubscriptions,
322        ControlMethod::PairingRequest,
323        ControlMethod::PairingPoll,
324    ];
325}
326
327#[cfg(test)]
328mod tests {
329    use super::*;
330    use std::collections::BTreeSet;
331
332    #[test]
333    fn every_method_has_a_unique_wire_name() {
334        let names: BTreeSet<&str> = ControlMethod::ALL.iter().map(|m| m.name()).collect();
335        assert_eq!(
336            names.len(),
337            ControlMethod::ALL.len(),
338            "duplicate or missing wire names in the catalog"
339        );
340    }
341
342    #[test]
343    fn from_name_round_trips_every_method() {
344        for &m in ControlMethod::ALL {
345            assert_eq!(ControlMethod::from_name(m.name()), Some(m));
346        }
347        assert_eq!(ControlMethod::from_name("control.nope"), None);
348        assert_eq!(ControlMethod::from_name(""), None);
349    }
350
351    #[test]
352    fn only_pairing_bootstrap_is_open() {
353        for &m in ControlMethod::ALL {
354            let open = matches!(
355                m,
356                ControlMethod::PairingRequest | ControlMethod::PairingPoll
357            );
358            assert_eq!(m.requires_auth(), !open, "{} auth mismatch", m.name());
359            assert_eq!(
360                m.routing() == Routing::OpenBootstrap,
361                open,
362                "{} routing mismatch",
363                m.name()
364            );
365        }
366    }
367
368    #[test]
369    fn pairing_admin_methods_are_exactly_three() {
370        let admin: Vec<&str> = ControlMethod::ALL
371            .iter()
372            .filter(|m| m.is_pairing_admin())
373            .map(|m| m.name())
374            .collect();
375        assert_eq!(
376            admin,
377            vec![
378                "control.pairing.list",
379                "control.pairing.approve",
380                "control.pairing.revoke"
381            ]
382        );
383    }
384
385    #[test]
386    fn delegated_set_matches_the_engine_surface() {
387        let delegated: BTreeSet<&str> = ControlMethod::ALL
388            .iter()
389            .filter(|m| m.routing() == Routing::Delegated)
390            .map(|m| m.name())
391            .collect();
392        let expected: BTreeSet<&str> = [
393            "control.peerStatus",
394            "control.peers.connect",
395            "control.peers.disconnect",
396            "control.subscribe",
397            "control.unsubscribe",
398            "control.listSubscriptions",
399        ]
400        .into_iter()
401        .collect();
402        assert_eq!(delegated, expected);
403    }
404
405    #[test]
406    fn every_method_has_a_nonempty_summary() {
407        for &m in ControlMethod::ALL {
408            assert!(!m.summary().is_empty(), "{} has no summary", m.name());
409        }
410    }
411}