# 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
```

```sh
# 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
```

```json
{
  "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.
