auths_sdk/workflows/
status.rs1use crate::result::{DeviceReadiness, NextStep};
4use chrono::{DateTime, Duration, Utc};
5
6pub struct StatusWorkflow;
13
14impl StatusWorkflow {
15 pub fn next_steps_from_readiness(
25 identity_present: bool,
26 readinesses: &[DeviceReadiness],
27 agent_running: bool,
28 ) -> Vec<NextStep> {
29 let mut steps = Vec::new();
30
31 if !identity_present {
33 steps.push(NextStep {
34 summary: "Initialize your identity".to_string(),
35 command: "auths init --profile developer".to_string(),
36 });
37 return steps;
38 }
39
40 if readinesses.is_empty() {
42 steps.push(NextStep {
43 summary: "Link this device to your identity".to_string(),
44 command: "auths pair".to_string(),
45 });
46 }
47
48 let expiring_soon = readinesses
50 .iter()
51 .filter(|r| **r == DeviceReadiness::ExpiringSoon)
52 .count();
53 if expiring_soon > 0 {
54 steps.push(NextStep {
55 summary: format!("{} device(s) expiring soon", expiring_soon),
56 command: "auths device extend".to_string(),
57 });
58 }
59
60 if Self::needs_recovery_device(readinesses) {
63 steps.push(NextStep {
64 summary: "Add a recovery device — with only one device, losing or compromising it \
65 leaves no way to recover your identity"
66 .to_string(),
67 command: "auths pair".to_string(),
68 });
69 }
70
71 if !agent_running {
73 steps.push(NextStep {
74 summary: "Start the authentication agent for signing".to_string(),
75 command: "auths agent start".to_string(),
76 });
77 }
78
79 if steps.is_empty() {
81 steps.push(NextStep {
82 summary: "Explore advanced features".to_string(),
83 command: "auths --help-all".to_string(),
84 });
85 }
86
87 steps
88 }
89
90 pub fn compute_readiness(
92 expires_at: Option<DateTime<Utc>>,
93 revoked_at: Option<DateTime<Utc>>,
94 now: DateTime<Utc>,
95 ) -> DeviceReadiness {
96 if revoked_at.is_some() {
97 return DeviceReadiness::Revoked;
98 }
99
100 match expires_at {
101 Some(exp) if exp < now => DeviceReadiness::Expired,
102 Some(exp) if exp - now < Duration::days(7) => DeviceReadiness::ExpiringSoon,
103 Some(_) => DeviceReadiness::Ok,
104 None => DeviceReadiness::Ok, }
106 }
107
108 fn needs_recovery_device(readinesses: &[DeviceReadiness]) -> bool {
115 readinesses
116 .iter()
117 .filter(|r| !matches!(r, DeviceReadiness::Revoked | DeviceReadiness::Expired))
118 .count()
119 == 1
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 #[test]
128 #[allow(clippy::disallowed_methods)]
129 fn test_compute_readiness_revoked() {
130 let now = Utc::now();
131 let readiness =
132 StatusWorkflow::compute_readiness(None, Some(now - Duration::hours(1)), now);
133 assert_eq!(readiness, DeviceReadiness::Revoked);
134 }
135
136 #[test]
137 #[allow(clippy::disallowed_methods)]
138 fn test_compute_readiness_expired() {
139 let now = Utc::now();
140 let exp = now - Duration::days(1);
141 let readiness = StatusWorkflow::compute_readiness(Some(exp), None, now);
142 assert_eq!(readiness, DeviceReadiness::Expired);
143 }
144
145 #[test]
146 #[allow(clippy::disallowed_methods)]
147 fn test_compute_readiness_expiring_soon() {
148 let now = Utc::now();
149 let exp = now + Duration::days(3);
150 let readiness = StatusWorkflow::compute_readiness(Some(exp), None, now);
151 assert_eq!(readiness, DeviceReadiness::ExpiringSoon);
152 }
153
154 #[test]
155 #[allow(clippy::disallowed_methods)]
156 fn test_compute_readiness_ok() {
157 let now = Utc::now();
158 let exp = now + Duration::days(30);
159 let readiness = StatusWorkflow::compute_readiness(Some(exp), None, now);
160 assert_eq!(readiness, DeviceReadiness::Ok);
161 }
162
163 #[test]
164 fn test_next_steps_no_identity() {
165 let steps = StatusWorkflow::next_steps_from_readiness(false, &[], false);
166 assert!(!steps.is_empty());
167 assert!(steps[0].command.contains("init"));
168 }
169
170 #[test]
171 fn single_usable_device_is_a_recovery_single_point_of_failure() {
172 assert!(StatusWorkflow::needs_recovery_device(&[
174 DeviceReadiness::Ok
175 ]));
176 assert!(!StatusWorkflow::needs_recovery_device(&[
178 DeviceReadiness::Ok,
179 DeviceReadiness::Ok
180 ]));
181 assert!(StatusWorkflow::needs_recovery_device(&[
184 DeviceReadiness::Ok,
185 DeviceReadiness::Revoked
186 ]));
187 assert!(!StatusWorkflow::needs_recovery_device(&[]));
189 }
190
191 #[test]
192 fn single_device_identity_is_told_to_add_a_recovery_device() {
193 let steps = StatusWorkflow::next_steps_from_readiness(true, &[DeviceReadiness::Ok], true);
194 assert!(
195 steps.iter().any(|s| s.summary.contains("recovery device")),
196 "a single-device identity must be told to add a recovery device, got {steps:?}"
197 );
198 }
199}