feat: M2 — management REST API with OpenAPI doc (control-pane groundwork)

A versioned control-plane REST API (/api/v1) on its own port (default
127.0.0.1:47990) serving host info, runtime status, paired-client
management, the pairing PIN flow, and session control (stop / force-IDR).
The OpenAPI 3.1 document is generated from the handlers by utoipa, served
live at /api/v1/openapi.json (+ Scalar docs at /api/docs), printable via
`lumen-host openapi`, and checked in at docs/api/openapi.json for client
codegen — a test fails if it drifts, mirroring the cbindgen header rule.

Auth: optional bearer token (--mgmt-token / LUMEN_MGMT_TOKEN), enforced on
everything but /health, and mandatory for non-loopback binds. PinGate
gains a waiter count so the API can report pin_pending; logs moved to
stderr so stdout stays machine-readable. Supersedes the web.rs stub.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-09 21:35:43 +00:00
parent 22a982a1cb
commit a339a0466e
10 changed files with 1862 additions and 49 deletions
+719
View File
@@ -0,0 +1,719 @@
{
"openapi": "3.1.0",
"info": {
"title": "lumen management API",
"description": "Control-plane API for managing a lumen streaming host: host capabilities, runtime status, paired clients, the pairing PIN flow, and session control. Authentication: HTTP bearer token, enforced on every route except `/api/v1/health` when the host is started with a management token (mandatory for non-loopback binds).",
"contact": {
"name": "unom"
},
"license": {
"name": "MIT OR Apache-2.0",
"identifier": "MIT OR Apache-2.0"
},
"version": "0.0.1"
},
"paths": {
"/api/v1/clients": {
"get": {
"tags": [
"clients"
],
"summary": "List paired clients",
"operationId": "listPairedClients",
"responses": {
"200": {
"description": "All certificate-pinned clients",
"content": {
"application/json": {
"schema": {
"type": "array",
"items": {
"$ref": "#/components/schemas/PairedClient"
}
}
}
}
},
"401": {
"description": "Missing or invalid bearer token",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
}
}
}
},
"/api/v1/clients/{fingerprint}": {
"delete": {
"tags": [
"clients"
],
"summary": "Unpair a client",
"description": "Removes the pinned certificate; the client must pair again to reconnect.",
"operationId": "unpairClient",
"parameters": [
{
"name": "fingerprint",
"in": "path",
"description": "Hex SHA-256 fingerprint of the client certificate DER (64 chars, case-insensitive)",
"required": true,
"schema": {
"type": "string"
}
}
],
"responses": {
"204": {
"description": "Client unpaired"
},
"400": {
"description": "Malformed fingerprint",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"401": {
"description": "Missing or invalid bearer token",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"404": {
"description": "No paired client with that fingerprint",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
}
}
}
},
"/api/v1/health": {
"get": {
"tags": [
"host"
],
"summary": "Liveness probe",
"description": "Always available without authentication.",
"operationId": "getHealth",
"responses": {
"200": {
"description": "Host is up",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Health"
}
}
}
}
}
}
},
"/api/v1/host": {
"get": {
"tags": [
"host"
],
"summary": "Host identity and capabilities",
"operationId": "getHostInfo",
"responses": {
"200": {
"description": "Host identity, versions, codecs, and port map",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/HostInfo"
}
}
}
},
"401": {
"description": "Missing or invalid bearer token",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
}
}
}
},
"/api/v1/pair": {
"get": {
"tags": [
"pairing"
],
"summary": "Pairing-flow status",
"description": "Poll this to know when to prompt the user for the PIN Moonlight displays.",
"operationId": "getPairingStatus",
"responses": {
"200": {
"description": "Whether a pairing handshake is waiting for a PIN",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/PairingStatus"
}
}
}
},
"401": {
"description": "Missing or invalid bearer token",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
}
}
}
},
"/api/v1/pair/pin": {
"post": {
"tags": [
"pairing"
],
"summary": "Submit the pairing PIN",
"description": "Delivers the PIN the Moonlight client is displaying, completing the out-of-band half\nof the pairing handshake.",
"operationId": "submitPairingPin",
"requestBody": {
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SubmitPin"
}
}
},
"required": true
},
"responses": {
"204": {
"description": "PIN delivered to the waiting handshake"
},
"400": {
"description": "Malformed PIN",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"401": {
"description": "Missing or invalid bearer token",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"409": {
"description": "No pairing handshake is waiting for a PIN",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
}
}
}
},
"/api/v1/session": {
"delete": {
"tags": [
"session"
],
"summary": "Stop the active session",
"description": "Kicks the connected client: stops the video/audio stream threads and clears the launch\nstate. Idempotent — succeeds even when nothing is streaming.",
"operationId": "stopSession",
"responses": {
"204": {
"description": "Session stopped (or none was active)"
},
"401": {
"description": "Missing or invalid bearer token",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
}
}
}
},
"/api/v1/session/idr": {
"post": {
"tags": [
"session"
],
"summary": "Force a keyframe",
"description": "Asks the encoder for an IDR frame on the active video stream (what a client requests\nafter unrecoverable loss — exposed for debugging).",
"operationId": "requestIdr",
"responses": {
"202": {
"description": "Keyframe requested"
},
"401": {
"description": "Missing or invalid bearer token",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
},
"409": {
"description": "No active video stream",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
}
}
}
},
"/api/v1/status": {
"get": {
"tags": [
"host"
],
"summary": "Live host status",
"operationId": "getStatus",
"responses": {
"200": {
"description": "Streaming/pairing state and the active session, if any",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/RuntimeStatus"
}
}
}
},
"401": {
"description": "Missing or invalid bearer token",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ApiError"
}
}
}
}
}
}
}
},
"components": {
"schemas": {
"ApiCodec": {
"type": "string",
"description": "Video codec identifier.",
"enum": [
"h264",
"h265",
"av1"
]
},
"ApiError": {
"type": "object",
"description": "Error envelope for every non-2xx response.",
"required": [
"error"
],
"properties": {
"error": {
"type": "string"
}
}
},
"Health": {
"type": "object",
"description": "Liveness + version probe.",
"required": [
"status",
"version",
"abi_version"
],
"properties": {
"abi_version": {
"type": "integer",
"format": "int32",
"description": "`lumen-core` C ABI version.",
"minimum": 0
},
"status": {
"type": "string",
"description": "Always `\"ok\"` when the host responds.",
"example": "ok"
},
"version": {
"type": "string",
"description": "`lumen-host` crate version."
}
}
},
"HostInfo": {
"type": "object",
"description": "Host identity and advertised capabilities (static for the life of the process).",
"required": [
"hostname",
"uniqueid",
"local_ip",
"version",
"abi_version",
"app_version",
"gfe_version",
"codecs",
"ports"
],
"properties": {
"abi_version": {
"type": "integer",
"format": "int32",
"description": "`lumen-core` C ABI version.",
"minimum": 0
},
"app_version": {
"type": "string",
"description": "GameStream host version advertised to Moonlight clients."
},
"codecs": {
"type": "array",
"items": {
"$ref": "#/components/schemas/ApiCodec"
},
"description": "Codecs the host can encode (NVENC)."
},
"gfe_version": {
"type": "string",
"description": "GFE version advertised to Moonlight clients."
},
"hostname": {
"type": "string"
},
"local_ip": {
"type": "string",
"description": "Best-effort primary LAN IP."
},
"ports": {
"$ref": "#/components/schemas/PortMap"
},
"uniqueid": {
"type": "string",
"description": "Stable per-host id (persisted across restarts), matched on pairing."
},
"version": {
"type": "string",
"description": "`lumen-host` crate version."
}
}
},
"PairedClient": {
"type": "object",
"description": "A paired (certificate-pinned) Moonlight client.",
"required": [
"fingerprint"
],
"properties": {
"fingerprint": {
"type": "string",
"description": "Lowercase hex SHA-256 of the client certificate DER — the client's stable id here.",
"example": "9f86d081884c7d659a2feaa0c55ad015a3bf4f1b2b0b822cd15d6c15b0f00a08"
},
"not_after_unix": {
"type": [
"integer",
"null"
],
"format": "int64",
"description": "Certificate validity end (unix seconds)."
},
"not_before_unix": {
"type": [
"integer",
"null"
],
"format": "int64",
"description": "Certificate validity start (unix seconds)."
},
"subject": {
"type": [
"string",
"null"
],
"description": "Certificate subject (e.g. `CN=NVIDIA GameStream Client`), if the DER parses."
}
}
},
"PairingStatus": {
"type": "object",
"description": "Pairing-flow status.",
"required": [
"pin_pending"
],
"properties": {
"pin_pending": {
"type": "boolean",
"description": "True while a pairing handshake is parked waiting for the user's PIN."
}
}
},
"PortMap": {
"type": "object",
"description": "Every port a client integration may need (Moonlight derives the stream ports from the\nHTTP base; a control pane should not have to).",
"required": [
"mgmt",
"http",
"https",
"rtsp",
"video",
"control",
"audio"
],
"properties": {
"audio": {
"type": "integer",
"format": "int32",
"minimum": 0
},
"control": {
"type": "integer",
"format": "int32",
"minimum": 0
},
"http": {
"type": "integer",
"format": "int32",
"description": "nvhttp plain HTTP (serverinfo, pairing).",
"minimum": 0
},
"https": {
"type": "integer",
"format": "int32",
"description": "nvhttp mutual-TLS HTTPS (post-pairing).",
"minimum": 0
},
"mgmt": {
"type": "integer",
"format": "int32",
"description": "This management API.",
"minimum": 0
},
"rtsp": {
"type": "integer",
"format": "int32",
"minimum": 0
},
"video": {
"type": "integer",
"format": "int32",
"minimum": 0
}
}
},
"RuntimeStatus": {
"type": "object",
"description": "Live host status (changes as clients launch/end sessions).",
"required": [
"video_streaming",
"audio_streaming",
"pin_pending",
"paired_clients"
],
"properties": {
"audio_streaming": {
"type": "boolean",
"description": "True while the audio stream thread is running."
},
"paired_clients": {
"type": "integer",
"format": "int32",
"description": "Number of pinned (paired) client certificates.",
"minimum": 0
},
"pin_pending": {
"type": "boolean",
"description": "True while a pairing handshake is parked waiting for the user's PIN\n(submit it via `POST /api/v1/pair/pin`)."
},
"session": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/SessionInfo",
"description": "The active launch session (set by Moonlight's `/launch`, cleared on cancel/stop)."
}
]
},
"stream": {
"oneOf": [
{
"type": "null"
},
{
"$ref": "#/components/schemas/StreamInfo",
"description": "The RTSP-negotiated stream parameters (present once a client has completed ANNOUNCE)."
}
]
},
"video_streaming": {
"type": "boolean",
"description": "True while the video stream thread is running."
}
}
},
"SessionInfo": {
"type": "object",
"description": "Client-requested launch parameters (key material is never exposed here).",
"required": [
"width",
"height",
"fps"
],
"properties": {
"fps": {
"type": "integer",
"format": "int32",
"minimum": 0
},
"height": {
"type": "integer",
"format": "int32",
"minimum": 0
},
"width": {
"type": "integer",
"format": "int32",
"minimum": 0
}
}
},
"StreamInfo": {
"type": "object",
"description": "RTSP-negotiated stream parameters.",
"required": [
"width",
"height",
"fps",
"bitrate_kbps",
"packet_size",
"min_fec",
"codec"
],
"properties": {
"bitrate_kbps": {
"type": "integer",
"format": "int32",
"minimum": 0
},
"codec": {
"$ref": "#/components/schemas/ApiCodec"
},
"fps": {
"type": "integer",
"format": "int32",
"minimum": 0
},
"height": {
"type": "integer",
"format": "int32",
"minimum": 0
},
"min_fec": {
"type": "integer",
"format": "int32",
"description": "Client's parity floor per FEC block (`minRequiredFecPackets`).",
"minimum": 0
},
"packet_size": {
"type": "integer",
"format": "int32",
"description": "Video payload size per packet (bytes).",
"minimum": 0
},
"width": {
"type": "integer",
"format": "int32",
"minimum": 0
}
}
},
"SubmitPin": {
"type": "object",
"description": "The PIN Moonlight displays during pairing.",
"required": [
"pin"
],
"properties": {
"pin": {
"type": "string",
"description": "116 ASCII digits (Moonlight shows 4).",
"example": "1234"
}
}
}
},
"securitySchemes": {
"bearerAuth": {
"type": "http",
"scheme": "bearer"
}
}
},
"security": [
{
"bearerAuth": []
}
],
"tags": [
{
"name": "host",
"description": "Host identity, capabilities, and liveness"
},
{
"name": "clients",
"description": "Paired Moonlight client management"
},
{
"name": "pairing",
"description": "Pairing PIN delivery (the out-of-band half of the GameStream pairing handshake)"
},
{
"name": "session",
"description": "Active streaming session control"
}
]
}