use axum::extract::{Path, State};
use axum::http::{HeaderValue, StatusCode, header, request::Parts};
use axum::response::{IntoResponse, Response};
use did_hosting_common::did_ops::{self, DidRecord, agent_name_key};
use did_hosting_common::server::mnemonic::validate_agent_name;
use tracing::debug;
use super::resolve_shared::extract_request_host;
use crate::error::AppError;
use crate::server::AppState;
pub async fn serve(
State(state): State<AppState>,
Path(params): Path<AgentNamePath>,
parts: Parts,
) -> Response {
if params.name.is_empty() {
return AppError::NotFound("no such agent name".to_string()).into_response();
}
match resolve(&state, ¶ms.name, &parts).await {
Ok(response) => response,
Err(e) => e.into_response(),
}
}
pub async fn serve_community(State(state): State<AppState>, parts: Parts) -> Response {
match resolve(&state, "", &parts).await {
Ok(response) => response,
Err(e) => e.into_response(),
}
}
#[derive(Debug, serde::Deserialize)]
pub struct AgentNamePath {
pub name: String,
#[serde(default)]
pub context: Option<String>,
}
async fn resolve(state: &AppState, raw_name: &str, parts: &Parts) -> Result<Response, AppError> {
if !state.config.features.agent_names {
return Err(AppError::NotFound(format!(
"no such agent name: @{raw_name}"
)));
}
validate_agent_name(raw_name).map_err(|_| {
AppError::NotFound(format!("no such agent name: @{raw_name}"))
})?;
let name = raw_name.strip_prefix('@').unwrap_or(raw_name);
let host = extract_request_host(parts, &state.trusted_proxy_cidrs);
let Some(host) = host.as_deref() else {
return Err(AppError::NotFound(format!("no such agent name: @{name}")));
};
let Some(mnemonic) = state
.dids_ks
.get_raw(agent_name_key(host, name).into_bytes())
.await?
.and_then(|bytes| String::from_utf8(bytes).ok())
else {
return Err(AppError::NotFound(format!("no such agent name: @{name}")));
};
let Some(record) = state
.dids_ks
.get::<DidRecord>(did_ops::did_key(&mnemonic))
.await?
else {
debug!(%name, %mnemonic, "agent name index entry with no DID record; ignoring");
return Err(AppError::NotFound(format!("no such agent name: @{name}")));
};
if record.disabled || record.deleted_at.is_some() {
return Err(AppError::NotFound(format!("no such agent name: @{name}")));
}
let enabled = record
.agent_names
.iter()
.any(|entry| entry.name == name && entry.enabled);
if !enabled {
return Err(AppError::NotFound(format!("no such agent name: @{name}")));
}
let Some(did) = record.did_id else {
return Err(AppError::NotFound(format!("no such agent name: @{name}")));
};
debug!(%name, %did, "agent name resolved");
let wants_html = parts
.headers
.get(header::ACCEPT)
.and_then(|v| v.to_str().ok())
.is_some_and(|a| a.contains("text/html"));
let target = if wants_html {
format!("/{mnemonic}/did.jsonl")
} else {
did
};
let location = HeaderValue::from_str(&target)
.map_err(|_| AppError::NotFound(format!("no such agent name: @{name}")))?;
Ok((
StatusCode::FOUND,
[
(header::LOCATION, location),
(header::VARY, HeaderValue::from_static("accept")),
],
[(
header::CACHE_CONTROL,
HeaderValue::from_static("public, max-age=300"),
)],
)
.into_response())
}