use chrono::{DateTime, Utc};
use tracing::info;
use trust_tasks_rs::specs::did_management::did::register::v0_1::{
DidRecord, PayloadDidData, Response,
};
use vti_common::audit::{AuditEvent, CommunityDidLogInstalledData};
use vti_common::error::AppError;
use crate::did_log_install::{self, InstallError, InstallRefusal};
use crate::routes::did_log::did_log_label;
use crate::server::AppState;
const ROOT_SLOT: &str = ".well-known";
fn root_host(did: &str) -> Option<String> {
let rest = did.strip_prefix("did:webvh:")?;
let mut parts = rest.split(':');
let _scid = parts.next().filter(|s| !s.is_empty())?;
let host = parts.next().filter(|s| !s.is_empty())?;
if parts.next().is_some() {
return None;
}
Some(host.replace("%3A", ":").replace("%3a", ":"))
}
fn refused(r: InstallRefusal) -> AppError {
match r {
InstallRefusal::InvalidLog(_) => {
AppError::Validation(format!("did-management/did/register:invalidLog: {r}"))
}
InstallRefusal::WrongDid { .. } => {
AppError::Validation(format!("did-management/did/register:hostMismatch: {r}"))
}
InstallRefusal::NotAnExtension(_) => {
AppError::Conflict(format!("org.openvtc.vtc:didLogNotAnExtension: {r}"))
}
InstallRefusal::NotServedHere(_) => {
AppError::NotFound(format!("org.openvtc.vtc:didLogNotSelfHosted: {r}"))
}
}
}
pub(crate) async fn register_inner(
state: &AppState,
actor: &str,
payload: trust_tasks_rs::specs::did_management::did::register::v0_1::Payload,
) -> Result<Response, AppError> {
if payload.method.as_str() != "webvh" {
return Err(AppError::Validation(format!(
"did-management/did/register:invalidLog: a community self-hosts a did:webvh log; \
method `{}` is not one",
payload.method.as_str()
)));
}
let PayloadDidData::String(log) = &payload.did_data else {
return Err(AppError::Validation(
"did-management/did/register:invalidLog: a webvh `didData` is the log as JSON Lines \
text"
.into(),
));
};
let (did, path) = {
let config = state.config.read().await;
let did = config.vtc_did.clone().ok_or_else(|| {
refused(InstallRefusal::NotServedHere(
"this community has no DID yet".into(),
))
})?;
let label = did_log_label(&did);
let path = label.map(|l| config.store.data_dir.join("did").join(format!("{l}.jsonl")));
(did, path)
};
let host = root_host(&did).ok_or_else(|| {
refused(InstallRefusal::NotServedHere(format!(
"{did} has a path, so a DID host publishes it; send the new entry there"
)))
})?;
if payload.path.as_str() != ROOT_SLOT {
return Err(AppError::Validation(format!(
"did-management/did/register:hostMismatch: this community serves one slot, `{ROOT_SLOT}`, \
not `{}`",
payload.path.as_str()
)));
}
if let Some(domain) = payload.domain.as_deref()
&& domain != host
{
return Err(AppError::Validation(format!(
"did-management/did/register:hostMismatch: this community serves {host}, not {domain}"
)));
}
let path = path.ok_or_else(|| {
refused(InstallRefusal::NotServedHere(format!(
"{did} has no servable log label"
)))
})?;
let accepted = match did_log_install::install(&did, &path, log.as_str()).await {
Ok(a) => a,
Err(InstallError::Refused(r)) => return Err(refused(r)),
Err(InstallError::Io(e)) => return Err(AppError::Io(e)),
};
if accepted.entries_added > 0 {
if let Some(writer) = state.audit_writer.as_ref() {
writer
.write(
actor,
None,
AuditEvent::CommunityDidLogInstalled(CommunityDidLogInstalledData {
did: did.clone(),
version_id: accepted.version_id.clone(),
previous_version_id: accepted.previous_version_id.clone(),
entries_added: accepted.entries_added as u64,
}),
)
.await?;
}
info!(
%did,
version_id = %accepted.version_id,
entries_added = accepted.entries_added,
"installed a delivered DID log"
);
}
let now = Utc::now();
let created = accepted
.created
.as_deref()
.and_then(|t| DateTime::parse_from_rfc3339(t).ok())
.map(|t| t.with_timezone(&Utc))
.unwrap_or(now);
let owner = state
.config
.read()
.await
.vta_did
.clone()
.unwrap_or_else(|| did.clone());
let record: DidRecord = DidRecord::builder()
.mnemonic(ROOT_SLOT.to_string())
.owner(owner)
.created_at(created)
.updated_at(now)
.version_count(accepted.entry_count as u64)
.did_id(Some(did.clone()))
.did_url(Some(format!("https://{host}/{ROOT_SLOT}/did.jsonl")))
.method(Some("webvh".to_string()))
.domain(Some(host))
.disabled(Some(false))
.try_into()
.map_err(|e| AppError::Internal(format!("build DidRecord: {e}")))?;
Response::builder()
.record(record)
.try_into()
.map_err(|e| AppError::Internal(format!("build register response: {e}")))
}
#[cfg(test)]
mod tests {
use super::root_host;
#[test]
fn only_a_root_did_is_self_hosted() {
assert_eq!(
root_host("did:webvh:Qm:vtc.example.com").as_deref(),
Some("vtc.example.com")
);
assert_eq!(
root_host("did:webvh:Qm:localhost%3A8100").as_deref(),
Some("localhost:8100")
);
assert_eq!(root_host("did:webvh:Qm:dids.example.com:vtc"), None);
assert_eq!(root_host("did:key:z6Mk"), None);
}
}