use axum::extract::{MatchedPath, Request};
use axum::http::{HeaderMap, HeaderValue};
use axum::middleware::Next;
use axum::response::Response;
use metrics::counter;
use vta_sdk::trust_tasks;
pub fn superseded(route: &'static str, successor: &'static str) -> HeaderMap {
counter!("deprecated_route_requests_total", "route" => route).increment(1);
let mut headers = HeaderMap::new();
headers.insert("deprecation", HeaderValue::from_static("true"));
if let Ok(link) = HeaderValue::from_str(&format!("<{successor}>; rel=\"successor-version\"")) {
headers.insert("link", link);
}
headers
}
pub fn superseded_table() -> &'static [(&'static str, &'static str, &'static str, &'static str)] {
SUPERSEDED
}
const SUPERSEDED: &[(&str, &str, &str, &str)] = &[
("GET", "/acl", "GET /acl", trust_tasks::TASK_ACL_LIST_0_1),
("POST", "/acl", "POST /acl", trust_tasks::TASK_ACL_GRANT_0_1),
(
"DELETE",
"/acl/{did}",
"DELETE /acl/{did}",
trust_tasks::TASK_ACL_REVOKE_0_1,
),
(
"GET",
"/acl/{did}",
"GET /acl/{did}",
trust_tasks::TASK_ACL_SHOW_0_1,
),
(
"PATCH",
"/acl/{did}",
"PATCH /acl/{did}",
trust_tasks::TASK_ACL_UPDATE_0_1,
),
(
"POST",
"/acl/{did}/change-role",
"POST /acl/{did}/change-role",
trust_tasks::TASK_ACL_CHANGE_ROLE_0_1,
),
(
"GET",
"/audit/logs",
"GET /audit/logs",
trust_tasks::TASK_AUDIT_LIST_0_1,
),
(
"GET",
"/audit/retention",
"GET /audit/retention",
trust_tasks::TASK_AUDIT_GET_RETENTION_1_0,
),
(
"PATCH",
"/audit/retention",
"PATCH /audit/retention",
trust_tasks::TASK_AUDIT_UPDATE_RETENTION_1_0,
),
(
"GET",
"/config",
"GET /config",
trust_tasks::TASK_CONFIG_SHOW_0_1,
),
(
"PATCH",
"/config",
"PATCH /config",
trust_tasks::TASK_CONFIG_PATCH_0_1,
),
(
"GET",
"/contexts",
"GET /contexts",
trust_tasks::TASK_CONTEXTS_LIST_1_0,
),
(
"POST",
"/contexts",
"POST /contexts",
trust_tasks::TASK_CONTEXTS_CREATE_1_0,
),
(
"DELETE",
"/contexts/{id}",
"DELETE /contexts/{id}",
trust_tasks::TASK_CONTEXTS_DELETE_1_0,
),
(
"GET",
"/contexts/{id}",
"GET /contexts/{id}",
trust_tasks::TASK_CONTEXTS_GET_1_0,
),
(
"PATCH",
"/contexts/{id}",
"PATCH /contexts/{id}",
trust_tasks::TASK_CONTEXTS_UPDATE_1_0,
),
(
"GET",
"/contexts/{id}/delete-preview",
"GET /contexts/{id}/delete-preview",
trust_tasks::TASK_CONTEXTS_PREVIEW_DELETE_1_0,
),
(
"PUT",
"/contexts/{id}/did",
"PUT /contexts/{id}/did",
trust_tasks::TASK_CONTEXTS_UPDATE_DID_1_0,
),
(
"GET",
"/contexts/{id}/did-templates",
"GET /contexts/{id}/did-templates",
trust_tasks::TASK_DID_TEMPLATES_LIST_2_0,
),
(
"POST",
"/contexts/{id}/did-templates",
"POST /contexts/{id}/did-templates",
trust_tasks::TASK_DID_TEMPLATES_CREATE_2_0,
),
(
"DELETE",
"/contexts/{id}/did-templates/{name}",
"DELETE /contexts/{id}/did-templates/{name}",
trust_tasks::TASK_DID_TEMPLATES_DELETE_2_0,
),
(
"GET",
"/contexts/{id}/did-templates/{name}",
"GET /contexts/{id}/did-templates/{name}",
trust_tasks::TASK_DID_TEMPLATES_GET_2_0,
),
(
"PUT",
"/contexts/{id}/did-templates/{name}",
"PUT /contexts/{id}/did-templates/{name}",
trust_tasks::TASK_DID_TEMPLATES_UPDATE_2_0,
),
(
"POST",
"/contexts/{id}/did-templates/{name}/render",
"POST /contexts/{id}/did-templates/{name}/render",
trust_tasks::TASK_DID_TEMPLATES_RENDER_2_0,
),
(
"GET",
"/did-templates",
"GET /did-templates",
trust_tasks::TASK_DID_TEMPLATES_LIST_2_0,
),
(
"POST",
"/did-templates",
"POST /did-templates",
trust_tasks::TASK_DID_TEMPLATES_CREATE_2_0,
),
(
"DELETE",
"/did-templates/{name}",
"DELETE /did-templates/{name}",
trust_tasks::TASK_DID_TEMPLATES_DELETE_2_0,
),
(
"GET",
"/did-templates/{name}",
"GET /did-templates/{name}",
trust_tasks::TASK_DID_TEMPLATES_GET_2_0,
),
(
"PUT",
"/did-templates/{name}",
"PUT /did-templates/{name}",
trust_tasks::TASK_DID_TEMPLATES_UPDATE_2_0,
),
(
"POST",
"/did-templates/{name}/render",
"POST /did-templates/{name}/render",
trust_tasks::TASK_DID_TEMPLATES_RENDER_2_0,
),
("GET", "/keys", "GET /keys", trust_tasks::TASK_KEYS_LIST_0_1),
(
"POST",
"/keys",
"POST /keys",
trust_tasks::TASK_KEYS_CREATE_0_1,
),
(
"POST",
"/keys/derive-and-sign",
"POST /keys/derive-and-sign",
trust_tasks::TASK_KEYS_DERIVE_AND_SIGN_0_1,
),
(
"POST",
"/keys/derive-and-sign-document",
"POST /keys/derive-and-sign-document",
trust_tasks::TASK_KEYS_DERIVE_AND_SIGN_DOCUMENT_0_1,
),
(
"POST",
"/keys/import",
"POST /keys/import",
trust_tasks::TASK_KEYS_IMPORT_0_1,
),
(
"GET",
"/keys/seeds",
"GET /keys/seeds",
trust_tasks::TASK_SEEDS_LIST_1_0,
),
(
"POST",
"/keys/seeds/rotate",
"POST /keys/seeds/rotate",
trust_tasks::TASK_SEEDS_ROTATE_1_0,
),
(
"DELETE",
"/keys/{key_id}",
"DELETE /keys/{key_id}",
trust_tasks::TASK_KEYS_REVOKE_0_1,
),
(
"GET",
"/keys/{key_id}",
"GET /keys/{key_id}",
trust_tasks::TASK_KEYS_SHOW_0_1,
),
(
"PATCH",
"/keys/{key_id}",
"PATCH /keys/{key_id}",
trust_tasks::TASK_KEYS_RENAME_0_1,
),
(
"GET",
"/keys/{key_id}/secret",
"GET /keys/{key_id}/secret",
trust_tasks::TASK_SEEDS_EXPORT_MNEMONIC_1_0,
),
(
"POST",
"/keys/{key_id}/sign",
"POST /keys/{key_id}/sign",
trust_tasks::TASK_KEYS_SIGN_0_1,
),
(
"POST",
"/vta/restart",
"POST /vta/restart",
trust_tasks::TASK_MANAGEMENT_RELOAD_SERVICES_1_0,
),
(
"GET",
"/webvh/dids",
"GET /webvh/dids",
trust_tasks::TASK_WEBVH_DIDS_LIST_1_0,
),
(
"POST",
"/webvh/dids",
"POST /webvh/dids",
trust_tasks::TASK_WEBVH_DIDS_CREATE_1_0,
),
(
"DELETE",
"/webvh/dids/{did}",
"DELETE /webvh/dids/{did}",
trust_tasks::TASK_WEBVH_DIDS_DELETE_1_0,
),
(
"GET",
"/webvh/dids/{did}",
"GET /webvh/dids/{did}",
trust_tasks::TASK_WEBVH_DIDS_GET_1_0,
),
(
"GET",
"/webvh/dids/{did}/log",
"GET /webvh/dids/{did}/log",
trust_tasks::TASK_WEBVH_DIDS_GET_1_0,
),
(
"POST",
"/webvh/dids/{did}/register-server",
"POST /webvh/dids/{did}/register-server",
trust_tasks::TASK_WEBVH_DIDS_REGISTER_WITH_SERVER_1_0,
),
(
"GET",
"/webvh/servers",
"GET /webvh/servers",
trust_tasks::TASK_WEBVH_SERVERS_LIST_1_0,
),
(
"POST",
"/webvh/servers",
"POST /webvh/servers",
trust_tasks::TASK_WEBVH_SERVERS_REGISTER_1_0,
),
(
"DELETE",
"/webvh/servers/{id}",
"DELETE /webvh/servers/{id}",
trust_tasks::TASK_WEBVH_SERVERS_REMOVE_1_0,
),
(
"PATCH",
"/webvh/servers/{id}",
"PATCH /webvh/servers/{id}",
trust_tasks::TASK_WEBVH_SERVERS_REGISTER_1_0,
),
(
"GET",
"/webvh/servers/{id}/domains",
"GET /webvh/servers/{id}/domains",
trust_tasks::TASK_WEBVH_SERVERS_DOMAINS_0_1,
),
(
"GET",
"/webvh/servers/{id}/reconcile",
"GET /webvh/servers/{id}/reconcile",
trust_tasks::TASK_WEBVH_SERVERS_RECONCILE_0_1,
),
(
"POST",
"/api/trust-tasks",
"POST /api/trust-tasks",
"/trust-tasks",
),
(
"GET",
"/services",
"GET /services",
trust_tasks::TASK_SERVICES_LIST_1_0,
),
(
"GET",
"/services/didcomm",
"GET /services/didcomm",
trust_tasks::TASK_SERVICES_GET_1_0,
),
(
"GET",
"/services/didcomm/drain",
"GET /services/didcomm/drain",
trust_tasks::TASK_SERVICES_DRAIN_LIST_1_0,
),
(
"POST",
"/services/didcomm/disable",
"POST /services/didcomm/disable",
trust_tasks::TASK_SERVICES_DISABLE_1_0,
),
(
"POST",
"/services/didcomm/drain",
"POST /services/didcomm/drain",
trust_tasks::TASK_SERVICES_DRAIN_CANCEL_1_0,
),
(
"POST",
"/services/didcomm/enable",
"POST /services/didcomm/enable",
trust_tasks::TASK_SERVICES_ENABLE_1_0,
),
(
"POST",
"/services/didcomm/rollback",
"POST /services/didcomm/rollback",
trust_tasks::TASK_SERVICES_ROLLBACK_1_0,
),
(
"POST",
"/services/didcomm/update",
"POST /services/didcomm/update",
trust_tasks::TASK_SERVICES_UPDATE_1_0,
),
(
"POST",
"/services/rest/disable",
"POST /services/rest/disable",
trust_tasks::TASK_SERVICES_DISABLE_1_0,
),
(
"POST",
"/services/rest/enable",
"POST /services/rest/enable",
trust_tasks::TASK_SERVICES_ENABLE_1_0,
),
(
"POST",
"/services/rest/rollback",
"POST /services/rest/rollback",
trust_tasks::TASK_SERVICES_ROLLBACK_1_0,
),
(
"POST",
"/services/rest/update",
"POST /services/rest/update",
trust_tasks::TASK_SERVICES_UPDATE_1_0,
),
(
"POST",
"/services/tsp/disable",
"POST /services/tsp/disable",
trust_tasks::TASK_SERVICES_DISABLE_1_0,
),
(
"POST",
"/services/tsp/enable",
"POST /services/tsp/enable",
trust_tasks::TASK_SERVICES_ENABLE_1_0,
),
(
"POST",
"/services/tsp/rollback",
"POST /services/tsp/rollback",
trust_tasks::TASK_SERVICES_ROLLBACK_1_0,
),
(
"POST",
"/services/tsp/update",
"POST /services/tsp/update",
trust_tasks::TASK_SERVICES_UPDATE_1_0,
),
(
"POST",
"/services/webauthn/disable",
"POST /services/webauthn/disable",
trust_tasks::TASK_SERVICES_DISABLE_1_0,
),
(
"POST",
"/services/webauthn/enable",
"POST /services/webauthn/enable",
trust_tasks::TASK_SERVICES_ENABLE_1_0,
),
(
"POST",
"/services/webauthn/rollback",
"POST /services/webauthn/rollback",
trust_tasks::TASK_SERVICES_ROLLBACK_1_0,
),
(
"POST",
"/services/webauthn/update",
"POST /services/webauthn/update",
trust_tasks::TASK_SERVICES_UPDATE_1_0,
),
];
pub async fn mark_superseded(req: Request, next: Next) -> Response {
let hit = req
.extensions()
.get::<MatchedPath>()
.map(MatchedPath::as_str)
.and_then(|path| {
let method = req.method().as_str();
SUPERSEDED
.iter()
.find(|(m, p, _, _)| *m == method && *p == path)
})
.copied();
let mut resp = next.run(req).await;
if let Some((_, _, label, successor)) = hit {
if resp.status().is_success() {
resp.headers_mut().extend(superseded(label, successor));
}
}
resp
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SupersededTask {
pub uri: &'static str,
pub successor: &'static str,
pub reason: &'static str,
}
#[allow(deprecated)] const SUPERSEDED_TASKS: &[SupersededTask] = &[
SupersededTask {
uri: trust_tasks::TASK_AUTH_STEP_UP_APPROVE_RESPONSE_0_1,
successor: trust_tasks::TASK_AUTH_STEP_UP_APPROVE_RESPONSE_0_2,
reason: "0.2 spells the evidence enum `didSigned` in camelCase; the payload is \
signed, so the two versions have separate typed handlers rather than \
an edge transform",
},
SupersededTask {
uri: trust_tasks::TASK_DEVICE_REGISTER_0_1,
successor: trust_tasks::TASK_DEVICE_REGISTER_0_2,
reason: "0.2 spells the enum values in camelCase",
},
SupersededTask {
uri: trust_tasks::TASK_DEVICE_HEARTBEAT_0_1,
successor: trust_tasks::TASK_DEVICE_HEARTBEAT_0_2,
reason: "0.2 spells the enum values in camelCase",
},
SupersededTask {
uri: trust_tasks::TASK_DEVICE_LIST_0_1,
successor: trust_tasks::TASK_DEVICE_LIST_0_2,
reason: "0.2 spells the enum values in camelCase",
},
SupersededTask {
uri: trust_tasks::TASK_DEVICE_SET_WAKE_0_1,
successor: trust_tasks::TASK_DEVICE_SET_WAKE_0_2,
reason: "no enum values changed; the bump is canonical-version alignment, so \
the whole device slice sits on one version",
},
SupersededTask {
uri: trust_tasks::TASK_DEVICE_WIPE_0_1,
successor: trust_tasks::TASK_DEVICE_WIPE_0_2,
reason: "0.2 spells the `scope` enum value `cacheAndKeys` in camelCase",
},
SupersededTask {
uri: trust_tasks::TASK_VAULT_LIST_0_1,
successor: trust_tasks::TASK_VAULT_LIST_0_2,
reason: "0.2 spells secretKind and the related enums in camelCase",
},
SupersededTask {
uri: trust_tasks::TASK_VAULT_LIST_0_2,
successor: trust_tasks::TASK_VAULT_LIST_0_3,
reason: "0.3 replaces AttachmentRef's bare-hex `sha256` with a multibase \
`digestMultibase`, which names its own hash algorithm",
},
SupersededTask {
uri: trust_tasks::TASK_VAULT_GET_0_1,
successor: trust_tasks::TASK_VAULT_GET_0_2,
reason: "0.2 spells the response enums in camelCase",
},
SupersededTask {
uri: trust_tasks::TASK_VAULT_GET_0_2,
successor: trust_tasks::TASK_VAULT_GET_0_3,
reason: "0.3 replaces AttachmentRef's bare-hex `sha256` with a multibase \
`digestMultibase`, which names its own hash algorithm",
},
SupersededTask {
uri: trust_tasks::TASK_VAULT_UPSERT_0_1,
successor: trust_tasks::TASK_VAULT_UPSERT_0_2,
reason: "0.2 spells the secretKind / sealed-envelope / target enums in camelCase",
},
SupersededTask {
uri: trust_tasks::TASK_VAULT_UPSERT_0_2,
successor: trust_tasks::TASK_VAULT_UPSERT_0_3,
reason: "0.3 replaces AttachmentRef's bare-hex `sha256` with a multibase \
`digestMultibase`, which names its own hash algorithm",
},
SupersededTask {
uri: trust_tasks::TASK_VAULT_RELEASE_0_1,
successor: trust_tasks::TASK_VAULT_RELEASE_0_2,
reason: "0.2 spells the secretKind / sealed-envelope / step-up-proof enums in \
camelCase, inside the sealed cleartext as well as around it",
},
SupersededTask {
uri: trust_tasks::TASK_VAULT_PROXY_LOGIN_0_1,
successor: trust_tasks::TASK_VAULT_PROXY_LOGIN_0_2,
reason: "0.2 spells the site-target / step-up-proof enums in camelCase, inside \
the sealed cleartext as well as around it",
},
SupersededTask {
uri: trust_tasks::TASK_VAULT_SIGN_TRUST_TASK_0_1,
successor: trust_tasks::TASK_VAULT_SIGN_TRUST_TASK_0_2,
reason: "0.2 spells the step-up-proof enums in camelCase",
},
];
pub fn superseded_tasks_table() -> &'static [SupersededTask] {
SUPERSEDED_TASKS
}
pub fn superseded_task(type_uri: &str) -> Option<&'static SupersededTask> {
SUPERSEDED_TASKS.iter().find(|t| t.uri == type_uri)
}
pub const DEPRECATION_MEMBER: &str = "org.openvtc.deprecation";
pub fn note_superseded_task(task: &SupersededTask) {
counter!("deprecated_trust_task_requests_total", "task" => task.uri).increment(1);
}
pub fn annotate_superseded(body: &mut Vec<u8>, task: &SupersededTask) {
let Ok(mut doc) = serde_json::from_slice::<serde_json::Value>(body) else {
return;
};
let Some(obj) = doc.as_object_mut() else {
return;
};
if obj.contains_key("proof") || obj.contains_key(DEPRECATION_MEMBER) {
return;
}
obj.insert(
DEPRECATION_MEMBER.to_string(),
serde_json::json!({
"supersededBy": task.successor,
"reason": task.reason,
}),
);
if let Ok(bytes) = serde_json::to_vec(&doc) {
*body = bytes;
}
}
#[cfg(test)]
mod superseded_task_tests {
use super::*;
#[test]
fn a_notice_rides_the_document_top_level_not_the_payload() {
let task = &SUPERSEDED_TASKS[0];
let mut body = serde_json::to_vec(&serde_json::json!({
"id": "urn:uuid:1",
"type": "https://trusttasks.org/spec/device/list/0.1#response",
"issuedAt": chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
"payload": { "devices": [] },
}))
.unwrap();
annotate_superseded(&mut body, task);
let doc: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(doc[DEPRECATION_MEMBER]["supersededBy"], task.successor);
assert_eq!(doc[DEPRECATION_MEMBER]["reason"], task.reason);
assert_eq!(doc["payload"], serde_json::json!({ "devices": [] }));
}
#[test]
fn a_signed_document_is_left_alone() {
let task = &SUPERSEDED_TASKS[0];
let original = serde_json::json!({
"id": "urn:uuid:1",
"type": "https://trusttasks.org/spec/device/list/0.1#response",
"issuedAt": chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
"payload": {},
"proof": { "type": "DataIntegrityProof" },
});
let mut body = serde_json::to_vec(&original).unwrap();
annotate_superseded(&mut body, task);
let doc: serde_json::Value = serde_json::from_slice(&body).unwrap();
assert_eq!(
doc, original,
"a proofed document must be returned untouched"
);
}
#[test]
fn annotating_twice_does_not_nest_or_duplicate() {
let task = &SUPERSEDED_TASKS[0];
let mut body = serde_json::to_vec(&serde_json::json!({
"id": "urn:uuid:1",
"type": "x",
"issuedAt": chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
"payload": {},
}))
.unwrap();
annotate_superseded(&mut body, task);
let once = body.clone();
annotate_superseded(&mut body, task);
assert_eq!(body, once);
}
#[test]
fn a_body_that_is_not_a_document_is_left_alone() {
let task = &SUPERSEDED_TASKS[0];
let mut empty: Vec<u8> = Vec::new();
annotate_superseded(&mut empty, task);
assert!(empty.is_empty());
let mut array = b"[1,2,3]".to_vec();
annotate_superseded(&mut array, task);
assert_eq!(array, b"[1,2,3]");
}
#[test]
fn every_row_names_a_different_successor() {
for t in SUPERSEDED_TASKS {
assert_ne!(
t.uri, t.successor,
"{} is listed as its own successor",
t.uri
);
assert!(!t.reason.is_empty(), "{} has no reason", t.uri);
}
}
#[test]
fn no_uri_is_listed_twice() {
let mut seen: Vec<&str> = SUPERSEDED_TASKS.iter().map(|t| t.uri).collect();
seen.sort_unstable();
let before = seen.len();
seen.dedup();
assert_eq!(before, seen.len(), "a URI is listed more than once");
}
}