1use serde::{Deserialize, Serialize};
5
6use crate::pod::{CommissionError, CommissionErrorCode, PodCandidate, WifiNetwork};
7
8pub const PROVISION_START_TARGET: &str = "/local/host/provision/start";
9pub const PROVISION_CODE_TARGET: &str = "/local/host/provision/code";
10pub const PROVISION_WIFI_TARGET: &str = "/local/host/provision/wifi";
11pub const PROVISION_WIFI_SCAN_TARGET: &str = "/local/host/provision/wifi/scan";
12pub const PROVISION_STATUS_TARGET: &str = "/local/host/provision/status";
13
14pub const PROVISION_CHANGED_EVENT: &str = "/local/host/provision/changed";
15
16pub const LOCAL_TARGETS: &[&str] = &[
17 PROVISION_START_TARGET,
18 PROVISION_CODE_TARGET,
19 PROVISION_WIFI_TARGET,
20 PROVISION_WIFI_SCAN_TARGET,
21 PROVISION_STATUS_TARGET,
22];
23pub const EVENT_TARGETS: &[&str] = &[PROVISION_CHANGED_EVENT];
24
25pub const APP_CAPABILITY: &str = "host";
27pub use crate::pod::{APP_INFO_LABEL, APP_INFO_LOCKED, APP_INFO_LOCKED_FOR_MS};
28pub const MAX_ATTRIBUTE_BYTES: usize = 512;
30pub const SCAN_TIMEOUT_MS: i64 = 12_000;
32pub const HOST_INFO_ENDPOINT: &str = "host-info";
33pub const HOST_ENDPOINT_UUIDS: &[(&str, u16)] = &[(HOST_INFO_ENDPOINT, 0xFF54)];
34pub const FINISH_GRACE_MS: i64 = 30_000;
36pub const CODE_LOCKOUT_MS: i64 = 30_000;
39pub const CODE_LOCKOUT_MAX_MS: i64 = 600_000;
40pub const CLAIM_TOKEN_LEN: usize = 43;
42
43pub fn is_claim_token(token: &str) -> bool {
44 token.len() == CLAIM_TOKEN_LEN
45 && token
46 .bytes()
47 .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-' || byte == b'_')
48}
49
50pub use crate::pod::{
54 CommissionCodeRequest as ProvisionCodeRequest,
55 CommissionSessionRequest as ProvisionSessionRequest,
56 CommissionStartRequest as ProvisionStartRequest, CommissionWifiRequest as ProvisionWifiRequest,
57};
58
59#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
60#[serde(rename_all = "snake_case")]
61pub enum ProvisionState {
62 Connecting,
63 AwaitingCode,
64 Securing,
65 ReadingInfo,
66 AwaitingWifi,
67 JoiningWifi,
68 Completed,
69 Failed,
70 TimedOut,
71}
72
73impl ProvisionState {
74 pub fn is_terminal(self) -> bool {
75 matches!(self, Self::Completed | Self::Failed | Self::TimedOut)
76 }
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
80#[serde(rename_all = "camelCase", deny_unknown_fields)]
81pub struct HostDeviceInfo {
82 pub host_id: String,
83 pub name: String,
84 #[serde(default, skip_serializing_if = "Option::is_none")]
85 pub product_id: Option<String>,
86 pub firmware_version: String,
87 pub fingerprint: String,
88 pub wifi_configured: bool,
89}
90
91#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
92#[serde(rename_all = "camelCase", deny_unknown_fields)]
93pub struct ProvisionSession {
94 pub session_id: String,
95 pub revision: u64,
96 pub state: ProvisionState,
97 pub candidate: PodCandidate,
98 pub expires_at_ms: i64,
99 #[serde(default, skip_serializing_if = "Option::is_none")]
100 pub host: Option<HostDeviceInfo>,
101 #[serde(default, skip_serializing_if = "Vec::is_empty")]
102 pub networks: Vec<WifiNetwork>,
103 #[serde(default, skip_serializing_if = "Vec::is_empty")]
105 pub addresses: Vec<String>,
106 #[serde(default, skip_serializing_if = "Option::is_none")]
108 pub claim_token: Option<String>,
109 #[serde(default, skip_serializing_if = "Option::is_none")]
110 pub error: Option<CommissionError>,
111}
112
113impl ProvisionSession {
114 pub fn validate(&self) -> Result<(), &'static str> {
115 match (self.state, self.error.as_ref().map(|error| error.code)) {
116 (ProvisionState::Failed | ProvisionState::TimedOut, None) => {
117 return Err("failed and timed_out sessions carry an error");
118 }
119 (ProvisionState::Failed | ProvisionState::TimedOut, Some(_))
120 | (_, None)
121 | (ProvisionState::AwaitingCode, Some(CommissionErrorCode::CodeRejected))
122 | (
123 ProvisionState::AwaitingWifi,
124 Some(
125 CommissionErrorCode::WifiAuthFailed
126 | CommissionErrorCode::WifiNotFound
127 | CommissionErrorCode::WifiFailed,
128 ),
129 ) => {}
130 _ => {
131 return Err(
132 "only failed, timed_out and retryable code or Wi-Fi states carry an error",
133 );
134 }
135 }
136 if let Some(code) = self.error.as_ref().map(|error| error.code) {
137 if matches!(
138 code,
139 CommissionErrorCode::IssueFailed
140 | CommissionErrorCode::DeliveryFailed
141 | CommissionErrorCode::ActivationFailed
142 ) {
143 return Err("credential error codes do not apply to Host provisioning");
144 }
145 }
146 if self.state == ProvisionState::Completed && self.host.is_none() {
147 return Err("completed sessions carry the Host information");
148 }
149 match self.claim_token.as_deref() {
150 Some(_) if self.state != ProvisionState::Completed => {
151 return Err("only completed sessions carry a claim token");
152 }
153 Some(token) if !is_claim_token(token) => {
154 return Err("claim tokens are 43 base64url characters");
155 }
156 _ => {}
157 }
158 Ok(())
159 }
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
163#[serde(rename_all = "camelCase", deny_unknown_fields)]
164pub struct ProvisionStatus {
165 pub session: Option<ProvisionSession>,
166}
167
168#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
171#[serde(rename_all = "camelCase")]
172pub struct HostFinishResponse {
173 pub ok: bool,
174 #[serde(default, skip_serializing_if = "Option::is_none")]
175 pub claim_token: Option<String>,
176}