use affinidi_did_resolver_cache_sdk::{DIDCacheClient, config::DIDCacheConfigBuilder};
use affinidi_tdk::secrets_resolver::secrets::Secret;
use serde::Deserialize;
use serde_json::{Value, json};
use trust_tasks_rs::Payload;
use trust_tasks_rs::specs::did_management::{
agent_name::{
check as agent_name_check, remove as agent_name_remove, update as agent_name_update,
},
did::{check_name, delete, info, list, register},
server::info as server_info,
};
use crate::did::{build_did_document, create_log_entry, encode_host};
use crate::error::{Result, WebVHError};
use crate::server::trust_tasks::send::{build_signed_request, post_trust_task_https};
use crate::types::*;
struct Requester {
did: String,
signer: Secret,
service_did: String,
did_resolver: DIDCacheClient,
}
pub struct WebVHClient {
server_url: String,
hosting_url: Option<String>,
requester: Option<Requester>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DidSummary {
pub mnemonic: String,
#[serde(default)]
pub did_id: Option<String>,
#[serde(default)]
pub version_count: u64,
#[serde(default)]
pub disabled: bool,
}
impl WebVHClient {
pub fn new(server_url: &str) -> Self {
Self {
server_url: server_url.trim_end_matches('/').to_string(),
hosting_url: None,
requester: None,
}
}
pub fn with_hosting_url(mut self, hosting_url: impl Into<String>) -> Self {
self.hosting_url = Some(hosting_url.into().trim_end_matches('/').to_string());
self
}
pub async fn sign_as(&mut self, did: &str, signer: &Secret, service_did: &str) -> Result<()> {
let did_resolver = DIDCacheClient::new(DIDCacheConfigBuilder::default().build())
.await
.map_err(|e| WebVHError::Resolver(e.to_string()))?;
self.requester = Some(Requester {
did: did.to_string(),
signer: signer.clone(),
service_did: service_did.to_string(),
did_resolver,
});
Ok(())
}
pub async fn server_info(&self) -> Result<Value> {
self.call(server_info::v0_1::Payload::TYPE_URI, json!({}))
.await
}
pub async fn check_name(&self, path: &str) -> Result<check_name::v0_1::Response> {
self.call(check_name::v0_1::Payload::TYPE_URI, json!({ "path": path }))
.await
}
pub async fn request_uri(&self, path: Option<&str>) -> Result<RequestUriResponse> {
let mut payload = json!({ "reserve": true });
if let Some(p) = path {
payload["path"] = json!(p);
}
let resp: Value = self
.call(check_name::v0_1::Payload::TYPE_URI, payload)
.await?;
if resp.get("reserved").and_then(Value::as_bool) != Some(true) {
return Err(WebVHError::Refused {
code: "did/check-name:notReserved".into(),
message: "the path is not available".into(),
});
}
let record = resp
.get("record")
.ok_or_else(|| WebVHError::Transport("reservation reply carries no record".into()))?;
let field = |name: &str| {
record
.get(name)
.and_then(Value::as_str)
.map(str::to_string)
.ok_or_else(|| WebVHError::Transport(format!("reserved record has no `{name}`")))
};
Ok(RequestUriResponse {
mnemonic: field("mnemonic")?,
did_url: field("didUrl")?,
})
}
pub async fn upload_did(&self, mnemonic: &str, content: &str) -> Result<()> {
let _: Value = self
.call(
register::v0_1::Payload::TYPE_URI,
json!({ "path": mnemonic, "method": "webvh", "didData": content }),
)
.await?;
Ok(())
}
pub async fn delete_did(&self, mnemonic: &str) -> Result<()> {
let _: Value = self
.call(
delete::v0_1::Payload::TYPE_URI,
json!({ "mnemonic": mnemonic }),
)
.await?;
Ok(())
}
pub async fn list_dids(&self) -> Result<Vec<DidSummary>> {
#[derive(Deserialize)]
struct Page {
records: Vec<DidSummary>,
total: u64,
}
const PAGE: u64 = 100;
let mut out = Vec::new();
loop {
let page: Page = self
.call(
list::v0_1::Payload::TYPE_URI,
json!({ "limit": PAGE, "offset": out.len() }),
)
.await?;
let got = page.records.len();
out.extend(page.records);
if got == 0 || out.len() as u64 >= page.total {
return Ok(out);
}
}
}
pub async fn get_did_detail(&self, mnemonic: &str) -> Result<Value> {
self.call(
info::v0_1::Payload::TYPE_URI,
json!({ "mnemonic": mnemonic }),
)
.await
}
pub async fn check_agent_name(&self, name: &str, domain: Option<&str>) -> Result<Value> {
let mut payload = json!({ "name": name });
if let Some(d) = domain {
payload["domain"] = json!(d);
}
self.call(agent_name_check::v0_1::Payload::TYPE_URI, payload)
.await
}
pub async fn agent_name_op(
&self,
op: &str,
mnemonic: &str,
name: &str,
did_log: &str,
) -> Result<Value> {
let base = json!({ "mnemonic": mnemonic, "name": name, "didData": did_log });
let (type_uri, payload) = match op {
"set" | "enable" => (
agent_name_update::v0_1::Payload::TYPE_URI,
with_member(base, "state", "active"),
),
"disable" => (
agent_name_update::v0_1::Payload::TYPE_URI,
with_member(base, "state", "parked"),
),
"remove" => (agent_name_remove::v0_1::Payload::TYPE_URI, base),
other => {
return Err(WebVHError::Transport(format!(
"unknown agent-name operation `{other}`"
)));
}
};
self.call(type_uri, payload).await
}
pub fn server_url(&self) -> &str {
&self.server_url
}
pub async fn create_did(&self, secret: &Secret, path: Option<&str>) -> Result<CreateDidResult> {
let create_resp = self.request_uri(path).await?;
let host_url = self.hosting_url.as_deref().unwrap_or(&self.server_url);
let host = encode_host(host_url)?;
let public_key_multibase = secret
.get_public_keymultibase()
.map_err(|e| WebVHError::DIDComm(format!("failed to get public key: {e}")))?;
let did_doc = build_did_document(
&host,
&create_resp.mnemonic,
&public_key_multibase,
&Default::default(),
);
let (scid, jsonl) = create_log_entry(&did_doc, secret).await?;
self.upload_did(&create_resp.mnemonic, &jsonl).await?;
let did_path = create_resp.mnemonic.replace('/', ":");
let did = format!("did:webvh:{scid}:{host}:{did_path}");
Ok(CreateDidResult {
mnemonic: create_resp.mnemonic,
did_url: create_resp.did_url,
scid,
did,
public_key_multibase,
})
}
pub async fn resolve_did(&self, mnemonic: &str) -> Result<String> {
let host_url = self.hosting_url.as_deref().unwrap_or(&self.server_url);
let resp = crate::http::outbound_client()
.get(format!("{host_url}/{mnemonic}/did.jsonl"))
.send()
.await?;
let status = resp.status();
if !status.is_success() {
return Err(WebVHError::Server {
status: status.as_u16(),
message: format!("HTTP {status}"),
});
}
Ok(resp.text().await?)
}
async fn call<R: serde::de::DeserializeOwned>(
&self,
type_uri: &str,
payload: Value,
) -> Result<R> {
let requester = self
.requester
.as_ref()
.ok_or(WebVHError::NotAuthenticated)?;
let doc = build_signed_request(
type_uri,
&requester.did,
&requester.service_did,
payload,
&requester.signer,
)
.await
.map_err(|e| WebVHError::Transport(e.to_string()))?;
let reply = post_trust_task_https(
&requester.did,
&requester.service_did,
&format!("{}/api/trust-tasks", self.server_url),
&doc,
Some(&requester.did_resolver),
)
.await
.map_err(|e| WebVHError::Transport(e.to_string()))?;
crate::witness_client::read_reply(type_uri, reply)
}
}
fn with_member(mut value: Value, key: &str, member: &str) -> Value {
value[key] = json!(member);
value
}