use axum::http::{StatusCode, request::Parts};
use axum::response::{IntoResponse, Response};
use did_hosting_common::method::webs::{CESR_CONTENT_TYPE, Webs};
use did_hosting_common::server::domain::assert_resolution_allowed;
use did_hosting_common::server::mnemonic::validate_webs_mnemonic;
use tracing::{debug, error};
use super::resolve_shared::extract_request_host;
use crate::did_ops::{self, DidRecord};
use crate::error::AppError;
use crate::server::AppState;
async fn webs_record(
state: &AppState,
mnemonic: &str,
request_host: Option<&str>,
) -> Result<Option<DidRecord>, AppError> {
let Some(record) = state
.dids_ks
.get::<DidRecord>(did_ops::did_key(mnemonic))
.await?
else {
return Ok(None);
};
if record.method != "webs" {
return Ok(None);
}
if record.disabled || record.deleted_at.is_some() {
return Err(AppError::NotFound(format!("content not found: {mnemonic}")));
}
if let Some(ref did_id) = record.did_id {
assert_resolution_allowed(&state.store, request_host, did_id).await?;
}
Ok(Some(record))
}
async fn load_keri_cesr(state: &AppState, mnemonic: &str) -> Result<Vec<u8>, AppError> {
let key = did_ops::content_log_key(mnemonic);
if let Some(cached) = state.did_cache.get(&key) {
#[cfg(feature = "metrics")]
did_hosting_common::server::metrics::inc_cache_hit();
return Ok((*cached).clone());
}
#[cfg(feature = "metrics")]
did_hosting_common::server::metrics::inc_cache_miss();
let data = state
.dids_ks
.get_raw(key.as_str())
.await?
.ok_or_else(|| AppError::NotFound(format!("content not found: {mnemonic}")))?;
state.did_cache.insert(key, data.clone());
Ok(data)
}
async fn serve_keri_cesr(
state: &AppState,
mnemonic: &str,
request_host: Option<&str>,
) -> Result<Response, AppError> {
if webs_record(state, mnemonic, request_host).await?.is_none() {
return Err(AppError::NotFound(format!("content not found: {mnemonic}")));
}
let content = load_keri_cesr(state, mnemonic).await?;
if let Some(ref collector) = state.stats_collector {
collector.record_resolve(mnemonic);
#[cfg(feature = "metrics")]
did_hosting_common::server::metrics::inc_resolve();
}
debug!(mnemonic = %mnemonic, size = content.len(), "keri.cesr resolved");
Ok((
StatusCode::OK,
[
("content-type", CESR_CONTENT_TYPE),
("cache-control", "public, max-age=300"),
],
content,
)
.into_response())
}
async fn serve_did_json(state: &AppState, record: &DidRecord) -> Result<Response, AppError> {
let mnemonic = &record.mnemonic;
let content = load_keri_cesr(state, mnemonic).await?;
let did_id = record.did_id.as_deref().ok_or_else(|| {
AppError::Internal(format!(
"did:webs record {mnemonic} has no did_id; cannot derive did.json"
))
})?;
let doc = Webs::derive_document(did_id, &content).map_err(|e| {
error!(
mnemonic = %mnemonic,
did = %did_id,
error = %e,
"stored keri.cesr no longer verifies; refusing to serve a did.json for it"
);
AppError::Internal(format!(
"stored key event log for {mnemonic} does not verify"
))
})?;
if let Some(ref collector) = state.stats_collector {
collector.record_resolve(mnemonic);
#[cfg(feature = "metrics")]
did_hosting_common::server::metrics::inc_resolve();
}
debug!(mnemonic = %mnemonic, size = doc.len(), "did:webs document derived");
Ok((
StatusCode::OK,
[
("content-type", "application/did+json"),
("cache-control", "public, max-age=300"),
],
doc,
)
.into_response())
}
pub async fn dispatch(state: &AppState, parts: &Parts) -> Option<Response> {
let path = parts.uri.path().trim_start_matches('/');
let host = extract_request_host(parts, &state.trusted_proxy_cidrs);
let host = host.as_deref();
if let Some(mnemonic) = path.strip_suffix("/keri.cesr")
&& !mnemonic.is_empty()
{
if let Err(e) = validate_webs_mnemonic(mnemonic) {
return Some(e.into_response());
}
return Some(
serve_keri_cesr(state, mnemonic, host)
.await
.unwrap_or_else(|e| e.into_response()),
);
}
if let Some(mnemonic) = path.strip_suffix("/did.json")
&& !mnemonic.is_empty()
{
if validate_webs_mnemonic(mnemonic).is_err() {
return None;
}
return match webs_record(state, mnemonic, host).await {
Ok(None) => None,
Ok(Some(record)) => Some(
serve_did_json(state, &record)
.await
.unwrap_or_else(|e| e.into_response()),
),
Err(e) => Some(e.into_response()),
};
}
None
}