Skip to content

Artifact Registry HTTP API

This is the reference for the read-only plexd release registry HTTP surface. It maps each operation to its OpenAPI schema, ReBAC gate, audit emission, and the closed Problem.code taxonomy. The wire-contract origin is api/openapi/plexsphere-v1.yaml; this doc is a map, not a duplicate contract.

The registry is an index over the upstream plexd OCI release stream: indexing happens out of band, and the HTTP surface is purely read. It adds exactly three operations — list the indexed releases (cursor-paginated), fetch one indexed release line by version, and fetch the verbatim Sigstore bundle attesting that release so an operator can re-verify the supply-chain attestation independently. There is no create, update, or delete surface here, and the read side leaves no domain-event trail: a single audit row per read is the only signal the call emits. For the why behind the registry — how releases are indexed, verified, and lifecycled — see the bounded-context reference ../../contexts/artifacts.md.

All three operations gate on a single platform-global ReBAC object, platform:plexsphere, relation read, mirroring the Node-listing surface's platform singleton; see ./nodes.md for that sibling. The gate runs before the persistence read. The two per-version reads fold an authorisation denial into a 404 so an unauthorised caller never learns whether a specific version is indexed; the list operation exposes no per-version existence side-channel and returns a plain 403 instead (its OpenAPI operation declares a 403 response).

Operations

MethodPathOperation IDReBAC gateAudit relationOutbox eventBody cap
GET/v1/artifacts/plexdListPlexdArtifactsplatform:plexsphere#read (BEFORE the persistence read)plexd_artifact.list(none)n/a
GET/v1/artifacts/plexd/{version}GetPlexdArtifactplatform:plexsphere#read (BEFORE the persistence read)plexd_artifact.get(none)n/a
GET/v1/artifacts/plexd/{version}/sigstoreGetPlexdArtifactSignatureplatform:plexsphere#read (BEFORE the persistence read)plexd_artifact.signature(none)n/a
  • Authentication first. A request that carries no authenticated principal surfaces as 401 unauthenticated, checked before any persistence read so an unauthenticated probe never reaches the catalogue. Neither operation accepts a request body.
  • Authorisation against the platform singleton. Both handlers resolve platform:plexsphere#read before the persistence read. The object is the single platform-global registry, not a per-version plexd_artifact:<version> tuple — the registry is one shared catalogue, not a per-release aggregate, so a single platform-wide read grant authorises the whole surface.
  • The two per-version reads return 404, not 403, on denial. For GetPlexdArtifact and GetPlexdArtifactSignature an authorisation denial maps to 404 release_not_found, not 403. The platform-global registry deliberately withholds the existence side-channel: an unauthorised caller and a caller asking for an unindexed version receive the same 404, so neither can distinguish "you may not see this" from "this does not exist". The OpenAPI contract for these two operations declares only 200 / 401 / 404 / 500 — there is no 403. The denial is still recorded: an audit row is emitted with outcome permission_denied.
  • The list operation paginates and returns 403 on denial.ListPlexdArtifacts (GET /v1/artifacts/plexd) is cursor-paginated: a limit query parameter clamped to [1, 200] (default 50) and an opaque cursor continuation token. Unlike the per-version reads it exposes no per-version existence side-channel, so an authorisation denial returns 403 with the PermissionDenied body its OpenAPI operation declares — not the 404 fold. The cursor is bound to the presenting caller: a malformed token surfaces as 400 invalid_cursor and a token minted for a different caller as 403 cursor_binding_mismatch. Every outcome emits one audit row under relation plexd_artifact.list.
  • Malformed version is also a 404. A malformed or empty {version} that reaches the handler maps to 404 release_not_found as well. The chi route only matches a non-empty path segment, so an empty version is unreachable through the router; the contract declares no 400/422 for these operations, so a malformed version stays inside the declared response set as a 404.
  • .sigstore has no architecture selector. The registry caches one Sigstore bundle per architecture, but the .sigstore path carries neither an ?arch query nor an architecture path segment. It serves the bundle of the lexicographically smallest architecture present in the release. This is the deterministic canonical choice — selecting the sorted-minimum architecture makes the served bundle stable regardless of how the persistence layer ordered the rows. A release that carries no cached bundle surfaces as 404 release_not_found. A future revision may add an explicit ?arch selector; until then the canonical bundle is the documented default.
  • Success streams verbatim bytes. GetPlexdArtifactSignature returns the cached bundle byte-for-byte as application/octet-stream, so an operator can pipe it into cosign verify-blob or a sigstore-go verifier against the pinned release-signing identity and confirm the build provenance without trusting this server.
  • Checksum encoding. The JSON checksum is the raw 32-byte SHA-256 digest of the build's plexd binary, hex-encoded as 64 lowercase hex characters.

Path, header & query parameters

OperationParameterTypeRequiredNotes
per-version readsversion (path)stringyesUpstream plexd release tag the registry indexes (for example a semver tag such as v1.4.2). The shared ArtifactVersion parameter component, bound on /v1/artifacts/plexd/{version} and its {version}/sigstore companion.
allAuthorization (header)stringyesBearer <access token> — the operator bearer credential. Missing, malformed, or rejected → 401 unauthenticated. The shared Authorization parameter component.
ListPlexdArtifactscursor (query)stringnoOpaque continuation token from a prior page's next_cursor. Bound to the presenting caller: a malformed token → 400 invalid_cursor, a token minted for a different caller → 403 cursor_binding_mismatch.
ListPlexdArtifactslimit (query)integernoPage size, clamped to [1, 200]; defaults to 50.

