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,
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_1::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
}
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),
&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,
}),
)
}