use serde::Serialize;
use tracing::info;
use did_hosting_common::did_ops::{DidRecord, did_key};
use did_hosting_common::server::acl;
use did_hosting_common::server::domain::{self, DomainEntry, DomainScope};
use did_hosting_common::server::identity::mnemonic_from_did;
use crate::auth::AuthClaims;
use crate::error::AppError;
use crate::server::AppState;
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()))
}
#[derive(Debug, Serialize)]
pub struct TimeSeriesPoint {
pub timestamp: u64,
pub resolves: u64,
pub updates: u64,
}
pub(crate) 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)
}
pub(crate) 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)
}
pub(crate) 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
}
#[derive(Debug, Serialize)]
pub struct DomainListResponse {
pub domains: Vec<DomainEntry>,
#[serde(skip_serializing_if = "Option::is_none")]
pub default: Option<String>,
}
pub(crate) async fn fetch_me_domains_for_caller(
auth: &AuthClaims,
state: &AppState,
) -> Result<DomainListResponse, AppError> {
let all = domain::list_domains(&state.store).await?;
let scope = match acl::get_acl_entry(&state.acl_ks, &auth.did).await? {
Some(entry) => entry.domains,
None => DomainScope::All,
};
let role_overrides_scope = matches!(
auth.role,
crate::acl::Role::Admin | crate::acl::Role::Service
);
let mut domains: Vec<DomainEntry> = if role_overrides_scope {
all
} else {
all.into_iter().filter(|d| scope.allows(&d.name)).collect()
};
domains.sort_by(|a, b| a.name.cmp(&b.name));
let default = match scope.default_domain() {
Some(d) => Some(d.to_string()),
None => domain::get_default_domain(&state.store).await?,
};
info!(
caller = %auth.did,
count = domains.len(),
"caller listed scoped domains"
);
Ok(DomainListResponse { domains, default })
}
pub(crate) async fn server_agent_names(state: &AppState) -> Vec<String> {
if !state.config.features.agent_names {
return Vec::new();
}
let Some(did) = state.config.server_did.as_deref() else {
return Vec::new();
};
let Some(mnemonic) = mnemonic_from_did(did) else {
return Vec::new();
};
let Ok(Some(record)) = state.dids_ks.get::<DidRecord>(did_key(&mnemonic)).await else {
return Vec::new();
};
if record.did_id.as_deref() != Some(did) || record.disabled || record.deleted_at.is_some() {
return Vec::new();
}
record
.agent_names
.iter()
.filter(|e| e.enabled)
.map(|e| e.name.clone())
.collect()
}