Skip to content

Publishing files

A static file registry hosts plain versioned files instead of crates: release archives, prebuilt binaries, datasets, firmware images, anything your pipeline produces that is not a .crate. The client is curl (or any HTTP client) instead of cargo, but underneath it is the same Haven: the same tokens, the same policy engine with held and promoted versions, the same audit log. Creating one is the third path at registry creation; see Registries and Getting started.

Everything is addressed by a package, a version, and a filename:

https://haven.example.com/r/{registry}/files/{package}/{version}/{filename}

Package names follow the crate-name rules. Versions are free-form by default: a semver version, a date, a tag are all accepted, but only semver versions can be resolved as “latest” or by range; a non-semver version uploads and downloads fine by exact name. A package can narrow that with a version scheme.

A package can declare the shape its versions must take, on its Settings tab or through the API. Haven then rejects an upload that does not match:

Scheme Accepts
none Anything the charset allows. The default.
semver 1.4.2, 2.0.0-rc.1.
calver 2026.08, 2026.08.21, 26.08, 2026.08.100, each with an optional -modifier. Impossible dates are refused.
sequential Plain integers that only go up: 1, 2, 3. Each release must exceed every version the package already holds, drafts included.
Terminal window
curl -X PUT -H "Authorization: Bearer $HAVEN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"scheme": "semver"}' \
https://haven.example.com/api/v1/registries/acme-assets/packages/some-package/version-scheme

Declaring semver is what unlocks the range surfaces: only a package that promised every version parses can carry a version pin or answer a ?req= resolution honestly. The other two schemes buy consistency, not a range vocabulary, and declaring one of them is also a promise that no version will ever look like semver, so a narrower-than-* read grant on such a package is refused where it could never match.

The default none sits in between: it promises nothing, so a range still means whatever it can match. A package holding both 1.0.0 and 2026-08-21 covers the first with ^1 and the second only with *.

The rule is forward-only. Versions published before the change are never re-judged and keep serving whatever they are named, so a package that started out undisciplined can still adopt a scheme. The UI says how many existing versions would not have matched.

The check is per version, not per file. A release that ships a binary, its checksum and a signature is three uploads under one version number, and only the first of them introduces that version: the rest land without being judged again. The same goes for adding a file to a version that predates the scheme.

Package owners and registry owners can change a scheme, except while a version pin names the package: a pin is a standing decision, so remove it from the registry’s policy first.

Rust crates are always semver, and the setting shows disabled on a crate page. Cargo parses semver or refuses the publish, so there is nothing to choose.

You can declare a scheme when you create the package, before its first upload. Naming one here takes the same permission the dedicated route does, so a publish-only CI token can create the package but not choose its scheme:

Terminal window
curl -X PUT -H "Authorization: Bearer $HAVEN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"version_scheme": "calver"}' \
https://haven.example.com/api/v1/registries/acme-assets/packages/some-package

A version is assembled file by file as a draft:

Terminal window
# Repeat per file; the first upload creates the version as a draft
curl -X PUT -H "Authorization: Bearer $HAVEN_TOKEN" \
--data-binary @some-file.tar.gz \
https://haven.example.com/r/acme-assets/files/some-package/1.0.0/some-file.tar.gz

Uploading and publishing take a full-scope token of the owning organization, wielded by a registry owner or a package owner of the package being uploaded (new package names always take a registry owner — or the owner creates the package ahead of the first upload and stages a package owner on it, who then uploads first). CI can skip the stored token entirely: trusted publishing works on files registries too.

While a version is a draft it is listed and downloadable by exact version, but invisible to resolution: CI can assemble a version file by file without any consumer resolving it half-built.

Files are immutable per (package, version, filename): re-uploading an existing filename answers 409, whatever the bytes.

Haven computes the SHA-256 of every upload’s received bytes itself; that digest is the file’s identity in object storage and the checksum served back in every listing and on every download. To prove the transfer end to end, declare the checksum with the upload:

Terminal window
sha256sum some-file.tar.gz
# 9f86d08... some-file.tar.gz
curl -X PUT -H "Authorization: Bearer $HAVEN_TOKEN" \
-H "X-Checksum-Sha256: 9f86d08..." \
--data-binary @some-file.tar.gz \
https://haven.example.com/r/acme-assets/files/some-package/1.0.0/some-file.tar.gz

A declared checksum is verified against the server-computed digest and a mismatch is refused, so a transfer corrupted on the way in can never land. Without the header the upload is accepted as whatever bytes arrived; a registry policy can close that gap with the require_checksum rule, which refuses any upload that does not declare X-Checksum-Sha256 — at the gate, like the size cap below. Consumers verify the other half of the journey against the same digest; see verifying a download.

A Content-Type sent with the upload is stored and served back on download.

