All notable changes to BatleHub will be documented in this file.
The format is based on Keep a Changelog.
- Host-based (subdomain) registry routing. A registry can now be bound to one
or more hostnames whose root serves it, in addition to
/proxy/{name}/…:https://npm.acme.io/lodashmeans exactly whathttps://hub.example.com/proxy/npm1/lodashmeans. Configure a wildcard with[subdomain_routing](enabled+base_domain), vanity hosts with a registry'shosts = […], or both. Every self-referencing URL the server generates — npmdist.tarball, the NuGet service index and registration@ids, the PyPI simple index, Composermetadata-url/dist, the Terraform providerdownload_url, the cargo indexdl/api— now reflects the ingress the client actually used. Off by default; with no hosts configured every generated URL is byte-identical to before. See the Host-based routing guide. registries[].path_routing = falsemakes a registry reachable only through its host(s);/proxy/{name}/…then returns 404 (not 403 — a disabled ingress should look absent). A registry with no reachable ingress is a config error.GET /api/v1/registriesgainedpublic_url, the registry's hostname-rooted URL when it has one. The Setup Guide and namespace upload snippets use it.[server].trusted_proxies— one server-level CIDR list governing which peers may setForwarded/X-Forwarded-Host/X-Forwarded-Proto/X-Forwarded-For. Previously the forwarded host and scheme were trusted unconditionally while only the client IP had a rule; now all three follow one verdict computed once per request. Bare IPs are accepted as/32(/128).- Config warnings —
AppConfig::warnings(), surfaced atGET /api/v1/admin/config/warnings, inline in the responses of/config/validateand/config/from-content, and rendered on the Config Reload admin page. First users: an unstated proxy-trust policy, a shadowed deprecated key, and a registry name that cannot become a DNS label. - Helm:
ingress.extraHostsfor the additional hostnames, and a documentedconfig.server.trusted_proxies. pending_createdon the config-reload responses.POST /config/from-contentanswers200with an empty diff both when it stages a pending reload and when the submitted content is byte-identical to the last load attempt, in which case there is nothing to stage; the flag tells the two apart instead of leaving the caller to find out from a404 No pending reloadat apply time.falsefor/config/validate(a dry run) and for/config/reloadand/config/pending/apply(which consume a pending rather than leave one).
[server].trusted_proxiesis now hot-reloadable, and is swapped just before the host-routing table it guards. A reload that turns host routing on used to keep the startup trust policy until the process restarted, which left routing driven byX-Forwarded-Hostfrom any peer — the state config validation exists to make unreachable.- The rate limiter buckets anonymous clients on the same client IP the IP-block
middleware bans, instead of the raw TCP peer. Behind a trusted proxy the two
previously disagreed: one abusive client could exhaust a bucket shared by every
anonymous user, and the resulting
429s then counted as violations against each innocent client's own IP. - Registry names are matched on the path actix routes on rather than the raw
URI. Percent-encoding a character of the name (
/proxy/npm%32/…) reached the registry's handler while slipping past both thepath_routing = false404 and the registry's rate limit. ui/openapi.jsonis tracked in git, so an API change shows up in review as a diff of the contract. Refresh it withtask dump-spec, which needs no database. The generated TypeScript client underui/src/client/stays untracked.
X-Forwarded-Foris now read right to left, skipping hops that fall insidetrusted_proxies, instead of taking the left-most entry. Each hop appends the address it observed, so everything left of the entry our own proxy wrote is client-supplied: behind a trusted proxy, any client could name the IP that[ip_blocking]bans and the anonymous rate-limit bucket is keyed on — evading its own ban, or getting a third party blocked. Entries are parsed as IP addresses (theip:portand[ipv6]:portforms included) and the walk stops at anything that does not parse, falling back to the TCP peer address rather than stepping over a hop it cannot classify. Deployments with notrusted_proxieslist are unaffected — they still ignore the header entirely.- The Setup Guide's
.netrcblock lists every host a client may authenticate against..netrcentries are matched by hostname, so a guide naming only the main host meant no credentials were sent to a host-routed registry and every authenticated install failed with401. - The Terraform
sourcesnippet drops the registry segment for a host-routed registry, where provider endpoints live at the root, and keeps the port, whichterraform initneeds on any deployment not served on 443. POST /config/from-contentreports the warnings of the config submitted rather than of the one still in force when the content matches the last load attempt. An admin staging a config with warnings could see an empty warning panel.
[ip_blocking].trusted_proxies— use[server].trusted_proxies. The old key keeps working (and now governs the forwarded host and scheme too, so an existing deployment can adopt host routing without touching it), but raises a config warning. When both are set,[server]wins. An entry of the old key that is not an IP or CIDR range is still dropped — with aproxy-trust.invalid-deprecated-entrywarning — rather than failing the boot, since that key never validated its entries before.
First stable release.
- SSRF hardening across registry adapters, including OpenVSX upstream requests
- Signed-release enforcement (
RequireSignedReleaseRule) — optionally require GitHub/OpenVSX/VS Code Marketplace releases to carry verifiable signatures before they're served, with a role-based bypass - Open-source release housekeeping:
LICENSE(Apache-2.0),SECURITY.md,CONTRIBUTING.md
- Fixed the config hot-reload watcher retriggering without a real change; a reload loop that fires more than a few times within 30s without settling now stops and surfaces a warning instead of looping forever
- Large hardening/bug-fix pass across handlers and services following an in-depth code review
- Frontend lint job added to CI (
front-test.yaml) - Dependency upgrades across the Rust workspace (including
sqlx) and the UI toolchain - Continued UI rework (routing, navigation) and codebase health cleanup (dead code, duplication)
0.5.0 - 2026-06-29
- 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)
- OSV vulnerability scanning — per-registry
cve_gaterule (min_severity,block/warn-only,bypass_roles); periodic background re-scan via[vulnerability_scan]task; findings stored inartifact_vulnerabilitiesDB table; per-version CVE status surfaced in the Package Explorer and admin views - Go module vulnerability database proxy — GOPROXY vuln endpoint (
/proxy/{reg}/goproxy/vuln/) proxied sogovulncheckand related tooling can query BatleHub directly without reaching the public database - NuGet vulnerability endpoint proxy — NuGet v3 vulnerability endpoint wired into the service index so
dotnet restorevulnerability checks flow through the proxy cache - Vulnerability scanner extension point — documented API for adding custom vulnerability scanners (
docs/adding-a-vulnerability-scanner.md);docs/vulnerability-proxy.mdcovers the proxy-side configuration
- User block management — DB-backed user block list (
028_user_blocksmigration);UserBlockMiddlewareevaluates the block list before any request handler and returns 403; admin API (GET/POST/DELETE /api/v1/admin/users/blocks); Admin Users page in the UI lists OIDC, Kubernetes, and static-token identities with block/unblock actions; fails open on DB errors to avoid locking out admins
- Eclipse Che workspace login — login page detects Eclipse Che environment variables and displays pre-configured connection instructions for workspace-hosted instances
- CLI download command (
batlehub-cli download) — downloads an artifact from any configured registry to a local file; auto-detects registry type and constructs the correct download URL - SonarCloud integration —
.github/workflows/sonar.yamlruns frontend (Vitest LCOV) and backend (cargo-llvm-cov LCOV, with Postgres/MinIO/Redis services) coverage and uploads both reports to SonarCloud on every push tomain
- JetBrains artifact post-copy path handling corrected; improved
docs/path-mapper.mdto clarify URL routing for large IDE archives - TOCTOU race condition fixes and general code-review hardening across several handler paths
- Correct handling of unreachable match arms and unused assigned values flagged by the compiler
- Code duplication reduced below 5% (tracked via SonarCloud)
- Container image updated to TiKV-based build;
ContainerfileandContainerfile.hardenedboth updated
0.2.0 - 2026-06-14
- npm — proxy with scoped package support; local/hybrid publish
- Cargo — sparse index proxy compatible with
cargosparse protocol; local/hybrid publish - GitHub Releases — artifact download proxy for GitHub release assets
- OpenVSX — VS Code extension proxy for the open-source marketplace
- VS Code Marketplace — VSIX download proxy for the official marketplace
- Go modules (GOPROXY) — Go module proxy protocol (
$GOPROXY); multi-segment module path routing via{module:[^@]+}pattern - Maven / Gradle — Maven Central-compatible metadata XML + JAR / POM downloads; private publishing via
mvn deploy(three-phase POM + JAR + checksum upload); dynamically generatedmaven-metadata.xmlfrom DB; local/hybrid mode - Terraform — provider and module proxy protocol; private module (tar.gz +
X-Terraform-Getredirect) and provider (version manifest + per-platform binary) publishing; local/hybrid mode - RubyGems — gem download and version listing; local/hybrid mode with yank / unyank
- Composer — Packagist v2 protocol (
packages.json, p2 metadata, dist downloads); private package ZIP upload; local/hybrid mode - PyPI — Simple API proxy with URL rewriting; private wheel / sdist publishing via
twine; Simple API served from DB; local/hybrid mode - Conda / Anaconda —
repodata.jsonproxy and channel merging;.tar.bz2and.condapackage parsing; private channel publishing; local/hybrid mode - NuGet — NuGet v3 service index + flat container proxy;
.nupkgand.nuspecdownloads; private publishing viadotnet nuget push;X-NuGet-ApiKeynormalised toAuthorization: Bearer; local/hybrid mode
- Static tokens — plain-text Bearer tokens and Argon2id PHC hashes in
config.toml;batlehub hash-token <token>CLI helper - 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
- Kubernetes service accounts — TokenReview API validation; role and group mapping; in-cluster defaults
- GitHub / Forgejo Actions OIDC (
type = "actions-oidc") — short-lived JWT validation for workflow jobs; claim-to-group mapping (repository,ref,environment,actor, …) with static and dynamic templates; glob and regex pattern matching; AND / OR condition logic per rule
- RBAC engine — role/group rules per registry with
pull/push/adminactions; evaluated byRbacRule - Built-in policy rules —
DenyLatestRule(block floatinglatesttags),BlockListRule(explicit package/version deny list),ReleaseAgeGateRule(reject versions younger than a configured age) - Rate limiting — per-user and per-registry token-bucket rate limits; per-group shared pools; hard block or soft warn enforcement;
Retry-AfterandX-RateLimit-*response headers; state resets on restart - IP blocking — fail2ban-style blocking via
IpBlockStore; configurable block duration and thresholds; outermostactix_web::middleware::Conditionmiddleware - Publish quota — per-user, per-group, and per-registry quotas on storage usage and package count;
X-Quota-*response headers; admin API for viewing and resetting quotas; enforcement policies: block or warn
- Cache-Control honouring — respects
no-cache,max-age, andno-storefrom upstream responses - Eviction policies — TTL-based expiry, "not accessed for N days", garbage-collect all versions except the latest N, storage-size cap with LRU eviction
- Content-addressable deduplication — identical artifact bytes stored once; ref-counted via
artifact_dedup_index/artifact_dedup_refs; backwards-compatible with pre-dedup artifacts - Proactive cache warming — pre-fetch known versions on startup and on demand via
POST /api/v1/admin/registries/{registry}/warm; configurablewarm_packages,warm_latest_n,warm_concurrency - Explore cache — 10-minute in-memory cache for the explore list and stats; stale-on-DB-error fallback; admin invalidation via
POST /api/v1/admin/explore/invalidate; auto-invalidated on local publish
- Local and hybrid operating modes for all supported registry types
- Ownership 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 - Versioning policies —
enforce_semver,allow_prerelease,version_pattern(regex) per registry; enforced at publish time with HTTP 422 - Beta / pre-release channel — per-registry allow-list of users or groups who may access unpublished versions (
BetaChannelPort, DB-backed) - Artifact signing —
X-Artifact-Signature/X-Signature-Typeheaders at publish; signature stored in DB and returned on download; optionalsigning.requiredenforcement - Bulk operations —
POST /api/v1/admin/registries/{registry}/bulk-yank|bulk-unyank|bulk-delete
- Per-artifact SPDX 2.3 and CycloneDX 1.4 generation at proxy time and at publish time; archive manifest extraction (Cargo.toml, package.json, pom.xml, go.mod, requirements.txt, …)
- Upstream SBOM fetch from GitHub dependency graph API and npm
bom.json - 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 required = truepolicy option in[registries.sbom]— deny publishing a private package when no manifest is found in the archive- Per-artifact SBOM download buttons (SPDX and CycloneDX) in the Package Explorer version detail view
HotConfigbehindArc<RwLock<HotConfig>>— in-flight requests finish with the old snapshot; config swap is atomic- File watcher (
notifycrate) — loads a pending reload; admin confirms viaPOST /api/v1/admin/config/pending/applyor discards withDELETE /api/v1/admin/config/pending - Schema validation and upstream connectivity probes before storing a pending reload
- Config audit trail — every reload is recorded in
config_changestable; retrievable viaGET /api/v1/admin/config/changes - Global admin banner — broadcast info / warning / error to all visitors; backed by in-memory, Redis, or PostgreSQL;
PUT/DELETE /api/v1/admin/banner BATLEHUB_DISABLE_HOT_RELOAD=1env var — disables the file watcher and all reload endpoints (for read-only Kubernetes ConfigMap mounts)
- Outbound notification channels: email (via
lettre), Slack, Microsoft Teams, HTTP webhooks - DB-backed subscriptions — subscribe to events per package, version, or registry (new version published, version deprecated, package removed)
- Fire-and-forget dispatch integrated into all publish and yank handlers
- Inbound webhook receiver — external systems (CI pipelines, security scanners) can push events into BatleHub
- Prometheus metrics endpoint (
/metrics) — request counts, cache hit/miss rates, latency percentiles, error rates per registry - Health check endpoint (
/healthz) — verifies connectivity to the database and all configured storage backends - Stats dashboard on the admin home screen — hits/misses, bandwidth saved, per-registry and aggregate
- Full command tree:
registry list|info,package list|versions,version yank|unyank|delete,owners list|add|remove,publish,auth whoami|login|refresh,token list|create|revoke,admin,config init|show|set,completion,hash-token batlehub-cli publish <file>— auto-detects registry type, package name, and version from the artifact (detect_meta); supports all local/hybrid registry typesbatlehub-cli auth login— OIDC Authorization Code browser flow with token caching; Kubernetes token path support; auto-refresh on startup- Shell completion for bash, zsh, fish, and others via
batlehub-cli completion - Named profile config at
~/.config/batlehub/config.toml; global flags--profile,--server,--token,--registry,--jsonandBATLEHUB_*env-var equivalents - TUI mode (
batlehub-cli tui) — ratatui / crossterm terminal UI with: registry list, package explorer with live search/filter, package detail (yank / unyank keybindings), publish form, setup wizard (scans local manifests and shows per-type config snippets + publish commands), login screen (OIDC / Kubernetes / static token)
- Package Explorer (
/explore) — collapsible registry catalog sidebar; search and sort across cached and upstream packages; per-package detail page with version history and gate/firewall status per version; independent search permissions via[registries.rbac.explore] - Setup Guide — API-driven; tabs appear only for registry types configured on the server; per-type config snippets and client commands defined in
ui/src/config/registryTypes.ts - Monofolio design system — OKLCH colour tokens, 2 px sharp corners, crimson + copper palette, JetBrains Mono font, cyber-grid background,
text-copperutility class - Admin pages — config reload (pending/apply flow, audit log), global banner editor, SBOM org export, webhook / notification subscription management
- Helm chart for Kubernetes deployment (
helm/) - Hardened OCI container image (
Containerfile.hardened) with minimal attack surface - Forgejo CI/CD workflows — lint (
cargo clippy -D warnings), format check, tests, ≥ 80% line coverage, container build sqlx-macrosandsqlx-mysqlpatched to empty stubs in[patch.crates-io]to remove thersacrate (RUSTSEC-2023-0071)aws-sdk-s3andaws-configwithdefault-features = falseto avoidlegacy-rustls-ring(RUSTSEC-2026-0098 / 0099 / 0104)- Fuzz targets for RBAC evaluation, cache key generation, deny-latest rule, and release age gate (
task fuzz)