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