Skip to main content

auths_sdk/workflows/
status.rs

1//! Status workflow — aggregates identity, device, and agent state for user-friendly reporting.
2
3use crate::result::{
4    AgentStatus, DeviceReadiness, DeviceStatus, IdentityStatus, NextStep, StatusReport,
5};
6use chrono::{DateTime, Duration, Utc};
7use std::path::Path;
8
9/// Status workflow for reporting Auths state.
10///
11/// This workflow aggregates information from identity storage, device attestations,
12/// and agent status to produce a unified StatusReport suitable for CLI display.
13///
14/// Usage:
15/// ```ignore
16/// let report = StatusWorkflow::query(&ctx, Utc::now())?;
17/// println!("Identity: {}", report.identity.controller_did);
18/// ```
19pub struct StatusWorkflow;
20
21impl StatusWorkflow {
22    /// Query the current status of the Auths system.
23    ///
24    /// Args:
25    /// * `repo_path` - Path to the Auths repository.
26    /// * `now` - Current time for expiry calculations.
27    ///
28    /// Returns a StatusReport with identity, device, and agent state.
29    ///
30    /// This is a placeholder implementation; the real version will integrate
31    /// with IdentityStorage, AttestationSource, and agent discovery ports.
32    pub fn query(repo_path: &Path, _now: DateTime<Utc>) -> Result<StatusReport, String> {
33        let _ = repo_path; // Placeholder to avoid unused warning
34        // TODO: In full implementation, load identity from IdentityStorage
35        let identity = None; // Placeholder
36
37        // TODO: In full implementation, load attestations from AttestationSource
38        // and aggregate by device with expiry checking
39        let devices = Vec::new(); // Placeholder
40
41        // TODO: In full implementation, check agent socket and PID
42        let agent = AgentStatus {
43            running: false,
44            pid: None,
45            socket_path: None,
46        };
47
48        // Compute next steps based on current state
49        let next_steps = Self::compute_next_steps(&identity, &devices, &agent);
50
51        Ok(StatusReport {
52            identity,
53            devices,
54            agent,
55            next_steps,
56        })
57    }
58
59    /// Compute suggested next steps based on current state.
60    fn compute_next_steps(
61        identity: &Option<IdentityStatus>,
62        devices: &[DeviceStatus],
63        agent: &AgentStatus,
64    ) -> Vec<NextStep> {
65        let mut steps = Vec::new();
66
67        // No identity initialized
68        if identity.is_none() {
69            steps.push(NextStep {
70                summary: "Initialize your identity".to_string(),
71                command: "auths init --profile developer".to_string(),
72            });
73            return steps;
74        }
75
76        // No devices linked
77        if devices.is_empty() {
78            steps.push(NextStep {
79                summary: "Link this device to your identity".to_string(),
80                command: "auths pair".to_string(),
81            });
82        }
83
84        // Device expiring soon
85        let expiring_soon = devices
86            .iter()
87            .filter(|d| d.readiness == DeviceReadiness::ExpiringSoon)
88            .count();
89        if expiring_soon > 0 {
90            steps.push(NextStep {
91                summary: format!("{} device(s) expiring soon", expiring_soon),
92                command: "auths device extend".to_string(),
93            });
94        }
95
96        // Agent not running
97        if !agent.running {
98            steps.push(NextStep {
99                summary: "Start the authentication agent for signing".to_string(),
100                command: "auths agent start".to_string(),
101            });
102        }
103
104        // Always suggest viewing help for deeper features
105        if steps.is_empty() {
106            steps.push(NextStep {
107                summary: "Explore advanced features".to_string(),
108                command: "auths --help-all".to_string(),
109            });
110        }
111
112        steps
113    }
114
115    /// Determine device readiness given expiration timestamps.
116    pub fn compute_readiness(
117        expires_at: Option<DateTime<Utc>>,
118        revoked_at: Option<DateTime<Utc>>,
119        now: DateTime<Utc>,
120    ) -> DeviceReadiness {
121        if revoked_at.is_some() {
122            return DeviceReadiness::Revoked;
123        }
124
125        match expires_at {
126            Some(exp) if exp < now => DeviceReadiness::Expired,
127            Some(exp) if exp - now < Duration::days(7) => DeviceReadiness::ExpiringSoon,
128            Some(_) => DeviceReadiness::Ok,
129            None => DeviceReadiness::Ok, // No expiry set
130        }
131    }
132}
133
134#[cfg(test)]
135mod tests {
136    use super::*;
137
138    #[test]
139    #[allow(clippy::disallowed_methods)]
140    fn test_compute_readiness_revoked() {
141        let now = Utc::now();
142        let readiness =
143            StatusWorkflow::compute_readiness(None, Some(now - Duration::hours(1)), now);
144        assert_eq!(readiness, DeviceReadiness::Revoked);
145    }
146
147    #[test]
148    #[allow(clippy::disallowed_methods)]
149    fn test_compute_readiness_expired() {
150        let now = Utc::now();
151        let exp = now - Duration::days(1);
152        let readiness = StatusWorkflow::compute_readiness(Some(exp), None, now);
153        assert_eq!(readiness, DeviceReadiness::Expired);
154    }
155
156    #[test]
157    #[allow(clippy::disallowed_methods)]
158    fn test_compute_readiness_expiring_soon() {
159        let now = Utc::now();
160        let exp = now + Duration::days(3);
161        let readiness = StatusWorkflow::compute_readiness(Some(exp), None, now);
162        assert_eq!(readiness, DeviceReadiness::ExpiringSoon);
163    }
164
165    #[test]
166    #[allow(clippy::disallowed_methods)]
167    fn test_compute_readiness_ok() {
168        let now = Utc::now();
169        let exp = now + Duration::days(30);
170        let readiness = StatusWorkflow::compute_readiness(Some(exp), None, now);
171        assert_eq!(readiness, DeviceReadiness::Ok);
172    }
173
174    #[test]
175    fn test_next_steps_no_identity() {
176        let steps = StatusWorkflow::compute_next_steps(
177            &None,
178            &[],
179            &AgentStatus {
180                running: false,
181                pid: None,
182                socket_path: None,
183            },
184        );
185        assert!(!steps.is_empty());
186        assert!(steps[0].command.contains("init"));
187    }
188}