Skip to main content

wasmer_wasix/runtime/resolver/
backend_source.rs

1use std::{io::Write, path::PathBuf, sync::Arc, time::Duration};
2
3use anyhow::{Context, Error};
4use bytes::Bytes;
5use http::{HeaderMap, Method};
6use semver::{Version, VersionReq};
7use url::Url;
8use wasmer_config::package::{NamedPackageId, PackageHash, PackageId, PackageIdent, PackageSource};
9use web_time::SystemTime;
10use webc::metadata::Manifest;
11
12use crate::{
13    http::{HttpClient, HttpRequest, USER_AGENT},
14    runtime::resolver::{
15        DistributionInfo, PackageInfo, PackageSummary, QueryError, Source, WebcHash,
16    },
17};
18
19/// A [`Source`] which will resolve dependencies by pinging a Wasmer-like GraphQL
20/// endpoint.
21#[derive(Debug, Clone)]
22pub struct BackendSource {
23    registry_endpoint: Url,
24    client: Arc<dyn HttpClient + Send + Sync>,
25    cache: Option<QueryCacheConfig>,
26    token: Option<String>,
27    preferred_webc_version: webc::Version,
28}
29
30impl BackendSource {
31    pub const WASMER_DEV_ENDPOINT: &'static str = "https://registry.wasmer.wtf/graphql";
32    pub const WASMER_PROD_ENDPOINT: &'static str = "https://registry.wasmer.io/graphql";
33
34    pub fn new(registry_endpoint: Url, client: Arc<dyn HttpClient + Send + Sync>) -> Self {
35        BackendSource {
36            registry_endpoint,
37            client,
38            cache: None,
39            token: None,
40            preferred_webc_version: webc::Version::V3,
41        }
42    }
43
44    /// Cache query results locally.
45    pub fn with_local_cache(self, cache_dir: impl Into<PathBuf>, timeout: Duration) -> Self {
46        self.with_query_cache(Arc::new(FileSystemQueryCache::new(cache_dir)), timeout)
47    }
48
49    /// Cache registry query results in target-provided persistent storage.
50    pub fn with_query_cache(self, cache: Arc<dyn QueryCache>, timeout: Duration) -> Self {
51        BackendSource {
52            cache: Some(QueryCacheConfig { cache, timeout }),
53            ..self
54        }
55    }
56
57    pub fn with_auth_token(self, token: impl Into<String>) -> Self {
58        BackendSource {
59            token: Some(token.into()),
60            ..self
61        }
62    }
63
64    pub fn with_preferred_webc_version(self, version: webc::Version) -> Self {
65        BackendSource {
66            preferred_webc_version: version,
67            ..self
68        }
69    }
70
71    pub fn registry_endpoint(&self) -> &Url {
72        &self.registry_endpoint
73    }
74
75    #[tracing::instrument(level = "debug", skip_all)]
76    async fn query_graphql_named(&self, package_name: &str) -> Result<WebQuery, Error> {
77        #[derive(serde::Serialize)]
78        struct Body {
79            query: String,
80        }
81
82        let body = Body {
83            query: WASMER_WEBC_QUERY_ALL.replace("$NAME", package_name),
84        };
85
86        let request = HttpRequest {
87            url: self.registry_endpoint.clone(),
88            method: Method::POST,
89            body: Some(serde_json::to_string(&body)?.into_bytes()),
90            headers: self.headers(),
91            options: Default::default(),
92        };
93
94        tracing::debug!(%request.url, %request.method, "Querying the GraphQL API");
95        tracing::trace!(?request.headers, request.body=body.query.as_str());
96
97        let response = self.client.request(request).await?;
98
99        if !response.is_ok() {
100            let url = &self.registry_endpoint;
101            let status = response.status;
102
103            let body = if let Some(body) = &response.body {
104                String::from_utf8_lossy(body).into_owned()
105            } else {
106                "<no body>".to_string()
107            };
108
109            tracing::warn!(
110                %url,
111                %status,
112                package=%package_name,
113                %body,
114                "failed to query package info from registry"
115            );
116
117            anyhow::bail!("\"{url}\" replied with {status}");
118        }
119
120        let body = response.body.unwrap_or_default();
121        tracing::trace!(
122            %response.status,
123            %response.redirected,
124            ?response.headers,
125            "Received a response from GraphQL",
126        );
127
128        let response: WebQuery =
129            serde_json::from_slice(&body).context("Unable to deserialize the response")?;
130
131        Ok(response)
132    }
133
134    #[tracing::instrument(level = "debug", skip_all)]
135    async fn query_graphql_by_hash(
136        &self,
137        hash: &PackageHash,
138    ) -> Result<Option<PackageWebc>, Error> {
139        #[derive(serde::Serialize)]
140        struct Body {
141            query: String,
142        }
143
144        let body = Body {
145            query: WASMER_WEBC_QUERY_BY_HASH.replace("$HASH", &hash.to_string()),
146        };
147
148        let request = HttpRequest {
149            url: self.registry_endpoint.clone(),
150            method: Method::POST,
151            body: Some(serde_json::to_string(&body)?.into_bytes()),
152            headers: self.headers(),
153            options: Default::default(),
154        };
155
156        tracing::debug!(%request.url, %request.method, "Querying the GraphQL API");
157        tracing::trace!(?request.headers, request.body=body.query.as_str());
158
159        let response = self.client.request(request).await?;
160
161        if !response.is_ok() {
162            let url = &self.registry_endpoint;
163            let status = response.status;
164
165            let body = if let Some(body) = &response.body {
166                String::from_utf8_lossy(body).into_owned()
167            } else {
168                "<no body>".to_string()
169            };
170
171            tracing::warn!(
172                %url,
173                %status,
174                %hash,
175                %body,
176                "failed to query package info from registry"
177            );
178
179            anyhow::bail!("\"{url}\" replied with {status}");
180        }
181
182        let body = response.body.unwrap_or_default();
183        tracing::trace!(
184            %response.status,
185            %response.redirected,
186            ?response.headers,
187            "Received a response from GraphQL",
188        );
189
190        let response: Reply<GetPackageRelease> =
191            serde_json::from_slice(&body).context("Unable to deserialize the response")?;
192
193        Ok(response.data.get_package_release)
194    }
195
196    fn headers(&self) -> HeaderMap {
197        let mut headers = HeaderMap::new();
198        headers.insert("Content-Type", "application/json".parse().unwrap());
199        headers.insert("User-Agent", USER_AGENT.parse().unwrap());
200
201        if let Some(token) = self.token.as_deref() {
202            let raw_header = format!("Bearer {token}");
203
204            match http::HeaderValue::from_str(&raw_header) {
205                Ok(header) => {
206                    headers.insert(http::header::AUTHORIZATION, header);
207                }
208                Err(e) => {
209                    tracing::warn!(
210                        error = &e as &dyn std::error::Error,
211                        "Unable to parse the token into a header",
212                    );
213                }
214            }
215        }
216
217        headers
218    }
219
220    async fn query_by_hash(
221        &self,
222        hash: &PackageHash,
223    ) -> Result<Option<PackageSummary>, anyhow::Error> {
224        // FIXME: implementing caching!
225
226        let Some(data) = self.query_graphql_by_hash(hash).await? else {
227            return Ok(None);
228        };
229
230        let summary = data.try_into_summary(hash.clone())?;
231
232        Ok(Some(summary))
233    }
234}
235
236#[async_trait::async_trait]
237impl Source for BackendSource {
238    #[tracing::instrument(level = "debug", skip_all, fields(%package))]
239    async fn query(&self, package: &PackageSource) -> Result<Vec<PackageSummary>, QueryError> {
240        let (package_name, version_constraint) = match package {
241            PackageSource::Ident(PackageIdent::Named(n)) => (
242                n.full_name(),
243                n.version_opt().cloned().unwrap_or(semver::VersionReq::STAR),
244            ),
245            PackageSource::Ident(PackageIdent::Hash(hash)) => {
246                // TODO: implement caching!
247                match self.query_by_hash(hash).await {
248                    Ok(Some(summary)) => return Ok(vec![summary]),
249                    Ok(None) => {
250                        return Err(QueryError::NoMatches {
251                            query: package.clone(),
252                            archived_versions: Vec::new(),
253                        });
254                    }
255                    Err(error) => {
256                        return Err(QueryError::new_other(error, package));
257                    }
258                }
259            }
260            _ => {
261                return Err(QueryError::Unsupported {
262                    query: package.clone(),
263                });
264            }
265        };
266
267        if let Some(cache) = &self.cache {
268            match lookup_cached_query(cache, &package_name).await {
269                Ok(Some(cached)) => {
270                    if let Ok(cached) = matching_package_summaries(
271                        package,
272                        cached,
273                        &version_constraint,
274                        self.preferred_webc_version,
275                    ) {
276                        tracing::debug!("Cache hit!");
277                        return Ok(cached);
278                    }
279                }
280                Ok(None) => {}
281                Err(e) => {
282                    tracing::warn!(
283                        package_name,
284                        error = &*e,
285                        "An unexpected error occurred while checking the local query cache",
286                    );
287                }
288            }
289        }
290
291        let response = self
292            .query_graphql_named(&package_name)
293            .await
294            .map_err(|error| QueryError::new_other(error, package))?;
295
296        if let Some(cache) = &self.cache
297            && let Err(e) = update_cached_query(cache, &package_name, &response).await
298        {
299            tracing::warn!(
300                package_name,
301                error = &*e,
302                "An error occurred while caching the GraphQL response",
303            );
304        }
305
306        matching_package_summaries(
307            package,
308            response,
309            &version_constraint,
310            self.preferred_webc_version,
311        )
312    }
313}
314
315#[allow(clippy::result_large_err)]
316fn matching_package_summaries(
317    query: &PackageSource,
318    response: WebQuery,
319    version_constraint: &VersionReq,
320    preferred_webc_version: webc::Version,
321) -> Result<Vec<PackageSummary>, QueryError> {
322    let mut summaries = Vec::new();
323
324    let WebQueryGetPackage {
325        namespace,
326        package_name,
327        versions,
328        ..
329    } = response
330        .data
331        .get_package
332        .ok_or_else(|| QueryError::NotFound {
333            query: query.clone(),
334        })?;
335    let mut archived_versions = Vec::new();
336
337    for pkg_version in versions {
338        let version = match Version::parse(&pkg_version.version) {
339            Ok(v) => v,
340            Err(e) => {
341                tracing::debug!(
342                    pkg.version = pkg_version.version.as_str(),
343                    error = &e as &dyn std::error::Error,
344                    "Skipping a version because it doesn't have a valid version number",
345                );
346                continue;
347            }
348        };
349
350        if pkg_version.is_archived {
351            tracing::debug!(
352                pkg.version=%version,
353                "Skipping an archived version",
354            );
355            archived_versions.push(version);
356            continue;
357        }
358
359        if version_constraint.matches(&version) {
360            match decode_summary(
361                &namespace,
362                &package_name,
363                pkg_version,
364                preferred_webc_version,
365            ) {
366                Ok(summary) => summaries.push(summary),
367                Err(e) => {
368                    tracing::debug!(
369                        version=%version,
370                        error=&*e,
371                        "Skipping version because its metadata couldn't be parsed"
372                    );
373                }
374            }
375        }
376    }
377
378    if summaries.is_empty() {
379        Err(QueryError::NoMatches {
380            query: query.clone(),
381            archived_versions,
382        })
383    } else {
384        Ok(summaries)
385    }
386}
387
388fn decode_summary(
389    namespace: &str,
390    package_name: &str,
391    pkg_version: WebQueryGetPackageVersion,
392    preferred_webc_version: webc::Version,
393) -> Result<PackageSummary, Error> {
394    let WebQueryGetPackageVersion {
395        v2:
396            WebQueryGetPackageVersionDistribution {
397                pirita_sha256_hash: v2_pirita_sha256_hash,
398                pirita_download_url: v2_pirita_download_url,
399                webc_manifest: v2_manifest,
400            },
401        v3:
402            WebQueryGetPackageVersionDistribution {
403                pirita_sha256_hash: v3_pirita_sha256_hash,
404                pirita_download_url: v3_pirita_download_url,
405                webc_manifest: v3_manifest,
406            },
407        ..
408    } = pkg_version;
409
410    let (version, pirita_sha256_hash, pirita_download_url, manifest) =
411        if preferred_webc_version == webc::Version::V3 {
412            (
413                webc::Version::V3,
414                v3_pirita_sha256_hash,
415                v3_pirita_download_url,
416                v3_manifest,
417            )
418        } else {
419            (
420                webc::Version::V2,
421                v2_pirita_sha256_hash,
422                v2_pirita_download_url,
423                v2_manifest,
424            )
425        };
426
427    let id = PackageId::Named(NamedPackageId {
428        full_name: format!("{namespace}/{package_name}"),
429        version: pkg_version
430            .version
431            .parse()
432            .context("could not parse package version")?,
433    });
434
435    let manifest = manifest.context("missing Manifest")?;
436    let hash = pirita_sha256_hash.context("missing sha256")?;
437    let webc = pirita_download_url.context("missing download URL")?;
438
439    let manifest: Manifest = serde_json::from_slice(manifest.as_bytes())
440        .context("Unable to deserialize the manifest")?;
441
442    let webc_sha256 = WebcHash::parse_hex(&hash).context("invalid webc sha256 hash in manifest")?;
443
444    Ok(PackageSummary {
445        pkg: PackageInfo::from_manifest(id, &manifest, version)?,
446        dist: DistributionInfo { webc, webc_sha256 },
447    })
448}
449
450/// Persistent storage for serialized registry query responses.
451#[async_trait::async_trait]
452pub trait QueryCache: Send + Sync + std::fmt::Debug {
453    /// Load the serialized cache entry for a package name.
454    async fn load(&self, package_name: &str) -> Result<Option<Bytes>, Error>;
455
456    /// Persist a serialized cache entry for a package name.
457    async fn save(&self, package_name: &str, bytes: Bytes) -> Result<(), Error>;
458
459    /// Remove the cache entry for a package name, if present.
460    async fn remove(&self, package_name: &str) -> Result<(), Error>;
461}
462
463#[derive(Debug, Clone)]
464struct QueryCacheConfig {
465    cache: Arc<dyn QueryCache>,
466    timeout: Duration,
467}
468
469/// A local filesystem cache for registry queries.
470#[derive(Debug, Clone)]
471struct FileSystemQueryCache {
472    cache_dir: PathBuf,
473}
474
475impl FileSystemQueryCache {
476    fn new(cache_dir: impl Into<PathBuf>) -> Self {
477        Self {
478            cache_dir: cache_dir.into(),
479        }
480    }
481
482    fn path(&self, package_name: &str) -> PathBuf {
483        self.cache_dir.join(package_name.replace(['/', '\\'], "#"))
484    }
485}
486
487#[async_trait::async_trait]
488impl QueryCache for FileSystemQueryCache {
489    async fn load(&self, package_name: &str) -> Result<Option<Bytes>, Error> {
490        let filename = self.path(package_name);
491
492        tracing::trace!(filename=%filename.display(), "Reading cached entry from disk");
493        let result = crate::spawn_blocking(move || match std::fs::read(&filename) {
494            Ok(json) => Ok(Some(Bytes::from(json))),
495            Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
496            Err(e) => {
497                Err(Error::new(e).context(format!("Unable to read \"{}\"", filename.display())))
498            }
499        })
500        .await
501        .context("Unable to run the cache read task")??;
502        if result.is_none() {
503            tracing::debug!("Cache miss");
504        }
505        Ok(result)
506    }
507
508    async fn save(&self, package_name: &str, bytes: Bytes) -> Result<(), Error> {
509        let cache_dir = self.cache_dir.clone();
510        let filename = self.path(package_name);
511        tracing::debug!(
512            filename=%filename.display(),
513            package_name,
514            "Saving the query to disk",
515        );
516
517        crate::spawn_blocking(move || -> Result<(), Error> {
518            let _ = std::fs::create_dir_all(&cache_dir);
519
520            let mut temp = tempfile::NamedTempFile::new_in(&cache_dir)
521                .context("Unable to create a temporary file")?;
522            temp.write_all(&bytes)
523                .context("Unable to write the cached query")?;
524            temp.as_file()
525                .sync_all()
526                .context("Flushing the temp file failed")?;
527
528            if let Some(parent) = filename.parent() {
529                let _ = std::fs::create_dir_all(parent);
530            }
531            temp.persist(&filename).with_context(|| {
532                format!(
533                    "Unable to persist the temp file to \"{}\"",
534                    filename.display()
535                )
536            })?;
537
538            Ok(())
539        })
540        .await
541        .context("Unable to run the cache write task")??;
542
543        Ok(())
544    }
545
546    async fn remove(&self, package_name: &str) -> Result<(), Error> {
547        let filename = self.path(package_name);
548        crate::spawn_blocking(move || match std::fs::remove_file(filename) {
549            Ok(()) => Ok(()),
550            Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
551            Err(error) => Err(error.into()),
552        })
553        .await
554        .context("Unable to run the cache removal task")?
555    }
556}
557
558async fn lookup_cached_query(
559    cache: &QueryCacheConfig,
560    package_name: &str,
561) -> Result<Option<WebQuery>, Error> {
562    let Some(json) = cache.cache.load(package_name).await? else {
563        return Ok(None);
564    };
565
566    let entry: CacheEntry = match serde_json::from_slice(&json) {
567        Ok(entry) => entry,
568        Err(error) => {
569            let _ = cache.cache.remove(package_name).await;
570            return Err(Error::new(error).context("Unable to parse the cached query"));
571        }
572    };
573
574    if !entry.is_still_valid(cache.timeout) {
575        tracing::debug!(timestamp = entry.unix_timestamp, "Cached entry is stale");
576        let _ = cache.cache.remove(package_name).await;
577        return Ok(None);
578    }
579
580    if entry.package_name != package_name {
581        let _ = cache.cache.remove(package_name).await;
582        anyhow::bail!(
583            "The cached response corresponds to the \"{}\" package, but expected \"{}\"",
584            entry.package_name,
585            package_name,
586        );
587    }
588
589    Ok(Some(entry.response))
590}
591
592async fn update_cached_query(
593    cache: &QueryCacheConfig,
594    package_name: &str,
595    response: &WebQuery,
596) -> Result<(), Error> {
597    let entry = CacheEntry {
598        unix_timestamp: SystemTime::UNIX_EPOCH
599            .elapsed()
600            .unwrap_or_default()
601            .as_secs(),
602        package_name: package_name.to_string(),
603        response: response.clone(),
604    };
605    let bytes = serde_json::to_vec_pretty(&entry).context("Unable to serialize cached query")?;
606    cache.cache.save(package_name, Bytes::from(bytes)).await
607}
608
609#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
610struct CacheEntry {
611    unix_timestamp: u64,
612    package_name: String,
613    response: WebQuery,
614}
615
616impl CacheEntry {
617    fn is_still_valid(&self, timeout: Duration) -> bool {
618        let timestamp = SystemTime::UNIX_EPOCH + Duration::from_secs(self.unix_timestamp);
619
620        match timestamp.elapsed() {
621            Ok(duration) if duration <= timeout => true,
622            Ok(_) => {
623                // The cached response is too old
624                false
625            }
626            Err(_) => {
627                // It looks like the current time is **after** the time this
628                // entry was recorded. That probably indicates a clock issue
629                // so we should mark the cached value as invalid.
630                false
631            }
632        }
633    }
634}
635
636#[allow(dead_code)]
637pub const WASMER_WEBC_QUERY_ALL: &str = r#"{
638    getPackage(name: "$NAME") {
639        packageName
640        namespace
641        versions {
642          version
643          isArchived
644          v2: distribution(version: V2) {
645            piritaDownloadUrl
646            piritaSha256Hash
647            webcManifest
648          }
649          v3: distribution(version: V3) {
650            piritaDownloadUrl
651            piritaSha256Hash
652            webcManifest
653          }
654        }
655    }
656    info {
657        defaultFrontend
658    }
659}"#;
660
661pub const WASMER_WEBC_QUERY_BY_HASH: &str = r#"{
662    getPackageRelease(hash: "$HASH") {
663        piritaManifest
664        isArchived
665        webcUrl
666    }
667}"#;
668
669#[derive(serde::Serialize, serde::Deserialize, Clone, Debug)]
670pub struct Reply<T> {
671    pub data: T,
672}
673
674#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
675struct GetPackageRelease {
676    #[serde(rename = "getPackageRelease")]
677    get_package_release: Option<PackageWebc>,
678}
679
680#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
681struct PackageWebc {
682    #[serde(rename = "piritaManifest")]
683    pub pirita_manifest: String,
684    #[serde(rename = "isArchived")]
685    pub is_archived: bool,
686    #[serde(rename = "webcUrl")]
687    pub webc_url: url::Url,
688}
689
690impl PackageWebc {
691    fn try_into_summary(self, hash: PackageHash) -> Result<PackageSummary, anyhow::Error> {
692        let manifest: Manifest = serde_json::from_str(&self.pirita_manifest)
693            .context("Unable to deserialize the manifest")?;
694
695        let id = PackageId::Hash(hash.clone());
696
697        let info = PackageInfo::from_manifest(id, &manifest, webc::Version::V3)
698            .context("could not convert the manifest ")?;
699
700        Ok(PackageSummary {
701            pkg: info,
702            dist: DistributionInfo {
703                webc: self.webc_url,
704                // TODO: replace with different hash type?
705                webc_sha256: WebcHash(hash.as_sha256().context("invalid hash")?.0),
706            },
707        })
708    }
709}
710
711#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
712pub struct WebQuery {
713    #[serde(rename = "data")]
714    pub data: WebQueryData,
715}
716
717#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
718pub struct WebQueryData {
719    #[serde(rename = "getPackage")]
720    pub get_package: Option<WebQueryGetPackage>,
721    pub info: WebQueryInfo,
722}
723
724#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
725pub struct WebQueryInfo {
726    #[serde(rename = "defaultFrontend")]
727    pub default_frontend: Url,
728}
729
730#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
731pub struct WebQueryGetPackage {
732    #[serde(rename = "packageName")]
733    pub package_name: String,
734    pub namespace: String,
735    pub versions: Vec<WebQueryGetPackageVersion>,
736}
737
738#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
739pub struct WebQueryGetPackageVersion {
740    pub version: String,
741    /// Has the package been archived?
742    #[serde(rename = "isArchived", default)]
743    pub is_archived: bool,
744    pub v2: WebQueryGetPackageVersionDistribution,
745    pub v3: WebQueryGetPackageVersionDistribution,
746}
747
748#[derive(Debug, serde::Serialize, serde::Deserialize, Clone, Default)]
749pub enum WebCVersion {
750    #[default]
751    V2,
752    V3,
753}
754
755impl From<WebCVersion> for webc::Version {
756    fn from(val: WebCVersion) -> Self {
757        match val {
758            WebCVersion::V2 => webc::Version::V2,
759            WebCVersion::V3 => webc::Version::V3,
760        }
761    }
762}
763
764#[derive(Debug, serde::Serialize, serde::Deserialize, Clone)]
765pub struct WebQueryGetPackageVersionDistribution {
766    #[serde(rename = "piritaDownloadUrl")]
767    pub pirita_download_url: Option<Url>,
768    #[serde(rename = "piritaSha256Hash")]
769    pub pirita_sha256_hash: Option<String>,
770    #[serde(rename = "webcManifest")]
771    pub webc_manifest: Option<String>,
772}
773
774#[cfg(test)]
775mod tests {
776    use std::{str::FromStr, sync::Mutex};
777
778    use http::{HeaderMap, StatusCode};
779
780    use crate::{
781        http::HttpResponse,
782        runtime::resolver::inputs::{DistributionInfo, PackageInfo},
783    };
784
785    use super::*;
786
787    // You can check the response with:
788    // curl https://registry.wasmer.io/graphql \
789    //      -H "Content-Type: application/json" \
790    //      -X POST \
791    //      -d '@wasmer_pack_cli_request.json' > wasmer_pack_cli_response.json
792    const WASMER_PACK_CLI_REQUEST: &[u8] = br#"
793    {
794        "query":"{\n    getPackage(name: \"wasmer/wasmer-pack-cli\") {\n        packageName\n        namespace\n        versions {\n          version\n          isArchived\n          v2: distribution(version: V2) {\n            piritaDownloadUrl\n            piritaSha256Hash\n            webcManifest\n          }\n          v3: distribution(version: V3) {\n            piritaDownloadUrl\n            piritaSha256Hash\n            webcManifest\n          }\n        }\n    }\n    info {\n        defaultFrontend\n    }\n}"
795    }
796    "#;
797    const WASMER_PACK_CLI_RESPONSE: &[u8] = br#"
798    {
799        "data": {
800          "getPackage": {
801            "packageName": "wasmer-pack-cli",
802            "namespace": "wasmer",
803            "versions": [
804              {
805                "version": "0.7.1",
806                "isArchived": false,
807                "v2": {
808                    "webcManifest": "{\"atoms\": {\"wasmer-pack\": {\"kind\": \"https://webc.org/kind/wasm\", \"signature\": \"sha256:gGeLZqPitpg893Jj/nvGa+1235RezSWA9FjssopzOZY=\"}}, \"package\": {\"wapm\": {\"name\": \"wasmer/wasmer-pack-cli\", \"readme\": {\"path\": \"README.md\", \"volume\": \"metadata\"}, \"license\": \"MIT\", \"version\": \"0.7.1\", \"homepage\": \"https://wasmer.io/\", \"repository\": \"https://github.com/wasmerio/wasmer-pack\", \"description\": \"A code generator that lets you treat WebAssembly modules like native dependencies.\"}}, \"commands\": {\"wasmer-pack\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"wasmer-pack\", \"package\": \"wasmer/wasmer-pack-cli\", \"main_args\": null}}}}, \"entrypoint\": \"wasmer-pack\"}",
809                  "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.7.1/wasmer-pack-cli-0.7.1.webc",
810                  "piritaSha256Hash": "e821047f446dd20fb6b43a1648fe98b882276dfc480f020df6f00a49f69771fa"
811                },
812                "v3": {
813                    "webcManifest": "{\"atoms\": {\"wasmer-pack\": {\"kind\": \"https://webc.org/kind/wasm\", \"signature\": \"sha256:gGeLZqPitpg893Jj/nvGa+1235RezSWA9FjssopzOZY=\"}}, \"package\": {\"wapm\": {\"name\": \"wasmer/wasmer-pack-cli\", \"readme\": {\"path\": \"README.md\", \"volume\": \"metadata\"}, \"license\": \"MIT\", \"version\": \"0.7.1\", \"homepage\": \"https://wasmer.io/\", \"repository\": \"https://github.com/wasmerio/wasmer-pack\", \"description\": \"A code generator that lets you treat WebAssembly modules like native dependencies.\"}}, \"commands\": {\"wasmer-pack\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"wasmer-pack\", \"package\": \"wasmer/wasmer-pack-cli\", \"main_args\": null}}}}, \"entrypoint\": \"wasmer-pack\"}",
814                  "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.7.1/wasmer-pack-cli-0.7.1.webc",
815                  "piritaSha256Hash": "e821047f446dd20fb6b43a1648fe98b882276dfc480f020df6f00a49f69771fa"
816                }
817              },
818              {
819                "version": "0.7.0",
820                "isArchived": false,
821                "v2": {
822                    "webcManifest": "{\"atoms\": {\"wasmer-pack\": {\"kind\": \"https://webc.org/kind/wasm\", \"signature\": \"sha256:FesCIAS6URjrIAAyy4G5u5HjJjGQBLGmnafjHPHRvqo=\"}}, \"package\": {\"wapm\": {\"name\": \"wasmer/wasmer-pack-cli\", \"readme\": {\"path\": \"/home/consulting/Documents/wasmer/wasmer-pack/crates/cli/../../README.md\", \"volume\": \"metadata\"}, \"license\": \"MIT\", \"version\": \"0.7.0\", \"homepage\": \"https://wasmer.io/\", \"repository\": \"https://github.com/wasmerio/wasmer-pack\", \"description\": \"A code generator that lets you treat WebAssembly modules like native dependencies.\"}}, \"commands\": {\"wasmer-pack\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"wasmer-pack\", \"package\": \"wasmer/wasmer-pack-cli\", \"main_args\": null}}}}, \"entrypoint\": \"wasmer-pack\"}",
823                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.7.0/wasmer-pack-cli-0.7.0.webc",
824                    "piritaSha256Hash": "d085869201aa602673f70abbd5e14e5a6936216fa93314c5b103cda3da56e29e"
825                },
826                "v3": {
827                    "webcManifest": "{\"atoms\": {\"wasmer-pack\": {\"kind\": \"https://webc.org/kind/wasm\", \"signature\": \"sha256:FesCIAS6URjrIAAyy4G5u5HjJjGQBLGmnafjHPHRvqo=\"}}, \"package\": {\"wapm\": {\"name\": \"wasmer/wasmer-pack-cli\", \"readme\": {\"path\": \"/home/consulting/Documents/wasmer/wasmer-pack/crates/cli/../../README.md\", \"volume\": \"metadata\"}, \"license\": \"MIT\", \"version\": \"0.7.0\", \"homepage\": \"https://wasmer.io/\", \"repository\": \"https://github.com/wasmerio/wasmer-pack\", \"description\": \"A code generator that lets you treat WebAssembly modules like native dependencies.\"}}, \"commands\": {\"wasmer-pack\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"wasmer-pack\", \"package\": \"wasmer/wasmer-pack-cli\", \"main_args\": null}}}}, \"entrypoint\": \"wasmer-pack\"}",
828                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.7.0/wasmer-pack-cli-0.7.0.webc",
829                    "piritaSha256Hash": "d085869201aa602673f70abbd5e14e5a6936216fa93314c5b103cda3da56e29e"
830                }
831              },
832              {
833                "version": "0.6.0",
834                "isArchived": false,
835                "v2": {
836                    "webcManifest": "{\"atoms\": {\"wasmer-pack\": {\"kind\": \"https://webc.org/kind/wasm\", \"signature\": \"sha256:CzzhNaav3gjBkCJECGbk7e+qAKurWbcIAzQvEqsr2Co=\"}}, \"package\": {\"wapm\": {\"name\": \"wasmer/wasmer-pack-cli\", \"readme\": {\"path\": \"/home/consulting/Documents/wasmer/wasmer-pack/crates/cli/../../README.md\", \"volume\": \"metadata\"}, \"license\": \"MIT\", \"version\": \"0.6.0\", \"homepage\": \"https://wasmer.io/\", \"repository\": \"https://github.com/wasmerio/wasmer-pack\", \"description\": \"A code generator that lets you treat WebAssembly modules like native dependencies.\"}}, \"commands\": {\"wasmer-pack\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"wasmer-pack\", \"package\": \"wasmer/wasmer-pack-cli\", \"main_args\": null}}}}, \"entrypoint\": \"wasmer-pack\"}",
837                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.6.0/wasmer-pack-cli-0.6.0.webc",
838                    "piritaSha256Hash": "7e1add1640d0037ff6a726cd7e14ea36159ec2db8cb6debd0e42fa2739bea52b"
839                },
840                "v3": {
841                    "webcManifest": "{\"atoms\": {\"wasmer-pack\": {\"kind\": \"https://webc.org/kind/wasm\", \"signature\": \"sha256:CzzhNaav3gjBkCJECGbk7e+qAKurWbcIAzQvEqsr2Co=\"}}, \"package\": {\"wapm\": {\"name\": \"wasmer/wasmer-pack-cli\", \"readme\": {\"path\": \"/home/consulting/Documents/wasmer/wasmer-pack/crates/cli/../../README.md\", \"volume\": \"metadata\"}, \"license\": \"MIT\", \"version\": \"0.6.0\", \"homepage\": \"https://wasmer.io/\", \"repository\": \"https://github.com/wasmerio/wasmer-pack\", \"description\": \"A code generator that lets you treat WebAssembly modules like native dependencies.\"}}, \"commands\": {\"wasmer-pack\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"wasmer-pack\", \"package\": \"wasmer/wasmer-pack-cli\", \"main_args\": null}}}}, \"entrypoint\": \"wasmer-pack\"}",
842                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.6.0/wasmer-pack-cli-0.6.0.webc",
843                    "piritaSha256Hash": "7e1add1640d0037ff6a726cd7e14ea36159ec2db8cb6debd0e42fa2739bea52b"
844                }
845              },
846              {
847                "version": "0.5.3",
848                "isArchived": false,
849                "v2": {
850                    "webcManifest": "{\"atoms\": {\"wasmer-pack\": {\"kind\": \"https://webc.org/kind/wasm\", \"signature\": \"sha256:qdiJVfpi4icJXdR7Y5US/pJ4PjqbAq9PkU+obMZIMlE=\"}}, \"package\": {\"wapm\": {\"name\": \"wasmer/wasmer-pack-cli\", \"readme\": {\"path\": \"/home/runner/work/wasmer-pack/wasmer-pack/crates/cli/../../README.md\", \"volume\": \"metadata\"}, \"license\": \"MIT\", \"version\": \"0.5.3\", \"homepage\": \"https://wasmer.io/\", \"repository\": \"https://github.com/wasmerio/wasmer-pack\", \"description\": \"A code generator that lets you treat WebAssembly modules like native dependencies.\"}}, \"commands\": {\"wasmer-pack\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"wasmer-pack\", \"package\": \"wasmer/wasmer-pack-cli\", \"main_args\": null}}}}, \"entrypoint\": \"wasmer-pack\"}",
851                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.5.3/wasmer-pack-cli-0.5.3.webc",
852                    "piritaSha256Hash": "44fdcdde23d34175887243d7c375e4e4a7e6e2cd1ae063ebffbede4d1f68f14a"
853                },
854                "v3": {
855                    "webcManifest": "{\"atoms\": {\"wasmer-pack\": {\"kind\": \"https://webc.org/kind/wasm\", \"signature\": \"sha256:qdiJVfpi4icJXdR7Y5US/pJ4PjqbAq9PkU+obMZIMlE=\"}}, \"package\": {\"wapm\": {\"name\": \"wasmer/wasmer-pack-cli\", \"readme\": {\"path\": \"/home/runner/work/wasmer-pack/wasmer-pack/crates/cli/../../README.md\", \"volume\": \"metadata\"}, \"license\": \"MIT\", \"version\": \"0.5.3\", \"homepage\": \"https://wasmer.io/\", \"repository\": \"https://github.com/wasmerio/wasmer-pack\", \"description\": \"A code generator that lets you treat WebAssembly modules like native dependencies.\"}}, \"commands\": {\"wasmer-pack\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"wasmer-pack\", \"package\": \"wasmer/wasmer-pack-cli\", \"main_args\": null}}}}, \"entrypoint\": \"wasmer-pack\"}",
856                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.5.3/wasmer-pack-cli-0.5.3.webc",
857                    "piritaSha256Hash": "44fdcdde23d34175887243d7c375e4e4a7e6e2cd1ae063ebffbede4d1f68f14a"
858                }
859              },
860              {
861                "version": "0.5.2",
862                "isArchived": false,
863                "v2": {
864                    "webcManifest": "{\"atoms\": {\"wasmer-pack\": {\"kind\": \"https://webc.org/kind/wasm\", \"signature\": \"sha256:xiwrUFAo+cU1xW/IE6MVseiyjNGHtXooRlkYKiOKzQc=\"}}, \"package\": {\"wapm\": {\"name\": \"wasmer/wasmer-pack-cli\", \"readme\": {\"path\": \"/home/consulting/Documents/wasmer/wasmer-pack/crates/cli/../../README.md\", \"volume\": \"metadata\"}, \"license\": \"MIT\", \"version\": \"0.5.2\", \"homepage\": \"https://wasmer.io/\", \"repository\": \"https://github.com/wasmerio/wasmer-pack\", \"description\": \"A code generator that lets you treat WebAssembly modules like native dependencies.\"}}, \"commands\": {\"wasmer-pack\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"wasmer-pack\", \"package\": \"wasmer/wasmer-pack-cli\", \"main_args\": null}}}}, \"entrypoint\": \"wasmer-pack\"}",
865                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.5.2/wasmer-pack-cli-0.5.2.webc",
866                    "piritaSha256Hash": "d1dbc8168c3a2491a7158017a9c88df9e0c15bed88ebcd6d9d756e4b03adde95"
867                },
868                "v3": {
869                    "webcManifest": "{\"atoms\": {\"wasmer-pack\": {\"kind\": \"https://webc.org/kind/wasm\", \"signature\": \"sha256:xiwrUFAo+cU1xW/IE6MVseiyjNGHtXooRlkYKiOKzQc=\"}}, \"package\": {\"wapm\": {\"name\": \"wasmer/wasmer-pack-cli\", \"readme\": {\"path\": \"/home/consulting/Documents/wasmer/wasmer-pack/crates/cli/../../README.md\", \"volume\": \"metadata\"}, \"license\": \"MIT\", \"version\": \"0.5.2\", \"homepage\": \"https://wasmer.io/\", \"repository\": \"https://github.com/wasmerio/wasmer-pack\", \"description\": \"A code generator that lets you treat WebAssembly modules like native dependencies.\"}}, \"commands\": {\"wasmer-pack\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"wasmer-pack\", \"package\": \"wasmer/wasmer-pack-cli\", \"main_args\": null}}}}, \"entrypoint\": \"wasmer-pack\"}",
870                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.5.2/wasmer-pack-cli-0.5.2.webc",
871                    "piritaSha256Hash": "d1dbc8168c3a2491a7158017a9c88df9e0c15bed88ebcd6d9d756e4b03adde95"
872                }
873              },
874              {
875                "version": "0.5.1",
876                "isArchived": false,
877                "v2": {
878                    "webcManifest": "{\"atoms\": {\"wasmer-pack\": {\"kind\": \"https://webc.org/kind/wasm\", \"signature\": \"sha256:TliPwutfkFvRite/3/k3OpLqvV0EBKGwyp3L5UjCuEI=\"}}, \"package\": {\"wapm\": {\"name\": \"wasmer/wasmer-pack-cli\", \"readme\": {\"path\": \"/home/runner/work/wasmer-pack/wasmer-pack/crates/cli/../../README.md\", \"volume\": \"metadata\"}, \"license\": \"MIT\", \"version\": \"0.5.1\", \"homepage\": \"https://wasmer.io/\", \"repository\": \"https://github.com/wasmerio/wasmer-pack\", \"description\": \"A code generator that lets you treat WebAssembly modules like native dependencies.\"}}, \"commands\": {\"wasmer-pack\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"wasmer-pack\", \"package\": \"wasmer/wasmer-pack-cli\", \"main_args\": null}}}}, \"entrypoint\": \"wasmer-pack\"}",
879                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.5.1/wasmer-pack-cli-0.5.1.webc",
880                    "piritaSha256Hash": "c42924619660e2befd69b5c72729388985dcdcbf912d51a00015237fec3e1ade"
881                },
882                "v3": {
883                    "webcManifest": "{\"atoms\": {\"wasmer-pack\": {\"kind\": \"https://webc.org/kind/wasm\", \"signature\": \"sha256:TliPwutfkFvRite/3/k3OpLqvV0EBKGwyp3L5UjCuEI=\"}}, \"package\": {\"wapm\": {\"name\": \"wasmer/wasmer-pack-cli\", \"readme\": {\"path\": \"/home/runner/work/wasmer-pack/wasmer-pack/crates/cli/../../README.md\", \"volume\": \"metadata\"}, \"license\": \"MIT\", \"version\": \"0.5.1\", \"homepage\": \"https://wasmer.io/\", \"repository\": \"https://github.com/wasmerio/wasmer-pack\", \"description\": \"A code generator that lets you treat WebAssembly modules like native dependencies.\"}}, \"commands\": {\"wasmer-pack\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"wasmer-pack\", \"package\": \"wasmer/wasmer-pack-cli\", \"main_args\": null}}}}, \"entrypoint\": \"wasmer-pack\"}",
884                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.5.1/wasmer-pack-cli-0.5.1.webc",
885                    "piritaSha256Hash": "c42924619660e2befd69b5c72729388985dcdcbf912d51a00015237fec3e1ade"
886                }
887              },
888              {
889                "version": "0.5.0",
890                "isArchived": false,
891                "v2": {
892                    "webcManifest": "{\"atoms\": {\"wasmer-pack\": {\"kind\": \"https://webc.org/kind/wasm\", \"signature\": \"sha256:6UD7NS4KtyNYa3TcnKOvd+kd3LxBCw+JQ8UWRpMXeC0=\"}}, \"package\": {\"wapm\": {\"name\": \"wasmer/wasmer-pack-cli\", \"readme\": {\"path\": \"README.md\", \"volume\": \"metadata\"}, \"license\": \"MIT\", \"version\": \"0.5.0\", \"homepage\": \"https://wasmer.io/\", \"repository\": \"https://github.com/wasmerio/wasmer-pack\", \"description\": \"A code generator that lets you treat WebAssembly modules like native dependencies.\"}}, \"commands\": {\"wasmer-pack\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"wasmer-pack\", \"package\": \"wasmer/wasmer-pack-cli\", \"main_args\": null}}}}, \"entrypoint\": \"wasmer-pack\"}",
893                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.5.0/wasmer-pack-cli-0.5.0.webc",
894                    "piritaSha256Hash": "d30ca468372faa96469163d2d1546dd34be9505c680677e6ab86a528a268e5f5"
895                },
896                "v3": {
897                    "webcManifest": "{\"atoms\": {\"wasmer-pack\": {\"kind\": \"https://webc.org/kind/wasm\", \"signature\": \"sha256:6UD7NS4KtyNYa3TcnKOvd+kd3LxBCw+JQ8UWRpMXeC0=\"}}, \"package\": {\"wapm\": {\"name\": \"wasmer/wasmer-pack-cli\", \"readme\": {\"path\": \"README.md\", \"volume\": \"metadata\"}, \"license\": \"MIT\", \"version\": \"0.5.0\", \"homepage\": \"https://wasmer.io/\", \"repository\": \"https://github.com/wasmerio/wasmer-pack\", \"description\": \"A code generator that lets you treat WebAssembly modules like native dependencies.\"}}, \"commands\": {\"wasmer-pack\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"wasmer-pack\", \"package\": \"wasmer/wasmer-pack-cli\", \"main_args\": null}}}}, \"entrypoint\": \"wasmer-pack\"}",
898                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.5.0/wasmer-pack-cli-0.5.0.webc",
899                    "piritaSha256Hash": "d30ca468372faa96469163d2d1546dd34be9505c680677e6ab86a528a268e5f5"
900                }
901              },
902              {
903                "version": "0.5.0-rc.1",
904                "isArchived": false,
905                "v2": {
906                    "webcManifest": "{\"atoms\": {\"wasmer-pack\": {\"kind\": \"https://webc.org/kind/wasm\", \"signature\": \"sha256:ThybHIc2elJEcDdQiq5ffT1TVaNs70+WAqoKw4Tkh3E=\"}}, \"package\": {\"wapm\": {\"name\": \"wasmer/wasmer-pack-cli\", \"readme\": {\"path\": \"README.md\", \"volume\": \"metadata\"}, \"license\": \"MIT\", \"version\": \"0.5.0-rc.1\", \"homepage\": \"https://wasmer.io/\", \"repository\": \"https://github.com/wasmerio/wasmer-pack\", \"description\": \"A code generator that lets you treat WebAssembly modules like native dependencies.\"}}, \"commands\": {\"wasmer-pack\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"wasmer-pack\", \"package\": \"wasmer/wasmer-pack-cli\", \"main_args\": null}}}}, \"entrypoint\": \"wasmer-pack\"}",
907                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.5.0-rc.1/wasmer-pack-cli-0.5.0-rc.1.webc",
908                    "piritaSha256Hash": "0cd5d6e4c33c92c52784afed3a60c056953104d719717948d4663ff2521fe2bb"
909                },
910                "v3": {
911                    "webcManifest": "{\"atoms\": {\"wasmer-pack\": {\"kind\": \"https://webc.org/kind/wasm\", \"signature\": \"sha256:ThybHIc2elJEcDdQiq5ffT1TVaNs70+WAqoKw4Tkh3E=\"}}, \"package\": {\"wapm\": {\"name\": \"wasmer/wasmer-pack-cli\", \"readme\": {\"path\": \"README.md\", \"volume\": \"metadata\"}, \"license\": \"MIT\", \"version\": \"0.5.0-rc.1\", \"homepage\": \"https://wasmer.io/\", \"repository\": \"https://github.com/wasmerio/wasmer-pack\", \"description\": \"A code generator that lets you treat WebAssembly modules like native dependencies.\"}}, \"commands\": {\"wasmer-pack\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"wasmer-pack\", \"package\": \"wasmer/wasmer-pack-cli\", \"main_args\": null}}}}, \"entrypoint\": \"wasmer-pack\"}",
912                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.5.0-rc.1/wasmer-pack-cli-0.5.0-rc.1.webc",
913                    "piritaSha256Hash": "0cd5d6e4c33c92c52784afed3a60c056953104d719717948d4663ff2521fe2bb"
914                }
915              }
916            ]
917          },
918          "info": {
919            "defaultFrontend": "https://wasmer.io"
920          }
921        }
922      }
923    "#;
924
925    #[derive(Debug)]
926    struct DummyClient {
927        requests: Mutex<Vec<HttpRequest>>,
928        responses: Mutex<Vec<HttpResponse>>,
929    }
930
931    impl DummyClient {
932        fn new(responses: Vec<HttpResponse>) -> Self {
933            DummyClient {
934                requests: Mutex::new(Vec::new()),
935                responses: Mutex::new(responses),
936            }
937        }
938
939        fn take_requests(&self) -> Vec<HttpRequest> {
940            std::mem::take(&mut *self.requests.lock().unwrap())
941        }
942    }
943
944    impl HttpClient for DummyClient {
945        fn request(
946            &self,
947            request: HttpRequest,
948        ) -> futures::future::BoxFuture<'_, Result<HttpResponse, anyhow::Error>> {
949            self.requests.lock().unwrap().push(request);
950            let response = self.responses.lock().unwrap().remove(0);
951            Box::pin(async { Ok(response) })
952        }
953    }
954
955    #[tokio::test]
956    async fn run_known_query() {
957        let response = HttpResponse {
958            body: Some(WASMER_PACK_CLI_RESPONSE.to_vec()),
959            redirected: false,
960            status: StatusCode::OK,
961            headers: HeaderMap::new(),
962        };
963        let client = Arc::new(DummyClient::new(vec![response]));
964        let registry_endpoint = BackendSource::WASMER_PROD_ENDPOINT.parse().unwrap();
965        let request = PackageSource::from_str("wasmer/wasmer-pack-cli@^0.6").unwrap();
966        let source = BackendSource::new(registry_endpoint, client.clone());
967
968        let summaries = source.query(&request).await.unwrap();
969
970        assert_eq!(
971            summaries,
972            [PackageSummary {
973                pkg: PackageInfo {
974                    id: PackageId::new_named("wasmer/wasmer-pack-cli", Version::new(0, 6, 0)),
975                    dependencies: Vec::new(),
976                    commands: vec![crate::runtime::resolver::Command {
977                        name: "wasmer-pack".to_string(),
978                    },],
979                    entrypoint: Some("wasmer-pack".to_string()),
980                    filesystem: vec![],
981                },
982                dist: DistributionInfo {
983                    webc: "https://storage.googleapis.com/wapm-registry-prod/webc/wasmer/wasmer-pack-cli/0.6.0/wasmer-pack-cli-0.6.0.webc"
984                        .parse()
985                        .unwrap(),
986                    webc_sha256: WebcHash::from_bytes([
987                        126, 26, 221, 22, 64, 208, 3, 127, 246, 167, 38, 205, 126, 20, 234, 54, 21,
988                        158, 194, 219, 140, 182, 222, 189, 14, 66, 250, 39, 57, 190, 165, 43,
989                    ]),
990                }
991            }]
992        );
993        let requests = client.take_requests();
994        assert_eq!(requests.len(), 1);
995        let request = &requests[0];
996        assert_eq!(request.method, http::Method::POST);
997        assert_eq!(request.url.as_str(), BackendSource::WASMER_PROD_ENDPOINT);
998        assert_eq!(request.headers.len(), 2);
999        assert_eq!(request.headers["User-Agent"], USER_AGENT);
1000        assert_eq!(request.headers["Content-Type"], "application/json");
1001        let body: serde_json::Value =
1002            serde_json::from_slice(request.body.as_deref().unwrap()).unwrap();
1003        let expected_body: serde_json::Value =
1004            serde_json::from_slice(WASMER_PACK_CLI_REQUEST).unwrap();
1005        assert_eq!(body, expected_body);
1006    }
1007
1008    /// For the full context, see #3946 on GitHub or the original conversation
1009    /// [on
1010    /// Slack](https://wasmerio.slack.com/archives/C03MX4KL6KH/p1685706988500919).
1011    #[tokio::test]
1012    async fn skip_package_versions_with_missing_fields() {
1013        let body = serde_json::json! {
1014            {
1015                "data": {
1016                    "getPackage": {
1017                        "packageName": "cowsay",
1018                        "namespace": "_",
1019                        "versions": [
1020                            {
1021                                "version": "0.2.0",
1022                                "v2": {
1023                                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/packages/_/cowsay/cowsay-0.2.0.webc",
1024                                    "piritaSha256Hash": "9586938a0a89219dafe4ae97a901c56d4b3e2a9941520d1309ae880c9a1868c9",
1025                                    "webcManifest": "{\"atoms\": {\"cowsay\": {\"kind\": \"https://webc.org/kind/wasm\", \"signature\": \"sha256:DPmhiSNXCg5261eTUi3BIvAc/aJttGj+nD+bGhQkVQo=\"}}, \"package\": {\"wapm\": {\"name\": \"cowsay\", \"readme\": {\"path\": \"README.md\", \"volume\": \"metadata\"}, \"version\": \"0.2.0\", \"repository\": \"https://github.com/wapm-packages/cowsay\", \"description\": \"cowsay is a program that generates ASCII pictures of a cow with a message\"}}, \"commands\": {\"cowsay\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"cowsay\", \"package\": null, \"main_args\": null}}}, \"cowthink\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"cowsay\", \"package\": null, \"main_args\": null}}}}}",
1026                                },
1027                                "v3": {
1028                                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/packages/_/cowsay/cowsay-0.2.0.webc",
1029                                    "piritaSha256Hash": "9586938a0a89219dafe4ae97a901c56d4b3e2a9941520d1309ae880c9a1868c9",
1030                                    "webcManifest": "{\"atoms\": {\"cowsay\": {\"kind\": \"https://webc.org/kind/wasm\", \"signature\": \"sha256:DPmhiSNXCg5261eTUi3BIvAc/aJttGj+nD+bGhQkVQo=\"}}, \"package\": {\"wapm\": {\"name\": \"cowsay\", \"readme\": {\"path\": \"README.md\", \"volume\": \"metadata\"}, \"version\": \"0.2.0\", \"repository\": \"https://github.com/wapm-packages/cowsay\", \"description\": \"cowsay is a program that generates ASCII pictures of a cow with a message\"}}, \"commands\": {\"cowsay\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"cowsay\", \"package\": null, \"main_args\": null}}}, \"cowthink\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"cowsay\", \"package\": null, \"main_args\": null}}}}}",
1031                                }
1032                            },
1033                            {
1034                                "version": "0.1.3",
1035                                "v2": {
1036                                    "piritaDownloadUrl": "https://example.com/",
1037                                    "piritaSha256Hash": "1234asdf"
1038                                },
1039                                "v3": {
1040                                    "piritaDownloadUrl": "https://example.com/",
1041                                    "piritaSha256Hash": "1234asdf"
1042                                }
1043                            },
1044                            {
1045                                "version": "0.1.2",
1046                                "v2": {
1047                                    "piritaDownloadUrl": "https://example.com/",
1048                                    "piritaSha256Hash": "1234asdf",
1049                                    "webcManifest": "{}",
1050                                },
1051                                "v3": {
1052                                    "piritaDownloadUrl": "https://example.com/",
1053                                    "piritaSha256Hash": "1234asdf",
1054                                    "webcManifest": "{}",
1055                                }
1056                            },
1057                            {
1058                                "version": "0.1.3",
1059                                "v2": {
1060                                    "webcManifest": "{}",
1061                                    "piritaDownloadUrl": "https://example.com/",
1062                                    "piritaSha256Hash": "1234asdf"
1063                                },
1064                                "v3": {
1065                                    "webcManifest": "{}",
1066                                    "piritaDownloadUrl": "https://example.com/",
1067                                    "piritaSha256Hash": "1234asdf"
1068                                }
1069                            }
1070                        ]
1071                    },
1072                    "info": {
1073                        "defaultFrontend": "https://wasmer.io/",
1074                    },
1075                }
1076            }
1077
1078        };
1079        let response = HttpResponse {
1080            body: Some(serde_json::to_vec(&body).unwrap()),
1081            redirected: false,
1082            status: StatusCode::OK,
1083            headers: HeaderMap::new(),
1084        };
1085        let client = Arc::new(DummyClient::new(vec![response]));
1086        let registry_endpoint = BackendSource::WASMER_PROD_ENDPOINT.parse().unwrap();
1087        let request = PackageSource::from_str("_/cowsay").unwrap();
1088        let source = BackendSource::new(registry_endpoint, client.clone());
1089
1090        let summaries = source.query(&request).await.unwrap();
1091
1092        assert_eq!(summaries.len(), 1);
1093        assert_eq!(
1094            summaries[0].pkg.id.as_named().unwrap().version.to_string(),
1095            "0.2.0"
1096        );
1097    }
1098
1099    #[tokio::test]
1100    async fn skip_archived_package_versions() {
1101        let body = serde_json::json! {
1102            {
1103                "data": {
1104                    "getPackage": {
1105                        "packageName": "python",
1106                        "namespace": "wasmer",
1107                        "versions": [
1108                            {
1109                                "version": "3.12.2",
1110                                "isArchived": true,
1111                                "v2": {
1112                                    "webcManifest": "{\"atoms\": {\"python\": {\"kind\": \"https://webc.org/kind/wasm\", \"signature\": \"sha256:ibsq6QL4qB4GtCE8IA2yfHVwI4fLoIGXsALsAx16y5M=\"}}, \"package\": {\"wapm\": {\"name\": \"wasmer/python\", \"license\": \"ISC\", \"version\": \"3.12.2\", \"repository\": \"https://github.com/wapm-packages/python\", \"description\": \"Python is an interpreted, high-level, general-purpose programming language\"}}, \"commands\": {\"python\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"python\", \"package\": null, \"main_args\": null}}}}, \"entrypoint\": \"python\"}",
1113                                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/packages/wasmer/python/python-3.12.0-build.5-a11e0414-c68d-473c-958f-fc96ef7adb20.webc",
1114                                    "piritaSha256Hash": "7771ed54376c16da86581736fad84fb761a049915902a7070e854965be0d5874"
1115                                },
1116                                "v3": {
1117                                    "webcManifest": "{\"atoms\": {\"python\": {\"kind\": \"https://webc.org/kind/wasm\", \"signature\": \"sha256:ibsq6QL4qB4GtCE8IA2yfHVwI4fLoIGXsALsAx16y5M=\"}}, \"package\": {\"wapm\": {\"name\": \"wasmer/python\", \"license\": \"ISC\", \"version\": \"3.12.2\", \"repository\": \"https://github.com/wapm-packages/python\", \"description\": \"Python is an interpreted, high-level, general-purpose programming language\"}}, \"commands\": {\"python\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"python\", \"package\": null, \"main_args\": null}}}}, \"entrypoint\": \"python\"}",
1118                                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/packages/wasmer/python/python-3.12.0-build.5-a11e0414-c68d-473c-958f-fc96ef7adb20.webc",
1119                                    "piritaSha256Hash": "7771ed54376c16da86581736fad84fb761a049915902a7070e854965be0d5874"
1120                                }
1121                            },
1122                            {
1123                                "version": "3.12.1",
1124                                "isArchived": false,
1125                                "v2": {
1126                                    "webcManifest": "{\"atoms\": {\"python\": {\"kind\": \"https://webc.org/kind/wasm\", \"signature\": \"sha256:O36BXLHv3/80cABbAiF7gzuSHzzin1blTfJ42LDhT18=\"}}, \"package\": {\"wapm\": {\"name\": \"wasmer/python\", \"license\": \"ISC\", \"version\": \"3.12.1\", \"repository\": \"https://github.com/wapm-packages/python\", \"description\": \"Python is an interpreted, high-level, general-purpose programming language\"}}, \"commands\": {\"python\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"python\", \"package\": null, \"main_args\": null}}}}, \"entrypoint\": \"python\"}",
1127                                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/packages/wasmer/python/python-3.12.0-build.2-ed98c999-fcda-4f80-96dc-7c0f8be8baa6.webc",
1128                                    "piritaSha256Hash": "7835401e3ca1977ba05b5e51541363783b8a7700da270dd851f10fe2e4f27f07"
1129                                },
1130                                "v3": {
1131                                    "webcManifest": "{\"atoms\": {\"python\": {\"kind\": \"https://webc.org/kind/wasm\", \"signature\": \"sha256:O36BXLHv3/80cABbAiF7gzuSHzzin1blTfJ42LDhT18=\"}}, \"package\": {\"wapm\": {\"name\": \"wasmer/python\", \"license\": \"ISC\", \"version\": \"3.12.1\", \"repository\": \"https://github.com/wapm-packages/python\", \"description\": \"Python is an interpreted, high-level, general-purpose programming language\"}}, \"commands\": {\"python\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"python\", \"package\": null, \"main_args\": null}}}}, \"entrypoint\": \"python\"}",
1132                                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/packages/wasmer/python/python-3.12.0-build.2-ed98c999-fcda-4f80-96dc-7c0f8be8baa6.webc",
1133                                    "piritaSha256Hash": "7835401e3ca1977ba05b5e51541363783b8a7700da270dd851f10fe2e4f27f07"
1134                                }
1135                            },
1136                            {
1137                                "version": "3.12.0",
1138                                "isArchived": true,
1139                                "v2": {
1140                                    "webcManifest": "{\"atoms\": {\"python\": {\"kind\": \"https://webc.org/kind/wasm\", \"signature\": \"sha256:O36BXLHv3/80cABbAiF7gzuSHzzin1blTfJ42LDhT18=\"}}, \"package\": {\"wapm\": {\"name\": \"wasmer/python\", \"license\": \"ISC\", \"version\": \"3.12.0\", \"repository\": \"https://github.com/wapm-packages/python\", \"description\": \"Python is an interpreted, high-level, general-purpose programming language\"}}, \"commands\": {\"python\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"python\", \"package\": null, \"main_args\": null}}}}, \"entrypoint\": \"python\"}",
1141                                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/packages/wasmer/python/python-3.12.0-32065e5e-84fe-4483-a380-0aa750772a3a.webc",
1142                                    "piritaSha256Hash": "e5d6e9d16db988eb323e34e2c152ebfb32dc7043d6b7ddc00ad57d3beae24adb"
1143                                },
1144                                "v3": {
1145                                    "webcManifest": "{\"atoms\": {\"python\": {\"kind\": \"https://webc.org/kind/wasm\", \"signature\": \"sha256:O36BXLHv3/80cABbAiF7gzuSHzzin1blTfJ42LDhT18=\"}}, \"package\": {\"wapm\": {\"name\": \"wasmer/python\", \"license\": \"ISC\", \"version\": \"3.12.0\", \"repository\": \"https://github.com/wapm-packages/python\", \"description\": \"Python is an interpreted, high-level, general-purpose programming language\"}}, \"commands\": {\"python\": {\"runner\": \"https://webc.org/runner/wasi/command@unstable_\", \"annotations\": {\"wasi\": {\"atom\": \"python\", \"package\": null, \"main_args\": null}}}}, \"entrypoint\": \"python\"}",
1146                                    "piritaDownloadUrl": "https://storage.googleapis.com/wapm-registry-prod/packages/wasmer/python/python-3.12.0-32065e5e-84fe-4483-a380-0aa750772a3a.webc",
1147                                    "piritaSha256Hash": "e5d6e9d16db988eb323e34e2c152ebfb32dc7043d6b7ddc00ad57d3beae24adb"
1148                                }
1149                            },
1150                        ]
1151                    },
1152                    "info": {
1153                        "defaultFrontend": "https://wasmer.io/",
1154                    },
1155                }
1156            }
1157        };
1158        let response = HttpResponse {
1159            body: Some(serde_json::to_vec(&body).unwrap()),
1160            redirected: false,
1161            status: StatusCode::OK,
1162            headers: HeaderMap::new(),
1163        };
1164        let client = Arc::new(DummyClient::new(vec![response]));
1165        let registry_endpoint = BackendSource::WASMER_PROD_ENDPOINT.parse().unwrap();
1166        let request = PackageSource::from_str("wasmer/python").unwrap();
1167        let source = BackendSource::new(registry_endpoint, client.clone());
1168
1169        let summaries = source.query(&request).await.unwrap();
1170
1171        assert_eq!(summaries.len(), 1);
1172        assert_eq!(
1173            summaries[0].pkg.id.as_named().unwrap().version.to_string(),
1174            "3.12.1"
1175        );
1176    }
1177
1178    #[tokio::test]
1179    async fn query_the_backend_again_if_cached_queries_dont_match() {
1180        let cached_value = serde_json::from_value(serde_json::json! {
1181            {
1182                "data": {
1183                    "getPackage": {
1184                        "packageName": "python",
1185                        "namespace": "wasmer",
1186                        "versions": [
1187                            {
1188                                "version": "3.12.0",
1189                                "v2": {
1190                                    "webcManifest": "{\"package\": {\"wapm\": {\"name\": \"wasmer/python\", \"version\": \"3.12.0\", \"description\": \"Python\"}}}",
1191                                    "piritaDownloadUrl": "https://wasmer.io/wasmer/python@3.12.0",
1192                                    "piritaSha256Hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
1193                                },
1194                                "v3": {
1195                                    "webcManifest": "{\"package\": {\"wapm\": {\"name\": \"wasmer/python\", \"version\": \"3.12.0\", \"description\": \"Python\"}}}",
1196                                    "piritaDownloadUrl": "https://wasmer.io/wasmer/python@3.12.0",
1197                                    "piritaSha256Hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
1198                                }
1199                            },
1200                        ]
1201                    },
1202                    "info": {
1203                        "defaultFrontend": "https://wasmer.io/",
1204                    },
1205                }
1206            }
1207        }).unwrap();
1208        let body = serde_json::json! {
1209            {
1210                "data": {
1211                    "getPackage": {
1212                        "packageName": "python",
1213                        "namespace": "wasmer",
1214                        "versions": [
1215                            {
1216                                "version": "4.0.0",
1217                                "v2": {
1218                                    "webcManifest": "{\"package\": {\"wapm\": {\"name\": \"wasmer/python\", \"version\": \"4.0.0\", \"description\": \"Python\"}}}",
1219                                    "piritaDownloadUrl": "https://wasmer.io/wasmer/python@4.0.0",
1220                                    "piritaSha256Hash": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
1221                                },
1222                                "v3": {
1223                                    "webcManifest": "{\"package\": {\"wapm\": {\"name\": \"wasmer/python\", \"version\": \"4.0.0\", \"description\": \"Python\"}}}",
1224                                    "piritaDownloadUrl": "https://wasmer.io/wasmer/python@4.0.0",
1225                                    "piritaSha256Hash": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb"
1226                                }
1227                            },
1228                            {
1229                                "version": "3.12.0",
1230                                "v2": {
1231                                    "webcManifest": "{\"package\": {\"wapm\": {\"name\": \"wasmer/python\", \"version\": \"3.12.0\", \"description\": \"Python\"}}}",
1232                                    "piritaDownloadUrl": "https://wasmer.io/wasmer/python@3.12.0",
1233                                    "piritaSha256Hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
1234                                },
1235                                "v3": {
1236                                    "webcManifest": "{\"package\": {\"wapm\": {\"name\": \"wasmer/python\", \"version\": \"3.12.0\", \"description\": \"Python\"}}}",
1237                                    "piritaDownloadUrl": "https://wasmer.io/wasmer/python@3.12.0",
1238                                    "piritaSha256Hash": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
1239                                }
1240                            },
1241                        ]
1242                    },
1243                    "info": {
1244                        "defaultFrontend": "https://wasmer.io/",
1245                    },
1246                }
1247            }
1248        };
1249        let response = HttpResponse {
1250            body: Some(serde_json::to_vec(&body).unwrap()),
1251            redirected: false,
1252            status: StatusCode::OK,
1253            headers: HeaderMap::new(),
1254        };
1255        let client = Arc::new(DummyClient::new(vec![response]));
1256        let registry_endpoint = BackendSource::WASMER_PROD_ENDPOINT.parse().unwrap();
1257        let request = PackageSource::from_str("wasmer/python@4.0.0").unwrap();
1258        let temp = tempfile::tempdir().unwrap();
1259        let source = BackendSource::new(registry_endpoint, client.clone())
1260            .with_local_cache(temp.path(), Duration::from_secs(0));
1261        update_cached_query(
1262            source.cache.as_ref().unwrap(),
1263            "wasmer/python",
1264            &cached_value,
1265        )
1266        .await
1267        .unwrap();
1268
1269        let summaries = source.query(&request).await.unwrap();
1270
1271        assert_eq!(summaries.len(), 1);
1272        assert_eq!(
1273            summaries[0].pkg.id.as_named().unwrap().version.to_string(),
1274            "4.0.0"
1275        );
1276    }
1277}