Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/holger

server/lib/src/repository.rs

//! The `Repository` config node and its backend factory — the seam between RON
//! config and a live `RepositoryBackendTrait`.
//!
//! A `Repository` is parsed from RON as inert `ron_*` fields; the wiring engine
//! (`lib::wire_holger`) then calls [`Repository::backend_from_config`] in a
//! second pass to materialize `backend_repository`, dispatching on
//! `ron_repo_type` to the right per-ecosystem backend. The three rust storage
//! flavors are the ones to keep straight: `ron_writable_archive` = live
//! seal-able znippy registry (the /sparring + /cache primary), `ron_archive_path`
//! = read-only drift snapshot, `ron_data_dir` = loose-file dev backend; the
//! `*-remote` types instead build read-only crates.io/Nexus/Artifactory
//! pull-through upstreams. Most ecosystems go through the `znippy_backed!` macro
//! (archive required); npm/nuget/oci are hand-rolled so `ron_base_url` can be
//! threaded into client-facing URLs (packument tarballs, V3 service index).
//!
//! Gotcha: `wired_upstreams`/`wired_members` are raw pointers
//! filled in *after* the graph is built and pinned — they are `null`/dangling
//! until wiring completes and must never be followed before then.
use anyhow::Context;
#[cfg(feature = "fmt-docker")]
use repository_file_oci::OciRepoFile;
#[cfg(feature = "fmt-rust")]
use repository_file_rust::RustRepoFile as RustRepo;
#[cfg(feature = "fmt-build-thing")]
use repository_znippy_build_thing::BuildThingRepoZnippy;
#[cfg(feature = "fmt-composer")]
use repository_znippy_composer::ComposerRepoZnippy;
#[cfg(feature = "fmt-conda")]
use repository_znippy_conda::CondaRepoZnippy;
#[cfg(feature = "fmt-deb")]
use repository_znippy_deb::DebRepoZnippy;
#[cfg(feature = "fmt-docker")]
use repository_znippy_docker::DockerRepoZnippy;
#[cfg(feature = "fmt-gem")]
use repository_znippy_gem::GemRepoZnippy;
#[cfg(feature = "fmt-go")]
use repository_znippy_go::GoRepoZnippy;
#[cfg(feature = "fmt-helm")]
use repository_znippy_helm::HelmRepoZnippy;
#[cfg(feature = "fmt-maven")]
use repository_znippy_maven::MavenRepoZnippy;
#[cfg(feature = "fmt-npm")]
use repository_znippy_npm::NpmRepoZnippy;
#[cfg(feature = "fmt-nuget")]
use repository_znippy_nuget::NugetRepoZnippy;
#[cfg(feature = "fmt-znippy")]
use repository_znippy_package::ZnippyRepoZnippy;
#[cfg(feature = "fmt-python")]
use repository_znippy_python::PipRepoZnippy;
#[cfg(feature = "fmt-rpm")]
use repository_znippy_rpm::RpmRepoZnippy;
#[cfg(feature = "fmt-rust")]
use repository_znippy_rust::RustRepoZnippy;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use std::sync::Arc;
use traits::{ArtifactFormat, RepositoryBackendTrait, UpstreamAuth};
// The `rust-remote` pull-through upstream is a live `ureq` HTTP client, so it is
// gated behind `online` (like reqwest/ureq themselves). The zero-egress airgap
// tiers (`embedded`/`mini`) omit `online`, do not compile this module, and the
// `rust-remote` repo arm below fails closed instead of reaching for the network.
#[cfg(feature = "fmt-raw")]
use crate::raw::RawBackend;
#[cfg(feature = "online")]
use crate::upstream_rust::RustRegistryUpstream;

// ── Which formats THIS binary compiled (stinky 1.4, 2026-09-21) ───────────────
//
// `ron_repo_type` → the `fmt-*` feature that links its backend. One row per
// dispatch arm below; the storage-shape spellings (`rust` = file + znippy,
// `docker` + `oci` = znippy + file) collapse onto the ONE feature that names
// the format, exactly as `traits::FormatHome::also` pairs them. `group` is not
// here: a virtual group owns no backend and is served in every build. The
// remote kinds (`rust-remote`, `*-remote`) are not here either — they are
// gated by `online` / `federation`, which already refuse by name.

/// Every `(ron_repo_type, fmt feature)` pair the dispatch knows, compiled or not.
pub const FORMAT_FEATURES: &[(&str, &str)] = &[
    ("raw", "fmt-raw"),
    ("rust", "fmt-rust"),
    ("maven3", "fmt-maven"),
    ("pip", "fmt-python"),
    ("npm", "fmt-npm"),
    ("nuget", "fmt-nuget"),
    ("go", "fmt-go"),
    ("gem", "fmt-gem"),
    ("deb", "fmt-deb"),
    ("rpm", "fmt-rpm"),
    ("helm", "fmt-helm"),
    ("docker", "fmt-docker"),
    ("oci", "fmt-docker"),
    ("conda", "fmt-conda"),
    ("composer", "fmt-composer"),
    ("znippy", "fmt-znippy"),
    ("znippy-build-thing", "fmt-build-thing"),
];

/// The `fmt-*` feature that serves `repo_type`, or `None` for a type that is
/// not a format (a group, a remote kind, a typo).
pub fn format_feature(repo_type: &str) -> Option<&'static str> {
    FORMAT_FEATURES
        .iter()
        .find(|(t, _)| *t == repo_type)
        .map(|(_, f)| *f)
}

/// Whether the backend for `feature` is linked into THIS binary. `cfg!`, so
/// the answer is the build's, not the config's.
pub fn format_feature_compiled(feature: &str) -> bool {
    #[allow(clippy::match_like_matches_macro)]
    match feature {
        "fmt-raw" => cfg!(feature = "fmt-raw"),
        "fmt-rust" => cfg!(feature = "fmt-rust"),
        "fmt-maven" => cfg!(feature = "fmt-maven"),
        "fmt-python" => cfg!(feature = "fmt-python"),
        "fmt-npm" => cfg!(feature = "fmt-npm"),
        "fmt-nuget" => cfg!(feature = "fmt-nuget"),
        "fmt-go" => cfg!(feature = "fmt-go"),
        "fmt-gem" => cfg!(feature = "fmt-gem"),
        "fmt-deb" => cfg!(feature = "fmt-deb"),
        "fmt-rpm" => cfg!(feature = "fmt-rpm"),
        "fmt-helm" => cfg!(feature = "fmt-helm"),
        "fmt-docker" => cfg!(feature = "fmt-docker"),
        "fmt-conda" => cfg!(feature = "fmt-conda"),
        "fmt-composer" => cfg!(feature = "fmt-composer"),
        "fmt-znippy" => cfg!(feature = "fmt-znippy"),
        "fmt-build-thing" => cfg!(feature = "fmt-build-thing"),
        _ => false,
    }
}

/// Whether `repo_type` can be instantiated by this binary. A type that is not
/// a format at all (`group`, the remote kinds) answers `true` here: their own
/// arms decide, and those refusals already have names.
pub fn repo_type_compiled(repo_type: &str) -> bool {
    format_feature(repo_type).is_none_or(format_feature_compiled)
}

/// The `ron_repo_type` spellings this binary serves — what the by-name refusal
/// lists so an operator sees the whole accepted set, not one missing word.
pub fn compiled_repo_types() -> Vec<&'static str> {
    FORMAT_FEATURES
        .iter()
        .filter(|(_, f)| format_feature_compiled(f))
        .map(|(t, _)| *t)
        .collect()
}

