use std::path::PathBuf;
use std::sync::Arc;
use chrono::Utc;
use dialoguer::{Confirm, Input, Select};
use didwebvh_rs::create::{CreateDIDConfig, create_did};
use didwebvh_rs::parameters::Parameters as WebVHParameters;
use serde_json::json;
use vta_sdk::did_secrets::{DidSecretsBundle, SecretEntry};
use crate::config::AppConfig;
use crate::contexts::{self, get_context, store_context};
use crate::keys::seed_store::create_seed_store;
use crate::keys::seeds::{get_active_seed_id, load_seed_bytes};
use crate::keys::{self, KeyType as SdkKeyType};
use crate::operations::did_webvh as ops;
use crate::setup;
use crate::store::Store;
pub struct CreateDidWebvhArgs {
pub config_path: Option<PathBuf>,
pub context: String,
pub label: Option<String>,
}
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("keys")?;
let contexts_ks = store.keyspace("contexts")?;
let seed_store = create_seed_store(&config)?;
let active_seed_id = get_active_seed_id(&keys_ks).await?;
let seed = load_seed_bytes(&keys_ks, &*seed_store, Some(active_seed_id)).await?;
let mut ctx = match 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 = 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 mut derived = keys::derive_entity_keys(
&seed,
&ctx.base_path,
&format!("{label} signing key"),
&format!("{label} key-agreement key"),
&keys_ks,
)
.await?;
let webvh_url = setup::prompt_webvh_url(label)?;
derived.signing_secret.id = [
"did:key:",
&derived.signing_secret.get_public_keymultibase().unwrap(),
"#",
&derived.signing_secret.get_public_keymultibase().unwrap(),
]
.concat();
let mut did_document = ops::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 service_choice = Select::new()
.with_prompt("Service endpoints")
.items(service_options)
.default(0)
.interact()?;
if service_choice == 0 {
did_document["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(&did_document)?
);
eprintln!();
if Confirm::new()
.with_prompt("Edit DID document in your editor?")
.default(false)
.interact()?
{
did_document = edit_did_document(did_document)?;
}
let portable = Confirm::new()
.with_prompt("Make this DID portable (can move to a different domain later)?")
.default(true)
.interact()?;
let (next_key_hashes, pre_rotation_keys) =
setup::prompt_pre_rotation_keys(&seed, &ctx.base_path, label, &keys_ks).await?;
let parameters = WebVHParameters {
update_keys: Some(Arc::new(vec![derived.signing_pub.clone().into()])),
portable: Some(portable),
next_key_hashes: if next_key_hashes.is_empty() {
None
} else {
Some(Arc::new(
next_key_hashes.into_iter().map(Into::into).collect(),
))
},
..Default::default()
};
let url_str = webvh_url
.get_http_url(None)
.map_err(|e| format!("{e}"))?
.to_string();
let create_config = CreateDIDConfig::builder()
.address(url_str)
.authorization_key(derived.signing_secret.clone())
.did_document(did_document)
.parameters(parameters)
.build()
.map_err(|e| format!("failed to build DID config: {e}"))?;
let result = create_did(create_config)
.await
.map_err(|e| format!("failed to create DID: {e}"))?;
let final_did = result.did().to_string();
eprintln!("\x1b[1;32mCreated DID:\x1b[0m {final_did}");
keys::save_entity_key_records(
&final_did,
&derived,
&keys_ks,
Some(&ctx.id),
Some(active_seed_id),
)
.await?;
for (i, pk) in pre_rotation_keys.iter().enumerate() {
keys::save_key_record(
&keys_ks,
&format!("{final_did}#pre-rotation-{i}"),
&pk.path,
SdkKeyType::Ed25519,
&pk.public_key,
&pk.label,
Some(&ctx.id),
Some(active_seed_id),
)
.await?;
}
ctx.did = Some(final_did.clone());
ctx.updated_at = Utc::now();
store_context(&contexts_ks, &ctx)
.await
.map_err(|e| format!("{e}"))?;
store.persist().await?;
let default_file = format!("{label}-did.jsonl");
let did_file: String = Input::new()
.with_prompt("Save DID log to file")
.default(default_file)
.interact_text()?;
result
.log_entry()
.save_to_file(&did_file)
.map_err(|e| format!("Failed to save DID log file: {e}"))?;
eprintln!(" DID log saved to: {did_file}");
eprintln!(" Context '{}' updated with DID: {final_did}", ctx.id);
if Confirm::new()
.with_prompt("Export DID secrets bundle?")
.default(false)
.interact()?
{
let bundle = DidSecretsBundle {
did: final_did.clone(),
secrets: vec![
SecretEntry {
key_id: format!("{final_did}#key-0"),
key_type: SdkKeyType::Ed25519,
private_key_multibase: derived.signing_priv.clone(),
},
SecretEntry {
key_id: format!("{final_did}#key-1"),
key_type: SdkKeyType::X25519,
private_key_multibase: derived.ka_priv.clone(),
},
],
};
let encoded = bundle.encode().map_err(|e| format!("{e}"))?;
eprintln!();
eprintln!("\x1b[1;33m╔══════════════════════════════════════════════════════════╗");
eprintln!("║ WARNING: The secrets bundle contains private keys. ║");
eprintln!("║ Store it securely and do not share it publicly. ║");
eprintln!("╚══════════════════════════════════════════════════════════╝\x1b[0m");
eprintln!();
println!("{encoded}");
eprintln!();
}
Ok(())
}
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)
}