From 5edd590fbe82e084738e2d6e498c95702c41a57e Mon Sep 17 00:00:00 2001 From: Enrico Piovesan Date: Sun, 12 Jul 2026 06:45:26 -0600 Subject: [PATCH 1/6] feat(registry): persist public contract metadata --- crates/traverse-cli/src/main.rs | 2 ++ .../src/public_registry_state.rs | 20 +++++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/crates/traverse-cli/src/main.rs b/crates/traverse-cli/src/main.rs index 6431a2fa..49d21dc6 100644 --- a/crates/traverse-cli/src/main.rs +++ b/crates/traverse-cli/src/main.rs @@ -7004,6 +7004,8 @@ mod tests { version: "1.0.0".to_string(), digest: "sha256:5647c39a".to_string(), artifact_url: "https://github.com/traverse-framework/registry/releases/download/artifacts/traverse-starter.process-1.0.0/traverse-starter.wasm".to_string(), + contract_digest: "sha256:5647c39a".to_string(), + contract_url: "https://github.com/traverse-framework/registry/releases/download/artifacts/traverse-starter.process-1.0.0/contract.json".to_string(), deprecated: false, }], } diff --git a/crates/traverse-registry/src/public_registry_state.rs b/crates/traverse-registry/src/public_registry_state.rs index 5faefec8..2909a426 100644 --- a/crates/traverse-registry/src/public_registry_state.rs +++ b/crates/traverse-registry/src/public_registry_state.rs @@ -24,6 +24,8 @@ pub struct PublicRegistryCapabilityRecord { pub version: String, pub digest: String, pub artifact_url: String, + pub contract_digest: String, + pub contract_url: String, pub deprecated: bool, } @@ -106,6 +108,20 @@ pub fn validate_public_registry_index( &record.artifact_url, "artifact_url", ); + validate_record_field( + &mut errors, + position, + "contract_digest", + &record.contract_digest, + "contract_digest", + ); + validate_record_field( + &mut errors, + position, + "contract_url", + &record.contract_url, + "contract_url", + ); if !seen.insert((&record.namespace, &record.id, &record.version)) { errors.push(error( PublicRegistryStateErrorCode::DuplicateRecord, @@ -348,6 +364,8 @@ fn capability_json_value(record: &PublicRegistryCapabilityRecord) -> Value { "version": record.version, "digest": record.digest, "artifact_url": record.artifact_url, + "contract_digest": record.contract_digest, + "contract_url": record.contract_url, "deprecated": record.deprecated }) } @@ -748,6 +766,8 @@ mod tests { version: "1.0.0".to_string(), digest: "sha256:5647".to_string(), artifact_url: "https://github.com/traverse-framework/registry/releases/download/artifacts/traverse-starter.process-1.0.0/traverse-starter.wasm".to_string(), + contract_digest: "sha256:5647".to_string(), + contract_url: "https://github.com/traverse-framework/registry/releases/download/artifacts/traverse-starter.process-1.0.0/contract.json".to_string(), deprecated: false, }], } From fc7e449caefa7b1633aae28ebd2c22600f22182a Mon Sep 17 00:00:00 2001 From: Enrico Piovesan Date: Sun, 12 Jul 2026 07:46:47 -0600 Subject: [PATCH 2/6] feat(registry): resolve synced registry references by range --- .../src/public_registry_state.rs | 110 ++++++++++++++++++ 1 file changed, 110 insertions(+) diff --git a/crates/traverse-registry/src/public_registry_state.rs b/crates/traverse-registry/src/public_registry_state.rs index 2909a426..27b57f63 100644 --- a/crates/traverse-registry/src/public_registry_state.rs +++ b/crates/traverse-registry/src/public_registry_state.rs @@ -1,3 +1,4 @@ +use semver::{Version, VersionReq}; use serde::{Deserialize, Serialize}; use serde_json::Value; use std::collections::BTreeSet; @@ -56,6 +57,9 @@ pub enum PublicRegistryStateErrorCode { IncompatibleSchemaVersion, IncompatibleWorkspaceState, StateWriteFailed, + InvalidVersionRange, + NoMatchingVersion, + OnlyDeprecatedVersions, } #[derive(Debug, Clone, PartialEq, Eq)] @@ -251,6 +255,67 @@ pub fn resolve_synced_public_registry_record( })) } +/// Resolves the highest non-deprecated public record that satisfies a semver range. +/// +/// # Errors +/// +/// Returns stable resolution errors for malformed ranges, an absent matching +/// version, or a range whose only matches are deprecated records. This function +/// reads local synced state only and never contacts a registry service. +pub fn resolve_synced_public_registry_range( + workspace_root: &Path, + workspace_id: &str, + namespace: &str, + id: &str, + version_range: &str, +) -> Result { + let requirement = VersionReq::parse(version_range).map_err(|error| { + single_error( + PublicRegistryStateErrorCode::InvalidVersionRange, + Path::new(version_range), + format!("invalid registry_ref version_range {version_range}: {error}"), + ) + })?; + let state = load_synced_public_registry_state(workspace_root, workspace_id)?; + let matching = state + .capabilities + .into_iter() + .filter_map(|record| { + if record.namespace != namespace || record.id != id { + return None; + } + Version::parse(&record.version) + .ok() + .filter(|version| requirement.matches(version)) + .map(|version| (version, record)) + }) + .collect::>(); + let mut active = matching + .iter() + .filter(|(_, record)| !record.deprecated) + .cloned() + .collect::>(); + active.sort_by(|left, right| right.0.cmp(&left.0)); + if let Some((_, record)) = active.into_iter().next() { + return Ok(record); + } + let code = if matching.is_empty() { + PublicRegistryStateErrorCode::NoMatchingVersion + } else { + PublicRegistryStateErrorCode::OnlyDeprecatedVersions + }; + let message = match code { + PublicRegistryStateErrorCode::NoMatchingVersion => format!( + "no synced public registry version for {namespace}:{id} satisfies {version_range}" + ), + PublicRegistryStateErrorCode::OnlyDeprecatedVersions => format!( + "only deprecated public registry versions for {namespace}:{id} satisfy {version_range}" + ), + _ => String::new(), + }; + Err(single_error(code, Path::new(version_range), message)) +} + fn validate_synced_public_registry_state( path: &Path, workspace_id: &str, @@ -649,6 +714,51 @@ mod tests { assert!(missing.is_none()); } + #[test] + fn range_resolution_selects_highest_active_version_and_reports_yanked_only() { + let workspace_root = unique_temp_dir(); + let mut index = valid_index(); + let mut newer = index.capabilities[0].clone(); + newer.version = "1.2.0".to_string(); + index.capabilities.push(newer); + let mut yanked = index.capabilities[0].clone(); + yanked.version = "2.0.0".to_string(); + yanked.deprecated = true; + index.capabilities.push(yanked); + write_synced_public_registry_state( + &workspace_root, + "local", + "traverse-framework/registry", + "index-v7", + "2026-07-06T00:00:00Z", + index, + ) + .expect("state should write"); + + let selected = resolve_synced_public_registry_range( + &workspace_root, + "local", + "traverse-starter", + "traverse-starter.process", + "^1.0", + ) + .expect("active range match should resolve"); + assert_eq!(selected.version, "1.2.0"); + + let yanked_only = resolve_synced_public_registry_range( + &workspace_root, + "local", + "traverse-starter", + "traverse-starter.process", + "^2.0", + ) + .expect_err("yanked-only range should fail"); + assert_eq!( + yanked_only.errors[0].code, + PublicRegistryStateErrorCode::OnlyDeprecatedVersions + ); + } + #[test] fn write_failure_when_parent_component_is_file_leaves_no_state() { let workspace_root = unique_temp_dir(); From c16734fe396a622c89e77b411666ebd942b436b1 Mon Sep 17 00:00:00 2001 From: Enrico Piovesan Date: Sun, 12 Jul 2026 07:47:51 -0600 Subject: [PATCH 3/6] feat(registry): cache verified public artifacts --- crates/traverse-registry/src/lib.rs | 2 + .../src/public_registry_cache.rs | 195 ++++++++++++++++++ 2 files changed, 197 insertions(+) create mode 100644 crates/traverse-registry/src/public_registry_cache.rs diff --git a/crates/traverse-registry/src/lib.rs b/crates/traverse-registry/src/lib.rs index 0488f262..f8e79a61 100644 --- a/crates/traverse-registry/src/lib.rs +++ b/crates/traverse-registry/src/lib.rs @@ -7,6 +7,7 @@ mod events; mod federation; mod graph; mod model_resolution; +mod public_registry_cache; mod public_registry_state; pub mod semver_resolver; mod workflows; @@ -21,6 +22,7 @@ pub use events::*; pub use federation::*; pub use graph::*; pub use model_resolution::*; +pub use public_registry_cache::*; pub use public_registry_state::*; pub use semver_resolver::{ AmbiguousCandidate, RangeResolutionError, ResolvedRangeCapability, resolve_version_range, diff --git a/crates/traverse-registry/src/public_registry_cache.rs b/crates/traverse-registry/src/public_registry_cache.rs new file mode 100644 index 00000000..3dce21f6 --- /dev/null +++ b/crates/traverse-registry/src/public_registry_cache.rs @@ -0,0 +1,195 @@ +use sha2::{Digest, Sha256}; +use std::fmt::Write as _; +use std::fs; +use std::path::{Path, PathBuf}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PublicRegistryCacheErrorCode { + InvalidDigest, + DigestMismatch, + CacheReadFailed, + CacheWriteFailed, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PublicRegistryCacheError { + pub code: PublicRegistryCacheErrorCode, + pub path: PathBuf, + pub message: String, +} + +/// Returns the shared content-addressed storage location for a SHA-256 digest. +#[must_use] +pub fn public_registry_cache_path(workspace_root: &Path, digest: &str) -> Option { + normalize_digest(digest).map(|digest| { + workspace_root + .join(".traverse") + .join("cache") + .join("sha256") + .join(digest) + }) +} + +/// Verifies bytes and persists them in the shared content-addressed cache. +/// +/// Existing cache entries are read and digest-verified before reuse. A cache +/// miss is written through a temporary sibling so callers never observe a +/// partial artifact. +/// +/// # Errors +/// +/// Returns a stable error when the declared digest is invalid, content does +/// not match it, an existing entry cannot be read, or the cache cannot be +/// committed atomically. +pub fn cache_verified_public_registry_bytes( + workspace_root: &Path, + declared_digest: &str, + bytes: &[u8], +) -> Result { + let Some(path) = public_registry_cache_path(workspace_root, declared_digest) else { + return Err(cache_error( + PublicRegistryCacheErrorCode::InvalidDigest, + workspace_root, + format!( + "public registry digest must be sha256: followed by 64 hex characters: {declared_digest}" + ), + )); + }; + let expected = normalize_digest(declared_digest).unwrap_or_default(); + if sha256_hex(bytes) != expected { + return Err(cache_error( + PublicRegistryCacheErrorCode::DigestMismatch, + &path, + format!( + "public registry content digest mismatch for {}", + path.display() + ), + )); + } + if path.exists() { + let cached = fs::read(&path).map_err(|error| { + cache_error( + PublicRegistryCacheErrorCode::CacheReadFailed, + &path, + format!("failed to read cached public registry content: {error}"), + ) + })?; + if sha256_hex(&cached) != expected { + return Err(cache_error( + PublicRegistryCacheErrorCode::DigestMismatch, + &path, + format!( + "cached public registry content digest mismatch for {}", + path.display() + ), + )); + } + return Ok(path); + } + let parent = path.parent().ok_or_else(|| { + cache_error( + PublicRegistryCacheErrorCode::CacheWriteFailed, + &path, + "public registry cache path has no parent".to_string(), + ) + })?; + fs::create_dir_all(parent).map_err(|error| { + cache_error( + PublicRegistryCacheErrorCode::CacheWriteFailed, + parent, + format!("failed to create public registry cache directory: {error}"), + ) + })?; + let temporary = path.with_extension("tmp"); + fs::write(&temporary, bytes).map_err(|error| { + cache_error( + PublicRegistryCacheErrorCode::CacheWriteFailed, + &temporary, + format!("failed to write public registry cache entry: {error}"), + ) + })?; + fs::rename(&temporary, &path).map_err(|error| { + let _ = fs::remove_file(&temporary); + cache_error( + PublicRegistryCacheErrorCode::CacheWriteFailed, + &path, + format!("failed to commit public registry cache entry: {error}"), + ) + })?; + Ok(path) +} + +fn normalize_digest(digest: &str) -> Option { + let digest = digest.strip_prefix("sha256:")?; + if digest.len() == 64 && digest.bytes().all(|byte| byte.is_ascii_hexdigit()) { + Some(digest.to_ascii_lowercase()) + } else { + None + } +} + +fn sha256_hex(bytes: &[u8]) -> String { + let digest = Sha256::digest(bytes); + let mut value = String::with_capacity(digest.len() * 2); + for byte in digest { + let _ = write!(value, "{byte:02x}"); + } + value +} + +fn cache_error( + code: PublicRegistryCacheErrorCode, + path: &Path, + message: String, +) -> PublicRegistryCacheError { + PublicRegistryCacheError { + code, + path: path.to_path_buf(), + message, + } +} + +#[cfg(test)] +mod tests { + #![allow(clippy::expect_used)] + + use super::*; + use std::sync::atomic::{AtomicU64, Ordering}; + + static TEMP_COUNTER: AtomicU64 = AtomicU64::new(0); + + #[test] + fn caches_and_revalidates_verified_content() { + let root = unique_temp_dir(); + let bytes = b"public contract"; + let digest = format!("sha256:{}", sha256_hex(bytes)); + let path = cache_verified_public_registry_bytes(&root, &digest, bytes) + .expect("content should cache"); + assert_eq!(fs::read(&path).expect("cache should read"), bytes); + cache_verified_public_registry_bytes(&root, &digest, bytes) + .expect("valid cache hit should succeed"); + fs::write(&path, b"tampered").expect("cache should be writable for test"); + let failure = cache_verified_public_registry_bytes(&root, &digest, bytes) + .expect_err("tampered cache must not be reused"); + assert_eq!(failure.code, PublicRegistryCacheErrorCode::DigestMismatch); + } + + #[test] + fn rejects_mismatched_declared_digest() { + let root = unique_temp_dir(); + let failure = cache_verified_public_registry_bytes( + &root, + "sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + b"different", + ) + .expect_err("mismatched bytes must fail"); + assert_eq!(failure.code, PublicRegistryCacheErrorCode::DigestMismatch); + } + + fn unique_temp_dir() -> PathBuf { + let counter = TEMP_COUNTER.fetch_add(1, Ordering::Relaxed); + let path = std::env::temp_dir().join(format!("traverse-public-cache-test-{counter}")); + fs::create_dir_all(&path).expect("temp directory must be created"); + path + } +} From 4892bd79fd3bceaf96f7e71ee32252da31fea3fe Mon Sep 17 00:00:00 2001 From: Enrico Piovesan Date: Wed, 15 Jul 2026 09:02:34 -0600 Subject: [PATCH 4/6] test(registry): isolate public cache temp paths --- crates/traverse-registry/src/public_registry_cache.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/traverse-registry/src/public_registry_cache.rs b/crates/traverse-registry/src/public_registry_cache.rs index 3dce21f6..3a21d80a 100644 --- a/crates/traverse-registry/src/public_registry_cache.rs +++ b/crates/traverse-registry/src/public_registry_cache.rs @@ -188,7 +188,12 @@ mod tests { fn unique_temp_dir() -> PathBuf { let counter = TEMP_COUNTER.fetch_add(1, Ordering::Relaxed); - let path = std::env::temp_dir().join(format!("traverse-public-cache-test-{counter}")); + let nanos = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .expect("system time should be after epoch") + .as_nanos(); + let path = + std::env::temp_dir().join(format!("traverse-public-cache-test-{nanos}-{counter}")); fs::create_dir_all(&path).expect("temp directory must be created"); path } From ae3c0e5ab901ebdb8ab2603761347b4d659865d2 Mon Sep 17 00:00:00 2001 From: Enrico Piovesan Date: Wed, 15 Jul 2026 09:05:56 -0600 Subject: [PATCH 5/6] test(registry): cover public registry failure paths --- .../src/public_registry_cache.rs | 94 ++++++++++++++++--- .../src/public_registry_state.rs | 51 +++++++--- 2 files changed, 121 insertions(+), 24 deletions(-) diff --git a/crates/traverse-registry/src/public_registry_cache.rs b/crates/traverse-registry/src/public_registry_cache.rs index 3a21d80a..7a0bde8f 100644 --- a/crates/traverse-registry/src/public_registry_cache.rs +++ b/crates/traverse-registry/src/public_registry_cache.rs @@ -86,17 +86,14 @@ pub fn cache_verified_public_registry_bytes( } return Ok(path); } - let parent = path.parent().ok_or_else(|| { + let parent = workspace_root + .join(".traverse") + .join("cache") + .join("sha256"); + fs::create_dir_all(&parent).map_err(|error| { cache_error( PublicRegistryCacheErrorCode::CacheWriteFailed, - &path, - "public registry cache path has no parent".to_string(), - ) - })?; - fs::create_dir_all(parent).map_err(|error| { - cache_error( - PublicRegistryCacheErrorCode::CacheWriteFailed, - parent, + &parent, format!("failed to create public registry cache directory: {error}"), ) })?; @@ -108,15 +105,19 @@ pub fn cache_verified_public_registry_bytes( format!("failed to write public registry cache entry: {error}"), ) })?; - fs::rename(&temporary, &path).map_err(|error| { + commit_cache_entry(&temporary, &path)?; + Ok(path) +} + +fn commit_cache_entry(temporary: &Path, path: &Path) -> Result<(), PublicRegistryCacheError> { + fs::rename(temporary, path).map_err(|error| { let _ = fs::remove_file(&temporary); cache_error( PublicRegistryCacheErrorCode::CacheWriteFailed, &path, format!("failed to commit public registry cache entry: {error}"), ) - })?; - Ok(path) + }) } fn normalize_digest(digest: &str) -> Option { @@ -186,6 +187,75 @@ mod tests { assert_eq!(failure.code, PublicRegistryCacheErrorCode::DigestMismatch); } + #[test] + fn rejects_invalid_digest_shapes() { + let root = unique_temp_dir(); + for digest in [ + "invalid", + "sha256:short", + "sha256:gggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg", + ] { + let failure = cache_verified_public_registry_bytes(&root, digest, b"content") + .expect_err("invalid digest must fail"); + assert_eq!(failure.code, PublicRegistryCacheErrorCode::InvalidDigest); + } + } + + #[test] + fn reports_cache_read_and_directory_creation_failures() { + let root = unique_temp_dir(); + let bytes = b"public contract"; + let digest = format!("sha256:{}", sha256_hex(bytes)); + let path = public_registry_cache_path(&root, &digest).expect("digest should be valid"); + fs::create_dir_all(&path).expect("cache path directory should be created"); + let read_failure = cache_verified_public_registry_bytes(&root, &digest, bytes) + .expect_err("directory cache entry must not read as bytes"); + assert_eq!( + read_failure.code, + PublicRegistryCacheErrorCode::CacheReadFailed + ); + + let blocked_root = unique_temp_dir(); + fs::write(blocked_root.join(".traverse"), b"not a directory") + .expect("blocking file should be created"); + let write_failure = cache_verified_public_registry_bytes(&blocked_root, &digest, bytes) + .expect_err("cache directory creation must fail through a file"); + assert_eq!( + write_failure.code, + PublicRegistryCacheErrorCode::CacheWriteFailed + ); + } + + #[test] + fn reports_temporary_write_and_commit_failures() { + let root = unique_temp_dir(); + let bytes = b"public contract"; + let digest = format!("sha256:{}", sha256_hex(bytes)); + let path = public_registry_cache_path(&root, &digest).expect("digest should be valid"); + let parent = path.parent().expect("cache path should have a parent"); + fs::create_dir_all(parent).expect("cache parent should be created"); + fs::create_dir(path.with_extension("tmp")).expect("temporary path should be blocked"); + let write_failure = cache_verified_public_registry_bytes(&root, &digest, bytes) + .expect_err("writing through a directory must fail"); + assert_eq!( + write_failure.code, + PublicRegistryCacheErrorCode::CacheWriteFailed + ); + + let commit_root = unique_temp_dir(); + let temporary = commit_root.join("entry.tmp"); + let destination = commit_root.join("entry"); + fs::write(&temporary, bytes).expect("temporary entry should be written"); + fs::create_dir(&destination).expect("destination directory should block rename"); + let commit_failure = commit_cache_entry(&temporary, &destination) + .expect_err("rename onto a directory must fail"); + assert_eq!( + commit_failure.code, + PublicRegistryCacheErrorCode::CacheWriteFailed + ); + assert!(!temporary.exists()); + } + fn unique_temp_dir() -> PathBuf { let counter = TEMP_COUNTER.fetch_add(1, Ordering::Relaxed); let nanos = std::time::SystemTime::now() diff --git a/crates/traverse-registry/src/public_registry_state.rs b/crates/traverse-registry/src/public_registry_state.rs index 27b57f63..4f0a9508 100644 --- a/crates/traverse-registry/src/public_registry_state.rs +++ b/crates/traverse-registry/src/public_registry_state.rs @@ -299,19 +299,20 @@ pub fn resolve_synced_public_registry_range( if let Some((_, record)) = active.into_iter().next() { return Ok(record); } - let code = if matching.is_empty() { - PublicRegistryStateErrorCode::NoMatchingVersion + let (code, message) = if matching.is_empty() { + ( + PublicRegistryStateErrorCode::NoMatchingVersion, + format!( + "no synced public registry version for {namespace}:{id} satisfies {version_range}" + ), + ) } else { - PublicRegistryStateErrorCode::OnlyDeprecatedVersions - }; - let message = match code { - PublicRegistryStateErrorCode::NoMatchingVersion => format!( - "no synced public registry version for {namespace}:{id} satisfies {version_range}" - ), - PublicRegistryStateErrorCode::OnlyDeprecatedVersions => format!( - "only deprecated public registry versions for {namespace}:{id} satisfy {version_range}" - ), - _ => String::new(), + ( + PublicRegistryStateErrorCode::OnlyDeprecatedVersions, + format!( + "only deprecated public registry versions for {namespace}:{id} satisfy {version_range}" + ), + ) }; Err(single_error(code, Path::new(version_range), message)) } @@ -757,6 +758,32 @@ mod tests { yanked_only.errors[0].code, PublicRegistryStateErrorCode::OnlyDeprecatedVersions ); + + let missing = resolve_synced_public_registry_range( + &workspace_root, + "local", + "other-namespace", + "missing-capability", + "^1.0", + ) + .expect_err("missing range match should fail"); + assert_eq!( + missing.errors[0].code, + PublicRegistryStateErrorCode::NoMatchingVersion + ); + + let invalid = resolve_synced_public_registry_range( + &workspace_root, + "local", + "traverse-starter", + "traverse-starter.process", + "not a range", + ) + .expect_err("invalid range should fail"); + assert_eq!( + invalid.errors[0].code, + PublicRegistryStateErrorCode::InvalidVersionRange + ); } #[test] From 7d7eb336cf535162ba675b9ce9ae47c30e788ff6 Mon Sep 17 00:00:00 2001 From: Enrico Piovesan Date: Wed, 15 Jul 2026 09:17:11 -0600 Subject: [PATCH 6/6] fix(registry): satisfy current clippy diagnostics --- crates/traverse-registry/src/public_registry_cache.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/traverse-registry/src/public_registry_cache.rs b/crates/traverse-registry/src/public_registry_cache.rs index 7a0bde8f..2ec9d81c 100644 --- a/crates/traverse-registry/src/public_registry_cache.rs +++ b/crates/traverse-registry/src/public_registry_cache.rs @@ -111,10 +111,10 @@ pub fn cache_verified_public_registry_bytes( fn commit_cache_entry(temporary: &Path, path: &Path) -> Result<(), PublicRegistryCacheError> { fs::rename(temporary, path).map_err(|error| { - let _ = fs::remove_file(&temporary); + let _ = fs::remove_file(temporary); cache_error( PublicRegistryCacheErrorCode::CacheWriteFailed, - &path, + path, format!("failed to commit public registry cache entry: {error}"), ) })