use std::path::PathBuf;
use thiserror::Error;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Ecosystem {
Npm,
Pypi,
Go,
}
impl Ecosystem {
pub fn parse(s: &str) -> Option<Self> {
match s {
"npm" => Some(Self::Npm),
"pypi" => Some(Self::Pypi),
"go" => Some(Self::Go),
_ => None,
}
}
pub fn as_str(self) -> &'static str {
match self {
Self::Npm => "npm",
Self::Pypi => "pypi",
Self::Go => "go",
}
}
}
#[derive(Debug)]
pub struct ProxyConfig {
pub ecosystem: Ecosystem,
pub config_blob: String,
pub canonical_location: PathBuf,
}
#[derive(Debug, Error)]
pub enum ProxyConfigError {
#[error("home directory not discoverable; cannot resolve canonical location for {0:?}")]
HomeDirUnavailable(Ecosystem),
#[error(
"inline_token requires a non-empty api_key (would emit broken auth header for {0:?})"
)]
InlineTokenEmpty(Ecosystem),
}
pub struct EmitOptions {
pub endpoint: String,
pub scope: Option<String>,
pub inline_token: bool,
pub api_key: Option<String>,
}
pub fn emit(ecosystem: Ecosystem, opts: &EmitOptions) -> Result<ProxyConfig, ProxyConfigError> {
if opts.inline_token {
let has_usable_key = opts
.api_key
.as_deref()
.map(|k| !k.trim().is_empty())
.unwrap_or(false);
if !has_usable_key {
return Err(ProxyConfigError::InlineTokenEmpty(ecosystem));
}
}
match ecosystem {
Ecosystem::Npm => emit_npm(opts),
Ecosystem::Pypi => emit_pypi(opts),
Ecosystem::Go => emit_go(opts),
}
}
fn token_expression(opts: &EmitOptions) -> String {
if opts.inline_token {
opts.api_key.clone().unwrap_or_default()
} else {
"${CLEANLIBRARY_API_KEY}".to_string()
}
}
fn endpoint_host(endpoint: &str) -> &str {
endpoint
.trim_end_matches('/')
.trim_start_matches("https://")
.trim_start_matches("http://")
}
fn emit_npm(opts: &EmitOptions) -> Result<ProxyConfig, ProxyConfigError> {
let home = dirs::home_dir().ok_or(ProxyConfigError::HomeDirUnavailable(Ecosystem::Npm))?;
let endpoint = opts.endpoint.trim_end_matches('/');
let registry_url = format!("{}/npm/", endpoint);
let host = endpoint_host(endpoint);
let token = token_expression(opts);
let config_blob = match opts.scope.as_deref() {
Some(scope) => format!(
"{scope}:registry={registry_url}\n//{host}/npm/:_authToken={token}\nalways-auth=true\n",
),
None => format!(
"registry={registry_url}\n//{host}/npm/:_authToken={token}\nalways-auth=true\n",
),
};
Ok(ProxyConfig {
ecosystem: Ecosystem::Npm,
config_blob,
canonical_location: home.join(".npmrc"),
})
}
fn emit_pypi(opts: &EmitOptions) -> Result<ProxyConfig, ProxyConfigError> {
let home = dirs::home_dir().ok_or(ProxyConfigError::HomeDirUnavailable(Ecosystem::Pypi))?;
let endpoint = opts.endpoint.trim_end_matches('/');
let host = endpoint_host(endpoint);
let token = token_expression(opts);
let config_blob = format!(
"[global]\nindex-url = https://{token}@{host}/pypi/simple/\nextra-index-url =\n\n[install]\ntrusted-host = {host}\n",
);
Ok(ProxyConfig {
ecosystem: Ecosystem::Pypi,
config_blob,
canonical_location: home.join(".config").join("pip").join("pip.conf"),
})
}
fn emit_go(opts: &EmitOptions) -> Result<ProxyConfig, ProxyConfigError> {
let endpoint = opts.endpoint.trim_end_matches('/');
let token = token_expression(opts);
let config_blob = format!(
"# CleanLibrary Go proxy — append to your shell config (~/.bashrc, ~/.zshrc, fish config)\n# or run the equivalent `go env -w GOPROXY=...` / `go env -w GOAUTH=...` invocations.\nexport GOPROXY={endpoint}/go,direct\nexport GOAUTH=\"Authorization: Bearer {token}\"\n",
);
Ok(ProxyConfig {
ecosystem: Ecosystem::Go,
config_blob,
canonical_location: PathBuf::new(),
})
}
#[cfg(test)]
mod tests {
use super::*;
fn opts(endpoint: &str) -> EmitOptions {
EmitOptions {
endpoint: endpoint.to_string(),
scope: None,
inline_token: false,
api_key: None,
}
}
#[test]
fn ecosystem_parse_vocab_locked() {
assert_eq!(Ecosystem::parse("npm"), Some(Ecosystem::Npm));
assert_eq!(Ecosystem::parse("pypi"), Some(Ecosystem::Pypi));
assert_eq!(Ecosystem::parse("go"), Some(Ecosystem::Go));
assert_eq!(Ecosystem::parse("NPM"), None);
assert_eq!(Ecosystem::parse("PyPI"), None);
assert_eq!(Ecosystem::parse("pip"), None);
assert_eq!(Ecosystem::parse("golang"), None);
}
#[test]
fn npm_emit_shell_expansion_default() {
let blob = emit_npm(&opts("https://cleanapp.clnstrt.dev")).unwrap().config_blob;
assert!(blob.contains("registry=https://cleanapp.clnstrt.dev/npm/"));
assert!(blob.contains("//cleanapp.clnstrt.dev/npm/:_authToken=${CLEANLIBRARY_API_KEY}"));
assert!(blob.contains("always-auth=true"));
}
#[test]
fn npm_emit_with_scope() {
let mut o = opts("https://cleanapp.clnstrt.dev");
o.scope = Some("@my-org".to_string());
let blob = emit_npm(&o).unwrap().config_blob;
assert!(blob.contains("@my-org:registry=https://cleanapp.clnstrt.dev/npm/"));
}
#[test]
fn npm_emit_inline_token() {
let mut o = opts("https://cleanapp.clnstrt.dev");
o.inline_token = true;
o.api_key = Some("cs_live_smoke".to_string());
let blob = emit_npm(&o).unwrap().config_blob;
assert!(blob.contains("_authToken=cs_live_smoke"));
assert!(!blob.contains("${CLEANLIBRARY_API_KEY}"));
}
#[test]
fn pypi_emit_index_url_with_token_in_url() {
let blob = emit_pypi(&opts("https://cleanapp.clnstrt.dev")).unwrap().config_blob;
assert!(blob.contains("index-url = https://${CLEANLIBRARY_API_KEY}@cleanapp.clnstrt.dev/pypi/simple/"));
assert!(blob.contains("trusted-host = cleanapp.clnstrt.dev"));
}
#[test]
fn go_emit_env_form() {
let blob = emit_go(&opts("https://cleanapp.clnstrt.dev")).unwrap().config_blob;
assert!(blob.contains("export GOPROXY=https://cleanapp.clnstrt.dev/go,direct"));
assert!(blob.contains("export GOAUTH=\"Authorization: Bearer ${CLEANLIBRARY_API_KEY}\""));
}
#[test]
fn go_emit_has_no_canonical_location() {
let cfg = emit_go(&opts("https://cleanapp.clnstrt.dev")).unwrap();
assert!(cfg.canonical_location.as_os_str().is_empty());
}
#[test]
fn endpoint_trailing_slash_tolerated() {
let blob = emit_npm(&opts("https://cleanapp.clnstrt.dev/")).unwrap().config_blob;
assert!(blob.contains("registry=https://cleanapp.clnstrt.dev/npm/"));
assert!(!blob.contains("//npm/"));
}
#[test]
fn emit_rejects_inline_token_with_none_api_key() {
let mut o = opts("https://cleanapp.clnstrt.dev");
o.inline_token = true;
o.api_key = None;
let err = emit(Ecosystem::Npm, &o).unwrap_err();
assert!(matches!(err, ProxyConfigError::InlineTokenEmpty(Ecosystem::Npm)));
}
#[test]
fn emit_rejects_inline_token_with_empty_string_api_key() {
let mut o = opts("https://cleanapp.clnstrt.dev");
o.inline_token = true;
o.api_key = Some(String::new());
let err = emit(Ecosystem::Pypi, &o).unwrap_err();
assert!(matches!(err, ProxyConfigError::InlineTokenEmpty(Ecosystem::Pypi)));
}
#[test]
fn emit_rejects_inline_token_with_whitespace_only_api_key() {
let mut o = opts("https://cleanapp.clnstrt.dev");
o.inline_token = true;
o.api_key = Some(" \t\n".to_string());
let err = emit(Ecosystem::Go, &o).unwrap_err();
assert!(matches!(err, ProxyConfigError::InlineTokenEmpty(Ecosystem::Go)));
}
#[test]
fn emit_accepts_inline_token_with_valid_api_key() {
let mut o = opts("https://cleanapp.clnstrt.dev");
o.inline_token = true;
o.api_key = Some("std_001".to_string());
let blob = emit(Ecosystem::Npm, &o).unwrap().config_blob;
assert!(blob.contains("_authToken=std_001"));
assert!(!blob.contains("_authToken=\n"));
assert!(!blob.contains("_authToken= "));
}
#[test]
fn emit_shell_expansion_path_unaffected_by_empty_key() {
let mut o = opts("https://cleanapp.clnstrt.dev");
o.inline_token = false;
o.api_key = None;
assert!(emit(Ecosystem::Npm, &o).is_ok());
assert!(emit(Ecosystem::Pypi, &o).is_ok());
assert!(emit(Ecosystem::Go, &o).is_ok());
}
}