use std::sync::Arc;
use axum::body::Bytes;
use axum::extract::State;
use axum::http::{HeaderMap, StatusCode};
use axum::response::IntoResponse;
use axum::Json;
use base64::engine::general_purpose::STANDARD as BASE64;
use base64::Engine;
use serde::{Deserialize, Serialize};
use crate as x0x;
use super::super::state::AppState;
use super::super::{
api_error, bad_request, has_withdrawn_same_stable_group_record, parse_optional_json,
ApiResponse,
};
#[derive(Debug, Deserialize)]
pub(in crate::server) struct AgentSignRequest {
context: String,
payload_b64: String,
}
#[derive(Debug, Deserialize)]
pub(in crate::server) struct AgentVerifyRequest {
payload_b64: String,
signature_b64: String,
public_key_b64: String,
context: String,
#[serde(default, deserialize_with = "deserialize_present")]
algorithm: Option<serde_json::Value>,
}
#[derive(Debug, Default, Deserialize)]
pub(in crate::server) struct AnnounceIdentityRequest {
#[serde(default)]
include_user_identity: bool,
#[serde(default)]
human_consent: bool,
}
pub(in crate::server) async fn agent_info(
State(state): State<Arc<AppState>>,
) -> Json<ApiResponse<AgentData>> {
use base64::Engine as _;
Json(ApiResponse {
ok: true,
data: AgentData {
agent_id: hex::encode(state.agent.agent_id().as_bytes()),
machine_id: hex::encode(state.agent.machine_id().as_bytes()),
user_id: state.agent.user_id().map(|u| hex::encode(u.as_bytes())),
kem_public_key_b64: BASE64.encode(&state.agent_kem_keypair.public_bytes),
},
})
}
#[derive(Debug, Deserialize)]
pub(in crate::server) struct IntroductionQuery {
#[serde(default)]
peer: Option<String>,
}
pub(in crate::server) async fn introduction(
State(state): State<Arc<AppState>>,
axum::extract::Query(query): axum::extract::Query<IntroductionQuery>,
) -> axum::response::Response {
use axum::response::IntoResponse;
let peer_trust = if let Some(ref peer_hex) = query.peer {
let Ok(peer_bytes) = hex::decode(peer_hex) else {
return (
StatusCode::BAD_REQUEST,
axum::Json(serde_json::json!({"error": "invalid peer agent_id hex"})),
)
.into_response();
};
if peer_bytes.len() != 32 {
return (
StatusCode::BAD_REQUEST,
axum::Json(serde_json::json!({"error": "peer agent_id must be 32 bytes"})),
)
.into_response();
}
let mut id_bytes = [0u8; 32];
id_bytes.copy_from_slice(&peer_bytes);
let peer_id = x0x::identity::AgentId(id_bytes);
state.contacts.read().await.trust_level(&peer_id)
} else {
x0x::contacts::TrustLevel::Unknown
};
if peer_trust == x0x::contacts::TrustLevel::Blocked {
return (
StatusCode::FORBIDDEN,
axum::Json(serde_json::json!({"error": "blocked"})),
)
.into_response();
}
let identity = state.agent.identity();
let all_services = vec![
x0x::identity::ServiceEntry {
name: "presence".to_string(),
description: "Online/offline presence visibility".to_string(),
min_trust: "unknown".to_string(),
},
x0x::identity::ServiceEntry {
name: "direct-message".to_string(),
description: "Send and receive direct encrypted messages".to_string(),
min_trust: "known".to_string(),
},
x0x::identity::ServiceEntry {
name: "mls-group".to_string(),
description: "Join MLS encrypted group conversations".to_string(),
min_trust: "known".to_string(),
},
x0x::identity::ServiceEntry {
name: "file-transfer".to_string(),
description: "Send and receive files".to_string(),
min_trust: "trusted".to_string(),
},
x0x::identity::ServiceEntry {
name: "payment".to_string(),
description: "Payment address exchange".to_string(),
min_trust: "trusted".to_string(),
},
];
let peer_rank = peer_trust.rank();
let visible_services: Vec<_> = all_services
.into_iter()
.filter(|s| {
s.min_trust
.parse::<x0x::contacts::TrustLevel>()
.map(|t| peer_rank >= t.rank())
.unwrap_or(false)
})
.collect();
let card =
match x0x::identity::IntroductionCard::from_identity(identity, None, visible_services) {
Ok(c) => c,
Err(e) => {
tracing::warn!("failed to build introduction card: {e}");
return (
StatusCode::INTERNAL_SERVER_ERROR,
axum::Json(serde_json::json!({
"error": "failed to build introduction card",
"detail": format!("{e}"),
})),
)
.into_response();
}
};
let data = match peer_trust {
x0x::contacts::TrustLevel::Unknown => IntroductionCardData {
agent_id: hex::encode(card.agent_id.as_bytes()),
machine_id: None,
user_id: None,
certificate: None,
display_name: card.display_name,
identity_words: card.identity_words,
services: card
.services
.iter()
.map(|s| ServiceEntryData {
name: s.name.clone(),
description: s.description.clone(),
min_trust: s.min_trust.clone(),
})
.collect(),
signature: None,
},
x0x::contacts::TrustLevel::Known => IntroductionCardData {
agent_id: hex::encode(card.agent_id.as_bytes()),
machine_id: Some(hex::encode(card.machine_id.as_bytes())),
user_id: card.user_id.map(|u| hex::encode(u.as_bytes())),
certificate: card.certificate.as_ref().map(|_| "(present)".to_string()),
display_name: card.display_name,
identity_words: card.identity_words,
services: card
.services
.iter()
.map(|s| ServiceEntryData {
name: s.name.clone(),
description: s.description.clone(),
min_trust: s.min_trust.clone(),
})
.collect(),
signature: Some(hex::encode(&card.signature[..8])),
},
_ => IntroductionCardData {
agent_id: hex::encode(card.agent_id.as_bytes()),
machine_id: Some(hex::encode(card.machine_id.as_bytes())),
user_id: card.user_id.map(|u| hex::encode(u.as_bytes())),
certificate: card.certificate.as_ref().map(|_| "(present)".to_string()),
display_name: card.display_name,
identity_words: card.identity_words,
services: card
.services
.iter()
.map(|s| ServiceEntryData {
name: s.name.clone(),
description: s.description.clone(),
min_trust: s.min_trust.clone(),
})
.collect(),
signature: Some(hex::encode(&card.signature[..8])),
},
};
axum::Json(ApiResponse { ok: true, data }).into_response()
}
pub(in crate::server) async fn announce_identity(
State(state): State<Arc<AppState>>,
headers: HeaderMap,
body: Bytes,
) -> impl IntoResponse {
let req: AnnounceIdentityRequest = match parse_optional_json(&headers, &body) {
Ok(r) => r,
Err(resp) => return resp.into_response(),
};
match state
.agent
.announce_identity(req.include_user_identity, req.human_consent)
.await
{
Ok(()) => (
StatusCode::OK,
Json(serde_json::json!({
"ok": true,
"include_user_identity": req.include_user_identity,
})),
)
.into_response(),
Err(e) => bad_request(format!("{e}")).into_response(),
}
}
#[derive(Debug, Deserialize)]
pub(in crate::server) struct ImportCardRequest {
card: String,
#[serde(default = "default_import_trust")]
trust_level: String,
}
fn default_import_trust() -> String {
"known".to_string()
}
#[derive(Debug, Deserialize)]
pub(in crate::server) struct CardQuery {
#[serde(default)]
pub(in crate::server) display_name: Option<String>,
#[serde(default)]
pub(in crate::server) include_groups: Option<bool>,
#[serde(default)]
pub(in crate::server) include_local_addresses: bool,
}
fn discover_local_card_addresses(port: u16, addresses: &mut Vec<String>, include_local: bool) {
for addr in x0x::collect_local_interface_addrs(port) {
if !include_local && !x0x::is_publicly_advertisable(addr) {
continue;
}
let s = addr.to_string();
if !addresses.contains(&s) {
addresses.push(s);
}
}
}
fn prioritize_local_card_addresses(addresses: &mut [String]) {
addresses.sort_by_key(|addr| {
addr.parse::<std::net::SocketAddr>()
.map(x0x::is_publicly_advertisable)
.unwrap_or(true)
});
}
pub(in crate::server) fn populate_invite_base_state_from_group_info(
invite: &mut x0x::groups::invite::SignedInvite,
info: &x0x::groups::GroupInfo,
) {
invite.stable_group_id = Some(info.stable_group_id().to_string());
invite.group_created_at = Some(info.created_at);
invite.group_description = Some(info.description.clone());
invite.policy = Some(info.policy.clone());
invite.genesis_creation_nonce = info.genesis.as_ref().map(|g| g.creation_nonce.clone());
invite.base_state_revision = Some(info.state_revision);
invite.base_state_hash = Some(info.state_hash.clone());
let slim_roster = info
.members_v2
.iter()
.map(|(agent_id, member)| {
let mut slim = member.clone();
slim.treekem_key_package_b64 = None;
slim.kem_public_key_b64 = None;
(agent_id.clone(), slim)
})
.collect();
invite.base_members_v2 = Some(slim_roster);
invite.base_prev_state_hash = info.prev_state_hash.clone();
invite.secure_plane = Some(info.secure_plane);
invite.base_secret_epoch = Some(info.secret_epoch);
invite.base_security_binding = info.security_binding.clone();
}
pub(in crate::server) async fn get_agent_card(
State(state): State<Arc<AppState>>,
axum::extract::Query(query): axum::extract::Query<CardQuery>,
) -> impl IntoResponse {
let agent_id = state.agent.agent_id();
let machine_id = hex::encode(state.agent.machine_id().as_bytes());
let display_name = query.display_name.unwrap_or_default();
let mut card = x0x::groups::card::AgentCard::new(display_name, &agent_id, &machine_id);
card.dm_capabilities = Some(x0x::dm::DmCapabilities::v1_gossip_ready(
state.agent_kem_keypair.public_bytes.clone(),
));
card.user_id = state.agent.user_id().map(|u| hex::encode(u.as_bytes()));
if let Some(network) = state.agent.network() {
if let Some(ns) = network.node_status().await {
card.addresses = ns
.external_addrs
.iter()
.filter(|a| query.include_local_addresses || x0x::is_publicly_advertisable(**a))
.map(|a| a.to_string())
.collect();
discover_local_card_addresses(
ns.local_addr.port(),
&mut card.addresses,
query.include_local_addresses,
);
if query.include_local_addresses {
prioritize_local_card_addresses(&mut card.addresses);
}
}
}
if query.include_groups.unwrap_or(false) {
let groups = state.named_groups.read().await;
for info in groups.values() {
if info.withdrawn
|| has_withdrawn_same_stable_group_record(
&groups,
&info.mls_group_id,
Some(info.stable_group_id()),
)
{
continue;
}
let mut invite = x0x::groups::invite::SignedInvite::new(
info.mls_group_id.clone(),
info.name.clone(),
&agent_id,
x0x::groups::invite::DEFAULT_EXPIRY_SECS,
);
populate_invite_base_state_from_group_info(&mut invite, info);
let invite_link = match invite.encode_link() {
Ok(link) => link,
Err(e) => {
tracing::warn!(
group_id = %info.mls_group_id,
actual = e.actual,
limit = e.limit,
"skipping oversized group invite in agent card: {e}"
);
continue;
}
};
card.groups.push(x0x::groups::card::CardGroup {
name: info.name.clone(),
invite_link,
});
}
}
let stores = state.kv_stores.read().await;
for (topic, _) in stores.iter() {
card.stores.push(x0x::groups::card::CardStore {
name: topic.clone(),
topic: topic.clone(),
});
}
if let Err(e) = card.sign(state.agent.identity().agent_keypair()) {
tracing::warn!("failed to sign agent card: {e}");
}
let link = card.to_link();
Json(serde_json::json!({
"ok": true,
"card": card,
"link": link,
}))
}
pub(in crate::server) async fn get_a2a_agent_card(
State(state): State<Arc<AppState>>,
) -> impl IntoResponse {
let agent_id = state.agent.agent_id();
let machine_id = hex::encode(state.agent.machine_id().as_bytes());
let mut card = x0x::groups::card::AgentCard::new(String::new(), &agent_id, &machine_id);
card.dm_capabilities = Some(x0x::dm::DmCapabilities::v1_gossip_ready(
state.agent_kem_keypair.public_bytes.clone(),
));
card.user_id = state.agent.user_id().map(|u| hex::encode(u.as_bytes()));
if let Some(network) = state.agent.network() {
if let Some(ns) = network.node_status().await {
card.addresses = ns
.external_addrs
.iter()
.filter(|a| x0x::is_publicly_advertisable(**a))
.map(|a| a.to_string())
.collect();
}
}
{
let stores = state.kv_stores.read().await;
for (topic, _) in stores.iter() {
card.stores.push(x0x::groups::card::CardStore {
name: topic.clone(),
topic: topic.clone(),
});
}
}
if let Err(e) = card.sign(state.agent.identity().agent_keypair()) {
tracing::warn!("failed to sign A2A agent card: {e}");
}
let certificate_b64 = state.agent.identity().agent_certificate().and_then(|c| {
use base64::Engine;
bincode::serialize(c)
.ok()
.map(|b| base64::engine::general_purpose::STANDARD.encode(b))
});
let ctx = x0x::a2a::A2aContext {
version: env!("CARGO_PKG_VERSION").to_string(),
exec_enabled: state.exec_service.enabled(),
certificate_b64,
};
Json(x0x::a2a::a2a_card_from(&card, &ctx))
}
pub(in crate::server) async fn import_agent_card(
State(state): State<Arc<AppState>>,
Json(req): Json<ImportCardRequest>,
) -> impl IntoResponse {
let card = match x0x::groups::card::AgentCard::from_link(&req.card) {
Ok(c) => c,
Err(e) => {
return bad_request(format!("invalid card: {e}"));
}
};
if card.signature.is_some() {
if let Err(e) = card.verify_signature() {
return bad_request(format!("agent card signature invalid: {e}"));
}
}
let trust: x0x::contacts::TrustLevel = match req.trust_level.parse() {
Ok(t) => t,
Err(e) => {
return (
StatusCode::BAD_REQUEST,
Json(serde_json::json!({ "ok": false, "error": e })),
);
}
};
let agent_id_bytes: [u8; 32] = match hex::decode(&card.agent_id) {
Ok(bytes) if bytes.len() == 32 => {
let mut arr = [0u8; 32];
arr.copy_from_slice(&bytes);
arr
}
_ => {
return bad_request("invalid agent_id in card");
}
};
let agent_id = x0x::identity::AgentId(agent_id_bytes);
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let (effective_trust, change_ignored) = {
let mut store = state.contacts.write().await;
let existing = store.get(&agent_id).cloned();
let (eff, ignored) = match &existing {
Some(c) if c.trust_level == trust => (trust, false),
Some(c) if c.trust_level == x0x::contacts::TrustLevel::Blocked => {
(x0x::contacts::TrustLevel::Blocked, true)
}
Some(c) if c.trust_level.rank() >= trust.rank() => (c.trust_level, true),
_ => (trust, false),
};
let contact = x0x::contacts::Contact {
agent_id,
trust_level: eff,
label: Some(card.display_name.clone()),
added_at: existing.as_ref().map_or(now, |c| c.added_at),
last_seen: existing.as_ref().and_then(|c| c.last_seen),
identity_type: existing
.as_ref()
.map_or(x0x::contacts::IdentityType::default(), |c| c.identity_type),
machines: existing.as_ref().map_or(Vec::new(), |c| c.machines.clone()),
dm_capabilities: card.dm_capabilities.clone(),
};
store.add(contact);
(eff, ignored)
};
let machine_id_bytes: [u8; 32] = hex::decode(&card.machine_id)
.ok()
.and_then(|b| b.try_into().ok())
.unwrap_or([0u8; 32]);
let addresses: Vec<std::net::SocketAddr> = card
.addresses
.iter()
.filter_map(|a| a.parse().ok())
.collect();
let capability_store = state.agent.capability_store();
let mut inserted_dm_capability = false;
if machine_id_bytes != [0u8; 32] {
if let Some(caps) = card.dm_capabilities.clone() {
if caps.gossip_inbox && !caps.kem_public_key.is_empty() {
capability_store.insert(
agent_id,
x0x::identity::MachineId(machine_id_bytes),
caps,
x0x::dm_capability::now_unix_ms(),
);
inserted_dm_capability = true;
}
}
}
tracing::debug!(
target: "dm.trace",
stage = "agent_card_import_capability",
agent_id = %hex::encode(agent_id.as_bytes()),
machine_id = %hex::encode(machine_id_bytes),
card_has_capability = card.dm_capabilities.is_some(),
inserted = inserted_dm_capability,
capability_store_entries = capability_store.len(),
);
if machine_id_bytes != [0u8; 32] || !addresses.is_empty() {
state
.agent
.insert_discovered_agent_for_testing(x0x::DiscoveredAgent {
agent_id,
machine_id: x0x::identity::MachineId(machine_id_bytes),
user_id: None,
addresses,
announced_at: now,
last_seen: now,
machine_public_key: Vec::new(),
nat_type: None,
can_receive_direct: None,
is_relay: None,
is_coordinator: None,
reachable_via: Vec::new(),
relay_candidates: Vec::new(),
cert_not_after: None,
agent_certificate: None,
agent_public_key: Vec::new(),
})
.await;
}
(
StatusCode::OK,
Json(serde_json::json!({
"ok": true,
"agent_id": card.agent_id,
"display_name": card.display_name,
"trust_level": format!("{effective_trust:?}"),
"trust_change_ignored": change_ignored,
"groups": card.groups.len(),
"stores": card.stores.len(),
})),
)
}
const AGENT_SIGN_MAX_PAYLOAD_BYTES: usize = crate::api::agent_signing::MAX_PAYLOAD_BYTES;
const AGENT_SIGN_SCHEME_ID: &str = crate::api::agent_signing::SCHEME_ID;
pub(in crate::server) async fn agent_sign(
State(state): State<Arc<AppState>>,
Json(req): Json<AgentSignRequest>,
) -> impl IntoResponse {
let payload = match BASE64.decode(&req.payload_b64) {
Ok(bytes) => bytes,
Err(e) => {
return bad_request(format!("invalid base64 payload: {e}"));
}
};
if payload.is_empty() {
return bad_request("payload must be non-empty");
}
if payload.len() > AGENT_SIGN_MAX_PAYLOAD_BYTES {
return api_error(
StatusCode::PAYLOAD_TOO_LARGE,
format!(
"payload exceeds maximum signable size of {} bytes",
AGENT_SIGN_MAX_PAYLOAD_BYTES
),
);
}
if let Err(e) = crate::api::agent_signing::validate_context(&req.context) {
return bad_request(e.to_string());
}
let canonical = crate::api::agent_signing::assemble_buffer(&req.context, &payload);
let identity = state.agent.identity();
let keypair = identity.agent_keypair();
let signature = match ant_quic::crypto::raw_public_keys::pqc::sign_with_ml_dsa(
keypair.secret_key(),
&canonical,
) {
Ok(sig) => sig,
Err(e) => {
return api_error(
StatusCode::INTERNAL_SERVER_ERROR,
format!("signing failed: {e:?}"),
);
}
};
let signature_b64 = BASE64.encode(signature.as_bytes());
let public_key_b64 = BASE64.encode(keypair.public_key().as_bytes());
let agent_id_hex = hex::encode(state.agent.agent_id().as_bytes());
let mut resp = serde_json::json!({
"ok": true,
"agent_id": agent_id_hex,
"public_key_b64": public_key_b64,
"signature_b64": signature_b64,
"algorithm": AGENT_SIGN_SCHEME_ID,
});
resp["context"] = serde_json::Value::String(req.context);
(StatusCode::OK, Json(resp))
}
pub(in crate::server) async fn agent_verify(
Json(req): Json<AgentVerifyRequest>,
) -> impl IntoResponse {
let payload = match BASE64.decode(&req.payload_b64) {
Ok(bytes) => bytes,
Err(e) => {
return bad_request(format!("invalid base64 payload: {e}"));
}
};
if payload.is_empty() {
return bad_request("payload must be non-empty");
}
if payload.len() > AGENT_SIGN_MAX_PAYLOAD_BYTES {
return api_error(
StatusCode::PAYLOAD_TOO_LARGE,
format!(
"payload exceeds maximum verifiable size of {} bytes",
AGENT_SIGN_MAX_PAYLOAD_BYTES
),
);
}
if let Err(e) = crate::api::agent_signing::validate_context(&req.context) {
return bad_request(e.to_string());
}
let canonical = crate::api::agent_signing::assemble_buffer(&req.context, &payload);
if let Some(algorithm) = req.algorithm.as_ref() {
if algorithm.as_str() != Some(AGENT_SIGN_SCHEME_ID) {
return bad_request(format!(
"unsupported algorithm: {algorithm} (expected {AGENT_SIGN_SCHEME_ID})"
));
}
}
let signature_bytes = match BASE64.decode(&req.signature_b64) {
Ok(bytes) => bytes,
Err(e) => {
return bad_request(format!("invalid base64 signature: {e}"));
}
};
if signature_bytes.len() != ant_quic::crypto::raw_public_keys::pqc::ML_DSA_65_SIGNATURE_SIZE {
return bad_request(format!(
"signature must be exactly {} bytes for ML-DSA-65, got {}",
ant_quic::crypto::raw_public_keys::pqc::ML_DSA_65_SIGNATURE_SIZE,
signature_bytes.len()
));
}
let signature = match ant_quic::crypto::raw_public_keys::pqc::MlDsaSignature::from_bytes(
&signature_bytes,
) {
Ok(sig) => sig,
Err(e) => {
return bad_request(format!("invalid signature format: {e:?}"));
}
};
let public_key_bytes = match BASE64.decode(&req.public_key_b64) {
Ok(bytes) => bytes,
Err(e) => {
return bad_request(format!("invalid base64 public key: {e}"));
}
};
if public_key_bytes.len() != ant_quic::crypto::raw_public_keys::pqc::ML_DSA_65_PUBLIC_KEY_SIZE {
return bad_request(format!(
"public key must be exactly {} bytes for ML-DSA-65, got {}",
ant_quic::crypto::raw_public_keys::pqc::ML_DSA_65_PUBLIC_KEY_SIZE,
public_key_bytes.len()
));
}
let public_key =
match ant_quic::crypto::raw_public_keys::pqc::MlDsaPublicKey::from_bytes(&public_key_bytes)
{
Ok(pk) => pk,
Err(e) => {
return bad_request(format!("invalid public key format: {e:?}"));
}
};
let valid = ant_quic::crypto::raw_public_keys::pqc::verify_with_ml_dsa(
&public_key,
&canonical,
&signature,
)
.is_ok();
(
StatusCode::OK,
Json(serde_json::json!({
"ok": true,
"valid": valid,
"algorithm": AGENT_SIGN_SCHEME_ID,
})),
)
}
pub(in crate::server) async fn agent_user_id_handler(
State(state): State<Arc<AppState>>,
) -> Json<serde_json::Value> {
let user_id = state.agent.user_id().map(|uid| hex::encode(uid.0));
Json(serde_json::json!({
"ok": true,
"user_id": user_id,
}))
}
fn deserialize_present<'de, T, D>(deserializer: D) -> Result<Option<T>, D::Error>
where
T: serde::Deserialize<'de>,
D: serde::Deserializer<'de>,
{
T::deserialize(deserializer).map(Some)
}
#[derive(Debug, Serialize)]
pub(in crate::server) struct AgentData {
agent_id: String,
machine_id: String,
user_id: Option<String>,
kem_public_key_b64: String,
}
#[derive(Debug, Serialize)]
pub(in crate::server) struct IntroductionCardData {
agent_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
machine_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
user_id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
certificate: Option<String>,
display_name: Option<String>,
identity_words: String,
services: Vec<ServiceEntryData>,
#[serde(skip_serializing_if = "Option::is_none")]
signature: Option<String>,
}
#[derive(Debug, Serialize)]
pub(in crate::server) struct ServiceEntryData {
name: String,
description: String,
min_trust: String,
}
#[derive(Debug, Deserialize)]
pub(in crate::server) struct RevokeRequest {
agent_id: Option<String>,
machine_id: Option<String>,
reason: Option<String>,
}
pub(in crate::server) async fn identity_revoke(
State(state): State<Arc<AppState>>,
Json(body): Json<RevokeRequest>,
) -> impl IntoResponse {
use x0x::revocation::RevokedSubject;
let subject = match (body.agent_id.as_deref(), body.machine_id.as_deref()) {
(Some(hex), None) => {
let bytes = hex::decode(hex)
.ok()
.and_then(|b| b.try_into().ok())
.map(x0x::identity::AgentId);
match bytes {
Some(id) => RevokedSubject::Agent(id),
None => return bad_request("agent_id must be 32-byte hex"),
}
}
(None, Some(hex)) => {
let bytes = hex::decode(hex)
.ok()
.and_then(|b| b.try_into().ok())
.map(x0x::identity::MachineId);
match bytes {
Some(id) => RevokedSubject::Machine(id),
None => return bad_request("machine_id must be 32-byte hex"),
}
}
(Some(_), Some(_)) => return bad_request("supply exactly one of agent_id or machine_id"),
(None, None) => return bad_request("supply exactly one of agent_id or machine_id"),
};
let issuer_keypair = state.agent.identity().agent_keypair();
match state
.agent
.revoke(issuer_keypair, subject, body.reason, None)
.await
{
Ok(record) => {
let resp = serde_json::json!({
"ok": true,
"subject": record.subject_hex(),
"subject_kind": record.subject_kind(),
"issuer": hex::encode(record.issuer_public_key_hash()),
"revoked_at": record.revoked_at,
"reason": record.reason,
});
(StatusCode::OK, Json(resp))
}
Err(e) => {
let msg = e.to_string();
if msg.contains("authority") || msg.contains("rejected") {
api_error(
StatusCode::FORBIDDEN,
format!("issuer lacks authority to revoke this subject: {e}"),
)
} else {
api_error(
StatusCode::INTERNAL_SERVER_ERROR,
format!("revocation failed: {e}"),
)
}
}
}
}
pub(in crate::server) async fn identity_revocations(
State(state): State<Arc<AppState>>,
) -> impl IntoResponse {
let records = state.agent.revocation_records().await;
let items: Vec<serde_json::Value> = records
.iter()
.map(|r| {
serde_json::json!({
"subject": r.subject_hex(),
"subject_kind": r.subject_kind(),
"issuer": hex::encode(r.issuer_public_key_hash()),
"revoked_at": r.revoked_at,
"reason": r.reason,
})
})
.collect();
(
StatusCode::OK,
Json(serde_json::json!({ "revocations": items })),
)
}