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
+11
View File
@@ -23,6 +23,8 @@ export interface ActorProfile {
skinTone?: string;
primaryColor?: string;
accentColor?: string;
hairColor?: string;
bodyShape?: "slim" | "average" | "broad";
};
}
@@ -202,6 +204,8 @@ function copyProfile(profile: ActorProfile): ActorProfile {
...(profile.appearance.skinTone === undefined ? {} : { skinTone: profile.appearance.skinTone }),
...(profile.appearance.primaryColor === undefined ? {} : { primaryColor: profile.appearance.primaryColor }),
...(profile.appearance.accentColor === undefined ? {} : { accentColor: profile.appearance.accentColor }),
...(profile.appearance.hairColor === undefined ? {} : { hairColor: profile.appearance.hairColor }),
...(profile.appearance.bodyShape === undefined ? {} : { bodyShape: profile.appearance.bodyShape }),
}
: undefined;
return {
@@ -227,10 +231,17 @@ function checkedIdentity(identity: ActorIdentity): Readonly<ActorIdentity> {
profile.appearance?.skinTone,
profile.appearance?.primaryColor,
profile.appearance?.accentColor,
profile.appearance?.hairColor,
];
if (strings.some((item) => item !== undefined && typeof item !== "string")) {
throw new RangeError("actor profile fields must be strings");
}
if (
profile.appearance?.bodyShape !== undefined &&
profile.appearance.bodyShape !== "slim" &&
profile.appearance.bodyShape !== "average" &&
profile.appearance.bodyShape !== "broad"
) throw new RangeError("actor body shape is invalid");
const copy: ActorIdentity = {
id: identity.id,
displayName: identity.displayName,
+2
View File
@@ -124,6 +124,8 @@ function buildActor(kind: ActorKind, identity: Readonly<ActorIdentity>): RiggedA
...(color(appearance?.skinTone) === undefined ? {} : { skinTone: color(appearance?.skinTone) }),
...(color(appearance?.primaryColor) === undefined ? {} : { outfitColor: color(appearance?.primaryColor) }),
...(color(appearance?.accentColor) === undefined ? {} : { accentColor: color(appearance?.accentColor) }),
...(color(appearance?.hairColor) === undefined ? {} : { hairColor: color(appearance?.hairColor) }),
...(appearance?.bodyShape === undefined ? {} : { bodyShape: appearance.bodyShape }),
}),
};
}
+83 -8
View File
@@ -69,6 +69,14 @@ import {
type JourneyEvent,
type JourneyState,
} from "./journey/index.ts";
import {
actorKindForPresence,
createDefaultLocalProfile,
loadLocalProfile,
resolveHumanoidAppearance,
saveLocalProfile,
type LocalProfile,
} from "./profile/index.ts";
/**
* Three type-only imports and not one value among them, which is what keeps the
* office and the instruments out of the entry chunk.
@@ -118,6 +126,12 @@ let journey: JourneyState = restoredJourney.status === "loaded"
profile: { displayName: "Guest" },
},
});
const LOCAL_PROFILE_KEY = "tera:profile:v2";
let localProfile: LocalProfile | null = null;
function humanoidAppearance() {
return localProfile ? resolveHumanoidAppearance(localProfile.appearance) : null;
}
function dispatchJourney(event: JourneyEvent): boolean {
const next = journeyReducer(journey, event);
@@ -801,7 +815,7 @@ async function mountCity(id: string) {
const handle = await createScene(stage, {
city: entry.city,
actor: {
kind: access.subject === null ? "crow" : "humanoid",
kind: actorKindForPresence(access.subject !== null, "outdoors"),
identity: {
id: access.subject ?? "anonymous",
displayName: access.subject ?? "Guest",
@@ -809,11 +823,22 @@ async function mountCity(id: string) {
profile: {
appearance: access.subject === null
? { primaryColor: "#11151a", accentColor: "#f2b134" }
: { primaryColor: "#151a20", accentColor: "#f2b134" },
: (() => {
const appearance = humanoidAppearance();
return appearance
? {
skinTone: appearance.skinTone,
primaryColor: appearance.outfitColor,
accentColor: appearance.accentColor,
hairColor: appearance.hairColor,
bodyShape: appearance.bodyShape,
}
: { primaryColor: "#151a20", accentColor: "#f2b134" };
})(),
},
},
mode: access.subject === null ? "flight" : "ground",
position: access.subject === null ? { y: 500 } : { y: 0 },
position: access.subject === null ? { y: 1_200 } : { y: 0 },
minFlightAltitude: 20,
maxFlightAltitude: 1_500,
...(access.subject === null
@@ -1010,6 +1035,7 @@ async function mountCity(id: string) {
*/
requestAnimationFrame(function pumpMinimap() {
requestAnimationFrame(pumpMinimap);
syncJourneyVehicle(performance.now());
// Only the mounted one. The other is still constructed and still holds a live
// camera, but its canvas is out of the document, so `clientWidth` is 0, every
// `resize()` puts it back to `ready = false`, and ticking it would be a
@@ -1034,6 +1060,8 @@ requestAnimationFrame(function pumpMinimap() {
* subtraction on the frames it skips.
*/
let livenessCheckedAt = 0;
let journeySyncedAt = 0;
let previousVehicleProgress: number | null = null;
function pollLiveness() {
const now = performance.now();
if (now - livenessCheckedAt < 1000) return;
@@ -1041,6 +1069,32 @@ function pollLiveness() {
renderSource();
}
/** Persist route progress cheaply and turn the hero's wrap into a real scale door. */
function syncJourneyVehicle(now: number): void {
if (!routeDriveIsActive() || !city || journey.route === null || journey.vehicle === null) {
previousVehicleProgress = null;
return;
}
const vehicle = city.vehicleState();
if (!vehicle) return;
const progress = vehicle.progress;
const arrived = journey.route.direction === 1
? previousVehicleProgress !== null && previousVehicleProgress > 0.94 && progress < 0.06
: previousVehicleProgress !== null && previousVehicleProgress < 0.06 && progress > 0.94;
previousVehicleProgress = progress;
if (arrived) {
const endpoint = journey.route.direction === 1 ? "san-francisco" : "los-angeles";
dispatchJourney({ type: "reach-route-endpoint", endpoint });
dispatchJourney({ type: "exit-vehicle" });
switchCity(endpoint === "san-francisco" ? "sf" : "socal");
return;
}
if (now - journeySyncedAt < 500) return;
journeySyncedAt = now;
dispatchJourney({ type: "update-route-progress", progress });
}
// ---- Office ---------------------------------------------------------------
/**
@@ -1108,9 +1162,9 @@ async function enterOffice() {
z: -Math.cos(pack.viewpoints[0].focus.rotation),
}
: { x: 0, z: -1 },
actor: access.subject === null
actor: actorKindForPresence(access.subject !== null, "office") === "dog"
? { kind: "anonymous-dog", coatColor: 0x17191c, collarColor: 0xf2b134 }
: { kind: "humanoid", outfitColor: 0x151a20, accentColor: 0xf2b134 },
: { kind: "humanoid", ...(humanoidAppearance() ?? { outfitColor: 0x151a20, accentColor: 0xf2b134 }) },
},
// Only when there is no sky to put behind it. A sited office computes a
// gradient and a horizon; painting the old flat colour over that is the
@@ -1593,6 +1647,9 @@ function renderLegend() {
renderOfficeBadge();
if (driveControls) driveControls.hidden = !routeDriveIsActive();
if (walkControls) walkControls.hidden = !(walking || exploring);
for (const control of walkControls?.querySelectorAll<HTMLElement>(".flight-only") ?? []) {
control.hidden = inside;
}
if (driveHint) driveHint.hidden = inside || cityId !== "california";
if (walkHint) walkHint.hidden = !(inside || city.actorState());
}
@@ -2091,7 +2148,8 @@ function publishCityActorActions(): boolean {
right: (heldDriveKeys.has("d") ? 1 : 0) - (heldDriveKeys.has("a") ? 1 : 0),
turn: (heldDriveKeys.has("d") ? 1 : 0) - (heldDriveKeys.has("a") ? 1 : 0),
sprint: heldDriveKeys.has(" "),
climb: heldDriveKeys.has(" ") ? 1 : 0,
climb: (heldDriveKeys.has("e") || heldDriveKeys.has(" ") ? 1 : 0) -
(heldDriveKeys.has("q") ? 1 : 0),
});
return true;
}
@@ -2232,7 +2290,10 @@ window.addEventListener("keydown", (event) => {
return;
}
const lower = event.key.toLowerCase();
if (lower === "w" || lower === "a" || lower === "s" || lower === "d" || event.key === " ") {
if (
lower === "w" || lower === "a" || lower === "s" || lower === "d" ||
lower === "q" || lower === "e" || event.key === " "
) {
heldDriveKeys.add(event.key === " " ? " " : lower);
if (publishVehicleActions()) {
event.preventDefault();
@@ -2698,6 +2759,17 @@ async function boot() {
if (bootStep) bootStep.textContent = "Asking the deployment who you are…";
access = await resolveAccess();
if (access.subject !== null) {
const loadedProfile = loadLocalProfile(sessionStorage, LOCAL_PROFILE_KEY);
localProfile = loadedProfile.status === "loaded"
? loadedProfile.profile
: createDefaultLocalProfile(access.subject, access.subject);
if (loadedProfile.status !== "loaded" || loadedProfile.migrated) {
saveLocalProfile(sessionStorage, LOCAL_PROFILE_KEY, localProfile);
}
} else {
localProfile = null;
}
dispatchJourney({
type: "sign-in-actor-swap",
actor: access.subject === null
@@ -2711,7 +2783,10 @@ async function boot() {
id: access.subject,
kind: "humanoid",
signedIn: true,
profile: { displayName: access.subject },
profile: {
displayName: localProfile?.displayName ?? access.subject,
color: humanoidAppearance()?.accentColor,
},
},
});
applyTimeControl();
+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;
}
+51
View File
@@ -0,0 +1,51 @@
export {
ACCENTS,
BODY_SHAPES,
HAIR_COLORS,
LOCAL_PROFILE_VERSION,
OUTFITS,
SKIN_TONES,
actorKindForPresence,
createDefaultLocalProfile,
decodeLocalProfile,
encodeLocalProfile,
isHumanoidAppearance,
isLocalProfile,
migrateLocalProfileV1,
resolveHumanoidAppearance,
type AccentChoice,
type BodyShapeChoice,
type HairColorChoice,
type HumanoidAppearanceChoices,
type LocalProfile,
type LocalProfileV1,
type LocalProfileV2,
type OutfitChoice,
type PresenceActorKind,
type PresenceContext,
type ProfileDecodeResult,
type ResolvedHumanoidAppearance,
type SkinToneChoice,
} from "./model.ts";
export {
clearLocalProfile,
loadLocalProfile,
saveLocalProfile,
type ProfileClearResult,
type ProfileLoadResult,
type ProfileSaveResult,
type ProfileStorageAdapter,
} from "./persistence.ts";
export {
createWebcamFaceConsent,
initialWebcamFaceConsent,
transitionWebcamFaceConsent,
type WebcamFaceConsentController,
type WebcamFaceConsentEvent,
type WebcamFaceConsentRejection,
type WebcamFaceConsentState,
type WebcamFaceConsentStatus,
type WebcamFaceConsentTransition,
} from "./webcamConsent.ts";
+260
View File
@@ -0,0 +1,260 @@
/** Versioned, JSON-only local profile data for an authenticated Tera member. */
export const LOCAL_PROFILE_VERSION = 2 as const;
export const SKIN_TONES = ["espresso", "umber", "sienna", "sand", "peach"] as const;
export const OUTFITS = ["ink", "navy", "slate", "sage", "clay"] as const;
export const ACCENTS = ["aqua", "amber", "coral", "violet", "lime"] as const;
export const HAIR_COLORS = ["black", "espresso", "auburn", "silver", "platinum"] as const;
export const BODY_SHAPES = ["slim", "average", "broad"] as const;
export type SkinToneChoice = (typeof SKIN_TONES)[number];
export type OutfitChoice = (typeof OUTFITS)[number];
export type AccentChoice = (typeof ACCENTS)[number];
export type HairColorChoice = (typeof HAIR_COLORS)[number];
export type BodyShapeChoice = (typeof BODY_SHAPES)[number];
export interface HumanoidAppearanceChoices {
skinTone: SkinToneChoice;
outfit: OutfitChoice;
accent: AccentChoice;
hair: HairColorChoice;
bodyShape: BodyShapeChoice;
}
/**
* This intentionally contains no identity id, auth credential, webcam state,
* face URL, image, device id, or arbitrary metadata bucket.
*/
export interface LocalProfileV2 {
version: typeof LOCAL_PROFILE_VERSION;
displayName: string;
appearance: HumanoidAppearanceChoices;
}
/** The only supported legacy shape: V1 kept appearance choices flat. */
export interface LocalProfileV1 {
version: 1;
displayName: string;
skinTone: SkinToneChoice;
outfit: OutfitChoice;
accent: AccentChoice;
hair: HairColorChoice;
bodyShape: BodyShapeChoice;
}
export type LocalProfile = LocalProfileV2;
export type ProfileDecodeResult =
| { ok: true; profile: LocalProfile; migrated: false }
| { ok: true; profile: LocalProfile; migrated: true; fromVersion: 1 }
| { ok: false; error: string };
export interface ResolvedHumanoidAppearance {
skinTone: string;
outfitColor: string;
accentColor: string;
hairColor: string;
bodyShape: BodyShapeChoice;
}
export type PresenceContext = "outdoors" | "office";
export type PresenceActorKind = "humanoid" | "dog" | "crow";
const SKIN_COLORS: Readonly<Record<SkinToneChoice, string>> = Object.freeze({
espresso: "#4b2d25",
umber: "#704534",
sienna: "#9a6248",
sand: "#c58d68",
peach: "#e6b28e",
});
const OUTFIT_COLORS: Readonly<Record<OutfitChoice, string>> = Object.freeze({
ink: "#18222d",
navy: "#1d3b58",
slate: "#53616d",
sage: "#566c5d",
clay: "#80594a",
});
const ACCENT_COLORS: Readonly<Record<AccentChoice, string>> = Object.freeze({
aqua: "#55b8c8",
amber: "#d99b39",
coral: "#db6d62",
violet: "#826faf",
lime: "#86a94a",
});
const HAIR_VALUES: Readonly<Record<HairColorChoice, string>> = Object.freeze({
black: "#171719",
espresso: "#2d211d",
auburn: "#65372b",
silver: "#8b9298",
platinum: "#d0c5ac",
});
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function exactKeys(value: Record<string, unknown>, expected: readonly string[]): boolean {
const actual = Object.keys(value).sort();
const wanted = [...expected].sort();
return actual.length === wanted.length && actual.every((key, index) => key === wanted[index]);
}
function oneOf<T extends string>(value: unknown, choices: readonly T[]): value is T {
return typeof value === "string" && (choices as readonly string[]).includes(value);
}
function validDisplayName(value: unknown): value is string {
return (
typeof value === "string" &&
value === value.trim() &&
value.length >= 1 &&
value.length <= 64 &&
!/[\u0000-\u001f\u007f]/u.test(value)
);
}
export function isHumanoidAppearance(value: unknown): value is HumanoidAppearanceChoices {
return (
isRecord(value) &&
exactKeys(value, ["skinTone", "outfit", "accent", "hair", "bodyShape"]) &&
oneOf(value.skinTone, SKIN_TONES) &&
oneOf(value.outfit, OUTFITS) &&
oneOf(value.accent, ACCENTS) &&
oneOf(value.hair, HAIR_COLORS) &&
oneOf(value.bodyShape, BODY_SHAPES)
);
}
export function isLocalProfile(value: unknown): value is LocalProfile {
return (
isRecord(value) &&
exactKeys(value, ["version", "displayName", "appearance"]) &&
value.version === LOCAL_PROFILE_VERSION &&
validDisplayName(value.displayName) &&
isHumanoidAppearance(value.appearance)
);
}
function isLocalProfileV1(value: unknown): value is LocalProfileV1 {
return (
isRecord(value) &&
exactKeys(value, ["version", "displayName", "skinTone", "outfit", "accent", "hair", "bodyShape"]) &&
value.version === 1 &&
validDisplayName(value.displayName) &&
oneOf(value.skinTone, SKIN_TONES) &&
oneOf(value.outfit, OUTFITS) &&
oneOf(value.accent, ACCENTS) &&
oneOf(value.hair, HAIR_COLORS) &&
oneOf(value.bodyShape, BODY_SHAPES)
);
}
function clone(profile: LocalProfile): LocalProfile {
return { ...profile, appearance: { ...profile.appearance } };
}
export function migrateLocalProfileV1(profile: LocalProfileV1): LocalProfile {
if (!isLocalProfileV1(profile)) throw new RangeError("profile: invalid V1 profile");
return {
version: LOCAL_PROFILE_VERSION,
displayName: profile.displayName,
appearance: {
skinTone: profile.skinTone,
outfit: profile.outfit,
accent: profile.accent,
hair: profile.hair,
bodyShape: profile.bodyShape,
},
};
}
/** Strictly decode current data or the one documented legacy version. */
export function decodeLocalProfile(payload: string | unknown): ProfileDecodeResult {
let value: unknown = payload;
if (typeof payload === "string") {
try {
value = JSON.parse(payload) as unknown;
} catch {
return { ok: false, error: "profile: data is not valid JSON" };
}
}
if (!isRecord(value) || !Number.isInteger(value.version)) {
return { ok: false, error: "profile: missing version" };
}
if (value.version === LOCAL_PROFILE_VERSION) {
if (!isLocalProfile(value)) return { ok: false, error: "profile: invalid V2 profile" };
return { ok: true, profile: clone(value), migrated: false };
}
if (value.version === 1) {
if (!isLocalProfileV1(value)) return { ok: false, error: "profile: invalid V1 profile" };
return { ok: true, profile: migrateLocalProfileV1(value), migrated: true, fromVersion: 1 };
}
return { ok: false, error: "profile: unsupported version" };
}
export function encodeLocalProfile(profile: LocalProfile): string {
if (!isLocalProfile(profile)) throw new RangeError("profile: cannot encode invalid profile");
return JSON.stringify(profile);
}
/**
* Stable per-id defaults without retaining the id or its hash. Salted hashes
* keep each appearance axis independent and reveal nothing in the output that
* was not already one of the public palette choices.
*/
export function createDefaultLocalProfile(identityId: string, displayName: string): LocalProfile {
if (typeof identityId !== "string" || identityId.length === 0) {
throw new RangeError("profile: identity id is required for deterministic defaults");
}
const name = displayName.trim();
if (!validDisplayName(name)) throw new RangeError("profile: display name is invalid");
function choose<T>(items: readonly T[], salt: string): T {
const item = items[stableHash(`${salt}\u0000${identityId}`) % items.length];
if (item === undefined) throw new Error("profile: empty choice catalogue");
return item;
}
return {
version: LOCAL_PROFILE_VERSION,
displayName: name,
appearance: {
skinTone: choose(SKIN_TONES, "skin"),
outfit: choose(OUTFITS, "outfit"),
accent: choose(ACCENTS, "accent"),
hair: choose(HAIR_COLORS, "hair"),
bodyShape: choose(BODY_SHAPES, "body"),
},
};
}
function stableHash(value: string): number {
let hash = 0x811c9dc5;
for (let index = 0; index < value.length; index++) {
hash ^= value.charCodeAt(index);
hash = Math.imul(hash, 0x01000193);
}
return hash >>> 0;
}
/** Material-ready values for `buildHumanoid`, still containing no render objects. */
export function resolveHumanoidAppearance(
appearance: HumanoidAppearanceChoices,
): ResolvedHumanoidAppearance {
if (!isHumanoidAppearance(appearance)) throw new RangeError("profile: invalid humanoid appearance");
return {
skinTone: SKIN_COLORS[appearance.skinTone],
outfitColor: OUTFIT_COLORS[appearance.outfit],
accentColor: ACCENT_COLORS[appearance.accent],
hairColor: HAIR_VALUES[appearance.hair],
bodyShape: appearance.bodyShape,
};
}
/** Signed-in members use their humanoid; anonymous presence follows environment role. */
export function actorKindForPresence(signedIn: boolean, context: PresenceContext): PresenceActorKind {
if (signedIn) return "humanoid";
return context === "office" ? "dog" : "crow";
}
+64
View File
@@ -0,0 +1,64 @@
/** Exception-contained persistence over a caller-owned local storage adapter. */
import { decodeLocalProfile, encodeLocalProfile, type LocalProfile } from "./model.ts";
export interface ProfileStorageAdapter {
getItem(key: string): string | null;
setItem(key: string, value: string): void;
removeItem(key: string): void;
}
export type ProfileSaveResult = { ok: true } | { ok: false; error: string };
export type ProfileClearResult = ProfileSaveResult;
export type ProfileLoadResult =
| { status: "loaded"; profile: LocalProfile; migrated: boolean }
| { status: "missing" }
| { status: "invalid"; error: string }
| { status: "unavailable"; error: string };
function validKey(key: string): boolean {
return typeof key === "string" && key.trim().length > 0;
}
export function saveLocalProfile(
storage: ProfileStorageAdapter,
key: string,
profile: LocalProfile,
): ProfileSaveResult {
if (!validKey(key)) return { ok: false, error: "profile: storage key is empty" };
let encoded: string;
try {
encoded = encodeLocalProfile(profile);
} catch (error) {
return { ok: false, error: error instanceof Error ? error.message : "profile: encoding failed" };
}
try {
storage.setItem(key, encoded);
return { ok: true };
} catch {
return { ok: false, error: "profile: storage is unavailable" };
}
}
export function loadLocalProfile(storage: ProfileStorageAdapter, key: string): ProfileLoadResult {
if (!validKey(key)) return { status: "invalid", error: "profile: storage key is empty" };
let encoded: string | null;
try {
encoded = storage.getItem(key);
} catch {
return { status: "unavailable", error: "profile: storage is unavailable" };
}
if (encoded === null) return { status: "missing" };
const decoded = decodeLocalProfile(encoded);
if (!decoded.ok) return { status: "invalid", error: decoded.error };
return { status: "loaded", profile: decoded.profile, migrated: decoded.migrated };
}
export function clearLocalProfile(storage: ProfileStorageAdapter, key: string): ProfileClearResult {
if (!validKey(key)) return { ok: false, error: "profile: storage key is empty" };
try {
storage.removeItem(key);
return { ok: true };
} catch {
return { ok: false, error: "profile: storage is unavailable" };
}
}
+125
View File
@@ -0,0 +1,125 @@
/**
* Ephemeral webcam-face consent state, with no media or persistence capability.
*
* A future browser adapter may react to an accepted `start` transition by
* calling `getUserMedia`; this module cannot do so, cannot fetch anything, and
* deliberately has no encode/storage API. Every fresh controller starts off.
*/
export type WebcamFaceConsentStatus =
| "off"
| "awaiting-consent"
| "active"
| "stopped"
| "revoked";
export interface WebcamFaceConsentState {
status: WebcamFaceConsentStatus;
/** True only between an explicit accepted start and stop/revoke. */
consentGranted: boolean;
readonly ephemeral: true;
readonly persistence: "none";
readonly autoFetch: false;
revision: number;
}
export type WebcamFaceConsentEvent =
| { type: "request-start" }
| { type: "start"; explicitConsent: true }
| { type: "stop" }
| { type: "revoke" };
export type WebcamFaceConsentRejection =
| "already-awaiting"
| "already-active"
| "already-stopped"
| "already-revoked"
| "consent-required";
export type WebcamFaceConsentTransition =
| { accepted: true; state: WebcamFaceConsentState }
| { accepted: false; state: WebcamFaceConsentState; reason: WebcamFaceConsentRejection };
export interface WebcamFaceConsentController {
state(): WebcamFaceConsentState;
requestStart(): WebcamFaceConsentTransition;
/** Only valid after `requestStart`; the literal true prevents accidental implicit consent. */
start(explicitConsent: true): WebcamFaceConsentTransition;
stop(): WebcamFaceConsentTransition;
revoke(): WebcamFaceConsentTransition;
}
export function initialWebcamFaceConsent(): WebcamFaceConsentState {
return {
status: "off",
consentGranted: false,
ephemeral: true,
persistence: "none",
autoFetch: false,
revision: 0,
};
}
function copy(state: WebcamFaceConsentState): WebcamFaceConsentState {
return { ...state };
}
function reject(
state: WebcamFaceConsentState,
reason: WebcamFaceConsentRejection,
): WebcamFaceConsentTransition {
return { accepted: false, state: copy(state), reason };
}
function accept(
state: WebcamFaceConsentState,
status: WebcamFaceConsentStatus,
consentGranted: boolean,
): WebcamFaceConsentTransition {
return {
accepted: true,
state: { ...state, status, consentGranted, revision: state.revision + 1 },
};
}
/** Pure transition primitive for UI stores and deterministic tests. */
export function transitionWebcamFaceConsent(
state: WebcamFaceConsentState,
event: WebcamFaceConsentEvent,
): WebcamFaceConsentTransition {
switch (event.type) {
case "request-start":
if (state.status === "awaiting-consent") return reject(state, "already-awaiting");
if (state.status === "active") return reject(state, "already-active");
return accept(state, "awaiting-consent", false);
case "start":
if (state.status === "active") return reject(state, "already-active");
if (state.status !== "awaiting-consent" || event.explicitConsent !== true) {
return reject(state, "consent-required");
}
return accept(state, "active", true);
case "stop":
if (state.status === "stopped" || state.status === "off") return reject(state, "already-stopped");
if (state.status === "revoked") return reject(state, "already-revoked");
return accept(state, "stopped", false);
case "revoke":
if (state.status === "revoked") return reject(state, "already-revoked");
return accept(state, "revoked", false);
}
}
export function createWebcamFaceConsent(): WebcamFaceConsentController {
let current = initialWebcamFaceConsent();
function apply(event: WebcamFaceConsentEvent): WebcamFaceConsentTransition {
const transition = transitionWebcamFaceConsent(current, event);
if (transition.accepted) current = transition.state;
return { ...transition, state: copy(transition.state) };
}
return {
state: () => copy(current),
requestStart: () => apply({ type: "request-start" }),
start: (explicitConsent) => apply({ type: "start", explicitConsent }),
stop: () => apply({ type: "stop" }),
revoke: () => apply({ type: "revoke" }),
};
}
+11
View File
@@ -0,0 +1,11 @@
export {
isEntityPoseSnapshot,
isInterestCell,
isRealtimeSequence,
isRealtimeTimestamp,
validateClientRealtimeMessage,
validateMotion,
validateServerRealtimeMessage,
} from "./protocol.ts";
export { PoseInterpolationBuffer } from "./interpolation.ts";
export type * from "./types.ts";
+181
View File
@@ -0,0 +1,181 @@
/** Deterministic snapshot interpolation with deliberately bounded prediction. */
import type {
BufferedPoseSample,
EntityPoseSnapshot,
InterpolationOptions,
PoseVelocity,
SpatialPose,
} from "./types.ts";
interface ResolvedOptions {
interpolationDelayMs: number;
maximumExtrapolationMs: number;
capacity: number;
}
function clamp(value: number, min: number, max: number): number {
return Math.max(min, Math.min(max, value));
}
function options(value: InterpolationOptions): ResolvedOptions {
return {
interpolationDelayMs: clamp(value.interpolationDelayMs ?? 100, 0, 2_000),
maximumExtrapolationMs: clamp(value.maximumExtrapolationMs ?? 150, 0, 1_000),
capacity: Math.floor(clamp(value.capacity ?? 32, 2, 256)),
};
}
function angle(from: number, to: number, t: number): number {
const delta = ((to - from + 540) % 360) - 180;
return from + delta * t;
}
function compatible(a: SpatialPose, b: SpatialPose): boolean {
if (a.space !== b.space) return false;
if (a.space === "geographic") return true;
return b.space === "local" && JSON.stringify(a.cell) === JSON.stringify(b.cell);
}
function mixVelocity(a: PoseVelocity, b: PoseVelocity, t: number): PoseVelocity {
return {
xMps: a.xMps + (b.xMps - a.xMps) * t,
yMps: a.yMps + (b.yMps - a.yMps) * t,
zMps: a.zMps + (b.zMps - a.zMps) * t,
yawDegPerSec: a.yawDegPerSec + (b.yawDegPerSec - a.yawDegPerSec) * t,
};
}
function mixPose(a: SpatialPose, b: SpatialPose, t: number): SpatialPose {
if (!compatible(a, b)) return { ...b };
if (a.space === "geographic" && b.space === "geographic") {
return {
space: "geographic",
lat: a.lat + (b.lat - a.lat) * t,
lng: a.lng + (b.lng - a.lng) * t,
altitudeM: a.altitudeM + (b.altitudeM - a.altitudeM) * t,
headingDeg: angle(a.headingDeg, b.headingDeg, t),
pitchDeg: a.pitchDeg + (b.pitchDeg - a.pitchDeg) * t,
};
}
if (a.space === "local" && b.space === "local") {
return {
space: "local",
cell: { ...b.cell },
xM: a.xM + (b.xM - a.xM) * t,
yM: a.yM + (b.yM - a.yM) * t,
zM: a.zM + (b.zM - a.zM) * t,
headingDeg: angle(a.headingDeg, b.headingDeg, t),
pitchDeg: a.pitchDeg + (b.pitchDeg - a.pitchDeg) * t,
};
}
return { ...b };
}
function predictPose(pose: SpatialPose, velocity: PoseVelocity, seconds: number): SpatialPose {
if (pose.space === "local") {
return {
...pose,
cell: { ...pose.cell },
xM: pose.xM + velocity.xMps * seconds,
yM: pose.yM + velocity.yMps * seconds,
zM: pose.zM + velocity.zMps * seconds,
headingDeg: pose.headingDeg + velocity.yawDegPerSec * seconds,
};
}
// Geographic velocity uses east/up/north axes matching local x/y/z.
const earth = 6_371_000;
const latitudeRadians = pose.lat * Math.PI / 180;
return {
...pose,
lat: pose.lat + (velocity.zMps * seconds / earth) * 180 / Math.PI,
lng: pose.lng + (velocity.xMps * seconds / (earth * Math.max(0.01, Math.cos(latitudeRadians)))) * 180 / Math.PI,
altitudeM: pose.altitudeM + velocity.yMps * seconds,
headingDeg: pose.headingDeg + velocity.yawDegPerSec * seconds,
};
}
function id(snapshot: EntityPoseSnapshot): string {
return snapshot.entity === "actor" ? `actor:${snapshot.actorId}` : `vehicle:${snapshot.vehicleId}`;
}
export class PoseInterpolationBuffer {
private readonly settings: ResolvedOptions;
private readonly snapshots: EntityPoseSnapshot[] = [];
private entityId: string | null = null;
constructor(optionsValue: InterpolationOptions = {}) {
this.settings = options(optionsValue);
}
clear(): void {
this.snapshots.length = 0;
this.entityId = null;
}
size(): number {
return this.snapshots.length;
}
/** Reject duplicates, stale sequence numbers, and accidental entity mixing. */
push(snapshot: EntityPoseSnapshot): boolean {
const incomingId = id(snapshot);
if (this.entityId !== null && incomingId !== this.entityId) return false;
const latest = this.snapshots.at(-1);
if (latest && (snapshot.sequence <= latest.sequence || snapshot.timestampMs <= latest.timestampMs)) {
return false;
}
this.entityId = incomingId;
this.snapshots.push(snapshot);
if (this.snapshots.length > this.settings.capacity) this.snapshots.shift();
return true;
}
/** Sample server time minus interpolation delay; never predicts past the configured cap. */
sample(serverNowMs: number): BufferedPoseSample | null {
if (!Number.isFinite(serverNowMs) || this.snapshots.length === 0) return null;
const target = serverNowMs - this.settings.interpolationDelayMs;
const first = this.snapshots[0];
const latest = this.snapshots.at(-1);
if (!first || !latest) return null;
if (target <= first.timestampMs) return this.from(first, "held", first.timestampMs);
for (let index = 1; index < this.snapshots.length; index += 1) {
const after = this.snapshots[index];
const before = this.snapshots[index - 1];
if (!after || !before || target > after.timestampMs) continue;
if (target === after.timestampMs) return this.from(after, "exact", target);
if (!compatible(before.pose, after.pose)) return this.from(after, "held", target);
const t = (target - before.timestampMs) / (after.timestampMs - before.timestampMs);
return {
mode: "interpolated",
pose: mixPose(before.pose, after.pose, t),
velocity: mixVelocity(before.velocity, after.velocity, t),
timestampMs: target,
sourceSequence: after.sequence,
};
}
const extrapolationMs = clamp(target - latest.timestampMs, 0, this.settings.maximumExtrapolationMs);
if (extrapolationMs === 0) return this.from(latest, "exact", latest.timestampMs);
return {
mode: target - latest.timestampMs > this.settings.maximumExtrapolationMs ? "held" : "extrapolated",
pose: predictPose(latest.pose, latest.velocity, extrapolationMs / 1_000),
velocity: { ...latest.velocity },
timestampMs: latest.timestampMs + extrapolationMs,
sourceSequence: latest.sequence,
};
}
private from(snapshot: EntityPoseSnapshot, mode: "exact" | "held", timestampMs: number): BufferedPoseSample {
return {
mode,
pose: snapshot.pose.space === "local"
? { ...snapshot.pose, cell: { ...snapshot.pose.cell } }
: { ...snapshot.pose },
velocity: { ...snapshot.velocity },
timestampMs,
sourceSequence: snapshot.sequence,
};
}
}
+262
View File
@@ -0,0 +1,262 @@
/** Strict runtime validation for untrusted realtime protocol payloads. */
import type {
ClientRealtimeMessage,
EntityPoseSnapshot,
InterestCell,
MotionLimits,
MotionValidationResult,
RealtimeSequence,
RealtimeValidationResult,
ServerRealtimeMessage,
SpatialPose,
} from "./types.ts";
const UINT32_MAX = 4_294_967_295;
const MAX_TIMESTAMP_MS = 8_640_000_000_000_000;
const MAX_INTERESTS = 128;
const MAX_ENTITIES_PER_MESSAGE = 2_048;
const MAX_STRING = 256;
const EARTH_RADIUS_M = 6_371_000;
function record(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function string(value: unknown, maximum = MAX_STRING): value is string {
return typeof value === "string" && value.length > 0 && value.length <= maximum;
}
function finite(value: unknown): value is number {
return typeof value === "number" && Number.isFinite(value);
}
export function isRealtimeSequence(value: unknown): value is RealtimeSequence {
return Number.isSafeInteger(value) && Number(value) >= 0 && Number(value) <= UINT32_MAX;
}
export function isRealtimeTimestamp(value: unknown): value is number {
return Number.isSafeInteger(value) && Number(value) >= 0 && Number(value) <= MAX_TIMESTAMP_MS;
}
export function isInterestCell(value: unknown): value is InterestCell {
if (!record(value)) return false;
switch (value.kind) {
case "california-tile":
return (
Number.isInteger(value.x) &&
Number.isInteger(value.y) &&
Number.isInteger(value.level) &&
Number(value.x) >= -1_000_000 &&
Number(value.x) <= 1_000_000 &&
Number(value.y) >= -1_000_000 &&
Number(value.y) <= 1_000_000 &&
Number(value.level) >= 0 &&
Number(value.level) <= 24
);
case "city":
return value.cityId === "bay-area" || value.cityId === "socal";
case "office":
return string(value.officeId);
case "floor":
return string(value.officeId) && string(value.floorId);
case "room":
return string(value.officeId) && string(value.floorId) && string(value.roomId);
default:
return false;
}
}
function isSpatialPose(value: unknown): value is SpatialPose {
if (!record(value)) return false;
if (value.space === "geographic") {
return (
finite(value.lat) && value.lat >= -90 && value.lat <= 90 &&
finite(value.lng) && value.lng >= -180 && value.lng <= 180 &&
finite(value.altitudeM) && value.altitudeM >= -500 && value.altitudeM <= 100_000 &&
finite(value.headingDeg) && value.headingDeg >= -360 && value.headingDeg <= 360 &&
finite(value.pitchDeg) && value.pitchDeg >= -90 && value.pitchDeg <= 90
);
}
return (
value.space === "local" &&
isInterestCell(value.cell) &&
value.cell.kind !== "california-tile" &&
finite(value.xM) && Math.abs(value.xM) <= 1_000_000 &&
finite(value.yM) && Math.abs(value.yM) <= 100_000 &&
finite(value.zM) && Math.abs(value.zM) <= 1_000_000 &&
finite(value.headingDeg) && value.headingDeg >= -360 && value.headingDeg <= 360 &&
finite(value.pitchDeg) && value.pitchDeg >= -90 && value.pitchDeg <= 90
);
}
function isVelocity(value: unknown): boolean {
return (
record(value) &&
finite(value.xMps) && Math.abs(value.xMps) <= 1_000 &&
finite(value.yMps) && Math.abs(value.yMps) <= 1_000 &&
finite(value.zMps) && Math.abs(value.zMps) <= 1_000 &&
finite(value.yawDegPerSec) && Math.abs(value.yawDegPerSec) <= 10_000
);
}
export function isEntityPoseSnapshot(value: unknown): value is EntityPoseSnapshot {
if (
!record(value) ||
!isRealtimeSequence(value.sequence) ||
!isRealtimeTimestamp(value.timestampMs) ||
!isSpatialPose(value.pose) ||
!isVelocity(value.velocity)
) return false;
if (value.entity === "actor") {
return (
string(value.actorId) &&
(value.kind === "humanoid" || value.kind === "dog" || value.kind === "crow")
);
}
return (
value.entity === "vehicle" &&
string(value.vehicleId) &&
value.kind === "model-x" &&
(value.driverActorId === null || string(value.driverActorId)) &&
finite(value.steering) && value.steering >= -1 && value.steering <= 1 &&
finite(value.wheelRadians) && Math.abs(value.wheelRadians) <= 1_000_000
);
}
function interests(value: unknown): value is readonly InterestCell[] {
return Array.isArray(value) && value.length <= MAX_INTERESTS && value.every(isInterestCell);
}
function entities(value: unknown): value is readonly EntityPoseSnapshot[] {
return Array.isArray(value) && value.length <= MAX_ENTITIES_PER_MESSAGE && value.every(isEntityPoseSnapshot);
}
function common(value: Record<string, unknown>): boolean {
return value.protocolVersion === 1 && string(value.requestId);
}
export function validateClientRealtimeMessage(
value: unknown,
): RealtimeValidationResult<ClientRealtimeMessage> {
if (!record(value) || !common(value)) return { ok: false, error: "realtime: invalid envelope" };
if (value.type === "join-request") {
if (
string(value.actorId) &&
(value.resumeToken === null || string(value.resumeToken, 512)) &&
(value.lastReceivedSequence === null || isRealtimeSequence(value.lastReceivedSequence)) &&
interests(value.interests)
) return { ok: true, value: value as unknown as ClientRealtimeMessage };
return { ok: false, error: "realtime: invalid join request" };
}
if (value.type === "resume-request") {
if (
string(value.sessionId) && string(value.resumeToken, 512) && string(value.serverEpoch) &&
isRealtimeSequence(value.lastReceivedSequence) && interests(value.interests)
) return { ok: true, value: value as unknown as ClientRealtimeMessage };
return { ok: false, error: "realtime: invalid resume request" };
}
return { ok: false, error: "realtime: unknown client message" };
}
export function validateServerRealtimeMessage(
value: unknown,
): RealtimeValidationResult<ServerRealtimeMessage> {
if (!record(value) || value.protocolVersion !== 1) {
return { ok: false, error: "realtime: invalid envelope" };
}
if (value.type === "join-grant") {
if (
string(value.requestId) && string(value.sessionId) && string(value.actorId) &&
(value.role === "visitor" || value.role === "member" || value.role === "admin") &&
string(value.serverEpoch) && isRealtimeTimestamp(value.serverTimeMs) &&
isRealtimeSequence(value.nextSequence) && string(value.resumeToken, 512) &&
interests(value.interests) && entities(value.initial)
) return { ok: true, value: value as unknown as ServerRealtimeMessage };
return { ok: false, error: "realtime: invalid join grant" };
}
if (value.type === "resume-grant") {
if (
string(value.requestId) && string(value.sessionId) && string(value.serverEpoch) &&
isRealtimeTimestamp(value.serverTimeMs) && isRealtimeSequence(value.nextSequence) &&
string(value.resumeToken, 512) && typeof value.continuous === "boolean" && entities(value.snapshot)
) return { ok: true, value: value as unknown as ServerRealtimeMessage };
return { ok: false, error: "realtime: invalid resume grant" };
}
if (value.type === "pose-delta") {
if (
string(value.serverEpoch) && isRealtimeSequence(value.sequence) &&
isRealtimeTimestamp(value.timestampMs) && entities(value.updates) &&
Array.isArray(value.removedEntityIds) &&
value.removedEntityIds.length <= MAX_ENTITIES_PER_MESSAGE &&
value.removedEntityIds.every((id) => string(id))
) return { ok: true, value: value as unknown as ServerRealtimeMessage };
return { ok: false, error: "realtime: invalid pose delta" };
}
if (value.type === "membership-revoked") {
const reasons = new Set([
"signed-out", "membership-revoked", "session-replaced", "protocol-violation", "server-shutdown",
]);
if (
string(value.serverEpoch) && isRealtimeSequence(value.sequence) &&
isRealtimeTimestamp(value.timestampMs) && string(value.sessionId) && string(value.actorId) &&
reasons.has(String(value.reason)) && typeof value.reconnectAllowed === "boolean"
) return { ok: true, value: value as unknown as ServerRealtimeMessage };
return { ok: false, error: "realtime: invalid membership revocation" };
}
return { ok: false, error: "realtime: unknown server message" };
}
function entityId(snapshot: EntityPoseSnapshot): string {
return snapshot.entity === "actor" ? `actor:${snapshot.actorId}` : `vehicle:${snapshot.vehicleId}`;
}
function localCellKey(pose: SpatialPose): string | null {
if (pose.space !== "local") return null;
return JSON.stringify(pose.cell);
}
function angleDeltaDegrees(from: number, to: number): number {
return ((to - from + 540) % 360) - 180;
}
function displacementMetres(from: SpatialPose, to: SpatialPose): { horizontal: number; vertical: number } | null {
if (from.space !== to.space) return null;
if (from.space === "local" && to.space === "local") {
if (localCellKey(from) !== localCellKey(to)) return null;
return { horizontal: Math.hypot(to.xM - from.xM, to.zM - from.zM), vertical: to.yM - from.yM };
}
if (from.space === "geographic" && to.space === "geographic") {
const meanLat = ((from.lat + to.lat) / 2) * Math.PI / 180;
const north = (to.lat - from.lat) * Math.PI / 180 * EARTH_RADIUS_M;
const east = (to.lng - from.lng) * Math.PI / 180 * Math.cos(meanLat) * EARTH_RADIUS_M;
return { horizontal: Math.hypot(north, east), vertical: to.altitudeM - from.altitudeM };
}
return null;
}
/** Server-side helper for rejecting impossible client-proposed movement. */
export function validateMotion(
previous: EntityPoseSnapshot,
proposed: EntityPoseSnapshot,
limits: MotionLimits,
): MotionValidationResult {
if (entityId(previous) !== entityId(proposed)) return { ok: false, reason: "identity-mismatch" };
const displacement = displacementMetres(previous.pose, proposed.pose);
if (!displacement) return { ok: false, reason: "space-mismatch" };
const dt = (proposed.timestampMs - previous.timestampMs) / 1_000;
const minimum = Math.max(0.001, limits.minimumDeltaSeconds ?? 1 / 240);
const maximum = Math.max(minimum, limits.maximumDeltaSeconds ?? 2);
if (!Number.isFinite(dt) || dt < minimum || dt > maximum || proposed.sequence <= previous.sequence) {
return { ok: false, reason: "time-invalid" };
}
const horizontalSpeedMps = Math.max(0, displacement.horizontal - Math.max(0, limits.positionSlackM)) / dt;
const verticalSpeedMps = Math.abs(displacement.vertical) / dt;
const turnRateDegPerSec = Math.abs(angleDeltaDegrees(previous.pose.headingDeg, proposed.pose.headingDeg)) / dt;
if (
!finite(limits.maximumHorizontalSpeedMps) || horizontalSpeedMps > limits.maximumHorizontalSpeedMps ||
!finite(limits.maximumVerticalSpeedMps) || verticalSpeedMps > limits.maximumVerticalSpeedMps ||
!finite(limits.maximumTurnRateDegPerSec) || turnRateDegPerSec > limits.maximumTurnRateDegPerSec
) return { ok: false, reason: "speed-exceeded" };
return { ok: true, horizontalSpeedMps, verticalSpeedMps, turnRateDegPerSec };
}
+219
View File
@@ -0,0 +1,219 @@
/** JSON-safe contracts for an eventual server-authoritative Tera session. */
export type RealtimeActorKind = "humanoid" | "dog" | "crow";
export type RealtimeVehicleKind = "model-x";
export type RealtimeMemberRole = "visitor" | "member" | "admin";
export type RealtimeRevocationReason =
| "signed-out"
| "membership-revoked"
| "session-replaced"
| "protocol-violation"
| "server-shutdown";
export interface CaliforniaTileInterest {
kind: "california-tile";
/** Discrete authored world grid, not web-map tiles. */
x: number;
y: number;
level: number;
}
export interface CityInterest {
kind: "city";
cityId: "bay-area" | "socal";
}
export interface OfficeInterest {
kind: "office";
officeId: string;
}
export interface FloorInterest {
kind: "floor";
officeId: string;
floorId: string;
}
export interface RoomInterest {
kind: "room";
officeId: string;
floorId: string;
roomId: string;
}
export type InterestCell =
| CaliforniaTileInterest
| CityInterest
| OfficeInterest
| FloorInterest
| RoomInterest;
/** Monotonic within one server epoch and bounded to an unsigned 32-bit integer. */
export type RealtimeSequence = number;
/** Unix epoch milliseconds, serialized as a safe integer. */
export type RealtimeTimestamp = number;
export interface GeographicPose {
space: "geographic";
lat: number;
lng: number;
altitudeM: number;
headingDeg: number;
pitchDeg: number;
}
export interface LocalPose {
space: "local";
/** The smallest spatial cell whose coordinates define this pose. */
cell: CityInterest | OfficeInterest | FloorInterest | RoomInterest;
xM: number;
yM: number;
zM: number;
headingDeg: number;
pitchDeg: number;
}
export type SpatialPose = GeographicPose | LocalPose;
export interface PoseVelocity {
xMps: number;
yMps: number;
zMps: number;
yawDegPerSec: number;
}
interface EntityPoseSnapshotBase {
sequence: RealtimeSequence;
timestampMs: RealtimeTimestamp;
pose: SpatialPose;
velocity: PoseVelocity;
}
export interface ActorPoseSnapshot extends EntityPoseSnapshotBase {
entity: "actor";
actorId: string;
kind: RealtimeActorKind;
}
export interface VehiclePoseSnapshot extends EntityPoseSnapshotBase {
entity: "vehicle";
vehicleId: string;
kind: RealtimeVehicleKind;
driverActorId: string | null;
steering: number;
wheelRadians: number;
}
export type EntityPoseSnapshot = ActorPoseSnapshot | VehiclePoseSnapshot;
export interface JoinRequest {
type: "join-request";
protocolVersion: 1;
requestId: string;
actorId: string;
resumeToken: string | null;
lastReceivedSequence: RealtimeSequence | null;
interests: readonly InterestCell[];
}
export interface JoinGrant {
type: "join-grant";
protocolVersion: 1;
requestId: string;
sessionId: string;
actorId: string;
role: RealtimeMemberRole;
serverEpoch: string;
serverTimeMs: RealtimeTimestamp;
nextSequence: RealtimeSequence;
resumeToken: string;
interests: readonly InterestCell[];
initial: readonly EntityPoseSnapshot[];
}
export interface ResumeRequest {
type: "resume-request";
protocolVersion: 1;
requestId: string;
sessionId: string;
resumeToken: string;
serverEpoch: string;
lastReceivedSequence: RealtimeSequence;
interests: readonly InterestCell[];
}
export interface ResumeGrant {
type: "resume-grant";
protocolVersion: 1;
requestId: string;
sessionId: string;
serverEpoch: string;
serverTimeMs: RealtimeTimestamp;
nextSequence: RealtimeSequence;
resumeToken: string;
/** True means queued deltas bridge the requested sequence without a gap. */
continuous: boolean;
snapshot: readonly EntityPoseSnapshot[];
}
export interface PoseDelta {
type: "pose-delta";
protocolVersion: 1;
serverEpoch: string;
sequence: RealtimeSequence;
timestampMs: RealtimeTimestamp;
updates: readonly EntityPoseSnapshot[];
removedEntityIds: readonly string[];
}
export interface MembershipRevoked {
type: "membership-revoked";
protocolVersion: 1;
serverEpoch: string;
sequence: RealtimeSequence;
timestampMs: RealtimeTimestamp;
sessionId: string;
actorId: string;
reason: RealtimeRevocationReason;
reconnectAllowed: boolean;
}
export type ClientRealtimeMessage = JoinRequest | ResumeRequest;
export type ServerRealtimeMessage = JoinGrant | ResumeGrant | PoseDelta | MembershipRevoked;
export type RealtimeValidationResult<T> =
| { ok: true; value: T }
| { ok: false; error: string };
export interface MotionLimits {
maximumHorizontalSpeedMps: number;
maximumVerticalSpeedMps: number;
maximumTurnRateDegPerSec: number;
/** Allows quantization and packet jitter without permitting teleportation. */
positionSlackM: number;
minimumDeltaSeconds?: number;
maximumDeltaSeconds?: number;
}
export type MotionValidationResult =
| { ok: true; horizontalSpeedMps: number; verticalSpeedMps: number; turnRateDegPerSec: number }
| {
ok: false;
reason: "identity-mismatch" | "space-mismatch" | "time-invalid" | "speed-exceeded";
};
export interface InterpolationOptions {
interpolationDelayMs?: number;
maximumExtrapolationMs?: number;
capacity?: number;
}
export type BufferedSampleMode = "exact" | "interpolated" | "extrapolated" | "held";
export interface BufferedPoseSample {
mode: BufferedSampleMode;
pose: SpatialPose;
velocity: PoseVelocity;
timestampMs: RealtimeTimestamp;
sourceSequence: RealtimeSequence;
}
+150
View File
@@ -0,0 +1,150 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
authorizeMediaSurface,
createMediaSurfaceLifecycle,
isMediaSurface,
parseMediaSurface,
parseMediaViewerContext,
transitionMediaSurface,
type MediaSurface,
type MediaSurfaceSource,
} from "../media/index.ts";
import type { VideoTexture } from "three";
const PRIVATE_SOURCE: MediaSurfaceSource = {
kind: "live-stream",
locator: "opaque:private-stream-token",
privacy: "private",
hasAudio: true,
autoplay: false,
muted: true,
};
function surface(over: Partial<MediaSurface> = {}): MediaSurface {
return {
screenId: "screen-1",
officeId: "office-1",
levelId: "level-1",
roomId: "room-1",
acl: {
audience: "office-members",
viewerIds: [],
presenterIds: ["presenter-1"],
moderatorIds: ["moderator-1"],
},
source: PRIVATE_SOURCE,
state: { status: "presenting", presenterId: "presenter-1", revision: 1 },
...over,
};
}
describe("media surface validation", () => {
it("accepts a strict JSON round trip and rejects extra/non-JSON fields", () => {
const parsed = parseMediaSurface(JSON.parse(JSON.stringify(surface())));
assert.deepEqual(parsed, surface());
assert.equal(isMediaSurface({ ...surface(), surprise: true }), false);
assert.equal(isMediaSurface({ ...surface(), screenId: "bad id" }), false);
assert.equal(isMediaSurface(Object.assign(Object.create(null), surface())), false);
});
it("enforces state/source consistency and safe playback literals", () => {
assert.throws(() => parseMediaSurface({ ...surface(), source: null }), /require a source/);
assert.throws(() => parseMediaSurface({
...surface(),
source: { ...PRIVATE_SOURCE, autoplay: true },
}), /autoplay/);
assert.throws(() => parseMediaSurface({
...surface(),
source: { ...PRIVATE_SOURCE, muted: false },
}), /muted/);
assert.throws(() => parseMediaSurface({
...surface(),
acl: { ...surface().acl, audience: "public" },
}), /private source/);
});
it("strictly validates the server-created viewer context", () => {
assert.deepEqual(parseMediaViewerContext({
viewerId: "viewer-1", officeIds: ["office-1"], optedIn: true,
}), { viewerId: "viewer-1", officeIds: ["office-1"], optedIn: true });
assert.throws(() => parseMediaViewerContext({
viewerId: null, officeIds: [], optedIn: true, clientClaimsAdmin: true,
}), /exactly/);
});
});
describe("server-enforceable authorization", () => {
it("never returns a private source to an anonymous public viewer", () => {
const decision = authorizeMediaSurface(surface(), { viewerId: null, officeIds: [], optedIn: true });
assert.equal(decision.authorized, false);
assert.equal(decision.canView, false);
assert.equal(decision.reason, "authentication_required");
assert.equal(decision.surface.source, null);
assert.ok(!JSON.stringify(decision).includes(PRIVATE_SOURCE.locator));
});
it("requires both trusted membership and explicit opt-in before releasing source", () => {
const context = { viewerId: "viewer-1", officeIds: ["office-1"], optedIn: false };
const waiting = authorizeMediaSurface(surface(), context);
assert.equal(waiting.authorized, true);
assert.equal(waiting.reason, "opt_in_required");
assert.equal(waiting.surface.source, null);
const ready = authorizeMediaSurface(surface(), { ...context, optedIn: true });
assert.equal(ready.canView, true);
assert.equal(ready.surface.source?.locator, PRIVATE_SOURCE.locator);
assert.deepEqual(ready.surface.playback, { autoplay: false, muted: true });
});
});
describe("presenter lifecycle", () => {
it("stops and revokes by clearing the sensitive source", () => {
const stopped = transitionMediaSurface(surface(), { type: "stop", presenterId: "presenter-1" });
assert.equal(stopped.state.status, "stopped");
assert.equal(stopped.source, null);
assert.equal(stopped.state.revision, 2);
const revoked = transitionMediaSurface(surface(), { type: "revoke", actorId: "moderator-1" });
assert.equal(revoked.state.status, "revoked");
assert.equal(revoked.source, null);
assert.throws(() => transitionMediaSurface(surface(), { type: "revoke", actorId: "stranger" }));
});
it("retains authorization through a deterministic disconnect/reconnect", () => {
const reconnecting = transitionMediaSurface(surface(), { type: "disconnect", presenterId: "presenter-1" });
assert.equal(reconnecting.state.status, "reconnecting");
const resumed = transitionMediaSurface(reconnecting, { type: "reconnect", presenterId: "presenter-1" });
assert.equal(resumed.state.status, "presenting");
assert.equal(resumed.state.revision, 3);
assert.throws(() => transitionMediaSurface(reconnecting, { type: "reconnect", presenterId: "other" }));
});
});
describe("caller-owned video/texture lifecycle", () => {
it("never plays, stops a track, or disposes a texture and releases references", () => {
let pauses = 0;
let stops = 0;
let disposals = 0;
const video = {
autoplay: true,
muted: false,
pause: () => { pauses += 1; },
} as unknown as HTMLVideoElement;
const texture = { dispose: () => { disposals += 1; } } as unknown as VideoTexture;
const track = { stop: () => { stops += 1; } } as unknown as MediaStreamTrack;
const lifecycle = createMediaSurfaceLifecycle();
lifecycle.bind({ video, texture, track });
assert.equal(video.autoplay, false);
assert.equal(video.muted, true);
assert.equal(lifecycle.texture(), null);
lifecycle.apply(authorizeMediaSurface(surface(), {
viewerId: "viewer-1", officeIds: ["office-1"], optedIn: true,
}));
assert.equal(lifecycle.texture(), texture);
lifecycle.dispose();
lifecycle.dispose();
assert.equal(lifecycle.texture(), null);
assert.ok(pauses >= 2);
assert.equal(stops, 0);
assert.equal(disposals, 0);
});
});
+184
View File
@@ -0,0 +1,184 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
actorKindForPresence,
clearLocalProfile,
createDefaultLocalProfile,
createWebcamFaceConsent,
decodeLocalProfile,
encodeLocalProfile,
initialWebcamFaceConsent,
loadLocalProfile,
resolveHumanoidAppearance,
saveLocalProfile,
transitionWebcamFaceConsent,
type LocalProfileV1,
type ProfileStorageAdapter,
} from "../profile/index.ts";
class MemoryStorage implements ProfileStorageAdapter {
readonly values = new Map<string, string>();
getItem(key: string): string | null { return this.values.get(key) ?? null; }
setItem(key: string, value: string): void { this.values.set(key, value); }
removeItem(key: string): void { this.values.delete(key); }
}
describe("local member profile", () => {
it("makes stable, useful defaults from identity without retaining or leaking the id", () => {
const identityId = "auth0|private-user-82914";
const first = createDefaultLocalProfile(identityId, " Avery ");
const second = createDefaultLocalProfile(identityId, "Avery");
assert.deepEqual(first, second);
assert.equal(first.displayName, "Avery");
const encoded = encodeLocalProfile(first);
assert.equal(encoded.includes(identityId), false);
assert.deepEqual(Object.keys(first).sort(), ["appearance", "displayName", "version"]);
assert.doesNotThrow(() => JSON.stringify(first));
const resolved = resolveHumanoidAppearance(first.appearance);
assert.match(resolved.skinTone, /^#[0-9a-f]{6}$/i);
assert.match(resolved.outfitColor, /^#[0-9a-f]{6}$/i);
assert.equal(resolved.bodyShape, first.appearance.bodyShape);
});
it("strictly rejects unknown, malformed and future profile data", () => {
const profile = createDefaultLocalProfile("member", "Member");
assert.equal(decodeLocalProfile({ ...profile, identityId: "leak" }).ok, false);
assert.equal(decodeLocalProfile({ ...profile, faceImageUrl: "camera://stream" }).ok, false);
assert.equal(decodeLocalProfile({ ...profile, appearance: { ...profile.appearance, outfit: "rainbow" } }).ok, false);
assert.deepEqual(decodeLocalProfile({ version: 99 }), { ok: false, error: "profile: unsupported version" });
assert.deepEqual(decodeLocalProfile("{"), { ok: false, error: "profile: data is not valid JSON" });
assert.throws(() => createDefaultLocalProfile("", "Member"), RangeError);
});
it("migrates the one exact V1 shape into an isolated V2 appearance", () => {
const legacy: LocalProfileV1 = {
version: 1,
displayName: "River",
skinTone: "umber",
outfit: "navy",
accent: "amber",
hair: "auburn",
bodyShape: "slim",
};
const decoded = decodeLocalProfile(JSON.stringify(legacy));
assert.equal(decoded.ok, true);
if (!decoded.ok) return;
assert.equal(decoded.migrated, true);
assert.deepEqual(decoded.profile, {
version: 2,
displayName: "River",
appearance: {
skinTone: "umber",
outfit: "navy",
accent: "amber",
hair: "auburn",
bodyShape: "slim",
},
});
assert.equal(decodeLocalProfile({ ...legacy, extra: true }).ok, false);
});
it("maps signed-in and anonymous roles by environment", () => {
assert.equal(actorKindForPresence(true, "outdoors"), "humanoid");
assert.equal(actorKindForPresence(true, "office"), "humanoid");
assert.equal(actorKindForPresence(false, "outdoors"), "crow");
assert.equal(actorKindForPresence(false, "office"), "dog");
});
});
describe("profile persistence boundary", () => {
it("round-trips, reports migration without an implicit rewrite, and clears", () => {
const storage = new MemoryStorage();
const profile = createDefaultLocalProfile("signed-in-1", "Morgan");
assert.deepEqual(saveLocalProfile(storage, "tera.profile", profile), { ok: true });
assert.deepEqual(loadLocalProfile(storage, "tera.profile"), {
status: "loaded",
profile,
migrated: false,
});
const legacy: LocalProfileV1 = {
version: 1,
displayName: "Morgan",
skinTone: "sand",
outfit: "sage",
accent: "aqua",
hair: "black",
bodyShape: "average",
};
storage.values.set("legacy", JSON.stringify(legacy));
const before = storage.values.get("legacy");
const loaded = loadLocalProfile(storage, "legacy");
assert.equal(loaded.status, "loaded");
if (loaded.status === "loaded") assert.equal(loaded.migrated, true);
assert.equal(storage.values.get("legacy"), before, "load never silently rewrites data");
assert.deepEqual(clearLocalProfile(storage, "tera.profile"), { ok: true });
assert.deepEqual(loadLocalProfile(storage, "tera.profile"), { status: "missing" });
});
it("contains adapter and encoder exceptions", () => {
const unavailable: ProfileStorageAdapter = {
getItem: () => { throw new Error("blocked"); },
setItem: () => { throw new Error("quota"); },
removeItem: () => { throw new Error("blocked"); },
};
const profile = createDefaultLocalProfile("signed-in-2", "Taylor");
assert.equal(loadLocalProfile(unavailable, "tera.profile").status, "unavailable");
assert.equal(saveLocalProfile(unavailable, "tera.profile", profile).ok, false);
assert.equal(clearLocalProfile(unavailable, "tera.profile").ok, false);
assert.equal(loadLocalProfile(unavailable, " ").status, "invalid");
const invalid = { ...profile, displayName: "" };
assert.equal(saveLocalProfile(new MemoryStorage(), "x", invalid).ok, false);
});
});
describe("ephemeral webcam face consent", () => {
it("starts off with explicit privacy invariants and cannot auto-start", () => {
const state = initialWebcamFaceConsent();
assert.deepEqual(state, {
status: "off",
consentGranted: false,
ephemeral: true,
persistence: "none",
autoFetch: false,
revision: 0,
});
const rejected = transitionWebcamFaceConsent(state, { type: "start", explicitConsent: true });
assert.equal(rejected.accepted, false);
if (!rejected.accepted) assert.equal(rejected.reason, "consent-required");
});
it("requires request then explicit start, and clears consent on stop", () => {
const consent = createWebcamFaceConsent();
assert.equal(consent.requestStart().accepted, true);
assert.equal(consent.state().status, "awaiting-consent");
assert.equal(consent.start(true).accepted, true);
assert.deepEqual(consent.state(), {
status: "active",
consentGranted: true,
ephemeral: true,
persistence: "none",
autoFetch: false,
revision: 2,
});
assert.equal(consent.stop().accepted, true);
assert.equal(consent.state().status, "stopped");
assert.equal(consent.state().consentGranted, false);
assert.equal(consent.start(true).accepted, false, "a stopped session needs a new request");
});
it("revokes from any live state and requires a fresh user request before reuse", () => {
const consent = createWebcamFaceConsent();
consent.requestStart();
consent.start(true);
assert.equal(consent.revoke().accepted, true);
assert.equal(consent.state().status, "revoked");
assert.equal(consent.state().consentGranted, false);
assert.equal(consent.start(true).accepted, false);
assert.equal(consent.requestStart().accepted, true);
assert.equal(consent.state().status, "awaiting-consent");
const leaked = consent.state();
leaked.status = "active";
assert.equal(consent.state().status, "awaiting-consent");
});
});
+262
View File
@@ -0,0 +1,262 @@
import assert from "node:assert/strict";
import { describe, it } from "node:test";
import {
PoseInterpolationBuffer,
isEntityPoseSnapshot,
isInterestCell,
isRealtimeSequence,
isRealtimeTimestamp,
validateClientRealtimeMessage,
validateMotion,
validateServerRealtimeMessage,
type ActorPoseSnapshot,
type InterestCell,
type VehiclePoseSnapshot,
} from "../realtime/index.ts";
const localActor = (
sequence: number,
timestampMs: number,
xM: number,
headingDeg = 0,
): ActorPoseSnapshot => ({
entity: "actor",
actorId: "actor-karti",
kind: "humanoid",
sequence,
timestampMs,
pose: {
space: "local",
cell: { kind: "floor", officeId: "lumbridge-hq", floorId: "level-1" },
xM,
yM: 0,
zM: 2,
headingDeg,
pitchDeg: 0,
},
velocity: { xMps: 10, yMps: 0, zMps: 0, yawDegPerSec: 10 },
});
const geographicVehicle = (
sequence: number,
timestampMs: number,
lat: number,
): VehiclePoseSnapshot => ({
entity: "vehicle",
vehicleId: "model-x-hero",
kind: "model-x",
driverActorId: "actor-karti",
sequence,
timestampMs,
pose: {
space: "geographic",
lat,
lng: -120,
altitudeM: 100,
headingDeg: 0,
pitchDeg: 0,
},
velocity: { xMps: 0, yMps: 0, zMps: 30, yawDegPerSec: 0 },
steering: 0,
wheelRadians: 2,
});
describe("realtime spatial protocol", () => {
it("validates all hierarchical interest cell kinds", () => {
const cells: InterestCell[] = [
{ kind: "california-tile", x: -4, y: 18, level: 6 },
{ kind: "city", cityId: "bay-area" },
{ kind: "office", officeId: "lumbridge-hq" },
{ kind: "floor", officeId: "lumbridge-hq", floorId: "level-1" },
{
kind: "room",
officeId: "lumbridge-hq",
floorId: "level-1",
roomId: "commons",
},
];
assert.ok(cells.every(isInterestCell));
assert.equal(isInterestCell({ kind: "city", cityId: "new-york" }), false);
assert.equal(isInterestCell({ kind: "california-tile", x: 0.5, y: 2, level: 4 }), false);
});
it("bounds sequence and timestamps before accepting an envelope", () => {
assert.equal(isRealtimeSequence(0), true);
assert.equal(isRealtimeSequence(4_294_967_295), true);
assert.equal(isRealtimeSequence(4_294_967_296), false);
assert.equal(isRealtimeTimestamp(1_765_000_000_000), true);
assert.equal(isRealtimeTimestamp(Number.MAX_SAFE_INTEGER), false);
});
it("accepts strict join/resume grants and membership revocation", () => {
const joinRequest = {
type: "join-request",
protocolVersion: 1,
requestId: "request-1",
actorId: "actor-karti",
resumeToken: null,
lastReceivedSequence: null,
interests: [{ kind: "city", cityId: "bay-area" }],
};
assert.equal(validateClientRealtimeMessage(joinRequest).ok, true);
const grant = {
type: "join-grant",
protocolVersion: 1,
requestId: "request-1",
sessionId: "session-1",
actorId: "actor-karti",
role: "member",
serverEpoch: "epoch-a",
serverTimeMs: 1_765_000_000_000,
nextSequence: 4,
resumeToken: "rotating-secret",
interests: joinRequest.interests,
initial: [localActor(3, 1_765_000_000_000, 0)],
};
assert.equal(validateServerRealtimeMessage(grant).ok, true);
assert.equal(validateClientRealtimeMessage({
type: "resume-request",
protocolVersion: 1,
requestId: "request-2",
sessionId: "session-1",
resumeToken: "rotating-secret",
serverEpoch: "epoch-a",
lastReceivedSequence: 3,
interests: joinRequest.interests,
}).ok, true);
assert.equal(validateServerRealtimeMessage({
type: "membership-revoked",
protocolVersion: 1,
serverEpoch: "epoch-a",
sequence: 5,
timestampMs: 1_765_000_000_010,
sessionId: "session-1",
actorId: "actor-karti",
reason: "membership-revoked",
reconnectAllowed: false,
}).ok, true);
});
it("rejects malformed, oversized, and non-finite server input", () => {
const actor = localActor(1, 1000, 0);
assert.equal(isEntityPoseSnapshot(actor), true);
assert.equal(isEntityPoseSnapshot({ ...actor, pose: { ...actor.pose, xM: Number.NaN } }), false);
assert.equal(validateServerRealtimeMessage({
type: "pose-delta",
protocolVersion: 1,
serverEpoch: "epoch-a",
sequence: 2,
timestampMs: 1000,
updates: [actor],
removedEntityIds: Array.from({ length: 2_049 }, (_, index) => `actor-${index}`),
}).ok, false);
assert.equal(validateServerRealtimeMessage({ type: "pose-delta", protocolVersion: 2 }).ok, false);
});
});
describe("authoritative motion validation", () => {
const limits = {
maximumHorizontalSpeedMps: 15,
maximumVerticalSpeedMps: 8,
maximumTurnRateDegPerSec: 180,
positionSlackM: 0.25,
};
it("accepts plausible actor movement and reports measured rates", () => {
const result = validateMotion(localActor(1, 1_000, 0), localActor(2, 2_000, 10), limits);
assert.equal(result.ok, true);
if (result.ok) {
assert.equal(result.horizontalSpeedMps, 9.75);
assert.equal(result.verticalSpeedMps, 0);
assert.equal(result.turnRateDegPerSec, 0);
}
});
it("rejects speed hacks, stale sequences, identity and interest-cell jumps", () => {
assert.deepEqual(validateMotion(localActor(1, 1_000, 0), localActor(2, 1_100, 50), limits), {
ok: false,
reason: "speed-exceeded",
});
assert.deepEqual(validateMotion(localActor(2, 2_000, 0), localActor(2, 3_000, 1), limits), {
ok: false,
reason: "time-invalid",
});
const another = { ...localActor(2, 2_000, 1), actorId: "actor-other" };
assert.deepEqual(validateMotion(localActor(1, 1_000, 0), another, limits), {
ok: false,
reason: "identity-mismatch",
});
const anotherFloor = localActor(2, 2_000, 1);
if (anotherFloor.pose.space === "local" && anotherFloor.pose.cell.kind === "floor") {
anotherFloor.pose.cell.floorId = "level-2";
}
assert.deepEqual(validateMotion(localActor(1, 1_000, 0), anotherFloor, limits), {
ok: false,
reason: "space-mismatch",
});
});
it("uses real geographic distance for statewide vehicle checks", () => {
const start = geographicVehicle(10, 10_000, 34);
const plausible = geographicVehicle(11, 11_000, 34.0002);
const result = validateMotion(start, plausible, {
maximumHorizontalSpeedMps: 40,
maximumVerticalSpeedMps: 5,
maximumTurnRateDegPerSec: 90,
positionSlackM: 1,
});
assert.equal(result.ok, true);
});
});
describe("pose interpolation buffer", () => {
it("interpolates deterministically at delayed server time", () => {
const a = new PoseInterpolationBuffer({ interpolationDelayMs: 100 });
const b = new PoseInterpolationBuffer({ interpolationDelayMs: 100 });
for (const buffer of [a, b]) {
assert.equal(buffer.push(localActor(1, 1_000, 0, 350)), true);
assert.equal(buffer.push(localActor(2, 1_200, 2, 10)), true);
}
const first = a.sample(1_200);
const second = b.sample(1_200);
assert.deepEqual(first, second);
assert.equal(first?.mode, "interpolated");
assert.equal(first?.timestampMs, 1_100);
assert.ok(first?.pose.space === "local");
if (first?.pose.space === "local") {
assert.equal(first.pose.xM, 1);
assert.equal(first.pose.headingDeg, 360, "heading follows the short arc through north");
}
});
it("caps extrapolation and then holds the capped predicted pose", () => {
const buffer = new PoseInterpolationBuffer({
interpolationDelayMs: 0,
maximumExtrapolationMs: 150,
});
buffer.push(localActor(1, 1_000, 0));
const predicted = buffer.sample(1_100);
assert.equal(predicted?.mode, "extrapolated");
assert.ok(predicted?.pose.space === "local");
if (predicted?.pose.space === "local") assert.equal(predicted.pose.xM, 1);
const capped = buffer.sample(10_000);
assert.equal(capped?.mode, "held");
assert.equal(capped?.timestampMs, 1_150);
assert.ok(capped?.pose.space === "local");
if (capped?.pose.space === "local") assert.equal(capped.pose.xM, 1.5);
});
it("rejects stale, duplicate, or mixed-entity input and caps capacity", () => {
const buffer = new PoseInterpolationBuffer({ capacity: 2 });
assert.equal(buffer.push(localActor(1, 1_000, 0)), true);
assert.equal(buffer.push(localActor(1, 1_100, 1)), false);
assert.equal(buffer.push(localActor(2, 1_200, 2)), true);
assert.equal(buffer.push(localActor(3, 1_300, 3)), true);
assert.equal(buffer.size(), 2);
assert.equal(buffer.push({ ...localActor(4, 1_400, 4), actorId: "other" }), false);
buffer.clear();
assert.equal(buffer.size(), 0);
assert.equal(buffer.sample(1_500), null);
});
});
+14
View File
@@ -74,6 +74,20 @@ describe("playable city scene actor", () => {
actor.dispose();
});
it("can exaggerate presentation scale without changing metre-space motion", () => {
const actor = createSceneActor(options({
sceneUnitsPerMetre: 0.001,
visualSceneUnitsPerMetre: 0.025,
active: true,
}));
assert.equal(actor.root.scale.x, 0.025);
actor.setActions({ forward: 1 });
actor.tick(0.2);
assert.ok(actor.state().z < 3);
assert.ok(Math.abs(actor.root.position.z - actor.state().z * 0.001) < 1e-12);
actor.dispose();
});
it("keeps its stable root and identity while replacing procedural rigs", () => {
const actor = createSceneActor(options({ active: true }));
const root = actor.root;
+9
View File
@@ -69,6 +69,15 @@ describe("walker input and clock", () => {
leaked.position.x = Number.NaN;
assert.deepEqual(controller.state().position, { x: 2, z: 2 });
});
it("uses and restores a normalized authored arrival facing", () => {
const controller = walker(planWith(), { facing: { x: 3, z: 4 } });
assert.deepEqual(controller.state().facing, { x: 0.6, z: 0.8 });
controller.tick(0.1, { x: -1, z: 0 });
assert.deepEqual(controller.state().facing, { x: -1, z: 0 });
controller.reset();
assert.deepEqual(controller.state().facing, { x: 0.6, z: 0.8 });
});
});
describe("walker collision", () => {