/// `holger-mvp-1`: the repository names a format this build did not compile.
pub fn uncompiled_format(repo_name: &str, repo_type: &str, feature: &str) -> anyhow::Error {
    anyhow::anyhow!(crate::errcode::MVP_FORMAT_NOT_COMPILED.tag(format!(
        "repository '{repo_name}' is `ron_repo_type: \"{repo_type}\"` but this server was \
         built without the `{feature}` feature; the formats this binary serves are \
         {:?} — rebuild with `--features {feature}` (or `fmt-all`), or remove the repository",
        compiled_repo_types()
    )))
}

/// Build a remote-registry-federation upstream backend (Artifactory · Nexus ·
/// Azure Artifacts · AWS CodeArtifact) by hiring Skidbladnir's harbour clients.
/// Only compiled in the `federation` (`full`) tier.
#[cfg(feature = "federation")]
fn make_remote_upstream(
    kind: &str,
    name: String,
    base: String,
    repo: String,
    auth: UpstreamAuth,
    format: ArtifactFormat,
) -> anyhow::Result<Arc<dyn RepositoryBackendTrait>> {
    use crate::upstream::SkidbladnirUpstream;
    let up = match kind {
        "nexus-remote" => SkidbladnirUpstream::nexus(name, base, repo, auth, format),
        "artifactory-remote" => SkidbladnirUpstream::artifactory(name, base, repo, auth, format),
        "azure-remote" => SkidbladnirUpstream::azure(name, base, repo, auth, format),
        "aws-remote" => SkidbladnirUpstream::aws(name, base, auth, format),
        other => anyhow::bail!("unknown remote upstream kind: {other}"),
    };
    Ok(Arc::new(up))
}

/// `federation`-off fallback: the harbour clients are compiled OUT of the
/// `mini`/`embedded` tiers (they carry no Skidbladnir / registry-client
/// dependency), so a config that asks for a remote upstream fails closed with a
/// clear rebuild hint rather than silently doing nothing.
#[cfg(not(feature = "federation"))]
fn make_remote_upstream(
    kind: &str,
    _name: String,
    _base: String,
    _repo: String,
    _auth: UpstreamAuth,
    _format: ArtifactFormat,
) -> anyhow::Result<Arc<dyn RepositoryBackendTrait>> {
    anyhow::bail!(
        "repository type `{kind}` needs upstream-registry federation, which is compiled OUT of this holger build (the `mini`/`embedded` tiers link no registry clients). Rebuild the `full` tier, e.g. `cargo build -p holger-server-lib --no-default-features --features full`."
    )
}

#[derive(Serialize, Deserialize)]
pub struct Repository {
    // Parsed from RON
    pub ron_name: String,
    pub ron_repo_type: String,      // rust/java/python/raw
    pub ron_upstreams: Vec<String>, // empty means no upstreams
    pub ron_in: Option<RepositoryIO>,
    pub ron_out: Option<RepositoryIO>,

    /// Member repositories of a **virtual `group`** repo (`ron_repo_type: "group"`).
    /// The group owns no storage; it resolves each request across these members in
    /// declaration order (first hit wins) — the Nexus/Artifactory "group repo".
    /// Only meaningful for the `group` type; empty for every other type.
    #[serde(default)]
    pub ron_members: Vec<String>,

    /// Optional path to a znippy archive (.znippy file)
    #[serde(default)]
    pub ron_archive_path: Option<String>,

    /// Optional storage directory for file-backed (writable) repos.
    /// Defaults to /var/lib/holger/{ron_name} when omitted.
    #[serde(default)]
    pub ron_data_dir: Option<String>,

    /// Public base URL used in config.json `dl` for the disk-backed Rust repo.
    /// E.g. `https://holger.example.com`.  Defaults to `https://127.0.0.1:8443`
    /// when omitted.
    #[serde(default)]
    pub ron_base_url: Option<String>,

    /// Optional path to a WRITABLE znippy archive (rust repos). When set, the repo
    /// is a live `/sparring`-style registry: crates are appended into this single
    /// `.znippy` (created on first push, reboot-safe re-seal per write) and can
    /// later be `seal`ed into a static "drift" snapshot. Distinct from
    /// `ron_archive_path` (read-only drift) and `ron_data_dir` (loose-file repo).
    #[serde(default)]
    pub ron_writable_archive: Option<String>,

    /// Remote-cache upstream: the Nexus/Artifactory repository name to proxy
    /// (e.g. `maven-central`, `cargo-proxy`). Used by the `nexus-remote` /
    /// `artifactory-remote` backends together with `ron_base_url`.
    #[serde(default)]
    pub ron_upstream_repo: Option<String>,

    /// Remote-cache upstream ecosystem/layout (`maven2`, `cargo`, `npm`, …).
    /// Selects the per-format coordinate→URL mapping. **Omitted** defaults to
    /// `rust` (cargo); a value present but not in
    /// [`ArtifactFormat::from_format_str`]'s alias table is a **boot error**, not
    /// a silent fallback — a typo like `mvn` would otherwise build a Maven remote
    /// that speaks crates.io paths and 404s everything.
    #[serde(default)]
    pub ron_upstream_format: Option<String>,

    /// **Which HANDLER reads this repository's container.**
    ///
    /// `ron_repo_type` / `ron_upstream_format` name the container; this names
    /// the reader. znippy is a container — compressed files in a directory,
    /// sometimes with an extra index — so a Skidbladnir bundle, a Rust-toolchain
    /// bundle and a plain sealed archive are all `ArtifactFormat::Znippy` and all
    /// correct. The format string cannot tell them apart; this can.
    ///
    /// ```text
    /// ron_upstream_format: "znippy",        the container
    /// ron_handler:         "skidbladnir",   the reader
    /// ```
    ///
    /// **Omitted or empty ⇒ nothing changes** — `ron_repo_type` dispatches
    /// exactly as before, and a `znippy` repo is served by the generic backend.
    /// A name that is present but not in the [handler
    /// catalog](crate::handlers::builtin_catalog) is a **boot error**, never a
    /// fallback to the generic reader — same rule as `ron_upstream_format` above
    /// and for the same reason: a typo that fell back would serve real bytes
    /// through the wrong reader with no warning at boot.
    ///
    /// Accepted values are the linked handlers plus every `.wasm` in
    /// [`Holger::ron_plugin_dir`](crate::Holger) — so a new handler is this
    /// string plus a module in that directory, with no holger rebuild.
    #[serde(default)]
    pub ron_handler: Option<String>,

    /// Remote-cache upstream auth (RON enum: `None`, `Basic`, `Bearer`,
    /// `ApiKey`, `Mtls`). Defaults to `None`.
    #[serde(default)]
    pub ron_upstream_auth: Option<UpstreamAuth>,

    /// **This repository demands an authenticated identity for READS**, even
    /// when the server-wide read gate (`auth.require_auth_for_reads`) is off.
    ///
    /// Omitted or `false` ⇒ nothing changes, byte for byte: the repository is
    /// served under whatever the global posture is. That is every config written
    /// before this key existed.
    ///
    /// # Why a per-REPOSITORY key, and not another global one
    ///
    /// holger already had both halves of "authenticated reads" except the scope.
    /// `auth.require_auth_for_reads` is one bool for the whole server: on, and
    /// EVERY read anywhere needs a credential; off, and none does. `repo_roles`
    /// is per repository, but it only refines what an ALREADY-AUTHENTICATED
    /// identity may do — with the gate off it never runs, and with the gate on
    /// an anonymous caller is already refused everywhere.
    ///
    /// Neither expresses the shape an on-prem mirror against a real cloud
    /// actually needs, and it is the shape the owner named: `crates` demands a
    /// client certificate while the rest of the server stays open. This key is
    /// that sentence, written once, on the repository it is about.
    ///
    /// # What it does and does not do
    ///
    /// It requires an IDENTITY, not a particular mechanism. Whatever the
    /// server's `auth.methods` can authenticate satisfies it — an mTLS client
    /// certificate, an OIDC bearer, a static token. The mechanism is the
    /// operator's choice; what this key says is that *anonymous is not one of
    /// them, here*. Authorization on top of that is `repo_roles`' job, unchanged:
    /// name a subject `Denied` and it is refused even with a valid certificate.
    ///
    /// For mTLS specifically it pairs with
    /// [`TlsSettings::client_auth_optional`](crate::exposed::tls::TlsSettings::client_auth_optional) —
    /// without that, the transport refuses a certless client before any
    /// repository name exists, which is the across-the-board posture and makes
    /// this key unreachable.
    ///
    /// # It refuses rather than degrades
    ///
    /// A repository that sets this on a server with no `auth.methods` at all
    /// **refuses the boot, naming the repository**. Nothing could ever
    /// authenticate, so the repository would be permanently unreachable while
    /// the config read as merely "protected" — and the alternative reading, that
    /// it should quietly serve open, is the one thing a credential declaration
    /// must never mean.
    #[serde(default)]
    pub ron_require_auth: bool,

