Skip to content

Consuming files

Downloading from a static file registry takes any HTTP client and a credential. Plain files have no client-side resolver and no index protocol, so Haven answers “which version” itself, server-side, and hands the bytes out through its object storage.

Downloads always require a credential, on public registries too. The minimal credential for a public registry is a personal public-read token; see Tokens & access.

Terminal window
curl -L -O -H "Authorization: Bearer $HAVEN_TOKEN" \
https://haven.example.com/r/acme-assets/files/some-package/1.0.0/some-file.tar.gz

Pass -L: when the object store supports it, the download answers a short-lived redirect to a presigned URL and the bytes come straight from storage; otherwise Haven streams them itself. Either way the response carries the file’s checksum in X-Checksum-Sha256, and the Content-Type the publisher uploaded with. To check the bytes you received against that checksum, see verifying a download.

A credential that is not entitled, a package that does not exist, and a held version all answer the same 404: the download endpoint discloses nothing. The reasons a held version refuses are one authenticated call away, on the version listing and on the package’s status in the UI.

Terminal window
# The latest published version
curl -H "Authorization: Bearer $HAVEN_TOKEN" \
https://haven.example.com/r/acme-assets/files/some-package
# The highest published version matching a range, cargo semantics
curl -H "Authorization: Bearer $HAVEN_TOKEN" \
"https://haven.example.com/r/acme-assets/files/some-package?req=^1.4"

The response names the winning version and lists its files with sizes, checksums, and content types, so a script can pick its file and download it. Only published, non-yanked, semver-parseable versions resolve — never a draft, however high its number. “Latest” prefers the highest stable version; a prerelease wins only when nothing stable exists, the crates.io posture.

One level deeper, GET /r/{registry}/files/{package}/{version} answers the same shape for one exact version, drafts and yanked versions included, flagged rather than hidden. This is where a script diagnosing a refused download finds held and held_because.

A ?req= range only means something against a package that numbers its versions in semver. A package on any other version scheme still resolves without req= (you get its latest semver-parseable version, or nothing), but a requirement has no vocabulary to match a date or a counter. The package’s Versions tab in the UI has a box that resolves a requirement against the live catalogue, on semver packages.

When the registry has version archives turned on (an owner setting, off by default), every published version also serves all of its files as one premade archive, zip or tar.gz per the registry’s setting:

Terminal window
curl -L -O -J -H "Authorization: Bearer $HAVEN_TOKEN" \
https://haven.example.com/r/acme-assets/files/some-package/1.0.0/_archive

The response is named {package}-{version}.{format} and carries the archive’s own checksum in X-Checksum-Sha256. The _archive segment can never collide with a real file: filenames must start with a letter or digit.

Archives are built right after a version publishes, by a worker. In the short window before the build lands the route answers 503 Service Unavailable with a Retry-After header — a script that publishes and immediately downloads should retry, not conclude the archive does not exist. Versions published before the registry turned archives on answer the same 503 until an owner builds them from the package page, so bound the retries when fetching older versions. A version whose build has permanently failed (its files are over the server’s archive size cap, or the build ran out of attempts) answers 404 with the reason instead: final, so a retry loop written against the 503 contract terminates. The version listing says where things stand: its archive field carries {sha256, size, format, built_at} once built, {"pending": true} while building, {"failed": true, "reason": ...} when no archive is coming, and null when the registry does not serve archives (or the version is a draft).

The same download rules apply as for single files: a credential is always required, drafts have no archive, a held version answers 404, and a yanked version stays downloadable by exact version.

Every file’s SHA-256 is computed by Haven from the bytes the publisher uploaded; it is the file’s identity in object storage, and publishers can verify it at upload time too. To verify a download end to end, read the expected checksum from the version listing, then hash what you received:

Terminal window
expected=$(curl -s -H "Authorization: Bearer $HAVEN_TOKEN" \
https://haven.example.com/r/acme-assets/files/some-package/1.0.0 \
| jq -r '.files[] | select(.filename == "some-file.tar.gz").sha256')
curl -L -O -H "Authorization: Bearer $HAVEN_TOKEN" \
https://haven.example.com/r/acme-assets/files/some-package/1.0.0/some-file.tar.gz
echo "$expected some-file.tar.gz" | sha256sum -c

The X-Checksum-Sha256 response header is not the thing to script against: on a presigned download it rides the redirect, and most HTTP clients following -L only surface the final response’s headers. The version listing is the reliable place to read the expected value — and it lets you pin the checksum before fetching the bytes.

When Haven streams the bytes itself instead of redirecting, it re-hashes them against the recorded checksum before serving: storage corruption surfaces as an error, never as silently wrong bytes.

Version archives verify the same way: the version listing’s archive.sha256 is the expected digest of the whole bundle, computed when the archive was built — and every file inside the archive was itself verified against its recorded checksum during the build, so the one digest covers the version end to end.

A public files registry is browsable by any signed-in user, but the byte surfaces stay behind a credential: resolution and download need at least a personal public-read token, the same rule as public cargo registries. Held versions stay invisible to the public; the gate applies to strangers exactly as it does to you.

Per-package grants work as they do for crates: an owner grants a named identity read access to one package, with a version requirement, without making anything else in the registry visible. The grantee’s existing token then resolves and downloads that package.

Terminal window
curl -X PUT -H "Authorization: Bearer $HAVEN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"subject": "customer-identity", "version_req": "^1"}' \
https://haven.example.com/api/v1/registries/acme-assets/files/some-package/grants

One files-specific rule: versions are not guaranteed to be semver, and a semver range cannot honestly match a version that is not one. * grants the whole package and is the only requirement that covers non-semver versions; narrower requirements cover only the versions they can parse. A package that declares the semver version scheme has no non-semver versions to worry about, so every requirement means what it says there. A package that declares calver or sequential has promised the opposite, so a narrower requirement there is refused when you write it: nothing it publishes could ever match. Use *, or change the scheme first. A grant can be added before the package’s first upload.

Withdrawing works the same as on crates, on the same path:

Terminal window
# One range, or every range this subject holds when version_req is omitted.
curl -X DELETE -H "Authorization: Bearer $HAVEN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"subject": "customer-identity", "version_req": "^1"}' \
https://haven.example.com/api/v1/registries/acme-assets/files/some-package/grants

The response lists the requirements actually removed. Withdrawing something that was never granted is not an error: it answers 200 with an empty list, because the access you asked for is already the access that exists.