1use prost::Message;
10use tracing::{debug, error, info, warn};
11
12use actr_protocol::{RegisterRequest, RegisterResponse};
13
14use crate::error::{HyperError, HyperResult};
15
16pub struct AisClient {
21 endpoint: String,
22 http: reqwest::Client,
23 realm_secret: Option<String>,
25}
26
27impl AisClient {
28 pub fn new(endpoint: impl Into<String>) -> Self {
32 let http = reqwest::Client::builder()
33 .timeout(std::time::Duration::from_secs(30))
34 .build()
35 .expect("reqwest::Client build failed (should never happen)");
36 Self {
37 endpoint: endpoint.into(),
38 http,
39 realm_secret: None,
40 }
41 }
42
43 pub fn with_realm_secret(mut self, secret: impl Into<String>) -> Self {
45 self.realm_secret = Some(secret.into());
46 self
47 }
48
49 pub async fn register_with_manifest(
55 &self,
56 req: RegisterRequest,
57 ) -> HyperResult<RegisterResponse> {
58 info!(
59 endpoint = %self.endpoint,
60 "initial registration: registering with AIS via MFR manifest"
61 );
62 self.do_register(req).await
63 }
64
65 pub async fn register_with_psk(&self, req: RegisterRequest) -> HyperResult<RegisterResponse> {
70 debug!(
71 endpoint = %self.endpoint,
72 "PSK renewal: renewing credential via existing PSK"
73 );
74 self.do_register(req).await
75 }
76
77 pub async fn register_linked(&self, req: RegisterRequest) -> HyperResult<RegisterResponse> {
82 info!(
83 endpoint = %self.endpoint,
84 "linked registration: registering with AIS via realm authorization"
85 );
86 self.do_register(req).await
87 }
88
89 async fn do_register(&self, req: RegisterRequest) -> HyperResult<RegisterResponse> {
94 let url = format!("{}/register", self.endpoint);
95
96 let body = req.encode_to_vec();
98
99 debug!(url = %url, body_len = body.len(), "sending AIS register request");
100
101 let mut request_builder = self
102 .http
103 .post(&url)
104 .header("Content-Type", "application/x-protobuf")
105 .header("Accept", "application/x-protobuf");
106
107 if let Some(ref secret) = self.realm_secret {
109 request_builder = request_builder.header("x-actrix-realm-secret", secret);
110 }
111
112 let response = request_builder.body(body).send().await.map_err(|e| {
113 error!(url = %url, error = %e, "AIS HTTP request failed");
114 HyperError::AisBootstrapFailed(format!("HTTP request failed: {e}"))
115 })?;
116
117 let status = response.status();
118 if !status.is_success() {
119 warn!(url = %url, status = %status, "AIS returned non-2xx status");
120 return Err(HyperError::AisBootstrapFailed(format!(
121 "AIS returned error status: {status}"
122 )));
123 }
124
125 let bytes = response.bytes().await.map_err(|e| {
126 error!(url = %url, error = %e, "failed to read AIS response body");
127 HyperError::AisBootstrapFailed(format!("failed to read response body: {e}"))
128 })?;
129
130 debug!(url = %url, response_len = bytes.len(), "received AIS response");
131
132 let resp = RegisterResponse::decode(bytes.as_ref()).map_err(|e| {
133 error!(url = %url, error = %e, "failed to decode AIS RegisterResponse");
134 HyperError::AisBootstrapFailed(format!("response protobuf decode failed: {e}"))
135 })?;
136
137 Ok(resp)
138 }
139}