use std::path::Path;
use anyhow::{anyhow, bail};
use serde::de::DeserializeOwned;
use serde_json::Value;
use trusty_mpm::secrets_client;
use trusty_secrets::api::methods::{ScopeKind, ScopesResponse, method};
use trusty_secrets::server::{ClientError, ErrorKind, OnDemandSecrets};
use trusty_secrets::{SecretKey, VaultName};
use super::ValueSource;
const MAX_GROUP_LEN: usize = 16;
pub(crate) struct Ctx<'a> {
pub(crate) client: &'a OnDemandSecrets,
pub(crate) project: &'a Path,
pub(crate) clipboard: &'a dyn ValueSource,
pub(crate) stdin: &'a dyn ValueSource,
pub(crate) ci: bool,
}
impl Ctx<'_> {
fn params(&self, params: Value) -> anyhow::Result<Value> {
secrets_client::with_project(self.project, params)
.ok_or_else(|| anyhow!("tm secrets: the working directory is not UTF-8"))
}
pub(crate) async fn call_raw(
&self,
method: &str,
params: Value,
) -> anyhow::Result<Result<Value, ClientError>> {
let params = self.params(params)?;
Ok(self.client.call(method, params).await)
}
pub(crate) async fn call<T: DeserializeOwned>(
&self,
method: &'static str,
params: Value,
) -> anyhow::Result<T> {
let result = self
.call_raw(method, params)
.await?
.map_err(|e| anyhow!(describe(&e, self.client.socket())))?;
serde_json::from_value(result)
.map_err(|_| anyhow!("tm secrets: {method}: the answer did not decode"))
}
pub(crate) async fn project_vault(&self) -> anyhow::Result<VaultName> {
let scopes: ScopesResponse = self.call(method::SCOPES, Value::Null).await?;
scopes
.scopes
.into_iter()
.find(|scope| scope.kind == ScopeKind::Project)
.map(|scope| scope.vault)
.ok_or_else(|| anyhow!("tm secrets: the server reported no project scope"))
}
}
pub(crate) fn describe(error: &ClientError, socket: &Path) -> String {
secrets_client::describe("tm secrets", error, socket)
}
pub(crate) fn is_project_refusal(error: &ClientError) -> bool {
matches!(
error,
ClientError::Rpc(failure) if matches!(
failure.kind,
Some(
ErrorKind::ProjectInvalid
| ErrorKind::ProjectUnresolved
| ErrorKind::RemoteHostUnsupported
)
)
)
}
pub(crate) fn entry_key(key: &str, group: Option<&str>) -> anyhow::Result<SecretKey> {
check_group(group)?;
let name = match group {
None => key.to_owned(),
Some(group) => format!("{group}.{key}"),
};
SecretKey::new(&name).map_err(|e| anyhow!("tm secrets: {e}"))
}
pub(crate) fn check_group(group: Option<&str>) -> anyhow::Result<()> {
let Some(group) = group else {
return Ok(());
};
let valid = !group.is_empty()
&& group.len() <= MAX_GROUP_LEN
&& group
.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-'));
if !valid {
bail!("tm secrets: a group is 1 to 16 of [A-Za-z0-9_-]");
}
Ok(())
}