go-bananas/openapi.yaml

2692 lines
75 KiB
YAML
Raw Normal View History

2026-10-07 15:11:35 +02:00
openapi: 3.1.0
info:
title: go-bananas
version: 1.0.0
summary: Runtime design-token service.
description: 'Author design-token themes and serve them to any application at run time.
## Two surfaces, two postures
`/rt/v1` is the **runtime** API: public, aggressively cacheable, and designed to be consumed from
a `<link>` tag with no credential. A theming service that required a credential could not be consumed
the way a theme is actually consumed.
`/api/v1` is the **administrative** API: authenticated, never cached, and every mutation audited.
## Versioning
Both surfaces are independently versioned. Adding an optional field, a new endpoint or a new query
parameter is not a breaking change. Changing the meaning of an existing one is, and gets a new major
version served alongside the old one for at least one cycle.'
license:
name: MIT
identifier: MIT
contact:
name: urpi platform team
url: https://github.com/urpi/go-bananas
servers:
- url: '{origin}'
description: This deployment.
variables:
origin:
default: https://themes.example.com
description: The externally reachable origin.
tags:
- name: runtime
description: Public. Consumed by applications and browsers.
- name: themes
description: Theme management. Requires authentication.
- name: history
description: Version history, diffing and rollback.
- name: tokens
description: Read-only analysis of a token document.
- name: auth
description: Session introspection and the login flow.
- name: preferences
description: The caller's own interface preferences. Any authenticated role.
- name: admin
description: Keys, users, settings and the audit log. Admin role.
- name: operational
description: Probes and metrics.
paths:
/rt/v1/theme.css:
get:
summary: A theme stylesheet
description: 'Returns the theme rendered as CSS custom properties, plus a small component layer
in `@layer bananas`, so an application''s own unlayered CSS always wins the cascade.
Responses are **immutable by URL**: publishing a theme changes its version, and a version-pinned
URL never changes. That lets a CDN cache them hard. `If-None-Match` and `If-Modified-Since` are
both honoured, so a redeploy costs a revalidation and no body.
A draft theme returns **404 rather than 403**, so a probe cannot confirm that it exists.'
tags:
- runtime
responses:
'200':
description: The stylesheet.
content:
text/css:
schema:
type: string
example: "[data-bananas-theme=\"midnight\"] {\n --bn-color-semantic-primary: #7c6bf5;\n\
}"
headers: &id001
ETag:
description: A strong validator for the response.
schema:
type: string
X-Theme-Version:
description: The version served.
schema:
type: string
X-Theme-Updated-At:
description: When it was published.
schema:
type: string
Cache-Control:
description: The caching policy applied.
schema:
type: string
X-Request-ID:
description: Correlation id for the server log.
schema:
type: string
'400':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'404':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
parameters:
- name: slug
in: query
required: true
description: The theme slug.
schema:
type: string
- name: appearance
in: query
required: false
description: The colour scheme. Defaults to the theme's own default, which is what makes a dark-first
theme correct without the parameter being passed.
schema:
type: string
enum:
- light
- dark
- name: variant
in: query
required: false
description: '`full` includes the component layer, `tokens` omits it, `minified` strips comments
for production.'
schema:
type: string
enum:
- full
- tokens
- minified
- name: v
in: query
required: false
description: 'Pin a version. This URL never changes, which is what makes a safe deploy possible:
build against version 7 and the render cannot change underneath you.'
schema:
type: integer
minimum: 1
security: []
/rt/v1/theme.json:
get:
summary: A theme's resolved tokens
description: Carries both a flat custom-property map and the nested DTCG document, so a consumer
can read either without a transformation step. Derived tokens such as the `on-*` foreground pairs
and the `-hover` / `-active` interaction states are included.
tags:
- runtime
responses:
'200':
description: The resolved theme.
content:
application/json:
schema:
$ref: '#/components/schemas/ResolvedTheme'
headers: *id001
'400':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'404':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
parameters:
- name: slug
in: query
required: true
description: The theme slug.
schema:
type: string
- name: appearance
in: query
required: false
description: The colour scheme. Defaults to the theme's default.
schema:
type: string
enum:
- light
- dark
security: []
/rt/v1/manifest.json:
get:
summary: The theme manifest
description: 'The discovery document a loader fetches once to build a theme picker. Deliberately
small: no token data, so a page with a picker does not download every theme. Cached briefly, because
it changes whenever a theme is published.'
tags:
- runtime
responses:
'200':
description: The manifest.
content:
application/json:
schema:
$ref: '#/components/schemas/Manifest'
headers:
Cache-Control:
description: Short-lived.
schema:
type: string
'404':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
security: []
/rt/v1/embed:
get:
summary: An integration snippet
description: 'Generates the markup to paste, from this deployment''s own configuration, so it cannot
drift from reality. Always answers JSON, whatever the target: the admin UI fetches this with a
JSON parser and an HTML response for the default target is what made the Integrate page report
that it could not parse the reply. The slug must name an existing theme, and every interpolated
value is escaped for the target language, because the output is pasted into someone else''s application.'
tags:
- runtime
responses:
'200':
description: The snippet.
content:
application/json:
schema:
$ref: '#/components/schemas/EmbedSnippet'
'400':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'404':
description: No such theme.
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
parameters:
- name: slug
in: query
required: false
description: The theme to generate for.
schema:
type: string
- name: target
in: query
required: false
description: The integration shape.
schema:
type: string
enum:
- html
- react
- javascript
security: []
/rt/v1/themes/{slug}/theme.css:
get:
summary: A theme stylesheet, by path
description: A REST-shaped equivalent of `/rt/v1/theme.css`, for consumers that prefer a path to
a query string, or a CDN configured with path patterns.
tags:
- runtime
responses:
'200':
description: The stylesheet.
content:
text/css:
schema:
type: string
headers: *id001
'400':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'404':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
parameters:
- name: slug
in: path
required: true
description: The theme slug.
schema:
type: string
security: []
/rt/v1/themes/{slug}/theme.json:
get:
summary: A theme's resolved tokens, by path
description: A REST-shaped equivalent of `/rt/v1/theme.json`.
tags:
- runtime
responses:
'200':
description: The resolved theme.
content:
application/json:
schema:
$ref: '#/components/schemas/ResolvedTheme'
headers: *id001
'404':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
parameters:
- name: slug
in: path
required: true
description: The theme slug.
schema:
type: string
security: []
/rt/v1/runtime.js:
get:
summary: The runtime.js bundle
description: 'The framework-free ES module: `applyTheme`, `setAppearance`, `loadManifest`, `getVariables`,
`watchSystemAppearance`, `subscribe`.
Served from this origin so a consuming application needs no build step and no npm dependency.
React is imported as a **bare specifier** and resolved by the consumer''s own import map, which
is what guarantees a single React instance in the page: two copies would each have their own hook
dispatcher.'
tags:
- runtime
responses:
'200':
description: The bundle.
content:
application/javascript:
schema:
type: string
headers:
Cache-Control:
description: Immutable. The filename is stable so an import map can name it.
schema:
type: string
security: []
/rt/v1/react.js:
get:
summary: The react.js bundle
description: 'The React provider and hooks: `BananasProvider`, `useBananas`, `useThemeVariable`,
`useThemeVariables`, `useAvailableThemes`.
Served from this origin so a consuming application needs no build step and no npm dependency.
React is imported as a **bare specifier** and resolved by the consumer''s own import map, which
is what guarantees a single React instance in the page: two copies would each have their own hook
dispatcher.'
tags:
- runtime
responses:
'200':
description: The bundle.
content:
application/javascript:
schema:
type: string
headers:
Cache-Control:
description: Immutable. The filename is stable so an import map can name it.
schema:
type: string
security: []
/rt/v1/global.js:
get:
summary: The global.js bundle
description: 'The same API attached to `window.Bananas`, for a page that cannot use modules.
Served from this origin so a consuming application needs no build step and no npm dependency.
React is imported as a **bare specifier** and resolved by the consumer''s own import map, which
is what guarantees a single React instance in the page: two copies would each have their own hook
dispatcher.'
tags:
- runtime
responses:
'200':
description: The bundle.
content:
application/javascript:
schema:
type: string
headers:
Cache-Control:
description: Immutable. The filename is stable so an import map can name it.
schema:
type: string
security: []
/rt/v1/embed.js:
get:
summary: The embed.js bundle
description: 'The one-line script-tag embed. Configured with data attributes.
Served from this origin so a consuming application needs no build step and no npm dependency.
React is imported as a **bare specifier** and resolved by the consumer''s own import map, which
is what guarantees a single React instance in the page: two copies would each have their own hook
dispatcher.'
tags:
- runtime
responses:
'200':
description: The bundle.
content:
application/javascript:
schema:
type: string
headers:
Cache-Control:
description: Immutable. The filename is stable so an import map can name it.
schema:
type: string
security: []
/auth/login:
get:
summary: Begin the login flow
description: Redirects to the identity provider. The return path and the PKCE verifier are carried
in a signed cookie rather than server-side state, so any replica behind the load balancer can
complete the flow.
tags:
- auth
responses:
'302':
description: Redirect to the provider.
'404':
description: No identity provider is configured.
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
parameters:
- name: returnTo
in: query
required: false
description: 'A **local** path to return to. An absolute URL is rejected: it would be an open
redirect that also primes a phishing flow.'
schema:
type: string
security: []
/auth/callback:
get:
summary: Complete the login flow
description: Exchanges the authorization code, verifies the ID token signature, issuer, audience
and nonce, resolves roles from the user's groups, and sets the session cookie. The transaction
state is single-use.
tags:
- auth
responses:
'302':
description: Redirect to the return path, with a session cookie set.
'401':
description: The login could not be completed. The message names the reason, because "invalid
state" and "user declined" need different actions.
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'502':
description: The provider returned an unexpected response.
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
security: []
/auth/logout:
get:
summary: End the session
description: Clears the session cookie and, when an OIDC provider is configured, ends the provider
session too.
tags:
- auth
responses:
'302':
description: Redirect, to the provider when configured.
security:
- sessionCookie: []
- bearerToken: []
post:
summary: End the session
description: Clears the session cookie and, when an OIDC provider is configured, ends the provider
session too.
tags:
- auth
responses:
'302':
description: Redirect, to the provider when configured.
security:
- sessionCookie: []
- bearerToken: []
/api/v1/auth/me:
get:
summary: The current session
description: The session, and what the caller may do.
tags:
- auth
responses:
'200':
description: The session.
content:
application/json:
schema:
$ref: '#/components/schemas/Session'
'401':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
security: []
/api/v1/auth/config:
get:
summary: Client-visible configuration
description: What the UI needs before a session exists. Contains no secret material.
tags:
- auth
responses:
'200':
description: The configuration.
content:
application/json:
schema:
$ref: '#/components/schemas/AppConfig'
'401':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
security: []
/api/v1/me/preferences:
get:
summary: The caller's interface preferences
description: Empty when nothing is stored, which is every account until it chooses once.
tags:
- preferences
responses:
'200':
description: The stored preference.
content:
application/json:
schema:
$ref: '#/components/schemas/UserPreferences'
'401':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
put:
summary: Record the caller's interface preferences
description: 'Any authenticated role may call this. The preference is keyed to the caller''s own
subject and grants nothing. An empty selectedTheme clears the choice. The response is authoritative:
the server rejects a theme that is not published, and reports a stored theme that has since disappeared
as no choice. tags: - preferences requestBody: required: true content: application/json: schema:
$ref: '
responses:
'200':
description: What the server stored.
content:
application/json:
schema:
$ref: '#/components/schemas/UserPreferences'
'400':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'401':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
delete:
summary: Forget the caller's preferences
description: Returns the interface to its defaults.
tags:
- preferences
responses:
'200':
description: The empty preference.
content:
application/json:
schema:
$ref: '#/components/schemas/UserPreferences'
'401':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
/api/v1/themes:
get:
summary: List themes
description: A page of themes, without their token documents.
tags:
- themes
responses:
'200':
description: A page of themes.
content:
application/json:
schema:
$ref: '#/components/schemas/ThemePage'
headers: &id002
Cache-Control: &id003
description: '`no-store`. Administration responses are per-user and change on every write.'
schema:
type: string
X-Request-ID: &id004
description: Correlation id for the server log.
schema:
type: string
'400':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'401':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'403':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
parameters:
- name: q
in: query
required: false
description: Matches slug, name or description. LIKE wildcards are escaped, so a literal `%` matches
a literal `%`.
schema:
type: string
- name: status
in: query
required: false
description: Filter by publication state.
schema: &id005
$ref: '#/components/schemas/ThemeStatus'
- name: system
in: query
required: false
description: Filter on the built-in flag.
schema:
type: boolean
- name: tag
in: query
required: false
description: Requires the tag in the metadata.
schema:
type: string
- name: limit
in: query
required: false
description: ''
schema:
type: integer
minimum: 1
maximum: 200
default: 50
- name: offset
in: query
required: false
description: ''
schema:
type: integer
minimum: 0
default: 0
- name: sort
in: query
required: false
description: Resolved through an allow-list. An unrecognised value falls back to the default rather
than reaching the SQL statement.
schema:
type: string
enum:
- updated
- created
- name
- slug
default: updated
- name: order
in: query
required: false
description: ''
schema:
type: string
enum:
- asc
- desc
default: desc
security:
- sessionCookie: []
- bearerToken: []
post:
summary: Create a theme
description: 'Creates a **draft**. A draft may be incomplete: validation gates publication, not
editing, which is what makes building a theme up incrementally practical.'
tags:
- themes
responses:
'201':
description: The created theme.
content:
application/json:
schema:
$ref: '#/components/schemas/Theme'
headers: *id002
'400':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'401':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'403':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'409':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ThemeMutation'
example:
name: Acme Product
slug: acme
description: The default theme for customer-facing applications
status: draft
tokens:
color:
semantic:
primary:
$type: color
$value: '#4f46e5'
security:
- sessionCookie: []
- bearerToken: []
/api/v1/themes/{slug}:
get:
summary: Get a theme
description: The theme with its full token document.
tags:
- themes
responses:
'200':
description: The theme.
content:
application/json:
schema:
$ref: '#/components/schemas/Theme'
headers: *id002
'401':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'404':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
parameters:
- name: slug
in: path
required: true
description: The theme slug.
schema:
type: string
security:
- sessionCookie: []
- bearerToken: []
patch:
summary: Update a theme
description: 'A partial update: absent fields are left unchanged. Every write bumps the version,
because the version is the render cache key and must change whenever the rendered output could.
`baseVersion` gives optimistic concurrency. Without it, two editors silently overwrite each other;
with it, the loser gets a 409 naming both versions.'
tags:
- themes
responses:
'200':
description: The updated theme.
content:
application/json:
schema:
$ref: '#/components/schemas/Theme'
headers: *id002
'400':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'403':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'404':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'409':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
parameters:
- name: slug
in: path
required: true
description: The theme slug.
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ThemeMutation'
example:
tokens:
color:
semantic:
primary:
$type: color
$value: '#7c6bf5'
baseVersion: 3
message: rebalanced the neutrals
security:
- sessionCookie: []
- bearerToken: []
delete:
summary: Delete a theme
description: 'Removes the theme and its history. Refused for a built-in theme with 409: built-ins
are immutable so that a consumer pinned to the slug cannot break on upgrade. Duplicate it instead.'
tags:
- themes
responses:
'204':
description: Deleted.
'401':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'403':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'404':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'409':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
parameters:
- name: slug
in: path
required: true
description: The theme slug.
schema:
type: string
security:
- sessionCookie: []
- bearerToken: []
/api/v1/themes/{slug}/publish:
post:
summary: Publish a theme
description: 'Validates the token document first: errors block, warnings do not. On success the
theme is served to every consuming application within one `s-maxage` window. A built-in theme
can be published but not deleted.'
tags:
- themes
responses:
'200':
description: The published theme.
content:
application/json:
schema:
$ref: '#/components/schemas/Theme'
headers: *id002
'400':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'404':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
parameters:
- name: slug
in: path
required: true
description: The theme slug.
schema:
type: string
security:
- sessionCookie: []
- bearerToken: []
/api/v1/themes/{slug}/archive:
post:
summary: Archive a theme
description: Hidden from listings and from the runtime manifest. The last published revision stays
resolvable for a pinned consumer, so archiving does not break a deployment in progress.
tags:
- themes
responses:
'200':
description: The archived theme.
content:
application/json:
schema:
$ref: '#/components/schemas/Theme'
headers: *id002
'404':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
parameters:
- name: slug
in: path
required: true
description: The theme slug.
schema:
type: string
security:
- sessionCookie: []
- bearerToken: []
/api/v1/themes/{slug}/duplicate:
post:
summary: Duplicate a theme
description: Copies a theme under a new slug as a draft, with its own history starting at version
1. The copy is no longer flagged as built-in, so it can then be deleted.
tags:
- themes
responses:
'201':
description: The copy.
content:
application/json:
schema:
$ref: '#/components/schemas/Theme'
headers: *id002
'400':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'404':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'409':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
parameters:
- name: slug
in: path
required: true
description: The source theme slug.
schema:
type: string
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- slug
properties:
slug:
type: string
description: The slug for the copy.
example:
slug: acme-v2
security:
- sessionCookie: []
- bearerToken: []
/api/v1/themes/{slug}/export:
get:
summary: Export a theme
description: A portable document. Import and export are exact inverses, so migrating a theme between
environments is a file copy rather than a project.
tags:
- themes
responses:
'200':
description: The export.
content:
application/json:
schema:
$ref: '#/components/schemas/ThemeExport'
headers:
Cache-Control: *id003
X-Request-ID: *id004
Content-Disposition:
description: A download filename.
schema:
type: string
'404':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
parameters:
- name: slug
in: path
required: true
description: The theme slug.
schema:
type: string
security:
- sessionCookie: []
- bearerToken: []
/api/v1/themes/import:
post:
summary: Import a theme
description: Creates or replaces a theme from a portable document. The document is validated before
anything is written, so a malformed import never reaches the database.
tags:
- themes
responses:
'201':
description: The imported theme.
content:
application/json:
schema:
$ref: '#/components/schemas/Theme'
headers: *id002
'400':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'404':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'409':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/ImportRequest'
example:
slug: acme
name: Acme
asDraft: true
tokens: {}
security:
- sessionCookie: []
- bearerToken: []
/api/v1/themes/{slug}/versions:
get:
summary: A theme's history
description: 'Summaries newest first, without token documents: a hundred revisions of a hundred-token
theme would otherwise be a very large response for no benefit.'
tags:
- history
responses:
'200':
description: The history.
content:
application/json:
schema:
type: object
required:
- items
properties:
items:
type: array
items:
$ref: '#/components/schemas/ThemeVersion'
headers: *id002
'404':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
parameters:
- name: slug
in: path
required: true
description: The theme slug.
schema:
type: string
- name: limit
in: query
required: false
description: ''
schema:
type: integer
minimum: 1
maximum: 200
default: 50
security:
- sessionCookie: []
- bearerToken: []
/api/v1/themes/{slug}/versions/{version}:
get:
summary: A historical snapshot
description: One immutable revision, including its token document.
tags:
- history
responses:
'200':
description: The snapshot.
content:
application/json:
schema:
$ref: '#/components/schemas/ThemeVersion'
headers: *id002
'400':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'404':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
parameters:
- name: slug
in: path
required: true
description: The theme slug.
schema:
type: string
- name: version
in: path
required: true
description: The revision number.
schema:
type: integer
minimum: 1
security:
- sessionCookie: []
- bearerToken: []
/api/v1/themes/{slug}/versions/{version}/rollback:
post:
summary: Roll back to a revision
description: Restores a snapshot as a **new** version rather than rewinding. History is append-only,
so an application deployed against version 7 can never have version 7 taken away from it.
tags:
- history
responses:
'200':
description: The theme after rollback.
content:
application/json:
schema:
$ref: '#/components/schemas/Theme'
headers: *id002
'404':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
parameters:
- name: slug
in: path
required: true
description: The theme slug.
schema:
type: string
- name: version
in: path
required: true
description: The revision to restore.
schema:
type: integer
minimum: 1
security:
- sessionCookie: []
- bearerToken: []
/api/v1/themes/{slug}/diff:
get:
summary: Diff two revisions
description: A token-level comparison. Only the token tree is compared; a metadata change is reported
as a boolean rather than field by field.
tags:
- history
responses:
'200':
description: The differences.
content:
application/json:
schema:
$ref: '#/components/schemas/VersionDiff'
headers: *id002
'400':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'404':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
parameters:
- name: slug
in: path
required: true
description: The theme slug.
schema:
type: string
- name: from
in: query
required: true
description: The base revision.
schema:
type: integer
minimum: 1
- name: to
in: query
required: true
description: The comparison revision.
schema:
type: integer
minimum: 1
security:
- sessionCookie: []
- bearerToken: []
/api/v1/tokens/validate:
post:
summary: Validate a token document
description: Runs the engine's validator without persisting anything. The editor calls it as the
user works, which is exactly why it must not write.
tags:
- tokens
responses:
'200':
description: The findings.
content:
application/json:
schema:
$ref: '#/components/schemas/ValidationResult'
headers: *id002
'400':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- tokens
properties:
tokens: &id006
$ref: '#/components/schemas/TokenDocument'
example:
tokens:
color:
semantic:
primary:
$type: color
$value: '#4f46e5'
security:
- sessionCookie: []
- bearerToken: []
/api/v1/tokens/contrast:
get:
summary: Measure WCAG contrast
description: Reports the ratio and the highest conformance level it satisfies. The editor uses this
to flag an unreadable combination before it is saved rather than after it is published.
tags:
- tokens
responses:
'200':
description: The measurement.
content:
application/json:
schema:
$ref: '#/components/schemas/ContrastResult'
headers: *id002
'400':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
parameters:
- name: fg
in: query
required: true
description: The foreground colour.
schema:
type: string
- name: bg
in: query
required: true
description: The background colour.
schema:
type: string
security:
- sessionCookie: []
- bearerToken: []
/api/v1/tokens/palette:
get:
summary: Generate a colour ramp
description: Produces the canonical 11-step ramp for a hue. Exposed so the editor's palette builder
and the built-in themes cannot diverge from the engine's ramp curve.
tags:
- tokens
responses:
'200':
description: The ramp.
content:
application/json:
schema:
$ref: '#/components/schemas/RampResponse'
headers: *id002
'400':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
parameters:
- name: hue
in: query
required: false
description: Hue in degrees.
schema:
type: number
minimum: 0
maximum: 360
default: 250
- name: saturation
in: query
required: false
description: Saturation at the mid steps, percent.
schema:
type: number
minimum: 0
maximum: 100
default: 78
- name: lightness
in: query
required: false
description: Lightness at step 500, percent.
schema:
type: number
minimum: 20
maximum: 80
default: 55
- name: hueSpread
in: query
required: false
description: Hue drift from the lightest to the darkest step. A value of 12 reproduces the warm-to-cool
lean of a modern perceptual ramp.
schema:
type: number
minimum: 0
maximum: 40
default: 12
security:
- sessionCookie: []
- bearerToken: []
/api/v1/tokens/schema:
get:
summary: The token schema
description: The group and kind registry, so a client renders the right control per token type instead
of duplicating a list that would drift.
tags:
- tokens
responses:
'200':
description: The schema.
content:
application/json:
schema:
$ref: '#/components/schemas/TokenSchema'
headers:
Cache-Control:
description: Cacheable.
schema:
type: string
'400':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
security:
- sessionCookie: []
- bearerToken: []
/api/v1/admin/users:
get:
summary: List known accounts
description: Accounts seen at least once at login. Roles are overwritten from the provider on every
login rather than merged, so a user removed from a group actually loses access.
tags:
- admin
responses:
'200':
description: The users.
content:
application/json:
schema:
type: object
properties:
items:
type: array
items:
$ref: '#/components/schemas/User'
headers: *id002
'401':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'403':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
parameters:
- name: limit
in: query
required: false
description: ''
schema:
type: integer
minimum: 1
maximum: 500
default: 100
security:
- sessionCookie: []
- bearerToken: []
/api/v1/admin/audit:
get:
summary: Read the activity log
description: Every mutation in the administration API.
tags:
- admin
responses:
'200':
description: A page of entries.
content:
application/json:
schema:
$ref: '#/components/schemas/AuditPage'
headers: *id002
'401':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'403':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
parameters:
- name: actor
in: query
required: false
description: Filter by actor.
schema:
type: string
- name: action
in: query
required: false
description: Filter by action.
schema:
type: string
- name: entityType
in: query
required: false
description: ''
schema:
type: string
- name: entityId
in: query
required: false
description: ''
schema:
type: string
- name: since
in: query
required: false
description: ''
schema:
type: string
format: date-time
- name: limit
in: query
required: false
description: ''
schema:
type: integer
minimum: 1
maximum: 500
default: 50
- name: offset
in: query
required: false
description: ''
schema:
type: integer
minimum: 0
default: 0
security:
- sessionCookie: []
- bearerToken: []
/api/v1/admin/settings:
get:
summary: Read instance settings
description: The settings the engine reads at run time. Everything requiring a restart is environment
configuration and belongs in the Helm chart, not in a database row.
tags:
- admin
responses:
'200':
description: The settings.
content:
application/json:
schema:
type: object
properties:
items:
type: object
additionalProperties:
$ref: '#/components/schemas/Setting'
headers: *id002
'401':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'403':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
security:
- sessionCookie: []
- bearerToken: []
put:
summary: Write instance settings
description: Writing the default theme clears the render cache, because it is baked into the runtime
manifest. Unrecognised keys are rejected rather than silently stored.
tags:
- admin
responses:
'200':
description: The keys written.
content:
application/json:
schema:
type: object
properties:
updated:
type: array
items:
type: string
headers: *id002
'400':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'401':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'403':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
requestBody:
required: true
content:
application/json:
schema:
type: object
additionalProperties:
type: string
description: 'Setting keys to values. Recognised keys: `default.theme`, `default.appearance`,
`runtime.public`, `runtime.apiBase`.'
example:
default.theme: midnight
default.appearance: dark
security:
- sessionCookie: []
- bearerToken: []
/api/v1/admin/cache/rebuild:
post:
summary: Rebuild the render cache
description: 'Clears and re-warms it. Useful after a bulk change, or to reclaim memory: the cache
holds every published theme multiplied by appearance and variant.'
tags:
- admin
responses:
'200':
description: What was warmed.
content:
application/json:
schema:
type: object
properties:
themes:
type: integer
artefacts:
type: integer
headers: *id002
'401':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'403':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
security:
- sessionCookie: []
- bearerToken: []
/api/v1/stats:
get:
summary: Dashboard counters
description: Theme, key, user and audit counts, plus the render cache hit rate.
tags:
- themes
responses:
'200':
description: The counters.
content:
application/json:
schema:
$ref: '#/components/schemas/Stats'
headers: *id002
'401':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
security:
- sessionCookie: []
- bearerToken: []
/healthz:
get:
summary: Liveness, with build information
description: Does **not** touch the database, deliberately. A liveness probe that failed on a slow
database would have the orchestrator restart a process that is otherwise healthy, turning a dependency
blip into an outage.
tags:
- operational
responses:
'200':
description: Healthy.
content:
application/json:
schema:
$ref: '#/components/schemas/Health'
'500':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
security: []
/livez:
get:
summary: Liveness
description: Answers as long as the process can serve HTTP.
tags:
- operational
responses:
'200':
description: Healthy.
content:
application/json:
schema:
$ref: '#/components/schemas/Health'
'500':
description: Error
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
security: []
/readyz:
get:
summary: Readiness
description: Reports whether the service can actually serve traffic, which means the database must
answer within a bounded time. Warns when the database is in-memory, which is legitimate in a test
and never in production.
tags:
- operational
responses:
'200':
description: Ready.
content:
application/json:
schema:
$ref: '#/components/schemas/Health'
headers: *id002
'503':
description: Not ready.
content:
application/json:
schema:
$ref: '#/components/schemas/Health'
security: []
/metrics:
get:
summary: Prometheus metrics
description: Request counts labelled by **route pattern** rather than concrete path — a thousand
distinct theme slugs must not become a thousand time series — a latency histogram, the render
cache hit rate, and Go runtime gauges. Disabled when `config.enableMetrics` is false.
tags:
- operational
responses:
'200':
description: The exposition text.
content:
text/plain:
schema:
type: string
headers: *id002
security: []
/rt/v1/embed-page:
get:
summary: A shareable integration page
description: 'A complete HTML page showing the snippet for a theme next to a live preview of it.
Separate from /rt/v1/embed because the two have different callers: this one is a page a human
opens, that one is data a UI fetches. Every code sample on the page comes from the same builders,
so what is copied here is byte-for-byte what the admin UI shows.'
tags:
- runtime
parameters:
- name: slug
in: query
required: false
description: The theme to show.
schema:
type: string
responses:
'200':
description: The page.
content:
text/html:
schema:
type: string
'400':
description: Invalid slug.
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
'404':
description: No such theme.
content:
application/json:
schema:
$ref: '#/components/schemas/ApiError'
security: []
components:
securitySchemes:
sessionCookie:
type: apiKey
in: cookie
name: go-bananas-session
description: An HMAC-signed session cookie. Set `HttpOnly`, `Secure` and `SameSite`. Issued by the
OIDC login flow. There is no server-side session table, so any replica can authenticate any request.
bearerToken:
type: http
scheme: bearer
description: An API key. May also be supplied as a `token` query parameter, because a stylesheet
link cannot carry a header. Stored as a SHA-256 digest.
schemas:
ApiError:
type: object
description: 'The uniform error body. `code` is stable and part of the contract: clients branch
on it, so its meaning must not change between releases.'
required:
- code
- message
properties:
code:
type: string
description: A stable machine-readable code.
examples:
- validation_failed
message:
type: string
description: A human-readable explanation.
details:
type: array
items:
$ref: '#/components/schemas/FieldError'
requestId:
type: string
description: Correlates the response with the server log.
FieldError:
type: object
required:
- field
- message
properties:
field:
type: string
description: The JSON path or dotted token path.
message:
type: string
Token:
type: object
required:
- $value
description: A single token leaf, in the W3C Design Tokens Community Group format.
properties:
$type:
type: string
description: The token type.
examples:
- color
- dimension
- fontSize
- cubicBezier
$value:
description: 'The value: a string, number, boolean or number array.'
oneOf:
- type: string
- type: integer
- type: boolean
- type: array
items:
type: integer
$description:
type: string
TokenDocument:
type: object
additionalProperties: true
description: A design-token document. `$appearances` holds a partial document per colour scheme,
deep-merged over the base, so a token absent from the dark overrides simply inherits its light
value.
properties:
$schema:
type: string
$appearances:
type: object
additionalProperties: true
ThemeStatus:
type: string
enum:
- draft
- published
- archived
description: Publication state. Only `published` is served to the runtime.
Appearance:
type: string
enum:
- light
- dark
description: Colour scheme.
Theme:
type: object
required:
- id
- slug
- name
- status
- defaultAppearance
- tokens
- system
- version
- createdAt
- updatedAt
properties:
id:
type: string
slug:
type: string
pattern: ^[a-z0-9]([a-z0-9-]{0,62}[a-z0-9])?$
name:
type: string
description:
type: string
status: *id005
defaultAppearance: &id007
$ref: '#/components/schemas/Appearance'
tokens: *id006
meta:
type: object
additionalProperties: true
description: Opaque to the engine apart from `tags`.
system:
type: boolean
description: A built-in theme. Cannot be deleted.
version:
type: integer
minimum: 1
description: Bumped on every mutation. The render cache key.
createdAt:
type: string
format: date-time
updatedAt:
type: string
format: date-time
createdBy:
type: string
updatedBy:
type: string
ThemeSummary:
type: object
description: 'A theme without its token document. Listings never include them: they are the bulk
of each row and a listing does not need them.'
properties:
id:
type: string
slug:
type: string
name:
type: string
description:
type: string
status: *id005
defaultAppearance: *id007
meta:
type: object
additionalProperties: true
system:
type: boolean
version:
type: integer
createdAt:
type: string
format: date-time
updatedAt:
type: string
format: date-time
createdBy:
type: string
updatedBy:
type: string
ThemePage:
type: object
required:
- items
- total
- limit
- offset
properties:
items:
type: array
items:
$ref: '#/components/schemas/ThemeSummary'
total:
type: integer
limit:
type: integer
offset:
type: integer
ThemeMutation:
type: object
description: Create or update. Absent fields are left unchanged on update.
properties:
slug:
type: string
pattern: ^[a-z0-9]([a-z0-9-]{0,62}[a-z0-9])?$
name:
type: string
description:
type: string
status: *id005
defaultAppearance: *id007
tokens: *id006
meta:
type: object
additionalProperties: true
message:
type: string
description: Recorded in the version history.
baseVersion:
type: integer
description: Optimistic concurrency. The write is rejected with 409 when the stored version
differs, which stops two editors silently overwriting each other.
ThemeVersion:
type: object
description: 'An immutable snapshot. History is append-only: a rollback restores a snapshot as a
new version rather than rewinding, so a deployed application can never lose the revision it was
built against.'
required:
- themeId
- version
- slug
- name
- createdAt
properties:
themeId:
type: string
version:
type: integer
slug:
type: string
name:
type: string
description:
type: string
status: *id005
defaultAppearance: *id007
tokens: *id006
message:
type: string
author:
type: string
createdAt:
type: string
format: date-time
VersionDiff:
type: object
required:
- fromVersion
- toVersion
- added
- removed
- changed
properties:
fromVersion:
type: integer
toVersion:
type: integer
added:
type: array
items:
type: string
removed:
type: array
items:
type: string
changed:
type: array
items:
$ref: '#/components/schemas/TokenDiff'
metaChanged:
type: boolean
TokenDiff:
type: object
required:
- path
- from
- to
properties:
path:
type: string
from:
type: string
to:
type: string
ValidationIssue:
type: object
required:
- path
- severity
- message
properties:
path:
type: string
severity:
type: string
enum:
- error
- warning
- info
message:
type: string
hint:
type: string
ValidationResult:
type: object
required:
- valid
- tokenCount
- issues
properties:
valid:
type: boolean
description: False when any issue is an error. Errors block publication; warnings do not.
tokenCount:
type: integer
issues:
type: array
items:
$ref: '#/components/schemas/ValidationIssue'
ContrastResult:
type: object
required:
- foreground
- background
- ratio
- level
properties:
foreground:
type: string
background:
type: string
ratio:
type: number
rounded:
type: number
level:
type: string
enum:
- fail
- aa-large
- aa
- aaa
thresholds:
type: object
properties:
aaLarge:
type: number
aa:
type: number
aaa:
type: number
bestForeground:
type: string
RampResponse:
type: object
description: A generated colour ramp.
properties:
options:
type: object
properties:
Hue:
type: number
Saturation:
type: number
Lightness:
type: number
HueSpread:
type: number
shades:
type: array
items:
type: string
ramp:
type: object
additionalProperties:
$ref: '#/components/schemas/Token'
TokenSchema:
type: object
description: The engine's group and kind registry, so a client renders the right control per token
type instead of duplicating a list that would drift.
properties:
id:
type: string
prefix:
type: string
kinds:
type: array
items:
type: string
groups:
type: array
items:
type: object
properties:
name:
type: string
kind:
type: string
description:
type: string
recommended:
type: array
items:
type: string
shades:
type: array
items:
type: string
ImportRequest:
type: object
required:
- slug
- tokens
properties:
slug:
type: string
name:
type: string
tokens: *id006
meta:
type: object
additionalProperties: true
asDraft:
type: boolean
default: true
description: Import as a draft, so an import can never publish something by accident.
overwrite:
type: boolean
description: Replace an existing theme. Without it, an existing slug is a 409 rather than a
silent overwrite.
message:
type: string
ThemeExport:
type: object
description: The portable document. Import and export are exact inverses, so migrating a theme between
environments is a file copy rather than a project.
properties:
$schema:
type: string
slug:
type: string
name:
type: string
status: *id005
version:
type: integer
tokens: *id006
meta:
type: object
additionalProperties: true
exportedAt:
type: string
format: date-time
exportedBy:
type: string
Session:
type: object
description: 'The current session. Always answers 200: an absent session is the normal state on
first load, not an error the UI should have to handle as one.'
required:
- authenticated
- mode
properties:
authenticated:
type: boolean
mode:
type: string
enum:
- oidc
- dev
- none
identity:
type: object
properties:
subject:
type: string
email:
type: string
name:
type: string
roles:
type: array
items:
type: string
enum:
- viewer
- editor
- admin
method:
type: string
expiresAt:
type: string
format: date-time
permissions:
type: object
additionalProperties:
type: boolean
description: A UI convenience only. The server enforces every check, so hiding a control is
never the control.
preferences:
$ref: '#/components/schemas/UserPreferences'
description: Present once authenticated. Reported with the session so the UI can preselect a
theme without a second request.
devBypass:
type: boolean
loginUrl:
type: string
UserPreferences:
type: object
description: 'Per-account interface preferences. Both fields empty means nothing has been chosen,
which is a valid state rather than a missing one: the client then applies its own default.'
properties:
selectedTheme:
type: string
description: A published theme slug, or empty for no choice. Names the theme rather than copying
it, so editing or unpublishing that theme shows through everywhere. A stored value naming
a theme that has since been deleted or archived reads back as empty.
selectedAppearance:
$ref: '#/components/schemas/Appearance'
description: Which of the theme''s two colour schemes to render. Ignored, and dropped on write,
when selectedTheme is empty.
AppConfig:
type: object
description: What the UI needs before a session exists. Contains no secret material.
properties:
mode:
type: string
issuer:
type: string
runtimeBase:
type: string
publicBase:
type: string
basePath:
type: string
runtimeApiVersion:
type: string
variablePrefix:
type: string
defaultTheme:
type: string
User:
type: object
properties:
subject:
type: string
email:
type: string
name:
type: string
roles:
type: array
items:
type: string
method:
type: string
firstSeen:
type: string
format: date-time
lastLogin:
type: string
format: date-time
AuditEntry:
type: object
description: One recorded action. The actor comes from the authenticated identity, never from the
request body.
required:
- id
- at
- actor
- action
properties:
id:
type: integer
at:
type: string
format: date-time
actor:
type: string
actorEmail:
type: string
action:
type: string
enum:
- theme.create
- theme.update
- theme.delete
- theme.publish
- theme.archive
- theme.duplicate
- theme.rollback
- theme.import
- key.create
- key.revoke
- auth.login
- auth.logout
entityType:
type: string
entityId:
type: string
summary:
type: string
detail:
type: object
additionalProperties: true
remoteIp:
type: string
AuditPage:
type: object
required:
- items
- total
- limit
- offset
properties:
items:
type: array
items:
$ref: '#/components/schemas/AuditEntry'
total:
type: integer
limit:
type: integer
offset:
type: integer
Setting:
type: object
required:
- key
- value
- updatedAt
properties:
key:
type: string
value:
type: string
updatedAt:
type: string
format: date-time
updatedBy:
type: string
Stats:
type: object
properties:
store:
type: object
properties:
themes:
type: integer
themesByStatus:
type: object
additionalProperties:
type: integer
users:
type: integer
auditEntries:
type: integer
databaseSizeBytes:
type: integer
versionsRetaind:
type: integer
cache:
type: object
properties:
CSSHits:
type: integer
CSSMisses:
type: integer
JSONHits:
type: integer
JSONMisses:
type: integer
Entries:
type: integer
ResolvedTheme:
type: object
description: A theme with every token resolved for one appearance, including derived tokens, because
consumers should not reimplement the derivation.
required:
- theme
- tokens
- variables
properties:
theme:
type: object
properties:
slug:
type: string
name:
type: string
description:
type: string
version:
type: integer
appearance: *id007
defaultAppearance: *id007
updatedAt:
type: string
format: date-time
tokens: *id006
variables:
type: object
additionalProperties:
type: string
description: Flat CSS custom property name to rendered value.
meta:
type: object
additionalProperties: true
ManifestEntry:
type: object
required:
- slug
- name
- defaultAppearance
- version
- css
- tokens
properties:
slug:
type: string
name:
type: string
description:
type: string
defaultAppearance: *id007
version:
type: integer
tags:
type: array
items:
type: string
css:
type: string
format: uri
tokens:
type: string
format: uri
Manifest:
type: object
description: 'The discovery document a loader fetches once to build a theme picker. Deliberately
small: no token data, so a page with a picker does not download every theme.'
required:
- apiVersion
- baseUrl
- themes
properties:
apiVersion:
type: string
default:
type: string
baseUrl:
type: string
format: uri
themes:
type: array
items:
$ref: '#/components/schemas/ManifestEntry'
Health:
type: object
properties:
status:
type: string
enum:
- ok
- ready
- not ready
version:
type: string
uptime:
type: string
checks:
type: object
additionalProperties: true
EmbedSnippet:
type: object
description: A ready-to-paste integration snippet for one theme.
required:
- slug
- name
- version
- baseUrl
- language
- snippet
properties:
slug:
type: string
description: 'The theme, which exists: the endpoint validates it.'
name:
type: string
description: The theme's display name.
version:
type: integer
description: The version the snippet was generated against.
baseUrl:
type: string
description: The runtime base URL baked into the snippet.
language:
type: string
enum:
- html
- js
- tsx
description: The syntax of the snippet.
snippet:
type: string
description: The code to paste. Values interpolated into it are escaped for the language.
note:
type: string
description: Anything else the integrator needs to know.
security:
- sessionCookie: []
- bearerToken: []