feat(host): unified host + native pairing over the management API
`serve --native` now runs the GameStream host AND the native punktfunk/1 (QUIC) host in ONE process, sharing a single NativePairing handle with the management API — so native pairing is operable from the web console instead of journalctl. - gamestream::serve gains a native_port: spawns crate::m3::serve in the same runtime and passes the shared NativePairing to mgmt::run. Validated live: one process binds both RTSP 48010 and QUIC 9777. - mgmt API: new `native` endpoints — GET /native/pair (status), POST /native/pair/arm (mint a fresh, time-limited PIN to DISPLAY), DELETE /native/pair (disarm), GET/DELETE /native/clients (list/unpair). GameStream-only hosts report enabled:false. OpenAPI regenerated (checked-in doc + drift test). - main.rs: serve --native / --native-port flags. The native host arms pairing on demand (the operator reads the PIN from the console; the SPAKE2 ceremony is host-shows-PIN). New mgmt + native_pairing tests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -194,6 +194,213 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/native/clients": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"native"
|
||||
],
|
||||
"summary": "List native paired clients",
|
||||
"operationId": "listNativeClients",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Paired native clients",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"$ref": "#/components/schemas/NativeClient"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Missing or invalid bearer token",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/native/clients/{fingerprint}": {
|
||||
"delete": {
|
||||
"tags": [
|
||||
"native"
|
||||
],
|
||||
"summary": "Unpair a native client",
|
||||
"description": "Removes a punktfunk/1 client from the native trust store by fingerprint.",
|
||||
"operationId": "unpairNativeClient",
|
||||
"parameters": [
|
||||
{
|
||||
"name": "fingerprint",
|
||||
"in": "path",
|
||||
"description": "Hex SHA-256 of the client certificate (case-insensitive)",
|
||||
"required": true,
|
||||
"schema": {
|
||||
"type": "string"
|
||||
}
|
||||
}
|
||||
],
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "Client unpaired"
|
||||
},
|
||||
"401": {
|
||||
"description": "Missing or invalid bearer token",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"404": {
|
||||
"description": "No paired native client with that fingerprint",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"503": {
|
||||
"description": "Native host not enabled",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/native/pair": {
|
||||
"get": {
|
||||
"tags": [
|
||||
"native"
|
||||
],
|
||||
"summary": "Native pairing status",
|
||||
"description": "The native (punktfunk/1) pairing window. Poll while armed to show the PIN + countdown.\n`enabled: false` means this host runs GameStream only (no `--native`).",
|
||||
"operationId": "getNativePairing",
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Native pairing status",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/NativePairStatus"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Missing or invalid bearer token",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"delete": {
|
||||
"tags": [
|
||||
"native"
|
||||
],
|
||||
"summary": "Disarm native pairing",
|
||||
"description": "Closes the pairing window immediately (no new ceremonies accepted).",
|
||||
"operationId": "disarmNativePairing",
|
||||
"responses": {
|
||||
"204": {
|
||||
"description": "Pairing disarmed"
|
||||
},
|
||||
"401": {
|
||||
"description": "Missing or invalid bearer token",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"503": {
|
||||
"description": "Native host not enabled",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/native/pair/arm": {
|
||||
"post": {
|
||||
"tags": [
|
||||
"native"
|
||||
],
|
||||
"summary": "Arm native pairing",
|
||||
"description": "Opens a pairing window and mints a fresh PIN to display. The user enters it on their device\nwithin `ttl_secs`; the device then appears in the native client list.",
|
||||
"operationId": "armNativePairing",
|
||||
"requestBody": {
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ArmNativePairing"
|
||||
}
|
||||
}
|
||||
},
|
||||
"required": true
|
||||
},
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "Pairing armed; the response carries the PIN to display",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/NativePairStatus"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"401": {
|
||||
"description": "Missing or invalid bearer token",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiError"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"503": {
|
||||
"description": "Native host not enabled (run `serve --native`)",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/ApiError"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"/api/v1/pair": {
|
||||
"get": {
|
||||
"tags": [
|
||||
@@ -416,6 +623,22 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"ArmNativePairing": {
|
||||
"type": "object",
|
||||
"description": "Arm-native-pairing request body.",
|
||||
"properties": {
|
||||
"ttl_secs": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "int32",
|
||||
"description": "Window length in seconds (default 120; clamped to 15–600).",
|
||||
"example": 120,
|
||||
"minimum": 0
|
||||
}
|
||||
}
|
||||
},
|
||||
"AvailableCompositor": {
|
||||
"type": "object",
|
||||
"description": "A compositor backend the host can drive a virtual output on, and whether it's usable now.",
|
||||
@@ -526,6 +749,67 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"NativeClient": {
|
||||
"type": "object",
|
||||
"description": "A paired native (punktfunk/1) client.",
|
||||
"required": [
|
||||
"name",
|
||||
"fingerprint"
|
||||
],
|
||||
"properties": {
|
||||
"fingerprint": {
|
||||
"type": "string",
|
||||
"description": "Hex SHA-256 of the client certificate — its stable id here."
|
||||
},
|
||||
"name": {
|
||||
"type": "string",
|
||||
"description": "The name the client supplied when pairing.",
|
||||
"example": "Living Room iPad"
|
||||
}
|
||||
}
|
||||
},
|
||||
"NativePairStatus": {
|
||||
"type": "object",
|
||||
"description": "Native (punktfunk/1) pairing status. Unlike GameStream, the **host** mints the PIN (the SPAKE2\nceremony needs it client-side first), so the console **displays** `pin` for the user to enter on\ntheir device — armed on demand for a short window.",
|
||||
"required": [
|
||||
"enabled",
|
||||
"armed",
|
||||
"paired_clients"
|
||||
],
|
||||
"properties": {
|
||||
"armed": {
|
||||
"type": "boolean",
|
||||
"description": "True while a pairing window is open."
|
||||
},
|
||||
"enabled": {
|
||||
"type": "boolean",
|
||||
"description": "Whether the native host is running (the unified host started with `--native`)."
|
||||
},
|
||||
"expires_in_secs": {
|
||||
"type": [
|
||||
"integer",
|
||||
"null"
|
||||
],
|
||||
"format": "int64",
|
||||
"description": "Seconds left in the window (null = disarmed, or armed with no expiry via the CLI flag).",
|
||||
"minimum": 0
|
||||
},
|
||||
"paired_clients": {
|
||||
"type": "integer",
|
||||
"format": "int32",
|
||||
"description": "Number of paired native clients.",
|
||||
"minimum": 0
|
||||
},
|
||||
"pin": {
|
||||
"type": [
|
||||
"string",
|
||||
"null"
|
||||
],
|
||||
"description": "The PIN to display while armed (null when disarmed).",
|
||||
"example": "1234"
|
||||
}
|
||||
}
|
||||
},
|
||||
"PairedClient": {
|
||||
"type": "object",
|
||||
"description": "A paired (certificate-pinned) Moonlight client.",
|
||||
@@ -797,6 +1081,10 @@
|
||||
"name": "pairing",
|
||||
"description": "Pairing PIN delivery (the out-of-band half of the GameStream pairing handshake)"
|
||||
},
|
||||
{
|
||||
"name": "native",
|
||||
"description": "Native punktfunk/1 pairing: arm a window, display the host PIN, manage paired devices"
|
||||
},
|
||||
{
|
||||
"name": "session",
|
||||
"description": "Active streaming session control"
|
||||
|
||||
Reference in New Issue
Block a user