# Packages API

> Reference for the read-only /api/pkgs JSON endpoints, the /pkgs markdown mirrors, the /llms.txt index, and the /pkgs/advisories.xml feed.

Source: https://minimal.dev/docs/reference/api

The `/api/pkgs/*` namespace is a read-only agent surface over the
minimal.dev package catalog. It exists for AI agents, integrations, and
`curl`-driven exploration — the webapp itself renders the same data
server-side, and both paths flow through one compose pipeline so they
cannot drift.

Every endpoint is `GET`. The floats-latest half is anonymous and
identity-invariant — no tokens, no cookies, the same body for every
caller — while the seven sha-pinned endpoints require a signed-in session
and answer `401` without one. This page is the human-facing companion to the machine-readable
[OpenAPI 3.1 document](#the-openapi-document) at `/api/pkgs/openapi.json`
and the [`/llms.txt`](#llmstxt-agent-index) agent index; the endpoint
inventory below is drift-guarded against that OpenAPI document so the two
can't diverge.

The same catalog is also exposed as typed tools by the hosted
[MCP server](https://minimal.dev/docs/reference/mcp) at `/api/mcp` — agents that speak the Model
Context Protocol can attach it by URL instead of calling these endpoints
directly.

## Conventions

**Base URL.** All paths are relative to `https://minimal.dev`.

**Authentication.** A browser session cookie, and only for the seven
sha-pinned endpoints under `/api/pkgs/bundle/{sha}/…`. There are no API
tokens yet, so a non-browser client authenticates by signing in at
`/auth/login` and replaying the session cookie. Everything else is
anonymous — see [Auth and caching contract](#auth-and-caching-contract).

**`schemaVersion`.** Every JSON payload carries a `schemaVersion` field.
Compare it against the major version of the OpenAPI document before
trusting a cached body; a bump signals a breaking change to the payload
shape.

**Cache classes.** Responses fall into two families:

- **Sha-pinned, authenticated** — URLs under `/api/pkgs/bundle/{sha}/…`
  are content-addressed by commit sha, so the body for a given URL never
  changes, but they are [auth-gated](#json-endpoints) and therefore
  uncacheable: `Cache-Control: private, no-store`. Each still carries a
  weak `ETag` that identifies the body, so a client keeping its own copy
  can tell that copy is still current; no endpoint answers a conditional
  `GET`.
- **Floats-latest** — URLs without a sha resolve the latest
  `gominimal/pkgs:main` commit at request time (`Cache-Control: public,
  max-age=60, s-maxage=3600, stale-while-revalidate=86400`).

**Path parameters.**

| Parameter | Grammar | On mismatch |
|-----------|---------|-------------|
| `{name}` | Package name: a leading alphanumeric, then alphanumerics plus `.`, `_`, `-` (case-insensitive). | `404` |
| `{sha}`, `{base}` | Canonical 40-hex lowercase commit sha on `gominimal/pkgs:main`. | `404` unknown/malformed |

Short sha prefixes (7–39 hex chars) are accepted on the sha-pinned
endpoints: a unique prefix `301`-redirects to the canonical 40-hex URL
(query params preserved), an ambiguous prefix returns `400
{"error":"ambiguous sha"}`, and an unknown or malformed sha returns
`404`. The redirect is cached `public, max-age=3600` for prefixes ≥12
hex chars and `private, no-store` for shorter ones.

## JSON endpoints

Fourteen endpoints make up the surface. The thirteen below are described by the
OpenAPI document; the fourteenth is the document itself
([`/api/pkgs/openapi.json`](#the-openapi-document)).

**The seven sha-pinned endpoints require a signed-in session.** Everything
under `/api/pkgs/bundle/{sha}/…` answers `401` with a
`{"error":"auth_required","message":…}` body unless the request carries a
session cookie; sign in at `/auth/login` to get one. No `WWW-Authenticate`
challenge is sent — the credential is a cookie, not an HTTP auth scheme.
The gate sits after sha resolution, so short shas still canonicalize exactly
as described above (`301`/`400`/`404` are unchanged for every caller) and
only the payload needs the session. Both the `401` and the authenticated
`200` are `private, no-store`. The floats-latest endpoints are unaffected,
and the [MCP server](https://minimal.dev/docs/reference/mcp) cannot present a session yet, so its
sha-addressed calls return an error result.

| Endpoint | Purpose | Cache class |
|----------|---------|-------------|
| `GET /api/pkgs/{name}.json` | Full per-package record at the latest commit | Floats-latest |
| `GET /api/pkgs/{name}/sbom.json` | SBOM at the latest commit | Floats-latest |
| `GET /api/pkgs/{name}/badge.svg` | Score/advisory SVG badge for READMEs | Floats-latest |
| `GET /api/pkgs/{name}/attestation.json` | Build attestations per architecture at the latest commit | Floats-latest |
| `GET /api/pkgs/recent-commits.json` | Last N commits on `pkgs:main` | Floats-latest |
| `GET /api/pkgs/bundle/latest.json` | Redirect to the current listing bundle (target needs a session) | 302 alias |
| `GET /api/pkgs/bundle/{sha}.json` | Listing-tier bundle at a sha | Sha-pinned, authenticated |
| `GET /api/pkgs/bundle/{sha}/full.json` | Full-tier bundle at a sha | Sha-pinned, authenticated |
| `GET /api/pkgs/bundle/{sha}/diff/{base}.json` | Catalog diff between two commits | Sha-pinned, authenticated |
| `GET /api/pkgs/bundle/{sha}/{name}/deps.json` | Direct-dependency rollup at a sha | Sha-pinned, authenticated |
| `GET /api/pkgs/bundle/{sha}/{name}/versions.json` | Version-history timeline at a sha | Sha-pinned, authenticated |
| `GET /api/pkgs/bundle/{sha}/{name}/sbom.json` | Reproducible sha-pinned SBOM | Sha-pinned, authenticated |
| `GET /api/pkgs/bundle/{sha}/{name}/attestation.json` | Build attestations for what that commit shipped | Sha-pinned, authenticated |

### `GET /api/pkgs/{name}.json`

Full composed record for one package — description, dependencies,
archive, full advisory payloads, and OpenSSF scorecard — wrapped in the
`{schemaVersion, commitSha, pushedAt, package}` envelope. Floats with the
latest `gominimal/pkgs:main` commit (the `commitSha` is resolved at
request time), so it is byte-identical to the `<script id="pkgs-detail">`
inline envelope on `/pkgs/{name}`. `scorecard.source` says who ran the
scan — `depsdev` and `openssf-api` are OpenSSF's own published result
(via deps.dev or OpenSSF's API), `ossf-self` is Minimal
running the OpenSSF Scorecard tool on a repository OpenSSF does not scan —
and `scorecard.repository` names the GitHub repository that was scored,
which is not always the package's own. `scorecard.overall` is OpenSSF
Scorecard's 0–10 aggregate and `scorecard.checks` holds each check's 0–10
score exactly as the tool reports it (`-1` means not applicable or
inconclusive). `scorecard.date` is when the scan ran; scorecard.dev may
already hold a newer one. Nothing is regrouped or rescaled. The `transitiveAdvisories` rollup is
omitted here — it is bundle-level. The endpoint that carries it,
[`/api/pkgs/bundle/{sha}/full.json`](#get-apipkgsbundleshafulljson),
[requires a signed-in session](#json-endpoints).

`404` on an unknown package name or a charset-violating segment.

```bash
curl -s https://minimal.dev/api/pkgs/jq.json | jq .package.name
```

### `GET /api/pkgs/{name}/sbom.json`

Software bill of materials with the transitive build + runtime closure
and a `vulnerabilities` block, for agents that want "the SBOM as of now".
The default format is CycloneDX 1.5
(`application/vnd.cyclonedx+json`); `?format=spdx` returns SPDX 2.3 JSON
(`application/spdx+json`). Any other `format` value — including omitting
it — resolves to the CycloneDX default, and the endpoint never `400`s;
the response `Content-Type` reflects the resolved format. Floats with the
latest commit. The sha-pinned sibling that gives a reproducible
point-in-time SBOM,
[`/api/pkgs/bundle/{sha}/{name}/sbom.json`](#get-apipkgsbundleshanamesbomjson),
[requires a signed-in session](#json-endpoints); this floating one is the
anonymous route.

`404` on an unknown package name or a charset-violating segment.

### `GET /api/pkgs/{name}/badge.svg`

A shields-style flat badge (`image/svg+xml`) for embedding in a README,
wiki, or runbook:

```markdown
![openssf scorecard](https://minimal.dev/api/pkgs/openssl/badge.svg)
```

The default `metric=score` renders the package's OpenSSF Scorecard
aggregate on its own 0–10 scale (`5.6`, as OpenSSF's badge would show),
labelled `openssf scorecard` and colored by Minimal's band for the
aggregate (red below 4, yellow to 7.9, green from 8); `?metric=advisories` renders
the active direct-advisory count, colored by max active severity. Any
other `metric` value — including omitting it — resolves to `score`, and
the endpoint never `400`s. Composed through the same data path as
[`/api/pkgs/{name}.json`](#get-apipkgsnamejson), so the badge numbers
never drift from the JSON record. Floats with the latest commit.

`404` on an unknown package name or a charset-violating segment.

### `GET /api/pkgs/{name}/attestation.json`

Build provenance for the artifacts the catalog ships, per architecture
(`amd64`, `arm64`). Every artifact is addressable from the catalog commit
through a per-commit package map; an artifact that a build run produced
fresh also carries a signed attestation (a DSSE envelope around an in-toto
Statement v1 in the SLSA provenance v1 format). The endpoint joins on that
map, so the answer describes exactly the artifact `commitSha` ships.

```bash
curl -s https://minimal.dev/api/pkgs/jq/attestation.json | jq '.arches | map_values(.state)'
```

Top level: `schemaVersion`, `commitSha`, `pushedAt`, `name`, `map`,
`catalogVerification` and `arches.{amd64,arm64}`.

| `map` | Meaning |
|-------|---------|
| `present` | The commit has a package map. The map itself never changes, and a per-arch `state` read from it is a settled fact — except `unavailable` (that envelope read failed and is retried on a later request) and `invalid` (a verdict about the envelope object's bytes, which a replaced upload can change). |
| `pending` | No map yet, and the commit is less than a day old. |
| `none-recorded` | No map, and the commit is a day old or more. Not permanent: a late map still turns this into `present`, which is why the answer is never cached for long. |
| `unavailable` | The map could not be read. Nothing is claimed either way. |

| Per-arch `state` | Meaning |
|------------------|---------|
| `verified` | A signed attestation exists, every signature checked out against the keys minimal.dev holds, and its subject matches the map's artifact. |
| `signed-unchecked` | Signed, none invalid, but at least one signature could not be checked (`reason`: `unknown-key` or `runtime-unsupported`). **This state does not imply that any signature was verified** — an envelope whose only signature is from a key minimal.dev does not hold lands here too. Read `signatures[].result` to see what was actually checked. |
| `invalid` | A signature failed, or the statement names a different artifact, package or architecture than the map. `statement` is `null`. |
| `unsigned` | Only the unsigned statement exists. |
| `not-attested` | The artifact is in the map and has no attestation. Permanent for that build: attestations are written only when a run builds the artifact fresh. |
| `not-in-map` | The package is not built for this architecture at this commit. |
| `no-map` | `map` is `pending` or `none-recorded`. |
| `unavailable` | The attestation store could not be read, or did not answer within the server's 5-second deadline. Nothing is claimed either way. |

The state reflects the verifier this deployment runs: which public keys it
holds and which algorithms its runtime supports. `signed-unchecked` can become
`verified` after a key or algorithm is added, without any change upstream.

`unavailable` is transient by design. The first request for a commit the
server has not read yet can time out against a slow upstream while the reads
complete in the background, so retrying after a few seconds is the right
response; the answer is sent `private, no-store` so nothing caches it.

One failure is outside that shape: if the catalog itself cannot be read —
the endpoint cannot resolve which commit is latest — there is no commit to
report a state for, so the endpoint answers `404` with `private, no-store`,
the same status an unknown package name gets. Retry rather than concluding
the package does not exist; the MCP `get_attestation` tool reports this case
as a temporary error instead.

Each arch carries `artifactSha256` and `artifactUrl` (the durable
identifiers), `specHash` and `envelopeUrl` (point-in-time: spec hashes
rotate), the parsed `statement` — subject, source commit, builder and its
revision, run id and times, `triggeredBy`, `resolvedDependencies` — and a
`signatures` array of `{keyid, algorithm, result, reason}`. Each dependency
carries `sha256Source`: `"statement"` when the attestation itself names the
built artifact, `"map"` when the attestation carried only the spec hash (a
dependency built in the same run) and the digest was resolved from the same
commit's package map under that spec hash, and `null` with `sha256: null`
when neither knows it. The statement's
`source.commit` is the commit that last rebuilt the package, which is
usually older than `commitSha`.

`catalogVerification` is non-null for commits whose closure catalog carries
Minimal's production Sigstore signature. It names the catalog, its bundle
and the certificate identity to pass to `cosign verify-blob`. That check
covers the catalog for the commit, not this package's artifact.

Signature checks are done by minimal.dev against public keys it pins;
independent verification of the attestation signatures is not published
yet.

`404` on an unknown package name or a charset-violating segment. Cached
floats-latest when the answer is final, `public, max-age=60, s-maxage=300`
while `map` is `pending` or an arch reads `invalid` — a verdict about the
envelope's bytes, which a replaced upload can change — and `private,
no-store` whenever anything is `unavailable`.

### `GET /api/pkgs/recent-commits.json`

The last N distinct commits on `gominimal/pkgs:main`, newest-first, each
with its first-line commit subject (`message` is `null` when
unrecoverable). Powers the "view at sha" picker. The `?limit=` query
param defaults to 30 and is clamped into `[1, 50]`; non-numeric values
fall back to the default.

The endpoint degrades gracefully: an upstream error **or** a genuinely
empty history responds `200 []` with `Cache-Control: no-store` (never a
`5xx`), so an empty list is indistinguishable from a transient failure by
design. A non-empty success carries the floats-latest cache class.

### `GET /api/pkgs/bundle/latest.json`

`302`-redirects to `/api/pkgs/bundle/{sha}.json` for the latest
`gominimal/pkgs:main` commit. The short-cached alias (`Cache-Control:
public, max-age=60`) keeps edge storage bounded — follow the `Location`
header to fetch the bundle itself. `404` when vulndb has no commits
(preview env, fresh deploy).

The alias itself is anonymous, but its target is one of the seven auth-gated
sha-pinned endpoints, so following the `Location` without a session answers
`401`.

### `GET /api/pkgs/bundle/{sha}.json`

> **Requires a signed-in session** — returns `401`
> `{"error":"auth_required"}` without one. See
> [JSON endpoints](#json-endpoints).

Card-only projection of every package at the given commit — name,
version, categories, stars, slim advisories — plus the derived
`allCategories`. Byte-identical to the `<script id="pkgs-bundle">` inline
payload on `/pkgs/search`. For full per-package records at the same sha use
[`/api/pkgs/bundle/{sha}/full.json`](#get-apipkgsbundleshafulljson).

### `GET /api/pkgs/bundle/{sha}/full.json`

> **Requires a signed-in session** — returns `401`
> `{"error":"auth_required"}` without one. See
> [JSON endpoints](#json-endpoints).

Every composed package record at the given commit — description,
dependencies, archive, full advisory payloads, and the BFS-derived
`transitiveAdvisories` rollup that single-package compose paths can't
compute. One request for everything at a snapshot instead of `1 + N`
round-trips (listing bundle + per-package).

Each `transitiveAdvisories` entry is a de-duped
`{ advisoryId, culpritPath }` reference rather than an embedded advisory
object — at catalog scale one leaf culprit fans out across hundreds of
dependants, so the unique advisory objects ship once in the response's
top-level `advisories` map (advisory id → full advisory object). Resolve
`advisoryId` through that map to reconstitute the advisory; `culpritPath`
stays per entry. When one advisory id affects multiple catalog packages
directly, the resolved object's `affectedPackage` reflects an arbitrary
(first-seen, alphabetically-first) affected package and need not match
`culpritPath.at(-1)`; read the culprit's own affected version from that
culprit's entry in the `packages` array. Per-package direct `advisories`
are unaffected.

### `GET /api/pkgs/bundle/{sha}/diff/{base}.json`

> **Requires a signed-in session** — returns `401`
> `{"error":"auth_required"}` without one. See
> [JSON endpoints](#json-endpoints).

What changed in the catalog **from** `base` (the older commit) **to**
`sha` (the newer commit): packages added and removed, version upgrades,
and advisory deltas (count changes and severity transitions, with
`fromSeverity`). This is the JSON twin of the `/pkgs/changes` HTML page —
both project through the same `diffCatalog` helper, so they can't drift.
The `(sha, base)` pair is content-addressed.

### `GET /api/pkgs/bundle/{sha}/{name}/deps.json`

> **Requires a signed-in session** — returns `401`
> `{"error":"auth_required"}` without one. See
> [JSON endpoints](#json-endpoints).

The focal package's **direct** build + runtime dependencies as of the
given commit, each enriched with the dependency's own version, license,
and active-advisory rollup at that sha. Content-addressed by
`(sha, name)`. `404` when the sha is unknown, a name segment violates the
charset, or the package has no lock-state row at this sha (unknown name,
or one that didn't exist yet at this commit).

### `GET /api/pkgs/bundle/{sha}/{name}/versions.json`

> **Requires a signed-in session** — returns `401`
> `{"error":"auth_required"}` without one. See
> [JSON endpoints](#json-endpoints).

Every version the package has shipped up to (and including) the given
commit, newest-first, with per-version file/line diff stats and
dependency name sets. Content-addressed by `(sha, name)`. `404` on an
unknown sha, a charset-violating name segment, or a name with no history
at this sha.

### `GET /api/pkgs/bundle/{sha}/{name}/sbom.json`

> **Requires a signed-in session** — returns `401`
> `{"error":"auth_required"}` without one. See
> [JSON endpoints](#json-endpoints).

The reproducible, sha-pinned variant of
[`/api/pkgs/{name}/sbom.json`](#get-apipkgsnamesbomjson). Default format
is CycloneDX 1.5; `?format=spdx` returns SPDX 2.3 JSON. `(sha, name,
format)` is content-addressed — the ETag carries the format suffix so
conditional GETs can't cross-talk between formats.

### `GET /api/pkgs/bundle/{sha}/{name}/attestation.json`

> **Requires a signed-in session** — see [JSON endpoints](#json-endpoints).

The sha-pinned variant of
[`/api/pkgs/{name}/attestation.json`](#get-apipkgsnameattestationjson): the
same payload, answered from the package map of `{sha}`, so it describes the
artifact that commit shipped rather than the latest one. A commit from before
package maps were recorded answers `map: "none-recorded"`.

`404` on an unknown sha, a charset-violating name segment, or a package that
commit does not hold. Short or uppercase shas `301` to the canonical URL.

## The OpenAPI document

`GET /api/pkgs/openapi.json` serves an OpenAPI 3.1 description of every
endpoint above. It is the machine-readable source of truth: the
`components.schemas` are generated from the same Zod wire schemas the
endpoints serve, and a drift-guard test asserts the documented path set
equals the route files under `src/pages/api/pkgs/`, so the document
cannot fall out of step with the code. Its `info.version` tracks the wire
`schemaVersion` (a major bump means a breaking change to every payload).

The document is prerendered — a pure function of the code with no data
dependencies — and cached `public, max-age=300, s-maxage=3600,
stale-while-revalidate=86400`.

```bash
curl -s https://minimal.dev/api/pkgs/openapi.json | jq '.info.version, (.paths | keys)'
```

## Other agent surfaces

The following surfaces are **not** described by the OpenAPI document
(which covers only `/api/pkgs`). They are documented here for
completeness because they serve the same agent and crawler audience.

### Markdown mirrors (`/pkgs/{name}.md`)

Every package detail page has a plain-text markdown mirror at
`/pkgs/{name}.md` for machine consumers that prefer text over HTML. The
HTML detail page advertises it via `<link rel="alternate"
type="text/markdown">`, and `/llms.txt` documents the pattern.
Package names may contain dots, so `llama.cpp` mirrors at
`/pkgs/llama.cpp.md`. Prerendered and served as a static file.

### `/llms.txt` agent index

The site-wide agent index at `/llms.txt` follows the
[llmstxt.org](https://llmstxt.org) format: it links the main pages, the
read-only pkgs JSON API, and the markdown mirrors so AI agents and
crawlers can discover the surface. `/sitemap.xml` enumerates every
package name, which is the anonymous entry point for expanding the
`{name}` placeholder and crawling the per-package endpoints.

### Advisory feed (`/pkgs/advisories.xml`)

An Atom 1.0 feed of the catalog's direct security advisories,
newest-first. Point a feed reader (or a Slack RSS app) here to see new
advisories affecting the curated catalog without polling the JSON
endpoints. Unlike the sha-pinned bundles it floats with the latest
lock-state — subscribers see new advisories within the cache window
(`public, max-age=60, s-maxage=3600, stale-while-revalidate=86400`)
rather than waiting for a redeploy.

## Auth and caching contract

Every surface on this page is read-only. The floats-latest endpoints, the
`/pkgs/*` markdown mirrors and feed, and `/llms.txt` are anonymous and safe
to cache: they are exempt from the auth middleware's `no-store` rewrite that
would otherwise apply to a signed-in request, so a logged-in caller receives
the same cacheable response as an anonymous one.

The seven sha-pinned endpoints are the exception. They are auth-gated, so both
the `401` and the authenticated `200` are `private, no-store` — a shared
cache holding either would let a stored body be served past the gate. Their
canonicalization arms answer every caller and keep their own posture: a
`404` on a well-formed-but-unknown sha takes the short edge negative cache,
and a `301` from a prefix of ≥12 hex chars stays `public, max-age=3600`.
Other error responses (`4xx`) and short sha-prefix redirects carry `private,
no-store`.

| Surface | Cache-Control |
|---------|---------------|
| `/api/pkgs/bundle/{sha}.json`, `/full.json`, `/diff/{base}.json`, `/{name}/deps.json`, `/{name}/versions.json`, `/{name}/sbom.json`, `/{name}/attestation.json` — [auth-gated](#json-endpoints) | `private, no-store` on both the `401` and the authenticated `200` |
| `/api/pkgs/{name}.json`, `/api/pkgs/{name}/sbom.json`, `/api/pkgs/{name}/badge.svg`, `/api/pkgs/recent-commits.json` | `public, max-age=60, s-maxage=3600, stale-while-revalidate=86400` |
| `/api/pkgs/{name}/attestation.json` | As the row above when the answer is final; `public, max-age=60, s-maxage=300` while the commit's map is `pending` or an arch reads `invalid`; `private, no-store` when anything is `unavailable` |
| `/api/pkgs/bundle/latest.json` (redirect) | `public, max-age=60` |
| `/api/pkgs/openapi.json` | `public, max-age=300, s-maxage=3600, stale-while-revalidate=86400` |
| `/pkgs/advisories.xml` | `public, max-age=60, s-maxage=3600, stale-while-revalidate=86400` |
| `/api/pkgs/*` `4xx` and short-prefix redirects | `private, no-store` (short prefix) or `public, max-age=3600` (≥12 hex) |
