386 lines
11 KiB
Rust
386 lines
11 KiB
Rust
use std::path::PathBuf;
|
|
|
|
use anyhow::{Result, bail};
|
|
use clap::{Parser, Subcommand, ValueEnum};
|
|
use frxd::config::Config;
|
|
use frxd::crypto::Keypair;
|
|
use frxd::message::{EXPOSURE_FULL, EXPOSURE_METADATA};
|
|
use frxd::{commands, node, onboard, relay};
|
|
|
|
#[derive(Parser)]
|
|
#[command(
|
|
name = "frxd",
|
|
version,
|
|
about = "FRX member node — querier, responder, and local index (Draft 0.5)"
|
|
)]
|
|
struct Cli {
|
|
#[arg(long, global = true, default_value = "frxd.toml")]
|
|
config: PathBuf,
|
|
#[arg(long)]
|
|
onboarding: bool,
|
|
#[arg(long)]
|
|
listen: Option<String>,
|
|
#[command(subcommand)]
|
|
command: Option<Command>,
|
|
}
|
|
|
|
#[derive(Subcommand)]
|
|
enum Command {
|
|
Init {
|
|
#[arg(long, default_value = "member")]
|
|
name: String,
|
|
#[arg(long, default_value = "127.0.0.1:7701")]
|
|
listen: String,
|
|
#[arg(long, default_value = "http://127.0.0.1:7700")]
|
|
relay: Vec<String>,
|
|
#[arg(long, default_value = "./frx-data")]
|
|
data_dir: String,
|
|
#[arg(long)]
|
|
force: bool,
|
|
#[arg(long)]
|
|
id: Option<String>,
|
|
#[arg(long)]
|
|
registry: Option<String>,
|
|
#[arg(long)]
|
|
ma_key: Option<String>,
|
|
#[arg(long)]
|
|
ca_cert: Option<String>,
|
|
#[arg(long)]
|
|
allow_insecure: bool,
|
|
},
|
|
Add {
|
|
path: PathBuf,
|
|
#[arg(long)]
|
|
name: Option<String>,
|
|
#[arg(long)]
|
|
shared: bool,
|
|
#[arg(long, value_enum, default_value_t = Exposure::Full)]
|
|
exposure: Exposure,
|
|
},
|
|
Reindex,
|
|
Search {
|
|
text: String,
|
|
#[arg(long, default_value_t = 10)]
|
|
limit: usize,
|
|
},
|
|
Query {
|
|
text: String,
|
|
#[arg(long)]
|
|
max_results: Option<usize>,
|
|
#[arg(long)]
|
|
timeout_ms: Option<u64>,
|
|
#[arg(long)]
|
|
local_only: bool,
|
|
},
|
|
Serve,
|
|
Relay {
|
|
#[arg(long, default_value = "127.0.0.1:7700")]
|
|
listen: String,
|
|
#[arg(long, default_value_t = relay::DEFAULT_CAPACITY)]
|
|
capacity: usize,
|
|
#[arg(long = "peer")]
|
|
peers: Vec<String>,
|
|
#[arg(long)]
|
|
url: Option<String>,
|
|
#[arg(long)]
|
|
registry: Option<String>,
|
|
#[arg(long)]
|
|
ma_key: Option<String>,
|
|
#[arg(long)]
|
|
ca_cert: Option<String>,
|
|
#[arg(long)]
|
|
allow_insecure: bool,
|
|
#[arg(long, default_value_t = 3)]
|
|
max_hops: usize,
|
|
},
|
|
Status,
|
|
Member {
|
|
#[command(subcommand)]
|
|
command: MemberCommand,
|
|
},
|
|
Key {
|
|
#[command(subcommand)]
|
|
command: KeyCommand,
|
|
},
|
|
Registry {
|
|
#[arg(long, default_value = "./frx-registry")]
|
|
dir: PathBuf,
|
|
#[command(subcommand)]
|
|
command: RegistryCommand,
|
|
},
|
|
Aggregates {
|
|
#[arg(long)]
|
|
from: Option<String>,
|
|
#[arg(long)]
|
|
period: Option<String>,
|
|
#[arg(long)]
|
|
timeout_ms: Option<u64>,
|
|
},
|
|
}
|
|
|
|
#[derive(Subcommand)]
|
|
enum MemberCommand {
|
|
Add {
|
|
name: String,
|
|
pubkey: String,
|
|
#[arg(long, default_value = "source")]
|
|
class: String,
|
|
#[arg(long = "previous")]
|
|
previous: Vec<String>,
|
|
},
|
|
Remove {
|
|
name: String,
|
|
},
|
|
List,
|
|
}
|
|
|
|
#[derive(Subcommand)]
|
|
enum KeyCommand {
|
|
Show,
|
|
ShowEnc,
|
|
Rotate,
|
|
}
|
|
|
|
#[derive(Subcommand)]
|
|
enum RegistryCommand {
|
|
Init {
|
|
#[arg(long, default_value = "frx.invalid")]
|
|
zone: String,
|
|
},
|
|
Add {
|
|
id: String,
|
|
pubkey: String,
|
|
#[arg(long, default_value = "source")]
|
|
class: String,
|
|
#[arg(long)]
|
|
not_before: Option<u64>,
|
|
#[arg(long)]
|
|
not_after: Option<u64>,
|
|
#[arg(long)]
|
|
enc_key: Option<String>,
|
|
},
|
|
SetEncKey {
|
|
id: String,
|
|
enc_key: String,
|
|
},
|
|
AddKey {
|
|
id: String,
|
|
pubkey: String,
|
|
#[arg(long)]
|
|
not_before: Option<u64>,
|
|
#[arg(long)]
|
|
not_after: Option<u64>,
|
|
},
|
|
RevokeKey {
|
|
id: String,
|
|
pubkey: String,
|
|
},
|
|
Remove {
|
|
id: String,
|
|
},
|
|
List,
|
|
Applications,
|
|
Approve {
|
|
id: String,
|
|
#[arg(long)]
|
|
registry_url: Option<String>,
|
|
},
|
|
Invite {
|
|
id: String,
|
|
#[arg(long)]
|
|
registry_url: Option<String>,
|
|
},
|
|
SetRelays {
|
|
#[arg(required = true)]
|
|
relays: Vec<String>,
|
|
},
|
|
Show,
|
|
Serve {
|
|
#[arg(long, default_value = "127.0.0.1:7800")]
|
|
listen: String,
|
|
},
|
|
}
|
|
|
|
#[derive(Clone, Copy, ValueEnum)]
|
|
enum Exposure {
|
|
Metadata,
|
|
Full,
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() -> Result<()> {
|
|
let cli = Cli::parse();
|
|
if cli.onboarding {
|
|
let mut input = std::io::stdin().lock();
|
|
let mut output = std::io::stdout().lock();
|
|
onboard::run(&mut input, &mut output, &cli.config, cli.listen.as_deref()).await?;
|
|
return Ok(());
|
|
}
|
|
let Some(command) = cli.command else {
|
|
bail!("no subcommand given (try --onboarding or --help)");
|
|
};
|
|
match command {
|
|
Command::Init {
|
|
name,
|
|
listen,
|
|
relay,
|
|
data_dir,
|
|
force,
|
|
id,
|
|
registry,
|
|
ma_key,
|
|
ca_cert,
|
|
allow_insecure,
|
|
} => {
|
|
if cli.config.exists() && !force {
|
|
bail!(
|
|
"config {} already exists (use --force to overwrite)",
|
|
cli.config.display()
|
|
);
|
|
}
|
|
if registry.is_some() && ma_key.is_none() {
|
|
bail!("--ma-key is required with --registry");
|
|
}
|
|
let mut config = Config::new(&name, &listen, relay, &data_dir);
|
|
config.node.id = id;
|
|
config.node.registry = registry;
|
|
config.node.ma_key = ma_key;
|
|
config.node.ca_cert = ca_cert;
|
|
config.node.allow_insecure = allow_insecure;
|
|
if config.node.registry.is_none() {
|
|
config.node.dev_bootstrap = true;
|
|
println!(
|
|
"warning: no registry configured; development open bootstrap (any valid key is accepted; do not deploy)"
|
|
);
|
|
}
|
|
let key = Keypair::generate();
|
|
config.save_key(&key)?;
|
|
let (enc_secret, enc_public) = frxd::crypto::generate_enc_keypair();
|
|
config.save_enc_key(&enc_secret)?;
|
|
config.save(&cli.config)?;
|
|
println!("wrote config {}", cli.config.display());
|
|
println!("wrote key {}", config.key_path().display());
|
|
println!("pubkey {}", key.public_hex());
|
|
println!("enc pubkey {enc_public}");
|
|
println!("data dir {}", config.data_dir().display());
|
|
}
|
|
Command::Add {
|
|
path,
|
|
name,
|
|
shared,
|
|
exposure,
|
|
} => {
|
|
let exposure = match exposure {
|
|
Exposure::Metadata => EXPOSURE_METADATA,
|
|
Exposure::Full => EXPOSURE_FULL,
|
|
};
|
|
commands::add(&cli.config, &path, name, shared, exposure)?;
|
|
}
|
|
Command::Reindex => commands::reindex(&cli.config)?,
|
|
Command::Search { text, limit } => commands::search(&cli.config, &text, limit).await?,
|
|
Command::Query {
|
|
text,
|
|
max_results,
|
|
timeout_ms,
|
|
local_only,
|
|
} => {
|
|
commands::query(&cli.config, &text, max_results, timeout_ms, !local_only).await?;
|
|
}
|
|
Command::Serve => {
|
|
let config = Config::load(&cli.config)?;
|
|
let handle = node::Node::start(config).await?;
|
|
println!("frxd listening on http://{}", handle.addr);
|
|
println!("pubkey {}", handle.pubkey);
|
|
println!("control API POST http://{}/v1/local/query", handle.addr);
|
|
tokio::signal::ctrl_c().await?;
|
|
}
|
|
Command::Relay {
|
|
listen,
|
|
capacity,
|
|
peers,
|
|
url,
|
|
registry,
|
|
ma_key,
|
|
ca_cert,
|
|
allow_insecure,
|
|
max_hops,
|
|
} => {
|
|
let options = relay::RelayOptions {
|
|
capacity,
|
|
peers,
|
|
url,
|
|
registry,
|
|
ma_key,
|
|
ca_cert,
|
|
allow_insecure,
|
|
max_hops,
|
|
};
|
|
let (listener, addr) = relay::bind(&listen).await?;
|
|
println!("relay listening on http://{addr} (capacity {capacity})");
|
|
relay::run(listener, options).await?;
|
|
}
|
|
Command::Status => commands::status(&cli.config).await?,
|
|
Command::Member { command } => match command {
|
|
MemberCommand::Add {
|
|
name,
|
|
pubkey,
|
|
class,
|
|
previous,
|
|
} => commands::member_add(&cli.config, &name, &pubkey, &class, &previous)?,
|
|
MemberCommand::Remove { name } => commands::member_remove(&cli.config, &name)?,
|
|
MemberCommand::List => commands::member_list(&cli.config)?,
|
|
},
|
|
Command::Key { command } => match command {
|
|
KeyCommand::Show => commands::key_show(&cli.config)?,
|
|
KeyCommand::ShowEnc => commands::key_show_enc(&cli.config)?,
|
|
KeyCommand::Rotate => commands::key_rotate(&cli.config)?,
|
|
},
|
|
Command::Registry { dir, command } => match command {
|
|
RegistryCommand::Init { zone } => commands::registry_init(&dir, &zone)?,
|
|
RegistryCommand::Add {
|
|
id,
|
|
pubkey,
|
|
class,
|
|
not_before,
|
|
not_after,
|
|
enc_key,
|
|
} => {
|
|
commands::registry_add(&dir, &id, &pubkey, &class, not_before, not_after, enc_key)?
|
|
}
|
|
RegistryCommand::SetEncKey { id, enc_key } => {
|
|
commands::registry_set_enc_key(&dir, &id, &enc_key)?
|
|
}
|
|
RegistryCommand::AddKey {
|
|
id,
|
|
pubkey,
|
|
not_before,
|
|
not_after,
|
|
} => commands::registry_add_key(&dir, &id, &pubkey, not_before, not_after)?,
|
|
RegistryCommand::RevokeKey { id, pubkey } => {
|
|
commands::registry_revoke_key(&dir, &id, &pubkey)?
|
|
}
|
|
RegistryCommand::Remove { id } => commands::registry_remove(&dir, &id)?,
|
|
RegistryCommand::List => commands::registry_list(&dir)?,
|
|
RegistryCommand::Applications => commands::registry_applications(&dir)?,
|
|
RegistryCommand::Approve { id, registry_url } => {
|
|
commands::registry_approve(&dir, &id, registry_url.as_deref())?
|
|
}
|
|
RegistryCommand::Invite { id, registry_url } => {
|
|
commands::registry_invite(&dir, &id, registry_url.as_deref())?
|
|
}
|
|
RegistryCommand::SetRelays { relays } => commands::registry_set_relays(&dir, &relays)?,
|
|
RegistryCommand::Show => commands::registry_show(&dir)?,
|
|
RegistryCommand::Serve { listen } => commands::registry_serve(&dir, &listen).await?,
|
|
},
|
|
Command::Aggregates {
|
|
from,
|
|
period,
|
|
timeout_ms,
|
|
} => {
|
|
commands::aggregates(&cli.config, from.as_deref(), period.as_deref(), timeout_ms)
|
|
.await?
|
|
}
|
|
}
|
|
Ok(())
|
|
}
|