    /// **The least role that may read or write this repository** — a floor under
    /// the policy's own answer, never a raise above it. `Some(Admin)` makes a
    /// repository an administrator's alone: every read and every write needs an
    /// identity whose role HERE is at least this, and an identity below it is
    /// refused (403) even when its role would read or write anywhere else.
    /// Implies [`ron_require_auth`](Self::ron_require_auth) (a floor is checked on
    /// an identity, so the read gate applies).
    ///
    /// Why it exists (holger-ui's thin-layer move, 2026-10-04): the estate's
    /// ledger backup (`monetize-backup`) was gated with `ron_require_auth`, which
    /// meant "the console's bearer only" while that bearer was the only identity
    /// there was. Once the people hold holger passkeys, every one of them
    /// authenticates — and a reader would read the backup, a writer write it.
    /// The floor says what was meant: the repository is an admin's.
    #[serde(default)]
    pub ron_require_role: Option<crate::auth::Role>,

    /// **Who reads this repository** (HCON-5): `visibility: Public` is read with no sign-in
    /// even when the box's `access.reads` is `Gated`; no word is `Private` (read under the box's
    /// posture).
    #[serde(default, alias = "visibility")]
    pub ron_visibility: crate::posture::Visibility,

    /// Optional path to a DER-encoded trusted root certificate. When set on a
    /// `znippy` repo, the backing archive's provenance (per-artifact + per-archive
    /// detached CMS signatures) is verified against this root BEFORE the repo is
    /// served; verification failure — including an unsigned archive or a signer
    /// that does not chain to this root — fails the wiring closed. Requires the
    /// server to be built with the `sign` feature. Omitted ⇒ served unverified
    /// (the default, byte-for-byte unchanged behaviour).
    #[serde(default)]
    pub ron_trusted_roots: Option<String>,

    /// Negative-cache TTL in **seconds** for a proxy repo (one with
    /// `ron_upstreams`). When set and non-zero, a coordinate that misses the
    /// primary AND every upstream is remembered as a miss for this long, so a
    /// repeated fetch of a not-yet-published artifact short-circuits without
    /// re-walking the (potentially slow / network) upstream chain — the
    /// Nexus/Artifactory "negative cache". Omitted or `0` ⇒ disabled (every miss
    /// re-queries the upstreams, byte-for-byte the prior behaviour). Only
    /// meaningful on a repo with upstreams; ignored otherwise.
    #[serde(default)]
    pub ron_negative_cache_secs: Option<u64>,

    // Wired in second pass
    #[serde(skip_serializing, skip_deserializing, default)]
    pub backend_repository: Option<Arc<dyn RepositoryBackendTrait>>,
    /// The digest this upstream's ORIGIN publishes for a coordinate, when this
    /// repository is an upstream that can be asked.
    ///
    /// `backend_repository` above is type-erased to `RepositoryBackendTrait`, which
    /// has no way to ask "what does the registry itself say this artifact hashes
    /// to?". That question is what separates caching bytes from LAUNDERING them:
    /// without it a pull-through repo writes whatever the origin handed over and
    /// then re-attests it under holger's own name. So the concrete backend's
    /// attestation seam is kept here, alongside the erased one, and
    /// `Holger::effective_backend` hands it to the proxy.
    ///
    /// `None` ⇒ this upstream cannot be asked (it has no such seam, or it is not an
    /// upstream at all), and the proxy caches exactly as it always did — silently
    /// turning every existing pull-through repo into a pass-through would be a
    /// product change nobody asked for.
    #[serde(skip_serializing, skip_deserializing, default)]
    pub upstream_attestation: Option<Arc<dyn traits::Upstream>>,

    #[serde(skip_serializing, skip_deserializing, default)]
    pub wired_upstreams: Vec<*const Repository>, // or &Repository pinned after build

    /// Wired members of a `group` repo (resolved from `ron_members` in the second
    /// pass). Same pinned-pointer discipline as `wired_upstreams`: `null`/dangling
    /// until wiring completes, never followed before then.
    #[serde(skip_serializing, skip_deserializing, default)]
    pub wired_members: Vec<*const Repository>,
}
/// **Where one repository's bytes are on disk** — the one answer the flow picture,
/// the `disk_bytes` meter and the customer read (owner 2026-09-27: "the customer
/// can see where their bytes are").
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StoragePlace {
    /// `archive (writable)` | `archive` | `files`.
    pub kind: &'static str,
    pub path: PathBuf,
}

impl Repository {
    /// **Where this repository keeps its bytes.** A group owns none (it is a view
    /// over its members). A writable archive, a read-only archive, and a loose-file
    /// data directory — defaulting to `/var/lib/holger/<name>` exactly as the
    /// backends do when none is written.
    pub fn storage_places(&self) -> Vec<StoragePlace> {
        if self.ron_repo_type == "group" {
            return Vec::new();
        }
        if let Some(a) = &self.ron_writable_archive {
            return vec![StoragePlace {
                kind: "archive (writable)",
                path: PathBuf::from(a),
            }];
        }
        if let Some(a) = &self.ron_archive_path {
            return vec![StoragePlace {
                kind: "archive",
                path: PathBuf::from(a),
            }];
        }
        let dir = self
            .ron_data_dir
            .clone()
            .unwrap_or_else(|| format!("/var/lib/holger/{}", self.ron_name));
        vec![StoragePlace {
            kind: "files",
            path: PathBuf::from(dir),
        }]
    }

    /// Materialize this repository's backend using only the handlers **linked**
    /// into this holger. The plugin directory is not read, because a single
    /// `Repository` does not own it — [`Holger::instantiate_backends`] builds
    /// one catalog for the whole config and passes it to
    /// [`Repository::backend_from_config_with`].
    pub fn backend_from_config(&mut self) -> anyhow::Result<()> {
        self.backend_from_config_with(&crate::handlers::builtin_catalog()?)
    }

