Sign In

Documentation

Generic artifacts

Firmware, datasets and build outputs, governed like packages.

Source: api.packr.blueforge.studio/-/docs/generic-artifacts

Generic artifacts

Raw files that belong to no package ecosystem: firmware images, datasets, build outputs, signed installers, model weights.

These are the usual reason a team runs a second registry beside this one. The reason was rarely that the second registry was better β€” it was that there was nowhere here to put a file that is not an npm tarball or a Maven jar.

Enable with generic in ENABLED_ECOSYSTEMS.

Endpoints

PUT /generic/{scope}/{name}/{version}/{file}
GET /generic/{scope}/{name}/{version}/{file}
GET /generic/{scope}/{name}/{version}      files in a version, with digests
GET /generic/{scope}/{name}                versions, newest first
# Publish
curl -X PUT --data-binary @firmware.bin \
  -H "Authorization: Bearer $PACKR_TOKEN" \
  https://api.packr.blueforge.studio/generic/acme/router-fw/2.4.1/firmware.bin

# List what a version contains
curl https://api.packr.blueforge.studio/generic/acme/router-fw/2.4.1
{
  "scope": "acme",
  "name": "router-fw",
  "version": "2.4.1",
  "files": [
    { "file": "firmware.bin", "sha256": "…" },
    { "file": "manifest.json", "sha256": "…" }
  ]
}

It is not a separate registry

This is a naming convention over the same core the package adapters use, which means a firmware image is governed exactly like a crate:

  • Publish authorization and per-scope token restrictions apply.
  • Visibility applies. A private package's files, listings and versions all return 404 to a caller who may not read it β€” the same status and body a package that does not exist gets (internal/generic/generic.go:155-156 gates through apierror.NotFound). 403 would confirm the name is taken, which is a discovery side-channel the code deliberately avoids. The rest of this document describes the 404 behaviour correctly; an earlier revision of this one line said 403 and contradicted it.
  • Yank and deprecate apply. A yanked version drops out of the version listing while staying downloadable by exact name.
  • Provenance is recorded on publish, like any other artifact.
  • The credential scan applies. Nothing about being a raw file makes it a safer place to leave an access key, and a publish carrying one is refused.

Several files per version

Firmware ships as an image plus a manifest plus release notes. Splitting those across versions would misrepresent them as separate releases, so a version holds as many files as you publish into it.

The first file published becomes the version's authoritative blob; the rest are stored beside it. That is why a version is created by its first upload rather than declared up front.

Rules

  • Artifacts are immutable. Republishing a filename is a 409. A build that pinned it by digest keeps resolving to the same bytes.
  • Downloads are opaque. application/octet-stream, with nosniff and an attachment disposition. The registry does not know what your bytes are and will not guess β€” sniffing a content type here would let a publisher serve arbitrary HTML from the registry's own origin. The one exception is opt-in and by extension, never by sniffing: GENERIC_TYPED_FILES=true serves .json, .sig, .png, .jpg/.jpeg, .webp, .avif and .svg inline with their real type, under a sandbox Content-Security-Policy so an SVG cannot run script, and never redirects them (an updater has to read latest.json). Turn it on only for an instance whose origin serves nothing else β€” the releases host, not the main registry.
  • Hidden means absent. A private package answers every read route with exactly the 404 β€” status and body β€” that route gives for a package that does not exist, so the read gate cannot be used to discover names.
  • Caching follows visibility. A published file never changes, so public files are Cache-Control: public, max-age=31536000, immutable; private ones are private, no-store.
  • Filenames are validated, not sanitised. A filename becomes part of a storage key. Anything that could climb out of the package's own prefix is refused rather than quietly rewritten, because rewriting changes what you asked for without telling you.
  • .packr/ is reserved for registry metadata such as provenance and SBOMs. A file published under that prefix would be invisible in its own listing. The reservation is wider than just the prefix: IsRegistrySidecar (internal/core/sidecar.go:47-50) also reserves any .asc suffix, and internal/generic/publish.go:16-18 rejects both, so it is refused instead.
  • MAX_GENERIC_SIZE defaults to 1 GiB, deliberately larger than any package ecosystem's ceiling.

Large binaries

Installers, disk images and datasets run to hundreds of megabytes. Nothing on the path holds a whole file in memory:

  • Uploads stream. On an S3/MinIO blob store, a file larger than one 16 MiB part goes up as a multipart upload one part at a time, so a 500 MB .dmg costs the registry 16 MiB of RAM. An upload that crosses MAX_GENERIC_SIZE mid-stream is aborted; no partial object becomes visible. (With the credential scan on, the upload is first spooled to a temp file, never to memory, so size the container's temp disk for your largest concurrent uploads.)
  • Transfers get their own deadline. The server-wide HTTP_READ_TIMEOUT (30s) / HTTP_WRITE_TIMEOUT (120s) stay tight for every other route; /generic/ uploads and proxied downloads extend their own deadline to GENERIC_TRANSFER_TIMEOUT (default 1h).
  • Downloads can bypass the registry. With GENERIC_DIRECT_DOWNLOAD=true and an S3/MinIO blob store, GET /generic/.../{file} answers 307 to a presigned object-store URL (valid DIRECT_DOWNLOAD_TTL, default 15m, with Cache-Control: no-store on the redirect). The object store serves the bytes and Range requests, so downloads resume without the registry in the path. The link forces application/octet-stream and an attachment named for the file. Set S3_PUBLIC_ENDPOINT when S3_ENDPOINT is a fleet-internal address clients cannot reach β€” links are signed for the public host. If presigning fails the registry streams instead.
  • Streamed downloads resume too. Without the redirect, a seekable blob (the filesystem store) is served with Range/If-Range support; each file's ETag is its sha256, so a resumed download is never spliced across two different builds.
  • Every download is checkable and counted. Both paths send X-Checksum-Sha256 and count the download against the version (GET only; HEAD is free).

Anything in front of the registry must allow the same sizes: a reverse proxy body cap or a CDN upload limit (Cloudflare's proxied plans cap request bodies at 100–500 MB) will reject a large upload before the registry sees it.