BatleHub — Roadmap
Planned features and improvements, grouped by theme. Within each group the order reflects rough implementation priority.
For discussion or to propose a feature, open an issue on the project repository.
New registry types
Current adapters: npm, Cargo, GitHub, Forgejo/Gitea, GitLab, OpenVSX, VS Code Marketplace, Go modules, Maven, RubyGems, Terraform, Composer, PyPI, Conda, NuGet, Deb (APT), RPM (YUM/DNF), Pacman, JetBrains (IDE archives), JetBrains Marketplace (plugins), Generic (path-addressed file mirror).
- [x] PyPI — Python package index; Simple API proxy with URL rewriting; wheel / sdist downloads; private publishing via
twineinlocal/hybridmode - [x] Maven / Gradle — Maven Central-compatible metadata XML + JAR / POM downloads; private publishing via
mvn deployinlocal/hybridmode - [x] RubyGems — gem downloads and version listing (proxy + local/hybrid with publish/yank/unyank)
- [x] NuGet — .NET package protocol; NuGet v3 service index + flat container proxy;
.nupkgand.nuspecdownloads; private publishing viadotnet nuget pushinlocal/hybridmode - [x] Deb / RPM — Debian APT (
type = "deb") and Red Hat YUM/DNF (type = "rpm") repository proxying and private hosting inlocal/hybridmode:.deb/.rpmpublish,Packages/Releaseandrepodata/regeneration, Ed25519 OpenPGP-signed metadata (InRelease/Release.gpg,repomd.xml.asc). Signing is hand-rolled (Ed25519 only) to avoid the bannedrsacrate - [x] JetBrains IDE archives —
type = "jetbrains": proxy-only path-based cache for JetBrains IDE installer archives (default upstreamdownload.jetbrains.com); reuses the genericPathProxyRegistryClient. No private hosting. IDE archives are large (~1-1.7 GB), solimits.max_artifact_size_bytesmust be raised - [x] JetBrains Marketplace —
type = "jetbrains-marketplace": full marketplace emulation for the plugin ecosystem (default upstreamplugins.jetbrains.com) inproxy/local/hybridmode. IDE-facing surface (search, compatible updates,meta.jsonblobs,plugin/download,pluginManager) complete enough foridea.plugins.hostfull replacement, plusupdatePlugins.xmlfor the additiveidea.plugin.hostsflow; marketplace-compatible multipart publish (POST /api/updates/upload) soplugin-repository-rest-client/GradlepublishPluginwork against local/hybrid registries. Metadata, artifacts, and forwarded query blobs are all cached with stale fallback, so previously-seen plugins keep resolving if plugins.jetbrains.com is unreachable - [x] VS Code extension gallery —
type = "openvsx"/"vscode-marketplace": the client-facing gallery protocol (POST …/vscode/gallery/extensionquery, assets,resourceUrlTemplate,item) so an editor can be pointed at BatleHub withextensionsGalleryinproduct.jsonand search, install and update through it, plus the OpenVSX REST API (/api/{namespace}/{extension},/api/-/search,…/file/{name}) forovsx. Both protocols render from one entry list, so blocked versions are hidden from both. Assets — manifest, README, changelog, licence, icon — are extracted from the cached VSIX, so one artifact answers them all. The editor sends no credentials to its gallery, so a gallery registry needsanonymousread or an authenticating ingress - [x] Terraform registry — provider and module proxy protocol; private module + provider publishing in
local/hybridmode - [x] GitLab releases and packages —
type = "gitlab": paginated release list/tag, link assets, source archives + raw files via the/-/URL scheme, nested groups,PRIVATE-TOKEN/Bearer auth; package registry passthrough (/api/v4/…, ideal for generic packages). Ecosystem package registries (npm/Maven/PyPI/…) are proxied via the matching typed adapter pointed at the GitLab package endpoint - [x] Forgejo releases and packages —
type = "forgejo": paginated Gitea/Forgejo/api/v1release list/tag, assets, source archives, raw files (reuses the GitHub URL scheme); package registry passthrough (/api/packages/…). Ecosystem registries via the matching typed adapter - [x] Composer — PHP Composer registry (Packagist v2 protocol —
packages.json, p2 metadata, dist downloads); private package publishing via ZIP upload inlocal/hybridmode - [x] Anaconda / Conda — Python data science package registry;
repodata.jsonproxy and channel merging;.tar.bz2and.condapackage parsing; private channel publishing inlocal/hybridmode - [x] Arch Linux / Pacman —
type = "pacman": proxy upstream Arch mirrors and private hosting inlocal/hybridmode:.pkg.tar.{zst,xz,gz}publish (metadata read from.PKGINFO), per-arch<repo>.db/<repo>.filesdatabase regeneration, Ed25519 OpenPGP-signed database (<repo>.db.sig) and packages (.sig+ embedded%PGPSIG%) soSigLevel = Requiredworks. Signing reuses the hand-rolled Ed25519 signer (thersacrate is banned) - [x] Generic file mirror —
type = "generic": proxy-only path-addressed cache for any HTTP file tree that has no package protocol at all (toolchain tarballs, vendor CDNs, release buckets). ReusesPathProxyRegistryClientlikejetbrains, but with a mandatory explicitupstreamsentry and a mandatorypath_allowglob allowlist so a registry pointed at a shared host (e.g.storage.googleapis.com) cannot become an open relay for every other path on it. Covers the toolchain sources that no typed adapter reaches — rustup (static.rust-lang.org), the Go toolchain (dl.google.com/go, distinct from thegoproxymodule adapter), and single-binary vendor CDNs (get.helm.sh,dl.min.io,binaries.sonarsource.com). Archives can be large, solimits.max_artifact_size_bytesmay need raising. Node (nodejs.org/dist) was on this list and is the reasonnodedistbelow exists:genericmirrors it correctly and can enforce nothing on it, because a path-addressed registry has no version to block.genericstays the right answer for a tree you want cached without policy
Not yet started, in rough priority order:
- [ ] Node distributions (
nvm, fnm,n, mise) —type = "nodedist": thenodejs.org/disttree as a typed adapter rather than agenericmirror, so a Node release can be blocked rather than merely cached.index.tabandindex.jsonare filtered listings; the tarballs andSHASUMS256.txtare artifacts under{name}/{version}/{file}. The point is the identity, not the bytes:genericalready caches this tree, and because it addresses everything as one synthetic package (repo/_) there is no version to block, nothing in explore and no per-version statistics — a gap the working cache hides.index.tabis also the enforcement chokepoint, becausenvm_remote_versionresolves every install through it, including a fully specified one.SHASUMS256.txtis passed through byte-exact: nvm verifies against it and a sibling.asc/.sigsigns it (RFCdocs/rfc/0010-toolchain-managers.md) - [ ] JVM toolchains (SDKMAN) —
type = "sdkman": the JDK, Gradle, Maven-the-distribution and the Kotlin compiler, none of which any BatleHub instance has ever seen. Proxies the candidates API and the download broker as one registry (broker_urlbeside cargo'sindex_url), filtersversions/all,candidates/defaultand the renderedsdk listtable, and refuses a blocked version atcandidates/validateso SDKMAN prints its own "is not a valid … version" rather than failing mid-download. The broker answers302to third-party CDNs, so the redirect chain is followed server-side through the SSRF guard — otherwise the 200 MB JDK still leaves the site and the proxy has mediated a policy decision and none of the bytes. Hook scripts are relayed byte-exact; they are bash the client sources and runs (RFCdocs/rfc/0010-toolchain-managers.md) - [ ] Generic file mirror,
local/hybridmode — publish arbitrary files under{name}/{version}/{filename}, the equivalent of GitLab's "generic packages": internal build artifacts, installers, blobs from CI. Inherits quotas, ownership, Ed25519 artifact signing, SBOM, yank and dedup from the shared local-registry machinery - [ ] Helm charts —
type = "helm": a real chart repository (index.yaml+.tgz) needs URL rewriting in the generated index, so it warrants its own adapter rather than agenericinstance;localmode would regenerateindex.yamlfrom the DB the waycondaregeneratesrepodata.json. Note that the helm binary (get.helm.sh) is already covered bygeneric— this entry is about charts. OCI-based charts stay out of scope (see below)
Not planned: Docker / OCI artifacts. Harbor solves this better than we could, unless concrete demand arises.
Cache policy
- [x] Honour
Cache-Controlheaders from upstream responses (no-cache,max-age,no-store) to decide whether and how long to cache - [x] Eviction policies: TTL-based expiry, "not accessed for N days", garbage-collect all versions except the latest N, storage size cap with LRU eviction
- [x] Cache index coherence: compare what is actually in the storage backend against what the registry metadata expects, and recover from corruption or manual deletions
- [x] Content-addressable deduplication: identical artifact bytes are stored once regardless of how many logical keys (registries, package names) reference them — ref-counted via
artifact_dedup_index/artifact_dedup_refs, backwards-compatible with pre-dedup artifacts - [ ] Storage-backend migration — move stored artifacts from one configured backend to another (filesystem ↔ S3) when
[storage]changes. Artifacts already record which backend holds them, but there is no migrate, move or rebalance operation, so changing the backend strands everything already written: it stays reachable only while the old backend remains configured. Needs a resumable walk, partial-failure tolerance and dedup ref-count awareness (RFC 0004-bis §13.3) - [x] Proactive cache warming: pre-fetch known versions of configured packages on startup and on demand via the admin API (
POST /api/v1/admin/registries/{registry}/warm); configurable per registry withwarm_packages,warm_latest_n, andwarm_concurrency
Metrics & observability
- [x] Prometheus metrics endpoint (
/metrics): request counts, cache hit/miss rates, latency percentiles, error rates per registry - [x] Health check endpoint (
/healthz) that verifies connectivity to the database and all configured storage backends - [x] Stats dashboard on the admin home screen: hits/misses, bandwidth saved, per-registry and aggregate
Artifact integrity & security
- [x] Verify checksums for downloaded artifacts when the upstream provides them — per-registry
[registries.integrity]: on the proxy fetch path the buffered bytes are hashed and compared against the metadata checksum (Cargo SHA-256, npm SRI/shasum, PyPI SHA-256). Supports SRI (sha512-…) and bare hex (algorithm inferred from length) - [x] Block serving an artifact if its integrity check fails, or optionally if the upstream provides no integrity metadata at all — a mismatch fails the download with
502and is never cached (not bypassable);require_metadata = trueadditionally blocks downloads with no advertised checksum (withbypass_roles) - [ ] Sigstore / npm provenance verification for npm packages
- [ ]
cargo verify-project-style verification for Cargo crates - [x] Detect and optionally require signed releases for GitHub, OpenVSX, and VS Code Marketplace —
RequireSignedReleaseRulereads a best-effortis_signedsignal populated by the GitHub/Forgejo and OpenVSX/VS Code Marketplace adapters;deny_missing_signature+bypass_rolescontrol enforcement - [x] Allowlist of trusted publishers —
trusted_publisherrule; supported for GitHub/GitLab/Forgejo (owner/group), npm (scope or publishing user), OpenVSX/VS Code Marketplace (publisher segment). Cargo crate owners not yet supported — crates.io ownership isn't in the sparse index and would need a separate API call - [x] Allowlist of approved versions; blocklist of specific versions with known issues —
version_gaterule (allow/blockwith exact or semver-range matching) - [x] Vulnerability scanning via the OSV API to block or warn about packages with known CVEs — periodic SBOM re-scan plus a per-registry
cve_gaterule (min_severity,block/warn-only,bypass_roles) - [x] Licence policy rule —
license_gate: allow/deny SPDX expressions per registry,allow_unknownfor the case where the licence is not yet known,blockvs warn-only andbypass_roleslikecve_gate. Distinct from RFC 0002's licence flag kind, which needs an external source to assert one; this reads what the package declares in its own manifest. Licence extraction landed with it:ArchiveSbomExtractornow returns the declared licence alongside the dependencies from the same parse, in five ecosystems (cargo, npm, maven, pypi, nuget), stored onartifact_sboms.licenseand surfaced on both the admin and explore package-detail pages. Two limits, both warned about in config rather than left to be discovered: the licence is read during SBOM generation, so the rule is inert unless[registries.sbom].enabled = true(license-gate.sbom-disabled); and it is inside the archive, so the first request for an uncached package cannot be gated (allow_unknown). Alicense_gateon one of the sixteen registry types with no parser raiseslicense-gate.no-extractor, orlicense-gate.denies-everythingwhenblock+allow_unknown = falsewould refuse every download (RFC 0004-bis §13.1, §14.1) - [ ] YARA rule evaluation for custom malware or policy patterns
- [ ] Antivirus scanning for binary artifacts (VSIX, Go module zips) via a configurable external REST API
- [x] Warn when an upstream registry is returning high error rates or slow responses and cached data may be stale — in-process EMA of upstream error rate / latency per registry (
ProxyMetrics); abatlehub_upstream_health_degraded{registry}gauge plus atracing::warn!on the healthy→degraded transition; also surfaced inGET /api/v1/admin/stats(upstream_degraded,upstream_error_rate,upstream_latency_ms) - [x] Warn when a registry does not provide integrity metadata for its artifacts — the proxy logs a warning and increments
batlehub_integrity_checks_total{outcome="missing"}when an artifact is fetched with no advertised checksum (and blocks instead whenrequire_metadata = true)
Authentication providers
- [x] Static tokens — plain-text and Argon2id-hashed Bearer tokens in
config.toml;batlehub hash-tokenCLI - [x] OIDC — JWT validation via OIDC discovery + JWKS; browser SSO (Authorization Code flow); role and group mapping from claims; namespaced group prefixes for multi-provider setups
- [x] Kubernetes service accounts — TokenReview API validation; role and group mapping; in-cluster defaults
- [x] GitHub / Forgejo Actions OIDC (
type = "actions-oidc") — validate short-lived JWTs issued to workflow jobs; map claims (repository,ref,environment,actor, …) to groups and roles via configurable rules; supports static group names and dynamic group templates (e.g."{name}/{repository}/{ref_name}"→"forgejo-action/batleforc-batlehub/main"); glob and regex pattern matching; AND / OR condition logic per rule - [ ] Groups on a personal access token — a PAT resolves to its creator's subject and a subset of their groups, rather than to no groups at all.
UserToken(crates/core/src/ports/auth/user_token_repo.rs) carries no groups andUserTokenAuthProvider::to_identity(crates/adapters/src/auth/user_token.rs) returnsgroups: vec![]for every token, so agroup:subject in RFC 0015's grant hierarchy can never match a PAT and all token-driven automation reads as an authenticated user with no groups — seeingpublicandinternaland nothing its owner was granted through a team. RFC 0015 §4.3 states the rule as settled and shippedpat_is_within_ownerto enforce the subset invariant, so the check exists and currently compares against an empty set. Needs the column and migration, the groups oncreate_tokencapped to the creator's own, the provider returning them, and the CLI flag. This is RFC 0011-bis's phase 1, the one gap of the three that RFC 0015 absorbed by requirement and did not build (docs/rfc/0011-bis-namespace-scoped-visibility.md); 0011-bis's first open question — PAT maximum TTL, since the TTL bounds how long a stale group snapshot grants read — is decided with it
Rate limiting & DoS protection
- [x] Per-user and per-registry rate limits on API requests and artifact downloads, with configurable thresholds and time windows (in-memory token bucket; state resets on restart)
- [x] Configurable enforcement policies: hard block vs. soft warn when a limit is reached
- [x] Explicit rate-limit warnings in API responses (
Retry-After,X-RateLimit-*headers) - [x] Per-group rate limits (shared token-bucket pools per OIDC/Kubernetes group; enforcement override per group)
- [x] IP-based blocking for abusive clients, with configurable block duration and thresholds
- [ ] Integration with external IP reputation services to automatically block known malicious IPs
Quota management
- [x] Per-user, per-group, and per-registry quotas on storage usage and number of published packages
- [x] Enforcement policies: block publish requests that exceed the quota, or allow with an explicit warning
- [x] Quota warnings in API responses and admin UI when a limit is being approached
- [x] Admin API for resetting quotas for specific users, groups, or registries
Hot reloading & dynamic config
- [x] Watch the config file for changes and prompt an admin for confirmation before applying — file watcher (using
notifycrate) loads a pending reload; admin confirms viaPOST /api/v1/admin/config/pending/applyor discards it - [x] Validate the new config before applying it (schema check + connectivity probes) to avoid breaking a running server — schema validation runs immediately; connectivity probes (
HEADto each upstream with a 5s timeout) run before the pending reload is stored - [x] Partial reloads: update RBAC rules or add/remove a registry without restarting the process — registries, policies, versioning, signing, and beta-channel maps are all behind
Arc<RwLock<HotConfig>>; in-flight requests finish with the old data before the swap - [x] API endpoint for triggering a config reload (
POST /api/v1/admin/config/reload) for automation when file-watching is unavailable — alsoGET /api/v1/admin/config/pending,POST /api/v1/admin/config/pending/apply,DELETE /api/v1/admin/config/pending - [x] Audit trail for all config changes (who triggered, what changed, when) — stored in
config_changestable; retrievable viaGET /api/v1/admin/config/changes - [x] Global admin banner — broadcast a message (info / warning / error) to all website visitors; automatically set during a reload and cleared on completion; backed by in-memory, Redis, or PostgreSQL depending on the cache backend;
PUT/DELETE /api/v1/admin/banner+/admin/config-reloadUI page - [x]
BATLEHUB_DISABLE_HOT_RELOAD=1— disable the file watcher and all reload endpoints (e.g. when config is a read-only Kubernetes ConfigMap) - [x] Config warnings — non-fatal problems (
AppConfig::warnings()) logged at startup and on every reload, served fromGET /api/v1/admin/config/warnings, returned inline by/config/validateand/config/from-content, and rendered on the Config Reload admin page. Each carries a stablecodeand thepathof the offending config location./config/from-contentreports the candidate's warnings even when the submitted bytes match the last load attempt, rather than those of the config still in force - [x]
pending_createdon the reload responses —/config/from-contentanswers200with an empty diff both when it stages a pending and when the submitted content is identical to the last load attempt and there is nothing to stage. The flag distinguishes them, and the Config Reload page shows a distinct notice instead of a success that sends the admin to an Apply button answering404 No pending reload - [ ] Clear the upstream-detail absence cache on reload —
UpstreamDetailCoordinatorremembers which coordinates an upstream answered404for, andclear_absentexists to forget them. Its doc says "for tests, and for a config reload that changes what 'absent' would mean"; audited 2026-09-01, no reload path has ever called it. So a reload that adds an upstream, fixes its base URL or repairs its credentials leaves every coordinate that failed under the old configuration still remembered as missing, and the operator's fix appears not to work until the process restarts. One call on the reload path, and a test that a reload forgets an absence - [ ] Dynamic blocking rules fetched from an external trusted source (e.g. a signed Git repository); verify signatures before applying
- [ ] Dynamic allowlists of trusted publishers or approved versions, fetched from an external source and merged into RBAC / block rules automatically
Ingress & routing
- [x] Host-based (subdomain) registry routing — bind a registry to one or more hostnames whose root serves it, in addition to
/proxy/{name}/….[subdomain_routing]derives<name>.<base_domain>for every registry;registries[].hostsadds vanity hosts. One outermost middleware rewrites the URI, so none of the ~249 route definitions change; the host table is hot-reloadable like every other registry-scoped map (RFCdocs/rfc/0001-subdomain-routing.md) - [x] Every self-referencing URL reflects the ingress the client used — one
registry_public_basehelper replaced 8 ad-hoc base-URL builders, ~25format!("{base}/proxy/{registry}/…")sites and thebase_urlcontract of 5LocalRegistryServicemethods - [x]
registries[].path_routing = false— make a registry reachable only by host, so isolated content does not also answer on the shared main host (404, not 403) - [x] Proxy trust (
[server].trusted_proxies) — one CIDR list governingForwarded/X-Forwarded-Host/X-Forwarded-Proto/X-Forwarded-Foralike, resolved once per request into aPeerTrustverdict every downstream middleware reads. Closes the previously unconditional trust of the forwarded host and scheme; deprecates[ip_blocking].trusted_proxies, whose unparseable entries are dropped with a config warning rather than refused at startup (that key predates the validator, so rejecting them would fail the boot of a config that never changed) - [x] The policy is hot-reloadable and swapped before the host table it guards, so a reload that turns host routing on can never run under the startup verdict — the two are not atomic together, and this order makes the gap fail closed
- [x] The rate limiter and the IP-block middleware bucket and ban on the same client IP — both read the shared verdict, so behind a trusted proxy neither keys on the proxy address
- [x] Registry names are matched on the path actix routes on, not the raw URI — percent-encoding a character of the name no longer slips past the
path_routing = false404 or a registry's rate limit - [x]
RegistryInfo.public_urlonGET /api/v1/registries, used by the Setup Guide and namespace upload snippets — the.netrcblock lists every host a client may authenticate against (entries are matched by hostname, so the main host alone left host-routed registries unauthenticated), and the Terraformsourcedrops the registry segment on a host-routed registry, where it is no longer part of the path - [x] Helm
ingress.extraHosts+ documentedconfig.server.trusted_proxies - [ ] Per-host TLS certificate management inside the server (currently the ingress's job)
- [ ] Instance-to-instance transfer for air-gapped estates — export a set of approved artifacts plus their metadata as a signed bundle, and import it into a disconnected instance.
[http_proxy]covers a restricted network; a genuinely offline one has no path in today except restoring a full backup, which moves the database, the config and every credential in it rather than the artifacts someone approved. Needs a bundle format, its signing, and a defined interaction with content-addressable dedup (RFC 0004-bis §13.2)
Webhooks & notifications
- [x] Subscribe to notifications for specific packages, versions, or registries (new version published, version deprecated, package removed)
- [x] Multiple notification channels: email, Slack, Microsoft Teams, outbound webhooks
- [x] User-configurable notification preferences and channel configuration in the UI
- [x] Inbound webhook API so external systems (CI pipelines, security scanners) can push events into BatleHub and trigger notifications or policy updates
Private registry features
Applies to registries running in local or hybrid mode.
- [ ] Seed a registry from an incumbent — import the contents of an existing Nexus, Artifactory or Verdaccio, not just the config for a new instance (
batlehub-cli registry suggestalready does the latter). This is the migration path for the users most likely to adopt BatleHub, and without it every adoption starts from an empty cache. Needs a per-source adapter plus an ingestion path that reuses the existing publish machinery so quotas, ownership, signing and SBOM all still apply (RFC 0004-bis §13.4)
Per-registry additions
- npm — versioning policies (enforce semantic versioning, allowlist version patterns)
- Cargo — versioning policies; verify full compatibility with the yank protocol from crates.io
- VS Code extensions — deprecation and unlisting; upload via the UI (form for VSIX + metadata), in addition to the existing
PUTAPI - [x] Maven — private artifact publishing via
mvn deploy; POM-triggered three-phase publish; JAR/checksum pre-upload; dynamically generatedmaven-metadata.xmlfrom DB;localandhybridmodes - [x] Terraform — private module publishing (tar.gz upload,
X-Terraform-Getredirect); private provider publishing (version manifest + per-platform binary upload);localandhybridmodes - [x] PyPI — private wheel / sdist publishing via
twine; Simple API served from DB;localandhybridmodes - [x] Conda — private channel with
repodata.jsongeneration from DB;.tar.bz2and.condapackage upload;localandhybridmodes
For all private registry types
- [ ] Writing grants at the package and version tiers (RFC 0017,
docs/rfc/0017-writing-grants-at-the-package-and-version-tiers.md) — RFC 0015 built the two deepest tiers of the grant hierarchy and left them without an editor. Thegrantstable carriesnode_kind IN ('package','version')(migration 041),PgGrantRepositorycan write either, andchain::stored_nodesreads both on everyauthorize— but the onlyput_grantcaller in the tree is the ownership projection, which writes package rows carrying exactlyreleases:publish+owners:read+owners:write. There is no way to grantreleases:readon one package to one group, which is RFC 0015 §4.4's own opening example, and no way to write a version-tier row at all. Two halves, and they must ship together: the admin API / CLI / console writer, and the per-version listing filter (filter_listing,package_visibility— built for §4.4 rule 2, never called, because with no version row a caller's verdict is uniform across every version and there is nothing to filter). The first version-tier grant makes the gap real with no other code change and no error: version indexes keep listing versions the caller may not read. The download gate still refuses them one at a time, so what leaks is the existence and the numbers rather than the bytes — bounded, and exactly what rule 2 decided against - [x] Artifact signing framework: publish with
X-Artifact-Signature/X-Signature-Typeheaders; signature stored in DB and returned on download; optionalsigning.requiredenforcement. Optional download-time verification of storeded25519signatures against configuredsigning.trusted_keys(signing.verify_on_download); Ed25519 only, since thersacrate is banned - [x] Ownership and team management: per-package owner table (user/group, admin/maintainer roles);
initialize_owneron first publish;can_publishcheck on subsequent publishes; admin API to list/add/remove owners - [x] Versioning policies:
enforce_semver,allow_prerelease,version_pattern(regex) per registry; enforced at publish time with HTTP 422 - [x] Beta / pre-release channel: allow specific users or groups to access unpublished versions before general release
- [x] Bulk operations:
POST /api/v1/admin/registries/{registry}/bulk-yank|bulk-unyank|bulk-delete - [x] A published version coordinate is never reused (RFC 0016): delete is a soft delete, so
1.4.0cannot mean two different things to two different lockfiles. The bytes go, a tombstone stays, and a re-publish onto the coordinate is refused for good — the crates.io/PyPI model rather than npm's, which matters more here because a private registry is frequently the only copy of what it holds. A package name is released when its last version goes, its owners with it; the version numbers stay spent - [x] Retention for locally published versions (RFC 0016):
keep_versions,keep_for_days,keep_if_pulled_daysas a union of vetoes — a version survives if any matches, so wrong configuration fails toward keeping.dry_runon by default, a per-version pin that outranks the policy, a floor date before which an absent download record proves nothing, and a rate limit for the first live run. Distinct from cache eviction, which discards something a re-fetch brings back - [x] Tombstone compaction: a deleted version's checksum, publisher and metadata age out on a window; the coordinate claim never does, and there is deliberately no setting that removes it
- [x] Content-addressable deduplication for stored artifacts (ref-counted via
artifact_dedup_index/artifact_dedup_refs) - [x] Integrity verification: verify checksums on re-serve, not only at publish time —
integrity.verify_on_servere-hashes stored bytes against a self-computed SHA-256 (recorded when first cached) on every serve (proxy cache hits and local reads); a mismatch fails with502and evicts the corrupt entry
CLI tool - batlehub-cli
- [x] a CLI for common private registry tasks (
publish,yank,list), suitable for use in CI pipelines —batlehub-clibinary incli/; global flags--profile,--server,--token,--registry,--jsonand env-var equivalents (BATLEHUB_*)- [x] Publish command that wraps the upload API, with support for multiple registry types and automatic metadata extraction from the artifact (e.g. extension, archive contents, manifest files) —
batlehub-cli publish <file>;detect_metaauto-detects registry type, name, and version - [x] Version management commands for yanking, unyanking, or deleting specific versions —
batlehub-cli version yank|unyank|delete - [x] Package management commands for listing versions, viewing metadata, or managing owners —
batlehub-cli package list|versionsandbatlehub-cli owners list|add|remove - [x] Authentication support for static tokens and token management —
batlehub-cli auth whoamiandtoken list|create|revoke; token passed via--token/BATLEHUB_TOKEN - [x] List of available registries and their types, with per-registry configuration details —
batlehub-cli registry list|info - [x] Suggest the set of registries a project needs —
batlehub-cli registry suggestscans the working directory (mise.toml/mise.locktool backends plus the usual manifests: Cargo.toml, package.json, go.mod, …), maps each detected source to a registry type, and emits the[[registries]]TOML block (or JSON) to paste intoconfig.toml.--client-envadditionally prints the environment variables that point each toolchain at BatleHub - [x] List packages and versions in a registry, with filtering options —
batlehub-cli package list|versions - [x] Autocompletion support for shell integration —
batlehub-cli completion bash|zsh|fish|...generates and prints the completion script; pipe to shell RC file - [x] Config file support for storing credentials and default options, with CLI overrides —
~/.config/batlehub/config.tomlwith named profiles;batlehub-cli config init|show|set - [x] Config file output for both CI automation and human use —
config initinteractive wizard;--jsonflag on all commands for machine-readable output
- [x] Publish command that wraps the upload API, with support for multiple registry types and automatic metadata extraction from the artifact (e.g. extension, archive contents, manifest files) —
- [x] A TUI mode for interactive use —
batlehub-cli tuilaunches aratatui/crosstermterminal UI- [x] List of registries with search and filter capabilities —
registry_listscreen - [x] Per-registry package explorer with version details and management actions —
package_listscreen (live search/filter) +package_detailscreen (yank / unyank keybindings) - [x] Interactive prompts for publishing new versions —
publish_formscreen with auto-detected name and version fields - [x] Help setup registry for a current project by scanning local files — TUI
SetupWizardscreen (sfrom registry list); detects Cargo.toml, go.mod, package.json, pyproject.toml, pom.xml, composer.json, *.gemspec, *.nuspec, *.csproj, *.tf, environment.yml; shows per-type config snippets and publish commands - [x] Auth workflow integration for OIDC and Kubernetes service accounts, including token caching and refresh —
batlehub-cli auth login(OIDC browser flow + K8s token path);auth refresh;oidc_refresh_token/oidc_expires_at/kubernetes_token_pathpersisted in profile; auto-refresh on startup; TUILoginscreen (Lfrom registry list) with three-tab method selector
- [x] List of registries with search and filter capabilities —
SBOM support
- [x] Proxy existing SBOMs from upstreams that provide them (GitHub dependency graph API, npm
bom.json) — enabled byfetch_upstream = truein[registries.sbom] - [x] Generate a minimal per-artifact SBOM (SPDX 2.3 or CycloneDX 1.4) at proxy time, from registry metadata and the downloaded archive
- [x] Org-level SBOM export: all artifacts served in a time range as a single merged document (
GET /api/v1/sbom/export?from=…&to=…&format=spdx|cyclonedx) — admin UI at/admin/sbom - [x] Generate SBOMs at upload time for private registries, extracting dependency manifests from the archive (
go.mod,Cargo.toml,package.json,pom.xml,requirements.txt) - [x] Policy option: deny publishing a private package if no manifest is found in the archive (
required = truein[registries.sbom]) - [x] Per-artifact SBOM accessible from the Package Explorer version detail view (SPDX and CycloneDX download buttons per version)
- [x] Periodically re-check cached SBOMs against vulnerability databases (see Artifact integrity) and update block / warn metadata automatically —
[vulnerability_scan]background task queries OSV and records findings, surfaced per-version in the Package Explorer and admin views - [ ] Third-party vulnerability flags — a declared external source (SOC, vendor feed) pushes version flags over an HMAC-authenticated, idempotent API, stating what it asserts (
kind: cve, malware, compromised release, licence…) separately from how hard BatleHub reacts (effect: inform / warn / gate / hard_block). Per-sourcemax_effectceiling;hard_blockdenies regardless of the registry's CVE-gate settings and does not fail open (RFCdocs/rfc/0002-vulnerability-flags-and-exposure.md) - [ ] Vulnerability exposure reporting — join the download audit trail with pushed flags and scanner findings alike, so an admin can ask "which consumers pulled a flagged version in the last N days", including the retroactive case where the advisory landed after the pull. Admin API + CLI + UI panel, read-only (same RFC)
UI improvements
- [x] Package explorer (
/explore) — collapsible catalog with registry sidebar; search and sort across all cached and upstream packages; per-package detail page showing version history with gate/firewall status per version;[registries.rbac.explore]config block for independent search permissions - [x] Package explorer caching and pagination for large registries (e.g. npm) to avoid fetching the entire index on every request; cache invalidation on new versions published or cache expiry
- [x] Package detail pages with version history and per-version download links (proxy URL constructed per registry type — cargo, npm, nuget, rubygems, pypi, conda, vsix)
- [x] User listing and block management in the admin panel (OIDC and Kubernetes-sourced identities, not just static tokens)
- [x] Config editor with validation and apply button (integrates with hot reload)
- [x] Read-only warning when the config file is mounted from a Kubernetes ConfigMap, with instructions for applying changes externally
- [ ] Web console redesign — full rework of
ui/: one catalog surface and one canonical package URL (today/packagesand/exploreduplicate the job, with two different detail URL shapes), an identity-driven shell, a designed first run for empty instances, four first-class list states (empty / filtered-empty / error / denied), a scope-and-count contract on destructive actions, one token source shared withdocs/, WCAG 2.2 AA, and French/English localisation (none exists today — every string is hardcoded English). Visual identity is reopened; the nameBatleHub.and the Monofolio lineage stay binding. Carried out with the Impeccable design skill as the working method —PRODUCT.mdis written,DESIGN.mdis the first deliverable, and its deterministic detector becomes a CI gate (RFCdocs/rfc/0003-ui-rework.md) - [x] Admin composition and the API surface the console is missing — the follow-on RFC 0003 deferred, in five phases. Every documented
200/201declares a body, enforced by a gate that walks the generatedApiDoc(136 responses carried only adescription, so generated clients emittedunknownand the docs site's API reference was blank),ui/src/lib/registry-types.tsand its four hand-written DTO mirrors are gone, andGET /api/v1/me/{quota,downloads,advisories}give a developer their own usage, pulls and advisories without an admin — scoped in the ports rather than the handlers, so no future caller can assemble the query without the filter./gained the quota meter and the advisories widget on what you recently pulled; the dashboard's trend reads a persisted hourly rollup rather than counters that reset with the process, under a[stats]block that also makes/metricsswitchable. All fifteen admin pages went through an Impeccable pass with authority to keep, update, split, merge, remove or add, one verdict per page; the three verdicts it only partly discharged were finished by RFC 0004-bis (RFCdocs/rfc/0004-admin-composition-and-api-surface.md) - [x] What RFC 0004 left, and the gates that could not see it — the residue RFC 0004 discovered and deliberately deferred, in six phases. Three gates that reported green over conditions they could not observe now observe them: the i18n audit reads component props and
<script>assignments rather than an enumerated attribute list, the catalogue test asserts every key is referenced (94 were not), and one merged gate measures the type ramp and display face on every rendered route instead of/admin/*only. The access-check simulator consults the account and IP block stores it used to ignore, so it stops answeringallowfor an account the adjacent page shows as blocked. Seven API gaps closed and one (A5, a config revert path) declined with its reason recorded. Every admin page gained the component test it never had, the explore cache stopped outliving the identity it was filled for, and fourteen free-text fields that named something the server already knew becameSelects or a real combobox. §13 adds four product gaps nothing was tracking, of which the licence gate is built and the other three are specified./packagesstays pinned red against its own design proof until O3 decides which side moves — a visible disagreement rather than a green gate over a page nobody was comparing to anything (RFCdocs/rfc/0004-bis-what-rfc-0004-left.md)
Testing
- [~] Unit tests for all registry adapters and policy evaluation logic — significant coverage added (entities, services, auth, storage router, registry adapters, web middleware, handler guards); ≥80% line coverage enforced by
task coverage-check; 2422 tests across 69 suites as of last run (1405 of them lib unit tests); recent additions cover the percent-encoded-registry bypass on both thepath_routing = false404 and the rate limiter,ProxyTrust::replace_fromreaching every clone, and the twopending_createdbranches ofload_pending_from_content - [x] CLI test suite — 23 unit tests (
parse_oidc_paste,is_token_expiring_soon,detect_project_typesfor all 9 manifest types) + 16 integration tests (registry, package, version yank/unyank/delete, publish, auth, shell completion, Kubernetes login); fixedInMemoryLocalRegistrycase-sensitivity bug so yank/delete tests pass end-to-end - [x] Protocol conformance fixtures — the client's paths, not ours —
crates/web/tests/protocol_conformance.rs: one table per ecosystem of the literal request lines a real package manager sends, each carrying the source it was read from, asserted to route; plus amust_findclass for collection endpoints, because a route that returns an empty200is indistinguishable from a stub by every other signal. Unserved paths carrynot_yet("phase N")and assert the opposite, so the list is a published inventory that can only shrink. This is the priority item in this section, and RFC 0009 is the argument for it: six findings, every one of them shipped green — four endpoints at addresses no client calls, one whose default client reads the single index we do not filter, and one stub the service index advertises as a feature. In each case a test existed and passed, because it was written from the implementation and could not discover that the implementation answers the wrong question (RFCdocs/rfc/0009-protocol-coverage.md§5) - [x] Integration tests against real upstream registries (gated, opt-in) — the discharge for RFC 0009 §12: the conformance tables encode what we believe each client calls, and until a real
npm auditandbundle installare captured against a live instance, that belief is reasoning rather than evidence. Captured, and it was not reasoning: seven ecosystems were run against a real server with real clients (npm, bundler, terraform, dotnet, micromamba, composer, pip, ovsx), finding twelve shipped bugs that every test in the repository passed (RFC 0009 §12). Now scripted, all of them:tests/heavy/{bundler,npm,pypi,openvsx,conda,nuget,composer,terraform}.shplusmarketplace.sh— each starts a real server behind a logging tap, drives the real client and asserts on the wire (task test:heavy, or onetask test:<ecosystem>-heavy). They run in CI asheavy-bundler,heavy-marketplaceand theheavy-clientmatrix, with the server undercargo llvm-covso the client's path counts toward coverage, and nightly because the clients come from outside this repository. Writing the scripts found six more shipped bugs in one pass, none of them a wrong path (RFC 0009 §12.16) - [ ] Broader fuzzing targets beyond the current four (RBAC, cache key, deny-latest, release age)
- [x] Cover code with SonarCloud —
.github/workflows/sonar.yamlruns frontend (vitest lcov) + backend (cargo-llvm-cov lcov, with Postgres/MinIO/Redis services) and uploads both reports to SonarCloud on every push tomain