use super::*;
pub(super) fn fetch_opts(token: Option<&str>) -> substrate::client::FetchOptions {
match token {
Some(t) => substrate::client::FetchOptions::with_bearer_token(t),
None => Default::default(),
}
}
pub(crate) fn decode_spore_manifest(
payload: &serde_json::Value,
) -> Result<substrate::Spore, crate::HyphaError> {
substrate::decode_spore(payload).map_err(|e| {
crate::HyphaError::new("manifest_failed", format!("Invalid spore manifest: {e}"))
})
}
pub(super) fn dist_git_url(entry: &substrate::SporeDist) -> Option<&str> {
entry.git_url()
}
pub(super) fn dist_git_ref(entry: &substrate::SporeDist) -> Option<&str> {
entry.git_ref()
}
pub(super) fn dist_has_type(entry: &substrate::SporeDist, expected: &str) -> bool {
entry.kind.as_str() == expected
}
pub(super) fn build_archive_url_from_endpoint(
endpoint: &CmnEndpoint,
hash: &str,
) -> Result<String, String> {
endpoint.resolve_url(hash).map_err(|e| {
format!(
"Invalid archive endpoint for format {:?}: {}",
endpoint.format, e
)
})
}
pub(super) fn build_archive_delta_url_from_endpoint(
endpoint: &CmnEndpoint,
hash: &str,
old_hash: &str,
) -> Result<Option<String>, String> {
endpoint.resolve_delta_url(hash, old_hash).map_err(|e| {
format!(
"Invalid archive delta endpoint for format {:?}: {}",
endpoint.format, e
)
})
}
pub(super) fn is_safe_bond_dir_name(name: &str) -> bool {
(!name.is_empty() && substrate::local_dir_name(Some(name), None, "") == name)
|| substrate::parse_hash(name).is_ok()
}
pub(super) async fn fetch_cmn_json(domain: &str) -> Result<CmnEntry, crate::HyphaError> {
let client = substrate::client::http_client(30)
.map_err(|e| crate::HyphaError::new("cmn_failed", format!("HTTP client error: {e}")))?;
substrate::client::fetch_cmn_entry(&client, domain, Default::default())
.await
.map_err(|e| {
let msg = e.to_string();
if msg.contains("returned 404") {
crate::HyphaError::with_hint(
"cmn_not_found",
msg,
"The domain must serve a cmn.json at /.well-known/cmn.json. Use 'hypha mycelium root' to initialize a CMN site, then deploy the public/ directory.",
)
} else {
crate::HyphaError::new("cmn_failed", msg)
}
})
}
pub(super) fn mtime_epoch_ms(path: impl AsRef<std::path::Path>) -> Option<u64> {
std::fs::metadata(path)
.and_then(|m| m.modified())
.ok()
.and_then(|t| t.duration_since(std::time::UNIX_EPOCH).ok())
.map(|d| d.as_millis() as u64)
}
pub(super) fn primary_capsule(entry: &CmnEntry) -> Result<&CmnCapsuleEntry, crate::HyphaError> {
entry
.primary_capsule()
.map_err(|e| crate::HyphaError::new("cmn_invalid", format!("Invalid cmn.json: {}", e)))
}
pub(super) fn can_synapse_fallback(
domain_cache: &crate::cache::DomainCache,
public_key: &str,
cfg: &crate::config::CacheConfig,
) -> bool {
let has_trusted_key = domain_cache.is_key_trusted(
public_key,
cfg.key_trust_ttl_s * 1000,
cfg.clock_skew_tolerance_s * 1000,
);
has_trusted_key || !cfg.require_domain_first_key
}
pub(super) fn resolve_default_synapse_url(
cfg: &crate::config::HyphaConfig,
) -> Option<(String, Option<String>)> {
let synapse_domain = cfg.defaults.synapse.as_deref()?;
let resolved = crate::config::resolve_synapse(Some(synapse_domain), None).ok()?;
Some((resolved.url, resolved.token_secret))
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
fn setup_domain_cache(domain: &str) -> (tempfile::TempDir, crate::cache::DomainCache) {
let dir = tempfile::tempdir().unwrap();
std::env::set_var("CMN_HOME", dir.path().to_str().unwrap());
let cache = crate::cache::CacheDir::new();
let dc = cache.domain(domain);
(dir, dc)
}
fn default_cache_config() -> crate::config::CacheConfig {
crate::config::CacheConfig::default()
}
#[test]
fn test_can_synapse_fallback_no_key_require_domain_true() {
let _lock = crate::config::ENV_LOCK.lock().unwrap();
let (_dir, dc) = setup_domain_cache("example.com");
let cfg = default_cache_config();
assert!(cfg.require_domain_first_key);
assert!(!can_synapse_fallback(&dc, "ed25519.test_key", &cfg));
std::env::remove_var("CMN_HOME");
}
#[test]
fn test_can_synapse_fallback_no_key_require_domain_false() {
let _lock = crate::config::ENV_LOCK.lock().unwrap();
let (_dir, dc) = setup_domain_cache("example.com");
let mut cfg = default_cache_config();
cfg.require_domain_first_key = false;
assert!(can_synapse_fallback(&dc, "ed25519.test_key", &cfg));
std::env::remove_var("CMN_HOME");
}
#[test]
fn test_can_synapse_fallback_with_trusted_key() {
let _lock = crate::config::ENV_LOCK.lock().unwrap();
let (_dir, dc) = setup_domain_cache("example.com");
let key = "ed25519.test_key";
dc.save_key_trust(key).unwrap();
let cfg = default_cache_config();
assert!(cfg.require_domain_first_key);
assert!(can_synapse_fallback(&dc, key, &cfg));
std::env::remove_var("CMN_HOME");
}
#[test]
fn test_can_synapse_fallback_wrong_key_not_trusted() {
let _lock = crate::config::ENV_LOCK.lock().unwrap();
let (_dir, dc) = setup_domain_cache("example.com");
dc.save_key_trust("ed25519.key_a").unwrap();
let cfg = default_cache_config();
assert!(!can_synapse_fallback(&dc, "ed25519.key_b", &cfg));
std::env::remove_var("CMN_HOME");
}
}