open-standards/joule-code/joule-code-rs/crates/joule-moq-web/web/room/remote.test.mjs
Transaction Science ebab8e16cd joule-moq-web: the remote page's sends run in the order they were asked for
A seal takes WebCrypto's time, so a send that seals could finish after a later one that does not:
a key's release could reach the host before its press. The viewer's sends now run one after
another; what arrives while one is sent goes at the next refresh with its actions' instants as
they were (the batch is no longer re-stamped). A test holds the order while a slow seal is made.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-26 10:52:47 -04:00

349 lines
19 KiB
JavaScript

// The browser viewer's remote desktop module (remote.js), against the corpus
// wai/live-conformance/remote/ (`node --test web/room/remote.test.mjs`): every input statement,
// floor statement and latency record of the accepted sessions written byte for byte (§5.1, §7.2,
// §8.1, the records signed again under the corpus's keys), every sealed input sealed again from
// what the corpus's opened level states it opens to (§5.5); every input of the accepted sessions
// given the outcome the corpus states by the module's own replay of the host's rules (§5.3,
// §7.3); every vector in which a host stated another outcome than the rules found out; HPKE
// against RFC 9180 A.1.1; the page's viewer sealing what it sends to the host's sealing key and
// nothing in the clear, and showing another controller's typing, never its text; and the page's
// table of keyboard codes the host agent's.
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import * as rd from "./remote.js";
const doc = JSON.parse(readFileSync(new URL("../../../../../../wai/live-conformance/remote/vectors.json", import.meta.url), "utf8"));
const vector = (name) => doc.vectors.find((v) => v.name === name);
// Every accepted session: the floor session, the open handover, and those whose floor statements
// §7.2 refuses, which change nothing.
const ACCEPTED = doc.vectors.filter((v) => v.outcome === "accept").map((v) => v.name);
const MISSTATED = doc.vectors.filter((v) => v.reason === "outcome_mismatch").map((v) => v.name);
const INDEX = { authority: 0, clock: 1, host: 2, alice: 3, bob: 4, viewer: 5, lead: 6, carol: 7 };
/** The corpus's key `i`: SHA-256 of its label, a zero byte and `i` (its README). */
const key = async (i) => rd.keyFromSeed(await rd.sha256(new TextEncoder().encode("wai:remote-desktop/corpus/key\x00"), Uint8Array.of(i)));
const byPrincipal = Object.fromEntries(Object.entries(doc.principals).map(([n, p]) => [p, INDEX[n]]));
function statements(v) {
const out = [];
for (const chain of v.chains) {
for (const h of chain) {
const ev = rd.readEvent(rd.unhex(h));
if (ev.kind !== 1) continue;
const s = rd.readStatement(ev.payload);
if (s) out.push({ ev, s });
}
}
return out;
}
test("input and floor statements are written byte for byte", () => {
let inputs = 0, floors = 0;
for (const name of ACCEPTED) {
for (const { ev, s } of statements(vector(name))) {
if (s.kind === "input") {
assert.equal(rd.hex(rd.inputPayload(s)), rd.hex(ev.payload), `${name}: an input statement`);
inputs++;
} else if (s.kind === "floor") {
const to = s.to === "0".repeat(64) ? null : s.to;
assert.equal(rd.hex(rd.floorPayload({ host: s.host, action: s.action, to })), rd.hex(ev.payload), `${name}: a floor statement`);
floors++;
}
}
}
assert.ok(inputs >= 15 && floors >= 5, `${inputs} inputs, ${floors} floor statements`);
});
test("latency records are written and signed byte for byte", async () => {
let n = 0;
for (const name of ACCEPTED) {
for (const { ev, s } of statements(vector(name))) {
if (s.kind !== "latency") continue;
const k = await key(byPrincipal[s.viewer]);
assert.equal(rd.hex(k.publicKey), s.viewer);
assert.equal(rd.hex(await rd.signLatency(s, k.privateKey)), rd.hex(ev.payload), `${name}: a latency record`);
n++;
}
}
assert.equal(n, ACCEPTED.reduce((k, name) => k + vector(name).latencies.length, 0), "every latency record the accepted sessions state");
});
test("every input of the accepted sessions gets the corpus's outcome from the module's rules", async () => {
for (const name of ACCEPTED) {
const v = vector(name);
const got = await rd.replay(v, doc.principals.clock, doc.principals.authority);
assert.equal(got.length, v.inputs.length, name);
got.forEach((g, k) => {
const want = v.inputs[k];
assert.equal(g.digest, want.digest);
const f = want.fate;
if (f.outcome === "unanswered") return assert.equal(g.outcome, "unanswered", `${name} input ${k}`);
assert.equal(g.outcome, f.outcome, `${name} input ${k}`);
if (f.outcome === "applied") {
assert.equal(g.rule, 0, `${name} input ${k}: the rules accept it`);
assert.equal(g.anchor.group, f.group);
} else if (g.sealUnknown && g.rule === 0) {
// A seal only the host's opening key opens: rule 9 is the host's to make (§11.4).
assert.equal(f.reason, "input_bad_seal", `${name} input ${k}: rules 1 to 8 accept it`);
} else {
assert.equal(rd.REFUSAL[g.rule], f.reason, `${name} input ${k}: the rule`);
assert.equal(g.stated, g.rule);
}
});
}
});
test("a floor statement §7.2 refuses changes nothing, and is stated with its reason", async () => {
let refused = 0;
for (const name of ACCEPTED) {
const v = vector(name);
const got = await rd.replayRoom(v, doc.principals.clock, doc.principals.authority);
assert.deepEqual(got.floor, v.floor, `${name}: the floor's changes`);
assert.deepEqual(got.floorRefused, v.floor_refused || [], `${name}: the floor statements refused`);
refused += got.floorRefused.length;
}
assert.ok(refused >= 9, `${refused} refused floor statements`);
// The late release: the host took the floor back, and the release after it passes nothing.
const late = await rd.replayRoom(vector("floor_late_release"), doc.principals.clock, doc.principals.authority);
assert.equal(late.floorRefused[0].reason, "floor_not_holder");
assert.deepEqual(late.floor, vector("floor_session").floor, "the holder after it is the holder before it");
});
test("a host that stated another outcome than the rules is found out", async () => {
assert.ok(MISSTATED.length >= 9, MISSTATED.join(", "));
for (const name of MISSTATED) {
const got = await rd.replay(vector(name), doc.principals.clock, doc.principals.authority);
assert.ok(got.some((g) => g.outcome !== "unanswered" && g.stated !== g.rule), `${name}: the module's rules disagree with the host`);
}
for (const name of ACCEPTED) {
const got = await rd.replay(vector(name), doc.principals.clock, doc.principals.authority);
assert.ok(got.every((g) => g.outcome === "unanswered" || g.stated === g.rule || (g.sealUnknown && g.rule === 0 && g.stated === 9)), name);
}
});
// ---------------------------------------------------------------- sealed actions (§5.5)
const te = new TextEncoder();
const le32 = (n) => Uint8Array.of(n & 0xff, (n >> 8) & 0xff, (n >> 16) & 0xff, (n >>> 24) & 0xff);
/** The corpus's seeds (its seal_seed): a sealed action's salt, and a seal's ephemeral key. */
const corpusSalt = (author, screen) => async (k, a) => (await rd.sha256(te.encode("wai:remote-desktop/corpus/salt\x00"), rd.unhex(author), rd.unhex(screen), Uint8Array.of(k), le32(a.dt_us))).subarray(0, 16);
const corpusIkm = (author) => async (clear) => rd.sha256(te.encode("wai:remote-desktop/corpus/enc\x00"), rd.unhex(author), clear);
const sealPair = async (i) => rd.deriveKeyPair(await rd.sha256(te.encode("wai:remote-desktop/corpus/seal\x00"), Uint8Array.of(i)));
test("HPKE base mode is RFC 9180's (Appendix A.1.1)", async () => {
const h = rd.unhex;
const r = await rd.deriveKeyPair(h("6db9df30aa07dd42ee5e8181afdb977e538f5e1fec8a06223f33f7013e525037"));
assert.equal(rd.hex(r.secret), "4612c550263fc8ad58375df3f557aac531d26850903e55a9f23f21d8534e8ac8");
assert.equal(rd.hex(r.publicKey), "3948cfe0ad1ddb695d780e59077195da6c56506b027329794ab02bca80815c4d");
const s = await rd.sealBase(r.publicKey, h("4f6465206f6e2061204772656369616e2055726e"), h("436f756e742d30"), h("4265617574792069732074727574682c20747275746820626561757479"), h("7268600d403fce431561aef583ee1613527cff655c1343f29812e66706df3234"));
assert.equal(rd.hex(s.enc), "37fda3567bdbd628e88668c3c8d7e97d1d1253b6d4ea6d44c150f741f1bf4431");
assert.equal(rd.hex(s.ct), "f938558b5d72f1a23810b4be2ab4f84331acc02fc97babc53a52ae8218a355a96d8770ac83d07bea87e13c512a");
});
test("sealed inputs are sealed again byte for byte from what they open to", async () => {
const v = vector("sealed_session");
const host = await sealPair(INDEX.host);
assert.equal(rd.hex(host.secret), doc.opening_key, "the host's opening key is the corpus's");
const screens = statements(v).filter((x) => x.s.kind === "screen");
assert.equal(screens[0].s.seal, rd.hex(host.publicKey), "the host's screen statement states its sealing key");
assert.equal(screens[1].s.seal, null, "and its second states none");
let n = 0;
for (const [k, fact] of v.opened.inputs.entries()) {
if (fact.seal !== "opened") continue;
const ev = statements(v).find((x) => x.s.kind === "input" && x.ev.principal === fact.author && x.ev.t === fact.t).ev;
const clear = rd.readStatement(rd.unhex(fact.opened));
assert.ok(clear.actions.every((a) => a.type !== "sealed") && !clear.seal, `input ${k} reads opened in the clear`);
const actions = clear.actions.map(({ bytes, ...a }) => a);
const sealed = await rd.sealInput({ room: v.room, author: fact.author, host: doc.principals.host, screen: clear.screen, sealKey: rd.hex(host.publicKey), actions, salt: corpusSalt(fact.author, clear.screen), ikm: corpusIkm(fact.author) });
assert.equal(rd.hex(sealed.payload), rd.hex(ev.payload), `input ${k}: the page seals what the corpus holds`);
assert.equal(v.inputs[k].sealed, sealed.input.actions.filter((a) => a.type === "sealed").length);
n++;
}
assert.ok(n >= 3, `${n} sealed inputs sealed again`);
});
test("the page's viewer seals keys, text and clipboard text to the host, and sends none in the clear", async () => {
const v = vector("sealed_session");
let alice = null;
for (const h of v.grants) if ((await rd.readGrant(rd.unhex(h))).principal === doc.principals.alice) alice = h;
const host = await sealPair(INDEX.host);
const { r, sent } = await viewerOf(v, "alice", alice);
r.joined = true;
r.screen = { ...r.screen, seal: rd.hex(host.publicKey) };
const payloads = [];
const emit = r.emit;
r.emit = async (k, p) => { payloads.push(p); return emit(k, p); };
r.salt = async () => new Uint8Array(16).fill(5);
r.ikm = async () => new Uint8Array(32).fill(6);
const typed = [{ type: "move", x: 3, y: 4, at: 0 }, { type: "key", usage: 0x0b, down: true, modifiers: 0, at: 0 }, { type: "text", text: "hunter2 secret", at: 0 }, { type: "clipboard", text: "a pasted password", at: 0 }];
r.batch.push(...typed);
await r.flush();
assert.equal(payloads.length, 1, "one input statement");
const s = sent[0];
assert.deepEqual(s.actions.map((a) => a.type), ["move", "sealed", "sealed", "sealed"], "the move in the clear, the rest sealed");
for (const secret of ["hunter2", "a pasted password"]) {
const needle = te.encode(secret);
assert.ok(!payloads[0].some((_, i) => needle.every((b, j) => payloads[0][i + j] === b)), `no trace of ${secret}`);
}
const want = await rd.sealInput({ room: v.room, author: doc.principals.alice, host: doc.principals.host, screen: r.screenDigest, sealKey: rd.hex(host.publicKey), actions: typed.map(({ at, ...a }) => ({ dt_us: 0, ...a })), salt: r.salt, ikm: r.ikm });
assert.equal(rd.hex(payloads[0]), rd.hex(want.payload), "sealed to the host's sealing key, the screen statement's");
// To a host that states no sealing key: no key, text or clipboard action is sent at all.
r.screen = { ...r.screen, seal: null };
payloads.length = 0;
r.batch.push({ type: "text", text: "hunter2", at: 0 });
await r.flush();
assert.equal(payloads.length, 0, "nothing in the clear");
assert.equal(r.counts.unsealed, 1);
r.batch.push({ type: "move", x: 1, y: 1, at: 0 });
await r.flush();
assert.equal(payloads.length, 1, "a pointer move still goes");
});
test("the page's sends keep their order while a seal is made", async () => {
const v = vector("sealed_session");
let alice = null;
for (const h of v.grants) if ((await rd.readGrant(rd.unhex(h))).principal === doc.principals.alice) alice = h;
const { r, sent } = await viewerOf(v, "alice", alice);
r.joined = true;
r.screen = { ...r.screen, seal: rd.hex((await sealPair(INDEX.host)).publicKey) };
// A seal that takes a while: a key pressed, then a move, sent while the key's seal is made.
r.salt = async () => { await new Promise((ok) => setTimeout(ok, 30)); return new Uint8Array(16).fill(5); };
r.batch.push({ type: "key", usage: 0x04, down: true, modifiers: 0, at: 0 });
const first = r.flush();
await new Promise((ok) => setTimeout(ok, 0));
r.batch.push({ type: "move", x: 2, y: 2, at: 0 });
const second = r.flush();
await Promise.all([first, second]);
assert.deepEqual(sent.map((s) => s.actions[0].type), ["sealed", "move"], "the key's statement before the move's");
});
test("a viewer's page shows that a controller is typing, never what", async () => {
const v = vector("sealed_session");
const { r } = await viewerOf(v, "viewer", null);
r.panel = { textContent: "" };
r.screen = { ...r.screen, scope: 1, redactions: [], encoder: "x@1", seal: "22".repeat(32) };
const i1 = statements(v).find((x) => x.s.kind === "input" && x.ev.principal === doc.principals.alice);
r.onRecord(rd.unhex(v.chains.flat().find((h) => rd.hex(rd.readEvent(rd.unhex(h)).body) === rd.hex(i1.ev.body))));
assert.match(r.panel.textContent, new RegExp(`${doc.principals.alice.slice(0, 8)} is typing`));
assert.ok(!/pässwörd|hunter/.test(r.panel.textContent), r.panel.textContent);
assert.equal(r.received.length, 1);
});
test("a text longer than a text action carries goes as several, never inside a character", () => {
const long = "é".repeat(40);
const pieces = rd.textPieces(long);
assert.equal(pieces.join(""), long);
assert.ok(pieces.every((p) => te.encode(p).length <= 64 && te.encode(p).length >= 1));
assert.deepEqual(rd.textPieces("abc"), ["abc"]);
});
test("the screen statements read, and the track digest is the host's", async () => {
const v = vector("floor_session");
const screens = statements(v).filter((x) => x.s.kind === "screen");
assert.equal(screens.length, v.screens.length);
for (const [k, { s }] of screens.entries()) {
assert.equal(s.encoder, v.screens[k].encoder);
assert.equal(s.width, v.screens[k].width);
assert.equal(s.track, await rd.screenTrack(v.room, doc.principals.host));
}
});
test("the page's keyboard codes are the host agent's HID usages", () => {
const rust = readFileSync(new URL("../../../joule-moq-remote/src/inject.rs", import.meta.url), "utf8");
const pairs = [...rust.matchAll(/\(0x([0-9A-F]{2}), "([A-Za-z0-9]+)"\)/g)].map((m) => [m[2], parseInt(m[1], 16)]);
assert.ok(pairs.length > 80);
assert.deepEqual(Object.fromEntries(pairs), rd.HID);
});
// The page's viewer in a room a room service keeps: its control power is its grant's, from each
// exchange of it the clock signs; it sends nothing without a grant, before the clock has recorded
// it joining, or of a class its grant in effect does not give.
async function viewerOf(v, who, grant) {
const sent = [];
let now = 0;
const grants = await Promise.all(v.grants.map(async (h) => ({ h, g: await rd.readGrant(rd.unhex(h)) })));
// The page's clock on the room's: every grant of the corpus room is good from the latest nbf.
const unix = Math.max(...grants.map((x) => x.g.nbf));
const r = new rd.RemoteViewer({
room: v.room, me: doc.principals[who], host: doc.principals.host, clock: doc.principals.clock, grant,
key: await key(INDEX[who]), roomNow: () => now, unixNow: () => unix, log: () => {},
grantOf: async (d) => (grants.find((x) => x.g.digest === d) || {}).h || null,
emit: async (k, p) => { sent.push(rd.readStatement(p)); return { digest: "00".repeat(32), t: now }; },
});
await r.start();
r.screen = { floor: 1, state: 1, accepts: 0x7f, width: 64, height: 64, clip_max: 100, max_age_ms: 1000 };
r.screenDigest = "11".repeat(32);
return { r, sent, at: (t) => { now = t; }, grants };
}
const move = (x) => ({ type: "move", x, y: 1, at: 0 });
test("the page's viewer sends nothing without its grant", async () => {
const v = vector("open_handover");
const { r, sent } = await viewerOf(v, "alice", null);
r.joined = true;
r.batch.push(move(1));
await r.flush();
await r.requestFloor();
assert.equal(sent.length, 0, "no grant, nothing sent");
assert.equal(r.counts.unsent, 1);
});
test("the page's viewer sends nothing once its grant has ended", async () => {
const v = vector("floor_session");
let alice = null;
for (const h of v.grants) if ((await rd.readGrant(rd.unhex(h))).principal === doc.principals.alice) alice = h;
const g = await rd.readGrant(rd.unhex(alice));
const { r, sent } = await viewerOf(v, "alice", alice);
r.joined = true;
for (const [unix, want] of [[g.nbf, 1], [g.exp - 1, 1], [g.exp, 0], [g.exp + 3600, 0]]) {
r.unixNow = () => unix;
sent.length = 0;
r.batch.push(move(4));
await r.flush();
assert.equal(sent.length, want, `at ${unix}, the grant good from ${g.nbf} to before ${g.exp}`);
}
});
test("the page's viewer takes its control power from its grant and each exchange the clock signs", async () => {
const v = vector("open_handover");
const firstOf = async (who) => {
for (const h of v.grants) {
const g = await rd.readGrant(rd.unhex(h));
if (g.principal === doc.principals[who]) return h;
}
return null;
};
// Every record of the clock, read by a viewer for each controller whose grant it exchanges.
const clock = v.chains.map((c) => c.map((h) => rd.unhex(h))).find((c) => rd.readEvent(c[0]).principal === doc.principals.clock);
for (const who of ["alice", "carol"]) {
const { r, sent, at } = await viewerOf(v, who, await firstOf(who));
const before = r.control;
r.batch.push(move(2));
await r.flush();
assert.equal(sent.length, 0, `${who}: nothing before the clock records it joining`);
const exchanges = [];
for (const b of clock) {
const ev = rd.readEvent(b);
r.onRecord(b);
await new Promise((ok) => setTimeout(ok, 0));
await new Promise((ok) => setTimeout(ok, 0));
if (ev.kind === 0x14 && rd.hex(ev.payload.subarray(0, 32)) === doc.principals[who]) exchanges.push(ev.t);
}
assert.equal(exchanges.length, 1, `${who}: one exchange of its grant`);
const t = exchanges[0];
assert.equal(r.controlAt(t - 1), before, `${who}: its first grant's power until the exchange`);
assert.notEqual(r.controlAt(t), before, `${who}: the new grant's power from the exchange's instant`);
// The clock's chain ends with everyone's leave: rejoin for the sends below.
r.joined = true;
for (const [instant, want] of [[t - 1, before], [t, r.controlAt(t)]]) {
at(instant);
sent.length = 0;
r.batch.push(move(3));
await r.flush();
assert.equal(sent.length, want & rd.CLASS.pointer ? 1 : 0, `${who} at ${instant}: a pointer move sent only under a grant that gives the pointer`);
if (sent.length) assert.equal(sent[0].kind, "input");
}
}
});