wai.live.e2ee in the browser: a page member in WebAssembly, frames sealed before publish and opened before decode, a room service carrying the group's messages (L10b) #366

Merged
dcharlot merged 17 commits from joule-moq/e2ee-web into main 2026-10-20 17:33:22 -04:00
Owner

L10b: end-to-end encrypted rooms in the browser

A browser page is now a member of an end-to-end encrypted room (WAI extensions/confidentiality.md). The page holds its own MLS state in WebAssembly. It seals each encoded frame before signing and publishing it, and opens each received object through the §6 checks, in order, before any decoder sees it. A room service (joule-moq-room, [room] e2ee = true) keeps such a room. It holds the room's WRE1 record and its clock, the MLS group's external sender, and it carries the group's messages in a log with one order. The spec gains §15, "A member in a browser", and §1's scope now covers the browser.

What is in it

  • joule-moq-mls: a member behind a carrier that orders the room's messages.

    • stage_create and stage_commit hold a member's GroupInfo or commit until the carrier returns it. receive then applies it, after the same checks every member runs. If another member's commit comes first, or the carrier refuses the post, the staged one is withdrawn.
    • Before its Welcome, a principal follows the group from its GroupInfo and handshake messages, using code it now shares with the clock. A Welcome addressed to someone else is ignored.
    • Clock::key_package_if keeps a KeyPackage only when it comes from a principal the room admitted.
    • Feature js builds the crate for wasm32.
  • joule-moq: a keyring now holds the locations it has opened per epoch, and drops them together with that epoch's keys. Memory in a long room is bounded by the epochs the page holds.

  • joule-moq-web, feature e2ee, built as a second wasm, joule_moq_web_e2ee, which a page loads only for such a room:

    • E2eeMember: KeyPackage, lifecycle events, log messages in order, staged create and commit, seal and open.
    • e2eeCheckRecord.
    • TrackSigner.signSealed: signs the receipt over the sealed bytes, with no WAI_RECEIPT.
    • UniItem.properties.
  • The room page. It shows that the room is end-to-end encrypted before entering, and offers no Watch and no seeking back. On entering it:

    • checks the record against the descriptor's room, authority and recording mode;
    • leaves a KeyPackage with the service;
    • follows the room's log (web/room/e2ee.js), checking each lifecycle record as the clock's (signature, room, chain) and passing each MLS message to the member;
    • creates the group or commits when §5 calls on it, and applies its own post only when the log returns it.

    Sending and receiving:

    • Frames are sealed, then signed, then sent, and get a group number only once sent.
    • Objects are verified against their receipt, then opened, then decoded.
    • An object from an epoch the page has not reached yet waits (bounded).
    • An object that does not open is counted with its reason and never reaches a decoder.
    • The page shows its epoch and roster.
    • The session can pass each object's property list to its sinks.
  • joule-moq-room:

    • every room gets a WRE1 record and an MLS Clock that follows the lifecycle;
    • GET /mls (long-poll of the room's log), POST /mls/key-package (granted principals only), POST /mls/post (the clock checks each post, and a refused post is not carried);
    • GET /room returns the record.
  • wai-live-web: roomDescriptor now returns recording_mode, which the page needs to check the record (§3.3 step 5).

  • Spec and docs:

    • confidentiality §1 scope, a new §15 (§15.1 sealing frames, §15.2 a room service as the carrier, §15.3 what a browser member does not do yet), and the intro;
    • a SPEC §5 sentence;
    • the joule-moq-web and joule-moq-room READMEs and room.example.toml;
    • both CHANGELOGs.
  • CI:

    • joule-moq-room and joule-moq-web workflows now also run when joule-moq-mls changes;
    • the conformance job runs cargo test -p joule-moq-web --features e2ee --lib. Those tests run only with the feature.
  • e2e/: a new e2ee scenario, built by run.sh (e2ee wasm under the new [profile.wasm], name section stripped).

Design decisions, and why

  • MLS in wasm: the MLS library and the pure-Rust crypto provider this tree already pins (0.9.1 and 0.6.0), unchanged, through the library's own browser feature. That feature gives the library browser randomness and a browser clock. The crypto provider seeds its ChaCha generator through an older randomness crate, which needs its own browser backend, so joule-moq-mls's feature js turns on both. On the host the feature changes nothing. The member builds for wasm32-unknown-unknown with no threads (the library skips its thread pool on wasm32) and no filesystem, and keys live only in the wasm member's memory.

  • Bundle size. The e2ee build is a separate wasm, so a page in any other room never loads MLS. Measured as run.sh builds them:

    build raw gzip
    joule_moq_web (unchanged) 776 KB 258 KB
    joule_moq_web_e2ee 2.04 MB 653 KB

    For the e2ee build:

    • with plain release and the name section kept it was 3.77 MB / 965 KB;
    • the new [profile.wasm] (LTO, one codegen unit, opt-level = "s") plus stripping the name and producers sections brings it to 2.04 MB / 653 KB;
    • a further size-optimisation pass made it smaller raw but larger gzipped, so it is not used.

    Most of the rest is the MLS library and its multi-suite crypto provider. The plain build is built exactly as before.

  • Carrier: the room service, not MoQ tracks.

    • §5.1 allows "a room service passing them directly".
    • The relay's grant check knows only events, audio and video tracks, so mls/keypackage tracks would have meant a relay change.
    • One log gives one order across lifecycle records and MLS messages, which members need: an Add must follow the join it answers.
    • The relay is unchanged and forwards sealed objects as before.
  • Staging is required, not a nicety, and §15.2 makes it a MUST. §5.5 names the committer from the commit's own proposals. So two members with different views (one has read a clock Add, the other also a Remove of the first) can each legitimately commit for the same epoch. If the first applied its commit at once, it would hold an epoch nobody else reaches. Members therefore apply their own post only when the log returns it.

  • Viewers in an E2EE room are not members. Their grant may subscribe, so it is keyed, but this service records presence from a publisher's tracks. A viewer is never added and opens nothing. The page hides Watch in such a room, and §15.3 says so. The e2e uses a viewer as "a subscriber without keys".

  • Inner digest: sealed on video, left out on audio (§4.4 SHOULD / MAY).

  • Group numbering. Frames encoded before the page is keyed are dropped without a group number. A late joiner's FETCH from group 0 then never asks the relay for groups it never had. Before this fix the relay forwarded such a FETCH to the publisher, which refuses requests; this showed up as one flaky e2e run.

  • Recording: the descriptor's mode is already 0, so §3.3 step 5 holds. The relay's DVR store keeps nothing of such a room, as the e2e checks.

Checks

All builds, tests and browsers ran on the build host through rb.

Check Result
cargo test --locked -p joule-moq-mls 14 lib + 4 corpus tests pass (4 new carrier tests)
cargo run -q --locked -p joule-moq-mls --bin joule_moq_e2ee_vectors verify ../../wai/live-conformance/e2ee exit 0
cargo test --locked -p joule-moq-web / --features e2ee 8 / 10 pass
cargo test --locked -p joule-moq --features secure-objects --lib secure_objects, --test secure_objects_vectors 8, 2 pass
cargo test --locked -p joule-moq-relay --test sealed_objects 2 pass
cargo build --locked -p joule-moq-relay -p joule-moq-ingest (dependents of joule-moq) ok
joule-moq-room: cargo test --locked --all-targets 5 unit + 2 integration pass (an_end_to_end_encrypted_room_keys_its_members_through_the_service new)
wai/wai-live-web: cargo test --locked, cargo clippy --locked --all-targets pass; nothing new
clippy --all-targets on joule-moq-mls, joule-moq-web (with and without e2ee), joule-moq-web e2ee for wasm32, joule-moq-room zero notes in the touched files. Remaining notes predate this PR: wai-rs unpinned_read, joule-moq-room/src/epoch.rs:418
wasm32 release builds, both variants, as run.sh ok
cargo metadata --locked --offline on the joule-code, joule-moq-room and examples/transport-moqt locks ok. Locks gained additions only: no package version removed or changed
wai registry export --check up to date
e2e/run.sh --runs 2, whole suite (compose, webtransport, websocket, goaway, dvr, service, e2ee), two separate invocations at the final merged head (after merging origin/main d9a614dd) 2 of 2 runs passed, 300 of 300 checks, both invocations. Two earlier invocations before that merge were green too
every live-conformance/*_verify.py with its corpus (shared text: SPEC, confidentiality) all exit 0
e2ee_verify.py every vector verifies; 18 of 18 mutated corpora fail
check-tests-in-ci.py, check-workflow-concurrency.sh, check-spec-refs.py, check-wai-section-refs.py, check-no-vanity-metrics.sh, test-public-text-checks.sh, check-comparative-prose.sh, check-orphan-sources.py all clean

What the e2ee scenario shows (31 checks per run; run.sh drives the relay over WebSocket with the door on, the room service with e2ee = true, and a DVR window over every track):

  • The page announces the room as end-to-end encrypted before entering.
  • Alice creates the group through the log, and Bob joins by her commit's Welcome.
  • Media decodes both ways. Every object is verified against its receipt over the sealed bytes and opened before it is decoded. The only refusals are of objects sealed before a member's own join.
  • The page shows its epoch and roster, and refuses to seek back.
  • A viewer outside the group receives exactly the sealed bytes, byte for byte as Alice sealed them, with the AEAD's length and no WAI1 envelope. It opens nothing and hands nothing to a decoder.
  • The full set of relay-style alterations to one object, presented before that object is opened, gives:
    alteration result
    a payload byte changed bad_tag
    the priority changed bad_tag
    moved to another group bad_tag
    the Key ID removed key_id_missing
    a Key ID added outside the Immutable Properties properties_malformed
    an Object ID of 2^32 object_id_too_large
    a KID naming an empty leaf key_id_unknown
    this member's own KID on the sender's track sender_not_owner
    as sent opens
    the same object again replayed
  • An object changed on the wire (re-signed, so the door passes it) is refused bad_tag and reaches no decoder. This is counted by envelopes that fail to read on the decode path. A decoder may still report the delta frame that follows the dropped one, so decoder errors are not what is counted.
  • Carol joins late with FETCH from group 0. Every earlier group is refused epoch_not_joined; the only fetched object that opens is from her own join epoch.
  • Carol keeps back one object, then stops publishing (PUBLISH_DONE) while keeping her session. The clock records her leave and Alice commits the removal. Carol still receives the new epoch's objects over the wire and refuses each removed_before_epoch, while Alice and Bob keep opening each other's. Carol's kept-back object is refused sender_removed.
  • The relay logs the room as sealed, so its DVR store keeps nothing.
  • The door refused nothing, and no page reports an error.

Falsifications

Each was applied on top of a commit, run, and reverted. F1 and F5 were run again after the final merge, with the same outcome.

Broken rule Result
F1: a member keeps its staged commit when another's comes first (the withdraw removed from receive) cargo test -p joule-moq-mls --lib exit 101: a_member_whose_commit_another_overtakes_drops_its_own_and_applies_the_other FAILED
F3: the service carries a post its clock refused joule-moq-room unit test a_room_with_a_record_and_a_log_two_members_key_through FAILED (exit 101); integration test an_end_to_end_encrypted_room_keys_its_members_through_the_service FAILED at "nothing refused was carried" (exit 101)
F4: the page signs and sends the envelope unsealed e2e --only e2ee exit 1: nothing opens, so nothing decodes ("alice decodes the other's media" never holds)
F5: the page hands an object that did not open to the decoder e2e --only e2ee exit 1: the viewer check fails (17 handed to a decoder) and the on-the-wire check fails (decode errors 0 → 2)

(F2, making stage_commit apply at once, broke every commit test. That is too blunt to say anything, so it is not counted.)

Not done

  • Watching without membership. A non-publishing member would need the service to record a viewer's presence and leave. Not built; §15.3.

  • WHIP/WebRTC ingest into an E2EE room. That would need the W3C encoded-transform interface. Not built; §15.3.

  • Seeking back in an E2EE room: by design the relay keeps nothing.

  • Production. The demo room keeps e2ee = false until the lead deploys. Turning it on needs:

    • e2ee = true in the room service's config;
    • the new page file web/room/e2ee.js, the build outputs web/pkg/joule_moq_web_e2ee{.js,_bg.wasm}, and the rebuilt wai_live_web (roomDescriptor now returns recording_mode).

    Nothing on the relay changes.

  • No new corpus vectors. The wire, record, reasons and key schedule are unchanged. The browser member runs the reference's code, and the e2e is its test.

🤖 Generated with Claude Code

## L10b: end-to-end encrypted rooms in the browser A browser page is now a member of an end-to-end encrypted room (WAI `extensions/confidentiality.md`). The page holds its own MLS state in WebAssembly. It seals each encoded frame before signing and publishing it, and opens each received object through the §6 checks, in order, before any decoder sees it. A room service (`joule-moq-room`, `[room] e2ee = true`) keeps such a room. It holds the room's `WRE1` record and its clock, the MLS group's external sender, and it carries the group's messages in a log with one order. The spec gains §15, "A member in a browser", and §1's scope now covers the browser. ### What is in it - **`joule-moq-mls`: a member behind a carrier that orders the room's messages.** - `stage_create` and `stage_commit` hold a member's GroupInfo or commit until the carrier returns it. `receive` then applies it, after the same checks every member runs. If another member's commit comes first, or the carrier refuses the post, the staged one is withdrawn. - Before its Welcome, a principal follows the group from its GroupInfo and handshake messages, using code it now shares with the clock. A Welcome addressed to someone else is ignored. - `Clock::key_package_if` keeps a KeyPackage only when it comes from a principal the room admitted. - Feature `js` builds the crate for wasm32. - **`joule-moq`**: a keyring now holds the locations it has opened per epoch, and drops them together with that epoch's keys. Memory in a long room is bounded by the epochs the page holds. - **`joule-moq-web`, feature `e2ee`**, built as a second wasm, `joule_moq_web_e2ee`, which a page loads only for such a room: - `E2eeMember`: KeyPackage, lifecycle events, log messages in order, staged create and commit, seal and open. - `e2eeCheckRecord`. - `TrackSigner.signSealed`: signs the receipt over the sealed bytes, with no `WAI_RECEIPT`. - `UniItem.properties`. - **The room page.** It shows that the room is end-to-end encrypted before entering, and offers no Watch and no seeking back. On entering it: - checks the record against the descriptor's room, authority and recording mode; - leaves a KeyPackage with the service; - follows the room's log (`web/room/e2ee.js`), checking each lifecycle record as the clock's (signature, room, chain) and passing each MLS message to the member; - creates the group or commits when §5 calls on it, and applies its own post only when the log returns it. Sending and receiving: - Frames are sealed, then signed, then sent, and get a group number only once sent. - Objects are verified against their receipt, then opened, then decoded. - An object from an epoch the page has not reached yet waits (bounded). - An object that does not open is counted with its reason and never reaches a decoder. - The page shows its epoch and roster. - The session can pass each object's property list to its sinks. - **`joule-moq-room`**: - every room gets a `WRE1` record and an MLS `Clock` that follows the lifecycle; - `GET /mls` (long-poll of the room's log), `POST /mls/key-package` (granted principals only), `POST /mls/post` (the clock checks each post, and a refused post is not carried); - `GET /room` returns the record. - **`wai-live-web`**: `roomDescriptor` now returns `recording_mode`, which the page needs to check the record (§3.3 step 5). - **Spec and docs**: - confidentiality §1 scope, a new §15 (§15.1 sealing frames, §15.2 a room service as the carrier, §15.3 what a browser member does not do yet), and the intro; - a SPEC §5 sentence; - the joule-moq-web and joule-moq-room READMEs and `room.example.toml`; - both CHANGELOGs. - **CI**: - joule-moq-room and joule-moq-web workflows now also run when joule-moq-mls changes; - the conformance job runs `cargo test -p joule-moq-web --features e2ee --lib`. Those tests run only with the feature. - **`e2e/`**: a new `e2ee` scenario, built by `run.sh` (e2ee wasm under the new `[profile.wasm]`, name section stripped). ### Design decisions, and why - **MLS in wasm: the MLS library and the pure-Rust crypto provider this tree already pins (0.9.1 and 0.6.0), unchanged, through the library's own browser feature.** That feature gives the library browser randomness and a browser clock. The crypto provider seeds its ChaCha generator through an older randomness crate, which needs its own browser backend, so `joule-moq-mls`'s feature `js` turns on both. On the host the feature changes nothing. The member builds for `wasm32-unknown-unknown` with no threads (the library skips its thread pool on wasm32) and no filesystem, and keys live only in the wasm member's memory. - **Bundle size.** The e2ee build is a separate wasm, so a page in any other room never loads MLS. Measured as `run.sh` builds them: | build | raw | gzip | |---|---|---| | `joule_moq_web` (unchanged) | 776 KB | 258 KB | | `joule_moq_web_e2ee` | 2.04 MB | 653 KB | For the e2ee build: - with plain release and the name section kept it was 3.77 MB / 965 KB; - the new `[profile.wasm]` (LTO, one codegen unit, `opt-level = "s"`) plus stripping the name and producers sections brings it to 2.04 MB / 653 KB; - a further size-optimisation pass made it smaller raw but larger gzipped, so it is not used. Most of the rest is the MLS library and its multi-suite crypto provider. The plain build is built exactly as before. - **Carrier: the room service, not MoQ tracks.** - §5.1 allows "a room service passing them directly". - The relay's grant check knows only `events`, `audio` and `video` tracks, so `mls`/`keypackage` tracks would have meant a relay change. - One log gives one order across lifecycle records and MLS messages, which members need: an Add must follow the `join` it answers. - The relay is unchanged and forwards sealed objects as before. - **Staging is required, not a nicety, and §15.2 makes it a MUST.** §5.5 names the committer from the commit's own proposals. So two members with different views (one has read a clock Add, the other also a Remove of the first) can each legitimately commit for the same epoch. If the first applied its commit at once, it would hold an epoch nobody else reaches. Members therefore apply their own post only when the log returns it. - **Viewers in an E2EE room are not members.** Their grant may subscribe, so it is keyed, but this service records presence from a publisher's tracks. A viewer is never added and opens nothing. The page hides Watch in such a room, and §15.3 says so. The e2e uses a viewer as "a subscriber without keys". - **Inner digest**: sealed on video, left out on audio (§4.4 SHOULD / MAY). - **Group numbering.** Frames encoded before the page is keyed are dropped without a group number. A late joiner's FETCH from group 0 then never asks the relay for groups it never had. Before this fix the relay forwarded such a FETCH to the publisher, which refuses requests; this showed up as one flaky e2e run. - **Recording**: the descriptor's mode is already 0, so §3.3 step 5 holds. The relay's DVR store keeps nothing of such a room, as the e2e checks. ### Checks All builds, tests and browsers ran on the build host through `rb`. | Check | Result | |---|---| | `cargo test --locked -p joule-moq-mls` | 14 lib + 4 corpus tests pass (4 new carrier tests) | | `cargo run -q --locked -p joule-moq-mls --bin joule_moq_e2ee_vectors verify ../../wai/live-conformance/e2ee` | exit 0 | | `cargo test --locked -p joule-moq-web` / `--features e2ee` | 8 / 10 pass | | `cargo test --locked -p joule-moq --features secure-objects --lib secure_objects`, `--test secure_objects_vectors` | 8, 2 pass | | `cargo test --locked -p joule-moq-relay --test sealed_objects` | 2 pass | | `cargo build --locked -p joule-moq-relay -p joule-moq-ingest` (dependents of joule-moq) | ok | | `joule-moq-room: cargo test --locked --all-targets` | 5 unit + 2 integration pass (`an_end_to_end_encrypted_room_keys_its_members_through_the_service` new) | | `wai/wai-live-web: cargo test --locked`, `cargo clippy --locked --all-targets` | pass; nothing new | | clippy `--all-targets` on joule-moq-mls, joule-moq-web (with and without e2ee), joule-moq-web e2ee for wasm32, joule-moq-room | zero notes in the touched files. Remaining notes predate this PR: `wai-rs` `unpinned_read`, `joule-moq-room/src/epoch.rs:418` | | wasm32 release builds, both variants, as `run.sh` | ok | | `cargo metadata --locked --offline` on the joule-code, joule-moq-room and examples/transport-moqt locks | ok. Locks gained additions only: no package version removed or changed | | `wai registry export --check` | up to date | | `e2e/run.sh --runs 2`, whole suite (compose, webtransport, websocket, goaway, dvr, service, e2ee), two separate invocations at the final merged head (after merging `origin/main` d9a614dd) | **2 of 2 runs passed, 300 of 300 checks**, both invocations. Two earlier invocations before that merge were green too | | every `live-conformance/*_verify.py` with its corpus (shared text: SPEC, confidentiality) | all exit 0 | | `e2ee_verify.py` | every vector verifies; 18 of 18 mutated corpora fail | | `check-tests-in-ci.py`, `check-workflow-concurrency.sh`, `check-spec-refs.py`, `check-wai-section-refs.py`, `check-no-vanity-metrics.sh`, `test-public-text-checks.sh`, `check-comparative-prose.sh`, `check-orphan-sources.py` | all clean | What the `e2ee` scenario shows (31 checks per run; run.sh drives the relay over WebSocket with the door on, the room service with `e2ee = true`, and a DVR window over every track): - The page announces the room as end-to-end encrypted before entering. - Alice creates the group through the log, and Bob joins by her commit's Welcome. - Media decodes both ways. Every object is verified against its receipt over the sealed bytes and opened before it is decoded. The only refusals are of objects sealed before a member's own join. - The page shows its epoch and roster, and refuses to seek back. - A viewer outside the group receives exactly the sealed bytes, byte for byte as Alice sealed them, with the AEAD's length and no `WAI1` envelope. It opens nothing and hands nothing to a decoder. - The full set of relay-style alterations to one object, presented before that object is opened, gives: | alteration | result | |---|---| | a payload byte changed | `bad_tag` | | the priority changed | `bad_tag` | | moved to another group | `bad_tag` | | the Key ID removed | `key_id_missing` | | a Key ID added outside the Immutable Properties | `properties_malformed` | | an Object ID of 2^32 | `object_id_too_large` | | a KID naming an empty leaf | `key_id_unknown` | | this member's own KID on the sender's track | `sender_not_owner` | | as sent | opens | | the same object again | `replayed` | - An object changed on the wire (re-signed, so the door passes it) is refused `bad_tag` and reaches no decoder. This is counted by envelopes that fail to read on the decode path. A decoder may still report the delta frame that follows the dropped one, so decoder errors are not what is counted. - Carol joins late with FETCH from group 0. Every earlier group is refused `epoch_not_joined`; the only fetched object that opens is from her own join epoch. - Carol keeps back one object, then stops publishing (PUBLISH_DONE) while keeping her session. The clock records her leave and Alice commits the removal. Carol still receives the new epoch's objects over the wire and refuses each `removed_before_epoch`, while Alice and Bob keep opening each other's. Carol's kept-back object is refused `sender_removed`. - The relay logs the room as sealed, so its DVR store keeps nothing. - The door refused nothing, and no page reports an error. ### Falsifications Each was applied on top of a commit, run, and reverted. F1 and F5 were run again after the final merge, with the same outcome. | Broken rule | Result | |---|---| | F1: a member keeps its staged commit when another's comes first (the `withdraw` removed from `receive`) | `cargo test -p joule-moq-mls --lib` exit 101: `a_member_whose_commit_another_overtakes_drops_its_own_and_applies_the_other` FAILED | | F3: the service carries a post its clock refused | joule-moq-room unit test `a_room_with_a_record_and_a_log_two_members_key_through` FAILED (exit 101); integration test `an_end_to_end_encrypted_room_keys_its_members_through_the_service` FAILED at "nothing refused was carried" (exit 101) | | F4: the page signs and sends the envelope unsealed | e2e `--only e2ee` exit 1: nothing opens, so nothing decodes ("alice decodes the other's media" never holds) | | F5: the page hands an object that did not open to the decoder | e2e `--only e2ee` exit 1: the viewer check fails (17 handed to a decoder) and the on-the-wire check fails (decode errors 0 → 2) | (F2, making `stage_commit` apply at once, broke every commit test. That is too blunt to say anything, so it is not counted.) ### Not done - **Watching without membership.** A non-publishing member would need the service to record a viewer's presence and leave. Not built; §15.3. - **WHIP/WebRTC ingest into an E2EE room.** That would need the W3C encoded-transform interface. Not built; §15.3. - **Seeking back** in an E2EE room: by design the relay keeps nothing. - **Production.** The demo room keeps `e2ee = false` until the lead deploys. Turning it on needs: - `e2ee = true` in the room service's config; - the new page file `web/room/e2ee.js`, the build outputs `web/pkg/joule_moq_web_e2ee{.js,_bg.wasm}`, and the rebuilt `wai_live_web` (`roomDescriptor` now returns `recording_mode`). Nothing on the relay changes. - **No new corpus vectors.** The wire, record, reasons and key schedule are unchanged. The browser member runs the reference's code, and the e2e is its test. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
An object presented again carries the same KID, so its epoch's set holds it; a set goes
when its epoch's keys are deleted, after which every object of that epoch is refused
epoch_not_joined anyway. A page in a long room keeps memory bounded by the epochs it holds.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
A room service that carries the room's messages in one order (confidentiality §5.1) is
how a browser member is reached. A member that applied its own commit before the carrier
ordered it could fork from a commit another member made for the same epoch, so:

- stage_create / stage_commit hold the member's GroupInfo or commit until the carrier
  returns it (receive applies it then, checked as every member checks it); another's
  first, or a refusal, withdraws it. create and commit still apply at once.
- before its Welcome a principal follows the group from its public handshake messages,
  as the clock does (the code is now shared), so its model knows whether a group exists
  and who is in it; a Welcome for another and a GroupInfo of a group it holds are ignored.
- feature js: openmls's js (getrandom 0.4 wasm_js, web-time) and getrandom 0.2's js for
  the crypto provider's generator; the crate builds for wasm32-unknown-unknown.

Tests: a room carried in one order (create, follow, commit, Welcome, removal), two
commits for one epoch where the carrier's first wins, a commit overtaken by another, and
a staged create the carrier refuses.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
E2eeMember wraps joule-moq-mls's RoomMember: KeyPackage, lifecycle events and MLS
messages in the carrier's order, staged create and commit, seal of an encoded frame
(inner digest, envelope flag in the Immutable Properties, delivery timeout beside them)
and open through the §6 checks, with the reason or the epoch to wait for. TrackSigner
signs a sealed object's receipt over its sealed bytes (signSealed, no WAI_RECEIPT), and
UniItem gives an object's property list as it was carried. e2eeCheckRecord checks WRE1.

The e2ee build is a second wasm (joule_moq_web_e2ee) that a page loads for such a room
alone; [profile.wasm] (release, LTO, one codegen unit, opt-level s) keeps it smaller.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
[room] e2ee = true gives every room a WRE1 record the authority signs (suite 0x0004,
recording mode 0 as the descriptor already states) and a joule-moq-mls Clock. The service
is the room's carrier of MLS messages (confidentiality §5.1): one log per room holding the
clock's lifecycle records as signed, its proposals, and every GroupInfo, commit and Welcome
a member posts, in the order the clock took them.

- GET /mls?room&from[&wait_ms]: the log from an entry, held up to 8 s while nothing is new;
- POST /mls/key-package: kept for the principal's Add, from a principal the room granted;
- POST /mls/post: a GroupInfo, or a commit and its Welcome, checked by the clock as every
  member checks it; refused (409) and not carried otherwise;
- GET /room carries the record.

Tests: the record and the log on a room (unit), and the service on a relay with two
members keying through HTTP as a page does: create, Add, Welcome, refused posts not
carried, each opens the other's sealed object, a leave removed by the next commit.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
The page checks the room's record, loads the e2ee build, leaves a KeyPackage, follows the
room service's log (web/room/e2ee.js), seals each frame before it is signed and published,
and opens each object through §6 before it is decoded; objects of an epoch not yet reached
wait. It shows the room is end-to-end encrypted, its epoch and its roster; no watching and
no seeking back in such a room. The session hands each object's property list to a sink.
wai-live-web's roomDescriptor gives the recording mode the record is checked against.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
§1's scope now covers the browser and states what a browser member does not do in this
revision (§15.3): watch without being a member, ingest by WHIP or WebRTC, seek back. §15.2
makes the carrier's order binding on a member's own create and commit: applied only when the
log returns it, dropped when another's commit for the epoch comes first.

CI: joule-moq-room and joule-moq-web run on a joule-moq-mls change; the conformance job runs
joule-moq-web's tests with feature e2ee, which run only with it.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Its FETCH copy is the same location, which §6 step 8 refuses as a replay.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Before the room's group keys the page nothing is sent; numbering those frames left a gap
below a track's first group, and a late joiner's FETCH from group 0 asked the relay for
groups it never had, which it passed to the publisher. A track's groups now run from 0.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
Co-Authored-By: Claude Opus 5.5 <[email protected]>
A decoder reports the delta frame after a dropped one, which refers to it; that is not
the dropped object reaching a decoder. Sealed bytes handed to the decode path never read
as a WAI1 envelope, which this counts.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Merge remote-tracking branch 'origin/main' into joule-moq/e2ee-web
All checks were successful
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Successful in 1m20s
joule-code conformance gate / joule-moq-relay, every target (pull_request) Successful in 1m32s
standards workspaces build + test / which workspaces this change touches (pull_request) Successful in 12s
WAI conformance / does this change touch it (pull_request) Successful in 13s
joule-code conformance gate / joule-moq-ingest, every target (pull_request) Successful in 1m41s
WAI energy receipts / does this change touch it (pull_request) Successful in 11s
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Successful in 23s
standards workspaces build + test / joule-code-rs (joule-code/joule-code-rs, joule-code-rs, linux-arm64, true) (pull_request) Successful in 3m43s
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Successful in 11s
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Successful in 29s
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Successful in 1m35s
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Successful in 8m15s
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Successful in 16m54s
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Successful in 23m34s
joule-moq-room room service / joule-moq-room · does this change touch it (pull_request) Successful in 5s
joule-moq-web browser client / joule-moq-web · does this change touch it (pull_request) Successful in 5s
joule-code conformance gate / Build + round-trip C1-C4 vectors (pull_request) Successful in 3m9s
standards workspaces build + test / macOS energy backends (pull_request) Successful in 8m1s
joule-moq-room room service / joule-moq-room, every target (pull_request) Successful in 1m4s
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Successful in 2m10s
WAI conformance / cross-arch byte-equality · linux (pull_request) Successful in 28s
WAI conformance / cargo-class corpora · macos (pull_request) Successful in 13m2s
WAI conformance / cross-arch byte-equality · macos (pull_request) Successful in 19s
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Successful in 42s
WAI energy receipts / energy receipts · macos (pull_request) Successful in 4m43s
WAI energy receipts / energy receipts · linux (pull_request) Successful in 9m44s
WAI conformance / cargo-class corpora · linux (pull_request) Successful in 21m58s
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Successful in 8m42s
c1f9f27f2d
# Conflicts:
#	wai/CHANGELOG.md
Merge remote-tracking branch 'origin/main' into joule-moq/e2ee-web
Some checks failed
joule-moq-web browser client / joule-moq-web · does this change touch it (pull_request) Successful in 5s
jouleclaw fabrics / cuda (linux, RTX 4050 + Vulkan loader) (pull_request) Successful in 18s
WAI energy receipts / does this change touch it (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux (pull_request) Has been cancelled
WAI energy receipts / energy receipts · linux-arm64 (pull_request) Has been cancelled
WAI energy receipts / energy receipts · macos (pull_request) Has been cancelled
WAI conformance / does this change touch it (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · linux-arm64 (pull_request) Has been cancelled
WAI conformance / cargo-class corpora · macos (pull_request) Has been cancelled
WAI conformance / quantum core and corpus under wasm32-wasip2 (wasmtime) (pull_request) Has been cancelled
WAI conformance / v2 vocabulary · manifests validate as stated (pull_request) Has been cancelled
WAI conformance / independent verifiers · claims, JWP, key discovery and integer payloads (Python) (pull_request) Has been cancelled
WAI conformance / delivery telemetry · CMCD and CMSD against an independent library (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · linux-arm64 (pull_request) Has been cancelled
WAI conformance / cross-arch byte-equality · macos (pull_request) Has been cancelled
WAI conformance / codec wrappers · fuzz smoke, text decode limits, C entry points, provenance, the semantic path (pull_request) Has been cancelled
WAI conformance / C2PA manifest emission · signed, read back, validated (pull_request) Has been cancelled
standards workspaces build + test / which workspaces this change touches (pull_request) Has been cancelled
standards workspaces build + test / macOS energy backends (pull_request) Has been cancelled
repository checks / lockfiles, tests in CI, workflow hygiene, spec and section references, public text (pull_request) Has been cancelled
jouleclaw fabrics / metal (macOS, Apple GPU) (pull_request) Has been cancelled
joule-moq-web browser client / joule-moq-web · browser end-to-end (macos) (pull_request) Has been cancelled
joule-moq-room room service / joule-moq-room, every target (pull_request) Has been cancelled
jouleclaw fabrics / vulkan (linux, RTX 4050 + Vulkan loader) (pull_request) Has been cancelled
jouleclaw public surface must stay documented / Public-surface audit (jouleclaw-rs) (pull_request) Has been cancelled
joule-code conformance gate / joule-moq-ingest, every target (pull_request) Has been cancelled
joule-code conformance gate / joule-moq-relay, every target (pull_request) Has been cancelled
joule-code conformance gate / Build + round-trip C1-C4 vectors (pull_request) Has been cancelled
2c59ee4240
# Conflicts:
#	joule-code/CHANGELOG.md
#	joule-code/joule-code-rs/Cargo.toml
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
Transaction-Science/open-standards!366
No description provided.