From 0114c225c2a68e6c0418529e89a69330d536fa29 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 19 Oct 2026 21:39:08 -0700 Subject: [PATCH 01/16] quest: claim quest/m0/audit-deps Co-Authored-By: Claude Opus 5.5 From c658bb0972475921c8efdfc23ab55c69ba430b65 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 19 Oct 2026 22:01:03 -0700 Subject: [PATCH 02/16] chore(gst): bump gstreamer to 0.24 gstreamer 0.24 keeps the 1.91 MSRV (0.25 needs 1.92) and swaps paste for pastey. Its Bus::timed_pop_filtered never polls with a zero timeout, so the tests' non-blocking pops use pop_filtered instead. The paste advisory stays ignored for its other parents, now named in deny.toml. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 50 ++++++++++++++++++++++--------------- deny.toml | 5 ++-- rs/moq-gst/Cargo.toml | 2 +- rs/moq-gst/src/sink/imp.rs | 5 ++-- rs/moq-gst/tests/element.rs | 3 +-- 5 files changed, 37 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 24913e5449..a672d5bcf9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2865,15 +2865,15 @@ checksum = "e629b9b98ef3dd8afe6ca2bd0f89306cec16d43d907889945bc5d6687f2f13c7" [[package]] name = "gio-sys" -version = "0.20.10" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "521e93a7e56fc89e84aea9a52cfc9436816a4b363b030260b699950ff1336c83" +checksum = "0071fe88dba8e40086c8ff9bbb62622999f49628344b1d1bf490a48a29d80f22" dependencies = [ "glib-sys", "gobject-sys", "libc", "system-deps", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -2889,9 +2889,9 @@ dependencies = [ [[package]] name = "glib" -version = "0.20.12" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffc4b6e352d4716d84d7dde562dd9aee2a7d48beb872dd9ece7f2d1515b2d683" +checksum = "16de123c2e6c90ce3b573b7330de19be649080ec612033d397d72da265f1bd8b" dependencies = [ "bitflags 2.13.2", "futures-channel", @@ -2910,9 +2910,9 @@ dependencies = [ [[package]] name = "glib-macros" -version = "0.20.12" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8084af62f09475a3f529b1629c10c429d7600ee1398ae12dd3bf175d74e7145" +checksum = "cf59b675301228a696fe01c3073974643365080a76cc3ed5bc2cbc466ad87f17" dependencies = [ "heck", "proc-macro-crate", @@ -2923,9 +2923,9 @@ dependencies = [ [[package]] name = "glib-sys" -version = "0.20.10" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ab79e1ed126803a8fb827e3de0e2ff95191912b8db65cee467edb56fc4cc215" +checksum = "2d95e1a3a19ae464a7286e14af9a90683c64d70c02532d88d87ce95056af3e6c" dependencies = [ "libc", "system-deps", @@ -2972,9 +2972,9 @@ dependencies = [ [[package]] name = "gobject-sys" -version = "0.20.10" +version = "0.21.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec9aca94bb73989e3cfdbf8f2e0f1f6da04db4d291c431f444838925c4c63eda" +checksum = "2dca35da0d19a18f4575f3cb99fe1c9e029a2941af5662f326f738a21edaf294" dependencies = [ "glib-sys", "libc", @@ -3029,9 +3029,9 @@ dependencies = [ [[package]] name = "gstreamer" -version = "0.23.7" +version = "0.24.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8757a87f3706560037a01a9f06a59fcc7bdb0864744dcf73546606e60c4316e1" +checksum = "1e8251db223ca38d9aefaf3d19f6f11581a9123cd12dacebd8b9e182da965023" dependencies = [ "cfg-if", "futures-channel", @@ -3040,13 +3040,13 @@ dependencies = [ "glib", "gstreamer-sys", "itertools 0.14.0", + "kstring", "libc", "muldiv", "num-integer", "num-rational", - "once_cell", "option-operations", - "paste", + "pastey", "pin-project-lite", "smallvec", "thiserror 2.0.21", @@ -3054,10 +3054,11 @@ dependencies = [ [[package]] name = "gstreamer-sys" -version = "0.23.6" +version = "0.24.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feea73b4d92dbf9c24a203c9cd0bcc740d584f6b5960d5faf359febf288919b2" +checksum = "b5d37c1a599ae57b8186948bd5699f2dbfc044baea9d400228b489a85bcf2759" dependencies = [ + "cfg-if", "glib-sys", "gobject-sys", "libc", @@ -4166,6 +4167,15 @@ dependencies = [ "libc", ] +[[package]] +name = "kstring" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a09b82a7f771ed02dc0dd9b27130a0fa5499fa15ed3027116c1e5e4e591bd9e" +dependencies = [ + "static_assertions", +] + [[package]] name = "lazy_static" version = "1.5.1" @@ -6825,11 +6835,11 @@ checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" [[package]] name = "option-operations" -version = "0.5.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c26d27bb1aeab65138e4bf7666045169d1717febcc9ff870166be8348b223d0" +checksum = "aca39cf52b03268400c16eeb9b56382ea3c3353409309b63f5c8f0b1faf42754" dependencies = [ - "paste", + "pastey", ] [[package]] diff --git a/deny.toml b/deny.toml index 64696f87a3..21e6eb1851 100644 --- a/deny.toml +++ b/deny.toml @@ -17,8 +17,9 @@ ignore = [ # when rustcrypto/RSA publishes a constant-time fix. "RUSTSEC-2023-0071", - # gstreamer 0.23 pulls paste (proc-macro). gstreamer 0.25 drops it but - # needs Rust 1.92, above the 1.91 workspace MSRV. Build-time only. + # paste is unmaintained (proc-macro, build-time only). Pulled by + # tikv-jemalloc-ctl (moq-tokio's `jemalloc` feature; 0.7 still uses it) and + # by iroh's netwatch via netlink-packet-core 0.8, which 0.9 drops. "RUSTSEC-2024-0436", # ttf-parser is unmaintained, with no upgrade available. Reachable only via diff --git a/rs/moq-gst/Cargo.toml b/rs/moq-gst/Cargo.toml index 60fbd5e197..4cf02e10bd 100644 --- a/rs/moq-gst/Cargo.toml +++ b/rs/moq-gst/Cargo.toml @@ -18,7 +18,7 @@ crate-type = ["cdylib", "rlib"] anyhow = { workspace = true, features = ["backtrace"] } bytes = { workspace = true } -gst = { package = "gstreamer", version = "0.23" } +gst = { package = "gstreamer", version = "0.24" } hang = { workspace = true } moq-json = { workspace = true } moq-mux = { workspace = true } diff --git a/rs/moq-gst/src/sink/imp.rs b/rs/moq-gst/src/sink/imp.rs index 5cc8ecf918..dc8d758172 100644 --- a/rs/moq-gst/src/sink/imp.rs +++ b/rs/moq-gst/src/sink/imp.rs @@ -2086,15 +2086,14 @@ mod tests { sink.imp().post_session_error(&first, "old session failed".to_string()); assert!( - bus.timed_pop_filtered(gst::ClockTime::ZERO, &[gst::MessageType::Error]) - .is_none(), + bus.pop_filtered(&[gst::MessageType::Error]).is_none(), "the stopped session posted an error into its replacement" ); sink.imp() .post_session_error(&replacement, "current session failed".to_string()); let message = bus - .timed_pop_filtered(gst::ClockTime::ZERO, &[gst::MessageType::Error]) + .pop_filtered(&[gst::MessageType::Error]) .expect("the current session posted its error"); let gst::MessageView::Error(error) = message.view() else { unreachable!(); diff --git a/rs/moq-gst/tests/element.rs b/rs/moq-gst/tests/element.rs index b72f0e8682..c667993ad7 100644 --- a/rs/moq-gst/tests/element.rs +++ b/rs/moq-gst/tests/element.rs @@ -1157,8 +1157,7 @@ fn replacing_the_session_from_eos_notify_discards_the_old_eos() { assert!(pad.send_event(gst::event::Eos::new())); assert!(restarted.load(Ordering::SeqCst)); assert!( - bus.timed_pop_filtered(gst::ClockTime::ZERO, &[gst::MessageType::Eos]) - .is_none(), + bus.pop_filtered(&[gst::MessageType::Eos]).is_none(), "the completed session posted EOS into its replacement" ); let _ = sink.set_state(gst::State::Null); From 14201ec17660b6f4225ed53072017914e5195546 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 19 Oct 2026 22:11:08 -0700 Subject: [PATCH 03/16] chore(tokio)!: bump tikv-jemallocator to 0.7 Takes jemalloc 5.3.1. moq-tokio re-exports tikv_jemallocator, so the major reaches its public API. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 12 ++++++------ rs/moq-tokio/Cargo.toml | 4 ++-- rs/moq-tokio/MIGRATE.md | 5 +++++ 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a672d5bcf9..3afa35d98a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9687,9 +9687,9 @@ dependencies = [ [[package]] name = "tikv-jemalloc-ctl" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "661f1f6a57b3a36dc9174a2c10f19513b4866816e13425d3e418b11cc37bc24c" +checksum = "3a184c43b8ab2f41df2733b55556e3f5f632f4aeaa205b1bb018f574b7f5f142" dependencies = [ "libc", "paste", @@ -9698,9 +9698,9 @@ dependencies = [ [[package]] name = "tikv-jemalloc-sys" -version = "0.6.1+5.3.0-1-ge13ca993e8ccb9ba9847cc330696e02839f328f7" +version = "0.7.1+5.3.1-0-g81034ce1f1373e37dc865038e1bc8eeecf559ce8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd8aa5b2ab86a2cefa406d889139c162cbb230092f7d1d7cbc1716405d852a3b" +checksum = "1a2825c78386b4ae0314074867860ba9577875de945f05992c38815cbec327f0" dependencies = [ "cc", "libc", @@ -9708,9 +9708,9 @@ dependencies = [ [[package]] name = "tikv-jemallocator" -version = "0.6.1" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0359b4327f954e0567e69fb191cf1436617748813819c94b8cd4a431422d053a" +checksum = "249f09e49ab1609436f34c776e84231bead18d6a955f119f939bdc1d847561bd" dependencies = [ "libc", "tikv-jemalloc-sys", diff --git a/rs/moq-tokio/Cargo.toml b/rs/moq-tokio/Cargo.toml index dc386ff27c..8bcbfca976 100644 --- a/rs/moq-tokio/Cargo.toml +++ b/rs/moq-tokio/Cargo.toml @@ -108,8 +108,8 @@ socket2 = { workspace = true, optional = true } str0m = { workspace = true, optional = true } subtle = { workspace = true, optional = true } thiserror = "2" -tikv-jemalloc-ctl = { version = "0.6", optional = true } -tikv-jemallocator = { version = "0.6", features = ["profiling", "unprefixed_malloc_on_supported_platforms"], optional = true } +tikv-jemalloc-ctl = { version = "0.7", optional = true } +tikv-jemallocator = { version = "0.7", features = ["profiling", "unprefixed_malloc_on_supported_platforms"], optional = true } time = { workspace = true } tokio = { workspace = true, features = ["full"] } toml = { workspace = true } diff --git a/rs/moq-tokio/MIGRATE.md b/rs/moq-tokio/MIGRATE.md index dbf63c0d52..f5666eca11 100644 --- a/rs/moq-tokio/MIGRATE.md +++ b/rs/moq-tokio/MIGRATE.md @@ -4,6 +4,11 @@ How to update your code for each breaking change, newest first. ## Unreleased +### `jemalloc::tikv_jemallocator` is 0.7 + +The re-exported allocator crate moved from tikv-jemallocator 0.6 to 0.7. +Name the allocator through `moq_tokio::jemalloc::tikv_jemallocator`, or bump your own tikv-jemallocator to 0.7. + ### Key logging is opt-in (#5907) `SSLKEYLOGFILE` alone no longer writes TLS session secrets. From 0e0f6cef49e72d87220ad09e08c9470422092a24 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 19 Oct 2026 22:12:18 -0700 Subject: [PATCH 04/16] chore(hang): replace lazy_static with std LazyLock The one lazy_static use predates std::sync::LazyLock, stable since Rust 1.80. Co-Authored-By: Claude Opus 5.5 --- Cargo.lock | 1 - Cargo.toml | 1 - rs/hang/Cargo.toml | 1 - rs/hang/src/catalog/video/av1.rs | 13 ++++++++----- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3afa35d98a..f9173124f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3137,7 +3137,6 @@ dependencies = [ "derive_more 2.1.1", "hex", "kio 0.6.2", - "lazy_static", "moq-json", "moq-mux", "moq-net", diff --git a/Cargo.toml b/Cargo.toml index 6a4f1c1cd3..9977cc1113 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -142,7 +142,6 @@ if-addrs = "0.15" io-uring = "0.7.15" jsonwebtoken = "11" kio = { version = "0.6.2", path = "rs/kio" } -lazy_static = "1.5" libc = "0.2" libloading = "0.9" linux-raw-sys = { version = "0.12.1", features = ["ioctl"] } diff --git a/rs/hang/Cargo.toml b/rs/hang/Cargo.toml index 24dfae2525..5300c906ed 100644 --- a/rs/hang/Cargo.toml +++ b/rs/hang/Cargo.toml @@ -16,7 +16,6 @@ categories = ["multimedia", "network-programming", "web-programming"] bytes = { workspace = true } hex = { workspace = true } kio = { workspace = true } -lazy_static = { workspace = true } moq-json = { workspace = true } moq-net = { workspace = true } regex = "1" diff --git a/rs/hang/src/catalog/video/av1.rs b/rs/hang/src/catalog/video/av1.rs index 06db4d37f7..ef3af30924 100644 --- a/rs/hang/src/catalog/video/av1.rs +++ b/rs/hang/src/catalog/video/av1.rs @@ -78,8 +78,9 @@ impl std::fmt::Display for AV1 { } } -lazy_static::lazy_static! { - static ref AV1_REGEX: regex::Regex = regex::Regex::new(&r" +static AV1_REGEX: std::sync::LazyLock = std::sync::LazyLock::new(|| { + regex::Regex::new( + &r" ^av01 \.(?[0-2]) \.(?\d{2})(?\w) @@ -92,9 +93,11 @@ lazy_static::lazy_static! { \.(?\d{2}) \.(?[01]) )? - $".replace(['\n', ' ', '\t'], "") - ).unwrap(); -} + $" + .replace(['\n', ' ', '\t'], ""), + ) + .unwrap() +}); impl std::str::FromStr for AV1 { type Err = Error; From d8de1caae0c29bd5534daae4ca15c24dacf23b3b Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 19 Oct 2026 22:13:49 -0700 Subject: [PATCH 05/16] ci(dependabot): drop stale holds and cover infra/moq-sh reqwest-middleware is already on 0.5 through the http-cache-reqwest alpha, and uniffi is already on 0.32 with a matching Go generator, so both holds were blocking nothing. uniffi now holds at the next minor, which needs the generator bumped with it. infra/moq-sh has its own Bun lock that `just js audit` checks but Dependabot never updated. Co-Authored-By: Claude Opus 5.5 --- .github/dependabot.yml | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 92eaed4cb5..ae8041e377 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -44,27 +44,19 @@ updates: patterns: - "*" ignore: - # reqwest-middleware 0.5 requires reqwest 0.13, but http-cache-reqwest's - # latest stable (0.16) still pins reqwest 0.12 / reqwest-middleware 0.4. - # The only pairing is http-cache-reqwest 1.0.0-alpha (pre-release), so we - # hold reqwest-middleware at 0.4 until that line ships stable. - - dependency-name: "reqwest-middleware" - versions: [">=0.5"] - - # The Go bindings are generated by NordSecurity/uniffi-bindgen-go, whose - # latest release (v0.7.1+v0.31.0) parses uniffi 0.31 metadata. uniffi 0.32 - # changed that encoding, so a 0.32 library makes bindgen-go fail with - # "Invalid string data". Hold uniffi at 0.31 until bindgen-go ships 0.32 - # support; the other languages use uniffi's own version-matched bindgen - # and are not affected. + # The Go bindings come from a uniffi-bindgen-go build that only reads the + # metadata of one uniffi minor (0.32 today). The generator is pinned in + # flake.nix and five other places, so a uniffi minor moves with it by + # hand; see the uniffi-bindgen-go comment in flake.nix. - dependency-name: "uniffi" - versions: [">=0.32"] + versions: [">=0.33"] - package-ecosystem: "bun" directories: - "/" - "/infra/apt" - "/infra/rpm" + - "/infra/moq-sh" - "/demo/pub" schedule: interval: "weekly" From eccc96c90fd2746e305cf1e71e1cf5310cc97a81 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 19 Oct 2026 22:14:13 -0700 Subject: [PATCH 06/16] docs(audio): correct the unsafe-libopus maintenance claim unsafe-libopus has not been pushed since 2024-11, and the opus crate no longer goes through audiopus_sys. The pure-Rust build is still the reason to keep it; the Opus backend quest now lists the current alternatives. Co-Authored-By: Claude Opus 5.5 --- quest/m2/audio-opus-backend.md | 6 ++++++ rs/moq-audio/Cargo.toml | 13 ++++++------- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/quest/m2/audio-opus-backend.md b/quest/m2/audio-opus-backend.md index ee9a225680..f064f1a089 100644 --- a/quest/m2/audio-opus-backend.md +++ b/quest/m2/audio-opus-backend.md @@ -14,6 +14,12 @@ a currently maintained upstream implementation, including unmeasured percentage claims as a decision. Verify the actual versions at implementation time. +Candidates seen in the 2026-10-19 dependency audit: unsafe-libopus itself has +been idle upstream since 2024-11. `opus` 0.4 now binds libopus through the +maintained `opusic-sys` rather than `audiopus_sys`, and `opus-rs` (restsend) +and `mousiki` are pure-Rust ports with recent releases. Check each one's +libopus baseline before measuring. + Measure relevant voice/music rates, frame sizes, DTX, loss behavior, startup, CPU, and package/toolchain cost. Keep native compilation optional and use the existing private backend seam; do not add another public configuration surface diff --git a/rs/moq-audio/Cargo.toml b/rs/moq-audio/Cargo.toml index 3a6734e784..dbbd2c5fa3 100644 --- a/rs/moq-audio/Cargo.toml +++ b/rs/moq-audio/Cargo.toml @@ -112,13 +112,12 @@ thiserror = { workspace = true } tokio = { workspace = true, optional = true, features = ["rt", "macros", "sync", "time"] } # Only used by the device features, for stream-error logging. tracing = { workspace = true, optional = true } -# Pure-Rust transpilation of libopus 1.3.1. No CMake toolchain, no C -# linker hackery, and crucially still maintained, unlike the -# `opus`/`audiopus_sys` combo which is stuck on a 5-year-old vendored -# libopus that breaks under CMake 4 (RUSTSEC-2026-0150). Trade-off: -# ~20% slower than the C version with intrinsics enabled, but for the -# 20 ms Opus frames moq-audio publishes that's still well under any -# real-time budget. +# Pure-Rust transpilation of libopus 1.3.1: no CMake toolchain and no C +# linker hackery. Upstream has been idle since 2024-11, while the `opus` +# bindings moved to the maintained opusic-sys, so this is a build-cost +# choice rather than a maintenance one. Trade-off: ~20% slower than the C +# version with intrinsics enabled, but for the 20 ms Opus frames moq-audio +# publishes that's still well under any real-time budget. unsafe-libopus = "0.2" # macOS-only, and only pulled in by the `capture` feature (see above); a no-op on From ab5803d56230eb4fa8a01fd42a1e93fc16b08d8f Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 19 Oct 2026 22:15:03 -0700 Subject: [PATCH 07/16] refactor(tokio): classify local IPv6 with std Ipv6Addr::is_unique_local and is_unicast_link_local (stable since 1.84) are the same fc00::/7 and fe80::/10 masks the check spelled out by hand. Co-Authored-By: Claude Opus 5.5 --- rs/moq-tokio/src/connection.rs | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/rs/moq-tokio/src/connection.rs b/rs/moq-tokio/src/connection.rs index 4c73db1add..9941384daf 100644 --- a/rs/moq-tokio/src/connection.rs +++ b/rs/moq-tokio/src/connection.rs @@ -312,13 +312,7 @@ fn is_local_ip(ip: std::net::IpAddr) -> bool { // it by that rather than by the v6 rules. std::net::IpAddr::V6(ip) => match ip.to_ipv4_mapped() { Some(v4) => is_local_v4(v4), - // Loopback (::1), unspecified (::), unique local (fc00::/7), link local (fe80::/10). - None => { - ip.is_loopback() - || ip.is_unspecified() - || (ip.segments()[0] & 0xfe00) == 0xfc00 - || (ip.segments()[0] & 0xffc0) == 0xfe80 - } + None => ip.is_loopback() || ip.is_unspecified() || ip.is_unique_local() || ip.is_unicast_link_local(), }, } } From 9cb0637d505b9d990fa91b5f1fcc29cbbe01ce1d Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 19 Oct 2026 22:15:32 -0700 Subject: [PATCH 08/16] fix(net): reject non-hex certificate hashes Hex.toBytes fed each pair to parseInt, which yields NaN (stored as 0) for a non-hex pair, so a mistyped serverCertificateHashes entry pinned a different hash instead of failing. It now validates like @moq/hang's copy. Co-Authored-By: Claude Opus 5.5 --- js/net/src/util/hex.test.ts | 19 +++++++++++++++++++ js/net/src/util/hex.ts | 20 ++++++++++++++++---- 2 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 js/net/src/util/hex.test.ts diff --git a/js/net/src/util/hex.test.ts b/js/net/src/util/hex.test.ts new file mode 100644 index 0000000000..dd88d9aac1 --- /dev/null +++ b/js/net/src/util/hex.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from "bun:test"; +import * as Hex from "./hex.ts"; + +describe("hex", () => { + it("round-trips", () => { + const bytes = new Uint8Array([0x01, 0x42, 0x00, 0x2a, 0xff]); + expect(Hex.fromBytes(bytes)).toBe("0142002aff"); + expect(Hex.toBytes("0x0142002AFF")).toEqual(bytes); + }); + + it("rejects an odd length", () => { + expect(() => Hex.toBytes("abc")).toThrow("invalid hex string length"); + }); + + // parseInt("zz", 16) is NaN, which a Uint8Array stores as 0. + it("rejects non-hex characters", () => { + expect(() => Hex.toBytes("00zz")).toThrow("invalid hex character 'z' at position 2"); + }); +}); diff --git a/js/net/src/util/hex.ts b/js/net/src/util/hex.ts index 282220d15c..5d07001324 100644 --- a/js/net/src/util/hex.ts +++ b/js/net/src/util/hex.ts @@ -1,15 +1,27 @@ +const HEX = /^[0-9a-fA-F]*$/; + +/** + * Decode a hex string (with optional `0x` prefix) into bytes. + * + * Throws on anything that isn't hex: `parseInt` stops at the first bad character rather than + * failing, so a typo in a pinned certificate hash would otherwise pin a different hash. + */ export function toBytes(hex: string): Uint8Array { hex = hex.startsWith("0x") ? hex.slice(2) : hex; if (hex.length % 2) { throw new Error("invalid hex string length"); } + if (!HEX.test(hex)) { + const index = hex.search(/[^0-9a-fA-F]/); + throw new Error(`invalid hex character '${hex[index]}' at position ${index}`); + } - const matches = hex.match(/.{2}/g); - if (!matches) { - throw new Error("invalid hex string format"); + const bytes = new Uint8Array(hex.length / 2); + for (let i = 0; i < bytes.length; i++) { + bytes[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16); } - return new Uint8Array(matches.map((byte) => parseInt(byte, 16))); + return bytes; } export function fromBytes(bytes: Uint8Array): string { From 2bdf22e95d9dbce7391e3194466e660265c6ec57 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 19 Oct 2026 22:16:11 -0700 Subject: [PATCH 09/16] chore(auth): use jose's base64url instead of @hexagon/base64 @moq/auth already depends on jose, whose base64url encodes the same unpadded alphabet, so the second base64 package goes. Co-Authored-By: Claude Opus 5.5 --- bun.lock | 3 --- js/auth/package.json | 1 - js/auth/src/cli.ts | 4 ++-- js/auth/src/key.test.ts | 17 ++++++++--------- 4 files changed, 10 insertions(+), 15 deletions(-) diff --git a/bun.lock b/bun.lock index 4b2bfb3cfd..389bb891e0 100644 --- a/bun.lock +++ b/bun.lock @@ -94,7 +94,6 @@ "moq-auth": "./src/cli.ts", }, "dependencies": { - "@hexagon/base64": "^2.0.4", "@moq/clock": "workspace:^", "@moq/pattern": "workspace:^", "@zod/mini": "^4.6.5", @@ -662,8 +661,6 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.28.2", "", { "os": "win32", "cpu": "x64" }, "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g=="], - "@hexagon/base64": ["@hexagon/base64@2.0.4", "", {}, "sha512-H/ZY6rGyaEuk0mwQgZ3BVi9hMjFTYpBNFbmtOuec/pPibuGhCMXd8fGtwBaO0h44FkWMurysMsDrpkJsBRmoWQ=="], - "@iconify-json/simple-icons": ["@iconify-json/simple-icons@1.2.87", "", { "dependencies": { "@iconify/types": "*" } }, "sha512-8YciStObhSji3OZFmWAWK6kBujyqO5bLCxeDwLxf3CR3F4PVelq7keC2LBvgTqviWzSTysj5/g4PCFLiAMVGsw=="], "@iconify/types": ["@iconify/types@2.0.0", "", {}, "sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg=="], diff --git a/js/auth/package.json b/js/auth/package.json index ca36cdb7c1..f4bed303c0 100644 --- a/js/auth/package.json +++ b/js/auth/package.json @@ -23,7 +23,6 @@ "release": "bun ../common/release.ts" }, "dependencies": { - "@hexagon/base64": "^2.0.4", "@moq/clock": "workspace:^", "@moq/pattern": "workspace:^", "@zod/mini": "^4.6.5", diff --git a/js/auth/src/cli.ts b/js/auth/src/cli.ts index 000eedb68a..0fbcdfc48e 100755 --- a/js/auth/src/cli.ts +++ b/js/auth/src/cli.ts @@ -1,8 +1,8 @@ #!/usr/bin/env node import { closeSync, fchmodSync, openSync, readFileSync, writeFileSync } from "node:fs"; -import * as base64 from "@hexagon/base64"; import { Command, Option } from "commander"; +import { base64url } from "jose"; import type { Algorithm } from "./algorithm.ts"; import { authorize, ClaimsSchema, type Scope, ScopeSchema } from "./claims.ts"; import { Key } from "./key.ts"; @@ -42,7 +42,7 @@ program // Written the legacy way when that says the same thing, so older readers load it. const json = JSON.stringify(k.scope ? { ...k, scope: encodeGrants(k.scope) } : k, null, 2); if (options.base64) { - return base64.fromArrayBuffer(new TextEncoder().encode(json).buffer, true); + return base64url.encode(json); } return json; }; diff --git a/js/auth/src/key.test.ts b/js/auth/src/key.test.ts index 04a036d105..f3d9e40682 100644 --- a/js/auth/src/key.test.ts +++ b/js/auth/src/key.test.ts @@ -1,6 +1,5 @@ import { expect, test } from "bun:test"; -import * as base64 from "@hexagon/base64"; -import { exportJWK, generateKeyPair, SignJWT } from "jose"; +import { base64url, exportJWK, generateKeyPair, SignJWT } from "jose"; import type { Algorithm } from "./algorithm.ts"; import { authorize, type Claims } from "./claims.ts"; import { INSECURE_TEST_RS256_OTHER, INSECURE_TEST_RSA_KEYS } from "./insecure-test-keys.ts"; @@ -10,7 +9,7 @@ import { Key, KeyIdSchema } from "./key.ts"; function encodeJwk(obj: unknown): string { const jsonString = JSON.stringify(obj); const data = new TextEncoder().encode(jsonString); - return base64.fromArrayBuffer(data.buffer as ArrayBuffer, true); // true for urlSafe + return base64url.encode(data); } const testKey = { @@ -95,7 +94,7 @@ test("parse - invalid base64url", () => { test("parse - invalid JSON after base64url decode", () => { // Base64url encode invalid JSON const data = new TextEncoder().encode("invalid json"); - const invalidJwk = base64.fromArrayBuffer(data.buffer as ArrayBuffer, true); // true for urlSafe + const invalidJwk = base64url.encode(data); expect(() => { Key.parse(invalidJwk); @@ -615,7 +614,7 @@ test("sign - includes kid in header when present", async () => { // Decode the header to verify kid is present const [headerB64] = token.split("."); - const headerBuffer = base64.toArrayBuffer(headerB64, true); // true for urlSafe + const headerBuffer = base64url.decode(headerB64); const header = JSON.parse(new TextDecoder().decode(headerBuffer)); expect(header.kid).toBe("test-key-1"); @@ -635,7 +634,7 @@ test("sign - no kid in header when not present", async () => { // Decode the header to verify kid is not present const [headerB64] = token.split("."); - const headerBuffer = base64.toArrayBuffer(headerB64, true); // true for urlSafe + const headerBuffer = base64url.decode(headerB64); const header = JSON.parse(new TextDecoder().decode(headerBuffer)); expect(header.kid).toBe(undefined); @@ -648,7 +647,7 @@ test("sign - writes iat only when the claims carry one", async () => { const decodePayload = (token: string) => { const [, payloadB64] = token.split("."); - const payloadBuffer = base64.toArrayBuffer(payloadB64, true); // true for urlSafe + const payloadBuffer = base64url.decode(payloadB64); return JSON.parse(new TextDecoder().decode(payloadBuffer)); }; @@ -688,10 +687,10 @@ test("verify - invalid payload structure", async () => { // Create a token with invalid payload structure const headerData = new TextEncoder().encode(JSON.stringify({ alg: "HS256", typ: "JWT" })); - const header = base64.fromArrayBuffer(headerData.buffer as ArrayBuffer, true); // true for urlSafe + const header = base64url.encode(headerData); const payloadData = new TextEncoder().encode(JSON.stringify({ invalid: "payload" })); - const payload = base64.fromArrayBuffer(payloadData.buffer as ArrayBuffer, true); // true for urlSafe + const payload = base64url.encode(payloadData); const signature = "invalid"; const invalidToken = `${header}.${payload}.${signature}`; From 57467c4b59dabd3fa17b2901b8a7214e953784e2 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 19 Oct 2026 22:16:55 -0700 Subject: [PATCH 10/16] docs(relay): say why http-cache-reqwest is a prerelease The reason lived only in a Dependabot hold that no longer applies. Co-Authored-By: Claude Opus 5.5 --- rs/moq-relay/Cargo.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rs/moq-relay/Cargo.toml b/rs/moq-relay/Cargo.toml index 644f03a238..1753a9a5ef 100644 --- a/rs/moq-relay/Cargo.toml +++ b/rs/moq-relay/Cargo.toml @@ -61,7 +61,9 @@ bytesize = "2.4.2" futures = { workspace = true } hex = { workspace = true } http-body = "1.0.1" -http-cache-reqwest = { version = "1.0.0-alpha.6", features = ["manager-moka", "reqwest-middleware", "url-standard"], default-features = false } +# A 1.0 prerelease: the latest stable (0.16) is still on reqwest 0.12 and +# reqwest-middleware 0.4, while this line matches reqwest 0.13 below. +http-cache-reqwest ={ version = "1.0.0-alpha.6", features = ["manager-moka", "reqwest-middleware", "url-standard"], default-features = false } humantime = { workspace = true } hyper-util = { workspace = true } moq-auth = { workspace = true, features = ["client"] } From 83399a667f32f54dcc32e803188716761620284c Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 19 Oct 2026 22:19:43 -0700 Subject: [PATCH 11/16] quest(m0): finish the dependency audit and schedule the m1 run Records the m0 baselines, upstream-owned duplicates, kept dependencies, and open majors in the m1 successor, and plans the three hand-rolled replacements the audit found: H.264 parsing on h264-reader, avcC/hvcC through mp4-atom, and relay metrics through prometheus-client. Co-Authored-By: Claude Opus 5.5 --- quest/m0/README.md | 1 - quest/m0/audit-deps.md | 69 ------------------- quest/m0/ci/audit.md | 2 +- quest/m1/README.md | 1 + quest/m1/audit-deps.md | 103 ++++++++++++++++++++++++++++ quest/m2/README.md | 3 + quest/m2/h264-reader.md | 37 ++++++++++ quest/m2/mp4-atom-config.md | 29 ++++++++ quest/m2/relay-prometheus-client.md | 27 ++++++++ 9 files changed, 201 insertions(+), 71 deletions(-) delete mode 100644 quest/m0/audit-deps.md create mode 100644 quest/m1/audit-deps.md create mode 100644 quest/m2/h264-reader.md create mode 100644 quest/m2/mp4-atom-config.md create mode 100644 quest/m2/relay-prometheus-client.md diff --git a/quest/m0/README.md b/quest/m0/README.md index a836aadb67..b5a93bf4db 100644 --- a/quest/m0/README.md +++ b/quest/m0/README.md @@ -98,7 +98,6 @@ Each lands its fixes in its own PR and plans only what needs a decision. - [Test and CI cost audit](/quest/m0/audit-test-cost.md) - the slowest, flakiest, and least valuable tests and CI jobs are fixed, deleted, or planned - [Test coverage audit](/quest/m0/audit-test-coverage.md) - untested behavior that matters is covered or planned, and every test runs in CI - [Documentation audit](/quest/m0/audit-docs.md) - stale pages, examples, links, and undocumented exports are fixed or planned -- [Dependency and alternatives audit](/quest/m0/audit-deps.md) - dependencies are current, deduplicated, needed, and the best maintained option - [Artifact size audit](/quest/m0/audit-size.md) - binary, FFI, WASM, and bundle growth since the last run is explained, fixed, or planned - [Simplification audit](/quest/m0/audit-simplify.md) - overbuilt code and APIs are collapsed, merged, or narrowed into a net simpler whole - [Quest tree audit](/quest/m0/audit-quests.md) - the tree agrees with itself and with main diff --git a/quest/m0/audit-deps.md b/quest/m0/audit-deps.md deleted file mode 100644 index 6fa9004b6c..0000000000 --- a/quest/m0/audit-deps.md +++ /dev/null @@ -1,69 +0,0 @@ -# [M] Dependency and alternatives audit - -## Goal - -Dependencies are current, deduplicated, needed, and the best maintained option, -and hand-rolled non-core code that a maintained library covers is planned for -replacement. - -## Plan - -Recurs once per major release (decided 2026-10-19). The PR that finishes -this run also adds the next one with the same file name at the end of the next -milestone's `Required` (m0 to m1, m1 to m2). The successor keeps the Goal -and this paragraph; the rest of its Plan is rewritten with what this run -learned: baselines, results, and what to check first. A successor placed in m2 -also lists the [release cut](/quest/m2/release/cut.md) under `Required`, so it -waits for the release in flight to ship. Only `/quest-delete` ends the -recurrence. Fixes land in the run's PR, one commit each, rather than as follow-ups. -Only a change that needs a maintainer decision (public API, scope) or is -independently landable and too large to review alongside the rest becomes a -follow-up through `/quest-plan`. - -Research the state of the art for each candidate: compare maintenance, -performance, and API fit before planning a swap. - -Starting points (2026-10-19): Dependabot bumps weekly with a 7-day cooldown, so -look for the majors it cannot take. `deny.toml` warns on duplicate versions; -count them in `Cargo.lock` and `bun.lock`. `cargo deny` runs only -nightly. Look for unmaintained dependencies (RUSTSEC -`unmaintained`, no release in a year) and for hand-rolled utilities a -maintained crate or package now covers. - -Partial run (2026-10-19): compatible Bun updates reduced the root audit from -27 findings across 11 packages to six across four. Vue's renderer, browser -metadata, brace-expansion, PostCSS, nanoid, and source-map-js were refreshed. -Wrangler 4.151.2 removed the vulnerable sharp pin in `infra/moq-sh`. -All four independent Worker lockfiles now audit cleanly. `just js audit` -checks all five Bun graphs nightly without installing dependencies, and -`deny.toml` now points to the actual Rust audit recipe. -The obsolete standalone `doc/bun.lock` was removed: `doc` belongs to the -root Bun workspace, and that lock still named its old package and tooling. - -Baselines: Cargo.lock has 1,078 entries, 91 names with multiple versions -(110 extra versions), plus 11 names repeated from different sources at the -same version. The root Bun lock moved from 70 duplicate names / 82 extra -versions to 68 / 79. Independent Worker locks have no duplicate names. -These counts are inventory, not evidence that incompatible major versions -can safely be unified. - -Remaining advisories need an upstream release or a scope decision; no new -exceptions or overrides were added: - -- ashpd 0.13.13 pulls zbus 5 and the unmaintained zvariant, zvariant_derive, - and zbus_names crates (RUSTSEC-2026-0370, 0371, 0372). Upstream's - [zbus 6 migration](https://github.com/bilelmoussaoui/ashpd/pull/491) is open. - Prefer its maintained release over removing desktop portal support or - adding a Git dependency. -- VitePress 1.6.4 pins Vite 5, which pins vulnerable esbuild. The replacement - VitePress line is still a prerelease. A migration, override, or advisory - exception needs a decision. -- `@tailwindcss/typography` 0.5.20 pins postcss-selector-parser 6.0.10; - the fix starts at 7.1.6. Prefer a maintained upstream update. -- braces 3.0.3 has no published fix for - [GHSA-vfj7-8cjw-p6xm](https://github.com/advisories/GHSA-vfj7-8cjw-p6xm). - It is pulled by fast-glob/micromatch and remark-cli's watcher chain. - -This run is incomplete: compare direct dependency majors, maintenance and -alternatives, investigate duplicate ownership, and review hand-rolled non-core -utilities before declaring the audit complete and adding its successor. diff --git a/quest/m0/ci/audit.md b/quest/m0/ci/audit.md index 3d40708ea0..395cb9d2f6 100644 --- a/quest/m0/ci/audit.md +++ b/quest/m0/ci/audit.md @@ -57,7 +57,7 @@ before and after: 52 crate names are duplicated on aarch64-linux (syn 1, 2 and 3; rand 0.8, 0.9 and 0.10; three toml versions; sha2 and digest 0.10 and 0.11). Cut what the gate doesn't need. Dependency upgrades belong to - [the dependency audit](/quest/m0/audit-deps.md). + [the dependency audit](/quest/m1/audit-deps.md). - **Nightly:** about 1250 runner-minutes and 2 hours of wall time, and the last 5 runs failed (the Features job, plus JS benchmarks cancelled at 60 minutes). With main's optional checks moved to nightly, a red nightly hides diff --git a/quest/m1/README.md b/quest/m1/README.md index 66954fab44..ecc7ef012d 100644 --- a/quest/m1/README.md +++ b/quest/m1/README.md @@ -126,6 +126,7 @@ lands before the cut. - [C++ and OBS package proof](/quest/m1/cpp-package-check.md) - the installed artifacts pass before their first release - [Dead code and API surface audit](/quest/m1/audit-dead-code.md) - repeat the removal audit before the next major release +- [Dependency and alternatives audit](/quest/m1/audit-deps.md) - dependencies are current, deduplicated, needed, and the best maintained option ## Related diff --git a/quest/m1/audit-deps.md b/quest/m1/audit-deps.md new file mode 100644 index 0000000000..f47b938941 --- /dev/null +++ b/quest/m1/audit-deps.md @@ -0,0 +1,103 @@ +# [M] Dependency and alternatives audit + +## Goal + +Dependencies are current, deduplicated, needed, and the best maintained option, +and hand-rolled non-core code that a maintained library covers is planned for +replacement. + +## Plan + +Recurs once per major release (decided 2026-10-19). The PR that finishes +this run also adds the next one with the same file name at the end of the next +milestone's `Required` (m0 to m1, m1 to m2). The successor keeps the Goal +and this paragraph; the rest of its Plan is rewritten with what this run +learned: baselines, results, and what to check first. A successor placed in m2 +also lists the [release cut](/quest/m2/release/cut.md) under `Required`, so it +waits for the release in flight to ship. Only `/quest-delete` ends the +recurrence. Fixes land in the run's PR, one commit each, rather than as follow-ups. +Only a change that needs a maintainer decision (public API, scope) or is +independently landable and too large to review alongside the rest becomes a +follow-up through `/quest-plan`. + +Research the state of the art for each candidate: compare maintenance, +performance, and API fit before planning a swap. Respect Dependabot's 7-day +cooldown for manual bumps too. + +### Check first + +Dependabot's grouped weekly PRs only took semver-compatible updates in the m0 +run: tikv-jemallocator 0.7 sat unproposed for five months. List majors (0.x +minors included) for every direct dependency with `--all-features` metadata +against the crates.io index, and every `package.json` range against npm. +Also confirm each Bun lockfile is in `.github/dependabot.yml` (infra/moq-sh +was missing) and that every Dependabot `ignore` still blocks something. + +Majors still open at the end of m0: + +- gstreamer 0.25 needs Rust 1.92; moq-gst is on 0.24 under the 1.91 MSRV. +- usage-rs 7 has [its own quest](/quest/m2/usage-rs-7.md). +- vt100 0.17 and 0.18 (moq-cli dev-dependency) were inside the cooldown. +- http-cache-reqwest stays on its 1.0 prerelease until 1.0 ships stable. + +Advisories waiting on upstream, with no exceptions or overrides added: + +- ashpd 0.13 pulls zbus 5 and the unmaintained zvariant, zvariant_derive, and + zbus_names (RUSTSEC-2026-0370, 0371, 0372). Upstream's + [zbus 6 port](https://github.com/bilelmoussaoui/ashpd/pull/491) is open. +- The root Bun audit has six findings in four packages: VitePress 1.6.4 pins + Vite 5 and its vulnerable esbuild (VitePress 2 is still alpha), + `@tailwindcss/typography` 0.5.20 pins postcss-selector-parser 6, and braces + 3.0.3 has no fix for + [GHSA-vfj7-8cjw-p6xm](https://github.com/advisories/GHSA-vfj7-8cjw-p6xm). + The four Worker lockfiles audit clean. +- paste (RUSTSEC-2024-0436) remains through tikv-jemalloc-ctl and iroh's + netlink-packet-core 0.8. tikv-jemallocator's unreleased main drops it. + +### Baselines (2026-10-19, end of m0) + +Cargo.lock: 1,079 packages, 91 names with several versions (110 extra). +Root bun.lock: 660 package names, 72 with several versions (81 extra), +counted per name in its `packages` table; the Worker locks have none. Most +duplicates are ecosystem transitions owned upstream: + +- RustCrypto's previous generation (digest 0.10, sha2 0.10, hmac 0.12, der + 0.7, pkcs8 0.10): rsa, stun-types, http-auth, srt-protocol, jsonwebtoken. +- retina (moq-rtsp) holds h264-reader 0.8 and its bitstream-io 2, + rfc6381-codec 0.2, mp4ra-rust, and mpeg4-audio-const chain, plus socket2 0.5 + and rand 0.8. +- srt-tokio / srt-protocol 0.4.4, unpushed since 2024-05, hold derive_more + 0.99, hmac 0.12, rand 0.8, and socket2 0.5. + [SRT on the shared socket](/quest/m2/one-port/srt-demux.md) drops srt-tokio. +- rustls-acme holds rcgen 0.13, pem 3, and async-web-client (thiserror 1, + webpki-roots 0.26). winit 0.30 holds rustix 0.38, x11rb 0.13, and objc2 0.5 + until 0.31 leaves beta. +- Our own crates: web-transport-iroh 0.8.3 holds kio 0.5 (main already takes + 0.6, waiting on a moq-dev/web-transport release), moq-vaapi holds bindgen + 0.70, libloading 0.8, and thiserror 1, and mp4-atom holds thiserror 1. +- Bun has no dedupe, so same-major duplicates (`@babel/types`, + `@vue/shared`, `minimatch`, `picomatch`, `yaml`) persist until a parent + re-resolves. + +### Kept on purpose + +- tracing-wasm: its repository is gone, but the maintained `wasm-tracing` + fork added 2.9 KB gzipped to `moq_wasm.wasm` for the same behavior. +- tracing-android: `tracing-logcat` would add rustix 0.38. +- humantime-serde, hex, send_wrapper, byteorder: unreleased for years but + complete, with no advisory. +- assert_matches, cpu-time, stats_alloc: moq-quic stays close to quinn. +- unsafe-libopus: the [Opus backend](/quest/m2/audio-opus-backend.md) quest + owns the comparison. + +### Hand-rolled code + +Planned: [H.264 parsing](/quest/m2/h264-reader.md), +[avcC and hvcC](/quest/m2/mp4-atom-config.md), and +[relay metrics](/quest/m2/relay-prometheus-client.md). Reviewed and kept, +each small or cold: about eleven `(d * 2).min(max)` backoff loops (moq-tokio +already has a `Backoff` that moq-relay's cluster loop could reuse), per-byte +hex formatting in moq-auth and moq-hls, the STUN token bucket, the JS +deep-equal pair, and duplicated WebVTT stamps and `jwt=` lookups. +`js/publish` and `js/watch` carry byte-identical UI helpers (`graph.ts` and +near copies), which is duplication rather than a dependency question. diff --git a/quest/m2/README.md b/quest/m2/README.md index cf2fc264f6..1ced9f7c90 100644 --- a/quest/m2/README.md +++ b/quest/m2/README.md @@ -167,5 +167,8 @@ ranked just below deterministic TS legs, which it requires. - [Relay exposes a verified peer address](/quest/m2/relay-peer-addr.md) - WebRTC and other embedded routes know the client's socket address - [RTMP client reports a publish or play error](/quest/m2/rtmp-client-error.md) - a refused publish or play surfaces its reason - [Adopt usage-rs 7](/quest/m2/usage-rs-7.md) - the workspace builds on usage-rs 7 +- [H.264 parsing with maintained crates](/quest/m2/h264-reader.md) - moq-mux parses SPS VUI with h264-reader and shares one bit reader across codec config parsers +- [avcC and hvcC through mp4-atom](/quest/m2/mp4-atom-config.md) - moq-mux stops hand-writing the decoder config records its mp4-atom dependency already parses +- [Relay metrics through prometheus-client](/quest/m2/relay-prometheus-client.md) - `/metrics` renders through the maintained client with the same series - [Dead routes on IETF name their cause](/quest/m2/js-ietf-dead-route-error.md) - a JS subscription on a dead moq-transport session ends with the session error, not `unauthorized` - [Error cleanups](/quest/m2/error-cleanups.md) - delete the unused `Error::SessionClosed`; raw transport failures already map to `Unroutable` diff --git a/quest/m2/h264-reader.md b/quest/m2/h264-reader.md new file mode 100644 index 0000000000..2363b274a2 --- /dev/null +++ b/quest/m2/h264-reader.md @@ -0,0 +1,37 @@ +# [M] Parse H.264 and codec config bits with maintained crates + +## Goal + +moq-mux parses H.264 parameter sets with `h264-reader`, the crate moq-video +already uses, instead of `h264-parser` plus a hand-written VUI and HRD walker, +and its codec config readers share one maintained bit reader. Catalog output +is unchanged. + +## Plan + +Found by the 2026-10-19 dependency audit. `h264-parser` stops at +`vui_parameters_present_flag`, so `rs/moq-mux/src/codec/h264/mod.rs` re-reads +the SPS bit by bit (about 230 lines of `Vui`, `sps_vui`, and `read_hrd`). +`h264-reader` 0.9 exposes the VUI, HRD, and bitstream restriction, and +`rs/moq-video/src/decode/vui.rs` duplicates moq-mux's `sps_color` on top of it. +The H.265 SPS fallback in `codec/h265/sps.rs` borrows `h264-parser`'s reader. + +Check first whether `h264-reader` covers the SEI recovery point and mastering +display parsing moq-mux does (`split.rs`, `color.rs`); keep those small +pieces hand-written if not. [Intra refresh import](/quest/m2/intra-refresh/h264-import.md) +reads `recovery_frame_cnt` through `h264-parser` today, so whichever lands +second adapts. + +Four MSB-first bit readers repeat the same job for codec config: VP9 +(`codec/vp9/mod.rs`), AAC (`codec/aac/mod.rs`, with a recording writer), the +TS Opus channel config (`container/ts/import.rs`), and moq-audio's own +AudioSpecificConfig scan (`rs/moq-audio/src/aac.rs`), which could call +moq-mux's parser instead. `bitstream-io` is already in the lock through +`h264-reader`. These are cold paths, so pick by API fit, not speed. + +The existing codec tests and catalog fixtures are the regression gate. No +public API or wire change. + +## Related + +- [avcC and hvcC through mp4-atom](/quest/m2/mp4-atom-config.md) - the other hand-written codec config in moq-mux diff --git a/quest/m2/mp4-atom-config.md b/quest/m2/mp4-atom-config.md new file mode 100644 index 0000000000..45ed86b6ad --- /dev/null +++ b/quest/m2/mp4-atom-config.md @@ -0,0 +1,29 @@ +# [S] avcC and hvcC through mp4-atom + +## Goal + +moq-mux parses and builds avcC and hvcC records with `mp4_atom::Avcc` and +`mp4_atom::Hvcc`, already its dependency, instead of about 330 hand-written +lines, or a comment at the hand-written code names the input mp4-atom +mishandles. + +## Plan + +Found by the 2026-10-19 dependency audit. `rs/moq-mux/src/codec/h264/mod.rs` +has `Avcc::parse`, `build_avcc`, `catalog_avcc`, `avcc_params`, and +`read_param_sets`; `codec/h265/mod.rs` has `build_hvcc` and `hvcc_params`. +The tests already check them against mp4-atom. The fMP4 importer decodes an +`mp4_atom::Avcc`, re-encodes it, and re-parses it with the hand-written parser +(`container/fmp4/import.rs`, `init_h264`), while its H.265 path reads the +`mp4_atom::Hvcc` fields directly. + +Check first for a leniency reason, such as how mp4-atom handles the +high-profile extension or a short record from a real encoder; keep the +hand-written path only for a reproduced case and test it. `moq_mux` exports +`codec::h264::Avcc`, so replacing or removing it is a breaking change with a +`MIGRATE.md` entry; prefer keeping the type and changing only how it is +filled when that is simpler. No wire change. + +## Related + +- [H.264 parsing with maintained crates](/quest/m2/h264-reader.md) - the SPS side of the same codec config diff --git a/quest/m2/relay-prometheus-client.md b/quest/m2/relay-prometheus-client.md new file mode 100644 index 0000000000..a1189e335d --- /dev/null +++ b/quest/m2/relay-prometheus-client.md @@ -0,0 +1,27 @@ +# [M] Relay metrics through prometheus-client + +## Goal + +The relay's `/metrics` endpoint renders through `prometheus-client`, the +Prometheus project's maintained Rust client, instead of hand-written text +exposition, with the same metric names, labels, and types. + +## Plan + +Found by the 2026-10-19 dependency audit; adding the dependency is the +maintainer's call. `render_metrics` in `rs/moq-relay/src/internal.rs` writes +its metric families by hand (about 430 lines, histograms included), and +`rs/moq-relay/src/stun.rs` adds more. Label values are interpolated without +escaping: safe while every label is an enum, wrong as soon as one carries a +broadcast path or peer string. + +`prometheus-client` 0.25.1 (released 2026-09) needs only `dtoa`, `itoa`, +`parking_lot`, and its derive crate without the protobuf feature. Keep the +snapshot model: build the registry from the `moq_net::stats::Snapshot` on +each scrape, or collect through its `Collector` trait, rather than adding +live counters to the hot path. Pin the current output with a golden test +before switching, so a renamed or retyped series fails loudly; +`doc/bin/relay/http.md` documents the endpoint. + +A no-go is valid if the crate cannot reproduce the existing series without +renames; then add label escaping and a test instead. From fa468307a9929079ead47d4c9ac469dd34853bb2 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 19 Oct 2026 23:07:01 -0700 Subject: [PATCH 12/16] style(relay): restore the space taplo expects Co-Authored-By: Claude Opus 5.5 --- rs/moq-relay/Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rs/moq-relay/Cargo.toml b/rs/moq-relay/Cargo.toml index 1753a9a5ef..5d2d9da796 100644 --- a/rs/moq-relay/Cargo.toml +++ b/rs/moq-relay/Cargo.toml @@ -63,7 +63,7 @@ hex = { workspace = true } http-body = "1.0.1" # A 1.0 prerelease: the latest stable (0.16) is still on reqwest 0.12 and # reqwest-middleware 0.4, while this line matches reqwest 0.13 below. -http-cache-reqwest ={ version = "1.0.0-alpha.6", features = ["manager-moka", "reqwest-middleware", "url-standard"], default-features = false } +http-cache-reqwest = { version = "1.0.0-alpha.6", features = ["manager-moka", "reqwest-middleware", "url-standard"], default-features = false } humantime = { workspace = true } hyper-util = { workspace = true } moq-auth = { workspace = true, features = ["client"] } From aa1459b2cdf167d84299bc5c3a2373700f8f89b2 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 19 Oct 2026 23:07:18 -0700 Subject: [PATCH 13/16] docs(tokio): link the jemalloc migration entry to its PR Co-Authored-By: Claude Opus 5.5 --- rs/moq-tokio/MIGRATE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rs/moq-tokio/MIGRATE.md b/rs/moq-tokio/MIGRATE.md index f5666eca11..38a9325dfd 100644 --- a/rs/moq-tokio/MIGRATE.md +++ b/rs/moq-tokio/MIGRATE.md @@ -4,7 +4,7 @@ How to update your code for each breaking change, newest first. ## Unreleased -### `jemalloc::tikv_jemallocator` is 0.7 +### `jemalloc::tikv_jemallocator` is 0.7 (#6053) The re-exported allocator crate moved from tikv-jemallocator 0.6 to 0.7. Name the allocator through `moq_tokio::jemalloc::tikv_jemallocator`, or bump your own tikv-jemallocator to 0.7. From 62ef955bf8365f823f0121e896837b862a2f4e38 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 19 Oct 2026 23:52:23 -0700 Subject: [PATCH 14/16] fix(auth): decode keys with jose instead of the dropped @hexagon/base64 key.ts still imported @hexagon/base64 after the dependency left the manifest and lockfile, so a clean install could not resolve it. Encode generated HMAC secrets with jose's base64url and decode through it too, mapping standard base64 onto the url alphabet. The old decoder never threw: unknown characters became zero bytes, so malformed fields passed validation and the standard-base64 fallback was unreachable. Both are now covered by tests. Co-Authored-By: Claude Opus 5.5 --- js/auth/src/key.test.ts | 38 ++++++++++++++++++++++++++++++++++++++ js/auth/src/key.ts | 16 ++++------------ 2 files changed, 42 insertions(+), 12 deletions(-) diff --git a/js/auth/src/key.test.ts b/js/auth/src/key.test.ts index f3d9e40682..5ca0502068 100644 --- a/js/auth/src/key.test.ts +++ b/js/auth/src/key.test.ts @@ -125,6 +125,44 @@ test("parse - secret too short", () => { }).toThrow(); }); +test("parse - a field with non-base64 characters is refused, whatever its length", () => { + // Long enough to pass the 32-byte check if the bad characters were silently zeroed. + const invalidKey = { ...testKey, k: `${testKey.k.slice(0, -2)}!!` }; + + expect(() => Key.parse(encodeJwk(invalidKey))).toThrow(); +}); + +test("parse - accepts a JWK in standard padded base64", () => { + // The note survives as "+" and "/" in standard base64, and the kid length forces padding. + const json = JSON.stringify({ ...testKey, kid: "key-1", note: "~~~???" }); + const encoded = Buffer.from(json).toString("base64"); + expect(encoded).toMatch(/\+/); + expect(encoded).toMatch(/\//); + expect(encoded).toMatch(/=$/); + + const key = Key.parse(encoded); + expect(key.kid).toBe("key-1"); + expect((key as { k?: string }).k).toBe(testKey.k); +}); + +test("generate - an HMAC secret is unpadded base64url that round-trips", async () => { + for (const [alg, bytes] of [ + ["HS256", 32], + ["HS384", 48], + ["HS512", 64], + ] as const) { + const key = await Key.generate(alg, "generated"); + const k = (key as { k: string }).k; + expect(k).toMatch(/^[A-Za-z0-9_-]+$/); + expect(base64url.decode(k).byteLength).toBe(bytes); + + const parsed = Key.parse(encodeJwk(key)); + expect(parsed).toEqual(key); + const claims = await Key.verify(parsed, await Key.sign(key, testClaims)); + expect(claims.root).toBe(testClaims.root); + } +}); + test("parse - missing required fields", () => { const invalidKey = { alg: "HS256", diff --git a/js/auth/src/key.ts b/js/auth/src/key.ts index 82e090801a..70944253ec 100644 --- a/js/auth/src/key.ts +++ b/js/auth/src/key.ts @@ -1,4 +1,3 @@ -import * as base64 from "@hexagon/base64"; import { wall } from "@moq/clock"; import * as z from "@zod/mini"; import * as jose from "jose"; @@ -280,12 +279,10 @@ async function generateHmacKey(alg: Algorithm, byteLength: number, kid: KeyId): const bytes = new Uint8Array(byteLength); crypto.getRandomValues(bytes); - const k = base64.fromArrayBuffer(bytes.buffer, true); - return { kty: "oct", alg, - k, + k: jose.base64url.encode(bytes), kid, key_ops: ["sign", "verify"], }; @@ -515,14 +512,9 @@ function decodeBase64Flexible(value: string): Uint8Array | null { } try { - // First decode as URL - return new Uint8Array(base64.toArrayBuffer(trimmed, true)); + // Accept standard base64 too by mapping it onto the base64url alphabet. + return jose.base64url.decode(trimmed.replaceAll("+", "-").replaceAll("/", "_")); } catch { - try { - // Fallback to standard base64 - return new Uint8Array(base64.toArrayBuffer(trimmed, false)); - } catch { - return null; - } + return null; } } From 8bb2e933493746893ba7046b40131134d2e8d11a Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Mon, 19 Oct 2026 23:59:52 -0700 Subject: [PATCH 15/16] docs(moq-gst): note the gstreamer 0.24 break for Rust embedders plugin_init, plugin_register_static, and the glib::Enum impls on ConnectionStatus and MediaContainer now carry gstreamer 0.24 / glib 0.21 types, so a crate linking gstmoq must bump its own gstreamer. Co-Authored-By: Claude Opus 5.5 --- rs/moq-gst/MIGRATE.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rs/moq-gst/MIGRATE.md b/rs/moq-gst/MIGRATE.md index bb1abfdaae..a625961e56 100644 --- a/rs/moq-gst/MIGRATE.md +++ b/rs/moq-gst/MIGRATE.md @@ -4,6 +4,12 @@ How to update your pipelines for each breaking change, newest first. ## Unreleased +### Rust embedders move to gstreamer 0.24 (#6053) + +Only Rust code that links `gstmoq` as a library is affected; pipelines and the loadable plugin are not. +`plugin_init`, `plugin_register_static`, and the `glib::Enum` impls on `ConnectionStatus` and `MediaContainer` now use gstreamer 0.24 and glib 0.21 types. +Bump your own `gstreamer` dependency to 0.24 (and `glib` to 0.21) so the `gst::Plugin`, `glib::BoolError`, and `glib::Value` types match. + ### Key logging is opt-in (#5907) `SSLKEYLOGFILE` alone no longer writes TLS session secrets. From 62c769ac6ac5d73350c942b16b110f1aef91b6f3 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Tue, 20 Oct 2026 01:10:26 -0700 Subject: [PATCH 16/16] fix(net): refuse an empty certificate hash The stricter Hex.toBytes accepted "" (and "0x") as a zero-length hash, which the old regex-free version refused. An empty /certificate.sha256 body or serverCertificateHashes value is a config error, so throw. Co-Authored-By: Claude Opus 5.5 --- js/net/src/util/hex.test.ts | 6 ++++++ js/net/src/util/hex.ts | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/js/net/src/util/hex.test.ts b/js/net/src/util/hex.test.ts index dd88d9aac1..464cf9d480 100644 --- a/js/net/src/util/hex.test.ts +++ b/js/net/src/util/hex.test.ts @@ -8,7 +8,13 @@ describe("hex", () => { expect(Hex.toBytes("0x0142002AFF")).toEqual(bytes); }); + it("rejects an empty string", () => { + expect(() => Hex.toBytes("")).toThrow("empty hex string"); + expect(() => Hex.toBytes("0x")).toThrow("empty hex string"); + }); + it("rejects an odd length", () => { + expect(() => Hex.toBytes("0xabc")).toThrow("invalid hex string length"); expect(() => Hex.toBytes("abc")).toThrow("invalid hex string length"); }); diff --git a/js/net/src/util/hex.ts b/js/net/src/util/hex.ts index 5d07001324..74845adc90 100644 --- a/js/net/src/util/hex.ts +++ b/js/net/src/util/hex.ts @@ -8,6 +8,10 @@ const HEX = /^[0-9a-fA-F]*$/; */ export function toBytes(hex: string): Uint8Array { hex = hex.startsWith("0x") ? hex.slice(2) : hex; + // A zero-length certificate hash can never match, so an empty value is a config error. + if (!hex) { + throw new Error("empty hex string"); + } if (hex.length % 2) { throw new Error("invalid hex string length"); }