Skip to content

GitHub ​

Proxy and cache the GitHub REST API — release listings and metadata, release assets, source tarballs/zipballs, and raw repository files. The first request is streamed from upstream and cached, gated by RBAC and the release-age gate; there is no private publish.

At a glance ​

Config typegithub
Default upstreamapi.github.com
Modesproxy-only
Addressingper-package
Private publish❌ proxy-only
Air gapoffline, the release listing and the release by tag are composed from the held assets; a pinned mise install works without a lock

Proxy setup ​

Address a repository by <owner>/<repo>. Replace <registry> with your configured registry name; add -H "Authorization: Bearer $BATLEHUB_TOKEN" when the registry requires auth:

bash
REG="https://batlehub.example.com/proxy/<registry>"

# List releases / get a release by tag
curl $REG/<owner>/<repo>/releases
curl $REG/<owner>/<repo>/releases/tags/v1.0.0

# Download a release asset by filename
curl -L -O $REG/<owner>/<repo>/releases/download/v1.0.0/app.tar.gz

# Source tarball / zip for a tag, branch, or commit
curl -L -O $REG/<owner>/<repo>/tarball/v1.0.0
curl -L -O $REG/<owner>/<repo>/zipball/v1.0.0

# Raw file
curl -L $REG/<owner>/<repo>/raw/main/README.md

Blocked versions ​

The release listing drops a blocked release, newest-first order intact, so a client never selects a release whose assets it will then be refused.

A release is identified by its tag, and the same release is tagged 1.2.3 in one repository and v1.2.3 in the next. A block matches either spelling, so it does not depend on which habit the repository follows.

The upstream document is cached for the registry's metadata_ttl; blocks are applied on top of the cached copy on every request, so blocking a version takes effect immediately rather than when the cache expires.

See blocking a package version for the two halves of a block, and which listings are filtered for the full table.

What a version is here ​

A package registry names immutable things. A forge does not: main is whatever it points at when you ask, and a tag can be moved. So every forge request resolves its ref to a commit before anything is fetched, and that commit is what the cache, the metadata and the supply-chain verdict key on (RFC 0019).

Every forge response carries the resolution:

HeaderMeaning
X-BatleHub-Ref-Kindtag, branch or commit
X-BatleHub-Resolved-Committhe commit that answered
X-BatleHub-Ref-Previous-Commitwhat the same ref answered with last time, when that differs
X-BatleHub-Ref-Requestedthe ref as you spelled it — the only place it survives once the coordinate has become the commit

Three facts about a ref change what a request gets, and [registries.refs] decides what each one does:

FactCodeDefault
the request followed a branchMUTABLE_REFwarn — served, and said so
a tag now resolves to a different commitTAG_MOVEDdeny
a release asset's digest changed since the bytes were cachedASSET_REPLACEDdeny

A moved tag and a replaced asset are the two forge-native ways to swap bytes under a stable coordinate, which is why they are refused by default. The first resolution of a tag is always trusted — there is nothing to compare it with — and a change is noticed on the next resolution after tag_ttl_secs (one hour by default).

On a registry with [registries.security] these facts ride the version's verdict, so a warned branch answers with X-BatleHub-Verdict: warned and batlehub why explains it. Without that section there is no verdict to carry them: a deny is a plain 403 naming the code, and a warn is the headers above. That is the honest degradation, and it is the reason X-BatleHub-Ref-Previous-Commit exists.

toml
[registries.refs]
branch_ttl_secs = 60      # how long a branch → commit resolution is trusted
tag_ttl_secs    = 3600    # and a tag's; also the detection latency of a moved tag
mutable_refs    = "warn"  # "warn" (default) | "deny"
tag_moved       = "deny"  # "deny" (default) | "warn"

Raw files ​

Raw content is off unless you turn it on. It used to be implicitly served; a registry that has no [registries.raw] block now refuses it, and the refusal says so in the body.

toml
[registries.raw]
enabled        = true
max_size_bytes = 10485760          # 10 MiB; must not exceed [limits].max_artifact_size_bytes
repos          = ["cli/*"]         # owner/repo globs; empty allows any repository
require_pinned = false             # true refuses a branch ref for raw content
scripts        = "warn"            # "warn" | "deny" | "ignore"

A file over the ceiling is refused, never truncated. scripts looks at the file's extension (.sh, .bash, .ps1, .py, .bat, …) and, under deny, at its first bytes as well — so an extensionless payload that starts with a shebang is refused too. scripts defaults to deny on a registry that has a [registries.security] profile and to warn on any other: opting into a quarantine is opting into "nothing unscanned is served", and a script passed through is the one artifact no scanner here reads.

Raw content is always served as application/octet-stream with X-Content-Type-Options: nosniff, so a raw HTML file cannot execute as a document on this origin.

Typed API reads ​

The release listing and the release by tag are served already. Three more read-only JSON routes are available on request:

toml
[registries.api_reads]
families = ["tags", "commits", "branches"]
FamilyRoute
tagsGET /proxy/<registry>/<owner>/<repo>/tags
commitsGET /proxy/<registry>/<owner>/<repo>/commits/<sha>
branchesGET /proxy/<registry>/<owner>/<repo>/branches/<name>

They answer BatleHub's own shape, not the forge's: there is no upstream URL in them to follow, no field that means something different per forge, and no room for a passthrough to grow into one. A family the registry did not ask for answers 404. contents and git/blobs are never accepted — they are raw content by another door, and [registries.raw] is where that decision lives.

Release documents are rewritten. tarball_url, zipball_url and each asset's browser_download_url are repointed at this proxy, and the forge's own API links are removed. A client that reads the release document instead of building a path — mise, gh — therefore stays behind the proxy, with its policy, its cache and its audit trail.

Authentication ​

Pass a BatleHub token as a Bearer header (-H "Authorization: Bearer $BATLEHUB_TOKEN") when the registry's RBAC requires it. For private GitHub repositories or to raise GitHub's rate limits, configure a GitHub token as the registry's upstream auth in the server config — the client never sees it.

Notes ​

  • Proxy/cache only: the first request is streamed from upstream and cached; later requests are served locally.
  • Forgejo/Gitea registries reuse this same URL scheme — see Forgejo.
  • Tools like mise (aqua, ubi backends) can be pointed here with URL replacements; see the Setup Guide.
  • Attestations and commit signatures are read where GitHub offers them: a release asset's build attestation (/attestations/{digest}, github.com and GitHub Enterprise 3.13 or later) and the commit's own verification object. What they say reaches the verdict as PROVENANCE_MISSING or PROVENANCE_INVALID; a forge with nothing to say reports missing, never a guess.

See also ​

Released under the Apache 2.0 License. Made with ❤️ and too much ☕.