Schemas

The OpenAPI spec is the authoritative source for field shapes. The schemas this surface uses are:

  • PlexdArtifact — the GetPlexdArtifact 200 response. Carries version (string, minLength 1), support_status (closed enum), verdict (closed enum), and architectures (array, minItems 1 of PlexdArtifactArch; no two builds share an architecture). The verbatim Sigstore bundle is not inlined — fetch it from the {version}/sigstore companion endpoint.
  • PlexdArtifactArch — one per-architecture build. Carries architecture (string, minLength 1) and checksum (string, pattern ^[0-9a-f]{64}$). architecture is deliberately not a closed enum: the upstream release pipeline owns the architecture set, not plexsphere, so the registry does not constrain it. checksum is the raw 32-byte SHA-256 digest hex-encoded as 64 lowercase hex characters.
  • PlexdArtifactRefList — the ListPlexdArtifacts 200 response. Carries items (array of PlexdArtifactRef — the indexed releases in the current page) and an optional next_cursor (string; absent or null on the last page).
  • PlexdArtifactRef — one row of a list page. Carries the same version, support_status, verdict, and architectures (array of PlexdArtifactArch) as PlexdArtifact, plus created_at / updated_at timestamps recording when the registry first indexed and last refreshed the release. Like PlexdArtifact, it does not inline the Sigstore bundle.
  • GetPlexdArtifactSignature 200 responseapplication/octet-stream binary: the verbatim cached Sigstore bundle bytes, served byte-for-byte.

support_status is a closed set describing the release line's lifecycle: supported is a fully supported, eligible upgrade target; deprecated still verifies and is served but operators should migrate off it; withdrawn is no longer a valid upgrade target. verdict is a closed set describing the supply-chain gate's pronouncement: verified means every artifact passed the Sigstore bundle and checksum verification gate, and unverified marks a record that never passed the gate and is therefore never served as an eligible upgrade target.

ReBAC and audit

All three operations gate on the single platform-global ReBAC object platform:plexsphere, relation read. The object is the canonical platform singleton — the same object the Node-listing surface stamps as its audit anchor (see ./nodes.md) — because the registry is one shared catalogue rather than a per-version aggregate. The check runs before the persistence read. For the two per-version reads an unauthorised caller never observes whether a given version is indexed — the denial folds into a 404 release_not_found; the list operation returns 403 with a PermissionDenied body instead.

Each read emits exactly one audit row through a transport-local sink, stamping the resolved principal as Subject, platform:plexsphere as Object, the per-operation relation (plexd_artifact.list, plexd_artifact.get, or plexd_artifact.signature), and one of three outcomes:

  • granted — the read was authorised and served.
  • permission_denied — the read check failed; the caller saw a 404, but the probe is recorded.
  • internal_error — an unexpected server-side failure on the read path; the caller saw a 500.

The audit row is observability, not a security control on the read path: a nil sink degrades silently and a sink error is logged but never fails the request. The two per-version reads carry no 403 and therefore no PermissionDenied envelope; ListPlexdArtifacts does return 403 with the PermissionDenied shape on an authorisation denial — see ./authz.md for that envelope.

Error taxonomy

Most error responses use the shared Problem envelope (application/problem+json) with a populated code. The one exception is the ListPlexdArtifacts authorisation denial, which uses the PermissionDenied envelope (403; see ReBAC and audit above) rather than a Problem code.

CodeStatusWhereMeaning
unauthenticated401allRequest carries no authenticated principal.
invalid_cursor400ListPlexdArtifactsMalformed or undecodable pagination cursor.
cursor_binding_mismatch403ListPlexdArtifactsCursor was minted for a different caller than the one presenting it.
release_not_found404per-version readsThe version is not indexed, OR (for .sigstore) the cached Sigstore bundle is absent, OR the caller lacks read on the registry (no existence side-channel), OR a malformed version reached the handler.
artifacts_not_provisioned501allThe composition root has not wired the read dependency bundle (Reader / Authz nil); the dispatch shim fails closed.
internal500allUnexpected server-side failure; the wire body stays generic — raw driver errors are never echoed back.

The release_not_found code is deliberately overloaded across four distinct causes so the registry never leaks which one applies. The artifacts_not_provisioned 501 is a composition-root wiring guard, not a per-request condition: it fires only while the surface is mounted but its Reader/Authz dependencies are still nil, so an unwired surface fails closed rather than panicking.

Cross-references

  • ../../../api/openapi/plexsphere-v1.yaml — OpenAPI 3.1 spec; the ListPlexdArtifacts / GetPlexdArtifact / GetPlexdArtifactSignature operations and the PlexdArtifactRefList / PlexdArtifactRef / PlexdArtifact / PlexdArtifactArch schemas plus the shared ArtifactVersion / Authorization parameters.
  • ../../../internal/transport/http/v1/artifacts/ — the transport-tier implementation: the three read handlers, the closed Problem.code taxonomy, the cursor-bound pagination, the canonical-architecture bundle selection, the audit-first refusal path, and the platform-global ReBAC gate.
  • ../../contexts/artifacts.md — the Artifact Registry bounded-context reference that explains how releases are indexed, verified, and lifecycled behind this surface.
  • ./nodes.md — the sibling surface that gates and audits against the same platform:plexsphere platform singleton.
  • ./authz.md — the extended PermissionDenied shape used by surfaces that return 403 (this surface does not).