use crate::auth::{AdminAuth, AuthClaims};
use crate::did_ops;
use crate::error::AppError;
use crate::server::AppState;
use crate::server_push;
use axum::Json;
use axum::extract::{Path, Query, State};
use axum::http::StatusCode;
use did_hosting_common::did_ops::LogMetadata;
use did_hosting_common::{
CheckNameResponse, DidListEntry, DidRegisterRequest, DidRegisterResponse, RequestUriResponse,
};
use serde::{Deserialize, Serialize};
use tracing::info;
fn clean_mnemonic(m: &str) -> &str {
m.trim_start_matches('/')
}
#[derive(Debug, Default, Deserialize)]
pub struct DomainQuery {
#[serde(default)]
pub domain: Option<String>,
}
#[derive(Debug, Deserialize)]
pub struct CheckNameRequest {
pub path: String,
}
pub async fn check_name(
auth: AuthClaims,
State(state): State<AppState>,
Json(req): Json<CheckNameRequest>,
) -> Result<Json<CheckNameResponse>, AppError> {
let result = did_ops::check_name(&state, &req.path).await?;
info!(did = %auth.did, path = %req.path, available = result.available, "name availability checked");
Ok(Json(result))
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentNameRequest {
pub mnemonic: String,
pub name: String,
#[serde(alias = "didData")]
pub did_log: String,
#[serde(default)]
pub domain: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentNameUpdateRequest {
pub mnemonic: String,
pub name: String,
pub state: did_ops::AgentNameState,
#[serde(alias = "didLog")]
pub did_data: String,
#[serde(default)]
pub domain: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentNameCheckRequest {
pub name: String,
#[serde(default)]
pub domain: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct AgentNameResolveRequest {
pub dids: Vec<String>,
}
#[derive(Debug, Serialize)]
pub struct AgentNameResolveResponse {
pub names: std::collections::HashMap<String, Vec<String>>,
}
pub(crate) fn agent_name_record_response(
state: &AppState,
record: &did_hosting_common::did_ops::DidRecord,
) -> serde_json::Value {
let base_url = state
.config
.did_hosting_url
.as_deref()
.or(state.config.public_url.as_deref())
.unwrap_or("http://localhost");
let did_url = format!(
"{}/{}/did.jsonl",
base_url.trim_end_matches('/'),
record.mnemonic
);
serde_json::json!({ "record": crate::messaging::spec_did_record_json(record, &did_url) })
}
pub async fn update_agent_name(
auth: AuthClaims,
State(state): State<AppState>,
Json(req): Json<AgentNameUpdateRequest>,
) -> Result<Json<serde_json::Value>, AppError> {
let record = did_ops::update_agent_name(
&auth,
&state,
&req.mnemonic,
&req.name,
&req.did_data,
req.domain.as_deref(),
req.state,
)
.await?;
server_push::notify_servers_did(&state, req.mnemonic.clone());
info!(did = %auth.did, mnemonic = %req.mnemonic, name = %req.name, state = ?req.state, "agent name updated via REST");
Ok(Json(agent_name_record_response(&state, &record)))
}
pub async fn remove_agent_name(
auth: AuthClaims,
State(state): State<AppState>,
Json(req): Json<AgentNameRequest>,
) -> Result<Json<serde_json::Value>, AppError> {
let record = did_ops::remove_agent_name(
&auth,
&state,
&req.mnemonic,
&req.name,
&req.did_log,
req.domain.as_deref(),
)
.await?;
server_push::notify_servers_did(&state, req.mnemonic.clone());
info!(did = %auth.did, mnemonic = %req.mnemonic, name = %req.name, "agent name removed via REST");
Ok(Json(agent_name_record_response(&state, &record)))
}
pub async fn resolve_agent_names(
_auth: AuthClaims,
State(state): State<AppState>,
Json(req): Json<AgentNameResolveRequest>,
) -> Result<Json<AgentNameResolveResponse>, AppError> {
let names = did_ops::resolve_agent_names(&state, &req.dids).await?;
Ok(Json(AgentNameResolveResponse { names }))
}
pub(crate) async fn resolve_agent_name_domain(
auth: &AuthClaims,
state: &AppState,
requested: Option<&str>,
) -> Result<String, AppError> {
let acl_scope =
match did_hosting_common::server::acl::get_acl_entry(&state.acl_ks, &auth.did).await? {
Some(e) => e.domains,
None => did_hosting_common::server::domain::DomainScope::All,
};
let system_default = did_hosting_common::server::domain::get_default_domain(&state.store)
.await
.ok()
.flatten();
did_hosting_common::server::domain::resolve_request_domain(
requested,
&acl_scope,
system_default.as_deref(),
)
.map_err(|e| AppError::Validation(e.to_string()))
}
pub async fn check_agent_name(
auth: AuthClaims,
State(state): State<AppState>,
Json(req): Json<AgentNameCheckRequest>,
) -> Result<Json<did_ops::AgentNameAvailability>, AppError> {
let domain = resolve_agent_name_domain(&auth, &state, req.domain.as_deref()).await?;
let result = did_ops::check_agent_name(&state, &domain, &req.name).await?;
info!(
did = %auth.did,
name = %req.name,
domain = %domain,
available = result.available,
"agent name availability checked"
);
Ok(Json(result))
}
#[derive(Debug, Deserialize, Default)]
pub struct RequestUriRequest {
pub path: Option<String>,
#[serde(default)]
pub force: bool,
pub domain: Option<String>,
}
pub async fn request_uri(
auth: AuthClaims,
State(state): State<AppState>,
body: Option<Json<RequestUriRequest>>,
) -> Result<(StatusCode, Json<RequestUriResponse>), AppError> {
let (path, force, request_domain) = match body {
Some(Json(b)) => (b.path, b.force, b.domain),
None => (None, false, None),
};
let acl_scope =
match did_hosting_common::server::acl::get_acl_entry(&state.acl_ks, &auth.did).await? {
Some(e) => e.domains,
None => did_hosting_common::server::domain::DomainScope::All,
};
let system_default = did_hosting_common::server::domain::get_default_domain(&state.store)
.await
.ok()
.flatten();
let resolved_domain = did_hosting_common::server::domain::resolve_request_domain(
request_domain.as_deref(),
&acl_scope,
system_default.as_deref(),
)
.ok();
let result = did_ops::create_did(
&auth,
&state,
path.as_deref(),
force,
resolved_domain.as_deref(),
)
.await?;
Ok((StatusCode::CREATED, Json(result)))
}
pub async fn register_did(
auth: AuthClaims,
State(state): State<AppState>,
Json(req): Json<DidRegisterRequest>,
) -> Result<Json<DidRegisterResponse>, AppError> {
let (method, payload) = req.resolve().map_err(AppError::Validation)?;
if method != "webvh" {
return Err(AppError::Validation(format!(
"registration via REST is currently webvh-only; received method = '{method}'. \
Use PUT /api/dids/{{mnemonic}} with the appropriate Content-Type once \
T26 follow-up wires per-method storage.",
)));
}
let did_log = std::str::from_utf8(&payload)
.map_err(|e| AppError::Validation(format!("webvh `did_data` is not valid UTF-8: {e}")))?;
let acl_scope =
match did_hosting_common::server::acl::get_acl_entry(&state.acl_ks, &auth.did).await? {
Some(e) => e.domains,
None => did_hosting_common::server::domain::DomainScope::All,
};
let system_default = did_hosting_common::server::domain::get_default_domain(&state.store)
.await
.ok()
.flatten();
let resolved_domain = did_hosting_common::server::domain::resolve_request_domain(
req.domain.as_deref(),
&acl_scope,
system_default.as_deref(),
)
.map_err(|e| AppError::Validation(e.to_string()))?;
info!(
did = %auth.did,
path = %req.path,
resolved_domain = %resolved_domain,
explicit = req.domain.is_some(),
"domain resolved for atomic register"
);
let result = did_ops::register_did_atomic(&auth, &state, &req.path, did_log, req.force).await?;
server_push::notify_servers_did(&state, result.mnemonic.clone());
info!(
did = %auth.did,
path = %result.mnemonic,
force = req.force,
"DID atomically registered via REST"
);
Ok(Json(DidRegisterResponse {
mnemonic: result.mnemonic,
did_url: result.did_url,
}))
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DidDetailResponse {
pub mnemonic: String,
pub created_at: u64,
pub updated_at: u64,
pub version_count: u64,
pub did_id: Option<String>,
pub owner: String,
pub disabled: bool,
pub log: Option<LogMetadata>,
#[serde(skip_serializing_if = "Option::is_none")]
pub method: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub domain: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub agent_names: Vec<did_hosting_common::did_ops::AgentNameEntry>,
}
pub async fn get_did(
auth: AuthClaims,
State(state): State<AppState>,
Path(mnemonic): Path<String>,
) -> Result<Json<DidDetailResponse>, AppError> {
let mnemonic = clean_mnemonic(&mnemonic);
let (record, log_metadata) = did_ops::get_did_info(&auth, &state, mnemonic).await?;
Ok(Json(DidDetailResponse {
mnemonic: record.mnemonic,
created_at: record.created_at,
updated_at: record.updated_at,
version_count: record.version_count,
did_id: record.did_id,
owner: record.owner,
disabled: record.disabled,
log: log_metadata,
method: (!record.method.is_empty()).then(|| record.method.clone()),
domain: (!record.domain.is_empty()).then(|| record.domain.clone()),
agent_names: record.agent_names,
}))
}
pub async fn get_did_log(
auth: AuthClaims,
State(state): State<AppState>,
Path(mnemonic): Path<String>,
) -> Result<Json<Vec<did_hosting_common::did_ops::LogEntryInfo>>, AppError> {
let mnemonic = clean_mnemonic(&mnemonic);
let entries = did_ops::get_did_log(&auth, &state, mnemonic).await?;
Ok(Json(entries))
}
pub async fn upload_did(
auth: AuthClaims,
State(state): State<AppState>,
Path(mnemonic): Path<String>,
Query(dq): Query<DomainQuery>,
headers: axum::http::HeaderMap,
body: String,
) -> Result<StatusCode, AppError> {
let mnemonic = clean_mnemonic(&mnemonic);
let content_type = headers
.get(axum::http::header::CONTENT_TYPE)
.and_then(|v| v.to_str().ok())
.map(|s| s.split(';').next().unwrap_or(s).trim().to_ascii_lowercase());
let method = match content_type.as_deref() {
None | Some("application/jsonl") | Some("application/jsonl+json") | Some("text/plain") => {
"webvh"
}
Some("application/did+json") | Some("application/json") => "web",
Some(other) => {
return Err(AppError::Validation(format!(
"unsupported Content-Type '{other}' on PUT /api/dids/{{mnemonic}}; \
use 'application/jsonl' (webvh) or 'application/did+json' (web)",
)));
}
};
if method != "webvh" {
return Err(AppError::Validation(format!(
"method '{method}' publish via PUT not yet wired; T26 follow-up will \
route per-method through `DidMethod::apply_update`",
)));
}
did_ops::publish_did(&auth, &state, mnemonic, &body, dq.domain.as_deref()).await?;
server_push::notify_servers_did(&state, mnemonic.to_string());
Ok(StatusCode::NO_CONTENT)
}
pub async fn upload_witness(
auth: AuthClaims,
State(state): State<AppState>,
Path(mnemonic): Path<String>,
body: String,
) -> Result<StatusCode, AppError> {
let mnemonic = clean_mnemonic(&mnemonic);
did_ops::upload_witness(&auth, &state, mnemonic, &body).await?;
server_push::notify_servers_did(&state, mnemonic.to_string());
Ok(StatusCode::NO_CONTENT)
}
pub async fn delete_did(
auth: AuthClaims,
State(state): State<AppState>,
Path(mnemonic): Path<String>,
Query(dq): Query<DomainQuery>,
) -> Result<StatusCode, AppError> {
let mnemonic = clean_mnemonic(&mnemonic);
did_ops::delete_did(&auth, &state, mnemonic, dq.domain.as_deref()).await?;
server_push::notify_servers_delete(&state, mnemonic.to_string());
Ok(StatusCode::NO_CONTENT)
}
#[derive(Debug, Deserialize)]
pub struct ChangeOwnerRequest {
pub new_owner: String,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ChangeOwnerResponse {
pub mnemonic: String,
pub owner: String,
pub updated_at: u64,
}
pub async fn change_owner(
auth: AuthClaims,
State(state): State<AppState>,
Path(mnemonic): Path<String>,
Json(req): Json<ChangeOwnerRequest>,
) -> Result<Json<ChangeOwnerResponse>, AppError> {
let mnemonic = clean_mnemonic(&mnemonic);
let record = did_ops::change_did_owner(&auth, &state, mnemonic, &req.new_owner).await?;
Ok(Json(ChangeOwnerResponse {
mnemonic: record.mnemonic,
owner: record.owner,
updated_at: record.updated_at,
}))
}
pub async fn disable_did(
auth: AuthClaims,
State(state): State<AppState>,
Path(mnemonic): Path<String>,
) -> Result<StatusCode, AppError> {
let mnemonic = clean_mnemonic(&mnemonic);
did_ops::set_did_disabled(&auth, &state, mnemonic, true).await?;
server_push::notify_servers_did(&state, mnemonic.to_string());
Ok(StatusCode::NO_CONTENT)
}
pub async fn enable_did(
auth: AuthClaims,
State(state): State<AppState>,
Path(mnemonic): Path<String>,
) -> Result<StatusCode, AppError> {
let mnemonic = clean_mnemonic(&mnemonic);
did_ops::set_did_disabled(&auth, &state, mnemonic, false).await?;
server_push::notify_servers_did(&state, mnemonic.to_string());
Ok(StatusCode::NO_CONTENT)
}
pub async fn rollback_did(
auth: AuthClaims,
State(state): State<AppState>,
Path(mnemonic): Path<String>,
) -> Result<Json<DidDetailResponse>, AppError> {
let mnemonic = clean_mnemonic(&mnemonic);
let (record, log_metadata) = did_ops::rollback_did(&auth, &state, mnemonic).await?;
server_push::notify_servers_did(&state, mnemonic.to_string());
Ok(Json(DidDetailResponse {
mnemonic: record.mnemonic,
created_at: record.created_at,
updated_at: record.updated_at,
version_count: record.version_count,
did_id: record.did_id,
owner: record.owner,
disabled: record.disabled,
log: log_metadata,
method: (!record.method.is_empty()).then(|| record.method.clone()),
domain: (!record.domain.is_empty()).then(|| record.domain.clone()),
agent_names: record.agent_names,
}))
}
pub async fn get_raw_log(
auth: AuthClaims,
State(state): State<AppState>,
Path(mnemonic): Path<String>,
) -> Result<
(
StatusCode,
[(axum::http::HeaderName, &'static str); 1],
String,
),
AppError,
> {
let mnemonic = clean_mnemonic(&mnemonic);
let content = did_ops::get_raw_log(&auth, &state, mnemonic).await?;
Ok((
StatusCode::OK,
[(
axum::http::header::CONTENT_TYPE,
"text/plain; charset=utf-8",
)],
content,
))
}
#[derive(Debug, Deserialize)]
pub struct ListDidsQuery {
pub owner: Option<String>,
pub limit: Option<usize>,
pub offset: Option<usize>,
}
pub async fn list_dids(
auth: AuthClaims,
State(state): State<AppState>,
Query(query): Query<ListDidsQuery>,
) -> Result<Json<Vec<DidListEntry>>, AppError> {
let entries = did_ops::list_dids(
&auth,
&state,
query.owner.as_deref(),
query.limit,
query.offset,
)
.await?;
Ok(Json(entries))
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ServerStatsResponse {
pub total_dids: u64,
pub total_resolves: u64,
pub total_updates: u64,
pub last_resolved_at: Option<u64>,
pub last_updated_at: Option<u64>,
}
pub async fn get_server_stats(
_auth: AuthClaims,
State(state): State<AppState>,
) -> Result<Json<ServerStatsResponse>, AppError> {
let agg = state.stats_collector.get_aggregate();
Ok(Json(ServerStatsResponse {
total_dids: agg.total_dids,
total_resolves: agg.total_resolves,
total_updates: agg.total_updates,
last_resolved_at: agg.last_resolved_at,
last_updated_at: agg.last_updated_at,
}))
}
pub async fn get_did_stats(
_auth: AuthClaims,
State(state): State<AppState>,
Path(mnemonic): Path<String>,
) -> Result<Json<did_hosting_common::DidStats>, AppError> {
let mnemonic = mnemonic.trim_start_matches('/');
let key = format!("stats:{mnemonic}");
let stats: did_hosting_common::DidStats = state.stats_ks.get(key).await?.unwrap_or_default();
Ok(Json(stats))
}
#[derive(Debug, Serialize)]
pub struct TimeSeriesPoint {
pub timestamp: u64,
pub resolves: u64,
pub updates: u64,
}
#[derive(Debug, Deserialize)]
pub struct TimeseriesQuery {
#[serde(default = "default_range")]
pub range: String,
pub domain: Option<String>,
}
fn default_range() -> String {
"24h".to_string()
}
pub async fn get_server_timeseries(
_auth: AuthClaims,
State(state): State<AppState>,
Query(params): Query<TimeseriesQuery>,
) -> Result<Json<Vec<TimeSeriesPoint>>, AppError> {
let points = match params.domain.as_deref() {
None | Some("") => query_timeseries(&state.timeseries_ks, "_all", ¶ms.range).await?,
Some(domain) => query_timeseries_by_domain(&state, domain, ¶ms.range).await?,
};
Ok(Json(points))
}
pub async fn get_did_timeseries(
_auth: AuthClaims,
State(state): State<AppState>,
Path(mnemonic): Path<String>,
Query(params): Query<TimeseriesQuery>,
) -> Result<Json<Vec<TimeSeriesPoint>>, AppError> {
let mnemonic = mnemonic.trim_start_matches('/');
let points = query_timeseries(&state.timeseries_ks, mnemonic, ¶ms.range).await?;
Ok(Json(points))
}
async fn query_timeseries(
timeseries_ks: &did_hosting_common::server::store::KeyspaceHandle,
mnemonic: &str,
range: &str,
) -> Result<Vec<TimeSeriesPoint>, AppError> {
use serde::Deserialize;
#[derive(Deserialize, Default)]
struct BucketData {
r: u64,
u: u64,
}
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let (duration, step) = match range {
"1h" => (3600u64, 300u64),
"7d" => (7 * 24 * 3600, 3600),
"30d" => (30 * 24 * 3600, 14400),
_ => (24 * 3600, 900), };
let cutoff = now.saturating_sub(duration);
let start = cutoff / 300 * 300; let end = now / 300 * 300;
let prefix = format!("ts:{mnemonic}:");
let raw = timeseries_ks.prefix_iter_raw(prefix.as_str()).await?;
let prefix_len = prefix.len();
let mut bucket_map: std::collections::HashMap<u64, (u64, u64)> =
std::collections::HashMap::new();
for (key, value) in &raw {
let key_str = std::str::from_utf8(key).unwrap_or_default();
if let Some(epoch_str) = key_str.get(prefix_len..)
&& let Ok(epoch) = epoch_str.parse::<u64>()
&& epoch >= cutoff
&& let Ok(data) = serde_json::from_slice::<BucketData>(value)
{
let entry = bucket_map.entry(epoch).or_insert((0, 0));
entry.0 += data.r;
entry.1 += data.u;
}
}
let mut points = Vec::new();
let mut ts = start;
while ts <= end {
let mut resolves = 0u64;
let mut updates = 0u64;
let mut bucket_ts = ts;
while bucket_ts < ts + step && bucket_ts <= end {
if let Some(&(r, u)) = bucket_map.get(&bucket_ts) {
resolves += r;
updates += u;
}
bucket_ts += 300;
}
points.push(TimeSeriesPoint {
timestamp: ts,
resolves,
updates,
});
ts += step;
}
Ok(points)
}
async fn query_timeseries_by_domain(
state: &AppState,
domain: &str,
range: &str,
) -> Result<Vec<TimeSeriesPoint>, AppError> {
use did_hosting_common::server::domain::extract_did_host;
let raw = state.dids_ks.prefix_iter_raw("did:").await?;
let mut mnemonics: Vec<String> = Vec::new();
for (_key, value) in &raw {
let Ok(record) = serde_json::from_slice::<did_hosting_common::did_ops::DidRecord>(value)
else {
continue;
};
let matches = if !record.domain.is_empty() {
record.domain == domain
} else if let Some(did_id) = record.did_id.as_deref() {
extract_did_host(did_id)
.map(|h| h == domain)
.unwrap_or(false)
} else {
false
};
if matches {
mnemonics.push(record.mnemonic);
}
}
use std::collections::HashMap;
#[derive(serde::Deserialize, Default)]
struct BucketData {
r: u64,
u: u64,
}
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.as_secs();
let (duration, step) = match range {
"1h" => (3600u64, 300u64),
"7d" => (7 * 24 * 3600, 3600),
"30d" => (30 * 24 * 3600, 14400),
_ => (24 * 3600, 900),
};
let cutoff = now.saturating_sub(duration);
let start = cutoff / 300 * 300;
let end = now / 300 * 300;
let mut bucket_map: HashMap<u64, (u64, u64)> = HashMap::new();
for mnemonic in &mnemonics {
let prefix = format!("ts:{mnemonic}:");
let raw = state.timeseries_ks.prefix_iter_raw(prefix.as_str()).await?;
let prefix_len = prefix.len();
for (key, value) in &raw {
let key_str = std::str::from_utf8(key).unwrap_or_default();
if let Some(epoch_str) = key_str.get(prefix_len..)
&& let Ok(epoch) = epoch_str.parse::<u64>()
&& epoch >= cutoff
&& let Ok(data) = serde_json::from_slice::<BucketData>(value)
{
let entry = bucket_map.entry(epoch).or_insert((0, 0));
entry.0 += data.r;
entry.1 += data.u;
}
}
}
let mut points = Vec::new();
let mut ts = start;
while ts <= end {
let mut resolves = 0u64;
let mut updates = 0u64;
let mut bucket_ts = ts;
let bucket_window_end = ts.saturating_add(step);
while bucket_ts < bucket_window_end && bucket_ts <= end {
if let Some(&(r, u)) = bucket_map.get(&bucket_ts) {
resolves += r;
updates += u;
}
bucket_ts = bucket_ts.saturating_add(300);
}
points.push(TimeSeriesPoint {
timestamp: ts,
resolves,
updates,
});
ts = ts.saturating_add(step);
}
Ok(points)
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ConfigResponse {
pub control_did: Option<String>,
pub mediator_did: Option<String>,
pub public_url: Option<String>,
pub did_hosting_url: Option<String>,
pub didcomm_enabled: bool,
pub tsp_enabled: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub advertised_services: Option<Vec<String>>,
pub rest_api_enabled: bool,
pub listen_address: String,
pub vta_url: Option<String>,
pub vta_did: Option<String>,
pub health_check_interval_secs: u64,
pub configured_instances: u64,
pub access_token_expiry: u64,
pub refresh_token_expiry: u64,
pub passkey_enrollment_ttl: u64,
pub deployment_mode: String,
pub agent_names_enabled: bool,
pub data_dir: String,
pub log_level: String,
pub log_format: String,
}
async fn control_advertised_services(state: &AppState) -> Option<Vec<String>> {
let did = state.config.server_did.as_deref()?;
let resolver = state.did_resolver.as_ref()?;
did_hosting_common::server::didcomm_profile::resolve_service_types(did, Some(resolver)).await
}
pub async fn get_config(_auth: AuthClaims, State(state): State<AppState>) -> Json<ConfigResponse> {
let advertised_services = control_advertised_services(&state).await;
let c = &state.config;
Json(ConfigResponse {
control_did: c.server_did.clone(),
mediator_did: c.mediator_did.clone(),
public_url: c.public_url.clone(),
did_hosting_url: c.did_hosting_url.clone(),
didcomm_enabled: c.features.didcomm,
tsp_enabled: c.features.tsp,
advertised_services,
rest_api_enabled: c.features.rest_api,
deployment_mode: c.features.deployment_mode.clone(),
agent_names_enabled: c.features.agent_names,
listen_address: format!("{}:{}", c.server.host, c.server.port),
vta_url: c.vta.url.clone(),
vta_did: c.vta.did.clone(),
health_check_interval_secs: c.registry.health_check_interval,
configured_instances: c.registry.instances.len() as u64,
access_token_expiry: c.auth.access_token_expiry,
refresh_token_expiry: c.auth.refresh_token_expiry,
passkey_enrollment_ttl: c.auth.passkey_enrollment_ttl,
data_dir: c.store.data_dir.display().to_string(),
log_level: c.log.level.clone(),
log_format: format!("{:?}", c.log.format).to_lowercase(),
})
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ServiceOverviewResponse {
pub control: ControlInfo,
pub services: Vec<ServiceInfo>,
pub aggregate: AggregateStats,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ControlInfo {
pub version: String,
pub server_did: Option<String>,
pub public_url: Option<String>,
pub didcomm_enabled: bool,
pub tsp_enabled: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub advertised_services: Option<Vec<String>>,
pub total_local_dids: u64,
pub enabled_methods: Vec<&'static str>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ServiceInfo {
pub instance_id: String,
pub service_type: String,
pub label: Option<String>,
pub url: String,
pub status: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub advertised_services: Option<Vec<String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub services_checked_at: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_inbound_transport:
Option<did_hosting_common::server::didcomm_profile::ObservedTransport>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_inbound_at: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_outbound_transport:
Option<did_hosting_common::server::didcomm_profile::ObservedTransport>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_outbound_at: Option<u64>,
pub last_health_check: Option<u64>,
pub registered_at: u64,
pub did: Option<String>,
pub stats: Option<ServiceStats>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ServiceStats {
pub total_dids: u64,
pub total_resolves: u64,
pub total_updates: u64,
pub last_resolved_at: Option<u64>,
pub last_updated_at: Option<u64>,
}
#[derive(Debug, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AggregateStats {
pub total_services: u64,
pub active_services: u64,
pub degraded_services: u64,
pub unreachable_services: u64,
pub total_dids: u64,
pub total_resolves: u64,
pub total_updates: u64,
}
pub async fn get_services_overview(
_auth: AdminAuth,
State(state): State<AppState>,
) -> Result<Json<ServiceOverviewResponse>, AppError> {
use crate::registry;
let instances = registry::list_instances(&state.registry_ks).await?;
let agg = state.stats_collector.get_aggregate();
let mut active = 0u64;
let mut degraded = 0u64;
let mut unreachable = 0u64;
let mut services: Vec<ServiceInfo> = Vec::with_capacity(instances.len());
for inst in &instances {
match inst.status {
registry::ServiceStatus::Active => active += 1,
registry::ServiceStatus::Degraded => degraded += 1,
registry::ServiceStatus::Unreachable => unreachable += 1,
}
let service_did = inst
.metadata
.get("did")
.and_then(|v| v.as_str())
.map(String::from);
let stats: Option<ServiceStats> = None;
let status_str = format!("{:?}", inst.status).to_lowercase();
services.push(ServiceInfo {
instance_id: inst.instance_id.clone(),
service_type: format!("{:?}", inst.service_type).to_lowercase(),
label: inst.label.clone(),
url: inst.url.clone(),
status: status_str,
last_health_check: inst.last_health_check,
registered_at: inst.registered_at,
did: service_did,
stats,
advertised_services: inst.advertised_services.clone(),
services_checked_at: inst.services_checked_at,
last_inbound_transport: inst.last_inbound_transport,
last_inbound_at: inst.last_inbound_at,
last_outbound_transport: inst.last_outbound_transport,
last_outbound_at: inst.last_outbound_at,
});
}
let local_dids = state.dids_ks.prefix_iter_raw("did:").await?.len() as u64;
let control_services = control_advertised_services(&state).await;
Ok(Json(ServiceOverviewResponse {
control: ControlInfo {
version: env!("CARGO_PKG_VERSION").to_string(),
server_did: state.config.server_did.clone(),
public_url: state.config.public_url.clone(),
didcomm_enabled: state.config.features.didcomm,
tsp_enabled: state.config.features.tsp,
advertised_services: control_services,
total_local_dids: local_dids,
enabled_methods: did_hosting_common::method::enabled_methods().to_vec(),
},
aggregate: AggregateStats {
total_services: services.len() as u64,
active_services: active,
degraded_services: degraded,
unreachable_services: unreachable,
total_dids: agg.total_dids.max(local_dids),
total_resolves: agg.total_resolves,
total_updates: agg.total_updates,
},
services,
}))
}