use std::sync::Arc;
use affinidi_did_resolver_cache_sdk::DIDCacheClient;
use didwebvh_rs::log_entry::LogEntryMethods;
use serde_json::Value;
use super::errors::UpdateDidWebvhError;
use super::keys::derive_webvh_keys;
use super::options::{RotateDidWebvhKeysOptions, UpdateDidWebvhOptions, UpdateDidWebvhResult};
use super::orchestrator::update_did_webvh;
use super::state::{find_record_by_scid, state_from_jsonl};
use crate::auth::AuthClaims;
use crate::didcomm_bridge::DIDCommBridge;
use crate::keys::seed_store::SeedStore;
use crate::store::KeyspaceHandle;
use crate::webvh_store;
#[allow(clippy::too_many_arguments)]
pub async fn rotate_did_webvh_keys(
keys_ks: &KeyspaceHandle,
imported_ks: &KeyspaceHandle,
contexts_ks: &KeyspaceHandle,
webvh_ks: &KeyspaceHandle,
audit_ks: &KeyspaceHandle,
seed_store: &dyn SeedStore,
auth: &AuthClaims,
scid: &str,
opts: RotateDidWebvhKeysOptions,
did_resolver: &DIDCacheClient,
didcomm_bridge: &Arc<DIDCommBridge>,
vta_did: Option<&str>,
auth_locks: &super::super::WebvhAuthLocks,
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")))?;
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 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.log_entries().last().ok_or_else(|| {
UpdateDidWebvhError::Library(format!("DID {} has no log entries", record.did))
})?;
let current_doc = last.log_entry.get_did_document().map_err(|e| {
UpdateDidWebvhError::Library(format!("extract document from last entry: {e}"))
})?;
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 mut new_doc = current_doc.clone();
let vms = new_doc
.as_object_mut()
.and_then(|o| o.get_mut("verificationMethod"))
.and_then(|v| v.as_array_mut())
.ok_or_else(|| {
UpdateDidWebvhError::Library("current doc has no verificationMethod array".into())
})?;
let vm_count = vms.len() as u32;
let derived_vms = derive_webvh_keys(keys_ks, seed_store, &context.base_path, vm_count).await?;
let first_new_fragment_id = record.next_fragment_id;
let pre_rotate_snapshot = crate::operations::did_webvh::RecordSnapshot::capture(&record);
let mut frag_remap: Vec<(String, String)> = Vec::with_capacity(vm_count as usize);
for (i, (vm, derived_key)) in vms.iter_mut().zip(derived_vms.iter()).enumerate() {
let old_id = vm
.get("id")
.and_then(Value::as_str)
.ok_or_else(|| {
UpdateDidWebvhError::Library(format!("verificationMethod[{i}] missing id"))
})?
.to_string();
let new_frag_id = record.next_fragment_id + i as u32;
let new_id = format!("{}#key-{new_frag_id}", record.did);
frag_remap.push((old_id, new_id.clone()));
let obj = vm.as_object_mut().unwrap();
obj.insert("id".into(), Value::String(new_id));
obj.insert(
"publicKeyMultibase".into(),
Value::String(derived_key.public_key.clone()),
);
}
for field in ["assertionMethod", "authentication", "keyAgreement"] {
if let Some(arr) = new_doc
.as_object_mut()
.and_then(|o| o.get_mut(field))
.and_then(|v| v.as_array_mut())
{
for entry in arr.iter_mut() {
if let Some(s) = entry.as_str()
&& let Some((_, new_id)) = frag_remap.iter().find(|(old, _)| old == s)
{
*entry = Value::String(new_id.clone());
}
}
}
}
let current = webvh_store::get_did(webvh_ks, &record.did)
.await
.map_err(|e| UpdateDidWebvhError::Persistence(format!("get_did (rotate CAS): {e}")))?
.ok_or_else(|| {
UpdateDidWebvhError::NotFound(format!("DID {} disappeared mid-rotate", record.did))
})?;
pre_rotate_snapshot
.assert_unchanged(¤t)
.map_err(|race| UpdateDidWebvhError::Conflict(race.to_string()))?;
record.next_fragment_id += vm_count;
webvh_store::store_did(webvh_ks, &record)
.await
.map_err(|e| UpdateDidWebvhError::Persistence(format!("store_did (frag bump): {e}")))?;
let label = opts
.label
.or_else(|| Some(format!("rotate-keys for {}", record.did)));
let result = update_did_webvh(
keys_ks,
imported_ks,
contexts_ks,
webvh_ks,
audit_ks,
seed_store,
auth,
scid,
UpdateDidWebvhOptions {
document: Some(new_doc),
pre_rotation_count: opts.pre_rotation_count,
witnesses: None,
watchers: None,
ttl: None,
label,
expected_version_id: None,
},
did_resolver,
didcomm_bridge,
vta_did,
auth_locks,
channel,
)
.await?;
tracing::info!(
channel,
did = %record.did,
scid = %scid,
first_fragment = first_new_fragment_id,
last_fragment = record.next_fragment_id - 1,
"did:webvh keys rotated"
);
Ok(result)
}