The room client took its relay, room service, fixture, WebTransport URL, certificate hashes and issuer directory from its query string as given, and followed a relay's GOAWAY to any URI. A crafted link and a press of Join would send a joiner's camera and microphone to another origin. - web/room/endpoint.js: an endpoint is taken only on the page's own origin (`https://<host>`, or `wss://<host>` for a relay), never with user information; a page served from the loopback interface (a fixture page, the e2e harness) takes loopback addresses alone. - app.js: `service`, `ws`, `wt`, `fixture` and `issuer_directory` are held to it; `hashes` only on a loopback page. A refused endpoint is recorded (`snapshot().refused`) and shown, Join stays off, and the page connects to nothing: `enter()` checks the relay before it asks for the camera, a grant or a session. A GOAWAY naming another origin is refused: the page closes its session and says so. - web/room/endpoint.test.mjs (run by e2e/run.sh): own origin, another origin, scheme-relative, the page's host as user information, javascript:, a plain scheme on a secure page, an HTTP page, and loopback for the harness. - e2e scenario `origin`: a foreign `?ws=` is refused at Join and nothing connects (no WebSocket, request, grant or camera); a foreign `?service=`, scheme-relative, behind user information or as javascript:, is refused with Join off; a GOAWAY to another origin is refused and the session ends. The `service` scenario checks that every request and WebSocket of every page stays on the page's origin, as the production `connect-src` requires. Co-Authored-By: Claude Opus 5.5 <[email protected]>
200 lines
11 KiB
YAML
200 lines
11 KiB
YAML
name: joule-moq-web browser client
|
|
|
|
# The browser MoQ client (joule-code/joule-code-rs/crates/joule-moq-web) against
|
|
# joule-moq-relay on loopback, in the host's installed browser, driven by the automation
|
|
# package of e2e/package.json, with a fake camera and microphone
|
|
# (crates/joule-moq-web/e2e/run.sh):
|
|
#
|
|
# compose live-conformance/compose in the page: every layout, mix, descriptor and
|
|
# composition case through wai-live-web's wasm build, and wai-rs's own
|
|
# corpus check run in the browser;
|
|
# webtransport two participants and the room's clock over WebTransport: media both ways,
|
|
# every object verified against its receipt (the relay's door checks each
|
|
# too), the composed canvas's tiles equal to the host's layout for the same
|
|
# descriptor and set, the mix in an AudioWorklet, captions with their origin
|
|
# label, and a late joiner served earlier groups by FETCH;
|
|
# websocket the same over the WebSocket binding, forced;
|
|
# goaway the relay shut down with GOAWAY naming a second: every page moves and
|
|
# closes its old session before the timeout;
|
|
# service a room service (joule-moq-room) keeps the room, as in production: pages
|
|
# make their own keys and enter from their buttons, the service grants and
|
|
# its clock authors the lifecycle, the relay (WebSocket only, behind a
|
|
# same-origin proxy) honours the grants by the service's authority key;
|
|
# age a room that admits by age (WAI age-assurance §5.6): pages answer the room
|
|
# service's Privacy Pass challenges with tokens they obtain by blind issuance
|
|
# from a test issuer over the corpus's published keys; a viewer at 16 is served
|
|
# the audio and refused the video its performer rates 18 (below_threshold), a
|
|
# viewer at 18 is served both, a token of 13 is refused by the service, a token
|
|
# spent twice is refused, and the admission receipt verifies and suppresses;
|
|
# e2ee an end-to-end encrypted room (WAI confidentiality) kept by a room service:
|
|
# pages key through the service's log, seal every frame and open every object
|
|
# before it is decoded; a viewer outside the group opens nothing, every change
|
|
# a relay could make is refused with its reason, a late joiner opens nothing
|
|
# sealed before its join, a member that stops publishing is removed and opens
|
|
# no later epoch, and the relay's DVR store keeps nothing of the room;
|
|
# co a room with co-performance (WAI co-performance) kept by a room service behind a
|
|
# relay that holds the floor and the audience's inputs: a lead starts turns from
|
|
# its page, a performer out of turn is marked by the clock and, under a schedule
|
|
# that refuses, held back by the relay; an AI participant's lines show ahead of
|
|
# when they play, one kept and one not; a late object is marked; the audience
|
|
# votes and sends prompts from the page, the relay withholding each input the
|
|
# rules refuse; the clock commits and selects, credited; and the room with the
|
|
# clock's log of objects verifies the same in the page and on the host.
|
|
# ageco a room that admits by age and keeps co-performance: the room's clock, the
|
|
# room's own service by its proof of the room's keys (age-assurance §5.5),
|
|
# logs both performers' rated tracks and marks an object out of turn, while a
|
|
# viewer at 16 is still refused the video rated 18.
|
|
# origin the endpoints a link may name: a relay or a room service on another origin
|
|
# is refused and nothing connects; a GOAWAY naming another origin is refused
|
|
# and the session ends; and every page of the service scenario connects to its
|
|
# own origin alone.
|
|
# binding the browser sink (WAI browser-sink): live-conformance/browser in the page,
|
|
# a wasm-route decode verified against its receipts with every frame bound,
|
|
# the binding through clone, transfer, copyTo and re-encode, the user agent's
|
|
# decoder's frames bound through the side table, no codec string outside the
|
|
# WebCodecs Codec Registry put to the user agent, a measured figure per decode
|
|
# beside powerEfficient, and a room whose tiles show verified, bound frames.
|
|
# cowatch a watch room (WAI co-watch) kept by a room service: three viewers check the
|
|
# source by its content hash and enter by a proof of their keys; a play, a
|
|
# pause and a seek applied by every page at their instants, a late joiner
|
|
# brought to the room's position, the clock's skew statements in sync within
|
|
# the bound, and the room verified by co_watch_verify.py.
|
|
#
|
|
# Before the browser, wai-live-web's own tests: the room's records a page reads, co-performance's
|
|
# among them, each statement read and the corpus verified through the crate.
|
|
#
|
|
# The macos runner is the host with the browser installed; the tests drive that browser
|
|
# (its executable named by BROWSER_BIN), not a bundled one. The automation package comes
|
|
# from e2e/package-lock.json with install scripts off. Each browser runs under the host's
|
|
# memguard where it exists. Nothing listens beyond loopback.
|
|
#
|
|
# Push filters match the push's delta, so every path the job reads is listed.
|
|
|
|
concurrency:
|
|
group: ${{ github.workflow }}-${{ github.ref }}
|
|
cancel-in-progress: true
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
paths:
|
|
- 'joule-code/joule-code-rs/crates/joule-moq-web/**'
|
|
- 'joule-code/joule-code-rs/crates/joule-moq/**'
|
|
- 'joule-code/joule-code-rs/crates/joule-moq-mls/**'
|
|
- 'joule-code/joule-code-rs/crates/joule-moq-relay/**'
|
|
- 'joule-code/joule-code-rs/crates/joule-moq-room/**'
|
|
- 'joule-code/joule-code-rs/crates/jwp/**'
|
|
- 'joule-code/joule-code-rs/Cargo.toml'
|
|
- 'joule-code/joule-code-rs/Cargo.lock'
|
|
- 'joule-code/joule-code-rs/vendor/**'
|
|
- 'wai/wai-live-web/**'
|
|
- 'wai/wai-rs/**'
|
|
- 'wai/wai-ops/**'
|
|
- 'wai/live-conformance/compose/**'
|
|
- 'wai/live-conformance/presentation/**'
|
|
# The page's Privacy Pass client test reads RFC 9578 A.2's vector from the age corpus, and
|
|
# the age scenario's issuer its published test keys.
|
|
- 'wai/live-conformance/age/**'
|
|
- 'wai/live-conformance/coperformance/**'
|
|
- 'wai/live-conformance/browser/**'
|
|
- 'wai/wai-rs/tests/vectors/int_video/**'
|
|
# The page's co-watch module is tested against the co-watch corpus, and the cowatch
|
|
# scenario verifies its room with the corpus's second implementation.
|
|
- 'wai/live-conformance/cowatch/**'
|
|
- 'wai/live-conformance/co_watch_verify.py'
|
|
- '.github/workflows/joule-moq-web.yml'
|
|
pull_request:
|
|
paths:
|
|
- 'joule-code/joule-code-rs/crates/joule-moq-web/**'
|
|
- 'joule-code/joule-code-rs/crates/joule-moq/**'
|
|
- 'joule-code/joule-code-rs/crates/joule-moq-mls/**'
|
|
- 'joule-code/joule-code-rs/crates/joule-moq-relay/**'
|
|
- 'joule-code/joule-code-rs/crates/joule-moq-room/**'
|
|
- 'joule-code/joule-code-rs/crates/jwp/**'
|
|
- 'joule-code/joule-code-rs/Cargo.toml'
|
|
- 'joule-code/joule-code-rs/Cargo.lock'
|
|
- 'joule-code/joule-code-rs/vendor/**'
|
|
- 'wai/wai-live-web/**'
|
|
- 'wai/wai-rs/**'
|
|
- 'wai/wai-ops/**'
|
|
- 'wai/live-conformance/compose/**'
|
|
- 'wai/live-conformance/presentation/**'
|
|
# The page's Privacy Pass client test reads RFC 9578 A.2's vector from the age corpus, and
|
|
# the age scenario's issuer its published test keys.
|
|
- 'wai/live-conformance/age/**'
|
|
- 'wai/live-conformance/coperformance/**'
|
|
- 'wai/live-conformance/browser/**'
|
|
- 'wai/wai-rs/tests/vectors/int_video/**'
|
|
# The page's co-watch module is tested against the co-watch corpus, and the cowatch
|
|
# scenario verifies its room with the corpus's second implementation.
|
|
- 'wai/live-conformance/cowatch/**'
|
|
- 'wai/live-conformance/co_watch_verify.py'
|
|
- '.github/workflows/joule-moq-web.yml'
|
|
|
|
jobs:
|
|
# Forgejo matches `paths:` against the push delta, so a rebase fires this workflow
|
|
# whatever the pull request changes. This job reads the pull request's own diff against
|
|
# the paths above (scripts/pr-touches-paths.py, which fails open).
|
|
relevant:
|
|
name: joule-moq-web · does this change touch it
|
|
runs-on: linux
|
|
outputs:
|
|
run: ${{ steps.gate.outputs.run }}
|
|
steps:
|
|
- uses: actions/checkout@v5
|
|
with:
|
|
fetch-depth: 0
|
|
- id: gate
|
|
run: |
|
|
set -euo pipefail
|
|
r=$(python3 scripts/pr-touches-paths.py .github/workflows/joule-moq-web.yml \
|
|
"${{ github.event.pull_request.base.sha }}" \
|
|
"${{ github.event.pull_request.head.sha }}")
|
|
echo "this change touches the browser client: $r"
|
|
echo "run=$r" >> "$GITHUB_OUTPUT"
|
|
|
|
e2e:
|
|
name: joule-moq-web · browser end-to-end (macos)
|
|
needs: relevant
|
|
if: needs.relevant.outputs.run == 'true'
|
|
runs-on: macos
|
|
timeout-minutes: 90
|
|
steps:
|
|
- uses: actions/checkout@v5
|
|
# launchd's PATH omits cargo on the macOS host runner.
|
|
- name: Put cargo on PATH
|
|
shell: bash
|
|
run: echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
|
|
# Pinned to the wasm-bindgen both crates pin: the CLI and the crate must match.
|
|
- name: wasm32 target and wasm-bindgen CLI 0.2.127
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
rustup target add wasm32-unknown-unknown
|
|
cargo install --locked wasm-bindgen-cli --version 0.2.127
|
|
# The page's codec is joule-moq built without its runtime; this is the build that
|
|
# holds the feature split.
|
|
- name: joule-moq without its runtime
|
|
shell: bash
|
|
working-directory: joule-code/joule-code-rs
|
|
run: nice -n 10 cargo check --locked -p joule-moq --no-default-features
|
|
# The page's records library, natively: its co-performance reading checked against the
|
|
# corpus live-conformance/coperformance (src/co.rs) beside its own cases.
|
|
- name: wai-live-web, its own tests
|
|
shell: bash
|
|
working-directory: wai/wai-live-web
|
|
run: nice -n 10 cargo test --locked --lib
|
|
- name: The browser end-to-end tests
|
|
shell: bash
|
|
env:
|
|
BROWSER_BIN: /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
|
|
MOQ_WEB_OUT: ${{ runner.temp }}/moq-web
|
|
run: |
|
|
set -euo pipefail
|
|
test -x "$BROWSER_BIN" || { echo "::error::this runner has no browser at $BROWSER_BIN"; exit 1; }
|
|
mg=/Volumes/macos_4TB_external_1/wai-build/bin/memguard
|
|
if [ -x "$mg" ]; then export MEMGUARD="$mg"; fi
|
|
nice -n 10 bash joule-code/joule-code-rs/crates/joule-moq-web/e2e/run.sh 2>&1 | tee "$RUNNER_TEMP/moq-web-e2e.log"
|
|
grep -qE '^room e2e: 1 of 1 runs passed; ' "$RUNNER_TEMP/moq-web-e2e.log" \
|
|
|| { echo "::error::the browser end-to-end run did not pass"; exit 1; }
|
|
if grep -q '^FAIL' "$RUNNER_TEMP/moq-web-e2e.log"; then echo "::error::a check failed"; exit 1; fi
|