use std::path::{Path, PathBuf};
use clap::{Args, Subcommand};
use net_sdk::identity::EntityId;
use net_sdk::org::NodeAuthority;
use serde::Serialize;
use crate::commands::identity::{parse_entity_hex, read_identity_file};
use crate::commands::org::{OrgCertFile, OrgFloorsFile, ORG_FILE_VERSION};
use crate::error::{generic, invalid_args, sdk, CliError};
use crate::prelude::{emit_value, OutputFormat};
#[derive(Subcommand, Debug)]
pub enum NodeCommand {
Adopt(AdoptArgs),
}
#[derive(Args, Debug)]
pub struct AdoptArgs {
#[arg(long, value_name = "PATH")]
pub cert: PathBuf,
#[arg(long, value_name = "PATH", conflicts_with = "entity")]
pub identity: Option<PathBuf>,
#[arg(long, value_name = "HEX")]
pub entity: Option<String>,
#[arg(long = "authority-dir", value_name = "DIR")]
pub authority_dir: Option<PathBuf>,
#[arg(long, value_name = "PATH")]
pub floors: Option<PathBuf>,
#[arg(long = "skew-secs", default_value_t = 0)]
pub skew_secs: u64,
#[arg(long)]
pub insecure_permissions: bool,
}
pub async fn run(cmd: NodeCommand, output: Option<OutputFormat>) -> Result<(), CliError> {
match cmd {
NodeCommand::Adopt(args) => run_adopt(args, output).await,
}
}
async fn run_adopt(args: AdoptArgs, output: Option<OutputFormat>) -> Result<(), CliError> {
if args.skew_secs > net_sdk::org::MAX_TOKEN_CLOCK_SKEW_SECS {
return Err(invalid_args(format!(
"--skew-secs {} exceeds the ceiling of {} seconds (MAX_TOKEN_CLOCK_SKEW_SECS)",
args.skew_secs,
net_sdk::org::MAX_TOKEN_CLOCK_SKEW_SECS
)));
}
let entity: EntityId = match (&args.identity, &args.entity) {
(Some(identity_path), None) => {
let file = read_identity_file(identity_path, args.insecure_permissions).await?;
parse_entity_hex(&file.public_key_hex)?
}
(None, Some(hex_id)) => parse_entity_hex(hex_id)?,
(None, None) => {
return Err(invalid_args(
"pass --identity <PATH> (node identity file) or --entity <HEX>",
));
}
(Some(_), Some(_)) => unreachable!("clap conflicts_with enforces exclusivity"),
};
let cert_text = tokio::fs::read_to_string(&args.cert).await.map_err(|e| {
generic(format!(
"failed to read certificate file {}: {e}",
args.cert.display()
))
})?;
let cert_file: OrgCertFile = serde_json::from_str(&cert_text).map_err(|e| {
invalid_args(format!(
"certificate file {} failed to parse: {e}",
args.cert.display()
))
})?;
if cert_file.version != ORG_FILE_VERSION {
return Err(invalid_args(format!(
"certificate file {} has unsupported version {}",
args.cert.display(),
cert_file.version
)));
}
let floors_bundle = match &args.floors {
Some(bundle_path) => {
let text = tokio::fs::read_to_string(bundle_path).await.map_err(|e| {
generic(format!(
"failed to read floors bundle {}: {e}",
bundle_path.display()
))
})?;
let floors_file: OrgFloorsFile = serde_json::from_str(&text).map_err(|e| {
invalid_args(format!(
"floors bundle {} failed to parse: {e}",
bundle_path.display()
))
})?;
if floors_file.version != ORG_FILE_VERSION {
return Err(invalid_args(format!(
"floors bundle {} has unsupported version {}",
bundle_path.display(),
floors_file.version
)));
}
Some(floors_file.bundle)
}
None => None,
};
let dir = match args.authority_dir.clone() {
Some(explicit) => explicit,
None => default_authority_dir().ok_or_else(|| {
invalid_args(
"cannot determine the default authority directory on this platform \
(no config dir); pass --authority-dir explicitly. Refusing to fall \
back to the working directory — the authority dir holds the raw \
owner audience key.",
)
})?,
};
#[cfg(windows)]
if args.authority_dir.is_some() {
eprintln!(
"warning: custom --authority-dir {}: the authority directory's own DACL is \
created owner-only / re-validated, but its PRE-EXISTING parent directories \
are not walked on Windows; keep it under a per-user protected location \
restricted to your account",
dir.display()
);
}
let authority = NodeAuthority::adopt(
&dir,
cert_file.cert,
&entity,
args.skew_secs,
floors_bundle.as_ref(),
)
.map_err(|e| sdk(format!("adopt refused: {e}")))?;
let summary = AdoptOutput {
authority_dir: dir.display().to_string(),
owner_org_hex: hex::encode(authority.owner_org().as_bytes()),
member_hex: hex::encode(entity.as_bytes()),
generation: authority.config.owner_cert.generation,
not_after: authority.config.owner_cert.not_after,
files: NodeAuthority::file_names(),
floors_applied: floors_bundle.as_ref().map(|b| b.floors().len()),
};
emit_value(OutputFormat::resolve_oneshot(output), &summary)
.map_err(|e| generic(format!("write summary: {e}")))?;
Ok(())
}
#[derive(Debug, Serialize)]
struct AdoptOutput {
authority_dir: String,
owner_org_hex: String,
member_hex: String,
generation: u32,
not_after: u64,
files: [&'static str; 3],
#[serde(skip_serializing_if = "Option::is_none")]
floors_applied: Option<usize>,
}
fn default_authority_dir() -> Option<PathBuf> {
dirs::config_dir().map(|d| d.join("net-mesh").join("authority"))
}
#[allow(unused)]
pub(crate) fn authority_dir_for(root: &Path) -> PathBuf {
root.join("authority")
}