use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use zeroize::Zeroizing;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct AdminRotationPayload {
pub authorization: serde_json::Value,
pub admin: DidKeyMaterial,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub vta_url: Option<String>,
pub vta_trust: VtaTrustBundle,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TemplateBootstrapPayload {
pub authorization: serde_json::Value,
pub secrets: BTreeMap<String, DidKeyMaterial>,
pub config: TemplateBootstrapConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct DidKeyMaterial {
pub did: String,
pub signing_key: KeyPair,
pub ka_key: KeyPair,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct KeyPair {
pub key_id: String,
pub public_key_multibase: String,
pub private_key_multibase: String,
}
impl KeyPair {
pub fn private_zeroizing(&self) -> Zeroizing<String> {
Zeroizing::new(self.private_key_multibase.clone())
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct TemplateBootstrapConfig {
pub template_name: String,
pub template_kind: String,
pub did_document: serde_json::Value,
pub outputs: Vec<TemplateOutput>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub vta_url: Option<String>,
pub vta_trust: VtaTrustBundle,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum TemplateOutput {
WebvhLog {
did: String,
log: String,
},
DidCommService {
did: String,
url: String,
accept: Vec<String>,
routing_keys: Vec<String>,
},
Generic {
kind: String,
payload: serde_json::Value,
},
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct VtaTrustBundle {
pub vta_did: String,
pub vta_did_document: serde_json::Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub vta_did_log: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sealed_transfer::SealedPayloadV1;
use serde_json::json;
fn sample_payload() -> TemplateBootstrapPayload {
TemplateBootstrapPayload {
authorization: json!({ "type": ["VerifiableCredential", "VtaAuthorizationCredential"] }),
secrets: BTreeMap::from([(
"did:webvh:mediator.example.com".to_string(),
DidKeyMaterial {
did: "did:webvh:mediator.example.com".into(),
signing_key: KeyPair {
key_id: "did:webvh:mediator.example.com#key-0".into(),
public_key_multibase: "z6Mk...".into(),
private_key_multibase: "z...".into(),
},
ka_key: KeyPair {
key_id: "did:webvh:mediator.example.com#key-1".into(),
public_key_multibase: "z6LS...".into(),
private_key_multibase: "z...".into(),
},
},
)]),
config: TemplateBootstrapConfig {
template_name: "didcomm-mediator".into(),
template_kind: "mediator".into(),
did_document: json!({ "id": "did:webvh:mediator.example.com" }),
outputs: vec![TemplateOutput::WebvhLog {
did: "did:webvh:mediator.example.com".into(),
log: "{...}".into(),
}],
vta_url: Some("https://vta.example.com".into()),
vta_trust: VtaTrustBundle {
vta_did: "did:webvh:vta.example.com".into(),
vta_did_document: json!({ "id": "did:webvh:vta.example.com" }),
vta_did_log: Some("{...}".into()),
},
},
}
}
#[test]
fn template_bootstrap_payload_json_round_trip() {
let payload = sample_payload();
let json = serde_json::to_string(&payload).unwrap();
let parsed: TemplateBootstrapPayload = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.config.template_name, "didcomm-mediator");
assert_eq!(parsed.secrets.len(), 1);
}
#[test]
fn sealed_payload_variant_round_trip() {
let payload = SealedPayloadV1::TemplateBootstrap(Box::new(sample_payload()));
let json = serde_json::to_string(&payload).unwrap();
let parsed: SealedPayloadV1 = serde_json::from_str(&json).unwrap();
match parsed {
SealedPayloadV1::TemplateBootstrap(p) => {
assert_eq!(p.config.template_kind, "mediator");
assert_eq!(p.config.outputs.len(), 1);
}
other => panic!("expected TemplateBootstrap, got {other:?}"),
}
}
#[test]
fn template_output_webvh_log_tag_on_wire() {
let out = TemplateOutput::WebvhLog {
did: "did:webvh:x".into(),
log: "line".into(),
};
let v = serde_json::to_value(&out).unwrap();
assert_eq!(v["type"], "webvh_log");
}
#[test]
fn template_output_generic_round_trips_arbitrary_payload() {
let original = TemplateOutput::Generic {
kind: "status-list".into(),
payload: json!({
"statusListUrl": "https://issuer.example.com/status",
"expires": "2026-12-31T00:00:00Z",
}),
};
let wire = serde_json::to_value(&original).unwrap();
assert_eq!(wire["type"], "generic");
assert_eq!(wire["kind"], "status-list");
assert_eq!(
wire["payload"]["statusListUrl"],
"https://issuer.example.com/status"
);
let parsed: TemplateOutput = serde_json::from_value(wire).unwrap();
match parsed {
TemplateOutput::Generic { kind, payload } => {
assert_eq!(kind, "status-list");
assert_eq!(payload["expires"], "2026-12-31T00:00:00Z");
}
other => panic!("expected Generic, got {other:?}"),
}
}
fn sample_admin_rotation_payload() -> AdminRotationPayload {
AdminRotationPayload {
authorization: json!({ "type": ["VerifiableCredential", "VtaAuthorizationCredential"] }),
admin: DidKeyMaterial {
did: "did:key:z6MkAdmin".into(),
signing_key: KeyPair {
key_id: "did:key:z6MkAdmin#z6MkAdmin".into(),
public_key_multibase: "z6MkAdminSigning".into(),
private_key_multibase: "zAdminSigningPriv".into(),
},
ka_key: KeyPair {
key_id: "did:key:z6MkAdmin#z6LSAdmin".into(),
public_key_multibase: "z6LSAdminKa".into(),
private_key_multibase: "zAdminKaPriv".into(),
},
},
vta_url: Some("https://vta.example.com".into()),
vta_trust: VtaTrustBundle {
vta_did: "did:webvh:vta.example.com".into(),
vta_did_document: json!({ "id": "did:webvh:vta.example.com" }),
vta_did_log: None,
},
}
}
#[test]
fn admin_rotation_payload_json_round_trip() {
let payload = sample_admin_rotation_payload();
let json = serde_json::to_string(&payload).unwrap();
let parsed: AdminRotationPayload = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.admin.did, "did:key:z6MkAdmin");
assert_eq!(parsed.vta_url.as_deref(), Some("https://vta.example.com"));
}
#[test]
fn admin_rotation_sealed_payload_variant_round_trip() {
let payload = SealedPayloadV1::AdminRotation(Box::new(sample_admin_rotation_payload()));
let json = serde_json::to_string(&payload).unwrap();
assert!(
json.contains("\"admin_rotation\""),
"wire tag must be snake_case `admin_rotation`, got: {json}"
);
let parsed: SealedPayloadV1 = serde_json::from_str(&json).unwrap();
match parsed {
SealedPayloadV1::AdminRotation(p) => {
assert_eq!(p.admin.did, "did:key:z6MkAdmin");
}
other => panic!("expected AdminRotation, got {other:?}"),
}
}
#[test]
fn template_output_generic_coexists_with_typed_variants() {
let outputs = vec![
TemplateOutput::WebvhLog {
did: "did:webvh:mediator.example".into(),
log: "one\ntwo\n".into(),
},
TemplateOutput::Generic {
kind: "oob-invitation".into(),
payload: json!({"url": "https://mediator.example/oob?c=abc"}),
},
];
let wire = serde_json::to_string(&outputs).unwrap();
let parsed: Vec<TemplateOutput> = serde_json::from_str(&wire).unwrap();
assert_eq!(parsed.len(), 2);
assert!(matches!(parsed[0], TemplateOutput::WebvhLog { .. }));
match &parsed[1] {
TemplateOutput::Generic { kind, .. } => assert_eq!(kind, "oob-invitation"),
other => panic!("expected Generic at index 1, got {other:?}"),
}
}
}