If the registry’s policy sets a per-file size cap, an oversized upload is refused outright, with the reason in the response: the bytes never land, so there is nothing to hold or waive.

A version’s license is an SPDX expression set by an owner while the version is a draft; publishing freezes it, the same way a crate’s license is frozen into its tarball:

Terminal window
curl -X PUT -H "Authorization: Bearer $HAVEN_TOKEN" \
-H "Content-Type: application/json" \
-d '{"license": "MIT OR Apache-2.0"}' \
https://haven.example.com/api/v1/registries/acme-assets/files/some-package/1.0.0/license

The registry’s license allowlist rule evaluates it exactly as it does a crate’s declared license.

Terminal window
curl -X POST -H "Authorization: Bearer $HAVEN_TOKEN" \
https://haven.example.com/api/v1/registries/acme-assets/files/some-package/1.0.0/publish

Publishing is permanent and one-way: it freezes the file set and the license, and makes the version resolvable. There is no unpublish, and a published version accepts no more files. A fixed release is the next version number; yank stays available forever.

If the registry has a policy, the publish response says whether the version starts held, and why, so a pipeline is never left wondering why nothing resolves.

A registry setting (off by default) bundles every published version’s files into one premade archive, zip or tar.gz, that consumers download in one request. The archive is built by a worker right after the publish — publishing freezes the file set, which is exactly what makes a one-time build sound. During the build every file is verified against its recorded checksum, and the finished archive’s own SHA-256 is recorded and served for end-to-end verification.

Things to know before turning it on:

  • Enabling covers versions published from then on. Versions published earlier get no archive automatically; an owner can build each one from its package page.
  • Archives are stored alongside the files and count toward storage billing — roughly doubling a registry’s stored bytes.
  • Versions whose files sum over the server’s archive size cap get no archive. The version listing’s archive field reports it as {"failed": true, "reason": ...} instead of a pending marker, and the archive download answers a final 404 rather than the retryable 503.
  • Turning the setting off stops building and serving archives but keeps the stored ones (still billed), so re-enabling is instant. Changing the format applies to versions published from then on; already built archives keep serving in their old format until rebuilt from their package pages.
  • A pipeline that publishes and immediately fetches the archive should expect a brief 503 Retry-After window while the build lands.

A version whose name does not match the package’s version scheme is refused at upload, before any bytes are stored, with a message naming what was expected. The refusal is in the audit log like every other publish-gate refusal.

Past that, one policy rule can reject a files publish outright: the license allowlist, and it does so by default. The reasoning is the same as for crates’ frozen facts: publishing freezes the license, so rejecting keeps the draft’s license editable, while accepting and holding would freeze the mistake in. Fix the license and publish the same version again. A standing promotion approval on the draft defeats the rejection, exactly as for crates.

A published version may start out (or become) held: stored, visible to owners on the package page and in the registry’s Held tab, but neither resolvable nor downloadable until it promotes. On a files registry, quarantine actually quarantines the bytes.

The rules a files policy can hold on are the subset a files version can carry:

  • quarantine: holds a version until it has aged past the delay, exactly as for crates.
  • license: the SPDX allowlist, when its reject toggle is off.
  • attestations: holds a version until enough distinct identities have attested it, with trusted_attesters narrowing whose sign-off counts — the same rule as for crates, on the same routes: attest with PUT /api/v1/registries/{registry}/packages/{name}/{version}/attestations (or the keyless /oidc-attestations twin). The version does not have to exist yet; a standing attestation counts the moment it publishes.
  • blocked: owners can manually pull a version pending re-review.
  • custom:<fact>: custom facts and rules work on files registries with two differences: rules target declared facts plus the one built-in both formats carry (haven:attestations_count — no other haven: built-ins, no extractors, and custom rules hold, never reject), and values can be pushed on drafts and published versions. Publishing freezes the file set and the license, never the annotations.

The cargo-only rules (SBOM, VCS provenance, publisher trust, advisories, pins) read facts that only a cargo publish carries; saving a files policy that names them is refused outright.

Holds clear the same ways as everywhere else: time passes, a passing value is recorded, or owners waive the rule with promotion approvals at the registry’s threshold. Drafts are pre-policy and are never held. See the Policy overview.

Terminal window
curl -X POST -H "Authorization: Bearer $HAVEN_TOKEN" \
https://haven.example.com/api/v1/registries/acme-assets/files/some-package/1.0.0/yank

DELETE on the same URL unyanks. Yank works as it does for crates: a flag, never a filter. A yanked version leaves latest and range resolution but stays downloadable by exact version and stays visible, flagged, in every listing. Both actions are audited.

  • Delete and overwrite do not exist: files are immutable per triple and publish is permanent. Yank is the retraction tool.
  • Upstream proxying: a files registry cannot mirror another server.