Appearance
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
| Method | Path | Operation ID | ReBAC gate | Audit relation | Outbox event | Body cap |
|---|---|---|---|---|---|---|
| GET | /v1/artifacts/plexd | ListPlexdArtifacts | platform:plexsphere#read (BEFORE the persistence read) | plexd_artifact.list | (none) | n/a |
| GET | /v1/artifacts/plexd/{version} | GetPlexdArtifact | platform:plexsphere#read (BEFORE the persistence read) | plexd_artifact.get | (none) | n/a |
| GET | /v1/artifacts/plexd/{version}/sigstore | GetPlexdArtifactSignature | platform: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#readbefore the persistence read. The object is the single platform-global registry, not a per-versionplexd_artifact:<version>tuple — the registry is one shared catalogue, not a per-release aggregate, so a single platform-widereadgrant authorises the whole surface. - The two per-version reads return 404, not 403, on denial. For
GetPlexdArtifactandGetPlexdArtifactSignaturean authorisation denial maps to404 release_not_found, not403. The platform-global registry deliberately withholds the existence side-channel: an unauthorised caller and a caller asking for an unindexed version receive the same404, so neither can distinguish "you may not see this" from "this does not exist". The OpenAPI contract for these two operations declares only200 / 401 / 404 / 500— there is no403. The denial is still recorded: an audit row is emitted with outcomepermission_denied. - The list operation paginates and returns 403 on denial.
ListPlexdArtifacts(GET /v1/artifacts/plexd) is cursor-paginated: alimitquery parameter clamped to[1, 200](default50) and an opaquecursorcontinuation token. Unlike the per-version reads it exposes no per-version existence side-channel, so an authorisation denial returns403with thePermissionDeniedbody its OpenAPI operation declares — not the404fold. The cursor is bound to the presenting caller: a malformed token surfaces as400 invalid_cursorand a token minted for a different caller as403 cursor_binding_mismatch. Every outcome emits one audit row under relationplexd_artifact.list. - Malformed version is also a 404. A malformed or empty
{version}that reaches the handler maps to404 release_not_foundas well. The chi route only matches a non-empty path segment, so an empty version is unreachable through the router; the contract declares no400/422for these operations, so a malformed version stays inside the declared response set as a404. .sigstorehas no architecture selector. The registry caches one Sigstore bundle per architecture, but the.sigstorepath carries neither an?archquery 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 as404 release_not_found. A future revision may add an explicit?archselector; until then the canonical bundle is the documented default.- Success streams verbatim bytes.
GetPlexdArtifactSignaturereturns the cached bundle byte-for-byte asapplication/octet-stream, so an operator can pipe it intocosign verify-blobor asigstore-goverifier against the pinned release-signing identity and confirm the build provenance without trusting this server. - Checksum encoding. The JSON
checksumis the raw 32-byte SHA-256 digest of the build's plexd binary, hex-encoded as 64 lowercase hex characters.
Path, header & query parameters
| Operation | Parameter | Type | Required | Notes |
|---|---|---|---|---|
| per-version reads | version (path) | string | yes | Upstream 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. |
| all | Authorization (header) | string | yes | Bearer <access token> — the operator bearer credential. Missing, malformed, or rejected → 401 unauthenticated. The shared Authorization parameter component. |
ListPlexdArtifacts | cursor (query) | string | no | Opaque 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. |
ListPlexdArtifacts | limit (query) | integer | no | Page 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— theGetPlexdArtifact200 response. Carriesversion(string,minLength 1),support_status(closed enum),verdict(closed enum), andarchitectures(array,minItems 1ofPlexdArtifactArch; no two builds share an architecture). The verbatim Sigstore bundle is not inlined — fetch it from the{version}/sigstorecompanion endpoint.PlexdArtifactArch— one per-architecture build. Carriesarchitecture(string,minLength 1) andchecksum(string, pattern^[0-9a-f]{64}$).architectureis deliberately not a closed enum: the upstream release pipeline owns the architecture set, not plexsphere, so the registry does not constrain it.checksumis the raw 32-byte SHA-256 digest hex-encoded as 64 lowercase hex characters.PlexdArtifactRefList— theListPlexdArtifacts200 response. Carriesitems(array ofPlexdArtifactRef— the indexed releases in the current page) and an optionalnext_cursor(string; absent ornullon the last page).PlexdArtifactRef— one row of a list page. Carries the sameversion,support_status,verdict, andarchitectures(array ofPlexdArtifactArch) asPlexdArtifact, pluscreated_at/updated_attimestamps recording when the registry first indexed and last refreshed the release. LikePlexdArtifact, it does not inline the Sigstore bundle.GetPlexdArtifactSignature200 response —application/octet-streambinary: 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— thereadcheck failed; the caller saw a404, but the probe is recorded.internal_error— an unexpected server-side failure on the read path; the caller saw a500.
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.
| Code | Status | Where | Meaning |
|---|---|---|---|
unauthenticated | 401 | all | Request carries no authenticated principal. |
invalid_cursor | 400 | ListPlexdArtifacts | Malformed or undecodable pagination cursor. |
cursor_binding_mismatch | 403 | ListPlexdArtifacts | Cursor was minted for a different caller than the one presenting it. |
release_not_found | 404 | per-version reads | The 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_provisioned | 501 | all | The composition root has not wired the read dependency bundle (Reader / Authz nil); the dispatch shim fails closed. |
internal | 500 | all | Unexpected 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; theListPlexdArtifacts/GetPlexdArtifact/GetPlexdArtifactSignatureoperations and thePlexdArtifactRefList/PlexdArtifactRef/PlexdArtifact/PlexdArtifactArchschemas plus the sharedArtifactVersion/Authorizationparameters.../../../internal/transport/http/v1/artifacts/— the transport-tier implementation: the three read handlers, the closedProblem.codetaxonomy, 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 sameplatform:plexsphereplatform singleton../authz.md— the extendedPermissionDeniedshape used by surfaces that return403(this surface does not).