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
+20
View File
@@ -23,3 +23,23 @@ export {
type OfficeScreenPanelOptions,
type OfficeScreenPanelState,
} from "./officeScreenPanel.ts";
export {
createRemoteOfficeMedia,
type PresenterStart,
type RemoteMediaConsent,
type RemoteMediaEndpoints,
type RemoteMediaRole,
type RemoteMediaState,
type RemoteMediaStatus,
type RemoteMediaTimers,
type RemoteOfficeMedia,
type RemoteOfficeMediaOptions,
type ViewerStart,
} from "./remoteMedia.ts";
export * from "./signalingTypes.ts";
export {
advanceScreenShareStreamCursor,
isScreenShareGrantActive,
parseScreenShareClientMessage,
parseScreenShareServerMessage,
} from "./signalingValidation.ts";
+591
View File
@@ -0,0 +1,591 @@
/** Privacy-gated WebRTC transport for the shared office signaling contract. */
import {
advanceScreenShareStreamCursor,
isScreenShareGrantActive,
parseScreenShareServerMessage,
} from "./signalingValidation.ts";
import {
SCREEN_SHARE_SIGNALING_PROTOCOL_VERSION,
type ScreenShareAccessGrant,
type ScreenShareBinding,
type ScreenShareClientMessage,
type ScreenShareCredential,
type ScreenShareParticipant,
type ScreenSharePeerMessage,
type ScreenShareResumeGrant,
type ScreenShareRole,
type ScreenShareServerMessage,
type ScreenShareSignalPayload,
type ScreenShareStreamCursor,
} from "./signalingTypes.ts";
import type { MediaAuthorizationDecision } from "./types.ts";
export type RemoteMediaRole = ScreenShareRole;
export type RemoteMediaStatus = "idle" | "connecting" | "live" | "reconnecting" | "stopped" | "revoked" | "disposed";
export interface RemoteMediaConsent { authorized: boolean; optedIn: boolean }
export interface RemoteMediaState {
role: RemoteMediaRole;
status: RemoteMediaStatus;
binding: ScreenShareBinding;
sessionId: string | null;
participantCount: number;
reconnectAttempt: number;
hasRemoteVideo: boolean;
}
export interface RemoteMediaEndpoints { join: string; signal: string; events: string; leave: string }
export interface RemoteMediaTimers {
setTimeout(callback: () => void, delayMs: number): ReturnType<typeof setTimeout>;
clearTimeout(handle: ReturnType<typeof setTimeout>): void;
}
export interface RemoteOfficeMediaOptions {
role: RemoteMediaRole;
binding: ScreenShareBinding;
/** Preferred: the app's authenticated fetch wrapper. Tokens remain in POST bodies. */
authenticatedFetch?: typeof globalThis.fetch;
fetch?: typeof globalThis.fetch;
peerConnectionFactory?: (configuration?: RTCConfiguration) => RTCPeerConnection;
peerConnectionConfiguration?: RTCConfiguration;
endpoints?: Partial<RemoteMediaEndpoints>;
timers?: RemoteMediaTimers;
now?: () => number;
reconnectBaseMs?: number;
reconnectMaximumMs?: number;
onStateChange?(state: RemoteMediaState): void;
onError?(error: Error): void;
}
export interface PresenterStart {
consent: RemoteMediaConsent;
/** Caller-owned. Exactly one live video track is published and never stopped here. */
stream: MediaStream;
video: HTMLVideoElement;
}
export interface ViewerStart {
/** A current `authorizeMediaSurface` decision with explicit viewer opt-in. */
decision: MediaAuthorizationDecision;
video: HTMLVideoElement;
}
export interface RemoteOfficeMedia {
startPresenter(input: PresenterStart): Promise<void>;
startViewer(input: ViewerStart): Promise<void>;
state(): RemoteMediaState;
stop(): Promise<void>;
revoke(): Promise<void>;
dispose(): Promise<void>;
}
interface PeerSlot {
readonly id: string;
readonly connection: RTCPeerConnection;
readonly pendingIce: (RTCIceCandidateInit | null)[];
}
const ENDPOINTS: RemoteMediaEndpoints = {
join: "/api/v1/media/join",
signal: "/api/v1/media/signal",
events: "/api/v1/media/events",
leave: "/api/v1/media/leave",
};
const TIMERS: RemoteMediaTimers = {
setTimeout: (callback, delay) => globalThis.setTimeout(callback, delay),
clearTimeout: (handle) => globalThis.clearTimeout(handle),
};
function copyBinding(value: ScreenShareBinding): ScreenShareBinding {
for (const key of ["officeId", "levelId", "screenId"] as const) {
if (typeof value[key] !== "string" || value[key].length < 1 || value[key].length > 128) {
throw new TypeError(`remote media: invalid ${key}`);
}
}
if (value.roomId !== null && (typeof value.roomId !== "string" || value.roomId.length < 1 || value.roomId.length > 128)) {
throw new TypeError("remote media: invalid roomId");
}
return { ...value };
}
function sameBinding(a: ScreenShareBinding, b: ScreenShareBinding): boolean {
return a.officeId === b.officeId && a.levelId === b.levelId && a.roomId === b.roomId && a.screenId === b.screenId;
}
function asError(reason: unknown, fallback: string): Error { return reason instanceof Error ? reason : new Error(fallback); }
function requestId(number: number): string { return `tera-media-${number}`; }
function sdp(value: RTCSessionDescriptionInit): ScreenShareSignalPayload {
if ((value.type !== "offer" && value.type !== "answer") || typeof value.sdp !== "string") {
throw new Error("remote media: local SDP is invalid");
}
return { kind: "sdp", descriptionType: value.type, sdp: value.sdp };
}
function remoteDescription(signal: Extract<ScreenShareSignalPayload, { kind: "sdp" }>): RTCSessionDescriptionInit {
return { type: signal.descriptionType, sdp: signal.sdp };
}
/**
* Capture-free and storage-free by construction. The adapter never invokes a
* media acquisition API, calls `play`, records, uploads media, or exposes the
* server grant in state/events. It owns peer connections and received tracks;
* the presenter's input stream, video, and track remain caller-owned.
*/
export function createRemoteOfficeMedia(options: RemoteOfficeMediaOptions): RemoteOfficeMedia {
if (options.authenticatedFetch && options.fetch) throw new TypeError("remote media: provide one fetch adapter");
const fetcher = options.authenticatedFetch ?? options.fetch ?? globalThis.fetch;
if (typeof fetcher !== "function") throw new TypeError("remote media: fetch is required");
const makePeer = options.peerConnectionFactory ?? ((configuration) => new RTCPeerConnection(configuration));
const binding = copyBinding(options.binding);
const endpoints = { ...ENDPOINTS, ...options.endpoints };
const timers = options.timers ?? TIMERS;
const now = options.now ?? Date.now;
const reconnectBase = options.reconnectBaseMs ?? 500;
const reconnectMaximum = options.reconnectMaximumMs ?? 15_000;
if (!(reconnectBase > 0) || !Number.isFinite(reconnectBase) || reconnectMaximum < reconnectBase) {
throw new RangeError("remote media: invalid reconnect bounds");
}
let status: RemoteMediaStatus = "idle";
let grant: ScreenShareAccessGrant | null = null;
let clientSequence = 0;
let counter = 0;
let cursor: ScreenShareStreamCursor | null = null;
let participants: readonly ScreenShareParticipant[] = [];
let presenter: PresenterStart | null = null;
let viewer: ViewerStart | null = null;
let receiverStream: MediaStream | null = null;
let eventAbort: AbortController | null = null;
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
let renewalTimer: ReturnType<typeof setTimeout> | null = null;
let reconnectAttempt = 0;
let generation = 0;
const peers = new Map<string, PeerSlot>();
function state(): RemoteMediaState {
return {
role: options.role,
status,
binding: { ...binding },
sessionId: grant?.credential.sessionId ?? null,
participantCount: peers.size,
reconnectAttempt,
hasRemoteVideo: receiverStream !== null,
};
}
function changed(): void { options.onStateChange?.(state()); }
function setStatus(next: RemoteMediaStatus): void { status = next; changed(); }
function envelope(): { protocolVersion: 1; sequence: number; timestampMs: number } {
const result = { protocolVersion: SCREEN_SHARE_SIGNALING_PROTOCOL_VERSION, sequence: clientSequence, timestampMs: now() };
clientSequence += 1;
return result;
}
async function post(endpoint: string, message: ScreenShareClientMessage, signal?: AbortSignal): Promise<Response> {
return fetcher(endpoint, {
method: "POST",
credentials: "same-origin",
cache: "no-store",
redirect: "error",
headers: { Accept: "application/json", "Content-Type": "application/json" },
body: JSON.stringify(message),
signal,
});
}
async function parsedResponse(response: Response): Promise<ScreenShareServerMessage> {
if (!response.ok) throw new Error(`remote media request failed (${response.status})`);
const parsed = parseScreenShareServerMessage(await response.json() as unknown);
if (!parsed.ok) throw new Error(parsed.error);
return parsed.value;
}
function releaseReceiver(): void {
if (receiverStream) for (const track of receiverStream.getTracks()) track.stop();
if (viewer?.video.srcObject === receiverStream) viewer.video.srcObject = null;
receiverStream = null;
}
function closePeer(id: string): void {
const slot = peers.get(id);
if (!slot) return;
slot.connection.onicecandidate = null;
slot.connection.ontrack = null;
slot.connection.onconnectionstatechange = null;
slot.connection.close();
peers.delete(id);
}
function closeTransport(): void {
generation += 1;
eventAbort?.abort();
eventAbort = null;
if (reconnectTimer !== null) timers.clearTimeout(reconnectTimer);
reconnectTimer = null;
if (renewalTimer !== null) timers.clearTimeout(renewalTimer);
renewalTimer = null;
for (const id of [...peers.keys()]) closePeer(id);
releaseReceiver();
}
function resetPeerMesh(): void {
for (const id of [...peers.keys()]) closePeer(id);
releaseReceiver();
}
async function sendSignal(targetParticipantId: string, signal: ScreenShareSignalPayload): Promise<void> {
if (!grant) throw new Error("remote media: signaling grant is missing");
const request: ScreenShareClientMessage = {
type: "screen-share-signal-request",
...envelope(),
binding,
credential: grant.credential,
targetParticipantId,
signal,
};
const response = await post(endpoints.signal, request);
if (!response.ok) throw new Error(`remote media signal failed (${response.status})`);
}
function configurePeer(participant: ScreenShareParticipant): PeerSlot {
const prior = peers.get(participant.participantId);
if (prior) return prior;
const activeGeneration = generation;
const connection = makePeer(options.peerConnectionConfiguration);
const slot: PeerSlot = { id: participant.participantId, connection, pendingIce: [] };
peers.set(slot.id, slot);
connection.onicecandidate = (event) => {
if (generation !== activeGeneration) return;
const signal: ScreenShareSignalPayload = event.candidate
? {
kind: "ice-candidate",
candidate: event.candidate.candidate,
sdpMid: event.candidate.sdpMid,
sdpMLineIndex: event.candidate.sdpMLineIndex,
usernameFragment: event.candidate.usernameFragment,
}
: { kind: "ice-complete" };
void sendSignal(slot.id, signal).catch((reason) => options.onError?.(asError(reason, "remote media ICE failed")));
};
connection.onconnectionstatechange = () => {
if (connection.connectionState === "connected") { reconnectAttempt = 0; setStatus("live"); }
else if (connection.connectionState === "failed" || connection.connectionState === "disconnected") scheduleReconnect();
};
if (options.role === "presenter") {
const track = presenter?.stream.getVideoTracks()[0];
if (!track || track.readyState !== "live") throw new Error("remote media: presenter track ended");
connection.addTrack(track, presenter!.stream);
} else {
connection.addTransceiver("video", { direction: "recvonly" });
connection.ontrack = (event) => {
if (generation !== activeGeneration || status === "revoked" || status === "disposed") {
event.track.stop();
return;
}
releaseReceiver();
receiverStream = event.streams[0] ?? new MediaStream([event.track]);
viewer!.video.srcObject = receiverStream;
changed();
};
}
changed();
return slot;
}
async function offer(participant: ScreenShareParticipant): Promise<void> {
const connection = configurePeer(participant).connection;
const description = await connection.createOffer();
await connection.setLocalDescription(description);
await sendSignal(participant.participantId, sdp(connection.localDescription ?? description));
}
async function reconcile(next: readonly ScreenShareParticipant[]): Promise<void> {
if (!grant) return;
const own = grant.credential.participantId;
const allowed = next.filter((item) => item.participantId !== own && item.role !== options.role);
const ids = new Set(allowed.map((item) => item.participantId));
for (const id of [...peers.keys()]) if (!ids.has(id)) closePeer(id);
participants = allowed;
if (options.role === "presenter") {
for (const participant of allowed) if (!peers.has(participant.participantId)) await offer(participant);
} else {
const presenterPeer = allowed.find((item) => item.role === "presenter");
if (presenterPeer) configurePeer(presenterPeer);
}
changed();
}
async function applyPeerMessage(message: ScreenSharePeerMessage): Promise<void> {
if (!grant || message.sessionId !== grant.credential.sessionId || !sameBinding(message.binding, binding)) return;
const advanced = advanceScreenShareStreamCursor(cursor, message);
if (!advanced.ok) return;
cursor = advanced.value;
if (message.type === "screen-share-stopped") {
closeTransport();
setStatus("stopped");
return;
}
if (message.type === "screen-share-revoked") {
if (message.scope === "session" || message.targetParticipantId === grant.credential.participantId) {
closeTransport();
grant = null;
setStatus("revoked");
} else if (message.targetParticipantId) closePeer(message.targetParticipantId);
return;
}
if (message.type === "screen-share-participants") {
await reconcile(message.participants);
return;
}
if (message.targetParticipantId !== grant.credential.participantId) return;
const from = participants.find((item) => item.participantId === message.fromParticipantId);
if (!from) return;
const connection = configurePeer(from).connection;
if (message.signal.kind === "ice-complete") {
if (connection.remoteDescription) await connection.addIceCandidate(null);
else peers.get(from.participantId)?.pendingIce.push(null);
return;
}
if (message.signal.kind === "ice-candidate") {
const incoming: RTCIceCandidateInit = {
candidate: message.signal.candidate,
sdpMid: message.signal.sdpMid,
sdpMLineIndex: message.signal.sdpMLineIndex,
usernameFragment: message.signal.usernameFragment ?? undefined,
};
if (connection.remoteDescription) await connection.addIceCandidate(incoming);
else peers.get(from.participantId)?.pendingIce.push(incoming);
return;
}
await connection.setRemoteDescription(remoteDescription(message.signal));
const slot = peers.get(from.participantId);
if (slot) {
for (const incoming of slot.pendingIce.splice(0)) await connection.addIceCandidate(incoming);
}
if (message.signal.descriptionType === "offer") {
const answer = await connection.createAnswer();
await connection.setLocalDescription(answer);
await sendSignal(from.participantId, sdp(connection.localDescription ?? answer));
}
}
async function readEvents(response: Response, activeGeneration: number, expectedRequestId: string): Promise<void> {
if (!response.ok || !response.body) throw new Error(`remote media events failed (${response.status})`);
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
let first = true;
try {
while (generation === activeGeneration) {
const chunk = await reader.read();
if (generation !== activeGeneration || chunk.done) break;
buffer += decoder.decode(chunk.value, { stream: true }).replace(/\r/g, "");
let boundary = buffer.indexOf("\n\n");
while (boundary >= 0) {
const block = buffer.slice(0, boundary);
buffer = buffer.slice(boundary + 2);
boundary = buffer.indexOf("\n\n");
const data = block.split("\n").filter((line) => line.startsWith("data:"))
.map((line) => line.slice(5).replace(/^ /, "")).join("\n");
if (!data) continue;
let raw: unknown;
try { raw = JSON.parse(data); } catch { continue; }
const parsed = parseScreenShareServerMessage(raw);
if (!parsed.ok) continue;
if (first) {
first = false;
if (parsed.value.type !== "screen-share-resume-grant" ||
parsed.value.requestId !== expectedRequestId || !(await acceptGrant(parsed.value))) {
throw new Error("remote media: event stream did not start with a valid resume grant");
}
} else if (parsed.value.type === "screen-share-signal-relay" ||
parsed.value.type === "screen-share-participants" ||
parsed.value.type === "screen-share-stopped" ||
parsed.value.type === "screen-share-revoked") {
await applyPeerMessage(parsed.value);
}
}
}
if (generation === activeGeneration && first) throw new Error("remote media: event stream ended before resume grant");
} finally { reader.releaseLock(); }
}
async function acceptGrant(message: ScreenShareResumeGrant): Promise<boolean> {
const current = grant?.credential;
if (!current || message.grant.credential.sessionId !== current.sessionId ||
message.grant.credential.participantId !== current.participantId ||
!sameBinding(message.binding, binding) || message.grant.credential.role !== options.role ||
!isScreenShareGrantActive(message.grant, now())) return false;
if (grant && (message.grant.credential.sessionId !== grant.credential.sessionId ||
message.grant.credential.participantId !== grant.credential.participantId)) return false;
grant = message.grant;
clientSequence = message.nextClientSequence;
cursor = { sessionId: message.grant.credential.sessionId, sequence: message.sequence, timestampMs: message.timestampMs };
if (!message.continuous) resetPeerMesh();
await reconcile(message.participants);
scheduleRenewal();
setStatus("connecting");
return true;
}
function scheduleRenewal(): void {
if (!grant || status === "revoked" || status === "stopped" || status === "disposed") return;
if (renewalTimer !== null) timers.clearTimeout(renewalTimer);
const delay = Math.max(1_000, grant.expiresAtMs - now() - 15_000);
renewalTimer = timers.setTimeout(() => {
renewalTimer = null;
void startEvents().catch((reason) => {
options.onError?.(asError(reason, "remote media lease renewal failed"));
scheduleReconnect();
});
}, delay);
}
function scheduleReconnect(): void {
if (!grant || status === "revoked" || status === "stopped" || status === "disposed" || reconnectTimer !== null) return;
// A failed RTCPeerConnection is not recoverable by renewing the signaling
// lease alone. Tear down the peer mesh and let the resume snapshot rebuild
// it; presenter input tracks remain untouched and reusable.
generation += 1;
eventAbort?.abort();
eventAbort = null;
if (renewalTimer !== null) timers.clearTimeout(renewalTimer);
renewalTimer = null;
resetPeerMesh();
setStatus("reconnecting");
const delay = Math.min(reconnectMaximum, reconnectBase * 2 ** reconnectAttempt);
reconnectAttempt += 1;
reconnectTimer = timers.setTimeout(() => {
reconnectTimer = null;
void startEvents().catch((reason) => { options.onError?.(asError(reason, "remote media reconnect failed")); scheduleReconnect(); });
}, delay);
}
async function startEvents(): Promise<void> {
if (!grant) throw new Error("remote media: grant is missing");
const activeGeneration = generation;
eventAbort?.abort();
const abort = new AbortController();
eventAbort = abort;
const request: ScreenShareClientMessage = {
type: "screen-share-resume-request",
...envelope(),
requestId: requestId(++counter),
binding,
credential: grant.credential,
lastReceivedSequence: cursor?.sequence ?? 0,
};
try {
const response = await post(endpoints.events, request, abort.signal);
await readEvents(response, activeGeneration, request.requestId);
if (generation === activeGeneration) scheduleReconnect();
} catch (reason) {
if (generation !== activeGeneration || abort.signal.aborted) return;
throw reason;
}
}
async function join(role: RemoteMediaRole, activeGeneration: number): Promise<void> {
const id = requestId(++counter);
const message: ScreenShareClientMessage = role === "presenter"
? { type: "screen-share-create-request", ...envelope(), requestId: id, binding, role }
: { type: "screen-share-join-request", ...envelope(), requestId: id, binding, role, viewerOptIn: true };
const response = await parsedResponse(await post(endpoints.join, message));
const expected = role === "presenter" ? "screen-share-create-grant" : "screen-share-join-grant";
if (response.type !== expected || response.requestId !== id || !sameBinding(response.binding, binding) ||
response.grant.credential.role !== role || !isScreenShareGrantActive(response.grant, now())) {
throw new Error("remote media: join grant is invalid");
}
if (generation !== activeGeneration || status === "disposed" || status === "revoked" || status === "stopped") {
throw new Error("remote media: join was superseded");
}
grant = response.grant;
clientSequence = response.nextClientSequence;
cursor = { sessionId: response.grant.credential.sessionId, sequence: response.sequence, timestampMs: response.timestampMs };
await reconcile(response.participants);
scheduleRenewal();
void startEvents().catch((reason) => { options.onError?.(asError(reason, "remote media events failed")); scheduleReconnect(); });
}
async function startPresenter(input: PresenterStart): Promise<void> {
if (options.role !== "presenter") throw new Error("remote media: adapter is not a presenter");
if (status === "disposed" || status === "revoked") throw new Error("remote media: session is terminal");
if (!input.consent.authorized || !input.consent.optedIn) throw new Error("remote media: authorization and explicit opt-in are required");
const tracks = input.stream.getVideoTracks().filter((track) => track.readyState === "live");
if (tracks.length !== 1) throw new Error("remote media: presenter requires exactly one live screen track");
input.video.autoplay = false;
input.video.muted = true;
closeTransport();
grant = null;
participants = [];
presenter = input;
viewer = null;
setStatus("connecting");
try {
await join("presenter", generation);
} catch (reason) {
if (status === "connecting") {
closeTransport();
grant = null;
presenter = null;
setStatus("idle");
}
throw reason;
}
}
async function startViewer(input: ViewerStart): Promise<void> {
if (options.role !== "viewer") throw new Error("remote media: adapter is not a viewer");
if (status === "disposed" || status === "revoked") throw new Error("remote media: session is terminal");
if (!input.decision.authorized || !input.decision.optedIn || !input.decision.canView ||
input.decision.surface.screenId !== binding.screenId || input.decision.surface.officeId !== binding.officeId ||
input.decision.surface.source?.kind !== "live-stream") {
throw new Error("remote media: a ready live-stream authorization decision is required");
}
input.video.autoplay = false;
input.video.muted = true;
closeTransport();
grant = null;
participants = [];
viewer = input;
presenter = null;
setStatus("connecting");
try {
await join("viewer", generation);
} catch (reason) {
if (status === "connecting") {
closeTransport();
grant = null;
viewer = null;
setStatus("idle");
}
throw reason;
}
}
async function leave(): Promise<void> {
const credential = grant?.credential;
closeTransport();
participants = [];
grant = null;
if (!credential) return;
// A presenter ending the whole share is always a stop. `revoke` is a
// policy word locally, not permission to claim a moderator action on wire.
const message: ScreenShareClientMessage = options.role === "presenter"
? {
type: "screen-share-stop-request", ...envelope(), requestId: requestId(++counter), binding,
credential: credential as ScreenShareCredential<"presenter">, reason: "presenter-stopped",
}
: {
type: "screen-share-revoke-request", ...envelope(), requestId: requestId(++counter), binding,
credential, scope: "participant",
targetParticipantId: credential.participantId,
reason: "viewer-left",
};
const response = await post(endpoints.leave, message);
if (!response.ok) throw new Error(`remote media leave failed (${response.status})`);
}
async function stop(): Promise<void> {
if (status === "disposed" || status === "stopped") return;
try { await leave(); } finally { presenter = null; viewer = null; setStatus("stopped"); }
}
async function revoke(): Promise<void> {
if (status === "disposed" || status === "revoked") return;
try { await leave(); } finally { presenter = null; viewer = null; setStatus("revoked"); }
}
async function dispose(): Promise<void> {
if (status === "disposed") return;
try { await leave(); } finally { presenter = null; viewer = null; status = "disposed"; changed(); }
}
return { startPresenter, startViewer, state, stop, revoke, dispose };
}
+251
View File
@@ -0,0 +1,251 @@
/**
* JSON-safe signaling contracts for an eventual server-authoritative office
* screen-share service. These types intentionally contain no browser or WebRTC
* runtime objects and no MediaSurfaceSource locator.
*/
export const SCREEN_SHARE_SIGNALING_PROTOCOL_VERSION = 1 as const;
/** One presenter plus at most seven viewers in the initial bounded P2P mesh. */
export const MAX_SCREEN_SHARE_PARTICIPANTS = 8 as const;
export type ScreenShareRole = "presenter" | "viewer";
export type ScreenShareSequence = number;
export type ScreenShareTimestamp = number;
/** The authored office surface to which a signaling session is bound. */
export interface ScreenShareBinding {
officeId: string;
levelId: string;
roomId: string | null;
screenId: string;
}
/**
* Private bearer material. It may cross the client/server boundary but must
* never be copied into a ScreenSharePeerMessage.
*/
export interface ScreenShareCredential<R extends ScreenShareRole = ScreenShareRole> {
sessionId: string;
participantId: string;
role: R;
grantToken: string;
}
export interface ScreenShareAccessGrant<R extends ScreenShareRole = ScreenShareRole> {
credential: ScreenShareCredential<R>;
issuedAtMs: ScreenShareTimestamp;
expiresAtMs: ScreenShareTimestamp;
}
/** Server-issued, session-scoped identity; never an account/profile identifier. */
export interface ScreenShareParticipant {
participantId: string;
role: ScreenShareRole;
}
export interface ScreenShareSdpSignal {
kind: "sdp";
descriptionType: "offer" | "answer";
sdp: string;
}
export interface ScreenShareIceCandidateSignal {
kind: "ice-candidate";
candidate: string;
sdpMid: string | null;
sdpMLineIndex: number | null;
usernameFragment: string | null;
}
export interface ScreenShareIceCompleteSignal {
kind: "ice-complete";
}
export type ScreenShareSignalPayload =
| ScreenShareSdpSignal
| ScreenShareIceCandidateSignal
| ScreenShareIceCompleteSignal;
interface ScreenShareEnvelope {
protocolVersion: typeof SCREEN_SHARE_SIGNALING_PROTOCOL_VERSION;
sequence: ScreenShareSequence;
timestampMs: ScreenShareTimestamp;
}
interface ScreenShareRequestedEnvelope extends ScreenShareEnvelope {
requestId: string;
}
export interface ScreenShareCreateRequest extends ScreenShareRequestedEnvelope {
type: "screen-share-create-request";
binding: ScreenShareBinding;
role: "presenter";
}
export interface ScreenShareJoinRequest extends ScreenShareRequestedEnvelope {
type: "screen-share-join-request";
binding: ScreenShareBinding;
role: "viewer";
/** A literal, affirmative action; omission and false are both invalid. */
viewerOptIn: true;
}
export interface ScreenShareResumeRequest extends ScreenShareRequestedEnvelope {
type: "screen-share-resume-request";
binding: ScreenShareBinding;
credential: ScreenShareCredential;
lastReceivedSequence: ScreenShareSequence;
}
export interface ScreenShareSignalRequest extends ScreenShareEnvelope {
type: "screen-share-signal-request";
binding: ScreenShareBinding;
credential: ScreenShareCredential;
targetParticipantId: string;
signal: ScreenShareSignalPayload;
}
export type ScreenShareClientStopReason = "presenter-stopped" | "capture-ended";
export interface ScreenShareStopRequest extends ScreenShareRequestedEnvelope {
type: "screen-share-stop-request";
binding: ScreenShareBinding;
credential: ScreenShareCredential<"presenter">;
reason: ScreenShareClientStopReason;
}
export type ScreenShareRevokeScope = "session" | "participant";
export type ScreenShareRevokeRequestReason =
| "viewer-left"
| "presenter-removed-viewer"
| "moderator-action";
export interface ScreenShareRevokeRequest extends ScreenShareRequestedEnvelope {
type: "screen-share-revoke-request";
binding: ScreenShareBinding;
credential: ScreenShareCredential;
scope: ScreenShareRevokeScope;
targetParticipantId: string | null;
reason: ScreenShareRevokeRequestReason;
}
interface ScreenShareGrantEnvelope<R extends ScreenShareRole> extends ScreenShareRequestedEnvelope {
binding: ScreenShareBinding;
grant: ScreenShareAccessGrant<R>;
nextClientSequence: ScreenShareSequence;
/** Authorized peers excluding the grant recipient. */
participants: readonly ScreenShareParticipant[];
}
export interface ScreenShareCreateGrant extends ScreenShareGrantEnvelope<"presenter"> {
type: "screen-share-create-grant";
}
export interface ScreenShareJoinGrant extends ScreenShareGrantEnvelope<"viewer"> {
type: "screen-share-join-grant";
}
export interface ScreenShareResumeGrant extends ScreenShareGrantEnvelope<ScreenShareRole> {
type: "screen-share-resume-grant";
/** False requires a fresh negotiation; no missed relays are implied. */
continuous: boolean;
}
/**
* Current authorized peers. Clients reconcile their P2P mesh to this complete
* list and close any connection absent from it.
*/
export interface ScreenShareParticipants extends ScreenShareEnvelope {
type: "screen-share-participants";
sessionId: string;
binding: ScreenShareBinding;
participants: readonly ScreenShareParticipant[];
leaseExpiresAtMs: ScreenShareTimestamp;
}
/** Credential-free message safe to deliver to one explicitly opted-in peer. */
export interface ScreenShareSignalRelay extends ScreenShareEnvelope {
type: "screen-share-signal-relay";
sessionId: string;
binding: ScreenShareBinding;
fromParticipantId: string;
targetParticipantId: string;
signal: ScreenShareSignalPayload;
}
export type ScreenShareStoppedReason =
| ScreenShareClientStopReason
| "grant-expired"
| "server-shutdown";
/** Credential-free terminal notice safe to deliver to session peers. */
export interface ScreenShareStopped extends ScreenShareEnvelope {
type: "screen-share-stopped";
sessionId: string;
binding: ScreenShareBinding;
reason: ScreenShareStoppedReason;
}
export type ScreenShareRevocationReason =
| "viewer-left"
| "presenter-removed-viewer"
| "moderator-action"
| "authorization-revoked"
| "membership-revoked"
| "protocol-violation"
| "session-replaced";
/** Credential-free revocation notice; participant ids are session-opaque. */
export interface ScreenShareRevoked extends ScreenShareEnvelope {
type: "screen-share-revoked";
sessionId: string;
binding: ScreenShareBinding;
scope: ScreenShareRevokeScope;
targetParticipantId: string | null;
reason: ScreenShareRevocationReason;
reconnectAllowed: boolean;
}
export type ScreenShareClientMessage =
| ScreenShareCreateRequest
| ScreenShareJoinRequest
| ScreenShareResumeRequest
| ScreenShareSignalRequest
| ScreenShareStopRequest
| ScreenShareRevokeRequest;
export type ScreenShareServerMessage =
| ScreenShareCreateGrant
| ScreenShareJoinGrant
| ScreenShareResumeGrant
| ScreenShareParticipants
| ScreenShareSignalRelay
| ScreenShareStopped
| ScreenShareRevoked;
/** The only server messages permitted on a peer fan-out channel. */
export type ScreenSharePeerMessage =
| ScreenShareParticipants
| ScreenShareSignalRelay
| ScreenShareStopped
| ScreenShareRevoked;
export type ScreenShareSessionMessage =
| ScreenShareResumeRequest
| ScreenShareSignalRequest
| ScreenShareStopRequest
| ScreenShareRevokeRequest
| ScreenShareParticipants
| ScreenShareSignalRelay
| ScreenShareStopped
| ScreenShareRevoked;
export interface ScreenShareStreamCursor {
sessionId: string;
sequence: ScreenShareSequence;
timestampMs: ScreenShareTimestamp;
}
export type ScreenShareValidationResult<T> =
| { ok: true; value: T }
| { ok: false; error: string };
+212
View File
@@ -0,0 +1,212 @@
/** Strict runtime boundary for the JSON-only office screen signaling protocol. */
import {
MAX_SCREEN_SHARE_PARTICIPANTS,
SCREEN_SHARE_SIGNALING_PROTOCOL_VERSION,
type ScreenShareAccessGrant,
type ScreenShareBinding,
type ScreenShareClientMessage,
type ScreenShareCredential,
type ScreenShareParticipant,
type ScreenSharePeerMessage,
type ScreenShareServerMessage,
type ScreenShareSignalPayload,
type ScreenShareStreamCursor,
type ScreenShareValidationResult,
} from "./signalingTypes.ts";
const MAX_ID = 256;
const MAX_TOKEN = 512;
const MAX_SDP = 24 * 1024;
const MAX_CANDIDATE = 4 * 1024;
type RecordValue = Record<string, unknown>;
function plain(value: unknown): value is RecordValue {
if (value === null || typeof value !== "object" || Array.isArray(value)) return false;
const prototype = Object.getPrototypeOf(value);
return prototype === Object.prototype;
}
function exact(value: unknown, keys: readonly string[]): value is RecordValue {
if (!plain(value)) return false;
const actual = Object.keys(value).sort();
const expected = [...keys].sort();
return actual.length === expected.length && actual.every((key, index) => key === expected[index]);
}
function id(value: unknown, maximum = MAX_ID): value is string {
return typeof value === "string" && value.length > 0 && value.length <= maximum;
}
function sequence(value: unknown): value is number {
return Number.isSafeInteger(value) && (value as number) >= 0;
}
function timestamp(value: unknown): value is number {
return Number.isSafeInteger(value) && (value as number) >= 0;
}
function envelope(value: RecordValue): boolean {
return value.protocolVersion === SCREEN_SHARE_SIGNALING_PROTOCOL_VERSION &&
sequence(value.sequence) && timestamp(value.timestampMs);
}
function binding(value: unknown): value is ScreenShareBinding {
return exact(value, ["officeId", "levelId", "roomId", "screenId"]) &&
id(value.officeId, 128) && id(value.levelId, 128) &&
(value.roomId === null || id(value.roomId, 128)) && id(value.screenId, 128);
}
function credential(value: unknown): value is ScreenShareCredential {
return exact(value, ["sessionId", "participantId", "role", "grantToken"]) &&
id(value.sessionId) && id(value.participantId) &&
(value.role === "presenter" || value.role === "viewer") && id(value.grantToken, MAX_TOKEN);
}
function participant(value: unknown): value is ScreenShareParticipant {
return exact(value, ["participantId", "role"]) && id(value.participantId) &&
(value.role === "presenter" || value.role === "viewer");
}
function participants(value: unknown): value is readonly ScreenShareParticipant[] {
if (!Array.isArray(value) || value.length > MAX_SCREEN_SHARE_PARTICIPANTS - 1 || !value.every(participant)) return false;
const ids = value.map((item) => item.participantId);
return new Set(ids).size === ids.length && value.filter((item) => item.role === "presenter").length <= 1;
}
function accessGrant(value: unknown): value is ScreenShareAccessGrant {
return exact(value, ["credential", "issuedAtMs", "expiresAtMs"]) && credential(value.credential) &&
timestamp(value.issuedAtMs) && timestamp(value.expiresAtMs) && value.expiresAtMs > value.issuedAtMs;
}
function signal(value: unknown): value is ScreenShareSignalPayload {
if (!plain(value)) return false;
if (value.kind === "sdp") {
return exact(value, ["kind", "descriptionType", "sdp"]) &&
(value.descriptionType === "offer" || value.descriptionType === "answer") &&
id(value.sdp, MAX_SDP);
}
if (value.kind === "ice-candidate") {
return exact(value, ["kind", "candidate", "sdpMid", "sdpMLineIndex", "usernameFragment"]) &&
typeof value.candidate === "string" && value.candidate.length <= MAX_CANDIDATE &&
(value.sdpMid === null || id(value.sdpMid, 256)) &&
(value.sdpMLineIndex === null || (Number.isInteger(value.sdpMLineIndex) && (value.sdpMLineIndex as number) >= 0 && (value.sdpMLineIndex as number) <= 65_535)) &&
(value.usernameFragment === null || id(value.usernameFragment, 256));
}
return value.kind === "ice-complete" && exact(value, ["kind"]);
}
function requested(value: RecordValue): boolean {
return envelope(value) && id(value.requestId);
}
export function parseScreenShareClientMessage(value: unknown): ScreenShareValidationResult<ScreenShareClientMessage> {
if (!plain(value) || !envelope(value) || typeof value.type !== "string") return failure("invalid client envelope");
switch (value.type) {
case "screen-share-create-request":
if (exact(value, ["type", "protocolVersion", "sequence", "timestampMs", "requestId", "binding", "role"]) &&
requested(value) && binding(value.binding) && value.role === "presenter") return success(value as unknown as ScreenShareClientMessage);
break;
case "screen-share-join-request":
if (exact(value, ["type", "protocolVersion", "sequence", "timestampMs", "requestId", "binding", "role", "viewerOptIn"]) &&
requested(value) && binding(value.binding) && value.role === "viewer" && value.viewerOptIn === true) return success(value as unknown as ScreenShareClientMessage);
break;
case "screen-share-resume-request":
if (exact(value, ["type", "protocolVersion", "sequence", "timestampMs", "requestId", "binding", "credential", "lastReceivedSequence"]) &&
requested(value) && binding(value.binding) && credential(value.credential) && sequence(value.lastReceivedSequence)) return success(value as unknown as ScreenShareClientMessage);
break;
case "screen-share-signal-request":
if (exact(value, ["type", "protocolVersion", "sequence", "timestampMs", "binding", "credential", "targetParticipantId", "signal"]) &&
binding(value.binding) && credential(value.credential) && id(value.targetParticipantId) && signal(value.signal) &&
value.targetParticipantId !== value.credential.participantId) return success(value as unknown as ScreenShareClientMessage);
break;
case "screen-share-stop-request":
if (exact(value, ["type", "protocolVersion", "sequence", "timestampMs", "requestId", "binding", "credential", "reason"]) &&
requested(value) && binding(value.binding) && credential(value.credential) && value.credential.role === "presenter" &&
(value.reason === "presenter-stopped" || value.reason === "capture-ended")) return success(value as unknown as ScreenShareClientMessage);
break;
case "screen-share-revoke-request":
if (exact(value, ["type", "protocolVersion", "sequence", "timestampMs", "requestId", "binding", "credential", "scope", "targetParticipantId", "reason"]) &&
requested(value) && binding(value.binding) && credential(value.credential) &&
(value.scope === "session" || value.scope === "participant") &&
(value.targetParticipantId === null || id(value.targetParticipantId)) &&
(value.reason === "viewer-left" || value.reason === "presenter-removed-viewer" || value.reason === "moderator-action") &&
((value.scope === "session" && value.targetParticipantId === null) || (value.scope === "participant" && id(value.targetParticipantId)))) {
return success(value as unknown as ScreenShareClientMessage);
}
break;
}
return failure("invalid client message");
}
function grantEnvelope(value: RecordValue, resume: boolean): boolean {
const keys = ["type", "protocolVersion", "sequence", "timestampMs", "requestId", "binding", "grant", "nextClientSequence", "participants"];
if (resume) keys.push("continuous");
return exact(value, keys) && requested(value) && binding(value.binding) && accessGrant(value.grant) &&
sequence(value.nextClientSequence) && participants(value.participants) && (!resume || typeof value.continuous === "boolean");
}
export function parseScreenShareServerMessage(value: unknown): ScreenShareValidationResult<ScreenShareServerMessage> {
if (!plain(value) || !envelope(value) || typeof value.type !== "string") return failure("invalid server envelope");
switch (value.type) {
case "screen-share-create-grant":
if (grantEnvelope(value, false) && (value.grant as ScreenShareAccessGrant).credential.role === "presenter") return success(value as unknown as ScreenShareServerMessage);
break;
case "screen-share-join-grant":
if (grantEnvelope(value, false) && (value.grant as ScreenShareAccessGrant).credential.role === "viewer") return success(value as unknown as ScreenShareServerMessage);
break;
case "screen-share-resume-grant":
if (grantEnvelope(value, true)) return success(value as unknown as ScreenShareServerMessage);
break;
case "screen-share-participants":
if (exact(value, ["type", "protocolVersion", "sequence", "timestampMs", "sessionId", "binding", "participants", "leaseExpiresAtMs"]) &&
id(value.sessionId) && binding(value.binding) && participants(value.participants) &&
timestamp(value.timestampMs) && timestamp(value.leaseExpiresAtMs) &&
(value.leaseExpiresAtMs as number) > (value.timestampMs as number)) return success(value as unknown as ScreenShareServerMessage);
break;
case "screen-share-signal-relay":
if (exact(value, ["type", "protocolVersion", "sequence", "timestampMs", "sessionId", "binding", "fromParticipantId", "targetParticipantId", "signal"]) &&
id(value.sessionId) && binding(value.binding) && id(value.fromParticipantId) && id(value.targetParticipantId) &&
value.fromParticipantId !== value.targetParticipantId && signal(value.signal)) return success(value as unknown as ScreenShareServerMessage);
break;
case "screen-share-stopped":
if (exact(value, ["type", "protocolVersion", "sequence", "timestampMs", "sessionId", "binding", "reason"]) &&
id(value.sessionId) && binding(value.binding) &&
(value.reason === "presenter-stopped" || value.reason === "capture-ended" || value.reason === "grant-expired" || value.reason === "server-shutdown")) {
return success(value as unknown as ScreenShareServerMessage);
}
break;
case "screen-share-revoked":
if (exact(value, ["type", "protocolVersion", "sequence", "timestampMs", "sessionId", "binding", "scope", "targetParticipantId", "reason", "reconnectAllowed"]) &&
id(value.sessionId) && binding(value.binding) && (value.scope === "session" || value.scope === "participant") &&
(value.targetParticipantId === null || id(value.targetParticipantId)) && typeof value.reconnectAllowed === "boolean" &&
["viewer-left", "presenter-removed-viewer", "moderator-action", "authorization-revoked", "membership-revoked", "protocol-violation", "session-replaced"].includes(String(value.reason)) &&
((value.scope === "session" && value.targetParticipantId === null) || (value.scope === "participant" && id(value.targetParticipantId)))) {
return success(value as unknown as ScreenShareServerMessage);
}
break;
}
return failure("invalid server message");
}
export function isScreenShareGrantActive(grant: ScreenShareAccessGrant, nowMs: number): boolean {
return accessGrant(grant) && timestamp(nowMs) && nowMs >= grant.issuedAtMs && nowMs < grant.expiresAtMs;
}
export function advanceScreenShareStreamCursor(
cursor: ScreenShareStreamCursor | null,
message: ScreenSharePeerMessage,
): ScreenShareValidationResult<ScreenShareStreamCursor> {
const parsed = parseScreenShareServerMessage(message);
if (!parsed.ok || !["screen-share-participants", "screen-share-signal-relay", "screen-share-stopped", "screen-share-revoked"].includes(parsed.value.type)) {
return failure("invalid peer message");
}
if (cursor && (cursor.sessionId !== message.sessionId || message.sequence <= cursor.sequence || message.timestampMs < cursor.timestampMs)) {
return failure("stale or foreign stream message");
}
return success({ sessionId: message.sessionId, sequence: message.sequence, timestampMs: message.timestampMs });
}
function success<T>(value: T): ScreenShareValidationResult<T> { return { ok: true, value }; }
function failure<T>(detail: string): ScreenShareValidationResult<T> { return { ok: false, error: `screen share: ${detail}` }; }
+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);
});
});