aperion_shield/orgmode/
smartflow_provider.rs1use std::sync::Arc;
16use std::time::Duration;
17
18use chrono::{DateTime, Utc};
19use thiserror::Error;
20use tokio::time::sleep;
21
22use super::client::{IdentityCheckRequest, IdentityCheckResponse, OrgApi};
23use crate::IdentityRequirement;
24
25const POLL_EVERY: Duration = Duration::from_secs(2);
28
29#[derive(Debug)]
31pub enum ResolveOutcome {
32 Verified(SmartflowProof),
36
37 HoldExpired { verify_url: String, challenge_id: String },
41
42 ProviderUnready { provider: String, message: String },
45
46 Error(SmartflowError),
50}
51
52#[derive(Debug, Clone)]
53pub struct SmartflowProof {
54 pub provider: String,
55 pub subject: String,
56 pub loa: u8,
57 pub expires_at: DateTime<Utc>,
58 pub signature: Option<String>,
62}
63
64#[derive(Debug, Error)]
65pub enum SmartflowError {
66 #[error("network/http: {0}")]
67 Net(String),
68 #[error("decode: {0}")]
69 Decode(String),
70 #[error("server response: {0}")]
71 Server(String),
72}
73
74pub struct SmartflowProvider {
75 api: Arc<OrgApi>,
76 pub hold_seconds: u64,
79}
80
81impl SmartflowProvider {
82 pub fn new(api: Arc<OrgApi>) -> Self {
83 Self {
84 api,
85 hold_seconds: 120,
86 }
87 }
88
89 pub fn with_hold_seconds(mut self, secs: u64) -> Self {
90 self.hold_seconds = secs;
91 self
92 }
93
94 fn requirement_to_request(req: &IdentityRequirement) -> IdentityCheckRequest {
97 IdentityCheckRequest {
98 provider: req.provider.clone(),
99 scope: req.scope.clone(),
100 allowed_subjects: req.allowed_subjects.iter().cloned().collect(),
101 min_loa: if req.loa == 0 { None } else { Some(req.loa) },
102 max_age_seconds: req.max_proof_age_seconds,
103 }
104 }
105
106 pub async fn resolve(&self, req: &IdentityRequirement) -> ResolveOutcome {
109 let wire = Self::requirement_to_request(req);
110 let first = match self.api.identity_check(&wire).await {
111 Ok(r) => r,
112 Err(super::client::OrgApiError::Http { status: 503, body }) => {
113 return ResolveOutcome::ProviderUnready {
114 provider: req.provider.clone(),
115 message: body,
116 };
117 }
118 Err(e) => return ResolveOutcome::Error(SmartflowError::Net(e.to_string())),
119 };
120
121 if first.verified {
122 return ResolveOutcome::Verified(proof_from_response(req, &first));
123 }
124
125 let (verify_url, challenge_id) = match (first.verify_url, first.challenge_id) {
128 (Some(u), Some(c)) => (u, c),
129 _ => {
130 return ResolveOutcome::Error(SmartflowError::Server(
131 "server reported unverified but did not return verify_url + challenge_id"
132 .into(),
133 ));
134 }
135 };
136
137 eprintln!(
142 "[shield] identity verification required for scope='{}' provider='{}'",
143 req.scope, req.provider
144 );
145 eprintln!("[shield] open: {}", verify_url);
146 eprintln!(
147 "[shield] holding tool call for {}s (challenge={})",
148 self.hold_seconds, challenge_id
149 );
150
151 let deadline = std::time::Instant::now() + Duration::from_secs(self.hold_seconds);
152 while std::time::Instant::now() < deadline {
153 sleep(POLL_EVERY).await;
154 match self.api.identity_result(&challenge_id).await {
155 Ok(r) if r.verified => {
156 return ResolveOutcome::Verified(proof_from_response(req, &r));
157 }
158 Ok(_) => continue,
159 Err(super::client::OrgApiError::Http { status: 404, .. }) => {
160 break;
162 }
163 Err(e) => {
164 log::warn!("[shield] identity_result poll error: {}", e);
165 }
168 }
169 }
170
171 ResolveOutcome::HoldExpired {
172 verify_url,
173 challenge_id,
174 }
175 }
176}
177
178fn proof_from_response(
179 req: &IdentityRequirement,
180 resp: &IdentityCheckResponse,
181) -> SmartflowProof {
182 SmartflowProof {
183 provider: req.provider.clone(),
184 subject: resp.subject.clone().unwrap_or_default(),
185 loa: resp.loa.unwrap_or(0),
186 expires_at: resp.expires_at.unwrap_or_else(Utc::now),
187 signature: resp.signature.clone(),
188 }
189}