    pub fn backend_from_config_with(
        &mut self,
        handlers: &holger_plugin_host::HandlerCatalog,
    ) -> anyhow::Result<()> {
        // A named handler IS the backend, whatever container it reads: the
        // reader is one level below the format, so it takes precedence over the
        // `ron_repo_type` dispatch below rather than living inside one arm of it.
        //
        // Absent, empty or whitespace ⇒ fall straight through to the match, so a
        // config that says nothing about handlers behaves byte-for-byte as it did
        // before this field existed.
        let named_handler = self
            .ron_handler
            .as_deref()
            .map(str::trim)
            .filter(|h| !h.is_empty())
            .map(str::to_string);
        if let Some(handler) = named_handler {
            return self.wire_handler(&handler, handlers);
        }

        // Build a znippy-backed repo that requires an archive path. Unused when a
        // build names only `fmt-raw` + `fmt-rust` (every user is a `#[cfg]`ed arm).
        #[allow(unused_macros)]
        macro_rules! znippy_backed {
            ($ty:ty, $label:expr) => {{
                if let Some(archive_path) = &self.ron_archive_path {
                    let repo = <$ty>::with_archive(
                        self.ron_name.clone(),
                        PathBuf::from(archive_path),
                    ).with_context(|| format!(
                        "repo '{}' ({}): failed to open znippy archive '{}' — corrupt, \
                         truncated, or written by an incompatible znippy version",
                        self.ron_name, $label, archive_path
                    ))?;
                    self.backend_repository = Some(Arc::new(repo));
                } else {
                    anyhow::bail!(concat!($label, " repository requires ron_archive_path to a znippy archive"));
                }
                Ok(())
            }};
        }

        match self.ron_repo_type.as_str() {
            #[cfg(feature = "fmt-rust")]
            "rust" => {
                if let Some(writable) = &self.ron_writable_archive {
                    // Writable znippy-archive registry (the /sparring + /cache
                    // primary): seal-able into a drift snapshot, reboot-safe.
                    let mut repo = RustRepoZnippy::with_writable_archive(
                        self.ron_name.clone(),
                        PathBuf::from(writable),
                    )
                    .with_auth_required(self.ron_require_auth);
                    if let Some(url) = &self.ron_base_url {
                        repo = repo.with_base_url(url.clone());
                    }
                    self.backend_repository = Some(Arc::new(repo));
                } else if let Some(archive_path) = &self.ron_archive_path {
                    let base_url = self
                        .ron_base_url
                        .clone()
                        .unwrap_or_else(|| "https://127.0.0.1:8443".to_string());
                    let repo = RustRepoZnippy::with_archive_and_url(
                        self.ron_name.clone(),
                        PathBuf::from(archive_path),
                        base_url,
                    )
                    .with_context(|| {
                        format!(
                            "repo '{}' (rust): failed to open znippy archive '{}' — corrupt, \
                         truncated, or written by an incompatible znippy version",
                            self.ron_name, archive_path
                        )
                    })?
                    .with_auth_required(self.ron_require_auth);
                    self.backend_repository = Some(Arc::new(repo));
                } else {
                    // Writable, disk-backed dev backend. Requires a storage dir.
                    let dir = self.ron_data_dir.clone().ok_or_else(|| {
                        anyhow::anyhow!(
                            "Rust file-backed repository requires ron_data_dir (storage directory)"
                        )
                    })?;
                    let base_url = self
                        .ron_base_url
                        .clone()
                        .unwrap_or_else(|| "https://127.0.0.1:8443".to_string());
                    let repo = RustRepo::new(self.ron_name.clone(), PathBuf::from(dir), base_url);
                    self.backend_repository = Some(Arc::new(repo));
                }
                Ok(())
            }
            // Read-only REMOTE rust registry (the /cache upstream). `ron_base_url` names
            // the sparse index base; none is refused (`holger-offline-3`), never crates.io.
            // Pair with a writable "rust" + `ron_upstreams` to get a caching mirror.
            #[cfg(feature = "online")]
            "rust-remote" => {
                let repo = match &self.ron_base_url {
                    Some(idx) => RustRegistryUpstream::new(
                        self.ron_name.clone(),
                        idx.clone(),
                        format!("{}/crates", idx.trim_end_matches('/')),
                    ),
                    // No upstream fallback (Holger v2.0 B2): an air-gapped default never
                    // reaches for a public registry it was not told to.
                    None => anyhow::bail!(holger_errcode::OFFLINE_NO_UPSTREAM.tag(format!(
                        "repository '{}' is a 'rust-remote' upstream and names no `ron_base_url`, so it has no upstream: holger falls back to none. Name one, e.g. ron_base_url: Some(\"https://index.crates.io\")",
                        self.ron_name
                    ))),
                };
                // ONE Arc, registered twice: erased for serving, concrete for the
                // digest question. The crates.io sparse index publishes a `cksum`
                // per version, so this upstream CAN be asked — and once it is
                // registered here the proxy refuses bytes that contradict it.
                let repo = Arc::new(repo);
                self.upstream_attestation = Some(repo.clone());
                self.backend_repository = Some(repo);
                Ok(())
            }
            // Zero-egress airgap tiers (`online` off) link no outbound HTTP client,
            // so the pull-through `rust-remote` upstream is not compiled. Fail
            // CLOSED with an actionable message rather than silently degrading —
            // an airgap operator must use a local 'rust' repo, not a network mirror.
            #[cfg(not(feature = "online"))]
            "rust-remote" => Err(anyhow::anyhow!(
                "repo '{}' is 'rust-remote' (a pull-through network upstream) but this \
                 binary was built WITHOUT the `online` feature (zero-egress airgap tier) \
                 — no HTTP client is linked. Rebuild with `--features online`, or serve \
                 crates from a local 'rust' repository backed by a znippy archive.",
                self.ron_name
            )),
            // Read-only REMOTE upstream-registry-federation harbours (holger's
            // `full`-tier feature). holger owns the orchestration (which repos
            // proxy which upstreams, pull-through caching via `ProxyBackend`,
            // compression via znippy); the registry-client MECHANISM lives in
            // Skidbladnir's `federation` "ship" (see `crate::upstream`). Shared
            // config fields: `ron_base_url` = harbour base, `ron_upstream_repo` =
            // repo/feed, `ron_upstream_format` = ecosystem (default cargo),
            // `ron_upstream_auth` = creds (default None). Pair with a writable
            // primary + `ron_upstreams` for transparent pull-through caching.
            //  - `nexus-remote`       → `ron_base_url` = Nexus base, `ron_upstream_repo` = hosted/proxy repo.
            //  - `artifactory-remote` → `ron_base_url` = Artifactory base, `ron_upstream_repo` = repo; auth may be Bearer or JFrog API-key.
            //  - `azure-remote`       → `ron_base_url` = Azure DevOps org, `ron_upstream_repo` = "{project}/{feed}" (or "{feed}" org-scoped); auth = PAT-as-Basic or AAD Bearer.
            //  - `aws-remote`         → `ron_base_url` = CodeArtifact repository endpoint; auth = Bearer authorization token.
            "nexus-remote" | "artifactory-remote" | "azure-remote" | "aws-remote" => {
                let base = self.ron_base_url.clone().ok_or_else(|| {
                    anyhow::anyhow!(
                        "{}: requires ron_base_url (the harbour base URL / org / repository endpoint)",
                        self.ron_repo_type
                    )
                })?;
                // aws-remote addresses artifacts off the single repository endpoint
                // and needs no separate repo name; the others require one.
                let repo = if self.ron_repo_type == "aws-remote" {
                    self.ron_upstream_repo.clone().unwrap_or_default()
                } else {
                    self.ron_upstream_repo.clone().ok_or_else(|| {
                        anyhow::anyhow!(
                            "{}: requires ron_upstream_repo (the hosted/proxy repo or project/feed)",
                            self.ron_repo_type
                        )
                    })?
                };
                // Omitted ⇒ default to Rust. PRESENT-but-unparseable ⇒ hard fail,
                // like the missing ron_base_url / ron_upstream_repo above.
                // Collapsing the two cases with `.and_then(…).unwrap_or(Rust)`
                // meant a typo (`"mvn"`, `"jar"`, `"gradle"` — none of them
                // aliases) silently built a Maven remote with a crates.io-shaped
                // upstream layout, so every coordinate 404'd with no warning at
                // boot and the operator saw an upstream that simply never had
                // anything.
                let format = match self.ron_upstream_format.as_deref() {
                    None => ArtifactFormat::Rust,
                    Some(s) => ArtifactFormat::from_format_str(s).ok_or_else(|| {
                        anyhow::anyhow!(
                            "{}: unrecognised ron_upstream_format '{}'. Accepted: \
                             maven/maven2/maven3/java, pip/pypi/python, rust/cargo, \
                             go/golang, nuget/dotnet, npm/node, gem/ruby/rubygems, \
                             deb/debian/apt, rpm/yum/dnf, helm/chart, docker/oci, \
                             conda/anaconda, composer/php, znippy, raw",
                            self.ron_repo_type,
                            s
                        )
                    })?,
                };
                let auth = self.ron_upstream_auth.clone().unwrap_or_default();
                let backend = make_remote_upstream(
                    &self.ron_repo_type,
                    self.ron_name.clone(),
                    base,
                    repo,
                    auth,
                    format,
                )?;
                self.backend_repository = Some(backend);
                Ok(())
            }
            #[cfg(feature = "fmt-maven")]
            "maven3" => znippy_backed!(MavenRepoZnippy, "Maven3"),
            #[cfg(feature = "fmt-python")]
            "pip" => znippy_backed!(PipRepoZnippy, "Pip"),
            // npm threads `ron_base_url` into the packument tarball links (R2): no
            // hardcoded host, so pulls work behind any public host / reverse proxy.
            // Unset ⇒ host-relative URLs.
            #[cfg(feature = "fmt-npm")]
            "npm" => {
                if let Some(archive_path) = &self.ron_archive_path {
                    let repo = NpmRepoZnippy::with_archive_and_url(
                        self.ron_name.clone(),
                        PathBuf::from(archive_path),
                        self.ron_base_url.clone().unwrap_or_default(),
                    )?;
                    self.backend_repository = Some(Arc::new(repo));
                    Ok(())
                } else {
                    // No archive: the intake chain's npm door (`promotion.intake.npm`, lane P5),
                    // which holds nothing of its own; `Holger::validate_npm_door` refused any
                    // other archive-less npm repository before this ran.
                    self.backend_repository =
                        Some(Arc::new(NpmRepoZnippy::new(self.ron_name.clone())));
                    Ok(())
                }
            }
            // NuGet threads `ron_base_url` into the V3 service-index
            // PackageBaseAddress so remote clients are pointed at the public host,
            // not a hardcoded loopback. Unset ⇒ the dev-loopback default.
            #[cfg(feature = "fmt-nuget")]
            "nuget" => {
                if let Some(archive_path) = &self.ron_archive_path {
                    let mut repo = NugetRepoZnippy::with_archive(
                        self.ron_name.clone(),
                        PathBuf::from(archive_path),
                    )?;
                    if let Some(url) = &self.ron_base_url {
                        repo = repo.with_base_url(url.clone());
                    }
                    self.backend_repository = Some(Arc::new(repo));
                    Ok(())
                } else {
                    anyhow::bail!("Nuget repository requires ron_archive_path to a znippy archive");
                }
            }
            #[cfg(feature = "fmt-go")]
            "go" => znippy_backed!(GoRepoZnippy, "Go"),
            #[cfg(feature = "fmt-gem")]
            "gem" => znippy_backed!(GemRepoZnippy, "Gem"),
            #[cfg(feature = "fmt-deb")]
            "deb" => znippy_backed!(DebRepoZnippy, "Deb"),
            #[cfg(feature = "fmt-rpm")]
            "rpm" => znippy_backed!(RpmRepoZnippy, "Rpm"),
            #[cfg(feature = "fmt-helm")]
            "helm" => znippy_backed!(HelmRepoZnippy, "Helm"),
            #[cfg(feature = "fmt-docker")]
            "docker" => znippy_backed!(DockerRepoZnippy, "Docker"),
            // Writable OCI registry (Distribution Spec v2). Push target for the
            // modern Helm flow (`helm push oci://…`) and any OCI artifact; needs
            // no archive — it owns a disk-backed, content-addressed store.
            #[cfg(feature = "fmt-docker")]
            "oci" => {
                let dir = self
                    .ron_data_dir
                    .clone()
                    .unwrap_or_else(|| format!("/var/lib/holger/{}", self.ron_name));
                let repo = OciRepoFile::new(self.ron_name.clone(), PathBuf::from(dir))?;
                self.backend_repository = Some(Arc::new(repo));
                Ok(())
            }
            // Generic / **raw** writable bucket for arbitrary blobs, addressed by
            // path (Nexus/Artifactory "raw hosted repo"). Owns a disk-backed,
            // path-addressed loose-file store — no archive, no index synthesis; a
            // `ron_data_dir` is its only requirement (defaults under /var/lib).
            #[cfg(feature = "fmt-raw")]
            "raw" => {
                let dir = self
                    .ron_data_dir
                    .clone()
                    .unwrap_or_else(|| format!("/var/lib/holger/{}", self.ron_name));
                let repo = RawBackend::new(self.ron_name.clone(), PathBuf::from(dir));
                self.backend_repository = Some(Arc::new(repo));
                Ok(())
            }
            #[cfg(feature = "fmt-conda")]
            "conda" => znippy_backed!(CondaRepoZnippy, "Conda"),
            // Hand-rolled rather than `znippy_backed!` for the same reason
            // nuget is: the synthesized p2 metadata embeds an ABSOLUTE
            // `dist.url`, and Composer follows it verbatim — leak the loopback
            // default and every remote `composer install` downloads from the
            // wrong host. `ron_base_url` must reach the backend.
            #[cfg(feature = "fmt-composer")]
            "composer" => {
                if let Some(archive_path) = &self.ron_archive_path {
                    let mut repo = ComposerRepoZnippy::with_archive(
                        self.ron_name.clone(),
                        PathBuf::from(archive_path),
                    )
                    .with_context(|| {
                        format!(
                            "repo '{}' (Composer): failed to open znippy archive '{}' — corrupt, \
                         truncated, or written by an incompatible znippy version",
                            self.ron_name, archive_path
                        )
                    })?;
                    if let Some(url) = &self.ron_base_url {
                        repo = repo.with_base_url(url.clone());
                    }
                    self.backend_repository = Some(Arc::new(repo));
                    Ok(())
                } else {
                    anyhow::bail!(
                        "Composer repository requires ron_archive_path to a znippy archive"
                    );
                }
            }
            // A virtual `group` repo owns no storage of its own — it aggregates
            // its `ron_members`' backends. The concrete `GroupBackend` is built in
            // the wiring pass (`wire_holger`) once every member backend exists, so
            // there is nothing to materialize here.
            "group" => {
                if self.ron_members.is_empty() {
                    anyhow::bail!(
                        "group repository '{}' requires ron_members (member repo names)",
                        self.ron_name
                    );
                }
                Ok(())
            }
            // The forge's BUILD HISTORY: attestations, not packages. Read-only —
            // the package is derived from the artifacts and regenerated if lost,
            // so a writable one would be a second writer of provenance.
            #[cfg(feature = "fmt-build-thing")]
            "znippy-build-thing" => znippy_backed!(BuildThingRepoZnippy, "BuildThing"),
            #[cfg(feature = "fmt-znippy")]
            "znippy" => {
                if self.ron_trusted_roots.is_some() {
                    // Provenance-enforced path (opt-in via `ron_trusted_roots`).
                    self.wire_verified_znippy()
                } else {
                    znippy_backed!(ZnippyRepoZnippy, "Znippy")
                }
            }
            // A format this build did NOT compile lands here because its arm above
            // is `#[cfg]`ed away. Refuse BY NAME (stinky 1.4): the repository, the
            // format and the feature that would serve it — never a disabled repo
            // that looks like an empty one.
            other => match format_feature(other) {
                Some(feature) => Err(uncompiled_format(&self.ron_name, other, feature)),
                None => anyhow::bail!("Unsupported repository type: {}", other),
            },
        }
    }

