Files
Maestro/spec.yaml
T
Admin 2ffdf77905 feat(auth): instance-owner gate — owner store, JWT sessions, setup/login UI (onboarding M1+M2)
lib/owner: argon2id credential store, HS256 access/refresh pair with kid,
hashed-jti session store, throttler (5 free failures then 4^n backoff).
handlers/auth: /api/auth/* + RequireSession/CSRF/GateWebSocket middleware;
every /api/* route is now gated (428 in setup mode). --reset-owner recovery.
Frontend: AuthScreen (setup/login), useAuth gate hook, gated data hooks +
single-flight silent refresh, Owner account section in settings.
spec.yaml documents the full surface.
2026-08-22 22:58:18 +01:00

752 lines
22 KiB
YAML

openapi: 3.0.3
info:
title: Dcntrlized Maestro API
version: 1.0.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/* 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. Mutating requests from browser origins are additionally checked against the request host (CSRF).
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
RegistryContainer:
type: object
required: [name, image]
properties:
name:
type: string
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
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
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.
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.
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
paths:
/api/actions/{containerId}/{actionName}:
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/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.
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)
/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
/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)
/api/ws:
get:
summary: WebSocket endpoint for live container events
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).
responses:
'101':
description: Switching Protocols - WebSocket upgrade
'401':
description: Not authenticated
'428':
description: Setup required
/api/peers:
get:
summary: List pending authorizations and the approved peer trust store
tags: ["security"]
description: >
Security theme M2 §3. Returns the shared auth service's first-contact
authorization requests (services whose certificate is valid but has
not been approved by the instance owner yet) and the approved keys.
Maestro is only the UI; the auth service owns the enforcement.
responses:
'200':
description: Combined pending + approved trust state
content:
application/json:
schema:
type: object
properties:
pending:
type: array
items:
$ref: '#/components/schemas/PendingAuthorization'
authorized:
type: array
items:
$ref: '#/components/schemas/AuthorizedKey'
'500':
description: Internal server error (auth management client unavailable)
'502':
description: Failed to reach the shared auth service
/api/peers/attestation:
get:
summary: Read the auth service's public signing key statement
tags: ["security"]
description: >
The auth service's RSA public key, its kid, and the sha256 fingerprint
peers should pin after first contact (TOFU, D3).
responses:
'200':
description: Auth public key attestation
content:
application/json:
schema:
$ref: '#/components/schemas/Attestation'
'502':
description: Failed to reach the shared auth service
/api/peers/approve:
post:
summary: Approve a peer's fingerprint in the auth service trust store
tags: ["security"]
description: >
Forwards the instance owner's approval for a service's identity
fingerprint. After this call the service's requests to /auth/* proceed
normally (enforced by the auth service itself).
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/PeerAction'
responses:
'200':
description: Peer approved
'400':
description: Missing service or fingerprint
'502':
description: Failed to reach the shared auth service
/api/peers/revoke:
post:
summary: Revoke a peer's fingerprint (returns it to the no-op path)
tags: ["security"]
description: >
Forwards the instance owner's revocation. Calls from that fingerprint
become uniform 403 no-ops again immediately (the SSH authorized_keys
"delete the line" action).
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/PeerAction'
responses:
'200':
description: Peer revoked
'400':
description: Missing service or fingerprint
'502':
description: Failed to reach the shared auth service
/api/peers/reject:
post:
summary: Reject a pending peer authorization request
tags: ["security"]
description: >
Denies a first-contact authorization request without authorizing it.
The auth service drops the request from the pending list and audits
the decision; the peer stays on the no-op path (a later contact simply
re-records a fresh pending request).
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/PeerAction'
responses:
'200':
description: Pending request rejected
'400':
description: Missing service or fingerprint
'502':
description: Failed to reach the shared auth service