Compare commits

..
Author SHA1 Message Date
enricobuehler 1d6f4760f3 Merge branch 'main' into worktree-stall-ride-through
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m8s
apple / swift (pull_request) Successful in 1m20s
apple / screenshots (pull_request) Skipped
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m7s
android / android (pull_request) Successful in 3m10s
ci / web (pull_request) Successful in 1m9s
ci / rust-arm64 (pull_request) Successful in 1m38s
ci / docs-site (pull_request) Successful in 1m25s
ci / rust (pull_request) Successful in 6m32s
2026-08-05 06:22:41 +00:00
enricobuehler 9dfbc2f895 Merge pull request 'fix(client-core): pad-audio references the WASAPI module by its mounted name' (#57) from fix/pad-audio-wasapi-module-path into main
ci / web (push) Successful in 1m7s
ci / rust-arm64 (push) Successful in 1m22s
apple / swift (push) Successful in 1m27s
ci / docs-site (push) Successful in 1m24s
deb / build-publish-client-arm64 (push) Successful in 2m46s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 6s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 7s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 5s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 4s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 6s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 4s
deb / build-publish-host (push) Successful in 4m8s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 49s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 1m12s
deb / build-publish (push) Successful in 6m26s
windows-msix / package (arm64, C:\Users\Public\ffmpeg-arm64, --no-default-features, aarch64-pc-windows-msvc, C:\t-a64) (push) Successful in 3m43s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Failing after 16s
docker / builders-arm64cross (push) Successful in 19s
apple / screenshots (push) Successful in 5m53s
android / android (push) Successful in 8m51s
arch / build-publish (push) Successful in 9m37s
ci / rust (push) Successful in 10m25s
docker / deploy-docs (push) Failing after 3m52s
flatpak / build-publish (push) Canceled after 5m43s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Canceled after 5m20s
windows-msix / package (x64, C:\Users\Public\ffmpeg, , x86_64-pc-windows-msvc, C:\t) (push) Canceled after 5m45s
windows / build (aarch64-pc-windows-msvc) (push) Canceled after 0s
windows / build (x86_64-pc-windows-msvc) (push) Canceled after 0s
2026-08-05 06:22:31 +00:00
enricobuehler 56adb47026 fix(client-core): pad-audio references the WASAPI module by its mounted name
ci / web (pull_request) Successful in 56s
apple / swift (pull_request) Successful in 1m25s
apple / screenshots (pull_request) Skipped
windows / build (aarch64-pc-windows-msvc) (pull_request) Successful in 1m40s
ci / docs-site (pull_request) Successful in 2m33s
ci / rust-arm64 (pull_request) Successful in 2m43s
windows / build (x86_64-pc-windows-msvc) (pull_request) Successful in 2m17s
android / android (pull_request) Successful in 4m12s
ci / rust (pull_request) Successful in 6m21s
The Windows build of pf-client-core has been red on main since the
pad-audio merge (#23): pad_audio.rs calls
`crate::audio_wasapi::device_by_id`, but lib.rs mounts audio_wasapi.rs AS
`crate::audio` via the #[path] per-OS swap — the `audio_wasapi` module
name never exists. Windows-gated call site, so every Linux leg stayed
green while both `windows / build` targets failed E0433.

One-line rename to the mounted path (+ the comment that pointed readers
at the phantom name). Verification is the PR's own windows leg — the
crate builds on no other platform this path compiles on.
2026-08-05 08:15:02 +02:00
enricobuehler 52a9d02355 Merge pull request 'fix(deps): close the undici, fast-uri, postcss and brace-expansion advisories' (#55) from worktree-audit-undici into main
audit / cargo-audit (push) Successful in 43s
audit / bun-audit (plugin-kit) (push) Successful in 16s
audit / bun-audit (sdk) (push) Successful in 17s
audit / bun-audit (web) (push) Successful in 21s
audit / docs-site-audit (push) Successful in 18s
audit / pnpm-audit (push) Successful in 9s
ci / web (push) Successful in 1m14s
ci / docs-site (push) Successful in 1m23s
ci / rust-arm64 (push) Successful in 2m20s
docker / builders (--build-arg FEDORA_VERSION=44, ci/fedora-rpm.Dockerfile, punktfunk-fedora44-rpm, -f44) (push) Successful in 13s
docker / builders (ci/android-ci.Dockerfile, punktfunk-android-ci) (push) Successful in 7s
docker / builders (ci/arch-ci.Dockerfile, punktfunk-arch-ci) (push) Successful in 5s
docker / builders (ci/fedora-rpm.Dockerfile, punktfunk-fedora-rpm) (push) Successful in 5s
docker / builders (ci/rust-ci-noble.Dockerfile, punktfunk-rust-ci-noble) (push) Successful in 4s
docker / builders (ci/rust-ci.Dockerfile, punktfunk-rust-ci) (push) Successful in 6s
docker / apps (., web/Dockerfile, punktfunk-web) (push) Successful in 46s
docker / apps (docs-site, docs-site/Dockerfile, punktfunk-docs) (push) Successful in 9s
deb / build-publish-client-arm64 (push) Successful in 2m45s
deb / build-publish (push) Successful in 5m12s
audit / license-gate (push) Successful in 5m44s
deb / build-publish-host (push) Successful in 4m56s
arch / build-publish (push) Successful in 11m54s
docker / builders-arm64cross (push) Successful in 49s
ci / rust (push) Successful in 10m46s
docker / deploy-docs (push) Failing after 3m45s
windows-host / package (push) Successful in 17m5s
windows-host / winget-source (push) Skipped
windows-host / canary-manifest (push) Successful in 14s
rpm / build-publish (44, fedora-44, punktfunk-fedora44-rpm) (push) Successful in 16m2s
rpm / build-publish (43, bazzite, punktfunk-fedora-rpm) (push) Successful in 19m10s
Reviewed-on: #55
2026-08-05 05:51:06 +00:00
enricobuehler e5ca213339 fix(core/abr): a starved window is never a decode-knee sample
apple / swift (pull_request) Successful in 1m25s
apple / screenshots (pull_request) Skipped
windows / build (aarch64-pc-windows-msvc) (pull_request) Failing after 6m2s
windows / build (x86_64-pc-windows-msvc) (pull_request) Failing after 2m57s
ci / rust-arm64 (pull_request) Successful in 1m24s
ci / web (pull_request) Successful in 1m13s
ci / docs-site (pull_request) Successful in 1m47s
android / android (pull_request) Successful in 5m30s
ci / rust (pull_request) Successful in 9m50s
Stall program T2 (amplification kill), the phantom-latch half. A deciding
window that delivered under a quarter of the target rate (a host-side
capture stall, an outage, a mid-window pause) carries starvation-shaped
distress — a jump-to-live flush, a keyframe-ask burst — that the decode-cap
latch read as decoder evidence: under a periodic capture stall (the RDNA4
standby-sink field cases, one stall every ~5 s) every edge offers another
'backoff' at the SAME rate, and one pair latches a phantom decoder knee at
whatever rate the display driver happened to interrupt. The session then
fights the cap's re-probe ladder (+12.5% per 16-128 clean windows) for
minutes on a decoder that was never the problem.

Starved windows still back off (real damage deserves the safe response) but
take the same 'not a knee sample either way' arm as a draining backoff:
they neither latch a decode cap nor erase the reference a genuine choke
set, so a real knee's pair still finds itself around the interruption. The
¼ bar sits deliberately far under the ×¾ utilization bar climbs require.

Gates: 44 abr tests green (2 new: the stall-cycle no-latch scenario and the
reference-preservation scenario), full core lib suite 346 green
(--features quic), fmt + clippy clean.
2026-08-05 00:28:25 +02:00
enricobuehler e5416646f9 fix(host/send): a stall-resume frame paces at the proven rate instead of blasting
Stall program T2 (amplification kill), the resume-burst half. The native
pace budget was min(0.9 × time-to-deadline, overflow at ~3× stream rate) —
for steady-state frames the rate term is smaller and decides, but for an
OVERSIZED frame (a capture-stall resume carrying seconds of scene delta, a
cold IDR) the deadline term clamped a multi-interval overflow into the
remainder of ONE: an instantaneous many-×-stream-rate blast that overruns
the socket tx-buffer and loses the very frame that would have ended the
freeze. Field fingerprint across three RDNA4 standby-sink cases:
WSAENOBUFS(10055) + loss_ppm spikes at stall edges, then a recovery-IDR
round trip per retry while the client shows 'current bitrate 0.1'.

The budget is now the overflow's wire time at the pace rate itself
(send_pacing::native_budget, pure + unit-tested), bounded by an absolute
100 ms ceiling so a pathological frame can't park the send thread; the
deadline stays a target, never a license to blast. Steady-state frames
produce byte-identical schedules (the rate term already decided);
PUNKTFUNK_PACE_FACTOR=0 keeps the legacy deadline-only spread; the
GameStream plane's Moonlight-pinned schedule is untouched.

Gates: host clippy --all-targets -D warnings + 9 send_pacing tests green
(linux/amd64 container), fmt clean.
2026-08-05 00:28:13 +02:00
enricobuehler b79d90b463 fix(deps): close the undici, fast-uri, postcss and brace-expansion advisories
ci / web (pull_request) Successful in 1m8s
ci / rust-arm64 (pull_request) Successful in 1m33s
ci / docs-site (pull_request) Successful in 1m21s
ci / rust (pull_request) Failing after 7m49s
audit.yml's three blocking bun-audit legs (web, sdk, plugin-kit) were all red on
main. Ten findings in sdk and plugin-kit, eight in web; every one of them a
transitive dependency, none reachable by bumping a direct dep.

web already carried the right mechanism — an `overrides` block whose `undici` and
`fast-uri` pins had simply gone stale — so it needed four bumps, not a new idea:
undici 7.28.0 -> ^7.29.0 and fast-uri 3.1.4 -> ^3.1.5 for the reported advisories,
plus postcss ^8.5.10 -> ^8.5.25 and brace-expansion ^5.0.8 -> ^5.0.9 for two more
that were published after the failing run and would have gone red on the next
audit anyway. All four stay inside their current major.

sdk and plugin-kit were harder and the fix deserves an explanation. Their single
finding is undici 8.7.0/8.8.0 pulled in by @effect/platform-node, a devDependency
pinned at 4.0.0-beta.98. That dependency already declares `undici: ^8.7.0`, which
permits the fixed 8.10.0 — the vulnerable version survives purely as a stale
lockfile resolution. Nothing bumps it in place: `bun update` only walks direct
dependencies, `bun install --force` preserves a resolution that still satisfies
its range, and every platform-node release through beta.103 declares the same
`^8.7.0`, so moving the dep changes nothing. Bun rejects the scoped form outright
("Bun currently does not support nested resolutions"), so a flat `overrides` entry
is the only mechanism available, and it necessarily also moves sdk's top-level
undici from 7.x to 8.x.

That is safe here, and was verified rather than assumed. The only source use is
sdk/src/config.ts, which does `new Agent({ connect: { ca } })` behind a dynamic
import and a try/catch with a documented plain-fetch fallback; `Agent` and its
`connect` option are unchanged between undici 7 and 8. sdk typechecks and its 72
tests pass against 8.10.0; plugin-kit typechecks and its 20 tests pass. Both trees
now dedupe to a single undici 8.10.0.

Consumers are deliberately untouched: `overrides` apply only at the root of the
tree that declares them and are not honored when the package is installed as a
dependency, so sdk's published `optionalDependencies: { undici: "^7.0.0" }` is
left alone — a consumer resolves the latest 7.x, which is the fixed 7.29.0. The
override governs this repo's own tree, which is exactly what audit.yml checks.
Worth knowing: sdk's dev tree therefore exercises undici 8 while consumers get 7.

One trap found on the way. Running `bun install` over plugin-kit's existing
lockfile emitted a lockfile with two byte-identical `@punktfunk/host` entries —
its `file:../sdk` dependency crossed with the new override — and bun then refuses
its own output with "Error loading lockfile: InvalidPackageKey". That reads as a
tooling error rather than a finding, so it would have taken the audit gate down
while looking like something else entirely. Regenerating the lockfile from scratch
produces a valid single entry; all three lockfiles are checked for duplicate keys.

Also worth recording, because it nearly shipped: deleting the pinned nested entry
from a lockfile makes `bun audit` report "No vulnerabilities found" while the
vulnerable copy is still installed on disk. bun audit reads the lockfile, not
node_modules. That is a vacuous green, not a fix, and was rejected.

Verified: `bun audit` clean in all three trees; web builds and typechecks (its
typecheck needs the build first, which generates routeTree.gen); sdk 72/72 and
plugin-kit 20/20 tests pass.
2026-08-05 00:22:25 +02:00
16 changed files with 324 additions and 220 deletions
Generated
+32 -32
View File
@@ -947,7 +947,7 @@ dependencies = [
[[package]]
name = "cursor-probe"
version = "0.25.0"
version = "0.24.0"
dependencies = [
"anyhow",
"pf-capture",
@@ -1036,7 +1036,7 @@ dependencies = [
[[package]]
name = "display-disturb"
version = "0.25.0"
version = "0.24.0"
dependencies = [
"windows 0.62.2 (registry+https://github.com/rust-lang/crates.io-index)",
]
@@ -2221,7 +2221,7 @@ dependencies = [
[[package]]
name = "latency-probe"
version = "0.25.0"
version = "0.24.0"
[[package]]
name = "lazy_static"
@@ -2326,7 +2326,7 @@ dependencies = [
[[package]]
name = "libvpl-sys"
version = "0.25.0"
version = "0.24.0"
dependencies = [
"bindgen",
"cmake",
@@ -2361,7 +2361,7 @@ checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
[[package]]
name = "loss-harness"
version = "0.25.0"
version = "0.24.0"
dependencies = [
"punktfunk-core",
]
@@ -2850,7 +2850,7 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "pf-capture"
version = "0.25.0"
version = "0.24.0"
dependencies = [
"anyhow",
"ashpd",
@@ -2871,7 +2871,7 @@ dependencies = [
[[package]]
name = "pf-client-core"
version = "0.25.0"
version = "0.24.0"
dependencies = [
"anyhow",
"ash",
@@ -2898,7 +2898,7 @@ dependencies = [
[[package]]
name = "pf-clipboard"
version = "0.25.0"
version = "0.24.0"
dependencies = [
"anyhow",
"ashpd",
@@ -2916,7 +2916,7 @@ dependencies = [
[[package]]
name = "pf-console-ui"
version = "0.25.0"
version = "0.24.0"
dependencies = [
"anyhow",
"ash",
@@ -2937,7 +2937,7 @@ dependencies = [
[[package]]
name = "pf-encode"
version = "0.25.0"
version = "0.24.0"
dependencies = [
"anyhow",
"ash",
@@ -2961,7 +2961,7 @@ dependencies = [
[[package]]
name = "pf-ffvk"
version = "0.25.0"
version = "0.24.0"
dependencies = [
"ash",
"bindgen",
@@ -2970,7 +2970,7 @@ dependencies = [
[[package]]
name = "pf-frame"
version = "0.25.0"
version = "0.24.0"
dependencies = [
"anyhow",
"libc",
@@ -2982,7 +2982,7 @@ dependencies = [
[[package]]
name = "pf-gpu"
version = "0.25.0"
version = "0.24.0"
dependencies = [
"anyhow",
"pf-host-config",
@@ -2996,11 +2996,11 @@ dependencies = [
[[package]]
name = "pf-host-config"
version = "0.25.0"
version = "0.24.0"
[[package]]
name = "pf-inject"
version = "0.25.0"
version = "0.24.0"
dependencies = [
"anyhow",
"ashpd",
@@ -3029,14 +3029,14 @@ dependencies = [
[[package]]
name = "pf-paths"
version = "0.25.0"
version = "0.24.0"
dependencies = [
"tracing",
]
[[package]]
name = "pf-presenter"
version = "0.25.0"
version = "0.24.0"
dependencies = [
"anyhow",
"ash",
@@ -3051,7 +3051,7 @@ dependencies = [
[[package]]
name = "pf-update"
version = "0.25.0"
version = "0.24.0"
dependencies = [
"serde",
"serde_json",
@@ -3059,7 +3059,7 @@ dependencies = [
[[package]]
name = "pf-update-check"
version = "0.25.0"
version = "0.24.0"
dependencies = [
"anyhow",
"base64",
@@ -3071,7 +3071,7 @@ dependencies = [
[[package]]
name = "pf-vdisplay"
version = "0.25.0"
version = "0.24.0"
dependencies = [
"anyhow",
"ashpd",
@@ -3104,7 +3104,7 @@ dependencies = [
[[package]]
name = "pf-win-display"
version = "0.25.0"
version = "0.24.0"
dependencies = [
"anyhow",
"pf-paths",
@@ -3116,7 +3116,7 @@ dependencies = [
[[package]]
name = "pf-zerocopy"
version = "0.25.0"
version = "0.24.0"
dependencies = [
"anyhow",
"ash",
@@ -3324,7 +3324,7 @@ dependencies = [
[[package]]
name = "punktfunk-cli"
version = "0.25.0"
version = "0.24.0"
dependencies = [
"pf-client-core",
"punktfunk-core",
@@ -3335,7 +3335,7 @@ dependencies = [
[[package]]
name = "punktfunk-client-android"
version = "0.25.0"
version = "0.24.0"
dependencies = [
"android_logger",
"jni",
@@ -3353,7 +3353,7 @@ dependencies = [
[[package]]
name = "punktfunk-client-linux"
version = "0.25.0"
version = "0.24.0"
dependencies = [
"anyhow",
"async-channel",
@@ -3370,7 +3370,7 @@ dependencies = [
[[package]]
name = "punktfunk-client-session"
version = "0.25.0"
version = "0.24.0"
dependencies = [
"anyhow",
"pf-client-core",
@@ -3385,7 +3385,7 @@ dependencies = [
[[package]]
name = "punktfunk-client-windows"
version = "0.25.0"
version = "0.24.0"
dependencies = [
"async-channel",
"ffmpeg-next",
@@ -3405,7 +3405,7 @@ dependencies = [
[[package]]
name = "punktfunk-core"
version = "0.25.0"
version = "0.24.0"
dependencies = [
"aes-gcm",
"bytes",
@@ -3437,7 +3437,7 @@ dependencies = [
[[package]]
name = "punktfunk-host"
version = "0.25.0"
version = "0.24.0"
dependencies = [
"aes",
"aes-gcm",
@@ -3522,7 +3522,7 @@ dependencies = [
[[package]]
name = "punktfunk-probe"
version = "0.25.0"
version = "0.24.0"
dependencies = [
"anyhow",
"mdns-sd",
@@ -3536,7 +3536,7 @@ dependencies = [
[[package]]
name = "punktfunk-tray"
version = "0.25.0"
version = "0.24.0"
dependencies = [
"anyhow",
"ksni",
@@ -3559,7 +3559,7 @@ checksum = "d55d956fa96f5ec02be2e13af0e20391a5aa83d6a074e3ad368959d0fab299ea"
[[package]]
name = "pyrowave-sys"
version = "0.25.0"
version = "0.24.0"
dependencies = [
"bindgen",
"cmake",
+1 -1
View File
@@ -53,7 +53,7 @@ exclude = [
ndk = { path = "clients/android/native/vendor/ndk" }
[workspace.package]
version = "0.25.0"
version = "0.24.0"
edition = "2021"
rust-version = "1.82"
license = "MIT OR Apache-2.0"
+4 -4
View File
@@ -921,10 +921,10 @@ fn pad_render_thread(
const BLOCK_ALIGN: usize = PAD_CHANNELS * 4; // f32 interleaved
let enumerator = wasapi::DeviceEnumerator::new().context("DeviceEnumerator")?;
// Not `get_device`: that helper resolves through a freed string — see
// [`crate::audio_wasapi::device_by_id`].
let device =
crate::audio_wasapi::device_by_id(&enumerator, &Direction::Render, endpoint_id)
.map_err(|e| anyhow!("correlated endpoint not found: {e:#}"))?;
// [`crate::audio::device_by_id`] (audio_wasapi.rs, mounted as `crate::audio` on
// Windows by lib.rs's `#[path]` swap — there is no `audio_wasapi` module name).
let device = crate::audio::device_by_id(&enumerator, &Direction::Render, endpoint_id)
.map_err(|e| anyhow!("correlated endpoint not found: {e:#}"))?;
let mut audio_client = device.get_iaudioclient().context("IAudioClient")?;
// FL|FR|BL|BR: front pair = the pad's speaker, back pair = the voice coils.
let desired = WaveFormat::new(32, 32, &SampleType::Float, 48_000, PAD_CHANNELS, Some(0x33));
+120
View File
@@ -159,6 +159,17 @@ const CAP_REPROBE_WINDOWS_MAX: u32 = 128;
/// choke again at the same place, and only backoffs at a climbed-to rate can agree within the
/// band (a cascade's second backoff sits at ×0.7 of the first: outside it by construction).
const DECODE_CAP_SIMILAR_DIV: u32 = 8;
/// A deciding window that DELIVERED under `current / STARVED_DELIVERY_DIV` is STARVED: the
/// stream barely flowed (a host-side capture stall, an outage, a mid-window pause), so whatever
/// distress the window carries — a flush, a keyframe-ask burst — is starvation-shaped, not
/// rate-shaped, and the decoder decoded almost nothing at the nominal rate. Such a window may
/// still back off (real damage deserves the safe response) but must never be a decode-knee
/// sample: latching `current_kbps` off a starved window teaches a phantom decoder cap at
/// whatever rate the stall interrupted (the periodic-capture-stall field case: every 5 s cycle
/// offers another pair of "backoffs" at the same rate — a bogus latch that then fights the
/// re-probe ladder for minutes). Deliberately far below the ×¾ utilization bar climbs require:
/// the band between them is ambiguous and keeps today's behavior.
const STARVED_DELIVERY_DIV: u32 = 4;
/// Rolling window (in 750 ms report windows, ~30 s) whose minimum mean is the OWD baseline.
/// Long enough to remember the uncongested floor, short enough to follow genuine path changes.
const BASELINE_WINDOWS: usize = 40;
@@ -697,6 +708,10 @@ impl BitrateController {
|| self.streak_decode_windows >= BAD_WINDOWS_TO_DECREASE
|| (recovery_kf >= RECOVERY_KF_BAD && loss_ppm < HEAVY_LOSS_PPM)
|| (flushed && (decode_bad || decode_mean_us.is_none()));
// Starved deciding window (see [`STARVED_DELIVERY_DIV`]): the stream barely flowed,
// so the window says nothing about what the decoder can hold at this rate.
let starved =
(actual_kbps as u64) * (STARVED_DELIVERY_DIV as u64) < self.current_kbps as u64;
if !self.climb_since_backoff {
// Still draining the previous backoff: the host acks a ×0.7 request in ~100 ms,
// so this window's rate is one the decoder never choked at while keeping up —
@@ -708,6 +723,17 @@ impl BitrateController {
"adaptive bitrate: backoff without an intervening climb — draining the \
previous choke, not a knee sample"
);
} else if starved {
// Same "not a knee sample either way" treatment as the draining arm: neither
// latch against a starved window nor let it erase the reference a real knee
// set — the next genuine choke at that rate must still find its pair.
tracing::debug!(
at_kbps = self.current_kbps,
actual_kbps,
reference_kbps = self.decode_backoff_kbps,
"adaptive bitrate: backoff in a starved window (delivery a fraction of \
the target) — starvation-shaped distress, not a knee sample"
);
} else if decode_evidence {
let rate = self.current_kbps;
let similar = self.decode_backoff_kbps > 0
@@ -2084,6 +2110,100 @@ mod tests {
rate - rate / 16
}
/// One capture-stall-shaped window at the current rate: almost nothing delivered
/// (current/10), nothing decoded, no loss — but a jump-to-live flush and a keyframe-ask
/// storm (the stall edge's damage signature). SEVERE, so it backs off; STARVED, so it must
/// never be a knee sample.
fn stall_choke(c: &mut BitrateController, start: Instant, tick: &mut u32) -> Option<u32> {
*tick += 2;
let r = c.on_window(
ticks(start, *tick),
0,
0,
None,
None,
None,
c.current_kbps / 10,
true,
RECOVERY_KF_SEVERE,
);
*tick += 1;
r
}
#[test]
fn capture_stall_windows_never_latch_a_decode_cap() {
// The periodic-capture-stall field case (RDNA4 standby-sink, 5 s cycle): every stall
// edge offers another flush + kf-storm "backoff" at the SAME rate — without the starved
// guard that pair latches a phantom decoder knee at whatever rate the display driver
// happened to interrupt, and the session then fights the re-probe ladder for minutes.
let mut c = BitrateController::new(240_000);
c.set_ceiling(900_000);
let start = Instant::now();
let mut t = 0;
for _ in 0..4 {
calm_window(&mut c, ticks(start, t));
t += 1;
}
climb_to(&mut c, start, &mut t, 400_000);
let at = c.current_kbps;
let r1 = stall_choke(&mut c, start, &mut t).expect("stall damage still backs off");
assert!(
c.decode_cap_kbps.is_none(),
"one starved window must not latch"
);
assert_eq!(
c.decode_backoff_kbps, 0,
"a starved window is not a knee sample — no reference recorded"
);
c.on_ack(r1);
climb_to(&mut c, start, &mut t, at - at / DECODE_CAP_SIMILAR_DIV);
let r2 = stall_choke(&mut c, start, &mut t).expect("second stall edge backs off too");
c.on_ack(r2);
assert!(
c.decode_cap_kbps.is_none(),
"a starved pair at the same rate must not latch a phantom knee"
);
}
#[test]
fn starved_window_preserves_the_knee_reference() {
// A REAL knee sample, then a stall edge, then the genuine re-climb choke: the starved
// window in the middle must neither latch nor ERASE the reference the real choke set —
// the genuine pair must still find each other around it.
let mut c = BitrateController::new(500_000);
c.set_ceiling(900_000);
let start = Instant::now();
let mut t = 0;
for _ in 0..4 {
calm_window(&mut c, ticks(start, t));
t += 1;
}
let knee = c.current_kbps;
let r1 = choke(&mut c, start, &mut t).expect("real choke backs off");
assert_eq!(
c.decode_backoff_kbps, knee,
"real choke records the reference"
);
c.on_ack(r1);
climb_to(&mut c, start, &mut t, knee - knee / DECODE_CAP_SIMILAR_DIV);
let r2 = stall_choke(&mut c, start, &mut t).expect("stall edge backs off");
assert_eq!(
c.decode_backoff_kbps, knee,
"the starved window must not erase the real reference"
);
assert!(c.decode_cap_kbps.is_none(), "and must not latch against it");
c.on_ack(r2);
climb_to(&mut c, start, &mut t, knee - knee / DECODE_CAP_SIMILAR_DIV);
let rate = c.current_kbps;
choke(&mut c, start, &mut t).expect("genuine re-climb choke backs off");
assert_eq!(
c.decode_cap_kbps,
Some(rate - rate / 16),
"the genuine pair still latches around the starved interruption"
);
}
#[test]
fn decode_cap_latches_when_the_reclimb_chokes_at_the_same_knee() {
// The 1440p120 field sawtooth: a decoder knee (~500 Mbps) well under the (inflated)
+32 -43
View File
@@ -441,26 +441,27 @@ fn idd_adaptive_enabled() -> bool {
/// Seal one access unit and send it with MICROBURST pacing (the shared
/// [`send_pacing`](crate::send_pacing) policy, native parameterization): the first `burst_cap`
/// bytes go out immediately (one absorbed burst the NIC / socket tx-buffer can swallow), and
/// only the OVERFLOW beyond that is spread across `min(~90% of the time to deadline, the time
/// the overflow needs at pace_rate_bps)` in ADAPTIVE chunks — 16 packets at today's rates,
/// coarsening to at most 64 (the GSO-segment cap) once the rate would otherwise skip every
/// sub-floor sleep, so ≥1 Gbps frames still pace instead of collapsing into an unpaced blast
/// (plan Phase 1.2). `burst_cap` `None` = auto: `max(128 KB, this AU's wire bytes / 4)`, so
/// the burst stays a bounded fraction of a high-rate frame instead of swallowing it whole
/// (plan Phase 1.3); `Some` = PUNKTFUNK_PACE_BURST_KB pinned an absolute cap. So a
/// normal-bitrate frame (≤ cap) leaves in one immediate burst at ~0 added latency, while a
/// genuine IDR / sustained-high-bitrate frame (≫ cap) still spreads — keeping the freeze fix
/// exactly where it's needed (an unpaced line-rate burst overruns the kernel tx buffer →
/// EAGAIN drop → under infinite GOP, a freeze until the next keyframe). With no slack
/// (encode ≈ interval) the budget collapses to 0 and even the overflow goes out immediately,
/// so this is never slower than unpaced.
/// only the OVERFLOW beyond that is spread across the time it needs at `pace_rate_bps` in
/// ADAPTIVE chunks — 16 packets at today's rates, coarsening to at most 64 (the GSO-segment
/// cap) once the rate would otherwise skip every sub-floor sleep, so ≥1 Gbps frames still pace
/// instead of collapsing into an unpaced blast (plan Phase 1.2). `burst_cap` `None` = auto:
/// `max(128 KB, this AU's wire bytes / 4)`, so the burst stays a bounded fraction of a
/// high-rate frame instead of swallowing it whole (plan Phase 1.3); `Some` =
/// PUNKTFUNK_PACE_BURST_KB pinned an absolute cap. So a normal-bitrate frame (≤ cap) leaves in
/// one immediate burst at ~0 added latency, while a genuine IDR / sustained-high-bitrate frame
/// (≫ cap) still spreads — keeping the freeze fix exactly where it's needed (an unpaced
/// line-rate burst overruns the kernel tx buffer → EAGAIN drop → under infinite GOP, a freeze
/// until the next keyframe).
///
/// `pace_rate_bps` (latency plan T1.2) bounds the spread from above: the deadline term alone
/// smears a big frame's tail across the whole remaining interval (~15 ms at 60 fps) even when
/// the link could drain it in 23 ms. The caller passes ~3× the live encoder bitrate — a rate
/// the link is proven to carry sustained, so the bounded excursion keeps the anti-freeze
/// property while the tail leaves as soon as the link plausibly allows. `0` = uncapped
/// (legacy smoothness-only spread, and the fallback when the bitrate isn't known yet).
/// `pace_rate_bps` (latency plan T1.2; resume-safe form, stall program T2): the caller passes
/// ~3× the live encoder bitrate — a rate the link is proven to carry sustained — and the
/// overflow's wire time at that rate IS the pace budget ([`crate::send_pacing::native_budget`],
/// [`crate::send_pacing::MAX_PACE_SPREAD`]-bounded). The frame deadline no longer under-cuts
/// the spread: for a steady-state frame the rate term was the smaller one anyway (tail gone in
/// a fraction of the interval), and for an oversized frame (stall-resume scene delta, cold
/// IDR) the old deadline clamp was exactly the line-rate blast → tx-overrun → freeze path this
/// module exists to prevent. `0` = uncapped legacy deadline-only spread
/// (PUNKTFUNK_PACE_FACTOR=0, and the fallback when the bitrate isn't known yet).
#[allow(clippy::too_many_arguments)]
fn paced_submit(
session: &mut Session,
@@ -498,34 +499,22 @@ fn pace_sealed(
chunk: crate::send_pacing::ChunkPolicy::Adaptive { base: 16, max: 64 },
sleep_floor: std::time::Duration::from_micros(500),
};
// T1.2 rate cap: the overflow's wire time at `pace_rate_bps`. Only the bytes past the
// burst pace at all, so only they bound the budget.
// T1.2 rate cap, resume-safe form (stall program T2): the overflow's wire time at
// `pace_rate_bps` IS the budget — the deadline no longer under-cuts it, so an oversized
// frame (a stall-resume scene delta, a cold IDR) paces at the proven 3× rate instead of
// collapsing into a line-rate blast that overruns the socket buffer and loses the very
// frame that ends a freeze. See `send_pacing::native_budget` for the full argument.
let overflow_bytes = wire_bytes.saturating_sub(burst_bytes) as u64;
let cap = if pace_rate_bps > 0 && overflow_bytes > 0 {
std::time::Duration::from_nanos(
(overflow_bytes * 8).saturating_mul(1_000_000_000) / pace_rate_bps,
)
} else {
std::time::Duration::MAX
};
let budget = crate::send_pacing::native_budget(deadline, pace_rate_bps, overflow_bytes);
// Time the socket handoff per chunk and fold it into the session's SealPerf split — the
// sleeps between chunks stay excluded, so sock_ns is pure send_gso/sendmmsg time.
let mut sock_ns = 0u64;
let result = crate::send_pacing::pace_frame(
&refs,
crate::send_pacing::PaceBudget::UntilDeadline {
deadline,
fraction: 0.9,
cap,
},
&cfg,
|chunk| {
let t0 = std::time::Instant::now();
let r = session.send_sealed(chunk).map(|_| ());
sock_ns += t0.elapsed().as_nanos() as u64;
r
},
);
let result = crate::send_pacing::pace_frame(&refs, budget, &cfg, |chunk| {
let t0 = std::time::Instant::now();
let r = session.send_sealed(chunk).map(|_| ());
sock_ns += t0.elapsed().as_nanos() as u64;
r
});
drop(refs); // release the borrow of `wires` so it can return to the seal pool
session.reclaim_wires(wires);
session.note_sock_ns(sock_ns);
+82 -2
View File
@@ -55,7 +55,7 @@ pub(crate) enum ChunkPolicy {
}
/// The time the paced (post-burst) packets spread across.
#[derive(Clone, Copy, Debug)]
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) enum PaceBudget {
/// `min((deadline now-after-burst) × fraction, cap)`, collapsing to 0 with no slack
/// (native: fraction 0.9). `cap` bounds the spread to the time the overflow actually needs
@@ -68,10 +68,53 @@ pub(crate) enum PaceBudget {
fraction: f32,
cap: Duration,
},
/// A precomputed fixed budget (GameStream: ¾ of the frame interval).
/// A precomputed fixed budget (GameStream: ¾ of the frame interval; native: the rate-cap
/// spread from [`native_budget`]).
Fixed(Duration),
}
/// Absolute ceiling on one frame's paced spread (native plane): a pathological frame must not
/// park the send thread for longer than this, whatever the rate math says. At the ceiling the
/// tail is late but delivered whole — still strictly better than the blast-loss → freeze →
/// recovery-IDR round trip it replaces.
pub(crate) const MAX_PACE_SPREAD: Duration = Duration::from_millis(100);
/// The native plane's pace budget for one frame (pure — unit-tested): with the T1.2 rate cap
/// active, the paced overflow spreads across exactly the time it needs at the pace rate
/// (`cap`, bounded by [`MAX_PACE_SPREAD`]) and is NEVER under-cut by the frame deadline.
///
/// The old schedule took `min(0.9 × time-to-deadline, cap)`. For a steady-state frame the cap
/// is the smaller term and nothing changes. But for an OVERSIZED frame — a stall-resume scene
/// delta after seconds of frozen composition, a cold IDR — the overflow needs SEVERAL frame
/// intervals at the pace rate, and the deadline term clamped that into the remainder of ONE:
/// an instantaneous many-×-stream-rate blast that overruns the socket tx-buffer and loses the
/// very frame that would have ended the freeze (field fingerprint: WSAENOBUFS 10055 +
/// `loss_ppm` spikes at capture-stall edges, then a recovery-IDR round trip per retry). The
/// pace rate is ~3× a rate the link demonstrably carries, so holding it past the deadline is
/// safe by the same argument that introduced the cap — the deadline stays a *target*, not a
/// license to blast.
///
/// `pace_rate_bps == 0` (PUNKTFUNK_PACE_FACTOR=0) or an overflow-free frame keeps the legacy
/// deadline-only spread.
pub(crate) fn native_budget(
deadline: Instant,
pace_rate_bps: u64,
overflow_bytes: u64,
) -> PaceBudget {
if pace_rate_bps > 0 && overflow_bytes > 0 {
let cap = Duration::from_nanos(
(overflow_bytes * 8).saturating_mul(1_000_000_000) / pace_rate_bps,
);
PaceBudget::Fixed(cap.min(MAX_PACE_SPREAD))
} else {
PaceBudget::UntilDeadline {
deadline,
fraction: 0.9,
cap: Duration::MAX,
}
}
}
/// Per-plane pacing parameters. See the module doc for the two canonical values.
#[derive(Clone, Copy, Debug)]
pub(crate) struct PaceCfg {
@@ -598,6 +641,43 @@ mod tests {
);
}
/// [`native_budget`]: with the rate cap active the budget is the overflow's wire time at
/// the pace rate — a FIXED spread the deadline can no longer under-cut — bounded by
/// [`MAX_PACE_SPREAD`]; rate 0 / no overflow keep the legacy deadline-only schedule.
#[test]
fn native_budget_is_rate_bound_never_deadline_cut() {
// The stall-resume case the fix exists for: a 3 MB overflow at 3×240 Mbps needs
// ~33 ms — an IMMINENT deadline (the old min() made this a blast) must not shrink it.
let deadline = Instant::now() + Duration::from_millis(4); // 240 fps interval
let b = native_budget(deadline, 720_000_000, 3_000_000);
assert_eq!(b, PaceBudget::Fixed(Duration::from_nanos(33_333_333)));
// A steady-state frame: overflow 90 KB at 3×240 Mbps = 1 ms — identical to what the
// old min(slack, cap) chose (cap was the smaller term), so nothing regresses.
let b = native_budget(deadline, 720_000_000, 90_000);
assert_eq!(b, PaceBudget::Fixed(Duration::from_micros(1_000)));
// A crater-rate resume (ABR backed off to 20 Mbps, pace 60 Mbps): the raw rate math
// says 400 ms for 3 MB — the absolute ceiling bounds the send thread's stall.
let b = native_budget(deadline, 60_000_000, 3_000_000);
assert_eq!(b, PaceBudget::Fixed(MAX_PACE_SPREAD));
// Rate cap off (PUNKTFUNK_PACE_FACTOR=0): the legacy deadline-only spread, uncapped.
let b = native_budget(deadline, 0, 3_000_000);
assert!(matches!(
b,
PaceBudget::UntilDeadline {
fraction,
cap: Duration::MAX,
..
} if fraction == 0.9
));
// No overflow (the whole frame bursts): budget is never consulted — legacy shape.
let b = native_budget(deadline, 720_000_000, 0);
assert!(matches!(b, PaceBudget::UntilDeadline { .. }));
}
/// `inject_video_drop` is a no-op when the knob is off (the default test env).
#[test]
fn drop_injection_off_by_default() {
-81
View File
@@ -1,81 +0,0 @@
Wire-compatible with 0.24.x — everything you have already paired keeps working, and you can update one side at a time. Nothing here changes how a host and a client agree on what to send each other, so an old client on a new host (or the other way round) streams exactly as it does today; the parts that are new switch themselves on only once both ends have them.
The headline is that a **DualSense plugged in by USB can now play a game's fine-grained haptics — the textured detail in the grips, not just the rumble motors — and its own speaker, streamed from the host**. That needs a Windows host with Steam installed and either the Android app or the desktop session client; everywhere else, nothing changes.
Behind it, three fronts. **Controllers** were swept end to end: rumble that faded on a Steam Deck, died for good after one hiccup on a phone, or kept buzzing after you quit; adaptive triggers and lightbars left stuck in a game's last state on your desk after the stream ended; player-number lights that never lit on anything but a DualSense — more than twenty separate faults, across every client and both hosts. **Sound** got the same treatment: desktop audio is encoded at roughly double the bitrate, hosts stopped routing the entire game mix through Steam's voice channel on PCs that had it installed, and audio that drifts behind the picture now pulls itself back instead of staying late for the rest of the session. And the **black screen** that some people hit on VPN-shaped networks — a session that connects, reports every gauge healthy, and then shows nothing at all, forever — is finally diagnosed, explained in the log, and healed on its own. Alongside those: the Steam Deck plugin is rebuilt as a launcher into the app, holding Select on any controller presses the host's Guide button, and saved settings profiles can be pinned to hosts without a mouse.
## New
- **Your DualSense's own haptics, carried from the host.** Games that drive the DualSense's fine-grained voice coils — the detailed, textured feedback in the grips, as distinct from the coarse rumble motors — now carry that across the stream to the controller in your hands, and the pad's built-in speaker can be carried with it. It needs all of: a DualSense or DualSense Edge **plugged into your device by USB** (over Bluetooth the pad exposes no audio device to play into, so there is nothing this can do), a **Windows host with Steam installed** (the per-controller audio device is built on Valve's Remote Play streaming-speakers driver), and either the Android app or the desktop session client. Anywhere else — a Linux host, the iPhone/iPad/Mac app, the ordinary Windows or Linux desktop app, a Bluetooth pad — nothing changes at all. A game that uses only ordinary rumble keeps rumbling exactly as it does today. On Android the controls are **Controller haptics** and **Controller speaker** under Controllers, alongside a **Test haptics** button that checks your phone can drive the pad at all without needing a stream running. Expect a new playback device named "DualSense Wireless Controller" to appear in the host's Windows sound settings — that is this feature, it is how games find the controller's speaker, and it will not take over as your default output.
- **Hold Select to press the host's Guide button.** Hold Select (Back / View) on its own for about a third of a second and the host sees its Guide button go down — and it stays down while you hold, so a longer hold reads as a long-press on the host, which is how a big-screen host opens its Quick Access Menu. A quick tap of Select still goes to the game, and Select as part of a combo — including the leave chord — passes through untouched. It is on by default on iPhone, iPad and Apple TV, where the system keeps the controller's own Home press for itself and this is the only reliable route to the host's overlay. Everywhere else the raw press already reaches the host, so the gesture stays off by default and Select keeps its exact timing. Update the client.
- **Get onto a host by asking, instead of typing a PIN.** From the Steam Deck panel, tapping a locked host now offers **Request access**: the stream opens and waits while whoever is at the host approves your Deck in its console, then the picture comes up by itself. It gives up after about three minutes like any failed connection. Offered only for hosts visible on your network — one you saved by typing an address has no advertised identity to check against, so those still use a PIN, and the sheet says why. Update the plugin; hosts already knew how to approve.
- **Pin a settings profile to a host from a controller.** Every controller-driven settings screen — the Deck and Linux console home, the Apple app's gamepad UI including Apple TV, and the Android app's controller UI including Android TV — gains a **Profiles** section showing each profile and where it is pinned ("Not pinned", "Pinned to 2 hosts"). Open one and press A on a host to pin or unpin. On Apple TV this is the only profile management there has ever been; on Android, pinning previously needed a touchscreen. Creating and editing profiles is still a desktop or phone job. Update the client.
- **Pinned profiles appear as their own cards on the console home.** A pinned profile shows up as an extra card right after its host, subtitled with the profile's name, and one press connects using those settings. A host already bound to a profile now names it next to its address, so you can see which settings a plain press will use. Update the client.
- **A lost audio packet is rebuilt exactly instead of being papered over.** Each audio packet can carry a copy of the one before it, so a single loss is reconstructed bit-for-bit rather than concealed with a synthesized approximation you can hear. It costs no extra delay — the copy rides on a packet that was already arriving in time. Needs 0.25.0 on both ends; with either side older, audio goes over the wire exactly as it did before.
- **Audio quality is now budgeted against your connection.** The higher bitrate and the packet redundancy above are worth having on a roomy link and much too expensive on a narrow one, and audio is not managed by the Automatic bitrate control — whatever it takes comes off the top. The host now picks quality and redundancy together against the session's video bitrate: full quality plus redundancy where there is room, redundancy dropped first as the link narrows, then the quality tier, never below a floor. Update the host.
- **Turn on Sony USB passthrough from a TV.** The DualSense / DualShock USB toggle only ever existed on the touch settings screen, so on an Android TV box there was no way to reach it at all. It now sits on the controller-driven screen beside the Steam Controller toggle. Update the client.
- **Press the host's Steam and Quick Access buttons from the Deck panel.** While a stream is running the panel shows a **Host menus** section with **Steam menu on host** and **Quick access on host**; either one presses that button on the host and closes the Deck's own menu so the host's shows through. Update the plugin and the client.
- **Two new command-line tools.** `punktfunk discover` lists the hosts on your network with their addresses, whether you have already saved them and whether you are paired, with a `--json` mode for scripts. `punktfunk launch <host> --request-access` is the Request access flow above from a terminal, for admitting a headless machine without a PIN. Update the client.
- **Hosts on a jumbo-frame network can opt into much larger video packets.** On a LAN deliberately configured end to end for 9000-byte frames, the host can send roughly six times fewer packets per frame. It is off by default, is only applied after the host has proven the path really carries them and the client has agreed, and it reverts on its own if those packets start disappearing. This is not a general speed-up: on an ordinary network it does nothing.
## Improved
- **Desktop audio is encoded at roughly double the bitrate.** Streamed sound now runs at 256 kbps in stereo rather than 128 kbps, which costs about one percent of what the video is already using. Because Punktfunk sends very short audio frames to keep latency down, the old rate was leaving real quality on the table — most audibly on music. Update the host; every existing client already plays whatever arrives.
- **The black screen now heals in seconds, mid-stream.** With 0.25.0 on both ends, a host that detects a constrained network path re-sizes the video packets of the session you are already in, a few seconds after diagnosing it — the picture simply appears, without you reconnecting. With a 0.25.0 host and an older client you still get the fix below: the session in progress stays black, but the next connection is sized correctly and works.
- **Hosts you reach over a VPN show as online on the Steam Deck.** The panel's list merges what it finds on the network with the hosts you have saved and probes the saved ones directly, so a box that never advertises itself — over Tailscale, or on another subnet — reads as up instead of unreachable. Rows sort online first, then most recently streamed.
- **Waking a sleeping host from the Deck waits for it properly.** The panel used to send the wake-up and then guess how long to wait before dialling. It now waits for the host to actually answer.
- **Two new troubleshooting sections on audio.** One explains what the host actually captures and why streamed sound can be worse than what you hear on the host itself — naming the Steam Streaming Microphone trap explicitly and showing the log line that identifies it. The other covers audio that lags the picture, why it should now correct itself, and what to check when it does not.
## Fixed
- **A host on a network that carries smaller packets than usual no longer streams a permanent black screen.** Everything small got through — the connection, your input, your sound — while every single video packet was slightly too big for one hop and died silently. The result connected fine, reported zero packet loss on the client, showed every gauge green on the host, and displayed nothing at all, with nothing written to either log to say why. The host now measures what the path to each client can really carry, warns with the actual diagnosis when it cannot carry full-size video, and remembers the measurement so the next connection from that client is sized to fit. The usual cause, and the one the warning names, is a VPN or overlay network adapter claiming the route. Update the host — this works with every client already out there.
- **Audio that falls behind the picture pulls itself back.** Every client kept a small buffer to absorb network jitter, and that buffer could only ever grow: one burst of Wi-Fi interference, one stutter on the host, or simply two devices' clocks running at fractionally different speeds pushed sound permanently behind the video, and the only cure was reconnecting. Android was worst, with no correction at all — it settled at its ceiling and stayed there for the whole session. All four clients now trim the buffer back a few milliseconds at a time under a crossfade, which is inaudible. Update the client; an older one keeps drifting no matter how new the host is.
- **The host stopped pushing your whole desktop mix through Steam's voice channel.** On a PC with Steam installed, the host was capturing Steam's Streaming Microphone device because it is silent on the host — but that device exists to carry voice, and if Windows had it set to mono or below 48 kHz, the entire game mix was squeezed through it before encoding, where no amount of bitrate could bring it back. A silent device now has to prove it can carry full-quality sound before being preferred over real hardware, and if nothing better exists the host says so in the log. Update the host.
- **Sound no longer cuts out over and over when something keeps changing your default playback device.** Some applications re-set the Windows default device every few seconds; each time, the host tore the whole capture down and rebuilt it, which is an audible dropout — one field log shows seven in sixteen seconds. The host now restores the default without dropping the stream, and if it happens repeatedly it stops fighting for a minute and says so once. Update the host.
- **Audio the host dropped internally is no longer silently glued over.** When the encoder fell behind, captured sound was discarded with nothing recording it: you heard a click, and everything after it stayed permanently shifted. Those drops are now counted and warned about, so a quiet host, a broken device and a stream damaging itself no longer look identical. Update the host.
- **Automatic bitrate stops sawtoothing when your device's decoder, not the network, is the limit.** The control loop has a mechanism for learning "this device cannot decode much past here, stop trying", and in practice it never once fired — one recording at 1440p120 shows it swinging between 220 and 450 Mb/s for nine solid minutes without ever learning the lesson. Three separate reasons it was unreachable are fixed, including one where a struggling decoder repeatedly asking for a fresh picture on an otherwise clean link was blamed on the network. Update the client.
- **Rumble stops fading in and out on a Steam Deck.** The Deck's motors need a fresh instruction every 40 ms or the repeat is discarded, and renewals kept colliding with that, stretching the real gap between motor writes to two and a half times what it should be — so sustained rumble came through weak and uneven. Update the client.
- **Rumble survives a hiccup instead of dying for the rest of the session.** On Android a single failure from the phone's vibration service silently killed the thread driving rumble, with nothing to notice or restart it, so rumble was gone until you restarted the app. And on every client, a stop instruction that never reached the controller used to be assumed to have worked — over USB there is no firmware timeout behind that, so a dropped stop left the motors running with nothing scheduled to try again. Update the client.
- **Two DualSenses stop rumbling for each other.** With two connected to an iPhone, iPad, Mac or Apple TV, both could end up driving the same physical controller, so one player's rumble came out of the other player's pad and the two fought over it. Each now drives its own. Very light rumble also stopped vanishing on that path — anything under about half a percent was being rounded away to nothing. Update the client.
- **A controller is handed back to you neutral when the stream ends.** Trigger resistance, lightbar colour and player lights live in the controller's own firmware, so they outlast the stream, the app, and even unplugging. Ending a session while a game held a weapon's trigger resistance left that trigger physically stiff on your desktop afterwards, with the lightbar still showing the game's last colour. Every client now releases both triggers, darkens the lightbar and clears the player lights on the way out — including on the exit paths that previously skipped it and left the pad buzzing after the stream was gone. Update the client.
- **A dropped lightbar or trigger change repairs itself instead of sticking.** These were sent once, when they changed, over packets that can be lost — so one lost packet could strand a controller on the previous weapon's trigger effect, or the last scene's lightbar colour, potentially for the rest of the level. The host now re-sends the current state once a second to repair it. Update the host.
- **A cut-off packet no longer cancels a trigger effect a game is holding.** A truncated adaptive-trigger packet decoded as an empty effect, and an empty effect is exactly what a controller reads as "let go" — so a weapon's resistance could silently vanish mid-fight. That shape is now rejected, while a genuine release still works.
- **Player-number lights work on controllers that are not a DualSense.** Xbox pads, Switch Pro controllers and everything else with player lights ignored the host's player number completely, so nothing lit at all. Update the client.
- **A centred stick reads as centred.** When the host presents your controller to games as a DualSense, DualSense Edge or DualShock 4, both sticks' vertical axes sat one step below true centre — a permanent, very slight downward pull, small enough to hide under most games' deadzones but plainly visible to any game reading the raw axis. Triggers on a controller presented as a Steam Deck pad also topped out just short of a full pull, so anything needing a genuine full press could never fire. Both are now exact. Update the host.
- **Two virtual controllers stop corrupting each other's rumble on a Windows host.** When a game drove two pads hard enough for their updates to overlap, two rumble instructions could be written into the same slot and arrive as one garbled instruction, or one could be skipped outright — and a skipped *stop* is the one that hurts, leaving the pad buzzing until a safety timer noticed the game had gone quiet. Update the host.
- **Delayed rumble effects fire at the right moment on a Linux host.** Games that schedule an effect to start after a short delay — routine for older Windows games running through Proton — had it start early and end early by the same amount, because the delay was read and then never applied. An effect still waiting its turn is also no longer cancelled by the idle safety-off before it has been felt. Update the host.
- **A controller driver that failed to attach no longer stalls the stream while the host works out why.** The diagnosis ran a slow system lookup on the very thread feeding controller input and rumble — up to two seconds per affected pad, at exactly the moment a session was already going wrong. It now runs in the background, and because it is off the critical path it can afford to wait long enough to report what it actually found. Update the host.
- **The Steam Deck keeps its trackpad mouse when a stream starts.** Starting a stream killed the built-in trackpad-as-mouse system-wide, and it only returned seconds later when the controller's own firmware watchdog restored it. Update the client.
- **Controller settings you cannot use no longer look live.** With "Forward controllers" off, the rows beneath it have nothing to act on, but on the Windows app and both controller-driven settings screens they stayed fully interactive — so you could sit there changing settings that did nothing. They are now dimmed until forwarding is back on. On Apple devices, starting a stream with forwarding off also stopped claiming every button's system gesture (which took away your screenshot and Home presses) and stopped powering up the controller's motion sensors for a stream that was not forwarding anything. Update the client.
- **A leftover folder from an uninstalled Sunshine or Apollo is no longer treated as a conflict.** Both uninstallers leave a settings folder behind, and Punktfunk counted any trace at all — a leftover folder, a file on disk, a registered but switched-off background service — as a live clash. Affected machines warned on every start and showed a red card in the web console reading that another streaming server was running, when nothing was. Only a server that is genuinely running, or set to start on its own, counts now; the console names exactly what it saw, and leftovers appear in the full report under a heading saying they need no action. Update the host.
- **A crashed host gives you your screen back.** In Exclusive display mode the host switches your own monitors off for the length of a session and back on when it ends. If the host crashed or was killed mid-session that never happened — the desk simply stayed dark, no timeout brought it back, and the way out was Windows' own display shortcut or a reboot. The host now records which screens it is about to switch off before switching them off, and forces every connected display back on the next time it starts. Recovery happens at that next start, not on a timer: if the host stays down, the screen stays dark until it runs again. Update the host.
- **Camera look survives pressing Escape on an iPad.** Pressing Escape mid-stream made iPadOS hand the pointer back to the system, and Punktfunk never took it back. Clicks kept landing exactly where you aimed, so input looked fine — but the game stopped receiving mouse movement, so camera look was dead for the rest of the session. Clicking back into the video now takes the pointer again, and if the system refuses the first time, the next click tries again. Update the client.
- **"Open log folder" on Windows opens the log folder.** On installed builds it opened your Documents folder instead: the path the client handed to Explorer was correct to write to but did not exist as a real folder, and Explorer quietly fell back. The same wrong path appeared in the startup line naming the log file and in the message shown when a session fails to start. All three now point at the real folder. Update the client.
## If you stream from a Steam Deck
The Decky plugin has been rebuilt as a **launcher**. It no longer contains a second, separate streaming client; it is now a short list of your hosts plus one button into the Punktfunk app, which has the full controller-driven interface. This makes the plugin far smaller and means the Deck stops having two implementations of everything that could disagree with each other — but some things genuinely moved, and one was removed:
- **Settings moved** to **Open Punktfunk → Settings**. Same rows, same saved values, still fully controller-navigable.
- **Adding, renaming and forgetting hosts moved** to **Open Punktfunk → Add host**.
- **Browsing a host's games moved** to **Open Punktfunk → Library**.
- **Pinned Games has been removed, with nothing to migrate to yet.** The panel's one-tap "Stream *game*" rows are gone: pinning now works on a host and a settings profile rather than on a game. Your old pin file is deliberately left alone on disk so a later release can migrate it, but in 0.25.0 those rows do not appear.
- **The Deck's Steam and `…` buttons now stay with the Deck.** One press used to open both menus at once, the Deck's own covering the stream, because SteamOS reacts to those buttons whatever the app does. Reach the host's menus with hold-Select, or the panel's new **Host menus** buttons. To restore the old behaviour, set **Open Punktfunk → Settings → Steam / guide button** to **Send to host**.
- **The plugin needs the Punktfunk client on the Deck to be 0.22.0 or newer**, because it drives everything through the client. An older one is detected explicitly and the panel offers the update button that fixes it, rather than silently showing an empty list.
## Under the hood (for developers)
- **Wire protocol 2 — unchanged**, despite substantial growth, because every addition is optional or capability-gated. What grew without a bump: an optional trailing `max_shard_payload: u16` on `Hello` (absent/0 = legacy, and it doubles as both the renegotiation capability flag and the jumbo receive ceiling); two new control messages `ShardPayloadChanged` (`0x08`) and `ShardPayloadAck` (`0x09`); a redundant desktop-audio datagram tag `0xD2` alongside the plain `0xC9`; a controller-audio plane at `0xD1` (`[0xD1][u8 pad][u8 kind][u32 seq LE][u64 pts_ns LE][opus payload]`, which is why `0xD2` skipped that value); and `MAX_DATAGRAM_BYTES` 2048 → 9216.
- **C ABI 14 → 16**, in two steps. **15** is unusual: no code changed and no symbol was added with it. It retroactively versions the shared rumble policy engine's C surface — `punktfunk_connection_next_rumble_cmd`, `punktfunk_connection_set_rumble_quirks` and the `PUNKTFUNK_RUMBLE_QUIRK_*` bits — which shipped while the constant still read 7 and never got one, so every core since has exported those symbols while advertising a version that did not promise them. That cannot be fixed retroactively, so 15 is declared as the **floor that guarantees** the surface: at or above 15 it is present, below it an embedder must probe for the symbol. **16** adds the controller-audio surface and mirrors its two capability bits into the C ABI.
- **Breaking for C embedders: 149 unprefixed macros are now `PUNKTFUNK_`-prefixed** (139 `#define`s renamed in the checked-in header). Names as generic as `MAX_PADS`, `TAG_LEN`, `ABI_VERSION`, `WIRE_VERSION`, `INPUT_MAGIC` and the whole `BTN_*` / `AXIS_*` family were landing in the namespace of every program that included the header. Fixing it is mechanical — add the prefix, the values are identical — and there is **no silent breakage**: the old spellings cease to exist, so it is always an undeclared-identifier error, never a wrong value. That is precisely the failure it removes, since a colliding `#define` does not fail to compile; the preprocessor silently takes the last definition, so an embedder whose own header defined `MAX_PADS` previously got a wrong value at runtime. Associated constants are untouched — the generator already qualifies those with their type name. Scheduled for a release boundary deliberately; nothing in-tree used the old spellings but one Swift test, updated in the same commit.
- **Four new capability bits, and the video-caps byte did not overflow.** In the handshake's client/host capability bytes: client `0x04` / host `0x20` for the redundant desktop-audio plane ("can decode it" / "is sending it"), and client `0x08` / host `0x40` for controller audio (`CLIENT_CAP_PAD_AUDIO` / `HOST_CAP_PAD_AUDIO`, mirrored into the C ABI as `PUNKTFUNK_*` and asserted equal to their wire twins). The video-caps byte still carries exactly the eight bits it carried at 0.24.0 — no ninth cap, no second byte, so nothing forced an ABI bump from that direction.
- **Unchanged:** virtual-display driver protocol 6 (minimum accepted 3) and the Windows virtual-gamepad channel 3 — `crates/pf-driver-proto` is byte-for-byte identical to v0.24.0.
- **Adaptive-trigger effects are now length-bounded** on both encode and decode against one shared constant, with the header emitting `uint8_t effect[PUNKTFUNK_HID_EFFECT_MAX]` in place of a literal `11` (same value, so the struct layout is byte-identical). A zero-length effect body is rejected rather than decoding as an empty — that is, a release — effect. Out-of-range pad indices are now dropped before either rumble consumer sees them; the reorder gate bounds-checked and the legacy queue did not, so an embedder draining it could be handed an index it would use to subscript its own array. The client also clamps the host's rumble lease receive-side at 5 s, where the existing ceiling was sender-side only.
- **Windows pad drivers publish their sequence counters with release ordering** (the host was already loading with acquire and pairing with nothing) and serialize the output-ring publish. The `/dev/uhid` event ABI, previously transcribed verbatim into all five Linux gamepad backends, is consolidated into one module with tests on the two accessors that had already drifted.
- **The controller-audio plane in detail.** `0xD1` carries one Opus frame per datagram behind a 15-byte header, with `PAD_AUDIO_KIND_HAPTICS = 0` (the pad's BACK channel pair — the voice coils — at 5 ms frames) and `PAD_AUDIO_KIND_SPEAKER = 1` (the FRONT pair, 10 ms). Best-effort like every audio plane: loss shows up as a sequence gap concealed by the gap tracker, and silence is a frozen sequence under the same mic-mute discipline, with the host gating at 60 dBFS on a 250 ms hangover. Alongside it, `HidOutput::AudioCtl` is a new `0xCD` kind `0x06` carrying the DualSense output report's volume/routing bytes, change-only and value-deduped — an older client drops it as an unknown kind. A client advertises per-pad intent through two new arrival flags (`1 << 8` haptics, `1 << 9` speaker), sent only toward a `HOST_CAP_PAD_AUDIO` host. **Capability-byte pressure is now worth watching:** `client_caps` has four bits free, but `host_caps` is down to its last one (`0x80`), and `video_caps` remains full from 0.23.0 — the standing "next video cap needs a second byte and an ABI bump" note still stands.
- **The controller-audio host gate is Windows-only and Steam-dependent.** `host_cap()` returns false unconditionally off Windows, and on Windows it still requires provisioning to have published at least one endpoint, which requires Valve's driver. The client only advertises its capability if a setting would actually render something, so a user with both toggles off never causes the host to provision anything. Note a real inconsistency to reconcile: the desktop session client defaults `pad_speaker` to `"pad"` (on) while Android defaults its speaker toggle to off, and the desktop side exposes these as serde-defaulted JSON keys with no settings UI at all. `pad_speaker = "mix"` is a declared TODO that logs once and behaves as `off`. The GameStream/Moonlight path always reports no pad-audio capability.
- **New host environment settings.** Controller audio: `PUNKTFUNK_PAD_AUDIO` (on unless set to `0`), `PUNKTFUNK_PAD_AUDIO_SLOTS` (default 1, max 4 — multi-pad needs an operator to raise it), and `PUNKTFUNK_PAD_AUDIO_STAMPS` (debug bisect hook), plus a `punktfunk-host pad-endpoint ensure|remove|status` devtest command. Audio: `PUNKTFUNK_AUDIO_QUALITY` (`low`/`standard`/`high`, default `high` = stereo 256 kbps; `standard` reproduces the pre-0.25 encoder exactly for an A/B, and a typo warns once rather than silently downgrading), `PUNKTFUNK_AUDIO_REDUNDANCY`, and `PUNKTFUNK_AUDIO_OUTPUT_MODE` (`client_only`/`host_and_client`/`follow_default`, default `client_only`, **Windows host only**). The legacy `PUNKTFUNK_HOST_AUDIO=1` and `PUNKTFUNK_KEEP_DEFAULT=1` still work, mapping to `host_and_client` and `follow_default`; `follow_default` wins if both are set. Wire: `PUNKTFUNK_WIRE_MTU` (pins on-wire IP MTU for all sessions; a value above 1500 also enables jumbo) and `PUNKTFUNK_JUMBO=1` (fixed 9000-MTU profile). All are documented on the troubleshooting page, not yet in the configuration reference.
- **Mid-session shard renegotiation is gated off for PyroWave sessions**, which parse the video stream in windows fixed at session start — re-sizing mid-stream would corrupt the parse. Those sessions get the next-session clamp only, and are excluded from jumbo. The decode-cap latch fix likewise does not apply to PyroWave, where adaptive bitrate is open-loop by design.
- **The Deck plugin's Python backend is now four thin shells over the `punktfunk` CLI** (`discover`, `hosts list --probe --json`, `pair`, `hosts add`); it parses no client data files and re-implements no client rules, and an outdated client reports itself deterministically as exit 5 + `unknown command "<verb>"` rather than being inferred from GTK startup noise. Host identity is matched by fingerprint first and address second in exactly one place, so a host that changed DHCP lease still matches its record while a different box inheriting the address does not inherit its pairing. `KnownHosts::read()` was split out of `load()` so `discover` can annotate against the store without minting-and-saving ids, which two parallel invocations could otherwise race.
- **The hold-Select gesture is one state machine** with unit tests in the shared client core, re-implemented to the same rules in the Apple capture layer and Android's router. A tapped Select is delivered on release with its release scheduled 50 ms behind, because a back-to-back down+up can otherwise fold into a single sequenced snapshot and vanish. `punktfunk-session` gained a per-user Unix control socket (`$XDG_RUNTIME_DIR[/app/$FLATPAK_ID]/punktfunk-session-ctl.sock`) with two verbs, `guide` and `qam` — the one runtime path a flatpak and the outside-the-sandbox Decky backend see identically.
- **Verification is build-level.** Clippy and test gates on Linux, the Windows runner and macOS; the desktop-audio, packet-sizing and iPad pointer work has not been confirmed on glass in these commits. **Controller audio in particular has never run on a real DualSense** — it is a hardware feature whose entire verification to date is unit tests and compile checks, and its rumble arbitration rests on an explicitly retracted assumption about whether the voice coils and the rumble motors are the same actuators (the evidence-based 500 ms idle window is correct either way, but the underlying exclusivity is unsettled). Android's arbiter is the evidence-based one; the desktop twin and the coil restore on Android's stop path are both still owed. Some Android OEM kernels also refuse the isochronous claim outright, which degrades to ordinary rumble and is reported by the self test.
-6
View File
@@ -1,6 +0,0 @@
• New: a DualSense plugged in by USB can play the host's fine-grained haptics through the pad itself. Needs a Windows host with Steam installed.
• Sound that drifts behind the picture now catches itself up instead of staying late all session.
• Rumble no longer dies for the rest of the session after one glitch.
• Automatic bitrate stops overshooting what your device can really decode.
• Hold Select to reach the host's Guide menu.
• Pin your settings profiles to hosts from the TV interface.
+16 -9
View File
@@ -21,12 +21,15 @@
],
},
},
"overrides": {
"undici": "^8.9.0",
},
"packages": {
"@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=="],
"@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=="],
"@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.99", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.21.0" }, "peerDependencies": { "effect": "^4.0.0-beta.99" } }, "sha512-POBAowafsAAb3bH1x1rJlWnv32yMAazFgEuRW5LhkW/JJA5VGoEk9OnuoUkIH1OW6K/X6IrdNpqcO+5e9lPQJA=="],
"@effect/platform-node-shared": ["@effect/platform-node-shared@4.0.0-beta.103", "", { "dependencies": { "@types/ws": "^8.18.1", "ws": "^8.21.0" }, "peerDependencies": { "effect": "^4.0.0-beta.103" } }, "sha512-0aCZMBid5ifqmY55TkfCDLaGTIM8qu3bNFUW7qL9vh/7jFOkaIAMX2MA8muG4deqW17XWxawddWu4v0fK+UW3g=="],
"@exodus/schemasafe": ["@exodus/schemasafe@1.3.0", "", {}, "sha512-5Aap/GaRupgNx/feGBwLLTVv8OQFfv3pq2lPRzPg9R+IOBnDgghTGW7l7EuVXOvg5cc/xSAlRW8rBrjIC3Nvqw=="],
@@ -44,15 +47,15 @@
"@msgpackr-extract/msgpackr-extract-win32-x64": ["@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4", "", { "os": "win32", "cpu": "x64" }, "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ=="],
"@punktfunk/host": ["@punktfunk/host@file:../sdk", { "devDependencies": { "@effect/openapi-generator": "4.0.0-beta.98", "@effect/platform-node": "4.0.0-beta.98", "@types/bun": "^1.3.0", "effect": "^4.0.0-beta.98", "typescript": "^5.9.3" }, "optionalDependencies": { "undici": "^7.0.0" }, "peerDependencies": { "effect": "^4.0.0-beta.98" }, "bin": { "punktfunk-scripting": "./dist/runner-cli.js" } }],
"@punktfunk/host": ["@punktfunk/host@file:../sdk", { "devDependencies": { "@effect/openapi-generator": "4.0.0-beta.98", "@effect/platform-node": "4.0.0-beta.98", "@types/bun": "^1.3.0", "bun2nix": "2.1.2", "effect": "^4.0.0-beta.98", "typescript": "^5.9.3" }, "optionalDependencies": { "undici": "^7.0.0" }, "peerDependencies": { "effect": "^4.0.0-beta.98" }, "bin": { "punktfunk-scripting": "./dist/runner-cli.js" } }],
"@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/node": ["@types/node@26.1.1", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw=="],
"@types/node": ["@types/node@26.1.2", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-Vu4a5UFA9rIIFJ7rB/Vaafh9lrCQszopTCx6KjFboXTGQbPNasehVR5TEiithSDGyd1DEiUByggTZsg8jukeIg=="],
"@types/react": ["@types/react@19.2.17", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw=="],
"@types/react": ["@types/react@19.2.18", "", { "dependencies": { "csstype": "^3.2.2" } }, "sha512-AnzbBERsrLKtk2XSfTbYRLjQPdy116Sty4q+T+Bp3IC4l6jNBvreVPAHmpq9qhXQM7CXZPjLVmGMw9sy+hxQ3w=="],
"@types/ws": ["@types/ws@8.18.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg=="],
@@ -62,6 +65,8 @@
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
"bun2nix": ["bun2nix@2.1.2", "", { "dependencies": { "sade": "^1.8.1" }, "bin": { "bun2nix": "index.ts" } }, "sha512-0wx6Ar5ccrz4aSD5prbShwymjDEXFh7Bucxs+YrpAMa67TnVB95Hv8FV3oaQEbtOx6QGgIAyOmap6Y3WCRqetg=="],
"call-me-maybe": ["call-me-maybe@1.0.2", "", {}, "sha512-HpX65o1Hnr9HH25ojC1YGs7HCQLq0GCOibSaWER0eNpgJ/Z1MZv2mTc7+xh6WOPxbRVcmgbv4hGU+uSQ/2xFZQ=="],
"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=="],
@@ -108,9 +113,11 @@
"mime": ["mime@4.1.0", "", { "bin": { "mime": "bin/cli.js" } }, "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw=="],
"mri": ["mri@1.2.0", "", {}, "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"msgpackr": ["msgpackr@2.0.4", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.4" } }, "sha512-o1C5KRmuRt+apqMr1HuGSqWStZoRBUpEsCsl15uM9VdAF1qHLtvMOU2En747EnTyEl6c4pzPewRMFF31s1CNbA=="],
"msgpackr": ["msgpackr@2.0.5", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.4" } }, "sha512-cef05H/dSYpLpqp3sj/qyZh5vhUYCalnaLO7j1yOmpsR0y/XwLVtK7r5gn+U/F7CTEfMowcGhlUQJDLcLf7jcA=="],
"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=="],
@@ -144,6 +151,8 @@
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
"sade": ["sade@1.8.1", "", { "dependencies": { "mri": "^1.1.0" } }, "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A=="],
"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=="],
"should-equal": ["should-equal@2.0.0", "", { "dependencies": { "should-type": "^1.4.0" } }, "sha512-ZP36TMrK9euEuWQYBig9W55WPC7uo37qzAEmbjHz4gfyuXrEUgF8cUvQVO+w+d3OMfPvSRQJ22lSm8MQJ43LTA=="],
@@ -170,7 +179,7 @@
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici": ["undici@7.28.0", "", {}, "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA=="],
"undici": ["undici@8.10.0", "", {}, "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ=="],
"undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
@@ -182,7 +191,7 @@
"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=="],
"ws": ["ws@8.21.2", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-54dMVAo4WIe6SKy3vBgN+9bJZqqQ8IMRevAkOLQALhi49qkkQDQfWdAZ8KQlXiEabw88ARXXdUrlvtbKQX+aKw=="],
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
@@ -192,8 +201,6 @@
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
"@effect/platform-node/undici": ["undici@8.8.0", "", {}, "sha512-ubshXMXwF3MQIMF1y/WxZdNBnjEKeSg2wF5mcGUtU55YTw34tnVVpKRlLf7ruDXZ5344KokPVX4RBx1wJm64Bw=="],
"oas-linter/yaml": ["yaml@1.10.3", "", {}, "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA=="],
"oas-resolver/yaml": ["yaml@1.10.3", "", {}, "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA=="],
+3
View File
@@ -57,5 +57,8 @@
"@types/react": "^19.2.16",
"effect": "4.0.0-beta.99",
"typescript": "^5.9.3"
},
"overrides": {
"undici": "^8.9.0"
}
}
+4 -3
View File
@@ -20,6 +20,9 @@
},
},
},
"overrides": {
"undici": "^8.9.0",
},
"packages": {
"@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=="],
@@ -169,7 +172,7 @@
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="],
"undici": ["undici@7.28.0", "", {}, "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA=="],
"undici": ["undici@8.10.0", "", {}, "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ=="],
"undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
@@ -191,8 +194,6 @@
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
"@effect/platform-node/undici": ["undici@8.7.0", "", {}, "sha512-N7iQtfyLhIMOFgQubvmLV26svHpO0bqKnAiWotTQCVKCmWrcGbBotPuW1x+xwYZ2VHdSTVUfPQQnlEt1/LouTQ=="],
"oas-linter/yaml": ["yaml@1.10.3", "", {}, "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA=="],
"oas-resolver/yaml": ["yaml@1.10.3", "", {}, "sha512-vIYeF1u3CjlhAFekPPAk2h/Kv4T3mAkMox5OymRiJQB0spDP10LHvt+K7G9Ny6NuuMAb25/6n1qyUjAcGNf/AA=="],
+3 -7
View File
@@ -313,13 +313,9 @@
url = "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz";
hash = "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==";
};
"undici@7.28.0" = fetchurl {
url = "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz";
hash = "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==";
};
"undici@8.7.0" = fetchurl {
url = "https://registry.npmjs.org/undici/-/undici-8.7.0.tgz";
hash = "sha512-N7iQtfyLhIMOFgQubvmLV26svHpO0bqKnAiWotTQCVKCmWrcGbBotPuW1x+xwYZ2VHdSTVUfPQQnlEt1/LouTQ==";
"undici@8.10.0" = fetchurl {
url = "https://registry.npmjs.org/undici/-/undici-8.10.0.tgz";
hash = "sha512-HvltHd7avK13QIw/oLe4qoOLyoVSoafqJ2jYOrtMRBkbYT31eiBQ8O0ehRKZiEZCMEyLFQNIADpgCWC5fALvYQ==";
};
"uuid@14.0.1" = fetchurl {
url = "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz";
+3
View File
@@ -55,5 +55,8 @@
},
"optionalDependencies": {
"undici": "^7.0.0"
},
"overrides": {
"undici": "^8.9.0"
}
}
+8 -8
View File
@@ -46,16 +46,16 @@
},
},
"overrides": {
"brace-expansion": "^5.0.8",
"brace-expansion": "^5.0.9",
"dompurify": "^3.4.12",
"fast-uri": "^3.1.4",
"fast-uri": "^3.1.5",
"immutable": "^4.3.9",
"js-yaml": "^4.3.0",
"linkify-it": "^5.0.2",
"postcss": "^8.5.10",
"postcss": "^8.5.25",
"sharp": "^0.35.3",
"tar": "^7.5.21",
"undici": "^7.28.0",
"undici": "^7.29.0",
},
"packages": {
"@adobe/css-tools": ["@adobe/css-tools@4.5.0", "", {}, "sha512-6OzddxPio9UiWTCemp4N8cYLV2ZN1ncRnV1cVGtve7dhPOtRkleRyx32GQCYSwDYgaHU3USMm84tNsvKzRCa1Q=="],
@@ -1118,7 +1118,7 @@
"body-scroll-lock": ["body-scroll-lock@4.0.0-beta.0", "", {}, "sha512-a7tP5+0Mw3YlUJcGAKUqIBkYYGlYxk2fnCasq/FUph1hadxlTRjF+gAcZksxANnaMnALjxEddmSi/H3OR8ugcQ=="],
"brace-expansion": ["brace-expansion@5.0.8", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-JZyDyq3D4AUifKTPOB7DELf6XsB3WdPuNxCtob1vFXPsSXhdAiHBWJ/tJ8HAc9aH84BK+5JFZLNkJKx3G9kzQg=="],
"brace-expansion": ["brace-expansion@5.0.9", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg=="],
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
@@ -1388,7 +1388,7 @@
"fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="],
"fast-uri": ["fast-uri@3.1.4", "", {}, "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw=="],
"fast-uri": ["fast-uri@3.1.5", "", {}, "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw=="],
"fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, "sha512-GGToxJ/w1x32s/D2EKND7kTil4n8OVk/9mycTc4VDza13lOvpUZTGX3mFSCtV9ksdGBVzvsyAVLM6mHFThxXxw=="],
@@ -1876,7 +1876,7 @@
"pluralize": ["pluralize@8.0.0", "", {}, "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA=="],
"postcss": ["postcss@8.5.22", "", { "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ=="],
"postcss": ["postcss@8.5.25", "", { "dependencies": { "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw=="],
"postcss-load-config": ["postcss-load-config@6.0.1", "", { "dependencies": { "lilconfig": "^3.1.1" }, "peerDependencies": { "jiti": ">=1.21.0", "postcss": ">=8.0.9", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["jiti", "postcss", "tsx", "yaml"] }, "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g=="],
@@ -2196,7 +2196,7 @@
"unctx": ["unctx@2.5.0", "", { "dependencies": { "acorn": "^8.15.0", "estree-walker": "^3.0.3", "magic-string": "^0.30.21", "unplugin": "^2.3.11" } }, "sha512-p+Rz9x0R7X+CYDkT+Xg8/GhpcShTlU8n+cf9OtOEf7zEQsNcCZO1dPKNRDqvUTaq+P32PMMkxWHwfrxkqfqAYg=="],
"undici": ["undici@7.28.0", "", {}, "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA=="],
"undici": ["undici@7.29.0", "", {}, "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw=="],
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
+12 -20
View File
@@ -2151,10 +2151,6 @@
url = "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-3.1.0.tgz";
hash = "sha512-Cg7TFGpIr01vOQNODXOOaGz2NpCU5gl8x1qJFbb6hbZxR7XrcE2vtbAsTAbJ7/xwJtUuJEw8K8Zr/AE0LHlesg==";
};
"balanced-match@1.0.2" = fetchurl {
url = "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz";
hash = "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==";
};
"balanced-match@4.0.4" = fetchurl {
url = "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz";
hash = "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==";
@@ -2199,13 +2195,9 @@
url = "https://registry.npmjs.org/body-scroll-lock/-/body-scroll-lock-4.0.0-beta.0.tgz";
hash = "sha512-a7tP5+0Mw3YlUJcGAKUqIBkYYGlYxk2fnCasq/FUph1hadxlTRjF+gAcZksxANnaMnALjxEddmSi/H3OR8ugcQ==";
};
"brace-expansion@2.1.2" = fetchurl {
url = "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.2.tgz";
hash = "sha512-w5JZcKgdhDOgOwm8H+KgbosopHMuGcl6qbulwjtz3SM7I7P3yW1eAjzMPLrIE+NQ9vjgANKHWeMHnrT0OXW1oA==";
};
"brace-expansion@5.0.7" = fetchurl {
url = "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz";
hash = "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==";
"brace-expansion@5.0.9" = fetchurl {
url = "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz";
hash = "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==";
};
"braces@3.0.3" = fetchurl {
url = "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz";
@@ -2815,9 +2807,9 @@
url = "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz";
hash = "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA==";
};
"fast-uri@3.1.4" = fetchurl {
url = "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.4.tgz";
hash = "sha512-8JnbkQ4juDyvYs4mgFGQqg4yCYtFDtUtmp2QIQq11ZZe5CFQ5wcqm1rqDgAh/QdMySuBnPzMUiJUNZG5N/AiQw==";
"fast-uri@3.1.5" = fetchurl {
url = "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz";
hash = "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==";
};
"fastq@1.20.1" = fetchurl {
url = "https://registry.npmjs.org/fastq/-/fastq-1.20.1.tgz";
@@ -3911,9 +3903,9 @@
url = "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz";
hash = "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==";
};
"postcss@8.5.22" = fetchurl {
url = "https://registry.npmjs.org/postcss/-/postcss-8.5.22.tgz";
hash = "sha512-KBDEIpLrvpv16pp3K0Fw+UCoZfopFjjgeB+0tA/aaThfEE74kKDLrgg603YvOWJyg3+WYtyq3xYsQWsIyZlPqQ==";
"postcss@8.5.25" = fetchurl {
url = "https://registry.npmjs.org/postcss/-/postcss-8.5.25.tgz";
hash = "sha512-DTPx3RWSSnWyzLxQnlH0rJP+EW5ekl16ZU4/psbIhA0e53kJfdgaN5vKM+xP7yJtXVu+nfdVFmlgFDEKAe4Pyw==";
};
"powershell-utils@0.1.0" = fetchurl {
url = "https://registry.npmjs.org/powershell-utils/-/powershell-utils-0.1.0.tgz";
@@ -4619,9 +4611,9 @@
url = "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz";
hash = "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==";
};
"undici@7.28.0" = fetchurl {
url = "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz";
hash = "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==";
"undici@7.29.0" = fetchurl {
url = "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz";
hash = "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==";
};
"unenv@2.0.0-rc.24" = fetchurl {
url = "https://registry.npmjs.org/unenv/-/unenv-2.0.0-rc.24.tgz";
+4 -4
View File
@@ -64,11 +64,11 @@
"dompurify": "^3.4.12",
"linkify-it": "^5.0.2",
"sharp": "^0.35.3",
"fast-uri": "^3.1.4",
"fast-uri": "^3.1.5",
"immutable": "^4.3.9",
"undici": "^7.28.0",
"postcss": "^8.5.10",
"undici": "^7.29.0",
"postcss": "^8.5.25",
"js-yaml": "^4.3.0",
"brace-expansion": "^5.0.8"
"brace-expansion": "^5.0.9"
}
}