use chrono::{Duration, Utc};
use dtg_credentials::DTGCredential;
use serde_json::Value;
use vti_common::acl::ActScope;
use vti_common::error::AppError;
use vti_common::store::KeyspaceHandle;
use crate::auth::AuthClaims;
use crate::server::AppState;
pub const PRESENTATION_LIFETIME: Duration = Duration::hours(4);
const AUTHORITY_TYPE: &str = "AuthorityCredential";
const MEMBERSHIP_TYPE: &str = "MembershipCredential";
#[derive(Debug, Clone, serde::Serialize)]
#[serde(rename_all = "camelCase")]
pub struct MintedPresentation {
pub presentation: Value,
pub expires_at: String,
}
pub async fn present(
state: &AppState,
auth: &AuthClaims,
agent_did: &str,
room_id: &str,
action: &str,
audience: Option<&str>,
nonce: Option<&str>,
) -> Result<MintedPresentation, AppError> {
let scope = auth.act_scope();
let vac = find_room_credential(&state.vault_ks, room_id, AUTHORITY_TYPE, &scope).await?;
let vmc = find_room_credential(&state.vault_ks, room_id, MEMBERSHIP_TYPE, &scope).await?;
let root: DTGCredential = serde_json::from_value(vac.clone()).map_err(|e| {
AppError::Internal(format!("stored authority credential for `{room_id}`: {e}"))
})?;
let now = Utc::now();
let expires = now + PRESENTATION_LIFETIME;
let mut leaf = root
.attenuate(
agent_did.to_string(),
vec![action.to_string()],
now,
Some(expires),
audience.map(str::to_string),
)
.map_err(|e| {
AppError::Validation(format!(
"cannot attenuate the principal's authority for `{room_id}` to `{action}`: {e}"
))
})?;
let keys = crate::operations::holder_keys::resolve_holder_keys(
&state.keys_ks,
&state.seed_store,
auth,
root.subject(),
)
.await?;
leaf.sign(&keys.consent_secret, None)
.await
.map_err(|e| AppError::Internal(format!("sign the attenuated credential: {e}")))?;
let leaf_json = serde_json::to_value(leaf.credential())
.map_err(|e| AppError::Internal(format!("serialise the attenuated credential: {e}")))?;
let mut presentation = serde_json::json!({
"membership": vmc,
"authority": [leaf_json, vac],
});
if let Some(nonce) = nonce {
presentation["nonce"] = Value::String(nonce.to_string());
}
Ok(MintedPresentation {
presentation,
expires_at: expires.to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
})
}
async fn find_room_credential(
vault: &KeyspaceHandle,
room_id: &str,
type_tag: &str,
scope: &ActScope,
) -> Result<Value, AppError> {
let query = crate::vault::query::CredentialQuery {
r#type: Some(type_tag.to_string()),
issuer_did: Some(room_id.to_string()),
..Default::default()
};
let found = crate::vault::query::search(vault, &query, scope).await?;
match found.len() {
0 => Err(AppError::NotFound(format!(
"this VTA holds no {type_tag} issued by room `{room_id}`"
))),
1 => {
let stored = crate::vault::storage::get(vault, &found[0].id)
.await?
.ok_or_else(|| {
AppError::Internal(format!("credential `{}` vanished mid-read", found[0].id))
})?;
serde_json::to_value(&stored.body)
.map_err(|e| AppError::Internal(format!("stored {type_tag} for `{room_id}`: {e}")))
}
n => Err(AppError::Conflict(format!(
"this VTA holds {n} {type_tag}s issued by room `{room_id}`; which one to \
attenuate from is not a question this can answer safely"
))),
}
}