use vtc_service::{config, did_key, keys, server, status, store};
#[cfg(feature = "setup")]
use vtc_service::{emergency, setup};
use std::path::PathBuf;
use clap::{Parser, Subcommand};
use config::{AppConfig, LogFormat};
use keys::seed_store::create_secret_store;
use tracing_subscriber::EnvFilter;
#[derive(Parser)]
#[command(name = "vtc", about = "Verifiable Trust Community", version)]
struct Cli {
#[arg(short, long, global = true)]
config: Option<PathBuf>,
#[command(subcommand)]
command: Option<Commands>,
}
#[derive(Subcommand)]
enum Commands {
Setup,
Status,
CreateDidKey {
#[arg(long)]
admin: bool,
#[arg(long)]
label: Option<String>,
},
Admin {
#[command(subcommand)]
command: AdminCommands,
},
}
#[derive(Subcommand)]
enum AdminCommands {
EmergencyBootstrap {
#[arg(long)]
yes: bool,
#[arg(long)]
context: Option<String>,
},
Invite {
#[arg(long)]
did: String,
#[arg(long, default_value_t = 900)]
ttl: u64,
},
}
#[tokio::main]
async fn main() {
let cli = Cli::parse();
#[cfg(feature = "keyring")]
if let Err(e) = vta_sdk::keyring_init::install_default_store() {
eprintln!("warning: OS keyring unavailable: {e}");
}
print_banner();
match cli.command {
Some(Commands::Setup) => {
#[cfg(feature = "setup")]
{
if let Err(e) = setup::run_setup_wizard(cli.config).await {
eprintln!("Setup failed: {e}");
std::process::exit(1);
}
}
#[cfg(not(feature = "setup"))]
{
eprintln!("Setup wizard not available (compiled without 'setup' feature)");
std::process::exit(1);
}
}
Some(Commands::Status) => {
if let Err(e) = status::run_status(cli.config).await {
eprintln!("Error: {e}");
std::process::exit(1);
}
}
Some(Commands::CreateDidKey { admin, label }) => {
let args = did_key::CreateDidKeyArgs {
config_path: cli.config,
admin,
label,
};
if let Err(e) = did_key::run_create_did_key(args).await {
eprintln!("Error: {e}");
std::process::exit(1);
}
}
Some(Commands::Admin { command }) => {
#[cfg(feature = "setup")]
{
match command {
AdminCommands::EmergencyBootstrap { yes, context } => {
if let Err(e) = run_emergency_bootstrap_cli(cli.config, yes, context).await
{
eprintln!("Emergency bootstrap failed: {e}");
std::process::exit(1);
}
}
AdminCommands::Invite { did, ttl } => {
if let Err(e) = run_invite_cli(cli.config, did, ttl).await {
eprintln!("Invite failed: {e}");
std::process::exit(1);
}
}
}
}
#[cfg(not(feature = "setup"))]
{
let _ = command;
eprintln!("admin subcommands are unavailable (compiled without 'setup')");
std::process::exit(1);
}
}
None => {
let config = match AppConfig::load(cli.config) {
Ok(config) => config,
Err(e) => {
eprintln!("Error: {e}");
eprintln!();
eprintln!("To set up a new VTC instance, run:");
eprintln!(" vtc setup");
eprintln!();
eprintln!("Or specify a config file:");
eprintln!(" vtc --config <path>");
std::process::exit(1);
}
};
init_tracing(&config);
let store = store::Store::open(&config.store).expect("failed to open store");
let secret_store = create_secret_store(&config).expect("failed to create secret store");
if let Err(e) = server::run(config, store, secret_store).await {
tracing::error!("server error: {e}");
std::process::exit(1);
}
}
}
}
#[cfg(feature = "setup")]
async fn run_emergency_bootstrap_cli(
config_path: Option<std::path::PathBuf>,
skip_confirm: bool,
context: Option<String>,
) -> Result<(), Box<dyn std::error::Error>> {
use dialoguer::Confirm;
eprintln!();
eprintln!("⚠️ EMERGENCY BOOTSTRAP");
eprintln!(
"This clears every existing admin ACL entry and admin sister record, then\n\
reopens the install carve-out so a new operator can claim a fresh install URL.\n\
\n\
The VTA accepts or rejects the recovery: if your PNM admin credential at the\n\
VTA is still valid, the VTA will accept it; otherwise this command fails and\n\
no local state is touched. The daemon's next boot emits a loud\n\
`EmergencyBootstrapInvoked` audit event.\n"
);
if !skip_confirm {
let ok = Confirm::new()
.with_prompt("Proceed?")
.default(false)
.interact()?;
if !ok {
eprintln!("aborted.");
return Ok(());
}
}
let outcome = emergency::run_emergency_bootstrap(emergency::EmergencyBootstrapArgs {
config_path,
context,
})
.await?;
eprintln!();
eprintln!("✅ emergency bootstrap complete");
eprintln!(
" admin ACL entries cleared: {}",
outcome.admin_entries_cleared
);
eprintln!(
" admin sister records: {}",
outcome.admin_records_cleared
);
eprintln!();
eprintln!("Install URL (one-shot, 15 min TTL):");
eprintln!(" {}", outcome.install_url);
eprintln!();
eprintln!("Claim code (required at claim time — keep separate from the URL):");
eprintln!(" {}", outcome.claim_code);
eprintln!();
eprintln!(
"Restart the daemon (`vtc`) so the `EmergencyBootstrapInvoked` audit event lands.\n\
Then claim the install URL with a fresh passkey, supplying the claim code above."
);
Ok(())
}
#[cfg(feature = "setup")]
async fn run_invite_cli(
config_path: Option<std::path::PathBuf>,
admin_did: String,
ttl_seconds: u64,
) -> Result<(), Box<dyn std::error::Error>> {
use chrono::{Duration as ChronoDuration, Utc};
use vtc_service::acl::{VtcAclEntry, VtcRole, get_acl_entry, store_acl_entry};
use vtc_service::auth::session::now_epoch;
use vtc_service::install::{InstallTokenSigner, InstallTokenStore, mint_install_token};
use vtc_service::keys::seed_store::create_secret_store;
use vtc_service::setup::VtcKeyBundle;
use vti_common::store::Store as VtiStore;
if !admin_did.starts_with("did:") {
return Err(format!("--did must start with 'did:' (got '{admin_did}')").into());
}
let config = vtc_service::config::AppConfig::load(config_path)?;
let vtc_did = config
.vtc_did
.clone()
.ok_or("config has no vtc_did — has setup completed?")?;
let base_url = config
.public_url
.clone()
.ok_or("config has no public_url — operators cannot build a clickable install URL")?;
let secret_store = create_secret_store(&config)?;
let bundle_bytes = secret_store
.get()
.await?
.ok_or("secret store has no VTC bundle — has setup completed?")?;
let bundle = VtcKeyBundle::from_secret_store_bytes(&bundle_bytes)?;
let ed25519 = bundle.ed25519_private_bytes()?;
let signer = InstallTokenSigner::from_master_seed(&*ed25519)?;
let store = VtiStore::open(&config.store)?;
let install_ks = store.keyspace("install")?;
let install_store = InstallTokenStore::new(install_ks);
let acl_ks = store.keyspace("acl")?;
let acl_already_present = get_acl_entry(&acl_ks, &admin_did).await?.is_some();
if !acl_already_present {
let entry = VtcAclEntry {
did: admin_did.clone(),
role: VtcRole::Admin,
label: Some("vtc admin invite".into()),
allowed_contexts: vec![],
created_at: now_epoch(),
created_by: format!("vtc-cli/{}", env!("CARGO_PKG_VERSION")),
expires_at: None,
};
store_acl_entry(&acl_ks, &entry).await?;
}
let minted = mint_install_token(&signer, &vtc_did, &admin_did, ttl_seconds)?;
let claim_code = vtc_service::install::claim_secret::generate();
let claim_code_hash = vtc_service::install::claim_secret::hash(&claim_code)?;
let exp = Utc::now() + ChronoDuration::seconds(ttl_seconds as i64);
install_store
.record_issued(
&minted.jti,
minted.cnonce_bytes,
*minted.ephemeral_signing_key,
exp,
Some(claim_code_hash),
Some(admin_did.clone()),
)
.await?;
let install_url = format!(
"{}/admin/install?token={}",
base_url.trim_end_matches('/'),
minted.jwt
);
eprintln!();
eprintln!("✅ install URL minted");
eprintln!(" Admin DID: {admin_did}");
eprintln!(
" ACL entry: {}",
if acl_already_present {
"pre-existing (left untouched)"
} else {
"created (role=admin)"
}
);
eprintln!(" TTL: {ttl_seconds}s");
eprintln!();
eprintln!("Install URL (one-shot):");
eprintln!(" {install_url}");
eprintln!();
eprintln!("Claim code (deliver via a SEPARATE channel — Signal/SMS/in person):");
eprintln!(" {claim_code}");
eprintln!();
eprintln!("Both the URL and the claim code are required to claim the passkey.");
eprintln!("A leaked URL alone is not enough — the daemon refuses claim without the code.");
eprintln!();
eprintln!("Restart the daemon (`vtc`) before claiming — the daemon must be running");
eprintln!("for the browser to reach `/admin/install` and `/v1/install/claim/*`.");
Ok(())
}
fn print_banner() {
let cyan = "\x1b[36m";
let magenta = "\x1b[35m";
let yellow = "\x1b[33m";
let dim = "\x1b[2m";
let reset = "\x1b[0m";
eprintln!(
r#"
{cyan} ██╗ ██╗{magenta}████████╗{yellow} ██████╗{reset}
{cyan} ██║ ██║{magenta}╚══██╔══╝{yellow}██╔════╝{reset}
{cyan} ██║ ██║{magenta} ██║ {yellow}██║ {reset}
{cyan} ╚██╗ ██╔╝{magenta} ██║ {yellow}██║ {reset}
{cyan} ╚████╔╝ {magenta} ██║ {yellow}╚██████╗{reset}
{cyan} ╚═══╝ {magenta} ╚═╝ {yellow} ╚═════╝{reset}
{dim} Verifiable Trust Community v{version}{reset}
"#,
version = env!("CARGO_PKG_VERSION"),
);
}
fn init_tracing(config: &AppConfig) {
let filter =
EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(&config.log.level));
let subscriber = tracing_subscriber::fmt().with_env_filter(filter);
match config.log.format {
LogFormat::Json => subscriber.json().init(),
LogFormat::Text => subscriber.init(),
}
}