use std::collections::BTreeMap;
use chrono::Duration;
use serde_json::Value;
use crate::provision_integration::ProvisionRequestBuilder;
pub const BUILTIN_MEDIATOR_TEMPLATE: &str = "didcomm-mediator";
pub const BUILTIN_VTA_ADMIN_TEMPLATE: &str = "vta-admin";
pub const BUILTIN_DID_HOSTING_CONTROL_TEMPLATE: &str = "did-hosting-control";
pub const BUILTIN_DID_HOSTING_DAEMON_TEMPLATE: &str = "did-hosting-daemon";
pub const BUILTIN_DID_HOSTING_SERVER_TEMPLATE: &str = "did-hosting-server";
#[deprecated(
since = "0.8.0",
note = "renamed to BUILTIN_DID_HOSTING_CONTROL_TEMPLATE"
)]
pub const BUILTIN_WEBVH_CONTROL_TEMPLATE: &str = "did-hosting-control";
#[deprecated(
since = "0.8.0",
note = "renamed to BUILTIN_DID_HOSTING_DAEMON_TEMPLATE"
)]
pub const BUILTIN_WEBVH_DAEMON_TEMPLATE: &str = "did-hosting-daemon";
#[deprecated(
since = "0.8.0",
note = "renamed to BUILTIN_DID_HOSTING_SERVER_TEMPLATE"
)]
pub const BUILTIN_WEBVH_SERVER_TEMPLATE: &str = "did-hosting-server";
pub const DEFAULT_VALIDITY: Duration = Duration::minutes(15);
#[derive(Debug, Clone)]
pub struct ProvisionAsk {
pub context: String,
pub integration_template: Option<String>,
pub integration_template_vars: BTreeMap<String, Value>,
pub admin_template: Option<String>,
pub admin_template_vars: BTreeMap<String, Value>,
pub label: Option<String>,
pub validity: Duration,
}
impl ProvisionAsk {
pub fn for_template(
name: impl Into<String>,
vars: BTreeMap<String, Value>,
context: impl Into<String>,
) -> Self {
Self {
context: context.into(),
integration_template: Some(name.into()),
integration_template_vars: vars,
admin_template: Some(BUILTIN_VTA_ADMIN_TEMPLATE.to_string()),
admin_template_vars: BTreeMap::new(),
label: None,
validity: DEFAULT_VALIDITY,
}
}
pub fn didcomm_mediator(context: impl Into<String>, mediator_url: impl Into<String>) -> Self {
let mut vars = BTreeMap::new();
vars.insert("URL".to_string(), Value::String(mediator_url.into()));
Self::for_template(BUILTIN_MEDIATOR_TEMPLATE, vars, context)
}
pub fn did_hosting_control(
context: impl Into<String>,
host_url: impl Into<String>,
mediator_did: impl Into<String>,
) -> Self {
let mut vars = BTreeMap::new();
vars.insert("URL".to_string(), Value::String(host_url.into()));
vars.insert(
"MEDIATOR_DID".to_string(),
Value::String(mediator_did.into()),
);
Self::for_template(BUILTIN_DID_HOSTING_CONTROL_TEMPLATE, vars, context)
}
pub fn did_hosting_daemon(context: impl Into<String>, host_url: impl Into<String>) -> Self {
let mut vars = BTreeMap::new();
vars.insert("URL".to_string(), Value::String(host_url.into()));
Self::for_template(BUILTIN_DID_HOSTING_DAEMON_TEMPLATE, vars, context)
}
pub fn did_hosting_server(context: impl Into<String>, mediator_did: impl Into<String>) -> Self {
let mut vars = BTreeMap::new();
vars.insert(
"MEDIATOR_DID".to_string(),
Value::String(mediator_did.into()),
);
Self::for_template(BUILTIN_DID_HOSTING_SERVER_TEMPLATE, vars, context)
}
#[deprecated(since = "0.8.0", note = "renamed to did_hosting_control")]
pub fn webvh_control(
context: impl Into<String>,
host_url: impl Into<String>,
mediator_did: impl Into<String>,
) -> Self {
Self::did_hosting_control(context, host_url, mediator_did)
}
#[deprecated(since = "0.8.0", note = "renamed to did_hosting_daemon")]
pub fn webvh_daemon(context: impl Into<String>, host_url: impl Into<String>) -> Self {
Self::did_hosting_daemon(context, host_url)
}
#[deprecated(since = "0.8.0", note = "renamed to did_hosting_server")]
pub fn webvh_server(context: impl Into<String>, mediator_did: impl Into<String>) -> Self {
Self::did_hosting_server(context, mediator_did)
}
pub fn vta_admin(context: impl Into<String>) -> Self {
let mut ask = Self::for_template(BUILTIN_VTA_ADMIN_TEMPLATE, BTreeMap::new(), context);
ask.admin_template = None;
ask
}
pub fn vta_admin_rotated(context: impl Into<String>) -> Self {
Self {
context: context.into(),
integration_template: None,
integration_template_vars: BTreeMap::new(),
admin_template: Some(BUILTIN_VTA_ADMIN_TEMPLATE.to_string()),
admin_template_vars: BTreeMap::new(),
label: None,
validity: DEFAULT_VALIDITY,
}
}
pub fn with_label(mut self, label: impl Into<String>) -> Self {
self.label = Some(label.into());
self
}
pub fn with_validity(mut self, d: Duration) -> Self {
self.validity = d;
self
}
#[cfg(test)]
pub fn without_admin_rollover(mut self) -> Self {
self.admin_template = None;
self.admin_template_vars.clear();
self
}
pub(crate) fn to_builder(&self) -> ProvisionRequestBuilder {
let mut builder = match &self.integration_template {
Some(name) => ProvisionRequestBuilder::new(name.clone())
.vars(self.integration_template_vars.clone()),
None => ProvisionRequestBuilder::for_admin_rotation(
self.admin_template
.clone()
.expect("ProvisionAsk with integration_template=None must set admin_template"),
),
};
builder = builder
.context_hint(self.context.clone())
.validity(self.validity);
if self.integration_template.is_some()
&& let Some(ref name) = self.admin_template
{
builder = builder.admin_template(name.clone());
}
for (k, v) in &self.admin_template_vars {
builder = builder.admin_template_var(k.clone(), v.clone());
}
if let Some(ref label) = self.label {
builder = builder.label(label.clone()).note(label.clone());
}
builder
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::did_templates::load_embedded;
use crate::provision_integration::BootstrapAsk;
#[test]
fn builtin_template_constants_resolve_in_registry() {
for name in [
BUILTIN_MEDIATOR_TEMPLATE,
BUILTIN_VTA_ADMIN_TEMPLATE,
BUILTIN_DID_HOSTING_CONTROL_TEMPLATE,
BUILTIN_DID_HOSTING_DAEMON_TEMPLATE,
BUILTIN_DID_HOSTING_SERVER_TEMPLATE,
] {
load_embedded(name).unwrap_or_else(|e| {
panic!("built-in template {name} not in vta-sdk registry: {e}")
});
}
}
#[test]
fn didcomm_mediator_sets_url_var_and_template() {
let ask = ProvisionAsk::didcomm_mediator("ctx", "https://m.example.com");
assert_eq!(ask.context, "ctx");
assert_eq!(
ask.integration_template.as_deref(),
Some(BUILTIN_MEDIATOR_TEMPLATE)
);
assert_eq!(
ask.integration_template_vars["URL"],
Value::String("https://m.example.com".into())
);
assert_eq!(
ask.admin_template.as_deref(),
Some(BUILTIN_VTA_ADMIN_TEMPLATE)
);
assert_eq!(ask.validity, DEFAULT_VALIDITY);
}
#[test]
fn did_hosting_server_sets_mediator_did_var() {
let ask = ProvisionAsk::did_hosting_server("ctx", "did:webvh:m.example.com");
assert_eq!(
ask.integration_template.as_deref(),
Some(BUILTIN_DID_HOSTING_SERVER_TEMPLATE)
);
assert_eq!(
ask.integration_template_vars["MEDIATOR_DID"],
Value::String("did:webvh:m.example.com".into())
);
}
#[test]
fn did_hosting_daemon_sets_url_var() {
let ask = ProvisionAsk::did_hosting_daemon("ctx", "https://h.example.com");
assert_eq!(
ask.integration_template.as_deref(),
Some(BUILTIN_DID_HOSTING_DAEMON_TEMPLATE)
);
assert_eq!(
ask.integration_template_vars["URL"],
Value::String("https://h.example.com".into())
);
}
#[test]
fn did_hosting_control_sets_url_and_mediator_did_vars() {
let ask = ProvisionAsk::did_hosting_control(
"ctx",
"https://h.example.com",
"did:webvh:m.example.com",
);
assert_eq!(
ask.integration_template.as_deref(),
Some(BUILTIN_DID_HOSTING_CONTROL_TEMPLATE)
);
assert_eq!(
ask.integration_template_vars["URL"],
Value::String("https://h.example.com".into())
);
assert_eq!(
ask.integration_template_vars["MEDIATOR_DID"],
Value::String("did:webvh:m.example.com".into())
);
}
#[test]
#[allow(deprecated)]
fn deprecated_webvh_aliases_match_did_hosting_canonical() {
let ctx = "ctx";
let host = "https://h.example.com";
let med = "did:webvh:m.example.com";
assert_eq!(
ProvisionAsk::webvh_control(ctx, host, med).integration_template,
ProvisionAsk::did_hosting_control(ctx, host, med).integration_template,
);
assert_eq!(
ProvisionAsk::webvh_daemon(ctx, host).integration_template,
ProvisionAsk::did_hosting_daemon(ctx, host).integration_template,
);
assert_eq!(
ProvisionAsk::webvh_server(ctx, med).integration_template,
ProvisionAsk::did_hosting_server(ctx, med).integration_template,
);
}
#[test]
fn vta_admin_disables_rollover() {
let ask = ProvisionAsk::vta_admin("ctx");
assert_eq!(
ask.integration_template.as_deref(),
Some(BUILTIN_VTA_ADMIN_TEMPLATE)
);
assert!(ask.integration_template_vars.is_empty());
assert!(ask.admin_template.is_none());
}
#[test]
fn for_template_defaults_to_admin_rollover_via_vta_admin() {
let ask = ProvisionAsk::for_template("custom-template", BTreeMap::new(), "ctx");
assert_eq!(ask.integration_template.as_deref(), Some("custom-template"));
assert_eq!(
ask.admin_template.as_deref(),
Some(BUILTIN_VTA_ADMIN_TEMPLATE)
);
}
#[test]
fn without_admin_rollover_clears_admin_fields() {
let ask = ProvisionAsk::didcomm_mediator("ctx", "https://m").without_admin_rollover();
assert!(ask.admin_template.is_none());
assert!(ask.admin_template_vars.is_empty());
}
#[test]
fn vta_admin_rotated_yields_admin_only_ask_shape() {
let ask = ProvisionAsk::vta_admin_rotated("ctx");
assert!(ask.integration_template.is_none());
assert!(ask.integration_template_vars.is_empty());
assert_eq!(
ask.admin_template.as_deref(),
Some(BUILTIN_VTA_ADMIN_TEMPLATE)
);
}
#[tokio::test]
async fn vta_admin_rotated_produces_admin_rotation_wire_ask() {
let ask = ProvisionAsk::vta_admin_rotated("ctx").with_label("openvtc-cli2");
let (seed, pub_bytes) = crate::sealed_transfer::generate_ed25519_keypair();
let client_did = affinidi_crypto::did_key::ed25519_pub_to_did_key(&pub_bytes);
let vp = ask
.to_builder()
.sign_with(&seed, &client_did)
.await
.expect("sign vta_admin_rotated");
match &vp.ask {
BootstrapAsk::AdminRotation(inner) => {
assert_eq!(inner.admin_template.name, BUILTIN_VTA_ADMIN_TEMPLATE);
assert_eq!(inner.context_hint.as_deref(), Some("ctx"));
}
other => panic!("expected AdminRotation, got {other:?}"),
}
assert_eq!(vp.label.as_deref(), Some("openvtc-cli2"));
}
#[tokio::test]
async fn curated_didcomm_mediator_equivalent_to_for_template() {
let curated = ProvisionAsk::didcomm_mediator("ctx", "https://m.example.com");
let mut vars = BTreeMap::new();
vars.insert(
"URL".to_string(),
Value::String("https://m.example.com".into()),
);
let generic = ProvisionAsk::for_template(BUILTIN_MEDIATOR_TEMPLATE, vars, "ctx");
assert_signed_vps_have_equivalent_ask(&curated, &generic).await;
}
#[tokio::test]
async fn curated_did_hosting_server_equivalent_to_for_template() {
let curated = ProvisionAsk::did_hosting_server("ctx", "did:webvh:m.example.com");
let mut vars = BTreeMap::new();
vars.insert(
"MEDIATOR_DID".to_string(),
Value::String("did:webvh:m.example.com".into()),
);
let generic = ProvisionAsk::for_template(BUILTIN_DID_HOSTING_SERVER_TEMPLATE, vars, "ctx");
assert_signed_vps_have_equivalent_ask(&curated, &generic).await;
}
#[tokio::test]
async fn curated_did_hosting_daemon_equivalent_to_for_template() {
let curated = ProvisionAsk::did_hosting_daemon("ctx", "https://h.example.com");
let mut vars = BTreeMap::new();
vars.insert(
"URL".to_string(),
Value::String("https://h.example.com".into()),
);
let generic = ProvisionAsk::for_template(BUILTIN_DID_HOSTING_DAEMON_TEMPLATE, vars, "ctx");
assert_signed_vps_have_equivalent_ask(&curated, &generic).await;
}
#[tokio::test]
async fn curated_did_hosting_control_equivalent_to_for_template() {
let curated = ProvisionAsk::did_hosting_control(
"ctx",
"https://h.example.com",
"did:webvh:m.example.com",
);
let mut vars = BTreeMap::new();
vars.insert(
"URL".to_string(),
Value::String("https://h.example.com".into()),
);
vars.insert(
"MEDIATOR_DID".to_string(),
Value::String("did:webvh:m.example.com".into()),
);
let generic = ProvisionAsk::for_template(BUILTIN_DID_HOSTING_CONTROL_TEMPLATE, vars, "ctx");
assert_signed_vps_have_equivalent_ask(&curated, &generic).await;
}
#[tokio::test]
async fn curated_vta_admin_equivalent_to_for_template_without_rollover() {
let curated = ProvisionAsk::vta_admin("ctx");
let mut generic =
ProvisionAsk::for_template(BUILTIN_VTA_ADMIN_TEMPLATE, BTreeMap::new(), "ctx");
generic.admin_template = None;
assert_signed_vps_have_equivalent_ask(&curated, &generic).await;
}
async fn assert_signed_vps_have_equivalent_ask(a: &ProvisionAsk, b: &ProvisionAsk) {
let (seed, pub_bytes) = crate::sealed_transfer::generate_ed25519_keypair();
let client_did = affinidi_crypto::did_key::ed25519_pub_to_did_key(&pub_bytes);
let vp_a = a
.to_builder()
.sign_with(&seed, &client_did)
.await
.expect("sign curated");
let vp_b = b
.to_builder()
.sign_with(&seed, &client_did)
.await
.expect("sign generic");
let BootstrapAsk::TemplateBootstrap(inner_a) = &vp_a.ask else {
panic!("expected TemplateBootstrap ask")
};
let BootstrapAsk::TemplateBootstrap(inner_b) = &vp_b.ask else {
panic!("expected TemplateBootstrap ask")
};
assert_eq!(inner_a.template.name, inner_b.template.name);
assert_eq!(inner_a.template.vars, inner_b.template.vars);
assert_eq!(inner_a.context_hint, inner_b.context_hint);
assert_eq!(
inner_a.admin_template.as_ref().map(|t| &t.name),
inner_b.admin_template.as_ref().map(|t| &t.name)
);
assert_eq!(
inner_a.admin_template.as_ref().map(|t| &t.vars),
inner_b.admin_template.as_ref().map(|t| &t.vars)
);
}
}