use std::sync::Arc;
use affinidi_did_resolver_cache_sdk::DIDCacheClient;
use affinidi_tdk::secrets_resolver::secrets::Secret;
use chrono::Utc;
use didwebvh_rs::log_entry::LogEntryMethods;
use didwebvh_rs::multibase_type::Multibase;
use didwebvh_rs::update::{UpdateDIDConfig, update_did};
use super::errors::UpdateDidWebvhError;
use super::keys::{
derive_secret_for_handle, derive_webvh_keys, install_derived_webvh_keys,
load_active_update_key, load_pre_rotation_signing_key,
};
use super::options::{UpdateDidWebvhOptions, UpdateDidWebvhResult};
use super::state::{find_record_by_scid, state_from_jsonl, state_to_jsonl};
use super::validate::{validate_document_for_update, validate_watchers, validate_witnesses};
use crate::audit;
use crate::auth::AuthClaims;
use crate::didcomm_bridge::DIDCommBridge;
use crate::keys::seed_store::SeedStore;
use crate::operations::did_webvh::WebvhTransport;
use crate::operations::did_webvh::webvh_keys::{self, WebvhKeyHandle, WebvhKeyRole};
use crate::store::KeyspaceHandle;
use crate::webvh_store;
#[allow(clippy::too_many_arguments)]
pub async fn update_did_webvh(
keys_ks: &KeyspaceHandle,
contexts_ks: &KeyspaceHandle,
webvh_ks: &KeyspaceHandle,
audit_ks: &KeyspaceHandle,
seed_store: &dyn SeedStore,
auth: &AuthClaims,
scid: &str,
opts: UpdateDidWebvhOptions,
did_resolver: &DIDCacheClient,
didcomm_bridge: &Arc<DIDCommBridge>,
channel: &str,
) -> Result<UpdateDidWebvhResult, UpdateDidWebvhError> {
let mut record = find_record_by_scid(webvh_ks, scid)
.await?
.ok_or_else(|| UpdateDidWebvhError::NotFound(format!("SCID {scid} not found")))?;
let initial_log_entry_count = record.log_entry_count;
auth.require_admin()
.map_err(|e| UpdateDidWebvhError::Forbidden(format!("admin required: {e}")))?;
auth.require_context(&record.context_id).map_err(|_| {
UpdateDidWebvhError::Forbidden(format!(
"caller has no admin role in context `{}`",
record.context_id
))
})?;
let new_doc = match opts.document {
Some(doc) => Some(validate_document_for_update(doc, &record.did)?),
None => None,
};
if let Some(ref w) = opts.witnesses {
validate_witnesses(w, did_resolver).await?;
}
if let Some(ref watch) = opts.watchers {
validate_watchers(watch)?;
}
let did_log = webvh_store::get_did_log(webvh_ks, &record.did)
.await
.map_err(|e| UpdateDidWebvhError::Persistence(format!("get_did_log: {e}")))?
.ok_or_else(|| {
UpdateDidWebvhError::Library(format!("DID log missing for {}", record.did))
})?;
let state = state_from_jsonl(&did_log)?;
let last_state = state.log_entries().last().ok_or_else(|| {
UpdateDidWebvhError::Library(format!("DID {} has no log entries", record.did))
})?;
if let Some(expected) = opts.expected_version_id.as_deref() {
let latest = last_state.get_version_id();
if latest != expected {
return Err(UpdateDidWebvhError::Conflict(format!(
"DID {} has been updated since you read it (expected versionId `{expected}`, \
current is `{latest}`). Re-fetch the document and re-apply your edits.",
record.did
)));
}
}
let last_params = last_state.validated_parameters.clone();
let last_update_keys: Vec<Multibase> = last_params
.update_keys
.as_ref()
.map(|arc| (**arc).clone())
.unwrap_or_default();
let last_next_key_hashes: Vec<String> = last_params
.next_key_hashes
.as_ref()
.map(|arc| arc.iter().map(|m| m.as_ref().to_string()).collect())
.unwrap_or_default();
let pre_rotation_active = !last_next_key_hashes.is_empty();
let pre_rotation_count = opts.pre_rotation_count.unwrap_or(record.pre_rotation_count);
let context = crate::contexts::get_context(contexts_ks, &record.context_id)
.await
.map_err(|e| UpdateDidWebvhError::Persistence(format!("get_context: {e}")))?
.ok_or_else(|| {
UpdateDidWebvhError::Library(format!(
"context `{}` referenced by DID is missing",
record.context_id
))
})?;
let derived_auth = if new_doc.is_some() && !pre_rotation_active {
derive_webvh_keys(keys_ks, seed_store, &context.base_path, 1).await?
} else {
vec![]
};
let derived_pre_rotation =
derive_webvh_keys(keys_ks, seed_store, &context.base_path, pre_rotation_count).await?;
tracing::info!(
scid,
did = %record.did,
pre_rotation_active,
next_key_hashes_count = last_next_key_hashes.len(),
update_keys_count = last_update_keys.len(),
"update_did_webvh: resolving signing key"
);
let signing_handle = if pre_rotation_active {
load_pre_rotation_signing_key(keys_ks, scid, &last_next_key_hashes).await?
} else {
load_active_update_key(keys_ks, scid, &last_update_keys).await?
};
tracing::info!(
scid,
signing_pubkey = %signing_handle.public_key,
signing_hash = %signing_handle.hash,
signing_role = ?signing_handle.role,
signing_version = %signing_handle.version_id,
"update_did_webvh: signing key resolved"
);
let signing_secret = derive_secret_for_handle(keys_ks, seed_store, &signing_handle).await?;
let mut builder = UpdateDIDConfig::<Secret, Secret>::builder_generic()
.state(state)
.signing_key(signing_secret);
if let Some(doc) = new_doc {
builder = builder.document(doc);
let new_keys: Vec<Multibase> = if pre_rotation_active {
vec![Multibase::from(signing_handle.public_key.clone())]
} else {
derived_auth
.iter()
.map(|k| Multibase::from(k.public_key.clone()))
.collect()
};
builder = builder.update_keys(new_keys);
} else if pre_rotation_active {
builder = builder.update_keys(vec![Multibase::from(signing_handle.public_key.clone())]);
}
if opts.pre_rotation_count.is_some() || record.pre_rotation_count > 0 {
let hashes: Vec<Multibase> = derived_pre_rotation
.iter()
.map(|k| Multibase::from(k.hash.clone()))
.collect();
builder = builder.next_key_hashes(hashes);
}
if let Some(w) = opts.witnesses.clone() {
builder = builder.witness(w);
}
if let Some(watch) = opts.watchers.clone() {
builder = builder.watchers(watch);
}
if let Some(t) = opts.ttl {
builder = builder.ttl(t);
}
let cfg = builder
.build()
.map_err(|e| UpdateDidWebvhError::Library(format!("build update config: {e}")))?;
let result = update_did(cfg)
.await
.map_err(|e| UpdateDidWebvhError::Library(format!("update_did: {e}")))?;
let new_log_entry = result.log_entry();
let new_version_id = new_log_entry
.get_version_id_fields()
.map(|(n, h)| format!("{n}-{h}"))
.map_err(|e| UpdateDidWebvhError::Library(format!("read version id: {e}")))?;
let new_scid = new_log_entry.get_scid().unwrap_or_default().to_string();
let new_log_entry_str = serde_json::to_string(new_log_entry)
.map_err(|e| UpdateDidWebvhError::Persistence(format!("serialize new entry: {e}")))?;
let current = webvh_store::get_did(webvh_ks, &record.did)
.await
.map_err(|e| UpdateDidWebvhError::Persistence(format!("get_did: {e}")))?
.ok_or_else(|| {
UpdateDidWebvhError::NotFound(format!("DID {} disappeared mid-update", record.did))
})?;
if current.log_entry_count != initial_log_entry_count {
return Err(UpdateDidWebvhError::Conflict(format!(
"DID {} was updated concurrently (expected log_entry_count {}, got {})",
record.did, initial_log_entry_count, current.log_entry_count
)));
}
let new_log_jsonl = state_to_jsonl(result.state())?;
webvh_store::store_did_log(webvh_ks, &record.did, &new_log_jsonl)
.await
.map_err(|e| UpdateDidWebvhError::Persistence(format!("store_did_log: {e}")))?;
if !derived_auth.is_empty() {
install_derived_webvh_keys(
keys_ks,
scid,
&new_version_id,
WebvhKeyRole::UpdateKey,
&derived_auth,
"update key",
)
.await?;
}
if !derived_pre_rotation.is_empty() {
install_derived_webvh_keys(
keys_ks,
scid,
&new_version_id,
WebvhKeyRole::PreRotation,
&derived_pre_rotation,
"pre-rotation key",
)
.await?;
}
if pre_rotation_active {
let revealed = WebvhKeyHandle {
scid: scid.to_string(),
version_id: new_version_id.clone(),
hash: signing_handle.hash.clone(),
public_key: signing_handle.public_key.clone(),
derivation_path: signing_handle.derivation_path.clone(),
seed_id: signing_handle.seed_id,
role: WebvhKeyRole::UpdateKey,
label: format!(
"revealed pre-rotation key (was version {})",
signing_handle.version_id
),
created_at: Utc::now(),
};
webvh_keys::install(keys_ks, &revealed)
.await
.map_err(|e| UpdateDidWebvhError::Persistence(format!("install revealed key: {e}")))?;
}
if let Some(prev) = result
.state()
.log_entries()
.iter()
.rev()
.nth(1)
.map(|e| {
e.log_entry
.get_version_id_fields()
.map(|(n, h)| format!("{n}-{h}"))
})
.transpose()
.unwrap_or(None)
{
webvh_keys::supersede_keys_for_version(keys_ks, scid, &prev)
.await
.map_err(|e| UpdateDidWebvhError::Persistence(format!("supersede: {e}")))?;
}
record.log_entry_count += 1;
record.pre_rotation_count = derived_pre_rotation.len() as u32;
record.updated_at = Utc::now();
webvh_store::store_did(webvh_ks, &record)
.await
.map_err(|e| UpdateDidWebvhError::Persistence(format!("store_did: {e}")))?;
if record.server_id != "serverless" {
let server = webvh_store::get_server(webvh_ks, &record.server_id)
.await
.map_err(|e| UpdateDidWebvhError::Persistence(format!("get_server: {e}")))?
.ok_or_else(|| {
UpdateDidWebvhError::Publish(format!(
"webvh server `{}` referenced by DID is missing",
record.server_id
))
})?;
let transport = WebvhTransport::from_server(&server, did_resolver, didcomm_bridge)
.await
.map_err(|e| UpdateDidWebvhError::Publish(format!("transport: {e}")))?;
transport
.publish_did(&record.mnemonic, &new_log_jsonl)
.await
.map_err(|e| UpdateDidWebvhError::Publish(format!("publish_did: {e}")))?;
}
let resource = format!(
"did:webvh:{scid} v{} → v{}",
initial_log_entry_count, record.log_entry_count
);
let label = opts.label.as_deref().unwrap_or("update");
if let Err(e) = audit::record(
audit_ks,
&format!("did.update:{label}"),
&auth.did,
Some(&resource),
"success",
Some(channel),
Some(&record.context_id),
)
.await
{
tracing::warn!(
channel,
did = %record.did,
error = %e,
"did.update audit emission failed; update committed"
);
}
tracing::info!(
channel,
did = %record.did,
scid = %scid,
new_version_id = %new_version_id,
label = ?opts.label,
"did:webvh updated"
);
let update_keys_count = if !derived_auth.is_empty() {
derived_auth.len() as u32
} else if pre_rotation_active {
1
} else {
last_update_keys.len() as u32
};
Ok(UpdateDidWebvhResult {
did: record.did.clone(),
new_version_id,
new_scid,
new_log_entry: new_log_entry_str,
update_keys_count,
pre_rotation_key_count: derived_pre_rotation.len() as u32,
serverless: record.server_id == "serverless",
})
}