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

126 lines
4.1 KiB
TypeScript

/**
* 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" }),
};
}