Skip to content

Catalog API

List Catalog

Backs both the Store grid (always entry_type=ct, decision: non-LXC entries never appear there) and, unfiltered, the full catalog table every discovered entry lands in regardless of type.

Both surfaces are real, which is why the variant exclusion below hangs off the entry_type=ct filter and not off the query as a whole: the grid must not show 28 blank duplicate cards, and the full catalog table must still account for every row discovery created.

sort is one of _SORTS: name (default), popularity, newest, updated. Anything else falls back to the default rather than erroring, because the Store rendering in the wrong order is a far better failure than the Store not rendering; the value never reaches SQL either way.

Ordering here is about CORRECTNESS, not paging: the frontend fetches every ct row and slices client side, so this decides which rows the user sees first, not which rows they receive.

Parameters

NameInTypeRequiredDescription
categoryquerystring | nullno
qquerystring | nullno
entry_typequerystring | nullno
sortquerystringno

Responses

StatusDescriptionSchema
200Successful Responseany
422Validation ErrorHTTPValidationError

Get Catalog Entry

Parameters

NameInTypeRequiredDescription
slugpathstringyes

Responses

StatusDescriptionSchema
200Successful Responseany
422Validation ErrorHTTPValidationError

Get Catalog Icon

The locally mirrored icon, so the Store renders with no network.

MUST stay registered above /{slug}: Starlette matches in registration order, and while a one-segment template cannot swallow a two-segment path today, the ordering rule this file already documents around /status is cheaper to follow than to re-derive.

PATH TRAVERSAL, closed twice over, because the slug arrives from the URL. First, the slug is never used to build a path: it is an exact-match DB lookup, and the filename comes from the ROW (icon_cache_path), which the sync wrote from our own slug plus a fixed extension allowlist. A slug of ../../etc/passwd matches no row and 404s before touching the filesystem. Second, the resolved path is required to sit inside the cache dir before it is opened, so even a corrupted column cannot escape. Belt and braces on purpose: this route reads files off disk on behalf of an HTTP caller, and that is worth two locks rather than one.

Parameters

NameInTypeRequiredDescription
slugpathstringyes

Responses

StatusDescriptionSchema
200Successful Responseany
422Validation ErrorHTTPValidationError

Install Catalog Entry

Parameters

NameInTypeRequiredDescription
slugpathstringyes

Request body (required), application/json, InstallIn

FieldTypeRequiredDescription
consentbooleanno
ctidinteger | nullno
host_idintegeryes
namestringyes
overridesobjectno

Responses

StatusDescriptionSchema
202Successful Responseany
422Validation ErrorHTTPValidationError

Refresh Catalog

Responses

StatusDescriptionSchema
202Successful Responseany

Catalog Status

RBAC: viewer. Entitlement: store.catalog.

How old the catalog cache is, for doc 01’s staleness indicator.

MUST stay registered above /{slug}: Starlette matches in registration order, so declaring it after would make this a lookup for a catalog entry named “status” and 404 forever (same trap api/apps.py documents around its lifecycle wildcard).

A separate route rather than a field on GET /catalog because that route returns a bare list and wrapping it now would break every existing caller for a banner.

Staleness is a real signal, not decoration: the catalog is refreshed by a system schedule, so a stale cache means that schedule is off or has been failing, and every install decision the operator makes is being taken against pinned scripts that upstream may have moved past.

A catalog that has never synced counts as stale. The threshold is the catalog_stale_after_s setting (default 48 hours).

Responses

StatusDescriptionSchema
200Successful Responseany