use std::path::{Path, PathBuf};
use bip39::Mnemonic;
use dialoguer::{Confirm, Input};
use didwebvh_rs::url::WebVHURL;
use rand::Rng;
use serde_json::{Value as JsonValue, json};
use url::Url;
use crate::config::ServicesConfig;
use crate::contexts::{self, ContextRecord};
use crate::store::KeyspaceHandle;
mod from_toml;
mod interactive;
pub trait SetupUi {
fn confirm_mnemonic(&self, mnemonic: &Mnemonic) -> Result<(), Box<dyn std::error::Error>>;
fn did_log_path(&self, label: &str, default: &Path) -> Option<PathBuf>;
}
pub struct SilentUi;
impl SetupUi for SilentUi {
fn confirm_mnemonic(&self, _mnemonic: &Mnemonic) -> Result<(), Box<dyn std::error::Error>> {
Ok(())
}
fn did_log_path(&self, _label: &str, default: &Path) -> Option<PathBuf> {
Some(default.to_path_buf())
}
}
#[allow(unused_imports)]
pub use from_toml::{
ExistingDataDirPolicy, MessagingInput, SecretsBackendInput, VtaDidInput, WizardInputs,
apply_inputs, run_setup_from_file,
};
pub use interactive::run_setup_wizard;
pub(crate) async fn create_seed_context(
contexts_ks: &KeyspaceHandle,
id: &str,
name: &str,
) -> Result<ContextRecord, Box<dyn std::error::Error>> {
contexts::create_context(contexts_ks, id, name).await
}
pub(crate) fn generate_mnemonic_silent() -> Result<Mnemonic, Box<dyn std::error::Error>> {
let mut entropy = [0u8; 32];
rand::rng().fill_bytes(&mut entropy);
Ok(Mnemonic::from_entropy(&entropy)?)
}
pub(crate) fn build_vta_additional_services(
services: &ServicesConfig,
public_url: Option<&str>,
) -> Option<Vec<JsonValue>> {
let mut additional = Vec::new();
if services.rest
&& let Some(url) = public_url.map(str::trim).filter(|u| !u.is_empty())
{
additional.push(json!({
"id": "{DID}#vta-rest",
"type": "VTARest",
"serviceEndpoint": url,
}));
}
if additional.is_empty() {
None
} else {
Some(additional)
}
}
pub(crate) fn derive_ws_url(http_url: &str) -> Option<String> {
let scheme_swapped = if let Some(rest) = http_url.strip_prefix("https://") {
format!("wss://{rest}")
} else if let Some(rest) = http_url.strip_prefix("http://") {
format!("ws://{rest}")
} else {
return None;
};
Some(format!("{}/ws", scheme_swapped.trim_end_matches('/')))
}
pub(crate) fn prompt_webvh_url(label: &str) -> Result<WebVHURL, Box<dyn std::error::Error>> {
eprintln!();
eprintln!(" Enter the URL where the {label} DID document will be hosted.");
eprintln!(" Examples:");
eprintln!(" https://example.com -> did:webvh:{{SCID}}:example.com");
eprintln!(" https://example.com/dids/vta -> did:webvh:{{SCID}}:example.com:dids:vta");
eprintln!(" http://localhost:8000 -> did:webvh:{{SCID}}:localhost%3A8000");
eprintln!();
loop {
let raw: String = Input::new()
.with_prompt(format!("{label} DID URL"))
.default("http://localhost:8000/".into())
.interact_text()?;
let parsed = match Url::parse(&raw) {
Ok(u) => u,
Err(e) => {
eprintln!("\x1b[31mInvalid URL: {e} — please try again.\x1b[0m");
continue;
}
};
match WebVHURL::parse_url(&parsed) {
Ok(webvh_url) => {
let did_display = webvh_url.to_string();
let http_url = webvh_url.get_http_url(None).map_err(|e| format!("{e}"))?;
eprintln!(" DID: {did_display}");
eprintln!(" URL: {http_url}");
if Confirm::new()
.with_prompt("Is this correct?")
.default(true)
.interact()?
{
return Ok(webvh_url);
}
}
Err(e) => {
eprintln!(
"\x1b[31mCould not convert to a webvh DID: {e} — please try again.\x1b[0m"
);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn build_vta_additional_services_matrix() {
let url = Some("https://vta.example.com");
let services = ServicesConfig {
rest: true,
didcomm: false,
webauthn: false,
};
let out = build_vta_additional_services(&services, url)
.expect("REST + URL must emit a service entry");
assert_eq!(out.len(), 1);
assert_eq!(out[0]["type"], "VTARest");
assert_eq!(out[0]["serviceEndpoint"], "https://vta.example.com");
assert_eq!(out[0]["id"], "{DID}#vta-rest");
let out = build_vta_additional_services(&services, Some(" https://vta.example.com "))
.expect("whitespace-padded URL must still emit");
assert_eq!(out[0]["serviceEndpoint"], "https://vta.example.com");
assert!(build_vta_additional_services(&services, None).is_none());
assert!(build_vta_additional_services(&services, Some("")).is_none());
assert!(build_vta_additional_services(&services, Some(" ")).is_none());
let services = ServicesConfig {
rest: false,
didcomm: true,
webauthn: false,
};
assert!(
build_vta_additional_services(&services, url).is_none(),
"URL must not be published as a service when REST is disabled"
);
let services = ServicesConfig {
rest: false,
didcomm: false,
webauthn: false,
};
assert!(build_vta_additional_services(&services, None).is_none());
}
#[test]
fn derive_ws_url_swaps_scheme_trims_and_appends_ws() {
assert_eq!(
derive_ws_url("https://mediator.example.com").as_deref(),
Some("wss://mediator.example.com/ws")
);
assert_eq!(
derive_ws_url("http://localhost:8000").as_deref(),
Some("ws://localhost:8000/ws")
);
assert_eq!(
derive_ws_url("https://mediator.example.com/").as_deref(),
Some("wss://mediator.example.com/ws")
);
assert_eq!(
derive_ws_url("https://example.com/mediator/v1/").as_deref(),
Some("wss://example.com/mediator/v1/ws")
);
assert_eq!(derive_ws_url("wss://already.ws/ws"), None);
assert_eq!(derive_ws_url("mediator.example.com"), None);
assert_eq!(derive_ws_url(""), None);
}
}