use std::path::PathBuf;
use std::sync::Arc;
use dialoguer::{Confirm, Input, Select};
use didwebvh_rs::url::WebVHURL;
use serde_json::json;
use url::Url;
use affinidi_did_resolver_cache_sdk::{DIDCacheClient, config::DIDCacheConfigBuilder};
use vta_sdk::did_secrets::{DidSecretsBundle, SecretEntry};
use vta_sdk::protocols::did_management::create::WebvhPathMode;
use crate::acl::{AclEntry, Role, store_acl_entry};
use crate::config::AppConfig;
use crate::keys::seed_store::create_seed_store;
use crate::operations;
use crate::operations::did_webvh::CreateDidWebvhParams;
use crate::setup;
use crate::store::Store;
use crate::webvh_cli::cli_super_admin;
pub struct CreateDidWebvhArgs {
pub config_path: Option<PathBuf>,
pub context: String,
pub label: Option<String>,
pub url: Option<String>,
pub export_secrets: bool,
pub admin: bool,
pub did_log_file: Option<PathBuf>,
}
pub async fn run_create_did_webvh(
args: CreateDidWebvhArgs,
) -> Result<(), Box<dyn std::error::Error>> {
let config = AppConfig::load(args.config_path)?;
let store = Store::open(&config.store)?;
let keys_ks = store.keyspace(crate::keyspaces::KEYS)?;
let imported_ks = store.keyspace(crate::keyspaces::IMPORTED_SECRETS)?;
let contexts_ks = store.keyspace(crate::keyspaces::CONTEXTS)?;
let webvh_ks = store.keyspace(crate::keyspaces::WEBVH)?;
let audit_ks = store.keyspace(crate::keyspaces::AUDIT)?;
let did_templates_ks = store.keyspace(crate::keyspaces::DID_TEMPLATES)?;
let ctx = match crate::contexts::get_context(&contexts_ks, &args.context).await? {
Some(ctx) => ctx,
None => {
eprintln!("Context '{}' does not exist.", args.context);
let name: String = Input::new()
.with_prompt("Create it with name")
.default(args.context.clone())
.interact_text()?;
let ctx = crate::contexts::create_context(&contexts_ks, &args.context, &name).await?;
eprintln!("Created context: {} ({})", ctx.id, ctx.base_path);
ctx
}
};
let label = args.label.as_deref().unwrap_or(&args.context);
let interactive = args.url.is_none();
let webvh_url = match &args.url {
Some(raw) => {
let parsed = Url::parse(raw).map_err(|e| format!("invalid --url `{raw}`: {e}"))?;
WebVHURL::parse_url(&parsed)
.map_err(|e| format!("invalid did:webvh hosting URL `{raw}`: {e}"))?
}
None => setup::prompt_webvh_url(label)?,
};
let url_str = webvh_url
.get_http_url(None)
.map_err(|e| format!("{e}"))?
.to_string();
let seed_store = create_seed_store(&config)?;
let seed = crate::keys::seeds::load_seed_bytes(
&keys_ks,
&*seed_store,
Some(
crate::keys::seeds::get_active_seed_id(&keys_ks)
.await
.map_err(|e| format!("{e}"))?,
),
)
.await
.map_err(|e| format!("{e}"))?;
let (did_document, pre_derived) = if interactive {
let (doc, derived) =
build_interactive_did_document(&seed, &ctx.base_path, label, &config, &keys_ks).await?;
(Some(doc), Some(derived))
} else {
(None, None)
};
let portable = if interactive {
Confirm::new()
.with_prompt("Make this DID portable (can move to a different domain later)?")
.default(true)
.interact()?
} else {
true
};
let pre_rotation_count: u32 = if interactive {
Input::new()
.with_prompt("Number of pre-rotation keys (0 = none, recommended: 1-3)")
.default(1u32)
.interact_text()?
} else {
1
};
let auth = cli_super_admin();
let did_resolver = DIDCacheClient::new(DIDCacheConfigBuilder::default().build()).await?;
let no_bridge: Arc<crate::didcomm_bridge::DIDCommBridge> =
Arc::new(crate::didcomm_bridge::DIDCommBridge::placeholder());
let params = CreateDidWebvhParams {
context_id: args.context.clone(),
server_id: None,
url: Some(url_str.clone()),
path_mode: WebvhPathMode::default(),
domain: None,
label: Some(label.to_string()),
portable,
add_mediator_service: !interactive && config.messaging.is_some(),
add_tsp_service: false,
additional_services: None,
pre_rotation_count,
did_document,
did_log: None,
set_primary: true,
pre_derived,
signing_key_id: None,
ka_key_id: None,
template: None,
template_context: None,
template_vars: std::collections::HashMap::new(),
is_vta_identity: false,
};
let auth_locks = operations::did_webvh::WebvhAuthLocks::new();
let deps = operations::did_webvh::CreateDidWebvhDeps {
keys_ks: &keys_ks,
imported_ks: &imported_ks,
contexts_ks: &contexts_ks,
webvh_ks: &webvh_ks,
did_templates_ks: &did_templates_ks,
audit_ks: &audit_ks,
seed_store: &*seed_store,
config: &config,
did_resolver: &did_resolver,
didcomm_bridge: &no_bridge,
auth_locks: &auth_locks,
};
let result = operations::did_webvh::create_did_webvh(&deps, &auth, params, "cli").await?;
let final_did = &result.did;
eprintln!("\x1b[1;32mCreated DID:\x1b[0m {final_did}");
if args.admin {
let acl_ks = store.keyspace(crate::keyspaces::ACL)?;
let entry = AclEntry::new(final_did.clone(), Role::Admin, "cli:create-did-webvh")
.with_label(args.label.clone())
.with_contexts(vec![args.context.clone()]);
store_acl_entry(&acl_ks, &entry).await?;
eprintln!(
"ACL entry created: {final_did} (admin, context: {})",
args.context
);
}
store.persist().await?;
if let Some(ref log_entry) = result.log_entry {
if interactive {
let default_file = args
.did_log_file
.as_ref()
.map(|p| p.display().to_string())
.unwrap_or_else(|| format!("{label}-did.jsonl"));
let did_file: String = Input::new()
.with_prompt("Save DID log to file")
.default(default_file)
.interact_text()?;
std::fs::write(&did_file, log_entry)
.map_err(|e| format!("failed to write did.jsonl to {did_file}: {e}"))?;
eprintln!(" DID log saved to: {did_file}");
eprintln!(" Context '{}' updated with DID: {final_did}", args.context);
eprintln!();
eprintln!(" \x1b[2mTo self-host this DID, upload {did_file} to:");
eprintln!(" {url_str}\x1b[0m");
} else {
if let Some(ref path) = args.did_log_file {
std::fs::write(path, log_entry)
.map_err(|e| format!("failed to write did.jsonl to {}: {e}", path.display()))?;
eprintln!(" DID log written to: {}", path.display());
}
eprintln!(" Context '{}' updated with DID: {final_did}", args.context);
if args.did_log_file.is_none() {
eprintln!(
" \x1b[2mDID log (did.jsonl) ready; pass --did-log-file <path> to save it. Self-host at: {url_str}\x1b[0m"
);
} else {
eprintln!(" \x1b[2mSelf-host the DID log at: {url_str}\x1b[0m");
}
}
}
let want_export = if args.export_secrets {
true
} else if interactive {
Confirm::new()
.with_prompt("Export DID secrets bundle?")
.default(false)
.interact()?
} else {
false
};
if want_export {
let signing_secret = crate::operations::keys::get_key_secret(
&keys_ks,
&imported_ks,
&Arc::from(seed_store),
&store.keyspace(crate::keyspaces::AUDIT)?,
&auth,
&result.signing_key_id,
"cli",
)
.await
.map_err(|e| format!("failed to fetch signing key secret: {e}"))?;
let mut secrets = vec![SecretEntry {
key_id: result.signing_key_id.clone(),
key_type: vta_sdk::keys::KeyType::Ed25519,
private_key_multibase: signing_secret.private_key_multibase,
}];
if !result.ka_key_id.is_empty() {
let ka_secret = crate::operations::keys::get_key_secret(
&keys_ks,
&imported_ks,
&Arc::from(create_seed_store(&config)?),
&store.keyspace(crate::keyspaces::AUDIT)?,
&auth,
&result.ka_key_id,
"cli",
)
.await
.map_err(|e| format!("failed to fetch KA key secret: {e}"))?;
secrets.push(SecretEntry {
key_id: result.ka_key_id.clone(),
key_type: vta_sdk::keys::KeyType::X25519,
private_key_multibase: ka_secret.private_key_multibase,
});
}
let bundle = DidSecretsBundle {
did: final_did.clone(),
secrets,
};
let json = serde_json::to_string_pretty(&bundle)?;
eprintln!();
eprintln!("\x1b[1;33m╔══════════════════════════════════════════════════════════╗");
eprintln!("║ WARNING: The secrets bundle contains private keys. ║");
eprintln!("║ Redirect to a file with restrictive permissions. ║");
eprintln!("╚══════════════════════════════════════════════════════════╝\x1b[0m");
eprintln!();
println!("{json}");
eprintln!();
}
Ok(())
}
async fn build_interactive_did_document(
seed: &[u8],
base_path: &str,
label: &str,
config: &AppConfig,
keys_ks: &vti_common::store::KeyspaceHandle,
) -> Result<(serde_json::Value, crate::keys::DerivedEntityKeys), Box<dyn std::error::Error>> {
let derived = crate::keys::derive_entity_keys(
seed,
base_path,
&format!("{label} signing key"),
&format!("{label} key-agreement key"),
keys_ks,
)
.await?;
let mut doc = operations::did_webvh::build_did_document(&derived, config, false, &None);
if let Some(ref msg) = config.messaging {
let service_options = &[
"DIDComm endpoint (references mediator DID for routing)",
"No service endpoints",
];
let want_didcomm = Select::new()
.with_prompt("Service endpoints")
.items(service_options)
.default(0)
.interact()?
== 0;
if want_didcomm {
doc["service"] = json!([
{
"id": "{DID}#vta-didcomm",
"type": "DIDCommMessaging",
"serviceEndpoint": [{
"accept": ["didcomm/v2"],
"uri": msg.mediator_did
}]
}
]);
}
}
eprintln!();
eprintln!(
"\x1b[2mDID Document:\n{}\x1b[0m",
serde_json::to_string_pretty(&doc)?
);
eprintln!();
if Confirm::new()
.with_prompt("Edit DID document in your editor?")
.default(false)
.interact()?
{
doc = edit_did_document(doc)?;
}
Ok((doc, derived))
}
fn edit_did_document(
doc: serde_json::Value,
) -> Result<serde_json::Value, Box<dyn std::error::Error>> {
use std::io::Write;
use std::process::Command;
let json = serde_json::to_string_pretty(&doc)?;
let mut tmp = tempfile::Builder::new().suffix(".json").tempfile()?;
tmp.write_all(json.as_bytes())?;
tmp.flush()?;
let path = tmp.path().to_path_buf();
let editor = std::env::var("VISUAL")
.or_else(|_| std::env::var("EDITOR"))
.unwrap_or_else(|_| "vi".to_string());
let status = Command::new(&editor)
.arg(&path)
.status()
.map_err(|e| format!("failed to launch editor '{editor}': {e}"))?;
if !status.success() {
return Err(format!("editor exited with {status}").into());
}
let edited = std::fs::read_to_string(&path)?;
let new_doc: serde_json::Value =
serde_json::from_str(&edited).map_err(|e| format!("invalid JSON from editor: {e}"))?;
if !new_doc.is_object() || !new_doc.get("id").is_some_and(|v| v.is_string()) {
return Err("DID document must be a JSON object with an \"id\" field".into());
}
eprintln!(
"\x1b[2mUpdated DID Document:\n{}\x1b[0m",
serde_json::to_string_pretty(&new_doc)?
);
Ok(new_doc)
}
#[cfg(all(test, feature = "config-seed"))]
mod tests {
use super::*;
use crate::acl::get_acl_entry;
use crate::keys::seeds::{SeedRecord, save_seed_record, set_active_seed_id};
use vti_common::acl::Role;
#[tokio::test]
async fn create_did_webvh_url_admin_export_is_noninteractive_and_grants_admin() {
let dir = tempfile::TempDir::new().expect("tempdir");
let data_dir = dir.path().join("data");
std::fs::create_dir_all(&data_dir).unwrap();
let config_path = dir.path().join("config.toml");
let seed_hex = hex::encode([7u8; 64]);
std::fs::write(
&config_path,
format!(
"[store]\ndata_dir = \"{}\"\n\n[secrets]\nseed = \"{seed_hex}\"\n",
data_dir.display()
),
)
.unwrap();
let config = AppConfig::load(Some(config_path.clone())).expect("load config");
let store = Store::open(&config.store).expect("open store");
let keys_ks = store.keyspace(crate::keyspaces::KEYS).unwrap();
let contexts_ks = store.keyspace(crate::keyspaces::CONTEXTS).unwrap();
save_seed_record(
&keys_ks,
&SeedRecord {
id: 0,
seed_hex: None,
seed_enc: None,
created_at: chrono::Utc::now(),
retired_at: None,
},
)
.await
.unwrap();
set_active_seed_id(&keys_ks, 0).await.unwrap();
crate::contexts::create_context(&contexts_ks, "agents", "Agents")
.await
.unwrap();
store.persist().await.unwrap();
drop(keys_ks);
drop(contexts_ks);
drop(store);
let args = CreateDidWebvhArgs {
config_path: Some(config_path.clone()),
context: "agents".to_string(),
label: Some("agent-1".to_string()),
url: Some("https://example.com/agents/agent-1".to_string()),
export_secrets: true,
admin: true,
did_log_file: None,
};
run_create_did_webvh(args).await.expect("create-did-webvh");
let store = Store::open(&config.store).expect("reopen store");
let webvh_ks = store.keyspace(crate::keyspaces::WEBVH).unwrap();
let acl_ks = store.keyspace(crate::keyspaces::ACL).unwrap();
let dids = crate::webvh_store::list_dids(&webvh_ks).await.unwrap();
assert_eq!(dids.len(), 1, "one did:webvh minted");
let did = &dids[0].did;
assert!(did.starts_with("did:webvh:"), "got {did}");
let keys_ks2 = store.keyspace(crate::keyspaces::KEYS).unwrap();
let key0: Option<crate::keys::KeyRecord> = keys_ks2
.get(crate::keys::store_key(&format!("{did}#key-0")))
.await
.unwrap();
let key1: Option<crate::keys::KeyRecord> = keys_ks2
.get(crate::keys::store_key(&format!("{did}#key-1")))
.await
.unwrap();
assert!(key0.is_some(), "#key-0 (signing) record present");
assert!(key1.is_some(), "#key-1 (key-agreement) record present");
assert_eq!(key1.unwrap().key_type, crate::keys::KeyType::X25519);
let entry = get_acl_entry(&acl_ks, did)
.await
.unwrap()
.expect("ACL entry created for the did:webvh");
assert_eq!(entry.role, Role::Admin);
assert_eq!(entry.allowed_contexts, vec!["agents".to_string()]);
}
#[tokio::test]
async fn did_log_file_writes_the_log_to_the_given_path() {
let dir = tempfile::TempDir::new().expect("tempdir");
let data_dir = dir.path().join("data");
std::fs::create_dir_all(&data_dir).unwrap();
let config_path = dir.path().join("config.toml");
let seed_hex = hex::encode([9u8; 64]);
std::fs::write(
&config_path,
format!(
"[store]\ndata_dir = \"{}\"\n\n[secrets]\nseed = \"{seed_hex}\"\n",
data_dir.display()
),
)
.unwrap();
let config = AppConfig::load(Some(config_path.clone())).expect("load config");
let store = Store::open(&config.store).expect("open store");
let keys_ks = store.keyspace(crate::keyspaces::KEYS).unwrap();
let contexts_ks = store.keyspace(crate::keyspaces::CONTEXTS).unwrap();
save_seed_record(
&keys_ks,
&SeedRecord {
id: 0,
seed_hex: None,
seed_enc: None,
created_at: chrono::Utc::now(),
retired_at: None,
},
)
.await
.unwrap();
set_active_seed_id(&keys_ks, 0).await.unwrap();
crate::contexts::create_context(&contexts_ks, "agents", "Agents")
.await
.unwrap();
store.persist().await.unwrap();
drop(keys_ks);
drop(contexts_ks);
drop(store);
let publish_dir = dir.path().join("pages");
std::fs::create_dir_all(&publish_dir).unwrap();
let log_path = publish_dir.join("did.jsonl");
let args = CreateDidWebvhArgs {
config_path: Some(config_path.clone()),
context: "agents".to_string(),
label: Some("agent-1".to_string()),
url: Some("https://example.com/agents/agent-1".to_string()),
export_secrets: false,
admin: false,
did_log_file: Some(log_path.clone()),
};
run_create_did_webvh(args).await.expect("create-did-webvh");
let written = std::fs::read_to_string(&log_path).expect("did.jsonl written");
assert!(!written.trim().is_empty(), "did.jsonl is not empty");
let first = written.lines().next().expect("at least one log entry");
let entry: serde_json::Value = serde_json::from_str(first).expect("log entry is JSON");
assert!(
entry.get("versionId").is_some(),
"log entry carries versionId, got: {entry}"
);
}
async fn setup_seeded_store(
dir: &tempfile::TempDir,
mediator_did: Option<&str>,
) -> (AppConfig, std::path::PathBuf) {
let data_dir = dir.path().join("data");
std::fs::create_dir_all(&data_dir).unwrap();
let config_path = dir.path().join("config.toml");
let seed_hex = hex::encode([42u8; 64]);
let messaging = mediator_did
.map(|did| format!("\n[messaging]\nmediator_did = \"{did}\"\n"))
.unwrap_or_default();
std::fs::write(
&config_path,
format!(
"[store]\ndata_dir = \"{}\"\n\n[secrets]\nseed = \"{seed_hex}\"\n{messaging}",
data_dir.display()
),
)
.unwrap();
let config = AppConfig::load(Some(config_path.clone())).expect("load config");
let store = Store::open(&config.store).expect("open store");
let keys_ks = store.keyspace(crate::keyspaces::KEYS).unwrap();
let contexts_ks = store.keyspace(crate::keyspaces::CONTEXTS).unwrap();
save_seed_record(
&keys_ks,
&SeedRecord {
id: 0,
seed_hex: None,
seed_enc: None,
created_at: chrono::Utc::now(),
retired_at: None,
},
)
.await
.unwrap();
set_active_seed_id(&keys_ks, 0).await.unwrap();
crate::contexts::create_context(&contexts_ks, "test-ctx", "Test")
.await
.unwrap();
store.persist().await.unwrap();
drop(keys_ks);
drop(contexts_ks);
drop(store);
(config, config_path)
}
async fn run_non_interactive_create(
dir: &tempfile::TempDir,
mediator_did: Option<&str>,
) -> (AppConfig, serde_json::Value) {
let (config, config_path) = setup_seeded_store(dir, mediator_did).await;
let log_path = dir.path().join("did.jsonl");
let args = CreateDidWebvhArgs {
config_path: Some(config_path.clone()),
context: "test-ctx".to_string(),
label: Some("test".to_string()),
url: Some("https://example.com/test".to_string()),
export_secrets: false,
admin: false,
did_log_file: Some(log_path.clone()),
};
run_create_did_webvh(args).await.expect("create-did-webvh");
let content = std::fs::read_to_string(&log_path).expect("did.jsonl");
let entry: serde_json::Value =
serde_json::from_str(content.lines().next().unwrap()).unwrap();
(config, entry)
}
#[tokio::test]
async fn non_interactive_did_doc_key_matches_stored_key() {
let dir = tempfile::TempDir::new().expect("tempdir");
let (config, entry) = run_non_interactive_create(&dir, None).await;
let state = entry.get("state").expect("state field");
let vms = state
.get("verificationMethod")
.and_then(|v| v.as_array())
.expect("verificationMethod array");
let doc_pubkey: [u8; 32] = vms
.iter()
.filter_map(|vm| vm.get("publicKeyMultibase")?.as_str())
.filter_map(|mb| multibase::decode(mb).ok())
.filter_map(|(_, bytes)| {
if bytes.starts_with(&[0xed, 0x01]) && bytes.len() == 34 {
<[u8; 32]>::try_from(&bytes[2..]).ok()
} else {
None
}
})
.next()
.expect("DID doc must have an Ed25519 key");
let store = Store::open(&config.store).expect("reopen store");
let keys_ks = store.keyspace(crate::keyspaces::KEYS).unwrap();
let imported_ks = store.keyspace(crate::keyspaces::IMPORTED_SECRETS).unwrap();
let audit_ks = store.keyspace(crate::keyspaces::AUDIT).unwrap();
let seed_store = Arc::from(create_seed_store(&config).unwrap());
let auth = cli_super_admin();
let did = entry
.get("state")
.and_then(|s| s.get("id"))
.and_then(|v| v.as_str())
.expect("DID id");
let key_id = format!("{did}#key-0");
let secret = crate::operations::keys::get_key_secret(
&keys_ks,
&imported_ks,
&seed_store,
&audit_ks,
&auth,
&key_id,
"test",
)
.await
.expect("fetch key secret");
let secret_bytes =
multibase::decode(&secret.private_key_multibase).expect("decode secret multibase");
let signing_key =
ed25519_dalek::SigningKey::from_bytes(secret_bytes.1[2..].try_into().unwrap());
let store_pubkey = signing_key.verifying_key().to_bytes();
assert_eq!(
doc_pubkey, store_pubkey,
"DID doc key must match stored key — if this fails, the path \
counter was double-allocated (preview + operation derived separately)"
);
}
#[tokio::test]
async fn non_interactive_did_doc_advertises_the_mediator() {
const MEDIATOR_DID: &str = "did:key:z6MkiSEhwSnqRMjfsZnfoqCkVsao8SYaBwQ2HREh6F91R5wL";
let dir = tempfile::TempDir::new().expect("tempdir");
let (_config, entry) = run_non_interactive_create(&dir, Some(MEDIATOR_DID)).await;
let services = entry
.get("state")
.and_then(|s| s.get("service"))
.and_then(|v| v.as_array())
.expect("DID doc must carry a service array when [messaging] is set");
let didcomm = services
.iter()
.find(|s| s.get("type").and_then(|t| t.as_str()) == Some("DIDCommMessaging"))
.expect("a DIDCommMessaging service must be advertised");
let endpoint = serde_json::to_string(didcomm).expect("serialize service");
assert!(
endpoint.contains(MEDIATOR_DID),
"the DIDComm service must route via the configured mediator DID, got {didcomm}"
);
}
#[tokio::test]
async fn caller_rendered_document_names_the_keys_the_store_holds() {
let dir = tempfile::TempDir::new().expect("tempdir");
let (config, _config_path) = setup_seeded_store(&dir, None).await;
let store = Store::open(&config.store).expect("open store");
let keys_ks = store.keyspace(crate::keyspaces::KEYS).unwrap();
let imported_ks = store.keyspace(crate::keyspaces::IMPORTED_SECRETS).unwrap();
let contexts_ks = store.keyspace(crate::keyspaces::CONTEXTS).unwrap();
let webvh_ks = store.keyspace(crate::keyspaces::WEBVH).unwrap();
let audit_ks = store.keyspace(crate::keyspaces::AUDIT).unwrap();
let did_templates_ks = store.keyspace(crate::keyspaces::DID_TEMPLATES).unwrap();
let seed_store = create_seed_store(&config).unwrap();
let ctx = crate::contexts::get_context(&contexts_ks, "test-ctx")
.await
.unwrap()
.expect("context");
let seed = crate::keys::seeds::load_seed_bytes(&keys_ks, &*seed_store, Some(0))
.await
.expect("load seed");
let derived = crate::keys::derive_entity_keys(
&seed,
&ctx.base_path,
"test signing key",
"test key-agreement key",
&keys_ks,
)
.await
.expect("derive");
let preview_signing_pub = derived.signing_pub.clone();
let did_document =
operations::did_webvh::build_did_document(&derived, &config, false, &None);
let did_resolver = DIDCacheClient::new(DIDCacheConfigBuilder::default().build())
.await
.unwrap();
let no_bridge: Arc<crate::didcomm_bridge::DIDCommBridge> =
Arc::new(crate::didcomm_bridge::DIDCommBridge::placeholder());
let auth_locks = operations::did_webvh::WebvhAuthLocks::new();
let deps = operations::did_webvh::CreateDidWebvhDeps {
keys_ks: &keys_ks,
imported_ks: &imported_ks,
contexts_ks: &contexts_ks,
webvh_ks: &webvh_ks,
did_templates_ks: &did_templates_ks,
audit_ks: &audit_ks,
seed_store: &*seed_store,
config: &config,
did_resolver: &did_resolver,
didcomm_bridge: &no_bridge,
auth_locks: &auth_locks,
};
let result = operations::did_webvh::create_did_webvh(
&deps,
&cli_super_admin(),
CreateDidWebvhParams {
context_id: "test-ctx".to_string(),
server_id: None,
url: Some("https://example.com/test".to_string()),
path_mode: WebvhPathMode::default(),
domain: None,
label: Some("test".to_string()),
portable: true,
add_mediator_service: false,
add_tsp_service: false,
additional_services: None,
pre_rotation_count: 1,
did_document: Some(did_document),
did_log: None,
set_primary: true,
pre_derived: Some(derived),
signing_key_id: None,
ka_key_id: None,
template: None,
template_context: None,
template_vars: std::collections::HashMap::new(),
is_vta_identity: false,
},
"test",
)
.await
.expect("create_did_webvh");
let secret = crate::operations::keys::get_key_secret(
&keys_ks,
&imported_ks,
&Arc::from(create_seed_store(&config).unwrap()),
&audit_ks,
&cli_super_admin(),
&format!("{}#key-0", result.did),
"test",
)
.await
.expect("fetch key secret");
let secret_bytes =
multibase::decode(&secret.private_key_multibase).expect("decode secret multibase");
let signing_key =
ed25519_dalek::SigningKey::from_bytes(secret_bytes.1[2..].try_into().unwrap());
let store_pub_mb = multibase::encode(
multibase::Base::Base58Btc,
[
&[0xed, 0x01][..],
&signing_key.verifying_key().to_bytes()[..],
]
.concat(),
);
assert_eq!(
store_pub_mb, preview_signing_pub,
"the store must hold the key the caller's document names — if this \
fails, the operation re-derived and allocated a second path index"
);
}
#[tokio::test]
async fn non_interactive_did_doc_omits_didcomm_without_messaging() {
let dir = tempfile::TempDir::new().expect("tempdir");
let (_config, entry) = run_non_interactive_create(&dir, None).await;
let has_didcomm = entry
.get("state")
.and_then(|s| s.get("service"))
.and_then(|v| v.as_array())
.is_some_and(|services| {
services
.iter()
.any(|s| s.get("type").and_then(|t| t.as_str()) == Some("DIDCommMessaging"))
});
assert!(
!has_didcomm,
"no [messaging] configured, so nothing should advertise DIDComm"
);
}
}