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;
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(crate) mod planner;
mod policy;
mod policy_gate;
#[cfg(feature = "webvh")]
mod provision_integration;
mod replay;
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;
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)] 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_2,
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/seeds/export-mnemonic/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/management/reload-services/1.0",
"https://trusttasks.org/spec/vta/backup/initiate-export/1.0",
"https://trusttasks.org/spec/vta/backup/complete-export/1.0",
"https://trusttasks.org/spec/vta/backup/initiate-import/1.0",
"https://trusttasks.org/spec/vta/backup/finalize-import/1.0",
"https://trusttasks.org/spec/vta/backup/abort/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",
"https://trusttasks.org/spec/vault/credentials/receive/0.1",
"https://trusttasks.org/spec/vault/credentials/query/0.1",
"https://trusttasks.org/spec/vault/credentials/get/0.1",
"https://trusttasks.org/spec/vault/credentials/archive/0.1",
"https://trusttasks.org/spec/vault/credentials/unarchive/0.1",
"https://trusttasks.org/spec/vault/credentials/delete/0.1",
"https://trusttasks.org/spec/vault/credentials/restore/0.1",
"https://trusttasks.org/spec/vault/credentials/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)|+ => $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}"),
},
))
}
}
}
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;
#[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()),
};
let superseded = crate::deprecation::superseded_task(&doc.type_uri.to_string());
let mut outcome = Box::pin(dispatch_trust_task_validated(state, auth, doc)).await;
if let Some(task) = superseded {
crate::deprecation::note_superseded_task(task);
crate::deprecation::annotate_superseded(&mut outcome.body, task);
}
outcome
}
const MAX_ISSUED_AT_SKEW: chrono::TimeDelta = chrono::TimeDelta::seconds(60);
fn check_freshness_bounds(
doc: &TrustTask<Value>,
now: chrono::DateTime<chrono::Utc>,
) -> Result<(), RejectReason> {
let Some(issued_at) = doc.issued_at else {
return Ok(());
};
if issued_at > now + MAX_ISSUED_AT_SKEW {
return Err(RejectReason::MalformedRequest {
reason: format!(
"issuedAt {} is more than {}s ahead of this consumer's clock",
issued_at.to_rfc3339(),
MAX_ISSUED_AT_SKEW.num_seconds(),
),
});
}
if let Some(expires_at) = doc.expires_at
&& expires_at <= issued_at
{
return Err(RejectReason::MalformedRequest {
reason: format!(
"expiresAt {} is at or before issuedAt {}, so the document was \
valid at no instant",
expires_at.to_rfc3339(),
issued_at.to_rfc3339(),
),
});
}
Ok(())
}
async fn dispatch_trust_task_validated(
state: &AppState,
auth: &AuthClaims,
doc: TrustTask<Value>,
) -> TrustTaskOutcome {
{
let now = chrono::Utc::now();
if let Err(reason) = check_freshness_bounds(&doc, now) {
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);
}
}
if !replay::check_and_record(&auth.did, &doc.id) {
return reject_with(
&doc,
RejectReason::TaskFailed {
reason: "duplicate".to_string(),
details: Some(serde_json::json!({
"id": doc.id,
"reason": "this request id was already submitted; the prior submission is \
authoritative — do not retry with the same id",
})),
},
);
}
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"
);
let vault_audit = vault_audit_action(&type_uri).map(|action| {
let resource = vault_audit_resource(&doc.payload);
let context_id = doc
.payload
.get("contextId")
.and_then(Value::as_str)
.map(str::to_string);
let detail = doc
.payload
.get("reason")
.and_then(Value::as_str)
.map(str::to_string);
(action, resource, context_id, detail)
});
if let Some(reject) = validate_payload(state, &type_uri, &doc).await {
return reject;
}
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)
} 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;
}
if let Some((action, resource, context_id, detail)) = vault_audit {
let label = vault_audit_outcome_label(&outcome);
if let Err(e) = crate::audit::record_with_detail(
&state.audit_sink,
&action,
&auth.did,
resource.as_deref(),
&label,
Some(helpers::TRANSPORT_TRUST_TASK),
context_id.as_deref(),
detail.as_deref(),
)
.await
{
tracing::warn!(error = %e, action = %action, "vault audit record failed");
}
}
outcome
}
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
=> 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_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_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_SEEDS_EXPORT_MNEMONIC_1_0 => seeds::handle_export_mnemonic
[ None Secret false ],
vta_sdk::trust_tasks::TASK_AUDIT_LIST_0_1 => audit::handle_list_logs
[ 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_1 => 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_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_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_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_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 ],
#[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_2
=> 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_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::*;
#[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 seed = class_for(vta_sdk::trust_tasks::TASK_SEEDS_EXPORT_MNEMONIC_1_0)
.expect("seed export is classified");
assert_eq!(
seed.exposure.discloses,
Discloses::Secret,
"exporting the mnemonic discloses a secret"
);
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": "2026-05-20T00:00:00Z",
"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": "2026-05-20T00:00:00Z",
"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 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);
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_0_1_form_superseded() {
for spec in wire_v0_2::WIRE_SPECS_V0_2 {
let row = crate::deprecation::superseded_task(spec.uri_0_1).unwrap_or_else(|| {
panic!(
"`{}` is dual-accepted at `{}` but its 0.1 form is not in \
`deprecation::SUPERSEDED_TASKS`, so nothing counts the callers \
still on it and it can never be retired on evidence",
spec.uri_0_1, spec.uri_0_2
)
});
assert_eq!(
row.successor, spec.uri_0_2,
"`{}` is down-converted from `{}` but its deprecation row points \
somewhere else",
spec.uri_0_1, spec.uri_0_2
);
}
}
#[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 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": "2026-07-14T00:00:00Z",
"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": "2026-08-22T00:00:00Z",
"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!(
check_freshness_bounds(&doc(Some(&soon), None), now).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 = check_freshness_bounds(&doc(Some(&far), None), now)
.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 = check_freshness_bounds(&doc(Some(&issued), Some(&expiry.to_rfc3339())), now)
.expect_err("a validity interval containing no instant is malformed");
assert!(
matches!(err, RejectReason::MalformedRequest { .. }),
"got {err:?}"
);
}
}
#[test]
fn a_document_without_issued_at_is_not_refused_here() {
assert!(check_freshness_bounds(&doc(None, None), Utc::now()).is_ok());
}
}
#[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;
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 = serde_json::to_vec(&json!({
"id": format!("urn:uuid:{}", uuid::Uuid::new_v4()),
"type": uri,
"issuer": "did:key:zTestAdmin",
"recipient": vta_did,
"issuedAt": "2026-08-26T00:00:00Z",
"payload": payload,
}))
.expect("envelope serialises");
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 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()
}
#[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 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;
}
}