From 7bf03ac5f08893a42056be75a8045acd8244ae59 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Wed, 23 Sep 2026 10:53:14 -0700 Subject: [PATCH 1/6] Follow moq HEAD's relay/CLI renames and moq auth moq-dev/moq HEAD (relay 0.15.0, moq-cli 0.12.0) renamed the relay's [server]/listen config to [listen]/bind, the --server-bind/--tls-* flags to --listen/--listen-tls-*, made --auth-public take patterns ("**"), renamed the CLI's --client-connect to --connect, and folded moq-token-cli into `moq auth`. - cloudflare.sh / moxygen.sh: always build HEAD, so use the new relay flags. - smoke.toml: new layout (mirrors moq's test/smoke/smoke.toml); the old one moves to smoke-legacy.toml. smoke.sh picks it, and --client-connect, from the binary's --help so published 0.14 channels and the nix HEAD lane both work. - nix lane: TOKEN_BIN is `moq auth` from the moq-cli flake package; token.sh accepts a command prefix and falls back to `moq auth`. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/smoke.yml | 4 ++-- README.md | 6 ++++-- clients/docker/moq-relay | 11 ++++++++--- cloudflare.sh | 7 ++++--- moxygen.sh | 7 ++++--- smoke-legacy.toml | 20 ++++++++++++++++++++ smoke.sh | 23 ++++++++++++++++++++--- smoke.toml | 8 +++++--- token.sh | 30 ++++++++++++++++++++---------- 9 files changed, 87 insertions(+), 29 deletions(-) create mode 100644 smoke-legacy.toml diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 99524ba..722afa6 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -153,11 +153,11 @@ jobs: run: | relay=$(nix build --refresh --no-link --print-out-paths 'github:moq-dev/moq#moq-relay') cli=$(nix build --refresh --no-link --print-out-paths 'github:moq-dev/moq#moq-cli') - token=$(nix build --refresh --no-link --print-out-paths 'github:moq-dev/moq#moq-token-cli') + # moq-token-cli was folded into `moq auth` (moq-dev/moq#3684). { echo "RELAY_BIN=$relay/bin/moq-relay" echo "MOQ_BIN=$cli/bin/moq" - echo "TOKEN_BIN=$token/bin/moq-token" + echo "TOKEN_BIN=$cli/bin/moq auth" } >> "$GITHUB_ENV" - name: Pull moqdev images (docker channel) diff --git a/README.md b/README.md index 8442928..175fd20 100644 --- a/README.md +++ b/README.md @@ -95,6 +95,7 @@ smoke.sh orchestrator: relay + media interop matrix cloudflare.sh orchestrator: Cloudflare client through both projects' relays moxygen.sh orchestrator: moxygen protocol client through the moq-dev relay smoke.toml relay config (anonymous, self-signed localhost) +smoke-legacy.toml same, pre-0.15 layout; smoke.sh picks it for relays without --listen token.sh orchestrator: moq-token generate/verify interop matrix clients/ python/smoke.py publish/subscribe via moq-rs (PyPI) @@ -123,13 +124,14 @@ published flavours, and this test proves they cross-verify: | Cell | Source under test | Install | |---|---|---| -| `rust` | the `moq-token` binary (crates.io / Homebrew tap / apt repo / the moq flake) | `cargo install moq-token-cli`, `brew install moq-dev/tap/moq-token-cli`, `apt install`, `nix run github:moq-dev/moq#moq-token-cli` | +| `rust` | the `moq-token` binary (crates.io / Homebrew tap / apt repo), or `moq auth` from the moq flake | `cargo install moq-token-cli`, `brew install moq-dev/tap/moq-token-cli`, `apt install`, `nix run github:moq-dev/moq#moq-cli -- auth` | | `js-node` | npm [`@moq/token`](https://www.npmjs.com/package/@moq/token)'s `moq-token` CLI, run under **node** | `npm i @moq/token` | | `js-bun` | the same published npm package, run under **bun** | `npm i @moq/token` | | `rust-docker` | the [`moqdev/moq-token-cli`](https://hub.docker.com/r/moqdev/moq-token-cli) Docker Hub image (`:latest`) | `docker run moqdev/moq-token-cli …` | Like `smoke.sh`, the Rust binary is taken from `PATH` (or `TOKEN_BIN`), preferring -`moq-token` and falling back to `moq-token-cli` while channels finish the rename; +`moq-token`, then `moq-token-cli`, then `moq auth` (which replaced +`moq-token-cli` upstream); `TOKEN_BIN` may be a command prefix like `moq auth`; `@moq/token` is installed from npm on each run; `rust-docker` `docker pull`s the `moqdev/moq-token-cli` image fresh (`:latest`) and runs the CLI in a throwaway container with the scratch diff --git a/clients/docker/moq-relay b/clients/docker/moq-relay index 36fa048..d855a3d 100755 --- a/clients/docker/moq-relay +++ b/clients/docker/moq-relay @@ -24,8 +24,13 @@ image="${MOQ_RELAY_IMAGE:-moqdev/moq-relay}" name="${MOQ_RELAY_CONTAINER:-moq-relay-smoke}" "$runtime" rm -f "$name" >/dev/null 2>&1 || true -# smoke.sh passes the config path as the final argument. +# smoke.sh passes the config path as the final argument (or just --help, to +# probe which config layout this relay accepts). +mount=() cfg="${*: -1}" -dir=$(cd "$(dirname "$cfg")" && pwd) +if [[ -f "$cfg" ]]; then + dir=$(cd "$(dirname -- "$cfg")" && pwd) + mount=(-v "$dir:$dir") +fi -exec "$runtime" run --rm -i --name "$name" --network host -v "$dir:$dir" "$image" "$@" +exec "$runtime" run --rm -i --name "$name" --network host "${mount[@]}" "$image" "$@" diff --git a/cloudflare.sh b/cloudflare.sh index 1e5bff5..597343a 100755 --- a/cloudflare.sh +++ b/cloudflare.sh @@ -136,11 +136,12 @@ start_relay() { ready="$HTTPS_URL/fingerprint" ;; moq-dev) - "$MOQ_RELAY" --server-bind "127.0.0.1:${PORT}" \ - --tls-cert "$TMP/localhost.crt" --tls-key "$TMP/localhost.key" \ + # Git HEAD relay: the >= 0.15 flag names and pattern-based public auth. + "$MOQ_RELAY" --listen "127.0.0.1:${PORT}" \ + --listen-tls-cert "$TMP/localhost.crt" --listen-tls-key "$TMP/localhost.key" \ --web-https-listen "127.0.0.1:${PORT}" \ --web-https-cert "$TMP/localhost.crt" --web-https-key "$TMP/localhost.key" \ - --auth-public "" >"$TMP/relay-moq-dev.log" 2>&1 & + --auth-public "**" >"$TMP/relay-moq-dev.log" 2>&1 & ready="$HTTPS_URL/certificate.sha256" ;; *) diff --git a/moxygen.sh b/moxygen.sh index 7a28b71..65c16da 100755 --- a/moxygen.sh +++ b/moxygen.sh @@ -91,11 +91,12 @@ if ! openssl req -x509 -newkey rsa:2048 -nodes \ fi echo "starting moq-dev relay on 127.0.0.1:${PORT}..." -"$MOQ_RELAY" --server-bind "127.0.0.1:${PORT}" \ - --tls-cert "$TMP/localhost.crt" --tls-key "$TMP/localhost.key" \ +# Git HEAD relay: the >= 0.15 flag names and pattern-based public auth. +"$MOQ_RELAY" --listen "127.0.0.1:${PORT}" \ + --listen-tls-cert "$TMP/localhost.crt" --listen-tls-key "$TMP/localhost.key" \ --web-https-listen "127.0.0.1:${PORT}" \ --web-https-cert "$TMP/localhost.crt" --web-https-key "$TMP/localhost.key" \ - --auth-public "" >"$TMP/relay.log" 2>&1 & + --auth-public "**" >"$TMP/relay.log" 2>&1 & RELAY_PID=$! ready=0 diff --git a/smoke-legacy.toml b/smoke-legacy.toml new file mode 100644 index 0000000..32b7eea --- /dev/null +++ b/smoke-legacy.toml @@ -0,0 +1,20 @@ +# Relay config for the cross-language interop smoke test. +# Anonymous access, self-signed localhost cert, QUIC + WebSocket on 127.0.0.1:4443. +# Pre-0.15 layout, used by smoke.sh for relays without --listen. Delete once every +# channel ships moq-relay >= 0.15. + +[log] +level = "info" + +[server] +# QUIC on UDP. 127.0.0.1 avoids IPv6 flakiness on CI runners. +listen = "127.0.0.1:4443" +tls.generate = ["localhost", "127.0.0.1"] + +[web.http] +# HTTP + WebSocket on TCP, also serving /certificate.sha256 for cert pinning. +listen = "127.0.0.1:4443" + +[auth] +# Allow anonymous access to everything. +public = "" diff --git a/smoke.sh b/smoke.sh index 6fac63c..269d0ac 100755 --- a/smoke.sh +++ b/smoke.sh @@ -428,6 +428,15 @@ require_tools echo "relay: $(command -v "$RELAY")" echo "moq: $(command -v "$MOQ")" +# moq-cli 0.12 renamed --client-connect to --connect and rejects the old name. +# Pick whichever this build accepts until every channel ships 0.12. +moq_help=$("$MOQ" --help 2>&1 || true) +if grep -qE -- '(^|[[:space:]])--connect\b' <<<"$moq_help"; then + MOQ_CONNECT=--connect +else + MOQ_CONNECT=--client-connect +fi + if needs python; then echo "installing python client (moq-rs from PyPI)..." PY="$TMP/venv/bin/python" @@ -589,7 +598,15 @@ fi echo "starting relay on 127.0.0.1:${PORT}..." # smoke.toml is the source of truth; rewrite its port into a scratch copy so a # busy 4443 (a dev relay, a parallel run) doesn't require editing the committed file. -sed "s/4443/${PORT}/g" "$SMOKE_DIR/smoke.toml" >"$TMP/relay.toml" +# Relays before 0.15 (no --listen flag) reject the renamed keys, so they get the +# legacy layout until every channel ships 0.15. +relay_config="$SMOKE_DIR/smoke.toml" +relay_help=$("$RELAY" --help 2>&1 || true) +if ! grep -q -- '--listen\b' <<<"$relay_help"; then + relay_config="$SMOKE_DIR/smoke-legacy.toml" +fi +echo "relay config: $(basename "$relay_config")" +sed "s/4443/${PORT}/g" "$relay_config" >"$TMP/relay.toml" "$RELAY" "$TMP/relay.toml" >"$TMP/relay.log" 2>&1 & RELAY_PID=$! for _ in $(seq 1 60); do @@ -624,7 +641,7 @@ start_publisher() { local lang="$1" broadcast="$2" log="$TMP/pub-$1.log" case "$lang" in rust) - (ffmpeg_h264 | "$MOQ" --client-connect "$URL" --broadcast "$broadcast" import avc3) >"$log" 2>&1 & + (ffmpeg_h264 | "$MOQ" "$MOQ_CONNECT" "$URL" --broadcast "$broadcast" import avc3) >"$log" 2>&1 & ;; python) (ffmpeg_h264 | "$PY" "$CLIENTS/python/smoke.py" \ @@ -655,7 +672,7 @@ run_subscriber() { # moq only handles SIGINT, so -k forces SIGKILL if it ignores the # SIGTERM that fires when no data arrives within the timeout. local n - n=$(timeout -k 3 "$TIMEOUT" "$MOQ" --client-connect "$URL" --broadcast "$broadcast" \ + n=$(timeout -k 3 "$TIMEOUT" "$MOQ" "$MOQ_CONNECT" "$URL" --broadcast "$broadcast" \ export fmp4 2>/dev/null | head -c 1 | wc -c | tr -d ' ' || true) [[ "${n:-0}" -ge 1 ]] ;; diff --git a/smoke.toml b/smoke.toml index c84e237..369ad71 100644 --- a/smoke.toml +++ b/smoke.toml @@ -1,12 +1,14 @@ # Relay config for the cross-language interop smoke test. # Anonymous access, self-signed localhost cert, QUIC + WebSocket on 127.0.0.1:4443. +# Mirrors moq-dev/moq's test/smoke/smoke.toml. smoke-legacy.toml carries the +# pre-0.15 layout for relays that predate the [server] -> [listen] rename. [log] level = "info" -[server] +[listen] # QUIC on UDP. 127.0.0.1 avoids IPv6 flakiness on CI runners. -listen = "127.0.0.1:4443" +bind = "127.0.0.1:4443" tls.generate = ["localhost", "127.0.0.1"] [web.http] @@ -15,4 +17,4 @@ listen = "127.0.0.1:4443" [auth] # Allow anonymous access to everything. -public = "" +public = "**" diff --git a/token.sh b/token.sh index e1ff96b..e5527e2 100755 --- a/token.sh +++ b/token.sh @@ -4,7 +4,8 @@ # moq-relay authenticates with JWTs minted by the moq-token tooling, which ships # in several flavours from several registries: # -# - rust : the moq-token binary (cargo / brew / apt / nix), on PATH +# - rust : the moq-token binary (cargo / brew / apt), or `moq auth` from +# moq-cli >= 0.12 where the token CLI now lives (nix) # - js-node : the @moq/token npm package's `moq-token` CLI, run under node # - js-bun : the same published npm package, run under bun # @@ -33,8 +34,9 @@ VERIFIERS="rust" # can't silently stop exercising one. Override with --algorithms / TOKEN_ALGORITHMS. ALGORITHMS="${TOKEN_ALGORITHMS:-HS256,EdDSA,ES256,RS256}" -# The Rust CLI under test. Whatever channel installed it (cargo/brew/apt/nix) -# just has to leave it on PATH; override here to point at a specific build. +# The Rust CLI under test, as a command prefix. Whatever channel installed it +# (cargo/brew/apt/nix) just has to leave it on PATH; override here to point at a +# specific build, e.g. TOKEN_BIN="/path/to/moq auth". TOKEN="${TOKEN_BIN:-}" # The published Docker image for the `rust-docker` cell. Untagged = :latest, the @@ -117,13 +119,16 @@ trap cleanup EXIT have() { command -v "$1" >/dev/null 2>&1; } resolve_token() { - # Prefer the renamed binary, but tolerate channels that still expose the old - # executable during rollout. TOKEN_BIN remains authoritative when set. + # Prefer the standalone binary while channels still ship it, then fall back to + # `moq auth`, which replaced moq-token-cli upstream (moq-dev/moq#3684). + # TOKEN_BIN remains authoritative when set. [[ -n "$TOKEN" ]] && return 0 if have moq-token; then TOKEN=moq-token elif have moq-token-cli; then TOKEN=moq-token-cli + elif have moq && moq auth --help >/dev/null 2>&1; then + TOKEN="moq auth" else TOKEN=moq-token fi @@ -149,7 +154,7 @@ cli_for() { # split is deliberate (runtime + path, or a whole `docker run ...` line), so # callers expand it unquoted. case "$1" in - rust) echo "$TOKEN" ;; + rust) echo "$TOKEN" ;; # may be multi-word, e.g. `moq auth` # Mount TMP at its real path so the in-container CLI reads/writes the same # key/token files token.sh hands it. The image bundles the nix store, so # the binary's libiconv deps resolve (the brew bottle's bug doesn't apply). @@ -227,19 +232,24 @@ verify() { } # ── setup ──────────────────────────────────────────────────────────────────── +rust_probe() { + # shellcheck disable=SC2086 # TOKEN is a deliberate command prefix + $TOKEN generate --algorithm HS256 --out "$TMP/rust-probe.jwk" >"$TMP/rust-probe.log" 2>&1 +} + "$SMOKE_DIR/freshness.sh" || echo "WARN: freshness check failed (see above); continuing" >&2 resolve_token if needs rust; then - if ! have "$TOKEN"; then - mark_broken rust "$TOKEN not found (cargo/brew/apt/nix install moq-token-cli)" + if ! have "${TOKEN%% *}"; then + mark_broken rust "$TOKEN not found (install moq-token-cli, or moq-cli >= 0.12 for moq auth)" # `have` only checks the file exists; actually run it once, since a broken # published binary (e.g. a Homebrew bottle that baked in a /nix/store rpath # and aborts on launch) is exactly the packaging failure this test exists to # catch. A broken CLI marks the whole rust row unavailable instead of crashing # mid-matrix. - elif "$TOKEN" generate --algorithm HS256 --out "$TMP/rust-probe.jwk" >"$TMP/rust-probe.log" 2>&1; then - echo "rust: $(command -v "$TOKEN")" + elif rust_probe; then + echo "rust: $TOKEN ($(command -v "${TOKEN%% *}"))" else mark_broken rust "$TOKEN on PATH but won't run (see below)" sed 's/^/ /' "$TMP/rust-probe.log" >&2 || true From 49f181c2b33c13fded0b56c3277fd69a58f86fd4 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Wed, 23 Sep 2026 13:02:02 -0700 Subject: [PATCH 2/6] Follow libmoq 0.6.0's C API in the C smoke client libmoq v0.6.0 (released 2026-09-23) added a config pointer to moq_session_connect (NULL = defaults) and renamed moq_origin_consume_announced to moq_origin_announced_broadcast. The C cells always build against the latest libmoq release, so every channel lane broke. Co-Authored-By: Claude Opus 5.5 --- clients/c/subscribe.c | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/clients/c/subscribe.c b/clients/c/subscribe.c index 5a46cba..fada93f 100644 --- a/clients/c/subscribe.c +++ b/clients/c/subscribe.c @@ -23,7 +23,7 @@ typedef struct { pthread_cond_t cv; int got; // a non-empty frame arrived int video_started; // guard: start the video track only once - int32_t broadcast; // handle delivered by moq_origin_consume_announced (0 until it arrives) + int32_t broadcast; // handle delivered by moq_origin_announced_broadcast (0 until it arrives) } ctx_t; // Callbacks run on libmoq's runtime thread; main waits on the condvar. ctx @@ -113,8 +113,9 @@ int main(int argc, char **argv) { return 1; } - // origin_publish = 0 disables publishing; consume via our origin. - int32_t session = moq_session_connect(url, strlen(url), 0, (uint32_t)origin, on_status, &c); + // NULL config dials with the defaults; origin_publish = 0 disables + // publishing; consume via our origin. + int32_t session = moq_session_connect(url, strlen(url), NULL, 0, (uint32_t)origin, on_status, &c); if (session <= 0) { fprintf(stderr, "error: moq_session_connect failed: %d\n", session); return 1; @@ -125,11 +126,11 @@ int main(int argc, char **argv) { deadline.tv_sec += (time_t)timeout_s; // The broadcast arrives over the network after connect, so wait for it to be - // announced. moq_origin_consume_announced resolves via on_broadcast once it's + // announced. moq_origin_announced_broadcast resolves via on_broadcast once it's // available; we block on the condvar until then (or the deadline). - int32_t wait = moq_origin_consume_announced((uint32_t)origin, broadcast, strlen(broadcast), on_broadcast, &c); + int32_t wait = moq_origin_announced_broadcast((uint32_t)origin, broadcast, strlen(broadcast), on_broadcast, &c); if (wait <= 0) { - fprintf(stderr, "error: moq_origin_consume_announced failed: %d\n", wait); + fprintf(stderr, "error: moq_origin_announced_broadcast failed: %d\n", wait); return 1; } From e8bd9263b30188e2ca84d80989c9398670fe19b1 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Wed, 23 Sep 2026 13:21:51 -0700 Subject: [PATCH 3/6] Follow the moq JS 2026-09-23 releases and moq auth everywhere @moq/token is replaced by @moq/auth 0.2.0 (bin moq-auth, generate --out), whose claims no longer interoperate with the discontinued moq-token 0.5.x. moq-cli 0.12.0 on crates.io and the moqdev/moq-cli image carry the Rust side as `moq auth`, so: - token.sh: the rust cell is `${MOQ_BIN:-moq} auth` (the same binary the media matrix tests) and rust-docker runs `auth` in moqdev/moq-cli. The moq-token / moq-token-cli fallbacks and CI installs are gone. - clients/token/js: drive @moq/auth's moq-auth CLI. - clients/js-native: @moq/net 0.4.0 dropped Established.consume(); consume through an Origin.Producer and origin.request(path, { announced: true }). Co-Authored-By: Claude Opus 5.5 --- .github/workflows/smoke.yml | 24 ++++++------- README.md | 30 ++++++++-------- clients/js-native/subscribe.ts | 40 +++++++++------------ clients/token/js/package.json | 4 +-- clients/token/js/resolve-bin.mjs | 10 +++--- freshness.sh | 14 ++++---- justfile | 6 ++-- token.sh | 60 +++++++++++--------------------- 8 files changed, 79 insertions(+), 109 deletions(-) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index 722afa6..f5d4842 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -120,7 +120,7 @@ jobs: # Honor each published binary crate's Cargo.lock. Without --locked, the # cargo channel can select a newly broken transitive dependency even when # the crate was published and tested against a working resolution. - run: cargo install --locked moq-relay moq-cli moq-token-cli + run: cargo install --locked moq-relay moq-cli - name: Install moq Rust packages (apt) if: matrix.channel == 'apt' @@ -130,13 +130,13 @@ jobs: echo "deb [signed-by=/usr/share/keyrings/moq-keyring.gpg] https://apt.moq.dev stable main" \ | sudo tee /etc/apt/sources.list.d/moq.list sudo apt-get update - sudo apt-get install -y moq-relay moq-cli moq-token-cli + sudo apt-get install -y moq-relay moq-cli - name: Install moq Rust packages (brew) if: matrix.channel == 'brew' run: | brew tap moq-dev/tap - brew install moq-dev/tap/moq-relay moq-dev/tap/moq-cli moq-dev/tap/moq-token-cli + brew install moq-dev/tap/moq-relay moq-dev/tap/moq-cli - name: Install Nix (nix channel) if: matrix.channel == 'nix' @@ -153,11 +153,9 @@ jobs: run: | relay=$(nix build --refresh --no-link --print-out-paths 'github:moq-dev/moq#moq-relay') cli=$(nix build --refresh --no-link --print-out-paths 'github:moq-dev/moq#moq-cli') - # moq-token-cli was folded into `moq auth` (moq-dev/moq#3684). { echo "RELAY_BIN=$relay/bin/moq-relay" echo "MOQ_BIN=$cli/bin/moq" - echo "TOKEN_BIN=$cli/bin/moq auth" } >> "$GITHUB_ENV" - name: Pull moqdev images (docker channel) @@ -214,17 +212,17 @@ jobs: # ── token interop ────────────────────────────────────────────────── # Independent of the media matrix: prove the published token tooling - # cross-verifies. moq-token rides the same channel as moq-relay/moq - # (cargo/apt/brew/nix, on PATH or TOKEN_BIN); @moq/token comes from npm and - # runs under both node and bun; rust-docker pulls the moqdev/moq-token-cli - # image. The negative pass inside token.sh confirms each verifier rejects + # cross-verifies. `moq auth` rides the same channel as moq-relay/moq + # (cargo/apt/brew/nix, on PATH or MOQ_BIN); @moq/auth comes from npm and + # runs under both node and bun; rust-docker runs `auth` in the + # moqdev/moq-cli image. The negative pass inside token.sh confirms each verifier rejects # tampered tokens and the wrong key. - name: Token interop run: | - # The `rust` impl needs moq-token on PATH, which only the - # cargo/apt/brew/nix channels install. The docker channel ships no such - # binary, so it exercises the Rust verifier through the - # moqdev/moq-token-cli image (rust-docker) instead of plain `rust`. + # The `rust` impl runs a native `moq auth`, which only the + # cargo/apt/brew/nix channels install. The docker channel's moq wrapper + # doesn't mount token.sh's scratch dir, so it exercises the Rust + # verifier through the rust-docker cell instead of plain `rust`. if [ "${{ matrix.channel }}" = "docker" ]; then impls="js-node,js-bun,rust-docker" else diff --git a/README.md b/README.md index 175fd20..4765fc8 100644 --- a/README.md +++ b/README.md @@ -96,7 +96,7 @@ cloudflare.sh orchestrator: Cloudflare client through both projects' moxygen.sh orchestrator: moxygen protocol client through the moq-dev relay smoke.toml relay config (anonymous, self-signed localhost) smoke-legacy.toml same, pre-0.15 layout; smoke.sh picks it for relays without --listen -token.sh orchestrator: moq-token generate/verify interop matrix +token.sh orchestrator: moq auth generate/verify interop matrix clients/ python/smoke.py publish/subscribe via moq-rs (PyPI) go/ publish/subscribe via moq-dev/moq-go (go get) @@ -108,7 +108,7 @@ clients/ js-native/subscribe.ts subscribe via @moq/net + @moq/hang + WebTransport polyfill (node, bun) (gst) subscribe via the moq-gst plugin (moqsrc); no client dir, driven by gst-launch docker/ moq-relay + moq wrappers: docker run the moqdev/* images (the docker channel) - token/js/ installs @moq/token (npm) for token.sh to drive under node + bun + token/js/ installs @moq/auth (npm) for token.sh to drive under node + bun cloudflare/ deterministic subgroup/datagram client using cloudflare/moq-rs Git HEAD freshness.sh enforces the "always latest, no package locks" policy .github/workflows/smoke.yml nightly + on-demand CI matrix (os x channel) @@ -124,17 +124,15 @@ published flavours, and this test proves they cross-verify: | Cell | Source under test | Install | |---|---|---| -| `rust` | the `moq-token` binary (crates.io / Homebrew tap / apt repo), or `moq auth` from the moq flake | `cargo install moq-token-cli`, `brew install moq-dev/tap/moq-token-cli`, `apt install`, `nix run github:moq-dev/moq#moq-cli -- auth` | -| `js-node` | npm [`@moq/token`](https://www.npmjs.com/package/@moq/token)'s `moq-token` CLI, run under **node** | `npm i @moq/token` | -| `js-bun` | the same published npm package, run under **bun** | `npm i @moq/token` | -| `rust-docker` | the [`moqdev/moq-token-cli`](https://hub.docker.com/r/moqdev/moq-token-cli) Docker Hub image (`:latest`) | `docker run moqdev/moq-token-cli …` | - -Like `smoke.sh`, the Rust binary is taken from `PATH` (or `TOKEN_BIN`), preferring -`moq-token`, then `moq-token-cli`, then `moq auth` (which replaced -`moq-token-cli` upstream); `TOKEN_BIN` may be a command prefix like `moq auth`; -`@moq/token` is installed from npm on each run; `rust-docker` `docker pull`s the -`moqdev/moq-token-cli` -image fresh (`:latest`) and runs the CLI in a throwaway container with the scratch +| `rust` | `moq auth` from the `moq` binary (crates.io / Homebrew tap / apt repo / the moq flake) | `cargo install moq-cli`, `brew install moq-dev/tap/moq-cli`, `apt install moq-cli`, `nix run github:moq-dev/moq#moq-cli -- auth` | +| `js-node` | npm [`@moq/auth`](https://www.npmjs.com/package/@moq/auth)'s `moq-auth` CLI, run under **node** | `npm i @moq/auth` | +| `js-bun` | the same published npm package, run under **bun** | `npm i @moq/auth` | +| `rust-docker` | the [`moqdev/moq-cli`](https://hub.docker.com/r/moqdev/moq-cli) Docker Hub image (`:latest`) | `docker run moqdev/moq-cli auth …` | + +Like `smoke.sh`, the Rust CLI is `moq` from `PATH` (or `MOQ_BIN`), run as +`moq auth` (it replaced `moq-token-cli` upstream; `TOKEN_BIN` overrides the whole +command prefix); `@moq/auth` is installed from npm on each run; `rust-docker` +`docker pull`s the `moqdev/moq-cli` image fresh (`:latest`) and runs the CLI in a throwaway container with the scratch dir bind-mounted. The image is built `FROM nixos/nix` and ships the nix store, so it's a genuinely different artifact from the `cargo`/`brew`/`apt` binaries — and in CI it runs only on the Linux runners (GitHub's macOS runners have no Docker @@ -143,7 +141,7 @@ daemon); set `TOKEN_DOCKER=podman` to drive it with podman. For every generator mints a key and signs a token, and the verifier checks it — covering both symmetric (`HS256`, shared secret) and asymmetric (`EdDSA`/`ES256`/`RS256`, sign-private/verify-public) keys, and the fact that one side's key encoding -(the Rust CLI writes base64url-JSON; `@moq/token` writes plain JSON) loads on the +(the Rust CLI writes base64url-JSON; `@moq/auth` writes plain JSON) loads on the other. A negative pass then confirms each verifier **rejects** a tampered token and a token signed by the wrong key, so a green cell means "accepts the valid one and refuses the bad ones", not "accepts everything". @@ -156,7 +154,7 @@ export that didn't survive `tsc`) shows up as a red cell. ```bash just token # default: rust generates + verifies (roundtrip + negatives) just token-full # full matrix: rust, js-node, js-bun + rust-docker (the - # moqdev/moq-token-cli image, where a container runtime is + # moqdev/moq-cli image, where a container runtime is # available; set TOKEN_DOCKER=podman to use podman) # or call it directly with explicit axes: ./token.sh --generators rust,js-node --verifiers rust,js-bun --algorithms HS256,EdDSA @@ -187,7 +185,7 @@ This test tracks the **latest published** packages, so it sometimes runs ahead o - **Native JS on node** (`js-native-node`): working. node briefly lagged bun here: `@moq/web-transport`'s `session.ts` did `import { NapiClient } from "../napi.js"` — a *named* import from a napi-rs CJS module whose exports node's ESM loader can't statically see, so node threw `does not provide an export named 'NapiClient'` while Bun's looser CJS interop accepted it. `@moq/web-transport` 0.1.2 shipped the predicted fix (default-import the now-`.cjs` binding, then destructure `NapiClient`), so this cell is green. Exactly the break-then-fix this repo exists to surface. - **Go (any role)**: working. The `moq-dev/moq-go` module was un-buildable (stuck at v0.2.15, missing the generated `moq.h` header and the prebuilt static libs, so `go get` + build failed); v0.2.22 now ships `moq.h` plus `libmoq_ffi.a` for linux (amd64/arm64), darwin, and windows, and a `CGO_ENABLED=1 go build` against it links cleanly — verified in a linux/amd64 container, clearing the blocker that kept this cell red. One caveat the matrix doesn't see: building the Go client on **macOS** still fails to link, because the module's darwin cgo `LDFLAGS` omit `-framework CoreServices` (needed by the bundled Rust `notify` crate's FSEvents backend); CI only builds Go on Linux. Tracked upstream in moq-dev/moq's `go/moq/cgo.go`. - **GStreamer subscribe** (`gst`): working. `moq-gst` ships apt/brew/rpm/tarball + nix artifacts, so the cell resolves the newest tag and selects the matching platform tarball from that release's asset metadata. The published plugin load-checks green — `gst-inspect-1.0 moq` exposes `moqsrc`/`moqsink` against a system GStreamer — and `moqsrc` reads a rust-published H.264 broadcast end-to-end. -- **Token interop** (`token.sh`): working on **cargo / apt / nix** plus the **`moqdev/moq-token-cli` Docker image** (Linux). The published `moq-token` binary (from crates.io / apt / nix / Docker Hub) and `@moq/token` (npm, under both node and bun) cross-verify every token across `HS256`, `EdDSA`, `ES256`, and `RS256`, and each verifier rejects tampered tokens and the wrong key. The Docker cell (`rust-docker`) proves the image — built `FROM nixos/nix`, so it carries the libiconv the brew bottle used to leak — runs cleanly. Subscriber-only languages don't ship token tooling yet, so the matrix is rust (binary + Docker) + the two JS runtimes for now. +- **Token interop** (`token.sh`): working on **cargo / apt / nix** plus the **`moqdev/moq-cli` Docker image** (Linux). The published `moq auth` (from crates.io / apt / nix / Docker Hub) and `@moq/auth` (npm, under both node and bun) cross-verify every token across `HS256`, `EdDSA`, `ES256`, and `RS256`, and each verifier rejects tampered tokens and the wrong key. The Docker cell (`rust-docker`) proves the image — built `FROM nixos/nix`, so it carries the libiconv the brew bottle used to leak — runs cleanly. Subscriber-only languages don't ship token tooling yet, so the matrix is rust (binary + Docker) + the two JS runtimes for now. - **Token interop on the Homebrew bottle** (`rust` cells, macOS `brew`): working. The `moq-dev/tap/moq-token-cli` package's `moq-token` binary used to abort on launch — it baked in a `/nix/store/…-libiconv/lib/libiconv.2.dylib` rpath from the build sandbox that doesn't exist on a user's Mac (`dyld: Library not loaded`). The 0.5.31 bottle fixes it: its only `LC_RPATH` is now `/usr/lib`, so `@rpath/libiconv.2.dylib` resolves to the system libiconv and the binary runs (verified locally — `generate --algorithm HS256` succeeds, no leaked `/nix/store` rpath). `token.sh` still probes the binary once at startup, so a relapse would be caught again. Exactly the break-then-fix this repo exists to surface. - **Cloudflare interoperability**: the Cloudflare client publishes and subscribes over WebTransport and raw QUIC through both `cloudflare/moq-rs`'s `moq-relay-ietf` and `moq-dev/moq`'s `moq-relay`, with sustained subgroup payloads checked byte-for-byte. Cloudflare's relay additionally exercises datagrams in both directions. This is a source-head smoke test, so a later upstream commit can intentionally turn it red. - **Moxygen interoperability**: currently **red**. Moxygen's published source-head interop client negotiates draft-16 and passes 5/6 relay scenarios through `moq-dev/moq`, but `announce-subscribe` closes the subscriber session instead of routing it to the announced publisher. The failure reproduces over WebTransport and raw QUIC with the published relay, and over WebTransport with current moq-dev HEAD. CI runs the full Linux/amd64 Docker lane as non-blocking diagnostic coverage until the mismatch is fixed; `just moxygen` still exits nonzero locally. diff --git a/clients/js-native/subscribe.ts b/clients/js-native/subscribe.ts index 8d154fb..80e9c73 100644 --- a/clients/js-native/subscribe.ts +++ b/clients/js-native/subscribe.ts @@ -48,37 +48,27 @@ function closeActiveConnection(): void { } async function run(): Promise { - const connection = await Moq.Connection.connect(new URL(url as string), { signal: abort.signal }); + // Since @moq/net 0.4.0 a session doesn't consume broadcasts itself: it feeds the + // peer's announcements into an origin, and consumers request paths from that. + const origin = new Moq.Origin.Producer(); + const connection = await Moq.Connection.connect({ url: new URL(url as string), consume: origin, signal: abort.signal }); activeConnection = connection; - try { - const path = Moq.Path.from(broadcast as string); - - // Wait for the broadcast to be announced before subscribing. Subscribing to a - // track on a broadcast the publisher hasn't announced yet races the relay, - // which resets the catalog stream (RESET_STREAM). The Rust API folds this - // wait into consume(); the JS API leaves it to the caller. The outer timeout - // below bounds how long we wait. - const announced = connection.announced(path); - try { - for (;;) { - const entry = await announced.next(); - if (!entry) throw new Error("connection closed before broadcast was announced"); - // Entry paths are relative to the prefix passed to announced() -- here - // the exact broadcast -- so any active entry is the one we asked for. - if (entry.active) break; - } - } finally { - announced.close(); - } - const bc = connection.consume(path); + // Wait for the broadcast to be announced before subscribing. Subscribing to a + // track on a broadcast the publisher hasn't announced yet races the relay, + // which resets the catalog stream (RESET_STREAM). `announced: true` folds that + // wait into the request; the outer timeout below bounds it. + const request = origin.request(Moq.Path.from(broadcast as string), { announced: true }); + try { + let bc = request.active.peek(); + while (!bc) bc = await request.active.changed(); // The .hang catalog lives on the Catalog.TRACK ("catalog.json") track, one // JSON snapshot per frame validated against RootSchema. (@moq/hang 0.3.0 // dropped the Catalog.Consumer helper, so read the frames directly.) A lazy // publisher may announce video in a later update, so keep pulling until one // carries a video track. - const catalog = bc.subscribe(Catalog.TRACK, { priority: Catalog.PRIORITY.catalog }); + const catalog = bc.track(Catalog.TRACK).subscribe({ priority: Catalog.PRIORITY.catalog }); let videoTrack: string | undefined; while (!videoTrack) { const group = await catalog.recvGroup(); @@ -90,7 +80,7 @@ async function run(): Promise { if (renditions) videoTrack = Object.keys(renditions)[0]; } - const video = bc.subscribe(videoTrack, { priority: 0 }); + const video = bc.track(videoTrack).subscribe({ priority: 0 }); let total = 0; for (;;) { const group = await video.recvGroup(); @@ -107,7 +97,9 @@ async function run(): Promise { } throw new Error("no frame data received"); } finally { + request.close(); if (activeConnection === connection) closeActiveConnection(); + origin.close(); } } diff --git a/clients/token/js/package.json b/clients/token/js/package.json index 8206e37..57c62cc 100644 --- a/clients/token/js/package.json +++ b/clients/token/js/package.json @@ -2,8 +2,8 @@ "name": "moq-smoke-token", "private": true, "type": "module", - "description": "Installs the published @moq/token package so token.sh can drive its CLI under node and bun.", + "description": "Installs the published @moq/auth package so token.sh can drive its CLI under node and bun.", "dependencies": { - "@moq/token": "latest" + "@moq/auth": "latest" } } diff --git a/clients/token/js/resolve-bin.mjs b/clients/token/js/resolve-bin.mjs index c1fb114..abcf0c6 100644 --- a/clients/token/js/resolve-bin.mjs +++ b/clients/token/js/resolve-bin.mjs @@ -1,9 +1,9 @@ -// Print the absolute path to the published @moq/token CLI entrypoint. +// Print the absolute path to the published @moq/auth CLI entrypoint. // // token.sh runs this with BOTH node and bun so each runtime resolves the same // installed package and we drive the *published* bin (compiled dist), not the // in-tree TypeScript source. We read the installed package.json straight off -// disk rather than via module resolution: @moq/token's `exports` map doesn't +// disk rather than via module resolution: @moq/auth's `exports` map doesn't // expose ./package.json, which Node's strict ESM resolver refuses (bun allows // it), so require.resolve would work under bun but throw under node. Reading the // file keeps both runtimes on the same path, and the bin name is still taken @@ -11,12 +11,12 @@ import { readFileSync } from "node:fs"; import { resolve } from "node:path"; -const pkgDir = resolve(process.cwd(), "node_modules/@moq/token"); +const pkgDir = resolve(process.cwd(), "node_modules/@moq/auth"); const pkg = JSON.parse(readFileSync(resolve(pkgDir, "package.json"), "utf8")); -const bin = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.["moq-token"]; +const bin = typeof pkg.bin === "string" ? pkg.bin : pkg.bin?.["moq-auth"]; if (!bin) { - console.error("@moq/token exposes no moq-token bin; published package changed shape"); + console.error("@moq/auth exposes no moq-auth bin; published package changed shape"); process.exit(1); } diff --git a/freshness.sh b/freshness.sh index a32361d..53af27a 100755 --- a/freshness.sh +++ b/freshness.sh @@ -45,18 +45,18 @@ for dep in @moq/net @moq/hang @moq/web-transport; do fail=1 fi done -# The token client (@moq/token, driven by token.sh under node and bun) must be latest. -ver=$(grep -oE "\"@moq/token\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" clients/token/js/package.json | sed -E 's/.*"([^"]*)"$/\1/') -if [[ "$ver" == "latest" ]]; then note ok "@moq/token -> \"$ver\""; else - note FAIL "@moq/token pinned to \"$ver\" (want \"latest\")" +# The token client (@moq/auth, driven by token.sh under node and bun) must be latest. +ver=$(grep -oE "\"@moq/auth\"[[:space:]]*:[[:space:]]*\"[^\"]*\"" clients/token/js/package.json | sed -E 's/.*"([^"]*)"$/\1/') +if [[ "$ver" == "latest" ]]; then note ok "@moq/auth -> \"$ver\""; else + note FAIL "@moq/auth pinned to \"$ver\" (want \"latest\")" fail=1 fi # The token Docker image must be the unpinned (:latest) tag, pulled fresh each run. # shellcheck disable=SC2016 # grepping for these literal strings in token.sh; the $vars must NOT expand here -if grep -qF 'DOCKER_TOKEN_IMAGE:-moqdev/moq-token-cli}' token.sh && grep -qF '"$DOCKER" pull "$DOCKER_TOKEN_IMAGE"' token.sh; then - note ok "moqdev/moq-token-cli -> :latest (pulled each run)" +if grep -qF 'DOCKER_TOKEN_IMAGE:-moqdev/moq-cli}' token.sh && grep -qF '"$DOCKER" pull "$DOCKER_TOKEN_IMAGE"' token.sh; then + note ok "moqdev/moq-cli (token cell) -> :latest (pulled each run)" else - note FAIL "token.sh no longer pulls an unpinned moqdev/moq-token-cli :latest" + note FAIL "token.sh no longer pulls an unpinned moqdev/moq-cli :latest" fail=1 fi # The media Docker channel (relay + cli wrappers) must use the unpinned (:latest) diff --git a/justfile b/justfile index ca00948..1f165cb 100644 --- a/justfile +++ b/justfile @@ -30,15 +30,15 @@ cloudflare: moxygen: ./moxygen.sh -# Token interop: install moq-token in each published flavour and cross-verify. -# The Rust moq-token binary comes from a channel (PATH); @moq/token comes from npm, +# Token interop: install moq auth in each published flavour and cross-verify. +# The Rust `moq auth` comes from a channel (PATH); @moq/auth comes from npm, # driven under both node and bun. Default: rust only. Pass flags through, e.g. # just token --generators rust,js-node --verifiers rust,js-bun --algorithms HS256 token *args: ./token.sh {{ args }} # Full token matrix: every implementation mints and verifies every other's -# tokens. rust-docker pulls the published moqdev/moq-token-cli image (needs a +# tokens. rust-docker pulls the published moqdev/moq-cli image (needs a # container runtime; set TOKEN_DOCKER=podman to use podman instead of docker). token-full: ./token.sh --generators rust,js-node,js-bun,rust-docker --verifiers rust,js-node,js-bun,rust-docker diff --git a/token.sh b/token.sh index e5527e2..d1744d4 100755 --- a/token.sh +++ b/token.sh @@ -1,12 +1,11 @@ #!/usr/bin/env bash # Cross-implementation token interop smoke test against the PUBLIC packages. # -# moq-relay authenticates with JWTs minted by the moq-token tooling, which ships +# moq-relay authenticates with JWTs minted by the moq auth tooling, which ships # in several flavours from several registries: # -# - rust : the moq-token binary (cargo / brew / apt), or `moq auth` from -# moq-cli >= 0.12 where the token CLI now lives (nix) -# - js-node : the @moq/token npm package's `moq-token` CLI, run under node +# - rust : `moq auth` from moq-cli (cargo / brew / apt / nix) +# - js-node : the @moq/auth npm package's `moq-auth` CLI, run under node # - js-bun : the same published npm package, run under bun # # A token minted by any one of these must verify under every other one, or a @@ -34,14 +33,14 @@ VERIFIERS="rust" # can't silently stop exercising one. Override with --algorithms / TOKEN_ALGORITHMS. ALGORITHMS="${TOKEN_ALGORITHMS:-HS256,EdDSA,ES256,RS256}" -# The Rust CLI under test, as a command prefix. Whatever channel installed it -# (cargo/brew/apt/nix) just has to leave it on PATH; override here to point at a -# specific build, e.g. TOKEN_BIN="/path/to/moq auth". -TOKEN="${TOKEN_BIN:-}" +# The Rust CLI under test, as a command prefix: the same `moq` binary smoke.sh +# tests (MOQ_BIN, else PATH), since moq-token-cli was folded into `moq auth` +# (moq-dev/moq#3684). TOKEN_BIN overrides the whole prefix. +TOKEN="${TOKEN_BIN:-${MOQ_BIN:-moq} auth}" # The published Docker image for the `rust-docker` cell. Untagged = :latest, the # tag the release pipeline moves to the newest version; pulled fresh each run. -DOCKER_TOKEN_IMAGE="${DOCKER_TOKEN_IMAGE:-moqdev/moq-token-cli}" +DOCKER_TOKEN_IMAGE="${DOCKER_TOKEN_IMAGE:-moqdev/moq-cli}" # Container runtime for that cell. `docker` by default (what GitHub's Linux # runners ship); set TOKEN_DOCKER=podman to use a drop-in-compatible one. DOCKER="${TOKEN_DOCKER:-docker}" @@ -94,8 +93,8 @@ needs() { } TMP=$(mktemp -d) -CLI_NODE="" # node + @moq/token CLI path (set in prepare) -CLI_BUN="" # bun + @moq/token CLI path (set in prepare) +CLI_NODE="" # node + @moq/auth CLI path (set in prepare) +CLI_BUN="" # bun + @moq/auth CLI path (set in prepare) BROKEN_IMPLS="" mark_broken() { @@ -118,22 +117,6 @@ trap cleanup EXIT have() { command -v "$1" >/dev/null 2>&1; } -resolve_token() { - # Prefer the standalone binary while channels still ship it, then fall back to - # `moq auth`, which replaced moq-token-cli upstream (moq-dev/moq#3684). - # TOKEN_BIN remains authoritative when set. - [[ -n "$TOKEN" ]] && return 0 - if have moq-token; then - TOKEN=moq-token - elif have moq-token-cli; then - TOKEN=moq-token-cli - elif have moq && moq auth --help >/dev/null 2>&1; then - TOKEN="moq auth" - else - TOKEN=moq-token - fi -} - # ── per-implementation adapters ────────────────────────────────────────────── # Each implementation's CLI differs (flag names, key encoding, verify output), # so every operation is funnelled through an adapter that normalises it. The @@ -147,18 +130,18 @@ resolve_token() { # Symmetric (HS256): sign.jwk == verify.jwk (shared secret). # Asymmetric (EdDSA/ES256/RS256): verify.jwk is the public half. # Key encodings cross over fine: the Rust CLI writes base64url-JSON and reads -# either; @moq/token writes plain JSON and reads either. +# either; @moq/auth writes plain JSON and reads either. cli_for() { - # The command prefix that runs each implementation's moq-token CLI. The word + # The command prefix that runs each implementation's token CLI. The word # split is deliberate (runtime + path, or a whole `docker run ...` line), so # callers expand it unquoted. case "$1" in - rust) echo "$TOKEN" ;; # may be multi-word, e.g. `moq auth` + rust) echo "$TOKEN" ;; # Mount TMP at its real path so the in-container CLI reads/writes the same # key/token files token.sh hands it. The image bundles the nix store, so # the binary's libiconv deps resolve (the brew bottle's bug doesn't apply). - rust-docker) echo "$DOCKER run --rm --user $(id -u):$(id -g) -v $TMP:$TMP -w $TMP $DOCKER_TOKEN_IMAGE" ;; + rust-docker) echo "$DOCKER run --rm --user $(id -u):$(id -g) -v $TMP:$TMP -w $TMP $DOCKER_TOKEN_IMAGE auth" ;; js-node) echo "node $CLI_NODE" ;; js-bun) echo "bun $CLI_BUN" ;; *) return 1 ;; @@ -187,11 +170,11 @@ gen() { js-node | js-bun) if [[ "$algo" == HS* ]]; then # shellcheck disable=SC2086 - $cli generate --key "$dir/sign.jwk" --algorithm "$algo" >/dev/null + $cli generate --out "$dir/sign.jwk" --algorithm "$algo" >/dev/null cp "$dir/sign.jwk" "$dir/verify.jwk" else # shellcheck disable=SC2086 - $cli generate --key "$dir/sign.jwk" --algorithm "$algo" --public "$dir/verify.jwk" >/dev/null + $cli generate --out "$dir/sign.jwk" --algorithm "$algo" --public "$dir/verify.jwk" >/dev/null fi ;; esac @@ -238,11 +221,10 @@ rust_probe() { } "$SMOKE_DIR/freshness.sh" || echo "WARN: freshness check failed (see above); continuing" >&2 -resolve_token if needs rust; then if ! have "${TOKEN%% *}"; then - mark_broken rust "$TOKEN not found (install moq-token-cli, or moq-cli >= 0.12 for moq auth)" + mark_broken rust "${TOKEN%% *} not found (cargo/brew/apt/nix install moq-cli)" # `have` only checks the file exists; actually run it once, since a broken # published binary (e.g. a Homebrew bottle that baked in a /nix/store rpath # and aborts on launch) is exactly the packaging failure this test exists to @@ -265,7 +247,7 @@ if needs rust-docker; then # always-latest install), then run it once to confirm the image works. elif "$DOCKER" pull "$DOCKER_TOKEN_IMAGE" >"$TMP/docker-pull.log" 2>&1 && "$DOCKER" run --rm --user "$(id -u):$(id -g)" -v "$TMP:$TMP" -w "$TMP" \ - "$DOCKER_TOKEN_IMAGE" generate --algorithm HS256 --out "$TMP/docker-probe.jwk" >"$TMP/docker-probe.log" 2>&1; then + "$DOCKER_TOKEN_IMAGE" auth generate --algorithm HS256 --out "$TMP/docker-probe.jwk" >"$TMP/docker-probe.log" 2>&1; then echo "rust-docker: $DOCKER_TOKEN_IMAGE (latest, via $DOCKER)" else mark_broken rust-docker "$DOCKER pull/run $DOCKER_TOKEN_IMAGE failed (see below)" @@ -274,14 +256,14 @@ if needs rust-docker; then fi if needs js-node || needs js-bun; then - echo "installing js token client (@moq/token from npm)..." + echo "installing js token client (@moq/auth from npm)..." if ! have bun; then for v in js-node js-bun; do needs "$v" && mark_broken "$v" "bun not found (needed to install)"; done elif (cd "$JS_DIR" && bun install) >"$TMP/js-install.log" 2>&1; then # Resolve the published CLI path under each runtime we actually need. if needs js-bun; then if CLI_BUN=$(cd "$JS_DIR" && bun resolve-bin.mjs 2>"$TMP/js-bun-resolve.log"); then :; else - mark_broken js-bun "could not resolve @moq/token CLI under bun" + mark_broken js-bun "could not resolve @moq/auth CLI under bun" sed 's/^/ /' "$TMP/js-bun-resolve.log" >&2 || true fi fi @@ -289,7 +271,7 @@ if needs js-node || needs js-bun; then if ! have node; then mark_broken js-node "node not found" elif CLI_NODE=$(cd "$JS_DIR" && node resolve-bin.mjs 2>"$TMP/js-node-resolve.log"); then :; else - mark_broken js-node "could not resolve @moq/token CLI under node" + mark_broken js-node "could not resolve @moq/auth CLI under node" sed 's/^/ /' "$TMP/js-node-resolve.log" >&2 || true fi fi From e86b60aafdaad59312d800d9d8fc255d041b78e2 Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Wed, 23 Sep 2026 14:22:39 -0700 Subject: [PATCH 4/6] Follow moq-go's move to moq.dev/moq and its v0.7 API moq-go v0.7.0 declares module moq.dev/moq (served by moq.dev, mirrored at moq-dev/moq-go), so `go get github.com/moq-dev/moq-go@latest` now fails on the module path mismatch. Fetch moq.dev/moq instead, and port the client: PublishMediaStream("avc3") is PublishVideoStream(VideoFormatAvc3), and SubscribeMedia / SubscribeCatalog take a context. v0.7.0 itself does not compile yet (it pins moq-ffi 0.4.0 but uses API from #3949; moq-dev/moq#3999), so the Go cells stay red until moq-ffi 0.4.1 and a follow-up wrapper release ship. Co-Authored-By: Claude Opus 5.5 --- README.md | 4 ++-- clients/go/go.mod | 4 ++-- clients/go/smoke.go | 16 ++++++++-------- flake.nix | 2 +- freshness.sh | 6 +++--- smoke.sh | 10 +++++----- 6 files changed, 21 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 4765fc8..b9d044e 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ We check that bytes move across implementations, not that H.264 decodes. |---|---|---| | `moq-relay` + `moq` (Rust) | crates.io / Homebrew tap / apt repo / the moq flake / Docker Hub | `cargo install`, `brew install moq-dev/tap/...`, `apt install`, `nix build github:moq-dev/moq#...`, `docker run moqdev/moq-relay` | | Python | [PyPI `moq-rs`](https://pypi.org/project/moq-rs/) (import `moq`) | `uv pip install moq-rs` | -| Go | [`github.com/moq-dev/moq-go`](https://github.com/moq-dev/moq-go) | `go get` | +| Go | [`moq.dev/moq`](https://pkg.go.dev/moq.dev/moq) (mirrored at [moq-dev/moq-go](https://github.com/moq-dev/moq-go)) | `go get` | | Browser | npm [`@moq/watch`](https://www.npmjs.com/package/@moq/watch) + [`@moq/publish`](https://www.npmjs.com/package/@moq/publish), delivered three ways | headless Chromium (Playwright) loading a **vite** bundle, an **esbuild** bundle, or straight from the **jsDelivr** ESM CDN | | Native JS | npm [`@moq/net`](https://www.npmjs.com/package/@moq/net) + [`@moq/hang`](https://www.npmjs.com/package/@moq/hang) + moq's own [`@moq/web-transport`](https://www.npmjs.com/package/@moq/web-transport) polyfill | non-browser runtimes: **node** and **bun** | | Swift | SPM [`moq-dev/moq-swift`](https://github.com/moq-dev/moq-swift) | `swift build` (macOS, Xcode toolchain) | @@ -99,7 +99,7 @@ smoke-legacy.toml same, pre-0.15 layout; smoke.sh picks it for relays wit token.sh orchestrator: moq auth generate/verify interop matrix clients/ python/smoke.py publish/subscribe via moq-rs (PyPI) - go/ publish/subscribe via moq-dev/moq-go (go get) + go/ publish/subscribe via moq.dev/moq (go get) js/ headless-Chromium publish/subscribe via @moq/watch + @moq/publish; three delivery variants: vite, esbuild, jsdelivr (shared jsdelivr/setup.js) swift/ subscribe via moq-dev/moq-swift (SPM, macOS) diff --git a/clients/go/go.mod b/clients/go/go.mod index a5d7bfe..5245e5b 100644 --- a/clients/go/go.mod +++ b/clients/go/go.mod @@ -2,6 +2,6 @@ module moqsmoke go 1.23 -require github.com/moq-dev/moq-go v0.5.0 +require moq.dev/moq v0.7.0 -require github.com/moq-dev/moq-go-ffi v0.3.2 // indirect +require moq.dev/moq-ffi v0.4.0 // indirect diff --git a/clients/go/smoke.go b/clients/go/smoke.go index 0c00681..9a0a662 100644 --- a/clients/go/smoke.go +++ b/clients/go/smoke.go @@ -1,7 +1,7 @@ // Cross-language interop client for the smoke test, built against the ergonomic -// github.com/moq-dev/moq-go wrapper (package moq: Dial, CreateBroadcast, -// PublishMediaStream, SubscribeMedia, range-over-func frame iterators). The raw -// uniffi-bindgen-go surface lives in github.com/moq-dev/moq-go-ffi; this client +// moq.dev/moq wrapper (package moq: Dial, CreateBroadcast, +// PublishVideoStream, SubscribeMedia, range-over-func frame iterators). The raw +// uniffi-bindgen-go surface lives in moq.dev/moq-ffi; this client // exercises the idiomatic wrapper a real Go user would reach for. // // publish reads raw Annex-B H.264 from stdin (e.g. piped from ffmpeg) and feeds @@ -21,7 +21,7 @@ import ( "os" "time" - "github.com/moq-dev/moq-go/moq" + "moq.dev/moq" ) const readChunk = 64 * 1024 @@ -77,9 +77,9 @@ func publish(url, broadcast string) error { defer producer.Finish() // avc3: a self-describing Annex-B H.264 stream the importer can frame on its - // own. PublishMediaStream feeds the raw byte stream; whole frames are emitted + // own. PublishVideoStream feeds the raw byte stream; whole frames are emitted // as they complete. - media, err := producer.PublishMediaStream("avc3") + media, err := producer.PublishVideoStream(moq.VideoFormatAvc3) if err != nil { return err } @@ -133,7 +133,7 @@ func subscribe(url, broadcast string, timeoutS float64) error { return err } - media, err := bc.SubscribeMedia(name, video.Container, nil) + media, err := bc.SubscribeMedia(ctx, name, video.Container, nil) if err != nil { return err } @@ -155,7 +155,7 @@ func subscribe(url, broadcast string, timeoutS float64) error { // lazy publisher (e.g. the browser, which only encodes on demand) may announce // video in a later update, not the first snapshot. func videoTrack(ctx context.Context, bc *moq.BroadcastConsumer) (string, moq.Video, error) { - cat, err := bc.SubscribeCatalog() + cat, err := bc.SubscribeCatalog(ctx) if err != nil { return "", moq.Video{}, err } diff --git a/flake.nix b/flake.nix index 05d231e..6de0f1e 100644 --- a/flake.nix +++ b/flake.nix @@ -43,7 +43,7 @@ uv python3 - # go client (go get moq-dev/moq-go); cgo links the prebuilt libmoq_ffi.a + # go client (go get moq.dev/moq); cgo links the prebuilt libmoq_ffi.a go # kotlin client (dev.moq:moq from Maven Central) on the JVM diff --git a/freshness.sh b/freshness.sh index 53af27a..0d347a2 100755 --- a/freshness.sh +++ b/freshness.sh @@ -82,10 +82,10 @@ else note FAIL "smoke.sh no longer installs moq-rs unpinned" fail=1 fi -if grep -q 'go get "github.com/moq-dev/moq-go@latest"' smoke.sh; then - note ok "moq-go -> go get @latest" +if grep -q 'go get "moq.dev/moq@latest"' smoke.sh; then + note ok "moq.dev/moq -> go get @latest" else - note FAIL "smoke.sh no longer go-gets moq-go @latest" + note FAIL "smoke.sh no longer go-gets moq.dev/moq @latest" fail=1 fi # Both relays and the local integrity client deliberately follow their diff --git a/smoke.sh b/smoke.sh index 269d0ac..2996267 100755 --- a/smoke.sh +++ b/smoke.sh @@ -449,20 +449,20 @@ if needs python; then fi if needs go; then - echo "building go client (moq-dev/moq-go from the module proxy)..." + echo "building go client (moq.dev/moq from the module proxy)..." GO_SMOKE="$TMP/go-smoke" # Pull the latest published module, then build the client against it. The - # ergonomic moq-go wrapper pulls a transitive moq-go-ffi; `go get moq-go` - # records only moq-go's own checksum, so `go mod tidy` fetches the rest (no + # ergonomic moq.dev/moq wrapper pulls a transitive moq.dev/moq-ffi; `go get` + # records only the wrapper's own checksum, so `go mod tidy` fetches the rest (no # go.sum is committed -- freshness bans lockfiles -- so it's regenerated here). # The module proxy / sum.golang.org occasionally reset mid-stream; retry the # fetch (idempotent) and build only once it succeeds. # shellcheck disable=SC2329 # invoked via retry - go_fetch() (cd "$CLIENTS/go" && go get "github.com/moq-dev/moq-go@latest" && go mod tidy) + go_fetch() (cd "$CLIENTS/go" && go get "moq.dev/moq@latest" && go mod tidy) if ! have go; then mark_broken go "go not found" elif (retry 3 go_fetch && cd "$CLIENTS/go" && CGO_ENABLED=1 go build -o "$GO_SMOKE" .) >"$TMP/go-build.log" 2>&1; then :; else - mark_broken go "go get/build of moq-dev/moq-go failed" + mark_broken go "go get/build of moq.dev/moq failed" sed 's/^/ /' "$TMP/go-build.log" >&2 || true fi fi From a85b9031ae2aee0be305c4fe6e5b3c3a00da566f Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Wed, 23 Sep 2026 15:40:41 -0700 Subject: [PATCH 5/6] Run token interop even when a media cell fails Token interop is independent of the media matrix, but it was skipped whenever Smoke failed, so one red cell (today: the broken moq.dev/moq v0.7.0) hid every token result. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/smoke.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/smoke.yml b/.github/workflows/smoke.yml index f5d4842..a25cc18 100644 --- a/.github/workflows/smoke.yml +++ b/.github/workflows/smoke.yml @@ -218,6 +218,8 @@ jobs: # moqdev/moq-cli image. The negative pass inside token.sh confirms each verifier rejects # tampered tokens and the wrong key. - name: Token interop + # Independent of the media matrix, so a red media cell mustn't hide it. + if: ${{ !cancelled() }} run: | # The `rust` impl runs a native `moq auth`, which only the # cargo/apt/brew/nix channels install. The docker channel's moq wrapper From 308980e41712cfc88987df1408c685e2eefcfcdb Mon Sep 17 00:00:00 2001 From: Luke Curley Date: Wed, 23 Sep 2026 20:53:46 -0700 Subject: [PATCH 6/6] Follow moq-rs 0.5.0 / moq.dev/moq v0.7.1 publish API moq-rs 0.5.0 and moq.dev/moq v0.7.x (both released 2026-09-24 on moq-ffi 0.4.1) renamed the raw-stream publisher and stopped advertising a broadcast on creation: create_broadcast only registers the path locally, and announce() advertises it to peers. The Python publisher failed every cell (no attribute publish_media_stream) and, once renamed, subscribers saw nothing until the broadcast was announced. - python: publish_video_stream(VideoFormat.AVC3), then announce(). - go: Announce(moq.Route{}) after PublishVideoStream. Co-Authored-By: Claude Opus 5.5 --- clients/go/go.mod | 4 ++-- clients/go/smoke.go | 6 ++++++ clients/python/smoke.py | 7 +++++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/clients/go/go.mod b/clients/go/go.mod index 5245e5b..bf9c0e5 100644 --- a/clients/go/go.mod +++ b/clients/go/go.mod @@ -2,6 +2,6 @@ module moqsmoke go 1.23 -require moq.dev/moq v0.7.0 +require moq.dev/moq v0.7.1 -require moq.dev/moq-ffi v0.4.0 // indirect +require moq.dev/moq-ffi v0.4.1 // indirect diff --git a/clients/go/smoke.go b/clients/go/smoke.go index 9a0a662..ac6f05a 100644 --- a/clients/go/smoke.go +++ b/clients/go/smoke.go @@ -85,6 +85,12 @@ func publish(url, broadcast string) error { } defer media.Finish() + // CreateBroadcast only registers the path locally; Announce advertises it to + // the relay, once its tracks exist. + if err := producer.Announce(moq.Route{}); err != nil { + return err + } + fmt.Fprintf(os.Stderr, "publishing %q (Annex-B H.264 from stdin) to %s\n", broadcast, url) buf := make([]byte, readChunk) diff --git a/clients/python/smoke.py b/clients/python/smoke.py index d33d8ac..c57adca 100644 --- a/clients/python/smoke.py +++ b/clients/python/smoke.py @@ -22,10 +22,13 @@ async def publish(url: str, broadcast: str) -> None: async with moq.Client(url, tls_verify=False) as client: # create_broadcast registers the broadcast and hands back its producer # (the old client.publish(broadcast, producer) split was removed in the - # ergonomic moq-rs API). publish_media_stream feeds a raw Annex-B pipe to + # ergonomic moq-rs API). publish_video_stream feeds a raw Annex-B pipe to # the streaming importer, which infers frame boundaries. producer = client.create_broadcast(broadcast) - media = producer.publish_media_stream("avc3") + media = producer.publish_video_stream(moq.VideoFormat.AVC3) + # create_broadcast only registers the path locally; announce advertises it + # to the relay, once its tracks exist. + producer.announce() print(f"publishing {broadcast!r} (Annex-B H.264 from stdin) to {url}") loop = asyncio.get_running_loop()