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
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]>
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]>
§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]>
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]>
[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]>
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]>
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]>
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]>