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
+41
View File
@@ -0,0 +1,41 @@
/** Server-authoritative lookup for authored office screen surfaces. */
import { Plan } from "../../../src/interiors/plan.ts";
import type { Office } from "../../../src/interiors/types.ts";
import FRONTIER_VALLEY from "../../../src/offices/frontier-valley.ts";
import LUMBRIDGE_HQ from "../../../src/offices/lumbridge-hq.ts";
import MATEO_COURT from "../../../src/offices/mateo-court.ts";
import type { ScreenShareBinding } from "../../../src/media/signalingTypes.ts";
const SCREEN_KINDS = new Set([
"tera:screen.monitor",
"tera:screen.wall-display",
]);
const BUNDLED_OFFICES: ReadonlyMap<string, Office> = new Map(
[LUMBRIDGE_HQ, FRONTIER_VALLEY, MATEO_COURT].map((office) => [office.id, office]),
);
/**
* A bundled pack is public sample geometry, so its exact authored screen
* catalogue is the only bundled exception. No real/private pack is inferred.
*/
export function bundledMediaOffice(officeId: string): Office | null {
return BUNDLED_OFFICES.get(officeId) ?? null;
}
/**
* Confirm an exact client binding against a validated/resolved office pack.
* Room membership is computed from the prop's authored position rather than
* trusting a caller-provided label.
*/
export function officeHasMediaBinding(office: Office, binding: ScreenShareBinding): boolean {
if (office.id !== binding.officeId) return false;
const plan = new Plan(office, { depth: "full", warn: false });
const level = plan.level(binding.levelId);
if (!level) return false;
const prop = level.props.find((candidate) => candidate.id === binding.screenId);
if (!prop || !SCREEN_KINDS.has(prop.kind)) return false;
const roomId = plan.roomAt(level.id, prop.position)?.id ?? null;
return roomId === binding.roomId;
}
+8
View File
@@ -0,0 +1,8 @@
export {
createMediaSignalService,
type MediaSignalFailureCode,
type MediaSignalResult,
type MediaSignalService,
type MediaSignalServiceOptions,
} from "./service.ts";
export { bundledMediaOffice, officeHasMediaBinding } from "./bindings.ts";
+406
View File
@@ -0,0 +1,406 @@
/** In-memory WebRTC signaling only: no tracks, media bytes, locators, or persistence. */
import { createHash, randomBytes, timingSafeEqual } from "node:crypto";
import {
MAX_SCREEN_SHARE_PARTICIPANTS,
SCREEN_SHARE_SIGNALING_PROTOCOL_VERSION,
type ScreenShareAccessGrant,
type ScreenShareBinding,
type ScreenShareCreateGrant,
type ScreenShareCreateRequest,
type ScreenShareCredential,
type ScreenShareJoinGrant,
type ScreenShareJoinRequest,
type ScreenShareParticipant,
type ScreenSharePeerMessage,
type ScreenShareResumeGrant,
type ScreenShareResumeRequest,
type ScreenShareRevokeRequest,
type ScreenShareRevoked,
type ScreenShareRole,
type ScreenShareSignalRelay,
type ScreenShareSignalRequest,
type ScreenShareStopRequest,
type ScreenShareStopped,
} from "../../../src/media/signalingTypes.ts";
export type MediaSignalFailureCode =
| "invalid" | "unauthorized" | "expired" | "conflict" | "capacity" | "rate-limited";
export type MediaSignalResult<T> =
| { ok: true; value: T }
| { ok: false; code: MediaSignalFailureCode; message: string };
export interface MediaSignalServiceOptions {
now?: () => number;
grantTtlMs?: number;
maximumSessions?: number;
maximumParticipantsPerSession?: number;
maximumQueuedMessagesPerParticipant?: number;
maximumSignalsPerWindow?: number;
rateWindowMs?: number;
}
export interface MediaSignalService {
join(request: ScreenShareCreateRequest | ScreenShareJoinRequest, subject: string): MediaSignalResult<ScreenShareCreateGrant | ScreenShareJoinGrant>;
resume(request: ScreenShareResumeRequest, subject: string): MediaSignalResult<ScreenShareResumeGrant>;
signal(request: ScreenShareSignalRequest, subject: string): MediaSignalResult<null>;
leave(request: ScreenShareStopRequest | ScreenShareRevokeRequest, subject: string, admin: boolean): MediaSignalResult<null>;
subscribe(credential: ScreenShareCredential, subject: string, listener: (message: ScreenSharePeerMessage) => void): MediaSignalResult<() => void>;
revalidate(credential: ScreenShareCredential, subject: string): MediaSignalResult<null>;
revokeSubject(subject: string): number;
cleanup(): number;
sessionCount(): number;
}
interface Participant {
id: string;
subject: string;
role: ScreenShareRole;
tokenHash: Buffer;
issuedAt: number;
expiresAt: number;
lastClientSequence: number;
rateStartedAt: number;
rateCount: number;
queue: ScreenSharePeerMessage[];
listeners: Set<(message: ScreenSharePeerMessage) => void>;
}
interface Session {
id: string;
binding: ScreenShareBinding;
sequence: number;
participants: Map<string, Participant>;
}
export function createMediaSignalService(options: MediaSignalServiceOptions = {}): MediaSignalService {
const now = options.now ?? Date.now;
const ttl = positive(options.grantTtlMs, 90_000);
const maximumSessions = count(options.maximumSessions, 128);
const maximumParticipants = Math.min(
MAX_SCREEN_SHARE_PARTICIPANTS,
count(options.maximumParticipantsPerSession, MAX_SCREEN_SHARE_PARTICIPANTS),
);
const maximumQueued = count(options.maximumQueuedMessagesPerParticipant, 64);
const maximumSignals = count(options.maximumSignalsPerWindow, 120);
const rateWindowMs = positive(options.rateWindowMs, 10_000);
const sessions = new Map<string, Session>();
function peerList(session: Session, except: string): ScreenShareParticipant[] {
return [...session.participants.values()]
.filter((participant) => participant.id !== except)
.map(({ id: participantId, role }) => ({ participantId, role }));
}
function nextSequence(session: Session): number {
session.sequence = Math.min(Number.MAX_SAFE_INTEGER, session.sequence + 1);
return session.sequence;
}
function stopNotice(session: Session, reason: ScreenShareStopped["reason"]): ScreenShareStopped {
return { type: "screen-share-stopped", protocolVersion: SCREEN_SHARE_SIGNALING_PROTOCOL_VERSION,
sequence: nextSequence(session), timestampMs: now(), sessionId: session.id,
binding: session.binding, reason };
}
function credential(session: Session, participant: Participant, token: string): ScreenShareCredential {
return { sessionId: session.id, participantId: participant.id, role: participant.role, grantToken: token };
}
function grant(session: Session, participant: Participant, token: string): ScreenShareAccessGrant {
return { credential: credential(session, participant, token), issuedAtMs: participant.issuedAt, expiresAtMs: participant.expiresAt };
}
function emit(participant: Participant, message: ScreenSharePeerMessage): void {
if (participant.listeners.size > 0) {
for (const listener of participant.listeners) listener(message);
return;
}
if (participant.queue.length >= maximumQueued) {
// Resume is deliberately discontinuous and carries a complete peer
// snapshot, so dropping the oldest relay is safer than letting a sender
// evict another participant by filling its queue.
participant.queue.shift();
}
participant.queue.push(message);
}
function participantSnapshot(session: Session): void {
const timestampMs = now();
for (const participant of [...session.participants.values()]) {
emit(participant, {
type: "screen-share-participants",
protocolVersion: SCREEN_SHARE_SIGNALING_PROTOCOL_VERSION,
sequence: nextSequence(session),
timestampMs,
sessionId: session.id,
binding: session.binding,
participants: peerList(session, participant.id),
leaseExpiresAtMs: participant.expiresAt,
});
}
}
function destroySession(session: Session, notice?: ScreenShareStopped | ScreenShareRevoked): void {
// Terminal messages are best effort for connected peers. Queuing one and
// then immediately destroying the queue would be dishonest, while routing
// through `emit` on a full queue could recurse into participant removal.
if (notice) {
for (const participant of session.participants.values()) {
for (const listener of participant.listeners) listener(notice);
}
}
sessions.delete(session.id);
for (const participant of session.participants.values()) {
participant.tokenHash.fill(0);
participant.queue.length = 0;
participant.listeners.clear();
}
session.participants.clear();
}
function removeParticipant(
session: Session,
participant: Participant,
reason: ScreenShareRevoked["reason"] = "viewer-left",
): void {
const notice: ScreenShareRevoked = {
type: "screen-share-revoked",
protocolVersion: SCREEN_SHARE_SIGNALING_PROTOCOL_VERSION,
sequence: nextSequence(session),
timestampMs: now(),
sessionId: session.id,
binding: session.binding,
scope: "participant",
targetParticipantId: participant.id,
reason,
reconnectAllowed: false,
};
for (const listener of participant.listeners) listener(notice);
session.participants.delete(participant.id);
participant.tokenHash.fill(0);
participant.queue.length = 0;
participant.listeners.clear();
participantSnapshot(session);
}
function authenticate(raw: ScreenShareCredential, subject: string): MediaSignalResult<{ session: Session; participant: Participant }> {
const session = sessions.get(raw.sessionId);
const participant = session?.participants.get(raw.participantId);
if (!session || !participant || participant.subject !== subject || participant.role !== raw.role ||
!tokenMatches(participant.tokenHash, raw.grantToken)) {
return { ok: false, code: "unauthorized", message: "Media signaling credential is invalid." };
}
if (now() >= participant.expiresAt) {
if (participant.role === "presenter") {
destroySession(session, stopNotice(session, "grant-expired"));
} else removeParticipant(session, participant, "authorization-revoked");
return { ok: false, code: "expired", message: "Media signaling grant expired." };
}
return { ok: true, value: { session, participant } };
}
function validateRequest(
session: Session,
participant: Participant,
binding: ScreenShareBinding,
sequence: number,
): MediaSignalResult<null> {
if (!sameBinding(session.binding, binding)) return { ok: false, code: "unauthorized", message: "Media binding does not match the grant." };
if (!Number.isSafeInteger(sequence) || sequence <= participant.lastClientSequence) {
return { ok: false, code: "conflict", message: "Media client sequence is not monotonic." };
}
participant.lastClientSequence = sequence;
return { ok: true, value: null };
}
function newParticipant(subject: string, role: ScreenShareRole, clientSequence: number): { participant: Participant; token: string } {
const token = opaque(32);
const issuedAt = now();
return { token, participant: {
id: opaque(18), subject, role, tokenHash: hash(token), issuedAt, expiresAt: issuedAt + ttl,
lastClientSequence: clientSequence, rateStartedAt: issuedAt, rateCount: 0, queue: [], listeners: new Set(),
} };
}
function join(request: ScreenShareCreateRequest | ScreenShareJoinRequest, subject: string): MediaSignalResult<ScreenShareCreateGrant | ScreenShareJoinGrant> {
cleanup();
if (request.type === "screen-share-create-request") {
if (sessions.size >= maximumSessions) return { ok: false, code: "capacity", message: "Media signaling is at capacity." };
if ([...sessions.values()].some((candidate) => sameBinding(candidate.binding, request.binding))) {
return { ok: false, code: "conflict", message: "That screen already has an active presenter." };
}
const session: Session = { id: opaque(18), binding: copyBinding(request.binding), sequence: 0, participants: new Map() };
const made = newParticipant(subject, "presenter", request.sequence);
session.participants.set(made.participant.id, made.participant);
sessions.set(session.id, session);
return { ok: true, value: {
type: "screen-share-create-grant", protocolVersion: SCREEN_SHARE_SIGNALING_PROTOCOL_VERSION,
requestId: request.requestId, sequence: nextSequence(session), timestampMs: now(), binding: session.binding,
grant: grant(session, made.participant, made.token) as ScreenShareAccessGrant<"presenter">,
nextClientSequence: request.sequence + 1, participants: [],
} };
}
const session = [...sessions.values()].find((candidate) => sameBinding(candidate.binding, request.binding));
if (!session) return { ok: false, code: "conflict", message: "That screen has no active presenter." };
if (session.participants.size >= maximumParticipants) return { ok: false, code: "capacity", message: "Media screen is at capacity." };
if ([...session.participants.values()].some((participant) => participant.subject === subject)) {
return { ok: false, code: "conflict", message: "Member is already active on that screen." };
}
const made = newParticipant(subject, "viewer", request.sequence);
const existing = peerList(session, made.participant.id);
session.participants.set(made.participant.id, made.participant);
participantSnapshot(session);
return { ok: true, value: {
type: "screen-share-join-grant", protocolVersion: SCREEN_SHARE_SIGNALING_PROTOCOL_VERSION,
requestId: request.requestId, sequence: nextSequence(session), timestampMs: now(), binding: session.binding,
grant: grant(session, made.participant, made.token) as ScreenShareAccessGrant<"viewer">,
nextClientSequence: request.sequence + 1, participants: existing,
} };
}
function resume(request: ScreenShareResumeRequest, subject: string): MediaSignalResult<ScreenShareResumeGrant> {
const found = authenticate(request.credential, subject);
if (!found.ok) return found;
const { session, participant } = found.value;
const valid = validateRequest(session, participant, request.binding, request.sequence);
if (!valid.ok) return valid;
const token = opaque(32);
participant.tokenHash.fill(0);
participant.tokenHash = hash(token);
participant.issuedAt = now();
participant.expiresAt = now() + ttl;
// A resume grant carries a complete participant snapshot. Dropping queued
// relays avoids replaying a message with a sequence older than that grant;
// clients renegotiate when `continuous` is false.
participant.queue.length = 0;
return { ok: true, value: {
type: "screen-share-resume-grant", protocolVersion: SCREEN_SHARE_SIGNALING_PROTOCOL_VERSION,
requestId: request.requestId, sequence: nextSequence(session), timestampMs: now(), binding: session.binding,
grant: grant(session, participant, token), nextClientSequence: request.sequence + 1,
participants: peerList(session, participant.id), continuous: false,
} };
}
function signal(request: ScreenShareSignalRequest, subject: string): MediaSignalResult<null> {
const found = authenticate(request.credential, subject);
if (!found.ok) return found;
const { session, participant } = found.value;
const valid = validateRequest(session, participant, request.binding, request.sequence);
if (!valid.ok) return valid;
if (now() - participant.rateStartedAt >= rateWindowMs) { participant.rateStartedAt = now(); participant.rateCount = 0; }
if (++participant.rateCount > maximumSignals) return { ok: false, code: "rate-limited", message: "Media signaling rate exceeded." };
const target = session.participants.get(request.targetParticipantId);
if (!target || target.role === participant.role) return { ok: false, code: "unauthorized", message: "Media signaling target is not permitted." };
if (request.signal.kind === "sdp") {
const permitted = participant.role === "presenter" ? request.signal.descriptionType === "offer" : request.signal.descriptionType === "answer";
if (!permitted) return { ok: false, code: "unauthorized", message: "Media SDP role is not permitted." };
}
const relay: ScreenShareSignalRelay = {
type: "screen-share-signal-relay", protocolVersion: SCREEN_SHARE_SIGNALING_PROTOCOL_VERSION,
sequence: nextSequence(session), timestampMs: now(), sessionId: session.id, binding: session.binding,
fromParticipantId: participant.id, targetParticipantId: target.id, signal: request.signal,
};
emit(target, relay);
return { ok: true, value: null };
}
function leave(request: ScreenShareStopRequest | ScreenShareRevokeRequest, subject: string, admin: boolean): MediaSignalResult<null> {
const found = authenticate(request.credential, subject);
if (!found.ok) return found;
const { session, participant } = found.value;
const valid = validateRequest(session, participant, request.binding, request.sequence);
if (!valid.ok) return valid;
if (request.type === "screen-share-stop-request") {
if (participant.role !== "presenter") return { ok: false, code: "unauthorized", message: "Only a presenter may stop a share." };
destroySession(session, stopNotice(session, request.reason));
return { ok: true, value: null };
}
if (request.reason === "moderator-action" && !admin) return { ok: false, code: "unauthorized", message: "Moderator action requires an administrator." };
if (request.scope === "session") {
if (participant.role !== "presenter" && !admin) return { ok: false, code: "unauthorized", message: "Only a presenter or administrator may revoke a session." };
destroySession(session, {
type: "screen-share-revoked", protocolVersion: SCREEN_SHARE_SIGNALING_PROTOCOL_VERSION,
sequence: nextSequence(session), timestampMs: now(), sessionId: session.id, binding: session.binding,
scope: "session", targetParticipantId: null, reason: request.reason, reconnectAllowed: false,
});
return { ok: true, value: null };
}
const targetId = request.targetParticipantId ?? participant.id;
const target = session.participants.get(targetId);
if (!target) return { ok: false, code: "invalid", message: "Media participant is unavailable." };
if (target.id !== participant.id && participant.role !== "presenter" && !admin) {
return { ok: false, code: "unauthorized", message: "Viewer may only leave its own session." };
}
if (target.role === "presenter") return { ok: false, code: "unauthorized", message: "Presenter must stop the session." };
removeParticipant(session, target, request.reason);
return { ok: true, value: null };
}
function subscribe(raw: ScreenShareCredential, subject: string, listener: (message: ScreenSharePeerMessage) => void): MediaSignalResult<() => void> {
const found = authenticate(raw, subject);
if (!found.ok) return found;
const participant = found.value.participant;
for (const message of participant.queue.splice(0)) listener(message);
participant.listeners.add(listener);
return { ok: true, value: () => participant.listeners.delete(listener) };
}
function revalidate(raw: ScreenShareCredential, subject: string): MediaSignalResult<null> {
const found = authenticate(raw, subject);
return found.ok ? { ok: true, value: null } : found;
}
function revokeSubject(subject: string): number {
let removed = 0;
for (const session of [...sessions.values()]) {
const matching = [...session.participants.values()].filter((participant) => participant.subject === subject);
const presenter = matching.find((participant) => participant.role === "presenter");
if (presenter) {
destroySession(session, stopNotice(session, "server-shutdown"));
removed += 1;
continue;
}
for (const participant of matching) {
removeParticipant(session, participant, "authorization-revoked");
removed += 1;
}
}
return removed;
}
function cleanup(): number {
let removed = 0;
for (const session of [...sessions.values()]) {
const expired = [...session.participants.values()].filter((participant) => now() >= participant.expiresAt);
const presenter = expired.find((participant) => participant.role === "presenter");
if (presenter) {
destroySession(session, stopNotice(session, "grant-expired"));
removed += 1;
continue;
}
for (const participant of expired) {
removeParticipant(session, participant, "authorization-revoked");
removed += 1;
}
}
return removed;
}
return { join, resume, signal, leave, subscribe, revalidate, revokeSubject, cleanup, sessionCount: () => sessions.size };
}
function opaque(bytes: number): string { return randomBytes(bytes).toString("base64url"); }
function hash(token: string): Buffer { return createHash("sha256").update(token).digest(); }
function tokenMatches(expected: Buffer, token: string): boolean {
if (typeof token !== "string" || token.length < 16 || token.length > 512) return false;
const actual = hash(token); return actual.length === expected.length && timingSafeEqual(actual, expected);
}
function positive(value: number | undefined, fallback: number): number {
return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : fallback;
}
function count(value: number | undefined, fallback: number): number { return Math.max(1, Math.floor(positive(value, fallback))); }
function copyBinding(binding: ScreenShareBinding): ScreenShareBinding { return { ...binding }; }
function sameBinding(a: ScreenShareBinding, b: ScreenShareBinding): boolean {
return a.officeId === b.officeId && a.levelId === b.levelId && a.roomId === b.roomId && a.screenId === b.screenId;
}