1
0

feat: add private office media signaling

This commit is contained in:
2026-08-11 20:48:19 -07:00
parent fc1f500019
commit d841575315
14 changed files with 2342 additions and 3 deletions
+343
View File
@@ -0,0 +1,343 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
createRemoteOfficeMedia,
type MediaAuthorizationDecision,
type RemoteMediaTimers,
type ScreenShareAccessGrant,
type ScreenShareBinding,
type ScreenShareParticipant,
} from "../media/index.ts";
const BINDING: ScreenShareBinding = {
officeId: "lumbridge-hq",
levelId: "level-1",
roomId: "commons",
screenId: "wall-display",
};
const NOW = 1_765_000_000_000;
class FakeTrack {
readonly kind = "video";
readyState: MediaStreamTrackState = "live";
stopped = 0;
stop(): void { this.stopped += 1; this.readyState = "ended"; }
}
class FakeStream {
readonly tracks: FakeTrack[];
constructor(tracks: FakeTrack[]) { this.tracks = tracks; }
getTracks(): MediaStreamTrack[] { return this.tracks as unknown as MediaStreamTrack[]; }
getVideoTracks(): MediaStreamTrack[] { return this.getTracks(); }
}
class FakeVideo {
autoplay = true;
muted = false;
srcObject: MediaProvider | null = null;
}
class FakePeer {
localDescription: RTCSessionDescription | null = null;
remoteDescription: RTCSessionDescription | null = null;
connectionState: RTCPeerConnectionState = "new";
onicecandidate: ((this: RTCPeerConnection, ev: RTCPeerConnectionIceEvent) => unknown) | null = null;
ontrack: ((this: RTCPeerConnection, ev: RTCTrackEvent) => unknown) | null = null;
onconnectionstatechange: ((this: RTCPeerConnection, ev: Event) => unknown) | null = null;
readonly added: MediaStreamTrack[] = [];
readonly remote: RTCSessionDescriptionInit[] = [];
readonly ice: (RTCIceCandidateInit | null)[] = [];
readonly transceivers: string[] = [];
closed = 0;
addTrack(track: MediaStreamTrack): RTCRtpSender { this.added.push(track); return {} as RTCRtpSender; }
addTransceiver(kind: string): RTCRtpTransceiver { this.transceivers.push(kind); return {} as RTCRtpTransceiver; }
async createOffer(): Promise<RTCSessionDescriptionInit> { return { type: "offer", sdp: "presenter-offer" }; }
async createAnswer(): Promise<RTCSessionDescriptionInit> { return { type: "answer", sdp: "viewer-answer" }; }
async setLocalDescription(value: RTCLocalSessionDescriptionInit): Promise<void> {
this.localDescription = value as RTCSessionDescription;
}
async setRemoteDescription(value: RTCSessionDescriptionInit): Promise<void> {
this.remote.push(value);
this.remoteDescription = value as RTCSessionDescription;
}
async addIceCandidate(value?: RTCIceCandidateInit | null): Promise<void> { this.ice.push(value ?? null); }
close(): void { this.closed += 1; this.connectionState = "closed"; }
connect(): void {
this.connectionState = "connected";
this.onconnectionstatechange?.call(this as unknown as RTCPeerConnection, new Event("connectionstatechange"));
}
fail(): void {
this.connectionState = "failed";
this.onconnectionstatechange?.call(this as unknown as RTCPeerConnection, new Event("connectionstatechange"));
}
receive(track: FakeTrack, stream: FakeStream): void {
this.ontrack?.call(this as unknown as RTCPeerConnection, {
track: track as unknown as MediaStreamTrack,
streams: [stream as unknown as MediaStream],
} as unknown as RTCTrackEvent);
}
}
class FakeTimers implements RemoteMediaTimers {
pending = new Map<number, () => void>();
next = 1;
setTimeout(callback: () => void): ReturnType<typeof setTimeout> {
const id = this.next++;
this.pending.set(id, callback);
return id as unknown as ReturnType<typeof setTimeout>;
}
clearTimeout(handle: ReturnType<typeof setTimeout>): void { this.pending.delete(handle as unknown as number); }
run(): void {
const first = this.pending.entries().next().value as [number, () => void] | undefined;
if (!first) throw new Error("no reconnect timer");
this.pending.delete(first[0]);
first[1]();
}
}
function access(role: "presenter" | "viewer", participantId: string, sessionId = "share-session"): ScreenShareAccessGrant {
return {
credential: { sessionId, participantId, role, grantToken: `private-${role}-grant` },
issuedAtMs: NOW - 1_000,
expiresAtMs: NOW + 60_000,
};
}
function grant(
type: "screen-share-create-grant" | "screen-share-join-grant" | "screen-share-resume-grant",
requestId: string,
role: "presenter" | "viewer",
participants: readonly ScreenShareParticipant[],
) {
return {
type,
protocolVersion: 1,
sequence: 1,
timestampMs: NOW,
requestId,
binding: BINDING,
grant: access(role, `${role}-opaque`),
nextClientSequence: 2,
participants,
...(type === "screen-share-resume-grant" ? { continuous: true } : {}),
};
}
function sse(messages: readonly unknown[]): Response {
const bytes = new TextEncoder().encode(messages.map((message) => `data: ${JSON.stringify(message)}\n\n`).join(""));
return new Response(new ReadableStream<Uint8Array>({
start(controller) { controller.enqueue(bytes); /* held open like the real fetch stream */ },
}), { status: 200, headers: { "Content-Type": "text/event-stream" } });
}
function decision(optedIn = true): MediaAuthorizationDecision {
return {
authorized: true,
optedIn,
canView: optedIn,
reason: optedIn ? "ready" : "opt_in_required",
surface: {
...BINDING,
status: "presenting",
source: optedIn ? {
kind: "live-stream", locator: "opaque", privacy: "private", hasAudio: false, autoplay: false, muted: true,
} : null,
playback: { autoplay: false, muted: true },
},
};
}
async function settle(): Promise<void> {
await Promise.resolve();
await Promise.resolve();
await new Promise<void>((resolve) => setTimeout(resolve, 0));
}
describe("remote office media presenter", () => {
it("requires opt-in, publishes one caller track, and keeps credentials in authenticated POST bodies", async () => {
const presenterTrack = new FakeTrack();
const stream = new FakeStream([presenterTrack]);
const video = new FakeVideo();
const peers: FakePeer[] = [];
const calls: Array<{ url: string; init: RequestInit; body: Record<string, unknown> }> = [];
const viewerPeer = { participantId: "viewer-opaque", role: "viewer" as const };
const fetcher: typeof fetch = async (input, init = {}) => {
const body = JSON.parse(String(init.body)) as Record<string, unknown>;
calls.push({ url: String(input), init, body });
if (String(input).endsWith("/join")) {
return Response.json(grant("screen-share-create-grant", String(body.requestId), "presenter", []), { status: 201 });
}
if (String(input).endsWith("/events")) {
return sse([grant("screen-share-resume-grant", String(body.requestId), "presenter", [viewerPeer])]);
}
return new Response(null, { status: 204 });
};
const media = createRemoteOfficeMedia({
role: "presenter", binding: BINDING, authenticatedFetch: fetcher, now: () => NOW,
peerConnectionFactory: () => { const peer = new FakePeer(); peers.push(peer); return peer as unknown as RTCPeerConnection; },
});
await assert.rejects(
media.startPresenter({ consent: { authorized: true, optedIn: false }, stream: stream as unknown as MediaStream, video: video as unknown as HTMLVideoElement }),
/explicit opt-in/,
);
await media.startPresenter({ consent: { authorized: true, optedIn: true }, stream: stream as unknown as MediaStream, video: video as unknown as HTMLVideoElement });
await settle();
assert.equal(peers.length, 1);
assert.equal(peers[0]?.added[0], presenterTrack as unknown as MediaStreamTrack);
assert.equal(video.autoplay, false);
assert.equal(video.muted, true);
const signal = calls.find((call) => call.url.endsWith("/signal"));
assert.equal((signal?.body.signal as { kind?: string }).kind, "sdp");
assert.equal(signal?.init.credentials, "same-origin");
assert.equal(calls.some((call) => call.url.includes("private-presenter-grant")), false);
assert.equal(JSON.stringify(media.state()).includes("private-presenter-grant"), false);
await media.stop();
assert.equal(presenterTrack.stopped, 0, "caller-owned capture track survives stop");
assert.equal(peers[0]?.closed, 1);
});
it("tears down failed peers and rebuilds them from a resumed participant snapshot", async () => {
const timers = new FakeTimers();
const peers: FakePeer[] = [];
const participant = { participantId: "viewer-opaque", role: "viewer" as const };
const fetcher: typeof fetch = async (input, init = {}) => {
const body = JSON.parse(String(init.body)) as Record<string, unknown>;
if (String(input).endsWith("/join")) return Response.json(grant("screen-share-create-grant", String(body.requestId), "presenter", [participant]), { status: 201 });
if (String(input).endsWith("/events")) return sse([grant("screen-share-resume-grant", String(body.requestId), "presenter", [participant])]);
return new Response(null, { status: 204 });
};
const media = createRemoteOfficeMedia({
role: "presenter", binding: BINDING, fetch: fetcher, timers, now: () => NOW,
peerConnectionFactory: () => { const peer = new FakePeer(); peers.push(peer); return peer as unknown as RTCPeerConnection; },
});
const track = new FakeTrack();
await media.startPresenter({ consent: { authorized: true, optedIn: true }, stream: new FakeStream([track]) as unknown as MediaStream, video: new FakeVideo() as unknown as HTMLVideoElement });
peers[0]?.fail();
assert.equal(media.state().status, "reconnecting");
assert.equal(peers[0]?.closed, 1);
timers.run();
await settle();
assert.equal(peers.length, 2);
assert.equal(peers[1]?.added[0], track as unknown as MediaStreamTrack);
await media.dispose();
assert.equal(track.stopped, 0);
});
it("does not resurrect a session when stop overtakes a slow join", async () => {
let resolveJoin: ((response: Response) => void) | null = null;
let joinRequestId = "";
const fetcher: typeof fetch = async (input, init = {}) => {
const body = JSON.parse(String(init.body)) as Record<string, unknown>;
if (String(input).endsWith("/join")) {
joinRequestId = String(body.requestId);
return new Promise<Response>((resolve) => { resolveJoin = resolve; });
}
return new Response(null, { status: 204 });
};
const track = new FakeTrack();
const media = createRemoteOfficeMedia({ role: "presenter", binding: BINDING, fetch: fetcher, now: () => NOW });
const starting = media.startPresenter({
consent: { authorized: true, optedIn: true },
stream: new FakeStream([track]) as unknown as MediaStream,
video: new FakeVideo() as unknown as HTMLVideoElement,
});
await media.stop();
assert.ok(resolveJoin);
(resolveJoin as (response: Response) => void)(Response.json(
grant("screen-share-create-grant", joinRequestId, "presenter", []), { status: 201 },
));
await assert.rejects(starting, /superseded/);
assert.equal(media.state().status, "stopped");
assert.equal(media.state().sessionId, null);
assert.equal(track.stopped, 0);
});
it("renews its lease before expiry and uses only the rotated in-memory grant", async () => {
const timers = new FakeTimers();
const eventTokens: string[] = [];
let eventCalls = 0;
const fetcher: typeof fetch = async (input, init = {}) => {
const body = JSON.parse(String(init.body)) as Record<string, unknown>;
if (String(input).endsWith("/join")) {
return Response.json(grant("screen-share-create-grant", String(body.requestId), "presenter", []), { status: 201 });
}
if (String(input).endsWith("/events")) {
eventCalls += 1;
const credential = body.credential as { grantToken: string };
eventTokens.push(credential.grantToken);
const resumed = grant("screen-share-resume-grant", String(body.requestId), "presenter", []);
resumed.grant.credential.grantToken = `rotated-${eventCalls}`;
return sse([resumed]);
}
return new Response(null, { status: 204 });
};
const media = createRemoteOfficeMedia({
role: "presenter", binding: BINDING, fetch: fetcher, timers, now: () => NOW,
peerConnectionFactory: () => new FakePeer() as unknown as RTCPeerConnection,
});
await media.startPresenter({
consent: { authorized: true, optedIn: true },
stream: new FakeStream([new FakeTrack()]) as unknown as MediaStream,
video: new FakeVideo() as unknown as HTMLVideoElement,
});
await settle();
assert.deepEqual(eventTokens, ["private-presenter-grant"]);
assert.equal(timers.pending.size, 1, "one pre-expiry renewal is scheduled");
timers.run();
await settle();
assert.deepEqual(eventTokens, ["private-presenter-grant", "rotated-1"]);
assert.equal(JSON.stringify(media.state()).includes("rotated-2"), false);
await media.dispose();
});
});
describe("remote office media viewer", () => {
it("accepts a ready decision only, answers offers, and stops receiver tracks on revoke", async () => {
const presenterPeer = { participantId: "presenter-opaque", role: "presenter" as const };
const peers: FakePeer[] = [];
const calls: Array<{ url: string; body: Record<string, unknown> }> = [];
const fetcher: typeof fetch = async (input, init = {}) => {
const body = JSON.parse(String(init.body)) as Record<string, unknown>;
calls.push({ url: String(input), body });
if (String(input).endsWith("/join")) return Response.json(grant("screen-share-join-grant", String(body.requestId), "viewer", [presenterPeer]), { status: 201 });
if (String(input).endsWith("/events")) {
const resume = grant("screen-share-resume-grant", String(body.requestId), "viewer", [presenterPeer]);
const ice = {
type: "screen-share-signal-relay", protocolVersion: 1, sequence: 2, timestampMs: NOW + 1,
sessionId: "share-session", binding: BINDING, fromParticipantId: "presenter-opaque",
targetParticipantId: "viewer-opaque",
signal: { kind: "ice-candidate", candidate: "candidate:remote", sdpMid: "0", sdpMLineIndex: 0, usernameFragment: null },
};
const relay = {
type: "screen-share-signal-relay", protocolVersion: 1, sequence: 3, timestampMs: NOW + 2,
sessionId: "share-session", binding: BINDING, fromParticipantId: "presenter-opaque",
targetParticipantId: "viewer-opaque", signal: { kind: "sdp", descriptionType: "offer", sdp: "presenter-offer" },
};
return sse([resume, ice, relay]);
}
return new Response(null, { status: 204 });
};
const media = createRemoteOfficeMedia({
role: "viewer", binding: BINDING, authenticatedFetch: fetcher, now: () => NOW,
peerConnectionFactory: () => { const peer = new FakePeer(); peers.push(peer); return peer as unknown as RTCPeerConnection; },
});
const video = new FakeVideo();
await assert.rejects(media.startViewer({ decision: decision(false), video: video as unknown as HTMLVideoElement }), /ready live-stream/);
await media.startViewer({ decision: decision(), video: video as unknown as HTMLVideoElement });
await settle();
assert.equal(peers[0]?.transceivers[0], "video");
assert.deepEqual(peers[0]?.remote[0], { type: "offer", sdp: "presenter-offer" });
assert.equal(peers[0]?.ice[0]?.candidate, "candidate:remote", "early ICE waits for remote SDP");
assert.ok(calls.some((call) => call.url.endsWith("/signal") &&
(call.body.signal as { descriptionType?: string }).descriptionType === "answer"));
const remoteTrack = new FakeTrack();
const remoteStream = new FakeStream([remoteTrack]);
peers[0]?.receive(remoteTrack, remoteStream);
assert.equal(video.srcObject, remoteStream as unknown as MediaStream);
await media.revoke();
assert.equal(remoteTrack.stopped, 1);
assert.equal(video.srcObject, null);
assert.equal(media.state().status, "revoked");
});
});
+115
View File
@@ -0,0 +1,115 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
advanceScreenShareStreamCursor,
isScreenShareGrantActive,
parseScreenShareClientMessage,
parseScreenShareServerMessage,
} from "../media/signalingValidation.ts";
import type {
ScreenShareAccessGrant,
ScreenShareBinding,
ScreenShareSignalRelay,
} from "../media/signalingTypes.ts";
const BINDING: ScreenShareBinding = {
officeId: "lumbridge-hq",
levelId: "level-1",
roomId: "lobby",
screenId: "lobby-monitor",
};
const CREDENTIAL = {
sessionId: "opaque-session",
participantId: "opaque-participant",
role: "viewer" as const,
grantToken: "opaque-secret-token-at-least-sixteen",
};
describe("office screen signaling protocol", () => {
it("requires literal viewer opt-in and exact JSON keys", () => {
const join = {
type: "screen-share-join-request",
protocolVersion: 1,
sequence: 0,
timestampMs: 1_000,
requestId: "join-1",
binding: BINDING,
role: "viewer",
viewerOptIn: true,
};
assert.equal(parseScreenShareClientMessage(join).ok, true);
assert.equal(parseScreenShareClientMessage({ ...join, viewerOptIn: false }).ok, false);
assert.equal(parseScreenShareClientMessage({ ...join, subject: "identity-leak" }).ok, false);
assert.equal(parseScreenShareClientMessage({ ...join, locator: "https://media.invalid" }).ok, false);
});
it("bounds SDP, ICE and disallows self-signaling", () => {
const request = {
type: "screen-share-signal-request",
protocolVersion: 1,
sequence: 1,
timestampMs: 1_001,
binding: BINDING,
credential: CREDENTIAL,
targetParticipantId: "opaque-presenter",
signal: { kind: "sdp", descriptionType: "answer", sdp: "v=0" },
};
assert.equal(parseScreenShareClientMessage(request).ok, true);
assert.equal(parseScreenShareClientMessage({ ...request, targetParticipantId: CREDENTIAL.participantId }).ok, false);
assert.equal(parseScreenShareClientMessage({
...request,
signal: { ...request.signal, sdp: "x".repeat(24 * 1024 + 1) },
}).ok, false);
});
it("accepts server-issued opaque peer snapshots but no credentials in fan-out", () => {
const message = {
type: "screen-share-participants",
protocolVersion: 1,
sequence: 3,
timestampMs: 1_003,
sessionId: CREDENTIAL.sessionId,
binding: BINDING,
participants: [{ participantId: "opaque-presenter", role: "presenter" }],
leaseExpiresAtMs: 2_000,
};
assert.equal(parseScreenShareServerMessage(message).ok, true);
assert.equal(parseScreenShareServerMessage({
...message,
participants: [{ ...message.participants[0], grantToken: "leak" }],
}).ok, false);
assert.equal(parseScreenShareServerMessage({
...message,
participants: Array.from({ length: 8 }, (_, index) => ({ participantId: `viewer-${index}`, role: "viewer" })),
}).ok, false);
});
it("checks grant time windows without exposing bearer material in state", () => {
const grant: ScreenShareAccessGrant = { credential: CREDENTIAL, issuedAtMs: 1_000, expiresAtMs: 2_000 };
assert.equal(isScreenShareGrantActive(grant, 1_000), true);
assert.equal(isScreenShareGrantActive(grant, 1_999), true);
assert.equal(isScreenShareGrantActive(grant, 2_000), false);
assert.equal(isScreenShareGrantActive(grant, 999), false);
});
it("advances only same-session monotonic peer messages", () => {
const relay: ScreenShareSignalRelay = {
type: "screen-share-signal-relay",
protocolVersion: 1,
sequence: 5,
timestampMs: 1_005,
sessionId: CREDENTIAL.sessionId,
binding: BINDING,
fromParticipantId: "opaque-presenter",
targetParticipantId: CREDENTIAL.participantId,
signal: { kind: "ice-complete" },
};
const first = advanceScreenShareStreamCursor(null, relay);
assert.equal(first.ok, true);
if (!first.ok) return;
assert.equal(advanceScreenShareStreamCursor(first.value, relay).ok, false);
assert.equal(advanceScreenShareStreamCursor(first.value, { ...relay, sequence: 6, sessionId: "foreign" }).ok, false);
assert.equal(advanceScreenShareStreamCursor(first.value, { ...relay, sequence: 6, timestampMs: 1_004 }).ok, false);
assert.equal(advanceScreenShareStreamCursor(first.value, { ...relay, sequence: 6, timestampMs: 1_006 }).ok, true);
});
});