use serde_json::Value;
use trust_tasks_rs::TrustTask;
use vti_common::acl::Capability;
use crate::auth::AuthClaims;
use crate::operations::room_issuance::{SigningContext, sign_as_room};
use crate::server::AppState;
use super::TrustTaskOutcome;
use super::helpers::{app_error_to_reject, parse_payload, success_response};
use super::room_group::record;
fn signing_context<'a>(state: &'a AppState, auth: &'a AuthClaims) -> SigningContext<'a> {
SigningContext {
keys_ks: &state.keys_ks,
imported_ks: &state.imported_ks,
internal_ks: &state.internal_ks,
contexts_ks: &state.contexts_ks,
acl_ks: &state.acl_ks,
seed_store: &state.seed_store,
audit: &state.audit_sink,
auth,
}
}
fn with_fresh_id(c: dtg_credentials::DTGCredential) -> dtg_credentials::DTGCredential {
c.with_id(format!("urn:uuid:{}", uuid::Uuid::new_v4()))
}
async fn issue(
state: &AppState,
auth: &AuthClaims,
doc: &TrustTask<Value>,
room_id: &str,
signing_key_id: &str,
audit_verb: &str,
credential: dtg_credentials::DTGCredential,
) -> TrustTaskOutcome {
let mut credential = with_fresh_id(credential);
match sign_as_room(
signing_context(state, auth),
signing_key_id,
room_id,
&mut credential,
)
.await
{
Ok((serialised, id)) => {
record(state, audit_verb, auth, room_id).await;
success_response(
doc,
serde_json::json!({ "credential": serialised, "credentialId": id }),
)
}
Err(e) => app_error_to_reject(doc, e),
}
}
pub(super) async fn handle_invite(
state: &AppState,
auth: &AuthClaims,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
if let Err(r) = super::helpers::require_capability(
state,
auth,
&doc,
Capability::CredentialWrite,
"inviting a party to a room",
)
.await
{
return r;
}
let req: trust_tasks_rs::specs::rooms::owner::invite::v0_1::Payload = match parse_payload(&doc)
{
Ok(r) => r,
Err(resp) => return resp,
};
let vic = dtg_credentials::DTGCredential::new_vic(
req.room_id.clone(),
req.subject.clone(),
chrono::Utc::now(),
req.valid_until,
);
issue(
state,
auth,
&doc,
&req.room_id,
&req.signing_key_id,
"rooms.owner.invite",
vic,
)
.await
}
pub(super) async fn handle_issue_membership(
state: &AppState,
auth: &AuthClaims,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
if let Err(r) = super::helpers::require_capability(
state,
auth,
&doc,
Capability::CredentialWrite,
"admitting a member to a room",
)
.await
{
return r;
}
let req: trust_tasks_rs::specs::rooms::owner::issue_membership::v0_1::Payload =
match parse_payload(&doc) {
Ok(r) => r,
Err(resp) => return resp,
};
let vmc = dtg_credentials::DTGCredential::new_vmc(
req.room_id.clone(),
req.subject.clone(),
chrono::Utc::now(),
req.valid_until,
false,
);
issue(
state,
auth,
&doc,
&req.room_id,
&req.signing_key_id,
"rooms.owner.issue-membership",
vmc,
)
.await
}
pub(super) async fn handle_issue_authority(
state: &AppState,
auth: &AuthClaims,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
if let Err(r) = super::helpers::require_capability(
state,
auth,
&doc,
Capability::CredentialWrite,
"granting authority in a room",
)
.await
{
return r;
}
let req: trust_tasks_rs::specs::rooms::owner::issue_authority::v0_2::Payload =
match parse_payload(&doc) {
Ok(r) => r,
Err(resp) => return resp,
};
let actions: Vec<String> = req.actions.iter().map(|a| a.to_string()).collect();
let vac = match dtg_credentials::DTGCredential::new_vac(
req.room_id.clone(),
req.subject.clone(),
req.room_id.clone(),
actions,
chrono::Utc::now(),
req.valid_until,
) {
Ok(v) => v,
Err(e) => {
return app_error_to_reject(
&doc,
vti_common::error::AppError::Validation(format!("build the grant: {e}")),
);
}
};
issue(
state,
auth,
&doc,
&req.room_id,
&req.signing_key_id,
"rooms.owner.issue-authority",
vac,
)
.await
}
#[cfg(feature = "webvh")]
pub(super) async fn handle_anchor(
state: &AppState,
auth: &AuthClaims,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
Box::pin(anchor_inner(state, auth, doc)).await
}
#[cfg(feature = "webvh")]
async fn anchor_inner(
state: &AppState,
auth: &AuthClaims,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
if let Err(r) = super::helpers::require_capability(
state,
auth,
&doc,
Capability::CredentialWrite,
"anchoring a room's state in its own log",
)
.await
{
return r;
}
let req: trust_tasks_rs::specs::rooms::owner::anchor::v0_1::Payload = match parse_payload(&doc)
{
Ok(r) => r,
Err(resp) => return resp,
};
let (vta_did, resolver) = match super::room_group::outbound_identity(state, &doc).await {
Ok(v) => v,
Err(resp) => return resp,
};
let (epoch, epoch_authenticator) = match crate::operations::room_groups::epoch_authenticator(
&state.room_groups_ks,
&req.room_id,
)
.await
{
Ok(a) => a,
Err(e) => return app_error_to_reject(&doc, e),
};
let minted =
match crate::operations::room_oracle::present(state, auth, &vta_did, &req.room_id, "read")
.await
{
Ok(m) => m,
Err(e) => return app_error_to_reject(&doc, e),
};
let key = format!("{vta_did}#key-0");
let reply = match crate::operations::room_host::send_room_task(
signing_context(state, auth),
&state.room_groups_ks,
&req.room_id,
&crate::operations::outbound::Outbound::from_app_state(state, &resolver),
&req.host,
&key,
&vta_did,
&key,
vti_rooms::wire::ROOMS_RECORDS_LIST_TYPE,
&format!("{}#response", vti_rooms::wire::ROOMS_RECORDS_LIST_TYPE),
serde_json::json!({
"roomId": req.room_id,
"presentation": minted.presentation,
"limit": 1,
}),
)
.await
{
Ok(v) => v,
Err(e) => return app_error_to_reject(&doc, e),
};
let head: vti_rooms::wire::ListRecordsResponse = match serde_json::from_value(reply) {
Ok(h) => h,
Err(e) => {
return app_error_to_reject(
&doc,
vti_common::error::AppError::Validation(format!(
"room host `{}` served a head this agent cannot read: {e}",
req.host
)),
);
}
};
let Some(head_version) = head.head_version else {
return app_error_to_reject(
&doc,
vti_common::error::AppError::Validation(format!(
"room host `{}` served no `headVersion`, so there is no state to anchor. A \
host that maintains no tree has nothing for this to pin.",
req.host
)),
);
};
match crate::operations::room_anchor::publish(
state,
auth,
&req.room_id,
&req.signing_key_id,
crate::operations::room_anchor::Anchor {
epoch,
epoch_authenticator,
head_version,
data_commitment: head.data_commitment.clone(),
record_count: head.record_count,
},
)
.await
{
Ok(published) => {
record(state, "rooms.owner.anchor", auth, &req.room_id).await;
success_response(
&doc,
serde_json::json!({
"roomId": req.room_id,
"anchored": published.anchored,
"versionId": published.version_id,
"reconciled": head
.record_count
.is_none_or(|committed| committed == head.records.len() as u64
|| head.cursor.is_some()),
}),
)
}
Err(e) => app_error_to_reject(&doc, e),
}
}
pub(super) async fn handle_register(
state: &AppState,
auth: &AuthClaims,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
if let Err(r) = super::helpers::require_capability(
state,
auth,
&doc,
Capability::CredentialWrite,
"registering a room with a host",
)
.await
{
return r;
}
let req: trust_tasks_rs::specs::rooms::owner::register::v0_1::Payload =
match parse_payload(&doc) {
Ok(r) => r,
Err(resp) => return resp,
};
let vta_did = match state.config.read().await.vta_did.clone() {
Some(d) => d,
None => {
return app_error_to_reject(
&doc,
vti_common::error::AppError::Validation(
"this agent has no DID of its own, so it cannot speak to a host as itself"
.into(),
),
);
}
};
let Some(resolver) = state.did_resolver.clone() else {
return app_error_to_reject(
&doc,
vti_common::error::AppError::Validation(
"this agent has no DID resolver configured, so it cannot find the host".into(),
),
);
};
let owner_did = req.owner_did.clone().unwrap_or_else(|| auth.did.clone());
let mut payload = serde_json::json!({
"roomId": req.room_id,
"visibility": req.visibility,
"ownerDid": owner_did,
});
if let Some(policy) = &req.retention_policy {
payload["retentionPolicy"] = serde_json::to_value(policy).unwrap_or(Value::Null);
}
if let Some(days) = req.retention_days {
payload["retentionDays"] = serde_json::json!(u64::from(days));
}
let key = format!("{vta_did}#key-0");
let reply = match crate::operations::room_host::send_room_task(
signing_context(state, auth),
&state.room_groups_ks,
&req.room_id,
&crate::operations::outbound::Outbound::from_app_state(state, &resolver),
&req.host,
&key,
&vta_did,
&key,
vti_rooms::wire::ROOMS_CREATE_TYPE,
&format!("{}#response", vti_rooms::wire::ROOMS_CREATE_TYPE),
payload,
)
.await
{
Ok(v) => v,
Err(e) => return app_error_to_reject(&doc, e),
};
let epoch = reply.get("epoch").and_then(Value::as_u64).unwrap_or(1);
success_response(
&doc,
serde_json::json!({
"roomId": req.room_id,
"host": req.host,
"epoch": epoch,
}),
)
}