use std::time::{SystemTime, UNIX_EPOCH};
use affinidi_tdk::didcomm::Message;
use affinidi_tdk::didcomm::message::pack;
use affinidi_tdk::secrets_resolver::secrets::Secret;
use serde::{Deserialize, Serialize};
use serde_json::json;
use tracing::debug;
use crate::error::{Result, ServerErrorBody, WebVHError};
use crate::types::*;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DidSyncEntry {
pub mnemonic: String,
pub did_id: Option<String>,
pub version_count: u64,
pub updated_at: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DidSyncUpdate {
pub mnemonic: String,
pub did_id: String,
pub log_content: String,
pub witness_content: Option<String>,
pub version_count: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RegisterServiceRequest {
pub service_type: String,
pub url: String,
pub label: Option<String>,
pub preloaded_dids: Vec<DidSyncEntry>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RegisterServiceResponse {
pub instance_id: String,
pub did_updates: Vec<DidSyncUpdate>,
pub did_hosting_url: Option<String>,
}
pub struct ControlClient {
http: reqwest::Client,
server_url: String,
access_token: Option<String>,
}
impl ControlClient {
pub fn new(server_url: &str) -> Self {
Self {
http: reqwest::Client::new(),
server_url: server_url.trim_end_matches('/').to_string(),
access_token: None,
}
}
pub async fn authenticate(
&mut self,
did: &str,
secret: &Secret,
) -> Result<AuthenticateResponse> {
let private_key_bytes: [u8; 32] = secret
.get_private_bytes()
.try_into()
.map_err(|_| WebVHError::DIDComm("signing key must be 32 bytes".into()))?;
let challenge_resp: ChallengeResponse = self
.http
.post(format!("{}/api/auth/challenge", self.server_url))
.json(&ChallengeRequest {
did: did.to_string(),
})
.send()
.await?
.error_for_status()
.map_err(|e| WebVHError::DIDComm(format!("challenge request rejected: {e}")))?
.json()
.await?;
debug!(session_id = %challenge_resp.session_id, "challenge received from control plane");
let created_time = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock before UNIX epoch")
.as_secs();
let msg = Message::build(
uuid::Uuid::new_v4().to_string(),
"https://affinidi.com/webvh/1.0/authenticate".to_string(),
json!({
"challenge": challenge_resp.challenge,
"session_id": challenge_resp.session_id,
}),
)
.from(did.to_string())
.created_time(created_time)
.finalize();
let packed = pack::pack_signed(&msg, &secret.id, &private_key_bytes)
.map_err(|e| WebVHError::DIDComm(format!("failed to pack signed message: {e}")))?;
let auth_resp: AuthenticateResponse = self
.http
.post(format!("{}/api/auth/", self.server_url))
.body(packed)
.send()
.await?
.error_for_status()
.map_err(|e| WebVHError::DIDComm(format!("authentication rejected: {e}")))?
.json()
.await?;
self.access_token = Some(auth_resp.tokens.access_token.clone());
debug!("authenticated with control plane");
Ok(auth_resp)
}
pub async fn register_service(
&self,
req: &RegisterServiceRequest,
) -> Result<RegisterServiceResponse> {
let resp = self
.auth_post("/api/control/register-service")?
.json(req)
.send()
.await?;
self.handle_response(resp).await
}
pub fn server_url(&self) -> &str {
&self.server_url
}
fn token(&self) -> Result<&str> {
self.access_token
.as_deref()
.ok_or(WebVHError::NotAuthenticated)
}
fn auth_post(&self, path: &str) -> Result<reqwest::RequestBuilder> {
let token = self.token()?;
Ok(self
.http
.post(format!("{}{path}", self.server_url))
.bearer_auth(token))
}
async fn handle_response<T: serde::de::DeserializeOwned>(
&self,
resp: reqwest::Response,
) -> Result<T> {
if !resp.status().is_success() {
return Err(self.extract_server_error(resp).await);
}
Ok(resp.json().await?)
}
async fn extract_server_error(&self, resp: reqwest::Response) -> WebVHError {
let status = resp.status().as_u16();
let message = match resp.json::<ServerErrorBody>().await {
Ok(body) => body.to_string(),
Err(_) => format!("HTTP {status}"),
};
WebVHError::Server { status, message }
}
}