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(test)]
mod produced_census;
#[cfg(feature = "webvh")]
mod provision_integration;
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)]
#[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/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
}
mod lifecycle_mapping {}
fn freshness_policy() -> trust_tasks_rs::FreshnessPolicy {
trust_tasks_rs::FreshnessPolicy::default().with_max_age(chrono::TimeDelta::minutes(10))
}
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 = freshness_policy().record_expiry(&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"
);
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;
}
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::di_proof::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");
}
}
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_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_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_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_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": 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,
"issuedAt": chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
"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_without_issued_at_is_not_refused_here() {
assert!(
doc(None, None)
.validate_freshness(Utc::now(), &freshness_policy())
.is_ok()
);
}
}
#[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 (a, b): (Value, Value) = (
serde_json::from_slice(&first.body).expect("first body"),
serde_json::from_slice(&second.body).expect("second body"),
);
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_PREVIEW_DELETE_1_0,
json!({ "id": "cov-contexts" }),
)
.await;
ok(
&state,
t::TASK_CONTEXTS_DELETE_1_0,
json!({ "id": "cov-contexts" }),
)
.await;
}
#[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;
}
}