Skip to content

Blueprint catalog sources HTTP API

This is the reference for the /v1/blueprint-catalogs 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 surface is the operator-facing front end of the externalised Blueprint Catalog — the aggregates, provenance, scope model, and import semantics behind it are documented in ../../contexts/provisioning/blueprints.md.

The surface mirrors /v1/blueprints and ships through the same 501-stub dispatch seam: until the composition root wires the dependency bundle, every operation answers 501 not-provisioned.

Operations

MethodPathOperation IDReBAC gateAudit relationBody cap
POST/v1/blueprint-catalogsRegisterCatalogSourcescope managecatalog_source.register (service-emitted)64 KiB
GET/v1/blueprint-catalogsListCatalogSourcesper-row scope read filtercatalog_source.list (granted, with post-filter item_count)n/a
GET/v1/blueprint-catalogs/{id}GetCatalogSourcescope read (AFTER persistence read)catalog_source.getn/a
DELETE/v1/blueprint-catalogs/{id}DeregisterCatalogSourcescope managecatalog_source.deregister (service-emitted)n/a
GET/v1/blueprint-catalogs/{id}/blueprintsBrowseCatalogSourcescope readcatalog_source.browsen/a
POST/v1/blueprint-catalogs/{id}/importImportFromCatalogSourcescope manageblueprint.import (service-emitted per Blueprint)64 KiB
  • body_cap = 64 KiB (defaultBodyLimit in internal/transport/http/v1/blueprint_catalogs/wiring.go) is enforced before the JSON decoder runs on the write paths; an over-cap body surfaces as 413 request_body_too_large.
  • ListCatalogSources returns the whole (small, operator-curated) set — there is no pagination. It layers a per-row scope read ReBAC check on top, so items is the subset the caller is authorised to see. A transport-level authz error (not a denial) drops the row closed and is counted in the audit row's catalog_source.list.authz_errors caveat.

ReBAC scope model

Catalog sources are operator configuration. There is no dedicated catalog_source ReBAC object — the gate targets the source's scope parent:

Source scopemanage gateread gate
Catalog-global (no domain_id)platform#manage (platform:plexsphere)platform#read
Domain-scoped (domain_id set)domain#manage (domain:<id>)domain#read

RegisterCatalogSource derives the scope from the request body's domain_id and authorises before the service persists anything, so an unauthorised caller never appends a CatalogSourceRegistered outbox row. The body must be decoded first to learn the scope, but the value objects are parsed only after the gate passes.

The by-id operations (Get, Deregister, Browse, Import) resolve the source before the authz check, because a source's scope lives on the persisted row and cannot be known from the URL. This is a load-then-check flow: an unauthorised caller can distinguish a missing source (404) from a forbidden one (403). The trade-off is accepted as low-sensitivity (catalog sources are operator config; a platform admin holds access to every scope) and is recorded as a DECISION: block in get.go.

Path & query parameters

OperationParameterTypeRequiredNotes
Get / Deregister / Browse / Importid (path)string (uuid)yesUUIDv7. Non-zero. Malformed → 400 invalid_catalog_source_id. The CatalogSourceID parameter component is shared across the four by-id operations.

The catalog-source surface takes no query parameters — the list is returned whole.

Schemas

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

  • Request: CatalogSourceCreateRequest, ImportRequest.
  • Response: CatalogSourceResponse (single), CatalogSourceList (whole set), CatalogBrowseResponse, ImportResult.
  • Embedded: OciReference (registry + repository + exactly one of tag or digest), VerificationPolicy (pinned carries identity_san + issuer, unsigned carries neither), TrackingPolicy (track-tag carries a positive interval_seconds and requires a tag-pinned reference, pinned carries no interval), CatalogBlueprintSummary (one per offered Blueprint), ImportOutcome (one per requested slug).