    /// Mount the package handler named by `ron_handler` over this repository's
    /// data directory.
    ///
    /// The repository name and the storage are the operator's — a handler never
    /// picks where it is served from or where it writes — so both are supplied
    /// here rather than read out of the handler's manifest.
    ///
    /// An unrecognised name is an error carrying the accepted set (see
    /// [`holger_plugin_host::HandlerError`]); nothing here falls back to the
    /// generic backend.
    fn wire_handler(
        &mut self,
        handler: &str,
        handlers: &holger_plugin_host::HandlerCatalog,
    ) -> anyhow::Result<()> {
        let dir = self
            .ron_data_dir
            .clone()
            .unwrap_or_else(|| format!("/var/lib/holger/{}", self.ron_name));
        let backend = handlers
            .mount_dir(handler, &self.ron_name, PathBuf::from(&dir))
            .with_context(|| {
                format!(
                    "repo '{}': ron_handler could not be honoured",
                    self.ron_name
                )
            })?;
        self.backend_repository = Some(backend.into_backend());
        Ok(())
    }

    /// Wire a `znippy` repo with provenance verification enabled (built with the
    /// `sign` feature). Loads the DER trusted root from `ron_trusted_roots` and
    /// verifies the backing archive before it is served; any read/parse/verify
    /// failure fails the wiring closed.
    #[cfg(all(feature = "sign", feature = "fmt-znippy"))]
    fn wire_verified_znippy(&mut self) -> anyhow::Result<()> {
        let roots_path = self
            .ron_trusted_roots
            .as_ref()
            .expect("caller checks ron_trusted_roots is Some");
        let archive_path = self.ron_archive_path.as_ref().ok_or_else(|| {
            anyhow::anyhow!("Znippy repository requires ron_archive_path to a znippy archive")
        })?;
        let repo = ZnippyRepoZnippy::with_archive_verified_der_roots(
            self.ron_name.clone(),
            PathBuf::from(archive_path),
            &[PathBuf::from(roots_path)],
        )
        .with_context(|| {
            format!(
                "repo '{}' (Znippy): provenance verification of archive '{}' against trusted \
                 root '{}' failed — archive is unsigned, tampered, or not chained to the root",
                self.ron_name, archive_path, roots_path
            )
        })?;
        self.backend_repository = Some(Arc::new(repo));
        Ok(())
    }

