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}
55
56#[non_exhaustive]
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
63pub enum ControlMethod {
64 Status,
67 ConfigGet,
69 ConfigSetUpstream,
71 LogSetLevel,
73
74 CacheGet,
77 CacheSetCap,
79 CacheClear,
81
82 HostedStoresList,
85 HostedStoresPin,
87 HostedStoresUnpin,
89 HostedStoresStatus,
91
92 SyncStatus,
95 SyncTrigger,
97
98 UpdaterStatus,
101 UpdaterSetChannel,
103 UpdaterPause,
105 UpdaterResume,
107 UpdaterCheckNow,
109
110 PairingList,
113 PairingApprove,
115 PairingRevoke,
117
118 PeerStatus,
121 PeersConnect,
123 PeersDisconnect,
125
126 Subscribe,
129 Unsubscribe,
131 ListSubscriptions,
133
134 PairingRequest,
137 PairingPoll,
139}
140
141impl ControlMethod {
142 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 pub fn from_name(name: &str) -> Option<ControlMethod> {
179 ControlMethod::ALL
180 .iter()
181 .copied()
182 .find(|m| m.name() == name)
183 }
184
185 pub const fn requires_auth(self) -> bool {
190 !matches!(
191 self,
192 ControlMethod::PairingRequest | ControlMethod::PairingPoll
193 )
194 }
195
196 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 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 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 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 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}