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_json::json;
use tracing::debug;
use crate::error::{Result, ServerErrorBody, WebVHError};
use crate::types::*;
pub struct WitnessClient {
http: reqwest::Client,
server_url: String,
access_token: Option<String>,
}
impl WitnessClient {
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");
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 witness server");
Ok(auth_resp)
}
pub async fn request_proof(
&self,
witness_id: &str,
version_id: &str,
) -> Result<SignProofResponse> {
let resp = self
.auth_post(&format!("/api/proof/{witness_id}"))?
.json(&SignProofRequest {
version_id: version_id.to_string(),
})
.send()
.await?;
self.handle_response(resp).await
}
pub async fn list_witnesses(&self) -> Result<WitnessListResponse> {
let resp = self.auth_get("/api/witnesses")?.send().await?;
self.handle_response(resp).await
}
pub async fn create_witness(&self, label: Option<&str>) -> Result<WitnessResponse> {
let resp = self
.auth_post("/api/witnesses")?
.json(&CreateWitnessRequest {
label: label.map(|s| s.to_string()),
})
.send()
.await?;
self.handle_response(resp).await
}
pub async fn get_witness(&self, witness_id: &str) -> Result<WitnessResponse> {
let resp = self
.auth_get(&format!("/api/witnesses/{witness_id}"))?
.send()
.await?;
self.handle_response(resp).await
}
pub async fn delete_witness(&self, witness_id: &str) -> Result<()> {
let resp = self
.auth_delete(&format!("/api/witnesses/{witness_id}"))?
.send()
.await?;
self.handle_response_no_body(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_get(&self, path: &str) -> Result<reqwest::RequestBuilder> {
let token = self.token()?;
Ok(self
.http
.get(format!("{}{path}", self.server_url))
.bearer_auth(token))
}
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))
}
fn auth_delete(&self, path: &str) -> Result<reqwest::RequestBuilder> {
let token = self.token()?;
Ok(self
.http
.delete(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 handle_response_no_body(&self, resp: reqwest::Response) -> Result<()> {
if !resp.status().is_success() {
return Err(self.extract_server_error(resp).await);
}
Ok(())
}
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 }
}
}