use std::collections::BTreeMap;
use serde_json::Value;
use tracing::info;
use crate::error::AppError;
use vta_sdk::did_key::decode_private_key_multibase;
use vta_sdk::did_templates::TemplateVars;
use vta_sdk::provision_integration::DidTemplateRef;
use vta_sdk::sealed_transfer::template_bootstrap::{DidKeyMaterial, KeyPair};
use super::{ProvisionIntegrationDeps, templates};
pub(super) struct MintedAdmin {
pub(super) material: DidKeyMaterial,
}
struct MintedDidKey {
material: DidKeyMaterial,
rendered_document: Value,
}
async fn mint_did_key_from_template(
state: &ProvisionIntegrationDeps,
context: &str,
template: &vta_sdk::did_templates::DidTemplate,
template_ref: &DidTemplateRef,
label: String,
purpose: &str, ) -> Result<MintedDidKey, AppError> {
use crate::keys::derive_and_store_did_key;
use crate::keys::seeds::{get_active_seed_id, load_seed_bytes};
let ctx = crate::contexts::get_context(&state.contexts_ks, context)
.await?
.ok_or_else(|| {
AppError::Internal(format!(
"context '{context}' disappeared between precondition check and did:key mint"
))
})?;
let active_seed_id = get_active_seed_id(&state.keys_ks)
.await
.map_err(|e| AppError::Internal(format!("active seed id: {e}")))?;
let seed = load_seed_bytes(&state.keys_ks, &*state.seed_store, Some(active_seed_id))
.await
.map_err(|e| AppError::Internal(format!("load seed: {e}")))?;
let (minted_did, signing_priv_mb) = derive_and_store_did_key(
&seed,
&ctx.base_path,
context,
&label,
&state.keys_ks,
Some(active_seed_id),
)
.await
.map_err(|e| AppError::Internal(format!("derive did:key: {e}")))?;
let signing_pub_mb = minted_did
.strip_prefix("did:key:")
.ok_or_else(|| {
AppError::Internal("derive_and_store_did_key returned a non-did:key DID".into())
})?
.to_string();
let signing_key_id = format!("{minted_did}#{signing_pub_mb}");
let mut tpl_vars = TemplateVars::new();
tpl_vars.insert_string("DID", &minted_did);
tpl_vars.insert_string("SIGNING_KEY_MB", &signing_pub_mb);
for (k, v) in &template_ref.vars {
tpl_vars.insert(k.clone(), v.clone());
}
let rendered_document = template.render(&tpl_vars).map_err(|e| {
AppError::Validation(format!(
"template '{}' render failed: {e}",
template_ref.name
))
})?;
let signing_seed: [u8; 32] = decode_private_key_multibase(&signing_priv_mb)
.map_err(|e| AppError::Internal(format!("decode signing seed: {e}")))?;
let signing_pub_bytes = affinidi_crypto::did_key::did_key_to_ed25519_pub(&minted_did)
.map_err(|e| AppError::Internal(format!("decode did:key pub: {e}")))?;
let ka_pub_bytes = affinidi_crypto::did_key::ed25519_pub_to_x25519_bytes(&signing_pub_bytes)
.map_err(|e| AppError::Internal(format!("derive X25519 pub: {e}")))?;
let ka_priv_bytes = affinidi_crypto::ed25519::ed25519_private_to_x25519(&signing_seed);
let ka_pub_mb =
crate::keys::encode_public_multibase(&crate::keys::KeyType::X25519, &ka_pub_bytes);
let ka_priv_mb =
crate::keys::encode_private_multibase(&crate::keys::KeyType::X25519, &ka_priv_bytes);
let ka_key_id = format!("{minted_did}#{ka_pub_mb}");
info!(
did = %minted_did,
context = %context,
template = %template_ref.name,
purpose,
"minted did:key via template"
);
Ok(MintedDidKey {
material: DidKeyMaterial {
did: minted_did,
signing_key: KeyPair {
key_id: signing_key_id,
public_key_multibase: signing_pub_mb,
private_key_multibase: signing_priv_mb,
},
ka_key: KeyPair {
key_id: ka_key_id,
public_key_multibase: ka_pub_mb,
private_key_multibase: ka_priv_mb,
},
},
rendered_document,
})
}
pub(super) async fn mint_admin_via_template(
state: &ProvisionIntegrationDeps,
context: &str,
admin_template_ref: &DidTemplateRef,
) -> Result<MintedAdmin, AppError> {
let admin_tpl =
templates::resolve_admin_template(state, context, &admin_template_ref.name).await?;
if admin_tpl.kind != "admin" {
return Err(AppError::Validation(format!(
"template '{}' has kind '{}', not 'admin'. Admin-DID rollover \
requires a template that declares kind=\"admin\" (e.g. the \
built-in 'vta-admin' template).",
admin_template_ref.name, admin_tpl.kind
)));
}
if !admin_tpl.methods.is_empty() && !admin_tpl.methods.iter().any(|m| m == "key") {
return Err(AppError::Validation(format!(
"admin template '{}' targets methods {:?}; phase 1 only \
supports 'key'. Use a did:key admin template (or omit \
`methods` in the template to accept any).",
admin_template_ref.name, admin_tpl.methods
)));
}
let minted = mint_did_key_from_template(
state,
context,
&admin_tpl,
admin_template_ref,
format!("admin DID for context {context} (provision-integration)"),
"admin",
)
.await?;
Ok(MintedAdmin {
material: minted.material,
})
}
pub(super) async fn mint_integration_via_did_key_template(
state: &ProvisionIntegrationDeps,
context: &str,
client_did: &str,
template_name: &str,
template_vars: &BTreeMap<String, Value>,
) -> Result<
(
String,
String,
String,
Value,
Option<String>,
DidKeyMaterial,
),
AppError,
> {
let template = templates::resolve_template_by_name(state, context, template_name)
.await
.map_err(|e| match e {
AppError::NotFound(_) => AppError::Validation(format!(
"integration template '{template_name}' is not registered on this VTA. \
Register it via 'pnm did-templates create {template_name} --file <path>' \
then retry."
)),
other => other,
})?;
let template_ref = DidTemplateRef {
name: template_name.to_string(),
vars: template_vars.clone(),
};
let label = format!(
"integration DID for context {context} (provision-integration, did:key, holder {client_did})"
);
let minted = mint_did_key_from_template(
state,
context,
&template,
&template_ref,
label,
"integration",
)
.await?;
Ok((
minted.material.did.clone(),
minted.material.signing_key.key_id.clone(),
minted.material.ka_key.key_id.clone(),
minted.rendered_document,
None, minted.material,
))
}