Skip to content
This repository was archived by the owner on Jul 28, 2025. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions crates/api/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,22 @@ pub mod v1;

use serde::{de::Unexpected, Deserialize, Serialize};

/// Relative URL path for the `WellKnownConfig`.
pub const WELL_KNOWN_PATH: &str = ".well-known/wasm-pkg/registry.json";

/// This config allows a domain to point to another URL where the registry
/// API is hosted.
#[derive(Debug, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct WellKnownConfig {
/// For OCI registries, the domain name where the registry is hosted.
pub oci_registry: Option<String>,
/// For OCI registries, a name prefix to use before the namespace.
pub oci_namespace_prefix: Option<String>,
/// For Warg registries, the URL where the registry is hosted.
pub warg_url: Option<String>,
}

/// A utility type for serializing and deserializing constant status codes.
struct Status<const CODE: u16>;

Expand Down
61 changes: 48 additions & 13 deletions crates/client/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,20 +12,24 @@ use secrecy::{ExposeSecret, Secret};
use serde::de::DeserializeOwned;
use std::borrow::Cow;
use thiserror::Error;
use warg_api::v1::{
content::{ContentError, ContentSourcesResponse},
fetch::{
FetchError, FetchLogsRequest, FetchLogsResponse, FetchPackageNamesRequest,
FetchPackageNamesResponse,
use warg_api::{
v1::{
content::{ContentError, ContentSourcesResponse},
fetch::{
FetchError, FetchLogsRequest, FetchLogsResponse, FetchPackageNamesRequest,
FetchPackageNamesResponse,
},
ledger::{LedgerError, LedgerSourcesResponse},
monitor::{CheckpointVerificationResponse, MonitorError},
package::{ContentSource, PackageError, PackageRecord, PublishRecordRequest},
paths,
proof::{
ConsistencyRequest, ConsistencyResponse, InclusionRequest, InclusionResponse,
ProofError,
},
REGISTRY_HEADER_NAME, REGISTRY_HINT_HEADER_NAME,
},
ledger::{LedgerError, LedgerSourcesResponse},
monitor::{CheckpointVerificationResponse, MonitorError},
package::{ContentSource, PackageError, PackageRecord, PublishRecordRequest},
paths,
proof::{
ConsistencyRequest, ConsistencyResponse, InclusionRequest, InclusionResponse, ProofError,
},
REGISTRY_HEADER_NAME, REGISTRY_HINT_HEADER_NAME,
WellKnownConfig, WELL_KNOWN_PATH,
};
use warg_crypto::hash::{AnyHash, HashError, Sha256};
use warg_protocol::{
Expand Down Expand Up @@ -107,6 +111,9 @@ pub enum ClientError {
/// The provided log was not found with hint header.
#[error("log `{0}` was not found in this registry, but the registry provided the hint header: `{1:?}`")]
LogNotFoundWithHint(LogId, HeaderValue),
/// Invalid well-known config.
#[error("registry `{0}` returned an invalid well-known config")]
InvalidWellKnownConfig(String),
/// An other error occurred during the requested operation.
#[error(transparent)]
Other(#[from] anyhow::Error),
Expand Down Expand Up @@ -216,6 +223,34 @@ impl Client {
pub fn url(&self) -> &RegistryUrl {
&self.url
}
/// Gets the `.well-known` configuration registry URL.
pub async fn well_known_config(&self) -> Result<Option<RegistryUrl>, ClientError> {
let url = self.url.join(WELL_KNOWN_PATH);
tracing::debug!(url, "getting `.well-known` config",);

let res = self.client.get(url).send().await?;

if !res.status().is_success() {
tracing::debug!("the `.well-known` config was not found");
return Ok(None);
}

if let Some(warg_url) = res
.json::<WellKnownConfig>()
.await
.map_err(|e| {
tracing::debug!("parsing `.well-known` config failed: {e}");
ClientError::InvalidWellKnownConfig(self.url.registry_domain().to_string())
})?
.warg_url
{
Ok(Some(RegistryUrl::new(warg_url)?))
} else {
tracing::debug!("the `.well-known` config did not have a `wargUrl` set");
Ok(None)
}
}

/// Gets the latest checkpoint from the registry.
pub async fn latest_checkpoint(
&self,
Expand Down
7 changes: 1 addition & 6 deletions crates/client/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -300,13 +300,8 @@ impl Config {

pub(crate) fn storage_paths_for_url(
&self,
url: Option<&str>,
registry_url: RegistryUrl,
) -> Result<StoragePaths, ClientError> {
let registry_url = RegistryUrl::new(
url.or(self.home_url.as_deref())
.ok_or(ClientError::NoHomeRegistryUrl)?,
)?;

let label = registry_url.safe_label();
let registries_dir = self.registries_dir()?.join(label);
let content_dir = self.content_dir()?;
Expand Down
95 changes: 66 additions & 29 deletions crates/client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,10 @@ pub use self::registry_url::RegistryUrl;

const DEFAULT_WAIT_INTERVAL: Duration = Duration::from_secs(1);

/// For Bytecode Alliance projects, the default registry is set to `bytecodealliance.org`.
/// The `.well-known` config path may resolve to another domain where the registry is hosted.
pub const DEFAULT_REGISTRY: &str = "bytecodealliance.org";

/// A client for a Warg registry.
pub struct Client<R, C, N>
where
Expand Down Expand Up @@ -1358,6 +1362,39 @@ pub enum StorageLockResult<T> {
}

impl FileSystemClient {
async fn storage_paths(
url: Option<&str>,
config: &Config,
disable_interactive: bool,
) -> Result<StoragePaths, ClientError> {
let checking_url_for_well_known = RegistryUrl::new(
url.or(config.home_url.as_deref())
.unwrap_or(DEFAULT_REGISTRY),
)?;

let url = if let Some(warg_url) =
api::Client::new(checking_url_for_well_known.to_string(), None)?
.well_known_config()
.await?
{
if !disable_interactive && warg_url != checking_url_for_well_known {
println!(
"Resolved `{well_known}` to registry hosted on `{registry}`",
well_known = checking_url_for_well_known.registry_domain(),
registry = warg_url.registry_domain(),
);
}
warg_url
} else {
RegistryUrl::new(
url.or(config.home_url.as_deref())
.ok_or(ClientError::NoHomeRegistryUrl)?,
)?
};

config.storage_paths_for_url(url)
}

/// Attempts to create a client for the given registry URL.
///
/// If the URL is `None`, the home registry URL is used; if there is no home registry
Expand All @@ -1366,30 +1403,20 @@ impl FileSystemClient {
/// If a lock cannot be acquired for a storage directory, then
/// `NewClientResult::Blocked` is returned with the path to the
/// directory that could not be locked.
pub fn try_new_with_config(
url: Option<&str>,
pub async fn try_new_with_config(
registry: Option<&str>,
config: &Config,
mut auth_token: Option<Secret<String>>,
) -> Result<StorageLockResult<Self>, ClientError> {
let disable_interactive =
cfg!(not(feature = "cli-interactive")) || config.disable_interactive;

let StoragePaths {
registry_url: url,
registries_dir,
content_dir,
namespace_map_path,
} = config.storage_paths_for_url(url)?;

let (packages, content, namespace_map) = match (
FileSystemRegistryStorage::try_lock(registries_dir.clone())?,
FileSystemContentStorage::try_lock(content_dir.clone())?,
FileSystemNamespaceMapStorage::new(namespace_map_path.clone()),
) {
(Some(packages), Some(content), namespace_map) => (packages, content, namespace_map),
(None, _, _) => return Ok(StorageLockResult::NotAcquired(registries_dir)),
(_, None, _) => return Ok(StorageLockResult::NotAcquired(content_dir)),
};

let disable_interactive =
cfg!(not(feature = "cli-interactive")) || config.disable_interactive;
} = Self::storage_paths(registry, config, disable_interactive).await?;

let (keyring_backend, keys) = if cfg!(feature = "keyring") {
(config.keyring_backend.clone(), config.keys.clone())
Expand All @@ -1402,6 +1429,16 @@ impl FileSystemClient {
auth_token = crate::keyring::Keyring::from_config(config)?.get_auth_token(&url)?
}

let (packages, content, namespace_map) = match (
FileSystemRegistryStorage::try_lock(registries_dir.clone())?,
FileSystemContentStorage::try_lock(content_dir.clone())?,
FileSystemNamespaceMapStorage::new(namespace_map_path.clone()),
) {
(Some(packages), Some(content), namespace_map) => (packages, content, namespace_map),
(None, _, _) => return Ok(StorageLockResult::NotAcquired(registries_dir)),
(_, None, _) => return Ok(StorageLockResult::NotAcquired(content_dir)),
};

Ok(StorageLockResult::Acquired(Self::new(
url.into_url(),
packages,
Expand All @@ -1427,10 +1464,11 @@ impl FileSystemClient {
///
/// Same as calling `try_new_with_config` with
/// `Config::from_default_file()?.unwrap_or_default()`.
pub fn try_new_with_default_config(
pub async fn try_new_with_default_config(
url: Option<&str>,
) -> Result<StorageLockResult<Self>, ClientError> {
Self::try_new_with_config(url, &Config::from_default_file()?.unwrap_or_default(), None)
.await
}

/// Creates a client for the given registry URL.
Expand All @@ -1439,20 +1477,20 @@ impl FileSystemClient {
/// URL, an error is returned.
///
/// This method blocks if storage locks cannot be acquired.
pub fn new_with_config(
url: Option<&str>,
pub async fn new_with_config(
registry: Option<&str>,
config: &Config,
mut auth_token: Option<Secret<String>>,
) -> Result<Self, ClientError> {
let disable_interactive =
cfg!(not(feature = "cli-interactive")) || config.disable_interactive;

let StoragePaths {
registry_url,
registry_url: url,
registries_dir,
content_dir,
namespace_map_path,
} = config.storage_paths_for_url(url)?;

let disable_interactive =
cfg!(not(feature = "cli-interactive")) || config.disable_interactive;
} = Self::storage_paths(registry, config, disable_interactive).await?;

let (keyring_backend, keys) = if cfg!(feature = "keyring") {
(config.keyring_backend.clone(), config.keys.clone())
Expand All @@ -1462,12 +1500,11 @@ impl FileSystemClient {

#[cfg(feature = "keyring")]
if auth_token.is_none() && config.keyring_auth {
auth_token =
crate::keyring::Keyring::from_config(config)?.get_auth_token(&registry_url)?
auth_token = crate::keyring::Keyring::from_config(config)?.get_auth_token(&url)?
}

Self::new(
registry_url.into_url(),
url.into_url(),
FileSystemRegistryStorage::lock(registries_dir)?,
FileSystemContentStorage::lock(content_dir)?,
FileSystemNamespaceMapStorage::new(namespace_map_path),
Expand All @@ -1489,8 +1526,8 @@ impl FileSystemClient {
///
/// Same as calling `new_with_config` with
/// `Config::from_default_file()?.unwrap_or_default()`.
pub fn new_with_default_config(url: Option<&str>) -> Result<Self, ClientError> {
Self::new_with_config(url, &Config::from_default_file()?.unwrap_or_default(), None)
pub async fn new_with_default_config(url: Option<&str>) -> Result<Self, ClientError> {
Self::new_with_config(url, &Config::from_default_file()?.unwrap_or_default(), None).await
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/client/src/registry_url.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use url::{Host, Url};

/// The base URL of a registry server.
// Note: The inner Url always has a scheme and host.
#[derive(Clone)]
#[derive(Clone, Eq, PartialEq)]
pub struct RegistryUrl(Url);

impl RegistryUrl {
Expand Down
8 changes: 5 additions & 3 deletions src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,17 +66,19 @@ impl CommonOptions {
}

/// Creates the warg client to use.
pub fn create_client(&self, config: &Config) -> Result<FileSystemClient, ClientError> {
pub async fn create_client(&self, config: &Config) -> Result<FileSystemClient, ClientError> {
let client =
match FileSystemClient::try_new_with_config(self.registry.as_deref(), config, None)? {
match FileSystemClient::try_new_with_config(self.registry.as_deref(), config, None)
.await?
{
StorageLockResult::Acquired(client) => Ok(client),
StorageLockResult::NotAcquired(path) => {
println!(
"blocking on lock for directory `{path}`...",
path = path.display()
);

FileSystemClient::new_with_config(self.registry.as_deref(), config, None)
FileSystemClient::new_with_config(self.registry.as_deref(), config, None).await
}
}?;
Ok(client)
Expand Down
2 changes: 1 addition & 1 deletion src/commands/bundle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ impl BundleCommand {
/// Executes the command.
pub async fn exec(self) -> Result<()> {
let config = self.common.read_config()?;
let client = self.common.create_client(&config)?;
let client = self.common.create_client(&config).await?;

let info = client.package(&self.package).await?;
client.bundle_component(&info).await?;
Expand Down
2 changes: 1 addition & 1 deletion src/commands/clear.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ impl ClearCommand {
/// Executes the command.
pub async fn exec(self) -> Result<()> {
let config = self.common.read_config()?;
let client = self.common.create_client(&config)?;
let client = self.common.create_client(&config).await?;

println!("clearing local content cache...");
client.clear_content_cache().await?;
Expand Down
2 changes: 1 addition & 1 deletion src/commands/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ impl ConfigCommand {

// reset when changing home registry
if changing_home_registry {
let client = self.common.create_client(&config)?;
let client = self.common.create_client(&config).await?;
client.reset_namespaces().await?;
client.reset_registry().await?;
}
Expand Down
2 changes: 1 addition & 1 deletion src/commands/dependencies.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ impl DependenciesCommand {
/// Executes the command.
pub async fn exec(self) -> Result<()> {
let config = self.common.read_config()?;
let client = self.common.create_client(&config)?;
let client = self.common.create_client(&config).await?;

let info = client.package(&self.package).await?;
Self::print_package_info(&client, &info).await?;
Expand Down
2 changes: 1 addition & 1 deletion src/commands/download.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ impl DownloadCommand {
/// Executes the command.
pub async fn exec(self) -> Result<()> {
let config = self.common.read_config()?;
let client = self.common.create_client(&config)?;
let client = self.common.create_client(&config).await?;

println!("Downloading `{name}`...", name = self.name);

Expand Down
4 changes: 2 additions & 2 deletions src/commands/info.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,9 @@ impl InfoCommand {
/// Executes the command.
pub async fn exec(self) -> Result<()> {
let config = self.common.read_config()?;
let client = self.common.create_client(&config)?;
let client = self.common.create_client(&config).await?;

println!("\nRegistry: {url}", url = client.url());
print!("\nRegistry: {url} ", url = client.url().registry_domain());
if config.keyring_auth
&& Keyring::from_config(&config)?
.get_auth_token(client.url())?
Expand Down
2 changes: 1 addition & 1 deletion src/commands/lock.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ impl LockCommand {
/// Executes the command.
pub async fn exec(self) -> Result<()> {
let config = self.common.read_config()?;
let client = self.common.create_client(&config)?;
let client = self.common.create_client(&config).await?;

let info = client.package(&self.package).await?;
client.lock_component(&info).await?;
Expand Down
Loading