use arch_program::pubkey::Pubkey;
use crate::{ArchSigner, LocalSigner, RemoteSigner, SignError};
const URL: &str = "COSIGNER_URL";
const TOKEN: &str = "COSIGNER_TOKEN";
const ROLE: &str = "COSIGNER_ROLE";
const PUBKEY: &str = "COSIGNER_PUBKEY";
const KEY_PATH: &str = "ARCH_KEY_PATH";
enum Backend {
Remote,
Local,
}
struct Env<'a> {
prefix: &'a str,
}
impl Env<'_> {
fn prefixed_name(&self, base: &str) -> Option<String> {
(!self.prefix.is_empty()).then(|| format!("{}_{base}", self.prefix))
}
fn prefixed(&self, base: &str) -> Option<String> {
self.prefixed_name(base).and_then(|name| get(&name))
}
fn bare(&self, base: &str) -> Option<String> {
get(base)
}
fn value(&self, base: &str) -> Option<String> {
self.prefixed(base).or_else(|| self.bare(base))
}
fn describe(&self, base: &str) -> String {
match self.prefixed_name(base) {
Some(prefixed) => format!("{prefixed} (or {base})"),
None => base.to_string(),
}
}
}
fn get(name: &str) -> Option<String> {
std::env::var(name).ok().filter(|v| !v.is_empty())
}
pub(crate) fn resolve(prefix: &str) -> Result<ArchSigner, SignError> {
let env = Env {
prefix: prefix.trim_end_matches('_'),
};
match choose_backend(&env)? {
Backend::Remote => build_remote(&env),
Backend::Local => {
let path = env
.value(KEY_PATH)
.expect("a local backend choice implies a key path");
Ok(ArchSigner::Local(LocalSigner::from_key_file(&path)?))
}
}
}
fn choose_backend(env: &Env) -> Result<Backend, SignError> {
if !env.prefix.is_empty() {
match (
env.prefixed(URL).is_some(),
env.prefixed(KEY_PATH).is_some(),
) {
(true, true) => {
return Err(SignError::Config(format!(
"both {0}_{URL} and {0}_{KEY_PATH} are set; configure exactly one backend",
env.prefix
)))
}
(true, false) => return Ok(Backend::Remote),
(false, true) => return Ok(Backend::Local),
(false, false) => {}
}
}
match (env.bare(URL).is_some(), env.bare(KEY_PATH).is_some()) {
(true, true) => Err(SignError::Config(format!(
"both {URL} and {KEY_PATH} are set; configure exactly one backend"
))),
(true, false) => Ok(Backend::Remote),
(false, true) => Ok(Backend::Local),
(false, false) => Err(SignError::Config(format!(
"no signer configured: set {} (remote) or {} (local)",
env.describe(URL),
env.describe(KEY_PATH)
))),
}
}
fn build_remote(env: &Env) -> Result<ArchSigner, SignError> {
let url = env
.value(URL)
.expect("a remote backend choice implies a URL");
let mut missing = Vec::new();
let mut want = |base: &str| {
let value = env.value(base);
if value.is_none() {
missing.push(env.describe(base));
}
value
};
let token = want(TOKEN);
let role = want(ROLE);
let pubkey_hex = want(PUBKEY);
let (Some(token), Some(role), Some(pubkey_hex)) = (token, role, pubkey_hex) else {
return Err(SignError::Config(format!(
"remote signer needs {}",
missing.join(", ")
)));
};
let bytes: [u8; 32] = hex::decode(&pubkey_hex)
.ok()
.and_then(|v| v.try_into().ok())
.ok_or_else(|| {
SignError::Config(format!(
"{} must be 64 hex characters",
env.describe(PUBKEY)
))
})?;
Ok(ArchSigner::Remote(RemoteSigner::new(
&url,
&token,
&role,
Pubkey::from_slice(&bytes),
)))
}