126 lines
4.1 KiB
TypeScript
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" }),
|
|
};
|
|
}
|