use serde::{Deserialize, Serialize};
pub const DEFAULT_IDENTITY_REGEXP: &str = r"^https://github\.com/Cliftonz/jarvy-tools/\.github/workflows/sign-manifest\.yml@refs/heads/main$";
pub const DEFAULT_OIDC_ISSUER: &str = "https://token.actions.githubusercontent.com";
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct RegistryConfig {
pub url: String,
#[serde(default)]
pub enabled: bool,
#[serde(default = "default_identity_regexp")]
pub signature_identity_regexp: String,
#[serde(default = "default_oidc_issuer")]
pub signature_oidc_issuer: String,
#[serde(default = "default_true")]
pub require_signature: bool,
}
fn default_identity_regexp() -> String {
DEFAULT_IDENTITY_REGEXP.to_string()
}
fn default_oidc_issuer() -> String {
DEFAULT_OIDC_ISSUER.to_string()
}
fn default_true() -> bool {
true
}
fn insecure_loopback_url_allowed(url: &str) -> bool {
#[cfg(not(feature = "test-bypass"))]
{
let _ = url;
false
}
#[cfg(feature = "test-bypass")]
{
if std::env::var_os("JARVY_REGISTRY_ALLOW_INSECURE_FETCH").is_none() {
return false;
}
let Some(after_scheme) = url.strip_prefix("http://") else {
return false;
};
let authority_end = after_scheme
.find(['/', '?', '#'])
.unwrap_or(after_scheme.len());
let authority = &after_scheme[..authority_end];
if authority.contains('@') {
return false;
}
let host_end = authority.find(':').unwrap_or(authority.len());
let host = &authority[..host_end];
matches!(host, "127.0.0.1" | "localhost")
}
}
impl Default for RegistryConfig {
fn default() -> Self {
Self {
url: String::new(),
enabled: false,
signature_identity_regexp: DEFAULT_IDENTITY_REGEXP.to_string(),
signature_oidc_issuer: DEFAULT_OIDC_ISSUER.to_string(),
require_signature: true,
}
}
}
impl RegistryConfig {
pub fn load() -> Option<Self> {
let path = crate::paths::config_toml().ok()?;
let content = std::fs::read_to_string(&path).ok()?;
#[derive(Deserialize)]
struct GlobalConfig {
registry: Option<RegistryConfig>,
}
let parsed: GlobalConfig = toml::from_str(&content).ok()?;
parsed.registry
}
pub fn is_active(&self) -> bool {
self.enabled && !self.url.is_empty()
}
pub fn validate_safety(&self) -> Result<(), String> {
if !self.url.starts_with("https://") && !insecure_loopback_url_allowed(&self.url) {
return Err(format!("registry.url must be https://, got {:?}", self.url));
}
if self.require_signature {
let r = &self.signature_identity_regexp;
if !r.starts_with('^') || !r.ends_with('$') {
return Err(format!(
"registry.signature_identity_regexp must be fully anchored \
(start with ^ and end with $); got {r:?}",
));
}
if !self.signature_oidc_issuer.starts_with("https://") {
return Err(format!(
"registry.signature_oidc_issuer must be https://, got {:?}",
self.signature_oidc_issuer
));
}
}
Ok(())
}
pub fn manifest_url(&self) -> String {
let base = self.url.trim_end_matches('/');
format!("{base}/manifest.json")
}
pub fn tool_url(&self, path: &str) -> String {
let base = self.url.trim_end_matches('/');
let path = path.trim_start_matches('/');
format!("{base}/{path}")
}
pub fn signature_url(&self) -> String {
format!("{}.sig", self.manifest_url())
}
pub fn certificate_url(&self) -> String {
format!("{}.pem", self.manifest_url())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn defaults_disable_registry() {
let cfg = RegistryConfig::default();
assert!(!cfg.enabled);
assert!(cfg.require_signature);
assert!(!cfg.is_active());
}
#[test]
fn is_active_requires_url_and_enabled() {
let cfg = RegistryConfig {
enabled: true,
..Default::default()
};
assert!(!cfg.is_active(), "empty url should fail");
let cfg = RegistryConfig {
enabled: true,
url: "https://example.com/registry/".into(),
..Default::default()
};
assert!(cfg.is_active());
}
#[test]
fn manifest_url_normalizes_trailing_slash() {
let cfg = RegistryConfig {
url: "https://example.com/r/".into(),
..Default::default()
};
assert_eq!(cfg.manifest_url(), "https://example.com/r/manifest.json");
let cfg2 = RegistryConfig {
url: "https://example.com/r".into(),
..Default::default()
};
assert_eq!(cfg2.manifest_url(), "https://example.com/r/manifest.json");
}
#[test]
fn tool_url_joins_relative_path() {
let cfg = RegistryConfig {
url: "https://example.com/r/".into(),
..Default::default()
};
assert_eq!(
cfg.tool_url("tools/foo.toml"),
"https://example.com/r/tools/foo.toml"
);
assert_eq!(
cfg.tool_url("/tools/foo.toml"),
"https://example.com/r/tools/foo.toml"
);
}
#[test]
fn cosign_companion_urls() {
let cfg = RegistryConfig {
url: "https://example.com/r/".into(),
..Default::default()
};
assert_eq!(
cfg.signature_url(),
"https://example.com/r/manifest.json.sig"
);
assert_eq!(
cfg.certificate_url(),
"https://example.com/r/manifest.json.pem"
);
}
#[test]
fn deserializes_from_toml() {
let toml_str = r#"
url = "https://example.com/r/"
enabled = true
require_signature = false
"#;
let cfg: RegistryConfig = toml::from_str(toml_str).unwrap();
assert!(cfg.enabled);
assert!(!cfg.require_signature);
assert_eq!(cfg.url, "https://example.com/r/");
assert_eq!(cfg.signature_oidc_issuer, DEFAULT_OIDC_ISSUER);
}
}