The CatalogSourceResponse carries the resolved id, name, oci_reference, verification, tracking, the optional credential_ref (namespace/name) and domain_id, the last_resolved_digest (or null before the first resolution), status (active / disabled), and the created_at / updated_at lifecycle timestamps. The shape is shared by RegisterCatalogSource, ListCatalogSources, and GetCatalogSource.

Import semantics

ImportFromCatalogSource requires the body to set exactly one of a non-empty slugs subset or all: true; supplying neither, both, or an empty / blank-entried slugs array surfaces as 400 invalid_import_request.

Import is per-Blueprint: the 200 response renders one ImportOutcome per requested slug rather than aborting the whole batch on the first failure. Only a whole-batch precondition failure — the source not existing (404), or the upstream registry failing to enumerate the bundle on an all import (502) — returns an error response.

Outcome statusMeaningCarries
importedA new Blueprint or version was published.version
unchangedAn identical re-import.version
driftThe same version label with changed content (a diagnostic, never an overwrite).version, detail (the diff)
conflictThe slug is owned in this scope by a different source.version, reason: blueprint_slug_conflict
errorA fetch, signature, verification, conversion, or publish failure.reason

The error reason is catalog_artifact_not_found when the bundle did not offer the slug, and the generic import_failed otherwise — the raw cause is logged server-side and never echoed on the wire.

Error taxonomy

All error responses use the shared Problem envelope (application/problem+json). The 403 path uses the richer PermissionDenied shape carrying the ReBAC denial reason, relation_path, and request correlation_id.

CodeStatusWhereMeaning
invalid_catalog_source_id400by-id familyMalformed UUID or zero UUID.
invalid_catalog_source400RegisterAggregate invariant rejected the body (malformed name or status).
invalid_oci_reference400Registeroci_reference was malformed (bad registry/repository, or not exactly one of tag/digest).
invalid_verification_policy400Registerverification policy was malformed.
invalid_tracking_policy400Registertracking policy was malformed (e.g. track-tag over a digest-pinned reference).
invalid_credential_ref400Registercredential_ref was not a valid namespace/name.
invalid_import_request400ImportBody set neither, both, or an empty/blank slugs/all.
invalid_body400Register / ImportBody could not be read or did not parse as the typed request shape.
unauthenticated401every operationRequest carries no authenticated principal.
catalog_source_not_found404by-id family / RegisterNo catalog source with the given {id} (or, on Register, the referenced Domain scope does not exist).
catalog_artifact_not_found404BrowseThe source's upstream OCI artifact did not resolve.
request_body_too_large413Register / ImportBody exceeded the 64 KiB ceiling.
catalog_upstream_error502BrowseThe upstream registry was unreachable or returned a malformed bundle.
authz_unavailable503every gated operationThe authorization backend was temporarily unavailable.
internal500every operationServer-side failure path.
blueprint_catalogs_not_provisioned501every operationThe in-package dependency bundle was half-wired (non-production builds only).

A 403 response carries Problem.code = permission_denied plus the extended PermissionDenied fields documented in ./authz.md. The dispatch-level 501 stub — answered before the bundle is wired at all — carries the shared not-provisioned code.

Cross-references

  • ../../../api/openapi/plexsphere-v1.yaml — OpenAPI 3.1 spec; the *CatalogSource* operations and the CatalogSourceCreateRequest / CatalogSourceResponse / CatalogSourceList / CatalogBrowseResponse / ImportRequest / ImportResult schemas.
  • ../../../internal/transport/http/v1/blueprint_catalogs/ — the transport-tier implementation: the six handlers, the closed Problem.code taxonomy, the body-cap constant, the per-row visibility filter on ListCatalogSources, and the load-then-check ordering.
  • ../../../schema/authz.zed — ReBAC schema; the platform and domain definitions declare the manage / read permissions this surface gates on.
  • ./blueprints.md — the sibling Blueprint Catalog read + authorship surface the imported Blueprints land in.
  • ../../contexts/provisioning/blueprints.md — the bounded-context reference for the CatalogSource aggregate, import provenance, scope model, and import flow.