Catalog API
GET /api/v1/catalog
Section titled “GET /api/v1/catalog”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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
category | query | string | null | no | |
q | query | string | null | no | |
entry_type | query | string | null | no | |
sort | query | string | no |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | Successful Response | any |
| 422 | Validation Error | HTTPValidationError |
GET /api/v1/catalog/{slug}
Section titled “GET /api/v1/catalog/{slug}”Get Catalog Entry
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
slug | path | string | yes |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | Successful Response | any |
| 422 | Validation Error | HTTPValidationError |
GET /api/v1/catalog/{slug}/icon
Section titled “GET /api/v1/catalog/{slug}/icon”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
| Name | In | Type | Required | Description |
|---|---|---|---|---|
slug | path | string | yes |
Responses
| Status | Description | Schema |
|---|---|---|
| 200 | Successful Response | any |
| 422 | Validation Error | HTTPValidationError |
POST /api/v1/catalog/{slug}/install
Section titled “POST /api/v1/catalog/{slug}/install”Install Catalog Entry
Parameters
| Name | In | Type | Required | Description |
|---|---|---|---|---|
slug | path | string | yes |
Request body (required), application/json, InstallIn
| Field | Type | Required | Description |
|---|---|---|---|
consent | boolean | no | |
ctid | integer | null | no | |
host_id | integer | yes | |
name | string | yes | |
overrides | object | no |
Responses
| Status | Description | Schema |
|---|---|---|
| 202 | Successful Response | any |
| 422 | Validation Error | HTTPValidationError |
POST /api/v1/catalog/refresh
Section titled “POST /api/v1/catalog/refresh”Refresh Catalog
Responses
| Status | Description | Schema |
|---|---|---|
| 202 | Successful Response | any |
GET /api/v1/catalog/status
Section titled “GET /api/v1/catalog/status”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
| Status | Description | Schema |
|---|---|---|
| 200 | Successful Response | any |