use affinidi_did_resolver_cache_sdk::DIDCacheClient;
use didwebvh_rs::witness::Witnesses;
use serde_json::Value;
use super::errors::UpdateDidWebvhError;
use super::options::WITNESS_RESOLVE_TIMEOUT;
pub(in crate::operations::did_webvh) fn validate_document_for_update(
document: Value,
existing_did: &str,
) -> Result<Value, UpdateDidWebvhError> {
let obj = document.as_object().ok_or_else(|| {
UpdateDidWebvhError::InvalidDocument("document must be a JSON object".into())
})?;
let id = obj
.get("id")
.and_then(Value::as_str)
.ok_or_else(|| UpdateDidWebvhError::InvalidDocument("document missing `id`".into()))?;
if id != existing_did {
return Err(UpdateDidWebvhError::InvalidDocument(format!(
"document.id `{id}` does not match existing DID `{existing_did}`"
)));
}
if obj.get("@context").is_none() {
return Err(UpdateDidWebvhError::InvalidDocument(
"document missing `@context`".into(),
));
}
if let Some(vm) = obj.get("verificationMethod") {
let vms = vm.as_array().ok_or_else(|| {
UpdateDidWebvhError::InvalidDocument("verificationMethod must be an array".into())
})?;
for (i, entry) in vms.iter().enumerate() {
let entry_obj = entry.as_object().ok_or_else(|| {
UpdateDidWebvhError::InvalidDocument(format!(
"verificationMethod[{i}] is not a JSON object"
))
})?;
for required in ["id", "type", "controller", "publicKeyMultibase"] {
if !entry_obj.contains_key(required) {
return Err(UpdateDidWebvhError::InvalidDocument(format!(
"verificationMethod[{i}] missing `{required}`"
)));
}
}
}
}
Ok(document)
}
pub(in crate::operations::did_webvh) fn validate_watchers(
urls: &[String],
) -> Result<(), UpdateDidWebvhError> {
for url_str in urls {
let url = url::Url::parse(url_str).map_err(|e| {
UpdateDidWebvhError::InvalidWatcher(format!("watcher URL `{url_str}`: {e}"))
})?;
let scheme_ok =
matches!(url.scheme(), "https") || (cfg!(debug_assertions) && url.scheme() == "http");
if !scheme_ok {
return Err(UpdateDidWebvhError::InvalidWatcher(format!(
"watcher URL `{url_str}` must use https"
)));
}
if url.fragment().is_some() {
return Err(UpdateDidWebvhError::InvalidWatcher(format!(
"watcher URL `{url_str}` must not contain a fragment"
)));
}
if url.query().is_some() {
return Err(UpdateDidWebvhError::InvalidWatcher(format!(
"watcher URL `{url_str}` must not contain a query string"
)));
}
}
Ok(())
}
pub(in crate::operations::did_webvh) async fn validate_witnesses(
new: &Witnesses,
did_resolver: &DIDCacheClient,
) -> Result<(), UpdateDidWebvhError> {
let (witnesses, threshold) = match new {
Witnesses::Empty {} => return Ok(()),
Witnesses::Value {
witnesses,
threshold,
} => (witnesses, *threshold),
};
if witnesses.is_empty() {
return Err(UpdateDidWebvhError::InvalidWitness(format!(
"witness configuration has threshold {threshold} but no witnesses listed"
)));
}
if (witnesses.len() as u32) < threshold {
return Err(UpdateDidWebvhError::InvalidWitness(format!(
"threshold {threshold} exceeds witness count {}",
witnesses.len()
)));
}
for w in witnesses {
let did = w.as_did();
match tokio::time::timeout(WITNESS_RESOLVE_TIMEOUT, did_resolver.resolve(&did)).await {
Ok(Ok(_)) => {}
Ok(Err(e)) => {
return Err(UpdateDidWebvhError::InvalidWitness(format!(
"witness {did} did not resolve: {e}"
)));
}
Err(_) => {
return Err(UpdateDidWebvhError::InvalidWitness(format!(
"witness {did} resolution timed out ({}s)",
WITNESS_RESOLVE_TIMEOUT.as_secs()
)));
}
}
}
Ok(())
}