feat(core): every connect introduces the device by name
The wire always had Hello.name and the host always honored it - but the connect path hardcoded None (only the PIN-pairing ceremony sent a name), so every no-PIN "request access" knock surfaced as the fingerprint placeholder "device abcd1234", and approving one without retyping a name persisted that placeholder into the trust store forever. NativeClient::connect now takes the device name. The session workers and the probe connects pass trust::device_name() (the hostname), the C ABI defaults to the same without a signature change (an ex10 variant can make it explicit if an embedder wants a custom label), and Android threads Build.MODEL through nativeConnect - the same convention its pairing dialogs already use for nativePair. The host, in turn, resolves the streaming client's display name (trust store first, so an approval-time rename wins; else the sanitized Hello name) and exposes it as client_name in GET /api/v1/local/summary for the tray's connect toast - a deliberate, documented loosening of that route's "no device names" contract, in the local user's favor: it tells them who is on their machine. A paired-but-idle device's name still never appears, which the mgmt tests now pin explicitly. openapi.json, its docs-site copy, and the SDK bindings regenerate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+10
-3
@@ -10,7 +10,7 @@
|
|||||||
"name": "MIT OR Apache-2.0",
|
"name": "MIT OR Apache-2.0",
|
||||||
"identifier": "MIT OR Apache-2.0"
|
"identifier": "MIT OR Apache-2.0"
|
||||||
},
|
},
|
||||||
"version": "0.20.0"
|
"version": "0.21.0"
|
||||||
},
|
},
|
||||||
"paths": {
|
"paths": {
|
||||||
"/api/v1/clients": {
|
"/api/v1/clients": {
|
||||||
@@ -1558,7 +1558,7 @@
|
|||||||
"host"
|
"host"
|
||||||
],
|
],
|
||||||
"summary": "Local status summary for the tray icon",
|
"summary": "Local status summary for the tray icon",
|
||||||
"description": "Non-sensitive status (counts and booleans only — no PIN values, no fingerprints, no device\nnames). Unauthenticated, but served to loopback peers only.",
|
"description": "Non-sensitive status (counts, booleans, and the streaming client's display name — no PIN\nvalues, no fingerprints). Unauthenticated, but served to loopback peers only.",
|
||||||
"operationId": "getLocalSummary",
|
"operationId": "getLocalSummary",
|
||||||
"responses": {
|
"responses": {
|
||||||
"200": {
|
"200": {
|
||||||
@@ -5686,7 +5686,7 @@
|
|||||||
},
|
},
|
||||||
"LocalSummary": {
|
"LocalSummary": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"description": "Non-sensitive host status for the local tray icon: counts and booleans only — no PIN values,\nno fingerprints, no device names. Served unauthenticated to LOOPBACK peers only (see\n`require_auth`): the bearer-token file is SYSTEM/Administrators-DACL'd on Windows, so the\nper-user tray process cannot authenticate — this narrow read-only route is its status source.",
|
"description": "Non-sensitive host status for the local tray icon: counts and booleans — no PIN values, no\nfingerprints. The ONE name exposed is `client_name`, the streaming client's display label\n(deliberate loosening for the tray's \"client connected\" toast: it tells the local user who is\non their machine, which is disclosure in the user's favor — and any local process could\nalready infer a session exists from the booleans here). Served unauthenticated to LOOPBACK\npeers only (see `require_auth`): the bearer-token file is SYSTEM/Administrators-DACL'd on\nWindows, so the per-user tray process cannot authenticate — this narrow read-only route is\nits status source.",
|
||||||
"required": [
|
"required": [
|
||||||
"version",
|
"version",
|
||||||
"video_streaming",
|
"video_streaming",
|
||||||
@@ -5702,6 +5702,13 @@
|
|||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"description": "True while audio is streaming on either plane (same rule as `video_streaming`)."
|
"description": "True while audio is streaming on either plane (same rule as `video_streaming`)."
|
||||||
},
|
},
|
||||||
|
"client_name": {
|
||||||
|
"type": [
|
||||||
|
"string",
|
||||||
|
"null"
|
||||||
|
],
|
||||||
|
"description": "Display name of the (first) streaming native client — the trust store's name for it, else\nthe name the device sent at connect. `null` when idle, for a nameless client, or for a\nGameStream session (that plane carries no device name)."
|
||||||
|
},
|
||||||
"conflicts": {
|
"conflicts": {
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"items": {
|
"items": {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package io.unom.punktfunk
|
package io.unom.punktfunk
|
||||||
|
|
||||||
import android.content.Context
|
import android.content.Context
|
||||||
|
import android.os.Build
|
||||||
import io.unom.punktfunk.kit.Gamepad
|
import io.unom.punktfunk.kit.Gamepad
|
||||||
import io.unom.punktfunk.kit.NativeBridge
|
import io.unom.punktfunk.kit.NativeBridge
|
||||||
import io.unom.punktfunk.kit.VideoDecoders
|
import io.unom.punktfunk.kit.VideoDecoders
|
||||||
@@ -53,6 +54,9 @@ suspend fun connectToHost(
|
|||||||
// the user's soft codec preference — the host resolves the emitted codec from both.
|
// the user's soft codec preference — the host resolves the emitted codec from both.
|
||||||
VideoDecoders.decodableCodecBits(), settings.preferredCodec(), timeoutMs,
|
VideoDecoders.decodableCodecBits(), settings.preferredCodec(), timeoutMs,
|
||||||
launch,
|
launch,
|
||||||
|
// The host's approval-list / trust-store label for this device — the same
|
||||||
|
// Build.MODEL convention the pairing dialogs use for nativePair.
|
||||||
|
Build.MODEL ?: "Android",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,6 +57,10 @@ object NativeBridge {
|
|||||||
/** Store-qualified library id (`steam:<appid>` / `custom:<id>`) to boot straight into a game,
|
/** Store-qualified library id (`steam:<appid>` / `custom:<id>`) to boot straight into a game,
|
||||||
* or `null`/empty for a plain desktop connect. Rides the Hello as `launch`. */
|
* or `null`/empty for a plain desktop connect. Rides the Hello as `launch`. */
|
||||||
launch: String?,
|
launch: String?,
|
||||||
|
/** This device's display name (rides the Hello as `name`) — what the host's pending-approval
|
||||||
|
* list and trust store show for it, same convention as [nativePair]'s `name`. `null`/blank ⇒
|
||||||
|
* the host falls back to a fingerprint-derived "device abcd1234" label. */
|
||||||
|
deviceName: String?,
|
||||||
): Long
|
): Long
|
||||||
|
|
||||||
/** 64-hex SHA-256 of the cert the host presented on [handle]; valid after a successful connect. */
|
/** 64-hex SHA-256 of the cert the host presented on [handle]; valid after a successful connect. */
|
||||||
|
|||||||
@@ -84,8 +84,11 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeSetLowLaten
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// `NativeBridge.nativeConnect(host, port, w, h, hz, certPem, keyPem, pinHex, bitrateKbps,
|
/// `NativeBridge.nativeConnect(host, port, w, h, hz, certPem, keyPem, pinHex, bitrateKbps,
|
||||||
/// compositorPref, gamepadPref, hdrEnabled, audioChannels, preferredCodec, timeoutMs, launch): Long`.
|
/// compositorPref, gamepadPref, hdrEnabled, audioChannels, preferredCodec, timeoutMs, launch,
|
||||||
|
/// deviceName): Long`.
|
||||||
/// `launch` (empty ⇒ none) is a store-qualified library id to boot straight into a game.
|
/// `launch` (empty ⇒ none) is a store-qualified library id to boot straight into a game.
|
||||||
|
/// `deviceName` (empty ⇒ none) rides the Hello as `name` — what the host's pending-approval list
|
||||||
|
/// and trust store show for this device (Kotlin passes `Build.MODEL`, its `nativePair` convention).
|
||||||
/// `certPem`/`keyPem` empty = anonymous, else presented as the persistent identity. `pinHex` empty
|
/// `certPem`/`keyPem` empty = anonymous, else presented as the persistent identity. `pinHex` empty
|
||||||
/// = TOFU (read `nativeHostFingerprint` after), else 64-hex SHA-256 to pin the host (mismatch → 0).
|
/// = TOFU (read `nativeHostFingerprint` after), else 64-hex SHA-256 to pin the host (mismatch → 0).
|
||||||
/// `bitrateKbps` 0 = host default. `compositorPref`/`gamepadPref` are `CompositorPref`/`GamepadPref`
|
/// `bitrateKbps` 0 = host default. `compositorPref`/`gamepadPref` are `CompositorPref`/`GamepadPref`
|
||||||
@@ -117,6 +120,7 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeConnect<'lo
|
|||||||
preferred_codec: jint,
|
preferred_codec: jint,
|
||||||
timeout_ms: jint,
|
timeout_ms: jint,
|
||||||
launch: JString<'local>,
|
launch: JString<'local>,
|
||||||
|
device_name: JString<'local>,
|
||||||
) -> jlong {
|
) -> jlong {
|
||||||
let host: String = match env.get_string(&host) {
|
let host: String = match env.get_string(&host) {
|
||||||
Ok(s) => s.into(),
|
Ok(s) => s.into(),
|
||||||
@@ -135,6 +139,14 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeConnect<'lo
|
|||||||
.map(Into::into)
|
.map(Into::into)
|
||||||
.ok()
|
.ok()
|
||||||
.filter(|s: &String| !s.is_empty());
|
.filter(|s: &String| !s.is_empty());
|
||||||
|
// The host's approval-list / trust-store label for this device; null / blank ⇒ None (the host
|
||||||
|
// falls back to its fingerprint-derived "device abcd1234" placeholder).
|
||||||
|
let device_name: Option<String> = env
|
||||||
|
.get_string(&device_name)
|
||||||
|
.map(Into::into)
|
||||||
|
.ok()
|
||||||
|
.map(|s: String| s.trim().to_string())
|
||||||
|
.filter(|s| !s.is_empty());
|
||||||
|
|
||||||
let identity: Option<(String, String)> = if cert.is_empty() || key.is_empty() {
|
let identity: Option<(String, String)> = if cert.is_empty() || key.is_empty() {
|
||||||
None
|
None
|
||||||
@@ -204,8 +216,9 @@ pub extern "system" fn Java_io_unom_punktfunk_kit_NativeBridge_nativeConnect<'lo
|
|||||||
// No non-video caps: this client does not render the host cursor locally (no shape/state
|
// No non-video caps: this client does not render the host cursor locally (no shape/state
|
||||||
// planes in the jni surface), so advertising CLIENT_CAP_CURSOR would stream cursor-less.
|
// planes in the jni surface), so advertising CLIENT_CAP_CURSOR would stream cursor-less.
|
||||||
0,
|
0,
|
||||||
launch, // a store-qualified library id to boot into a game, or None for the desktop
|
launch, // a store-qualified library id to boot into a game, or None for the desktop
|
||||||
pin, // Some → Crypto on host-fp mismatch
|
device_name, // Kotlin's Build.MODEL — the host's approval-list / trust-store label
|
||||||
|
pin, // Some → Crypto on host-fp mismatch
|
||||||
identity, // owned (cert, key) PEM, or None (anonymous)
|
identity, // owned (cert, key) PEM, or None (anonymous)
|
||||||
// Handshake budget from Kotlin: ~10 s for a normal connect, ~185 s for "request access"
|
// Handshake budget from Kotlin: ~10 s for a normal connect, ~185 s for "request access"
|
||||||
// (the host parks the connection until the operator approves the device — see ConnectScreen).
|
// (the host parks the connection until the operator approves the device — see ConnectScreen).
|
||||||
|
|||||||
@@ -657,6 +657,7 @@ punktfunk:// link takes. Exit codes: 0 ok, 2 connect, 3 trust, 4 renderer, 5 not
|
|||||||
None, // display_hdr
|
None, // display_hdr
|
||||||
0, // client_caps: nothing renders a cursor
|
0, // client_caps: nothing renders a cursor
|
||||||
None, // launch
|
None, // launch
|
||||||
|
Some(punktfunk_core::client::device_name()),
|
||||||
Some(pin),
|
Some(pin),
|
||||||
Some(identity),
|
Some(identity),
|
||||||
Duration::from_secs(15),
|
Duration::from_secs(15),
|
||||||
|
|||||||
@@ -699,6 +699,9 @@ impl AppModel {
|
|||||||
None, // display_hdr: probe connect, nothing presents
|
None, // display_hdr: probe connect, nothing presents
|
||||||
0, // client_caps: probe connect, nothing renders a cursor
|
0, // client_caps: probe connect, nothing renders a cursor
|
||||||
None, // launch: probe connect, no game
|
None, // launch: probe connect, no game
|
||||||
|
// Knock under this device's name, not a fingerprint placeholder, when the
|
||||||
|
// probed host doesn't know us yet.
|
||||||
|
Some(pf_client_core::trust::device_name()),
|
||||||
pin,
|
pin,
|
||||||
Some(identity),
|
Some(identity),
|
||||||
std::time::Duration::from_secs(15),
|
std::time::Duration::from_secs(15),
|
||||||
|
|||||||
@@ -699,6 +699,9 @@ impl AppModel {
|
|||||||
None, // display_hdr: probe connect, nothing presents
|
None, // display_hdr: probe connect, nothing presents
|
||||||
0, // client_caps: probe connect, nothing renders a cursor
|
0, // client_caps: probe connect, nothing renders a cursor
|
||||||
None, // launch: probe connect, no game
|
None, // launch: probe connect, no game
|
||||||
|
// Knock under this device's name, not a fingerprint placeholder, when the
|
||||||
|
// probed host doesn't know us yet.
|
||||||
|
Some(pf_client_core::trust::device_name()),
|
||||||
pin,
|
pin,
|
||||||
Some(identity),
|
Some(identity),
|
||||||
std::time::Duration::from_secs(15),
|
std::time::Duration::from_secs(15),
|
||||||
|
|||||||
@@ -58,6 +58,9 @@ pub fn run_speed_probe(
|
|||||||
None, // display_hdr: probe connect, nothing presents
|
None, // display_hdr: probe connect, nothing presents
|
||||||
0, // client_caps: probe connect, nothing renders a cursor
|
0, // client_caps: probe connect, nothing renders a cursor
|
||||||
None, // launch: no game
|
None, // launch: no game
|
||||||
|
// Same label a real session sends — a speed test against a host that doesn't know us yet
|
||||||
|
// should knock under this device's name, not a fingerprint placeholder.
|
||||||
|
Some(punktfunk_core::client::device_name()),
|
||||||
pin,
|
pin,
|
||||||
Some(identity),
|
Some(identity),
|
||||||
Duration::from_secs(15),
|
Duration::from_secs(15),
|
||||||
|
|||||||
@@ -273,6 +273,9 @@ fn pump(
|
|||||||
0
|
0
|
||||||
},
|
},
|
||||||
params.launch.clone(),
|
params.launch.clone(),
|
||||||
|
// The host's approval-list / trust-store label for this client. Without it every no-PIN
|
||||||
|
// "request access" knock showed up as the fingerprint placeholder "device abcd1234".
|
||||||
|
Some(crate::trust::device_name()),
|
||||||
params.pin,
|
params.pin,
|
||||||
Some(params.identity),
|
Some(params.identity),
|
||||||
params.connect_timeout,
|
params.connect_timeout,
|
||||||
|
|||||||
@@ -344,21 +344,10 @@ pub fn persist_host(name: &str, addr: &str, port: u16, fp_hex: &str, paired: boo
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// This machine's name — the label a host files this client under in its paired-devices list.
|
/// This machine's name — the label a host files this client under in its paired-devices list.
|
||||||
/// `/etc/hostname` first (the answer on any Linux box, and the only one available in a minimal
|
/// Now owned by punktfunk-core (`client::device_name`) so the connect path and the C ABI share
|
||||||
/// build with no GTK to ask), then the usual environment fallbacks.
|
/// the same default; re-exported here for the existing pairing-path callers.
|
||||||
pub fn device_name() -> String {
|
pub fn device_name() -> String {
|
||||||
#[cfg(target_os = "linux")]
|
punktfunk_core::client::device_name()
|
||||||
if let Ok(s) = std::fs::read_to_string("/etc/hostname") {
|
|
||||||
let s = s.trim();
|
|
||||||
if !s.is_empty() {
|
|
||||||
return s.to_string();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
std::env::var("COMPUTERNAME")
|
|
||||||
.or_else(|_| std::env::var("HOSTNAME"))
|
|
||||||
.ok()
|
|
||||||
.filter(|s| !s.trim().is_empty())
|
|
||||||
.unwrap_or_else(|| "This device".into())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Drop an fp-less placeholder entry for `addr:port`. A host added by address before any
|
/// Drop an fp-less placeholder entry for `addr:port`. A host added by address before any
|
||||||
|
|||||||
@@ -1872,6 +1872,11 @@ unsafe fn connect_ex_impl(
|
|||||||
// ([`punktfunk_connection_next_cursor_shape`]/`_state`) may set it. ex7/ex8 pass 0.
|
// ([`punktfunk_connection_next_cursor_shape`]/`_state`) may set it. ex7/ex8 pass 0.
|
||||||
client_caps,
|
client_caps,
|
||||||
launch,
|
launch,
|
||||||
|
// The C ABI has no device-name parameter (only `punktfunk_pair` takes one), so every
|
||||||
|
// embedder gets the OS hostname default — this is what the host's pending-approval
|
||||||
|
// list shows when an unpaired embedder knocks. An `ex10` variant can make it explicit
|
||||||
|
// if an embedder ever wants a custom label (e.g. the platform's marketing name).
|
||||||
|
Some(crate::client::device_name()),
|
||||||
pin,
|
pin,
|
||||||
identity,
|
identity,
|
||||||
std::time::Duration::from_millis(timeout_ms as u64),
|
std::time::Duration::from_millis(timeout_ms as u64),
|
||||||
|
|||||||
@@ -292,6 +292,26 @@ fn register_hot_tid(reg: &Mutex<Vec<i32>>) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// This machine's name — the default value for [`NativeClient::connect`]'s `name` parameter
|
||||||
|
/// (what a host shows in its pending-approval list and files this client under when approved).
|
||||||
|
/// `/etc/hostname` first (the answer on any Linux box, and available in a minimal build with no
|
||||||
|
/// desktop toolkit to ask), then the usual environment fallbacks. Lives here (not in a client
|
||||||
|
/// shell crate) so the C ABI's `punktfunk_connect` can share the same default.
|
||||||
|
pub fn device_name() -> String {
|
||||||
|
#[cfg(target_os = "linux")]
|
||||||
|
if let Ok(s) = std::fs::read_to_string("/etc/hostname") {
|
||||||
|
let s = s.trim();
|
||||||
|
if !s.is_empty() {
|
||||||
|
return s.to_string();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::env::var("COMPUTERNAME")
|
||||||
|
.or_else(|_| std::env::var("HOSTNAME"))
|
||||||
|
.ok()
|
||||||
|
.filter(|s| !s.trim().is_empty())
|
||||||
|
.unwrap_or_else(|| "This device".into())
|
||||||
|
}
|
||||||
|
|
||||||
impl NativeClient {
|
impl NativeClient {
|
||||||
/// Connect to a `punktfunk/1` host and start the session at (up to) `mode`. Blocks until the
|
/// Connect to a `punktfunk/1` host and start the session at (up to) `mode`. Blocks until the
|
||||||
/// handshake completes or `timeout` elapses.
|
/// handshake completes or `timeout` elapses.
|
||||||
@@ -335,6 +355,11 @@ impl NativeClient {
|
|||||||
// streams with NO visible cursor at all. `0` = today's composited behavior.
|
// streams with NO visible cursor at all. `0` = today's composited behavior.
|
||||||
client_caps: u8,
|
client_caps: u8,
|
||||||
launch: Option<String>,
|
launch: Option<String>,
|
||||||
|
// This device's display name, carried in [`crate::quic::Hello::name`]: what the host's
|
||||||
|
// pending-approval list shows when an unpaired client knocks, and what its trust store
|
||||||
|
// files the device under on delegated approval. `None` = the host falls back to a
|
||||||
|
// fingerprint-derived "device abcd1234" label. Embedders usually pass [`device_name`].
|
||||||
|
name: Option<String>,
|
||||||
pin: Option<[u8; 32]>,
|
pin: Option<[u8; 32]>,
|
||||||
identity: Option<(String, String)>,
|
identity: Option<(String, String)>,
|
||||||
timeout: Duration,
|
timeout: Duration,
|
||||||
@@ -414,6 +439,7 @@ impl NativeClient {
|
|||||||
display_hdr,
|
display_hdr,
|
||||||
client_caps,
|
client_caps,
|
||||||
launch,
|
launch,
|
||||||
|
name,
|
||||||
pin,
|
pin,
|
||||||
identity,
|
identity,
|
||||||
connect_timeout: timeout,
|
connect_timeout: timeout,
|
||||||
|
|||||||
@@ -120,9 +120,10 @@ pub(super) async fn connect_and_handshake(args: &WorkerArgs) -> Result<Handshake
|
|||||||
compositor,
|
compositor,
|
||||||
gamepad,
|
gamepad,
|
||||||
bitrate_kbps,
|
bitrate_kbps,
|
||||||
// No device name yet: the connect ABI has no name parameter (pairing does). The
|
// The embedder's device name — what the host's pending-approval list and paired-
|
||||||
// host falls back to a fingerprint-derived label in its pending-approval list.
|
// devices store show for this client. `None` makes the host fall back to a
|
||||||
name: None,
|
// fingerprint-derived "device abcd1234" label.
|
||||||
|
name: args.name.clone(),
|
||||||
// Library id to launch this session, if the embedder asked for one.
|
// Library id to launch this session, if the embedder asked for one.
|
||||||
launch: launch.clone(),
|
launch: launch.clone(),
|
||||||
// The embedder's decode/present caps (e.g. the Windows client advertises
|
// The embedder's decode/present caps (e.g. the Windows client advertises
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ pub(crate) struct WorkerArgs {
|
|||||||
pub(crate) display_hdr: Option<HdrMeta>,
|
pub(crate) display_hdr: Option<HdrMeta>,
|
||||||
pub(crate) client_caps: u8,
|
pub(crate) client_caps: u8,
|
||||||
pub(crate) launch: Option<String>,
|
pub(crate) launch: Option<String>,
|
||||||
|
/// This device's display name, sent in `Hello` (the host's approval list / trust store label).
|
||||||
|
pub(crate) name: Option<String>,
|
||||||
pub(crate) pin: Option<[u8; 32]>,
|
pub(crate) pin: Option<[u8; 32]>,
|
||||||
pub(crate) identity: Option<(String, String)>,
|
pub(crate) identity: Option<(String, String)>,
|
||||||
/// The embedder's connect budget (the same value `connect` bounds `ready_rx` with): the
|
/// The embedder's connect budget (the same value `connect` bounds `ready_rx` with): the
|
||||||
|
|||||||
@@ -187,10 +187,14 @@ pub(crate) struct StreamInfo {
|
|||||||
last_resize_ms: Option<u32>,
|
last_resize_ms: Option<u32>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Non-sensitive host status for the local tray icon: counts and booleans only — no PIN values,
|
/// Non-sensitive host status for the local tray icon: counts and booleans — no PIN values, no
|
||||||
/// no fingerprints, no device names. Served unauthenticated to LOOPBACK peers only (see
|
/// fingerprints. The ONE name exposed is `client_name`, the streaming client's display label
|
||||||
/// `require_auth`): the bearer-token file is SYSTEM/Administrators-DACL'd on Windows, so the
|
/// (deliberate loosening for the tray's "client connected" toast: it tells the local user who is
|
||||||
/// per-user tray process cannot authenticate — this narrow read-only route is its status source.
|
/// on their machine, which is disclosure in the user's favor — and any local process could
|
||||||
|
/// already infer a session exists from the booleans here). Served unauthenticated to LOOPBACK
|
||||||
|
/// peers only (see `require_auth`): the bearer-token file is SYSTEM/Administrators-DACL'd on
|
||||||
|
/// Windows, so the per-user tray process cannot authenticate — this narrow read-only route is
|
||||||
|
/// its status source.
|
||||||
#[derive(Serialize, ToSchema)]
|
#[derive(Serialize, ToSchema)]
|
||||||
pub(crate) struct LocalSummary {
|
pub(crate) struct LocalSummary {
|
||||||
/// Host version (mirrors `/health`).
|
/// Host version (mirrors `/health`).
|
||||||
@@ -203,6 +207,11 @@ pub(crate) struct LocalSummary {
|
|||||||
/// The active session: GameStream's launch (Moonlight `/launch`) when present, else the first
|
/// The active session: GameStream's launch (Moonlight `/launch`) when present, else the first
|
||||||
/// live native session. `null` when nothing is streaming.
|
/// live native session. `null` when nothing is streaming.
|
||||||
session: Option<SessionInfo>,
|
session: Option<SessionInfo>,
|
||||||
|
/// Display name of the (first) streaming native client — the trust store's name for it, else
|
||||||
|
/// the name the device sent at connect. `null` when idle, for a nameless client, or for a
|
||||||
|
/// GameStream session (that plane carries no device name).
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
client_name: Option<String>,
|
||||||
/// Number of pinned (paired) GameStream client certificates.
|
/// Number of pinned (paired) GameStream client certificates.
|
||||||
paired_clients: u32,
|
paired_clients: u32,
|
||||||
/// Number of paired native (punktfunk/1) devices.
|
/// Number of paired native (punktfunk/1) devices.
|
||||||
@@ -457,8 +466,8 @@ pub(crate) async fn get_status(State(st): State<Arc<MgmtState>>) -> Json<Runtime
|
|||||||
|
|
||||||
/// Local status summary for the tray icon
|
/// Local status summary for the tray icon
|
||||||
///
|
///
|
||||||
/// Non-sensitive status (counts and booleans only — no PIN values, no fingerprints, no device
|
/// Non-sensitive status (counts, booleans, and the streaming client's display name — no PIN
|
||||||
/// names). Unauthenticated, but served to loopback peers only.
|
/// values, no fingerprints). Unauthenticated, but served to loopback peers only.
|
||||||
#[utoipa::path(
|
#[utoipa::path(
|
||||||
get,
|
get,
|
||||||
path = "/local/summary",
|
path = "/local/summary",
|
||||||
@@ -506,6 +515,9 @@ pub(crate) async fn get_local_summary(State(st): State<Arc<MgmtState>>) -> Json<
|
|||||||
video_streaming: st.app.streaming.load(Ordering::SeqCst) || !native.is_empty(),
|
video_streaming: st.app.streaming.load(Ordering::SeqCst) || !native.is_empty(),
|
||||||
audio_streaming: st.app.audio_streaming.load(Ordering::SeqCst) || !native.is_empty(),
|
audio_streaming: st.app.audio_streaming.load(Ordering::SeqCst) || !native.is_empty(),
|
||||||
session,
|
session,
|
||||||
|
// The first native session's display name (matches the `session` fallback order — a
|
||||||
|
// GameStream launch carries no device name, so the field stays absent there).
|
||||||
|
client_name: native.first().and_then(|s| s.client_name.clone()),
|
||||||
paired_clients: st
|
paired_clients: st
|
||||||
.app
|
.app
|
||||||
.paired
|
.paired
|
||||||
|
|||||||
@@ -316,6 +316,7 @@ fn fake_native_session(
|
|||||||
quit: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
quit: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||||
force_idr: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
force_idr: Arc::new(std::sync::atomic::AtomicBool::new(false)),
|
||||||
client: "test-client".into(),
|
client: "test-client".into(),
|
||||||
|
client_name: Some("studio-deck".into()),
|
||||||
hdr: false,
|
hdr: false,
|
||||||
ttff_ms: Arc::new(std::sync::atomic::AtomicU32::new(0)),
|
ttff_ms: Arc::new(std::sync::atomic::AtomicU32::new(0)),
|
||||||
last_resize_ms: Arc::new(std::sync::atomic::AtomicU32::new(0)),
|
last_resize_ms: Arc::new(std::sync::atomic::AtomicU32::new(0)),
|
||||||
@@ -345,17 +346,28 @@ async fn local_summary_reports_a_native_session_as_streaming() {
|
|||||||
assert_eq!(body["session"]["width"], 3840);
|
assert_eq!(body["session"]["width"], 3840);
|
||||||
assert_eq!(body["session"]["height"], 2160);
|
assert_eq!(body["session"]["height"], 2160);
|
||||||
assert_eq!(body["session"]["fps"], 120);
|
assert_eq!(body["session"]["fps"], 120);
|
||||||
|
// The STREAMING client's display name rides along (the tray's connect toast); see the
|
||||||
|
// non-sensitive test below for the idle-side guarantee.
|
||||||
|
assert_eq!(body["client_name"], "studio-deck");
|
||||||
|
|
||||||
// Session over → back to idle.
|
// Session over → back to idle, and the name goes with it.
|
||||||
drop(session);
|
drop(session);
|
||||||
let (_, body) = send(&app, summary_req()).await;
|
let (_, body) = send(&app, summary_req()).await;
|
||||||
assert_eq!(body["video_streaming"], false);
|
assert_eq!(body["video_streaming"], false);
|
||||||
assert_eq!(body["session"], serde_json::Value::Null);
|
assert_eq!(body["session"], serde_json::Value::Null);
|
||||||
|
assert_eq!(
|
||||||
|
body["client_name"],
|
||||||
|
serde_json::Value::Null,
|
||||||
|
"no live session → no client name in the summary"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The tray's `/local/summary` is unauthenticated for LOOPBACK peers only — a LAN peer is
|
/// The tray's `/local/summary` is unauthenticated for LOOPBACK peers only — a LAN peer is
|
||||||
/// rejected even though the route needs no bearer token, and the body never carries secret
|
/// rejected even though the route needs no bearer token, and the body never carries secret
|
||||||
/// material (no PIN values, no fingerprints, no device names — counts/booleans only).
|
/// material (no PIN values, no fingerprints). The ONE name it may carry is the *streaming*
|
||||||
|
/// client's display name (`client_name`, for the tray's connect toast) — a paired-but-idle
|
||||||
|
/// device's name must still never appear, which is what this test pins (it pairs a device and
|
||||||
|
/// registers NO session).
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn local_summary_is_loopback_only_and_non_sensitive() {
|
async fn local_summary_is_loopback_only_and_non_sensitive() {
|
||||||
let _serial = SESSION_REGISTRY_LOCK.lock().await;
|
let _serial = SESSION_REGISTRY_LOCK.lock().await;
|
||||||
|
|||||||
@@ -1460,6 +1460,23 @@ async fn serve_session(
|
|||||||
let client_label = endpoint::peer_fingerprint(&conn)
|
let client_label = endpoint::peer_fingerprint(&conn)
|
||||||
.map(|fp| fingerprint_hex(&fp)[..12].to_string())
|
.map(|fp| fingerprint_hex(&fp)[..12].to_string())
|
||||||
.unwrap_or_else(|| conn.remote_address().ip().to_string());
|
.unwrap_or_else(|| conn.remote_address().ip().to_string());
|
||||||
|
// The client's DISPLAY name for the status surface (local summary → the tray's connect
|
||||||
|
// toast): the trust store's operator-curated name for this fingerprint first (a rename at
|
||||||
|
// approval time wins over whatever the device calls itself), else the sanitized Hello name.
|
||||||
|
// `None` (nameless knock from an old client / Android) keeps the summary name-free.
|
||||||
|
let client_name = endpoint::peer_fingerprint(&conn)
|
||||||
|
.map(|fp| fingerprint_hex(&fp))
|
||||||
|
.and_then(|fp_hex| {
|
||||||
|
np.list()
|
||||||
|
.into_iter()
|
||||||
|
.find(|c| c.fingerprint == fp_hex)
|
||||||
|
.map(|c| c.name)
|
||||||
|
.or_else(|| {
|
||||||
|
let raw = hello.name.as_deref().unwrap_or("").trim();
|
||||||
|
(!raw.is_empty())
|
||||||
|
.then(|| crate::native_pairing::sanitize_device_name(raw, &fp_hex))
|
||||||
|
})
|
||||||
|
});
|
||||||
// Transition-trace handles for the data plane (P0.1): the punch stamp + the virtual-stream
|
// Transition-trace handles for the data plane (P0.1): the punch stamp + the virtual-stream
|
||||||
// stages ride the same per-session trace; resizes write their totals into the shared slot.
|
// stages ride the same per-session trace; resizes write their totals into the shared slot.
|
||||||
let bringup_dp = bringup.clone();
|
let bringup_dp = bringup.clone();
|
||||||
@@ -1556,6 +1573,7 @@ async fn serve_session(
|
|||||||
streamed_au,
|
streamed_au,
|
||||||
stats: stats_dp,
|
stats: stats_dp,
|
||||||
client_label,
|
client_label,
|
||||||
|
client_name,
|
||||||
launch: launch_for_dp,
|
launch: launch_for_dp,
|
||||||
launch_target,
|
launch_target,
|
||||||
client_hdr,
|
client_hdr,
|
||||||
@@ -2260,6 +2278,7 @@ mod tests {
|
|||||||
None, // display_hdr
|
None, // display_hdr
|
||||||
0, // client_caps
|
0, // client_caps
|
||||||
None, // launch
|
None, // launch
|
||||||
|
None, // name
|
||||||
None, // pin (TOFU)
|
None, // pin (TOFU)
|
||||||
None, // identity (host doesn't require pairing)
|
None, // identity (host doesn't require pairing)
|
||||||
std::time::Duration::from_secs(10),
|
std::time::Duration::from_secs(10),
|
||||||
@@ -2431,6 +2450,7 @@ mod tests {
|
|||||||
None, // display_hdr
|
None, // display_hdr
|
||||||
0, // client_caps
|
0, // client_caps
|
||||||
None, // launch
|
None, // launch
|
||||||
|
None, // name: absent on purpose — this test asserts the fingerprint-derived label
|
||||||
None, // pin: TOFU — the operator's approval (not a PIN) authorizes this client
|
None, // pin: TOFU — the operator's approval (not a PIN) authorizes this client
|
||||||
Some((cert, key)),
|
Some((cert, key)),
|
||||||
std::time::Duration::from_secs(15),
|
std::time::Duration::from_secs(15),
|
||||||
@@ -2499,6 +2519,7 @@ mod tests {
|
|||||||
None, // display_hdr
|
None, // display_hdr
|
||||||
0, // client_caps
|
0, // client_caps
|
||||||
None, // launch
|
None, // launch
|
||||||
|
None, // name
|
||||||
None,
|
None,
|
||||||
None,
|
None,
|
||||||
timeout
|
timeout
|
||||||
@@ -2529,6 +2550,7 @@ mod tests {
|
|||||||
None, // display_hdr
|
None, // display_hdr
|
||||||
0, // client_caps
|
0, // client_caps
|
||||||
None, // launch
|
None, // launch
|
||||||
|
None, // name
|
||||||
Some(host_fp),
|
Some(host_fp),
|
||||||
Some((cert.clone(), key.clone())),
|
Some((cert.clone(), key.clone())),
|
||||||
timeout,
|
timeout,
|
||||||
|
|||||||
@@ -999,6 +999,9 @@ pub(super) struct SessionContext {
|
|||||||
/// Short client label (cert-fingerprint prefix, else peer IP) seeded into the capture meta on
|
/// Short client label (cert-fingerprint prefix, else peer IP) seeded into the capture meta on
|
||||||
/// the first armed stats registration.
|
/// the first armed stats registration.
|
||||||
pub(super) client_label: String,
|
pub(super) client_label: String,
|
||||||
|
/// The client's display name (trust-store name, else sanitized Hello name; `None` = nameless
|
||||||
|
/// knock) — published to the live-session registry for the local summary's connect toast.
|
||||||
|
pub(super) client_name: Option<String>,
|
||||||
/// The session's requested launch, `None` = none. On Windows the store-qualified library id
|
/// The session's requested launch, `None` = none. On Windows the store-qualified library id
|
||||||
/// (spawned into the interactive user session once capture is live); on other hosts the shell
|
/// (spawned into the interactive user session once capture is live); on other hosts the shell
|
||||||
/// command already resolved against the host's own library — nested into gamescope's bare spawn
|
/// command already resolved against the host's own library — nested into gamescope's bare spawn
|
||||||
@@ -1140,6 +1143,7 @@ pub(super) fn virtual_stream(ctx: SessionContext, prepared: Option<PreparedDispl
|
|||||||
streamed_au,
|
streamed_au,
|
||||||
stats,
|
stats,
|
||||||
client_label,
|
client_label,
|
||||||
|
client_name,
|
||||||
launch,
|
launch,
|
||||||
launch_target,
|
launch_target,
|
||||||
client_hdr,
|
client_hdr,
|
||||||
@@ -1480,6 +1484,7 @@ pub(super) fn virtual_stream(ctx: SessionContext, prepared: Option<PreparedDispl
|
|||||||
quit: quit.clone(),
|
quit: quit.clone(),
|
||||||
force_idr: force_idr.clone(),
|
force_idr: force_idr.clone(),
|
||||||
client: client_label,
|
client: client_label,
|
||||||
|
client_name,
|
||||||
hdr: plan.hdr,
|
hdr: plan.hdr,
|
||||||
ttff_ms: bringup.total_slot(),
|
ttff_ms: bringup.total_slot(),
|
||||||
last_resize_ms: resize_ms.clone(),
|
last_resize_ms: resize_ms.clone(),
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ struct LiveSession {
|
|||||||
force_idr: Arc<AtomicBool>,
|
force_idr: Arc<AtomicBool>,
|
||||||
/// Short client label (cert-fingerprint prefix / peer IP) — carried on the lifecycle events.
|
/// Short client label (cert-fingerprint prefix / peer IP) — carried on the lifecycle events.
|
||||||
client: String,
|
client: String,
|
||||||
|
/// The client's display name (trust-store name, else its sanitized Hello name) — what the
|
||||||
|
/// local summary's connect toast shows. `None` for a nameless knock (old client / Android).
|
||||||
|
client_name: Option<String>,
|
||||||
/// Whether the session negotiated HDR — carried on the lifecycle events.
|
/// Whether the session negotiated HDR — carried on the lifecycle events.
|
||||||
hdr: bool,
|
hdr: bool,
|
||||||
/// Completed bring-up total (hello → first packet), ms; 0 until the first packet left. Written
|
/// Completed bring-up total (hello → first packet), ms; 0 until the first packet left. Written
|
||||||
@@ -55,13 +58,16 @@ struct LiveSession {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A resolved read of one live session, for the `/status` view.
|
/// A resolved read of one live session, for the `/status` view.
|
||||||
#[derive(Clone, Copy)]
|
#[derive(Clone)]
|
||||||
pub struct SessionSnapshot {
|
pub struct SessionSnapshot {
|
||||||
pub width: u32,
|
pub width: u32,
|
||||||
pub height: u32,
|
pub height: u32,
|
||||||
pub fps: u32,
|
pub fps: u32,
|
||||||
pub bitrate_kbps: u32,
|
pub bitrate_kbps: u32,
|
||||||
pub codec: Codec,
|
pub codec: Codec,
|
||||||
|
/// The client's display name (trust-store name, else its sanitized Hello name); `None` for a
|
||||||
|
/// nameless client.
|
||||||
|
pub client_name: Option<String>,
|
||||||
/// Bring-up total (hello → first packet), ms; 0 while still bringing up (latency plan P0.1).
|
/// Bring-up total (hello → first packet), ms; 0 while still bringing up (latency plan P0.1).
|
||||||
pub time_to_first_frame_ms: u32,
|
pub time_to_first_frame_ms: u32,
|
||||||
/// Most recent mid-stream resize total, ms; 0 = no resize this session.
|
/// Most recent mid-stream resize total, ms; 0 = no resize this session.
|
||||||
@@ -106,6 +112,8 @@ pub struct Registration {
|
|||||||
pub force_idr: Arc<AtomicBool>,
|
pub force_idr: Arc<AtomicBool>,
|
||||||
/// Short client label (cert-fingerprint prefix / peer IP).
|
/// Short client label (cert-fingerprint prefix / peer IP).
|
||||||
pub client: String,
|
pub client: String,
|
||||||
|
/// The client's display name, when it has one (trust-store name, else sanitized Hello name).
|
||||||
|
pub client_name: Option<String>,
|
||||||
pub hdr: bool,
|
pub hdr: bool,
|
||||||
/// Bring-up total slot (hello → first packet), ms.
|
/// Bring-up total slot (hello → first packet), ms.
|
||||||
pub ttff_ms: Arc<AtomicU32>,
|
pub ttff_ms: Arc<AtomicU32>,
|
||||||
@@ -127,6 +135,7 @@ pub fn register(reg: Registration) -> LiveSessionGuard {
|
|||||||
quit,
|
quit,
|
||||||
force_idr,
|
force_idr,
|
||||||
client,
|
client,
|
||||||
|
client_name,
|
||||||
hdr,
|
hdr,
|
||||||
ttff_ms,
|
ttff_ms,
|
||||||
last_resize_ms,
|
last_resize_ms,
|
||||||
@@ -142,6 +151,7 @@ pub fn register(reg: Registration) -> LiveSessionGuard {
|
|||||||
quit,
|
quit,
|
||||||
force_idr,
|
force_idr,
|
||||||
client,
|
client,
|
||||||
|
client_name,
|
||||||
hdr,
|
hdr,
|
||||||
ttff_ms,
|
ttff_ms,
|
||||||
last_resize_ms,
|
last_resize_ms,
|
||||||
@@ -197,6 +207,7 @@ pub fn snapshot() -> Vec<SessionSnapshot> {
|
|||||||
fps,
|
fps,
|
||||||
bitrate_kbps: s.bitrate_kbps.load(Ordering::Relaxed),
|
bitrate_kbps: s.bitrate_kbps.load(Ordering::Relaxed),
|
||||||
codec: s.codec,
|
codec: s.codec,
|
||||||
|
client_name: s.client_name.clone(),
|
||||||
time_to_first_frame_ms: s.ttff_ms.load(Ordering::Relaxed),
|
time_to_first_frame_ms: s.ttff_ms.load(Ordering::Relaxed),
|
||||||
last_resize_ms: s.last_resize_ms.load(Ordering::Relaxed),
|
last_resize_ms: s.last_resize_ms.load(Ordering::Relaxed),
|
||||||
}
|
}
|
||||||
|
|||||||
+5476
-62
File diff suppressed because it is too large
Load Diff
@@ -63,8 +63,8 @@ export type LaunchSpec = { readonly "kind": string, readonly "value": string }
|
|||||||
export const LaunchSpec = Schema.Struct({ "kind": Schema.String.annotate({ "description": "`\"steam_appid\"` or `\"command\"`." }), "value": Schema.String.annotate({ "description": "The appid (for `steam_appid`) or the shell command (for `command`)." }) }).annotate({ "description": "How the host would launch a title (consumed by the session launcher in a later step). Kept\nopen-ended so new stores slot in: `steam_appid` → `steam steam://rungameid/<value>`;\n`command` → run `<value>` nested in a gamescope session." })
|
export const LaunchSpec = Schema.Struct({ "kind": Schema.String.annotate({ "description": "`\"steam_appid\"` or `\"command\"`." }), "value": Schema.String.annotate({ "description": "The appid (for `steam_appid`) or the shell command (for `command`)." }) }).annotate({ "description": "How the host would launch a title (consumed by the session launcher in a later step). Kept\nopen-ended so new stores slot in: `steam_appid` → `steam steam://rungameid/<value>`;\n`command` → run `<value>` nested in a gamescope session." })
|
||||||
export type LayoutMode = "auto-row" | "manual"
|
export type LayoutMode = "auto-row" | "manual"
|
||||||
export const LayoutMode = Schema.Literals(["auto-row", "manual"]).annotate({ "description": "How group members are arranged in the desktop coordinate space. Stored at Stage 0; applied from\nthe multi-monitor stage." })
|
export const LayoutMode = Schema.Literals(["auto-row", "manual"]).annotate({ "description": "How group members are arranged in the desktop coordinate space. Stored at Stage 0; applied from\nthe multi-monitor stage." })
|
||||||
export type LocalSummary = { readonly "audio_streaming": boolean, readonly "conflicts"?: ReadonlyArray<string>, readonly "games"?: ReadonlyArray<string>, readonly "kept_displays": number, readonly "native_paired_clients": number, readonly "paired_clients": number, readonly "pending_approvals": number, readonly "pin_pending": boolean, readonly "session"?: null | { readonly "fps": number, readonly "height": number, readonly "width": number }, readonly "version": string, readonly "video_streaming": boolean }
|
export type LocalSummary = { readonly "audio_streaming": boolean, readonly "client_name"?: string | null, readonly "conflicts"?: ReadonlyArray<string>, readonly "games"?: ReadonlyArray<string>, readonly "kept_displays": number, readonly "native_paired_clients": number, readonly "paired_clients": number, readonly "pending_approvals": number, readonly "pin_pending": boolean, readonly "session"?: null | { readonly "fps": number, readonly "height": number, readonly "width": number }, readonly "version": string, readonly "video_streaming": boolean }
|
||||||
export const LocalSummary = Schema.Struct({ "audio_streaming": Schema.Boolean.annotate({ "description": "True while audio is streaming on either plane (same rule as `video_streaming`)." }), "conflicts": Schema.optionalKey(Schema.Array(Schema.String).annotate({ "description": "Other Moonlight-compatible hosts (Sunshine/Apollo/…) detected on this machine at startup —\nrunning one alongside Punktfunk is unsupported. Compact labels (e.g. `Sunshine (running)`);\nthe tray/console surface them so the clash is visible before pairing silently fails." })), "games": Schema.optionalKey(Schema.Array(Schema.String).annotate({ "description": "Launched games the host is tracking, as compact labels (`Hades`, `Hades (closing in 4:12)`).\n\nThe countdown form is the one that matters: it means the game's client is gone and the host\nwill end the game when the window closes — something a user at the machine should be able to\nsee (and stop) without opening the console. Empty when nothing was launched." })), "kept_displays": Schema.Number.annotate({ "description": "Virtual displays being KEPT with no live session — lingering (keep-alive window) or pinned\n(`keep_alive: forever`). Non-zero means a display (and, exclusive, your physical monitors) is\nheld; the tray surfaces it + a one-click release. Active (in-use) displays are not counted.", "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "native_paired_clients": Schema.Number.annotate({ "description": "Number of paired native (punktfunk/1) devices.", "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "paired_clients": Schema.Number.annotate({ "description": "Number of pinned (paired) GameStream client certificates.", "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "pending_approvals": Schema.Number.annotate({ "description": "Native pairing knocks awaiting the operator's approval (count only).", "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "pin_pending": Schema.Boolean.annotate({ "description": "True while a GameStream pairing handshake is parked waiting for the user's PIN." }), "session": Schema.optionalKey(Schema.Union([Schema.Null, Schema.Struct({ "fps": Schema.Number.annotate({ "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "height": Schema.Number.annotate({ "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "width": Schema.Number.annotate({ "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)) }).annotate({ "description": "The active session: GameStream's launch (Moonlight `/launch`) when present, else the first\nlive native session. `null` when nothing is streaming." })], { mode: "oneOf" })), "version": Schema.String.annotate({ "description": "Host version (mirrors `/health`)." }), "video_streaming": Schema.Boolean.annotate({ "description": "True while video is streaming on EITHER plane: the GameStream media pipeline, or a live\nnative (punktfunk/1) session — the default plane, invisible in the GameStream flag alone." }) }).annotate({ "description": "Non-sensitive host status for the local tray icon: counts and booleans only — no PIN values,\nno fingerprints, no device names. Served unauthenticated to LOOPBACK peers only (see\n`require_auth`): the bearer-token file is SYSTEM/Administrators-DACL'd on Windows, so the\nper-user tray process cannot authenticate — this narrow read-only route is its status source." })
|
export const LocalSummary = Schema.Struct({ "audio_streaming": Schema.Boolean.annotate({ "description": "True while audio is streaming on either plane (same rule as `video_streaming`)." }), "client_name": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "Display name of the (first) streaming native client — the trust store's name for it, else\nthe name the device sent at connect. `null` when idle, for a nameless client, or for a\nGameStream session (that plane carries no device name)." })), "conflicts": Schema.optionalKey(Schema.Array(Schema.String).annotate({ "description": "Other Moonlight-compatible hosts (Sunshine/Apollo/…) detected on this machine at startup —\nrunning one alongside Punktfunk is unsupported. Compact labels (e.g. `Sunshine (running)`);\nthe tray/console surface them so the clash is visible before pairing silently fails." })), "games": Schema.optionalKey(Schema.Array(Schema.String).annotate({ "description": "Launched games the host is tracking, as compact labels (`Hades`, `Hades (closing in 4:12)`).\n\nThe countdown form is the one that matters: it means the game's client is gone and the host\nwill end the game when the window closes — something a user at the machine should be able to\nsee (and stop) without opening the console. Empty when nothing was launched." })), "kept_displays": Schema.Number.annotate({ "description": "Virtual displays being KEPT with no live session — lingering (keep-alive window) or pinned\n(`keep_alive: forever`). Non-zero means a display (and, exclusive, your physical monitors) is\nheld; the tray surfaces it + a one-click release. Active (in-use) displays are not counted.", "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "native_paired_clients": Schema.Number.annotate({ "description": "Number of paired native (punktfunk/1) devices.", "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "paired_clients": Schema.Number.annotate({ "description": "Number of pinned (paired) GameStream client certificates.", "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "pending_approvals": Schema.Number.annotate({ "description": "Native pairing knocks awaiting the operator's approval (count only).", "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "pin_pending": Schema.Boolean.annotate({ "description": "True while a GameStream pairing handshake is parked waiting for the user's PIN." }), "session": Schema.optionalKey(Schema.Union([Schema.Null, Schema.Struct({ "fps": Schema.Number.annotate({ "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "height": Schema.Number.annotate({ "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "width": Schema.Number.annotate({ "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)) }).annotate({ "description": "The active session: GameStream's launch (Moonlight `/launch`) when present, else the first\nlive native session. `null` when nothing is streaming." })], { mode: "oneOf" })), "version": Schema.String.annotate({ "description": "Host version (mirrors `/health`)." }), "video_streaming": Schema.Boolean.annotate({ "description": "True while video is streaming on EITHER plane: the GameStream media pipeline, or a live\nnative (punktfunk/1) session — the default plane, invisible in the GameStream flag alone." }) }).annotate({ "description": "Non-sensitive host status for the local tray icon: counts and booleans — no PIN values, no\nfingerprints. The ONE name exposed is `client_name`, the streaming client's display label\n(deliberate loosening for the tray's \"client connected\" toast: it tells the local user who is\non their machine, which is disclosure in the user's favor — and any local process could\nalready infer a session exists from the booleans here). Served unauthenticated to LOOPBACK\npeers only (see `require_auth`): the bearer-token file is SYSTEM/Administrators-DACL'd on\nWindows, so the per-user tray process cannot authenticate — this narrow read-only route is\nits status source." })
|
||||||
export type LogEntry = { readonly "level": string, readonly "msg": string, readonly "seq": number, readonly "target": string, readonly "ts_ms": number }
|
export type LogEntry = { readonly "level": string, readonly "msg": string, readonly "seq": number, readonly "target": string, readonly "ts_ms": number }
|
||||||
export const LogEntry = Schema.Struct({ "level": Schema.String.annotate({ "description": "`ERROR` | `WARN` | `INFO` | `DEBUG` | `TRACE`." }), "msg": Schema.String.annotate({ "description": "The formatted message, structured fields appended as `key=value`." }), "seq": Schema.Number.annotate({ "description": "Monotonic sequence number (1-based) — pass the last one back as the `after` cursor.", "format": "int64" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "target": Schema.String.annotate({ "description": "The emitting module path (tracing target)." }), "ts_ms": Schema.Number.annotate({ "description": "Unix timestamp in milliseconds.", "format": "int64" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)) }).annotate({ "description": "One captured log event." })
|
export const LogEntry = Schema.Struct({ "level": Schema.String.annotate({ "description": "`ERROR` | `WARN` | `INFO` | `DEBUG` | `TRACE`." }), "msg": Schema.String.annotate({ "description": "The formatted message, structured fields appended as `key=value`." }), "seq": Schema.Number.annotate({ "description": "Monotonic sequence number (1-based) — pass the last one back as the `after` cursor.", "format": "int64" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "target": Schema.String.annotate({ "description": "The emitting module path (tracing target)." }), "ts_ms": Schema.Number.annotate({ "description": "Unix timestamp in milliseconds.", "format": "int64" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)) }).annotate({ "description": "One captured log event." })
|
||||||
export type ModeConflict = "separate" | "steal" | "join" | "reject"
|
export type ModeConflict = "separate" | "steal" | "join" | "reject"
|
||||||
@@ -1524,8 +1524,8 @@ readonly "listLibraryScanners": <Config extends OperationConfig>(options: { read
|
|||||||
*/
|
*/
|
||||||
readonly "setLibraryScanner": <Config extends OperationConfig>(id: string, options: { readonly payload: typeof SetLibraryScannerRequestJson.Encoded; readonly config?: Config | undefined }) => Effect.Effect<WithOptionalResponse<typeof SetLibraryScanner200.Type, Config>, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"SetLibraryScanner401", typeof SetLibraryScanner401.Type> | PunktfunkError<"SetLibraryScanner404", typeof SetLibraryScanner404.Type> | PunktfunkError<"SetLibraryScanner500", typeof SetLibraryScanner500.Type>>
|
readonly "setLibraryScanner": <Config extends OperationConfig>(id: string, options: { readonly payload: typeof SetLibraryScannerRequestJson.Encoded; readonly config?: Config | undefined }) => Effect.Effect<WithOptionalResponse<typeof SetLibraryScanner200.Type, Config>, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"SetLibraryScanner401", typeof SetLibraryScanner401.Type> | PunktfunkError<"SetLibraryScanner404", typeof SetLibraryScanner404.Type> | PunktfunkError<"SetLibraryScanner500", typeof SetLibraryScanner500.Type>>
|
||||||
/**
|
/**
|
||||||
* Non-sensitive status (counts and booleans only — no PIN values, no fingerprints, no device
|
* Non-sensitive status (counts, booleans, and the streaming client's display name — no PIN
|
||||||
* names). Unauthenticated, but served to loopback peers only.
|
* values, no fingerprints). Unauthenticated, but served to loopback peers only.
|
||||||
*/
|
*/
|
||||||
readonly "getLocalSummary": <Config extends OperationConfig>(options: { readonly config?: Config | undefined } | undefined) => Effect.Effect<WithOptionalResponse<typeof GetLocalSummary200.Type, Config>, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"GetLocalSummary401", typeof GetLocalSummary401.Type>>
|
readonly "getLocalSummary": <Config extends OperationConfig>(options: { readonly config?: Config | undefined } | undefined) => Effect.Effect<WithOptionalResponse<typeof GetLocalSummary200.Type, Config>, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"GetLocalSummary401", typeof GetLocalSummary401.Type>>
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user