1
0

feat: add private profile media and realtime contracts

This commit is contained in:
2026-08-11 19:21:49 -07:00
parent a2a52bfdae
commit 1c37ef8f3e
22 changed files with 2321 additions and 8 deletions
+13
View File
@@ -0,0 +1,13 @@
export * from "./types.ts";
export {
isMediaSurface,
parseMediaSource,
parseMediaSurface,
parseMediaViewerContext,
} from "./validation.ts";
export { authorizeMediaSurface, transitionMediaSurface } from "./policy.ts";
export {
createMediaSurfaceLifecycle,
type MediaSurfaceBinding,
type MediaSurfaceLifecycle,
} from "./lifecycle.ts";
+62
View File
@@ -0,0 +1,62 @@
import type { VideoTexture } from "three";
import type { MediaAuthorizationDecision } from "./types.ts";
export interface MediaSurfaceBinding {
/** All objects are caller-owned. This adapter never obtains or fetches them. */
video: HTMLVideoElement;
texture: VideoTexture;
track?: MediaStreamTrack;
}
export interface MediaSurfaceLifecycle {
bind(binding: MediaSurfaceBinding): void;
apply(decision: MediaAuthorizationDecision): void;
texture(): VideoTexture | null;
optedIn(): boolean;
dispose(): void;
}
/**
* Gate an already-created video texture behind a server decision and explicit
* viewer consent. `play`, `getUserMedia`, `fetch`, `track.stop`, and
* `texture.dispose` are intentionally absent: acquisition and ownership remain
* with the caller.
*/
export function createMediaSurfaceLifecycle(): MediaSurfaceLifecycle {
let binding: MediaSurfaceBinding | null = null;
let allowed = false;
let consent = false;
let disposed = false;
function silence(video: HTMLVideoElement): void {
video.autoplay = false;
video.muted = true;
video.pause();
}
return {
bind(next) {
if (disposed) throw new Error("media surface lifecycle is disposed");
if (binding) silence(binding.video);
binding = next;
silence(next.video);
},
apply(decision) {
if (disposed) return;
allowed = decision.canView && decision.surface.source !== null;
consent = decision.optedIn;
if (!allowed && binding) silence(binding.video);
},
texture: () => (!disposed && allowed && consent ? binding?.texture ?? null : null),
optedIn: () => consent,
dispose() {
if (disposed) return;
disposed = true;
if (binding) silence(binding.video);
binding = null;
allowed = false;
consent = false;
},
};
}
+127
View File
@@ -0,0 +1,127 @@
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 },
});
}
+84
View File
@@ -0,0 +1,84 @@
/** JSON-only contracts for office screen surfaces. No browser or renderer types. */
export type MediaAudience = "public" | "authenticated" | "office-members" | "allowlist";
export type MediaSourcePrivacy = "public" | "private";
export type MediaSourceKind = "live-stream" | "recording";
export type MediaSurfaceStatus = "idle" | "presenting" | "reconnecting" | "stopped" | "revoked";
export interface MediaSurfaceAcl {
audience: MediaAudience;
viewerIds: string[];
presenterIds: string[];
moderatorIds: string[];
}
export interface MediaSurfaceSource {
kind: MediaSourceKind;
/** Opaque server-issued locator. Never expose it before authorization and opt-in. */
locator: string;
privacy: MediaSourcePrivacy;
hasAudio: boolean;
/** Deliberately literal invariants: a surface never asks a browser to autoplay. */
autoplay: false;
muted: true;
}
export interface MediaSurfaceState {
status: MediaSurfaceStatus;
presenterId: string | null;
revision: number;
}
export interface MediaSurface {
screenId: string;
officeId: string;
levelId: string;
roomId: string | null;
acl: MediaSurfaceAcl;
source: MediaSurfaceSource | null;
state: MediaSurfaceState;
}
export interface MediaViewerContext {
/** Trusted server identity. `null` is the anonymous public viewer. */
viewerId: string | null;
/** Trusted memberships, not values supplied by the browser. */
officeIds: string[];
/** Viewing never starts merely because authorization succeeded. */
optedIn: boolean;
}
export type MediaAuthorizationReason =
| "ready"
| "not_presenting"
| "authentication_required"
| "office_membership_required"
| "not_allowlisted"
| "opt_in_required";
/** Safe response shape. `source` is null unless both policy and opt-in passed. */
export interface MediaSurfaceView {
screenId: string;
officeId: string;
levelId: string;
roomId: string | null;
status: MediaSurfaceStatus;
source: MediaSurfaceSource | null;
playback: { autoplay: false; muted: true };
}
export interface MediaAuthorizationDecision {
authorized: boolean;
optedIn: boolean;
canView: boolean;
reason: MediaAuthorizationReason;
surface: MediaSurfaceView;
}
export type MediaSurfaceEvent =
| { type: "present"; presenterId: string; source: MediaSurfaceSource }
| { type: "stop"; presenterId: string }
| { type: "revoke"; actorId: string }
| { type: "disconnect"; presenterId: string }
| { type: "reconnect"; presenterId: string; source?: MediaSurfaceSource };
+144
View File
@@ -0,0 +1,144 @@
import type {
MediaSurface,
MediaSurfaceAcl,
MediaSurfaceSource,
MediaSurfaceState,
MediaViewerContext,
} from "./types.ts";
const ID = /^[a-zA-Z0-9][a-zA-Z0-9._:-]{0,127}$/;
const AUDIENCES = new Set(["public", "authenticated", "office-members", "allowlist"]);
const SOURCE_KINDS = new Set(["live-stream", "recording"]);
const PRIVACY = new Set(["public", "private"]);
const STATUSES = new Set(["idle", "presenting", "reconnecting", "stopped", "revoked"]);
export function parseMediaSurface(value: unknown): MediaSurface {
const surface = record(value, "surface", [
"screenId", "officeId", "levelId", "roomId", "acl", "source", "state",
]);
const parsed: MediaSurface = {
screenId: id(surface.screenId, "screenId"),
officeId: id(surface.officeId, "officeId"),
levelId: id(surface.levelId, "levelId"),
roomId: surface.roomId === null ? null : id(surface.roomId, "roomId"),
acl: parseAcl(surface.acl),
source: surface.source === null ? null : parseSource(surface.source),
state: parseState(surface.state),
};
const live = parsed.state.status === "presenting" || parsed.state.status === "reconnecting";
if (live !== (parsed.source !== null)) {
throw new TypeError("presenting/reconnecting surfaces require a source; all other states forbid one");
}
if (parsed.source?.privacy === "private" && parsed.acl.audience === "public") {
throw new TypeError("a private source cannot have a public audience");
}
if (live !== (parsed.state.presenterId !== null)) {
throw new TypeError("presenting/reconnecting surfaces require a presenter; all other states forbid one");
}
if (parsed.state.presenterId && !parsed.acl.presenterIds.includes(parsed.state.presenterId)) {
throw new TypeError("current presenter is not permitted by acl.presenterIds");
}
return parsed;
}
export function isMediaSurface(value: unknown): value is MediaSurface {
try {
parseMediaSurface(value);
return true;
} catch {
return false;
}
}
export function parseMediaSource(value: unknown): MediaSurfaceSource {
return parseSource(value);
}
/** Validate the trusted context assembled by a server/session boundary. */
export function parseMediaViewerContext(value: unknown): MediaViewerContext {
const viewer = record(value, "viewer", ["viewerId", "officeIds", "optedIn"]);
if (viewer.viewerId !== null && typeof viewer.viewerId !== "string") {
throw new TypeError("viewer.viewerId is invalid");
}
if (typeof viewer.optedIn !== "boolean") throw new TypeError("viewer.optedIn must be boolean");
return {
viewerId: viewer.viewerId === null ? null : id(viewer.viewerId, "viewer.viewerId"),
officeIds: ids(viewer.officeIds, "viewer.officeIds"),
optedIn: viewer.optedIn,
};
}
function parseAcl(value: unknown): MediaSurfaceAcl {
const acl = record(value, "acl", ["audience", "viewerIds", "presenterIds", "moderatorIds"]);
if (typeof acl.audience !== "string" || !AUDIENCES.has(acl.audience)) {
throw new TypeError("acl.audience is invalid");
}
const parsed: MediaSurfaceAcl = {
audience: acl.audience as MediaSurfaceAcl["audience"],
viewerIds: ids(acl.viewerIds, "acl.viewerIds"),
presenterIds: ids(acl.presenterIds, "acl.presenterIds"),
moderatorIds: ids(acl.moderatorIds, "acl.moderatorIds"),
};
if (parsed.audience === "allowlist" && parsed.viewerIds.length === 0) {
throw new TypeError("an allowlist audience requires acl.viewerIds");
}
return parsed;
}
function parseSource(value: unknown): MediaSurfaceSource {
const source = record(value, "source", ["kind", "locator", "privacy", "hasAudio", "autoplay", "muted"]);
if (typeof source.kind !== "string" || !SOURCE_KINDS.has(source.kind)) throw new TypeError("source.kind is invalid");
if (typeof source.privacy !== "string" || !PRIVACY.has(source.privacy)) throw new TypeError("source.privacy is invalid");
if (typeof source.locator !== "string" || source.locator.length < 1 || source.locator.length > 2048) {
throw new TypeError("source.locator must be a non-empty string of at most 2048 characters");
}
if (typeof source.hasAudio !== "boolean") throw new TypeError("source.hasAudio must be boolean");
if (source.autoplay !== false) throw new TypeError("media sources must set autoplay to false");
if (source.muted !== true) throw new TypeError("media sources must start muted");
return {
kind: source.kind as MediaSurfaceSource["kind"],
locator: source.locator,
privacy: source.privacy as MediaSurfaceSource["privacy"],
hasAudio: source.hasAudio,
autoplay: false,
muted: true,
};
}
function parseState(value: unknown): MediaSurfaceState {
const state = record(value, "state", ["status", "presenterId", "revision"]);
if (typeof state.status !== "string" || !STATUSES.has(state.status)) throw new TypeError("state.status is invalid");
if (state.presenterId !== null && typeof state.presenterId !== "string") throw new TypeError("state.presenterId is invalid");
if (!Number.isSafeInteger(state.revision) || (state.revision as number) < 0) {
throw new TypeError("state.revision must be a non-negative safe integer");
}
return {
status: state.status as MediaSurfaceState["status"],
presenterId: state.presenterId === null ? null : id(state.presenterId, "state.presenterId"),
revision: state.revision as number,
};
}
function record(value: unknown, name: string, keys: readonly string[]): Record<string, unknown> {
if (typeof value !== "object" || value === null || Array.isArray(value) || Object.getPrototypeOf(value) !== Object.prototype) {
throw new TypeError(`${name} must be a plain JSON object`);
}
const result = value as Record<string, unknown>;
const actual = Object.keys(result);
if (actual.length !== keys.length || actual.some((key) => !keys.includes(key))) {
throw new TypeError(`${name} must contain exactly: ${keys.join(", ")}`);
}
return result;
}
function id(value: unknown, name: string): string {
if (typeof value !== "string" || !ID.test(value)) throw new TypeError(`${name} is not a valid id`);
return value;
}
function ids(value: unknown, name: string): string[] {
if (!Array.isArray(value) || value.length > 1024) throw new TypeError(`${name} must be an array`);
const result = value.map((item, index) => id(item, `${name}[${index}]`));
if (new Set(result).size !== result.length) throw new TypeError(`${name} contains duplicates`);
return result;
}