Appearance
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
| Method | Path | Operation ID | ReBAC gate | Audit relation | Body cap |
|---|---|---|---|---|---|
| POST | /v1/blueprint-catalogs | RegisterCatalogSource | scope manage | catalog_source.register (service-emitted) | 64 KiB |
| GET | /v1/blueprint-catalogs | ListCatalogSources | per-row scope read filter | catalog_source.list (granted, with post-filter item_count) | n/a |
| GET | /v1/blueprint-catalogs/{id} | GetCatalogSource | scope read (AFTER persistence read) | catalog_source.get | n/a |
| DELETE | /v1/blueprint-catalogs/{id} | DeregisterCatalogSource | scope manage | catalog_source.deregister (service-emitted) | n/a |
| GET | /v1/blueprint-catalogs/{id}/blueprints | BrowseCatalogSource | scope read | catalog_source.browse | n/a |
| POST | /v1/blueprint-catalogs/{id}/import | ImportFromCatalogSource | scope manage | blueprint.import (service-emitted per Blueprint) | 64 KiB |
body_cap = 64 KiB(defaultBodyLimitininternal/transport/http/v1/blueprint_catalogs/wiring.go) is enforced before the JSON decoder runs on the write paths; an over-cap body surfaces as413 request_body_too_large.ListCatalogSourcesreturns the whole (small, operator-curated) set — there is no pagination. It layers a per-row scopereadReBAC check on top, soitemsis 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'scatalog_source.list.authz_errorscaveat.
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 scope | manage gate | read 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
| Operation | Parameter | Type | Required | Notes |
|---|---|---|---|---|
| Get / Deregister / Browse / Import | id (path) | string (uuid) | yes | UUIDv7. 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 oftagordigest),VerificationPolicy(pinnedcarriesidentity_san+issuer,unsignedcarries neither),TrackingPolicy(track-tagcarries a positiveinterval_secondsand requires a tag-pinned reference,pinnedcarries 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 status | Meaning | Carries |
|---|---|---|
imported | A new Blueprint or version was published. | version |
unchanged | An identical re-import. | version |
drift | The same version label with changed content (a diagnostic, never an overwrite). | version, detail (the diff) |
conflict | The slug is owned in this scope by a different source. | version, reason: blueprint_slug_conflict |
error | A 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.
| Code | Status | Where | Meaning |
|---|---|---|---|
invalid_catalog_source_id | 400 | by-id family | Malformed UUID or zero UUID. |
invalid_catalog_source | 400 | Register | Aggregate invariant rejected the body (malformed name or status). |
invalid_oci_reference | 400 | Register | oci_reference was malformed (bad registry/repository, or not exactly one of tag/digest). |
invalid_verification_policy | 400 | Register | verification policy was malformed. |
invalid_tracking_policy | 400 | Register | tracking policy was malformed (e.g. track-tag over a digest-pinned reference). |
invalid_credential_ref | 400 | Register | credential_ref was not a valid namespace/name. |
invalid_import_request | 400 | Import | Body set neither, both, or an empty/blank slugs/all. |
invalid_body | 400 | Register / Import | Body could not be read or did not parse as the typed request shape. |
unauthenticated | 401 | every operation | Request carries no authenticated principal. |
catalog_source_not_found | 404 | by-id family / Register | No catalog source with the given {id} (or, on Register, the referenced Domain scope does not exist). |
catalog_artifact_not_found | 404 | Browse | The source's upstream OCI artifact did not resolve. |
request_body_too_large | 413 | Register / Import | Body exceeded the 64 KiB ceiling. |
catalog_upstream_error | 502 | Browse | The upstream registry was unreachable or returned a malformed bundle. |
authz_unavailable | 503 | every gated operation | The authorization backend was temporarily unavailable. |
internal | 500 | every operation | Server-side failure path. |
blueprint_catalogs_not_provisioned | 501 | every operation | The 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 theCatalogSourceCreateRequest/CatalogSourceResponse/CatalogSourceList/CatalogBrowseResponse/ImportRequest/ImportResultschemas.../../../internal/transport/http/v1/blueprint_catalogs/— the transport-tier implementation: the six handlers, the closedProblem.codetaxonomy, the body-cap constant, the per-row visibility filter onListCatalogSources, and the load-then-check ordering.../../../schema/authz.zed— ReBAC schema; theplatformanddomaindefinitions declare themanage/readpermissions 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.