use anyhow::Result;
use clap::Args;
use crate::cli::input;
use crate::crypto::envelope::Envelope;
use crate::crypto::signing::SignedEnvelope;
use crate::env::{self, filter};
use crate::keys;
use crate::transfer;
use crate::ui::display;
#[derive(Args)]
pub struct ShareArgs {
pub file: Option<String>,
#[arg(long)]
pub secret: Option<String>,
#[arg(long)]
pub label: Option<String>,
#[arg(long, value_name = "KEY")]
pub r#as: Option<String>,
#[arg(long)]
pub to: Option<String>,
#[arg(long)]
pub output: Option<String>,
#[arg(long, value_parser = clap::value_parser!(u16).range(2..=5))]
pub words: Option<u16>,
#[arg(long)]
pub exclude: Option<String>,
#[arg(long)]
pub include: Option<String>,
#[arg(long)]
pub no_interpolate: bool,
#[arg(long, value_name = "NAME")]
pub env: Option<String>,
#[arg(long)]
pub no_filter: bool,
#[arg(long, env = "ENSEAL_RELAY")]
pub relay: Option<String>,
#[arg(long, short)]
pub quiet: bool,
}
pub async fn run(args: ShareArgs) -> Result<()> {
if args.env.is_some() && args.file.is_some() {
anyhow::bail!("--env and a file argument are mutually exclusive");
}
if args.output.is_some() && args.to.is_none() {
anyhow::bail!("--output requires --to (file drop is only available in identity mode)");
}
if args.no_filter && (args.include.is_some() || args.exclude.is_some()) {
anyhow::bail!("--no-filter cannot be used with --include or --exclude");
}
let manifest = crate::config::Manifest::load(None)?;
let effective_relay: Option<String> = args.relay.clone().or(manifest.defaults.relay.clone());
let words: u16 = args
.words
.unwrap_or_else(|| manifest.defaults.words.map(|w| w as u16).unwrap_or(2));
let effective_to: Option<String> = args
.to
.clone()
.or(manifest.identity.default_recipient.clone());
let file_arg = if let Some(ref profile) = args.env {
let resolved = env::profile::resolve(profile, std::path::Path::new("."))?;
Some(resolved.to_string_lossy().into_owned())
} else {
args.file.clone()
};
let payload = input::select_input(
args.secret.as_deref(),
args.r#as.as_deref(),
args.label.as_deref(),
file_arg.as_deref(),
args.quiet,
)?;
let content = if payload.format == input::PayloadFormat::Env && !args.no_filter {
let env_file = env::parser::parse(&payload.content)?;
if !args.quiet {
let issues = env::validator::validate(&env_file);
for issue in &issues {
display::warning(&issue.message);
}
}
let env_file = if args.no_interpolate {
env_file
} else {
env::interpolation::interpolate(&env_file)?
};
let exclude_pattern =
build_exclude_pattern(args.exclude.as_deref(), &manifest.filter.exclude);
let filtered = filter::filter(
&env_file,
args.include.as_deref(),
exclude_pattern.as_deref(),
)?;
if filtered.var_count() == 0 {
anyhow::bail!("all variables were filtered out (check --include/--exclude patterns)");
}
filtered.to_string()
} else {
payload.content.clone()
};
let mut envelope = Envelope::seal(&content, payload.format.clone(), payload.label.clone())?;
envelope.metadata.project = manifest.project_name();
if !args.quiet {
if let Some(count) = envelope.metadata.var_count {
display::info("Secrets:", &format!("{} variables", count));
}
if let Some(ref label) = envelope.metadata.label {
display::info("Label:", label);
}
if let Some(ref project) = envelope.metadata.project {
display::info("Project:", project);
}
}
if let Some(ref recipient_name) = effective_to {
send_identity_mode(
&args,
&envelope,
recipient_name,
effective_relay.as_deref(),
words,
)
.await
} else {
send_anonymous_mode(&args, &envelope, effective_relay.as_deref(), words).await
}
}
fn build_exclude_pattern(
cli_exclude: Option<&str>,
manifest_patterns: &[String],
) -> Option<String> {
let mut patterns: Vec<&str> = manifest_patterns.iter().map(String::as_str).collect();
if let Some(p) = cli_exclude {
patterns.push(p);
}
if patterns.is_empty() {
None
} else {
Some(patterns.join("|"))
}
}
async fn send_anonymous_mode(
args: &ShareArgs,
envelope: &Envelope,
relay_url: Option<&str>,
words: u16,
) -> Result<()> {
if let Some(relay_url) = relay_url {
let code = transfer::relay::generate_code();
if !args.quiet {
display::info("Share code:", &code);
display::info("Expires:", "on first receive");
} else {
println!("{}", code);
}
let wire_bytes = envelope.to_bytes()?;
transfer::relay::send(&wire_bytes, relay_url, &code).await?;
if !args.quiet {
display::ok("sent");
}
} else {
let (code, mailbox) = transfer::wormhole::create_mailbox(None, words.into()).await?;
if !args.quiet {
display::info("Share code:", &code);
display::info("Expires:", "on first receive (server-dependent TTL)");
} else {
println!("{}", code);
}
transfer::wormhole::send(envelope, mailbox).await?;
if !args.quiet {
display::ok("sent");
}
}
Ok(())
}
async fn send_identity_mode(
args: &ShareArgs,
envelope: &Envelope,
recipient_name: &str,
relay_url: Option<&str>,
words: u16,
) -> Result<()> {
let identities = keys::resolve_to_identities(recipient_name)?;
let store = keys::store::KeyStore::open()?;
let sender = keys::identity::EnsealIdentity::load(&store)?;
let trusted_keys: Vec<keys::identity::TrustedKey> = identities
.iter()
.map(|id| keys::identity::TrustedKey::load(&store, id))
.collect::<Result<Vec<_>>>()?;
let age_recipients: Vec<&age::x25519::Recipient> =
trusted_keys.iter().map(|k| &k.age_recipient).collect();
let display_name = if identities.len() == 1 {
identities[0].clone()
} else {
format!("{} ({} recipients)", recipient_name, identities.len())
};
if !args.quiet {
display::info("To:", &display_name);
if identities.len() == 1 {
display::info("Fingerprint:", &trusted_keys[0].fingerprint());
}
}
if let Some(ref output_dir) = args.output {
let filename = if identities.len() > 1 {
recipient_name.to_string()
} else {
identities[0].clone()
};
let dest = transfer::filedrop::write(
envelope,
&age_recipients,
&sender,
std::path::Path::new(output_dir),
&filename,
)?;
if !args.quiet {
display::ok(&format!(
"encrypted to {}, written to {}",
display_name,
dest.display()
));
}
} else if let Some(relay_url) = relay_url {
let inner_bytes = envelope.to_bytes()?;
let signed = SignedEnvelope::seal(&inner_bytes, &age_recipients, &sender)?;
let wire_bytes = signed.to_bytes()?;
for tk in &trusted_keys {
let channel_id = tk.channel_id();
transfer::relay::push(&wire_bytes, relay_url, &channel_id).await?;
}
if !args.quiet {
display::ok(&format!("pushed to {}", display_name));
}
} else {
let (code, wire_bytes, mailbox) = transfer::identity::create_mailbox(
envelope,
&age_recipients,
&sender,
None,
words.into(),
)
.await?;
if !args.quiet {
display::info("Share code:", &code);
display::info("Expires:", "on first receive (server-dependent TTL)");
} else {
println!("{}", code);
}
transfer::identity::send(wire_bytes, mailbox).await?;
if !args.quiet {
display::ok(&format!("encrypted to {}, signed by you", display_name));
}
}
Ok(())
}