use anyhow::{Context, Result};
use clap_complete::generate;
use colored::Colorize;
use tsafe_cli::cli::{Cli, Shell};
use tsafe_core::audit::AuditEntry;
use crate::helpers::*;
fn entropy_bits(length: usize, pool_size: usize) -> f64 {
if length == 0 || pool_size == 0 {
return 0.0;
}
(length as f64) * (pool_size as f64).log2()
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn cmd_gen(
profile: &str,
key: &str,
length: usize,
charset: &str,
words: Option<usize>,
tags: Vec<String>,
print: bool,
exclude_ambiguous: bool,
) -> Result<()> {
let (value, desc, entropy_note) = if let Some(n) = words {
if n == 0 {
anyhow::bail!("--words must be at least 1");
}
let passphrase = generate_passphrase(n);
let note = format!(
"~{:.0} bits entropy (EFF word list, {n} words)",
entropy_bits(n, 200)
);
(passphrase, format!("{n} words"), note)
} else {
if length == 0 {
anyhow::bail!("--length must be at least 1");
}
let value = if exclude_ambiguous {
tsafe_core::gen::generate_unambiguous(length, charset)
} else {
tsafe_core::gen::generate(length, charset)
};
let pool_size = tsafe_core::gen::effective_pool_size(charset, exclude_ambiguous);
let note = format!(
"~{:.0} bits entropy ({length} chars, pool {pool_size})",
entropy_bits(length, pool_size)
);
let charset_label = if exclude_ambiguous {
format!("{charset} (no ambiguous)")
} else {
charset.to_string()
};
(
value,
format!("{length} chars, charset={charset_label}"),
note,
)
};
let tag_map = parse_tags_map(&tags);
let mut vault = open_vault(profile)?;
vault
.set(key, &value, tag_map)
.context("failed to store generated secret")?;
audit(profile)
.append(&AuditEntry::success(profile, "gen", Some(key)))
.ok();
if print {
println!("{value}");
} else {
println!(
"{} Generated '{}' ({}) — {}",
"✓".green(),
key,
desc,
entropy_note
);
}
Ok(())
}
fn generate_passphrase(count: usize) -> String {
use rand::seq::SliceRandom;
const WORDS: &[&str] = &[
"acid", "acorn", "acre", "acts", "afar", "afoot", "aged", "agile", "aging", "agony",
"ahead", "aide", "aisle", "ajar", "alarm", "alert", "alias", "alibi", "alien", "align",
"amber", "amend", "ample", "anchor", "angel", "anger", "angle", "ankle", "annex", "anvil",
"apart", "apple", "april", "apron", "arena", "argue", "arise", "armor", "arrow", "asset",
"atlas", "atom", "attic", "audio", "audit", "avoid", "awake", "award", "bacon", "badge",
"bagel", "baker", "balmy", "banjo", "barge", "baron", "basin", "batch", "beach", "beard",
"bench", "berry", "birch", "blade", "blame", "blank", "blast", "blaze", "bleed", "blend",
"blimp", "block", "bloom", "blown", "blurt", "board", "boat", "bonus", "booth", "bound",
"brace", "brain", "brave", "bread", "break", "brick", "bride", "brief", "bring", "brisk",
"broad", "broil", "brook", "brunt", "brush", "budge", "buggy", "build", "bulge", "bunch",
"cabin", "cable", "camel", "candy", "cargo", "carry", "catch", "cause", "cedar", "chain",
"chair", "chalk", "champ", "charm", "cheap", "check", "chess", "chief", "child", "chill",
"chips", "chose", "chunk", "civic", "claim", "clamp", "clash", "clasp", "class", "clean",
"clerk", "click", "cliff", "climb", "cling", "clock", "clone", "cloth", "cloud", "coach",
"coast", "coral", "couch", "could", "cover", "crack", "craft", "crane", "crash", "crate",
"crawl", "crazy", "cream", "creek", "crisp", "cross", "crowd", "crush", "cubic", "curve",
"cycle", "daily", "dance", "darts", "debug", "decay", "decoy", "delta", "dense", "depot",
"derby", "desk", "digit", "ditch", "dodge", "doing", "donor", "donut", "draft", "drain",
"drama", "drawn", "dried", "drift", "drill", "drive", "drone", "drove", "dwarf", "dwell",
"eagle", "earth", "easel", "eight", "elder", "elite", "ember", "empty", "envoy", "equal",
];
let mut rng = rand::thread_rng();
(0..count)
.map(|_| *WORDS.choose(&mut rng).unwrap())
.collect::<Vec<&str>>()
.join("-")
}
pub(crate) fn cmd_completions(shell: Shell) -> Result<()> {
use clap::CommandFactory;
let mut buf: Vec<u8> = Vec::new();
generate(shell, &mut Cli::command(), "tsafe", &mut buf);
let script = String::from_utf8_lossy(&buf);
let patched = patch_completions(shell, &script);
print!("{patched}");
Ok(())
}
fn patch_completions(shell: Shell, script: &str) -> String {
match shell {
Shell::Bash => patch_bash(script),
Shell::Zsh => patch_zsh(script),
Shell::Fish => patch_fish(script),
Shell::PowerShell => patch_powershell(script),
_ => script.to_string(),
}
}
fn patch_bash(script: &str) -> String {
let profile_static = "--profile)\n COMPREPLY=($(compgen -f \"${cur}\"))\n return 0\n ;;";
let profile_dynamic = "--profile)\n COMPREPLY=($(compgen -W \"$(tsafe _completions-data profiles 2>/dev/null)\" -- \"${cur}\"))\n return 0\n ;;";
let contract_static = "--contract)\n COMPREPLY=($(compgen -f \"${cur}\"))\n return 0\n ;;";
let contract_dynamic = "--contract)\n COMPREPLY=($(compgen -W \"$(tsafe _completions-data contracts 2>/dev/null)\" -- \"${cur}\"))\n return 0\n ;;";
script
.replace(profile_static, profile_dynamic)
.replace(contract_static, contract_dynamic)
}
fn patch_zsh(script: &str) -> String {
let patched = script
.replace(
"'--profile=[Named vault / profile. Defaults to the persisted default (or '\\''default'\\''). Override with TSAFE_PROFILE env var]:PROFILE:_files'",
"'--profile=[Named vault / profile]:PROFILE:->tsafe_profiles'",
)
.replace(
"'(-p --profile)'{-p,--profile}'=[Named vault / profile. Defaults to the persisted default (or '\\''default'\\''). Override with TSAFE_PROFILE env var]:PROFILE:_files'",
"'(-p --profile)'{-p,--profile}'=[Named vault / profile]:PROFILE:->tsafe_profiles'",
)
.replace(
"'--contract=[Load a named authority contract from the nearest .tsafe.yml (or .tsafe.json) manifest. The contract sets the profile, namespace, allowed/required secrets, allowed targets, and trust posture. Explicit flags (--ns, --keys, --mode, etc.) still override contract values.]:NAME:_files'",
"'--contract=[Named authority contract from nearest .tsafe.yml]:NAME:->tsafe_contracts'",
);
format!(
"{patched}
# tsafe dynamic completion handlers
_tsafe_dynamic() {{
case $state in
tsafe_profiles)
local profiles
profiles=($(tsafe _completions-data profiles 2>/dev/null))
_describe 'profile' profiles
;;
tsafe_contracts)
local contracts
contracts=($(tsafe _completions-data contracts 2>/dev/null))
_describe 'contract' contracts
;;
esac
}}
"
)
}
fn patch_powershell(script: &str) -> String {
let injection = r#"
# Dynamic value completion for --profile and --contract
$prevToken = ''
for ($i = 1; $i -lt $commandElements.Count; $i++) {
if ($commandElements[$i].Value -eq $wordToComplete -and $i -gt 0) {
$prevToken = $commandElements[$i - 1].Value
break
}
}
if ($prevToken -in @('--profile', '-p')) {
$profiles = (tsafe _completions-data profiles 2>$null) -split "`n" |
ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' }
foreach ($p in $profiles) {
if ($p -like "$wordToComplete*") {
[CompletionResult]::new($p, $p, [CompletionResultType]::ParameterValue, "Profile: $p")
}
}
return
}
if ($prevToken -eq '--contract') {
$contracts = (tsafe _completions-data contracts 2>$null) -split "`n" |
ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' }
foreach ($c in $contracts) {
if ($c -like "$wordToComplete*") {
[CompletionResult]::new($c, $c, [CompletionResultType]::ParameterValue, "Contract: $c")
}
}
return
}
"#;
let normalised = script.replace("\r\n", "\n");
normalised.replacen(
"\n $completions = @(switch ($command) {",
&format!("\n{injection}\n $completions = @(switch ($command) {{"),
1,
)
}
fn patch_fish(script: &str) -> String {
format!(
"{script}
# Dynamic completions for --profile and --contract
complete -c tsafe -l profile -f -a '(tsafe _completions-data profiles 2>/dev/null)' -d 'Vault profile name'
complete -c tsafe -n '__fish_seen_subcommand_from exec' -l contract -f -a '(tsafe _completions-data contracts 2>/dev/null)' -d 'Authority contract name'
"
)
}
pub(crate) fn cmd_completions_data(data_type: &str) -> Result<()> {
match data_type {
"profiles" => {
let profiles = tsafe_core::profile::list_profiles().unwrap_or_default();
for p in profiles {
println!("{p}");
}
}
"contracts" => {
let cwd = std::env::current_dir().unwrap_or_default();
if let Some(path) = tsafe_core::contracts::find_contracts_manifest(&cwd) {
if let Ok(contracts) = tsafe_core::contracts::load_contracts(&path) {
for name in contracts.keys() {
println!("{name}");
}
}
}
}
_ => {} }
Ok(())
}