Skip to main content

core_api/
node_commissioning.rs

1//! LAN Node commissioning: authenticated checks and durable identity acceptance.
2use serde::{Deserialize, Serialize};
3use serde_json::{Map, Value};
4
5pub const NODE_PREFLIGHT_SCHEMA_VERSION: &str = "mhome.node.preflight.v1";
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8#[serde(rename_all = "camelCase")]
9pub struct NodeCommissioningAcceptRequest {
10    pub token: String,
11    pub node_id: String,
12    pub hub_id: String,
13    pub hub_url: String,
14}
15#[derive(Debug, Clone, Serialize, Deserialize)]
16#[serde(rename_all = "camelCase")]
17pub struct NodeCommissioningAcceptResponse {
18    pub ok: bool,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
22#[serde(rename_all = "camelCase")]
23pub struct NodeCommissioningErrorResponse {
24    pub ok: bool,
25    pub reason: String,
26    pub error: String,
27    #[serde(skip_serializing_if = "Option::is_none")]
28    pub detail: Option<String>,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
32#[serde(rename_all = "camelCase")]
33pub struct NodeCommissioningPrepareRequest {
34    #[serde(flatten)]
35    pub commissioning: NodeCommissioningAcceptRequest,
36}
37#[derive(Debug, Clone, Serialize, Deserialize)]
38#[serde(rename_all = "camelCase")]
39pub struct NodeCommissioningStatusRequest {
40    #[serde(flatten)]
41    pub commissioning: NodeCommissioningAcceptRequest,
42}
43#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
44#[serde(rename_all = "snake_case")]
45pub enum NodePreflightState {
46    Preparing,
47    Ready,
48    Failed,
49}
50#[derive(Debug, Clone, Serialize, Deserialize)]
51#[serde(rename_all = "camelCase")]
52pub struct NodePreflightError {
53    pub code: String,
54    pub detail: String,
55    pub retryable: bool,
56}
57#[derive(Debug, Clone, Serialize, Deserialize)]
58#[serde(rename_all = "camelCase")]
59pub struct NodePreflightReadiness {
60    pub schema_version: String,
61    pub state: NodePreflightState,
62    pub revision: u64,
63    pub commissionable: bool,
64    pub runtime_usable: bool,
65    #[serde(deserialize_with = "required_nullable_error")]
66    pub error: Option<NodePreflightError>,
67    pub details: Map<String, Value>,
68}
69fn required_nullable_error<'de, D: serde::Deserializer<'de>>(
70    deserializer: D,
71) -> Result<Option<NodePreflightError>, D::Error> {
72    Option::<NodePreflightError>::deserialize(deserializer)
73}
74impl NodePreflightReadiness {
75    pub fn validate(&self) -> Result<(), &'static str> {
76        if self.schema_version != NODE_PREFLIGHT_SCHEMA_VERSION {
77            return Err("unsupported preflight schema");
78        }
79        if self.commissionable && (!self.runtime_usable || self.state != NodePreflightState::Ready)
80        {
81            return Err("commissionable requires a ready, usable runtime");
82        }
83        if (self.state == NodePreflightState::Failed) != self.error.is_some() {
84            return Err("failed preflight requires an error, other states must not contain one");
85        }
86        if self
87            .error
88            .as_ref()
89            .is_some_and(|e| e.code.trim().is_empty() || e.detail.trim().is_empty())
90        {
91            return Err("preflight error must include code and detail");
92        }
93        Ok(())
94    }
95}
96#[derive(Debug, Clone, Serialize, Deserialize)]
97#[serde(rename_all = "camelCase")]
98pub struct NodeCommissioningPrepareResponse {
99    pub ok: bool,
100    pub readiness: NodePreflightReadiness,
101}
102#[derive(Debug, Clone, Serialize, Deserialize)]
103#[serde(rename_all = "camelCase")]
104pub struct NodeCommissioningStatusResponse {
105    pub ok: bool,
106    pub readiness: NodePreflightReadiness,
107}
108
109#[cfg(test)]
110mod tests {
111    use super::*;
112    #[test]
113    fn requires_nullable_error_field() {
114        let mut value = serde_json::json!({"schemaVersion":NODE_PREFLIGHT_SCHEMA_VERSION,"state":"ready",
115            "revision":1,"commissionable":true,"runtimeUsable":true,"details":{}});
116        assert!(serde_json::from_value::<NodePreflightReadiness>(value.clone()).is_err());
117        value["error"] = Value::Null;
118        assert!(serde_json::from_value::<NodePreflightReadiness>(value)
119            .unwrap()
120            .validate()
121            .is_ok());
122    }
123    #[test]
124    fn rejects_private_readiness_and_inconsistent_success() {
125        assert!(serde_json::from_value::<NodePreflightReadiness>(
126            serde_json::json!({"commissionable":true})
127        )
128        .is_err());
129        let mut r = NodePreflightReadiness {
130            schema_version: NODE_PREFLIGHT_SCHEMA_VERSION.into(),
131            state: NodePreflightState::Ready,
132            revision: 1,
133            commissionable: true,
134            runtime_usable: true,
135            error: None,
136            details: Map::new(),
137        };
138        assert!(r.validate().is_ok());
139        r.runtime_usable = false;
140        assert!(r.validate().is_err());
141        r.runtime_usable = true;
142        r.schema_version = "matter.readiness.v1".into();
143        assert!(r.validate().is_err());
144    }
145}