Bring spec.yaml in line with the implemented surface: the retired PKI/federation routes, the ungated /api/{service} data plane and its WebSocket upgrade, the app catalog and public downloads, the hub SPA mount, and the web app link resolution and container fields.
1850 lines
64 KiB
YAML
1850 lines
64 KiB
YAML
openapi: 3.0.3
|
|
info:
|
|
title: Dcntrlized Maestro API
|
|
version: 2.2.0
|
|
description: The API of the Maestro orchestration router. Maestro manages the lifecycle of Dcntrlized service containers and routes inter-service action calls to them. Every /api/* route except /api/auth/*, the deliberately public GET /api/instance (hub-app theme) and the public catalog download/icon routes requires an instance-owner session (cookies issued by the auth endpoints); in setup mode gated routes answer 428 ERR_SETUP_REQUIRED, otherwise 401 ERR_UNAUTHENTICATED (an unreadable owner store fails closed with 500 ERR_INTERNAL). Mutating requests from browser origins are additionally checked against the request host (CSRF). **Single data plane (user ruling, 2026-10-18).** Requests to a service are expected to be routed through Maestro and may be unauthenticated; services own their own auth (usually via dcntrlized-auth). Every service namespace an app needs is reachable under `/api/{service}/…` — `action`, `auth` and `ws` — without an owner session, and an instance therefore needs exactly ONE open port. Direct service addressing is not a supported deployment. Contract rev 9 removed the PKI/mTLS identity layer and the federation/peer trust surfaces — all internal edges are plaintext HTTP on a single 8080 listener, /api/peers* and /api/instances* are gone, and runtime config delivery is authorized by a dedicated ES256 /config token (see /api/services/{service}/config).
|
|
|
|
components:
|
|
schemas:
|
|
ActionRequest:
|
|
type: object
|
|
properties:
|
|
payload:
|
|
type: object
|
|
description: Action-specific parameters
|
|
ActionResponse:
|
|
type: object
|
|
properties:
|
|
status:
|
|
type: string
|
|
data:
|
|
type: object
|
|
|
|
SuccessResponse:
|
|
type: object
|
|
properties:
|
|
status:
|
|
type: string
|
|
example: "success"
|
|
message:
|
|
type: string
|
|
|
|
ApiError:
|
|
type: object
|
|
description: The uniform API error shape (onboarding theme M1).
|
|
properties:
|
|
error:
|
|
type: object
|
|
properties:
|
|
code:
|
|
type: string
|
|
example: "ERR_UNAUTHENTICATED"
|
|
|
|
AuthStatus:
|
|
type: object
|
|
description: The instance-owner gate state; the SPA bootstraps from it.
|
|
properties:
|
|
setup_required:
|
|
type: boolean
|
|
description: True until the first owner account exists.
|
|
authenticated:
|
|
type: boolean
|
|
username:
|
|
type: string
|
|
|
|
Credentials:
|
|
type: object
|
|
required: [username, password]
|
|
properties:
|
|
username:
|
|
type: string
|
|
description: "3-64 chars: lowercase letters, digits and . _ - ; must start alphanumeric."
|
|
password:
|
|
type: string
|
|
minLength: 10
|
|
|
|
SessionResponse:
|
|
type: object
|
|
description: >-
|
|
Login/setup/rotation result. The access and refresh tokens ride as
|
|
HttpOnly SameSite=Lax cookies (maestro_access / maestro_refresh).
|
|
properties:
|
|
username:
|
|
type: string
|
|
role:
|
|
type: string
|
|
example: "admin"
|
|
access_ttl_s:
|
|
type: integer
|
|
example: 900
|
|
|
|
PasswordChange:
|
|
type: object
|
|
required: [current_password, new_password]
|
|
properties:
|
|
current_password:
|
|
type: string
|
|
new_password:
|
|
type: string
|
|
minLength: 10
|
|
|
|
PendingAuthorization:
|
|
type: object
|
|
description: A first-contact authorization request awaiting the instance owner (M2 §3).
|
|
properties:
|
|
service:
|
|
type: string
|
|
example: "keepnote"
|
|
fingerprint:
|
|
type: string
|
|
example: "sha256:aa:bb:cc:dd"
|
|
first_seen:
|
|
type: string
|
|
last_seen:
|
|
type: string
|
|
|
|
AuthorizedKey:
|
|
type: object
|
|
description: An approved peer row of the auth service's trust store.
|
|
properties:
|
|
service:
|
|
type: string
|
|
fingerprint:
|
|
type: string
|
|
approved_by:
|
|
type: string
|
|
approved_at:
|
|
type: string
|
|
revoked_at:
|
|
type: string
|
|
nullable: true
|
|
|
|
PeerAction:
|
|
type: object
|
|
required: [service, fingerprint]
|
|
properties:
|
|
service:
|
|
type: string
|
|
description: The service name (cert CN / org.dcntrlized.service).
|
|
fingerprint:
|
|
type: string
|
|
description: The SHA256 public-key fingerprint (sha256:…) shown to the owner.
|
|
|
|
Attestation:
|
|
type: object
|
|
description: The auth service's public signing key statement (D3).
|
|
properties:
|
|
public_key:
|
|
type: string
|
|
fingerprint:
|
|
type: string
|
|
example: "sha256:…"
|
|
kid:
|
|
type: string
|
|
issued_at:
|
|
type: string
|
|
|
|
InstanceIdentity:
|
|
type: object
|
|
description: The instance's public self-description (hub-app M1).
|
|
required: [name]
|
|
properties:
|
|
name:
|
|
type: string
|
|
example: "home-node"
|
|
title:
|
|
type: string
|
|
description:
|
|
type: string
|
|
fingerprint:
|
|
type: string
|
|
description: Instance CA public-key SHA256 (lib/pki); omitted when no CA is provisioned.
|
|
example: "sha256:…"
|
|
|
|
PublicService:
|
|
type: object
|
|
description: |
|
|
Deliberately reduced view of a running service — no container id,
|
|
docker name, private IP, or port mapping leaves the instance (hub-app D3).
|
|
required: [service_name, status, image]
|
|
properties:
|
|
service_name:
|
|
type: string
|
|
title:
|
|
type: string
|
|
description:
|
|
type: string
|
|
status:
|
|
type: string
|
|
version:
|
|
type: string
|
|
capabilities:
|
|
type: array
|
|
items:
|
|
type: string
|
|
has_auth:
|
|
type: boolean
|
|
image:
|
|
type: string
|
|
|
|
TrustedInstance:
|
|
type: object
|
|
description: |
|
|
Legacy remote-instance row kept for source compatibility with the
|
|
hub-app profile. Contract rev 9 retired federation, so the public
|
|
profile's trusted_instances array is always empty; the type is never
|
|
populated with live rows.
|
|
required: [name, url, fingerprint, status]
|
|
properties:
|
|
name:
|
|
type: string
|
|
url:
|
|
type: string
|
|
fingerprint:
|
|
type: string
|
|
example: "sha256:…"
|
|
status:
|
|
type: string
|
|
example: "authorized"
|
|
scopes:
|
|
type: array
|
|
items:
|
|
type: string
|
|
|
|
InstancePublicProfile:
|
|
type: object
|
|
description: |
|
|
The unauthenticated GET /api/instance response (hub-app theme). With
|
|
DCNTRLIZED_PUBLIC_PROFILE=false only `instance` is returned. After the
|
|
rev-9 federation removal trusted_instances is always [] (never null);
|
|
`instance.fingerprint` is derived from the persisted instance key.
|
|
required: [instance]
|
|
properties:
|
|
instance:
|
|
$ref: '#/components/schemas/InstanceIdentity'
|
|
services:
|
|
type: array
|
|
nullable: true
|
|
items:
|
|
$ref: '#/components/schemas/PublicService'
|
|
trusted_instances:
|
|
type: array
|
|
description: Always empty after the rev-9 federation removal.
|
|
items:
|
|
$ref: '#/components/schemas/TrustedInstance'
|
|
apps:
|
|
type: array
|
|
nullable: true
|
|
description: >-
|
|
Operator-authored app catalog (app-catalog theme A3): the public
|
|
projection with absolute download/icon URLs and no added_by. Empty
|
|
catalog → []; the whole key is omitted when
|
|
DCNTRLIZED_PUBLIC_PROFILE=false.
|
|
items:
|
|
$ref: '#/components/schemas/CatalogApp'
|
|
|
|
CatalogArtifact:
|
|
type: object
|
|
description: The installable artifact of a catalog app (A4).
|
|
required: [filename, size_bytes, sha256, kind]
|
|
properties:
|
|
filename:
|
|
type: string
|
|
example: "keepnote-1.4.2.apk"
|
|
size_bytes:
|
|
type: integer
|
|
format: int64
|
|
sha256:
|
|
type: string
|
|
description: Lowercase hex sha256 of the artifact bytes.
|
|
content_type:
|
|
type: string
|
|
example: "application/vnd.android.package-archive"
|
|
kind:
|
|
type: string
|
|
enum: [hosted, link]
|
|
description: hosted = Maestro stores/serves a copy; link = 302 to source_url.
|
|
download_url:
|
|
type: string
|
|
description: Absolute URL of this instance's artifact endpoint.
|
|
signer_sha256:
|
|
type: string
|
|
|
|
CatalogApp:
|
|
type: object
|
|
description: >-
|
|
One app-catalog record (app-catalog theme). id is stable across edits
|
|
("a_" + first 12 hex of sha256(package)); added_by appears only on the
|
|
owner-gated view; internal paths never serialize (A7).
|
|
required: [id, package, name, version_name, artifact, added_at, updated_at]
|
|
properties:
|
|
id:
|
|
type: string
|
|
example: "a_3f9c1d2ab7e4"
|
|
package:
|
|
type: string
|
|
example: "org.dcntrlized.keepnote"
|
|
name:
|
|
type: string
|
|
summary:
|
|
type: string
|
|
description:
|
|
type: string
|
|
category:
|
|
type: string
|
|
version_name:
|
|
type: string
|
|
version_code:
|
|
type: integer
|
|
min_android:
|
|
type: integer
|
|
icon_url:
|
|
type: string
|
|
artifact:
|
|
$ref: '#/components/schemas/CatalogArtifact'
|
|
source_url:
|
|
type: string
|
|
description: Optional project/source page (not the artifact).
|
|
added_at:
|
|
type: string
|
|
updated_at:
|
|
type: string
|
|
added_by:
|
|
type: string
|
|
description: Owner username; owner-gated responses only, never public.
|
|
|
|
CatalogAppAdd:
|
|
type: object
|
|
description: Body of POST /api/catalog/apps (app-catalog theme).
|
|
required: [package, name, version_name, artifact_url]
|
|
properties:
|
|
package:
|
|
type: string
|
|
name:
|
|
type: string
|
|
summary:
|
|
type: string
|
|
description:
|
|
type: string
|
|
category:
|
|
type: string
|
|
version_name:
|
|
type: string
|
|
version_code:
|
|
type: integer
|
|
min_android:
|
|
type: integer
|
|
icon_url:
|
|
type: string
|
|
source_url:
|
|
type: string
|
|
artifact_url:
|
|
type: string
|
|
description: Absolute http(s) URL Maestro fetches (or links, with link_only).
|
|
filename:
|
|
type: string
|
|
description: Optional override; defaults to the URL basename.
|
|
content_type:
|
|
type: string
|
|
sha256:
|
|
type: string
|
|
description: Optional expected checksum; mismatch → 400 and nothing stored.
|
|
signer_sha256:
|
|
type: string
|
|
size_bytes:
|
|
type: integer
|
|
format: int64
|
|
description: Operator-reported size for link_only entries.
|
|
link_only:
|
|
type: boolean
|
|
description: Skip the fetch, require sha256, store kind="link".
|
|
|
|
CatalogAppEdit:
|
|
type: object
|
|
description: >-
|
|
Body of PUT /api/catalog/apps/{id}: a full replacement of the editable
|
|
metadata (omitted optional fields clear; name/version_name required;
|
|
package immutable).
|
|
properties:
|
|
package:
|
|
type: string
|
|
name:
|
|
type: string
|
|
summary:
|
|
type: string
|
|
description:
|
|
type: string
|
|
category:
|
|
type: string
|
|
version_name:
|
|
type: string
|
|
version_code:
|
|
type: integer
|
|
min_android:
|
|
type: integer
|
|
icon_url:
|
|
type: string
|
|
source_url:
|
|
type: string
|
|
signer_sha256:
|
|
type: string
|
|
|
|
RegistryContainer:
|
|
type: object
|
|
required: [name, image]
|
|
properties:
|
|
name:
|
|
type: string
|
|
example: "keepnote"
|
|
service_name:
|
|
type: string
|
|
description: >-
|
|
The service identity declared by the image's org.dcntrlized.service
|
|
label. A container package without it is not a Maestro service and
|
|
is not listed.
|
|
example: "KeepNote"
|
|
version:
|
|
type: string
|
|
example: "0.0.1"
|
|
image:
|
|
type: string
|
|
example: "192.168.122.183:3000/dcntrlized/keepnote:dev"
|
|
digest:
|
|
type: string
|
|
title:
|
|
type: string
|
|
example: "KeepNote"
|
|
vendor:
|
|
type: string
|
|
author:
|
|
type: string
|
|
description:
|
|
type: string
|
|
source:
|
|
type: string
|
|
description: >-
|
|
The image's declared source repository (OCI
|
|
org.opencontainers.image.source), served verbatim for display.
|
|
Untrusted: it is never a link target by itself.
|
|
source_href:
|
|
type: string
|
|
description: >-
|
|
`source` projected through the http/https allowlist (same validator
|
|
as `web.url`: http/https exactly, no userinfo, no control chars,
|
|
non-empty host). Present only when `source` is safe to link;
|
|
clients must render `source` as plain text when it is absent and
|
|
must never build an href from `source` themselves.
|
|
revision:
|
|
type: string
|
|
licenses:
|
|
type: string
|
|
url:
|
|
type: string
|
|
documentation:
|
|
type: string
|
|
created_at:
|
|
type: string
|
|
hidden:
|
|
type: boolean
|
|
description: Hidden infrastructure containers (e.g. the shared dcntrlized-db) are excluded from the service grid.
|
|
example: true
|
|
web:
|
|
$ref: '#/components/schemas/WebApp'
|
|
|
|
WebApp:
|
|
type: object
|
|
description: >-
|
|
The service's Web App capability (contract rev 8.2 §16.2/§16.3). On
|
|
GET /api/containers `href` is resolved SERVER-SIDE from the frozen
|
|
precedence: manifest `url` -> per-service operator override -> global
|
|
base-domain template -> derived `http(s)://<request host>:<published
|
|
port>`. A present-but-invalid source is skipped, and a service whose
|
|
block yields no valid href omits `web` entirely. Clients must only ever
|
|
open `href`, never build a URL from service strings.
|
|
required: [enabled]
|
|
properties:
|
|
enabled:
|
|
type: boolean
|
|
description: Whether the service advertises a web app at all.
|
|
port:
|
|
type: integer
|
|
description: The container port the web app listens on.
|
|
url:
|
|
type: string
|
|
description: >-
|
|
The base URL declared by the service manifest. Untrusted: it is
|
|
only echoed for diagnostics and is never a link target by itself.
|
|
href:
|
|
type: string
|
|
description: >-
|
|
The server-resolved absolute http(s) link target. Present only when
|
|
a valid target exists; absent from registry-listing records.
|
|
|
|
PortMapping:
|
|
type: object
|
|
properties:
|
|
host:
|
|
type: integer
|
|
guest:
|
|
type: integer
|
|
|
|
IPAddress:
|
|
type: object
|
|
properties:
|
|
version:
|
|
type: string
|
|
enum: [v4, v6]
|
|
address:
|
|
type: string
|
|
|
|
Network:
|
|
type: object
|
|
properties:
|
|
name:
|
|
type: string
|
|
ipAddress:
|
|
$ref: '#/components/schemas/IPAddress'
|
|
mode:
|
|
type: string
|
|
enum: [bridge, host, none, overlay, ipvlan, macvlan]
|
|
ports:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/PortMapping'
|
|
|
|
Container:
|
|
type: object
|
|
properties:
|
|
id:
|
|
type: string
|
|
name:
|
|
type: string
|
|
service_name:
|
|
type: string
|
|
role:
|
|
type: string
|
|
example: "db"
|
|
hidden:
|
|
type: boolean
|
|
description: Hidden infrastructure containers (e.g. the shared dcntrlized-db) are excluded from the service grid.
|
|
external:
|
|
type: boolean
|
|
description: >-
|
|
True when the container was started outside Maestro (e.g. a repo's
|
|
own docker-compose.debug.yaml). The console may stop it but must not
|
|
start, uninstall, update or reconfigure it.
|
|
capabilities:
|
|
type: array
|
|
items:
|
|
type: string
|
|
example: "database"
|
|
description: Capabilities declared by the service manifest (e.g. database).
|
|
has_auth:
|
|
type: boolean
|
|
description: >-
|
|
Whether the service consumes the shared auth service (manifest
|
|
has_auth). True for dcntrlized-auth itself and for auth-consuming
|
|
services.
|
|
public_key:
|
|
type: string
|
|
description: >-
|
|
The auth service's RS256 public key (manifest public_key); the hook
|
|
for a future Maestro dashboard auth that validates dcntrlized-auth
|
|
tokens.
|
|
config:
|
|
type: array
|
|
description: >-
|
|
Operator-configurable fields the service declares in its manifest
|
|
config block (Service Config & Secrets theme). Each element is a
|
|
single-key object: the key names the field, the value is its spec.
|
|
items:
|
|
$ref: '#/components/schemas/ServiceConfigDeclaration'
|
|
web:
|
|
$ref: '#/components/schemas/WebApp'
|
|
docker_name:
|
|
type: string
|
|
image:
|
|
type: string
|
|
version:
|
|
type: string
|
|
status:
|
|
type: string
|
|
created_at:
|
|
type: string
|
|
network:
|
|
$ref: '#/components/schemas/Network'
|
|
title:
|
|
type: string
|
|
vendor:
|
|
type: string
|
|
author:
|
|
type: string
|
|
description:
|
|
type: string
|
|
source:
|
|
type: string
|
|
|
|
WebLinksConfig:
|
|
type: object
|
|
description: >-
|
|
Operator-controlled URL sources for the Web App capability (contract
|
|
rev 8.2 §16.2): a global base-domain template and per-service overrides.
|
|
properties:
|
|
base_domain_template:
|
|
type: string
|
|
description: >-
|
|
Global template, e.g. https://<service>.<domain>. Must contain the
|
|
literal <service> placeholder.
|
|
example: "https://<service>.home.example"
|
|
overrides:
|
|
type: object
|
|
additionalProperties:
|
|
type: string
|
|
description: Per-service base URL, keyed by service name.
|
|
|
|
ServiceConfigField:
|
|
type: object
|
|
description: >-
|
|
The spec of one operator-configurable field declared by a service's
|
|
manifest config block (Service Config & Secrets theme). The manifest is
|
|
a pure schema — it never carries a value.
|
|
required: [type, key]
|
|
properties:
|
|
type:
|
|
type: string
|
|
enum: [cleartext, password]
|
|
description: cleartext is read-write; password is write-only and encrypted at rest.
|
|
description:
|
|
type: string
|
|
description: Optional human-readable explanation shown in the UI.
|
|
key:
|
|
type: string
|
|
description: The container env var name Maestro injects the value under.
|
|
|
|
ServiceConfigDeclaration:
|
|
type: object
|
|
description: >-
|
|
A single-key config declaration inside a service manifest's config
|
|
array. The key names the field; the value is the field spec.
|
|
additionalProperties:
|
|
$ref: '#/components/schemas/ServiceConfigField'
|
|
minProperties: 1
|
|
maxProperties: 1
|
|
|
|
ServiceConfigEntry:
|
|
type: object
|
|
description: >-
|
|
The read-only merged view of one config field served by
|
|
GET /api/services/{service}/config: the schema plus the operator's set
|
|
state. Password values are never included; a cleartext value
|
|
(read-write) is.
|
|
required: [field, key, type, set]
|
|
properties:
|
|
field:
|
|
type: string
|
|
description: The manifest config field name (the config array element key).
|
|
key:
|
|
type: string
|
|
description: The container env var name.
|
|
type:
|
|
type: string
|
|
enum: [cleartext, password]
|
|
description:
|
|
type: string
|
|
set:
|
|
type: boolean
|
|
description: Whether a value is stored. The only thing a password row reveals.
|
|
value:
|
|
type: string
|
|
description: The stored value of a cleartext field; never present for passwords.
|
|
|
|
ServiceConfigValue:
|
|
type: object
|
|
description: Request body for setting a config value (PUT .../config/{key}).
|
|
required: [value]
|
|
properties:
|
|
value:
|
|
type: string
|
|
|
|
paths:
|
|
/api/actions/{containerId}/{actionName}:
|
|
description: >-
|
|
LEGACY owner-gated console route, kept for compatibility. Apps must use
|
|
/api/{service}/action/{actionName}, which reaches the same service without
|
|
an owner session. This route is behind the session gate only because
|
|
commit 2ffdf77 wrapped a service data-plane route in the operator gate; it
|
|
was never console traffic, and it must not acquire that history's
|
|
successors.
|
|
parameters:
|
|
- name: actionName
|
|
in: path
|
|
required: true
|
|
description: The name of the specific action to execute
|
|
schema:
|
|
type: string
|
|
- name: containerId
|
|
in: path
|
|
required: true
|
|
description: The ID or name of the target container/service. Accepts a full or partial container ID, a container name, or the service name (matched case-insensitively).
|
|
schema:
|
|
type: string
|
|
get:
|
|
summary: Retrieve data or status from a service action
|
|
tags: ["actions"]
|
|
description: Forwards the request (method, headers, query string, and body) to the target container's /action/{actionName} endpoint. The target service is responsible for authentication; any client-supplied auth headers are forwarded as-is.
|
|
responses:
|
|
'200':
|
|
description: Request successful
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/ActionResponse'
|
|
'400':
|
|
description: Bad Request - Invalid input parameters
|
|
'401':
|
|
description: Unauthorized - the target service rejected the request
|
|
'404':
|
|
description: Action or container not found
|
|
'500':
|
|
description: Failed to forward the request to the container
|
|
post:
|
|
summary: Execute a state-changing action in the service
|
|
tags: ["actions"]
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/ActionRequest'
|
|
responses:
|
|
'200':
|
|
description: Action executed successfully
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/ActionResponse'
|
|
'400':
|
|
description: Bad Request - Invalid input parameters
|
|
'401':
|
|
description: Unauthorized - the target service rejected the request
|
|
'404':
|
|
description: Action or container not found
|
|
'500':
|
|
description: Failed to forward the request to the container
|
|
put:
|
|
summary: Update configuration for a specific action
|
|
tags: ["actions"]
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/ActionRequest'
|
|
responses:
|
|
'200':
|
|
description: Configuration updated
|
|
'400':
|
|
description: Bad Request - Invalid input parameters
|
|
'401':
|
|
description: Unauthorized - the target service rejected the request
|
|
'404':
|
|
description: Action or container not found
|
|
'500':
|
|
description: Failed to forward the request to the container
|
|
delete:
|
|
tags: ["actions"]
|
|
summary: Reset or remove an action's state
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/ActionRequest'
|
|
responses:
|
|
'200':
|
|
description: Action state reset
|
|
'400':
|
|
description: Bad Request - Invalid input parameters
|
|
'401':
|
|
description: Unauthorized - the target service rejected the request
|
|
'404':
|
|
description: Action or container not found
|
|
'500':
|
|
description: Failed to forward the request to the container
|
|
|
|
/api/containers:
|
|
get:
|
|
summary: List the services Maestro manages
|
|
tags: ["containers"]
|
|
responses:
|
|
'200':
|
|
description: List of services
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/Container'
|
|
'500':
|
|
description: Internal server error
|
|
|
|
/api/containers/install:
|
|
post:
|
|
summary: Install a service container (downloads the compose file and pulls the image)
|
|
tags: ["containers"]
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/RegistryContainer'
|
|
responses:
|
|
'200':
|
|
description: Container downloaded and image pulled successfully
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/SuccessResponse'
|
|
'400':
|
|
description: Invalid JSON payload or missing required fields (name, image)
|
|
'500':
|
|
description: Failed to install container
|
|
|
|
/api/containers/uninstall:
|
|
post:
|
|
summary: Uninstall a service container and remove it from the cache
|
|
tags: ["containers"]
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [name]
|
|
properties:
|
|
name:
|
|
type: string
|
|
responses:
|
|
'200':
|
|
description: Container uninstalled successfully
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/SuccessResponse'
|
|
'400':
|
|
description: Invalid JSON payload or missing required field (name)
|
|
'500':
|
|
description: Failed to uninstall container
|
|
|
|
/api/containers/start:
|
|
post:
|
|
summary: Start a previously installed service container
|
|
tags: ["containers"]
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [name]
|
|
properties:
|
|
name:
|
|
type: string
|
|
responses:
|
|
'200':
|
|
description: Container started successfully
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/SuccessResponse'
|
|
'400':
|
|
description: Invalid JSON payload or missing required field (name)
|
|
'500':
|
|
description: Failed to start container
|
|
|
|
/api/containers/stop:
|
|
post:
|
|
summary: Stop a running service container
|
|
tags: ["containers"]
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: object
|
|
required: [name]
|
|
properties:
|
|
name:
|
|
type: string
|
|
responses:
|
|
'200':
|
|
description: Container stopped successfully
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/SuccessResponse'
|
|
'400':
|
|
description: Invalid JSON payload or missing required field (name)
|
|
'500':
|
|
description: Failed to stop container
|
|
|
|
/api/registry:
|
|
get:
|
|
summary: List the services available in the Dcntrlized registry
|
|
tags: ["registry"]
|
|
responses:
|
|
'200':
|
|
description: List of registry services
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/RegistryContainer'
|
|
'500':
|
|
description: Internal server error
|
|
'502':
|
|
description: Failed to connect to upstream registry
|
|
|
|
/api/instance:
|
|
get:
|
|
summary: Public instance profile (unauthenticated)
|
|
tags: ["instance"]
|
|
security: []
|
|
description: >-
|
|
The one read surface the owner gate deliberately allows through
|
|
(hub-app theme M1/D2): what this instance is, which non-hidden services
|
|
it runs (public fields only), which remote instances it trusts, and the
|
|
operator-authored app catalog it offers (public projection, absolute
|
|
download/icon URLs). DCNTRLIZED_PUBLIC_PROFILE=false omits services,
|
|
trusted_instances and apps.
|
|
responses:
|
|
'200':
|
|
description: The instance public profile
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/InstancePublicProfile'
|
|
|
|
/api/auth/status:
|
|
get:
|
|
summary: Instance-owner gate state (public)
|
|
tags: ["auth"]
|
|
description: >-
|
|
The SPA bootstraps from this endpoint. All other /api/* routes are
|
|
gated by the instance-owner session (onboarding theme M1): in setup
|
|
mode they answer 428 ERR_SETUP_REQUIRED, otherwise 401
|
|
ERR_UNAUTHENTICATED without a valid session; an unreadable owner store
|
|
fails closed with 500 ERR_INTERNAL everywhere.
|
|
responses:
|
|
'200':
|
|
description: Gate state
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/AuthStatus'
|
|
'500':
|
|
description: Internal server error
|
|
|
|
/api/auth/setup:
|
|
post:
|
|
summary: Create the first owner account (admin)
|
|
tags: ["auth"]
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Credentials'
|
|
responses:
|
|
'200':
|
|
description: Owner created; session cookies issued
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/SessionResponse'
|
|
'400':
|
|
description: Invalid credentials (ERR_INVALID_REQUEST / ERR_INVALID_CREDENTIALS)
|
|
'409':
|
|
description: Setup already completed (ERR_SETUP_COMPLETED)
|
|
'429':
|
|
description: Rate limited (ERR_RATE_LIMITED)
|
|
'500':
|
|
description: Owner store unreadable/unwritable (ERR_INTERNAL)
|
|
|
|
/api/auth/login:
|
|
post:
|
|
summary: Log in as the instance owner
|
|
tags: ["auth"]
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/Credentials'
|
|
responses:
|
|
'200':
|
|
description: Session cookies issued
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/SessionResponse'
|
|
'400':
|
|
description: Invalid JSON payload
|
|
'401':
|
|
description: Unknown user or wrong password (ERR_INVALID_CREDENTIALS)
|
|
'429':
|
|
description: Rate limited (ERR_RATE_LIMITED) with Retry-After
|
|
'500':
|
|
description: Owner store unreadable (ERR_INTERNAL)
|
|
|
|
/api/auth/refresh:
|
|
post:
|
|
summary: Rotate the access/refresh pair
|
|
tags: ["auth"]
|
|
description: >-
|
|
Uses the maestro_refresh cookie. Reuse of an already-rotated jti is a
|
|
theft signal and revokes every session of the user
|
|
(ERR_SESSION_REVOKED).
|
|
responses:
|
|
'200':
|
|
description: Fresh pair issued
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/SessionResponse'
|
|
'401':
|
|
description: Missing/invalid/replayed refresh cookie
|
|
|
|
/api/auth/logout:
|
|
post:
|
|
summary: Revoke the current refresh token and clear cookies
|
|
tags: ["auth"]
|
|
responses:
|
|
'200':
|
|
description: Logged out (idempotent, bodyless-safe)
|
|
|
|
/api/auth/change-password:
|
|
post:
|
|
summary: Change the owner's password (session required)
|
|
tags: ["auth"]
|
|
description: Revokes every existing session of the user on success.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/PasswordChange'
|
|
responses:
|
|
'200':
|
|
description: Password changed; all sessions revoked
|
|
'400':
|
|
description: Invalid payload or wrong current password
|
|
'401':
|
|
description: Not authenticated
|
|
'429':
|
|
description: Rate limited (ERR_RATE_LIMITED)
|
|
'500':
|
|
description: Owner store unreadable/unwritable (ERR_INTERNAL)
|
|
|
|
/api/ws:
|
|
get:
|
|
summary: WebSocket endpoint for live container events (internal, owner-gated)
|
|
tags: ["containers"]
|
|
description: >-
|
|
Establishes a WebSocket connection that streams container lifecycle
|
|
events as JSON. The upgrade is gated by the instance-owner session;
|
|
anonymous handshakes close with 401 (428 in setup mode). This is
|
|
Maestro's own internal broadcast channel, not the service data plane:
|
|
everything a *service* emits or consumes over a socket goes through
|
|
/api/{service}/ws instead.
|
|
responses:
|
|
'101':
|
|
description: Switching Protocols - WebSocket upgrade
|
|
'401':
|
|
description: Not authenticated
|
|
'428':
|
|
description: Setup required
|
|
|
|
/api/{service}/action/{actionName}:
|
|
parameters:
|
|
- name: service
|
|
in: path
|
|
required: true
|
|
description: >-
|
|
The service reference: full or partial container id, container name,
|
|
image or the org.dcntrlized.service label (matched case-insensitively
|
|
by Maestro's shared resolver).
|
|
schema:
|
|
type: string
|
|
- name: actionName
|
|
in: path
|
|
required: true
|
|
description: The action to run, forwarded verbatim as the service's /action/{actionName}.
|
|
schema:
|
|
type: string
|
|
get:
|
|
summary: Read through a service action without an owner session
|
|
tags: ["services"]
|
|
description: Same contract as POST — the method, query string and headers are forwarded unchanged.
|
|
responses:
|
|
'200':
|
|
description: Mirrored from the service (status, body and headers unchanged).
|
|
'4XX':
|
|
description: >-
|
|
Mirrored from the service, including its own error envelope — the
|
|
service is the gate on this surface.
|
|
'404':
|
|
description: ERR_NOT_FOUND - unknown service (no installed container matches the reference).
|
|
'409':
|
|
description: ERR_NOT_RUNNING - the service container exists but is stopped.
|
|
'500':
|
|
description: >-
|
|
ERR_INTERNAL - Maestro could not resolve the service (Docker daemon
|
|
unavailable, no network address, no service port). Plumbing failure,
|
|
deliberately NOT a 502.
|
|
'502':
|
|
description: ERR_UPSTREAM_UNREACHABLE - the service did not answer.
|
|
post:
|
|
summary: Run a service action (the app data-plane entry point)
|
|
tags: ["services"]
|
|
description: >-
|
|
Forwards the request (method, body, query string and application
|
|
headers) to the target service's `/action/{actionName}` and mirrors the
|
|
answer. This is the surface apps use: Maestro is the instance's single
|
|
data plane, so an instance needs exactly one open port and a service
|
|
needs no public hostname.
|
|
|
|
|
|
DELIBERATELY NOT OWNER-GATED. Apps and phones hold no Maestro owner
|
|
session — that cookie belongs to the instance operator and must never be
|
|
shipped in an app. The service is the gate: it decides whether a request
|
|
needs a token and answers 401/403 itself, normally after validating a
|
|
token issued through dcntrlized-auth. Maestro never inspects credentials;
|
|
it is a dumb pipe.
|
|
|
|
|
|
Forwarded: the query string VERBATIM and every application header except
|
|
Host, Content-Length, the hop-by-hop set, Cookie and Authorization.
|
|
Cookie/Authorization are excluded on purpose: a Maestro session cookie
|
|
must never ride into a service, and services authenticate with their own
|
|
tokens (X-Access-Token), which are forwarded untouched.
|
|
|
|
|
|
The service's response is mirrored byte-for-byte — status, body,
|
|
Content-Type, redirects included (never followed) and its own error
|
|
envelope untouched. Only Maestro's own failures use the JSON envelope
|
|
{"error":{"code","message"}}. Any HTTP method is relayed identically
|
|
(GET/POST/PUT/PATCH/DELETE/HEAD/OPTIONS); see the one below for a
|
|
method-specific summary.
|
|
responses:
|
|
'200':
|
|
description: Mirrored from the service (status, body and headers unchanged).
|
|
'4XX':
|
|
description: >-
|
|
Mirrored from the service, including its own error envelope — the
|
|
service is the gate on this surface.
|
|
'404':
|
|
description: ERR_NOT_FOUND - unknown service (no installed container matches the reference).
|
|
'409':
|
|
description: ERR_NOT_RUNNING - the service container exists but is stopped.
|
|
'500':
|
|
description: >-
|
|
ERR_INTERNAL - Maestro could not resolve the service (Docker daemon
|
|
unavailable, no network address, no service port). Plumbing failure,
|
|
deliberately NOT a 502.
|
|
'502':
|
|
description: ERR_UPSTREAM_UNREACHABLE - the service did not answer.
|
|
put:
|
|
summary: Update through a service action
|
|
tags: ["services"]
|
|
description: Same contract as POST — the method, body and query are forwarded unchanged.
|
|
responses:
|
|
'200':
|
|
description: Mirrored from the service (status, body and headers unchanged).
|
|
'4XX':
|
|
description: >-
|
|
Mirrored from the service, including its own error envelope — the
|
|
service is the gate on this surface.
|
|
'404':
|
|
description: ERR_NOT_FOUND - unknown service (no installed container matches the reference).
|
|
'409':
|
|
description: ERR_NOT_RUNNING - the service container exists but is stopped.
|
|
'500':
|
|
description: >-
|
|
ERR_INTERNAL - Maestro could not resolve the service (Docker daemon
|
|
unavailable, no network address, no service port). Plumbing failure,
|
|
deliberately NOT a 502.
|
|
'502':
|
|
description: ERR_UPSTREAM_UNREACHABLE - the service did not answer.
|
|
delete:
|
|
summary: Delete through a service action
|
|
tags: ["services"]
|
|
description: Same contract as POST — the method, body and query are forwarded unchanged.
|
|
responses:
|
|
'200':
|
|
description: Mirrored from the service (status, body and headers unchanged).
|
|
'4XX':
|
|
description: >-
|
|
Mirrored from the service, including its own error envelope — the
|
|
service is the gate on this surface.
|
|
'404':
|
|
description: ERR_NOT_FOUND - unknown service (no installed container matches the reference).
|
|
'409':
|
|
description: ERR_NOT_RUNNING - the service container exists but is stopped.
|
|
'500':
|
|
description: >-
|
|
ERR_INTERNAL - Maestro could not resolve the service (Docker daemon
|
|
unavailable, no network address, no service port). Plumbing failure,
|
|
deliberately NOT a 502.
|
|
'502':
|
|
description: ERR_UPSTREAM_UNREACHABLE - the service did not answer.
|
|
/api/{service}/auth/{path...}:
|
|
parameters:
|
|
- name: service
|
|
in: path
|
|
required: true
|
|
description: >-
|
|
The service reference: full or partial container id, container name,
|
|
image or the org.dcntrlized.service label (matched case-insensitively
|
|
by Maestro's shared resolver).
|
|
schema:
|
|
type: string
|
|
- name: path
|
|
in: path
|
|
required: true
|
|
description: >-
|
|
The service's auth path, forwarded verbatim: /api/alarm/auth/login
|
|
reaches the alarm service's /auth/login. A prefix, not a fixed list —
|
|
a service adding an auth sub-path needs no Maestro change.
|
|
schema:
|
|
type: string
|
|
get:
|
|
summary: Call a service's auth endpoint (session, profile, refresh)
|
|
tags: ["services"]
|
|
description: >-
|
|
Forwards the request to the target service's /auth/{path...} and mirrors
|
|
the answer. The phone clients call /auth/login, /auth/register,
|
|
/auth/logout, /auth/me and /auth/refresh directly, so under the
|
|
single-origin model those must be reachable through Maestro without an
|
|
owner session.
|
|
|
|
|
|
Raw body semantics: the JSON body is forwarded untouched (services decode
|
|
{username,password[,email]} themselves), the response is mirrored
|
|
byte-for-byte, and Maestro never wraps, unwraps or inspects credentials.
|
|
Cookies issued by the service are NOT mirrored back (Maestro's origin is
|
|
the console's; a service token travels in the body or in its own
|
|
X-Access-Token header).
|
|
|
|
|
|
DELIBERATELY NOT OWNER-GATED, same reason as the action surface: the
|
|
caller is an app, the service is the gate.
|
|
responses:
|
|
'200':
|
|
description: Mirrored from the service.
|
|
'4XX':
|
|
description: Mirrored from the service (invalid credentials are its 401, not Maestro's).
|
|
'404':
|
|
description: ERR_NOT_FOUND - unknown service.
|
|
'409':
|
|
description: ERR_NOT_RUNNING - the service container exists but is stopped.
|
|
'500':
|
|
description: ERR_INTERNAL - Maestro could not resolve the service (never a 502).
|
|
'502':
|
|
description: ERR_UPSTREAM_UNREACHABLE - the service did not answer.
|
|
post:
|
|
summary: Authenticate against a service (login, register, logout, refresh)
|
|
tags: ["services"]
|
|
description: >-
|
|
Same contract as GET; the body and method are forwarded unchanged. This
|
|
is the call a phone makes before it holds any token at all, which is why
|
|
the surface cannot be owner-gated.
|
|
responses:
|
|
'200':
|
|
description: Mirrored from the service.
|
|
'4XX':
|
|
description: Mirrored from the service.
|
|
'404':
|
|
description: ERR_NOT_FOUND - unknown service.
|
|
'500':
|
|
description: ERR_INTERNAL - Maestro could not resolve the service.
|
|
'502':
|
|
description: ERR_UPSTREAM_UNREACHABLE - the service did not answer.
|
|
|
|
/api/{service}/ws:
|
|
parameters:
|
|
- name: service
|
|
in: path
|
|
required: true
|
|
description: >-
|
|
The service reference: full or partial container id, container name,
|
|
image or the org.dcntrlized.service label (matched case-insensitively
|
|
by Maestro's shared resolver).
|
|
schema:
|
|
type: string
|
|
get:
|
|
summary: Proxy a WebSocket to a service's socket namespace (service data plane)
|
|
tags: ["services"]
|
|
description: >-
|
|
Dials the target service's own socket (its /ws namespace) and pumps
|
|
frames verbatim in both directions. Part of the instance's single data
|
|
plane (see /api/{service}/action/{actionName} for the model):
|
|
Maestro resolves the service container on dcntrlized_net and then acts
|
|
as a dumb pipe.
|
|
|
|
|
|
DELIBERATELY NOT OWNER-GATED. The callers are phones, which hold no
|
|
Maestro owner session (that cookie belongs to the instance operator and
|
|
must never be shipped in an app). The service is the gate: it validates
|
|
the access token itself — normally via the X-Access-Token header
|
|
forwarded by this proxy, or the ?accessToken= query parameter — and
|
|
refuses the upgrade with 401 before accepting a single frame. Maestro
|
|
never inspects the token.
|
|
|
|
|
|
Forwarded: the query string VERBATIM (?alertId, ?token, ?accessToken …,
|
|
never re-encoded) and the X-Access-Token, Origin, User-Agent,
|
|
Accept-Language and X-Requested-With headers. Not forwarded: Host,
|
|
Connection/Upgrade, Sec-WebSocket-*, Content-Length, Authorization and
|
|
cookies (a Maestro session cookie must never ride into a service).
|
|
|
|
|
|
Framing is end-to-end: ping/pong control frames are relayed to the other
|
|
hop instead of being answered by Maestro (so a service's own ping/pong
|
|
liveness accounting is truthful), close codes and reasons are propagated
|
|
unchanged, and no read/write deadline is imposed on the socket — an idle
|
|
alarm socket lives until one side closes it.
|
|
responses:
|
|
'101':
|
|
description: >-
|
|
Switching Protocols — the service accepted the socket. Statuses
|
|
below are answered before the upgrade, while the response is still
|
|
modifiable.
|
|
'400':
|
|
description: >-
|
|
ERR_INVALID_REQUEST - the request is not a WebSocket upgrade
|
|
("this endpoint only accepts WebSocket upgrades").
|
|
'401':
|
|
description: >-
|
|
ERR_UNAUTHORIZED - mirrored from the service: it rejected the
|
|
handshake (missing or invalid access token / alert token). Body is
|
|
the service's own, never reformatted.
|
|
'403':
|
|
description: >-
|
|
Mirrored from the service (token present but not authorized). Body
|
|
is the service's own.
|
|
'404':
|
|
description: >-
|
|
ERR_NOT_FOUND - unknown service; no installed container matches the
|
|
reference.
|
|
'409':
|
|
description: >-
|
|
ERR_NOT_RUNNING - the service container exists but is not running.
|
|
'500':
|
|
description: >-
|
|
ERR_INTERNAL - Maestro could not resolve the service (Docker daemon
|
|
unavailable, no network address, no service port). Plumbing failure,
|
|
deliberately NOT a 502.
|
|
'502':
|
|
description: >-
|
|
ERR_UPSTREAM_UNREACHABLE - the service's socket could not be reached
|
|
(dial failed, connect refused, handshake timeout).
|
|
|
|
/api/{service}/ws/{path...}:
|
|
parameters:
|
|
- name: service
|
|
in: path
|
|
required: true
|
|
description: >-
|
|
The service reference: full or partial container id, container name,
|
|
image or the org.dcntrlized.service label (matched case-insensitively
|
|
by Maestro's shared resolver).
|
|
schema:
|
|
type: string
|
|
- name: path
|
|
in: path
|
|
required: false
|
|
description: >-
|
|
Sub-path below the service's /ws namespace, forwarded verbatim: a
|
|
service that mounts its socket at /ws/alarm is reachable as
|
|
/api/alarm/ws/alarm.
|
|
schema:
|
|
type: string
|
|
get:
|
|
summary: Proxy a WebSocket to a sub-path of a service's socket namespace
|
|
tags: ["services"]
|
|
description: >-
|
|
Same contract, framing and status codes as /api/{service}/ws. Scope is
|
|
deliberately narrow: only /ws and /ws/... on the service are reachable
|
|
through this path, so /config and /_manifest can never be tunnelled
|
|
through it (/auth/* has its own allowlisted namespace at
|
|
/api/{service}/auth/{path...}). Dot
|
|
segments and unusual characters in {path...} are rejected (404).
|
|
|
|
|
|
Implementation note for future readers (this bit is not visible in the
|
|
URL space): NO pattern of this surface can be registered directly next
|
|
to /api/actions/{containerId}/{actionName} — this one and the action form
|
|
both match /api/actions/<literal>/<x>, neither is more specific, and the
|
|
mux panics at boot. The whole surface (action, auth, ws) is therefore
|
|
dispatched by a single fallback registered on /api/, which the mux only
|
|
reaches when no concrete route matched.
|
|
That fallback is also what answers an unknown /api/* path, and it does so
|
|
with the uniform envelope {"error":{"code":"ERR_NOT_FOUND","message":
|
|
"unknown API path"}} — never plain text, because clients unwrap the
|
|
envelope.
|
|
responses:
|
|
'101':
|
|
description: Switching Protocols - WebSocket upgrade
|
|
'400':
|
|
description: ERR_INVALID_REQUEST - not a WebSocket upgrade.
|
|
'401':
|
|
description: Mirrored from the service (invalid access token).
|
|
'404':
|
|
description: >-
|
|
ERR_NOT_FOUND - unknown service, a sub-path outside the service's
|
|
/ws namespace, or (from the fallback) an unknown /api/* path.
|
|
'409':
|
|
description: ERR_NOT_RUNNING - the service is not running.
|
|
'500':
|
|
description: ERR_INTERNAL - Maestro could not resolve the service.
|
|
'502':
|
|
description: >-
|
|
ERR_UPSTREAM_UNREACHABLE - the service's socket could not be
|
|
reached.
|
|
|
|
/api/catalog/apps:
|
|
get:
|
|
summary: List the operator-authored app catalog (owner only)
|
|
tags: ["catalog"]
|
|
description: >-
|
|
Every catalog record with absolute download/icon URLs and the private
|
|
added_by field (app-catalog theme A1/A7). Insertion order is stable and
|
|
an empty catalog is [].
|
|
responses:
|
|
'200':
|
|
description: Catalog entries
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/CatalogApp'
|
|
'401':
|
|
description: ERR_UNAUTHENTICATED - no valid owner session
|
|
'428':
|
|
description: ERR_SETUP_REQUIRED - owner setup not completed
|
|
post:
|
|
summary: Add an app from an artifact URL (owner only)
|
|
tags: ["catalog"]
|
|
description: >-
|
|
Fetches artifact_url (redirects followed, 60s timeout, 512 MiB cap),
|
|
computes size_bytes + sha256 and hosts the bytes, then answers 201. A
|
|
supplied sha256 that does not match stores nothing (400
|
|
ERR_CHECKSUM_MISMATCH). link_only:true skips the fetch, requires the
|
|
operator sha256 and stores kind:"link". Fetch failure → 502
|
|
ERR_ARTIFACT_FETCH; duplicate package → 409 ERR_APP_EXISTS. Also accepts
|
|
multipart/form-data with the same flat snake_case fields plus the
|
|
artifact in the part named `file`, so a new app can be published from a
|
|
local file in one step.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/CatalogAppAdd'
|
|
multipart/form-data:
|
|
schema:
|
|
type: object
|
|
required: [package, name, version_name]
|
|
properties:
|
|
file:
|
|
type: string
|
|
format: binary
|
|
description: The artifact to host; when present artifact_url is not required.
|
|
package:
|
|
type: string
|
|
name:
|
|
type: string
|
|
version_name:
|
|
type: string
|
|
summary:
|
|
type: string
|
|
description:
|
|
type: string
|
|
category:
|
|
type: string
|
|
version_code:
|
|
type: integer
|
|
min_android:
|
|
type: integer
|
|
icon_url:
|
|
type: string
|
|
source_url:
|
|
type: string
|
|
filename:
|
|
type: string
|
|
content_type:
|
|
type: string
|
|
sha256:
|
|
type: string
|
|
signer_sha256:
|
|
type: string
|
|
link_only:
|
|
type: boolean
|
|
responses:
|
|
'201':
|
|
description: App created (hosted artifact or link)
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/CatalogApp'
|
|
'400':
|
|
description: Invalid payload or checksum mismatch (ERR_INVALID_PAYLOAD / ERR_CHECKSUM_MISMATCH)
|
|
'401':
|
|
description: ERR_UNAUTHENTICATED - no valid owner session
|
|
'409':
|
|
description: Duplicate package (ERR_APP_EXISTS)
|
|
'428':
|
|
description: ERR_SETUP_REQUIRED - owner setup not completed
|
|
'502':
|
|
description: Artifact fetch failed (ERR_ARTIFACT_FETCH)
|
|
|
|
/api/catalog/apps/{id}:
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
description: The stable app id ("a_" + sha256(package)[:12]).
|
|
schema:
|
|
type: string
|
|
put:
|
|
summary: Edit an app's metadata (owner only)
|
|
tags: ["catalog"]
|
|
description: >-
|
|
Full replacement of the editable metadata: present fields are applied,
|
|
optional fields omitted (or null/empty) are cleared, and name +
|
|
version_name are required. package and id are immutable (a changed
|
|
package → 400 ERR_INVALID_PAYLOAD). Unknown id → 404 ERR_NOT_FOUND. The
|
|
artifact is replaced through its own endpoint.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/CatalogAppEdit'
|
|
responses:
|
|
'200':
|
|
description: Updated app
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/CatalogApp'
|
|
'400':
|
|
description: Invalid payload or immutable package (ERR_INVALID_PAYLOAD)
|
|
'401':
|
|
description: ERR_UNAUTHENTICATED - no valid owner session
|
|
'404':
|
|
description: Unknown app (ERR_NOT_FOUND)
|
|
'428':
|
|
description: ERR_SETUP_REQUIRED - owner setup not completed
|
|
delete:
|
|
summary: Remove an app and its hosted bytes (owner only)
|
|
tags: ["catalog"]
|
|
description: >-
|
|
Drops the record, the hosted artifact copy and the uploaded icon.
|
|
Unknown id → 404 ERR_NOT_FOUND.
|
|
responses:
|
|
'200':
|
|
description: App removed
|
|
'401':
|
|
description: ERR_UNAUTHENTICATED - no valid owner session
|
|
'404':
|
|
description: Unknown app (ERR_NOT_FOUND)
|
|
'428':
|
|
description: ERR_SETUP_REQUIRED - owner setup not completed
|
|
|
|
/api/catalog/apps/{id}/artifact:
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
put:
|
|
summary: Replace an app's artifact from a multipart upload (owner only)
|
|
tags: ["catalog"]
|
|
description: >-
|
|
multipart/form-data with a `file` part. Recomputes size_bytes + sha256,
|
|
switches kind to "hosted", and prunes the previous copy. 512 MiB cap;
|
|
unknown id → 404.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
multipart/form-data:
|
|
schema:
|
|
type: object
|
|
required: [file]
|
|
properties:
|
|
file:
|
|
type: string
|
|
format: binary
|
|
responses:
|
|
'200':
|
|
description: Updated app
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/CatalogApp'
|
|
'400':
|
|
description: Missing/invalid upload or size cap (ERR_INVALID_PAYLOAD)
|
|
'404':
|
|
description: Unknown app (ERR_NOT_FOUND)
|
|
|
|
/api/catalog/apps/{id}/icon:
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
put:
|
|
summary: Replace an app's uploaded icon (owner only)
|
|
tags: ["catalog"]
|
|
description: >-
|
|
multipart/form-data with a `file` part; png, jpeg or webp, at most
|
|
512 KiB. The icon is then served at /api/catalog/icons/{id}.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
multipart/form-data:
|
|
schema:
|
|
type: object
|
|
required: [file]
|
|
properties:
|
|
file:
|
|
type: string
|
|
format: binary
|
|
responses:
|
|
'200':
|
|
description: Updated app
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/CatalogApp'
|
|
'400':
|
|
description: Missing/invalid image or size cap (ERR_INVALID_PAYLOAD)
|
|
'404':
|
|
description: Unknown app (ERR_NOT_FOUND)
|
|
|
|
/api/catalog/artifacts/{id}/{filename}:
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
- name: filename
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
get:
|
|
summary: Download a catalog artifact (unauthenticated)
|
|
tags: ["catalog"]
|
|
security: []
|
|
description: >-
|
|
Exact artifact bytes with Content-Type, Content-Length and
|
|
X-Content-SHA256, plus Access-Control-Allow-Origin: * for browser
|
|
clients. kind:"link" entries answer 302 to the operator URL. Unknown
|
|
entries and DCNTRLIZED_PUBLIC_PROFILE=false answer 404 ERR_NOT_FOUND.
|
|
responses:
|
|
'200':
|
|
description: Artifact bytes
|
|
content:
|
|
application/octet-stream:
|
|
schema:
|
|
type: string
|
|
format: binary
|
|
'302':
|
|
description: Redirect to the operator-supplied URL (kind=link)
|
|
'404':
|
|
description: Unknown artifact or catalog not public (ERR_NOT_FOUND)
|
|
|
|
/api/catalog/icons/{id}:
|
|
parameters:
|
|
- name: id
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
get:
|
|
summary: Fetch an app's uploaded icon (unauthenticated)
|
|
tags: ["catalog"]
|
|
security: []
|
|
description: >-
|
|
Uploaded icon bytes, or 404 ERR_NOT_FOUND when the app has no uploaded
|
|
icon or DCNTRLIZED_PUBLIC_PROFILE=false.
|
|
responses:
|
|
'200':
|
|
description: Icon bytes
|
|
content:
|
|
image/png:
|
|
schema:
|
|
type: string
|
|
format: binary
|
|
'404':
|
|
description: Unknown icon or catalog not public (ERR_NOT_FOUND)
|
|
|
|
/api/web-links:
|
|
get:
|
|
summary: Read the operator Web App URL sources (owner)
|
|
tags: ["containers"]
|
|
description: >-
|
|
The owner-controlled base-domain template and per-service overrides that
|
|
Maestro layers between a service's manifest web.url and the derived
|
|
request-host fallback (contract rev 8.2 §16.2). The global template can
|
|
also be supplied via DCNTRLIZED_WEB_BASE_DOMAIN, which wins over the
|
|
persisted value.
|
|
responses:
|
|
'200':
|
|
description: Current sources
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/WebLinksConfig'
|
|
'401':
|
|
description: No owner session (ERR_UNAUTHENTICATED)
|
|
|
|
put:
|
|
summary: Replace the operator Web App URL sources (owner)
|
|
tags: ["containers"]
|
|
description: >-
|
|
Full replacement: omitted fields clear. The template must contain the
|
|
literal <service> placeholder; every value must be an http(s) base URL
|
|
with a host, no userinfo and no control characters.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/WebLinksConfig'
|
|
responses:
|
|
'200':
|
|
description: Saved sources
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/WebLinksConfig'
|
|
'400':
|
|
description: Invalid base URL or template (ERR_INVALID_PAYLOAD)
|
|
'401':
|
|
description: No owner session (ERR_UNAUTHENTICATED)
|
|
|
|
/api/services/{service}/config:
|
|
parameters:
|
|
- name: service
|
|
in: path
|
|
required: true
|
|
description: >-
|
|
The service (matched case-insensitively against the install record /
|
|
container label, as in /api/containers).
|
|
schema:
|
|
type: string
|
|
get:
|
|
summary: Merge a service's declared config schema with its stored values
|
|
tags: ["service-config"]
|
|
description: >-
|
|
Owner-gated (Service Config & Secrets theme M1). Lists each field the
|
|
service's manifest config block declares, merged with the operator's
|
|
set state. Password fields are only ever reported as set:false/true;
|
|
cleartext fields (read-write) carry their stored value.
|
|
responses:
|
|
'200':
|
|
description: The merged per-field view
|
|
content:
|
|
application/json:
|
|
schema:
|
|
type: array
|
|
items:
|
|
$ref: '#/components/schemas/ServiceConfigEntry'
|
|
'401':
|
|
description: ERR_UNAUTHENTICATED - no valid owner session
|
|
'404':
|
|
description: ERR_NOT_FOUND - unknown service
|
|
'428':
|
|
description: ERR_SETUP_REQUIRED - owner setup not completed
|
|
|
|
/api/services/{service}/config/{key}:
|
|
parameters:
|
|
- name: service
|
|
in: path
|
|
required: true
|
|
schema:
|
|
type: string
|
|
- name: key
|
|
in: path
|
|
required: true
|
|
description: The manifest config field name (the config array element key).
|
|
schema:
|
|
type: string
|
|
put:
|
|
summary: Set a service config value
|
|
tags: ["service-config"]
|
|
description: >-
|
|
Owner-gated (Service Config & Secrets theme M1). Stores the value for
|
|
the field declared by the service manifest. Cleartext fields are
|
|
stored plaintext (read-write); password fields are AES-256-GCM
|
|
encrypted at rest, keyed by <dataRoot>/maestro/secrets.key, and never
|
|
returned again — the response is only a ServiceConfigEntry with set:true.
|
|
The value is injected as the field's env key at start, and, when the
|
|
service is running and exposes POST /config (D6 write-back), delivered
|
|
to it at runtime. Delivery targets the service's single plaintext 8080
|
|
listener and carries an ES256 Bearer token minted with Maestro's
|
|
DEDICATED config-signing keypair (<dataRoot>/maestro/config-signing.key,
|
|
NOT the retired instance CA): iss=dcntrlized-maestro, aud=sub=<service>,
|
|
scope="config", 60 s TTL. The service verifies it with the
|
|
DCNTRLIZED_CONFIG_PUBKEY (base64 DER SPKI) Maestro injects and can never
|
|
mint tokens itself.
|
|
requestBody:
|
|
required: true
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/ServiceConfigValue'
|
|
responses:
|
|
'200':
|
|
description: Value stored
|
|
content:
|
|
application/json:
|
|
schema:
|
|
$ref: '#/components/schemas/ServiceConfigEntry'
|
|
'400':
|
|
description: ERR_INVALID_PAYLOAD - unparseable body or empty password
|
|
'401':
|
|
description: ERR_UNAUTHENTICATED - no valid owner session
|
|
'404':
|
|
description: ERR_NOT_FOUND - unknown service or config field
|
|
'428':
|
|
description: ERR_SETUP_REQUIRED - owner setup not completed
|
|
delete:
|
|
summary: Clear a service config value
|
|
tags: ["service-config"]
|
|
description: >-
|
|
Owner-gated (Service Config & Secrets theme M1). Removes the stored
|
|
value; on the next start the compose file's own default applies. When
|
|
the service is running and exposes POST /config (D6 write-back), a
|
|
best-effort clear {field, action:"clear"} is delivered at runtime (same
|
|
plaintext 8080 listener + ES256 Bearer token) so the running process
|
|
drops the value immediately instead of waiting for a restart.
|
|
responses:
|
|
'200':
|
|
description: Value cleared
|
|
'401':
|
|
description: ERR_UNAUTHENTICATED - no valid owner session
|
|
'404':
|
|
description: ERR_NOT_FOUND - unknown service or config field
|
|
'428':
|
|
description: ERR_SETUP_REQUIRED - owner setup not completed
|