1
0
This repository has been archived on 2026-08-25. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
tera/src/media/policy.ts
T

128 lines
4.9 KiB
TypeScript

import type {
MediaAuthorizationDecision,
MediaAuthorizationReason,
MediaSurface,
MediaSurfaceEvent,
MediaSurfaceSource,
MediaViewerContext,
} from "./types.ts";
import { parseMediaSource, parseMediaSurface, parseMediaViewerContext } from "./validation.ts";
/**
* Produce the response a server may return to this viewer. The source locator
* is copied only after authorization, presentation state, and explicit opt-in.
*/
export function authorizeMediaSurface(
input: MediaSurface,
viewer: MediaViewerContext,
): MediaAuthorizationDecision {
const surface = parseMediaSurface(input);
const context = parseMediaViewerContext(viewer);
const access = audienceDecision(surface, context);
const presenting = surface.state.status === "presenting" && surface.source !== null;
let reason: MediaAuthorizationReason = access.reason;
if (access.authorized && !presenting) reason = "not_presenting";
else if (access.authorized && presenting && !context.optedIn) reason = "opt_in_required";
else if (access.authorized && presenting && context.optedIn) reason = "ready";
const canView = reason === "ready";
return {
authorized: access.authorized,
optedIn: context.optedIn,
canView,
reason,
surface: {
screenId: surface.screenId,
officeId: surface.officeId,
levelId: surface.levelId,
roomId: surface.roomId,
status: surface.state.status,
source: canView && surface.source ? { ...surface.source } : null,
playback: { autoplay: false, muted: true },
},
};
}
/** Strict state machine for presenter stop/revoke/disconnect/reconnect flows. */
export function transitionMediaSurface(input: MediaSurface, event: MediaSurfaceEvent): MediaSurface {
const surface = parseMediaSurface(input);
switch (event.type) {
case "present": {
requirePresenter(surface, event.presenterId);
const source = parseMediaSource(event.source);
if (source.privacy === "private" && surface.acl.audience === "public") {
throw new Error("a private source cannot be presented to a public audience");
}
return next(surface, "presenting", event.presenterId, source);
}
case "stop":
requireCurrent(surface, event.presenterId);
return next(surface, "stopped", null, null);
case "revoke":
if (event.actorId !== surface.state.presenterId && !surface.acl.moderatorIds.includes(event.actorId)) {
throw new Error("only the current presenter or a moderator may revoke a surface");
}
return next(surface, "revoked", null, null);
case "disconnect":
requireCurrent(surface, event.presenterId);
if (surface.state.status !== "presenting") throw new Error("only a presenting surface can disconnect");
return next(surface, "reconnecting", event.presenterId, surface.source);
case "reconnect":
requireCurrent(surface, event.presenterId);
if (surface.state.status !== "reconnecting") throw new Error("only a reconnecting surface can reconnect");
return next(
surface,
"presenting",
event.presenterId,
event.source === undefined ? surface.source : parseMediaSource(event.source),
);
}
}
function audienceDecision(
surface: MediaSurface,
viewer: MediaViewerContext,
): { authorized: boolean; reason: MediaAuthorizationReason } {
const id = viewer.viewerId;
switch (surface.acl.audience) {
case "public":
return { authorized: true, reason: "ready" };
case "authenticated":
return id ? { authorized: true, reason: "ready" } : { authorized: false, reason: "authentication_required" };
case "office-members":
return id && viewer.officeIds.includes(surface.officeId)
? { authorized: true, reason: "ready" }
: { authorized: false, reason: id ? "office_membership_required" : "authentication_required" };
case "allowlist":
return id && surface.acl.viewerIds.includes(id)
? { authorized: true, reason: "ready" }
: { authorized: false, reason: id ? "not_allowlisted" : "authentication_required" };
}
}
function requirePresenter(surface: MediaSurface, presenterId: string): void {
if (!surface.acl.presenterIds.includes(presenterId)) throw new Error("presenter is not permitted by this surface");
}
function requireCurrent(surface: MediaSurface, presenterId: string): void {
if (surface.state.presenterId !== presenterId) throw new Error("caller is not the current presenter");
}
function next(
surface: MediaSurface,
status: MediaSurface["state"]["status"],
presenterId: string | null,
source: MediaSurfaceSource | null,
): MediaSurface {
return parseMediaSurface({
...surface,
acl: {
...surface.acl,
viewerIds: [...surface.acl.viewerIds],
presenterIds: [...surface.acl.presenterIds],
moderatorIds: [...surface.acl.moderatorIds],
},
source: source ? { ...source } : null,
state: { status, presenterId, revision: surface.state.revision + 1 },
});
}