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 base = self.endpoint.to_string().trim_end_matches('/').to_string();
95 let url = format!("{}/register", base);
96
97 let body = req.encode_to_vec();
99
100 debug!(url = %url, body_len = body.len(), "sending AIS register request");
101
102 let mut request_builder = self
103 .http
104 .post(&url)
105 .header("Content-Type", "application/x-protobuf")
106 .header("Accept", "application/x-protobuf");
107
108 if let Some(ref secret) = self.realm_secret {
110 request_builder = request_builder.header("x-actrix-realm-secret", secret);
111 }
112
113 let response = request_builder.body(body).send().await.map_err(|e| {
114 error!(url = %url, error = %e, "AIS HTTP request failed");
115 HyperError::AisBootstrapFailed(format!("HTTP request failed: {e}"))
116 })?;
117
118 let status = response.status();
119 if !status.is_success() {
120 warn!(url = %url, status = %status, "AIS returned non-2xx status");
121 return Err(HyperError::AisBootstrapFailed(format!(
122 "AIS returned error status: {status}"
123 )));
124 }
125
126 let bytes = response.bytes().await.map_err(|e| {
127 error!(url = %url, error = %e, "failed to read AIS response body");
128 HyperError::AisBootstrapFailed(format!("failed to read response body: {e}"))
129 })?;
130
131 debug!(url = %url, response_len = bytes.len(), "received AIS response");
132
133 let resp = RegisterResponse::decode(bytes.as_ref()).map_err(|e| {
134 error!(url = %url, error = %e, "failed to decode AIS RegisterResponse");
135 HyperError::AisBootstrapFailed(format!("response protobuf decode failed: {e}"))
136 })?;
137
138 Ok(resp)
139 }
140}