use std::sync::atomic::{AtomicBool, Ordering};
use affinidi_messaging_didcomm::Message;
use affinidi_messaging_didcomm_service::DIDCommService;
use did_hosting_common::DidSyncUpdate;
use did_hosting_common::did_ops::{
AgentNameEntry, DidRecord, agent_name_key, content_log_key, content_witness_key, did_key,
extract_agent_names, extract_service_types, owner_key, validate_did_jsonl,
};
use did_hosting_common::didcomm_types::MSG_SERVER_REGISTER;
use did_hosting_common::server::acl::{AclEntry, Role, get_acl_entry, store_acl_entry};
use did_hosting_common::server::didcomm_profile::{
PeerTransport, TransportFallback, resolve_transport,
};
use did_hosting_common::server::domain::safety::extract_did_host;
use did_hosting_common::server::domain::{DomainStatus, list_domains};
use did_hosting_common::server::mnemonic::validate_agent_name_binding;
use did_hosting_common::server::trust_tasks::send::{
Retry, build_request, send_trust_task_with_retry,
};
use serde_json::json;
use tracing::{info, warn};
use crate::server::AppState;
use crate::store::{KeyspaceHandle, Store};
static REGISTERED: AtomicBool = AtomicBool::new(false);
pub fn is_registered() -> bool {
REGISTERED.load(Ordering::Relaxed)
}
pub async fn register_via_didcomm(state: &AppState, didcomm_svc: &DIDCommService) {
let server_did = match &state.config.server_did {
Some(did) => did.clone(),
None => {
warn!("cannot register: server_did not configured");
return;
}
};
let control_did = match &state.config.control_did {
Some(did) => did.clone(),
None => {
info!("no control_did configured — skipping registration");
return;
}
};
info!(
server_did = %server_did,
control_did = %control_did,
"registering with control plane via DIDComm"
);
match get_acl_entry(&state.acl_ks, &control_did).await {
Ok(Some(entry)) => {
info!(
control_did = %control_did,
role = %entry.role,
"control plane DID already in ACL"
);
}
Ok(None) => {
let entry = AclEntry {
did: control_did.clone(),
role: Role::Service,
label: Some("control-plane".to_string()),
created_at: crate::auth::session::now_epoch(),
max_total_size: None,
max_did_count: None,
domains: did_hosting_common::server::domain::DomainScope::All,
};
if let Err(e) = store_acl_entry(&state.acl_ks, &entry).await {
warn!(error = %e, "failed to add control plane DID to ACL");
} else {
info!(control_did = %control_did, "added control plane DID to ACL with service role");
}
}
Err(e) => {
warn!(error = %e, "failed to check ACL for control plane DID");
}
}
let public_url = state.config.public_url.clone().unwrap_or_default();
if let Err(e) = didcomm_svc
.wait_connected("server", std::time::Duration::from_secs(30))
.await
{
warn!(error = %e, "timed out waiting for mediator connection — skipping registration");
return;
}
let enabled_methods: Vec<&str> = did_hosting_common::method::enabled_methods().to_vec();
let served_domains: Vec<String> = match list_domains(&state.store).await {
Ok(entries) => entries
.into_iter()
.filter(|d| matches!(d.status, DomainStatus::Active))
.map(|d| d.name)
.collect(),
Err(e) => {
warn!(
error = %e,
"failed to list local domains for registration — aborting this register attempt; retry loop will pick it up"
);
return;
}
};
let preloaded_dids: Vec<serde_json::Value> = match state.dids_ks.prefix_iter_raw("did:").await {
Ok(raw) => raw
.into_iter()
.filter_map(|(_k, v)| serde_json::from_slice::<DidRecord>(&v).ok())
.filter(|r| r.version_count > 0)
.map(|r| json!({ "mnemonic": r.mnemonic, "version_count": r.version_count }))
.collect(),
Err(e) => {
warn!(error = %e, "failed to enumerate local DIDs for registration — control plane will full-sync");
Vec::new()
}
};
let body = json!({
"public_url": public_url,
"label": "did-hosting-server",
"enabled_methods": enabled_methods,
"served_domains": served_domains,
"protocol_version": "1.0",
"trust_task_capable": true,
"sync_batch": true,
"preloaded_dids": preloaded_dids,
});
let control_speaks_tsp = matches!(
resolve_transport(&control_did, state.did_resolver.as_ref()).await,
Some((PeerTransport::Tsp, _))
);
let fallback = TransportFallback::from_config(
state.config.mediator_did.as_deref(),
state.config.features.tsp,
);
let outcome = if control_speaks_tsp {
match build_request(MSG_SERVER_REGISTER, &server_did, &control_did, body) {
Ok(doc) => send_trust_task_with_retry(
didcomm_svc,
"server",
&server_did,
&control_did,
&doc,
&fallback,
state.did_resolver.as_ref(),
Retry {
attempts: 10,
delay: std::time::Duration::from_secs(5),
},
)
.await
.map(|transport| {
info!(?transport, "server registration sent as trust task");
}),
Err(e) => Err(e),
}
} else {
let msg = Message::build(
uuid::Uuid::new_v4().to_string(),
MSG_SERVER_REGISTER.to_string(),
body,
)
.from(server_did.clone())
.to(control_did.clone())
.created_time(crate::auth::session::now_epoch())
.finalize();
didcomm_svc
.send_message_with_retry(
"server",
msg,
&control_did,
10,
std::time::Duration::from_secs(5),
)
.await
.map_err(|e| Box::new(e) as Box<dyn std::error::Error + Send + Sync>)
};
match outcome {
Ok(()) => {
REGISTERED.store(true, Ordering::Relaxed);
info!(control_did = %control_did, "server registered with control plane");
}
Err(e) => {
warn!(
error = %e,
"server registration failed — will accept sync but may not receive pushes"
);
}
}
}
pub async fn apply_did_updates(
dids_ks: &KeyspaceHandle,
store: &Store,
updates: &[DidSyncUpdate],
did_cache: &crate::cache::ContentCache,
) {
for update in updates {
if let Err(e) = apply_single_update(dids_ks, store, update, did_cache).await {
warn!(
mnemonic = %update.mnemonic,
error = %e,
"failed to apply DID sync update"
);
}
}
}
pub async fn apply_single_update(
dids_ks: &KeyspaceHandle,
store: &Store,
update: &DidSyncUpdate,
did_cache: &crate::cache::ContentCache,
) -> Result<(), crate::error::AppError> {
use crate::auth::session::now_epoch;
validate_did_jsonl(&update.log_content).map_err(crate::error::AppError::Validation)?;
let now = now_epoch();
let did_host = extract_did_host(&update.did_id).unwrap_or_default();
let record = DidRecord {
owner: "system".to_string(),
mnemonic: update.mnemonic.clone(),
created_at: now,
updated_at: now,
version_count: update.version_count,
did_id: Some(update.did_id.clone()),
content_size: update.log_content.len() as u64,
disabled: false,
deleted_at: None,
method: "webvh".to_string(),
domain: String::new(),
services: extract_service_types(&update.log_content),
agent_names: extract_agent_names(&update.log_content, &did_host)
.into_iter()
.filter(|name| validate_agent_name_binding(name, &update.mnemonic).is_ok())
.map(|name| AgentNameEntry {
name,
enabled: true,
created_at: now,
})
.collect(),
};
let previous: Option<DidRecord> = dids_ks.get(did_key(&update.mnemonic)).await.ok().flatten();
let mut batch = store.batch();
batch.insert(dids_ks, did_key(&update.mnemonic), &record)?;
if let Some(prev) = previous.as_ref() {
for old in &prev.agent_names {
if !record.agent_names.iter().any(|n| n.name == old.name) {
batch.remove(dids_ks, agent_name_key(&did_host, &old.name));
}
}
}
for entry in &record.agent_names {
batch.insert_raw(
dids_ks,
agent_name_key(&did_host, &entry.name),
update.mnemonic.as_bytes().to_vec(),
);
}
batch.insert_raw(
dids_ks,
content_log_key(&update.mnemonic),
update.log_content.as_bytes().to_vec(),
);
batch.insert_raw(
dids_ks,
owner_key("system", &update.mnemonic),
update.mnemonic.as_bytes().to_vec(),
);
if let Some(ref witness) = update.witness_content {
batch.insert_raw(
dids_ks,
content_witness_key(&update.mnemonic),
witness.as_bytes().to_vec(),
);
}
batch.commit().await?;
did_cache.invalidate(&content_log_key(&update.mnemonic));
info!(
mnemonic = %update.mnemonic,
did = %update.did_id,
"applied DID sync update from control plane"
);
Ok(())
}