use super::{CliError, load_cli_plugin_host, registered_auth_provider, selected_auth_method};
use bcode_provider_auth_models::{AuthCredentialSource, AuthMethodContribution};
use std::collections::BTreeMap;
use std::io::{IsTerminal as _, Write as _};
use std::path::PathBuf;
pub fn validate_launch_selection() -> Result<(), CliError> {
let config = bcode_config::load_config()?;
let selection = config.resolved_model_selection();
selection
.validate_selection()
.map_err(|error| CliError::InvalidArguments(error.to_string()))?;
bcode_provider_auth::inspect_auth_selection(
&bcode_provider_auth::ProviderRequestContextResolution {
config: &config,
selection,
},
)?;
Ok(())
}
pub fn edit_setting(
file: PathBuf,
keys: &[String],
value: Option<&str>,
remove: bool,
) -> Result<(), CliError> {
if !std::io::stdin().is_terminal() || !std::io::stdout().is_terminal() {
return Err(CliError::InvalidArguments(
"Settings editing requires an interactive review".to_owned(),
));
}
let value = match (remove, value) {
(true, None) => None,
(false, Some(value)) => {
let parsed: toml::Value = toml::from_str(&format!("value = {value}"))
.map_err(|_| CliError::InvalidArguments("Value must be valid TOML".to_owned()))?;
Some(
parsed
.get("value")
.cloned()
.ok_or_else(|| CliError::InvalidArguments("Missing value".to_owned()))?,
)
}
_ => {
return Err(CliError::InvalidArguments(
"Specify --value or --remove".to_owned(),
));
}
};
let edit = bcode_config::edit::plan_field_edit(file, keys, value)?;
println!(
"Configuration file: {}\nField: {}\nOperation: {}",
edit.path().display(),
keys.join(" / "),
if remove {
"Remove override"
} else {
"Set supplied value"
}
);
println!(
"Higher-priority configuration may override this file. Changes apply when configuration is next loaded."
);
print!("Type apply to confirm: ");
std::io::stdout().flush()?;
let mut answer = String::new();
std::io::stdin().read_line(&mut answer)?;
if answer.trim() == "apply" {
edit.apply()?;
println!("Configuration updated.");
}
Ok(())
}
pub fn discover_for_onboarding(enabled: bool) -> Result<(), CliError> {
if enabled {
discover_auth_credentials()
} else {
println!("Automatic credential discovery is disabled.");
Ok(())
}
}
pub fn handle_command(command: super::AuthCommand) -> Result<(), CliError> {
match command {
super::AuthCommand::Discover => discover_auth_credentials(),
super::AuthCommand::Import {
provider,
method,
credential,
source,
profile,
vault,
} => import_auth_credential(
&provider,
&method,
&credential,
source,
profile.as_deref(),
vault,
),
_ => Err(CliError::InvalidArguments(
"Expected credential discovery or import".to_owned(),
)),
}
}
fn authorized_home() -> Result<PathBuf, CliError> {
std::env::home_dir().ok_or_else(|| {
CliError::LoginProfile(
"Home directory is unavailable; credential discovery is disabled.".to_owned(),
)
})
}
pub fn discover_auth_credentials() -> Result<(), CliError> {
let config = bcode_config::load_config()?;
if !config
.onboarding
.credential_discovery_enabled(false, &bcode_config::ProcessConfigEnvironment)
{
println!("Automatic credential discovery is disabled.");
return Ok(());
}
let home = authorized_home()?;
let mut host = load_cli_plugin_host()?;
for provider in host.auth_provider_registry().providers() {
for method in &provider.contribution.methods {
let AuthMethodContribution::SecretFields { fields, .. } = method else {
continue;
};
for field in fields {
for (index, status) in bcode_provider_auth::discovery::discover(true, &home, field)
{
if status == bcode_provider_auth::discovery::CredentialSourceStatus::Missing {
continue;
}
println!(
"{} / {} / {} / source {index}: {} — {status:?}",
provider.contribution.provider_id,
method.method_id(),
field.credential_id,
source_label(&field.discovery_sources[index])
);
}
}
}
}
host.deactivate_all()?;
println!(
"Use bcode auth import PROVIDER --source INDEX to review a copy into sshenv. OAuth sign-ins require a supported fresh login; tokens are not copied."
);
Ok(())
}
fn source_label(source: &AuthCredentialSource) -> String {
match source {
AuthCredentialSource::Environment { name } => format!("environment {name}"),
AuthCredentialSource::JsonFile {
application,
relative_path,
..
} => format!("{application}: ~/{relative_path}"),
}
}
pub fn import_auth_credential(
provider_id: &str,
method_id: &str,
credential_id: &str,
source: usize,
profile: Option<&str>,
vault: Option<PathBuf>,
) -> Result<(), CliError> {
if !std::io::stdin().is_terminal() || !std::io::stdout().is_terminal() {
return Err(CliError::LoginProfile(
"Credential import requires an interactive review.".to_owned(),
));
}
let mut host = load_cli_plugin_host()?;
let result = import_with_host(
&host,
provider_id,
method_id,
credential_id,
source,
profile,
vault,
);
let cleanup = host.deactivate_all().map_err(CliError::from);
result.and(cleanup)
}
fn import_with_host(
host: &bcode_plugin::PluginHost,
provider_id: &str,
method_id: &str,
credential_id: &str,
source: usize,
profile: Option<&str>,
vault: Option<PathBuf>,
) -> Result<(), CliError> {
let provider = registered_auth_provider(host, provider_id)?;
let method = selected_auth_method(&provider, Some(method_id))?;
let AuthMethodContribution::SecretFields { fields, .. } = method else {
return Err(CliError::LoginProfile(
"Sign-in tokens cannot be copied. Use auth login for this method.".to_owned(),
));
};
let field = fields
.iter()
.find(|field| field.credential_id == credential_id)
.ok_or_else(|| {
CliError::LoginProfile("Credential is not declared by this provider.".to_owned())
})?;
let declaration = field.discovery_sources.get(source).ok_or_else(|| {
CliError::LoginProfile("Source is not declared by this provider.".to_owned())
})?;
let config = bcode_config::load_config()?;
let runtime = bcode_config::try_load_runtime_auth_subscriptions()?;
let plan = bcode_provider_auth::enrollment::plan(
&config,
&runtime,
&provider.contribution,
&provider.plugin_id,
method_id,
bcode_provider_auth::enrollment::EnrollmentDestination {
profile: super::registered_auth_profile_hint(&config, &runtime, &provider, profile),
vault,
recipient_key: None,
},
bcode_provider_auth::enrollment::PoolEnrollment::Standalone,
None,
)
.map_err(|error| CliError::LoginProfile(error.to_string()))?;
let resolved = &plan.prepared.resolved;
let lifecycle = bcode_provider_auth::lifecycle::AuthVaultLifecycle::new(
resolved,
provider_id,
&provider.plugin_id,
method,
)
.map_err(|_| {
CliError::LoginProfile(
"Destination profile ownership or method is inconsistent.".to_owned(),
)
})?;
println!("Copy from: {}", source_label(declaration));
println!(
"Provider: {provider_id}\nProfile: {}\nVault: {}",
resolved.profile_name,
resolved
.profile
.settings
.get("vault")
.map_or("(unresolved)", String::as_str)
);
println!("The source will not be modified. No remote verification is performed.");
print!("Type import to confirm: ");
std::io::stdout().flush()?;
let mut answer = String::new();
std::io::stdin().read_line(&mut answer)?;
if answer.trim() != "import" {
return Ok(());
}
let value = bcode_provider_auth::discovery::read_selected(&authorized_home()?, field, source)
.map_err(|_| {
CliError::LoginProfile(
"Selected credential source is unavailable or incompatible.".to_owned(),
)
})?
.ok_or_else(|| {
CliError::LoginProfile("Selected credential is no longer present.".to_owned())
})?;
lifecycle
.import_new(BTreeMap::from([(
credential_id.to_owned(),
value.to_string(),
)]))
.map_err(|_| {
CliError::LoginProfile(
"Import could not be completed safely; inspect the destination before retrying."
.to_owned(),
)
})?;
bcode_provider_auth::enrollment::complete(&plan).map_err(|_| {
CliError::LoginProfile(
"Credential copied, but authentication metadata publication failed. Inspect authentication state before retrying; do not repeat import merely to repair metadata.".to_owned(),
)
})?;
println!(
"Credential copied into the owned sshenv profile. Connection has not been remotely verified."
);
Ok(())
}