use axum::extract::State;
use axum::response::{IntoResponse, Response};
use serde_json::Value;
use trust_tasks_rs::TrustTask;
use crate::auth::AuthClaims;
use crate::error::AppError;
use crate::server::AppState;
mod acl;
mod app_state;
mod audit;
#[cfg(test)]
mod audit_coverage;
mod auth;
mod backup;
pub(crate) mod ceremony;
mod config;
#[cfg(test)]
mod conformance;
mod consent;
mod consent_request;
mod contexts;
mod cred_vault;
mod credential_exchange;
mod credentials;
mod device;
mod did_templates;
mod discovery;
mod helpers;
mod idempotency;
mod keys;
mod management;
mod memory;
mod messaging;
#[cfg(all(feature = "webvh", feature = "didcomm"))]
mod passkey_vms;
pub mod pending_replies;
mod persona;
pub(crate) mod planner;
mod policy;
mod policy_gate;
#[cfg(test)]
mod produced_census;
#[cfg(feature = "webvh")]
mod provision_integration;
mod room_group;
mod room_keys;
mod room_owner;
mod seeds;
#[cfg(feature = "webvh")]
mod services;
mod task_consent;
pub(crate) mod transport;
pub(crate) mod step_up;
pub(crate) use policy_gate::rest_gate;
mod vault;
#[cfg(feature = "webvh")]
pub(crate) mod webvh;
pub(crate) mod wire_v0_2;
pub(crate) use helpers::TrustTaskOutcome;
#[cfg(feature = "tsp")]
pub(crate) use helpers::malformed_request_response;
use helpers::{body_parse_error_response, method_not_found, reject_with};
use trust_tasks_rs::RejectReason;
#[allow(dead_code)] const REST_ROUTED: &[&str] = vta_sdk::trust_tasks::REST_ROUTED_URIS;
#[allow(dead_code)]
#[allow(deprecated)]
const KNOWN_FEATURE_GATED_URIS: &[&str] = &[
vta_sdk::trust_tasks::TASK_PASSKEY_VMS_ENROLL_CHALLENGE_0_1,
vta_sdk::trust_tasks::TASK_PASSKEY_VMS_ENROLL_SUBMIT_0_1,
vta_sdk::trust_tasks::TASK_PASSKEY_VMS_LIST_0_1,
vta_sdk::trust_tasks::TASK_PASSKEY_VMS_REVOKE_0_1,
vta_sdk::trust_tasks::TASK_PROVISION_INTEGRATION_0_3,
vta_sdk::trust_tasks::TASK_WEBVH_SERVERS_LIST_1_0,
vta_sdk::trust_tasks::TASK_WEBVH_SERVERS_REGISTER_1_0,
vta_sdk::trust_tasks::TASK_WEBVH_SERVERS_REMOVE_1_0,
vta_sdk::trust_tasks::TASK_WEBVH_DIDS_LIST_1_0,
vta_sdk::trust_tasks::TASK_WEBVH_DIDS_CREATE_1_0,
vta_sdk::trust_tasks::TASK_WEBVH_DIDS_GET_1_0,
vta_sdk::trust_tasks::TASK_WEBVH_DIDS_DELETE_1_0,
vta_sdk::trust_tasks::TASK_WEBVH_DIDS_UPDATE_1_0,
vta_sdk::trust_tasks::TASK_WEBVH_DIDS_ROTATE_KEYS_1_0,
vta_sdk::trust_tasks::TASK_WEBVH_DIDS_REGISTER_WITH_SERVER_1_0,
vta_sdk::trust_tasks::TASK_WEBVH_AGENT_NAME_LIST_1_0,
vta_sdk::trust_tasks::TASK_WEBVH_AGENT_NAME_CHECK_1_0,
vta_sdk::trust_tasks::TASK_WEBVH_AGENT_NAME_SET_1_0,
vta_sdk::trust_tasks::TASK_WEBVH_AGENT_NAME_REMOVE_1_0,
vta_sdk::trust_tasks::TASK_WEBVH_AGENT_NAME_DISABLE_1_0,
vta_sdk::trust_tasks::TASK_WEBVH_AGENT_NAME_ENABLE_1_0,
vta_sdk::trust_tasks::TASK_DID_MANAGEMENT_DID_REGISTER_0_1,
vta_sdk::trust_tasks::TASK_DID_MANAGEMENT_DID_PUBLISH_0_1,
vta_sdk::trust_tasks::TASK_DID_MANAGEMENT_DID_DELETE_0_1,
vta_sdk::trust_tasks::TASK_DID_MANAGEMENT_DID_ENABLE_0_1,
vta_sdk::trust_tasks::TASK_DID_MANAGEMENT_DID_DISABLE_0_1,
vta_sdk::trust_tasks::TASK_DID_MANAGEMENT_DID_LIST_0_1,
vta_sdk::trust_tasks::TASK_DID_MANAGEMENT_DID_INFO_0_1,
vta_sdk::trust_tasks::TASK_DID_MANAGEMENT_DID_CHECK_NAME_0_1,
vta_sdk::trust_tasks::TASK_DID_MANAGEMENT_DID_CHANGE_OWNER_0_1,
vta_sdk::trust_tasks::TASK_DID_MANAGEMENT_DID_ROLLBACK_0_1,
vta_sdk::trust_tasks::TASK_DID_MANAGEMENT_DID_PROBLEM_REPORT_0_1,
vta_sdk::trust_tasks::TASK_DID_MANAGEMENT_DOMAIN_CREATE_0_1,
vta_sdk::trust_tasks::TASK_DID_MANAGEMENT_DOMAIN_UPDATE_0_1,
vta_sdk::trust_tasks::TASK_DID_MANAGEMENT_DOMAIN_DISABLE_0_1,
vta_sdk::trust_tasks::TASK_DID_MANAGEMENT_DOMAIN_PURGE_0_1,
vta_sdk::trust_tasks::TASK_DID_MANAGEMENT_DOMAIN_SET_DEFAULT_0_1,
vta_sdk::trust_tasks::TASK_DID_MANAGEMENT_DOMAIN_ASSIGN_0_1,
vta_sdk::trust_tasks::TASK_DID_MANAGEMENT_DOMAIN_UNASSIGN_0_1,
vta_sdk::trust_tasks::TASK_DID_MANAGEMENT_SERVER_REGISTER_0_1,
vta_sdk::trust_tasks::TASK_DID_MANAGEMENT_SERVER_HEALTH_0_1,
vta_sdk::trust_tasks::TASK_DID_MANAGEMENT_SERVER_STATS_SYNC_0_1,
vta_sdk::trust_tasks::TASK_DID_MANAGEMENT_REGISTRY_ADMIN_REGISTER_0_1,
vta_sdk::trust_tasks::TASK_DID_MANAGEMENT_REGISTRY_DEREGISTER_0_1,
];
#[allow(dead_code)] const UNSPECCED_DISPATCHED_URIS: &[&str] = &[
"https://trusttasks.org/spec/vta/seeds/list/1.0",
"https://trusttasks.org/spec/vta/seeds/rotate/1.0",
"https://trusttasks.org/spec/vta/audit/get-retention/1.0",
"https://trusttasks.org/spec/vta/audit/update-retention/1.0",
"https://trusttasks.org/spec/vta/attestation/status/1.0",
"https://trusttasks.org/spec/vta/attestation/report/1.0",
"https://trusttasks.org/spec/vault/archive/0.1",
"https://trusttasks.org/spec/vault/unarchive/0.1",
"https://trusttasks.org/spec/vault/restore/0.1",
"https://trusttasks.org/spec/vault/purge/0.1",
];
macro_rules! dispatch_table {
(
$(
$(#[$meta:meta])*
$($uri:path)|+ => $handler:path
[ $se:ident $disc:ident $acts:literal ]
),+ $(,)?
) => {
#[allow(deprecated)]
async fn dispatch_typed(
state: &AppState,
auth: &AuthClaims,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
let type_uri = doc.type_uri.to_string();
match type_uri.as_str() {
$(
$(#[$meta])*
$($uri)|+ => Box::pin($handler(state, auth, doc)).await,
)+
_ => method_not_found(doc, &type_uri),
}
}
#[allow(deprecated, dead_code)]
pub(crate) fn class_for(type_uri: &str) -> Option<$crate::policy::TaskClass> {
match type_uri {
$(
$(#[$meta])*
$($uri)|+ => Some($crate::policy::TaskClass::new(
$crate::policy::SideEffectLevel::$se,
$crate::policy::Discloses::$disc,
$acts,
)),
)+
_ => None,
}
}
#[allow(deprecated)]
pub(crate) fn dispatched_uris() -> Vec<&'static str> {
let mut v: Vec<&'static str> = Vec::new();
$(
$(#[$meta])*
v.extend([$($uri),+]);
)+
v
}
};
}
pub async fn dispatch_trust_task(
auth: AuthClaims,
State(state): State<AppState>,
body: axum::body::Bytes,
) -> Result<Response, AppError> {
Ok(dispatch_trust_task_core(
&state,
&auth,
&body,
transport::TransportConfidentiality::HopByHop,
)
.await
.into_response())
}
async fn validate_payload(
state: &AppState,
type_uri: &str,
doc: &TrustTask<Value>,
) -> Option<TrustTaskOutcome> {
let Some(schema) = trust_tasks_rs::schema_index::schema_for(type_uri) else {
if state.config.read().await.policy.require_payload_schema {
return Some(helpers::reject_with(
doc,
RejectReason::MalformedRequest {
reason: format!(
"no payload schema is known for `{type_uri}`, and this VTA is configured \
to refuse tasks it cannot validate"
),
},
));
}
tracing::debug!(
type_uri,
"no payload schema known — dispatching unvalidated (set \
policy.require_payload_schema to refuse instead)"
);
return None;
};
match trust_tasks_rs::validate::against_schema(schema, &doc.payload) {
Ok(()) => None,
Err(e) => {
tracing::info!(type_uri, error = %e, "payload failed schema validation");
Some(helpers::reject_with(
doc,
RejectReason::MalformedRequest {
reason: format!("payload does not conform to {type_uri}: {e}"),
},
))
}
}
}
async fn sign_success_response(state: &AppState, outcome: TrustTaskOutcome) -> TrustTaskOutcome {
if !outcome.status.is_success() {
return outcome;
}
let (Some(resolver), Some(vm_id)) = (
state.secrets_resolver.as_ref(),
state.signing_vm_id.as_ref(),
) else {
return outcome;
};
use affinidi_tdk::secrets_resolver::SecretsResolver as _;
let Some(secret) = resolver.get_secret(vm_id).await else {
tracing::error!(%vm_id, "no resident secret for the signing key");
return cannot_sign(vm_id, "its signing key is not resident");
};
match attach_proof(&secret, &outcome.body).await {
Some(body) => TrustTaskOutcome {
status: outcome.status,
body,
},
None => {
tracing::error!(%vm_id, "the signature would not attach");
cannot_sign(vm_id, "its signature would not attach")
}
}
}
fn cannot_sign(vm_id: &str, why: &str) -> TrustTaskOutcome {
let body = serde_json::json!({
"type": "https://trusttasks.org/spec/trust-task-error/0.5",
"payload": {
"code": "internalError",
"message": format!(
"this agent completed the request and could not sign its answer, because {why} ({vm_id}). The work is done — do not retry a change on this error. An unsigned answer is bytes rather than evidence, so it is withheld rather than sent."
),
"retryable": false,
},
});
TrustTaskOutcome {
status: axum::http::StatusCode::INTERNAL_SERVER_ERROR,
body: serde_json::to_vec(&body).unwrap_or_else(|_| b"{}".to_vec()),
}
}
async fn attach_proof(
secret: &affinidi_secrets_resolver::secrets::Secret,
body: &[u8],
) -> Option<Vec<u8>> {
let mut doc: serde_json::Value = match serde_json::from_slice(body) {
Ok(d) => d,
Err(e) => {
tracing::error!(error = %e, "success response is not JSON; returning it unsigned");
return None;
}
};
if !attach_proof_in_place(secret, &mut doc).await {
return None;
}
serde_json::to_vec(&doc)
.inspect_err(|e| tracing::error!(error = %e, "could not serialise the signed response"))
.ok()
}
pub(crate) async fn attach_proof_in_place(
secret: &affinidi_secrets_resolver::secrets::Secret,
doc: &mut serde_json::Value,
) -> bool {
let Some(obj) = doc.as_object_mut() else {
return false;
};
obj.remove("proof");
let proof = match affinidi_data_integrity::DataIntegrityProof::sign(
&*doc,
secret,
affinidi_data_integrity::SignOptions::new(),
)
.await
{
Ok(p) => p,
Err(e) => {
tracing::error!(error = %e, "could not sign the success response; answering unsigned");
return false;
}
};
let Ok(proof_value) = serde_json::to_value(&proof)
.inspect_err(|e| tracing::error!(error = %e, "could not serialise the response proof"))
else {
return false;
};
match doc.as_object_mut() {
Some(obj) => {
obj.insert("proof".into(), proof_value);
true
}
None => false,
}
}
#[cfg(any(feature = "didcomm", feature = "tsp"))]
pub(crate) async fn accept_from_proven_sender(
state: &AppState,
sender_vid: &str,
body: &[u8],
confidentiality: transport::TransportConfidentiality,
) -> TrustTaskOutcome {
use vta_sdk::inbound::{Inbound, classify};
let silent = || TrustTaskOutcome {
status: axum::http::StatusCode::NO_CONTENT,
body: Vec::new(),
};
let kind = match serde_json::from_slice::<Value>(body) {
Ok(v) => classify(&v),
Err(_) => Inbound::Request,
};
let deliver = |state: &AppState, body: &[u8]| -> bool {
serde_json::from_slice::<TrustTask<Value>>(body)
.map(|d| state.pending_replies.complete(&d))
.unwrap_or(false)
};
match kind {
Inbound::Error => {
let reason = serde_json::from_slice::<Value>(body)
.ok()
.and_then(|v| {
let p = v.get("payload")?;
let code = p.get("code").and_then(Value::as_str).unwrap_or("?");
let message = p.get("message").and_then(Value::as_str).unwrap_or("");
Some(format!("{code}: {message}"))
})
.unwrap_or_else(|| "<unreadable payload>".to_string());
tracing::warn!(
sender = %sender_vid,
%reason,
"inbound trust-task error from a peer — terminal, not answered"
);
deliver(state, body);
silent()
}
Inbound::Response if deliver(state, body) => {
tracing::debug!(sender = %sender_vid, "inbound response delivered to a waiting request");
silent()
}
Inbound::Response | Inbound::Request => {
match crate::messaging::auth::auth_for_trust_task_envelope(state, sender_vid, body)
.await
{
Ok(auth) => dispatch_trust_task_core(state, &auth, body, confidentiality).await,
Err(e) => reject_trust_task(
body,
trust_tasks_rs::RejectReason::PermissionDenied {
reason: e.to_string(),
},
),
}
}
}
}
pub(crate) async fn dispatch_trust_task_core(
state: &AppState,
auth: &AuthClaims,
body: &[u8],
confidentiality: transport::TransportConfidentiality,
) -> TrustTaskOutcome {
let outcome = transport::with_confidentiality(confidentiality, async move {
dispatch_trust_task_inner(state, auth, body).await
})
.await;
let outcome = sign_success_response(state, outcome).await;
#[cfg(any(test, feature = "test-support"))]
let outcome =
match crate::test_support::response_conformance::observe(outcome.status, &outcome.body) {
Some(body) => TrustTaskOutcome {
status: axum::http::StatusCode::INTERNAL_SERVER_ERROR,
body,
},
None => outcome,
};
outcome
}
async fn dispatch_trust_task_inner(
state: &AppState,
auth: &AuthClaims,
body: &[u8],
) -> TrustTaskOutcome {
let doc: TrustTask<Value> = match serde_json::from_slice(body) {
Ok(d) => d,
Err(e) => return body_parse_error_response(&e.to_string()),
};
if state.pending_replies.complete(&doc) {
tracing::debug!(
thread_id = ?doc.thread_id,
"inbound document delivered to a waiting request"
);
return TrustTaskOutcome {
status: axum::http::StatusCode::NO_CONTENT,
body: Vec::new(),
};
}
let superseded = crate::deprecation::superseded_task(&doc.type_uri.to_string());
let dispatch_audit = DispatchAudit::capture(&doc);
let mut outcome = Box::pin(dispatch_trust_task_validated(state, auth, doc)).await;
dispatch_audit.record(state, &auth.did, &outcome).await;
if let Some(task) = superseded {
crate::deprecation::note_superseded_task(task);
crate::deprecation::annotate_superseded(&mut outcome.body, task);
}
outcome
}
mod lifecycle_mapping {}
fn freshness_policy() -> trust_tasks_rs::FreshnessPolicy {
trust_tasks_rs::FreshnessPolicy::default()
.with_max_age(chrono::TimeDelta::minutes(10))
.requiring_issued_at()
}
fn retain_until(
doc: &TrustTask<Value>,
now: chrono::DateTime<chrono::Utc>,
) -> Option<chrono::DateTime<chrono::Utc>> {
let policy = freshness_policy();
let expiry = policy.record_expiry(doc, now)?;
match (doc.issued_at, policy.max_age) {
(Some(issued_at), Some(max_age)) => Some(expiry.min(issued_at + max_age + policy.skew)),
_ => Some(expiry),
}
}
static REPLAY_GUARD: std::sync::LazyLock<trust_tasks_rs::InMemoryReplayGuard> =
std::sync::LazyLock::new(trust_tasks_rs::InMemoryReplayGuard::default);
async fn dispatch_trust_task_validated(
state: &AppState,
auth: &AuthClaims,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
let now = chrono::Utc::now();
{
if let Err(reason) = doc.validate_freshness(now, &freshness_policy()) {
return reject_with(&doc, reason);
}
let vta_did = state.config.read().await.vta_did.clone();
if let Some(my_vid) = vta_did.as_deref()
&& let Err(reason) = doc.validate_basic(now, my_vid)
{
return reject_with(&doc, reason);
}
}
let doc_id = doc.id.clone();
let digest = match trust_tasks_rs::document_digest(&doc) {
Ok(d) => d,
Err(e) => {
return reject_with(
&doc,
RejectReason::InternalError {
reason: format!(
"cannot canonicalise the document to key its replay record: {e}"
),
},
);
}
};
let retain_until = retain_until(&doc, now);
match trust_tasks_rs::ReplayGuard::claim(&*REPLAY_GUARD, &doc.id, &digest, retain_until, now)
.await
{
Ok(trust_tasks_rs::ReplayVerdict::Fresh) => {}
Ok(trust_tasks_rs::ReplayVerdict::Duplicate {
prior_response,
in_flight,
}) => {
return match prior_response {
Some(v) => match serde_json::to_vec(&v) {
Ok(body) => TrustTaskOutcome {
status: axum::http::StatusCode::OK,
body,
},
Err(e) => reject_with(
&doc,
RejectReason::InternalError {
reason: format!("prior response is unserialisable: {e}"),
},
),
},
None if in_flight => TrustTaskOutcome {
status: axum::http::StatusCode::ACCEPTED,
body: Vec::new(),
},
None => TrustTaskOutcome {
status: axum::http::StatusCode::NO_CONTENT,
body: Vec::new(),
},
};
}
Ok(trust_tasks_rs::ReplayVerdict::Conflict) => {
return reject_with(&doc, RejectReason::IdConflict);
}
Err(e) => {
return reject_with(
&doc,
RejectReason::Unavailable {
retry_after: {
tracing::error!(error = %e, id = %doc.id, "replay guard unavailable");
None
},
},
);
}
Ok(other) => {
return reject_with(
&doc,
RejectReason::Unavailable {
retry_after: {
tracing::error!(
verdict = ?other,
id = %doc.id,
"replay guard returned a verdict this build does not know",
);
None
},
},
);
}
}
let _ = auth;
use wire_v0_2::{WIRE_VERSION, WireVersion};
let type_uri = doc.type_uri.to_string();
tracing::info!(
type_uri = %type_uri,
actor = %auth.did,
id = %doc.id,
"trust-task received"
);
if let Some(reject) = validate_payload(state, &type_uri, &doc).await {
return reject;
}
if let Some(policy) = trust_tasks_rs::schema_index::spec_policy_for(&type_uri)
&& let Err(reason) = policy.enforce(&doc)
{
tracing::info!(
type_uri,
?reason,
"document refused by its specification's policy"
);
return reject_with(&doc, reason);
}
if doc.proof.is_some() {
match vti_common::auth::verify_trust_task_proof_with(&doc, &state.trust_task_vm_resolver())
.await
{
Ok(signer) => {
if doc.issuer.as_deref() != Some(signer.as_str()) {
tracing::warn!(
type_uri,
issuer = ?doc.issuer,
signer = %signer,
"document proof verifies, but not as its issuer"
);
return reject_with(
&doc,
RejectReason::ProofInvalid {
reason: "the proof verifies as a DID other than the document's issuer"
.to_string(),
},
);
}
}
Err(e) => {
tracing::info!(type_uri, error = %e, "document proof failed verification");
return reject_with(
&doc,
RejectReason::ProofInvalid {
reason: e.to_string(),
},
);
}
}
}
let idem_claim = match idempotency::claim(&state.idempotency_ks, &auth.did, &doc).await {
idempotency::Claim::Answer(outcome) => return *outcome,
idempotency::Claim::Proceed { key, safety } => Some((key, safety)),
idempotency::Claim::Skip => None,
};
let mut delegated_contexts: Vec<String> = Vec::new();
let outcome =
match policy_gate::policy_gate(state, auth, &type_uri, &doc, &mut delegated_contexts).await
{
Some(reject_outcome) => reject_outcome,
None => {
let delegated_auth = (!delegated_contexts.is_empty())
.then(|| auth.with_delegated_authority(&delegated_contexts));
let auth = delegated_auth.as_ref().unwrap_or(auth);
if let Some(spec) = wire_v0_2::lookup_0_2(&type_uri) {
let mut doc = doc;
wire_v0_2::downconvert_request(&mut doc.payload, spec);
if let Ok(uri_0_1) = spec.uri_0_1.parse() {
doc.type_uri = uri_0_1;
}
let outcome = WIRE_VERSION
.scope(WireVersion::V0_2, dispatch_typed(state, auth, doc))
.await;
wire_v0_2::upconvert_response(outcome, spec, &type_uri)
} else {
WIRE_VERSION
.scope(WireVersion::V0_1, dispatch_typed(state, auth, doc))
.await
}
}
};
if let Some((key, safety)) = idem_claim {
idempotency::record_outcome(&state.idempotency_ks, &auth.did, &key, safety, &outcome).await;
}
{
let guard: &dyn trust_tasks_rs::ReplayGuard = &*REPLAY_GUARD;
if outcome.status.is_success() {
let recorded = serde_json::from_slice::<serde_json::Value>(&outcome.body).ok();
if let Err(e) = guard.record_response(&doc_id, recorded.as_ref()).await {
tracing::warn!(error = %e, id = %doc_id, "replay guard: response not recorded");
}
} else if let Err(e) = guard.release(&doc_id, &digest).await {
tracing::warn!(error = %e, id = %doc_id, "replay guard: claim not released");
}
}
outcome
}
struct DispatchAudit {
vault_action: Option<String>,
type_uri: String,
consequential: bool,
resource: Option<String>,
context_id: Option<String>,
detail: Option<String>,
}
impl DispatchAudit {
fn capture(doc: &TrustTask<Value>) -> Self {
let type_uri = doc.type_uri.to_string();
let consequential = class_for(&type_uri)
.map(|class| class.side_effects != crate::policy::SideEffectLevel::None)
.unwrap_or(true);
Self {
vault_action: vault_audit_action(&type_uri),
resource: vault_audit_resource(&doc.payload),
context_id: doc
.payload
.get("contextId")
.and_then(Value::as_str)
.map(str::to_string),
detail: doc
.payload
.get("reason")
.and_then(Value::as_str)
.map(str::to_string),
type_uri,
consequential,
}
}
async fn record(&self, state: &AppState, actor: &str, outcome: &TrustTaskOutcome) {
let label = vault_audit_outcome_label(outcome);
let (action, resource) = match (&self.vault_action, outcome.status.is_success()) {
(Some(action), _) => (action.as_str(), self.resource.as_deref()),
(None, false) if self.consequential => ("task.refused", Some(self.type_uri.as_str())),
(None, _) => return,
};
if let Err(e) = crate::audit::record_with_detail(
&state.audit_sink,
action,
actor,
resource,
&label,
Some(helpers::TRANSPORT_TRUST_TASK),
self.context_id.as_deref(),
self.detail.as_deref(),
)
.await
{
tracing::warn!(error = %e, action, "dispatch audit record failed");
}
}
}
fn vault_audit_action(type_uri: &str) -> Option<String> {
let rest = type_uri.split("/spec/vault/").nth(1)?;
let segs: Vec<&str> = rest.split('/').filter(|s| !s.is_empty()).collect();
match segs.as_slice() {
["credentials", verb, ..] => Some(format!("vault.cred.{verb}")),
[verb, ..] => Some(format!("vault.{verb}")),
_ => None,
}
}
fn vault_audit_resource(payload: &Value) -> Option<String> {
for key in ["id", "entryId", "credentialId"] {
if let Some(v) = payload.get(key).and_then(Value::as_str) {
return Some(v.to_string());
}
}
None
}
fn vault_audit_outcome_label(outcome: &TrustTaskOutcome) -> String {
if outcome.status.is_success() {
return "success".to_string();
}
if let Ok(v) = serde_json::from_slice::<Value>(&outcome.body)
&& let Some(code) = v
.get("payload")
.and_then(|p| p.get("code"))
.and_then(Value::as_str)
{
return format!("denied:{code}");
}
"denied".to_string()
}
#[cfg(any(feature = "didcomm", feature = "tsp"))]
pub(crate) fn reject_trust_task(body: &[u8], reason: RejectReason) -> TrustTaskOutcome {
match serde_json::from_slice::<TrustTask<Value>>(body) {
Ok(doc) => reject_with(&doc, reason),
Err(e) => body_parse_error_response(&e.to_string()),
}
}
dispatch_table! {
vta_sdk::trust_tasks::TASK_AUTH_REVOKE_SESSION_0_1 => auth::handle_revoke_session
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_AUTH_WHOAMI_0_1 => auth::handle_whoami
[ None Metadata false ],
vta_sdk::trust_tasks::TASK_AUTH_SESSIONS_LIST_0_1 => auth::handle_sessions_list
[ None Metadata false ],
vta_sdk::trust_tasks::TASK_AUTH_STEP_UP_APPROVE_RESPONSE_0_1
| vta_sdk::trust_tasks::TASK_AUTH_STEP_UP_APPROVE_RESPONSE_0_2
| vta_sdk::trust_tasks::TASK_AUTH_STEP_UP_APPROVE_RESPONSE_0_3
=> step_up::handle_approve_response
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_POLICY_LIST_0_2 => policy::handle_list
[ None Metadata false ],
vta_sdk::trust_tasks::TASK_POLICY_GET_0_1 => policy::handle_get
[ None Metadata false ],
vta_sdk::trust_tasks::TASK_POLICY_UPSERT_0_2 => policy::handle_upsert
[ Destructive None false ],
vta_sdk::trust_tasks::TASK_POLICY_DELETE_0_1 => policy::handle_delete
[ Destructive None false ],
vta_sdk::trust_tasks::TASK_CONSENT_REQUEST_1_0 => consent::handle_request
[ None None false ],
vta_sdk::trust_tasks::TASK_CONSENT_DECISION_1_0 => consent::handle_decision
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_CONSENT_REVOKE_1_0 => consent::handle_revoke
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_CONSENT_LIST_1_0 => consent::handle_list
[ None Metadata false ],
vta_sdk::trust_tasks::TASK_CONSENT_APPROVER_SET_1_0 => consent::handle_approver_set
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_CONSENT_APPROVER_LIST_1_0 => consent::handle_approver_list
[ None Metadata false ],
vta_sdk::trust_tasks::TASK_TASK_CONSENT_DECISION_0_1 => task_consent::handle_decision
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_ACL_LIST_0_1 => acl::handle_list
[ None Metadata false ],
vta_sdk::trust_tasks::TASK_ACL_GRANT_0_1 => acl::handle_create
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_ACL_SHOW_0_1 => acl::handle_get
[ None Metadata false ],
vta_sdk::trust_tasks::TASK_ACL_UPDATE_0_1 => acl::handle_update
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_ACL_CHANGE_ROLE_0_1 => acl::handle_change_role
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_ACL_REVOKE_0_1 => acl::handle_delete
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_ACL_SWAP_KEY_0_1 => acl::handle_swap_key
[ Destructive None false ],
vta_sdk::trust_tasks::TASK_DEVICE_REGISTER_0_1 => device::handle_register
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_DEVICE_HEARTBEAT_0_1 => device::handle_heartbeat
[ None None false ],
vta_sdk::trust_tasks::TASK_DEVICE_LIST_0_1 => device::handle_list
[ None Metadata false ],
vta_sdk::trust_tasks::TASK_DEVICE_DISABLE_0_1 => device::handle_disable
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_DEVICE_WIPE_0_1 => device::handle_wipe
[ Destructive None false ],
vta_sdk::trust_tasks::TASK_DEVICE_SET_WAKE_0_1 => device::handle_set_wake
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_MESSAGING_PING_0_1 => messaging::handle_ping
[ None None false ],
#[cfg(feature = "webvh")]
vta_sdk::trust_tasks::TASK_SERVICES_LIST_1_0 => services::handle_list
[ None Metadata false ],
#[cfg(feature = "webvh")]
vta_sdk::trust_tasks::TASK_SERVICES_GET_1_0 => services::handle_get
[ None Metadata false ],
#[cfg(feature = "webvh")]
vta_sdk::trust_tasks::TASK_SERVICES_ENABLE_1_0 => services::handle_enable
[ Mutating None false ],
#[cfg(feature = "webvh")]
vta_sdk::trust_tasks::TASK_SERVICES_UPDATE_1_0 => services::handle_update
[ Mutating None false ],
#[cfg(feature = "webvh")]
vta_sdk::trust_tasks::TASK_SERVICES_DISABLE_1_0 => services::handle_disable
[ Mutating None false ],
#[cfg(feature = "webvh")]
vta_sdk::trust_tasks::TASK_SERVICES_ROLLBACK_1_0 => services::handle_rollback
[ Mutating None false ],
#[cfg(feature = "webvh")]
vta_sdk::trust_tasks::TASK_SERVICES_DRAIN_LIST_1_0 => services::handle_drain_list
[ None Metadata false ],
#[cfg(feature = "webvh")]
vta_sdk::trust_tasks::TASK_SERVICES_DRAIN_CANCEL_1_0 => services::handle_drain_cancel
[ Destructive None false ],
vta_sdk::trust_tasks::TASK_CONTEXTS_LIST_1_0 => contexts::handle_list
[ None Metadata false ],
vta_sdk::trust_tasks::TASK_CONTEXTS_CREATE_1_0 => contexts::handle_create
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_CONTEXTS_GET_1_0 => contexts::handle_get
[ None Metadata false ],
vta_sdk::trust_tasks::TASK_CONTEXTS_UPDATE_1_0 => contexts::handle_update
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_CONTEXTS_SECRETS_1_0 => contexts::handle_secrets
[ None Secret false ],
vta_sdk::trust_tasks::TASK_CONTEXTS_UPDATE_DID_1_0 => contexts::handle_update_did
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_CONTEXTS_PREVIEW_DELETE_1_0 => contexts::handle_preview_delete
[ None Metadata false ],
vta_sdk::trust_tasks::TASK_CONTEXTS_DELETE_1_0 => contexts::handle_delete
[ Destructive None false ],
vta_sdk::trust_tasks::TASK_KEYS_LIST_0_1 => keys::handle_list
[ None Metadata false ],
vta_sdk::trust_tasks::TASK_KEYS_CREATE_0_1 => keys::handle_create
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_KEYS_IMPORT_0_1 => keys::handle_import
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_KEYS_SHOW_0_1 => keys::handle_get
[ None Metadata false ],
vta_sdk::trust_tasks::TASK_KEYS_RENAME_0_1 => keys::handle_rename
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_KEYS_REVOKE_0_1 => keys::handle_revoke
[ Destructive None false ],
vta_sdk::trust_tasks::TASK_KEYS_SET_EXPORTABILITY_0_1 => keys::handle_set_exportability
[ Mutating Metadata false ],
vta_sdk::trust_tasks::TASK_KEYS_EXPORT_SECRET_0_1 => keys::handle_export_secret
[ None Secret false ],
vta_sdk::trust_tasks::TASK_KEYS_SIGN_0_1 => keys::handle_sign
[ None None true ],
vta_sdk::trust_tasks::TASK_KEYS_DERIVE_AND_SIGN_0_1 => keys::handle_derive_and_sign
[ Mutating None true ],
vta_sdk::trust_tasks::TASK_KEYS_DERIVE_AND_SIGN_DOCUMENT_0_1 => keys::handle_derive_and_sign_document
[ Mutating None true ],
vta_sdk::trust_tasks::TASK_SEEDS_LIST_1_0 => seeds::handle_list
[ None Metadata false ],
vta_sdk::trust_tasks::TASK_SEEDS_ROTATE_1_0 => seeds::handle_rotate
[ Destructive None false ],
vta_sdk::trust_tasks::TASK_AUDIT_LIST_0_1 => audit::handle_list_logs
[ None Metadata false ],
vta_sdk::trust_tasks::TASK_AUDIT_VERIFY_0_1 => audit::handle_verify_chain
[ None Metadata false ],
vta_sdk::trust_tasks::TASK_AUDIT_GET_RETENTION_1_0 => audit::handle_get_retention
[ None Metadata false ],
vta_sdk::trust_tasks::TASK_AUDIT_UPDATE_RETENTION_1_0 => audit::handle_update_retention
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_TRUST_TASK_DISCOVERY_0_1 => discovery::handle_trust_task_discovery
[ None None false ],
vta_sdk::protocols::credential_exchange::PENDING_LIST
=> credential_exchange::handle_pending_list
[ None Metadata false ],
vta_sdk::protocols::credential_exchange::PENDING_APPROVE
=> credential_exchange::handle_pending_approve
[ Mutating None true ],
vta_sdk::protocols::credential_exchange::PENDING_DENY
=> credential_exchange::handle_pending_deny
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_VAULT_LIST_0_1 => vault::handle_list
[ None Metadata false ],
vta_sdk::trust_tasks::TASK_VAULT_GET_0_1 => vault::handle_get
[ None Metadata false ],
vta_sdk::trust_tasks::TASK_VAULT_UPSERT_0_1 => vault::handle_upsert
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_VAULT_DELETE_0_1 => vault::handle_delete
[ Destructive None false ],
vta_sdk::trust_tasks::TASK_VAULT_RELEASE_0_1 => vault::handle_release
[ Mutating Secret false ],
vta_sdk::trust_tasks::TASK_VAULT_PROXY_LOGIN_0_1 => vault::handle_proxy_login
[ Mutating Secret true ],
vta_sdk::trust_tasks::TASK_VAULT_SIGN_TRUST_TASK_0_1 => vault::handle_sign_trust_task
[ Mutating None true ],
vta_sdk::trust_tasks::TASK_VAULT_ARCHIVE_0_1 => vault::handle_archive
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_VAULT_UNARCHIVE_0_1 => vault::handle_unarchive
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_VAULT_RESTORE_0_1 => vault::handle_restore
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_VAULT_PURGE_0_1 => vault::handle_purge
[ Destructive None false ],
vta_sdk::trust_tasks::TASK_VAULT_CREDENTIALS_RECEIVE_0_1 => cred_vault::handle_receive
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_VAULT_CREDENTIALS_QUERY_0_1 => cred_vault::handle_query
[ None Metadata false ],
vta_sdk::trust_tasks::TASK_VAULT_CREDENTIALS_GET_0_1 => cred_vault::handle_get
[ None Metadata false ],
vta_sdk::trust_tasks::TASK_VAULT_CREDENTIALS_ARCHIVE_0_1 => cred_vault::handle_archive
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_VAULT_CREDENTIALS_UNARCHIVE_0_1 => cred_vault::handle_unarchive
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_VAULT_CREDENTIALS_DELETE_0_1 => cred_vault::handle_delete
[ Destructive None false ],
vta_sdk::trust_tasks::TASK_VAULT_CREDENTIALS_RESTORE_0_1 => cred_vault::handle_restore
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_VAULT_CREDENTIALS_PURGE_0_1 => cred_vault::handle_purge
[ Destructive None false ],
vta_sdk::trust_tasks::TASK_VTA_CREDENTIALS_ISSUE_0_2 => credentials::handle_issue
[ Mutating None true ],
vta_sdk::trust_tasks::TASK_VTA_CREDENTIALS_REVOKE_0_1 => credentials::handle_revoke
[ Destructive None false ],
vta_sdk::trust_tasks::TASK_VTA_CREDENTIALS_LIST_0_1 => credentials::handle_list
[ None None false ],
vta_sdk::trust_tasks::TASK_VTA_MEMORY_PUT_0_1 => memory::handle_put
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_VTA_MEMORY_LIST_0_1 => memory::handle_list
[ None Metadata false ],
vta_sdk::trust_tasks::TASK_VTA_MEMORY_DELETE_0_1 => memory::handle_delete
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_ROOMS_KEYS_PRESENT_0_2 => room_keys::handle_present
[ None Metadata false ],
vta_sdk::trust_tasks::TASK_ROOMS_KEYS_KEY_PACKAGE_0_1 => room_group::handle_key_package
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_ROOMS_KEYS_WELCOME_0_1 => room_group::handle_welcome
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_ROOMS_KEYS_COMMIT_0_1 => room_group::handle_commit
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_ROOMS_KEYS_OPEN_0_1 => room_group::handle_open
[ None Metadata false ],
vta_sdk::trust_tasks::TASK_ROOMS_KEYS_CHAIN_0_1 => room_group::handle_chain
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_ROOMS_KEYS_BACKFILL_0_1 => room_group::handle_backfill
[ Mutating Metadata true ],
#[cfg(feature = "webvh")]
vta_sdk::trust_tasks::TASK_ROOMS_OWNER_ANCHOR_0_1 => room_owner::handle_anchor
[ Mutating Metadata true ],
vta_sdk::trust_tasks::TASK_ROOMS_KEYS_READ_0_1 => room_group::handle_read
[ None Secret true ],
vta_sdk::trust_tasks::TASK_ROOMS_KEYS_BROWSE_0_1 => room_group::handle_browse
[ None Metadata true ],
vta_sdk::trust_tasks::TASK_ROOMS_KEYS_SEAL_0_1 => room_group::handle_seal
[ None Metadata false ],
vta_sdk::trust_tasks::TASK_ROOMS_KEYS_LIST_0_1 => room_group::handle_list
[ None Metadata false ],
vta_sdk::trust_tasks::TASK_ROOMS_OWNER_INVITE_0_1 => room_owner::handle_invite
[ Mutating Metadata false ],
vta_sdk::trust_tasks::TASK_ROOMS_OWNER_REGISTER_0_1 => room_owner::handle_register
[ Mutating Metadata true ],
vta_sdk::trust_tasks::TASK_ROOMS_OWNER_ISSUE_MEMBERSHIP_0_1 => room_owner::handle_issue_membership
[ Mutating Metadata false ],
vta_sdk::trust_tasks::TASK_ROOMS_OWNER_ISSUE_AUTHORITY_0_2 => room_owner::handle_issue_authority
[ Mutating Metadata false ],
vta_sdk::trust_tasks::TASK_VTA_APP_STATE_GET_1_0 => app_state::handle_get
[ None Metadata false ],
vta_sdk::trust_tasks::TASK_VTA_APP_STATE_PUT_1_0 => app_state::handle_put
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_VTA_APP_STATE_LIST_1_0 => app_state::handle_list
[ None Metadata false ],
vta_sdk::trust_tasks::TASK_VTA_APP_STATE_DELETE_1_0 => app_state::handle_delete
[ Destructive None false ],
vta_sdk::trust_tasks::TASK_VTA_APP_STATE_GET_MANY_1_0 => app_state::handle_get_many
[ None Metadata false ],
vta_sdk::trust_tasks::TASK_VTA_APP_STATE_PUT_MANY_1_0 => app_state::handle_put_many
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_PERSONA_ATTRIBUTE_PUT_1_0 => persona::handle_attribute_put
[ Mutating Metadata false ],
vta_sdk::trust_tasks::TASK_PERSONA_ATTRIBUTE_LIST_1_0 => persona::handle_attribute_list
[ None Metadata false ],
vta_sdk::trust_tasks::TASK_PERSONA_ATTRIBUTE_DELETE_1_0 => persona::handle_attribute_delete
[ Destructive None false ],
vta_sdk::trust_tasks::TASK_PERSONA_ATTRIBUTE_PURGE_VERSION_1_0 => persona::handle_attribute_purge_version
[ Destructive None false ],
vta_sdk::trust_tasks::TASK_PERSONA_ATTRIBUTE_PROMOTE_1_0 => persona::handle_attribute_promote
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_PERSONA_PROFILE_COMPOSE_1_0 => persona::handle_profile_compose
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_PERSONA_PROFILE_RETIRE_1_0 => persona::handle_profile_retire
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_PERSONA_PROFILE_REINSTATE_1_0 => persona::handle_profile_reinstate
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_PERSONA_PROFILE_USAGE_1_0 => persona::handle_profile_usage
[ None Metadata false ],
vta_sdk::trust_tasks::TASK_PERSONA_PROFILE_TIMELINE_1_0 => persona::handle_profile_timeline
[ None Metadata false ],
vta_sdk::trust_tasks::TASK_PERSONA_PROFILE_PUT_1_0 => persona::handle_profile_put
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_PERSONA_PROFILE_GET_1_0 => persona::handle_profile_get
[ None Metadata false ],
vta_sdk::trust_tasks::TASK_PERSONA_PROFILE_LIST_1_0 => persona::handle_profile_list
[ None Metadata false ],
vta_sdk::trust_tasks::TASK_PERSONA_FACET_PUT_1_0 => persona::handle_facet_put
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_PERSONA_FACET_LIST_1_0 => persona::handle_facet_list
[ None Metadata false ],
vta_sdk::trust_tasks::TASK_PERSONA_FACET_DELETE_1_0 => persona::handle_facet_delete
[ Destructive None false ],
vta_sdk::trust_tasks::TASK_PERSONA_PROFILE_DELETE_1_0 => persona::handle_profile_delete
[ Destructive None false ],
vta_sdk::trust_tasks::TASK_PERSONA_BINDING_SET_1_0 => persona::handle_binding_set
[ Mutating Metadata false ],
vta_sdk::trust_tasks::TASK_PERSONA_BINDING_GET_1_0 => persona::handle_binding_get
[ None Metadata false ],
vta_sdk::trust_tasks::TASK_PERSONA_BINDING_LIST_1_0 => persona::handle_binding_list
[ None Metadata false ],
vta_sdk::trust_tasks::TASK_PERSONA_CONTACT_PUT_1_0 => persona::handle_contact_put
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_PERSONA_CONTACT_GET_1_0 => persona::handle_contact_get
[ None Metadata false ],
vta_sdk::trust_tasks::TASK_PERSONA_CONTACT_LIST_1_0 => persona::handle_contact_list
[ None Metadata false ],
vta_sdk::trust_tasks::TASK_PERSONA_CONTACT_DELETE_1_0 => persona::handle_contact_delete
[ Destructive None false ],
vta_sdk::trust_tasks::TASK_PERSONA_DISCLOSURE_HISTORY_1_0 => persona::handle_disclosure_history
[ None Metadata false ],
vta_sdk::trust_tasks::TASK_PERSONA_DISCLOSURE_PREVIEW_1_0 => persona::handle_disclosure_preview
[ Mutating Metadata false ],
vta_sdk::trust_tasks::TASK_PERSONA_DISCLOSURE_PRESENT_1_0 => persona::handle_disclosure_present
[ Mutating Secret true ],
vta_sdk::trust_tasks::TASK_PERSONA_CORRELATION_ANALYZE_1_0 => persona::handle_correlation_analyze
[ None Metadata false ],
vta_sdk::trust_tasks::TASK_PERSONA_RENDERERS_LIST_1_0 => persona::handle_renderers_list
[ None None false ],
vta_sdk::trust_tasks::TASK_PERSONA_CLAIM_TYPES_LIST_1_0 => persona::handle_claim_types_list
[ None None false ],
vta_sdk::trust_tasks::TASK_PERSONA_LOCAL_PROFILE_PUT_1_0 => persona::handle_local_profile_put
[ Mutating Metadata false ],
vta_sdk::trust_tasks::TASK_PERSONA_LOCAL_PROFILE_GET_1_0 => persona::handle_local_profile_get
[ None Metadata false ],
vta_sdk::trust_tasks::TASK_PERSONA_LOCAL_PROFILE_LIST_1_0 => persona::handle_local_profile_list
[ None Metadata false ],
vta_sdk::trust_tasks::TASK_PERSONA_LOCAL_PROFILE_DELETE_1_0 => persona::handle_local_profile_delete
[ Destructive None false ],
vta_sdk::trust_tasks::TASK_PERSONA_LOCAL_BINDING_SET_1_0 => persona::handle_local_binding_set
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_CONFIG_SHOW_0_1 => config::handle_get
[ None Metadata false ],
vta_sdk::trust_tasks::TASK_CONFIG_PATCH_0_1 => config::handle_update
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_MANAGEMENT_RELOAD_SERVICES_1_0 => management::handle_reload_services
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_BACKUP_INITIATE_EXPORT_1_0 => backup::handle_initiate_export
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_BACKUP_COMPLETE_EXPORT_1_0 => backup::handle_complete_export
[ Mutating Secret false ],
vta_sdk::trust_tasks::TASK_BACKUP_INITIATE_IMPORT_1_0 => backup::handle_initiate_import
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_BACKUP_FINALIZE_IMPORT_1_0 => backup::handle_finalize_import
[ Destructive None false ],
vta_sdk::trust_tasks::TASK_BACKUP_ABORT_1_0 => backup::handle_abort
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_BACKUP_INITIATE_EXPORT_1_1 => backup::handle_initiate_export_1_1
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_BACKUP_INITIATE_IMPORT_1_1 => backup::handle_initiate_import_1_1
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_BACKUP_FINALIZE_IMPORT_1_1 => backup::handle_finalize_import_1_1
[ Destructive None false ],
vta_sdk::trust_tasks::TASK_BACKUP_GET_CHUNK_1_0 => backup::handle_get_chunk
[ Mutating Secret false ],
vta_sdk::trust_tasks::TASK_BACKUP_PUT_CHUNK_1_0 => backup::handle_put_chunk
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_DID_TEMPLATES_LIST_2_0 => did_templates::handle_list
[ None Metadata false ],
vta_sdk::trust_tasks::TASK_DID_TEMPLATES_CREATE_2_0 => did_templates::handle_create
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_DID_TEMPLATES_GET_2_0 => did_templates::handle_get
[ None Metadata false ],
vta_sdk::trust_tasks::TASK_DID_TEMPLATES_UPDATE_2_0 => did_templates::handle_update
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_DID_TEMPLATES_DELETE_2_0 => did_templates::handle_delete
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_DID_TEMPLATES_RENDER_2_0 => did_templates::handle_render
[ None Metadata false ],
vta_sdk::trust_tasks::TASK_DID_TEMPLATES_LIST_3_0 => did_templates::handle_list
[ None Metadata false ],
vta_sdk::trust_tasks::TASK_DID_TEMPLATES_CREATE_3_0 => did_templates::handle_create
[ Mutating None false ],
vta_sdk::trust_tasks::TASK_DID_TEMPLATES_GET_3_0 => did_templates::handle_get
[ None Metadata false ],
vta_sdk::trust_tasks::TASK_DID_TEMPLATES_UPDATE_3_0 => did_templates::handle_update
[ Mutating None false ],
#[cfg(all(feature = "webvh", feature = "didcomm"))]
vta_sdk::trust_tasks::TASK_PASSKEY_VMS_ENROLL_CHALLENGE_0_1
=> passkey_vms::handle_enroll_challenge
[ None None false ],
#[cfg(all(feature = "webvh", feature = "didcomm"))]
vta_sdk::trust_tasks::TASK_PASSKEY_VMS_ENROLL_SUBMIT_0_1 => passkey_vms::handle_enroll_submit
[ Mutating None false ],
#[cfg(all(feature = "webvh", feature = "didcomm"))]
vta_sdk::trust_tasks::TASK_PASSKEY_VMS_LIST_0_1 => passkey_vms::handle_list
[ None Metadata false ],
#[cfg(all(feature = "webvh", feature = "didcomm"))]
vta_sdk::trust_tasks::TASK_PASSKEY_VMS_REVOKE_0_1 => passkey_vms::handle_revoke
[ Destructive None false ],
#[cfg(feature = "webvh")]
vta_sdk::trust_tasks::TASK_PROVISION_INTEGRATION_0_3
=> provision_integration::handle_request
[ Mutating Secret false ],
#[cfg(feature = "webvh")]
vta_sdk::trust_tasks::TASK_WEBVH_SERVERS_LIST_1_0 => webvh::handle_servers_list
[ None Metadata false ],
#[cfg(feature = "webvh")]
vta_sdk::trust_tasks::TASK_WEBVH_SERVERS_REGISTER_1_0 => webvh::handle_servers_register
[ Mutating None false ],
#[cfg(feature = "webvh")]
vta_sdk::trust_tasks::TASK_WEBVH_SERVERS_REMOVE_1_0 => webvh::handle_servers_remove
[ Mutating None false ],
#[cfg(feature = "webvh")]
vta_sdk::trust_tasks::TASK_WEBVH_SERVERS_DOMAINS_0_1 => webvh::handle_servers_domains
[ None Metadata false ],
#[cfg(feature = "webvh")]
vta_sdk::trust_tasks::TASK_WEBVH_SERVERS_RECONCILE_0_1 => webvh::handle_servers_reconcile
[ None Metadata false ],
#[cfg(feature = "webvh")]
vta_sdk::trust_tasks::TASK_WEBVH_SERVERS_RETIRE_ORPHAN_0_1 => webvh::handle_servers_retire_orphan
[ Destructive None false ],
#[cfg(feature = "webvh")]
vta_sdk::trust_tasks::TASK_WEBVH_DIDS_LIST_1_0 => webvh::handle_dids_list
[ None Metadata false ],
#[cfg(feature = "webvh")]
vta_sdk::trust_tasks::TASK_WEBVH_DIDS_CREATE_1_0 => webvh::handle_dids_create
[ Mutating None false ],
#[cfg(feature = "webvh")]
vta_sdk::trust_tasks::TASK_WEBVH_DIDS_GET_1_0 => webvh::handle_dids_get
[ None Metadata false ],
#[cfg(feature = "webvh")]
vta_sdk::trust_tasks::TASK_WEBVH_DIDS_DELETE_1_0 => webvh::handle_dids_delete
[ Destructive None false ],
#[cfg(feature = "webvh")]
vta_sdk::trust_tasks::TASK_WEBVH_DIDS_UPDATE_1_0 => webvh::handle_dids_update
[ Destructive None false ],
#[cfg(feature = "webvh")]
vta_sdk::trust_tasks::TASK_WEBVH_DIDS_ROTATE_KEYS_1_0 => webvh::handle_dids_rotate_keys
[ Destructive None false ],
#[cfg(feature = "webvh")]
vta_sdk::trust_tasks::TASK_WEBVH_DIDS_REALIGN_KEYS_1_0 => webvh::handle_dids_realign_keys
[ Mutating None false ],
#[cfg(feature = "webvh")]
vta_sdk::trust_tasks::TASK_WEBVH_DIDS_REGISTER_WITH_SERVER_1_0
=> webvh::handle_dids_register_with_server
[ Mutating None false ],
#[cfg(feature = "webvh")]
vta_sdk::trust_tasks::TASK_WEBVH_AGENT_NAME_LIST_1_0 => webvh::handle_agent_name_list
[ None Metadata false ],
#[cfg(feature = "webvh")]
vta_sdk::trust_tasks::TASK_WEBVH_AGENT_NAME_CHECK_1_0 => webvh::handle_agent_name_check
[ None Metadata false ],
#[cfg(feature = "webvh")]
vta_sdk::trust_tasks::TASK_WEBVH_AGENT_NAME_SET_1_0 => webvh::handle_agent_name_set
[ Destructive None false ],
#[cfg(feature = "webvh")]
vta_sdk::trust_tasks::TASK_WEBVH_AGENT_NAME_REMOVE_1_0 => webvh::handle_agent_name_remove
[ Destructive None false ],
#[cfg(feature = "webvh")]
vta_sdk::trust_tasks::TASK_WEBVH_AGENT_NAME_DISABLE_1_0 => webvh::handle_agent_name_disable
[ Destructive None false ],
#[cfg(feature = "webvh")]
vta_sdk::trust_tasks::TASK_WEBVH_AGENT_NAME_ENABLE_1_0 => webvh::handle_agent_name_enable
[ Destructive None false ],
}
#[cfg(test)]
mod tests {
use trust_tasks_rs::TrustTask;
use super::*;
#[tokio::test]
async fn a_success_response_is_signed() {
let secret = test_secret();
let body =
br#"{"id":"urn:uuid:x","type":"https://example.org/t#response","payload":{"ok":true}}"#;
let signed = super::attach_proof(&secret, body)
.await
.expect("a well-formed response signs");
let doc: Value = serde_json::from_slice(&signed).expect("signed document parses");
let proof = doc
.get("proof")
.expect("a success response with no proof is unattributable");
assert_eq!(
proof.get("cryptosuite").and_then(Value::as_str),
Some("eddsa-jcs-2022")
);
assert!(proof.get("proofValue").and_then(Value::as_str).is_some());
assert_eq!(
proof.get("verificationMethod").and_then(Value::as_str),
Some(secret.id.as_str()),
"the proof must name the key that signed it"
);
assert_eq!(doc["payload"]["ok"], Value::Bool(true));
}
#[tokio::test]
async fn an_existing_proof_is_replaced_not_nested() {
let secret = test_secret();
let body = br#"{"id":"urn:uuid:x","type":"https://example.org/t#response","proof":{"stale":true},"payload":{}}"#;
let signed = super::attach_proof(&secret, body).await.expect("signs");
let doc: Value = serde_json::from_slice(&signed).expect("parses");
assert!(
doc["proof"].get("stale").is_none(),
"the stale proof survived: {}",
doc["proof"]
);
}
#[tokio::test]
async fn an_unsignable_body_degrades_rather_than_panicking() {
assert!(
super::attach_proof(&test_secret(), b"not json")
.await
.is_none()
);
assert!(
super::attach_proof(&test_secret(), b"[1,2,3]")
.await
.is_none()
);
}
#[test]
fn only_the_spine_parses_a_trust_task_document() {
const ALLOWED: &[(&str, &str)] = &[(
"routes/auth.rs",
"Pre-login: `auth/{challenge,authenticate,refresh}` carry no session, so they cannot pass `AuthClaims` through the dispatcher's extractor and are served as dedicated REST routes. `vta_sdk`'s `REST_ROUTED_URIS` is the canonical list and names exactly these.",
)];
fn rust_files(dir: &std::path::Path, out: &mut Vec<std::path::PathBuf>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
rust_files(&path, out);
} else if path.extension().is_some_and(|e| e == "rs") {
out.push(path);
}
}
}
let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("src");
let mut files = Vec::new();
for area in ["messaging", "routes"] {
rust_files(&root.join(area), &mut files);
}
assert!(
!files.is_empty(),
"the sweep found no transport sources — it has stopped checking anything"
);
let mut offenders: Vec<String> = Vec::new();
let mut allowed_seen: Vec<&str> = Vec::new();
for file in &files {
let rel = file
.strip_prefix(&root)
.unwrap_or(file)
.to_string_lossy()
.replace('\\', "/");
let src = std::fs::read_to_string(file).expect("read a transport source");
let production = src.split("#[cfg(test)]").next().unwrap_or(&src);
for (n, line) in production.lines().enumerate() {
let parses = line.contains("TrustTask<")
&& (line.contains("from_slice")
|| line.contains("from_str")
|| line.contains("from_value"));
if !parses {
continue;
}
if ALLOWED.iter().any(|(f, _)| *f == rel) {
if !allowed_seen.contains(&rel.as_str()) {
allowed_seen.push(Box::leak(rel.clone().into_boxed_str()));
}
continue;
}
offenders.push(format!("{rel}:{} {}", n + 1, line.trim()));
}
}
assert!(
offenders.is_empty(),
"these transport modules parse a Trust-Task document themselves:\n {}\n\n\
A transport opens its own binding and hands the bytes to \
`dispatch_trust_task_core`; it does not read the document. Parsing one \
here is how a second dispatch path starts — first to read a field, then \
to branch on it, and then with its own idea of validation, replay and \
signing. If this is genuinely a pre-dispatch surface like \
`routes/auth.rs`, add it to ALLOWED with the reason.",
offenders.join("\n ")
);
for (file, _) in ALLOWED {
assert!(
allowed_seen.contains(file),
"`{file}` is allow-listed but no longer parses a document — remove \
the entry so the list shrinks"
);
}
}
#[test]
fn the_spine_signs_every_response_it_returns() {
let src = include_str!("mod.rs");
let start = src
.find("pub(crate) async fn dispatch_trust_task_core(")
.expect("the spine is still named that");
let body = &src[start..];
let end = body.find("\n}\n").expect("the spine has an end");
assert!(
body[..end].contains("sign_success_response("),
"the dispatch spine no longer signs its responses. 265 published \
specifications require a proof on the response (SPEC §7.3 item 7). \
`VtaClient` verifies one since #1341, so dropping this would break \
every round-trip test — but it would break them by blaming the \
reply, which is a long way from the cause."
);
}
#[tokio::test]
async fn the_signer_reads_the_agents_resident_key() {
use affinidi_secrets_resolver::ThreadedSecretsResolver;
use affinidi_tdk::secrets_resolver::SecretsResolver as _;
let (mut state, _dir) = crate::test_support::build_signing_test_app_state().await;
let outcome = || TrustTaskOutcome {
status: axum::http::StatusCode::OK,
body: br#"{"id":"urn:uuid:x","type":"https://example.org/t#response","payload":{}}"#
.to_vec(),
};
state.secrets_resolver = None;
state.signing_vm_id = None;
let unsigned = super::sign_success_response(&state, outcome()).await;
let doc: Value = serde_json::from_slice(&unsigned.body).expect("parses");
assert!(doc.get("proof").is_none());
assert!(
unsigned.status.is_success(),
"an unsigned answer is still an answer"
);
let secret = test_secret();
let vm_id = secret.id.clone();
let (resolver, _task) = ThreadedSecretsResolver::new(None).await;
resolver.insert(secret).await;
state.secrets_resolver = Some(std::sync::Arc::new(resolver));
state.signing_vm_id = Some(vm_id.clone());
let signed = super::sign_success_response(&state, outcome()).await;
let doc: Value = serde_json::from_slice(&signed.body).expect("parses");
assert_eq!(
doc["proof"]["verificationMethod"].as_str(),
Some(vm_id.as_str()),
"signed by the wrong key, or not at all: {doc}"
);
}
fn test_secret() -> affinidi_secrets_resolver::secrets::Secret {
use ed25519_dalek::SigningKey;
let sk = SigningKey::from_bytes(&[7u8; 32]);
let mut mc = vec![0xed, 0x01];
mc.extend_from_slice(sk.verifying_key().as_bytes());
let did = format!(
"did:key:{}",
multibase::encode(multibase::Base::Base58Btc, mc)
);
let secrets =
vta_sdk::did_key::secrets_from_did_key(&did, &sk.to_bytes()).expect("did:key secrets");
secrets.signing
}
#[test]
#[allow(deprecated)]
fn class_for_carries_authoritative_classification() {
use crate::policy::{Discloses, SideEffectLevel};
let release = class_for(vta_sdk::trust_tasks::TASK_VAULT_RELEASE_0_1)
.expect("vault/release is classified");
assert_eq!(release.side_effects, SideEffectLevel::Mutating);
assert_eq!(release.exposure.discloses, Discloses::Secret);
assert!(!release.exposure.acts_as_subject);
let proxy = class_for(vta_sdk::trust_tasks::TASK_VAULT_PROXY_LOGIN_0_1)
.expect("proxy-login is classified");
assert!(
proxy.exposure.acts_as_subject,
"proxy-login acts as the subject"
);
let export = class_for(vta_sdk::trust_tasks::TASK_KEYS_EXPORT_SECRET_0_1)
.expect("key export is classified");
assert_eq!(
export.exposure.discloses,
Discloses::Secret,
"releasing a key's private half discloses a secret"
);
assert_eq!(
export.side_effects,
SideEffectLevel::None,
"it reads one key and changes nothing — the act is disclosure, not mutation"
);
assert!(
class_for("https://trusttasks.org/spec/does-not-exist/9.9").is_none(),
"an unknown URI is unclassified — caller applies the floor"
);
}
#[test]
fn body_parse_error_wire_shape() {
let resp = body_parse_error_response("expected `,`");
let _ = resp;
}
#[test]
fn framework_requires_canonical_uri_in_wire_type_field() {
let canonical = serde_json::json!({
"id": "urn:uuid:00000000-0000-0000-0000-000000000001",
"type": "https://trusttasks.org/spec/auth/revoke-session/0.1",
"issuer": "did:example:alice",
"recipient": "did:example:vta",
"issuedAt": chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
"payload": { "session_id": "sess-1" }
});
let bytes = serde_json::to_vec(&canonical).unwrap();
let parsed: Result<TrustTask<Value>, _> = serde_json::from_slice(&bytes);
assert!(
parsed.is_ok(),
"canonical URI must parse: {:?}",
parsed.err()
);
let flat = serde_json::json!({
"id": "urn:uuid:00000000-0000-0000-0000-000000000001",
"type": "https://trusttasks.org/vta/auth/revoke-session/1.0",
"issuer": "did:example:alice",
"recipient": "did:example:vta",
"issuedAt": chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
"payload": { "session_id": "sess-1" }
});
let bytes = serde_json::to_vec(&flat).unwrap();
let parsed: Result<TrustTask<Value>, _> = serde_json::from_slice(&bytes);
assert!(
parsed.is_err(),
"flat URI must NOT parse — if this changes, the framework \
relaxed its parser and Phase 3 design can simplify"
);
}
#[test]
#[allow(deprecated)] fn phase_2_uri_registry_present() {
let _ = vta_sdk::trust_tasks::TASK_AUTH_CHALLENGE_0_1;
let _ = vta_sdk::trust_tasks::TASK_AUTH_AUTHENTICATE_0_1;
let _ = vta_sdk::trust_tasks::TASK_AUTH_REFRESH_0_1;
let _ = vta_sdk::trust_tasks::TASK_AUTH_REVOKE_SESSION_0_1;
let _ = vta_sdk::trust_tasks::TASK_AUTH_WHOAMI_0_1;
let _ = vta_sdk::trust_tasks::TASK_AUTH_SESSIONS_LIST_0_1;
let _ = vta_sdk::trust_tasks::TASK_AUTH_PASSKEY_LOGIN_START_0_1;
let _ = vta_sdk::trust_tasks::TASK_AUTH_PASSKEY_LOGIN_FINISH_0_1;
}
#[test]
fn dispatcher_handles_every_vta_sdk_uri() {
let dispatched = dispatched_uris();
for declared in vta_sdk::trust_tasks::ALL_URIS {
let in_dispatched = dispatched.contains(declared);
let in_rest_routed = REST_ROUTED.contains(declared);
let in_feature_gated = KNOWN_FEATURE_GATED_URIS.contains(declared);
let in_wire_v0_2 = wire_v0_2::WIRE_V0_2_URIS.contains(declared);
assert!(
in_dispatched || in_rest_routed || in_feature_gated || in_wire_v0_2,
"vta-sdk declares URI `{declared}` but it is not tracked in this dispatcher — \
either (a) add a `dispatch_table!` entry (`URI => slice::handler`), \
(b) add it to `REST_ROUTED` if it lives on a dedicated REST route, \
(c) add it to `KNOWN_FEATURE_GATED_URIS` with a comment explaining the gating, or \
(d) register it in `wire_v0_2::WIRE_V0_2_URIS` if it's an edge-transformed 0.2 URI"
);
}
}
#[test]
fn the_spec_index_is_populated() {
for uri in [
"https://trusttasks.org/spec/acl/grant/0.1",
"https://trusttasks.org/spec/auth/authenticate/0.1",
"https://trusttasks.org/spec/vault/list/0.3",
] {
assert!(
trust_tasks_rs::schema_index::schema_for(uri).is_some(),
"no schema for `{uri}` — this build's `trust-tasks-rs` carries no spec \
families, so payload validation and SPEC §7.2 enforcement are both off"
);
assert!(
trust_tasks_rs::schema_index::spec_policy_for(uri).is_some(),
"no spec policy for `{uri}` — §7.2's recipient/proof/issuedAt checks \
cannot fire for it"
);
}
}
#[test]
fn superseded_tasks_are_dispatched() {
let dispatched = dispatched_uris();
for task in crate::deprecation::superseded_tasks_table() {
let served = dispatched.contains(&task.uri)
|| KNOWN_FEATURE_GATED_URIS.contains(&task.uri)
|| wire_v0_2::WIRE_V0_2_URIS.contains(&task.uri);
assert!(
served,
"`{}` is marked superseded but nothing dispatches it, so its counter \
reads zero forever and would report the task as safe to retire when \
it has already been retired. Drop the row if the task is gone; fix \
the URI if it is a typo; move it to `deprecation::SUPERSEDED` if the \
operation is served by a REST route rather than this dispatcher.",
task.uri
);
}
}
#[test]
fn superseded_task_successors_are_served() {
let dispatched = dispatched_uris();
for task in crate::deprecation::superseded_tasks_table() {
let served = dispatched.contains(&task.successor)
|| KNOWN_FEATURE_GATED_URIS.contains(&task.successor)
|| REST_ROUTED.contains(&task.successor)
|| wire_v0_2::WIRE_V0_2_URIS.contains(&task.successor);
assert!(
served,
"`{}` is advertised as the successor to `{}`, but this VTA does not \
serve it — the notice would send a migrating client onto an \
unsupported type",
task.successor, task.uri
);
}
}
#[test]
fn every_dual_accepted_spec_marks_its_older_forms_superseded() {
for spec in wire_v0_2::WIRE_SPECS_V0_2 {
let mut chain = vec![spec.uri_0_1];
chain.extend(spec.uris_wire.iter().rev());
let newest = chain.pop().expect("a spec has at least one wire form");
for superseded in chain {
let row = crate::deprecation::superseded_task(superseded).unwrap_or_else(|| {
panic!(
"`{superseded}` is superseded (this spec also accepts \
`{newest}`) but it is not in \
`deprecation::SUPERSEDED_TASKS`, so nothing counts the \
callers still on it and it can never be retired on \
evidence"
)
});
assert!(
spec.uris_wire.contains(&row.successor),
"`{superseded}`'s deprecation row points at `{}`, which this \
spec does not accept on the wire",
row.successor
);
}
}
}
#[test]
fn every_served_uri_has_a_published_spec_or_is_tracked_debt() {
let mut served: std::collections::BTreeSet<&str> = dispatched_uris().into_iter().collect();
served.extend(REST_ROUTED);
served.extend(KNOWN_FEATURE_GATED_URIS);
served.extend(wire_v0_2::WIRE_V0_2_URIS);
let unspecced: Vec<&&str> = served
.iter()
.filter(|uri| {
trust_tasks_rs::schema_index::schema_for(uri).is_none()
&& !UNSPECCED_DISPATCHED_URIS.contains(uri)
})
.collect();
assert!(
unspecced.is_empty(),
"this service serves URIs the published registry (trust-tasks-rs) \
has no spec for, and they are not acknowledged in \
UNSPECCED_DISPATCHED_URIS:\n {}\n\n\
Author the spec upstream in trustoverip/dtgwg-trust-tasks-tf and \
bump trust-tasks-rs — growing the allowlist is the wrong fix \
(see issue #854 and docs/05-design-notes/registry-drift-triage.md).",
unspecced
.iter()
.map(|u| u.to_string())
.collect::<Vec<_>>()
.join("\n ")
);
for uri in UNSPECCED_DISPATCHED_URIS {
assert!(
trust_tasks_rs::schema_index::schema_for(uri).is_none(),
"`{uri}` is now published in the registry — remove it from \
UNSPECCED_DISPATCHED_URIS so the debt shrinks monotonically"
);
assert!(
served.contains(uri),
"`{uri}` is in UNSPECCED_DISPATCHED_URIS but this service no \
longer serves it — remove the stale entry"
);
}
}
#[test]
fn passkey_vms_0_1_dispatched() {
let dispatched = dispatched_uris();
let tracked = |u: &&str| dispatched.contains(u) || KNOWN_FEATURE_GATED_URIS.contains(u);
for v0_1 in [
vta_sdk::trust_tasks::TASK_PASSKEY_VMS_ENROLL_CHALLENGE_0_1,
vta_sdk::trust_tasks::TASK_PASSKEY_VMS_ENROLL_SUBMIT_0_1,
vta_sdk::trust_tasks::TASK_PASSKEY_VMS_LIST_0_1,
vta_sdk::trust_tasks::TASK_PASSKEY_VMS_REVOKE_0_1,
] {
assert!(tracked(&v0_1), "canonical 0.1 URI not dispatched: {v0_1}");
assert!(v0_1.ends_with("/0.1"), "version-label mismatch for {v0_1}");
}
}
#[test]
fn provision_clients_dispatch_the_version_this_service_serves() {
use vta_sdk::protocols::provision_integration_management::ProvisionSpecVersion;
let uri = ProvisionSpecVersion::CURRENT.request_uri();
let dispatched = dispatched_uris();
assert!(
dispatched.contains(&uri) || KNOWN_FEATURE_GATED_URIS.contains(&uri),
"vta-sdk's provisioning clients dispatch `{uri}` \
(`ProvisionSpecVersion::CURRENT`), but this service does not serve \
it. A provision-integration version cut-over has to move both \
halves: the `dispatch_table!` entry and `CURRENT`."
);
}
#[test]
fn no_uri_is_both_dispatched_and_rest_routed() {
let dispatched = dispatched_uris();
for uri in REST_ROUTED {
assert!(
!dispatched.contains(uri),
"URI `{uri}` is in REST_ROUTED but also in a `dispatch_table!` entry — \
a URI must live on exactly one transport"
);
}
}
}
#[cfg(all(test, feature = "webvh"))]
mod payload_validation_tests {
use serde_json::{Value, json};
use trust_tasks_rs::TrustTask;
const WEBVH_UPDATE: &str = "https://trusttasks.org/spec/vta/webvh/dids/update/1.0";
fn doc(payload: Value) -> TrustTask<Value> {
serde_json::from_value(json!({
"id": "urn:uuid:00000000-0000-0000-0000-000000000042",
"type": WEBVH_UPDATE,
"issuer": "did:key:zTestAdmin",
"recipient": "did:example:vta",
"issuedAt": chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
"payload": payload,
}))
.expect("valid trust task")
}
#[tokio::test]
async fn a_precondition_in_the_wrong_case_is_refused_not_ignored() {
let (state, _dir) = crate::test_support::build_signing_test_app_state().await;
let d = doc(json!({
"did": "did:webvh:QmScid:example.com:acme",
"expected_version_id": "3-QmPrior"
}));
let reject = super::validate_payload(&state, WEBVH_UPDATE, &d)
.await
.expect("an unrecognised member must be refused");
let body: Value = serde_json::from_slice(&reject.body).unwrap();
let msg = body.to_string();
assert!(
msg.contains("does not conform"),
"expected a schema-conformance refusal, got: {msg}"
);
}
#[tokio::test]
async fn the_correct_casing_passes() {
let (state, _dir) = crate::test_support::build_signing_test_app_state().await;
let d = doc(json!({
"did": "did:webvh:QmScid:example.com:acme",
"document": { "id": "did:webvh:QmScid:example.com:acme" },
"expectedVersionId": "3-QmPrior"
}));
assert!(
super::validate_payload(&state, WEBVH_UPDATE, &d)
.await
.is_none()
);
}
#[tokio::test]
async fn the_ext_slot_the_relay_stamps_an_origin_into_is_permitted() {
let (state, _dir) = crate::test_support::build_signing_test_app_state().await;
let d = doc(json!({
"did": "did:webvh:QmScid:example.com:acme",
"ext": { "openvtc.origin": "https://control.example.com" }
}));
assert!(
super::validate_payload(&state, WEBVH_UPDATE, &d)
.await
.is_none(),
"closed payloads must still admit `ext`, or the relay cannot stamp an origin"
);
}
#[tokio::test]
async fn a_partial_edit_from_the_cli_validates() {
use vta_sdk::protocols::did_management::update::UpdateDidWebvhBody;
let (state, _dir) = crate::test_support::build_signing_test_app_state().await;
let body = UpdateDidWebvhBody {
label: Some("resync".into()),
..Default::default()
};
let mut payload = serde_json::to_value(&body).expect("serialises");
payload
.as_object_mut()
.expect("object")
.insert("did".into(), json!("did:webvh:QmScid:example.com:acme"));
assert!(
!payload.to_string().contains("null"),
"the CLI's own payload must carry no nulls: {payload}"
);
let reject = super::validate_payload(&state, WEBVH_UPDATE, &doc(payload.clone())).await;
assert!(
reject.is_none(),
"a label-only edit must validate, got: {:?}",
reject.map(|r| String::from_utf8_lossy(&r.body).into_owned())
);
}
#[tokio::test]
async fn the_null_form_that_broke_the_cli_is_still_refused() {
let (state, _dir) = crate::test_support::build_signing_test_app_state().await;
let d = doc(json!({
"did": "did:webvh:QmScid:example.com:acme",
"document": Value::Null,
"preRotationCount": Value::Null,
"witnesses": Value::Null,
"watchers": Value::Null,
"ttl": Value::Null,
"label": "resync",
"expectedVersionId": Value::Null,
}));
assert!(
super::validate_payload(&state, WEBVH_UPDATE, &d)
.await
.is_some(),
"an explicit null is not a valid member value — if this passes, the \
schema stopped typing its members and the fix above proves nothing"
);
}
#[tokio::test]
async fn an_invented_member_is_refused() {
let (state, _dir) = crate::test_support::build_signing_test_app_state().await;
let d = doc(json!({ "did": "did:webvh:x", "skipApproval": true }));
assert!(
super::validate_payload(&state, WEBVH_UPDATE, &d)
.await
.is_some()
);
}
#[tokio::test]
async fn an_unspecced_task_proceeds_by_default_and_can_be_refused() {
let (state, _dir) = crate::test_support::build_signing_test_app_state().await;
let Some(unspecced) = super::UNSPECCED_DISPATCHED_URIS
.iter()
.copied()
.find(|u| trust_tasks_rs::schema_index::schema_for(u).is_none())
else {
return;
};
let d = doc(json!({}));
assert!(
super::validate_payload(&state, unspecced, &d)
.await
.is_none(),
"by default an unvalidatable task still dispatches — refusing it would \
break the many tasks that have no spec yet"
);
state.config.write().await.policy.require_payload_schema = true;
assert!(
super::validate_payload(&state, unspecced, &d)
.await
.is_some(),
"an operator who would rather fail closed can"
);
}
}
#[cfg(test)]
mod superseded_task_dispatch_tests {
use serde_json::{Value, json};
use crate::deprecation::DEPRECATION_MEMBER;
use crate::test_support::{build_signing_test_app_state, super_admin_claims};
use crate::trust_tasks::transport::TransportConfidentiality;
async fn dispatch(type_uri: &str, payload: Value) -> Value {
let (state, _dir) = build_signing_test_app_state().await;
let vta_did = state
.config
.read()
.await
.vta_did
.clone()
.expect("the signing test state configures a vta_did");
let body = serde_json::to_vec(&json!({
"id": format!("urn:uuid:{}", uuid::Uuid::new_v4()),
"type": type_uri,
"issuer": "did:key:zTestAdmin",
"recipient": vta_did,
"issuedAt": chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
"payload": payload,
}))
.unwrap();
let outcome = super::dispatch_trust_task_core(
&state,
&super_admin_claims(),
&body,
TransportConfidentiality::HopByHop,
)
.await;
serde_json::from_slice(&outcome.body).expect("a response document")
}
#[tokio::test]
#[allow(deprecated)] async fn a_superseded_task_names_its_successor_in_the_response() {
let uri = vta_sdk::trust_tasks::TASK_DEVICE_LIST_0_1;
let doc = dispatch(uri, json!({})).await;
assert_eq!(
doc["type"],
format!("{uri}#response"),
"expected a success response to annotate, got: {doc}"
);
let notice = &doc[DEPRECATION_MEMBER];
assert_eq!(
notice["supersededBy"],
vta_sdk::trust_tasks::TASK_DEVICE_LIST_0_2,
"the response must name what to send instead, so a client can act \
rather than guess; got document: {doc}"
);
assert!(
notice["reason"].as_str().is_some_and(|r| !r.is_empty()),
"the notice must say why, got: {notice}"
);
}
#[tokio::test]
#[allow(deprecated)] async fn a_rejected_superseded_task_still_names_its_successor() {
let uri = vta_sdk::trust_tasks::TASK_DEVICE_REGISTER_0_1;
let doc = dispatch(uri, json!({})).await;
assert_eq!(
doc["payload"]["code"], "malformedRequest",
"expected a rejection to annotate, got: {doc}"
);
assert_eq!(
doc[DEPRECATION_MEMBER]["supersededBy"],
vta_sdk::trust_tasks::TASK_DEVICE_REGISTER_0_2,
"a rejection must carry the successor too: {doc}"
);
}
#[tokio::test]
async fn a_current_task_carries_no_notice() {
let doc = dispatch(vta_sdk::trust_tasks::TASK_AUTH_WHOAMI_0_1, json!({})).await;
assert!(
doc.get(DEPRECATION_MEMBER).is_none(),
"a task that is not superseded must not be advertised as one: {doc}"
);
}
#[tokio::test]
async fn the_payload_is_untouched_by_the_notice() {
#[allow(deprecated)]
let uri = vta_sdk::trust_tasks::TASK_DEVICE_LIST_0_1;
let doc = dispatch(uri, json!({})).await;
let payload = doc.get("payload").expect("a response carries a payload");
assert!(
payload.get(DEPRECATION_MEMBER).is_none() && payload.get("ext").is_none(),
"the notice must not reach the payload: {payload}"
);
}
}
#[cfg(test)]
mod freshness_bounds {
use super::*;
use chrono::{TimeDelta, Utc};
use serde_json::json;
fn doc(issued_at: Option<&str>, expires_at: Option<&str>) -> TrustTask<Value> {
let mut v = json!({
"id": "urn:uuid:11111111-1111-1111-1111-111111111111",
"type": vta_sdk::trust_tasks::TASK_AUTH_WHOAMI_0_1,
"issuer": "did:key:zTestAdmin",
"payload": {},
});
if let Some(i) = issued_at {
v["issuedAt"] = json!(i);
}
if let Some(e) = expires_at {
v["expiresAt"] = json!(e);
}
serde_json::from_value(v).expect("a document")
}
#[test]
fn a_document_inside_the_skew_window_is_accepted() {
let now = Utc::now();
let soon = (now + TimeDelta::seconds(30)).to_rfc3339();
assert!(
doc(Some(&soon), None)
.validate_freshness(now, &freshness_policy())
.is_ok(),
"a modestly fast producer clock is the ordinary case, not a defect"
);
}
#[test]
fn a_future_dated_document_is_malformed_not_expired() {
let now = Utc::now();
let far = (now + TimeDelta::seconds(600)).to_rfc3339();
let err = doc(Some(&far), None)
.validate_freshness(now, &freshness_policy())
.expect_err("beyond the skew tolerance must be refused");
assert!(
matches!(err, RejectReason::MalformedRequest { .. }),
"it must be malformed, never expired: `expired` names a document \
that was once acceptable and tells the producer to wait, when \
what it must do is reissue. Got {err:?}"
);
}
#[test]
fn an_expiry_at_or_before_issuance_is_malformed() {
let now = Utc::now();
let issued = now.to_rfc3339();
for expiry in [now, now - TimeDelta::seconds(1)] {
let err = doc(Some(&issued), Some(&expiry.to_rfc3339()))
.validate_freshness(now, &freshness_policy())
.expect_err("a validity interval containing no instant is malformed");
assert!(
matches!(err, RejectReason::MalformedRequest { .. }),
"got {err:?}"
);
}
}
#[test]
fn a_document_with_no_timestamps_is_malformed_not_expired() {
let err = doc(None, None)
.validate_freshness(Utc::now(), &freshness_policy())
.expect_err("an unbounded document cannot sit in a bounded window");
assert!(
matches!(
&err,
RejectReason::MalformedRequest { reason }
if reason == trust_tasks_rs::freshness::ISSUED_AT_REQUIRED
),
"it must be malformed and must name the missing member, never \
`expired`: a producer told to wait reissues the same unboundable \
document and loops. Got {err:?}"
);
}
#[test]
fn an_expiry_alone_bounds_the_record_but_not_this_consumers_window() {
let expires =
(Utc::now() + TimeDelta::minutes(5)).to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
assert!(
doc(None, Some(&expires))
.validate_freshness(Utc::now(), &trust_tasks_rs::FreshnessPolicy::default())
.is_ok(),
"an `expiresAt` is enough to bound the record, so the library's \
permissive default takes it"
);
let err = doc(None, Some(&expires))
.validate_freshness(Utc::now(), &freshness_policy())
.expect_err("this service runs a replay guard, so it needs the window too");
assert!(
matches!(err, RejectReason::MalformedRequest { .. }),
"got {err:?}"
);
}
#[test]
fn the_services_policy_bounds_both_the_window_and_the_member() {
let policy = freshness_policy();
assert_eq!(
policy.max_age,
Some(TimeDelta::minutes(10)),
"the acceptance window is also the replay record's retention \
(SPEC §7.2); dropping it makes the guard's horizon load-dependent"
);
assert!(
policy.require_issued_at,
"without it a timestamp-less document is answered `expired`, and \
the spec's own `issuedAt` rule further down the spine never runs"
);
}
}
#[cfg(test)]
mod record_retention {
use super::*;
use chrono::{SubsecRound, TimeDelta, Utc};
use serde_json::json;
fn now() -> chrono::DateTime<Utc> {
Utc::now().trunc_subsecs(0)
}
fn doc(issued_at: chrono::DateTime<Utc>, expires_at: Option<&str>) -> TrustTask<Value> {
let mut v = json!({
"id": "urn:uuid:22222222-2222-2222-2222-222222222222",
"type": vta_sdk::trust_tasks::TASK_AUTH_WHOAMI_0_1,
"issuedAt": issued_at.to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
"issuer": "did:key:zTestAdmin",
"payload": {},
});
if let Some(e) = expires_at {
v["expiresAt"] = json!(e);
}
serde_json::from_value(v).expect("a document")
}
#[test]
fn a_far_future_expiry_does_not_pin_the_record_past_the_window() {
let now = now();
let far = (now + TimeDelta::days(3650)).to_rfc3339_opts(chrono::SecondsFormat::Secs, true);
let d = doc(now, Some(&far));
let uncapped = freshness_policy()
.record_expiry(&d, now)
.expect("a bounded policy always yields one");
assert!(
uncapped > now + TimeDelta::days(3000),
"precondition: the library takes the producer's `expiresAt` verbatim"
);
let capped = retain_until(&d, now).expect("a bounded policy always yields one");
let window = now + TimeDelta::minutes(10) + trust_tasks_rs::DEFAULT_SKEW;
assert_eq!(
capped, window,
"retention past the acceptance window is retention the guard can \
never draw on, and it evicts records that are still needed"
);
}
#[test]
fn a_near_expiry_is_left_alone() {
let now = now();
let soon = now + TimeDelta::minutes(2);
let d = doc(
now,
Some(&soon.to_rfc3339_opts(chrono::SecondsFormat::Secs, true)),
);
assert_eq!(
retain_until(&d, now).expect("a bounded policy always yields one"),
soon,
"an expiry inside the window is the tighter bound and stands"
);
}
#[test]
fn without_an_expiry_the_window_is_the_retention() {
let now = now();
assert_eq!(
retain_until(&doc(now, None), now).expect("a bounded policy always yields one"),
now + TimeDelta::minutes(10),
"`issuedAt + max_age`, and the cap adds only the skew allowance"
);
}
}
#[cfg(test)]
mod replay_guard {
use super::*;
use serde_json::json;
async fn twice(payload: Value, type_uri: &str) -> (TrustTaskOutcome, TrustTaskOutcome) {
let (state, _dir) = crate::test_support::build_signing_test_app_state().await;
let vta_did = state.config.read().await.vta_did.clone().expect("vta_did");
let body = serde_json::to_vec(&json!({
"id": "urn:uuid:5eaf00d0-0000-4000-8000-00000000dead",
"type": type_uri,
"issuer": "did:key:zTestAdmin",
"recipient": vta_did,
"issuedAt": chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
"payload": payload,
}))
.expect("envelope");
let claims = crate::test_support::super_admin_claims();
let first = super::dispatch_trust_task_core(
&state,
&claims,
&body,
transport::TransportConfidentiality::HopByHop,
)
.await;
let second = super::dispatch_trust_task_core(
&state,
&claims,
&body,
transport::TransportConfidentiality::HopByHop,
)
.await;
(first, second)
}
#[tokio::test]
async fn a_redelivered_document_is_absorbed_not_executed_again() {
let (first, second) = twice(json!({}), vta_sdk::trust_tasks::TASK_CONTEXTS_LIST_1_0).await;
assert!(
first.status.is_success(),
"the first delivery must run: {}",
String::from_utf8_lossy(&first.body)
);
assert!(
second.status.is_success(),
"a duplicate is not a failure — §7.2 is explicit that it is never \
reported as `taskFailed`, because the task did not fail, it \
already happened"
);
let (mut a, mut b): (Value, Value) = (
serde_json::from_slice(&first.body).expect("first body"),
serde_json::from_slice(&second.body).expect("second body"),
);
for doc in [&mut a, &mut b] {
let proof = doc
.as_object_mut()
.expect("a response is an object")
.remove("proof");
assert!(
proof.is_some(),
"every response this spine returns carries a proof, duplicate or not"
);
}
assert_eq!(
a, b,
"the duplicate must be answered with the prior response"
);
}
#[tokio::test]
async fn a_different_document_under_the_same_id_is_an_id_conflict() {
let (state, _dir) = crate::test_support::build_signing_test_app_state().await;
let vta_did = state.config.read().await.vta_did.clone().expect("vta_did");
let claims = crate::test_support::super_admin_claims();
let envelope = |issued: &str| {
serde_json::to_vec(&json!({
"id": "urn:uuid:5eaf00d0-0000-4000-8000-0000000c0nf1",
"type": vta_sdk::trust_tasks::TASK_CONTEXTS_LIST_1_0,
"issuedAt": chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
"issuer": "did:key:zTestAdmin",
"recipient": vta_did,
"issuedAt": issued,
"payload": {},
}))
.expect("envelope")
};
let first = super::dispatch_trust_task_core(
&state,
&claims,
&envelope(&chrono::Utc::now().to_rfc3339()),
transport::TransportConfidentiality::HopByHop,
)
.await;
assert!(first.status.is_success());
let second = super::dispatch_trust_task_core(
&state,
&claims,
&envelope(&(chrono::Utc::now() - chrono::TimeDelta::seconds(5)).to_rfc3339()),
transport::TransportConfidentiality::HopByHop,
)
.await;
let doc: Value = serde_json::from_slice(&second.body).expect("a response document");
assert_eq!(
doc["payload"]["code"], "idConflict",
"a different document under a spent id must be refused, not \
absorbed as a retry: {doc}"
);
}
}
#[cfg(test)]
mod response_coverage {
use super::*;
use base64::Engine;
use serde_json::json;
use vta_sdk::trust_tasks as t;
use crate::test_support::build_signing_test_app_state;
pub(super) fn signed_body(uri: &str, vta_did: &str, payload: Value) -> Vec<u8> {
let mut doc: TrustTask<Value> = serde_json::from_value(json!({
"id": format!("urn:uuid:{}", uuid::Uuid::new_v4()),
"type": uri,
"issuer": crate::test_support::test_admin_did().0,
"recipient": vta_did,
"issuedAt": chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
"payload": payload,
}))
.expect("envelope deserialises");
crate::test_support::sign_as_test_admin(&mut doc);
serde_json::to_vec(&doc).expect("envelope serialises")
}
async fn ok(state: &crate::server::AppState, uri: &str, payload: Value) -> Value {
let vta_did = state.config.read().await.vta_did.clone().expect("vta_did");
let body = signed_body(uri, &vta_did, payload);
let outcome = super::dispatch_trust_task_core(
state,
&crate::test_support::super_admin_claims(),
&body,
transport::TransportConfidentiality::HopByHop,
)
.await;
let doc: Value = serde_json::from_slice(&outcome.body).expect("a response document");
assert_eq!(
doc["type"],
format!("{uri}#response"),
"expected a success response from {uri}, got: {doc}"
);
doc["payload"].clone()
}
async fn ok_e2e(state: &crate::server::AppState, uri: &str, payload: Value) -> Value {
let vta_did = state.config.read().await.vta_did.clone().expect("vta_did");
let body = signed_body(uri, &vta_did, payload);
let outcome = super::dispatch_trust_task_core(
state,
&crate::test_support::super_admin_claims(),
&body,
transport::TransportConfidentiality::EndToEnd,
)
.await;
let doc: Value = serde_json::from_slice(&outcome.body).expect("a response document");
assert_eq!(
doc["type"],
format!("{uri}#response"),
"expected a success response from {uri}, got: {doc}"
);
doc["payload"].clone()
}
async fn a_key(state: &crate::server::AppState, label: &str) -> String {
let p = ok(
state,
t::TASK_KEYS_CREATE_0_1,
json!({ "keyType": "ed25519", "derivationPath": "m/26'/2'/0'/0'", "label": label }),
)
.await;
p["key"]["keyId"]
.as_str()
.or_else(|| p["keyId"].as_str())
.unwrap_or_else(|| panic!("keys/create must name the key it made: {p}"))
.to_owned()
}
async fn a_context(state: &crate::server::AppState, id: &str) {
ok(
state,
t::TASK_CONTEXTS_CREATE_1_0,
json!({ "id": id, "name": id }),
)
.await;
}
#[tokio::test]
async fn revoke_all_is_refused_as_unsupported_not_malformed() {
let (state, _dir) = build_signing_test_app_state().await;
let vta_did = state.config.read().await.vta_did.clone().expect("vta_did");
let body = signed_body(
t::TASK_AUTH_REVOKE_SESSION_0_1,
&vta_did,
json!({ "all": true }),
);
let outcome = super::dispatch_trust_task_core(
&state,
&crate::test_support::super_admin_claims(),
&body,
transport::TransportConfidentiality::HopByHop,
)
.await;
let doc: Value = serde_json::from_slice(&outcome.body).expect("a response document");
assert_eq!(
doc["payload"]["code"], "taskFailed",
"a legal document must not be called malformed: {doc}"
);
assert!(
doc["payload"]["message"]
.as_str()
.is_some_and(|m| m.contains("revoke_all_unsupported")),
"the refusal must name the option it cannot honour: {doc}"
);
}
#[tokio::test]
async fn derive_and_sign_ping_and_revoke_session() {
let (state, _dir) = build_signing_test_app_state().await;
let payload = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(b"coverage");
ok(
&state,
t::TASK_KEYS_DERIVE_AND_SIGN_0_1,
json!({
"keyType": "ed25519",
"derivationPath": "m/26'/2'/0'/7'",
"payload": payload,
"algorithm": "EdDSA",
}),
)
.await;
ok(&state, t::TASK_MESSAGING_PING_0_1, json!({})).await;
}
#[tokio::test]
async fn credentials_issue_then_revoke() {
let (state, _dir) = build_signing_test_app_state().await;
let issued = ok(
&state,
t::TASK_VTA_CREDENTIALS_ISSUE_0_2,
json!({
"holder": "did:key:z6MkCoverageHolder",
"claims": { "role": "coverage" },
"validitySeconds": 3600,
}),
)
.await;
let id = issued["credentialId"]
.as_str()
.or_else(|| issued["credential"]["id"].as_str())
.unwrap_or_else(|| panic!("issue must name the credential: {issued}"))
.to_owned();
ok(
&state,
t::TASK_VTA_CREDENTIALS_REVOKE_0_1,
json!({ "credentialId": id, "reason": "coverage" }),
)
.await;
}
#[tokio::test]
async fn contexts_update_did() {
let (state, _dir) = build_signing_test_app_state().await;
a_context(&state, "cov-update-did").await;
ok(
&state,
t::TASK_CONTEXTS_UPDATE_DID_1_0,
json!({ "id": "cov-update-did", "did": "did:key:z6MkCovContextDid" }),
)
.await;
}
#[tokio::test]
async fn swap_key_without_a_link_proof_names_the_policy() {
let (state, _dir) = build_signing_test_app_state().await;
let claims = crate::test_support::super_admin_claims();
let vta_did = state.config.read().await.vta_did.clone().expect("vta_did");
let body = signed_body(
t::TASK_ACL_SWAP_KEY_0_1,
&vta_did,
json!({
"currentSubject": claims.did,
"newSubject": "did:key:z6MkCovNewSubject",
}),
);
let outcome = super::dispatch_trust_task_core(
&state,
&claims,
&body,
transport::TransportConfidentiality::HopByHop,
)
.await;
let doc: Value = serde_json::from_slice(&outcome.body).expect("a response document");
assert_eq!(
doc["payload"]["code"], "taskFailed",
"a document the schema accepts must not be called malformed: {doc}"
);
assert!(
doc["payload"]["message"]
.as_str()
.is_some_and(|m| m.contains("link_proof_required")),
"the refusal must name what this maintainer wants: {doc}"
);
}
#[tokio::test]
async fn device_lifecycle() {
let (state, _dir) = build_signing_test_app_state().await;
let claims = crate::test_support::super_admin_claims();
crate::test_support::seed_acl_entry(
&state.acl_ks,
&claims.did,
crate::acl::Role::Admin,
vec![],
)
.await;
ok(
&state,
t::TASK_DEVICE_REGISTER_0_2,
json!({
"consumerKind": { "kind": "companion", "formFactor": "mobile" },
"displayName": "Coverage Phone",
}),
)
.await;
ok(&state, t::TASK_DEVICE_HEARTBEAT_0_2, json!({})).await;
ok(&state, t::TASK_DEVICE_SET_WAKE_0_2, json!({})).await;
}
#[tokio::test]
async fn a_proof_that_does_not_verify_is_refused() {
let (state, _dir) = build_signing_test_app_state().await;
let vta_did = state.config.read().await.vta_did.clone().expect("vta_did");
let mut doc: TrustTask<Value> = serde_json::from_value(json!({
"id": format!("urn:uuid:{}", uuid::Uuid::new_v4()),
"type": t::TASK_AUTH_WHOAMI_0_1,
"issuer": crate::test_support::test_admin_did().0,
"recipient": vta_did,
"issuedAt": chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
"payload": {},
}))
.expect("envelope");
crate::test_support::sign_as(0xEE, &mut doc);
let outcome = super::dispatch_trust_task_core(
&state,
&crate::test_support::super_admin_claims(),
&serde_json::to_vec(&doc).expect("bytes"),
transport::TransportConfidentiality::HopByHop,
)
.await;
let v: Value = serde_json::from_slice(&outcome.body).expect("a response");
assert_eq!(
v["payload"]["code"], "proofInvalid",
"a proof from a key the issuer does not control must be refused: {v}"
);
}
#[allow(deprecated)]
#[tokio::test]
async fn device_lifecycle_canonical() {
let (state, _dir) = build_signing_test_app_state().await;
let claims = crate::test_support::super_admin_claims();
crate::test_support::seed_acl_entry(
&state.acl_ks,
&claims.did,
crate::acl::Role::Admin,
vec![],
)
.await;
let registered = ok(
&state,
t::TASK_DEVICE_REGISTER_0_1,
json!({
"consumerKind": { "kind": "companion", "formFactor": "mobile" },
"displayName": "Coverage Phone (canonical)",
}),
)
.await;
let device_id = registered["binding"]["deviceId"]
.as_str()
.expect("register returns the binding's deviceId")
.to_string();
ok(&state, t::TASK_DEVICE_HEARTBEAT_0_1, json!({})).await;
ok(&state, t::TASK_DEVICE_SET_WAKE_0_1, json!({})).await;
ok(
&state,
t::TASK_DEVICE_WIPE_0_1,
json!({
"deviceId": device_id,
"scope": "cache-and-keys",
"reason": "coverage",
}),
)
.await;
ok(
&state,
t::TASK_DEVICE_DISABLE_0_1,
json!({ "deviceId": device_id, "reason": "coverage" }),
)
.await;
}
#[tokio::test]
async fn keys_import_and_derive_and_sign() {
let (state, _dir) = build_signing_test_app_state().await;
let seed = [0x5Au8; 32];
ok_e2e(
&state,
t::TASK_KEYS_IMPORT_0_1,
json!({
"keyType": "ed25519",
"privateKeyMultibase": multibase::encode(multibase::Base::Base58Btc, seed),
"label": "imported",
}),
)
.await;
ok(
&state,
t::TASK_KEYS_DERIVE_AND_SIGN_DOCUMENT_0_1,
json!({
"keyType": "ed25519",
"derivationPath": "m/26'/9'/0'",
"document": {
"type": "https://trusttasks.org/spec/auth/authenticate/0.1",
"payload": { "challenge": "abc", "sessionId": "s1" },
},
}),
)
.await;
}
async fn a_vault_entry(state: &crate::server::AppState, id: &str, context_id: &str) -> String {
use vti_common::vault::{
SecretKind, SiteTarget, StoredVaultEntry, VaultEntry, VaultSecret, VaultStatus,
put_stored_vault_entry,
};
let now = "2026-01-01T00:00:00Z".to_string();
let entry = StoredVaultEntry {
entry: VaultEntry {
id: id.to_string(),
context_id: context_id.to_string(),
targets: vec![SiteTarget::WebOrigin {
origin: "https://example.com".to_string(),
}],
label: "Coverage entry".to_string(),
secret_kind: SecretKind::Password,
tags: Vec::new(),
notes: None,
favicon: None,
selectors: Vec::new(),
custom_field_names: Vec::new(),
attachments: Vec::new(),
expires_at: None,
breached_at: None,
password_changed_at: None,
created_at: now.clone(),
created_by: None,
updated_at: now,
updated_by: None,
last_used_at: None,
version: 1,
principal_did: None,
status: VaultStatus::Active,
archived_at: None,
deleted_at: None,
grace_until: None,
},
secret: VaultSecret::Password {
username: Some("alice".to_string()),
password: "hunter2-very-secret".to_string(),
totp: None,
login_config: None,
secure_notes: None,
custom_fields: Vec::new(),
},
};
put_stored_vault_entry(&state.vault_ks, &entry)
.await
.expect("seed the vault entry");
id.to_string()
}
#[allow(deprecated)]
#[tokio::test]
async fn vault_entry_lifecycle() {
let (state, _dir) = build_signing_test_app_state().await;
a_context(&state, "vault-ctx").await;
let entry_id = a_vault_entry(&state, "vault-cov-1", "vault-ctx").await;
ok(
&state,
t::TASK_VAULT_UPSERT_0_1,
json!({
"id": entry_id,
"contextId": "vault-ctx",
"targets": [{ "kind": "web-origin", "origin": "https://example.com" }],
"label": "Coverage entry (renamed)",
"secretKind": "password",
}),
)
.await;
}
#[tokio::test]
async fn acl_swap_key_rotates_the_callers_own_entry() {
use ed25519_dalek::SigningKey;
let (state, _dir) = build_signing_test_app_state().await;
let claims = crate::test_support::super_admin_claims();
crate::test_support::seed_acl_entry(
&state.acl_ks,
&claims.did,
crate::acl::Role::Admin,
vec![],
)
.await;
let vta_did = state.config.read().await.vta_did.clone().expect("vta_did");
let new_sk = SigningKey::from_bytes(&[0xD1; 32]);
let (new_did, _vm) = crate::test_support::did_for_seed(0xD1);
let now = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
let link_proof = vta_sdk::protocols::acl_management::swap::build_swap_presentation(
&new_sk, &new_did, &vta_did, now, 300, None,
);
ok(
&state,
t::TASK_ACL_SWAP_KEY_0_1,
json!({
"currentSubject": claims.did,
"newSubject": new_did,
"linkProof": link_proof,
}),
)
.await;
}
#[tokio::test]
async fn services_drain_cancel() {
let (state, _dir) = build_signing_test_app_state().await;
let mediator_did = "did:key:z6MkDrainedMediator";
state
.mediator_registry
.record_drain_persisted(
&state.drains_ks,
mediator_did,
"wss://drained.example".into(),
chrono::Utc::now() + chrono::Duration::minutes(30),
)
.await
.expect("seed an active drain");
ok(
&state,
t::TASK_SERVICES_DRAIN_CANCEL_1_0,
json!({ "mediatorDid": mediator_did }),
)
.await;
}
#[tokio::test]
async fn pending_approve_presents_a_held_credential() {
use crate::operations::credential_exchange::pending::{
PendingPresentation, put as put_pending,
};
const VCT: &str = "https://openvtc.org/credentials/MembershipCredential";
let (state, _dir) = build_signing_test_app_state().await;
let holder = crate::test_support::seed_holder_key(&state, "m/26'/2'/0'/0'", None).await;
let credential_id =
crate::test_support::seed_held_credential(&state.vault_ks, VCT, "givenName", &holder)
.await;
let record: PendingPresentation = serde_json::from_value(json!({
"id": "cov-pending-1",
"verifier_did": "did:web:stranger.example",
"requested": [{
"credential_query_id": "membership",
"credential_id": credential_id,
"claims": ["givenName"]
}],
"purpose": "coverage",
"query": {
"dcql_query": {
"credentials": [{
"id": "membership",
"format": "dc+sd-jwt",
"meta": { "vct_values": [VCT] },
"claims": [{ "path": ["givenName"] }]
}]
},
"nonce": "n-1",
"purpose": "coverage"
},
"status": "pending",
"created_at": "2026-06-12T00:00:00Z",
"expires_at": "2126-06-12T00:00:00Z"
}))
.expect("pending record deserialises");
put_pending(&state.vault_ks, &record)
.await
.expect("seed the deferral");
ok(
&state,
vta_sdk::protocols::credential_exchange::PENDING_APPROVE,
json!({ "id": "cov-pending-1" }),
)
.await;
}
#[tokio::test]
async fn services_read_paths() {
let (state, _dir) = build_signing_test_app_state().await;
ok(&state, t::TASK_SERVICES_LIST_1_0, json!({})).await;
ok(
&state,
t::TASK_SERVICES_GET_1_0,
json!({ "service": "rest" }),
)
.await;
ok(&state, t::TASK_SERVICES_DRAIN_LIST_1_0, json!({})).await;
}
#[tokio::test]
async fn webvh_read_paths() {
let (state, _dir) = build_signing_test_app_state().await;
let did = "did:webvh:example.com:coverage";
let record = vta_sdk::webvh::WebvhDidRecord {
did: did.to_string(),
server_id: "serverless".into(),
mnemonic: "coverage-slot".into(),
scid: "scid-coverage".into(),
context_id: "cov-webvh".into(),
portable: false,
log_entry_count: 1,
pre_rotation_count: 0,
next_fragment_id: 1,
created_at: chrono::Utc::now(),
updated_at: chrono::Utc::now(),
};
crate::webvh_store::store_did(&state.webvh_ks, &record)
.await
.expect("seed the webvh record");
ok(&state, t::TASK_WEBVH_DIDS_LIST_1_0, json!({})).await;
let _ = did;
}
#[tokio::test]
async fn did_templates_lifecycle() {
let (state, _dir) = build_signing_test_app_state().await;
a_context(&state, "cov-templates").await;
let template = json!({
"schemaVersion": 1,
"name": "cov-template",
"kind": "app",
"requiredVars": ["LABEL"],
"document": {
"id": "{DID}",
"service": [{ "id": "#cov", "type": "VTARest", "serviceEndpoint": "{LABEL}" }],
},
});
ok(
&state,
t::TASK_DID_TEMPLATES_CREATE_2_0,
json!({ "contextId": "cov-templates", "template": template }),
)
.await;
ok(
&state,
t::TASK_DID_TEMPLATES_LIST_2_0,
json!({ "contextId": "cov-templates" }),
)
.await;
ok(
&state,
t::TASK_DID_TEMPLATES_GET_2_0,
json!({ "contextId": "cov-templates", "name": "cov-template" }),
)
.await;
let mut updated = template.clone();
updated["description"] = json!("renamed");
ok(
&state,
t::TASK_DID_TEMPLATES_UPDATE_2_0,
json!({ "contextId": "cov-templates", "name": "cov-template", "template": updated }),
)
.await;
ok(
&state,
t::TASK_DID_TEMPLATES_RENDER_2_0,
json!({
"contextId": "cov-templates",
"name": "cov-template",
"vars": { "LABEL": "https://cov.example", "DID": "did:key:z6MkCovRender" },
}),
)
.await;
ok(
&state,
t::TASK_DID_TEMPLATES_DELETE_2_0,
json!({ "contextId": "cov-templates", "name": "cov-template" }),
)
.await;
}
#[tokio::test]
async fn policy_lifecycle() {
let (state, _dir) = build_signing_test_app_state().await;
a_context(&state, "cov-policy").await;
let created = ok(
&state,
t::TASK_POLICY_UPSERT_0_2,
json!({
"name": "cov-policy",
"module": "package vta.cov\n\ndefault allow := false\n",
}),
)
.await;
let id = created["policy"]["id"]
.as_str()
.or_else(|| created["id"].as_str())
.unwrap_or_else(|| panic!("upsert must name the policy it stored: {created}"))
.to_owned();
ok(&state, t::TASK_POLICY_LIST_0_2, json!({})).await;
ok(&state, t::TASK_POLICY_GET_0_1, json!({ "id": id })).await;
ok(
&state,
t::TASK_POLICY_DELETE_0_1,
json!({ "id": id, "reason": "coverage" }),
)
.await;
}
#[tokio::test]
async fn contexts_lifecycle() {
let (state, _dir) = build_signing_test_app_state().await;
a_context(&state, "cov-contexts").await;
ok(&state, t::TASK_CONTEXTS_LIST_1_0, json!({})).await;
ok(
&state,
t::TASK_CONTEXTS_GET_1_0,
json!({ "id": "cov-contexts" }),
)
.await;
ok(
&state,
t::TASK_CONTEXTS_UPDATE_1_0,
json!({ "id": "cov-contexts", "name": "renamed" }),
)
.await;
ok(
&state,
t::TASK_CONTEXTS_CREATE_1_0,
json!({ "id": "sub", "name": "sub", "parent": "cov-contexts" }),
)
.await;
let preview = ok(
&state,
t::TASK_CONTEXTS_PREVIEW_DELETE_1_0,
json!({ "id": "cov-contexts" }),
)
.await;
assert_eq!(
preview["subContexts"],
json!(["cov-contexts/sub"]),
"the preview must name the subtree on the wire, camelCase: {preview}"
);
ok(
&state,
t::TASK_CONTEXTS_DELETE_1_0,
json!({ "id": "cov-contexts", "force": true }),
)
.await;
}
#[tokio::test]
async fn a_delete_without_force_answers_not_empty_on_the_wire() {
let (state, _dir) = build_signing_test_app_state().await;
a_context(&state, "cov-notempty").await;
ok(
&state,
t::TASK_CONTEXTS_CREATE_1_0,
json!({ "id": "sub", "name": "sub", "parent": "cov-notempty" }),
)
.await;
let vta_did = state.config.read().await.vta_did.clone().expect("vta_did");
let body = signed_body(
t::TASK_CONTEXTS_DELETE_1_0,
&vta_did,
json!({ "id": "cov-notempty" }),
);
let outcome = super::dispatch_trust_task_core(
&state,
&crate::test_support::super_admin_claims(),
&body,
transport::TransportConfidentiality::HopByHop,
)
.await;
let doc: Value = serde_json::from_slice(&outcome.body).expect("a response document");
assert_eq!(
doc["payload"]["code"], "vta/contexts/delete:notEmpty",
"the specification names this code for this refusal: {doc}"
);
assert_eq!(doc["payload"]["details"]["subContexts"], 1, "{doc}");
let still = ok(
&state,
t::TASK_CONTEXTS_GET_1_0,
json!({ "id": "cov-notempty/sub" }),
)
.await;
assert_eq!(still["id"], "cov-notempty/sub");
}
#[tokio::test]
async fn every_contexts_task_answers_its_own_not_found() {
let (state, _dir) = build_signing_test_app_state().await;
let vta_did = state.config.read().await.vta_did.clone().expect("vta_did");
a_context(&state, "cov-real").await;
let scoped = crate::test_support::admin_claims_for_context("cov-other");
for (uri, slug, payload) in [
(
t::TASK_CONTEXTS_GET_1_0,
"vta/contexts/get",
json!({ "id": "PLACEHOLDER" }),
),
(
t::TASK_CONTEXTS_UPDATE_DID_1_0,
"vta/contexts/update-did",
json!({ "id": "PLACEHOLDER", "did": "did:key:z6MkTest" }),
),
(
t::TASK_CONTEXTS_PREVIEW_DELETE_1_0,
"vta/contexts/preview-delete",
json!({ "id": "PLACEHOLDER" }),
),
(
t::TASK_CONTEXTS_DELETE_1_0,
"vta/contexts/delete",
json!({ "id": "PLACEHOLDER", "force": true }),
),
] {
let mut codes = Vec::new();
for id in ["cov-real", "cov-ghost"] {
let mut p = payload.clone();
p["id"] = json!(id);
let body = signed_body(uri, &vta_did, p);
let outcome = super::dispatch_trust_task_core(
&state,
&scoped,
&body,
transport::TransportConfidentiality::HopByHop,
)
.await;
let doc: Value =
serde_json::from_slice(&outcome.body).expect("a response document");
assert_eq!(
doc["payload"]["code"],
json!(format!("{slug}:notFound")),
"{uri} must answer its own notFound for `{id}`: {doc}"
);
codes.push(doc["payload"]["message"].clone());
}
assert_eq!(
codes[0], codes[1],
"{uri} distinguishes a real id from an absent one, which is the leak"
);
}
}
#[tokio::test]
async fn app_state_lifecycle() {
let (state, _dir) = build_signing_test_app_state().await;
a_context(&state, "cov-appstate").await;
let base = json!({ "contextId": "cov-appstate", "namespace": "cov", "key": "k1" });
let mut put = base.clone();
put["value"] = json!({ "hello": "world" });
ok(&state, t::TASK_VTA_APP_STATE_PUT_1_0, put).await;
ok(&state, t::TASK_VTA_APP_STATE_GET_1_0, base.clone()).await;
ok(
&state,
t::TASK_VTA_APP_STATE_LIST_1_0,
json!({ "contextId": "cov-appstate", "includeValues": true }),
)
.await;
ok(
&state,
t::TASK_VTA_APP_STATE_PUT_MANY_1_0,
json!({
"contextId": "cov-appstate",
"namespace": "cov",
"writes": [{ "key": "k2", "value": {"n": 1} }],
}),
)
.await;
ok(
&state,
t::TASK_VTA_APP_STATE_GET_MANY_1_0,
json!({ "contextId": "cov-appstate", "namespace": "cov", "keys": ["k1", "k2"] }),
)
.await;
ok(&state, t::TASK_VTA_APP_STATE_DELETE_1_0, base).await;
}
#[tokio::test]
async fn memory_lifecycle() {
let (state, _dir) = build_signing_test_app_state().await;
a_context(&state, "cov-memory").await;
ok(
&state,
t::TASK_VTA_MEMORY_PUT_0_1,
json!({ "contextId": "cov-memory", "key": "m1", "value": "remembered" }),
)
.await;
ok(
&state,
t::TASK_VTA_MEMORY_LIST_0_1,
json!({ "contextId": "cov-memory" }),
)
.await;
ok(
&state,
t::TASK_VTA_MEMORY_DELETE_0_1,
json!({ "contextId": "cov-memory", "key": "m1" }),
)
.await;
}
#[tokio::test]
async fn keys_show_and_sign() {
let (state, _dir) = build_signing_test_app_state().await;
let key_id = a_key(&state, "coverage-show-sign").await;
ok(&state, t::TASK_KEYS_SHOW_0_1, json!({ "keyId": key_id })).await;
let payload = base64::engine::general_purpose::URL_SAFE_NO_PAD.encode(b"coverage");
ok(
&state,
t::TASK_KEYS_SIGN_0_1,
json!({ "keyId": key_id, "payload": payload, "algorithm": "EdDSA" }),
)
.await;
}
#[tokio::test]
async fn an_internal_key_is_actually_internal() {
let (state, _dir) = build_signing_test_app_state().await;
let p = ok(
&state,
t::TASK_KEYS_CREATE_0_1,
json!({
"keyType": "ed25519",
"internal": true,
"keyId": "cov-unexportable",
"label": "unexportable",
}),
)
.await;
let record = p.get("key").unwrap_or(&p);
assert_eq!(
record["keyId"], "cov-unexportable",
"the caller's `keyId` must be honoured, not replaced: {p}"
);
assert_eq!(
record["origin"], "internal",
"the key must come back marked internal — a `derived` here is \
exactly the silent downgrade the operator was warned about and \
did not get: {p}"
);
assert_eq!(
record["derivationPath"], "internal",
"the sentinel is the current behaviour; when `derivationPath` \
becomes optional this should assert absence instead: {p}"
);
}
#[tokio::test]
async fn a_create_without_a_derivation_path_succeeds() {
let (state, _dir) = build_signing_test_app_state().await;
ok(
&state,
t::TASK_CONTEXTS_CREATE_1_0,
json!({ "id": "coverage-ctx", "name": "Coverage" }),
)
.await;
ok(
&state,
t::TASK_KEYS_CREATE_0_1,
json!({ "keyType": "ed25519", "contextId": "coverage-ctx" }),
)
.await;
}
#[tokio::test]
async fn keys_rename_then_revoke() {
let (state, _dir) = build_signing_test_app_state().await;
let key_id = a_key(&state, "coverage-rename").await;
let renamed = "coverage-renamed-key".to_string();
ok(
&state,
t::TASK_KEYS_RENAME_0_1,
json!({ "keyId": key_id, "newKeyId": renamed }),
)
.await;
ok(
&state,
t::TASK_KEYS_REVOKE_0_1,
json!({ "keyId": renamed, "reason": "coverage" }),
)
.await;
}
}