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
+2
View File
@@ -18,6 +18,7 @@ import { loadConfig, type Config } from "./config.ts";
import { registerFlights } from "./routes/flights.ts";
import { registerHealth } from "./routes/health.ts";
import { registerMarkers } from "./routes/markers.ts";
import { registerMedia } from "./routes/media.ts";
import { registerOffices } from "./routes/offices.ts";
import { registerPresence } from "./routes/presence.ts";
import { registerRealtime } from "./routes/realtime.ts";
@@ -50,6 +51,7 @@ export function buildApp(config: Config = loadConfig()): FastifyInstance {
registerSatellites(app, services);
registerWeather(app, services);
registerMarkers(app, services);
registerMedia(app, services);
registerOffices(app, services);
registerPresence(app, services);
registerRealtime(app, services);
+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;
}
+145
View File
@@ -0,0 +1,145 @@
/** Authenticated, POST-body-only office WebRTC signaling routes. */
import type { FastifyInstance, FastifyReply } from "fastify";
import { parseScreenShareClientMessage } from "../../../src/media/signalingValidation.ts";
import type {
ScreenShareBinding,
ScreenShareCreateRequest,
ScreenShareJoinRequest,
ScreenShareResumeRequest,
ScreenShareRevokeRequest,
ScreenShareSignalRequest,
ScreenShareStopRequest,
} from "../../../src/media/signalingTypes.ts";
import { bundledMediaOffice, officeHasMediaBinding, type MediaSignalFailureCode } from "../media/index.ts";
import type { Services } from "../services.ts";
const BODY_LIMIT = 32 * 1024;
const REVALIDATE_MS = 15_000;
const UNAUTHORIZED = { error: "unauthorized", message: "Media signaling requires a signed-in member." };
function status(code: MediaSignalFailureCode): number {
if (code === "unauthorized" || code === "expired") return 401;
if (code === "conflict") return 409;
if (code === "capacity" || code === "rate-limited") return 429;
return 400;
}
function fail(reply: FastifyReply, failure: { code: MediaSignalFailureCode; message: string }) {
const code = status(failure.code);
if (code === 401) reply.header("www-authenticate", "Bearer");
return reply.code(code).send({ error: failure.code, message: failure.message });
}
async function bindingAllowed(binding: ScreenShareBinding, services: Services): Promise<boolean> {
const doc = await services.offices.get(binding.officeId);
const office = doc?.floor ?? bundledMediaOffice(binding.officeId);
return office !== null && officeHasMediaBinding(office, binding);
}
function parse(body: unknown) {
const parsed = parseScreenShareClientMessage(body);
return parsed.ok ? parsed.value : null;
}
export function registerMedia(app: FastifyInstance, services: Services): void {
app.post<{ Body: unknown }>("/api/v1/media/join", { bodyLimit: BODY_LIMIT }, async (req, reply) => {
const viewer = await services.auth.resolve(req);
if (!viewer.authenticated || viewer.subject === null) {
return reply.code(401).header("www-authenticate", "Bearer").send(UNAUTHORIZED);
}
const message = parse(req.body);
if (!message || (message.type !== "screen-share-create-request" && message.type !== "screen-share-join-request")) {
return reply.code(400).send({ error: "invalid", message: "Media join request is invalid." });
}
// Authentication precedes lookup so anonymous callers cannot enumerate private packs.
if (!(await bindingAllowed(message.binding, services))) {
return reply.code(404).send({ error: "not_found", message: "No such authored office screen." });
}
const joined = services.media.join(message as ScreenShareCreateRequest | ScreenShareJoinRequest, viewer.subject);
if (!joined.ok) return fail(reply, joined);
return reply.code(201).send(joined.value);
});
app.post<{ Body: unknown }>("/api/v1/media/signal", { bodyLimit: BODY_LIMIT }, async (req, reply) => {
const viewer = await services.auth.resolve(req);
if (!viewer.authenticated || viewer.subject === null) {
return reply.code(401).header("www-authenticate", "Bearer").send(UNAUTHORIZED);
}
const message = parse(req.body);
if (!message || message.type !== "screen-share-signal-request") {
return reply.code(400).send({ error: "invalid", message: "Media signal request is invalid." });
}
if (!(await bindingAllowed(message.binding, services))) {
return reply.code(404).send({ error: "not_found", message: "No such authored office screen." });
}
const signaled = services.media.signal(message as ScreenShareSignalRequest, viewer.subject);
if (!signaled.ok) return fail(reply, signaled);
return reply.code(204).send();
});
app.post<{ Body: unknown }>("/api/v1/media/events", { bodyLimit: BODY_LIMIT }, async (req, reply) => {
const viewer = await services.auth.resolve(req);
if (!viewer.authenticated || viewer.subject === null) {
return reply.code(401).header("www-authenticate", "Bearer").send(UNAUTHORIZED);
}
const subject = viewer.subject;
const message = parse(req.body);
if (!message || message.type !== "screen-share-resume-request") {
return reply.code(400).send({ error: "invalid", message: "Media resume request is invalid." });
}
if (!(await bindingAllowed(message.binding, services))) {
return reply.code(404).send({ error: "not_found", message: "No such authored office screen." });
}
const resumed = services.media.resume(message as ScreenShareResumeRequest, subject);
if (!resumed.ok) return fail(reply, resumed);
reply.hijack();
reply.raw.writeHead(200, {
"cache-control": "private, no-store",
connection: "keep-alive",
"content-type": "text/event-stream; charset=utf-8",
"x-accel-buffering": "no",
});
const write = (value: unknown) => reply.raw.write(`data: ${JSON.stringify(value)}\n\n`);
write(resumed.value);
const activeCredential = resumed.value.grant.credential;
const subscribed = services.media.subscribe(activeCredential, subject, write);
if (!subscribed.ok) { reply.raw.end(); return; }
const interval = setInterval(() => {
// Re-resolve the caller, rather than treating the identity result from
// stream-open as permanent. This bounds issuer revocation and JWT expiry
// even while WebRTC itself continues peer-to-peer.
void services.auth.resolve(req).then((current) => {
if (!current.authenticated || current.subject !== subject) {
reply.raw.end();
return;
}
const valid = services.media.revalidate(activeCredential, subject);
if (!valid.ok) reply.raw.end();
else reply.raw.write(": keepalive\n\n");
}).catch(() => reply.raw.end());
}, REVALIDATE_MS);
interval.unref();
// The POST request body closes as soon as it is parsed; the response owns
// the fetch-streamed SSE lifetime.
reply.raw.once("close", () => { clearInterval(interval); subscribed.value(); });
});
app.post<{ Body: unknown }>("/api/v1/media/leave", { bodyLimit: BODY_LIMIT }, async (req, reply) => {
const viewer = await services.auth.resolve(req);
if (!viewer.authenticated || viewer.subject === null) {
return reply.code(401).header("www-authenticate", "Bearer").send(UNAUTHORIZED);
}
const message = parse(req.body);
if (!message || (message.type !== "screen-share-stop-request" && message.type !== "screen-share-revoke-request")) {
return reply.code(400).send({ error: "invalid", message: "Media leave request is invalid." });
}
if (!(await bindingAllowed(message.binding, services))) {
return reply.code(404).send({ error: "not_found", message: "No such authored office screen." });
}
const left = services.media.leave(message as ScreenShareStopRequest | ScreenShareRevokeRequest, viewer.subject, viewer.admin);
if (!left.ok) return fail(reply, left);
return reply.code(204).send();
});
}
+3
View File
@@ -10,6 +10,7 @@
import { createAuth, type AuthService } from "./auth/index.ts";
import { createFlightsService, type FlightsService } from "./flights/index.ts";
import { createMarkerStore, type MarkerStore } from "./markers/store.ts";
import { createMediaSignalService, type MediaSignalService } from "./media/index.ts";
import { createOfficeStore, type OfficeStore } from "./offices/store.ts";
import { createPresenceStore, type PresenceStore } from "./presence/store.ts";
import { createRealtimeService, type RealtimeService } from "./realtime/index.ts";
@@ -23,6 +24,7 @@ export interface Services {
flights: FlightsService;
satellites: SatellitesService;
markers: MarkerStore;
media: MediaSignalService;
offices: OfficeStore;
presence: PresenceStore;
realtime: RealtimeService;
@@ -43,6 +45,7 @@ export function createServices(config: Config, log: ServiceLog): Services {
flights: createFlightsService(config, log),
satellites: createSatellitesService(config, log),
markers: createMarkerStore(config, log),
media: createMediaSignalService(),
offices: createOfficeStore(config.offices.dir),
presence: createPresenceStore(config.presence.dir),
realtime: createRealtimeService(),
+166
View File
@@ -0,0 +1,166 @@
import assert from "node:assert/strict";
import { createHmac } from "node:crypto";
import { after, describe, it } from "node:test";
import { buildApp } from "../app.ts";
import { loadConfig } from "../config.ts";
import { createMediaSignalService, officeHasMediaBinding } from "../media/index.ts";
import LUMBRIDGE_HQ from "../../../src/offices/lumbridge-hq.ts";
import type {
ScreenShareBinding,
ScreenShareCreateRequest,
ScreenShareJoinRequest,
ScreenShareRevokeRequest,
ScreenShareSignalRequest,
} from "../../../src/media/signalingTypes.ts";
const BINDING: ScreenShareBinding = {
officeId: "lumbridge-hq", levelId: "level-1", roomId: "lobby", screenId: "lobby-monitor",
};
const create = (sequence = 1): ScreenShareCreateRequest => ({
type: "screen-share-create-request", protocolVersion: 1, requestId: "request-create", sequence,
timestampMs: 1_000, binding: BINDING, role: "presenter",
});
const join = (sequence = 1): ScreenShareJoinRequest => ({
type: "screen-share-join-request", protocolVersion: 1, requestId: "request-join", sequence,
timestampMs: 1_000, binding: BINDING, role: "viewer", viewerOptIn: true,
});
function grants(service: ReturnType<typeof createMediaSignalService>) {
const presenter = service.join(create(), "presenter-subject");
const viewer = service.join(join(), "viewer-subject");
assert.equal(presenter.ok, true); assert.equal(viewer.ok, true);
if (!presenter.ok || !viewer.ok || presenter.value.type !== "screen-share-create-grant" || viewer.value.type !== "screen-share-join-grant") {
throw new Error("grant setup failed");
}
return { presenter: presenter.value, viewer: viewer.value };
}
describe("office media signaling service", () => {
it("issues opaque participants, snapshots peers, and enforces P2P signaling roles", () => {
const service = createMediaSignalService();
const { presenter, viewer } = grants(service);
const p = presenter.grant.credential; const v = viewer.grant.credential;
assert.notEqual(p.sessionId, p.participantId); assert.notEqual(p.participantId, p.grantToken);
assert.deepEqual(presenter.participants, []);
assert.deepEqual(viewer.participants, [{ participantId: p.participantId, role: "presenter" }]);
assert.equal(JSON.stringify(viewer).includes("subject"), false);
const received: string[] = [];
assert.equal(service.subscribe(v, "viewer-subject", (message) => received.push(message.type)).ok, true);
const offer: ScreenShareSignalRequest = {
type: "screen-share-signal-request", protocolVersion: 1, sequence: 2, timestampMs: 1_001,
binding: BINDING, credential: p, targetParticipantId: v.participantId,
signal: { kind: "sdp", descriptionType: "offer", sdp: "v=0" },
};
assert.equal(service.signal(offer, "presenter-subject").ok, true);
assert.equal(received.includes("screen-share-signal-relay"), true);
assert.equal(service.signal({ ...offer, sequence: 3, credential: v, targetParticipantId: p.participantId }, "viewer-subject").ok, false);
assert.equal(service.signal({ ...offer, sequence: 4, credential: p }, "wrong-subject").ok, false);
});
it("rotates short-lived hashed grants and rejects stale capabilities", () => {
let now = 10_000;
const service = createMediaSignalService({ now: () => now, grantTtlMs: 100 });
const { presenter } = grants(service);
const old = presenter.grant.credential;
const resumed = service.resume({
type: "screen-share-resume-request", protocolVersion: 1, requestId: "request-resume", sequence: 2,
timestampMs: now, binding: BINDING, credential: old, lastReceivedSequence: 0,
}, "presenter-subject");
assert.equal(resumed.ok, true);
if (!resumed.ok) return;
assert.notEqual(resumed.value.grant.credential.grantToken, old.grantToken);
assert.equal(service.revalidate(old, "presenter-subject").ok, false);
now += 101;
assert.equal(service.revalidate(resumed.value.grant.credential, "presenter-subject").ok, false);
assert.equal(service.sessionCount(), 0);
});
it("bounds the mesh and sends viewer lifecycle snapshots", () => {
const service = createMediaSignalService({ maximumParticipantsPerSession: 2 });
const { presenter, viewer } = grants(service);
assert.equal(service.join(join(2), "third-viewer").ok, false);
const snapshots: number[] = [];
service.subscribe(presenter.grant.credential, "presenter-subject", (message) => {
if (message.type === "screen-share-participants") snapshots.push(message.participants.length);
});
const leave: ScreenShareRevokeRequest = {
type: "screen-share-revoke-request", protocolVersion: 1, requestId: "request-leave", sequence: 2,
timestampMs: 1_002, binding: BINDING, credential: viewer.grant.credential,
scope: "participant", targetParticipantId: viewer.grant.credential.participantId, reason: "viewer-left",
};
assert.equal(service.leave(leave, "viewer-subject", false).ok, true);
assert.equal(snapshots.at(-1), 0);
});
it("validates the exact authored level, room and screen prop", () => {
assert.equal(officeHasMediaBinding(LUMBRIDGE_HQ, BINDING), true);
assert.equal(officeHasMediaBinding(LUMBRIDGE_HQ, { ...BINDING, roomId: "open-floor" }), false);
assert.equal(officeHasMediaBinding(LUMBRIDGE_HQ, { ...BINDING, screenId: "not-authored" }), false);
});
});
const SECRET = "media-route-test-secret-that-is-long-enough";
function bearer(subject: string) {
const encode = (value: unknown) => Buffer.from(JSON.stringify(value)).toString("base64url");
const signed = `${encode({ alg: "HS256", typ: "JWT" })}.${encode({ sub: subject, exp: Math.floor(Date.now() / 1000) + 600 })}`;
return { authorization: `Bearer ${signed}.${createHmac("sha256", SECRET).update(signed).digest("base64url")}` };
}
describe("office media signaling routes", () => {
it("authenticates before exact lookup and keeps capabilities in POST bodies", async () => {
const config = loadConfig({ TERA_AUTH_MODE: "jwt", TERA_AUTH_JWT_SECRET: SECRET }); config.logLevel = "silent";
const app = buildApp(config); after(() => app.close());
const anonymous = await app.inject({ method: "POST", url: "/api/v1/media/join", payload: create() });
assert.equal(anonymous.statusCode, 401);
const locator = await app.inject({ method: "POST", url: "/api/v1/media/join", headers: bearer("presenter"), payload: { ...create(), sourceUrl: "https://private.invalid/stream" } });
assert.equal(locator.statusCode, 400);
const wrongRoom = await app.inject({ method: "POST", url: "/api/v1/media/join", headers: bearer("viewer"), payload: { ...join(), binding: { ...BINDING, roomId: "not-the-room" } } });
assert.equal(wrongRoom.statusCode, 404);
const presenter = await app.inject({ method: "POST", url: "/api/v1/media/join", headers: bearer("presenter"), payload: create() });
const viewer = await app.inject({ method: "POST", url: "/api/v1/media/join", headers: bearer("viewer"), payload: join() });
assert.equal(presenter.statusCode, 201); assert.equal(viewer.statusCode, 201);
assert.equal(presenter.headers["cache-control"], "private, no-store"); assert.equal(presenter.headers.location, undefined);
const p = presenter.json<{ grant: { credential: ScreenShareSignalRequest["credential"] } }>();
const v = viewer.json<{ grant: { credential: ScreenShareSignalRequest["credential"] } }>();
const sent = await app.inject({ method: "POST", url: "/api/v1/media/signal", headers: bearer("presenter"), payload: {
type: "screen-share-signal-request", protocolVersion: 1, sequence: 2, timestampMs: 1_002,
binding: BINDING, credential: p.grant.credential, targetParticipantId: v.grant.credential.participantId,
signal: { kind: "sdp", descriptionType: "offer", sdp: "v=0" },
} });
assert.equal(sent.statusCode, 204);
const stolen = await app.inject({ method: "POST", url: "/api/v1/media/signal", headers: bearer("viewer"), payload: {
type: "screen-share-signal-request", protocolVersion: 1, sequence: 3, timestampMs: 1_003,
binding: BINDING, credential: p.grant.credential, targetParticipantId: v.grant.credential.participantId,
signal: { kind: "ice-complete" },
} });
assert.equal(stolen.statusCode, 401);
});
it("opens SSE with a POST-body resume grant and rotates the capability", async () => {
const config = loadConfig({ TERA_AUTH_MODE: "jwt", TERA_AUTH_JWT_SECRET: SECRET }); config.logLevel = "silent";
const app = buildApp(config); after(() => app.close());
const origin = await app.listen({ host: "127.0.0.1", port: 0 });
const created = await fetch(`${origin}/api/v1/media/join`, {
method: "POST", headers: { ...bearer("presenter"), "content-type": "application/json" }, body: JSON.stringify(create()),
});
const body = await created.json() as { grant: { credential: ScreenShareSignalRequest["credential"] } };
const controller = new AbortController();
const streamed = await fetch(`${origin}/api/v1/media/events`, {
method: "POST", signal: controller.signal,
headers: { ...bearer("presenter"), "content-type": "application/json" },
body: JSON.stringify({
type: "screen-share-resume-request", protocolVersion: 1, requestId: "request-events", sequence: 2,
timestampMs: 1_003, binding: BINDING, credential: body.grant.credential, lastReceivedSequence: 0,
}),
});
assert.equal(streamed.status, 200);
assert.match(streamed.headers.get("content-type") ?? "", /^text\/event-stream/);
const first = await streamed.body?.getReader().read();
const text = new TextDecoder().decode(first?.value);
assert.match(text, /screen-share-resume-grant/);
assert.equal(text.includes(body.grant.credential.grantToken), false);
assert.equal(streamed.url.includes("grantToken"), false);
controller.abort();
});
});