1#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
22pub enum Routing {
23 Owned,
25 Delegated,
27 OpenBootstrap,
29}
30
31#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
33pub enum Category {
34 Status,
36 Config,
38 Log,
40 Cache,
42 HostedStores,
44 Sync,
46 Updater,
48 Pairing,
50 Peers,
52 Subscriptions,
54 Wallet,
56}
57
58#[non_exhaustive]
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
65pub enum ControlMethod {
66 Status,
69 ConfigGet,
71 ConfigSetUpstream,
73 LogSetLevel,
75
76 CacheGet,
79 CacheSetCap,
81 CacheClear,
83
84 HostedStoresList,
87 HostedStoresPin,
89 HostedStoresUnpin,
91 HostedStoresStatus,
93
94 SyncStatus,
97 SyncTrigger,
99
100 UpdaterStatus,
103 UpdaterSetChannel,
105 UpdaterPause,
107 UpdaterResume,
109 UpdaterCheckNow,
111
112 PairingList,
115 PairingApprove,
117 PairingRevoke,
119
120 PeerStatus,
123 PeersConnect,
125 PeersDisconnect,
127
128 Subscribe,
131 Unsubscribe,
133 ListSubscriptions,
135
136 WalletBalance,
139
140 PairingRequest,
143 PairingPoll,
145}
146
147impl ControlMethod {
148 pub const fn name(self) -> &'static str {
150 match self {
151 ControlMethod::Status => "control.status",
152 ControlMethod::ConfigGet => "control.config.get",
153 ControlMethod::ConfigSetUpstream => "control.config.setUpstream",
154 ControlMethod::LogSetLevel => "control.log.setLevel",
155 ControlMethod::CacheGet => "control.cache.get",
156 ControlMethod::CacheSetCap => "control.cache.setCap",
157 ControlMethod::CacheClear => "control.cache.clear",
158 ControlMethod::HostedStoresList => "control.hostedStores.list",
159 ControlMethod::HostedStoresPin => "control.hostedStores.pin",
160 ControlMethod::HostedStoresUnpin => "control.hostedStores.unpin",
161 ControlMethod::HostedStoresStatus => "control.hostedStores.status",
162 ControlMethod::SyncStatus => "control.sync.status",
163 ControlMethod::SyncTrigger => "control.sync.trigger",
164 ControlMethod::UpdaterStatus => "control.updater.status",
165 ControlMethod::UpdaterSetChannel => "control.updater.setChannel",
166 ControlMethod::UpdaterPause => "control.updater.pause",
167 ControlMethod::UpdaterResume => "control.updater.resume",
168 ControlMethod::UpdaterCheckNow => "control.updater.checkNow",
169 ControlMethod::PairingList => "control.pairing.list",
170 ControlMethod::PairingApprove => "control.pairing.approve",
171 ControlMethod::PairingRevoke => "control.pairing.revoke",
172 ControlMethod::PeerStatus => "control.peerStatus",
173 ControlMethod::PeersConnect => "control.peers.connect",
174 ControlMethod::PeersDisconnect => "control.peers.disconnect",
175 ControlMethod::Subscribe => "control.subscribe",
176 ControlMethod::Unsubscribe => "control.unsubscribe",
177 ControlMethod::ListSubscriptions => "control.listSubscriptions",
178 ControlMethod::WalletBalance => "control.wallet.balance",
179 ControlMethod::PairingRequest => "pairing.request",
180 ControlMethod::PairingPoll => "pairing.poll",
181 }
182 }
183
184 pub fn from_name(name: &str) -> Option<ControlMethod> {
186 ControlMethod::ALL
187 .iter()
188 .copied()
189 .find(|m| m.name() == name)
190 }
191
192 pub const fn requires_auth(self) -> bool {
197 !matches!(
198 self,
199 ControlMethod::PairingRequest | ControlMethod::PairingPoll
200 )
201 }
202
203 pub const fn is_pairing_admin(self) -> bool {
209 matches!(
210 self,
211 ControlMethod::PairingList
212 | ControlMethod::PairingApprove
213 | ControlMethod::PairingRevoke
214 )
215 }
216
217 pub const fn routing(self) -> Routing {
219 match self {
220 ControlMethod::PeerStatus
221 | ControlMethod::PeersConnect
222 | ControlMethod::PeersDisconnect
223 | ControlMethod::Subscribe
224 | ControlMethod::Unsubscribe
225 | ControlMethod::ListSubscriptions
226 | ControlMethod::WalletBalance => Routing::Delegated,
227 ControlMethod::PairingRequest | ControlMethod::PairingPoll => Routing::OpenBootstrap,
228 _ => Routing::Owned,
229 }
230 }
231
232 pub const fn category(self) -> Category {
234 match self {
235 ControlMethod::Status => Category::Status,
236 ControlMethod::ConfigGet | ControlMethod::ConfigSetUpstream => Category::Config,
237 ControlMethod::LogSetLevel => Category::Log,
238 ControlMethod::CacheGet | ControlMethod::CacheSetCap | ControlMethod::CacheClear => {
239 Category::Cache
240 }
241 ControlMethod::HostedStoresList
242 | ControlMethod::HostedStoresPin
243 | ControlMethod::HostedStoresUnpin
244 | ControlMethod::HostedStoresStatus => Category::HostedStores,
245 ControlMethod::SyncStatus | ControlMethod::SyncTrigger => Category::Sync,
246 ControlMethod::UpdaterStatus
247 | ControlMethod::UpdaterSetChannel
248 | ControlMethod::UpdaterPause
249 | ControlMethod::UpdaterResume
250 | ControlMethod::UpdaterCheckNow => Category::Updater,
251 ControlMethod::PairingList
252 | ControlMethod::PairingApprove
253 | ControlMethod::PairingRevoke
254 | ControlMethod::PairingRequest
255 | ControlMethod::PairingPoll => Category::Pairing,
256 ControlMethod::PeerStatus
257 | ControlMethod::PeersConnect
258 | ControlMethod::PeersDisconnect => Category::Peers,
259 ControlMethod::Subscribe
260 | ControlMethod::Unsubscribe
261 | ControlMethod::ListSubscriptions => Category::Subscriptions,
262 ControlMethod::WalletBalance => Category::Wallet,
263 }
264 }
265
266 pub const fn summary(self) -> &'static str {
268 match self {
269 ControlMethod::Status => "A rich node status snapshot (version, uptime, addr, cache, hosted/pinned counts, sync availability).",
270 ControlMethod::ConfigGet => "The node's effective configuration (addr/port, upstream + override, cache dir/shared, config path, sync availability).",
271 ControlMethod::ConfigSetUpstream => "Persist an upstream-RPC override; takes effect on next node start (requires_restart).",
272 ControlMethod::LogSetLevel => "Live-swap the running node's tracing EnvFilter directive (not persisted).",
273 ControlMethod::CacheGet => "The on-disk content-cache view: cap_bytes, used_bytes, dir, shared.",
274 ControlMethod::CacheSetCap => "Set the on-disk cache size cap in bytes (floored at 64 MiB).",
275 ControlMethod::CacheClear => "Delete all locally cached DIG content.",
276 ControlMethod::HostedStoresList => "Every held/pinned store, merged, with each store's cached capsules and a pinned flag.",
277 ControlMethod::HostedStoresPin => "Pin a store (storeId[:rootHash]); pre-fetches the capsule when a root is given and §21 sync is available.",
278 ControlMethod::HostedStoresUnpin => "Unpin a store and evict its cached capsules.",
279 ControlMethod::HostedStoresStatus => "Per-store status: pinned flag, cached capsules, total bytes.",
280 ControlMethod::SyncStatus => "Whether authenticated §21 whole-store sync is available, plus pinned-store cache coverage.",
281 ControlMethod::SyncTrigger => "Trigger a §21 sync for one capsule (storeId + root).",
282 ControlMethod::UpdaterStatus => "The DIG auto-update beacon's current status (proxied from dig-updater).",
283 ControlMethod::UpdaterSetChannel => "Set the beacon's update channel (\"nightly\" | \"stable\").",
284 ControlMethod::UpdaterPause => "Suspend the beacon's auto-updates (optionally until a unix time).",
285 ControlMethod::UpdaterResume => "Resume the beacon's auto-updates.",
286 ControlMethod::UpdaterCheckNow => "Force an immediate beacon update check.",
287 ControlMethod::PairingList => "List pending pairing requests and issued paired tokens (MASTER token only).",
288 ControlMethod::PairingApprove => "Approve a pending pairing, minting a scoped token (MASTER token only).",
289 ControlMethod::PairingRevoke => "Revoke an issued paired token by token_id (MASTER token only).",
290 ControlMethod::PeerStatus => "Live peer-pool + relay-reservation snapshot, including the per-peer connected array.",
291 ControlMethod::PeersConnect => "Dial a peer by address, or resolve an already-connected peer_id, via the live gossip pool.",
292 ControlMethod::PeersDisconnect => "Drop a pooled peer by peer_id, closing its mTLS link (idempotent).",
293 ControlMethod::Subscribe => "Subscribe the node to a store it actively watches and gap-fills.",
294 ControlMethod::Unsubscribe => "Stop watching a store.",
295 ControlMethod::ListSubscriptions => "The node's persisted subscription set + count.",
296 ControlMethod::WalletBalance => "READ-only: the confirmed spendable balance for an address + asset (plus pending, sync freshness, and the peak height it reflects).",
297 ControlMethod::PairingRequest => "OPEN: request a control-token pairing; returns a pairing_id + pairing_code to compare.",
298 ControlMethod::PairingPoll => "OPEN: poll a pairing by id; once the operator approves, returns the scoped token once.",
299 }
300 }
301
302 pub const ALL: &'static [ControlMethod] = &[
305 ControlMethod::Status,
306 ControlMethod::ConfigGet,
307 ControlMethod::ConfigSetUpstream,
308 ControlMethod::LogSetLevel,
309 ControlMethod::CacheGet,
310 ControlMethod::CacheSetCap,
311 ControlMethod::CacheClear,
312 ControlMethod::HostedStoresList,
313 ControlMethod::HostedStoresPin,
314 ControlMethod::HostedStoresUnpin,
315 ControlMethod::HostedStoresStatus,
316 ControlMethod::SyncStatus,
317 ControlMethod::SyncTrigger,
318 ControlMethod::UpdaterStatus,
319 ControlMethod::UpdaterSetChannel,
320 ControlMethod::UpdaterPause,
321 ControlMethod::UpdaterResume,
322 ControlMethod::UpdaterCheckNow,
323 ControlMethod::PairingList,
324 ControlMethod::PairingApprove,
325 ControlMethod::PairingRevoke,
326 ControlMethod::PeerStatus,
327 ControlMethod::PeersConnect,
328 ControlMethod::PeersDisconnect,
329 ControlMethod::Subscribe,
330 ControlMethod::Unsubscribe,
331 ControlMethod::ListSubscriptions,
332 ControlMethod::WalletBalance,
333 ControlMethod::PairingRequest,
334 ControlMethod::PairingPoll,
335 ];
336}
337
338#[cfg(test)]
339mod tests {
340 use super::*;
341 use std::collections::BTreeSet;
342
343 #[test]
344 fn every_method_has_a_unique_wire_name() {
345 let names: BTreeSet<&str> = ControlMethod::ALL.iter().map(|m| m.name()).collect();
346 assert_eq!(
347 names.len(),
348 ControlMethod::ALL.len(),
349 "duplicate or missing wire names in the catalog"
350 );
351 }
352
353 #[test]
354 fn from_name_round_trips_every_method() {
355 for &m in ControlMethod::ALL {
356 assert_eq!(ControlMethod::from_name(m.name()), Some(m));
357 }
358 assert_eq!(ControlMethod::from_name("control.nope"), None);
359 assert_eq!(ControlMethod::from_name(""), None);
360 }
361
362 #[test]
363 fn only_pairing_bootstrap_is_open() {
364 for &m in ControlMethod::ALL {
365 let open = matches!(
366 m,
367 ControlMethod::PairingRequest | ControlMethod::PairingPoll
368 );
369 assert_eq!(m.requires_auth(), !open, "{} auth mismatch", m.name());
370 assert_eq!(
371 m.routing() == Routing::OpenBootstrap,
372 open,
373 "{} routing mismatch",
374 m.name()
375 );
376 }
377 }
378
379 #[test]
380 fn pairing_admin_methods_are_exactly_three() {
381 let admin: Vec<&str> = ControlMethod::ALL
382 .iter()
383 .filter(|m| m.is_pairing_admin())
384 .map(|m| m.name())
385 .collect();
386 assert_eq!(
387 admin,
388 vec![
389 "control.pairing.list",
390 "control.pairing.approve",
391 "control.pairing.revoke"
392 ]
393 );
394 }
395
396 #[test]
397 fn delegated_set_matches_the_engine_surface() {
398 let delegated: BTreeSet<&str> = ControlMethod::ALL
399 .iter()
400 .filter(|m| m.routing() == Routing::Delegated)
401 .map(|m| m.name())
402 .collect();
403 let expected: BTreeSet<&str> = [
404 "control.peerStatus",
405 "control.peers.connect",
406 "control.peers.disconnect",
407 "control.subscribe",
408 "control.unsubscribe",
409 "control.listSubscriptions",
410 "control.wallet.balance",
411 ]
412 .into_iter()
413 .collect();
414 assert_eq!(delegated, expected);
415 }
416
417 #[test]
418 fn every_method_has_a_nonempty_summary() {
419 for &m in ControlMethod::ALL {
420 assert!(!m.summary().is_empty(), "{} has no summary", m.name());
421 }
422 }
423}