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),
Status(crate::commands::lifecycle::StatusArgs),
}
#[derive(Args, Debug)]
pub struct AdoptArgs {
#[arg(long)]
pub inspect_target: bool,
#[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, value_name = "PATH")]
pub audience: 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>,
config_path: Option<&Path>,
profile_name: &str,
) -> Result<(), CliError> {
match cmd {
NodeCommand::Adopt(args) => run_adopt(args, output, config_path, profile_name).await,
NodeCommand::Status(args) => {
crate::commands::lifecycle::run_status(args, output, profile_name).await
}
}
}
async fn run_adopt(
args: AdoptArgs,
output: Option<OutputFormat>,
config_path: Option<&Path>,
profile_name: &str,
) -> 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 dir = resolve_authority_dir(args.authority_dir.as_deref())?;
if args.inspect_target {
let profile = crate::context::resolve_profile(config_path, profile_name).await?;
let mut target = crate::target::TargetInspection::local(&profile, "persistent_store");
target.store = Some(dir);
target.source = Some(args.cert.clone());
target.subject_fingerprint = Some(crate::target::public_fingerprint(entity.as_bytes()));
target.provenance(
"store",
if args.authority_dir.is_some() {
"flag"
} else {
"default"
},
);
target.provenance("source", "flag");
target.provenance(
"subject",
if args.identity.is_some() {
"identity_file"
} else {
"flag"
},
);
let view = AdoptInspection {
target,
identity_source: args.identity,
floors_source: args.floors,
files: NodeAuthority::file_names(),
};
return emit_value(OutputFormat::resolve_oneshot(output), &view)
.map_err(|e| generic(format!("write inspection: {e}")));
}
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,
};
#[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 audience = match &args.audience {
Some(path) => Some(super::org::load_org_audience(path, false).await?),
None => None,
};
let authority = match &audience {
Some(audience) => NodeAuthority::adopt_with_audience(
&dir,
cert_file.cert,
&entity,
args.skew_secs,
floors_bundle.as_ref(),
audience,
),
None => NodeAuthority::adopt(
&dir,
cert_file.cert,
&entity,
args.skew_secs,
floors_bundle.as_ref(),
),
}
.map_err(|e| sdk(format!("adopt refused: {e}")))?;
super::lifecycle::clear_org_left(&dir);
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(Serialize)]
struct AdoptInspection {
#[serde(flatten)]
target: crate::target::TargetInspection,
#[serde(skip_serializing_if = "Option::is_none")]
identity_source: Option<PathBuf>,
#[serde(skip_serializing_if = "Option::is_none")]
floors_source: Option<PathBuf>,
files: [&'static str; 3],
}
fn resolve_authority_dir(explicit: Option<&Path>) -> Result<PathBuf, CliError> {
explicit
.map(Path::to_path_buf)
.or_else(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.",
)
})
}
#[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")
}