diff --git a/sdk/README.md b/sdk/README.md index f633fbfe..dfb4a74f 100644 --- a/sdk/README.md +++ b/sdk/README.md @@ -6,7 +6,8 @@ stream start/stop, pairing, displays, library) — built on [Effect](https://eff Two surfaces, one core: -- **`@punktfunk/host`** — the Promise facade, the front door. `connect()`, `await`, `.on()`. +- **`@punktfunk/host`** — the Promise facade, the front door. `connect()`, then `pf.api.*` (the + typed management API — every endpoint autocompletes, every response is typed) and `pf.events.on()`. You never need to know Effect exists. - **`@punktfunk/host/effect`** — the Effect-native surface for plugins and composed programs: the `PunktfunkHost` service + layer, `Stream`-based events, typed errors @@ -21,15 +22,25 @@ import { connect } from "@punktfunk/host"; const pf = await connect(); // zero config on the host box +// Typed API — autocomplete every endpoint, typed responses, no hand-written paths or casts. +const clients = await pf.api.listPairedClients(); +console.log(`${clients.length} paired clients`); + +// Live events: pf.events.on("stream.started", (e) => { console.log(`${e.stream.client} started ${e.stream.mode}${e.stream.hdr ? " HDR" : ""}`); }); pf.events.on("pairing.pending", async (e) => { - // notify your phone, then decide through the API: - // await pf.request("POST", `/native/pending/${id}/approve`); + // notify your phone, then decide through the typed API: + const pending = await pf.api.listPendingDevices(); + const match = pending.find((d) => d.fingerprint === e.device.fingerprint); + if (match) await pf.api.approvePendingDevice(String(match.id), { payload: {} }); }); ``` +Need something the generated client doesn't cover? `pf.request(method, path, body)` is the untyped +escape hatch (returns `unknown`). + The same, Effect-native: ```ts @@ -43,6 +54,18 @@ const program = events().pipe( Effect.runPromise(program.pipe(Effect.provide(PunktfunkHostLive()))); ``` +## Examples + +A complexity ladder in [`examples/`](./examples) — start at the top: + +1. [`tail-events.ts`](./examples/tail-events.ts) — **hello world**: connect, one typed call, tail events. +2. [`notify-pairing.ts`](./examples/notify-pairing.ts) — **event → decision**: approve/deny pairing through the typed API. +3. [`provider-sync.ts`](./examples/provider-sync.ts) — **typed bulk REST**: declaratively reconcile a game-library provider. +4. [`couch-preset.effect.ts`](./examples/couch-preset.effect.ts) — **advanced, Effect-native**: only if you're composing Effect programs. + +Examples 1–3 are the plain Promise facade and cover most automation; you only need example 4's +Effect surface for composed, interruptible programs. Run any with `bun examples/.ts`. + ## Connection resolution `connect()` / `PunktfunkHostLive()` resolve, in order: @@ -166,7 +189,7 @@ Windows Task Scheduler: a task triggered *At log on* running ```sh bun install -bun run gen # regenerate src/gen/schemas.ts from ../api/openapi.json +bun run gen # regenerate src/gen/punktfunk.ts from ../api/openapi.json (@effect/openapi-generator) bun run typecheck bun test ``` diff --git a/sdk/bun.lock b/sdk/bun.lock index 29aaaa0b..07b1eccf 100644 --- a/sdk/bun.lock +++ b/sdk/bun.lock @@ -5,11 +5,12 @@ "": { "name": "@punktfunk/host", "dependencies": { - "effect": "^3.19.0", + "effect": "^4.0.0-beta.98", }, "devDependencies": { + "@effect/openapi-generator": "4.0.0-beta.98", + "@effect/platform-node": "4.0.0-beta.98", "@types/bun": "^1.3.0", - "orval": "^8.20.0", "typescript": "^5.9.3", }, "optionalDependencies": { @@ -18,286 +19,178 @@ }, }, "packages": { - "@commander-js/extra-typings": ["@commander-js/extra-typings@15.0.0", "", { "peerDependencies": { "commander": "~15.0.0" } }, "sha512-yeJlba62xqmkgELUsn7356MEnzLLu/fw2x4lofFqGnXh6YysRdEs2BaLeLtg1+KU0AXvMeqQvTTp+3hBEBK+EA=="], + "@effect/openapi-generator": ["@effect/openapi-generator@4.0.0-beta.98", "", { "dependencies": { "swagger2openapi": "^7.0.8" }, "peerDependencies": { "@effect/platform-node": "^4.0.0-beta.98", "effect": "^4.0.0-beta.98" }, "bin": { "openapigen": "dist/bin.js" } }, "sha512-7bqawr/HqJWqQ8H/bHyzBlLPA3LIIm3Y+cGYlIxnC/QVK795QpiEXb7uxTnP7V7w49V0sBtTerv4/9ZjsMffLQ=="], - "@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.28.1", "", { "os": "aix", "cpu": "ppc64" }, "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ=="], + "@effect/platform-node": ["@effect/platform-node@4.0.0-beta.98", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-beta.98", "mime": "^4.1.0", "undici": "^8.7.0" }, "peerDependencies": { "effect": "^4.0.0-beta.98", "ioredis": "^5.7.0" } }, "sha512-IQu1TiLXQEDSGkDBllyYjVadf+UqdjptryqX4mmktVTTbGDq7X4uVxe7cSgXuqZvyfG6kagTzwj2lfynxOaKQg=="], - "@esbuild/android-arm": ["@esbuild/android-arm@0.28.1", "", { "os": "android", "cpu": "arm" }, "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ=="], + "@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.98", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.21.0" }, "peerDependencies": { "effect": "^4.0.0-beta.98" } }, "sha512-iySXaffnCJX1sNAIp79ghhIeui9E5qwUQyqd1VLPkB9UNO4vdpd9B5fTEXwe7S/GusL4jsk9vSvX38XJgRFG1w=="], - "@esbuild/android-arm64": ["@esbuild/android-arm64@0.28.1", "", { "os": "android", "cpu": "arm64" }, "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg=="], + "@exodus/schemasafe": ["@exodus/schemasafe@1.3.0", "", {}, "sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw=="], - "@esbuild/android-x64": ["@esbuild/android-x64@0.28.1", "", { "os": "android", "cpu": "x64" }, "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng=="], + "@ioredis/commands": ["@ioredis/commands@1.10.0", "", {}, "sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q=="], - "@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.28.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q=="], + "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ=="], - "@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.28.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ=="], + "@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w=="], - "@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.28.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw=="], + "@msgpackr-extract/msgpackr-extract-linux-arm": ["@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4", "", { "os": "linux", "cpu": "arm" }, "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw=="], - "@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.28.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ=="], + "@msgpackr-extract/msgpackr-extract-linux-arm64": ["@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4", "", { "os": "linux", "cpu": "arm64" }, "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw=="], - "@esbuild/linux-arm": ["@esbuild/linux-arm@0.28.1", "", { "os": "linux", "cpu": "arm" }, "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ=="], + "@msgpackr-extract/msgpackr-extract-linux-x64": ["@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4", "", { "os": "linux", "cpu": "x64" }, "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ=="], - "@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.28.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g=="], - - "@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.28.1", "", { "os": "linux", "cpu": "ia32" }, "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w=="], - - "@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg=="], - - "@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ=="], - - "@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.28.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ=="], - - "@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.28.1", "", { "os": "linux", "cpu": "none" }, "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ=="], - - "@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.28.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag=="], - - "@esbuild/linux-x64": ["@esbuild/linux-x64@0.28.1", "", { "os": "linux", "cpu": "x64" }, "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA=="], - - "@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw=="], - - "@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.28.1", "", { "os": "none", "cpu": "x64" }, "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg=="], - - "@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.28.1", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q=="], - - "@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.28.1", "", { "os": "openbsd", "cpu": "x64" }, "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw=="], - - "@esbuild/openharmony-arm64": ["@esbuild/openharmony-arm64@0.28.1", "", { "os": "none", "cpu": "arm64" }, "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg=="], - - "@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.28.1", "", { "os": "sunos", "cpu": "x64" }, "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ=="], - - "@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.28.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA=="], - - "@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.28.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg=="], - - "@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.1", "", { "os": "win32", "cpu": "x64" }, "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A=="], - - "@gerrit0/mini-shiki": ["@gerrit0/mini-shiki@3.23.0", "", { "dependencies": { "@shikijs/engine-oniguruma": "^3.23.0", "@shikijs/langs": "^3.23.0", "@shikijs/themes": "^3.23.0", "@shikijs/types": "^3.23.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-bEMORlG0cqdjVyCEuU0cDQbORWX+kYCeo0kV1lbxF5bt4r7SID2l9bqsxJEM0zndaxpOUT7riCyIVEuqq/Ynxg=="], - - "@orval/angular": ["@orval/angular@8.22.0", "", { "dependencies": { "@orval/core": "8.22.0" } }, "sha512-lSvaNj+VHGIWBQOG104HfPNrk2xGjpArwy67u6ZBPzHo/OtDE5Tm1t+ARYseCOElFr4z3i3A62qjFLFy6zj00Q=="], - - "@orval/axios": ["@orval/axios@8.22.0", "", { "dependencies": { "@orval/core": "8.22.0" } }, "sha512-fFu0UgTbpI9a1ayM7qCs55MMy6MlVgOpE3BBXGukCTBSwNwFLN47WdzQev/x0mAcs58pNKYT0KL4R9MXGnmgfg=="], - - "@orval/core": ["@orval/core@8.22.0", "", { "dependencies": { "@scalar/openapi-types": "0.8.0", "acorn": "^8.15.0", "compare-versions": "^6.1.1", "debug": "^4.4.3", "esbuild": "^0.28.0", "esutils": "2.0.3", "fs-extra": "^11.3.2", "jiti": "^2.6.1", "jsesc": "^3.0.0", "remeda": "^2.33.6", "tinyglobby": "^0.2.16", "typedoc": "^0.28.19" }, "peerDependencies": { "@faker-js/faker": ">=10" }, "optionalPeers": ["@faker-js/faker"] }, "sha512-uKYi7+Smg6oQ2MxE0AS2FNI7bwssoxGLh391Uk0FU+DcTcSv9q2GmvoM8uwd827Hok29kD7AegHE8Mhmsa5uWg=="], - - "@orval/effect": ["@orval/effect@8.22.0", "", { "dependencies": { "@orval/core": "8.22.0", "remeda": "^2.33.6" } }, "sha512-DACiw2+0ZsPJPKQbYlb9qFFFppaCBaT/HgIq2S1asH9ZCOOTZKm9VrRXpeCWINHC2w1BpdSATpytv+61UT5Y/A=="], - - "@orval/fetch": ["@orval/fetch@8.22.0", "", { "dependencies": { "@orval/core": "8.22.0" } }, "sha512-G0r6hOdZG963H/8S4Vk1kWfU/hkQC/cwpDZL+mzcXgzrdG9NDloshTqmge/jP5lPsFAwfcRUmmts10OmuIE4BQ=="], - - "@orval/hono": ["@orval/hono@8.22.0", "", { "dependencies": { "@orval/core": "8.22.0", "@orval/zod": "8.22.0", "fs-extra": "^11.3.2" }, "peerDependencies": { "typescript": ">=5" }, "optionalPeers": ["typescript"] }, "sha512-GMCpGZqCuGYSu10KTt2q3WYzCqEcTGxr18z3HgHv9dX22sv/V76dlLBh3SE72xp8Z7/jmoq3GKBdgU9k98ju0w=="], - - "@orval/mcp": ["@orval/mcp@8.22.0", "", { "dependencies": { "@orval/core": "8.22.0", "@orval/fetch": "8.22.0", "@orval/zod": "8.22.0" } }, "sha512-ySa4EenAF8YMCpbmddJGO3lItTPC8Uf/iT+P2ezaowVgxOCPX/fjG7dd0fnVmrr0vaphi53yWx5o5DlT17FzLg=="], - - "@orval/mock": ["@orval/mock@8.22.0", "", { "dependencies": { "@orval/core": "8.22.0", "remeda": "^2.33.6" } }, "sha512-ZCEFpdi4z+YOCg0Tsgz6DO/M1TSfYxE5urDWJgYNUKeD8XDEHFb2IgTiiaAwJunqWnRQuiMfGp9G81+u10Kx6w=="], - - "@orval/query": ["@orval/query@8.22.0", "", { "dependencies": { "@orval/core": "8.22.0", "@orval/fetch": "8.22.0", "remeda": "^2.33.6" } }, "sha512-IH9049z860CLUeGEezGv+yOvUvcAyDnd9doDe1Ryi0WxsDul2Vh3LjCRUEQf4JZiyZezadWYYGzs0lwnCn/afA=="], - - "@orval/solid-start": ["@orval/solid-start@8.22.0", "", { "dependencies": { "@orval/core": "8.22.0" } }, "sha512-Q1ZCWOrA6/VJnyj62XpPxXti9NpAA4lDZVqPL2uQ5gbxv+T+azEaazKqYIpSBJXbl1aEezujdcyg/DVUakWKEw=="], - - "@orval/swr": ["@orval/swr@8.22.0", "", { "dependencies": { "@orval/core": "8.22.0", "@orval/fetch": "8.22.0" } }, "sha512-OSeWX3Af8ESapKIo3XpbOaTMaG8oeEtuucFyOUauqg6NyC6/9Ikcf2csBdF0AuIKA0QTcjcXxra/ccj1f7KnRg=="], - - "@orval/zod": ["@orval/zod@8.22.0", "", { "dependencies": { "@orval/core": "8.22.0", "jsesc": "^3.0.0", "remeda": "^2.33.6" } }, "sha512-briHtUTz79fvCeIFfGW3IbmUIF9DOotUoWFa/sKUjRUG8PMGDWSjQzO8QdLzLoETVGo9g4TfGwp5Xjcs81GZTA=="], - - "@scalar/helpers": ["@scalar/helpers@0.9.1", "", {}, "sha512-UKSLIPfN++f+zzbsZ+F6I0lNrR4yX4PtokjHgGwGiHapxT5VJqAF4zFTIP2KCd27XG7KnAIA7qq62O4xRBxc6w=="], - - "@scalar/json-magic": ["@scalar/json-magic@0.12.18", "", { "dependencies": { "@scalar/helpers": "0.9.1", "pathe": "^2.0.3", "yaml": "^2.8.3" } }, "sha512-3oZr+jUUiwD3C+x2CROBtlyIUnwt9ScRhFfczuOTPZlD7Pnb5cyTTfkiHbxxU4qd66ij0Z3JA4X/Nj0OADyEiA=="], - - "@scalar/openapi-parser": ["@scalar/openapi-parser@0.28.9", "", { "dependencies": { "@scalar/helpers": "0.9.1", "@scalar/json-magic": "0.12.18", "@scalar/openapi-types": "0.9.2", "@scalar/openapi-upgrader": "0.2.10", "ajv": "^8.17.1", "ajv-draft-04": "^1.0.0", "ajv-formats": "^3.0.1", "jsonpointer": "^5.0.1", "leven": "^4.0.0", "yaml": "^2.8.3" } }, "sha512-AYmqO7dR6zc1TSVhW5BpwY0nUMfG5JuF85/8YXn5ZXcCvjgoJu8dbxX52vIGK9rRwSn07HXCdKk7KPIF4xfOTA=="], - - "@scalar/openapi-types": ["@scalar/openapi-types@0.8.0", "", {}, "sha512-WmaxVSfvY5K/TwcG2B2TU1WOe1As1uc2s7myswtP6dBlcjU3hM08SApxv/jmyGaCE8t4gO5BBhmHY4pDUfmr2g=="], - - "@scalar/openapi-upgrader": ["@scalar/openapi-upgrader@0.2.10", "", { "dependencies": { "@scalar/openapi-types": "0.9.2" } }, "sha512-FRYwCl4IXRi7yAF5/3Jho0jc2akTZfr/vRTYMjhm+96Fq0LaEuHJAvPcSkkU+j3IWGie6LaCQMZTngcSjoIzgA=="], - - "@sec-ant/readable-stream": ["@sec-ant/readable-stream@0.4.1", "", {}, "sha512-831qok9r2t8AlxLko40y2ebgSDhenenCatLVeW/uBtnHPyhHOvG0C7TvfgecV+wHzIm5KUICgzmVpWS+IMEAeg=="], - - "@shikijs/engine-oniguruma": ["@shikijs/engine-oniguruma@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0", "@shikijs/vscode-textmate": "^10.0.2" } }, "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g=="], - - "@shikijs/langs": ["@shikijs/langs@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg=="], - - "@shikijs/themes": ["@shikijs/themes@3.23.0", "", { "dependencies": { "@shikijs/types": "3.23.0" } }, "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA=="], - - "@shikijs/types": ["@shikijs/types@3.23.0", "", { "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", "@types/hast": "^3.0.4" } }, "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ=="], - - "@shikijs/vscode-textmate": ["@shikijs/vscode-textmate@10.0.2", "", {}, "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg=="], - - "@sindresorhus/merge-streams": ["@sindresorhus/merge-streams@4.0.0", "", {}, "sha512-tlqY9xq5ukxTUZBmoOp+m61cqwQD5pHJtFY3Mn8CA8ps6yghLH/Hw8UPdqg4OLmFW3IFlcXnQNmo/dh8HzXYIQ=="], + "@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4", "", { "os": "win32", "cpu": "x64" }, "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ=="], "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], "@types/bun": ["@types/bun@1.3.14", "", { "dependencies": { "bun-types": "1.3.14" } }, "sha512-h1hFqFVcvAvD9j9K7ZW7vd82aSA+rTdznZa+5bwvCwqSB1jmmfLcbIWhOLx1/+boy/xmjgCs/OMUL8hRJSmnPw=="], - "@types/hast": ["@types/hast@3.0.5", "", { "dependencies": { "@types/unist": "*" } }, "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g=="], - "@types/node": ["@types/node@26.1.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw=="], - "@types/unist": ["@types/unist@3.0.3", "", {}, "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q=="], + "@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="], - "acorn": ["acorn@8.17.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-xRQbDb9BnwDafYNn6Vwl839DYVjqXYb1XVGtWAZ1kcDc6iwAL4hg3B1dZlRiuENFeO2H53gFG3in621AdERVAg=="], + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], - "ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], - - "ajv-draft-04": ["ajv-draft-04@1.0.0", "", { "peerDependencies": { "ajv": "^8.5.0" }, "optionalPeers": ["ajv"] }, "sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw=="], - - "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], - - "argparse": ["argparse@2.0.1", "", {}, "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="], - - "balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="], - - "brace-expansion": ["brace-expansion@5.0.7", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA=="], + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="], - "chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="], + "call-me-maybe": ["call-me-maybe@1.0.2", "", {}, "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ=="], - "commander": ["commander@15.0.0", "", {}, "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg=="], + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], - "compare-versions": ["compare-versions@6.1.1", "", {}, "sha512-4hm4VPpIecmlg59CHXnRDnqGplJFrbLG4aFEl5vl6cK1u76ws3LLvX7ikFnTDl5vo39sjWD6AaDPYodJp/NNHg=="], + "cluster-key-slot": ["cluster-key-slot@1.1.1", "", {}, "sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw=="], - "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], - "effect": ["effect@3.22.0", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-jhYFe0zTlIRqYFrKTS+6luhmS/Tm0f+JLo0K9KUxvtFab1SUGEszQi2ehOP6QzAZvy831lDmTwwzvVDZSPNz3g=="], + "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="], - "entities": ["entities@4.5.0", "", {}, "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw=="], + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], - "esbuild": ["esbuild@0.28.1", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.28.1", "@esbuild/android-arm": "0.28.1", "@esbuild/android-arm64": "0.28.1", "@esbuild/android-x64": "0.28.1", "@esbuild/darwin-arm64": "0.28.1", "@esbuild/darwin-x64": "0.28.1", "@esbuild/freebsd-arm64": "0.28.1", "@esbuild/freebsd-x64": "0.28.1", "@esbuild/linux-arm": "0.28.1", "@esbuild/linux-arm64": "0.28.1", "@esbuild/linux-ia32": "0.28.1", "@esbuild/linux-loong64": "0.28.1", "@esbuild/linux-mips64el": "0.28.1", "@esbuild/linux-ppc64": "0.28.1", "@esbuild/linux-riscv64": "0.28.1", "@esbuild/linux-s390x": "0.28.1", "@esbuild/linux-x64": "0.28.1", "@esbuild/netbsd-arm64": "0.28.1", "@esbuild/netbsd-x64": "0.28.1", "@esbuild/openbsd-arm64": "0.28.1", "@esbuild/openbsd-x64": "0.28.1", "@esbuild/openharmony-arm64": "0.28.1", "@esbuild/sunos-x64": "0.28.1", "@esbuild/win32-arm64": "0.28.1", "@esbuild/win32-ia32": "0.28.1", "@esbuild/win32-x64": "0.28.1" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw=="], + "effect": ["effect@4.0.0-beta.98", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.9.0", "find-my-way-ts": "^0.1.6", "ini": "^7.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^2.0.4", "multipasta": "^0.2.8", "toml": "^4.1.2", "uuid": "^14.0.1", "yaml": "^2.9.0" } }, "sha512-oz+bsG5h+6RNrw4t5GMfQrk/xBS8ROoqkYsuvRhBr5O7mCOrpvH/hbw+QrDzvKIpX4HJClwm86F94c87W0sJxg=="], - "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], - "execa": ["execa@9.6.1", "", { "dependencies": { "@sindresorhus/merge-streams": "^4.0.0", "cross-spawn": "^7.0.6", "figures": "^6.1.0", "get-stream": "^9.0.0", "human-signals": "^8.0.1", "is-plain-obj": "^4.1.0", "is-stream": "^4.0.1", "npm-run-path": "^6.0.0", "pretty-ms": "^9.2.0", "signal-exit": "^4.1.0", "strip-final-newline": "^4.0.0", "yoctocolors": "^2.1.1" } }, "sha512-9Be3ZoN4LmYR90tUoVu2te2BsbzHfhJyfEiAVfz7N5/zv+jduIfLrV2xdQXOHbaD6KgpGdO9PRPM1Y4Q9QkPkA=="], + "es6-promise": ["es6-promise@3.3.1", "", {}, "sha512-SOp9Phqvqn7jtEUxPWdWfWoLmyt2VaJ6MpvP9Comy1MceMXqE6bxvaTu4iaxpYYPzhny28Lc+M87/c2cPK6lDg=="], - "fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="], + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], - "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + "fast-check": ["fast-check@4.9.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-7ms6T7SybUev/PQITciI0yLM2pOSFy5zpG8Ty7tQofcVaQUvrMXp6CBwqF6fThLCLOrfBtuHAtwq6Yu4XPCllg=="], - "fast-uri": ["fast-uri@3.1.3", "", {}, "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg=="], + "fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="], - "fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg=="], + "find-my-way-ts": ["find-my-way-ts@0.1.6", "", {}, "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA=="], - "figures": ["figures@6.1.0", "", { "dependencies": { "is-unicode-supported": "^2.0.0" } }, "sha512-d+l3qxjSesT4V7v2fh+QnmFnUWv9lSpjarhShNTgBOfA0ttejbQUAlHLitbjkoRiDulW0OPoQPYIGhIC8ohejg=="], + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], - "find-up": ["find-up@8.0.0", "", { "dependencies": { "locate-path": "^8.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-JGG8pvDi2C+JxidYdIwQDyS/CgcrIdh18cvgxcBge3wSHRQOrooMD3GlFBcmMJAN9M42SAZjDp5zv1dglJjwww=="], + "http2-client": ["http2-client@1.3.5", "", {}, "sha512-EC2utToWl4RKfs5zd36Mxq7nzHHBuomZboI0yYL6Y0RmBgT7Sgkq4rQ0ezFTYoIsSs7Tm9SJe+o2FcAg6GBhGA=="], - "fs-extra": ["fs-extra@11.3.6", "", { "dependencies": { "graceful-fs": "^4.2.0", "jsonfile": "^6.0.1", "universalify": "^2.0.0" } }, "sha512-w8ZNZr2mKIc7qeNaQ9AVPT1+iFaI+Avd4xudVOvdDJ8VytREi1Ft5Ih7hd9jjehod8vAM5GMsfQ/TpPf4EyoEA=="], + "ini": ["ini@7.0.0", "", {}, "sha512-ifK0CgjALofS5bkrcTy4RaQ9Vx2Knf/eLeIO+NaswQEpH1UblrtTSCIvN71qQDMq0PeQ/SSPojvEJp9vvvfr+w=="], - "get-stream": ["get-stream@9.0.1", "", { "dependencies": { "@sec-ant/readable-stream": "^0.4.1", "is-stream": "^4.0.1" } }, "sha512-kVCxPF3vQM/N0B1PmoqVUqgHP+EeVjmZSQn+1oCRPxd2P21P2F19lIgbR3HBosbB1PUhOAoctJnfEn2GbN2eZA=="], + "ioredis": ["ioredis@5.11.1", "", { "dependencies": { "@ioredis/commands": "1.10.0", "cluster-key-slot": "1.1.1", "debug": "4.4.3", "denque": "2.1.0", "redis-errors": "1.2.0", "redis-parser": "3.0.0", "standard-as-callback": "2.1.0" } }, "sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A=="], - "get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA=="], + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], - "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + "kubernetes-types": ["kubernetes-types@1.30.0", "", {}, "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q=="], - "human-signals": ["human-signals@8.0.1", "", {}, "sha512-eKCa6bwnJhvxj14kZk5NCPc6Hb6BdsU9DZcOnmQKSnO1VKrfV0zCvtttPZUsBvjmNDn8rpcJfpwSYnHBjc95MQ=="], - - "is-plain-obj": ["is-plain-obj@4.1.0", "", {}, "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg=="], - - "is-stream": ["is-stream@4.0.1", "", {}, "sha512-Dnz92NInDqYckGEUJv689RbRiTSEHCQ7wOVeALbkOz999YpqT46yMRIGtSNl2iCL1waAZSx40+h59NV/EwzV/A=="], - - "is-unicode-supported": ["is-unicode-supported@2.1.0", "", {}, "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ=="], - - "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], - - "jiti": ["jiti@2.7.0", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ=="], - - "js-yaml": ["js-yaml@4.2.0", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": { "js-yaml": "bin/js-yaml.js" } }, "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw=="], - - "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], - - "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], - - "jsonfile": ["jsonfile@6.2.1", "", { "dependencies": { "universalify": "^2.0.0" }, "optionalDependencies": { "graceful-fs": "^4.1.6" } }, "sha512-zwOTdL3rFQ/lRdBnntKVOX6k5cKJwEc1HdilT71BWEu7J41gXIB2MRp+vxduPSwZJPWBxEzv4yH1wYLJGUHX4Q=="], - - "jsonpointer": ["jsonpointer@5.0.1", "", {}, "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ=="], - - "leven": ["leven@4.1.0", "", {}, "sha512-KZ9W9nWDT7rF7Dazg8xyLHGLrmpgq2nVNFUckhqdW3szVP6YhCpp/RAnpmVExA9JvrMynjwSLVrEj3AepHR6ew=="], - - "linkify-it": ["linkify-it@5.0.2", "", { "dependencies": { "uc.micro": "^2.0.0" } }, "sha512-ONTm2jCMAVZjgQa/Fy1kScXsuOoF5NPTsoFBdE1KVIZ2vAh/r9+Bqo+0jINCBYnavTPQZz38QzFTme79ENoN3Q=="], - - "locate-path": ["locate-path@8.0.0", "", { "dependencies": { "p-locate": "^6.0.0" } }, "sha512-XT9ewWAC43tiAV7xDAPflMkG0qOPn2QjHqlgX8FOqmWa/rxnyYDulF9T0F7tRy1u+TVTmK/M//6VIOye+2zDXg=="], - - "lunr": ["lunr@2.3.9", "", {}, "sha512-zTU3DaZaF3Rt9rhN3uBMGQD3dD2/vFQqnvZCDv4dl5iOzq2IZQqTxu90r4E5J+nP70J3ilqVCrbho2eWaeW8Ow=="], - - "markdown-it": ["markdown-it@14.3.0", "", { "dependencies": { "argparse": "^2.0.1", "entities": "^4.5.0", "linkify-it": "^5.0.2", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" }, "bin": { "markdown-it": "bin/markdown-it.mjs" } }, "sha512-RCEsPjR+sr0x+AuYp601tKTkgFG4YEPLCzHST3cQ/fhlJkqAkz1L2/Qbp1j9qw5SBwQHFBoW8+hoN5xssOF0Tw=="], - - "mdurl": ["mdurl@2.0.0", "", {}, "sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w=="], - - "minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg=="], + "mime": ["mime@4.1.0", "", { "bin": { "mime": "bin/cli.js" } }, "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], - "npm-run-path": ["npm-run-path@6.0.0", "", { "dependencies": { "path-key": "^4.0.0", "unicorn-magic": "^0.3.0" } }, "sha512-9qny7Z9DsQU8Ou39ERsPU4OZQlSTP47ShQzuKZ6PRXpYLtIFgl/DEBYEXKlvcEa+9tHVcK8CF81Y2V72qaZhWA=="], + "msgpackr": ["msgpackr@2.0.4", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.4" } }, "sha512-o1C5KRmuRt+apqMr1HuGSqWStZoRBUpEsCsl15uM9VdAF1qHLtvMOU2En747EnTyEl6c4pzPewRMFF31s1CNbA=="], - "orval": ["orval@8.22.0", "", { "dependencies": { "@commander-js/extra-typings": "^15.0.0", "@orval/angular": "8.22.0", "@orval/axios": "8.22.0", "@orval/core": "8.22.0", "@orval/effect": "8.22.0", "@orval/fetch": "8.22.0", "@orval/hono": "8.22.0", "@orval/mcp": "8.22.0", "@orval/mock": "8.22.0", "@orval/query": "8.22.0", "@orval/solid-start": "8.22.0", "@orval/swr": "8.22.0", "@orval/zod": "8.22.0", "@scalar/json-magic": "^0.12.16", "@scalar/openapi-parser": "^0.28.7", "@scalar/openapi-types": "0.8.0", "chokidar": "^5.0.0", "commander": "^15.0.0", "execa": "^9.6.1", "find-up": "8.0.0", "fs-extra": "^11.3.2", "get-tsconfig": "^4.14.0", "jiti": "^2.6.1", "js-yaml": "4.2.0", "remeda": "^2.33.6", "string-argv": "^0.3.2", "typedoc": "^0.28.19", "typedoc-plugin-coverage": "^4.0.2", "typedoc-plugin-markdown": "^4.10.0" }, "peerDependencies": { "prettier": ">=3.0.0" }, "optionalPeers": ["prettier"], "bin": { "orval": "dist/bin/orval.mjs" } }, "sha512-N8UmB4DOhW+Z2SzVq4oS/pN3DsRPq+msrRU8NyRldP1i0yiqT6qo8mUxHPk2umqYjyY0FlR2mvPbi/Jf5CiP7A=="], + "msgpackr-extract": ["msgpackr-extract@3.0.4", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw=="], - "p-limit": ["p-limit@4.0.0", "", { "dependencies": { "yocto-queue": "^1.0.0" } }, "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ=="], + "multipasta": ["multipasta@0.2.8", "", {}, "sha512-ZPWuMKyv0cSO29f7hozp+k6+crZbQijV8ipMvxNxRf2SwtYGTX1ZX89Kd20VV4H9Znonx+EQn+iy1wGQsJ+b+Q=="], - "p-locate": ["p-locate@6.0.0", "", { "dependencies": { "p-limit": "^4.0.0" } }, "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw=="], + "node-fetch": ["node-fetch@2.7.0", "", { "dependencies": { "whatwg-url": "^5.0.0" }, "peerDependencies": { "encoding": "^0.1.0" }, "optionalPeers": ["encoding"] }, "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="], - "parse-ms": ["parse-ms@4.0.0", "", {}, "sha512-TXfryirbmq34y8QBwgqCVLi+8oA3oWx2eAnSn62ITyEhEYaWRlVZ2DvMM9eZbMs/RfxPu/PK/aBLyGj4IrqMHw=="], + "node-fetch-h2": ["node-fetch-h2@2.3.0", "", { "dependencies": { "http2-client": "^1.2.5" } }, "sha512-ofRW94Ab0T4AOh5Fk8t0h8OBWrmjb0SSB20xh1H8YnPV9EJ+f5AMoYSUQ2zgJ4Iq2HAK0I2l5/Nequ8YzFS3Hg=="], - "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="], - "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], + "node-readfiles": ["node-readfiles@0.2.0", "", { "dependencies": { "es6-promise": "^3.2.1" } }, "sha512-SU00ZarexNlE4Rjdm83vglt5Y9yiQ+XI1XpflWlb7q7UTN1JUItm69xMeiQCTxtTfnzt+83T8Cx+vI2ED++VDA=="], - "picomatch": ["picomatch@4.0.5", "", {}, "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A=="], + "oas-kit-common": ["oas-kit-common@1.0.8", "", { "dependencies": { "fast-safe-stringify": "^2.0.7" } }, "sha512-pJTS2+T0oGIwgjGpw7sIRU8RQMcUoKCDWFLdBqKB2BNmGpbBMH2sdqAaOXUg8OzonZHU0L7vfJu1mJFEiYDWOQ=="], - "pretty-ms": ["pretty-ms@9.3.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-gjVS5hOP+M3wMm5nmNOucbIrqudzs9v/57bWRHQWLYklXqoXKrVfYW2W9+glfGsqtPgpiz5WwyEEB+ksXIx3gQ=="], + "oas-linter": ["oas-linter@3.2.2", "", { "dependencies": { "@exodus/schemasafe": "^1.0.0-rc.2", "should": "^13.2.1", "yaml": "^1.10.0" } }, "sha512-KEGjPDVoU5K6swgo9hJVA/qYGlwfbFx+Kg2QB/kd7rzV5N8N5Mg6PlsoCMohVnQmo+pzJap/F610qTodKzecGQ=="], - "punycode.js": ["punycode.js@2.3.1", "", {}, "sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA=="], + "oas-resolver": ["oas-resolver@2.5.6", "", { "dependencies": { "node-fetch-h2": "^2.3.0", "oas-kit-common": "^1.0.8", "reftools": "^1.1.9", "yaml": "^1.10.0", "yargs": "^17.0.1" }, "bin": { "resolve": "resolve.js" } }, "sha512-Yx5PWQNZomfEhPPOphFbZKi9W93CocQj18NlD2Pa4GWZzdZpSJvYwoiuurRI7m3SpcChrnO08hkuQDL3FGsVFQ=="], - "pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="], + "oas-schema-walker": ["oas-schema-walker@1.1.5", "", {}, "sha512-2yucenq1a9YPmeNExoUa9Qwrt9RFkjqaMAA1X+U7sbb0AqBeTIdMHky9SQQ6iN94bO5NW0W4TRYXerG+BdAvAQ=="], - "readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="], + "oas-validator": ["oas-validator@5.0.8", "", { "dependencies": { "call-me-maybe": "^1.0.1", "oas-kit-common": "^1.0.8", "oas-linter": "^3.2.2", "oas-resolver": "^2.5.6", "oas-schema-walker": "^1.1.5", "reftools": "^1.1.9", "should": "^13.2.1", "yaml": "^1.10.0" } }, "sha512-cu20/HE5N5HKqVygs3dt94eYJfBi0TsZvPVXDhbXQHiEityDN+RROTleefoKRKKJ9dFAF2JBkDHgvWj0sjKGmw=="], - "remeda": ["remeda@2.39.0", "", {}, "sha512-3Ki8dU1o3OVu4dwIQ2Pj+yiuP7OnEbmWAGmJ3yDRqopily5jsj8NWzPvbS89H85d6UdONKEcUnrfuHY6jN9vyw=="], + "pure-rand": ["pure-rand@8.4.2", "", {}, "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng=="], - "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + "redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="], - "resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="], + "redis-parser": ["redis-parser@3.0.0", "", { "dependencies": { "redis-errors": "^1.0.0" } }, "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A=="], - "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + "reftools": ["reftools@1.1.9", "", {}, "sha512-OVede/NQE13xBQ+ob5CKd5KyeJYU2YInb1bmV4nRoOfquZPkAkxuOXicSe1PvqIuZZ4kD13sPKBbR7UFDmli6w=="], - "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], - "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + "should": ["should@13.2.3", "", { "dependencies": { "should-equal": "^2.0.0", "should-format": "^3.0.3", "should-type": "^1.4.0", "should-type-adaptors": "^1.0.1", "should-util": "^1.0.0" } }, "sha512-ggLesLtu2xp+ZxI+ysJTmNjh2U0TsC+rQ/pfED9bUZZ4DKefP27D+7YJVVTvKsmjLpIi9jAa7itwDGkDDmt1GQ=="], - "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], + "should-equal": ["should-equal@2.0.0", "", { "dependencies": { "should-type": "^1.4.0" } }, "sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA=="], - "strip-final-newline": ["strip-final-newline@4.0.0", "", {}, "sha512-aulFJcD6YK8V1G7iRB5tigAP4TsHBZZrOV8pjV++zdUwmeV8uzbY7yn6h9MswN62adStNZFuCIx4haBnRuMDaw=="], + "should-format": ["should-format@3.0.3", "", { "dependencies": { "should-type": "^1.3.0", "should-type-adaptors": "^1.0.1" } }, "sha512-hZ58adtulAk0gKtua7QxevgUaXTTXxIi8t41L3zo9AHvjXO1/7sdLECuHeIN2SRtYXpNkmhoUP2pdeWgricQ+Q=="], - "tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="], + "should-type": ["should-type@1.4.0", "", {}, "sha512-MdAsTu3n25yDbIe1NeN69G4n6mUnJGtSJHygX3+oN0ZbO3DTiATnf7XnYJdGT42JCXurTb1JI0qOBR65shvhPQ=="], - "typedoc": ["typedoc@0.28.20", "", { "dependencies": { "@gerrit0/mini-shiki": "^3.23.0", "lunr": "^2.3.9", "markdown-it": "^14.3.0", "minimatch": "^10.2.5", "yaml": "^2.9.0" }, "peerDependencies": { "typescript": "5.0.x || 5.1.x || 5.2.x || 5.3.x || 5.4.x || 5.5.x || 5.6.x || 5.7.x || 5.8.x || 5.9.x || 6.0.x" }, "bin": { "typedoc": "bin/typedoc" } }, "sha512-uSKqkh8Cr48vllnEy+jdaAgOeR6Y+QCBW7usgUsKj7gJEfR7stw9U/fE49LBnj2tPRKPY0c0EBJSWe9Appmplg=="], + "should-type-adaptors": ["should-type-adaptors@1.1.0", "", { "dependencies": { "should-type": "^1.3.0", "should-util": "^1.0.0" } }, "sha512-JA4hdoLnN+kebEp2Vs8eBe9g7uy0zbRo+RMcU0EsNy+R+k049Ki+N5tT5Jagst2g7EAja+euFuoXFCa8vIklfA=="], - "typedoc-plugin-coverage": ["typedoc-plugin-coverage@4.0.3", "", { "peerDependencies": { "typedoc": "0.28.x" } }, "sha512-baim3wyMkqpX7rBzL/6iZ7wzKJuSr9ffP16RHOsdTUNoHUZeXLIZHSUBtUhXmNHaUNRgfqdmKLBwyggbJjGdeQ=="], + "should-util": ["should-util@1.0.1", "", {}, "sha512-oXF8tfxx5cDk8r2kYqlkUJzZpDBqVY/II2WhvU0n9Y3XYvAYRmeaf1PvvIvTgPnv4KJ+ES5M0PyDq5Jp+Ygy2g=="], - "typedoc-plugin-markdown": ["typedoc-plugin-markdown@4.12.0", "", { "peerDependencies": { "typedoc": "0.28.x" } }, "sha512-eJDEMAfxCmede22c/Jw7d0FA13ggAQv+KkwQYKYCdqI02cin6Rc9QRwbG/7XvvHWinuFejySnZVUWDtvGk3Vbg=="], + "standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="], + + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "swagger2openapi": ["swagger2openapi@7.0.8", "", { "dependencies": { "call-me-maybe": "^1.0.1", "node-fetch": "^2.6.1", "node-fetch-h2": "^2.3.0", "node-readfiles": "^0.2.0", "oas-kit-common": "^1.0.8", "oas-resolver": "^2.5.6", "oas-schema-walker": "^1.1.5", "oas-validator": "^5.0.8", "reftools": "^1.1.9", "yaml": "^1.10.0", "yargs": "^17.0.1" }, "bin": { "swagger2openapi": "swagger2openapi.js", "oas-validate": "oas-validate.js", "boast": "boast.js" } }, "sha512-upi/0ZGkYgEcLeGieoz8gT74oWHA0E7JivX7aN9mAf+Tc7BQoRBvnIGHoPDw+f9TXTW4s6kGYCZJtauP6OYp7g=="], + + "toml": ["toml@4.3.0", "", {}, "sha512-lVb8X9BsPVuH0M4BKeS91tXAmJvCjQ5UIyAbQFaxkKGyUFK2RPkhwaFSQH8vbpl1d23eu/IBH+dwVMHWaq9A5A=="], + + "tr46": ["tr46@0.0.3", "", {}, "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], - "uc.micro": ["uc.micro@2.1.0", "", {}, "sha512-ARDJmphmdvUk6Glw7y9DQ2bFkKBHwQHLi2lsaH6PPmz/Ka9sFOBsBluozhDltWmnv9u/cF6Rt87znRTPV+yp/A=="], - "undici": ["undici@7.28.0", "", {}, "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA=="], "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="], - "unicorn-magic": ["unicorn-magic@0.3.0", "", {}, "sha512-+QBBXBCvifc56fsbuxZQ6Sic3wqqc3WWaqxs58gvJrcOuN83HGTCwz3oS5phzU9LthRNE9VrJCFCLUgHeeFnfA=="], + "uuid": ["uuid@14.0.1", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew=="], - "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], + "webidl-conversions": ["webidl-conversions@3.0.1", "", {}, "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="], - "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "whatwg-url": ["whatwg-url@5.0.0", "", { "dependencies": { "tr46": "~0.0.3", "webidl-conversions": "^3.0.0" } }, "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="], + + "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "ws": ["ws@8.21.1", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw=="], + + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], "yaml": ["yaml@2.9.0", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA=="], - "yocto-queue": ["yocto-queue@1.2.2", "", {}, "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ=="], + "yargs": ["yargs@17.7.3", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g=="], - "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], - "@scalar/openapi-parser/@scalar/openapi-types": ["@scalar/openapi-types@0.9.2", "", {}, "sha512-J0SZkNPgCrsFN0Rv8sRqZpDOQcRV44TCT82LsQcyWmcglTr7qtXukDmMlIyXs7JDd+DoPLbT2ea65bEHRs2W2g=="], + "@effect/platform-node/undici": ["undici@8.7.0", "", {}, "sha512-N7iQtfyLhIMOFgQubvmLV26svHpO0bqKnAiWotTQCVKCmWrcGbBotPuW1x+xwYZ2VHdSTVUfPQQnlEt1/LouTQ=="], - "@scalar/openapi-upgrader/@scalar/openapi-types": ["@scalar/openapi-types@0.9.2", "", {}, "sha512-J0SZkNPgCrsFN0Rv8sRqZpDOQcRV44TCT82LsQcyWmcglTr7qtXukDmMlIyXs7JDd+DoPLbT2ea65bEHRs2W2g=="], + "oas-linter/yaml": ["yaml@1.10.3", "", {}, "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA=="], - "npm-run-path/path-key": ["path-key@4.0.0", "", {}, "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ=="], + "oas-resolver/yaml": ["yaml@1.10.3", "", {}, "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA=="], + + "oas-validator/yaml": ["yaml@1.10.3", "", {}, "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA=="], + + "swagger2openapi/yaml": ["yaml@1.10.3", "", {}, "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA=="], } } diff --git a/sdk/examples/couch-preset.effect.ts b/sdk/examples/couch-preset.effect.ts index a1ae5738..20964db0 100644 --- a/sdk/examples/couch-preset.effect.ts +++ b/sdk/examples/couch-preset.effect.ts @@ -1,6 +1,11 @@ -// The RFC §7 quickstart, Effect-native: when the living-room TV starts a stream, apply a -// display preset — a composed, typed, interruptible program. -import { Effect, Stream } from "effect"; +// ── Example 4/4 · ADVANCED, Effect-native ─────────────────────────────────────────────────── +// You do NOT need this to use the SDK — examples 1–3 (the plain Promise `connect()` facade) cover +// most automation. Reach for `@punktfunk/host/effect` only when you're composing Effect programs +// and want the event stream, typed errors, and structured interruption as first-class values. +// +// Here: when the living-room TV starts a stream, apply a display preset — as a single composed, +// typed, interruptible program (a `Stream` of events piped into a request, provided a live layer). +import { Cause, Effect, Stream } from "effect"; import { events, PunktfunkHost, PunktfunkHostLive } from "../src/effect.js"; const program = Effect.gen(function* () { @@ -12,7 +17,11 @@ const program = Effect.gen(function* () { Stream.runForEach(() => pf .request("PUT", "/display/settings", { mode: "preset", preset: "couch" }) - .pipe(Effect.catchAll((e) => Effect.logWarning(`preset failed: ${e}`))), + .pipe( + Effect.catchCause((cause) => + Effect.logWarning(`preset failed: ${Cause.pretty(cause)}`), + ), + ), ), ); }); diff --git a/sdk/examples/notify-pairing.ts b/sdk/examples/notify-pairing.ts index 8631909a..a31e795d 100644 --- a/sdk/examples/notify-pairing.ts +++ b/sdk/examples/notify-pairing.ts @@ -1,5 +1,7 @@ -// The flagship hook-with-a-decision pattern (Promise facade): watch pairing requests, notify, -// and approve/deny THROUGH the API — asynchronously, the punktfunk way (hooks never veto). +// ── Example 2/4 · event → decision ────────────────────────────────────────────────────────── +// The flagship pattern: watch pairing requests, notify yourself, and approve/deny THROUGH the +// typed API — asynchronously, the punktfunk way (hooks never veto). Every call is checked and +// autocompleted; no hand-written paths, no casts. import { connect } from "../src/index.js"; const pf = await connect(); @@ -7,9 +9,13 @@ console.log("watching for pairing requests…"); pf.events.on("pairing.pending", async (e) => { console.log(`pairing request: ${e.device.name} (${e.device.fingerprint})`); - // Wire your real notifier here (ntfy, Pushover, Home Assistant, …), then decide: - // const pending = await pf.request("GET", "/native/pending") as { id: number }[]; - // await pf.request("POST", `/native/pending/${pending[0].id}/approve`); + + // Wire your real notifier here (ntfy, Pushover, Home Assistant, …). Then decide by calling + // the API — find the pending device by fingerprint and approve it (send `{}` to keep the + // name it knocked with): + const pending = await pf.api.listPendingDevices(); + const match = pending.find((d) => d.fingerprint === e.device.fingerprint); + if (match) await pf.api.approvePendingDevice(String(match.id), { payload: {} }); }); pf.events.on("pairing.completed", (e) => console.log(`paired: ${e.device.name}`)); pf.events.on("pairing.denied", (e) => console.log(`denied: ${e.device.name}`)); diff --git a/sdk/examples/provider-sync.ts b/sdk/examples/provider-sync.ts index 0b943248..fe67e443 100644 --- a/sdk/examples/provider-sync.ts +++ b/sdk/examples/provider-sync.ts @@ -1,6 +1,10 @@ +// ── Example 3/4 · typed bulk REST ─────────────────────────────────────────────────────────── // The external game-library provider pattern (RFC §8): compute your desired title list and -// declaratively PUT it — the host diffs by your `external_id`, keeps host ids stable across -// syncs, drops orphans, and never touches manual entries. Uninstall = one DELETE. +// declaratively reconcile it — the host diffs by your `external_id`, keeps host ids stable across +// syncs, drops orphans, and never touches manual entries. Uninstall = one call. +// +// Note how the typed `payload` catches shape mistakes at compile time: `launch` is a +// `LaunchSpec` — `{ kind, value }`, not a bare command string. import { connect } from "../src/index.js"; const PROVIDER = "romm"; // your punktfunk-plugin-* name @@ -12,15 +16,23 @@ pf.events.on("library.changed", (e) => { // Fetch your source of truth (a ROM manager, itch.io, a curated list…), then reconcile: const desired = [ - { external_id: "rom-1", title: "Chrono Trigger", launch: { command: "retroarch ..." } }, - { external_id: "rom-2", title: "Super Metroid", launch: { command: "retroarch ..." } }, + { + external_id: "rom-1", + title: "Chrono Trigger", + launch: { kind: "command", value: "retroarch -L snes9x chrono-trigger.sfc" }, + }, + { + external_id: "rom-2", + title: "Super Metroid", + launch: { kind: "command", value: "retroarch -L snes9x super-metroid.sfc" }, + }, ]; -const entries = (await pf.request("PUT", `/library/provider/${PROVIDER}`, desired)) as { - id: string; - title: string; -}[]; -console.log(`synced ${entries.length} titles:`, entries.map((e) => `${e.title} (custom:${e.id})`)); +const entries = await pf.api.reconcileProviderEntries(PROVIDER, { payload: desired }); +console.log( + `synced ${entries.length} titles:`, + entries.map((e) => `${e.title} (custom:${e.id})`), +); // …run on a schedule, or keep watching your source. Clean uninstall: -// await pf.request("DELETE", `/library/provider/${PROVIDER}`); +// await pf.api.deleteProviderEntries(PROVIDER); pf.close(); diff --git a/sdk/examples/tail-events.ts b/sdk/examples/tail-events.ts index d106f38d..9dd009ca 100644 --- a/sdk/examples/tail-events.ts +++ b/sdk/examples/tail-events.ts @@ -1,12 +1,17 @@ -// Tail the host's lifecycle events — the SDK "hello world" (Promise facade). +// ── Example 1/4 · the "hello world" ───────────────────────────────────────────────────────── +// Connect, make one typed API call, and tail the host's lifecycle events. No Effect knowledge +// needed — `connect()` returns a plain Promise client. // // bun examples/tail-events.ts (on the host box: zero config) // PUNKTFUNK_MGMT_URL=… PUNKTFUNK_MGMT_TOKEN=… bun examples/tail-events.ts import { connect } from "../src/index.js"; const pf = await connect(); -const host = (await pf.request("GET", "/host")) as { hostname?: string }; -console.log(`connected to ${host.hostname ?? "host"} — tailing events (^C to stop)`); + +// `pf.api.*` is the typed front door: `host` is a fully-typed HostInfo — no cast, autocomplete +// for every field. +const host = await pf.api.getHostInfo(); +console.log(`connected to ${host.hostname} — tailing events (^C to stop)`); pf.events.on("*", (e) => console.log(`[${e.seq}] ${e.kind}`, JSON.stringify(e))); pf.events.on("unknown", (e) => console.log("[unknown kind]", JSON.stringify(e))); diff --git a/sdk/orval.config.ts b/sdk/orval.config.ts deleted file mode 100644 index ece7faec..00000000 --- a/sdk/orval.config.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { defineConfig } from "orval"; - -// Generates the SDK's Effect Schemas from the host's checked-in OpenAPI document — the same -// single source of truth the web console's react-query client is generated from (RFC §7: the -// chain is Rust structs (utoipa) → OpenAPI → Effect Schemas → inferred TS types). Regenerate -// after any management-API change: `bun run gen` (CI drift-tests the output). -export default defineConfig({ - punktfunk: { - input: { - target: "../api/openapi.json", - }, - output: { - mode: "single", - target: "./src/gen/schemas.ts", - client: "effect", - clean: true, - }, - }, -}); diff --git a/sdk/package.json b/sdk/package.json index 8025af04..0b82c0ee 100644 --- a/sdk/package.json +++ b/sdk/package.json @@ -9,18 +9,19 @@ "./effect": "./src/effect.ts" }, "scripts": { - "gen": "orval --config ./orval.config.ts", + "gen": "openapigen --spec ../api/openapi.json --name Punktfunk --format httpclient > src/gen/punktfunk.ts", "typecheck": "tsc --noEmit", "test": "bun test", "runner": "bun src/runner-cli.ts" }, "dependencies": { - "effect": "^3.19.0" + "effect": "^4.0.0-beta.98" }, "devDependencies": { - "orval": "^8.20.0", - "typescript": "^5.9.3", - "@types/bun": "^1.3.0" + "@effect/openapi-generator": "4.0.0-beta.98", + "@effect/platform-node": "4.0.0-beta.98", + "@types/bun": "^1.3.0", + "typescript": "^5.9.3" }, "optionalDependencies": { "undici": "^7.0.0" diff --git a/sdk/src/api.ts b/sdk/src/api.ts new file mode 100644 index 00000000..14f7ccff --- /dev/null +++ b/sdk/src/api.ts @@ -0,0 +1,98 @@ +// The typed management API for the Promise facade (RFC §7): every REST endpoint of the host as +// an autocompletable method with checked request/response types — named exactly as in the +// OpenAPI document (`listPairedClients`, `stopSession`, `reconcileProviderEntries`, …). This is +// the typed front door; `pf.request(method, path, body)` stays as the untyped escape hatch. +// +// It's a thin, zero-drift veneer over the generated client (`./gen/punktfunk.ts`, from +// `@effect/openapi-generator`): the generated methods return Effects, so each is run to a Promise +// here. The transport is the SAME CA-pinning fetch the rest of the SDK uses (config.ts), fed to +// Effect's `HttpClient` via the overridable `FetchHttpClient.Fetch` reference — so the loopback +// pin is preserved and there is still exactly one place that knows how to reach the host. +import { Effect } from "effect"; +import * as FetchHttpClient from "effect/unstable/http/FetchHttpClient"; +import * as HttpClient from "effect/unstable/http/HttpClient"; +import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest"; +import type { ResolvedConfig } from "./config.js"; +import * as gen from "./gen/punktfunk.js"; + +/** + * Make a trailing argument that merely *accepts* `undefined` genuinely optional. The generated + * methods type their options bag as `{…} | undefined` (a required param that tolerates `undefined`), + * so without this you'd have to write `listPairedClients(undefined)`. Endpoints with a required + * body (`{ payload }`, not `undefined`-able) are left untouched. + */ +type OptionalizeTail = A extends readonly [ + ...infer Init, + infer Last, +] + ? undefined extends Last + ? [...Init, Last?] + : A + : A; + +/** Each generated method returns an Effect; expose it as a Promise of the decoded success value. */ +type Promiseify = T extends ( + ...args: infer A +) => Effect.Effect + ? (...args: OptionalizeTail) => Promise + : never; + +/** The event stream (an Effect `Stream`) and the void catch-all — served by `pf.events` instead. */ +type NonApiMethods = "httpClient" | "streamEvents" | "streamEventsSse"; + +/** + * The host's management API, fully typed: `await pf.api.listPairedClients()` gives a typed array, + * `await pf.api.reconcileProviderEntries("romm", { payload })` checks the body at compile time — + * no hand-written paths, no `unknown` casts. A failing call rejects with the endpoint's typed + * error. The live-event SSE endpoint is intentionally absent — subscribe via `pf.events`. + */ +export type HostApi = { + readonly [K in Exclude]: Promiseify< + gen.Punktfunk[K] + >; +}; + +/** Build the pinning `HttpClient`: base URL + bearer + the config's CA-pinning fetch. */ +const httpClientFor = (cfg: ResolvedConfig): Promise => + Effect.runPromise( + HttpClient.HttpClient.pipe( + Effect.map((client) => + HttpClient.mapRequest((request: HttpClientRequest.HttpClientRequest) => + request.pipe( + HttpClientRequest.prependUrl(cfg.url), + HttpClientRequest.bearerToken(cfg.token), + HttpClientRequest.acceptJson, + ), + )(client), + ), + Effect.provide(FetchHttpClient.layer), + // Override the default `globalThis.fetch` with the CA-pinning one (config.ts). + Effect.provideService(FetchHttpClient.Fetch, cfg.fetch), + ), + ); + +/** Skipped at runtime too (not just in the type): these aren't Promise-shaped API calls. */ +const NON_API: ReadonlySet = new Set([ + "httpClient", + "streamEvents", + "streamEventsSse", +]); + +/** The typed API surface over a resolved connection — each method runs its Effect to a Promise. */ +export const makeHostApi = async (cfg: ResolvedConfig): Promise => { + const client = gen.make(await httpClientFor(cfg)) as unknown as Record< + string, + unknown + >; + const api: Record = {}; + for (const key of Object.keys(client)) { + if (NON_API.has(key)) continue; + const method = client[key]; + if (typeof method !== "function") continue; + api[key] = (...args: unknown[]) => + Effect.runPromise( + (method as (...a: unknown[]) => Effect.Effect)(...args), + ); + } + return api as HostApi; +}; diff --git a/sdk/src/client.ts b/sdk/src/client.ts index 0f4aa732..9cb8bf20 100644 --- a/sdk/src/client.ts +++ b/sdk/src/client.ts @@ -8,7 +8,7 @@ import { Data, Effect, Layer, - Option, + Result, Schema as S, Stream, } from "effect"; @@ -62,7 +62,7 @@ export interface PunktfunkHostService { /** GET + schema-validate (the generated schemas from `@punktfunk/host/effect`'s `api`). */ readonly get: ( path: string, - schema: S.Schema, + schema: S.Codec, ) => Effect.Effect; /** * The lifecycle-event stream: decoded [`HostEvent`]s with automatic reconnect + @@ -78,10 +78,10 @@ export interface PunktfunkHostService { ) => Stream.Stream; } -export class PunktfunkHost extends Context.Tag("@punktfunk/host/PunktfunkHost")< +export class PunktfunkHost extends Context.Service< PunktfunkHost, PunktfunkHostService ->() {} +>()("@punktfunk/host/PunktfunkHost") {} const toRequestError = (path: string, cause: unknown): RequestError => { if (cause instanceof HttpStatusError) { @@ -98,10 +98,10 @@ export const makeService = (cfg: ResolvedConfig): PunktfunkHostService => { try: () => httpRequest(cfg, method, path, body), catch: (cause) => toRequestError(path, cause), }); - const get = (path: string, schema: S.Schema) => + const get = (path: string, schema: S.Codec) => request("GET", path).pipe( Effect.flatMap((body) => - S.decodeUnknown(schema)(body).pipe( + S.decodeUnknownEffect(schema)(body).pipe( Effect.mapError( (e) => new VersionSkew({ path, issue: String(e) }), ), @@ -119,29 +119,32 @@ export const makeService = (cfg: ResolvedConfig): PunktfunkHostService => { const events = (opts?: EventStreamOptions) => { const warn = opts?.onWarning ?? ((m: string) => console.warn(`[punktfunk] ${m}`)); + // Drop-or-emit per frame. v4's `Stream.filterMap` wants a `Filter`; flat-mapping to + // `Stream.empty` (drop) / `Stream.succeed` (emit) expresses the same in stable + // primitives and keeps the `warn` side effects exactly where they were. return eventsRaw(opts).pipe( - Stream.filterMap((frame) => { + Stream.flatMap((frame) => { if (frame.event === "dropped") { warn( "event cursor fell off the host's ring — resync via the REST snapshots", ); - return Option.none(); + return Stream.empty; } let json: unknown; try { json = JSON.parse(frame.data); } catch { warn(`unparseable event frame (${frame.event})`); - return Option.none(); + return Stream.empty; } const decoded = decodeHostEvent(json); - if (decoded._tag === "Left") { + if (Result.isFailure(decoded)) { // An unknown kind from a NEWER host is expected (additive-only wire) — // it rides the raw channel; a consumer that wants it uses eventsRaw. warn(`unknown/undecodable event kind "${frame.event}"`); - return Option.none(); + return Stream.empty; } - return Option.some(decoded.right); + return Stream.succeed(decoded.success); }), ); }; diff --git a/sdk/src/effect.ts b/sdk/src/effect.ts index 82b5a76a..04798d8e 100644 --- a/sdk/src/effect.ts +++ b/sdk/src/effect.ts @@ -39,8 +39,11 @@ export { export { type ConnectOptions, configDir, resolveConfig } from "./config.js"; export type { EventStreamOptions, SseFrame } from "./sse.js"; export * from "./wire.js"; -/** The generated REST wire schemas (orval `client: 'effect'` over `api/openapi.json`). */ -export * as api from "./gen/schemas.js"; +/** + * The generated REST surface: wire Schemas plus a typed `HttpClient`-based client (`make`), + * from `@effect/openapi-generator` over `api/openapi.json`. Regenerate with `bun run gen`. + */ +export * as api from "./gen/punktfunk.js"; /** The decoded lifecycle-event stream of the ambient [`PunktfunkHost`]. */ export const events = ( @@ -65,6 +68,6 @@ export const request = ( /** GET + schema-validate on the ambient service (schemas from [`api`]). */ export const get = ( path: string, - schema: S.Schema, + schema: S.Codec, ): Effect.Effect => Effect.flatMap(PunktfunkHost, (s) => s.get(path, schema)); diff --git a/sdk/src/gen/punktfunk.ts b/sdk/src/gen/punktfunk.ts new file mode 100644 index 00000000..92b0489f --- /dev/null +++ b/sdk/src/gen/punktfunk.ts @@ -0,0 +1,1233 @@ +import * as Data from "effect/Data" +import * as Effect from "effect/Effect" +import type { SchemaError } from "effect/Schema" +import * as Schema from "effect/Schema" +import * as Stream from "effect/Stream" +import * as Sse from "effect/unstable/encoding/Sse" +import * as HttpClient from "effect/unstable/http/HttpClient" +import * as HttpClientError from "effect/unstable/http/HttpClientError" +import * as HttpClientRequest from "effect/unstable/http/HttpClientRequest" +import * as HttpClientResponse from "effect/unstable/http/HttpClientResponse" +// non-recursive definitions +export type ApiCodec = "h264" | "hevc" | "av1" | "pyrowave" +export const ApiCodec = Schema.Literals(["h264", "hevc", "av1", "pyrowave"]).annotate({ "description": "Video codec identifier. The wire token matches the codec's canonical name used across the\nstack (SDP/GameStream advertisement, the stats-capture `CaptureMeta.codec`, and the encoder's\n[`Codec::label`]) — notably `H.265` serializes as `\"hevc\"`, not `\"h265\"`, so the same codec\nreads identically on every console page." }) +export type ApiDisplayInfo = { readonly "backend": string, readonly "client"?: string | null, readonly "display_index": number, readonly "expires_in_ms"?: never, readonly "group": number, readonly "identity_slot"?: never, readonly "mode": string, readonly "sessions": number, readonly "slot": number, readonly "state": string, readonly "topology": string, readonly "x": number, readonly "y": number } +export const ApiDisplayInfo = Schema.Struct({ "backend": Schema.String.annotate({ "description": "Backend name (`pf-vdisplay`, `kwin`, …)." }), "client": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "Short client label, when the owner tracks it." })), "display_index": Schema.Number.annotate({ "description": "This display's ordinal within its group, in acquire order (0-based).", "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "expires_in_ms": Schema.optionalKey(Schema.Never), "group": Schema.Number.annotate({ "description": "Display group (shared desktop) id — several displays with the same group form one desktop (§6A).", "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "identity_slot": Schema.optionalKey(Schema.Never), "mode": Schema.String.annotate({ "description": "`WIDTHxHEIGHT@HZ`." }), "sessions": Schema.Number.annotate({ "description": "Live sessions holding the display.", "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "slot": Schema.Number.annotate({ "description": "Stable-enough id for the `/display/release` `slot` argument.", "format": "int64" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "state": Schema.String.annotate({ "description": "`active` | `lingering` | `pinned`." }), "topology": Schema.String.annotate({ "description": "Effective topology for this display's group (`extend` | `primary` | `exclusive`)." }), "x": Schema.Number.annotate({ "description": "Desktop-space top-left `x` (auto-row or the console's manual arrangement, §6.2).", "format": "int32" }).check(Schema.isInt()), "y": Schema.Number.annotate({ "description": "Desktop-space top-left `y`.", "format": "int32" }).check(Schema.isInt()) }).annotate({ "description": "One live or kept virtual display." }) +export type ApiError = { readonly "error": string } +export const ApiError = Schema.Struct({ "error": Schema.String }).annotate({ "description": "Error envelope for every non-2xx response." }) +export type ApiGpu = { readonly "id": string, readonly "name": string, readonly "vendor": string, readonly "vram_mb": number } +export const ApiGpu = Schema.Struct({ "id": Schema.String.annotate({ "description": "Stable identifier (`vendorid-deviceid-occurrence`, hex PCI ids) — pass to `setGpuPreference`.\nStable across reboots and driver updates, unlike an adapter index or LUID." }), "name": Schema.String.annotate({ "description": "Adapter/marketing name." }), "vendor": Schema.String.annotate({ "description": "`nvidia` | `amd` | `intel` | `other`." }), "vram_mb": Schema.Number.annotate({ "description": "Dedicated VRAM in MiB (0 where the platform doesn't expose it).", "format": "int64" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)) }).annotate({ "description": "One hardware GPU on the host (software/WARP adapters are never listed)." }) +export type ApprovePending = { readonly "name"?: string | null } +export const ApprovePending = Schema.Struct({ "name": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "Operator-chosen label for the device (defaults to the name it knocked with)." })) }).annotate({ "description": "Approve-pending-device request body. Send `{}` to keep the device's own name." }) +export type ArmNativePairing = { readonly "fingerprint"?: string | null, readonly "ttl_secs"?: never } +export const ArmNativePairing = Schema.Struct({ "fingerprint": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "Optional: bind the window to ONE device fingerprint (hex SHA-256, e.g. from a pending knock).\nWhen set, only a pairing attempt from that fingerprint consumes the window — so an unpaired\nLAN peer can neither pair nor burn a window armed for a specific device (security-review #9).\nOmit for an unbound window (any device may use the PIN — trusted-LAN only)." })), "ttl_secs": Schema.optionalKey(Schema.Never) }).annotate({ "description": "Arm-native-pairing request body." }) +export type Artwork = { readonly "header"?: string | null, readonly "hero"?: string | null, readonly "logo"?: string | null, readonly "portrait"?: string | null } +export const Artwork = Schema.Struct({ "header": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "Horizontal header (Steam `header.jpg`) — the universal fallback." })), "hero": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "Wide background (Steam `library_hero.jpg`)." })), "logo": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "Transparent title logo (Steam `logo.png`)." })), "portrait": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "Vertical capsule / poster (Steam `library_600x900.jpg`). Best for a grid." })) }).annotate({ "description": "Cover art for a title. All fields are URLs (the Steam CDN for Steam titles, user-supplied for\ncustom). The client prefers `portrait` for a grid and falls back to `header` when a title has\nno 600×900 capsule (common for older Steam apps)." }) +export type AvailableCompositor = { readonly "available": boolean, readonly "default": boolean, readonly "id": string, readonly "label": string } +export const AvailableCompositor = Schema.Struct({ "available": Schema.Boolean.annotate({ "description": "Usable on this host right now: the live session's own compositor, or gamescope wherever\nits binary is installed." }), "default": Schema.Boolean.annotate({ "description": "True for the backend an `Auto` (unspecified) request resolves to right now." }), "id": Schema.String.annotate({ "description": "Stable identifier (`\"kwin\"` | `\"wlroots\"` | `\"mutter\"` | `\"gamescope\"`) — pass this to a\nclient's `--compositor` flag." }), "label": Schema.String.annotate({ "description": "Human-readable label for UIs." }) }).annotate({ "description": "A compositor backend the host can drive a virtual output on, and whether it's usable now." }) +export type CaptureMeta = { readonly "client": string, readonly "codec": string, readonly "duration_ms": number, readonly "fps": number, readonly "height": number, readonly "id": string, readonly "kind": string, readonly "sample_count": number, readonly "started_unix_ms": number, readonly "width": number } +export const CaptureMeta = Schema.Struct({ "client": Schema.String.annotate({ "description": "Short label / fingerprint prefix, or `\"\"` if unknown." }), "codec": Schema.String.annotate({ "description": "`\"h264\" | \"hevc\" | \"av1\"`." }), "duration_ms": Schema.Number.annotate({ "format": "int64" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "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)), "id": Schema.String.annotate({ "description": "e.g. `\"2026-06-26T20-14-03Z_5120x1440\"` — also the filename stem." }), "kind": Schema.String.annotate({ "description": "`\"native\" | \"gamestream\"`." }), "sample_count": Schema.Number.annotate({ "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "started_unix_ms": Schema.Number.annotate({ "format": "int64" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "width": Schema.Number.annotate({ "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)) }).annotate({ "description": "Capture summary — the filename stem plus the negotiated mode/codec/client. Stored at the head\nof each on-disk recording and listed standalone (without the sample body) by\n[`StatsRecorder::list`]." }) +export type DisconnectReason = "quit" | "timeout" | "error" +export const DisconnectReason = Schema.Literals(["quit", "timeout", "error"]).annotate({ "description": "Why a client went away. `Quit` is a deliberate user \"stop\" (the typed close code);\n`Timeout` is a transport idle timeout (the client vanished); `Error` is everything else." }) +export type GameSession = "auto" | "dedicated" +export const GameSession = Schema.Literals(["auto", "dedicated"]).annotate({ "description": "How a session that **launches a game** (a library id on the Hello / apps.json / Decky pin) is\nserved (`design/gamemode-and-dedicated-sessions.md` §5.2). Orthogonal to the preset/lifecycle axes\n— a top-level [`DisplayPolicy`] field, NOT part of [`EffectivePolicy`], so a preset never clobbers\nit. Linux-only in effect (a launching Windows session opens into the one desktop)." }) +export type Health = { readonly "abi_version": number, readonly "status": string, readonly "version": string } +export const Health = Schema.Struct({ "abi_version": Schema.Number.annotate({ "description": "`punktfunk-core` C ABI version.", "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "status": Schema.String.annotate({ "description": "Always `\"ok\"` when the host responds." }), "version": Schema.String.annotate({ "description": "`punktfunk-host` crate version." }) }).annotate({ "description": "Liveness + version probe." }) +export type HookEntry = { readonly "debounce_ms"?: number, readonly "filter"?: null | { readonly "app"?: string | null, readonly "client"?: string | null, readonly "fingerprint"?: string | null, readonly "plane"?: null | "native" | "gamestream" }, readonly "hmac_secret_file"?: string | null, readonly "on": string, readonly "run"?: string | null, readonly "timeout_s"?: number, readonly "webhook"?: string | null } +export const HookEntry = Schema.Struct({ "debounce_ms": Schema.optionalKey(Schema.Number.annotate({ "description": "Minimum interval between firings of this hook, in milliseconds. 0 = fire every time.", "format": "int64" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0))), "filter": Schema.optionalKey(Schema.Union([Schema.Null, Schema.Struct({ "app": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "Launched app id/title (`stream.*` events)." })), "client": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "Client/device name (for `session.*`: the short client label the Dashboard shows)." })), "fingerprint": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "Certificate fingerprint (hex, case-insensitive)." })), "plane": Schema.optionalKey(Schema.Union([Schema.Null, Schema.Literals(["native", "gamestream"]).annotate({ "description": "Protocol plane (`native` / `gamestream`)." })], { mode: "oneOf" })) }).annotate({ "description": "Exact-match constraints on the event's fields; every present field must match." })], { mode: "oneOf" })), "hmac_secret_file": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "File holding the webhook HMAC secret (`X-Punktfunk-Signature: sha256=`). The file\nshould be operator-owned and private; a world-readable secret is warned about." })), "on": Schema.String.annotate({ "description": "Which events fire this hook: an exact kind (`stream.started`) or a `domain.*` prefix\n(`pairing.*`) — the same vocabulary as the SSE `?kinds=` filter." }), "run": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "Shell command to execute (detached, event JSON on stdin + `PF_EVENT_*` env)." })), "timeout_s": Schema.optionalKey(Schema.Number.annotate({ "description": "Exec timeout in seconds (1–600, default 30); the process group is killed on expiry.", "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0))), "webhook": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "URL to POST the event JSON to." })) }).annotate({ "description": "One hook: fire `run` and/or `webhook` when an event matching `on` (+ `filter`) occurs." }) +export type Identity = "shared" | "per-client" | "per-client-mode" +export const Identity = Schema.Literals(["shared", "per-client", "per-client-mode"]).annotate({ "description": "Stable display identity, so desktop environments persist per-display config (KDE scaling). Stored\nat Stage 0; carriers wired from the identity stage." }) +export type KeepAlive = { readonly "mode": "off" } | { readonly "mode": "duration", readonly "seconds": number } | { readonly "mode": "forever" } +export const KeepAlive = Schema.Union([Schema.Struct({ "mode": Schema.Literal("off") }).annotate({ "description": "Tear the display down at session end (today's default on every backend but Windows, which\nlingers 10 s)." }), Schema.Struct({ "mode": Schema.Literal("duration"), "seconds": Schema.Number.annotate({ "description": "Linger window in seconds.", "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)) }).annotate({ "description": "Keep the display for `seconds` after the last session leaves, then tear it down; a reconnect\ninside the window reuses it." }), Schema.Struct({ "mode": Schema.Literal("forever") }).annotate({ "description": "Keep the display until host shutdown or an explicit release (the `Pinned` lifecycle state).\n**Not honored until the display-lifecycle stage** — rejected by the mgmt PUT at Stage 0." })], { mode: "oneOf" }).annotate({ "description": "How long a virtual display (and, on gamescope's bare spawn, the nested session + its game)\nsurvives after the last client session detaches. Serialized as an object tagged on `mode`\n(`{\"mode\":\"off\"}` / `{\"mode\":\"duration\",\"seconds\":300}` / `{\"mode\":\"forever\"}`) so the web form\nand the OpenAPI schema stay simple." }) +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/`;\n`command` → run `` nested in a gamescope session." }) +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 type LocalSummary = { readonly "audio_streaming": boolean, readonly "conflicts"?: ReadonlyArray, 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 the audio stream thread is running." }), "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." })), "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 launch session (set by Moonlight's `/launch`, cleared on cancel/stop)." })], { mode: "oneOf" })), "version": Schema.String.annotate({ "description": "Host version (mirrors `/health`)." }), "video_streaming": Schema.Boolean.annotate({ "description": "True while the video stream thread is running." }) }).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 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 type ModeConflict = "separate" | "steal" | "join" | "reject" +export const ModeConflict = Schema.Literals(["separate", "steal", "join", "reject"]).annotate({ "description": "Admission when a *different* client connects while a display/session is already live and asks for\na different mode. Stored at Stage 0; enforced from the mode-conflict admission stage." }) +export type NativeClient = { readonly "fingerprint": string, readonly "name": string } +export const NativeClient = Schema.Struct({ "fingerprint": Schema.String.annotate({ "description": "Hex SHA-256 of the client certificate — its stable id here." }), "name": Schema.String.annotate({ "description": "The name the client supplied when pairing." }) }).annotate({ "description": "A paired native (punktfunk/1) client." }) +export type NativePairStatus = { readonly "armed": boolean, readonly "enabled": boolean, readonly "expires_in_secs"?: never, readonly "paired_clients": number, readonly "pin"?: string | null } +export const NativePairStatus = Schema.Struct({ "armed": Schema.Boolean.annotate({ "description": "True while a pairing window is open." }), "enabled": Schema.Boolean.annotate({ "description": "Whether the native host is running (the unified host started with `--native`)." }), "expires_in_secs": Schema.optionalKey(Schema.Never), "paired_clients": Schema.Number.annotate({ "description": "Number of paired native clients.", "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "pin": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "The PIN to display while armed (null when disarmed)." })) }).annotate({ "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." }) +export type PairedClient = { readonly "fingerprint": string, readonly "not_after_unix"?: never, readonly "not_before_unix"?: never, readonly "subject"?: string | null } +export const PairedClient = Schema.Struct({ "fingerprint": Schema.String.annotate({ "description": "Lowercase hex SHA-256 of the client certificate DER — the client's stable id here." }), "not_after_unix": Schema.optionalKey(Schema.Never), "not_before_unix": Schema.optionalKey(Schema.Never), "subject": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "Certificate subject (e.g. `CN=NVIDIA GameStream Client`), if the DER parses." })) }).annotate({ "description": "A paired (certificate-pinned) Moonlight client." }) +export type PairingStatus = { readonly "pin_pending": boolean } +export const PairingStatus = Schema.Struct({ "pin_pending": Schema.Boolean.annotate({ "description": "True while a pairing handshake is parked waiting for the user's PIN." }) }).annotate({ "description": "Pairing-flow status." }) +export type PendingDevice = { readonly "age_secs": number, readonly "fingerprint": string, readonly "id": number, readonly "name": string } +export const PendingDevice = Schema.Struct({ "age_secs": Schema.Number.annotate({ "description": "Seconds since the device last knocked.", "format": "int64" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "fingerprint": Schema.String.annotate({ "description": "Hex SHA-256 of the device's certificate — what approval pins." }), "id": Schema.Number.annotate({ "description": "Id to address approve/deny (per-process; entries expire after ~10 minutes).", "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "name": Schema.String.annotate({ "description": "Best-effort device label (the client's own name, else fingerprint-derived)." }) }).annotate({ "description": "An unpaired device that tried to connect while the host requires pairing — awaiting\n**delegated approval** (approve it here instead of fetching the host PIN out of band)." }) +export type Plane = "native" | "gamestream" +export const Plane = Schema.Literals(["native", "gamestream"]).annotate({ "description": "Which protocol plane an event originated from. Hooks and scripts filter on it — a hook\nthat fires for native clients but not Moonlight clients is a bug, not a v2 feature." }) +export type PortMap = { readonly "audio": number, readonly "control": number, readonly "http": number, readonly "https": number, readonly "mgmt": number, readonly "rtsp": number, readonly "video": number } +export const PortMap = Schema.Struct({ "audio": Schema.Number.annotate({ "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "control": Schema.Number.annotate({ "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "http": Schema.Number.annotate({ "description": "nvhttp plain HTTP (serverinfo, pairing).", "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "https": Schema.Number.annotate({ "description": "nvhttp mutual-TLS HTTPS (post-pairing).", "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "mgmt": Schema.Number.annotate({ "description": "This management API.", "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "rtsp": Schema.Number.annotate({ "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "video": Schema.Number.annotate({ "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)) }).annotate({ "description": "Every port a client integration may need (Moonlight derives the stream ports from the\nHTTP base; a control pane should not have to)." }) +export type Position = { readonly "x": number, readonly "y": number } +export const Position = Schema.Struct({ "x": Schema.Number.annotate({ "format": "int32" }).check(Schema.isInt()), "y": Schema.Number.annotate({ "format": "int32" }).check(Schema.isInt()) }).annotate({ "description": "A desktop-space offset for a display (top-left origin)." }) +export type PrepCmd = { readonly "do": string, readonly "undo"?: string | null } +export const PrepCmd = Schema.Struct({ "do": Schema.String.annotate({ "description": "Command run before launch. Same execution recipe and ownership checks as hook `run`\ncommands (event-less: stdin is empty JSON, env carries the `PF_APP_*` context)." }), "undo": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "Command run after the session ends. Skipped when its `do` failed (it never took effect)." })) }).annotate({ "description": "One per-app preparation step (RFC §6 — deliberate Sunshine `prep-cmd` parity): `do` runs\n**synchronously before the app launches** (an HDR toggle or a MangoHud env change must land\nfirst), `undo` runs at session end — reverse order across steps, best-effort, on every exit\npath including a crash-unwind (RAII via [`PrepGuard`])." }) +export type Preset = "custom" | "default" | "gaming-rig" | "shared-desktop" | "hotdesk" | "workstation" +export const Preset = Schema.Literals(["custom", "default", "gaming-rig", "shared-desktop", "hotdesk", "workstation"]).annotate({ "description": "A named bundle of the fields below. `Custom` (the default) means the explicit fields rule; any\nother preset ignores the stored fields and expands to its own ([`DisplayPolicy::effective`])." }) +export type ProviderRemoved = { readonly "removed": number } +export const ProviderRemoved = Schema.Struct({ "removed": Schema.Number.annotate({ "description": "How many entries the provider owned (and were removed)." }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)) }).annotate({ "description": "The count envelope a provider uninstall returns." }) +export type ReleaseDisplayRequest = { readonly "slot"?: never } +export const ReleaseDisplayRequest = Schema.Struct({ "slot": Schema.optionalKey(Schema.Never) }).annotate({ "description": "Request body for `releaseDisplay`." }) +export type ReleaseDisplayResult = { readonly "released": number } +export const ReleaseDisplayResult = Schema.Struct({ "released": Schema.Number.annotate({ "description": "Number of kept displays torn down." }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)) }).annotate({ "description": "Result of a `/display/release`." }) +export type SessionRef = { readonly "client": string, readonly "hdr": boolean, readonly "id": number, readonly "mode": string } +export const SessionRef = Schema.Struct({ "client": Schema.String.annotate({ "description": "Short client label (cert-fingerprint prefix, or peer IP for an anonymous client)." }), "hdr": Schema.Boolean, "id": Schema.Number.annotate({ "description": "Host-local session id (unique within this host process).", "format": "int64" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "mode": Schema.String.annotate({ "description": "Negotiated mode, `WxH@Hz` (e.g. `\"3840x2160@120\"`)." }) }).annotate({ "description": "A live A/V session (the plane-neutral notion the Dashboard shows)." }) +export type SetGpuPreference = { readonly "gpu_id"?: string | null, readonly "mode": string } +export const SetGpuPreference = Schema.Struct({ "gpu_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "Required when `mode` is `manual`: the stable `id` of a currently listed GPU\n(see `listGpus`)." })), "mode": Schema.String.annotate({ "description": "`auto` (env pin, else max dedicated VRAM — the default) or `manual`." }) }).annotate({ "description": "Request body for `setGpuPreference`." }) +export type StageTiming = { readonly "name": string, readonly "p50_us": number, readonly "p99_us": number } +export const StageTiming = Schema.Struct({ "name": Schema.String.annotate({ "description": "`\"capture\" | \"submit\" | \"encode\" | \"packetize\" | \"send\"` (path-dependent)." }), "p50_us": Schema.Number.annotate({ "format": "float" }).check(Schema.isFinite()), "p99_us": Schema.Number.annotate({ "format": "float" }).check(Schema.isFinite()) }).annotate({ "description": "One pipeline stage's latency in an aggregation window (microseconds)." }) +export type StatsStatus = { readonly "armed": boolean, readonly "elapsed_ms": number, readonly "kind": string, readonly "sample_count": number, readonly "started_unix_ms": number } +export const StatsStatus = Schema.Struct({ "armed": Schema.Boolean.annotate({ "description": "Capture currently running." }), "elapsed_ms": Schema.Number.annotate({ "description": "Host-measured elapsed time of the in-progress capture, in ms (`0` if idle). Computed from the\nhost's MONOTONIC clock, so a console can show elapsed time without subtracting `started_unix_ms`\nfrom its own (possibly skewed) wall clock.", "format": "int64" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "kind": Schema.String.annotate({ "description": "Path of the in-progress capture (`\"\"` if idle)." }), "sample_count": Schema.Number.annotate({ "description": "Samples in the in-progress capture.", "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "started_unix_ms": Schema.Number.annotate({ "description": "Unix start time of the in-progress capture (`0` if idle).", "format": "int64" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)) }).annotate({ "description": "Snapshot of the in-progress capture for the management API." }) +export type SubmitPin = { readonly "pin": string } +export const SubmitPin = Schema.Struct({ "pin": Schema.String.annotate({ "description": "1–16 ASCII digits (Moonlight shows 4)." }) }).annotate({ "description": "The PIN Moonlight displays during pairing." }) +export type Topology = "auto" | "extend" | "primary" | "exclusive" +export const Topology = Schema.Literals(["auto", "extend", "primary", "exclusive"]).annotate({ "description": "What the host does to the box's display topology while managed virtual displays are up." }) +export type RuntimeStatus = { readonly "active_sessions": number, readonly "audio_streaming": boolean, readonly "paired_clients": number, readonly "pin_pending": boolean, readonly "session"?: null | { readonly "fps": number, readonly "height": number, readonly "width": number }, readonly "stream"?: null | { readonly "bitrate_kbps": number, readonly "codec": ApiCodec, readonly "fps": number, readonly "height": number, readonly "min_fec": number, readonly "packet_size": number, readonly "width": number }, readonly "video_streaming": boolean } +export const RuntimeStatus = Schema.Struct({ "active_sessions": Schema.Number.annotate({ "description": "Number of live streaming sessions across BOTH planes (GameStream + native punktfunk/1). The\nnative server admits concurrent sessions, so this can exceed 1; `session`/`stream` below\ndescribe a single representative session for the detail card.", "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "audio_streaming": Schema.Boolean.annotate({ "description": "True while the audio stream thread is running." }), "paired_clients": Schema.Number.annotate({ "description": "Number of pinned (paired) client certificates.", "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "pin_pending": Schema.Boolean.annotate({ "description": "True while a pairing handshake is parked waiting for the user's PIN\n(submit it via `POST /api/v1/pair/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": "A representative active session. GameStream's launch (Moonlight `/launch`) when present, else\nthe first live native session. `null` when nothing is streaming." })], { mode: "oneOf" })), "stream": Schema.optionalKey(Schema.Union([Schema.Null, Schema.Struct({ "bitrate_kbps": Schema.Number.annotate({ "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "codec": ApiCodec, "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)), "min_fec": Schema.Number.annotate({ "description": "Client's parity floor per FEC block (`minRequiredFecPackets`).", "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "packet_size": Schema.Number.annotate({ "description": "Video payload size per packet (bytes).", "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 stream's parameters — RTSP-negotiated for GameStream, or the live native session's\nmode/codec/bitrate. `null` when nothing is streaming." })], { mode: "oneOf" })), "video_streaming": Schema.Boolean.annotate({ "description": "True while the video stream thread is running." }) }).annotate({ "description": "Live host status (changes as clients launch/end sessions)." }) +export type DisplayStateResponse = { readonly "displays": ReadonlyArray } +export const DisplayStateResponse = Schema.Struct({ "displays": Schema.Array(ApiDisplayInfo) }).annotate({ "description": "The host's managed virtual displays right now." }) +export type GpuState = { readonly "active"?: null | { readonly "backend": string, readonly "id": string, readonly "name": string, readonly "sessions": number, readonly "vendor": string }, readonly "env_override"?: string | null, readonly "gpus": ReadonlyArray, readonly "mode": string, readonly "preferred_available": boolean, readonly "preferred_id"?: string | null, readonly "preferred_name"?: string | null, readonly "selected"?: null | { readonly "id": string, readonly "name": string, readonly "source": string, readonly "vendor": string } } +export const GpuState = Schema.Struct({ "active": Schema.optionalKey(Schema.Union([Schema.Null, Schema.Struct({ "backend": Schema.String.annotate({ "description": "The encode backend in use (`nvenc` | `amf` | `qsv` | `vaapi` | `software`)." }), "id": Schema.String.annotate({ "description": "Stable id matching an entry of `gpus` (empty for the CPU/software encoder)." }), "name": Schema.String, "sessions": Schema.Number.annotate({ "description": "Number of live encode sessions on it.", "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "vendor": Schema.String.annotate({ "description": "`nvidia` | `amd` | `intel` | `other`." }) }).annotate({ "description": "The GPU live sessions use right now (absent while nothing is streaming)." })], { mode: "oneOf" })), "env_override": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "`PUNKTFUNK_RENDER_ADAPTER` (the host.env pin), when set — it applies while `mode` is\n`auto`; a manual preference overrides it." })), "gpus": Schema.Array(ApiGpu).annotate({ "description": "The host's hardware GPUs." }), "mode": Schema.String.annotate({ "description": "`auto` or `manual`." }), "preferred_available": Schema.Boolean.annotate({ "description": "Whether the preferred GPU is currently present." }), "preferred_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "The manually preferred GPU's stable id, when one is stored (kept while `mode` is `auto` so\na console can offer returning to it). May reference a GPU that is currently absent." })), "preferred_name": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "The stored name of the preferred GPU (a usable label even when it is absent)." })), "selected": Schema.optionalKey(Schema.Union([Schema.Null, Schema.Struct({ "id": Schema.String, "name": Schema.String, "source": Schema.String.annotate({ "description": "Why this GPU was selected: `preference` (the manual choice), `env`\n(`PUNKTFUNK_RENDER_ADAPTER`), `auto` (max dedicated VRAM / platform default), or\n`preference_missing` (a manual choice is set but that GPU is absent — auto-selected\ninstead so the host keeps streaming)." }), "vendor": Schema.String.annotate({ "description": "`nvidia` | `amd` | `intel` | `other`." }) }).annotate({ "description": "The GPU the next session will use." })], { mode: "oneOf" })) }).annotate({ "description": "Full GPU-selection state for the console: inventory, the persisted preference, what the next\nsession will use, and what is in use right now." }) +export type GameEntry = { readonly "art": Artwork, readonly "id": string, readonly "launch"?: null | { readonly "kind": string, readonly "value": string }, readonly "provider"?: string | null, readonly "store": string, readonly "title": string } +export const GameEntry = Schema.Struct({ "art": Artwork, "id": Schema.String.annotate({ "description": "Stable, store-qualified id: `steam:` or `custom:`." }), "launch": Schema.optionalKey(Schema.Union([Schema.Null, 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 it, when known." })], { mode: "oneOf" })), "provider": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "The external provider owning this entry (custom-store entries synced by a provider\nplugin, RFC §8) — `None` for installed-store titles and manual custom entries. The\nconsole uses it for attribution; `GET /library?provider=` filters on it." })), "store": Schema.String.annotate({ "description": "Which store surfaced it: `\"steam\"` or `\"custom\"`." }), "title": Schema.String }).annotate({ "description": "One title in the unified library, regardless of which store it came from." }) +export type HooksConfig = { readonly "hooks"?: ReadonlyArray } +export const HooksConfig = Schema.Struct({ "hooks": Schema.optionalKey(Schema.Array(HookEntry)) }).annotate({ "description": "The operator's hook configuration — the `hooks.json` document and the `/api/v1/hooks` body." }) +export type LogPage = { readonly "dropped": boolean, readonly "entries": ReadonlyArray, readonly "next": number } +export const LogPage = Schema.Struct({ "dropped": Schema.Boolean.annotate({ "description": "True when entries between `after` and the first returned one were already evicted." }), "entries": Schema.Array(LogEntry), "next": Schema.Number.annotate({ "description": "Cursor for the next poll (the last returned seq, or the request's `after` when empty).", "format": "int64" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)) }).annotate({ "description": "One poll's worth of log entries." }) +export type ClientRef = { readonly "fingerprint"?: string | null, readonly "name": string, readonly "plane": Plane } +export const ClientRef = Schema.Struct({ "fingerprint": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "Hex SHA-256 certificate fingerprint, when the client presented one." })), "name": Schema.String.annotate({ "description": "Client-supplied device name; may be empty (an anonymous or compat-plane client)." }), "plane": Plane }).annotate({ "description": "The connecting/disconnecting client's identity." }) +export type DeviceRef = { readonly "fingerprint": string, readonly "name": string, readonly "plane": Plane } +export const DeviceRef = Schema.Struct({ "fingerprint": Schema.String.annotate({ "description": "Hex certificate fingerprint." }), "name": Schema.String.annotate({ "description": "Sanitized device name (the pairing store's copy)." }), "plane": Plane }).annotate({ "description": "A device in the pairing flow." }) +export type StreamRef = { readonly "app"?: string | null, readonly "client": string, readonly "hdr": boolean, readonly "mode": string, readonly "plane": Plane } +export const StreamRef = Schema.Struct({ "app": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "The launched app/title for this stream, when one was requested (store-qualified id on\nthe native plane, app title on the GameStream plane)." })), "client": Schema.String.annotate({ "description": "Client-supplied device name; may be empty." }), "hdr": Schema.Boolean, "mode": Schema.String.annotate({ "description": "Negotiated mode, `WxH@Hz`." }), "plane": Plane }).annotate({ "description": "A live video stream (what the stream marker file reflects)." }) +export type HostInfo = { readonly "abi_version": number, readonly "app_version": string, readonly "codecs": ReadonlyArray, readonly "gamestream": boolean, readonly "gfe_version": string, readonly "hostname": string, readonly "local_ip": string, readonly "ports": PortMap, readonly "uniqueid": string, readonly "version": string } +export const HostInfo = Schema.Struct({ "abi_version": Schema.Number.annotate({ "description": "`punktfunk-core` C ABI version.", "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "app_version": Schema.String.annotate({ "description": "GameStream host version advertised to Moonlight clients." }), "codecs": Schema.Array(ApiCodec).annotate({ "description": "Codecs the host can encode (NVENC)." }), "gamestream": Schema.Boolean.annotate({ "description": "Whether the GameStream/Moonlight-compat planes are running (`--gamestream`). `false` on the\nsecure default (native punktfunk/1 only) — a console can hide Moonlight-only UI (e.g. the\nMoonlight PIN pairing card, which could never receive a PIN when this is `false`)." }), "gfe_version": Schema.String.annotate({ "description": "GFE version advertised to Moonlight clients." }), "hostname": Schema.String, "local_ip": Schema.String.annotate({ "description": "Best-effort primary LAN IP." }), "ports": PortMap, "uniqueid": Schema.String.annotate({ "description": "Stable per-host id (persisted across restarts), matched on pairing." }), "version": Schema.String.annotate({ "description": "`punktfunk-host` crate version." }) }).annotate({ "description": "Host identity and advertised capabilities (static for the life of the process)." }) +export type DisplayLayoutRequest = { readonly "positions"?: { readonly [x: string]: Position } } +export const DisplayLayoutRequest = Schema.Struct({ "positions": Schema.optionalKey(Schema.Record(Schema.String, Position).annotate({ "description": "`{\"\": {\"x\": …, \"y\": …}}` — where each arranged display's top-left sits." }).check(Schema.isPropertyNames(Schema.String))) }).annotate({ "description": "Request body for `setDisplayLayout`: per-identity-slot desktop offsets, keyed by the identity-slot\nid as a string (the same id `/display/state` reports as `identity_slot`)." }) +export type Layout = { readonly "mode"?: LayoutMode, readonly "positions"?: { readonly [x: string]: Position } } +export const Layout = Schema.Struct({ "mode": Schema.optionalKey(LayoutMode), "positions": Schema.optionalKey(Schema.Record(Schema.String, Position).check(Schema.isPropertyNames(Schema.String))) }).annotate({ "description": "Group layout: the arrangement mode plus, for [`LayoutMode::Manual`], per-slot offsets keyed by\nidentity-slot id (string keys for stable JSON)." }) +export type CustomEntry = { readonly "art"?: Artwork, readonly "external_id"?: string | null, readonly "id": string, readonly "launch"?: null | LaunchSpec, readonly "prep"?: ReadonlyArray, readonly "provider"?: string | null, readonly "title": string } +export const CustomEntry = Schema.Struct({ "art": Schema.optionalKey(Artwork), "external_id": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "The provider's own stable key for this title — the reconcile diff key, so the\nhost-assigned `id` stays stable across reconciles. Present iff `provider` is." })), "id": Schema.String.annotate({ "description": "Host-assigned, stable for the life of the entry (the `{id}` in the CRUD path)." }), "launch": Schema.optionalKey(Schema.Union([Schema.Null, LaunchSpec], { mode: "oneOf" })), "prep": Schema.optionalKey(Schema.Array(PrepCmd).annotate({ "description": "Per-title prep/undo steps (RFC §6): each `do` runs before this title launches, each\n`undo` at session end in reverse order (see [`crate::hooks::run_prep`])." })), "provider": Schema.optionalKey(Schema.Union([Schema.String, Schema.Null]).annotate({ "description": "The external provider owning this entry (RFC §8), set ONLY by the provider reconcile\nAPI — `None` = a manual entry, which no provider operation ever touches, and which the\nmanual CRUD alone may edit (the converse holds too: manual CRUD refuses provider-owned\nentries, so ownership is never ambiguous)." })), "title": Schema.String }).annotate({ "description": "A user-added title, persisted in `~/.config/punktfunk/library.json`. Same shape the API\nreturns and the web console edits." }) +export type CustomInput = { readonly "art"?: Artwork, readonly "launch"?: null | LaunchSpec, readonly "prep"?: ReadonlyArray, readonly "title": string } +export const CustomInput = Schema.Struct({ "art": Schema.optionalKey(Artwork), "launch": Schema.optionalKey(Schema.Union([Schema.Null, LaunchSpec], { mode: "oneOf" })), "prep": Schema.optionalKey(Schema.Array(PrepCmd).annotate({ "description": "Per-title prep/undo steps — commands run as the host user; operator-privileged config." })), "title": Schema.String }).annotate({ "description": "Request body to create or replace a custom entry (no `id` — the host owns it)." }) +export type ProviderEntryInput = { readonly "art"?: Artwork, readonly "external_id": string, readonly "launch"?: null | LaunchSpec, readonly "prep"?: ReadonlyArray, readonly "title": string } +export const ProviderEntryInput = Schema.Struct({ "art": Schema.optionalKey(Artwork), "external_id": Schema.String.annotate({ "description": "The provider's stable id for this title (the reconcile diff key)." }), "launch": Schema.optionalKey(Schema.Union([Schema.Null, LaunchSpec], { mode: "oneOf" })), "prep": Schema.optionalKey(Schema.Array(PrepCmd).annotate({ "description": "Per-title prep/undo steps — commands run as the host user; operator-privileged config." })), "title": Schema.String }).annotate({ "description": "One title in a provider's declarative reconcile payload (RFC §8): [`CustomInput`] plus the\nprovider's required stable key." }) +export type StatsSample = { readonly "bitrate_kbps": number, readonly "fec_recovered": number, readonly "fps": number, readonly "frames_dropped": number, readonly "mbps": number, readonly "packets_dropped": number, readonly "repeat_fps": number, readonly "send_dropped": number, readonly "session_id": number, readonly "stages": ReadonlyArray, readonly "t_ms": number } +export const StatsSample = Schema.Struct({ "bitrate_kbps": Schema.Number.annotate({ "description": "Configured target bitrate.", "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "fec_recovered": Schema.Number.annotate({ "description": "FEC shards recovered this window (delta).", "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "fps": Schema.Number.annotate({ "description": "Genuine NEW frames/s from the source.", "format": "float" }).check(Schema.isFinite()), "frames_dropped": Schema.Number.annotate({ "description": "Frames dropped this window (delta).", "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "mbps": Schema.Number.annotate({ "description": "Transmit goodput (Mb/s).", "format": "float" }).check(Schema.isFinite()), "packets_dropped": Schema.Number.annotate({ "description": "Packets dropped this window (receiver-side / reassembler, where known).", "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "repeat_fps": Schema.Number.annotate({ "description": "Re-encoded holds/s (source-starvation indicator).", "format": "float" }).check(Schema.isFinite()), "send_dropped": Schema.Number.annotate({ "description": "Host send-buffer overflow / EAGAIN this window (delta).", "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "session_id": Schema.Number.annotate({ "description": "Disambiguates concurrent sessions (usually constant).", "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "stages": Schema.Array(StageTiming).annotate({ "description": "Ordered pipeline stages for this path." }), "t_ms": Schema.Number.annotate({ "description": "Milliseconds since capture start (monotonic; stamped by [`StatsRecorder::push_sample`]).", "format": "int64" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)) }).annotate({ "description": "One aggregated sample (~ every 2 s native, ~ every 1 s GameStream)." }) +export type HostEvent = { readonly "client": ClientRef, readonly "kind": "client.connected", readonly "schema": number, readonly "seq": number, readonly "ts_ms": number } | { readonly "client": ClientRef, readonly "kind": "client.disconnected", readonly "reason": DisconnectReason, readonly "schema": number, readonly "seq": number, readonly "ts_ms": number } | { readonly "kind": "session.started", readonly "session": SessionRef, readonly "schema": number, readonly "seq": number, readonly "ts_ms": number } | { readonly "kind": "session.ended", readonly "session": SessionRef, readonly "schema": number, readonly "seq": number, readonly "ts_ms": number } | { readonly "kind": "stream.started", readonly "stream": StreamRef, readonly "schema": number, readonly "seq": number, readonly "ts_ms": number } | { readonly "kind": "stream.stopped", readonly "stream": StreamRef, readonly "schema": number, readonly "seq": number, readonly "ts_ms": number } | { readonly "device": DeviceRef, readonly "kind": "pairing.pending", readonly "schema": number, readonly "seq": number, readonly "ts_ms": number } | { readonly "device": DeviceRef, readonly "kind": "pairing.completed", readonly "schema": number, readonly "seq": number, readonly "ts_ms": number } | { readonly "device": DeviceRef, readonly "kind": "pairing.denied", readonly "schema": number, readonly "seq": number, readonly "ts_ms": number } | { readonly "backend": string, readonly "kind": "display.created", readonly "mode": string, readonly "schema": number, readonly "seq": number, readonly "ts_ms": number } | { readonly "count": number, readonly "kind": "display.released", readonly "schema": number, readonly "seq": number, readonly "ts_ms": number } | { readonly "kind": "library.changed", readonly "source": string, readonly "schema": number, readonly "seq": number, readonly "ts_ms": number } | { readonly "gamestream": boolean, readonly "kind": "host.started", readonly "version": string, readonly "schema": number, readonly "seq": number, readonly "ts_ms": number } | { readonly "kind": "host.stopping", readonly "schema": number, readonly "seq": number, readonly "ts_ms": number } +export const HostEvent = Schema.Union([Schema.Struct({ "client": ClientRef, "kind": Schema.Literal("client.connected"), "schema": Schema.Number.annotate({ "description": "Wire-shape version ([`SCHEMA_VERSION`]).", "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "seq": Schema.Number.annotate({ "description": "Monotonic sequence number (1-based) — a consumer resumes with `since = last seen`.", "format": "int64" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "ts_ms": Schema.Number.annotate({ "description": "Unix timestamp in milliseconds (the [`crate::log_capture::LogEntry`] convention).", "format": "int64" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)) }), Schema.Struct({ "client": ClientRef, "kind": Schema.Literal("client.disconnected"), "reason": DisconnectReason, "schema": Schema.Number.annotate({ "description": "Wire-shape version ([`SCHEMA_VERSION`]).", "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "seq": Schema.Number.annotate({ "description": "Monotonic sequence number (1-based) — a consumer resumes with `since = last seen`.", "format": "int64" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "ts_ms": Schema.Number.annotate({ "description": "Unix timestamp in milliseconds (the [`crate::log_capture::LogEntry`] convention).", "format": "int64" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)) }), Schema.Struct({ "kind": Schema.Literal("session.started"), "session": SessionRef, "schema": Schema.Number.annotate({ "description": "Wire-shape version ([`SCHEMA_VERSION`]).", "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "seq": Schema.Number.annotate({ "description": "Monotonic sequence number (1-based) — a consumer resumes with `since = last seen`.", "format": "int64" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "ts_ms": Schema.Number.annotate({ "description": "Unix timestamp in milliseconds (the [`crate::log_capture::LogEntry`] convention).", "format": "int64" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)) }), Schema.Struct({ "kind": Schema.Literal("session.ended"), "session": SessionRef, "schema": Schema.Number.annotate({ "description": "Wire-shape version ([`SCHEMA_VERSION`]).", "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "seq": Schema.Number.annotate({ "description": "Monotonic sequence number (1-based) — a consumer resumes with `since = last seen`.", "format": "int64" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "ts_ms": Schema.Number.annotate({ "description": "Unix timestamp in milliseconds (the [`crate::log_capture::LogEntry`] convention).", "format": "int64" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)) }), Schema.Struct({ "kind": Schema.Literal("stream.started"), "stream": StreamRef, "schema": Schema.Number.annotate({ "description": "Wire-shape version ([`SCHEMA_VERSION`]).", "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "seq": Schema.Number.annotate({ "description": "Monotonic sequence number (1-based) — a consumer resumes with `since = last seen`.", "format": "int64" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "ts_ms": Schema.Number.annotate({ "description": "Unix timestamp in milliseconds (the [`crate::log_capture::LogEntry`] convention).", "format": "int64" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)) }), Schema.Struct({ "kind": Schema.Literal("stream.stopped"), "stream": StreamRef, "schema": Schema.Number.annotate({ "description": "Wire-shape version ([`SCHEMA_VERSION`]).", "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "seq": Schema.Number.annotate({ "description": "Monotonic sequence number (1-based) — a consumer resumes with `since = last seen`.", "format": "int64" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "ts_ms": Schema.Number.annotate({ "description": "Unix timestamp in milliseconds (the [`crate::log_capture::LogEntry`] convention).", "format": "int64" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)) }), Schema.Struct({ "device": DeviceRef, "kind": Schema.Literal("pairing.pending"), "schema": Schema.Number.annotate({ "description": "Wire-shape version ([`SCHEMA_VERSION`]).", "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "seq": Schema.Number.annotate({ "description": "Monotonic sequence number (1-based) — a consumer resumes with `since = last seen`.", "format": "int64" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "ts_ms": Schema.Number.annotate({ "description": "Unix timestamp in milliseconds (the [`crate::log_capture::LogEntry`] convention).", "format": "int64" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)) }), Schema.Struct({ "device": DeviceRef, "kind": Schema.Literal("pairing.completed"), "schema": Schema.Number.annotate({ "description": "Wire-shape version ([`SCHEMA_VERSION`]).", "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "seq": Schema.Number.annotate({ "description": "Monotonic sequence number (1-based) — a consumer resumes with `since = last seen`.", "format": "int64" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "ts_ms": Schema.Number.annotate({ "description": "Unix timestamp in milliseconds (the [`crate::log_capture::LogEntry`] convention).", "format": "int64" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)) }), Schema.Struct({ "device": DeviceRef, "kind": Schema.Literal("pairing.denied"), "schema": Schema.Number.annotate({ "description": "Wire-shape version ([`SCHEMA_VERSION`]).", "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "seq": Schema.Number.annotate({ "description": "Monotonic sequence number (1-based) — a consumer resumes with `since = last seen`.", "format": "int64" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "ts_ms": Schema.Number.annotate({ "description": "Unix timestamp in milliseconds (the [`crate::log_capture::LogEntry`] convention).", "format": "int64" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)) }), Schema.Struct({ "backend": Schema.String.annotate({ "description": "The virtual-display backend that minted it (`VirtualDisplay::name`)." }), "kind": Schema.Literal("display.created"), "mode": Schema.String.annotate({ "description": "`WxH@Hz`." }), "schema": Schema.Number.annotate({ "description": "Wire-shape version ([`SCHEMA_VERSION`]).", "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "seq": Schema.Number.annotate({ "description": "Monotonic sequence number (1-based) — a consumer resumes with `since = last seen`.", "format": "int64" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "ts_ms": Schema.Number.annotate({ "description": "Unix timestamp in milliseconds (the [`crate::log_capture::LogEntry`] convention).", "format": "int64" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)) }), Schema.Struct({ "count": Schema.Number.annotate({ "description": "How many kept displays this release retired.", "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "kind": Schema.Literal("display.released"), "schema": Schema.Number.annotate({ "description": "Wire-shape version ([`SCHEMA_VERSION`]).", "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "seq": Schema.Number.annotate({ "description": "Monotonic sequence number (1-based) — a consumer resumes with `since = last seen`.", "format": "int64" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "ts_ms": Schema.Number.annotate({ "description": "Unix timestamp in milliseconds (the [`crate::log_capture::LogEntry`] convention).", "format": "int64" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)) }), Schema.Struct({ "kind": Schema.Literal("library.changed"), "source": Schema.String.annotate({ "description": "What mutated the library: `\"manual\"` today; a provider id once the provider\nAPI (RFC §8) lands." }), "schema": Schema.Number.annotate({ "description": "Wire-shape version ([`SCHEMA_VERSION`]).", "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "seq": Schema.Number.annotate({ "description": "Monotonic sequence number (1-based) — a consumer resumes with `since = last seen`.", "format": "int64" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "ts_ms": Schema.Number.annotate({ "description": "Unix timestamp in milliseconds (the [`crate::log_capture::LogEntry`] convention).", "format": "int64" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)) }), Schema.Struct({ "gamestream": Schema.Boolean.annotate({ "description": "Whether the GameStream/Moonlight compat plane is enabled." }), "kind": Schema.Literal("host.started"), "version": Schema.String, "schema": Schema.Number.annotate({ "description": "Wire-shape version ([`SCHEMA_VERSION`]).", "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "seq": Schema.Number.annotate({ "description": "Monotonic sequence number (1-based) — a consumer resumes with `since = last seen`.", "format": "int64" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "ts_ms": Schema.Number.annotate({ "description": "Unix timestamp in milliseconds (the [`crate::log_capture::LogEntry`] convention).", "format": "int64" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)) }), Schema.Struct({ "kind": Schema.Literal("host.stopping"), "schema": Schema.Number.annotate({ "description": "Wire-shape version ([`SCHEMA_VERSION`]).", "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "seq": Schema.Number.annotate({ "description": "Monotonic sequence number (1-based) — a consumer resumes with `since = last seen`.", "format": "int64" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "ts_ms": Schema.Number.annotate({ "description": "Unix timestamp in milliseconds (the [`crate::log_capture::LogEntry`] convention).", "format": "int64" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)) })], { mode: "oneOf" }).annotate({ "description": "The event kind + payload, flattened: `\"kind\": \"stream.started\", …payload…`." }) +export type CustomPreset = { readonly "fields": { readonly "identity": Identity, readonly "keep_alive": KeepAlive, readonly "layout": Layout, readonly "max_displays": number, readonly "mode_conflict": ModeConflict, readonly "topology": Topology }, readonly "game_session"?: "auto" | "dedicated", readonly "id": string, readonly "name": string } +export const CustomPreset = Schema.Struct({ "fields": Schema.Struct({ "identity": Identity, "keep_alive": KeepAlive, "layout": Layout, "max_displays": Schema.Number.annotate({ "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "mode_conflict": ModeConflict, "topology": Topology }).annotate({ "description": "The six display-behavior axes this preset applies (the same shape a built-in preset expands to)." }), "game_session": Schema.optionalKey(Schema.Literals(["auto", "dedicated"]).annotate({ "description": "The game-session routing this preset applies (orthogonal to the six axes; see [`GameSession`]).\nA custom preset captures the operator's *full* setup, so — unlike a built-in preset — applying\none does set this axis." })), "id": Schema.String.annotate({ "description": "Host-assigned, stable for the life of the entry (the `{id}` in the CRUD path)." }), "name": Schema.String.annotate({ "description": "User-facing name shown on the preset card; editable." }) }).annotate({ "description": "A user-defined named preset: a saved bundle of the six display-behavior axes (exactly what a\nbuilt-in [`Preset`] expands to) plus the orthogonal game-session axis, that the operator names\nand applies from the console.\n\nUnlike the built-in [`Preset`]s (a closed enum), custom presets are **data** — a catalog stored in\n`/display-presets.json`. Applying one writes a `Custom` [`DisplayPolicy`] carrying these\nfields (the console reuses `PUT /display/settings`), so [`DisplayPolicy::effective`] stays pure and\nthe built-in set is never touched. The catalog is decoupled from the active `display-settings.json`:\nediting or deleting a preset never mutates the running policy (re-apply to adopt a change)." }) +export type DisplayPolicy = { readonly "ddc_power_off"?: boolean, readonly "game_session"?: "auto" | "dedicated", readonly "identity"?: Identity, readonly "keep_alive"?: KeepAlive, readonly "layout"?: Layout, readonly "max_displays"?: number, readonly "mode_conflict"?: ModeConflict, readonly "pnp_disable_monitors"?: boolean, readonly "preset"?: Preset, readonly "topology"?: Topology, readonly "version"?: number } +export const DisplayPolicy = Schema.Struct({ "ddc_power_off": Schema.optionalKey(Schema.Boolean.annotate({ "description": "EXPERIMENTAL (Windows): command physical monitors' panels off over DDC/CI (VCP 0xD6 →\nDPMS off) right before an `Exclusive` isolate deactivates them, and back on at restore.\nTargets the \"connected-but-dark head\" periodic-stutter class (monitor standby\nauto-input-scan / DP link churn while the virtual display is the sole active display) at\nthe monitor-firmware level. Best-effort — monitors without DDC/CI (or with it disabled in\nthe OSD) are skipped. Orthogonal to `preset` (like `game_session`): preserved across\npreset changes; `#[serde(default)]` = off so existing `display-settings.json` files are\nuntouched." })), "game_session": Schema.optionalKey(Schema.Literals(["auto", "dedicated"]).annotate({ "description": "How a game-launching session is served (`design/gamemode-and-dedicated-sessions.md` §5.2).\nOrthogonal to `preset`/lifecycle — preserved across preset changes; `#[serde(default)]` = `Auto`\nso existing `display-settings.json` files are untouched." })), "identity": Schema.optionalKey(Identity), "keep_alive": Schema.optionalKey(KeepAlive), "layout": Schema.optionalKey(Layout), "max_displays": Schema.optionalKey(Schema.Number.annotate({ "description": "Upper bound on simultaneously-live virtual displays (clamped to `1..=16` on write).", "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0))), "mode_conflict": Schema.optionalKey(ModeConflict), "pnp_disable_monitors": Schema.optionalKey(Schema.Boolean.annotate({ "description": "EXPERIMENTAL (Windows): DISABLE physical monitors' PnP device nodes for the stream's\nduration (persistently, so a standby monitor/TV whose hot-plug events re-arrive stays\ndisabled) and re-enable them at teardown. Two selectors: the monitors an `Exclusive`\nisolate deactivated, plus — in ANY topology — external monitors that are connected but not\npart of the desktop (the standby TV that was never active, whose input auto-scan /\ninstant-on HPD cycling re-probes the link every few seconds). Targets the same\n\"connected-but-dark head\" periodic-stutter class as [`Self::ddc_power_off`], but at the\nWindows-reaction level: a disabled devnode's wake events trigger no PnP arrival, no CCD\nre-evaluation, no DWM invalidation. A crash-recovery journal re-enables leftovers on host\nstartup. Orthogonal to `preset` (like `game_session`); `#[serde(default)]` = off." })), "preset": Schema.optionalKey(Preset), "topology": Schema.optionalKey(Topology), "version": Schema.optionalKey(Schema.Number.annotate({ "description": "Schema version (currently 1) — lets a future field addition migrate rather than reject.", "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0))) }).annotate({ "description": "The user-facing display-management policy — what `display-settings.json` holds and what the mgmt\nAPI GETs/PUTs. When [`preset`](Self::preset) is not [`Preset::Custom`] the explicit fields are\nignored (the console writes one or the other); [`effective`](Self::effective) resolves both to a\nsingle [`EffectivePolicy`]." }) +export type EffectivePolicy = { readonly "identity": Identity, readonly "keep_alive": KeepAlive, readonly "layout": Layout, readonly "max_displays": number, readonly "mode_conflict": ModeConflict, readonly "topology": Topology } +export const EffectivePolicy = Schema.Struct({ "identity": Identity, "keep_alive": KeepAlive, "layout": Layout, "max_displays": Schema.Number.annotate({ "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "mode_conflict": ModeConflict, "topology": Topology }).annotate({ "description": "The six resolved fields after preset expansion — what the lifecycle/registry and the Stage-0 call\nsites read, and what the mgmt API echoes as the \"currently in force\" policy. Pure output of\n[`DisplayPolicy::effective`]." }) +export type PresetInfo = { readonly "fields": { readonly "identity": Identity, readonly "keep_alive": KeepAlive, readonly "layout": Layout, readonly "max_displays": number, readonly "mode_conflict": ModeConflict, readonly "topology": Topology }, readonly "id": string, readonly "summary": string } +export const PresetInfo = Schema.Struct({ "fields": Schema.Struct({ "identity": Identity, "keep_alive": KeepAlive, "layout": Layout, "max_displays": Schema.Number.annotate({ "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "mode_conflict": ModeConflict, "topology": Topology }).annotate({ "description": "The effective policy this preset expands to (the same fields a `custom` policy carries)." }), "id": Schema.String.annotate({ "description": "The preset id (`default` | `gaming-rig` | `shared-desktop` | `hotdesk` | `workstation`)." }), "summary": Schema.String.annotate({ "description": "One-line story shown next to the option." }) }).annotate({ "description": "One preset's human-facing description + the fields it expands to, so the console can render a\npreset picker with an accurate \"what this does\" preview without hardcoding the expansion." }) +export type Capture = { readonly "meta": CaptureMeta, readonly "samples": ReadonlyArray } +export const Capture = Schema.Struct({ "meta": CaptureMeta, "samples": Schema.Array(StatsSample) }).annotate({ "description": "A full capture: summary + the sample time-series. The wire + on-disk shape." }) +export type CustomPresetInput = { readonly "fields": EffectivePolicy, readonly "game_session"?: GameSession, readonly "name": string } +export const CustomPresetInput = Schema.Struct({ "fields": EffectivePolicy, "game_session": Schema.optionalKey(GameSession), "name": Schema.String }).annotate({ "description": "Request body to create or replace a custom preset (no `id` — the host owns it)." }) +export type DisplaySettingsState = { readonly "configured": boolean, readonly "custom_presets": ReadonlyArray, readonly "effective": { readonly "identity": Identity, readonly "keep_alive": KeepAlive, readonly "layout": Layout, readonly "max_displays": number, readonly "mode_conflict": ModeConflict, readonly "topology": Topology }, readonly "enforced": ReadonlyArray, readonly "presets": ReadonlyArray, readonly "settings": { readonly "ddc_power_off"?: boolean, readonly "game_session"?: "auto" | "dedicated", readonly "identity"?: Identity, readonly "keep_alive"?: KeepAlive, readonly "layout"?: Layout, readonly "max_displays"?: number, readonly "mode_conflict"?: ModeConflict, readonly "pnp_disable_monitors"?: boolean, readonly "preset"?: Preset, readonly "topology"?: Topology, readonly "version"?: number } } +export const DisplaySettingsState = Schema.Struct({ "configured": Schema.Boolean.annotate({ "description": "True once a `display-settings.json` exists (the console has configured this host)." }), "custom_presets": Schema.Array(CustomPreset).annotate({ "description": "The operator's saved custom presets (`display-presets.json`) — named field-bundles rendered\nalongside the built-ins. Managed via `POST/PUT/DELETE /display/presets`; applied by writing a\n`Custom` policy carrying the preset's fields." }), "effective": Schema.Struct({ "identity": Identity, "keep_alive": KeepAlive, "layout": Layout, "max_displays": Schema.Number.annotate({ "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0)), "mode_conflict": ModeConflict, "topology": Topology }).annotate({ "description": "The effective (preset-expanded) policy currently in force." }), "enforced": Schema.Array(Schema.String).annotate({ "description": "Option names this build enforces right now. All five axes are now acted on (keep_alive +\ntopology since Stage 0-2, identity Stage 3, mode_conflict Stage 4, layout Stage 5) — the console\nreads this to know which controls are live vs. \"coming soon\" (per-backend nuance, e.g. layout\nposition apply being KWin-only, is reported per display in `/display/state`)." }), "presets": Schema.Array(PresetInfo).annotate({ "description": "Every named preset and what it expands to (for the picker's preview)." }), "settings": Schema.Struct({ "ddc_power_off": Schema.optionalKey(Schema.Boolean.annotate({ "description": "EXPERIMENTAL (Windows): command physical monitors' panels off over DDC/CI (VCP 0xD6 →\nDPMS off) right before an `Exclusive` isolate deactivates them, and back on at restore.\nTargets the \"connected-but-dark head\" periodic-stutter class (monitor standby\nauto-input-scan / DP link churn while the virtual display is the sole active display) at\nthe monitor-firmware level. Best-effort — monitors without DDC/CI (or with it disabled in\nthe OSD) are skipped. Orthogonal to `preset` (like `game_session`): preserved across\npreset changes; `#[serde(default)]` = off so existing `display-settings.json` files are\nuntouched." })), "game_session": Schema.optionalKey(Schema.Literals(["auto", "dedicated"]).annotate({ "description": "How a game-launching session is served (`design/gamemode-and-dedicated-sessions.md` §5.2).\nOrthogonal to `preset`/lifecycle — preserved across preset changes; `#[serde(default)]` = `Auto`\nso existing `display-settings.json` files are untouched." })), "identity": Schema.optionalKey(Identity), "keep_alive": Schema.optionalKey(KeepAlive), "layout": Schema.optionalKey(Layout), "max_displays": Schema.optionalKey(Schema.Number.annotate({ "description": "Upper bound on simultaneously-live virtual displays (clamped to `1..=16` on write).", "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0))), "mode_conflict": Schema.optionalKey(ModeConflict), "pnp_disable_monitors": Schema.optionalKey(Schema.Boolean.annotate({ "description": "EXPERIMENTAL (Windows): DISABLE physical monitors' PnP device nodes for the stream's\nduration (persistently, so a standby monitor/TV whose hot-plug events re-arrive stays\ndisabled) and re-enable them at teardown. Two selectors: the monitors an `Exclusive`\nisolate deactivated, plus — in ANY topology — external monitors that are connected but not\npart of the desktop (the standby TV that was never active, whose input auto-scan /\ninstant-on HPD cycling re-probes the link every few seconds). Targets the same\n\"connected-but-dark head\" periodic-stutter class as [`Self::ddc_power_off`], but at the\nWindows-reaction level: a disabled devnode's wake events trigger no PnP arrival, no CCD\nre-evaluation, no DWM invalidation. A crash-recovery journal re-enables leftovers on host\nstartup. Orthogonal to `preset` (like `game_session`); `#[serde(default)]` = off." })), "preset": Schema.optionalKey(Preset), "topology": Schema.optionalKey(Topology), "version": Schema.optionalKey(Schema.Number.annotate({ "description": "Schema version (currently 1) — lets a future field addition migrate rather than reject.", "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0))) }).annotate({ "description": "The stored policy (preset + custom fields), or the built-in default when unconfigured." }) }).annotate({ "description": "Full display-management state for the console: the stored policy, every preset's expansion, the\nresolved effective policy, and which options this build actually enforces yet (Stage 0 wires\nkeep-alive linger + topology; the rest are stored but not yet acted on)." }) +// schemas +export type ListPairedClients200 = ReadonlyArray +export const ListPairedClients200 = Schema.Array(PairedClient) +export type ListPairedClients401 = ApiError +export const ListPairedClients401 = ApiError +export type UnpairClient400 = ApiError +export const UnpairClient400 = ApiError +export type UnpairClient401 = ApiError +export const UnpairClient401 = ApiError +export type UnpairClient404 = ApiError +export const UnpairClient404 = ApiError +export type ListCompositors200 = ReadonlyArray +export const ListCompositors200 = Schema.Array(AvailableCompositor) +export type ListCompositors401 = ApiError +export const ListCompositors401 = ApiError +export type SetDisplayLayoutRequestJson = DisplayLayoutRequest +export const SetDisplayLayoutRequestJson = DisplayLayoutRequest +export type SetDisplayLayout200 = DisplaySettingsState +export const SetDisplayLayout200 = DisplaySettingsState +export type SetDisplayLayout401 = ApiError +export const SetDisplayLayout401 = ApiError +export type SetDisplayLayout500 = ApiError +export const SetDisplayLayout500 = ApiError +export type ListCustomPresets200 = ReadonlyArray +export const ListCustomPresets200 = Schema.Array(CustomPreset) +export type ListCustomPresets401 = ApiError +export const ListCustomPresets401 = ApiError +export type CreateCustomPresetRequestJson = CustomPresetInput +export const CreateCustomPresetRequestJson = CustomPresetInput +export type CreateCustomPreset201 = CustomPreset +export const CreateCustomPreset201 = CustomPreset +export type CreateCustomPreset400 = ApiError +export const CreateCustomPreset400 = ApiError +export type CreateCustomPreset401 = ApiError +export const CreateCustomPreset401 = ApiError +export type CreateCustomPreset500 = ApiError +export const CreateCustomPreset500 = ApiError +export type UpdateCustomPresetRequestJson = CustomPresetInput +export const UpdateCustomPresetRequestJson = CustomPresetInput +export type UpdateCustomPreset200 = CustomPreset +export const UpdateCustomPreset200 = CustomPreset +export type UpdateCustomPreset400 = ApiError +export const UpdateCustomPreset400 = ApiError +export type UpdateCustomPreset401 = ApiError +export const UpdateCustomPreset401 = ApiError +export type UpdateCustomPreset404 = ApiError +export const UpdateCustomPreset404 = ApiError +export type UpdateCustomPreset500 = ApiError +export const UpdateCustomPreset500 = ApiError +export type DeleteCustomPreset401 = ApiError +export const DeleteCustomPreset401 = ApiError +export type DeleteCustomPreset404 = ApiError +export const DeleteCustomPreset404 = ApiError +export type DeleteCustomPreset500 = ApiError +export const DeleteCustomPreset500 = ApiError +export type ReleaseDisplayRequestJson = ReleaseDisplayRequest +export const ReleaseDisplayRequestJson = ReleaseDisplayRequest +export type ReleaseDisplay200 = ReleaseDisplayResult +export const ReleaseDisplay200 = ReleaseDisplayResult +export type ReleaseDisplay401 = ApiError +export const ReleaseDisplay401 = ApiError +export type GetDisplaySettings200 = DisplaySettingsState +export const GetDisplaySettings200 = DisplaySettingsState +export type GetDisplaySettings401 = ApiError +export const GetDisplaySettings401 = ApiError +export type SetDisplaySettingsRequestJson = DisplayPolicy +export const SetDisplaySettingsRequestJson = DisplayPolicy +export type SetDisplaySettings200 = DisplaySettingsState +export const SetDisplaySettings200 = DisplaySettingsState +export type SetDisplaySettings400 = ApiError +export const SetDisplaySettings400 = ApiError +export type SetDisplaySettings401 = ApiError +export const SetDisplaySettings401 = ApiError +export type SetDisplaySettings500 = ApiError +export const SetDisplaySettings500 = ApiError +export type GetDisplayState200 = DisplayStateResponse +export const GetDisplayState200 = DisplayStateResponse +export type GetDisplayState401 = ApiError +export const GetDisplayState401 = ApiError +export type StreamEventsParams = { readonly "since"?: number, readonly "kinds"?: string, readonly "Last-Event-ID"?: never } +export const StreamEventsParams = Schema.Struct({ "since": Schema.optionalKey(Schema.Number.annotate({ "format": "int64" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0))), "kinds": Schema.optionalKey(Schema.String), "Last-Event-ID": Schema.optionalKey(Schema.Never) }) +export type StreamEvents200Sse = HostEvent +export const StreamEvents200Sse = HostEvent +export type StreamEvents401 = ApiError +export const StreamEvents401 = ApiError +export type StreamEvents503 = ApiError +export const StreamEvents503 = ApiError +export type ListGpus200 = GpuState +export const ListGpus200 = GpuState +export type ListGpus401 = ApiError +export const ListGpus401 = ApiError +export type SetGpuPreferenceRequestJson = SetGpuPreference +export const SetGpuPreferenceRequestJson = SetGpuPreference +export type SetGpuPreference200 = GpuState +export const SetGpuPreference200 = GpuState +export type SetGpuPreference400 = ApiError +export const SetGpuPreference400 = ApiError +export type SetGpuPreference401 = ApiError +export const SetGpuPreference401 = ApiError +export type SetGpuPreference500 = ApiError +export const SetGpuPreference500 = ApiError +export type GetHealth200 = Health +export const GetHealth200 = Health +export type GetHooks200 = HooksConfig +export const GetHooks200 = HooksConfig +export type GetHooks401 = ApiError +export const GetHooks401 = ApiError +export type SetHooksRequestJson = HooksConfig +export const SetHooksRequestJson = HooksConfig +export type SetHooks200 = HooksConfig +export const SetHooks200 = HooksConfig +export type SetHooks400 = ApiError +export const SetHooks400 = ApiError +export type SetHooks401 = ApiError +export const SetHooks401 = ApiError +export type SetHooks500 = ApiError +export const SetHooks500 = ApiError +export type GetHostInfo200 = HostInfo +export const GetHostInfo200 = HostInfo +export type GetHostInfo401 = ApiError +export const GetHostInfo401 = ApiError +export type GetLibraryParams = { readonly "provider"?: string } +export const GetLibraryParams = Schema.Struct({ "provider": Schema.optionalKey(Schema.String) }) +export type GetLibrary200 = ReadonlyArray +export const GetLibrary200 = Schema.Array(GameEntry) +export type GetLibrary401 = ApiError +export const GetLibrary401 = ApiError +export type GetLibraryArt401 = ApiError +export const GetLibraryArt401 = ApiError +export type GetLibraryArt404 = ApiError +export const GetLibraryArt404 = ApiError +export type CreateCustomGameRequestJson = CustomInput +export const CreateCustomGameRequestJson = CustomInput +export type CreateCustomGame201 = CustomEntry +export const CreateCustomGame201 = CustomEntry +export type CreateCustomGame400 = ApiError +export const CreateCustomGame400 = ApiError +export type CreateCustomGame401 = ApiError +export const CreateCustomGame401 = ApiError +export type CreateCustomGame500 = ApiError +export const CreateCustomGame500 = ApiError +export type UpdateCustomGameRequestJson = CustomInput +export const UpdateCustomGameRequestJson = CustomInput +export type UpdateCustomGame200 = CustomEntry +export const UpdateCustomGame200 = CustomEntry +export type UpdateCustomGame400 = ApiError +export const UpdateCustomGame400 = ApiError +export type UpdateCustomGame401 = ApiError +export const UpdateCustomGame401 = ApiError +export type UpdateCustomGame404 = ApiError +export const UpdateCustomGame404 = ApiError +export type UpdateCustomGame500 = ApiError +export const UpdateCustomGame500 = ApiError +export type DeleteCustomGame401 = ApiError +export const DeleteCustomGame401 = ApiError +export type DeleteCustomGame404 = ApiError +export const DeleteCustomGame404 = ApiError +export type DeleteCustomGame500 = ApiError +export const DeleteCustomGame500 = ApiError +export type ReconcileProviderEntriesRequestJson = ReadonlyArray +export const ReconcileProviderEntriesRequestJson = Schema.Array(ProviderEntryInput) +export type ReconcileProviderEntries200 = ReadonlyArray +export const ReconcileProviderEntries200 = Schema.Array(CustomEntry) +export type ReconcileProviderEntries400 = ApiError +export const ReconcileProviderEntries400 = ApiError +export type ReconcileProviderEntries401 = ApiError +export const ReconcileProviderEntries401 = ApiError +export type ReconcileProviderEntries500 = ApiError +export const ReconcileProviderEntries500 = ApiError +export type DeleteProviderEntries200 = ProviderRemoved +export const DeleteProviderEntries200 = ProviderRemoved +export type DeleteProviderEntries400 = ApiError +export const DeleteProviderEntries400 = ApiError +export type DeleteProviderEntries401 = ApiError +export const DeleteProviderEntries401 = ApiError +export type DeleteProviderEntries500 = ApiError +export const DeleteProviderEntries500 = ApiError +export type GetLocalSummary200 = LocalSummary +export const GetLocalSummary200 = LocalSummary +export type GetLocalSummary401 = ApiError +export const GetLocalSummary401 = ApiError +export type LogsGetParams = { readonly "after"?: number, readonly "limit"?: number } +export const LogsGetParams = Schema.Struct({ "after": Schema.optionalKey(Schema.Number.annotate({ "format": "int64" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0))), "limit": Schema.optionalKey(Schema.Number.annotate({ "format": "int32" }).check(Schema.isInt()).check(Schema.isGreaterThanOrEqualTo(0))) }) +export type LogsGet200 = LogPage +export const LogsGet200 = LogPage +export type LogsGet401 = ApiError +export const LogsGet401 = ApiError +export type ListNativeClients200 = ReadonlyArray +export const ListNativeClients200 = Schema.Array(NativeClient) +export type ListNativeClients401 = ApiError +export const ListNativeClients401 = ApiError +export type UnpairNativeClient401 = ApiError +export const UnpairNativeClient401 = ApiError +export type UnpairNativeClient404 = ApiError +export const UnpairNativeClient404 = ApiError +export type UnpairNativeClient503 = ApiError +export const UnpairNativeClient503 = ApiError +export type GetNativePairing200 = NativePairStatus +export const GetNativePairing200 = NativePairStatus +export type GetNativePairing401 = ApiError +export const GetNativePairing401 = ApiError +export type DisarmNativePairing401 = ApiError +export const DisarmNativePairing401 = ApiError +export type DisarmNativePairing503 = ApiError +export const DisarmNativePairing503 = ApiError +export type ArmNativePairingRequestJson = ArmNativePairing +export const ArmNativePairingRequestJson = ArmNativePairing +export type ArmNativePairing200 = NativePairStatus +export const ArmNativePairing200 = NativePairStatus +export type ArmNativePairing401 = ApiError +export const ArmNativePairing401 = ApiError +export type ArmNativePairing503 = ApiError +export const ArmNativePairing503 = ApiError +export type ListPendingDevices200 = ReadonlyArray +export const ListPendingDevices200 = Schema.Array(PendingDevice) +export type ListPendingDevices401 = ApiError +export const ListPendingDevices401 = ApiError +export type ApprovePendingDeviceRequestJson = ApprovePending +export const ApprovePendingDeviceRequestJson = ApprovePending +export type ApprovePendingDevice200 = NativeClient +export const ApprovePendingDevice200 = NativeClient +export type ApprovePendingDevice401 = ApiError +export const ApprovePendingDevice401 = ApiError +export type ApprovePendingDevice404 = ApiError +export const ApprovePendingDevice404 = ApiError +export type ApprovePendingDevice500 = ApiError +export const ApprovePendingDevice500 = ApiError +export type ApprovePendingDevice503 = ApiError +export const ApprovePendingDevice503 = ApiError +export type DenyPendingDevice401 = ApiError +export const DenyPendingDevice401 = ApiError +export type DenyPendingDevice404 = ApiError +export const DenyPendingDevice404 = ApiError +export type DenyPendingDevice503 = ApiError +export const DenyPendingDevice503 = ApiError +export type GetPairingStatus200 = PairingStatus +export const GetPairingStatus200 = PairingStatus +export type GetPairingStatus401 = ApiError +export const GetPairingStatus401 = ApiError +export type SubmitPairingPinRequestJson = SubmitPin +export const SubmitPairingPinRequestJson = SubmitPin +export type SubmitPairingPin400 = ApiError +export const SubmitPairingPin400 = ApiError +export type SubmitPairingPin401 = ApiError +export const SubmitPairingPin401 = ApiError +export type SubmitPairingPin409 = ApiError +export const SubmitPairingPin409 = ApiError +export type SubmitPairingPin415 = ApiError +export const SubmitPairingPin415 = ApiError +export type SubmitPairingPin422 = ApiError +export const SubmitPairingPin422 = ApiError +export type StopSession401 = ApiError +export const StopSession401 = ApiError +export type RequestIdr401 = ApiError +export const RequestIdr401 = ApiError +export type RequestIdr409 = ApiError +export const RequestIdr409 = ApiError +export type StatsCaptureLive200 = Capture +export const StatsCaptureLive200 = Capture +export type StatsCaptureLive401 = ApiError +export const StatsCaptureLive401 = ApiError +export type StatsCaptureLive404 = ApiError +export const StatsCaptureLive404 = ApiError +export type StatsCaptureStart200 = StatsStatus +export const StatsCaptureStart200 = StatsStatus +export type StatsCaptureStart401 = ApiError +export const StatsCaptureStart401 = ApiError +export type StatsCaptureStatus200 = StatsStatus +export const StatsCaptureStatus200 = StatsStatus +export type StatsCaptureStatus401 = ApiError +export const StatsCaptureStatus401 = ApiError +export type StatsCaptureStop200 = CaptureMeta +export const StatsCaptureStop200 = CaptureMeta +export type StatsCaptureStop401 = ApiError +export const StatsCaptureStop401 = ApiError +export type StatsCaptureStop500 = ApiError +export const StatsCaptureStop500 = ApiError +export type StatsRecordingsList200 = ReadonlyArray +export const StatsRecordingsList200 = Schema.Array(CaptureMeta) +export type StatsRecordingsList401 = ApiError +export const StatsRecordingsList401 = ApiError +export type StatsRecordingGet200 = Capture +export const StatsRecordingGet200 = Capture +export type StatsRecordingGet401 = ApiError +export const StatsRecordingGet401 = ApiError +export type StatsRecordingGet404 = ApiError +export const StatsRecordingGet404 = ApiError +export type StatsRecordingGet500 = ApiError +export const StatsRecordingGet500 = ApiError +export type StatsRecordingDelete401 = ApiError +export const StatsRecordingDelete401 = ApiError +export type StatsRecordingDelete404 = ApiError +export const StatsRecordingDelete404 = ApiError +export type StatsRecordingDelete500 = ApiError +export const StatsRecordingDelete500 = ApiError +export type GetStatus200 = RuntimeStatus +export const GetStatus200 = RuntimeStatus +export type GetStatus401 = ApiError +export const GetStatus401 = ApiError + +export interface OperationConfig { + /** + * Whether or not the response should be included in the value returned from + * an operation. + * + * If set to `true`, a tuple of `[A, HttpClientResponse]` will be returned, + * where `A` is the success type of the operation. + * + * If set to `false`, only the success type of the operation will be returned. + */ + readonly includeResponse?: boolean | undefined +} + +/** + * A utility type which optionally includes the response in the return result + * of an operation based upon the value of the `includeResponse` configuration + * option. + */ +export type WithOptionalResponse = Config extends { + readonly includeResponse: true +} ? [A, HttpClientResponse.HttpClientResponse] : A + +export const make = ( + httpClient: HttpClient.HttpClient, + options: { + readonly transformClient?: ((client: HttpClient.HttpClient) => Effect.Effect) | undefined + } = {} +): Punktfunk => { + const unexpectedStatus = (response: HttpClientResponse.HttpClientResponse) => + Effect.flatMap( + Effect.orElseSucceed(response.json, () => "Unexpected status code"), + (description) => + Effect.fail( + new HttpClientError.HttpClientError({ + reason: new HttpClientError.StatusCodeError({ + request: response.request, + response, + description: typeof description === "string" ? description : JSON.stringify(description), + }), + }), + ), + ) + const withResponse = (config: Config | undefined) => ( + f: (response: HttpClientResponse.HttpClientResponse) => Effect.Effect, + ): (request: HttpClientRequest.HttpClientRequest) => Effect.Effect => { + const withOptionalResponse = ( + config?.includeResponse + ? (response: HttpClientResponse.HttpClientResponse) => Effect.map(f(response), (a) => [a, response]) + : (response: HttpClientResponse.HttpClientResponse) => f(response) + ) as any + return options?.transformClient + ? (request) => + Effect.flatMap( + Effect.flatMap(options.transformClient!(httpClient), (client) => client.execute(request)), + withOptionalResponse + ) + : (request) => Effect.flatMap(httpClient.execute(request), withOptionalResponse) + } + const sseRequest = < + Type, + DecodingServices + >( + schema: Schema.ConstraintDecoder + ) => + ( + request: HttpClientRequest.HttpClientRequest + ): Stream.Stream< + { readonly event: string; readonly id: string | undefined; readonly data: Type }, + HttpClientError.HttpClientError | SchemaError | Sse.Retry, + DecodingServices + > => + HttpClient.filterStatusOk(httpClient).execute(request).pipe( + Effect.map((response) => response.stream), + Stream.unwrap, + Stream.decodeText(), + Stream.pipeThroughChannel(Sse.decodeDataSchema(schema)) + ) + const decodeSuccess = + (schema: Schema) => + (response: HttpClientResponse.HttpClientResponse) => + HttpClientResponse.schemaBodyJson(schema)(response) + const decodeError = + (tag: Tag, schema: Schema) => + (response: HttpClientResponse.HttpClientResponse) => + Effect.flatMap( + HttpClientResponse.schemaBodyJson(schema)(response), + (cause) => Effect.fail(PunktfunkError(tag, cause, response)), + ) + return { + httpClient, + "listPairedClients": (options) => HttpClientRequest.get(`/api/v1/clients`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListPairedClients200), + "401": decodeError("ListPairedClients401", ListPairedClients401), + orElse: unexpectedStatus + })) + ), + "unpairClient": (fingerprint, options) => HttpClientRequest.delete(`/api/v1/clients/${fingerprint}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "400": decodeError("UnpairClient400", UnpairClient400), + "401": decodeError("UnpairClient401", UnpairClient401), + "404": decodeError("UnpairClient404", UnpairClient404), + "204": () => Effect.void, + orElse: unexpectedStatus + })) + ), + "listCompositors": (options) => HttpClientRequest.get(`/api/v1/compositors`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListCompositors200), + "401": decodeError("ListCompositors401", ListCompositors401), + orElse: unexpectedStatus + })) + ), + "setDisplayLayout": (options) => HttpClientRequest.put(`/api/v1/display/layout`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(SetDisplayLayout200), + "401": decodeError("SetDisplayLayout401", SetDisplayLayout401), + "500": decodeError("SetDisplayLayout500", SetDisplayLayout500), + orElse: unexpectedStatus + })) + ), + "listCustomPresets": (options) => HttpClientRequest.get(`/api/v1/display/presets`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListCustomPresets200), + "401": decodeError("ListCustomPresets401", ListCustomPresets401), + orElse: unexpectedStatus + })) + ), + "createCustomPreset": (options) => HttpClientRequest.post(`/api/v1/display/presets`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateCustomPreset201), + "400": decodeError("CreateCustomPreset400", CreateCustomPreset400), + "401": decodeError("CreateCustomPreset401", CreateCustomPreset401), + "500": decodeError("CreateCustomPreset500", CreateCustomPreset500), + orElse: unexpectedStatus + })) + ), + "updateCustomPreset": (id, options) => HttpClientRequest.put(`/api/v1/display/presets/${id}`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UpdateCustomPreset200), + "400": decodeError("UpdateCustomPreset400", UpdateCustomPreset400), + "401": decodeError("UpdateCustomPreset401", UpdateCustomPreset401), + "404": decodeError("UpdateCustomPreset404", UpdateCustomPreset404), + "500": decodeError("UpdateCustomPreset500", UpdateCustomPreset500), + orElse: unexpectedStatus + })) + ), + "deleteCustomPreset": (id, options) => HttpClientRequest.delete(`/api/v1/display/presets/${id}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "401": decodeError("DeleteCustomPreset401", DeleteCustomPreset401), + "404": decodeError("DeleteCustomPreset404", DeleteCustomPreset404), + "500": decodeError("DeleteCustomPreset500", DeleteCustomPreset500), + "204": () => Effect.void, + orElse: unexpectedStatus + })) + ), + "releaseDisplay": (options) => HttpClientRequest.post(`/api/v1/display/release`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ReleaseDisplay200), + "401": decodeError("ReleaseDisplay401", ReleaseDisplay401), + orElse: unexpectedStatus + })) + ), + "getDisplaySettings": (options) => HttpClientRequest.get(`/api/v1/display/settings`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetDisplaySettings200), + "401": decodeError("GetDisplaySettings401", GetDisplaySettings401), + orElse: unexpectedStatus + })) + ), + "setDisplaySettings": (options) => HttpClientRequest.put(`/api/v1/display/settings`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(SetDisplaySettings200), + "400": decodeError("SetDisplaySettings400", SetDisplaySettings400), + "401": decodeError("SetDisplaySettings401", SetDisplaySettings401), + "500": decodeError("SetDisplaySettings500", SetDisplaySettings500), + orElse: unexpectedStatus + })) + ), + "getDisplayState": (options) => HttpClientRequest.get(`/api/v1/display/state`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetDisplayState200), + "401": decodeError("GetDisplayState401", GetDisplayState401), + orElse: unexpectedStatus + })) + ), + "streamEvents": (options) => HttpClientRequest.get(`/api/v1/events`).pipe( + HttpClientRequest.setUrlParams({ "since": options?.params?.["since"] as any, "kinds": options?.params?.["kinds"] as any }), + HttpClientRequest.setHeaders({ "Last-Event-ID": options?.params?.["Last-Event-ID"] ?? undefined }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "401": decodeError("StreamEvents401", StreamEvents401), + "503": decodeError("StreamEvents503", StreamEvents503), + orElse: unexpectedStatus + })) + ), + "streamEventsSse": (options) => HttpClientRequest.get(`/api/v1/events`).pipe( + HttpClientRequest.setUrlParams({ "since": options?.params?.["since"] as any, "kinds": options?.params?.["kinds"] as any }), + HttpClientRequest.setHeaders({ "Last-Event-ID": options?.params?.["Last-Event-ID"] ?? undefined }), + sseRequest(StreamEvents200Sse) + ), + "listGpus": (options) => HttpClientRequest.get(`/api/v1/gpus`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListGpus200), + "401": decodeError("ListGpus401", ListGpus401), + orElse: unexpectedStatus + })) + ), + "setGpuPreference": (options) => HttpClientRequest.put(`/api/v1/gpus/preference`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(SetGpuPreference200), + "400": decodeError("SetGpuPreference400", SetGpuPreference400), + "401": decodeError("SetGpuPreference401", SetGpuPreference401), + "500": decodeError("SetGpuPreference500", SetGpuPreference500), + orElse: unexpectedStatus + })) + ), + "getHealth": (options) => HttpClientRequest.get(`/api/v1/health`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetHealth200), + orElse: unexpectedStatus + })) + ), + "getHooks": (options) => HttpClientRequest.get(`/api/v1/hooks`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetHooks200), + "401": decodeError("GetHooks401", GetHooks401), + orElse: unexpectedStatus + })) + ), + "setHooks": (options) => HttpClientRequest.put(`/api/v1/hooks`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(SetHooks200), + "400": decodeError("SetHooks400", SetHooks400), + "401": decodeError("SetHooks401", SetHooks401), + "500": decodeError("SetHooks500", SetHooks500), + orElse: unexpectedStatus + })) + ), + "getHostInfo": (options) => HttpClientRequest.get(`/api/v1/host`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetHostInfo200), + "401": decodeError("GetHostInfo401", GetHostInfo401), + orElse: unexpectedStatus + })) + ), + "getLibrary": (options) => HttpClientRequest.get(`/api/v1/library`).pipe( + HttpClientRequest.setUrlParams({ "provider": options?.params?.["provider"] as any }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetLibrary200), + "401": decodeError("GetLibrary401", GetLibrary401), + orElse: unexpectedStatus + })) + ), + "getLibraryArt": (id, kind, options) => HttpClientRequest.get(`/api/v1/library/art/${id}/${kind}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "401": decodeError("GetLibraryArt401", GetLibraryArt401), + "404": decodeError("GetLibraryArt404", GetLibraryArt404), + orElse: unexpectedStatus + })) + ), + "createCustomGame": (options) => HttpClientRequest.post(`/api/v1/library/custom`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(CreateCustomGame201), + "400": decodeError("CreateCustomGame400", CreateCustomGame400), + "401": decodeError("CreateCustomGame401", CreateCustomGame401), + "500": decodeError("CreateCustomGame500", CreateCustomGame500), + orElse: unexpectedStatus + })) + ), + "updateCustomGame": (id, options) => HttpClientRequest.put(`/api/v1/library/custom/${id}`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(UpdateCustomGame200), + "400": decodeError("UpdateCustomGame400", UpdateCustomGame400), + "401": decodeError("UpdateCustomGame401", UpdateCustomGame401), + "404": decodeError("UpdateCustomGame404", UpdateCustomGame404), + "500": decodeError("UpdateCustomGame500", UpdateCustomGame500), + orElse: unexpectedStatus + })) + ), + "deleteCustomGame": (id, options) => HttpClientRequest.delete(`/api/v1/library/custom/${id}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "401": decodeError("DeleteCustomGame401", DeleteCustomGame401), + "404": decodeError("DeleteCustomGame404", DeleteCustomGame404), + "500": decodeError("DeleteCustomGame500", DeleteCustomGame500), + "204": () => Effect.void, + orElse: unexpectedStatus + })) + ), + "reconcileProviderEntries": (provider, options) => HttpClientRequest.put(`/api/v1/library/provider/${provider}`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ReconcileProviderEntries200), + "400": decodeError("ReconcileProviderEntries400", ReconcileProviderEntries400), + "401": decodeError("ReconcileProviderEntries401", ReconcileProviderEntries401), + "500": decodeError("ReconcileProviderEntries500", ReconcileProviderEntries500), + orElse: unexpectedStatus + })) + ), + "deleteProviderEntries": (provider, options) => HttpClientRequest.delete(`/api/v1/library/provider/${provider}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(DeleteProviderEntries200), + "400": decodeError("DeleteProviderEntries400", DeleteProviderEntries400), + "401": decodeError("DeleteProviderEntries401", DeleteProviderEntries401), + "500": decodeError("DeleteProviderEntries500", DeleteProviderEntries500), + orElse: unexpectedStatus + })) + ), + "getLocalSummary": (options) => HttpClientRequest.get(`/api/v1/local/summary`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetLocalSummary200), + "401": decodeError("GetLocalSummary401", GetLocalSummary401), + orElse: unexpectedStatus + })) + ), + "logsGet": (options) => HttpClientRequest.get(`/api/v1/logs`).pipe( + HttpClientRequest.setUrlParams({ "after": options?.params?.["after"] as any, "limit": options?.params?.["limit"] as any }), + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(LogsGet200), + "401": decodeError("LogsGet401", LogsGet401), + orElse: unexpectedStatus + })) + ), + "listNativeClients": (options) => HttpClientRequest.get(`/api/v1/native/clients`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListNativeClients200), + "401": decodeError("ListNativeClients401", ListNativeClients401), + orElse: unexpectedStatus + })) + ), + "unpairNativeClient": (fingerprint, options) => HttpClientRequest.delete(`/api/v1/native/clients/${fingerprint}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "401": decodeError("UnpairNativeClient401", UnpairNativeClient401), + "404": decodeError("UnpairNativeClient404", UnpairNativeClient404), + "503": decodeError("UnpairNativeClient503", UnpairNativeClient503), + "204": () => Effect.void, + orElse: unexpectedStatus + })) + ), + "getNativePairing": (options) => HttpClientRequest.get(`/api/v1/native/pair`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetNativePairing200), + "401": decodeError("GetNativePairing401", GetNativePairing401), + orElse: unexpectedStatus + })) + ), + "disarmNativePairing": (options) => HttpClientRequest.delete(`/api/v1/native/pair`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "401": decodeError("DisarmNativePairing401", DisarmNativePairing401), + "503": decodeError("DisarmNativePairing503", DisarmNativePairing503), + "204": () => Effect.void, + orElse: unexpectedStatus + })) + ), + "armNativePairing": (options) => HttpClientRequest.post(`/api/v1/native/pair/arm`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ArmNativePairing200), + "401": decodeError("ArmNativePairing401", ArmNativePairing401), + "503": decodeError("ArmNativePairing503", ArmNativePairing503), + orElse: unexpectedStatus + })) + ), + "listPendingDevices": (options) => HttpClientRequest.get(`/api/v1/native/pending`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ListPendingDevices200), + "401": decodeError("ListPendingDevices401", ListPendingDevices401), + orElse: unexpectedStatus + })) + ), + "approvePendingDevice": (id, options) => HttpClientRequest.post(`/api/v1/native/pending/${id}/approve`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(ApprovePendingDevice200), + "401": decodeError("ApprovePendingDevice401", ApprovePendingDevice401), + "404": decodeError("ApprovePendingDevice404", ApprovePendingDevice404), + "500": decodeError("ApprovePendingDevice500", ApprovePendingDevice500), + "503": decodeError("ApprovePendingDevice503", ApprovePendingDevice503), + orElse: unexpectedStatus + })) + ), + "denyPendingDevice": (id, options) => HttpClientRequest.post(`/api/v1/native/pending/${id}/deny`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "401": decodeError("DenyPendingDevice401", DenyPendingDevice401), + "404": decodeError("DenyPendingDevice404", DenyPendingDevice404), + "503": decodeError("DenyPendingDevice503", DenyPendingDevice503), + "204": () => Effect.void, + orElse: unexpectedStatus + })) + ), + "getPairingStatus": (options) => HttpClientRequest.get(`/api/v1/pair`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetPairingStatus200), + "401": decodeError("GetPairingStatus401", GetPairingStatus401), + orElse: unexpectedStatus + })) + ), + "submitPairingPin": (options) => HttpClientRequest.post(`/api/v1/pair/pin`).pipe( + HttpClientRequest.bodyJsonUnsafe(options.payload), + withResponse(options.config)(HttpClientResponse.matchStatus({ + "400": decodeError("SubmitPairingPin400", SubmitPairingPin400), + "401": decodeError("SubmitPairingPin401", SubmitPairingPin401), + "409": decodeError("SubmitPairingPin409", SubmitPairingPin409), + "415": decodeError("SubmitPairingPin415", SubmitPairingPin415), + "422": decodeError("SubmitPairingPin422", SubmitPairingPin422), + "204": () => Effect.void, + orElse: unexpectedStatus + })) + ), + "stopSession": (options) => HttpClientRequest.delete(`/api/v1/session`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "401": decodeError("StopSession401", StopSession401), + "204": () => Effect.void, + orElse: unexpectedStatus + })) + ), + "requestIdr": (options) => HttpClientRequest.post(`/api/v1/session/idr`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "401": decodeError("RequestIdr401", RequestIdr401), + "409": decodeError("RequestIdr409", RequestIdr409), + "202": () => Effect.void, + orElse: unexpectedStatus + })) + ), + "statsCaptureLive": (options) => HttpClientRequest.get(`/api/v1/stats/capture/live`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(StatsCaptureLive200), + "401": decodeError("StatsCaptureLive401", StatsCaptureLive401), + "404": decodeError("StatsCaptureLive404", StatsCaptureLive404), + orElse: unexpectedStatus + })) + ), + "statsCaptureStart": (options) => HttpClientRequest.post(`/api/v1/stats/capture/start`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(StatsCaptureStart200), + "401": decodeError("StatsCaptureStart401", StatsCaptureStart401), + orElse: unexpectedStatus + })) + ), + "statsCaptureStatus": (options) => HttpClientRequest.get(`/api/v1/stats/capture/status`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(StatsCaptureStatus200), + "401": decodeError("StatsCaptureStatus401", StatsCaptureStatus401), + orElse: unexpectedStatus + })) + ), + "statsCaptureStop": (options) => HttpClientRequest.post(`/api/v1/stats/capture/stop`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(StatsCaptureStop200), + "401": decodeError("StatsCaptureStop401", StatsCaptureStop401), + "500": decodeError("StatsCaptureStop500", StatsCaptureStop500), + "204": () => Effect.void, + orElse: unexpectedStatus + })) + ), + "statsRecordingsList": (options) => HttpClientRequest.get(`/api/v1/stats/recordings`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(StatsRecordingsList200), + "401": decodeError("StatsRecordingsList401", StatsRecordingsList401), + orElse: unexpectedStatus + })) + ), + "statsRecordingGet": (id, options) => HttpClientRequest.get(`/api/v1/stats/recordings/${id}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(StatsRecordingGet200), + "401": decodeError("StatsRecordingGet401", StatsRecordingGet401), + "404": decodeError("StatsRecordingGet404", StatsRecordingGet404), + "500": decodeError("StatsRecordingGet500", StatsRecordingGet500), + orElse: unexpectedStatus + })) + ), + "statsRecordingDelete": (id, options) => HttpClientRequest.delete(`/api/v1/stats/recordings/${id}`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "401": decodeError("StatsRecordingDelete401", StatsRecordingDelete401), + "404": decodeError("StatsRecordingDelete404", StatsRecordingDelete404), + "500": decodeError("StatsRecordingDelete500", StatsRecordingDelete500), + "204": () => Effect.void, + orElse: unexpectedStatus + })) + ), + "getStatus": (options) => HttpClientRequest.get(`/api/v1/status`).pipe( + withResponse(options?.config)(HttpClientResponse.matchStatus({ + "2xx": decodeSuccess(GetStatus200), + "401": decodeError("GetStatus401", GetStatus401), + orElse: unexpectedStatus + })) + ) + } +} + +export interface Punktfunk { + readonly httpClient: HttpClient.HttpClient + /** +* List paired clients +*/ +readonly "listPairedClients": (options: { readonly config?: Config | undefined } | undefined) => Effect.Effect, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"ListPairedClients401", typeof ListPairedClients401.Type>> + /** +* Removes the client's certificate from the pairing store. Caveat: the nvhttp TLS layer +* does not yet reject unlisted certificates (`gamestream/tls.rs` accepts any well-formed +* client cert — a planned hardening step), so until that lands this removes the client +* from the listing without severing its ability to reconnect. +*/ +readonly "unpairClient": (fingerprint: string, options: { readonly config?: Config | undefined } | undefined) => Effect.Effect, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"UnpairClient400", typeof UnpairClient400.Type> | PunktfunkError<"UnpairClient401", typeof UnpairClient401.Type> | PunktfunkError<"UnpairClient404", typeof UnpairClient404.Type>> + /** +* Lists every backend the host knows how to drive, flags which are usable right now, and marks +* the one an unspecified (`Auto`) client request resolves to. Clients pass an `id` to their +* `--compositor` flag (or `PUNKTFUNK_COMPOSITOR_*` over the C ABI) to request it. +*/ +readonly "listCompositors": (options: { readonly config?: Config | undefined } | undefined) => Effect.Effect, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"ListCompositors401", typeof ListCompositors401.Type>> + /** +* Set the **manual** desktop arrangement — per-identity-slot `(x, y)` offsets so a multi-monitor +* group (§6A/§6B) comes back where the operator placed it. Persisted into the policy's layout block +* and switched to manual mode; applied from the next connect (a live group re-applies on its next +* acquire). Locks in the current effective behavior as explicit fields, so arranging displays never +* silently changes keep-alive/topology/conflict/identity. See `design/display-management.md` §6.2. +*/ +readonly "setDisplayLayout": (options: { readonly payload: typeof SetDisplayLayoutRequestJson.Encoded; readonly config?: Config | undefined }) => Effect.Effect, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"SetDisplayLayout401", typeof SetDisplayLayout401.Type> | PunktfunkError<"SetDisplayLayout500", typeof SetDisplayLayout500.Type>> + /** +* The operator's named field-bundles (`display-presets.json`). These also ride the +* `GET /display/settings` response (`custom_presets`), so the console rarely needs this directly. +*/ +readonly "listCustomPresets": (options: { readonly config?: Config | undefined } | undefined) => Effect.Effect, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"ListCustomPresets401", typeof ListCustomPresets401.Type>> + /** +* Stores a named bundle of the display-behavior axes (+ the game-session axis) the operator can +* apply later. The host assigns a stable id, returned in the body. Applying a preset is a +* `PUT /display/settings` with a `Custom` policy carrying its `fields` — no separate apply route. +*/ +readonly "createCustomPreset": (options: { readonly payload: typeof CreateCustomPresetRequestJson.Encoded; readonly config?: Config | undefined }) => Effect.Effect, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"CreateCustomPreset400", typeof CreateCustomPreset400.Type> | PunktfunkError<"CreateCustomPreset401", typeof CreateCustomPreset401.Type> | PunktfunkError<"CreateCustomPreset500", typeof CreateCustomPreset500.Type>> + /** +* Update a custom preset +*/ +readonly "updateCustomPreset": (id: string, options: { readonly payload: typeof UpdateCustomPresetRequestJson.Encoded; readonly config?: Config | undefined }) => Effect.Effect, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"UpdateCustomPreset400", typeof UpdateCustomPreset400.Type> | PunktfunkError<"UpdateCustomPreset401", typeof UpdateCustomPreset401.Type> | PunktfunkError<"UpdateCustomPreset404", typeof UpdateCustomPreset404.Type> | PunktfunkError<"UpdateCustomPreset500", typeof UpdateCustomPreset500.Type>> + /** +* Removes it from the catalog. The active policy is untouched — if this preset was the one applied, +* the running behavior stays exactly as it was (the catalog and `display-settings.json` are decoupled). +*/ +readonly "deleteCustomPreset": (id: string, options: { readonly config?: Config | undefined } | undefined) => Effect.Effect, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"DeleteCustomPreset401", typeof DeleteCustomPreset401.Type> | PunktfunkError<"DeleteCustomPreset404", typeof DeleteCustomPreset404.Type> | PunktfunkError<"DeleteCustomPreset500", typeof DeleteCustomPreset500.Type>> + /** +* Tear down lingering/pinned displays now — so a physical-screen user gets their screen back +* without waiting out the linger. `slot` releases one; omit it to release all kept displays. +* Active (streaming) displays are never torn down here (that is session control). +*/ +readonly "releaseDisplay": (options: { readonly payload: typeof ReleaseDisplayRequestJson.Encoded; readonly config?: Config | undefined }) => Effect.Effect, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"ReleaseDisplay401", typeof ReleaseDisplay401.Type>> + /** +* The stored virtual-display policy (lifecycle, topology, conflict handling, identity, layout), +* every preset's expansion, and which options this build enforces yet. See +* `design/display-management.md`. +*/ +readonly "getDisplaySettings": (options: { readonly config?: Config | undefined } | undefined) => Effect.Effect, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"GetDisplaySettings401", typeof GetDisplaySettings401.Type>> + /** +* Persists a new policy (validated + clamped) and applies it from the next connect/teardown — a +* running session keeps the display it opened on. `keep_alive: forever` (the gaming-rig preset) is +* honored (the display is Pinned; free it via `POST /display/release`). +*/ +readonly "setDisplaySettings": (options: { readonly payload: typeof SetDisplaySettingsRequestJson.Encoded; readonly config?: Config | undefined }) => Effect.Effect, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"SetDisplaySettings400", typeof SetDisplaySettings400.Type> | PunktfunkError<"SetDisplaySettings401", typeof SetDisplaySettings401.Type> | PunktfunkError<"SetDisplaySettings500", typeof SetDisplaySettings500.Type>> + /** +* The host's managed virtual displays right now — active (streaming), lingering (kept after +* disconnect, counting down to teardown), or pinned (kept indefinitely). See +* `design/display-management.md`. +*/ +readonly "getDisplayState": (options: { readonly config?: Config | undefined } | undefined) => Effect.Effect, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"GetDisplayState401", typeof GetDisplayState401.Type>> + /** +* Server-Sent Events stream of the host's lifecycle events: client connect/disconnect, session +* and stream start/end, pairing decisions, display create/release, library changes, host +* start/stop — both protocol planes. Frames carry `id:` = the event's monotonic `seq`, +* `event:` = its kind, and `data:` = the event JSON (schema-versioned, additive-only). +* +* Resume: standard `Last-Event-ID` (or `?since=`) replays from the in-memory ring; a consumer +* that fell off the ring receives an `event: dropped` frame first and should resync via the +* REST snapshots. Keep-alive comments are sent every 15 s. +*/ +readonly "streamEvents": (options: { readonly params?: typeof StreamEventsParams.Encoded | undefined; readonly config?: Config | undefined } | undefined) => Effect.Effect, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"StreamEvents401", typeof StreamEvents401.Type> | PunktfunkError<"StreamEvents503", typeof StreamEvents503.Type>> + /** +* Server-Sent Events stream of the host's lifecycle events: client connect/disconnect, session +* and stream start/end, pairing decisions, display create/release, library changes, host +* start/stop — both protocol planes. Frames carry `id:` = the event's monotonic `seq`, +* `event:` = its kind, and `data:` = the event JSON (schema-versioned, additive-only). +* +* Resume: standard `Last-Event-ID` (or `?since=`) replays from the in-memory ring; a consumer +* that fell off the ring receives an `event: dropped` frame first and should resync via the +* REST snapshots. Keep-alive comments are sent every 15 s. +*/ +readonly "streamEventsSse": (options: { readonly params?: typeof StreamEventsParams.Encoded | undefined } | undefined) => Stream.Stream<{ readonly event: string; readonly id: string | undefined; readonly data: typeof StreamEvents200Sse.Type }, HttpClientError.HttpClientError | SchemaError | Sse.Retry, typeof StreamEvents200Sse.DecodingServices> + /** +* Lists the host's hardware GPUs, the persisted auto/manual preference, the GPU the next session +* will use (and why), and the GPU live sessions encode on right now. +*/ +readonly "listGpus": (options: { readonly config?: Config | undefined } | undefined) => Effect.Effect, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"ListGpus401", typeof ListGpus401.Type>> + /** +* `auto` restores automatic selection (`PUNKTFUNK_RENDER_ADAPTER` pin, else max dedicated VRAM); +* `manual` pins capture + encode to the given GPU. Persisted across restarts; applies to the +* **next** session (a running session keeps its GPU). If the preferred GPU is absent at session +* start the host falls back to automatic selection rather than failing. +*/ +readonly "setGpuPreference": (options: { readonly payload: typeof SetGpuPreferenceRequestJson.Encoded; readonly config?: Config | undefined }) => Effect.Effect, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"SetGpuPreference400", typeof SetGpuPreference400.Type> | PunktfunkError<"SetGpuPreference401", typeof SetGpuPreference401.Type> | PunktfunkError<"SetGpuPreference500", typeof SetGpuPreference500.Type>> + /** +* Always available without authentication. +*/ +readonly "getHealth": (options: { readonly config?: Config | undefined } | undefined) => Effect.Effect, HttpClientError.HttpClientError | SchemaError> + /** +* The operator's `hooks.json`: commands and webhooks fired on host lifecycle events. Empty +* when unconfigured. +*/ +readonly "getHooks": (options: { readonly config?: Config | undefined } | undefined) => Effect.Effect, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"GetHooks401", typeof GetHooks401.Type>> + /** +* Validates and persists a full `hooks.json` document (this is a whole-document PUT, not a +* patch). Applies from the next event — no restart. Hook commands run as the host user +* (interactive user session on Windows): treat this configuration as operator-privileged. +*/ +readonly "setHooks": (options: { readonly payload: typeof SetHooksRequestJson.Encoded; readonly config?: Config | undefined }) => Effect.Effect, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"SetHooks400", typeof SetHooks400.Type> | PunktfunkError<"SetHooks401", typeof SetHooks401.Type> | PunktfunkError<"SetHooks500", typeof SetHooks500.Type>> + /** +* Host identity and capabilities +*/ +readonly "getHostInfo": (options: { readonly config?: Config | undefined } | undefined) => Effect.Effect, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"GetHostInfo401", typeof GetHostInfo401.Type>> + /** +* Every installed-store title (Steam, read from the host's local files — no Steam API key) +* merged with the user's custom entries, sorted by title. Artwork fields are URLs the client +* fetches directly (the public Steam CDN for Steam titles). `?provider=` narrows to the +* entries a given external provider owns. +*/ +readonly "getLibrary": (options: { readonly params?: typeof GetLibraryParams.Encoded | undefined; readonly config?: Config | undefined } | undefined) => Effect.Effect, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"GetLibrary401", typeof GetLibrary401.Type>> + /** +* Resolves `kind` (`portrait` | `hero` | `logo` | `header`) for the given library id and streams +* the image bytes. For a Steam title, the host's own local Steam cache is tried first (exact — +* it's what the user's Steam client already shows for it), the public Steam CDN's flat URL +* convention as a fallback (newer titles' CDN assets can live at a per-asset-hash path the host +* can't predict, in which case this 404s and the client falls through to its next art candidate). +* Only Steam ids are backed today; any other store 404s. +*/ +readonly "getLibraryArt": (id: string, kind: string, options: { readonly config?: Config | undefined } | undefined) => Effect.Effect, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"GetLibraryArt401", typeof GetLibraryArt401.Type> | PunktfunkError<"GetLibraryArt404", typeof GetLibraryArt404.Type>> + /** +* Creates a user-curated title (e.g. a non-Steam game, an emulator, a ROM) with caller-supplied +* artwork URLs. The host assigns a stable id, returned in the body. +*/ +readonly "createCustomGame": (options: { readonly payload: typeof CreateCustomGameRequestJson.Encoded; readonly config?: Config | undefined }) => Effect.Effect, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"CreateCustomGame400", typeof CreateCustomGame400.Type> | PunktfunkError<"CreateCustomGame401", typeof CreateCustomGame401.Type> | PunktfunkError<"CreateCustomGame500", typeof CreateCustomGame500.Type>> + /** +* Update a custom library entry +*/ +readonly "updateCustomGame": (id: string, options: { readonly payload: typeof UpdateCustomGameRequestJson.Encoded; readonly config?: Config | undefined }) => Effect.Effect, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"UpdateCustomGame400", typeof UpdateCustomGame400.Type> | PunktfunkError<"UpdateCustomGame401", typeof UpdateCustomGame401.Type> | PunktfunkError<"UpdateCustomGame404", typeof UpdateCustomGame404.Type> | PunktfunkError<"UpdateCustomGame500", typeof UpdateCustomGame500.Type>> + /** +* Delete a custom library entry +*/ +readonly "deleteCustomGame": (id: string, options: { readonly config?: Config | undefined } | undefined) => Effect.Effect, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"DeleteCustomGame401", typeof DeleteCustomGame401.Type> | PunktfunkError<"DeleteCustomGame404", typeof DeleteCustomGame404.Type> | PunktfunkError<"DeleteCustomGame500", typeof DeleteCustomGame500.Type>> + /** +* Atomically replaces the full entry set owned by `{provider}` (RFC §8): the payload is the +* provider's desired list, keyed by its own stable `external_id` — the host diffs, keeps each +* surviving title's host id stable across reconciles, drops orphans, and never touches manual +* entries or other providers'. An empty array removes everything the provider owns. Emits +* `library.changed` with the provider as `source`. +*/ +readonly "reconcileProviderEntries": (provider: string, options: { readonly payload: typeof ReconcileProviderEntriesRequestJson.Encoded; readonly config?: Config | undefined }) => Effect.Effect, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"ReconcileProviderEntries400", typeof ReconcileProviderEntries400.Type> | PunktfunkError<"ReconcileProviderEntries401", typeof ReconcileProviderEntries401.Type> | PunktfunkError<"ReconcileProviderEntries500", typeof ReconcileProviderEntries500.Type>> + /** +* Deletes every entry owned by `{provider}` — the clean-uninstall path for a provider plugin +* (RFC §8). Emits `library.changed` when anything was removed. +*/ +readonly "deleteProviderEntries": (provider: string, options: { readonly config?: Config | undefined } | undefined) => Effect.Effect, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"DeleteProviderEntries400", typeof DeleteProviderEntries400.Type> | PunktfunkError<"DeleteProviderEntries401", typeof DeleteProviderEntries401.Type> | PunktfunkError<"DeleteProviderEntries500", typeof DeleteProviderEntries500.Type>> + /** +* Non-sensitive status (counts and booleans only — no PIN values, no fingerprints, no device +* names). Unauthenticated, but served to loopback peers only. +*/ +readonly "getLocalSummary": (options: { readonly config?: Config | undefined } | undefined) => Effect.Effect, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"GetLocalSummary401", typeof GetLocalSummary401.Type>> + /** +* The host's recent log entries — an in-memory ring of the newest few thousand, captured at +* DEBUG and above regardless of `RUST_LOG`. Follow live by polling with `after` set to the last +* response's `next` cursor; a `dropped: true` means entries were evicted between polls (the ring +* wrapped). Bearer-only: logs can reference client identities and host paths, so this is part of +* the loopback-only admin surface, never the LAN-readable mTLS one. +*/ +readonly "logsGet": (options: { readonly params?: typeof LogsGetParams.Encoded | undefined; readonly config?: Config | undefined } | undefined) => Effect.Effect, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"LogsGet401", typeof LogsGet401.Type>> + /** +* List native paired clients +*/ +readonly "listNativeClients": (options: { readonly config?: Config | undefined } | undefined) => Effect.Effect, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"ListNativeClients401", typeof ListNativeClients401.Type>> + /** +* Removes a punktfunk/1 client from the native trust store by fingerprint. +*/ +readonly "unpairNativeClient": (fingerprint: string, options: { readonly config?: Config | undefined } | undefined) => Effect.Effect, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"UnpairNativeClient401", typeof UnpairNativeClient401.Type> | PunktfunkError<"UnpairNativeClient404", typeof UnpairNativeClient404.Type> | PunktfunkError<"UnpairNativeClient503", typeof UnpairNativeClient503.Type>> + /** +* The native (punktfunk/1) pairing window. Poll while armed to show the PIN + countdown. +* `enabled: false` means this host runs GameStream only (no `--native`). +*/ +readonly "getNativePairing": (options: { readonly config?: Config | undefined } | undefined) => Effect.Effect, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"GetNativePairing401", typeof GetNativePairing401.Type>> + /** +* Closes the pairing window immediately (no new ceremonies accepted). +*/ +readonly "disarmNativePairing": (options: { readonly config?: Config | undefined } | undefined) => Effect.Effect, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"DisarmNativePairing401", typeof DisarmNativePairing401.Type> | PunktfunkError<"DisarmNativePairing503", typeof DisarmNativePairing503.Type>> + /** +* Opens a pairing window and mints a fresh PIN to display. The user enters it on their device +* within `ttl_secs`; the device then appears in the native client list. +*/ +readonly "armNativePairing": (options: { readonly payload: typeof ArmNativePairingRequestJson.Encoded; readonly config?: Config | undefined }) => Effect.Effect, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"ArmNativePairing401", typeof ArmNativePairing401.Type> | PunktfunkError<"ArmNativePairing503", typeof ArmNativePairing503.Type>> + /** +* Unpaired devices that tried to connect while the host requires pairing. Approve one to pair +* it without a PIN (delegated approval); entries expire after ~10 minutes. +*/ +readonly "listPendingDevices": (options: { readonly config?: Config | undefined } | undefined) => Effect.Effect, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"ListPendingDevices401", typeof ListPendingDevices401.Type>> + /** +* Pairs the device's certificate fingerprint — it can connect immediately (no PIN). Optionally +* relabel it via the body; send `{}` to keep the name it knocked with. +*/ +readonly "approvePendingDevice": (id: string, options: { readonly payload: typeof ApprovePendingDeviceRequestJson.Encoded; readonly config?: Config | undefined }) => Effect.Effect, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"ApprovePendingDevice401", typeof ApprovePendingDevice401.Type> | PunktfunkError<"ApprovePendingDevice404", typeof ApprovePendingDevice404.Type> | PunktfunkError<"ApprovePendingDevice500", typeof ApprovePendingDevice500.Type> | PunktfunkError<"ApprovePendingDevice503", typeof ApprovePendingDevice503.Type>> + /** +* Drops the request. Not a blocklist — the device's next attempt knocks again. +*/ +readonly "denyPendingDevice": (id: string, options: { readonly config?: Config | undefined } | undefined) => Effect.Effect, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"DenyPendingDevice401", typeof DenyPendingDevice401.Type> | PunktfunkError<"DenyPendingDevice404", typeof DenyPendingDevice404.Type> | PunktfunkError<"DenyPendingDevice503", typeof DenyPendingDevice503.Type>> + /** +* Poll this to know when to prompt the user for the PIN Moonlight displays. +*/ +readonly "getPairingStatus": (options: { readonly config?: Config | undefined } | undefined) => Effect.Effect, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"GetPairingStatus401", typeof GetPairingStatus401.Type>> + /** +* Delivers the PIN the Moonlight client is displaying, completing the out-of-band half +* of the pairing handshake. +*/ +readonly "submitPairingPin": (options: { readonly payload: typeof SubmitPairingPinRequestJson.Encoded; readonly config?: Config | undefined }) => Effect.Effect, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"SubmitPairingPin400", typeof SubmitPairingPin400.Type> | PunktfunkError<"SubmitPairingPin401", typeof SubmitPairingPin401.Type> | PunktfunkError<"SubmitPairingPin409", typeof SubmitPairingPin409.Type> | PunktfunkError<"SubmitPairingPin415", typeof SubmitPairingPin415.Type> | PunktfunkError<"SubmitPairingPin422", typeof SubmitPairingPin422.Type>> + /** +* Kicks the connected client: stops the video/audio stream threads and clears the launch +* state. Idempotent — succeeds even when nothing is streaming. +*/ +readonly "stopSession": (options: { readonly config?: Config | undefined } | undefined) => Effect.Effect, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"StopSession401", typeof StopSession401.Type>> + /** +* Asks the encoder for an IDR frame on the active video stream (what a client requests +* after unrecoverable loss — exposed for debugging). +*/ +readonly "requestIdr": (options: { readonly config?: Config | undefined } | undefined) => Effect.Effect, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"RequestIdr401", typeof RequestIdr401.Type> | PunktfunkError<"RequestIdr409", typeof RequestIdr409.Type>> + /** +* The full sample time-series of the capture currently recording, for live graphing. `404` when +* nothing is armed. +*/ +readonly "statsCaptureLive": (options: { readonly config?: Config | undefined } | undefined) => Effect.Effect, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"StatsCaptureLive401", typeof StatsCaptureLive401.Type> | PunktfunkError<"StatsCaptureLive404", typeof StatsCaptureLive404.Type>> + /** +* Arms a new performance-stats capture. Idempotent: if a capture is already running this returns +* the current status unchanged. While armed, the streaming loops emit aggregated samples (~ every +* 1–2 s) into the in-progress capture, readable live via `GET /stats/capture/live`. +*/ +readonly "statsCaptureStart": (options: { readonly config?: Config | undefined } | undefined) => Effect.Effect, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"StatsCaptureStart401", typeof StatsCaptureStart401.Type>> + /** +* Whether a capture is armed, its sample count, and start time. Poll this (e.g. every 2 s) to +* drive the capture-control UI. +*/ +readonly "statsCaptureStatus": (options: { readonly config?: Config | undefined } | undefined) => Effect.Effect, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"StatsCaptureStatus401", typeof StatsCaptureStatus401.Type>> + /** +* Disarms the in-progress capture and writes it to disk atomically, returning its summary. If +* nothing was recording, returns `204 No Content`. +*/ +readonly "statsCaptureStop": (options: { readonly config?: Config | undefined } | undefined) => Effect.Effect, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"StatsCaptureStop401", typeof StatsCaptureStop401.Type> | PunktfunkError<"StatsCaptureStop500", typeof StatsCaptureStop500.Type>> + /** +* Every saved capture's summary (the `meta` head only — not the sample body), newest first. +*/ +readonly "statsRecordingsList": (options: { readonly config?: Config | undefined } | undefined) => Effect.Effect, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"StatsRecordingsList401", typeof StatsRecordingsList401.Type>> + /** +* The full capture (meta + samples) for `id`, for graphing or download. +*/ +readonly "statsRecordingGet": (id: string, options: { readonly config?: Config | undefined } | undefined) => Effect.Effect, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"StatsRecordingGet401", typeof StatsRecordingGet401.Type> | PunktfunkError<"StatsRecordingGet404", typeof StatsRecordingGet404.Type> | PunktfunkError<"StatsRecordingGet500", typeof StatsRecordingGet500.Type>> + /** +* Removes the recording `id` from disk. `404` if there is no such recording. +*/ +readonly "statsRecordingDelete": (id: string, options: { readonly config?: Config | undefined } | undefined) => Effect.Effect, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"StatsRecordingDelete401", typeof StatsRecordingDelete401.Type> | PunktfunkError<"StatsRecordingDelete404", typeof StatsRecordingDelete404.Type> | PunktfunkError<"StatsRecordingDelete500", typeof StatsRecordingDelete500.Type>> + /** +* Live host status +*/ +readonly "getStatus": (options: { readonly config?: Config | undefined } | undefined) => Effect.Effect, HttpClientError.HttpClientError | SchemaError | PunktfunkError<"GetStatus401", typeof GetStatus401.Type>> +} + +export interface PunktfunkError { + readonly _tag: Tag + readonly request: HttpClientRequest.HttpClientRequest + readonly response: HttpClientResponse.HttpClientResponse + readonly cause: E +} + +class PunktfunkErrorImpl extends Data.Error<{ + _tag: string + cause: any + request: HttpClientRequest.HttpClientRequest + response: HttpClientResponse.HttpClientResponse +}> {} + +export const PunktfunkError = ( + tag: Tag, + cause: E, + response: HttpClientResponse.HttpClientResponse, +): PunktfunkError => + new PunktfunkErrorImpl({ + _tag: tag, + cause, + response, + request: response.request, + }) as any diff --git a/sdk/src/gen/schemas.ts b/sdk/src/gen/schemas.ts deleted file mode 100644 index 49bac604..00000000 --- a/sdk/src/gen/schemas.ts +++ /dev/null @@ -1,1684 +0,0 @@ -/** - * Generated by orval v8.22.0 🍺 - * Do not edit manually. - * punktfunk management API - * Control-plane API for managing a punktfunk 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). - * OpenAPI spec version: 0.12.0 - */ -import { - Schema as S -} from 'effect'; - -/** - * @summary List paired clients - */ -export const ListPairedClientsResponseItem = S.Struct({ - "fingerprint": S.String.annotations({ description: 'Lowercase hex SHA-256 of the client certificate DER — the client\'s stable id here.' }), - "not_after_unix": S.optional(S.NullOr(S.Number)).annotations({ description: 'Certificate validity end (unix seconds).' }), - "not_before_unix": S.optional(S.NullOr(S.Number)).annotations({ description: 'Certificate validity start (unix seconds).' }), - "subject": S.optional(S.NullOr(S.String)).annotations({ description: 'Certificate subject (e.g. `CN=NVIDIA GameStream Client`), if the DER parses.' }) -}).annotations({ description: 'A paired (certificate-pinned) Moonlight client.' }) -export const ListPairedClientsResponse = S.Array(ListPairedClientsResponseItem) - - -/** - * Removes the client's certificate from the pairing store. Caveat: the nvhttp TLS layer - * does not yet reject unlisted certificates (`gamestream/tls.rs` accepts any well-formed - * client cert — a planned hardening step), so until that lands this removes the client - * from the listing without severing its ability to reconnect. - * @summary Unpair a client - */ -export const UnpairClientParams = S.Struct({ - "fingerprint": S.String.annotations({ description: 'Hex SHA-256 fingerprint of the client certificate DER (64 chars, case-insensitive)' }) -}) - - -/** - * Lists every backend the host knows how to drive, flags which are usable right now, and marks - * the one an unspecified (`Auto`) client request resolves to. Clients pass an `id` to their - * `--compositor` flag (or `PUNKTFUNK_COMPOSITOR_*` over the C ABI) to request it. - * @summary Available compositor backends - */ -export const ListCompositorsResponseItem = S.Struct({ - "available": S.Boolean.annotations({ description: 'Usable on this host right now: the live session\'s own compositor, or gamescope wherever\nits binary is installed.' }), - "default": S.Boolean.annotations({ description: 'True for the backend an `Auto` (unspecified) request resolves to right now.' }), - "id": S.String.annotations({ description: 'Stable identifier (`\"kwin\"` | `\"wlroots\"` | `\"mutter\"` | `\"gamescope\"`) — pass this to a\nclient\'s `--compositor` flag.' }), - "label": S.String.annotations({ description: 'Human-readable label for UIs.' }) -}).annotations({ description: 'A compositor backend the host can drive a virtual output on, and whether it\'s usable now.' }) -export const ListCompositorsResponse = S.Array(ListCompositorsResponseItem) - - -/** - * Set the **manual** desktop arrangement — per-identity-slot `(x, y)` offsets so a multi-monitor - * group (§6A/§6B) comes back where the operator placed it. Persisted into the policy's layout block - * and switched to manual mode; applied from the next connect (a live group re-applies on its next - * acquire). Locks in the current effective behavior as explicit fields, so arranging displays never - * silently changes keep-alive/topology/conflict/identity. See `design/display-management.md` §6.2. - * @summary Arrange virtual displays - */ -export const SetDisplayLayoutBody = S.Struct({ - "positions": S.optional(S.Record({ key: S.String, value: S.Struct({ - "x": S.Number, - "y": S.Number -}).annotations({ description: 'A desktop-space offset for a display (top-left origin).' }) })).annotations({ description: '`{\"\": {\"x\": …, \"y\": …}}` — where each arranged display\'s top-left sits.' }) -}).annotations({ description: 'Request body for `setDisplayLayout`: per-identity-slot desktop offsets, keyed by the identity-slot\nid as a string (the same id `\/display\/state` reports as `identity_slot`).' }) - -export const setDisplayLayoutResponseCustomPresetsItemFieldsKeepAliveTwoSecondsMin = 0; - -export const setDisplayLayoutResponseCustomPresetsItemFieldsMaxDisplaysMin = 0; - -export const setDisplayLayoutResponseEffectiveKeepAliveTwoSecondsMin = 0; - -export const setDisplayLayoutResponseEffectiveMaxDisplaysMin = 0; - -export const setDisplayLayoutResponsePresetsItemFieldsKeepAliveTwoSecondsMin = 0; - -export const setDisplayLayoutResponsePresetsItemFieldsMaxDisplaysMin = 0; - -export const setDisplayLayoutResponseSettingsKeepAliveTwoSecondsMin = 0; - -export const setDisplayLayoutResponseSettingsMaxDisplaysMin = 0; - -export const setDisplayLayoutResponseSettingsVersionMin = 0; - - - -export const SetDisplayLayoutResponse = S.Struct({ - "configured": S.Boolean.annotations({ description: 'True once a `display-settings.json` exists (the console has configured this host).' }), - "custom_presets": S.Array(S.Struct({ - "fields": S.Struct({ - "identity": S.Literal('shared', 'per-client', 'per-client-mode').annotations({ description: 'Stable display identity, so desktop environments persist per-display config (KDE scaling). Stored\nat Stage 0; carriers wired from the identity stage.' }), - "keep_alive": S.Union(S.Struct({ - "mode": S.Literal('off') -}).annotations({ description: 'Tear the display down at session end (today\'s default on every backend but Windows, which\nlingers 10 s).' }), S.Struct({ - "mode": S.Literal('duration'), - "seconds": S.Number.pipe(S.greaterThanOrEqualTo(setDisplayLayoutResponseCustomPresetsItemFieldsKeepAliveTwoSecondsMin)).annotations({ description: 'Linger window in seconds.' }) -}).annotations({ description: 'Keep the display for `seconds` after the last session leaves, then tear it down; a reconnect\ninside the window reuses it.' }), S.Struct({ - "mode": S.Literal('forever') -}).annotations({ description: 'Keep the display until host shutdown or an explicit release (the `Pinned` lifecycle state).\n\*\*Not honored until the display-lifecycle stage\*\* — rejected by the mgmt PUT at Stage 0.' })).annotations({ description: 'How long a virtual display (and, on gamescope\'s bare spawn, the nested session + its game)\nsurvives after the last client session detaches. Serialized as an object tagged on `mode`\n(`{\"mode\":\"off\"}` \/ `{\"mode\":\"duration\",\"seconds\":300}` \/ `{\"mode\":\"forever\"}`) so the web form\nand the OpenAPI schema stay simple.' }), - "layout": S.Struct({ - "mode": S.optional(S.Literal('auto-row', 'manual')).annotations({ description: 'How group members are arranged in the desktop coordinate space. Stored at Stage 0; applied from\nthe multi-monitor stage.' }), - "positions": S.optional(S.Record({ key: S.String, value: S.Struct({ - "x": S.Number, - "y": S.Number -}).annotations({ description: 'A desktop-space offset for a display (top-left origin).' }) })) -}).annotations({ description: 'Group layout: the arrangement mode plus, for [`LayoutMode::Manual`], per-slot offsets keyed by\nidentity-slot id (string keys for stable JSON).' }), - "max_displays": S.Number.pipe(S.greaterThanOrEqualTo(setDisplayLayoutResponseCustomPresetsItemFieldsMaxDisplaysMin)), - "mode_conflict": S.Literal('separate', 'steal', 'join', 'reject').annotations({ description: 'Admission when a \*different\* client connects while a display\/session is already live and asks for\na different mode. Stored at Stage 0; enforced from the mode-conflict admission stage.' }), - "topology": S.Literal('auto', 'extend', 'primary', 'exclusive').annotations({ description: 'What the host does to the box\'s display topology while managed virtual displays are up.' }) -}).annotations({ description: 'The six display-behavior axes this preset applies (the same shape a built-in preset expands to).' }), - "game_session": S.optional(S.Literal('auto', 'dedicated')).annotations({ description: 'The game-session routing this preset applies (orthogonal to the six axes; see [`GameSession`]).\nA custom preset captures the operator\'s \*full\* setup, so — unlike a built-in preset — applying\none does set this axis.' }), - "id": S.String.annotations({ description: 'Host-assigned, stable for the life of the entry (the `{id}` in the CRUD path).' }), - "name": S.String.annotations({ description: 'User-facing name shown on the preset card; editable.' }) -}).annotations({ description: 'A user-defined named preset: a saved bundle of the six display-behavior axes (exactly what a\nbuilt-in [`Preset`] expands to) plus the orthogonal game-session axis, that the operator names\nand applies from the console.\n\nUnlike the built-in [`Preset`]s (a closed enum), custom presets are \*\*data\*\* — a catalog stored in\n`\/display-presets.json`. Applying one writes a `Custom` [`DisplayPolicy`] carrying these\nfields (the console reuses `PUT \/display\/settings`), so [`DisplayPolicy::effective`] stays pure and\nthe built-in set is never touched. The catalog is decoupled from the active `display-settings.json`:\nediting or deleting a preset never mutates the running policy (re-apply to adopt a change).' })).annotations({ description: 'The operator\'s saved custom presets (`display-presets.json`) — named field-bundles rendered\nalongside the built-ins. Managed via `POST\/PUT\/DELETE \/display\/presets`; applied by writing a\n`Custom` policy carrying the preset\'s fields.' }), - "effective": S.Struct({ - "identity": S.Literal('shared', 'per-client', 'per-client-mode').annotations({ description: 'Stable display identity, so desktop environments persist per-display config (KDE scaling). Stored\nat Stage 0; carriers wired from the identity stage.' }), - "keep_alive": S.Union(S.Struct({ - "mode": S.Literal('off') -}).annotations({ description: 'Tear the display down at session end (today\'s default on every backend but Windows, which\nlingers 10 s).' }), S.Struct({ - "mode": S.Literal('duration'), - "seconds": S.Number.pipe(S.greaterThanOrEqualTo(setDisplayLayoutResponseEffectiveKeepAliveTwoSecondsMin)).annotations({ description: 'Linger window in seconds.' }) -}).annotations({ description: 'Keep the display for `seconds` after the last session leaves, then tear it down; a reconnect\ninside the window reuses it.' }), S.Struct({ - "mode": S.Literal('forever') -}).annotations({ description: 'Keep the display until host shutdown or an explicit release (the `Pinned` lifecycle state).\n\*\*Not honored until the display-lifecycle stage\*\* — rejected by the mgmt PUT at Stage 0.' })).annotations({ description: 'How long a virtual display (and, on gamescope\'s bare spawn, the nested session + its game)\nsurvives after the last client session detaches. Serialized as an object tagged on `mode`\n(`{\"mode\":\"off\"}` \/ `{\"mode\":\"duration\",\"seconds\":300}` \/ `{\"mode\":\"forever\"}`) so the web form\nand the OpenAPI schema stay simple.' }), - "layout": S.Struct({ - "mode": S.optional(S.Literal('auto-row', 'manual')).annotations({ description: 'How group members are arranged in the desktop coordinate space. Stored at Stage 0; applied from\nthe multi-monitor stage.' }), - "positions": S.optional(S.Record({ key: S.String, value: S.Struct({ - "x": S.Number, - "y": S.Number -}).annotations({ description: 'A desktop-space offset for a display (top-left origin).' }) })) -}).annotations({ description: 'Group layout: the arrangement mode plus, for [`LayoutMode::Manual`], per-slot offsets keyed by\nidentity-slot id (string keys for stable JSON).' }), - "max_displays": S.Number.pipe(S.greaterThanOrEqualTo(setDisplayLayoutResponseEffectiveMaxDisplaysMin)), - "mode_conflict": S.Literal('separate', 'steal', 'join', 'reject').annotations({ description: 'Admission when a \*different\* client connects while a display\/session is already live and asks for\na different mode. Stored at Stage 0; enforced from the mode-conflict admission stage.' }), - "topology": S.Literal('auto', 'extend', 'primary', 'exclusive').annotations({ description: 'What the host does to the box\'s display topology while managed virtual displays are up.' }) -}).annotations({ description: 'The effective (preset-expanded) policy currently in force.' }), - "enforced": S.Array(S.String).annotations({ description: 'Option names this build enforces right now. All five axes are now acted on (keep_alive +\ntopology since Stage 0-2, identity Stage 3, mode_conflict Stage 4, layout Stage 5) — the console\nreads this to know which controls are live vs. \"coming soon\" (per-backend nuance, e.g. layout\nposition apply being KWin-only, is reported per display in `\/display\/state`).' }), - "presets": S.Array(S.Struct({ - "fields": S.Struct({ - "identity": S.Literal('shared', 'per-client', 'per-client-mode').annotations({ description: 'Stable display identity, so desktop environments persist per-display config (KDE scaling). Stored\nat Stage 0; carriers wired from the identity stage.' }), - "keep_alive": S.Union(S.Struct({ - "mode": S.Literal('off') -}).annotations({ description: 'Tear the display down at session end (today\'s default on every backend but Windows, which\nlingers 10 s).' }), S.Struct({ - "mode": S.Literal('duration'), - "seconds": S.Number.pipe(S.greaterThanOrEqualTo(setDisplayLayoutResponsePresetsItemFieldsKeepAliveTwoSecondsMin)).annotations({ description: 'Linger window in seconds.' }) -}).annotations({ description: 'Keep the display for `seconds` after the last session leaves, then tear it down; a reconnect\ninside the window reuses it.' }), S.Struct({ - "mode": S.Literal('forever') -}).annotations({ description: 'Keep the display until host shutdown or an explicit release (the `Pinned` lifecycle state).\n\*\*Not honored until the display-lifecycle stage\*\* — rejected by the mgmt PUT at Stage 0.' })).annotations({ description: 'How long a virtual display (and, on gamescope\'s bare spawn, the nested session + its game)\nsurvives after the last client session detaches. Serialized as an object tagged on `mode`\n(`{\"mode\":\"off\"}` \/ `{\"mode\":\"duration\",\"seconds\":300}` \/ `{\"mode\":\"forever\"}`) so the web form\nand the OpenAPI schema stay simple.' }), - "layout": S.Struct({ - "mode": S.optional(S.Literal('auto-row', 'manual')).annotations({ description: 'How group members are arranged in the desktop coordinate space. Stored at Stage 0; applied from\nthe multi-monitor stage.' }), - "positions": S.optional(S.Record({ key: S.String, value: S.Struct({ - "x": S.Number, - "y": S.Number -}).annotations({ description: 'A desktop-space offset for a display (top-left origin).' }) })) -}).annotations({ description: 'Group layout: the arrangement mode plus, for [`LayoutMode::Manual`], per-slot offsets keyed by\nidentity-slot id (string keys for stable JSON).' }), - "max_displays": S.Number.pipe(S.greaterThanOrEqualTo(setDisplayLayoutResponsePresetsItemFieldsMaxDisplaysMin)), - "mode_conflict": S.Literal('separate', 'steal', 'join', 'reject').annotations({ description: 'Admission when a \*different\* client connects while a display\/session is already live and asks for\na different mode. Stored at Stage 0; enforced from the mode-conflict admission stage.' }), - "topology": S.Literal('auto', 'extend', 'primary', 'exclusive').annotations({ description: 'What the host does to the box\'s display topology while managed virtual displays are up.' }) -}).annotations({ description: 'The effective policy this preset expands to (the same fields a `custom` policy carries).' }), - "id": S.String.annotations({ description: 'The preset id (`default` | `gaming-rig` | `shared-desktop` | `hotdesk` | `workstation`).' }), - "summary": S.String.annotations({ description: 'One-line story shown next to the option.' }) -}).annotations({ description: 'One preset\'s human-facing description + the fields it expands to, so the console can render a\npreset picker with an accurate \"what this does\" preview without hardcoding the expansion.' })).annotations({ description: 'Every named preset and what it expands to (for the picker\'s preview).' }), - "settings": S.Struct({ - "ddc_power_off": S.optional(S.Boolean).annotations({ description: 'EXPERIMENTAL (Windows): command physical monitors\' panels off over DDC\/CI (VCP 0xD6 →\nDPMS off) right before an `Exclusive` isolate deactivates them, and back on at restore.\nTargets the \"connected-but-dark head\" periodic-stutter class (monitor standby\nauto-input-scan \/ DP link churn while the virtual display is the sole active display) at\nthe monitor-firmware level. Best-effort — monitors without DDC\/CI (or with it disabled in\nthe OSD) are skipped. Orthogonal to `preset` (like `game_session`): preserved across\npreset changes; `#[serde(default)]` = off so existing `display-settings.json` files are\nuntouched.' }), - "game_session": S.optional(S.Literal('auto', 'dedicated')).annotations({ description: 'How a game-launching session is served (`design\/gamemode-and-dedicated-sessions.md` §5.2).\nOrthogonal to `preset`\/lifecycle — preserved across preset changes; `#[serde(default)]` = `Auto`\nso existing `display-settings.json` files are untouched.' }), - "identity": S.optional(S.Literal('shared', 'per-client', 'per-client-mode')).annotations({ description: 'Stable display identity, so desktop environments persist per-display config (KDE scaling). Stored\nat Stage 0; carriers wired from the identity stage.' }), - "keep_alive": S.optional(S.Union(S.Struct({ - "mode": S.Literal('off') -}).annotations({ description: 'Tear the display down at session end (today\'s default on every backend but Windows, which\nlingers 10 s).' }), S.Struct({ - "mode": S.Literal('duration'), - "seconds": S.Number.pipe(S.greaterThanOrEqualTo(setDisplayLayoutResponseSettingsKeepAliveTwoSecondsMin)).annotations({ description: 'Linger window in seconds.' }) -}).annotations({ description: 'Keep the display for `seconds` after the last session leaves, then tear it down; a reconnect\ninside the window reuses it.' }), S.Struct({ - "mode": S.Literal('forever') -}).annotations({ description: 'Keep the display until host shutdown or an explicit release (the `Pinned` lifecycle state).\n\*\*Not honored until the display-lifecycle stage\*\* — rejected by the mgmt PUT at Stage 0.' }))).annotations({ description: 'How long a virtual display (and, on gamescope\'s bare spawn, the nested session + its game)\nsurvives after the last client session detaches. Serialized as an object tagged on `mode`\n(`{\"mode\":\"off\"}` \/ `{\"mode\":\"duration\",\"seconds\":300}` \/ `{\"mode\":\"forever\"}`) so the web form\nand the OpenAPI schema stay simple.' }), - "layout": S.optional(S.Struct({ - "mode": S.optional(S.Literal('auto-row', 'manual')).annotations({ description: 'How group members are arranged in the desktop coordinate space. Stored at Stage 0; applied from\nthe multi-monitor stage.' }), - "positions": S.optional(S.Record({ key: S.String, value: S.Struct({ - "x": S.Number, - "y": S.Number -}).annotations({ description: 'A desktop-space offset for a display (top-left origin).' }) })) -})).annotations({ description: 'Group layout: the arrangement mode plus, for [`LayoutMode::Manual`], per-slot offsets keyed by\nidentity-slot id (string keys for stable JSON).' }), - "max_displays": S.optional(S.Number.pipe(S.greaterThanOrEqualTo(setDisplayLayoutResponseSettingsMaxDisplaysMin))).annotations({ description: 'Upper bound on simultaneously-live virtual displays (clamped to `1..=16` on write).' }), - "mode_conflict": S.optional(S.Literal('separate', 'steal', 'join', 'reject')).annotations({ description: 'Admission when a \*different\* client connects while a display\/session is already live and asks for\na different mode. Stored at Stage 0; enforced from the mode-conflict admission stage.' }), - "pnp_disable_monitors": S.optional(S.Boolean).annotations({ description: 'EXPERIMENTAL (Windows): DISABLE physical monitors\' PnP device nodes for the stream\'s\nduration (persistently, so a standby monitor\/TV whose hot-plug events re-arrive stays\ndisabled) and re-enable them at teardown. Two selectors: the monitors an `Exclusive`\nisolate deactivated, plus — in ANY topology — external monitors that are connected but not\npart of the desktop (the standby TV that was never active, whose input auto-scan \/\ninstant-on HPD cycling re-probes the link every few seconds). Targets the same\n\"connected-but-dark head\" periodic-stutter class as [`Self::ddc_power_off`], but at the\nWindows-reaction level: a disabled devnode\'s wake events trigger no PnP arrival, no CCD\nre-evaluation, no DWM invalidation. A crash-recovery journal re-enables leftovers on host\nstartup. Orthogonal to `preset` (like `game_session`); `#[serde(default)]` = off.' }), - "preset": S.optional(S.Literal('custom', 'default', 'gaming-rig', 'shared-desktop', 'hotdesk', 'workstation')).annotations({ description: 'A named bundle of the fields below. `Custom` (the default) means the explicit fields rule; any\nother preset ignores the stored fields and expands to its own ([`DisplayPolicy::effective`]).' }), - "topology": S.optional(S.Literal('auto', 'extend', 'primary', 'exclusive')).annotations({ description: 'What the host does to the box\'s display topology while managed virtual displays are up.' }), - "version": S.optional(S.Number.pipe(S.greaterThanOrEqualTo(setDisplayLayoutResponseSettingsVersionMin))).annotations({ description: 'Schema version (currently 1) — lets a future field addition migrate rather than reject.' }) -}).annotations({ description: 'The stored policy (preset + custom fields), or the built-in default when unconfigured.' }) -}).annotations({ description: 'Full display-management state for the console: the stored policy, every preset\'s expansion, the\nresolved effective policy, and which options this build actually enforces yet (Stage 0 wires\nkeep-alive linger + topology; the rest are stored but not yet acted on).' }) - - -/** - * The operator's named field-bundles (`display-presets.json`). These also ride the - * `GET /display/settings` response (`custom_presets`), so the console rarely needs this directly. - * @summary List the saved custom presets - */ -export const listCustomPresetsResponseFieldsKeepAliveTwoSecondsMin = 0; - -export const listCustomPresetsResponseFieldsMaxDisplaysMin = 0; - - - -export const ListCustomPresetsResponseItem = S.Struct({ - "fields": S.Struct({ - "identity": S.Literal('shared', 'per-client', 'per-client-mode').annotations({ description: 'Stable display identity, so desktop environments persist per-display config (KDE scaling). Stored\nat Stage 0; carriers wired from the identity stage.' }), - "keep_alive": S.Union(S.Struct({ - "mode": S.Literal('off') -}).annotations({ description: 'Tear the display down at session end (today\'s default on every backend but Windows, which\nlingers 10 s).' }), S.Struct({ - "mode": S.Literal('duration'), - "seconds": S.Number.pipe(S.greaterThanOrEqualTo(listCustomPresetsResponseFieldsKeepAliveTwoSecondsMin)).annotations({ description: 'Linger window in seconds.' }) -}).annotations({ description: 'Keep the display for `seconds` after the last session leaves, then tear it down; a reconnect\ninside the window reuses it.' }), S.Struct({ - "mode": S.Literal('forever') -}).annotations({ description: 'Keep the display until host shutdown or an explicit release (the `Pinned` lifecycle state).\n\*\*Not honored until the display-lifecycle stage\*\* — rejected by the mgmt PUT at Stage 0.' })).annotations({ description: 'How long a virtual display (and, on gamescope\'s bare spawn, the nested session + its game)\nsurvives after the last client session detaches. Serialized as an object tagged on `mode`\n(`{\"mode\":\"off\"}` \/ `{\"mode\":\"duration\",\"seconds\":300}` \/ `{\"mode\":\"forever\"}`) so the web form\nand the OpenAPI schema stay simple.' }), - "layout": S.Struct({ - "mode": S.optional(S.Literal('auto-row', 'manual')).annotations({ description: 'How group members are arranged in the desktop coordinate space. Stored at Stage 0; applied from\nthe multi-monitor stage.' }), - "positions": S.optional(S.Record({ key: S.String, value: S.Struct({ - "x": S.Number, - "y": S.Number -}).annotations({ description: 'A desktop-space offset for a display (top-left origin).' }) })) -}).annotations({ description: 'Group layout: the arrangement mode plus, for [`LayoutMode::Manual`], per-slot offsets keyed by\nidentity-slot id (string keys for stable JSON).' }), - "max_displays": S.Number.pipe(S.greaterThanOrEqualTo(listCustomPresetsResponseFieldsMaxDisplaysMin)), - "mode_conflict": S.Literal('separate', 'steal', 'join', 'reject').annotations({ description: 'Admission when a \*different\* client connects while a display\/session is already live and asks for\na different mode. Stored at Stage 0; enforced from the mode-conflict admission stage.' }), - "topology": S.Literal('auto', 'extend', 'primary', 'exclusive').annotations({ description: 'What the host does to the box\'s display topology while managed virtual displays are up.' }) -}).annotations({ description: 'The six display-behavior axes this preset applies (the same shape a built-in preset expands to).' }), - "game_session": S.optional(S.Literal('auto', 'dedicated')).annotations({ description: 'The game-session routing this preset applies (orthogonal to the six axes; see [`GameSession`]).\nA custom preset captures the operator\'s \*full\* setup, so — unlike a built-in preset — applying\none does set this axis.' }), - "id": S.String.annotations({ description: 'Host-assigned, stable for the life of the entry (the `{id}` in the CRUD path).' }), - "name": S.String.annotations({ description: 'User-facing name shown on the preset card; editable.' }) -}).annotations({ description: 'A user-defined named preset: a saved bundle of the six display-behavior axes (exactly what a\nbuilt-in [`Preset`] expands to) plus the orthogonal game-session axis, that the operator names\nand applies from the console.\n\nUnlike the built-in [`Preset`]s (a closed enum), custom presets are \*\*data\*\* — a catalog stored in\n`\/display-presets.json`. Applying one writes a `Custom` [`DisplayPolicy`] carrying these\nfields (the console reuses `PUT \/display\/settings`), so [`DisplayPolicy::effective`] stays pure and\nthe built-in set is never touched. The catalog is decoupled from the active `display-settings.json`:\nediting or deleting a preset never mutates the running policy (re-apply to adopt a change).' }) -export const ListCustomPresetsResponse = S.Array(ListCustomPresetsResponseItem) - - -/** - * Stores a named bundle of the display-behavior axes (+ the game-session axis) the operator can - * apply later. The host assigns a stable id, returned in the body. Applying a preset is a - * `PUT /display/settings` with a `Custom` policy carrying its `fields` — no separate apply route. - * @summary Save a custom preset - */ -export const createCustomPresetBodyFieldsKeepAliveTwoSecondsMin = 0; - -export const createCustomPresetBodyFieldsMaxDisplaysMin = 0; - - - -export const CreateCustomPresetBody = S.Struct({ - "fields": S.Struct({ - "identity": S.Literal('shared', 'per-client', 'per-client-mode').annotations({ description: 'Stable display identity, so desktop environments persist per-display config (KDE scaling). Stored\nat Stage 0; carriers wired from the identity stage.' }), - "keep_alive": S.Union(S.Struct({ - "mode": S.Literal('off') -}).annotations({ description: 'Tear the display down at session end (today\'s default on every backend but Windows, which\nlingers 10 s).' }), S.Struct({ - "mode": S.Literal('duration'), - "seconds": S.Number.pipe(S.greaterThanOrEqualTo(createCustomPresetBodyFieldsKeepAliveTwoSecondsMin)).annotations({ description: 'Linger window in seconds.' }) -}).annotations({ description: 'Keep the display for `seconds` after the last session leaves, then tear it down; a reconnect\ninside the window reuses it.' }), S.Struct({ - "mode": S.Literal('forever') -}).annotations({ description: 'Keep the display until host shutdown or an explicit release (the `Pinned` lifecycle state).\n\*\*Not honored until the display-lifecycle stage\*\* — rejected by the mgmt PUT at Stage 0.' })).annotations({ description: 'How long a virtual display (and, on gamescope\'s bare spawn, the nested session + its game)\nsurvives after the last client session detaches. Serialized as an object tagged on `mode`\n(`{\"mode\":\"off\"}` \/ `{\"mode\":\"duration\",\"seconds\":300}` \/ `{\"mode\":\"forever\"}`) so the web form\nand the OpenAPI schema stay simple.' }), - "layout": S.Struct({ - "mode": S.optional(S.Literal('auto-row', 'manual')).annotations({ description: 'How group members are arranged in the desktop coordinate space. Stored at Stage 0; applied from\nthe multi-monitor stage.' }), - "positions": S.optional(S.Record({ key: S.String, value: S.Struct({ - "x": S.Number, - "y": S.Number -}).annotations({ description: 'A desktop-space offset for a display (top-left origin).' }) })) -}).annotations({ description: 'Group layout: the arrangement mode plus, for [`LayoutMode::Manual`], per-slot offsets keyed by\nidentity-slot id (string keys for stable JSON).' }), - "max_displays": S.Number.pipe(S.greaterThanOrEqualTo(createCustomPresetBodyFieldsMaxDisplaysMin)), - "mode_conflict": S.Literal('separate', 'steal', 'join', 'reject').annotations({ description: 'Admission when a \*different\* client connects while a display\/session is already live and asks for\na different mode. Stored at Stage 0; enforced from the mode-conflict admission stage.' }), - "topology": S.Literal('auto', 'extend', 'primary', 'exclusive').annotations({ description: 'What the host does to the box\'s display topology while managed virtual displays are up.' }) -}).annotations({ description: 'The six resolved fields after preset expansion — what the lifecycle\/registry and the Stage-0 call\nsites read, and what the mgmt API echoes as the \"currently in force\" policy. Pure output of\n[`DisplayPolicy::effective`].' }), - "game_session": S.optional(S.Literal('auto', 'dedicated')).annotations({ description: 'How a session that \*\*launches a game\*\* (a library id on the Hello \/ apps.json \/ Decky pin) is\nserved (`design\/gamemode-and-dedicated-sessions.md` §5.2). Orthogonal to the preset\/lifecycle axes\n— a top-level [`DisplayPolicy`] field, NOT part of [`EffectivePolicy`], so a preset never clobbers\nit. Linux-only in effect (a launching Windows session opens into the one desktop).' }), - "name": S.String -}).annotations({ description: 'Request body to create or replace a custom preset (no `id` — the host owns it).' }) - - -/** - * @summary Update a custom preset - */ -export const UpdateCustomPresetParams = S.Struct({ - "id": S.String.annotations({ description: 'The custom preset id' }) -}) - -export const updateCustomPresetBodyFieldsKeepAliveTwoSecondsMin = 0; - -export const updateCustomPresetBodyFieldsMaxDisplaysMin = 0; - - - -export const UpdateCustomPresetBody = S.Struct({ - "fields": S.Struct({ - "identity": S.Literal('shared', 'per-client', 'per-client-mode').annotations({ description: 'Stable display identity, so desktop environments persist per-display config (KDE scaling). Stored\nat Stage 0; carriers wired from the identity stage.' }), - "keep_alive": S.Union(S.Struct({ - "mode": S.Literal('off') -}).annotations({ description: 'Tear the display down at session end (today\'s default on every backend but Windows, which\nlingers 10 s).' }), S.Struct({ - "mode": S.Literal('duration'), - "seconds": S.Number.pipe(S.greaterThanOrEqualTo(updateCustomPresetBodyFieldsKeepAliveTwoSecondsMin)).annotations({ description: 'Linger window in seconds.' }) -}).annotations({ description: 'Keep the display for `seconds` after the last session leaves, then tear it down; a reconnect\ninside the window reuses it.' }), S.Struct({ - "mode": S.Literal('forever') -}).annotations({ description: 'Keep the display until host shutdown or an explicit release (the `Pinned` lifecycle state).\n\*\*Not honored until the display-lifecycle stage\*\* — rejected by the mgmt PUT at Stage 0.' })).annotations({ description: 'How long a virtual display (and, on gamescope\'s bare spawn, the nested session + its game)\nsurvives after the last client session detaches. Serialized as an object tagged on `mode`\n(`{\"mode\":\"off\"}` \/ `{\"mode\":\"duration\",\"seconds\":300}` \/ `{\"mode\":\"forever\"}`) so the web form\nand the OpenAPI schema stay simple.' }), - "layout": S.Struct({ - "mode": S.optional(S.Literal('auto-row', 'manual')).annotations({ description: 'How group members are arranged in the desktop coordinate space. Stored at Stage 0; applied from\nthe multi-monitor stage.' }), - "positions": S.optional(S.Record({ key: S.String, value: S.Struct({ - "x": S.Number, - "y": S.Number -}).annotations({ description: 'A desktop-space offset for a display (top-left origin).' }) })) -}).annotations({ description: 'Group layout: the arrangement mode plus, for [`LayoutMode::Manual`], per-slot offsets keyed by\nidentity-slot id (string keys for stable JSON).' }), - "max_displays": S.Number.pipe(S.greaterThanOrEqualTo(updateCustomPresetBodyFieldsMaxDisplaysMin)), - "mode_conflict": S.Literal('separate', 'steal', 'join', 'reject').annotations({ description: 'Admission when a \*different\* client connects while a display\/session is already live and asks for\na different mode. Stored at Stage 0; enforced from the mode-conflict admission stage.' }), - "topology": S.Literal('auto', 'extend', 'primary', 'exclusive').annotations({ description: 'What the host does to the box\'s display topology while managed virtual displays are up.' }) -}).annotations({ description: 'The six resolved fields after preset expansion — what the lifecycle\/registry and the Stage-0 call\nsites read, and what the mgmt API echoes as the \"currently in force\" policy. Pure output of\n[`DisplayPolicy::effective`].' }), - "game_session": S.optional(S.Literal('auto', 'dedicated')).annotations({ description: 'How a session that \*\*launches a game\*\* (a library id on the Hello \/ apps.json \/ Decky pin) is\nserved (`design\/gamemode-and-dedicated-sessions.md` §5.2). Orthogonal to the preset\/lifecycle axes\n— a top-level [`DisplayPolicy`] field, NOT part of [`EffectivePolicy`], so a preset never clobbers\nit. Linux-only in effect (a launching Windows session opens into the one desktop).' }), - "name": S.String -}).annotations({ description: 'Request body to create or replace a custom preset (no `id` — the host owns it).' }) - -export const updateCustomPresetResponseFieldsKeepAliveTwoSecondsMin = 0; - -export const updateCustomPresetResponseFieldsMaxDisplaysMin = 0; - - - -export const UpdateCustomPresetResponse = S.Struct({ - "fields": S.Struct({ - "identity": S.Literal('shared', 'per-client', 'per-client-mode').annotations({ description: 'Stable display identity, so desktop environments persist per-display config (KDE scaling). Stored\nat Stage 0; carriers wired from the identity stage.' }), - "keep_alive": S.Union(S.Struct({ - "mode": S.Literal('off') -}).annotations({ description: 'Tear the display down at session end (today\'s default on every backend but Windows, which\nlingers 10 s).' }), S.Struct({ - "mode": S.Literal('duration'), - "seconds": S.Number.pipe(S.greaterThanOrEqualTo(updateCustomPresetResponseFieldsKeepAliveTwoSecondsMin)).annotations({ description: 'Linger window in seconds.' }) -}).annotations({ description: 'Keep the display for `seconds` after the last session leaves, then tear it down; a reconnect\ninside the window reuses it.' }), S.Struct({ - "mode": S.Literal('forever') -}).annotations({ description: 'Keep the display until host shutdown or an explicit release (the `Pinned` lifecycle state).\n\*\*Not honored until the display-lifecycle stage\*\* — rejected by the mgmt PUT at Stage 0.' })).annotations({ description: 'How long a virtual display (and, on gamescope\'s bare spawn, the nested session + its game)\nsurvives after the last client session detaches. Serialized as an object tagged on `mode`\n(`{\"mode\":\"off\"}` \/ `{\"mode\":\"duration\",\"seconds\":300}` \/ `{\"mode\":\"forever\"}`) so the web form\nand the OpenAPI schema stay simple.' }), - "layout": S.Struct({ - "mode": S.optional(S.Literal('auto-row', 'manual')).annotations({ description: 'How group members are arranged in the desktop coordinate space. Stored at Stage 0; applied from\nthe multi-monitor stage.' }), - "positions": S.optional(S.Record({ key: S.String, value: S.Struct({ - "x": S.Number, - "y": S.Number -}).annotations({ description: 'A desktop-space offset for a display (top-left origin).' }) })) -}).annotations({ description: 'Group layout: the arrangement mode plus, for [`LayoutMode::Manual`], per-slot offsets keyed by\nidentity-slot id (string keys for stable JSON).' }), - "max_displays": S.Number.pipe(S.greaterThanOrEqualTo(updateCustomPresetResponseFieldsMaxDisplaysMin)), - "mode_conflict": S.Literal('separate', 'steal', 'join', 'reject').annotations({ description: 'Admission when a \*different\* client connects while a display\/session is already live and asks for\na different mode. Stored at Stage 0; enforced from the mode-conflict admission stage.' }), - "topology": S.Literal('auto', 'extend', 'primary', 'exclusive').annotations({ description: 'What the host does to the box\'s display topology while managed virtual displays are up.' }) -}).annotations({ description: 'The six display-behavior axes this preset applies (the same shape a built-in preset expands to).' }), - "game_session": S.optional(S.Literal('auto', 'dedicated')).annotations({ description: 'The game-session routing this preset applies (orthogonal to the six axes; see [`GameSession`]).\nA custom preset captures the operator\'s \*full\* setup, so — unlike a built-in preset — applying\none does set this axis.' }), - "id": S.String.annotations({ description: 'Host-assigned, stable for the life of the entry (the `{id}` in the CRUD path).' }), - "name": S.String.annotations({ description: 'User-facing name shown on the preset card; editable.' }) -}).annotations({ description: 'A user-defined named preset: a saved bundle of the six display-behavior axes (exactly what a\nbuilt-in [`Preset`] expands to) plus the orthogonal game-session axis, that the operator names\nand applies from the console.\n\nUnlike the built-in [`Preset`]s (a closed enum), custom presets are \*\*data\*\* — a catalog stored in\n`\/display-presets.json`. Applying one writes a `Custom` [`DisplayPolicy`] carrying these\nfields (the console reuses `PUT \/display\/settings`), so [`DisplayPolicy::effective`] stays pure and\nthe built-in set is never touched. The catalog is decoupled from the active `display-settings.json`:\nediting or deleting a preset never mutates the running policy (re-apply to adopt a change).' }) - - -/** - * Removes it from the catalog. The active policy is untouched — if this preset was the one applied, - * the running behavior stays exactly as it was (the catalog and `display-settings.json` are decoupled). - * @summary Delete a custom preset - */ -export const DeleteCustomPresetParams = S.Struct({ - "id": S.String.annotations({ description: 'The custom preset id' }) -}) - - -/** - * Tear down lingering/pinned displays now — so a physical-screen user gets their screen back - * without waiting out the linger. `slot` releases one; omit it to release all kept displays. - * Active (streaming) displays are never torn down here (that is session control). - * @summary Release kept virtual displays - */ -export const releaseDisplayBodySlotMin = 0; - - - -export const ReleaseDisplayBody = S.Struct({ - "slot": S.optional(S.NullOr(S.Number.pipe(S.greaterThanOrEqualTo(releaseDisplayBodySlotMin)))).annotations({ description: 'Slot to release (see `state`); omit to release \*\*all\*\* kept displays.' }) -}).annotations({ description: 'Request body for `releaseDisplay`.' }) - -export const releaseDisplayResponseReleasedMin = 0; - - - -export const ReleaseDisplayResponse = S.Struct({ - "released": S.Number.pipe(S.greaterThanOrEqualTo(releaseDisplayResponseReleasedMin)).annotations({ description: 'Number of kept displays torn down.' }) -}).annotations({ description: 'Result of a `\/display\/release`.' }) - - -/** - * The stored virtual-display policy (lifecycle, topology, conflict handling, identity, layout), - * every preset's expansion, and which options this build enforces yet. See - * `design/display-management.md`. - * @summary Display-management policy - */ -export const getDisplaySettingsResponseCustomPresetsItemFieldsKeepAliveTwoSecondsMin = 0; - -export const getDisplaySettingsResponseCustomPresetsItemFieldsMaxDisplaysMin = 0; - -export const getDisplaySettingsResponseEffectiveKeepAliveTwoSecondsMin = 0; - -export const getDisplaySettingsResponseEffectiveMaxDisplaysMin = 0; - -export const getDisplaySettingsResponsePresetsItemFieldsKeepAliveTwoSecondsMin = 0; - -export const getDisplaySettingsResponsePresetsItemFieldsMaxDisplaysMin = 0; - -export const getDisplaySettingsResponseSettingsKeepAliveTwoSecondsMin = 0; - -export const getDisplaySettingsResponseSettingsMaxDisplaysMin = 0; - -export const getDisplaySettingsResponseSettingsVersionMin = 0; - - - -export const GetDisplaySettingsResponse = S.Struct({ - "configured": S.Boolean.annotations({ description: 'True once a `display-settings.json` exists (the console has configured this host).' }), - "custom_presets": S.Array(S.Struct({ - "fields": S.Struct({ - "identity": S.Literal('shared', 'per-client', 'per-client-mode').annotations({ description: 'Stable display identity, so desktop environments persist per-display config (KDE scaling). Stored\nat Stage 0; carriers wired from the identity stage.' }), - "keep_alive": S.Union(S.Struct({ - "mode": S.Literal('off') -}).annotations({ description: 'Tear the display down at session end (today\'s default on every backend but Windows, which\nlingers 10 s).' }), S.Struct({ - "mode": S.Literal('duration'), - "seconds": S.Number.pipe(S.greaterThanOrEqualTo(getDisplaySettingsResponseCustomPresetsItemFieldsKeepAliveTwoSecondsMin)).annotations({ description: 'Linger window in seconds.' }) -}).annotations({ description: 'Keep the display for `seconds` after the last session leaves, then tear it down; a reconnect\ninside the window reuses it.' }), S.Struct({ - "mode": S.Literal('forever') -}).annotations({ description: 'Keep the display until host shutdown or an explicit release (the `Pinned` lifecycle state).\n\*\*Not honored until the display-lifecycle stage\*\* — rejected by the mgmt PUT at Stage 0.' })).annotations({ description: 'How long a virtual display (and, on gamescope\'s bare spawn, the nested session + its game)\nsurvives after the last client session detaches. Serialized as an object tagged on `mode`\n(`{\"mode\":\"off\"}` \/ `{\"mode\":\"duration\",\"seconds\":300}` \/ `{\"mode\":\"forever\"}`) so the web form\nand the OpenAPI schema stay simple.' }), - "layout": S.Struct({ - "mode": S.optional(S.Literal('auto-row', 'manual')).annotations({ description: 'How group members are arranged in the desktop coordinate space. Stored at Stage 0; applied from\nthe multi-monitor stage.' }), - "positions": S.optional(S.Record({ key: S.String, value: S.Struct({ - "x": S.Number, - "y": S.Number -}).annotations({ description: 'A desktop-space offset for a display (top-left origin).' }) })) -}).annotations({ description: 'Group layout: the arrangement mode plus, for [`LayoutMode::Manual`], per-slot offsets keyed by\nidentity-slot id (string keys for stable JSON).' }), - "max_displays": S.Number.pipe(S.greaterThanOrEqualTo(getDisplaySettingsResponseCustomPresetsItemFieldsMaxDisplaysMin)), - "mode_conflict": S.Literal('separate', 'steal', 'join', 'reject').annotations({ description: 'Admission when a \*different\* client connects while a display\/session is already live and asks for\na different mode. Stored at Stage 0; enforced from the mode-conflict admission stage.' }), - "topology": S.Literal('auto', 'extend', 'primary', 'exclusive').annotations({ description: 'What the host does to the box\'s display topology while managed virtual displays are up.' }) -}).annotations({ description: 'The six display-behavior axes this preset applies (the same shape a built-in preset expands to).' }), - "game_session": S.optional(S.Literal('auto', 'dedicated')).annotations({ description: 'The game-session routing this preset applies (orthogonal to the six axes; see [`GameSession`]).\nA custom preset captures the operator\'s \*full\* setup, so — unlike a built-in preset — applying\none does set this axis.' }), - "id": S.String.annotations({ description: 'Host-assigned, stable for the life of the entry (the `{id}` in the CRUD path).' }), - "name": S.String.annotations({ description: 'User-facing name shown on the preset card; editable.' }) -}).annotations({ description: 'A user-defined named preset: a saved bundle of the six display-behavior axes (exactly what a\nbuilt-in [`Preset`] expands to) plus the orthogonal game-session axis, that the operator names\nand applies from the console.\n\nUnlike the built-in [`Preset`]s (a closed enum), custom presets are \*\*data\*\* — a catalog stored in\n`\/display-presets.json`. Applying one writes a `Custom` [`DisplayPolicy`] carrying these\nfields (the console reuses `PUT \/display\/settings`), so [`DisplayPolicy::effective`] stays pure and\nthe built-in set is never touched. The catalog is decoupled from the active `display-settings.json`:\nediting or deleting a preset never mutates the running policy (re-apply to adopt a change).' })).annotations({ description: 'The operator\'s saved custom presets (`display-presets.json`) — named field-bundles rendered\nalongside the built-ins. Managed via `POST\/PUT\/DELETE \/display\/presets`; applied by writing a\n`Custom` policy carrying the preset\'s fields.' }), - "effective": S.Struct({ - "identity": S.Literal('shared', 'per-client', 'per-client-mode').annotations({ description: 'Stable display identity, so desktop environments persist per-display config (KDE scaling). Stored\nat Stage 0; carriers wired from the identity stage.' }), - "keep_alive": S.Union(S.Struct({ - "mode": S.Literal('off') -}).annotations({ description: 'Tear the display down at session end (today\'s default on every backend but Windows, which\nlingers 10 s).' }), S.Struct({ - "mode": S.Literal('duration'), - "seconds": S.Number.pipe(S.greaterThanOrEqualTo(getDisplaySettingsResponseEffectiveKeepAliveTwoSecondsMin)).annotations({ description: 'Linger window in seconds.' }) -}).annotations({ description: 'Keep the display for `seconds` after the last session leaves, then tear it down; a reconnect\ninside the window reuses it.' }), S.Struct({ - "mode": S.Literal('forever') -}).annotations({ description: 'Keep the display until host shutdown or an explicit release (the `Pinned` lifecycle state).\n\*\*Not honored until the display-lifecycle stage\*\* — rejected by the mgmt PUT at Stage 0.' })).annotations({ description: 'How long a virtual display (and, on gamescope\'s bare spawn, the nested session + its game)\nsurvives after the last client session detaches. Serialized as an object tagged on `mode`\n(`{\"mode\":\"off\"}` \/ `{\"mode\":\"duration\",\"seconds\":300}` \/ `{\"mode\":\"forever\"}`) so the web form\nand the OpenAPI schema stay simple.' }), - "layout": S.Struct({ - "mode": S.optional(S.Literal('auto-row', 'manual')).annotations({ description: 'How group members are arranged in the desktop coordinate space. Stored at Stage 0; applied from\nthe multi-monitor stage.' }), - "positions": S.optional(S.Record({ key: S.String, value: S.Struct({ - "x": S.Number, - "y": S.Number -}).annotations({ description: 'A desktop-space offset for a display (top-left origin).' }) })) -}).annotations({ description: 'Group layout: the arrangement mode plus, for [`LayoutMode::Manual`], per-slot offsets keyed by\nidentity-slot id (string keys for stable JSON).' }), - "max_displays": S.Number.pipe(S.greaterThanOrEqualTo(getDisplaySettingsResponseEffectiveMaxDisplaysMin)), - "mode_conflict": S.Literal('separate', 'steal', 'join', 'reject').annotations({ description: 'Admission when a \*different\* client connects while a display\/session is already live and asks for\na different mode. Stored at Stage 0; enforced from the mode-conflict admission stage.' }), - "topology": S.Literal('auto', 'extend', 'primary', 'exclusive').annotations({ description: 'What the host does to the box\'s display topology while managed virtual displays are up.' }) -}).annotations({ description: 'The effective (preset-expanded) policy currently in force.' }), - "enforced": S.Array(S.String).annotations({ description: 'Option names this build enforces right now. All five axes are now acted on (keep_alive +\ntopology since Stage 0-2, identity Stage 3, mode_conflict Stage 4, layout Stage 5) — the console\nreads this to know which controls are live vs. \"coming soon\" (per-backend nuance, e.g. layout\nposition apply being KWin-only, is reported per display in `\/display\/state`).' }), - "presets": S.Array(S.Struct({ - "fields": S.Struct({ - "identity": S.Literal('shared', 'per-client', 'per-client-mode').annotations({ description: 'Stable display identity, so desktop environments persist per-display config (KDE scaling). Stored\nat Stage 0; carriers wired from the identity stage.' }), - "keep_alive": S.Union(S.Struct({ - "mode": S.Literal('off') -}).annotations({ description: 'Tear the display down at session end (today\'s default on every backend but Windows, which\nlingers 10 s).' }), S.Struct({ - "mode": S.Literal('duration'), - "seconds": S.Number.pipe(S.greaterThanOrEqualTo(getDisplaySettingsResponsePresetsItemFieldsKeepAliveTwoSecondsMin)).annotations({ description: 'Linger window in seconds.' }) -}).annotations({ description: 'Keep the display for `seconds` after the last session leaves, then tear it down; a reconnect\ninside the window reuses it.' }), S.Struct({ - "mode": S.Literal('forever') -}).annotations({ description: 'Keep the display until host shutdown or an explicit release (the `Pinned` lifecycle state).\n\*\*Not honored until the display-lifecycle stage\*\* — rejected by the mgmt PUT at Stage 0.' })).annotations({ description: 'How long a virtual display (and, on gamescope\'s bare spawn, the nested session + its game)\nsurvives after the last client session detaches. Serialized as an object tagged on `mode`\n(`{\"mode\":\"off\"}` \/ `{\"mode\":\"duration\",\"seconds\":300}` \/ `{\"mode\":\"forever\"}`) so the web form\nand the OpenAPI schema stay simple.' }), - "layout": S.Struct({ - "mode": S.optional(S.Literal('auto-row', 'manual')).annotations({ description: 'How group members are arranged in the desktop coordinate space. Stored at Stage 0; applied from\nthe multi-monitor stage.' }), - "positions": S.optional(S.Record({ key: S.String, value: S.Struct({ - "x": S.Number, - "y": S.Number -}).annotations({ description: 'A desktop-space offset for a display (top-left origin).' }) })) -}).annotations({ description: 'Group layout: the arrangement mode plus, for [`LayoutMode::Manual`], per-slot offsets keyed by\nidentity-slot id (string keys for stable JSON).' }), - "max_displays": S.Number.pipe(S.greaterThanOrEqualTo(getDisplaySettingsResponsePresetsItemFieldsMaxDisplaysMin)), - "mode_conflict": S.Literal('separate', 'steal', 'join', 'reject').annotations({ description: 'Admission when a \*different\* client connects while a display\/session is already live and asks for\na different mode. Stored at Stage 0; enforced from the mode-conflict admission stage.' }), - "topology": S.Literal('auto', 'extend', 'primary', 'exclusive').annotations({ description: 'What the host does to the box\'s display topology while managed virtual displays are up.' }) -}).annotations({ description: 'The effective policy this preset expands to (the same fields a `custom` policy carries).' }), - "id": S.String.annotations({ description: 'The preset id (`default` | `gaming-rig` | `shared-desktop` | `hotdesk` | `workstation`).' }), - "summary": S.String.annotations({ description: 'One-line story shown next to the option.' }) -}).annotations({ description: 'One preset\'s human-facing description + the fields it expands to, so the console can render a\npreset picker with an accurate \"what this does\" preview without hardcoding the expansion.' })).annotations({ description: 'Every named preset and what it expands to (for the picker\'s preview).' }), - "settings": S.Struct({ - "ddc_power_off": S.optional(S.Boolean).annotations({ description: 'EXPERIMENTAL (Windows): command physical monitors\' panels off over DDC\/CI (VCP 0xD6 →\nDPMS off) right before an `Exclusive` isolate deactivates them, and back on at restore.\nTargets the \"connected-but-dark head\" periodic-stutter class (monitor standby\nauto-input-scan \/ DP link churn while the virtual display is the sole active display) at\nthe monitor-firmware level. Best-effort — monitors without DDC\/CI (or with it disabled in\nthe OSD) are skipped. Orthogonal to `preset` (like `game_session`): preserved across\npreset changes; `#[serde(default)]` = off so existing `display-settings.json` files are\nuntouched.' }), - "game_session": S.optional(S.Literal('auto', 'dedicated')).annotations({ description: 'How a game-launching session is served (`design\/gamemode-and-dedicated-sessions.md` §5.2).\nOrthogonal to `preset`\/lifecycle — preserved across preset changes; `#[serde(default)]` = `Auto`\nso existing `display-settings.json` files are untouched.' }), - "identity": S.optional(S.Literal('shared', 'per-client', 'per-client-mode')).annotations({ description: 'Stable display identity, so desktop environments persist per-display config (KDE scaling). Stored\nat Stage 0; carriers wired from the identity stage.' }), - "keep_alive": S.optional(S.Union(S.Struct({ - "mode": S.Literal('off') -}).annotations({ description: 'Tear the display down at session end (today\'s default on every backend but Windows, which\nlingers 10 s).' }), S.Struct({ - "mode": S.Literal('duration'), - "seconds": S.Number.pipe(S.greaterThanOrEqualTo(getDisplaySettingsResponseSettingsKeepAliveTwoSecondsMin)).annotations({ description: 'Linger window in seconds.' }) -}).annotations({ description: 'Keep the display for `seconds` after the last session leaves, then tear it down; a reconnect\ninside the window reuses it.' }), S.Struct({ - "mode": S.Literal('forever') -}).annotations({ description: 'Keep the display until host shutdown or an explicit release (the `Pinned` lifecycle state).\n\*\*Not honored until the display-lifecycle stage\*\* — rejected by the mgmt PUT at Stage 0.' }))).annotations({ description: 'How long a virtual display (and, on gamescope\'s bare spawn, the nested session + its game)\nsurvives after the last client session detaches. Serialized as an object tagged on `mode`\n(`{\"mode\":\"off\"}` \/ `{\"mode\":\"duration\",\"seconds\":300}` \/ `{\"mode\":\"forever\"}`) so the web form\nand the OpenAPI schema stay simple.' }), - "layout": S.optional(S.Struct({ - "mode": S.optional(S.Literal('auto-row', 'manual')).annotations({ description: 'How group members are arranged in the desktop coordinate space. Stored at Stage 0; applied from\nthe multi-monitor stage.' }), - "positions": S.optional(S.Record({ key: S.String, value: S.Struct({ - "x": S.Number, - "y": S.Number -}).annotations({ description: 'A desktop-space offset for a display (top-left origin).' }) })) -})).annotations({ description: 'Group layout: the arrangement mode plus, for [`LayoutMode::Manual`], per-slot offsets keyed by\nidentity-slot id (string keys for stable JSON).' }), - "max_displays": S.optional(S.Number.pipe(S.greaterThanOrEqualTo(getDisplaySettingsResponseSettingsMaxDisplaysMin))).annotations({ description: 'Upper bound on simultaneously-live virtual displays (clamped to `1..=16` on write).' }), - "mode_conflict": S.optional(S.Literal('separate', 'steal', 'join', 'reject')).annotations({ description: 'Admission when a \*different\* client connects while a display\/session is already live and asks for\na different mode. Stored at Stage 0; enforced from the mode-conflict admission stage.' }), - "pnp_disable_monitors": S.optional(S.Boolean).annotations({ description: 'EXPERIMENTAL (Windows): DISABLE physical monitors\' PnP device nodes for the stream\'s\nduration (persistently, so a standby monitor\/TV whose hot-plug events re-arrive stays\ndisabled) and re-enable them at teardown. Two selectors: the monitors an `Exclusive`\nisolate deactivated, plus — in ANY topology — external monitors that are connected but not\npart of the desktop (the standby TV that was never active, whose input auto-scan \/\ninstant-on HPD cycling re-probes the link every few seconds). Targets the same\n\"connected-but-dark head\" periodic-stutter class as [`Self::ddc_power_off`], but at the\nWindows-reaction level: a disabled devnode\'s wake events trigger no PnP arrival, no CCD\nre-evaluation, no DWM invalidation. A crash-recovery journal re-enables leftovers on host\nstartup. Orthogonal to `preset` (like `game_session`); `#[serde(default)]` = off.' }), - "preset": S.optional(S.Literal('custom', 'default', 'gaming-rig', 'shared-desktop', 'hotdesk', 'workstation')).annotations({ description: 'A named bundle of the fields below. `Custom` (the default) means the explicit fields rule; any\nother preset ignores the stored fields and expands to its own ([`DisplayPolicy::effective`]).' }), - "topology": S.optional(S.Literal('auto', 'extend', 'primary', 'exclusive')).annotations({ description: 'What the host does to the box\'s display topology while managed virtual displays are up.' }), - "version": S.optional(S.Number.pipe(S.greaterThanOrEqualTo(getDisplaySettingsResponseSettingsVersionMin))).annotations({ description: 'Schema version (currently 1) — lets a future field addition migrate rather than reject.' }) -}).annotations({ description: 'The stored policy (preset + custom fields), or the built-in default when unconfigured.' }) -}).annotations({ description: 'Full display-management state for the console: the stored policy, every preset\'s expansion, the\nresolved effective policy, and which options this build actually enforces yet (Stage 0 wires\nkeep-alive linger + topology; the rest are stored but not yet acted on).' }) - - -/** - * Persists a new policy (validated + clamped) and applies it from the next connect/teardown — a - * running session keeps the display it opened on. `keep_alive: forever` (the gaming-rig preset) is - * honored (the display is Pinned; free it via `POST /display/release`). - * @summary Set the display-management policy - */ -export const setDisplaySettingsBodyKeepAliveTwoSecondsMin = 0; - -export const setDisplaySettingsBodyMaxDisplaysMin = 0; - -export const setDisplaySettingsBodyVersionMin = 0; - - - -export const SetDisplaySettingsBody = S.Struct({ - "ddc_power_off": S.optional(S.Boolean).annotations({ description: 'EXPERIMENTAL (Windows): command physical monitors\' panels off over DDC\/CI (VCP 0xD6 →\nDPMS off) right before an `Exclusive` isolate deactivates them, and back on at restore.\nTargets the \"connected-but-dark head\" periodic-stutter class (monitor standby\nauto-input-scan \/ DP link churn while the virtual display is the sole active display) at\nthe monitor-firmware level. Best-effort — monitors without DDC\/CI (or with it disabled in\nthe OSD) are skipped. Orthogonal to `preset` (like `game_session`): preserved across\npreset changes; `#[serde(default)]` = off so existing `display-settings.json` files are\nuntouched.' }), - "game_session": S.optional(S.Literal('auto', 'dedicated')).annotations({ description: 'How a game-launching session is served (`design\/gamemode-and-dedicated-sessions.md` §5.2).\nOrthogonal to `preset`\/lifecycle — preserved across preset changes; `#[serde(default)]` = `Auto`\nso existing `display-settings.json` files are untouched.' }), - "identity": S.optional(S.Literal('shared', 'per-client', 'per-client-mode')).annotations({ description: 'Stable display identity, so desktop environments persist per-display config (KDE scaling). Stored\nat Stage 0; carriers wired from the identity stage.' }), - "keep_alive": S.optional(S.Union(S.Struct({ - "mode": S.Literal('off') -}).annotations({ description: 'Tear the display down at session end (today\'s default on every backend but Windows, which\nlingers 10 s).' }), S.Struct({ - "mode": S.Literal('duration'), - "seconds": S.Number.pipe(S.greaterThanOrEqualTo(setDisplaySettingsBodyKeepAliveTwoSecondsMin)).annotations({ description: 'Linger window in seconds.' }) -}).annotations({ description: 'Keep the display for `seconds` after the last session leaves, then tear it down; a reconnect\ninside the window reuses it.' }), S.Struct({ - "mode": S.Literal('forever') -}).annotations({ description: 'Keep the display until host shutdown or an explicit release (the `Pinned` lifecycle state).\n\*\*Not honored until the display-lifecycle stage\*\* — rejected by the mgmt PUT at Stage 0.' }))).annotations({ description: 'How long a virtual display (and, on gamescope\'s bare spawn, the nested session + its game)\nsurvives after the last client session detaches. Serialized as an object tagged on `mode`\n(`{\"mode\":\"off\"}` \/ `{\"mode\":\"duration\",\"seconds\":300}` \/ `{\"mode\":\"forever\"}`) so the web form\nand the OpenAPI schema stay simple.' }), - "layout": S.optional(S.Struct({ - "mode": S.optional(S.Literal('auto-row', 'manual')).annotations({ description: 'How group members are arranged in the desktop coordinate space. Stored at Stage 0; applied from\nthe multi-monitor stage.' }), - "positions": S.optional(S.Record({ key: S.String, value: S.Struct({ - "x": S.Number, - "y": S.Number -}).annotations({ description: 'A desktop-space offset for a display (top-left origin).' }) })) -})).annotations({ description: 'Group layout: the arrangement mode plus, for [`LayoutMode::Manual`], per-slot offsets keyed by\nidentity-slot id (string keys for stable JSON).' }), - "max_displays": S.optional(S.Number.pipe(S.greaterThanOrEqualTo(setDisplaySettingsBodyMaxDisplaysMin))).annotations({ description: 'Upper bound on simultaneously-live virtual displays (clamped to `1..=16` on write).' }), - "mode_conflict": S.optional(S.Literal('separate', 'steal', 'join', 'reject')).annotations({ description: 'Admission when a \*different\* client connects while a display\/session is already live and asks for\na different mode. Stored at Stage 0; enforced from the mode-conflict admission stage.' }), - "pnp_disable_monitors": S.optional(S.Boolean).annotations({ description: 'EXPERIMENTAL (Windows): DISABLE physical monitors\' PnP device nodes for the stream\'s\nduration (persistently, so a standby monitor\/TV whose hot-plug events re-arrive stays\ndisabled) and re-enable them at teardown. Two selectors: the monitors an `Exclusive`\nisolate deactivated, plus — in ANY topology — external monitors that are connected but not\npart of the desktop (the standby TV that was never active, whose input auto-scan \/\ninstant-on HPD cycling re-probes the link every few seconds). Targets the same\n\"connected-but-dark head\" periodic-stutter class as [`Self::ddc_power_off`], but at the\nWindows-reaction level: a disabled devnode\'s wake events trigger no PnP arrival, no CCD\nre-evaluation, no DWM invalidation. A crash-recovery journal re-enables leftovers on host\nstartup. Orthogonal to `preset` (like `game_session`); `#[serde(default)]` = off.' }), - "preset": S.optional(S.Literal('custom', 'default', 'gaming-rig', 'shared-desktop', 'hotdesk', 'workstation')).annotations({ description: 'A named bundle of the fields below. `Custom` (the default) means the explicit fields rule; any\nother preset ignores the stored fields and expands to its own ([`DisplayPolicy::effective`]).' }), - "topology": S.optional(S.Literal('auto', 'extend', 'primary', 'exclusive')).annotations({ description: 'What the host does to the box\'s display topology while managed virtual displays are up.' }), - "version": S.optional(S.Number.pipe(S.greaterThanOrEqualTo(setDisplaySettingsBodyVersionMin))).annotations({ description: 'Schema version (currently 1) — lets a future field addition migrate rather than reject.' }) -}).annotations({ description: 'The user-facing display-management policy — what `display-settings.json` holds and what the mgmt\nAPI GETs\/PUTs. When [`preset`](Self::preset) is not [`Preset::Custom`] the explicit fields are\nignored (the console writes one or the other); [`effective`](Self::effective) resolves both to a\nsingle [`EffectivePolicy`].' }) - -export const setDisplaySettingsResponseCustomPresetsItemFieldsKeepAliveTwoSecondsMin = 0; - -export const setDisplaySettingsResponseCustomPresetsItemFieldsMaxDisplaysMin = 0; - -export const setDisplaySettingsResponseEffectiveKeepAliveTwoSecondsMin = 0; - -export const setDisplaySettingsResponseEffectiveMaxDisplaysMin = 0; - -export const setDisplaySettingsResponsePresetsItemFieldsKeepAliveTwoSecondsMin = 0; - -export const setDisplaySettingsResponsePresetsItemFieldsMaxDisplaysMin = 0; - -export const setDisplaySettingsResponseSettingsKeepAliveTwoSecondsMin = 0; - -export const setDisplaySettingsResponseSettingsMaxDisplaysMin = 0; - -export const setDisplaySettingsResponseSettingsVersionMin = 0; - - - -export const SetDisplaySettingsResponse = S.Struct({ - "configured": S.Boolean.annotations({ description: 'True once a `display-settings.json` exists (the console has configured this host).' }), - "custom_presets": S.Array(S.Struct({ - "fields": S.Struct({ - "identity": S.Literal('shared', 'per-client', 'per-client-mode').annotations({ description: 'Stable display identity, so desktop environments persist per-display config (KDE scaling). Stored\nat Stage 0; carriers wired from the identity stage.' }), - "keep_alive": S.Union(S.Struct({ - "mode": S.Literal('off') -}).annotations({ description: 'Tear the display down at session end (today\'s default on every backend but Windows, which\nlingers 10 s).' }), S.Struct({ - "mode": S.Literal('duration'), - "seconds": S.Number.pipe(S.greaterThanOrEqualTo(setDisplaySettingsResponseCustomPresetsItemFieldsKeepAliveTwoSecondsMin)).annotations({ description: 'Linger window in seconds.' }) -}).annotations({ description: 'Keep the display for `seconds` after the last session leaves, then tear it down; a reconnect\ninside the window reuses it.' }), S.Struct({ - "mode": S.Literal('forever') -}).annotations({ description: 'Keep the display until host shutdown or an explicit release (the `Pinned` lifecycle state).\n\*\*Not honored until the display-lifecycle stage\*\* — rejected by the mgmt PUT at Stage 0.' })).annotations({ description: 'How long a virtual display (and, on gamescope\'s bare spawn, the nested session + its game)\nsurvives after the last client session detaches. Serialized as an object tagged on `mode`\n(`{\"mode\":\"off\"}` \/ `{\"mode\":\"duration\",\"seconds\":300}` \/ `{\"mode\":\"forever\"}`) so the web form\nand the OpenAPI schema stay simple.' }), - "layout": S.Struct({ - "mode": S.optional(S.Literal('auto-row', 'manual')).annotations({ description: 'How group members are arranged in the desktop coordinate space. Stored at Stage 0; applied from\nthe multi-monitor stage.' }), - "positions": S.optional(S.Record({ key: S.String, value: S.Struct({ - "x": S.Number, - "y": S.Number -}).annotations({ description: 'A desktop-space offset for a display (top-left origin).' }) })) -}).annotations({ description: 'Group layout: the arrangement mode plus, for [`LayoutMode::Manual`], per-slot offsets keyed by\nidentity-slot id (string keys for stable JSON).' }), - "max_displays": S.Number.pipe(S.greaterThanOrEqualTo(setDisplaySettingsResponseCustomPresetsItemFieldsMaxDisplaysMin)), - "mode_conflict": S.Literal('separate', 'steal', 'join', 'reject').annotations({ description: 'Admission when a \*different\* client connects while a display\/session is already live and asks for\na different mode. Stored at Stage 0; enforced from the mode-conflict admission stage.' }), - "topology": S.Literal('auto', 'extend', 'primary', 'exclusive').annotations({ description: 'What the host does to the box\'s display topology while managed virtual displays are up.' }) -}).annotations({ description: 'The six display-behavior axes this preset applies (the same shape a built-in preset expands to).' }), - "game_session": S.optional(S.Literal('auto', 'dedicated')).annotations({ description: 'The game-session routing this preset applies (orthogonal to the six axes; see [`GameSession`]).\nA custom preset captures the operator\'s \*full\* setup, so — unlike a built-in preset — applying\none does set this axis.' }), - "id": S.String.annotations({ description: 'Host-assigned, stable for the life of the entry (the `{id}` in the CRUD path).' }), - "name": S.String.annotations({ description: 'User-facing name shown on the preset card; editable.' }) -}).annotations({ description: 'A user-defined named preset: a saved bundle of the six display-behavior axes (exactly what a\nbuilt-in [`Preset`] expands to) plus the orthogonal game-session axis, that the operator names\nand applies from the console.\n\nUnlike the built-in [`Preset`]s (a closed enum), custom presets are \*\*data\*\* — a catalog stored in\n`\/display-presets.json`. Applying one writes a `Custom` [`DisplayPolicy`] carrying these\nfields (the console reuses `PUT \/display\/settings`), so [`DisplayPolicy::effective`] stays pure and\nthe built-in set is never touched. The catalog is decoupled from the active `display-settings.json`:\nediting or deleting a preset never mutates the running policy (re-apply to adopt a change).' })).annotations({ description: 'The operator\'s saved custom presets (`display-presets.json`) — named field-bundles rendered\nalongside the built-ins. Managed via `POST\/PUT\/DELETE \/display\/presets`; applied by writing a\n`Custom` policy carrying the preset\'s fields.' }), - "effective": S.Struct({ - "identity": S.Literal('shared', 'per-client', 'per-client-mode').annotations({ description: 'Stable display identity, so desktop environments persist per-display config (KDE scaling). Stored\nat Stage 0; carriers wired from the identity stage.' }), - "keep_alive": S.Union(S.Struct({ - "mode": S.Literal('off') -}).annotations({ description: 'Tear the display down at session end (today\'s default on every backend but Windows, which\nlingers 10 s).' }), S.Struct({ - "mode": S.Literal('duration'), - "seconds": S.Number.pipe(S.greaterThanOrEqualTo(setDisplaySettingsResponseEffectiveKeepAliveTwoSecondsMin)).annotations({ description: 'Linger window in seconds.' }) -}).annotations({ description: 'Keep the display for `seconds` after the last session leaves, then tear it down; a reconnect\ninside the window reuses it.' }), S.Struct({ - "mode": S.Literal('forever') -}).annotations({ description: 'Keep the display until host shutdown or an explicit release (the `Pinned` lifecycle state).\n\*\*Not honored until the display-lifecycle stage\*\* — rejected by the mgmt PUT at Stage 0.' })).annotations({ description: 'How long a virtual display (and, on gamescope\'s bare spawn, the nested session + its game)\nsurvives after the last client session detaches. Serialized as an object tagged on `mode`\n(`{\"mode\":\"off\"}` \/ `{\"mode\":\"duration\",\"seconds\":300}` \/ `{\"mode\":\"forever\"}`) so the web form\nand the OpenAPI schema stay simple.' }), - "layout": S.Struct({ - "mode": S.optional(S.Literal('auto-row', 'manual')).annotations({ description: 'How group members are arranged in the desktop coordinate space. Stored at Stage 0; applied from\nthe multi-monitor stage.' }), - "positions": S.optional(S.Record({ key: S.String, value: S.Struct({ - "x": S.Number, - "y": S.Number -}).annotations({ description: 'A desktop-space offset for a display (top-left origin).' }) })) -}).annotations({ description: 'Group layout: the arrangement mode plus, for [`LayoutMode::Manual`], per-slot offsets keyed by\nidentity-slot id (string keys for stable JSON).' }), - "max_displays": S.Number.pipe(S.greaterThanOrEqualTo(setDisplaySettingsResponseEffectiveMaxDisplaysMin)), - "mode_conflict": S.Literal('separate', 'steal', 'join', 'reject').annotations({ description: 'Admission when a \*different\* client connects while a display\/session is already live and asks for\na different mode. Stored at Stage 0; enforced from the mode-conflict admission stage.' }), - "topology": S.Literal('auto', 'extend', 'primary', 'exclusive').annotations({ description: 'What the host does to the box\'s display topology while managed virtual displays are up.' }) -}).annotations({ description: 'The effective (preset-expanded) policy currently in force.' }), - "enforced": S.Array(S.String).annotations({ description: 'Option names this build enforces right now. All five axes are now acted on (keep_alive +\ntopology since Stage 0-2, identity Stage 3, mode_conflict Stage 4, layout Stage 5) — the console\nreads this to know which controls are live vs. \"coming soon\" (per-backend nuance, e.g. layout\nposition apply being KWin-only, is reported per display in `\/display\/state`).' }), - "presets": S.Array(S.Struct({ - "fields": S.Struct({ - "identity": S.Literal('shared', 'per-client', 'per-client-mode').annotations({ description: 'Stable display identity, so desktop environments persist per-display config (KDE scaling). Stored\nat Stage 0; carriers wired from the identity stage.' }), - "keep_alive": S.Union(S.Struct({ - "mode": S.Literal('off') -}).annotations({ description: 'Tear the display down at session end (today\'s default on every backend but Windows, which\nlingers 10 s).' }), S.Struct({ - "mode": S.Literal('duration'), - "seconds": S.Number.pipe(S.greaterThanOrEqualTo(setDisplaySettingsResponsePresetsItemFieldsKeepAliveTwoSecondsMin)).annotations({ description: 'Linger window in seconds.' }) -}).annotations({ description: 'Keep the display for `seconds` after the last session leaves, then tear it down; a reconnect\ninside the window reuses it.' }), S.Struct({ - "mode": S.Literal('forever') -}).annotations({ description: 'Keep the display until host shutdown or an explicit release (the `Pinned` lifecycle state).\n\*\*Not honored until the display-lifecycle stage\*\* — rejected by the mgmt PUT at Stage 0.' })).annotations({ description: 'How long a virtual display (and, on gamescope\'s bare spawn, the nested session + its game)\nsurvives after the last client session detaches. Serialized as an object tagged on `mode`\n(`{\"mode\":\"off\"}` \/ `{\"mode\":\"duration\",\"seconds\":300}` \/ `{\"mode\":\"forever\"}`) so the web form\nand the OpenAPI schema stay simple.' }), - "layout": S.Struct({ - "mode": S.optional(S.Literal('auto-row', 'manual')).annotations({ description: 'How group members are arranged in the desktop coordinate space. Stored at Stage 0; applied from\nthe multi-monitor stage.' }), - "positions": S.optional(S.Record({ key: S.String, value: S.Struct({ - "x": S.Number, - "y": S.Number -}).annotations({ description: 'A desktop-space offset for a display (top-left origin).' }) })) -}).annotations({ description: 'Group layout: the arrangement mode plus, for [`LayoutMode::Manual`], per-slot offsets keyed by\nidentity-slot id (string keys for stable JSON).' }), - "max_displays": S.Number.pipe(S.greaterThanOrEqualTo(setDisplaySettingsResponsePresetsItemFieldsMaxDisplaysMin)), - "mode_conflict": S.Literal('separate', 'steal', 'join', 'reject').annotations({ description: 'Admission when a \*different\* client connects while a display\/session is already live and asks for\na different mode. Stored at Stage 0; enforced from the mode-conflict admission stage.' }), - "topology": S.Literal('auto', 'extend', 'primary', 'exclusive').annotations({ description: 'What the host does to the box\'s display topology while managed virtual displays are up.' }) -}).annotations({ description: 'The effective policy this preset expands to (the same fields a `custom` policy carries).' }), - "id": S.String.annotations({ description: 'The preset id (`default` | `gaming-rig` | `shared-desktop` | `hotdesk` | `workstation`).' }), - "summary": S.String.annotations({ description: 'One-line story shown next to the option.' }) -}).annotations({ description: 'One preset\'s human-facing description + the fields it expands to, so the console can render a\npreset picker with an accurate \"what this does\" preview without hardcoding the expansion.' })).annotations({ description: 'Every named preset and what it expands to (for the picker\'s preview).' }), - "settings": S.Struct({ - "ddc_power_off": S.optional(S.Boolean).annotations({ description: 'EXPERIMENTAL (Windows): command physical monitors\' panels off over DDC\/CI (VCP 0xD6 →\nDPMS off) right before an `Exclusive` isolate deactivates them, and back on at restore.\nTargets the \"connected-but-dark head\" periodic-stutter class (monitor standby\nauto-input-scan \/ DP link churn while the virtual display is the sole active display) at\nthe monitor-firmware level. Best-effort — monitors without DDC\/CI (or with it disabled in\nthe OSD) are skipped. Orthogonal to `preset` (like `game_session`): preserved across\npreset changes; `#[serde(default)]` = off so existing `display-settings.json` files are\nuntouched.' }), - "game_session": S.optional(S.Literal('auto', 'dedicated')).annotations({ description: 'How a game-launching session is served (`design\/gamemode-and-dedicated-sessions.md` §5.2).\nOrthogonal to `preset`\/lifecycle — preserved across preset changes; `#[serde(default)]` = `Auto`\nso existing `display-settings.json` files are untouched.' }), - "identity": S.optional(S.Literal('shared', 'per-client', 'per-client-mode')).annotations({ description: 'Stable display identity, so desktop environments persist per-display config (KDE scaling). Stored\nat Stage 0; carriers wired from the identity stage.' }), - "keep_alive": S.optional(S.Union(S.Struct({ - "mode": S.Literal('off') -}).annotations({ description: 'Tear the display down at session end (today\'s default on every backend but Windows, which\nlingers 10 s).' }), S.Struct({ - "mode": S.Literal('duration'), - "seconds": S.Number.pipe(S.greaterThanOrEqualTo(setDisplaySettingsResponseSettingsKeepAliveTwoSecondsMin)).annotations({ description: 'Linger window in seconds.' }) -}).annotations({ description: 'Keep the display for `seconds` after the last session leaves, then tear it down; a reconnect\ninside the window reuses it.' }), S.Struct({ - "mode": S.Literal('forever') -}).annotations({ description: 'Keep the display until host shutdown or an explicit release (the `Pinned` lifecycle state).\n\*\*Not honored until the display-lifecycle stage\*\* — rejected by the mgmt PUT at Stage 0.' }))).annotations({ description: 'How long a virtual display (and, on gamescope\'s bare spawn, the nested session + its game)\nsurvives after the last client session detaches. Serialized as an object tagged on `mode`\n(`{\"mode\":\"off\"}` \/ `{\"mode\":\"duration\",\"seconds\":300}` \/ `{\"mode\":\"forever\"}`) so the web form\nand the OpenAPI schema stay simple.' }), - "layout": S.optional(S.Struct({ - "mode": S.optional(S.Literal('auto-row', 'manual')).annotations({ description: 'How group members are arranged in the desktop coordinate space. Stored at Stage 0; applied from\nthe multi-monitor stage.' }), - "positions": S.optional(S.Record({ key: S.String, value: S.Struct({ - "x": S.Number, - "y": S.Number -}).annotations({ description: 'A desktop-space offset for a display (top-left origin).' }) })) -})).annotations({ description: 'Group layout: the arrangement mode plus, for [`LayoutMode::Manual`], per-slot offsets keyed by\nidentity-slot id (string keys for stable JSON).' }), - "max_displays": S.optional(S.Number.pipe(S.greaterThanOrEqualTo(setDisplaySettingsResponseSettingsMaxDisplaysMin))).annotations({ description: 'Upper bound on simultaneously-live virtual displays (clamped to `1..=16` on write).' }), - "mode_conflict": S.optional(S.Literal('separate', 'steal', 'join', 'reject')).annotations({ description: 'Admission when a \*different\* client connects while a display\/session is already live and asks for\na different mode. Stored at Stage 0; enforced from the mode-conflict admission stage.' }), - "pnp_disable_monitors": S.optional(S.Boolean).annotations({ description: 'EXPERIMENTAL (Windows): DISABLE physical monitors\' PnP device nodes for the stream\'s\nduration (persistently, so a standby monitor\/TV whose hot-plug events re-arrive stays\ndisabled) and re-enable them at teardown. Two selectors: the monitors an `Exclusive`\nisolate deactivated, plus — in ANY topology — external monitors that are connected but not\npart of the desktop (the standby TV that was never active, whose input auto-scan \/\ninstant-on HPD cycling re-probes the link every few seconds). Targets the same\n\"connected-but-dark head\" periodic-stutter class as [`Self::ddc_power_off`], but at the\nWindows-reaction level: a disabled devnode\'s wake events trigger no PnP arrival, no CCD\nre-evaluation, no DWM invalidation. A crash-recovery journal re-enables leftovers on host\nstartup. Orthogonal to `preset` (like `game_session`); `#[serde(default)]` = off.' }), - "preset": S.optional(S.Literal('custom', 'default', 'gaming-rig', 'shared-desktop', 'hotdesk', 'workstation')).annotations({ description: 'A named bundle of the fields below. `Custom` (the default) means the explicit fields rule; any\nother preset ignores the stored fields and expands to its own ([`DisplayPolicy::effective`]).' }), - "topology": S.optional(S.Literal('auto', 'extend', 'primary', 'exclusive')).annotations({ description: 'What the host does to the box\'s display topology while managed virtual displays are up.' }), - "version": S.optional(S.Number.pipe(S.greaterThanOrEqualTo(setDisplaySettingsResponseSettingsVersionMin))).annotations({ description: 'Schema version (currently 1) — lets a future field addition migrate rather than reject.' }) -}).annotations({ description: 'The stored policy (preset + custom fields), or the built-in default when unconfigured.' }) -}).annotations({ description: 'Full display-management state for the console: the stored policy, every preset\'s expansion, the\nresolved effective policy, and which options this build actually enforces yet (Stage 0 wires\nkeep-alive linger + topology; the rest are stored but not yet acted on).' }) - - -/** - * The host's managed virtual displays right now — active (streaming), lingering (kept after - * disconnect, counting down to teardown), or pinned (kept indefinitely). See - * `design/display-management.md`. - * @summary Live virtual displays - */ -export const getDisplayStateResponseDisplaysItemDisplayIndexMin = 0; - -export const getDisplayStateResponseDisplaysItemExpiresInMsMin = 0; - -export const getDisplayStateResponseDisplaysItemGroupMin = 0; - -export const getDisplayStateResponseDisplaysItemIdentitySlotMin = 0; - -export const getDisplayStateResponseDisplaysItemSessionsMin = 0; - -export const getDisplayStateResponseDisplaysItemSlotMin = 0; - - - -export const GetDisplayStateResponse = S.Struct({ - "displays": S.Array(S.Struct({ - "backend": S.String.annotations({ description: 'Backend name (`pf-vdisplay`, `kwin`, …).' }), - "client": S.optional(S.NullOr(S.String)).annotations({ description: 'Short client label, when the owner tracks it.' }), - "display_index": S.Number.pipe(S.greaterThanOrEqualTo(getDisplayStateResponseDisplaysItemDisplayIndexMin)).annotations({ description: 'This display\'s ordinal within its group, in acquire order (0-based).' }), - "expires_in_ms": S.optional(S.NullOr(S.Number.pipe(S.greaterThanOrEqualTo(getDisplayStateResponseDisplaysItemExpiresInMsMin)))).annotations({ description: 'Milliseconds until a lingering display is torn down (absent when active\/pinned).' }), - "group": S.Number.pipe(S.greaterThanOrEqualTo(getDisplayStateResponseDisplaysItemGroupMin)).annotations({ description: 'Display group (shared desktop) id — several displays with the same group form one desktop (§6A).' }), - "identity_slot": S.optional(S.NullOr(S.Number.pipe(S.greaterThanOrEqualTo(getDisplayStateResponseDisplaysItemIdentitySlotMin)))).annotations({ description: 'Stable per-client identity slot keying persistent config + manual layout (absent = shared\/anonymous).' }), - "mode": S.String.annotations({ description: '`WIDTHxHEIGHT@HZ`.' }), - "sessions": S.Number.pipe(S.greaterThanOrEqualTo(getDisplayStateResponseDisplaysItemSessionsMin)).annotations({ description: 'Live sessions holding the display.' }), - "slot": S.Number.pipe(S.greaterThanOrEqualTo(getDisplayStateResponseDisplaysItemSlotMin)).annotations({ description: 'Stable-enough id for the `\/display\/release` `slot` argument.' }), - "state": S.String.annotations({ description: '`active` | `lingering` | `pinned`.' }), - "topology": S.String.annotations({ description: 'Effective topology for this display\'s group (`extend` | `primary` | `exclusive`).' }), - "x": S.Number.annotations({ description: 'Desktop-space top-left `x` (auto-row or the console\'s manual arrangement, §6.2).' }), - "y": S.Number.annotations({ description: 'Desktop-space top-left `y`.' }) -}).annotations({ description: 'One live or kept virtual display.' })) -}).annotations({ description: 'The host\'s managed virtual displays right now.' }) - - -/** - * Server-Sent Events stream of the host's lifecycle events: client connect/disconnect, session - * and stream start/end, pairing decisions, display create/release, library changes, host - * start/stop — both protocol planes. Frames carry `id:` = the event's monotonic `seq`, - * `event:` = its kind, and `data:` = the event JSON (schema-versioned, additive-only). - * - * Resume: standard `Last-Event-ID` (or `?since=`) replays from the in-memory ring; a consumer - * that fell off the ring receives an `event: dropped` frame first and should resync via the - * REST snapshots. Keep-alive comments are sent every 15 s. - * @summary Stream host lifecycle events (SSE) - */ -export const streamEventsQuerySinceMin = 0; - - - -export const StreamEventsQueryParams = S.Struct({ - "since": S.optional(S.Number.pipe(S.greaterThanOrEqualTo(streamEventsQuerySinceMin))).annotations({ description: 'Resume cursor: only events with `seq` greater than this are sent (the ring keeps the newest ~1024). `Last-Event-ID` takes precedence.' }), - "kinds": S.optional(S.String).annotations({ description: 'Comma-separated server-side kind filter: exact kinds (`pairing.pending`) or `domain.\*` prefixes (`stream.\*`).' }) -}) - -export const streamEventsHeaderLastEventIDMin = 0; - - - -export const StreamEventsHeader = S.Struct({ - "Last-Event-ID": S.optional(S.NullOr(S.Number.pipe(S.greaterThanOrEqualTo(streamEventsHeaderLastEventIDMin)))).annotations({ description: 'SSE auto-reconnect cursor — the `id:` of the last received frame.' }) -}) - - -/** - * Lists the host's hardware GPUs, the persisted auto/manual preference, the GPU the next session - * will use (and why), and the GPU live sessions encode on right now. - * @summary GPU inventory and selection - */ -export const listGpusResponseActiveTwoSessionsMin = 0; - -export const listGpusResponseGpusItemVramMbMin = 0; - - - -export const ListGpusResponse = S.Struct({ - "active": S.optional(S.Union(S.Null, S.Struct({ - "backend": S.String.annotations({ description: 'The encode backend in use (`nvenc` | `amf` | `qsv` | `vaapi` | `software`).' }), - "id": S.String.annotations({ description: 'Stable id matching an entry of `gpus` (empty for the CPU\/software encoder).' }), - "name": S.String, - "sessions": S.Number.pipe(S.greaterThanOrEqualTo(listGpusResponseActiveTwoSessionsMin)).annotations({ description: 'Number of live encode sessions on it.' }), - "vendor": S.String.annotations({ description: '`nvidia` | `amd` | `intel` | `other`.' }) -}).annotations({ description: 'The GPU live sessions use right now (absent while nothing is streaming).' }))), - "env_override": S.optional(S.NullOr(S.String)).annotations({ description: '`PUNKTFUNK_RENDER_ADAPTER` (the host.env pin), when set — it applies while `mode` is\n`auto`; a manual preference overrides it.' }), - "gpus": S.Array(S.Struct({ - "id": S.String.annotations({ description: 'Stable identifier (`vendorid-deviceid-occurrence`, hex PCI ids) — pass to `setGpuPreference`.\nStable across reboots and driver updates, unlike an adapter index or LUID.' }), - "name": S.String.annotations({ description: 'Adapter\/marketing name.' }), - "vendor": S.String.annotations({ description: '`nvidia` | `amd` | `intel` | `other`.' }), - "vram_mb": S.Number.pipe(S.greaterThanOrEqualTo(listGpusResponseGpusItemVramMbMin)).annotations({ description: 'Dedicated VRAM in MiB (0 where the platform doesn\'t expose it).' }) -}).annotations({ description: 'One hardware GPU on the host (software\/WARP adapters are never listed).' })).annotations({ description: 'The host\'s hardware GPUs.' }), - "mode": S.String.annotations({ description: '`auto` or `manual`.' }), - "preferred_available": S.Boolean.annotations({ description: 'Whether the preferred GPU is currently present.' }), - "preferred_id": S.optional(S.NullOr(S.String)).annotations({ description: 'The manually preferred GPU\'s stable id, when one is stored (kept while `mode` is `auto` so\na console can offer returning to it). May reference a GPU that is currently absent.' }), - "preferred_name": S.optional(S.NullOr(S.String)).annotations({ description: 'The stored name of the preferred GPU (a usable label even when it is absent).' }), - "selected": S.optional(S.Union(S.Null, S.Struct({ - "id": S.String, - "name": S.String, - "source": S.String.annotations({ description: 'Why this GPU was selected: `preference` (the manual choice), `env`\n(`PUNKTFUNK_RENDER_ADAPTER`), `auto` (max dedicated VRAM \/ platform default), or\n`preference_missing` (a manual choice is set but that GPU is absent — auto-selected\ninstead so the host keeps streaming).' }), - "vendor": S.String.annotations({ description: '`nvidia` | `amd` | `intel` | `other`.' }) -}).annotations({ description: 'The GPU the next session will use.' }))) -}).annotations({ description: 'Full GPU-selection state for the console: inventory, the persisted preference, what the next\nsession will use, and what is in use right now.' }) - - -/** - * `auto` restores automatic selection (`PUNKTFUNK_RENDER_ADAPTER` pin, else max dedicated VRAM); - * `manual` pins capture + encode to the given GPU. Persisted across restarts; applies to the - * **next** session (a running session keeps its GPU). If the preferred GPU is absent at session - * start the host falls back to automatic selection rather than failing. - * @summary Set the GPU preference - */ -export const SetGpuPreferenceBody = S.Struct({ - "gpu_id": S.optional(S.NullOr(S.String)).annotations({ description: 'Required when `mode` is `manual`: the stable `id` of a currently listed GPU\n(see `listGpus`).' }), - "mode": S.String.annotations({ description: '`auto` (env pin, else max dedicated VRAM — the default) or `manual`.' }) -}).annotations({ description: 'Request body for `setGpuPreference`.' }) - -export const setGpuPreferenceResponseActiveTwoSessionsMin = 0; - -export const setGpuPreferenceResponseGpusItemVramMbMin = 0; - - - -export const SetGpuPreferenceResponse = S.Struct({ - "active": S.optional(S.Union(S.Null, S.Struct({ - "backend": S.String.annotations({ description: 'The encode backend in use (`nvenc` | `amf` | `qsv` | `vaapi` | `software`).' }), - "id": S.String.annotations({ description: 'Stable id matching an entry of `gpus` (empty for the CPU\/software encoder).' }), - "name": S.String, - "sessions": S.Number.pipe(S.greaterThanOrEqualTo(setGpuPreferenceResponseActiveTwoSessionsMin)).annotations({ description: 'Number of live encode sessions on it.' }), - "vendor": S.String.annotations({ description: '`nvidia` | `amd` | `intel` | `other`.' }) -}).annotations({ description: 'The GPU live sessions use right now (absent while nothing is streaming).' }))), - "env_override": S.optional(S.NullOr(S.String)).annotations({ description: '`PUNKTFUNK_RENDER_ADAPTER` (the host.env pin), when set — it applies while `mode` is\n`auto`; a manual preference overrides it.' }), - "gpus": S.Array(S.Struct({ - "id": S.String.annotations({ description: 'Stable identifier (`vendorid-deviceid-occurrence`, hex PCI ids) — pass to `setGpuPreference`.\nStable across reboots and driver updates, unlike an adapter index or LUID.' }), - "name": S.String.annotations({ description: 'Adapter\/marketing name.' }), - "vendor": S.String.annotations({ description: '`nvidia` | `amd` | `intel` | `other`.' }), - "vram_mb": S.Number.pipe(S.greaterThanOrEqualTo(setGpuPreferenceResponseGpusItemVramMbMin)).annotations({ description: 'Dedicated VRAM in MiB (0 where the platform doesn\'t expose it).' }) -}).annotations({ description: 'One hardware GPU on the host (software\/WARP adapters are never listed).' })).annotations({ description: 'The host\'s hardware GPUs.' }), - "mode": S.String.annotations({ description: '`auto` or `manual`.' }), - "preferred_available": S.Boolean.annotations({ description: 'Whether the preferred GPU is currently present.' }), - "preferred_id": S.optional(S.NullOr(S.String)).annotations({ description: 'The manually preferred GPU\'s stable id, when one is stored (kept while `mode` is `auto` so\na console can offer returning to it). May reference a GPU that is currently absent.' }), - "preferred_name": S.optional(S.NullOr(S.String)).annotations({ description: 'The stored name of the preferred GPU (a usable label even when it is absent).' }), - "selected": S.optional(S.Union(S.Null, S.Struct({ - "id": S.String, - "name": S.String, - "source": S.String.annotations({ description: 'Why this GPU was selected: `preference` (the manual choice), `env`\n(`PUNKTFUNK_RENDER_ADAPTER`), `auto` (max dedicated VRAM \/ platform default), or\n`preference_missing` (a manual choice is set but that GPU is absent — auto-selected\ninstead so the host keeps streaming).' }), - "vendor": S.String.annotations({ description: '`nvidia` | `amd` | `intel` | `other`.' }) -}).annotations({ description: 'The GPU the next session will use.' }))) -}).annotations({ description: 'Full GPU-selection state for the console: inventory, the persisted preference, what the next\nsession will use, and what is in use right now.' }) - - -/** - * Always available without authentication. - * @summary Liveness probe - */ -export const getHealthResponseAbiVersionMin = 0; - - - -export const GetHealthResponse = S.Struct({ - "abi_version": S.Number.pipe(S.greaterThanOrEqualTo(getHealthResponseAbiVersionMin)).annotations({ description: '`punktfunk-core` C ABI version.' }), - "status": S.String.annotations({ description: 'Always `\"ok\"` when the host responds.' }), - "version": S.String.annotations({ description: '`punktfunk-host` crate version.' }) -}).annotations({ description: 'Liveness + version probe.' }) - - -/** - * The operator's `hooks.json`: commands and webhooks fired on host lifecycle events. Empty - * when unconfigured. - * @summary Get the hook configuration - */ -export const getHooksResponseHooksItemDebounceMsMin = 0; - -export const getHooksResponseHooksItemTimeoutSMin = 0; - - - -export const GetHooksResponse = S.Struct({ - "hooks": S.optional(S.Array(S.Struct({ - "debounce_ms": S.optional(S.Number.pipe(S.greaterThanOrEqualTo(getHooksResponseHooksItemDebounceMsMin))).annotations({ description: 'Minimum interval between firings of this hook, in milliseconds. 0 = fire every time.' }), - "filter": S.optional(S.Union(S.Null, S.Struct({ - "app": S.optional(S.NullOr(S.String)).annotations({ description: 'Launched app id\/title (`stream.\*` events).' }), - "client": S.optional(S.NullOr(S.String)).annotations({ description: 'Client\/device name (for `session.\*`: the short client label the Dashboard shows).' }), - "fingerprint": S.optional(S.NullOr(S.String)).annotations({ description: 'Certificate fingerprint (hex, case-insensitive).' }), - "plane": S.optional(S.Union(S.Null, S.Literal('native', 'gamestream').annotations({ description: 'Protocol plane (`native` \/ `gamestream`).' }))) -}).annotations({ description: 'Exact-match constraints on the event\'s fields; every present field must match.' }))), - "hmac_secret_file": S.optional(S.NullOr(S.String)).annotations({ description: 'File holding the webhook HMAC secret (`X-Punktfunk-Signature: sha256=`). The file\nshould be operator-owned and private; a world-readable secret is warned about.' }), - "on": S.String.annotations({ description: 'Which events fire this hook: an exact kind (`stream.started`) or a `domain.\*` prefix\n(`pairing.\*`) — the same vocabulary as the SSE `?kinds=` filter.' }), - "run": S.optional(S.NullOr(S.String)).annotations({ description: 'Shell command to execute (detached, event JSON on stdin + `PF_EVENT_\*` env).' }), - "timeout_s": S.optional(S.Number.pipe(S.greaterThanOrEqualTo(getHooksResponseHooksItemTimeoutSMin))).annotations({ description: 'Exec timeout in seconds (1–600, default 30); the process group is killed on expiry.' }), - "webhook": S.optional(S.NullOr(S.String)).annotations({ description: 'URL to POST the event JSON to.' }) -}).annotations({ description: 'One hook: fire `run` and\/or `webhook` when an event matching `on` (+ `filter`) occurs.' }))) -}).annotations({ description: 'The operator\'s hook configuration — the `hooks.json` document and the `\/api\/v1\/hooks` body.' }) - - -/** - * Validates and persists a full `hooks.json` document (this is a whole-document PUT, not a - * patch). Applies from the next event — no restart. Hook commands run as the host user - * (interactive user session on Windows): treat this configuration as operator-privileged. - * @summary Replace the hook configuration - */ -export const setHooksBodyHooksItemDebounceMsMin = 0; - -export const setHooksBodyHooksItemTimeoutSMin = 0; - - - -export const SetHooksBody = S.Struct({ - "hooks": S.optional(S.Array(S.Struct({ - "debounce_ms": S.optional(S.Number.pipe(S.greaterThanOrEqualTo(setHooksBodyHooksItemDebounceMsMin))).annotations({ description: 'Minimum interval between firings of this hook, in milliseconds. 0 = fire every time.' }), - "filter": S.optional(S.Union(S.Null, S.Struct({ - "app": S.optional(S.NullOr(S.String)).annotations({ description: 'Launched app id\/title (`stream.\*` events).' }), - "client": S.optional(S.NullOr(S.String)).annotations({ description: 'Client\/device name (for `session.\*`: the short client label the Dashboard shows).' }), - "fingerprint": S.optional(S.NullOr(S.String)).annotations({ description: 'Certificate fingerprint (hex, case-insensitive).' }), - "plane": S.optional(S.Union(S.Null, S.Literal('native', 'gamestream').annotations({ description: 'Protocol plane (`native` \/ `gamestream`).' }))) -}).annotations({ description: 'Exact-match constraints on the event\'s fields; every present field must match.' }))), - "hmac_secret_file": S.optional(S.NullOr(S.String)).annotations({ description: 'File holding the webhook HMAC secret (`X-Punktfunk-Signature: sha256=`). The file\nshould be operator-owned and private; a world-readable secret is warned about.' }), - "on": S.String.annotations({ description: 'Which events fire this hook: an exact kind (`stream.started`) or a `domain.\*` prefix\n(`pairing.\*`) — the same vocabulary as the SSE `?kinds=` filter.' }), - "run": S.optional(S.NullOr(S.String)).annotations({ description: 'Shell command to execute (detached, event JSON on stdin + `PF_EVENT_\*` env).' }), - "timeout_s": S.optional(S.Number.pipe(S.greaterThanOrEqualTo(setHooksBodyHooksItemTimeoutSMin))).annotations({ description: 'Exec timeout in seconds (1–600, default 30); the process group is killed on expiry.' }), - "webhook": S.optional(S.NullOr(S.String)).annotations({ description: 'URL to POST the event JSON to.' }) -}).annotations({ description: 'One hook: fire `run` and\/or `webhook` when an event matching `on` (+ `filter`) occurs.' }))) -}).annotations({ description: 'The operator\'s hook configuration — the `hooks.json` document and the `\/api\/v1\/hooks` body.' }) - -export const setHooksResponseHooksItemDebounceMsMin = 0; - -export const setHooksResponseHooksItemTimeoutSMin = 0; - - - -export const SetHooksResponse = S.Struct({ - "hooks": S.optional(S.Array(S.Struct({ - "debounce_ms": S.optional(S.Number.pipe(S.greaterThanOrEqualTo(setHooksResponseHooksItemDebounceMsMin))).annotations({ description: 'Minimum interval between firings of this hook, in milliseconds. 0 = fire every time.' }), - "filter": S.optional(S.Union(S.Null, S.Struct({ - "app": S.optional(S.NullOr(S.String)).annotations({ description: 'Launched app id\/title (`stream.\*` events).' }), - "client": S.optional(S.NullOr(S.String)).annotations({ description: 'Client\/device name (for `session.\*`: the short client label the Dashboard shows).' }), - "fingerprint": S.optional(S.NullOr(S.String)).annotations({ description: 'Certificate fingerprint (hex, case-insensitive).' }), - "plane": S.optional(S.Union(S.Null, S.Literal('native', 'gamestream').annotations({ description: 'Protocol plane (`native` \/ `gamestream`).' }))) -}).annotations({ description: 'Exact-match constraints on the event\'s fields; every present field must match.' }))), - "hmac_secret_file": S.optional(S.NullOr(S.String)).annotations({ description: 'File holding the webhook HMAC secret (`X-Punktfunk-Signature: sha256=`). The file\nshould be operator-owned and private; a world-readable secret is warned about.' }), - "on": S.String.annotations({ description: 'Which events fire this hook: an exact kind (`stream.started`) or a `domain.\*` prefix\n(`pairing.\*`) — the same vocabulary as the SSE `?kinds=` filter.' }), - "run": S.optional(S.NullOr(S.String)).annotations({ description: 'Shell command to execute (detached, event JSON on stdin + `PF_EVENT_\*` env).' }), - "timeout_s": S.optional(S.Number.pipe(S.greaterThanOrEqualTo(setHooksResponseHooksItemTimeoutSMin))).annotations({ description: 'Exec timeout in seconds (1–600, default 30); the process group is killed on expiry.' }), - "webhook": S.optional(S.NullOr(S.String)).annotations({ description: 'URL to POST the event JSON to.' }) -}).annotations({ description: 'One hook: fire `run` and\/or `webhook` when an event matching `on` (+ `filter`) occurs.' }))) -}).annotations({ description: 'The operator\'s hook configuration — the `hooks.json` document and the `\/api\/v1\/hooks` body.' }) - - -/** - * @summary Host identity and capabilities - */ -export const getHostInfoResponseAbiVersionMin = 0; - -export const getHostInfoResponsePortsAudioMin = 0; - -export const getHostInfoResponsePortsControlMin = 0; - -export const getHostInfoResponsePortsHttpMin = 0; - -export const getHostInfoResponsePortsHttpsMin = 0; - -export const getHostInfoResponsePortsMgmtMin = 0; - -export const getHostInfoResponsePortsRtspMin = 0; - -export const getHostInfoResponsePortsVideoMin = 0; - - - -export const GetHostInfoResponse = S.Struct({ - "abi_version": S.Number.pipe(S.greaterThanOrEqualTo(getHostInfoResponseAbiVersionMin)).annotations({ description: '`punktfunk-core` C ABI version.' }), - "app_version": S.String.annotations({ description: 'GameStream host version advertised to Moonlight clients.' }), - "codecs": S.Array(S.Literal('h264', 'hevc', 'av1', 'pyrowave').annotations({ description: 'Video codec identifier. The wire token matches the codec\'s canonical name used across the\nstack (SDP\/GameStream advertisement, the stats-capture `CaptureMeta.codec`, and the encoder\'s\n[`Codec::label`]) — notably `H.265` serializes as `\"hevc\"`, not `\"h265\"`, so the same codec\nreads identically on every console page.' })).annotations({ description: 'Codecs the host can encode (NVENC).' }), - "gamestream": S.Boolean.annotations({ description: 'Whether the GameStream\/Moonlight-compat planes are running (`--gamestream`). `false` on the\nsecure default (native punktfunk\/1 only) — a console can hide Moonlight-only UI (e.g. the\nMoonlight PIN pairing card, which could never receive a PIN when this is `false`).' }), - "gfe_version": S.String.annotations({ description: 'GFE version advertised to Moonlight clients.' }), - "hostname": S.String, - "local_ip": S.String.annotations({ description: 'Best-effort primary LAN IP.' }), - "ports": S.Struct({ - "audio": S.Number.pipe(S.greaterThanOrEqualTo(getHostInfoResponsePortsAudioMin)), - "control": S.Number.pipe(S.greaterThanOrEqualTo(getHostInfoResponsePortsControlMin)), - "http": S.Number.pipe(S.greaterThanOrEqualTo(getHostInfoResponsePortsHttpMin)).annotations({ description: 'nvhttp plain HTTP (serverinfo, pairing).' }), - "https": S.Number.pipe(S.greaterThanOrEqualTo(getHostInfoResponsePortsHttpsMin)).annotations({ description: 'nvhttp mutual-TLS HTTPS (post-pairing).' }), - "mgmt": S.Number.pipe(S.greaterThanOrEqualTo(getHostInfoResponsePortsMgmtMin)).annotations({ description: 'This management API.' }), - "rtsp": S.Number.pipe(S.greaterThanOrEqualTo(getHostInfoResponsePortsRtspMin)), - "video": S.Number.pipe(S.greaterThanOrEqualTo(getHostInfoResponsePortsVideoMin)) -}).annotations({ description: 'Every port a client integration may need (Moonlight derives the stream ports from the\nHTTP base; a control pane should not have to).' }), - "uniqueid": S.String.annotations({ description: 'Stable per-host id (persisted across restarts), matched on pairing.' }), - "version": S.String.annotations({ description: '`punktfunk-host` crate version.' }) -}).annotations({ description: 'Host identity and advertised capabilities (static for the life of the process).' }) - - -/** - * Every installed-store title (Steam, read from the host's local files — no Steam API key) - * merged with the user's custom entries, sorted by title. Artwork fields are URLs the client - * fetches directly (the public Steam CDN for Steam titles). `?provider=` narrows to the - * entries a given external provider owns. - * @summary List the game library - */ -export const GetLibraryQueryParams = S.Struct({ - "provider": S.optional(S.String).annotations({ description: 'Only entries owned by this external provider' }) -}) - -export const GetLibraryResponseItem = S.Struct({ - "art": S.Struct({ - "header": S.optional(S.NullOr(S.String)).annotations({ description: 'Horizontal header (Steam `header.jpg`) — the universal fallback.' }), - "hero": S.optional(S.NullOr(S.String)).annotations({ description: 'Wide background (Steam `library_hero.jpg`).' }), - "logo": S.optional(S.NullOr(S.String)).annotations({ description: 'Transparent title logo (Steam `logo.png`).' }), - "portrait": S.optional(S.NullOr(S.String)).annotations({ description: 'Vertical capsule \/ poster (Steam `library_600x900.jpg`). Best for a grid.' }) -}).annotations({ description: 'Cover art for a title. All fields are URLs (the Steam CDN for Steam titles, user-supplied for\ncustom). The client prefers `portrait` for a grid and falls back to `header` when a title has\nno 600×900 capsule (common for older Steam apps).' }), - "id": S.String.annotations({ description: 'Stable, store-qualified id: `steam:` or `custom:`.' }), - "launch": S.optional(S.Union(S.Null, S.Struct({ - "kind": S.String.annotations({ description: '`\"steam_appid\"` or `\"command\"`.' }), - "value": S.String.annotations({ description: 'The appid (for `steam_appid`) or the shell command (for `command`).' }) -}).annotations({ description: 'How the host would launch it, when known.' }))), - "provider": S.optional(S.NullOr(S.String)).annotations({ description: 'The external provider owning this entry (custom-store entries synced by a provider\nplugin, RFC §8) — `None` for installed-store titles and manual custom entries. The\nconsole uses it for attribution; `GET \/library?provider=` filters on it.' }), - "store": S.String.annotations({ description: 'Which store surfaced it: `\"steam\"` or `\"custom\"`.' }), - "title": S.String -}).annotations({ description: 'One title in the unified library, regardless of which store it came from.' }) -export const GetLibraryResponse = S.Array(GetLibraryResponseItem) - - -/** - * Resolves `kind` (`portrait` | `hero` | `logo` | `header`) for the given library id and streams - * the image bytes. For a Steam title, the host's own local Steam cache is tried first (exact — - * it's what the user's Steam client already shows for it), the public Steam CDN's flat URL - * convention as a fallback (newer titles' CDN assets can live at a per-asset-hash path the host - * can't predict, in which case this 404s and the client falls through to its next art candidate). - * Only Steam ids are backed today; any other store 404s. - * @summary Fetch one cover-art image for a library entry - */ -export const GetLibraryArtParams = S.Struct({ - "id": S.String.annotations({ description: 'The store-qualified library id, e.g. `steam:570`' }), - "kind": S.String.annotations({ description: '`portrait` | `hero` | `logo` | `header`' }) -}) - - -/** - * Creates a user-curated title (e.g. a non-Steam game, an emulator, a ROM) with caller-supplied - * artwork URLs. The host assigns a stable id, returned in the body. - * @summary Add a custom library entry - */ -export const CreateCustomGameBody = S.Struct({ - "art": S.optional(S.Struct({ - "header": S.optional(S.NullOr(S.String)).annotations({ description: 'Horizontal header (Steam `header.jpg`) — the universal fallback.' }), - "hero": S.optional(S.NullOr(S.String)).annotations({ description: 'Wide background (Steam `library_hero.jpg`).' }), - "logo": S.optional(S.NullOr(S.String)).annotations({ description: 'Transparent title logo (Steam `logo.png`).' }), - "portrait": S.optional(S.NullOr(S.String)).annotations({ description: 'Vertical capsule \/ poster (Steam `library_600x900.jpg`). Best for a grid.' }) -})).annotations({ description: 'Cover art for a title. All fields are URLs (the Steam CDN for Steam titles, user-supplied for\ncustom). The client prefers `portrait` for a grid and falls back to `header` when a title has\nno 600×900 capsule (common for older Steam apps).' }), - "launch": S.optional(S.Union(S.Null, S.Struct({ - "kind": S.String.annotations({ description: '`\"steam_appid\"` or `\"command\"`.' }), - "value": S.String.annotations({ description: 'The appid (for `steam_appid`) or the shell command (for `command`).' }) -}).annotations({ 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\/`;\n`command` → run `` nested in a gamescope session.' }))), - "prep": S.optional(S.Array(S.Struct({ - "do": S.String.annotations({ description: 'Command run before launch. Same execution recipe and ownership checks as hook `run`\ncommands (event-less: stdin is empty JSON, env carries the `PF_APP_\*` context).' }), - "undo": S.optional(S.NullOr(S.String)).annotations({ description: 'Command run after the session ends. Skipped when its `do` failed (it never took effect).' }) -}).annotations({ description: 'One per-app preparation step (RFC §6 — deliberate Sunshine `prep-cmd` parity): `do` runs\n\*\*synchronously before the app launches\*\* (an HDR toggle or a MangoHud env change must land\nfirst), `undo` runs at session end — reverse order across steps, best-effort, on every exit\npath including a crash-unwind (RAII via [`PrepGuard`]).' }))).annotations({ description: 'Per-title prep\/undo steps — commands run as the host user; operator-privileged config.' }), - "title": S.String -}).annotations({ description: 'Request body to create or replace a custom entry (no `id` — the host owns it).' }) - - -/** - * @summary Update a custom library entry - */ -export const UpdateCustomGameParams = S.Struct({ - "id": S.String.annotations({ description: 'The custom entry id (without the `custom:` prefix)' }) -}) - -export const UpdateCustomGameBody = S.Struct({ - "art": S.optional(S.Struct({ - "header": S.optional(S.NullOr(S.String)).annotations({ description: 'Horizontal header (Steam `header.jpg`) — the universal fallback.' }), - "hero": S.optional(S.NullOr(S.String)).annotations({ description: 'Wide background (Steam `library_hero.jpg`).' }), - "logo": S.optional(S.NullOr(S.String)).annotations({ description: 'Transparent title logo (Steam `logo.png`).' }), - "portrait": S.optional(S.NullOr(S.String)).annotations({ description: 'Vertical capsule \/ poster (Steam `library_600x900.jpg`). Best for a grid.' }) -})).annotations({ description: 'Cover art for a title. All fields are URLs (the Steam CDN for Steam titles, user-supplied for\ncustom). The client prefers `portrait` for a grid and falls back to `header` when a title has\nno 600×900 capsule (common for older Steam apps).' }), - "launch": S.optional(S.Union(S.Null, S.Struct({ - "kind": S.String.annotations({ description: '`\"steam_appid\"` or `\"command\"`.' }), - "value": S.String.annotations({ description: 'The appid (for `steam_appid`) or the shell command (for `command`).' }) -}).annotations({ 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\/`;\n`command` → run `` nested in a gamescope session.' }))), - "prep": S.optional(S.Array(S.Struct({ - "do": S.String.annotations({ description: 'Command run before launch. Same execution recipe and ownership checks as hook `run`\ncommands (event-less: stdin is empty JSON, env carries the `PF_APP_\*` context).' }), - "undo": S.optional(S.NullOr(S.String)).annotations({ description: 'Command run after the session ends. Skipped when its `do` failed (it never took effect).' }) -}).annotations({ description: 'One per-app preparation step (RFC §6 — deliberate Sunshine `prep-cmd` parity): `do` runs\n\*\*synchronously before the app launches\*\* (an HDR toggle or a MangoHud env change must land\nfirst), `undo` runs at session end — reverse order across steps, best-effort, on every exit\npath including a crash-unwind (RAII via [`PrepGuard`]).' }))).annotations({ description: 'Per-title prep\/undo steps — commands run as the host user; operator-privileged config.' }), - "title": S.String -}).annotations({ description: 'Request body to create or replace a custom entry (no `id` — the host owns it).' }) - -export const UpdateCustomGameResponse = S.Struct({ - "art": S.optional(S.Struct({ - "header": S.optional(S.NullOr(S.String)).annotations({ description: 'Horizontal header (Steam `header.jpg`) — the universal fallback.' }), - "hero": S.optional(S.NullOr(S.String)).annotations({ description: 'Wide background (Steam `library_hero.jpg`).' }), - "logo": S.optional(S.NullOr(S.String)).annotations({ description: 'Transparent title logo (Steam `logo.png`).' }), - "portrait": S.optional(S.NullOr(S.String)).annotations({ description: 'Vertical capsule \/ poster (Steam `library_600x900.jpg`). Best for a grid.' }) -})).annotations({ description: 'Cover art for a title. All fields are URLs (the Steam CDN for Steam titles, user-supplied for\ncustom). The client prefers `portrait` for a grid and falls back to `header` when a title has\nno 600×900 capsule (common for older Steam apps).' }), - "external_id": S.optional(S.NullOr(S.String)).annotations({ description: 'The provider\'s own stable key for this title — the reconcile diff key, so the\nhost-assigned `id` stays stable across reconciles. Present iff `provider` is.' }), - "id": S.String.annotations({ description: 'Host-assigned, stable for the life of the entry (the `{id}` in the CRUD path).' }), - "launch": S.optional(S.Union(S.Null, S.Struct({ - "kind": S.String.annotations({ description: '`\"steam_appid\"` or `\"command\"`.' }), - "value": S.String.annotations({ description: 'The appid (for `steam_appid`) or the shell command (for `command`).' }) -}).annotations({ 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\/`;\n`command` → run `` nested in a gamescope session.' }))), - "prep": S.optional(S.Array(S.Struct({ - "do": S.String.annotations({ description: 'Command run before launch. Same execution recipe and ownership checks as hook `run`\ncommands (event-less: stdin is empty JSON, env carries the `PF_APP_\*` context).' }), - "undo": S.optional(S.NullOr(S.String)).annotations({ description: 'Command run after the session ends. Skipped when its `do` failed (it never took effect).' }) -}).annotations({ description: 'One per-app preparation step (RFC §6 — deliberate Sunshine `prep-cmd` parity): `do` runs\n\*\*synchronously before the app launches\*\* (an HDR toggle or a MangoHud env change must land\nfirst), `undo` runs at session end — reverse order across steps, best-effort, on every exit\npath including a crash-unwind (RAII via [`PrepGuard`]).' }))).annotations({ description: 'Per-title prep\/undo steps (RFC §6): each `do` runs before this title launches, each\n`undo` at session end in reverse order (see [`crate::hooks::run_prep`]).' }), - "provider": S.optional(S.NullOr(S.String)).annotations({ description: 'The external provider owning this entry (RFC §8), set ONLY by the provider reconcile\nAPI — `None` = a manual entry, which no provider operation ever touches, and which the\nmanual CRUD alone may edit (the converse holds too: manual CRUD refuses provider-owned\nentries, so ownership is never ambiguous).' }), - "title": S.String -}).annotations({ description: 'A user-added title, persisted in `~\/.config\/punktfunk\/library.json`. Same shape the API\nreturns and the web console edits.' }) - - -/** - * @summary Delete a custom library entry - */ -export const DeleteCustomGameParams = S.Struct({ - "id": S.String.annotations({ description: 'The custom entry id (without the `custom:` prefix)' }) -}) - - -/** - * Atomically replaces the full entry set owned by `{provider}` (RFC §8): the payload is the - * provider's desired list, keyed by its own stable `external_id` — the host diffs, keeps each - * surviving title's host id stable across reconciles, drops orphans, and never touches manual - * entries or other providers'. An empty array removes everything the provider owns. Emits - * `library.changed` with the provider as `source`. - * @summary Replace a provider's library entries (declarative reconcile) - */ -export const ReconcileProviderEntriesParams = S.Struct({ - "provider": S.String.annotations({ description: 'The provider id ([a-z0-9._-], `manual` reserved)' }) -}) - -export const ReconcileProviderEntriesBodyItem = S.Struct({ - "art": S.optional(S.Struct({ - "header": S.optional(S.NullOr(S.String)).annotations({ description: 'Horizontal header (Steam `header.jpg`) — the universal fallback.' }), - "hero": S.optional(S.NullOr(S.String)).annotations({ description: 'Wide background (Steam `library_hero.jpg`).' }), - "logo": S.optional(S.NullOr(S.String)).annotations({ description: 'Transparent title logo (Steam `logo.png`).' }), - "portrait": S.optional(S.NullOr(S.String)).annotations({ description: 'Vertical capsule \/ poster (Steam `library_600x900.jpg`). Best for a grid.' }) -})).annotations({ description: 'Cover art for a title. All fields are URLs (the Steam CDN for Steam titles, user-supplied for\ncustom). The client prefers `portrait` for a grid and falls back to `header` when a title has\nno 600×900 capsule (common for older Steam apps).' }), - "external_id": S.String.annotations({ description: 'The provider\'s stable id for this title (the reconcile diff key).' }), - "launch": S.optional(S.Union(S.Null, S.Struct({ - "kind": S.String.annotations({ description: '`\"steam_appid\"` or `\"command\"`.' }), - "value": S.String.annotations({ description: 'The appid (for `steam_appid`) or the shell command (for `command`).' }) -}).annotations({ 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\/`;\n`command` → run `` nested in a gamescope session.' }))), - "prep": S.optional(S.Array(S.Struct({ - "do": S.String.annotations({ description: 'Command run before launch. Same execution recipe and ownership checks as hook `run`\ncommands (event-less: stdin is empty JSON, env carries the `PF_APP_\*` context).' }), - "undo": S.optional(S.NullOr(S.String)).annotations({ description: 'Command run after the session ends. Skipped when its `do` failed (it never took effect).' }) -}).annotations({ description: 'One per-app preparation step (RFC §6 — deliberate Sunshine `prep-cmd` parity): `do` runs\n\*\*synchronously before the app launches\*\* (an HDR toggle or a MangoHud env change must land\nfirst), `undo` runs at session end — reverse order across steps, best-effort, on every exit\npath including a crash-unwind (RAII via [`PrepGuard`]).' }))).annotations({ description: 'Per-title prep\/undo steps — commands run as the host user; operator-privileged config.' }), - "title": S.String -}).annotations({ description: 'One title in a provider\'s declarative reconcile payload (RFC §8): [`CustomInput`] plus the\nprovider\'s required stable key.' }) -export const ReconcileProviderEntriesBody = S.Array(ReconcileProviderEntriesBodyItem) - -export const ReconcileProviderEntriesResponseItem = S.Struct({ - "art": S.optional(S.Struct({ - "header": S.optional(S.NullOr(S.String)).annotations({ description: 'Horizontal header (Steam `header.jpg`) — the universal fallback.' }), - "hero": S.optional(S.NullOr(S.String)).annotations({ description: 'Wide background (Steam `library_hero.jpg`).' }), - "logo": S.optional(S.NullOr(S.String)).annotations({ description: 'Transparent title logo (Steam `logo.png`).' }), - "portrait": S.optional(S.NullOr(S.String)).annotations({ description: 'Vertical capsule \/ poster (Steam `library_600x900.jpg`). Best for a grid.' }) -})).annotations({ description: 'Cover art for a title. All fields are URLs (the Steam CDN for Steam titles, user-supplied for\ncustom). The client prefers `portrait` for a grid and falls back to `header` when a title has\nno 600×900 capsule (common for older Steam apps).' }), - "external_id": S.optional(S.NullOr(S.String)).annotations({ description: 'The provider\'s own stable key for this title — the reconcile diff key, so the\nhost-assigned `id` stays stable across reconciles. Present iff `provider` is.' }), - "id": S.String.annotations({ description: 'Host-assigned, stable for the life of the entry (the `{id}` in the CRUD path).' }), - "launch": S.optional(S.Union(S.Null, S.Struct({ - "kind": S.String.annotations({ description: '`\"steam_appid\"` or `\"command\"`.' }), - "value": S.String.annotations({ description: 'The appid (for `steam_appid`) or the shell command (for `command`).' }) -}).annotations({ 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\/`;\n`command` → run `` nested in a gamescope session.' }))), - "prep": S.optional(S.Array(S.Struct({ - "do": S.String.annotations({ description: 'Command run before launch. Same execution recipe and ownership checks as hook `run`\ncommands (event-less: stdin is empty JSON, env carries the `PF_APP_\*` context).' }), - "undo": S.optional(S.NullOr(S.String)).annotations({ description: 'Command run after the session ends. Skipped when its `do` failed (it never took effect).' }) -}).annotations({ description: 'One per-app preparation step (RFC §6 — deliberate Sunshine `prep-cmd` parity): `do` runs\n\*\*synchronously before the app launches\*\* (an HDR toggle or a MangoHud env change must land\nfirst), `undo` runs at session end — reverse order across steps, best-effort, on every exit\npath including a crash-unwind (RAII via [`PrepGuard`]).' }))).annotations({ description: 'Per-title prep\/undo steps (RFC §6): each `do` runs before this title launches, each\n`undo` at session end in reverse order (see [`crate::hooks::run_prep`]).' }), - "provider": S.optional(S.NullOr(S.String)).annotations({ description: 'The external provider owning this entry (RFC §8), set ONLY by the provider reconcile\nAPI — `None` = a manual entry, which no provider operation ever touches, and which the\nmanual CRUD alone may edit (the converse holds too: manual CRUD refuses provider-owned\nentries, so ownership is never ambiguous).' }), - "title": S.String -}).annotations({ description: 'A user-added title, persisted in `~\/.config\/punktfunk\/library.json`. Same shape the API\nreturns and the web console edits.' }) -export const ReconcileProviderEntriesResponse = S.Array(ReconcileProviderEntriesResponseItem) - - -/** - * Deletes every entry owned by `{provider}` — the clean-uninstall path for a provider plugin - * (RFC §8). Emits `library.changed` when anything was removed. - * @summary Remove a provider's library entries - */ -export const DeleteProviderEntriesParams = S.Struct({ - "provider": S.String.annotations({ description: 'The provider id' }) -}) - -export const deleteProviderEntriesResponseRemovedMin = 0; - - - -export const DeleteProviderEntriesResponse = S.Struct({ - "removed": S.Number.pipe(S.greaterThanOrEqualTo(deleteProviderEntriesResponseRemovedMin)).annotations({ description: 'How many entries the provider owned (and were removed).' }) -}).annotations({ description: 'The count envelope a provider uninstall returns.' }) - - -/** - * Non-sensitive status (counts and booleans only — no PIN values, no fingerprints, no device - * names). Unauthenticated, but served to loopback peers only. - * @summary Local status summary for the tray icon - */ -export const getLocalSummaryResponseKeptDisplaysMin = 0; - -export const getLocalSummaryResponseNativePairedClientsMin = 0; - -export const getLocalSummaryResponsePairedClientsMin = 0; - -export const getLocalSummaryResponsePendingApprovalsMin = 0; - -export const getLocalSummaryResponseSessionTwoFpsMin = 0; - -export const getLocalSummaryResponseSessionTwoHeightMin = 0; - -export const getLocalSummaryResponseSessionTwoWidthMin = 0; - - - -export const GetLocalSummaryResponse = S.Struct({ - "audio_streaming": S.Boolean.annotations({ description: 'True while the audio stream thread is running.' }), - "conflicts": S.optional(S.Array(S.String)).annotations({ 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.' }), - "kept_displays": S.Number.pipe(S.greaterThanOrEqualTo(getLocalSummaryResponseKeptDisplaysMin)).annotations({ 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.' }), - "native_paired_clients": S.Number.pipe(S.greaterThanOrEqualTo(getLocalSummaryResponseNativePairedClientsMin)).annotations({ description: 'Number of paired native (punktfunk\/1) devices.' }), - "paired_clients": S.Number.pipe(S.greaterThanOrEqualTo(getLocalSummaryResponsePairedClientsMin)).annotations({ description: 'Number of pinned (paired) GameStream client certificates.' }), - "pending_approvals": S.Number.pipe(S.greaterThanOrEqualTo(getLocalSummaryResponsePendingApprovalsMin)).annotations({ description: 'Native pairing knocks awaiting the operator\'s approval (count only).' }), - "pin_pending": S.Boolean.annotations({ description: 'True while a GameStream pairing handshake is parked waiting for the user\'s PIN.' }), - "session": S.optional(S.Union(S.Null, S.Struct({ - "fps": S.Number.pipe(S.greaterThanOrEqualTo(getLocalSummaryResponseSessionTwoFpsMin)), - "height": S.Number.pipe(S.greaterThanOrEqualTo(getLocalSummaryResponseSessionTwoHeightMin)), - "width": S.Number.pipe(S.greaterThanOrEqualTo(getLocalSummaryResponseSessionTwoWidthMin)) -}).annotations({ description: 'The active launch session (set by Moonlight\'s `\/launch`, cleared on cancel\/stop).' }))), - "version": S.String.annotations({ description: 'Host version (mirrors `\/health`).' }), - "video_streaming": S.Boolean.annotations({ description: 'True while the video stream thread is running.' }) -}).annotations({ 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.' }) - - -/** - * The host's recent log entries — an in-memory ring of the newest few thousand, captured at - * DEBUG and above regardless of `RUST_LOG`. Follow live by polling with `after` set to the last - * response's `next` cursor; a `dropped: true` means entries were evicted between polls (the ring - * wrapped). Bearer-only: logs can reference client identities and host paths, so this is part of - * the loopback-only admin surface, never the LAN-readable mTLS one. - * @summary Host logs - */ -export const logsGetQueryAfterMin = 0; - -export const logsGetQueryLimitMin = 0; - - - -export const LogsGetQueryParams = S.Struct({ - "after": S.optional(S.Number.pipe(S.greaterThanOrEqualTo(logsGetQueryAfterMin))).annotations({ description: 'Return entries with seq greater than this (omitted\/0 = oldest retained)' }), - "limit": S.optional(S.Number.pipe(S.greaterThanOrEqualTo(logsGetQueryLimitMin))).annotations({ description: 'Max entries per response (default and cap 1000)' }) -}) - -export const logsGetResponseEntriesItemSeqMin = 0; - -export const logsGetResponseEntriesItemTsMsMin = 0; - -export const logsGetResponseNextMin = 0; - - - -export const LogsGetResponse = S.Struct({ - "dropped": S.Boolean.annotations({ description: 'True when entries between `after` and the first returned one were already evicted.' }), - "entries": S.Array(S.Struct({ - "level": S.String.annotations({ description: '`ERROR` | `WARN` | `INFO` | `DEBUG` | `TRACE`.' }), - "msg": S.String.annotations({ description: 'The formatted message, structured fields appended as `key=value`.' }), - "seq": S.Number.pipe(S.greaterThanOrEqualTo(logsGetResponseEntriesItemSeqMin)).annotations({ description: 'Monotonic sequence number (1-based) — pass the last one back as the `after` cursor.' }), - "target": S.String.annotations({ description: 'The emitting module path (tracing target).' }), - "ts_ms": S.Number.pipe(S.greaterThanOrEqualTo(logsGetResponseEntriesItemTsMsMin)).annotations({ description: 'Unix timestamp in milliseconds.' }) -}).annotations({ description: 'One captured log event.' })), - "next": S.Number.pipe(S.greaterThanOrEqualTo(logsGetResponseNextMin)).annotations({ description: 'Cursor for the next poll (the last returned seq, or the request\'s `after` when empty).' }) -}).annotations({ description: 'One poll\'s worth of log entries.' }) - - -/** - * @summary List native paired clients - */ -export const ListNativeClientsResponseItem = S.Struct({ - "fingerprint": S.String.annotations({ description: 'Hex SHA-256 of the client certificate — its stable id here.' }), - "name": S.String.annotations({ description: 'The name the client supplied when pairing.' }) -}).annotations({ description: 'A paired native (punktfunk\/1) client.' }) -export const ListNativeClientsResponse = S.Array(ListNativeClientsResponseItem) - - -/** - * Removes a punktfunk/1 client from the native trust store by fingerprint. - * @summary Unpair a native client - */ -export const UnpairNativeClientParams = S.Struct({ - "fingerprint": S.String.annotations({ description: 'Hex SHA-256 of the client certificate (case-insensitive)' }) -}) - - -/** - * The native (punktfunk/1) pairing window. Poll while armed to show the PIN + countdown. - * `enabled: false` means this host runs GameStream only (no `--native`). - * @summary Native pairing status - */ -export const getNativePairingResponseExpiresInSecsMin = 0; - -export const getNativePairingResponsePairedClientsMin = 0; - - - -export const GetNativePairingResponse = S.Struct({ - "armed": S.Boolean.annotations({ description: 'True while a pairing window is open.' }), - "enabled": S.Boolean.annotations({ description: 'Whether the native host is running (the unified host started with `--native`).' }), - "expires_in_secs": S.optional(S.NullOr(S.Number.pipe(S.greaterThanOrEqualTo(getNativePairingResponseExpiresInSecsMin)))).annotations({ description: 'Seconds left in the window (null = disarmed, or armed with no expiry via the CLI flag).' }), - "paired_clients": S.Number.pipe(S.greaterThanOrEqualTo(getNativePairingResponsePairedClientsMin)).annotations({ description: 'Number of paired native clients.' }), - "pin": S.optional(S.NullOr(S.String)).annotations({ description: 'The PIN to display while armed (null when disarmed).' }) -}).annotations({ 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.' }) - - -/** - * Opens a pairing window and mints a fresh PIN to display. The user enters it on their device - * within `ttl_secs`; the device then appears in the native client list. - * @summary Arm native pairing - */ -export const armNativePairingBodyTtlSecsMin = 0; - - - -export const ArmNativePairingBody = S.Struct({ - "fingerprint": S.optional(S.NullOr(S.String)).annotations({ description: 'Optional: bind the window to ONE device fingerprint (hex SHA-256, e.g. from a pending knock).\nWhen set, only a pairing attempt from that fingerprint consumes the window — so an unpaired\nLAN peer can neither pair nor burn a window armed for a specific device (security-review #9).\nOmit for an unbound window (any device may use the PIN — trusted-LAN only).' }), - "ttl_secs": S.optional(S.NullOr(S.Number.pipe(S.greaterThanOrEqualTo(armNativePairingBodyTtlSecsMin)))).annotations({ description: 'Window length in seconds (default 120; clamped to 15–600).' }) -}).annotations({ description: 'Arm-native-pairing request body.' }) - -export const armNativePairingResponseExpiresInSecsMin = 0; - -export const armNativePairingResponsePairedClientsMin = 0; - - - -export const ArmNativePairingResponse = S.Struct({ - "armed": S.Boolean.annotations({ description: 'True while a pairing window is open.' }), - "enabled": S.Boolean.annotations({ description: 'Whether the native host is running (the unified host started with `--native`).' }), - "expires_in_secs": S.optional(S.NullOr(S.Number.pipe(S.greaterThanOrEqualTo(armNativePairingResponseExpiresInSecsMin)))).annotations({ description: 'Seconds left in the window (null = disarmed, or armed with no expiry via the CLI flag).' }), - "paired_clients": S.Number.pipe(S.greaterThanOrEqualTo(armNativePairingResponsePairedClientsMin)).annotations({ description: 'Number of paired native clients.' }), - "pin": S.optional(S.NullOr(S.String)).annotations({ description: 'The PIN to display while armed (null when disarmed).' }) -}).annotations({ 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.' }) - - -/** - * Unpaired devices that tried to connect while the host requires pairing. Approve one to pair - * it without a PIN (delegated approval); entries expire after ~10 minutes. - * @summary List devices awaiting pairing approval - */ -export const listPendingDevicesResponseAgeSecsMin = 0; - -export const listPendingDevicesResponseIdMin = 0; - - - -export const ListPendingDevicesResponseItem = S.Struct({ - "age_secs": S.Number.pipe(S.greaterThanOrEqualTo(listPendingDevicesResponseAgeSecsMin)).annotations({ description: 'Seconds since the device last knocked.' }), - "fingerprint": S.String.annotations({ description: 'Hex SHA-256 of the device\'s certificate — what approval pins.' }), - "id": S.Number.pipe(S.greaterThanOrEqualTo(listPendingDevicesResponseIdMin)).annotations({ description: 'Id to address approve\/deny (per-process; entries expire after ~10 minutes).' }), - "name": S.String.annotations({ description: 'Best-effort device label (the client\'s own name, else fingerprint-derived).' }) -}).annotations({ description: 'An unpaired device that tried to connect while the host requires pairing — awaiting\n\*\*delegated approval\*\* (approve it here instead of fetching the host PIN out of band).' }) -export const ListPendingDevicesResponse = S.Array(ListPendingDevicesResponseItem) - - -/** - * Pairs the device's certificate fingerprint — it can connect immediately (no PIN). Optionally - * relabel it via the body; send `{}` to keep the name it knocked with. - * @summary Approve a pending device - */ -export const approvePendingDevicePathIdMin = 0; - - - -export const ApprovePendingDeviceParams = S.Struct({ - "id": S.Number.pipe(S.greaterThanOrEqualTo(approvePendingDevicePathIdMin)).annotations({ description: 'Pending-request id from the pending list' }) -}) - -export const ApprovePendingDeviceBody = S.Struct({ - "name": S.optional(S.NullOr(S.String)).annotations({ description: 'Operator-chosen label for the device (defaults to the name it knocked with).' }) -}).annotations({ description: 'Approve-pending-device request body. Send `{}` to keep the device\'s own name.' }) - -export const ApprovePendingDeviceResponse = S.Struct({ - "fingerprint": S.String.annotations({ description: 'Hex SHA-256 of the client certificate — its stable id here.' }), - "name": S.String.annotations({ description: 'The name the client supplied when pairing.' }) -}).annotations({ description: 'A paired native (punktfunk\/1) client.' }) - - -/** - * Drops the request. Not a blocklist — the device's next attempt knocks again. - * @summary Deny a pending device - */ -export const denyPendingDevicePathIdMin = 0; - - - -export const DenyPendingDeviceParams = S.Struct({ - "id": S.Number.pipe(S.greaterThanOrEqualTo(denyPendingDevicePathIdMin)).annotations({ description: 'Pending-request id from the pending list' }) -}) - - -/** - * Poll this to know when to prompt the user for the PIN Moonlight displays. - * @summary Pairing-flow status - */ -export const GetPairingStatusResponse = S.Struct({ - "pin_pending": S.Boolean.annotations({ description: 'True while a pairing handshake is parked waiting for the user\'s PIN.' }) -}).annotations({ description: 'Pairing-flow status.' }) - - -/** - * Delivers the PIN the Moonlight client is displaying, completing the out-of-band half - * of the pairing handshake. - * @summary Submit the pairing PIN - */ -export const SubmitPairingPinBody = S.Struct({ - "pin": S.String.annotations({ description: '1–16 ASCII digits (Moonlight shows 4).' }) -}).annotations({ description: 'The PIN Moonlight displays during pairing.' }) - - -/** - * The full sample time-series of the capture currently recording, for live graphing. `404` when - * nothing is armed. - * @summary Live in-progress capture - */ -export const statsCaptureLiveResponseMetaDurationMsMin = 0; - -export const statsCaptureLiveResponseMetaFpsMin = 0; - -export const statsCaptureLiveResponseMetaHeightMin = 0; - -export const statsCaptureLiveResponseMetaSampleCountMin = 0; - -export const statsCaptureLiveResponseMetaStartedUnixMsMin = 0; - -export const statsCaptureLiveResponseMetaWidthMin = 0; - -export const statsCaptureLiveResponseSamplesItemBitrateKbpsMin = 0; - -export const statsCaptureLiveResponseSamplesItemFecRecoveredMin = 0; - -export const statsCaptureLiveResponseSamplesItemFramesDroppedMin = 0; - -export const statsCaptureLiveResponseSamplesItemPacketsDroppedMin = 0; - -export const statsCaptureLiveResponseSamplesItemSendDroppedMin = 0; - -export const statsCaptureLiveResponseSamplesItemSessionIdMin = 0; - -export const statsCaptureLiveResponseSamplesItemTMsMin = 0; - - - -export const StatsCaptureLiveResponse = S.Struct({ - "meta": S.Struct({ - "client": S.String.annotations({ description: 'Short label \/ fingerprint prefix, or `\"\"` if unknown.' }), - "codec": S.String.annotations({ description: '`\"h264\" | \"hevc\" | \"av1\"`.' }), - "duration_ms": S.Number.pipe(S.greaterThanOrEqualTo(statsCaptureLiveResponseMetaDurationMsMin)), - "fps": S.Number.pipe(S.greaterThanOrEqualTo(statsCaptureLiveResponseMetaFpsMin)), - "height": S.Number.pipe(S.greaterThanOrEqualTo(statsCaptureLiveResponseMetaHeightMin)), - "id": S.String.annotations({ description: 'e.g. `\"2026-06-26T20-14-03Z_5120x1440\"` — also the filename stem.' }), - "kind": S.String.annotations({ description: '`\"native\" | \"gamestream\"`.' }), - "sample_count": S.Number.pipe(S.greaterThanOrEqualTo(statsCaptureLiveResponseMetaSampleCountMin)), - "started_unix_ms": S.Number.pipe(S.greaterThanOrEqualTo(statsCaptureLiveResponseMetaStartedUnixMsMin)), - "width": S.Number.pipe(S.greaterThanOrEqualTo(statsCaptureLiveResponseMetaWidthMin)) -}).annotations({ description: 'Capture summary — the filename stem plus the negotiated mode\/codec\/client. Stored at the head\nof each on-disk recording and listed standalone (without the sample body) by\n[`StatsRecorder::list`].' }), - "samples": S.Array(S.Struct({ - "bitrate_kbps": S.Number.pipe(S.greaterThanOrEqualTo(statsCaptureLiveResponseSamplesItemBitrateKbpsMin)).annotations({ description: 'Configured target bitrate.' }), - "fec_recovered": S.Number.pipe(S.greaterThanOrEqualTo(statsCaptureLiveResponseSamplesItemFecRecoveredMin)).annotations({ description: 'FEC shards recovered this window (delta).' }), - "fps": S.Number.annotations({ description: 'Genuine NEW frames\/s from the source.' }), - "frames_dropped": S.Number.pipe(S.greaterThanOrEqualTo(statsCaptureLiveResponseSamplesItemFramesDroppedMin)).annotations({ description: 'Frames dropped this window (delta).' }), - "mbps": S.Number.annotations({ description: 'Transmit goodput (Mb\/s).' }), - "packets_dropped": S.Number.pipe(S.greaterThanOrEqualTo(statsCaptureLiveResponseSamplesItemPacketsDroppedMin)).annotations({ description: 'Packets dropped this window (receiver-side \/ reassembler, where known).' }), - "repeat_fps": S.Number.annotations({ description: 'Re-encoded holds\/s (source-starvation indicator).' }), - "send_dropped": S.Number.pipe(S.greaterThanOrEqualTo(statsCaptureLiveResponseSamplesItemSendDroppedMin)).annotations({ description: 'Host send-buffer overflow \/ EAGAIN this window (delta).' }), - "session_id": S.Number.pipe(S.greaterThanOrEqualTo(statsCaptureLiveResponseSamplesItemSessionIdMin)).annotations({ description: 'Disambiguates concurrent sessions (usually constant).' }), - "stages": S.Array(S.Struct({ - "name": S.String.annotations({ description: '`\"capture\" | \"submit\" | \"encode\" | \"packetize\" | \"send\"` (path-dependent).' }), - "p50_us": S.Number, - "p99_us": S.Number -}).annotations({ description: 'One pipeline stage\'s latency in an aggregation window (microseconds).' })).annotations({ description: 'Ordered pipeline stages for this path.' }), - "t_ms": S.Number.pipe(S.greaterThanOrEqualTo(statsCaptureLiveResponseSamplesItemTMsMin)).annotations({ description: 'Milliseconds since capture start (monotonic; stamped by [`StatsRecorder::push_sample`]).' }) -}).annotations({ description: 'One aggregated sample (~ every 2 s native, ~ every 1 s GameStream).' })) -}).annotations({ description: 'A full capture: summary + the sample time-series. The wire + on-disk shape.' }) - - -/** - * Arms a new performance-stats capture. Idempotent: if a capture is already running this returns - * the current status unchanged. While armed, the streaming loops emit aggregated samples (~ every - * 1–2 s) into the in-progress capture, readable live via `GET /stats/capture/live`. - * @summary Start a stats capture - */ -export const statsCaptureStartResponseElapsedMsMin = 0; - -export const statsCaptureStartResponseSampleCountMin = 0; - -export const statsCaptureStartResponseStartedUnixMsMin = 0; - - - -export const StatsCaptureStartResponse = S.Struct({ - "armed": S.Boolean.annotations({ description: 'Capture currently running.' }), - "elapsed_ms": S.Number.pipe(S.greaterThanOrEqualTo(statsCaptureStartResponseElapsedMsMin)).annotations({ description: 'Host-measured elapsed time of the in-progress capture, in ms (`0` if idle). Computed from the\nhost\'s MONOTONIC clock, so a console can show elapsed time without subtracting `started_unix_ms`\nfrom its own (possibly skewed) wall clock.' }), - "kind": S.String.annotations({ description: 'Path of the in-progress capture (`\"\"` if idle).' }), - "sample_count": S.Number.pipe(S.greaterThanOrEqualTo(statsCaptureStartResponseSampleCountMin)).annotations({ description: 'Samples in the in-progress capture.' }), - "started_unix_ms": S.Number.pipe(S.greaterThanOrEqualTo(statsCaptureStartResponseStartedUnixMsMin)).annotations({ description: 'Unix start time of the in-progress capture (`0` if idle).' }) -}).annotations({ description: 'Snapshot of the in-progress capture for the management API.' }) - - -/** - * Whether a capture is armed, its sample count, and start time. Poll this (e.g. every 2 s) to - * drive the capture-control UI. - * @summary Stats capture status - */ -export const statsCaptureStatusResponseElapsedMsMin = 0; - -export const statsCaptureStatusResponseSampleCountMin = 0; - -export const statsCaptureStatusResponseStartedUnixMsMin = 0; - - - -export const StatsCaptureStatusResponse = S.Struct({ - "armed": S.Boolean.annotations({ description: 'Capture currently running.' }), - "elapsed_ms": S.Number.pipe(S.greaterThanOrEqualTo(statsCaptureStatusResponseElapsedMsMin)).annotations({ description: 'Host-measured elapsed time of the in-progress capture, in ms (`0` if idle). Computed from the\nhost\'s MONOTONIC clock, so a console can show elapsed time without subtracting `started_unix_ms`\nfrom its own (possibly skewed) wall clock.' }), - "kind": S.String.annotations({ description: 'Path of the in-progress capture (`\"\"` if idle).' }), - "sample_count": S.Number.pipe(S.greaterThanOrEqualTo(statsCaptureStatusResponseSampleCountMin)).annotations({ description: 'Samples in the in-progress capture.' }), - "started_unix_ms": S.Number.pipe(S.greaterThanOrEqualTo(statsCaptureStatusResponseStartedUnixMsMin)).annotations({ description: 'Unix start time of the in-progress capture (`0` if idle).' }) -}).annotations({ description: 'Snapshot of the in-progress capture for the management API.' }) - - -/** - * Disarms the in-progress capture and writes it to disk atomically, returning its summary. If - * nothing was recording, returns `204 No Content`. - * @summary Stop the stats capture - */ -export const statsCaptureStopResponseDurationMsMin = 0; - -export const statsCaptureStopResponseFpsMin = 0; - -export const statsCaptureStopResponseHeightMin = 0; - -export const statsCaptureStopResponseSampleCountMin = 0; - -export const statsCaptureStopResponseStartedUnixMsMin = 0; - -export const statsCaptureStopResponseWidthMin = 0; - - - -export const StatsCaptureStopResponse = S.Struct({ - "client": S.String.annotations({ description: 'Short label \/ fingerprint prefix, or `\"\"` if unknown.' }), - "codec": S.String.annotations({ description: '`\"h264\" | \"hevc\" | \"av1\"`.' }), - "duration_ms": S.Number.pipe(S.greaterThanOrEqualTo(statsCaptureStopResponseDurationMsMin)), - "fps": S.Number.pipe(S.greaterThanOrEqualTo(statsCaptureStopResponseFpsMin)), - "height": S.Number.pipe(S.greaterThanOrEqualTo(statsCaptureStopResponseHeightMin)), - "id": S.String.annotations({ description: 'e.g. `\"2026-06-26T20-14-03Z_5120x1440\"` — also the filename stem.' }), - "kind": S.String.annotations({ description: '`\"native\" | \"gamestream\"`.' }), - "sample_count": S.Number.pipe(S.greaterThanOrEqualTo(statsCaptureStopResponseSampleCountMin)), - "started_unix_ms": S.Number.pipe(S.greaterThanOrEqualTo(statsCaptureStopResponseStartedUnixMsMin)), - "width": S.Number.pipe(S.greaterThanOrEqualTo(statsCaptureStopResponseWidthMin)) -}).annotations({ description: 'Capture summary — the filename stem plus the negotiated mode\/codec\/client. Stored at the head\nof each on-disk recording and listed standalone (without the sample body) by\n[`StatsRecorder::list`].' }) - - -/** - * Every saved capture's summary (the `meta` head only — not the sample body), newest first. - * @summary List saved recordings - */ -export const statsRecordingsListResponseDurationMsMin = 0; - -export const statsRecordingsListResponseFpsMin = 0; - -export const statsRecordingsListResponseHeightMin = 0; - -export const statsRecordingsListResponseSampleCountMin = 0; - -export const statsRecordingsListResponseStartedUnixMsMin = 0; - -export const statsRecordingsListResponseWidthMin = 0; - - - -export const StatsRecordingsListResponseItem = S.Struct({ - "client": S.String.annotations({ description: 'Short label \/ fingerprint prefix, or `\"\"` if unknown.' }), - "codec": S.String.annotations({ description: '`\"h264\" | \"hevc\" | \"av1\"`.' }), - "duration_ms": S.Number.pipe(S.greaterThanOrEqualTo(statsRecordingsListResponseDurationMsMin)), - "fps": S.Number.pipe(S.greaterThanOrEqualTo(statsRecordingsListResponseFpsMin)), - "height": S.Number.pipe(S.greaterThanOrEqualTo(statsRecordingsListResponseHeightMin)), - "id": S.String.annotations({ description: 'e.g. `\"2026-06-26T20-14-03Z_5120x1440\"` — also the filename stem.' }), - "kind": S.String.annotations({ description: '`\"native\" | \"gamestream\"`.' }), - "sample_count": S.Number.pipe(S.greaterThanOrEqualTo(statsRecordingsListResponseSampleCountMin)), - "started_unix_ms": S.Number.pipe(S.greaterThanOrEqualTo(statsRecordingsListResponseStartedUnixMsMin)), - "width": S.Number.pipe(S.greaterThanOrEqualTo(statsRecordingsListResponseWidthMin)) -}).annotations({ description: 'Capture summary — the filename stem plus the negotiated mode\/codec\/client. Stored at the head\nof each on-disk recording and listed standalone (without the sample body) by\n[`StatsRecorder::list`].' }) -export const StatsRecordingsListResponse = S.Array(StatsRecordingsListResponseItem) - - -/** - * The full capture (meta + samples) for `id`, for graphing or download. - * @summary Get a saved recording - */ -export const StatsRecordingGetParams = S.Struct({ - "id": S.String.annotations({ description: 'The recording id (its filename stem)' }) -}) - -export const statsRecordingGetResponseMetaDurationMsMin = 0; - -export const statsRecordingGetResponseMetaFpsMin = 0; - -export const statsRecordingGetResponseMetaHeightMin = 0; - -export const statsRecordingGetResponseMetaSampleCountMin = 0; - -export const statsRecordingGetResponseMetaStartedUnixMsMin = 0; - -export const statsRecordingGetResponseMetaWidthMin = 0; - -export const statsRecordingGetResponseSamplesItemBitrateKbpsMin = 0; - -export const statsRecordingGetResponseSamplesItemFecRecoveredMin = 0; - -export const statsRecordingGetResponseSamplesItemFramesDroppedMin = 0; - -export const statsRecordingGetResponseSamplesItemPacketsDroppedMin = 0; - -export const statsRecordingGetResponseSamplesItemSendDroppedMin = 0; - -export const statsRecordingGetResponseSamplesItemSessionIdMin = 0; - -export const statsRecordingGetResponseSamplesItemTMsMin = 0; - - - -export const StatsRecordingGetResponse = S.Struct({ - "meta": S.Struct({ - "client": S.String.annotations({ description: 'Short label \/ fingerprint prefix, or `\"\"` if unknown.' }), - "codec": S.String.annotations({ description: '`\"h264\" | \"hevc\" | \"av1\"`.' }), - "duration_ms": S.Number.pipe(S.greaterThanOrEqualTo(statsRecordingGetResponseMetaDurationMsMin)), - "fps": S.Number.pipe(S.greaterThanOrEqualTo(statsRecordingGetResponseMetaFpsMin)), - "height": S.Number.pipe(S.greaterThanOrEqualTo(statsRecordingGetResponseMetaHeightMin)), - "id": S.String.annotations({ description: 'e.g. `\"2026-06-26T20-14-03Z_5120x1440\"` — also the filename stem.' }), - "kind": S.String.annotations({ description: '`\"native\" | \"gamestream\"`.' }), - "sample_count": S.Number.pipe(S.greaterThanOrEqualTo(statsRecordingGetResponseMetaSampleCountMin)), - "started_unix_ms": S.Number.pipe(S.greaterThanOrEqualTo(statsRecordingGetResponseMetaStartedUnixMsMin)), - "width": S.Number.pipe(S.greaterThanOrEqualTo(statsRecordingGetResponseMetaWidthMin)) -}).annotations({ description: 'Capture summary — the filename stem plus the negotiated mode\/codec\/client. Stored at the head\nof each on-disk recording and listed standalone (without the sample body) by\n[`StatsRecorder::list`].' }), - "samples": S.Array(S.Struct({ - "bitrate_kbps": S.Number.pipe(S.greaterThanOrEqualTo(statsRecordingGetResponseSamplesItemBitrateKbpsMin)).annotations({ description: 'Configured target bitrate.' }), - "fec_recovered": S.Number.pipe(S.greaterThanOrEqualTo(statsRecordingGetResponseSamplesItemFecRecoveredMin)).annotations({ description: 'FEC shards recovered this window (delta).' }), - "fps": S.Number.annotations({ description: 'Genuine NEW frames\/s from the source.' }), - "frames_dropped": S.Number.pipe(S.greaterThanOrEqualTo(statsRecordingGetResponseSamplesItemFramesDroppedMin)).annotations({ description: 'Frames dropped this window (delta).' }), - "mbps": S.Number.annotations({ description: 'Transmit goodput (Mb\/s).' }), - "packets_dropped": S.Number.pipe(S.greaterThanOrEqualTo(statsRecordingGetResponseSamplesItemPacketsDroppedMin)).annotations({ description: 'Packets dropped this window (receiver-side \/ reassembler, where known).' }), - "repeat_fps": S.Number.annotations({ description: 'Re-encoded holds\/s (source-starvation indicator).' }), - "send_dropped": S.Number.pipe(S.greaterThanOrEqualTo(statsRecordingGetResponseSamplesItemSendDroppedMin)).annotations({ description: 'Host send-buffer overflow \/ EAGAIN this window (delta).' }), - "session_id": S.Number.pipe(S.greaterThanOrEqualTo(statsRecordingGetResponseSamplesItemSessionIdMin)).annotations({ description: 'Disambiguates concurrent sessions (usually constant).' }), - "stages": S.Array(S.Struct({ - "name": S.String.annotations({ description: '`\"capture\" | \"submit\" | \"encode\" | \"packetize\" | \"send\"` (path-dependent).' }), - "p50_us": S.Number, - "p99_us": S.Number -}).annotations({ description: 'One pipeline stage\'s latency in an aggregation window (microseconds).' })).annotations({ description: 'Ordered pipeline stages for this path.' }), - "t_ms": S.Number.pipe(S.greaterThanOrEqualTo(statsRecordingGetResponseSamplesItemTMsMin)).annotations({ description: 'Milliseconds since capture start (monotonic; stamped by [`StatsRecorder::push_sample`]).' }) -}).annotations({ description: 'One aggregated sample (~ every 2 s native, ~ every 1 s GameStream).' })) -}).annotations({ description: 'A full capture: summary + the sample time-series. The wire + on-disk shape.' }) - - -/** - * Removes the recording `id` from disk. `404` if there is no such recording. - * @summary Delete a saved recording - */ -export const StatsRecordingDeleteParams = S.Struct({ - "id": S.String.annotations({ description: 'The recording id (its filename stem)' }) -}) - - -/** - * @summary Live host status - */ -export const getStatusResponseActiveSessionsMin = 0; - -export const getStatusResponsePairedClientsMin = 0; - -export const getStatusResponseSessionTwoFpsMin = 0; - -export const getStatusResponseSessionTwoHeightMin = 0; - -export const getStatusResponseSessionTwoWidthMin = 0; - -export const getStatusResponseStreamTwoBitrateKbpsMin = 0; - -export const getStatusResponseStreamTwoFpsMin = 0; - -export const getStatusResponseStreamTwoHeightMin = 0; - -export const getStatusResponseStreamTwoMinFecMin = 0; - -export const getStatusResponseStreamTwoPacketSizeMin = 0; - -export const getStatusResponseStreamTwoWidthMin = 0; - - - -export const GetStatusResponse = S.Struct({ - "active_sessions": S.Number.pipe(S.greaterThanOrEqualTo(getStatusResponseActiveSessionsMin)).annotations({ description: 'Number of live streaming sessions across BOTH planes (GameStream + native punktfunk\/1). The\nnative server admits concurrent sessions, so this can exceed 1; `session`\/`stream` below\ndescribe a single representative session for the detail card.' }), - "audio_streaming": S.Boolean.annotations({ description: 'True while the audio stream thread is running.' }), - "paired_clients": S.Number.pipe(S.greaterThanOrEqualTo(getStatusResponsePairedClientsMin)).annotations({ description: 'Number of pinned (paired) client certificates.' }), - "pin_pending": S.Boolean.annotations({ description: 'True while a pairing handshake is parked waiting for the user\'s PIN\n(submit it via `POST \/api\/v1\/pair\/pin`).' }), - "session": S.optional(S.Union(S.Null, S.Struct({ - "fps": S.Number.pipe(S.greaterThanOrEqualTo(getStatusResponseSessionTwoFpsMin)), - "height": S.Number.pipe(S.greaterThanOrEqualTo(getStatusResponseSessionTwoHeightMin)), - "width": S.Number.pipe(S.greaterThanOrEqualTo(getStatusResponseSessionTwoWidthMin)) -}).annotations({ description: 'A representative active session. GameStream\'s launch (Moonlight `\/launch`) when present, else\nthe first live native session. `null` when nothing is streaming.' }))), - "stream": S.optional(S.Union(S.Null, S.Struct({ - "bitrate_kbps": S.Number.pipe(S.greaterThanOrEqualTo(getStatusResponseStreamTwoBitrateKbpsMin)), - "codec": S.Literal('h264', 'hevc', 'av1', 'pyrowave').annotations({ description: 'Video codec identifier. The wire token matches the codec\'s canonical name used across the\nstack (SDP\/GameStream advertisement, the stats-capture `CaptureMeta.codec`, and the encoder\'s\n[`Codec::label`]) — notably `H.265` serializes as `\"hevc\"`, not `\"h265\"`, so the same codec\nreads identically on every console page.' }), - "fps": S.Number.pipe(S.greaterThanOrEqualTo(getStatusResponseStreamTwoFpsMin)), - "height": S.Number.pipe(S.greaterThanOrEqualTo(getStatusResponseStreamTwoHeightMin)), - "min_fec": S.Number.pipe(S.greaterThanOrEqualTo(getStatusResponseStreamTwoMinFecMin)).annotations({ description: 'Client\'s parity floor per FEC block (`minRequiredFecPackets`).' }), - "packet_size": S.Number.pipe(S.greaterThanOrEqualTo(getStatusResponseStreamTwoPacketSizeMin)).annotations({ description: 'Video payload size per packet (bytes).' }), - "width": S.Number.pipe(S.greaterThanOrEqualTo(getStatusResponseStreamTwoWidthMin)) -}).annotations({ description: 'The active stream\'s parameters — RTSP-negotiated for GameStream, or the live native session\'s\nmode\/codec\/bitrate. `null` when nothing is streaming.' }))), - "video_streaming": S.Boolean.annotations({ description: 'True while the video stream thread is running.' }) -}).annotations({ description: 'Live host status (changes as clients launch\/end sessions).' }) diff --git a/sdk/src/index.ts b/sdk/src/index.ts index 8380f432..69e462f5 100644 --- a/sdk/src/index.ts +++ b/sdk/src/index.ts @@ -9,7 +9,8 @@ // pf.events.on("pairing.pending", async (e) => { // await notifyPhone(`Pairing request from ${e.device.name}`); // }); -import type { Effect } from "effect"; +import { type Effect, Result } from "effect"; +import { type HostApi, makeHostApi } from "./api.js"; import type { PunktfunkHost } from "./client.js"; import { type ConnectOptions, @@ -26,6 +27,7 @@ import { kindMatches, } from "./wire.js"; +export type { HostApi } from "./api.js"; export { HttpStatusError } from "./core.js"; export type { ConnectOptions } from "./config.js"; export type { @@ -66,7 +68,13 @@ export interface PunktfunkEvents { export interface Punktfunk { /** Resolved connection (URL/token/CA). */ readonly config: ResolvedConfig; - /** One management-API request under `/api/v1` (`request("GET", "/status")`). */ + /** + * The **typed** management API — every REST endpoint as an autocompletable method with + * checked request/response types (`await pf.api.listPairedClients()`). This is the front + * door; reach for [`request`] only for something the generated client doesn't cover. + */ + readonly api: HostApi; + /** Untyped escape hatch: one raw `/api/v1` request (`request("GET", "/status")`). */ request(method: string, path: string, body?: unknown): Promise; /** The lifecycle-event subscription surface. */ readonly events: PunktfunkEvents; @@ -83,6 +91,7 @@ export const connect = async (options?: ConnectOptions): Promise => { const cfg = await resolveConfig(options); // Fail fast on bad credentials/URL: one cheap authenticated probe. await httpRequest(cfg, "GET", "/host"); + const api = await makeHostApi(cfg); const listeners = new Set(); let pump: AsyncGenerator | undefined; @@ -118,11 +127,11 @@ export const connect = async (options?: ConnectOptions): Promise => { continue; } const decoded = decodeHostEvent(json); - if (decoded._tag === "Left") { + if (Result.isFailure(decoded)) { dispatch("unknown", json); continue; } - dispatch(decoded.right.kind, decoded.right); + dispatch(decoded.success.kind, decoded.success); } } catch (e) { if (!closed) warn(`event stream stopped: ${e}`); @@ -132,6 +141,7 @@ export const connect = async (options?: ConnectOptions): Promise => { return { config: cfg, + api, request: (method, path, body) => httpRequest(cfg, method, path, body), events: { on(pattern: string, cb: (ev: never) => unknown) { diff --git a/sdk/src/runner.ts b/sdk/src/runner.ts index 55b0c76a..8e3ffe18 100644 --- a/sdk/src/runner.ts +++ b/sdk/src/runner.ts @@ -38,7 +38,7 @@ export interface RunnerOptions { /** Connection overrides handed to every unit's client/layer. */ connect?: ConnectOptions; /** Restart backoff base (test seam). Default 1 s, capped at 60 s, jittered. */ - restartBase?: Duration.DurationInput; + restartBase?: Duration.Input; /** Line sink. Default: stamped stdout. */ log?: (line: string) => void; } @@ -167,10 +167,12 @@ export const superviseUnit = ( options: RunnerOptions = {}, ): Effect.Effect => { const log = options.log ?? defaultLog; - const restart = Schedule.exponential(options.restartBase ?? "1 second").pipe( - Schedule.union(Schedule.spaced("60 seconds")), // cap - Schedule.jittered, - ); + // Exponential backoff, capped at 60 s (min-delay of the two schedules), then jittered. + // (v4 replaced `Schedule.union` with the array-form `Schedule.min`.) + const restart = Schedule.min([ + Schedule.exponential(options.restartBase ?? "1 second"), + Schedule.spaced("60 seconds"), + ]).pipe(Schedule.jittered); let attempt = 0; const once = Effect.suspend(() => { attempt += 1; @@ -187,13 +189,13 @@ export const superviseUnit = ( ), ), ), - Effect.tapErrorCause((cause) => + Effect.tapCause((cause) => Effect.sync(() => log(`[${unit.name}] failed: ${Cause.pretty(cause).split("\n")[0]}`), ), ), Effect.retry(restart), - Effect.catchAllCause((cause) => + Effect.catchCause((cause) => // A retry schedule that gives up (it doesn't, but stay total) — log and end. Effect.sync(() => log(`[${unit.name}] gave up: ${Cause.pretty(cause)}`)), ), diff --git a/sdk/src/wire.ts b/sdk/src/wire.ts index 79df7530..61087a47 100644 --- a/sdk/src/wire.ts +++ b/sdk/src/wire.ts @@ -6,10 +6,10 @@ // keys (Effect's default), and an unknown `kind` surfaces on the raw channel, never a throw. import { Schema as S } from "effect"; -export const Plane = S.Literal("native", "gamestream"); +export const Plane = S.Literals(["native", "gamestream"]); export type Plane = S.Schema.Type; -export const DisconnectReason = S.Literal("quit", "timeout", "error"); +export const DisconnectReason = S.Literals(["quit", "timeout", "error"]); export type DisconnectReason = S.Schema.Type; export const ClientRef = S.Struct({ @@ -124,7 +124,7 @@ export const HostStopping = S.Struct({ }); /** Every known lifecycle event — discriminated on `kind`. */ -export const HostEvent = S.Union( +export const HostEvent = S.Union([ ClientConnected, ClientDisconnected, SessionStarted, @@ -139,7 +139,7 @@ export const HostEvent = S.Union( LibraryChanged, HostStarted, HostStopping, -); +]); export type HostEvent = S.Schema.Type; /** The known event kinds (for filters and the facade's `on()`). */ @@ -148,7 +148,12 @@ export type HostEventKind = HostEvent["kind"]; /** Narrow a HostEvent by kind: `EventOf<"stream.started">`. */ export type EventOf = Extract; -export const decodeHostEvent = S.decodeUnknownEither(HostEvent); +/** + * Decode one event JSON into a [`HostEvent`], as a [`Result`]: `Success` for a known kind, + * `Failure` for an unknown/undecodable one (a newer host — rides the raw channel, never throws). + * (v4 replaced `Either` with `Result`; the callers branch on `Result.isFailure`.) + */ +export const decodeHostEvent = S.decodeUnknownResult(HostEvent); /** * Does `pattern` select `kind`? Exact kinds (`stream.started`) or `domain.*` prefixes on the diff --git a/sdk/test/api.test.ts b/sdk/test/api.test.ts new file mode 100644 index 00000000..c7e20f9b --- /dev/null +++ b/sdk/test/api.test.ts @@ -0,0 +1,71 @@ +// The typed `pf.api.*` surface end to end against a mock host: proves the generated client is +// reachable through the facade — base URL, bearer auth, the CA-pinning fetch injection, and +// schema decode all wired. (The event stream is covered by surfaces.test.ts.) +import { describe, expect, test } from "bun:test"; +import { connect } from "../src/index.js"; + +const TOKEN = "test-token"; + +const mockHost = () => + Bun.serve({ + port: 0, + fetch(req) { + const url = new URL(req.url); + if (req.headers.get("authorization") !== `Bearer ${TOKEN}`) { + return Response.json({ error: "invalid credentials" }, { status: 401 }); + } + switch (url.pathname) { + case "/api/v1/host": + return Response.json({ hostname: "mock" }); // connect() probe (undecoded) + case "/api/v1/clients": + return Response.json([ + { fingerprint: "abc123", subject: "CN=Living Room TV" }, + ]); + case "/api/v1/session": + // stopSession → 204 No Content (idempotent). + return new Response(null, { status: 204 }); + default: + return Response.json({ error: "not found" }, { status: 404 }); + } + }, + }); + +describe("typed api surface", () => { + test("pf.api.* returns decoded, typed results", async () => { + const server = mockHost(); + try { + const pf = await connect({ + url: `http://127.0.0.1:${server.port}`, + token: TOKEN, + }); + + const clients = await pf.api.listPairedClients(); + expect(clients).toHaveLength(1); + // Typed field access — no cast. + expect(clients[0]?.fingerprint).toBe("abc123"); + expect(clients[0]?.subject).toBe("CN=Living Room TV"); + + // A 204 endpoint resolves to void, not a thrown "empty body". + await expect(pf.api.stopSession()).resolves.toBeUndefined(); + + pf.close(); + } finally { + server.stop(true); + } + }); + + test("a failing call rejects", async () => { + const server = mockHost(); + try { + const pf = await connect({ + url: `http://127.0.0.1:${server.port}`, + token: TOKEN, + }); + // /api/v1/gpus isn't served → 404 → the endpoint's typed error rejects. + await expect(pf.api.listGpus()).rejects.toThrow(); + pf.close(); + } finally { + server.stop(true); + } + }); +}); diff --git a/sdk/test/wire.test.ts b/sdk/test/wire.test.ts index 5cd1d31a..2fa4efa8 100644 --- a/sdk/test/wire.test.ts +++ b/sdk/test/wire.test.ts @@ -10,10 +10,10 @@ describe("wire", () => { '{"seq":4182,"ts_ms":1700000000000,"schema":1,"kind":"stream.started","stream":{"mode":"3840x2160@120","hdr":true,"client":"Living Room TV","app":"steam:570","plane":"native"}}', ), ); - expect(stream._tag).toBe("Right"); - if (stream._tag === "Right" && stream.right.kind === "stream.started") { - expect(stream.right.stream.mode).toBe("3840x2160@120"); - expect(stream.right.stream.app).toBe("steam:570"); + expect(stream._tag).toBe("Success"); + if (stream._tag === "Success" && stream.success.kind === "stream.started") { + expect(stream.success.stream.mode).toBe("3840x2160@120"); + expect(stream.success.stream.app).toBe("steam:570"); } const disc = decodeHostEvent( @@ -21,15 +21,15 @@ describe("wire", () => { '{"seq":1,"ts_ms":1700000000000,"schema":1,"kind":"client.disconnected","client":{"name":"Deck","fingerprint":"b1c2","plane":"gamestream"},"reason":"timeout"}', ), ); - expect(disc._tag).toBe("Right"); - if (disc._tag === "Right" && disc.right.kind === "client.disconnected") { - expect(disc.right.reason).toBe("timeout"); + expect(disc._tag).toBe("Success"); + if (disc._tag === "Success" && disc.success.kind === "client.disconnected") { + expect(disc.success.reason).toBe("timeout"); } const stopping = decodeHostEvent( JSON.parse('{"seq":2,"ts_ms":1700000000000,"schema":1,"kind":"host.stopping"}'), ); - expect(stopping._tag).toBe("Right"); + expect(stopping._tag).toBe("Success"); }); test("tolerates unknown keys (additive-only wire)", () => { @@ -41,7 +41,7 @@ describe("wire", () => { source: "manual", future_field: { anything: true }, }); - expect(r._tag).toBe("Right"); + expect(r._tag).toBe("Success"); }); test("unknown kinds fail decode (they ride the raw channel)", () => { @@ -51,7 +51,7 @@ describe("wire", () => { schema: 1, kind: "totally.new", }); - expect(r._tag).toBe("Left"); + expect(r._tag).toBe("Failure"); }); test("kindMatches mirrors the host filter semantics", () => { diff --git a/sdk/tsconfig.json b/sdk/tsconfig.json index 3c44f397..19149df0 100644 --- a/sdk/tsconfig.json +++ b/sdk/tsconfig.json @@ -9,5 +9,5 @@ "noEmit": true, "types": ["bun"] }, - "include": ["src", "test"] + "include": ["src", "test", "examples"] }