    /// Fail-closed stub for builds WITHOUT the `sign` feature: a configured
    /// `ron_trusted_roots` cannot be honoured, so refuse to wire rather than
    /// silently serving the archive unverified.
    #[cfg(all(feature = "fmt-znippy", not(feature = "sign")))]
    fn wire_verified_znippy(&mut self) -> anyhow::Result<()> {
        anyhow::bail!(
            "repo '{}': ron_trusted_roots is configured but this server was built without both \
             `sign` and `fmt-znippy`; provenance cannot be enforced — rebuild with \
             `--features sign,fmt-znippy` or remove ron_trusted_roots",
            self.ron_name
        )
    }
}

/// A repository's side of the wiring graph: the storage and exposed endpoints
/// it names. `wire_holger` checks both names resolve; nothing is pointed at.
#[derive(Serialize, Deserialize)]
pub struct RepositoryIO {
    pub ron_storage_endpoint: String,
    pub ron_exposed_endpoint: String,
}

/// A minimal, fully-specified `Repository` for tests.
///
/// `pub(crate)` and test-only so `lib.rs`'s wiring tests build the same shape
/// rather than keeping a second copy that drifts every time a field is added —
/// which is exactly how `upstream_attestation` would have been missed.
#[cfg(test)]
pub(crate) fn test_repo(name: &str) -> Repository {
    Repository {
        ron_name: name.to_string(),
        ron_require_auth: false,
        ron_require_role: None,
        ron_visibility: Default::default(),
        ron_repo_type: "rust".to_string(),
        ron_upstreams: vec![],
        ron_in: None,
        ron_out: None,
        ron_members: vec![],
        ron_archive_path: None,
        ron_data_dir: None,
        ron_base_url: None,
        ron_writable_archive: None,
        ron_upstream_repo: None,
        ron_upstream_format: None,
        ron_handler: None,
        ron_upstream_auth: None,
        ron_trusted_roots: None,
        ron_negative_cache_secs: None,
        backend_repository: None,
        upstream_attestation: None,
        wired_upstreams: vec![],
        wired_members: vec![],
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use traits::ArtifactId;

    use super::test_repo as rust_repo;

    /// `ron_writable_archive` (the `/sparring` + `/cache`-primary config) builds a
    /// WRITABLE znippy-backed rust backend that accepts a crate and serves it back.
    #[test]
    fn writable_archive_config_builds_writable_backend() {
        let dir = tempfile::tempdir().unwrap();
        let archive = dir.path().join("sparring.znippy");

        let mut repo = rust_repo("sparring");
        repo.ron_writable_archive = Some(archive.to_string_lossy().into_owned());
        repo.backend_from_config().unwrap();

        let backend = repo.backend_repository.as_ref().expect("backend built");
        assert!(
            backend.is_writable(),
            "ron_writable_archive must yield a writable backend"
        );

        let id = ArtifactId {
            namespace: None,
            name: "serde".into(),
            version: "1.0.0".into(),
        };
        backend.put(&id, b"crate-bytes").unwrap();
        assert_eq!(
            backend.fetch(&id).unwrap().as_deref(),
            Some(b"crate-bytes".as_slice()),
            "writable backend must serve the crate it was given"
        );
    }

    /// `ron_archive_path` stays the read-only "drift" backend: seal a sparring
    /// archive (via the writable path), reopen it read-only, confirm not writable
    /// but still serves the sealed crate.
    #[test]
    fn archive_path_config_is_read_only_drift() {
        let dir = tempfile::tempdir().unwrap();
        let archive = dir.path().join("drift.znippy");
        let id = ArtifactId {
            namespace: None,
            name: "x".into(),
            version: "1.0.0".into(),
        };

        let mut seed = rust_repo("seed");
        seed.ron_writable_archive = Some(archive.to_string_lossy().into_owned());
        seed.backend_from_config().unwrap();
        seed.backend_repository
            .unwrap()
            .put(&id, b"sealed-bytes")
            .unwrap();

        let mut ro = rust_repo("drift");
        ro.ron_archive_path = Some(archive.to_string_lossy().into_owned());
        ro.backend_from_config().unwrap();
        let backend = ro.backend_repository.unwrap();
        assert!(
            !backend.is_writable(),
            "ron_archive_path must yield a read-only backend"
        );
        assert_eq!(
            backend.fetch(&id).unwrap().as_deref(),
            Some(b"sealed-bytes".as_slice()),
            "read-only drift backend must serve the sealed crate"
        );
    }

    /// `rust-remote` builds a read-only remote-registry upstream (the /cache
    /// upstream) over the index it names, no archive/dir required.
    #[test]
    fn rust_remote_config_builds_read_only_upstream() {
        let mut repo = rust_repo("crates-io");
        repo.ron_repo_type = "rust-remote".to_string();
        repo.ron_base_url = Some("https://index.crates.io".into());
        repo.backend_from_config().unwrap();
        let backend = repo.backend_repository.as_ref().expect("backend built");
        assert!(!backend.is_writable(), "remote upstream must be read-only");
        assert_eq!(backend.name(), "crates-io");
    }

    /// `nexus-remote` builds a read-only Nexus upstream from `ron_base_url` +
    /// `ron_upstream_repo`; missing required config fails cleanly. Federation
    /// (`full`) tier only — the mechanism is Skidbladnir's ship.
    #[cfg(feature = "federation")]
    #[test]
    fn nexus_remote_config_builds_read_only_upstream() {
        let mut repo = rust_repo("nexus-cache");
        repo.ron_repo_type = "nexus-remote".to_string();
        // missing ron_base_url + ron_upstream_repo -> error
        assert!(
            repo.backend_from_config().is_err(),
            "nexus-remote needs base_url + repo"
        );

        repo.ron_base_url = Some("https://nexus.corp".to_string());
        repo.ron_upstream_repo = Some("crates-proxy".to_string());
        repo.ron_upstream_auth = Some(UpstreamAuth::Basic {
            username: "u".into(),
            password: "p".into(),
        });
        repo.backend_from_config().unwrap();
        let backend = repo.backend_repository.as_ref().expect("backend built");
        assert!(!backend.is_writable(), "nexus upstream must be read-only");
        assert_eq!(backend.name(), "nexus-cache");
    }

    /// `artifactory-remote` builds a read-only Artifactory upstream (`full` tier).
    #[cfg(feature = "federation")]
    #[test]
    fn artifactory_remote_config_builds_read_only_upstream() {
        let mut repo = rust_repo("jfrog-cache");
        repo.ron_repo_type = "artifactory-remote".to_string();
        repo.ron_base_url = Some("https://jfrog.corp".to_string());
        repo.ron_upstream_repo = Some("maven-remote".to_string());
        repo.ron_upstream_format = Some("maven2".to_string());
        repo.backend_from_config().unwrap();
        let backend = repo.backend_repository.as_ref().expect("backend built");
        assert!(
            !backend.is_writable(),
            "artifactory upstream must be read-only"
        );
        assert_eq!(backend.format(), ArtifactFormat::Maven3);
    }

    /// A typo'd `ron_upstream_format` must FAIL THE BOOT, not silently fall back
    /// to the Rust layout.
    ///
    /// RED-when-broken: `"mvn"`, `"jar"` and `"gradle"` are not aliases (the
    /// Maven spellings are maven/maven2/maven3/java), so the old
    /// `.and_then(from_format_str).unwrap_or(Rust)` built a Maven remote that
    /// asked crates.io-shaped upstream paths and 404'd every coordinate, with no
    /// warning anywhere.
    #[cfg(feature = "federation")]
    #[test]
    fn unrecognised_upstream_format_fails_boot_instead_of_defaulting_to_rust() {
        for typo in ["mvn", "jar", "gradle"] {
            let mut repo = rust_repo("jfrog-cache");
            repo.ron_repo_type = "artifactory-remote".to_string();
            repo.ron_base_url = Some("https://jfrog.corp".to_string());
            repo.ron_upstream_repo = Some("maven-remote".to_string());
            repo.ron_upstream_format = Some(typo.to_string());
            let err = repo
                .backend_from_config()
                .expect_err("an unrecognised format must not silently become Rust");
            let msg = err.to_string();
            assert!(
                msg.contains(typo),
                "error should name the bad value, got: {msg}"
            );
            assert!(
                msg.contains("maven3"),
                "error should list accepted spellings, got: {msg}"
            );
        }

        // Omission still defaults to Rust — the fix must not turn the documented
        // default into an error.
        let mut omitted = rust_repo("jfrog-cache");
        omitted.ron_repo_type = "artifactory-remote".to_string();
        omitted.ron_base_url = Some("https://jfrog.corp".to_string());
        omitted.ron_upstream_repo = Some("crates-remote".to_string());
        omitted.ron_upstream_format = None;
        omitted
            .backend_from_config()
            .expect("an omitted format defaults to Rust");
        assert_eq!(
            omitted.backend_repository.as_ref().unwrap().format(),
            ArtifactFormat::Rust
        );
    }

    /// The two new cloud harbours (`azure-remote`, `aws-remote`) build read-only
    /// upstreams from RON config (`full` tier).
    #[cfg(feature = "federation")]
    #[test]
    fn azure_and_aws_remote_configs_build_read_only_upstreams() {
        let mut az = rust_repo("azure-cache");
        az.ron_repo_type = "azure-remote".to_string();
        az.ron_base_url = Some("contoso".to_string()); // Azure DevOps org
        az.ron_upstream_repo = Some("proj/myfeed".to_string()); // project/feed
        az.ron_upstream_format = Some("npm".to_string());
        az.backend_from_config().unwrap();
        assert!(!az.backend_repository.as_ref().unwrap().is_writable());

        let mut aws = rust_repo("aws-cache");
        aws.ron_repo_type = "aws-remote".to_string();
        aws.ron_base_url = Some(
            "https://d-111122223333.d.codeartifact.us-west-2.amazonaws.com/npm/my-repo".to_string(),
        );
        aws.ron_upstream_format = Some("npm".to_string());
        aws.ron_upstream_auth = Some(UpstreamAuth::Bearer {
            token: "AUTHTOK".into(),
        });
        aws.backend_from_config().unwrap(); // aws-remote needs no ron_upstream_repo
        assert!(!aws.backend_repository.as_ref().unwrap().is_writable());
    }

    /// `mini`/`embedded` tier: the federation mechanism is compiled OUT, so a
    /// config asking for a remote harbour FAILS CLOSED (never a silent no-op).
    #[cfg(not(feature = "federation"))]
    #[test]
    fn remote_upstream_configs_fail_closed_without_federation() {
        for kind in [
            "nexus-remote",
            "artifactory-remote",
            "azure-remote",
            "aws-remote",
        ] {
            let mut repo = rust_repo("cache");
            repo.ron_repo_type = kind.to_string();
            repo.ron_base_url = Some("https://example.corp".to_string());
            repo.ron_upstream_repo = Some("some-repo".to_string());
            let err = repo.backend_from_config().unwrap_err().to_string();
            assert!(
                err.contains("federation") && err.contains("full"),
                "{kind}: mini must fail closed with a rebuild-`full` hint, got: {err}"
            );
        }
    }

    // ── ron_handler: the config names the READER, not just the container ──────

    /// A `znippy` repo holding a Skidbladnir bundle as a loose file in its data
    /// directory. The generic znippy backend cannot serve this at all (it wants a
    /// single `.znippy` ARCHIVE via `ron_archive_path`), which is what makes the
    /// pair below a real red/green rather than two greens.
    fn bundle_repo(name: &str, dir: &std::path::Path) -> Repository {
        let mut r = rust_repo(name);
        r.ron_repo_type = "znippy".to_string();
        r.ron_data_dir = Some(dir.to_string_lossy().into_owned());
        r
    }

    /// Bytes chosen to survive nothing but a byte-exact path: a NUL, a comma, a
    /// colon and a 0xff, which is what znippy's JSON-ish row format could not
    /// carry.
    const BUNDLE_BYTES: &[u8] = b"\x00sealed,bundle:bytes\xff";

    fn seed_bundle(dir: &std::path::Path) {
        std::fs::create_dir_all(dir).unwrap();
        std::fs::write(dir.join("tillsynia-20260706.znippy"), BUNDLE_BYTES).unwrap();
    }

    /// **The claim.** One repository, one directory of bytes, two configs that
    /// differ by a single string — and only the one naming the reader can serve.
    #[test]
    fn a_named_handler_serves_bytes_the_generic_znippy_backend_cannot() {
        let tmp = tempfile::tempdir().unwrap();
        let dir = tmp.path().join("bundles");
        seed_bundle(&dir);

        // RED: the same repo with NO ron_handler. The generic dispatch is
        // untouched, so it still demands ron_archive_path and refuses to wire.
        let mut generic = bundle_repo("bundles", &dir);
        let err = generic
            .backend_from_config()
            .expect_err("the generic znippy backend wired without an archive");
        assert!(
            err.to_string().contains("ron_archive_path"),
            "the pre-existing generic-znippy error changed: {err}"
        );
        assert!(
            generic.backend_repository.is_none(),
            "a failed wiring left a backend behind"
        );

        // GREEN: name the reader.
        let mut handled = bundle_repo("bundles", &dir);
        handled.ron_handler = Some("skidbladnir".to_string());
        handled
            .backend_from_config()
            .expect("ron_handler = skidbladnir must wire");
        let backend = handled.backend_repository.as_ref().expect("backend built");

        // Applied output, not a state round-trip: the bytes off the wire.
        assert_eq!(backend.name(), "bundles");
        assert_eq!(
            backend.format(),
            ArtifactFormat::Znippy,
            "the container is still znippy"
        );
        let id = ArtifactId {
            namespace: None,
            name: "tillsynia".into(),
            version: "20260706".into(),
        };
        assert_eq!(
            backend.fetch(&id).expect("fetch"),
            Some(traits::Bytes::from_static(BUNDLE_BYTES)),
            "the handler resolved but served the wrong bytes"
        );

        // And a route NO generic znippy backend has: the shared member
        // classifier on the wire. This is the assertion that cannot pass if the
        // generic reader had been selected.
        let (status, _, body) = backend
            .handle_http2_request(
                "GET",
                "/bundles/tillsynia/20260706/classify/secrets/secrets.age",
                b"",
            )
            .expect("classify");
        assert_eq!(status, 200);
        assert_eq!(
            String::from_utf8_lossy(&body),
            "kind\tsecret\ncomponent\tsecrets\nencrypted\ttrue"
        );
    }

    /// The two handlers holger links are both reachable BY NAME, and they are
    /// genuinely different readers — not one handler answering to two strings.
    #[test]
    fn each_linked_handler_answers_to_its_own_name() {
        let tmp = tempfile::tempdir().unwrap();
        let dir = tmp.path().join("bundles");
        std::fs::create_dir_all(&dir).unwrap();
        std::fs::write(dir.join("rust-dev-rhel8-1.97.1.znippy"), BUNDLE_BYTES).unwrap();

        let mut r = bundle_repo("toolchains", &dir);
        r.ron_handler = Some("rust-toolchain".to_string());
        r.backend_from_config()
            .expect("ron_handler = rust-toolchain must wire");
        let backend = r.backend_repository.as_ref().expect("backend built");

        // The rust-toolchain classifier, which Skidbladnir's would not produce.
        let (status, _, body) = backend
            .handle_http2_request(
                "GET",
                "/toolchains/rust-dev-rhel8/1.97.1/classify/vendor/serde-1.0.219/Cargo.toml",
                b"",
            )
            .expect("classify");
        assert_eq!(status, 200);
        let line = String::from_utf8_lossy(&body);
        assert!(
            line.starts_with("kind\tvendor-crate"),
            "rust-toolchain served a foreign classification: {line}"
        );

        // Same repo, same bytes, the OTHER handler: Skidbladnir does not claim a
        // vendored crate path, so the two names really are two readers.
        let mut s = bundle_repo("toolchains", &dir);
        s.ron_handler = Some("skidbladnir".to_string());
        s.backend_from_config().unwrap();
        let (status, _, _) = s
            .backend_repository
            .as_ref()
            .unwrap()
            .handle_http2_request(
                "GET",
                "/toolchains/rust-dev-rhel8/1.97.1/classify/vendor/serde-1.0.219/Cargo.toml",
                b"",
            )
            .expect("classify");
        assert_eq!(status, 404, "both handler names produced the same answer");
    }

    /// A typo must FAIL THE BOOT and say what IS accepted — the same rule
    /// `ron_upstream_format` above already follows, and for the same reason: a
    /// fallback to the generic reader would serve real bytes through the wrong
    /// reader with no warning anywhere.
    #[test]
    fn an_unknown_ron_handler_fails_boot_and_lists_what_is_accepted() {
        let tmp = tempfile::tempdir().unwrap();
        let dir = tmp.path().join("bundles");
        seed_bundle(&dir);

        for typo in ["skidbladnr", "skidbladnir-bundle", "SKIDBLADNIR", "wasm"] {
            let mut r = bundle_repo("bundles", &dir);
            r.ron_handler = Some(typo.to_string());
            let e = r
                .backend_from_config()
                .expect_err(&format!("'{typo}' resolved to a handler"));
            let err = e.to_string();
            // anyhow's Display shows only the outermost context; the accepted
            // list is in the source, so render the whole chain the way a boot log
            // does (`{:#}`).
            let chain = format!("{e:#}");
            assert!(
                err.contains("bundles"),
                "the error does not name the repo: {err}"
            );
            assert!(
                chain.contains(typo),
                "the error does not name the bad value: {chain}"
            );
            assert!(
                chain.contains("skidbladnir") && chain.contains("rust-toolchain"),
                "the error does not list what IS accepted: {chain}"
            );
            assert!(
                r.backend_repository.is_none(),
                "{typo}: a rejected handler still wired"
            );
        }
    }

    /// Absent, empty and whitespace are all "say nothing", and saying nothing
    /// must leave the `ron_repo_type` dispatch exactly as it was.
    #[test]
    fn an_absent_or_empty_ron_handler_changes_nothing() {
        let dir = tempfile::tempdir().unwrap();

        for (label, value) in [
            ("absent", None),
            ("empty", Some(String::new())),
            ("whitespace", Some("   ".to_string())),
        ] {
            let mut repo = rust_repo(&format!("sparring-{label}"));
            repo.ron_writable_archive = Some(
                dir.path()
                    .join(format!("{label}.znippy"))
                    .to_string_lossy()
                    .into_owned(),
            );
            repo.ron_handler = value;
            repo.backend_from_config()
                .unwrap_or_else(|e| panic!("{label}: the untouched rust dispatch broke: {e:#}"));
            let backend = repo.backend_repository.as_ref().expect("backend built");
            assert!(
                backend.is_writable(),
                "{label}: not the writable rust backend"
            );
            assert_eq!(backend.format(), ArtifactFormat::Rust, "{label}");

            // Applied output: the same put/fetch round trip the rust backend
            // served before this field existed.
            let id = ArtifactId {
                namespace: None,
                name: "demo".into(),
                version: "0.1.0".into(),
            };
            backend
                .put(&id, b"crate-bytes")
                .unwrap_or_else(|e| panic!("{label}: put: {e:#}"));
            assert_eq!(
                backend.fetch(&id).unwrap(),
                Some(traits::Bytes::from_static(b"crate-bytes")),
                "{label}"
            );
        }
    }

    // ── the pull-through upstream must be ASKABLE, not just servable ─────────
    //
    // `backend_repository` is erased to `RepositoryBackendTrait`, which cannot be
    // asked what digest the ORIGIN publishes for a coordinate. Without that
    // question a pull-through repo writes whatever the upstream handed over and
    // re-attests it in holger's own sparse index — laundering, not caching. The
    // concrete backend answers it; this is the wiring that keeps hold of it, and
    // it is invisible at runtime when it breaks (both gates just go quiet).

    #[cfg(feature = "online")]
    #[test]
    fn a_rust_remote_upstream_registers_the_digest_its_origin_publishes() {
        let mut repo = rust_repo("crates-io");
        repo.ron_repo_type = "rust-remote".to_string();
        repo.ron_base_url = Some("https://index.crates.io".into());
        repo.backend_from_config()
            .expect("rust-remote instantiates");
        assert!(
            repo.upstream_attestation.is_some(),
            "a crates.io upstream publishes a `cksum` per version — it CAN be asked, \
             so the proxy's digest gates must be able to ask it"
        );
    }

    /// A plain local repository is not an origin and has nothing to publish, so it
    /// registers nothing — and the proxy then caches from it exactly as before.
    #[test]
    fn a_local_repository_registers_no_attestation() {
        let dir = tempfile::tempdir().unwrap();
        let mut repo = rust_repo("rust-local");
        repo.ron_data_dir = Some(dir.path().to_string_lossy().to_string());
        repo.backend_from_config()
            .expect("a local rust repo instantiates");
        assert!(repo.upstream_attestation.is_none());
    }
}