@incodetech/core 0.0.0-dev-20260126-4504c5b

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1221 @@
1
+ import { d as addEvent, n as eventModuleNames } from "./events-B8ZkhAZo.esm.js";
2
+ import { C as createManager, S as stopCameraStream, a as FaceDetectionProvider, c as OpenViduRecordingProvider, f as BrowserStorageProvider, h as StreamCanvasCapture, m as StreamCanvasProcessingSession, r as WasmUtilProvider, u as LocalRecordingProvider, x as requestCameraAccess } from "./src-DYtpbFY5.esm.js";
3
+ import "./getDeviceClass-DkfbtsIJ.esm.js";
4
+ import { a as fromPromise, i as fromCallback, n as setup, o as createActor, r as assign, t as endpoints } from "./endpoints-BUsSVoJV.esm.js";
5
+ import { a as uploadDeepsightVideo, c as getDeviceClass, i as stopRecording$1, n as createRecordingSession, o as checkPermission, r as startRecording, s as requestPermission, t as streamingEvents } from "./streamingEvents-CfEJv3xH.esm.js";
6
+ import { r as getToken, t as api } from "./api-DfRLAneb.esm.js";
7
+ import "./deepsightService-j5zMt6wf.esm.js";
8
+ import { t as addDeviceStats } from "./stats-DnU4uUFv.esm.js";
9
+
10
+ //#region src/modules/selfie/types.ts
11
+ const FACE_ERROR_CODES = {
12
+ FACE_OCCLUDED: "FACE_OCCLUDED",
13
+ LIVENESS: "LIVENESS_ERROR",
14
+ BRIGHTNESS: "BRIGHTNESS_ERROR",
15
+ LENSES: "LENSES_ERROR",
16
+ MASK: "MASK_ERROR",
17
+ CLOSED_EYES: "CLOSED_EYES_ERROR",
18
+ HEAD_COVER: "HEAD_COVER_ERROR",
19
+ SERVER: "SERVER_ERROR",
20
+ FACE_NOT_FOUND: "FACE_NOT_FOUND",
21
+ MULTIPLE_FACES: "MULTIPLE_FACES",
22
+ TOO_BLURRY: "TOO_BLURRY_ERROR",
23
+ TOO_DARK: "TOO_DARK_ERROR",
24
+ USER_IS_NOT_RECOGNIZED: "USER_IS_NOT_RECOGNIZED",
25
+ SPOOF_ATTEMPT_DETECTED: "SPOOF_ATTEMPT_DETECTED",
26
+ FACE_TOO_DARK: "FACE_TOO_DARK",
27
+ LENSES_DETECTED: "LENSES_DETECTED",
28
+ FACE_MASK_DETECTED: "FACE_MASK_DETECTED",
29
+ CLOSED_EYES_DETECTED: "CLOSED_EYES_DETECTED",
30
+ HEAD_COVER_DETECTED: "HEAD_COVER_DETECTED",
31
+ FACE_CROPPING_FAILED: "FACE_CROPPING_FAILED",
32
+ FACE_TOO_SMALL: "FACE_TOO_SMALL",
33
+ FACE_TOO_BLURRY: "FACE_TOO_BLURRY",
34
+ BAD_PHOTO_QUALITY: "BAD_PHOTO_QUALITY",
35
+ PROCESSING_ERROR: "PROCESSING_ERROR",
36
+ BAD_REQUEST: "BAD_REQUEST",
37
+ NONEXISTENT_CUSTOMER: "NONEXISTENT_CUSTOMER",
38
+ HINT_NOT_PROVIDED: "HINT_NOT_PROVIDED",
39
+ SELFIE_IMAGE_LOW_QUALITY: "SELFIE_IMAGE_LOW_QUALITY"
40
+ };
41
+
42
+ //#endregion
43
+ //#region src/modules/selfie/selfieErrorUtils.ts
44
+ const FACE_ERROR_CODE_VALUES = Object.values(FACE_ERROR_CODES);
45
+ const isFaceErrorCode = (value) => {
46
+ return FACE_ERROR_CODE_VALUES.includes(value);
47
+ };
48
+ const getFaceErrorCodeFromUnknown = (value) => {
49
+ if (value instanceof Error && typeof value.message === "string") return isFaceErrorCode(value.message) ? value.message : void 0;
50
+ if (typeof value === "string") return isFaceErrorCode(value) ? value : void 0;
51
+ };
52
+ const validateUploadResponse = ({ config, response }) => {
53
+ if (!response) return FACE_ERROR_CODES.SERVER;
54
+ if (response.confidence !== 0) return FACE_ERROR_CODES.LIVENESS;
55
+ if (config.validateBrightness && !response.isBright) return FACE_ERROR_CODES.BRIGHTNESS;
56
+ if (config.validateLenses && response.hasLenses) return FACE_ERROR_CODES.LENSES;
57
+ if (config.validateFaceMask && response.hasFaceMask) return FACE_ERROR_CODES.MASK;
58
+ if (config.validateClosedEyes && response.hasClosedEyes) return FACE_ERROR_CODES.CLOSED_EYES;
59
+ if (config.validateHeadCover && response.hasHeadCover) return FACE_ERROR_CODES.HEAD_COVER;
60
+ };
61
+
62
+ //#endregion
63
+ //#region src/modules/selfie/selfieUploadService.ts
64
+ /**
65
+ * Uploads a selfie image to the backend.
66
+ */
67
+ async function uploadSelfie({ encryptedBase64Image, faceCoordinates, signal, metadata, recordingId }) {
68
+ try {
69
+ const payload = {
70
+ base64Image: encryptedBase64Image,
71
+ faceCoordinates: faceCoordinates ?? void 0,
72
+ encrypted: true,
73
+ clientInfo: { deviceClass: getDeviceClass() },
74
+ metadata: metadata ?? void 0
75
+ };
76
+ const query = { imageType: "selfie" };
77
+ if (recordingId) query.recordingId = recordingId;
78
+ const res = await api.post(endpoints.selfie, payload, {
79
+ signal,
80
+ query
81
+ });
82
+ if (!res.ok) throw new Error(`POST ${endpoints.selfie} failed: ${res.status} ${res.statusText}`);
83
+ return res.data;
84
+ } catch (error) {
85
+ const errorCode = getFaceErrorCodeFromHttpError(error);
86
+ if (errorCode) throw new Error(errorCode);
87
+ throw new Error(FACE_ERROR_CODES.SERVER);
88
+ }
89
+ }
90
+ const getFaceErrorCodeFromHttpError = (error) => {
91
+ const err = error;
92
+ if (err.ok !== false || typeof err.status !== "number") return;
93
+ if (err.status !== 400) return FACE_ERROR_CODES.SERVER;
94
+ if (typeof err.data?.status !== "number") return FACE_ERROR_CODES.BAD_REQUEST;
95
+ return {
96
+ 3004: FACE_ERROR_CODES.FACE_NOT_FOUND,
97
+ 3005: FACE_ERROR_CODES.FACE_NOT_FOUND,
98
+ 3006: FACE_ERROR_CODES.TOO_BLURRY,
99
+ 3007: FACE_ERROR_CODES.TOO_DARK,
100
+ 4010: FACE_ERROR_CODES.MULTIPLE_FACES,
101
+ 4019: FACE_ERROR_CODES.FACE_NOT_FOUND,
102
+ 4077: FACE_ERROR_CODES.BAD_PHOTO_QUALITY,
103
+ 4078: FACE_ERROR_CODES.FACE_OCCLUDED
104
+ }[err.data.status] ?? FACE_ERROR_CODES.BAD_REQUEST;
105
+ };
106
+ async function processFace(imageType = "selfie", signal) {
107
+ return (await api.post(endpoints.processFace, {}, {
108
+ query: { imageType },
109
+ signal
110
+ })).data;
111
+ }
112
+
113
+ //#endregion
114
+ //#region src/modules/selfie/selfieServices.ts
115
+ function sendLabelInspectionEvent() {
116
+ return addDeviceStats({ cameraLabelInspectionStatus: "FAIL" });
117
+ }
118
+ const CAMERA_CONSTRAINTS = {
119
+ video: {
120
+ facingMode: "user",
121
+ height: { ideal: 480 },
122
+ width: { ideal: 640 }
123
+ },
124
+ audio: false
125
+ };
126
+ function stopStream(stream) {
127
+ stopCameraStream(stream);
128
+ }
129
+ async function initializeCamera(params) {
130
+ const { config, dependencies } = params;
131
+ const provider = new FaceDetectionProvider();
132
+ await provider.initialize({ autocaptureInterval: config.autoCaptureTimeout * 1e3 });
133
+ provider.setChecksEnabled({
134
+ lenses: config.validateLenses,
135
+ mask: config.validateFaceMask,
136
+ closedEyes: config.validateClosedEyes,
137
+ headWear: config.validateHeadCover,
138
+ occlusion: false
139
+ });
140
+ console.log("performPrcCheck init");
141
+ const timerName = `performPrcCheck ${Math.random()}`;
142
+ console.time(timerName);
143
+ await params.deepsightService.performPrcCheck({
144
+ constraints: { video: CAMERA_CONSTRAINTS.video },
145
+ ds: config.ds,
146
+ storage: dependencies.storage
147
+ });
148
+ console.timeEnd(timerName);
149
+ console.log("performPrcCheck end");
150
+ return {
151
+ stream: await requestCameraAccess({
152
+ video: CAMERA_CONSTRAINTS.video,
153
+ audio: false
154
+ }),
155
+ provider
156
+ };
157
+ }
158
+ /**
159
+ * Encrypts the provided selfie image using WASM.
160
+ */
161
+ async function encryptSelfieImage({ canvas, dependencies }) {
162
+ const base64Image = canvas.getBase64Image();
163
+ if (!base64Image) throw new Error("Canvas image is empty or null");
164
+ return (await dependencies.getWasmUtil()).encryptImage(base64Image);
165
+ }
166
+ function startDetection(params) {
167
+ let lastStatus;
168
+ let session;
169
+ const { provider } = params;
170
+ const setStatus = (status) => {
171
+ if (session?.isDisposed() === true) return;
172
+ if (lastStatus === status) return;
173
+ lastStatus = status;
174
+ params.onUpdate(status);
175
+ };
176
+ const stopDetectionLoop = () => {
177
+ session?.dispose();
178
+ };
179
+ const reset = () => {
180
+ provider.reset();
181
+ };
182
+ const cleanup = () => {
183
+ stopDetectionLoop();
184
+ };
185
+ (async () => {
186
+ try {
187
+ provider.setCallbacks({
188
+ onFarAway: () => setStatus("tooFar"),
189
+ onTooClose: () => setStatus("tooClose"),
190
+ onTooManyFaces: () => setStatus("tooManyFaces"),
191
+ onNoFace: () => setStatus("idle"),
192
+ onCenterFace: () => setStatus("centerFace"),
193
+ onGetReady: () => setStatus("getReady"),
194
+ onGetReadyFinished: () => setStatus("getReadyFinished"),
195
+ onDark: () => {
196
+ if (params.config.validateBrightness) setStatus("dark");
197
+ },
198
+ onBlur: () => setStatus("blur"),
199
+ onFaceAngle: () => setStatus("faceAngle"),
200
+ onLenses: () => {
201
+ if (params.config.validateLenses) setStatus("lenses");
202
+ },
203
+ onMask: () => {
204
+ if (params.config.validateFaceMask) setStatus("faceMask");
205
+ },
206
+ onEyesClosed: () => {
207
+ if (params.config.validateClosedEyes) setStatus("eyesClosed");
208
+ },
209
+ onHeadWear: () => {
210
+ if (params.config.validateHeadCover) setStatus("headWear");
211
+ },
212
+ onFaceOccluded: () => {},
213
+ onSwitchToManualCapture: () => {
214
+ setStatus("manualCapture");
215
+ stopDetectionLoop();
216
+ },
217
+ onCapture: (canvas, faceCoordinates) => {
218
+ setStatus("success");
219
+ params.onSuccess(canvas, faceCoordinates);
220
+ cleanup();
221
+ }
222
+ });
223
+ setStatus("detecting");
224
+ session = new StreamCanvasProcessingSession({
225
+ capturer: params.capturer,
226
+ provider,
227
+ onFrame: params.onFrame
228
+ });
229
+ } catch {
230
+ setStatus("error");
231
+ cleanup();
232
+ }
233
+ })();
234
+ return {
235
+ cleanup,
236
+ reset
237
+ };
238
+ }
239
+ function buildResolutionFromStream(stream) {
240
+ const track = stream.getVideoTracks()[0];
241
+ if (!track) return;
242
+ const settings = track.getSettings();
243
+ const width = settings.width;
244
+ const height = settings.height;
245
+ if (typeof width === "number" && typeof height === "number") return `${width}x${height}`;
246
+ }
247
+ async function startRecordingSession(params) {
248
+ if (params.config.deepsightLiveness === "VIDEOLIVENESS") return;
249
+ if (params.config.enableFaceRecording !== true) return;
250
+ if (params.existing) return params.existing;
251
+ const provider = params.config.recording?.capability ?? new OpenViduRecordingProvider();
252
+ const clonedStream = params.clonedStream;
253
+ const hasAudio = clonedStream.getAudioTracks().length > 0;
254
+ const resolution = buildResolutionFromStream(clonedStream);
255
+ const session = await createRecordingSession("selfie");
256
+ const connection = await provider.connect({
257
+ sessionToken: session.token,
258
+ stream: clonedStream,
259
+ events: {
260
+ onSessionConnected: (sessionId) => {
261
+ addEvent({
262
+ code: streamingEvents.strSessionDidConnect,
263
+ module: eventModuleNames.selfie,
264
+ payload: {
265
+ message: "Recording session connected",
266
+ sessionId
267
+ }
268
+ });
269
+ },
270
+ onSessionDisconnected: (sessionId) => {
271
+ addEvent({
272
+ code: streamingEvents.strSessionDidDisconnect,
273
+ module: eventModuleNames.selfie,
274
+ payload: {
275
+ message: "Recording session disconnected",
276
+ sessionId
277
+ }
278
+ });
279
+ },
280
+ onSessionException: (event) => {
281
+ addEvent({
282
+ code: streamingEvents.strSessionDidFailWithError,
283
+ module: eventModuleNames.selfie,
284
+ payload: {
285
+ message: "Recording session failed due to an error",
286
+ eventName: event.name,
287
+ type: "OpenViduException",
288
+ errorMessage: event.message,
289
+ sessionId: event.sessionId
290
+ }
291
+ });
292
+ },
293
+ onPublisherCreated: (p) => {
294
+ addEvent({
295
+ code: streamingEvents.strStreamPublisherCreated,
296
+ module: eventModuleNames.selfie,
297
+ payload: {
298
+ message: "Recording publisher created",
299
+ sessionId: p.sessionId,
300
+ streamId: p.streamId
301
+ }
302
+ });
303
+ },
304
+ onPublisherError: (p) => {
305
+ addEvent({
306
+ code: streamingEvents.strStreamPublisherDidFailWithError,
307
+ module: eventModuleNames.selfie,
308
+ payload: {
309
+ message: "Recording publisher failed due to an error",
310
+ sessionId: p.sessionId,
311
+ streamId: p.streamId,
312
+ error: { message: p.message ?? "Unknown error" }
313
+ }
314
+ });
315
+ }
316
+ }
317
+ });
318
+ await startRecording({
319
+ videoRecordingId: session.videoRecordingId,
320
+ type: "selfie",
321
+ resolution,
322
+ hasAudio
323
+ });
324
+ addEvent({
325
+ code: streamingEvents.strStreamVideoCaptureStart,
326
+ module: eventModuleNames.selfie,
327
+ payload: {
328
+ message: "Recording capture started",
329
+ resolution,
330
+ videoRecordingId: session.videoRecordingId,
331
+ sessionId: session.sessionId,
332
+ streamId: connection.publisher.getStreamId()
333
+ }
334
+ });
335
+ return {
336
+ token: session.token,
337
+ sessionId: session.sessionId,
338
+ videoRecordingId: session.videoRecordingId,
339
+ connection,
340
+ resolution,
341
+ hasAudio
342
+ };
343
+ }
344
+ function stopRecording(session) {
345
+ (async () => {
346
+ try {
347
+ addEvent({
348
+ code: streamingEvents.strStreamVideoCaptureStop,
349
+ module: eventModuleNames.selfie,
350
+ payload: {
351
+ message: "Recording capture stopped",
352
+ videoRecordingId: session.videoRecordingId,
353
+ sessionId: session.sessionId,
354
+ streamId: session.connection.publisher.getStreamId()
355
+ }
356
+ });
357
+ await stopRecording$1(session.videoRecordingId);
358
+ addEvent({
359
+ code: streamingEvents.strStreamPublisherDestroyed,
360
+ module: eventModuleNames.selfie,
361
+ payload: {
362
+ message: "Recording publisher destroyed",
363
+ sessionId: session.sessionId,
364
+ streamId: session.connection.publisher.getStreamId()
365
+ }
366
+ });
367
+ } finally {
368
+ await session.connection.disconnect();
369
+ addEvent({
370
+ code: streamingEvents.strSessionDidDisconnect,
371
+ module: eventModuleNames.selfie,
372
+ payload: {
373
+ message: "Recording session disconnected",
374
+ sessionId: session.sessionId
375
+ }
376
+ });
377
+ }
378
+ })();
379
+ }
380
+ async function initializeDeepsightSession() {
381
+ const { loadDeepsightSession } = await import("./deepsightLoader-BMT0FSg6.esm.js");
382
+ return loadDeepsightSession();
383
+ }
384
+
385
+ //#endregion
386
+ //#region src/modules/selfie/recordingService.ts
387
+ /**
388
+ * Records on-device and uploads encrypted video for liveness.
389
+ */
390
+ var LocalRecordingService = class {
391
+ constructor(wasmUtil, sessionToken) {
392
+ this.recorder = new LocalRecordingProvider();
393
+ this.sessionToken = sessionToken;
394
+ this.wasmUtil = wasmUtil;
395
+ }
396
+ async start(stream) {
397
+ this.recorder.startRecording(stream);
398
+ }
399
+ async stop() {
400
+ if (!this.recorder.isRecording) return { recordingId: null };
401
+ const result = await this.recorder.stopRecording(10, (base64) => this.wasmUtil.encryptImage(base64), (buffer) => this.wasmUtil.ckvcks(buffer));
402
+ if (!this.sessionToken) return { recordingId: null };
403
+ return { recordingId: await uploadDeepsightVideo(result.encryptedVideo, this.sessionToken) };
404
+ }
405
+ cleanup() {
406
+ this.recorder.reset();
407
+ }
408
+ };
409
+ /**
410
+ * Records through OpenVidu and manages the session lifecycle.
411
+ */
412
+ var OpenViduRecordingService = class {
413
+ constructor(config) {
414
+ this.provider = new OpenViduRecordingProvider();
415
+ this.config = config;
416
+ }
417
+ async start(stream) {
418
+ this.session = await startRecordingSession({
419
+ config: {
420
+ ...this.config,
421
+ recording: { capability: this.provider }
422
+ },
423
+ clonedStream: stream.clone(),
424
+ existing: this.session
425
+ });
426
+ }
427
+ async stop() {
428
+ if (this.session) {
429
+ stopRecording(this.session);
430
+ this.session = void 0;
431
+ }
432
+ return { recordingId: null };
433
+ }
434
+ cleanup() {
435
+ if (this.session) stopRecording(this.session);
436
+ }
437
+ };
438
+ /**
439
+ * Creates the recording service for the current configuration.
440
+ */
441
+ function createRecordingService(params) {
442
+ if (params.config.deepsightLiveness === "VIDEOLIVENESS") return new LocalRecordingService(params.wasmUtil, params.sessionToken);
443
+ if (params.config.enableFaceRecording === true) return new OpenViduRecordingService(params.config);
444
+ }
445
+
446
+ //#endregion
447
+ //#region src/modules/selfie/selfieStateMachine.ts
448
+ const _selfieMachine = setup({
449
+ types: {
450
+ context: {},
451
+ events: {},
452
+ input: {}
453
+ },
454
+ actors: {
455
+ checkPermission: fromPromise(async () => {
456
+ return checkPermission();
457
+ }),
458
+ requestPermission: fromPromise(async () => {
459
+ return requestPermission();
460
+ }),
461
+ initializeCamera: fromPromise(async ({ input }) => {
462
+ return initializeCamera({
463
+ config: input.config,
464
+ dependencies: input.dependencies,
465
+ deepsightService: input.deepsightService
466
+ });
467
+ }),
468
+ runDetection: fromCallback(({ input, sendBack }) => {
469
+ if (!input.frameCapturer || !input.provider) {
470
+ sendBack({
471
+ type: "DETECTION_UPDATE",
472
+ status: "error"
473
+ });
474
+ return () => {};
475
+ }
476
+ const { cleanup, reset } = startDetection({
477
+ config: input.config,
478
+ capturer: input.frameCapturer,
479
+ onUpdate: (status) => sendBack({
480
+ type: "DETECTION_UPDATE",
481
+ status
482
+ }),
483
+ onFrame: (frame) => sendBack({
484
+ type: "DETECTION_FRAME",
485
+ frame
486
+ }),
487
+ onSuccess: (canvas, faceCoordinates) => sendBack({
488
+ type: "DETECTION_SUCCESS",
489
+ canvas,
490
+ faceCoordinates
491
+ }),
492
+ provider: input.provider
493
+ });
494
+ sendBack({
495
+ type: "DETECTION_RESET_READY",
496
+ reset
497
+ });
498
+ return cleanup;
499
+ }),
500
+ initializeDeepsightSession: fromPromise(async () => {
501
+ return await initializeDeepsightSession();
502
+ }),
503
+ startRecording: fromPromise(async ({ input }) => {
504
+ if (!input.stream) return input.recordingService;
505
+ const wasmUtil = await input.dependencies.getWasmUtil();
506
+ const sessionToken = getToken();
507
+ const service = input.recordingService ?? createRecordingService({
508
+ config: input.config,
509
+ wasmUtil,
510
+ sessionToken
511
+ });
512
+ if (service) await service.start(input.stream);
513
+ return service;
514
+ }),
515
+ checkVirtualCamera: fromPromise(async ({ input }) => {
516
+ if (!input.deepsightService || !input.stream) return false;
517
+ const videoTrack = input.stream.getVideoTracks()[0];
518
+ if (!videoTrack) return false;
519
+ return input.deepsightService.checkVirtualCamera(videoTrack);
520
+ }),
521
+ uploadSelfie: fromPromise(async ({ input, signal }) => {
522
+ let recordingId = null;
523
+ const sessionToken = getToken();
524
+ try {
525
+ if (input.deepsightService) {
526
+ const [_, recordingServiceStopResult] = await Promise.all([
527
+ input.deepsightService.performVirtualCameraCheck(sessionToken, "SELFIE"),
528
+ input.recordingService?.stop(),
529
+ input.deepsightService.analyzeFrame(input.canvas.getImageData())
530
+ ]);
531
+ recordingId = recordingServiceStopResult?.recordingId ?? null;
532
+ }
533
+ } catch (error) {}
534
+ return uploadSelfie({
535
+ encryptedBase64Image: await encryptSelfieImage({
536
+ canvas: input.canvas,
537
+ dependencies: input.dependencies
538
+ }),
539
+ faceCoordinates: input.faceCoordinates,
540
+ metadata: input.deepsightService?.getMetadata(),
541
+ recordingId,
542
+ signal
543
+ });
544
+ }),
545
+ processFace: fromPromise(async ({ signal }) => {
546
+ return processFace("selfie", signal);
547
+ })
548
+ },
549
+ actions: {
550
+ stopMediaStream: assign(({ context }) => {
551
+ context.frameCapturer?.dispose();
552
+ if (context.stream) stopStream(context.stream);
553
+ context.provider?.dispose();
554
+ return {
555
+ stream: void 0,
556
+ provider: void 0,
557
+ frameCapturer: void 0
558
+ };
559
+ }),
560
+ setStreamAndCapturer: assign({
561
+ stream: ({ event }) => {
562
+ if ("output" in event) return event.output.stream;
563
+ },
564
+ provider: ({ event }) => {
565
+ if ("output" in event) return event.output.provider;
566
+ },
567
+ frameCapturer: ({ event }) => {
568
+ if ("output" in event) return new StreamCanvasCapture(event.output.stream);
569
+ }
570
+ }),
571
+ trackTutorialSelfie: () => {
572
+ addEvent({
573
+ code: "tutorialSelfie",
574
+ module: eventModuleNames.selfie,
575
+ payload: { tutorialSelfie: true }
576
+ });
577
+ },
578
+ trackContinue: () => {
579
+ addEvent({
580
+ code: "continue",
581
+ module: eventModuleNames.selfie
582
+ });
583
+ },
584
+ resetContext: assign(({ context }) => ({
585
+ stream: void 0,
586
+ provider: void 0,
587
+ frameCapturer: void 0,
588
+ error: void 0,
589
+ detectionStatus: "idle",
590
+ debugFrame: void 0,
591
+ capturedImage: void 0,
592
+ faceCoordinates: void 0,
593
+ uploadResponse: void 0,
594
+ processResponse: void 0,
595
+ recordingService: void 0,
596
+ attemptsRemaining: context.config.captureAttempts,
597
+ uploadError: void 0,
598
+ permissionResult: void 0,
599
+ resetDetection: void 0,
600
+ deepsightService: void 0
601
+ })),
602
+ resetDetection: ({ context }) => {
603
+ context.resetDetection?.();
604
+ },
605
+ captureImage: assign({ capturedImage: ({ context }) => {
606
+ if (context.capturedImage) return context.capturedImage;
607
+ return context.frameCapturer?.getLatestCanvas() ?? void 0;
608
+ } }),
609
+ captureLatestFrame: assign({ capturedImage: ({ context }) => {
610
+ return context.frameCapturer?.getLatestCanvas() ?? void 0;
611
+ } }),
612
+ clearUploadFailure: assign({
613
+ uploadError: () => void 0,
614
+ detectionStatus: () => "idle",
615
+ capturedImage: () => void 0
616
+ }),
617
+ clearStreamForRetry: assign({
618
+ stream: () => void 0,
619
+ provider: () => void 0,
620
+ frameCapturer: () => void 0
621
+ }),
622
+ decrementAttemptsRemaining: assign(({ context }) => ({ attemptsRemaining: context.attemptsRemaining - 1 })),
623
+ setUploadErrorFromUploadValidation: assign({ uploadError: ({ context }) => validateUploadResponse({
624
+ config: context.config,
625
+ response: context.uploadResponse
626
+ }) ?? FACE_ERROR_CODES.SERVER }),
627
+ clearRecordingService: assign({ recordingService: () => void 0 }),
628
+ cleanup: ({ context }) => {
629
+ context.deepsightService?.cleanup();
630
+ context.recordingService?.cleanup();
631
+ }
632
+ },
633
+ guards: {
634
+ hasShowTutorial: ({ context }) => context.config.showTutorial,
635
+ isPermissionGranted: ({ event }) => {
636
+ if ("output" in event) return event.output === "granted";
637
+ return false;
638
+ },
639
+ isPermissionDeniedError: ({ event }) => {
640
+ if ("error" in event) {
641
+ const error = event.error;
642
+ return error?.name === "NotAllowedError" || error?.name === "PermissionDeniedError";
643
+ }
644
+ return false;
645
+ },
646
+ hasStream: ({ context }) => context.stream !== void 0,
647
+ isCameraAndDeepsightReady: ({ context }) => context.stream !== void 0 && context.deepsightService !== void 0,
648
+ hasAttemptsRemaining: ({ context }) => context.attemptsRemaining > 0,
649
+ hasCapturedImage: ({ context }) => context.capturedImage !== void 0,
650
+ hasUploadValidationError: ({ context }) => validateUploadResponse({
651
+ config: context.config,
652
+ response: context.uploadResponse
653
+ }) !== void 0
654
+ }
655
+ }).createMachine({
656
+ id: "selfie",
657
+ initial: "idle",
658
+ context: ({ input }) => ({
659
+ config: input.config,
660
+ dependencies: input.dependencies,
661
+ stream: void 0,
662
+ provider: void 0,
663
+ frameCapturer: void 0,
664
+ error: void 0,
665
+ detectionStatus: "idle",
666
+ debugFrame: void 0,
667
+ capturedImage: void 0,
668
+ faceCoordinates: void 0,
669
+ uploadResponse: void 0,
670
+ processResponse: void 0,
671
+ recordingService: void 0,
672
+ attemptsRemaining: input.config.captureAttempts,
673
+ uploadError: void 0,
674
+ permissionResult: void 0,
675
+ resetDetection: void 0,
676
+ deepsightService: void 0
677
+ }),
678
+ on: { QUIT: { target: "#selfie.closed" } },
679
+ states: {
680
+ idle: { on: { LOAD: [{
681
+ target: "tutorial",
682
+ guard: "hasShowTutorial"
683
+ }, { target: "loading" }] } },
684
+ loading: { invoke: [{
685
+ id: "checkPermissionLoading",
686
+ src: "checkPermission",
687
+ onDone: [{
688
+ target: "capture",
689
+ guard: "isPermissionGranted",
690
+ actions: assign({ permissionResult: ({ event }) => event.output })
691
+ }, {
692
+ target: "permissions",
693
+ actions: assign({ permissionResult: ({ event }) => event.output })
694
+ }]
695
+ }, {
696
+ id: "loadingInitDeepsight",
697
+ src: "initializeDeepsightSession",
698
+ onDone: { actions: assign({ deepsightService: ({ event }) => event.output }) },
699
+ onError: { actions: () => void 0 }
700
+ }] },
701
+ tutorial: {
702
+ initial: "checkingPermission",
703
+ entry: "trackTutorialSelfie",
704
+ states: {
705
+ checkingPermission: { invoke: {
706
+ id: "checkPermissionTutorial",
707
+ src: "checkPermission",
708
+ onDone: [{
709
+ target: "initializingCamera",
710
+ guard: "isPermissionGranted",
711
+ actions: assign({ permissionResult: ({ event }) => event.output })
712
+ }, {
713
+ target: "ready",
714
+ actions: assign({ permissionResult: ({ event }) => event.output })
715
+ }]
716
+ } },
717
+ initializingCamera: {
718
+ type: "parallel",
719
+ states: {
720
+ cameraInit: {
721
+ initial: "initializingDeepsight",
722
+ states: {
723
+ initializingDeepsight: { invoke: {
724
+ id: "tutorialInitDeepsight",
725
+ src: "initializeDeepsightSession",
726
+ onDone: {
727
+ target: "initializingStream",
728
+ actions: assign({ deepsightService: ({ event }) => event.output })
729
+ },
730
+ onError: { target: "#selfie.tutorial.ready" }
731
+ } },
732
+ initializingStream: { invoke: {
733
+ id: "tutorialInitCamera",
734
+ src: "initializeCamera",
735
+ input: ({ context }) => ({
736
+ config: context.config,
737
+ dependencies: context.dependencies,
738
+ deepsightService: context.deepsightService
739
+ }),
740
+ onDone: {
741
+ target: "ready",
742
+ actions: "setStreamAndCapturer"
743
+ },
744
+ onError: [{
745
+ target: "#selfie.tutorial.ready",
746
+ guard: "isPermissionDeniedError",
747
+ actions: assign({ permissionResult: () => "denied" })
748
+ }, {
749
+ target: "#selfie.tutorial.ready",
750
+ actions: assign({ error: ({ event }) => String(event.error) })
751
+ }]
752
+ } },
753
+ ready: { type: "final" }
754
+ }
755
+ },
756
+ userIntent: {
757
+ initial: "waiting",
758
+ states: {
759
+ waiting: { on: { NEXT_STEP: {
760
+ target: "clicked",
761
+ actions: "trackContinue"
762
+ } } },
763
+ clicked: { type: "final" }
764
+ }
765
+ }
766
+ },
767
+ onDone: { target: "#selfie.capture" }
768
+ },
769
+ ready: {
770
+ initial: "idle",
771
+ states: {
772
+ idle: { always: [{
773
+ target: "initializingDeepsight",
774
+ guard: ({ context }) => context.deepsightService === void 0
775
+ }, { target: "readyForNext" }] },
776
+ initializingDeepsight: { invoke: {
777
+ id: "initializeDeepsightTutorial",
778
+ src: "initializeDeepsightSession",
779
+ onDone: {
780
+ target: "readyForNext",
781
+ actions: assign({ deepsightService: ({ event }) => event.output })
782
+ },
783
+ onError: { target: "readyForNext" }
784
+ } },
785
+ readyForNext: { on: { NEXT_STEP: {
786
+ target: "#selfie.tutorial.waitingForPermission",
787
+ actions: "trackContinue"
788
+ } } }
789
+ }
790
+ },
791
+ waitingForPermission: { invoke: {
792
+ id: "checkPermissionWaiting",
793
+ src: "checkPermission",
794
+ onDone: [{
795
+ target: "#selfie.capture",
796
+ guard: "isPermissionGranted",
797
+ actions: assign({ permissionResult: ({ event }) => event.output })
798
+ }, {
799
+ target: "#selfie.permissions",
800
+ actions: assign({ permissionResult: ({ event }) => event.output })
801
+ }]
802
+ } }
803
+ }
804
+ },
805
+ permissions: {
806
+ initial: "checkingDeepsight",
807
+ states: {
808
+ checkingDeepsight: { always: [{
809
+ target: "initializingDeepsight",
810
+ guard: ({ context }) => context.deepsightService === void 0
811
+ }, { target: "idle" }] },
812
+ initializingDeepsight: { invoke: {
813
+ id: "initializeDeepsightPerms",
814
+ src: "initializeDeepsightSession",
815
+ onDone: {
816
+ target: "idle",
817
+ actions: assign({ deepsightService: ({ event }) => event.output })
818
+ },
819
+ onError: { target: "idle" }
820
+ } },
821
+ idle: {
822
+ invoke: {
823
+ id: "checkPermissionIdle",
824
+ src: "checkPermission",
825
+ onDone: [{
826
+ target: "#selfie.capture",
827
+ guard: "isPermissionGranted",
828
+ actions: assign({ permissionResult: ({ event }) => event.output })
829
+ }, {
830
+ target: "denied",
831
+ guard: ({ event }) => event.output === "denied",
832
+ actions: assign({ permissionResult: ({ event }) => event.output })
833
+ }]
834
+ },
835
+ on: {
836
+ REQUEST_PERMISSION: "requesting",
837
+ GO_TO_LEARN_MORE: "learnMore"
838
+ }
839
+ },
840
+ learnMore: { on: {
841
+ BACK: "idle",
842
+ REQUEST_PERMISSION: "requesting"
843
+ } },
844
+ requesting: { invoke: {
845
+ id: "requestPermission",
846
+ src: "requestPermission",
847
+ onDone: [
848
+ {
849
+ target: "#selfie.capture",
850
+ guard: "isPermissionGranted",
851
+ actions: assign({ permissionResult: ({ event }) => event.output })
852
+ },
853
+ {
854
+ target: "denied",
855
+ guard: ({ event }) => event.output === "denied",
856
+ actions: assign({ permissionResult: ({ event }) => event.output })
857
+ },
858
+ {
859
+ target: "idle",
860
+ actions: assign({ permissionResult: ({ event }) => event.output })
861
+ }
862
+ ],
863
+ onError: { target: "denied" }
864
+ } },
865
+ denied: { entry: assign({ permissionResult: () => "refresh" }) }
866
+ }
867
+ },
868
+ capture: {
869
+ initial: "checkingDeepsight",
870
+ exit: [
871
+ "stopMediaStream",
872
+ "cleanup",
873
+ "clearRecordingService"
874
+ ],
875
+ states: {
876
+ checkingDeepsight: { always: [{
877
+ target: "initializingDeepsight",
878
+ guard: ({ context }) => context.deepsightService === void 0
879
+ }, { target: "checkingStream" }] },
880
+ initializingDeepsight: { invoke: {
881
+ id: "initializeDeepsightCapture",
882
+ src: "initializeDeepsightSession",
883
+ onDone: {
884
+ target: "checkingStream",
885
+ actions: [assign({ deepsightService: ({ event }) => event.output })]
886
+ },
887
+ onError: { target: "#selfie.permissions" }
888
+ } },
889
+ checkingStream: { always: [
890
+ {
891
+ target: "initializingDeepsight",
892
+ guard: ({ context }) => context.deepsightService === void 0
893
+ },
894
+ {
895
+ target: "detecting",
896
+ guard: "hasStream"
897
+ },
898
+ { target: "initializing" }
899
+ ] },
900
+ initializing: { invoke: {
901
+ id: "initializeCamera",
902
+ src: "initializeCamera",
903
+ input: ({ context }) => ({
904
+ config: context.config,
905
+ dependencies: context.dependencies,
906
+ deepsightService: context.deepsightService
907
+ }),
908
+ onDone: {
909
+ target: "detecting",
910
+ actions: ["setStreamAndCapturer"]
911
+ },
912
+ onError: [{
913
+ target: "#selfie.permissions",
914
+ guard: "isPermissionDeniedError",
915
+ actions: assign({ permissionResult: () => "denied" })
916
+ }, {
917
+ target: "#selfie.error",
918
+ actions: assign({ error: ({ event }) => String(event.error) })
919
+ }]
920
+ } },
921
+ detecting: {
922
+ entry: [assign({ detectionStatus: () => "detecting" })],
923
+ invoke: [
924
+ {
925
+ id: "checkVirtualCamera",
926
+ src: "checkVirtualCamera",
927
+ input: ({ context }) => ({
928
+ stream: context.stream,
929
+ deepsightService: context.deepsightService
930
+ }),
931
+ onDone: [{
932
+ target: "#selfie.processing",
933
+ guard: ({ event }) => event.output === true,
934
+ actions: () => {
935
+ sendLabelInspectionEvent();
936
+ }
937
+ }],
938
+ onError: { actions: () => void 0 }
939
+ },
940
+ {
941
+ id: "startRecording",
942
+ src: "startRecording",
943
+ input: ({ context }) => ({
944
+ config: context.config,
945
+ dependencies: context.dependencies,
946
+ recordingService: context.recordingService,
947
+ stream: context.stream
948
+ }),
949
+ onDone: { actions: assign({ recordingService: ({ context, event }) => {
950
+ return event.output ?? context.recordingService;
951
+ } }) },
952
+ onError: { actions: () => void 0 }
953
+ },
954
+ {
955
+ id: "runDetection",
956
+ src: "runDetection",
957
+ input: ({ context }) => ({
958
+ frameCapturer: context.frameCapturer,
959
+ provider: context.provider,
960
+ config: context.config
961
+ })
962
+ }
963
+ ],
964
+ on: {
965
+ DETECTION_UPDATE: { actions: assign({ detectionStatus: ({ event }) => event.status }) },
966
+ DETECTION_FRAME: { actions: assign({ debugFrame: ({ event }) => event.frame }) },
967
+ DETECTION_RESET_READY: { actions: assign({ resetDetection: ({ event }) => event.reset }) },
968
+ DETECTION_SUCCESS: {
969
+ target: "capturing",
970
+ actions: assign({
971
+ capturedImage: ({ event }) => event.canvas,
972
+ faceCoordinates: ({ event }) => event.faceCoordinates
973
+ })
974
+ },
975
+ MANUAL_CAPTURE: { target: "capturingManual" }
976
+ }
977
+ },
978
+ capturing: {
979
+ entry: ["captureImage"],
980
+ always: [{
981
+ target: "uploading",
982
+ guard: "hasCapturedImage"
983
+ }, {
984
+ target: "uploadError",
985
+ actions: assign(({ context }) => ({
986
+ uploadError: FACE_ERROR_CODES.SERVER,
987
+ attemptsRemaining: context.attemptsRemaining - 1
988
+ }))
989
+ }]
990
+ },
991
+ capturingManual: {
992
+ entry: ["captureLatestFrame"],
993
+ always: [{
994
+ target: "uploading",
995
+ guard: "hasCapturedImage"
996
+ }, {
997
+ target: "uploadError",
998
+ actions: assign(({ context }) => ({
999
+ uploadError: FACE_ERROR_CODES.SERVER,
1000
+ attemptsRemaining: context.attemptsRemaining - 1
1001
+ }))
1002
+ }]
1003
+ },
1004
+ uploading: { invoke: {
1005
+ id: "uploadSelfie",
1006
+ src: "uploadSelfie",
1007
+ input: ({ context }) => {
1008
+ const canvas = context.capturedImage;
1009
+ if (!canvas) throw new Error(FACE_ERROR_CODES.SERVER);
1010
+ return {
1011
+ canvas,
1012
+ faceCoordinates: context.faceCoordinates,
1013
+ deepsightService: context.deepsightService,
1014
+ dependencies: context.dependencies,
1015
+ recordingService: context.recordingService
1016
+ };
1017
+ },
1018
+ onDone: {
1019
+ target: "validatingUpload",
1020
+ actions: assign({ uploadResponse: ({ event }) => event.output })
1021
+ },
1022
+ onError: {
1023
+ target: "uploadError",
1024
+ actions: assign(({ context, event }) => ({
1025
+ uploadError: getFaceErrorCodeFromUnknown(event.error) ?? FACE_ERROR_CODES.SERVER,
1026
+ attemptsRemaining: context.attemptsRemaining - 1
1027
+ }))
1028
+ }
1029
+ } },
1030
+ validatingUpload: { always: [{
1031
+ target: "uploadError",
1032
+ guard: "hasUploadValidationError",
1033
+ actions: ["setUploadErrorFromUploadValidation", "decrementAttemptsRemaining"]
1034
+ }, { target: "success" }] },
1035
+ uploadError: { on: { RETRY_CAPTURE: {
1036
+ target: "checkingStream",
1037
+ guard: "hasAttemptsRemaining",
1038
+ actions: [
1039
+ "resetDetection",
1040
+ "clearUploadFailure",
1041
+ "clearStreamForRetry"
1042
+ ]
1043
+ } } },
1044
+ success: {
1045
+ entry: "cleanup",
1046
+ after: { 3e3: { target: "#selfie.processing" } }
1047
+ }
1048
+ }
1049
+ },
1050
+ processing: { invoke: {
1051
+ id: "processFace",
1052
+ src: "processFace",
1053
+ onDone: {
1054
+ target: "finished",
1055
+ actions: assign({ processResponse: ({ event }) => event.output })
1056
+ },
1057
+ onError: { target: "finished" }
1058
+ } },
1059
+ finished: {
1060
+ entry: "stopMediaStream",
1061
+ type: "final"
1062
+ },
1063
+ closed: {
1064
+ entry: "stopMediaStream",
1065
+ type: "final"
1066
+ },
1067
+ error: {
1068
+ entry: "stopMediaStream",
1069
+ on: { RESET: {
1070
+ target: "idle",
1071
+ actions: "resetContext"
1072
+ } }
1073
+ }
1074
+ }
1075
+ });
1076
+ /**
1077
+ * The selfie capture state machine.
1078
+ *
1079
+ * Note: Uses AnyStateMachine type for declaration file portability.
1080
+ * Type safety is ensured via the machine configuration.
1081
+ */
1082
+ const selfieMachine = _selfieMachine;
1083
+
1084
+ //#endregion
1085
+ //#region src/modules/selfie/selfieActor.ts
1086
+ function createSelfieActor(options) {
1087
+ const dependencies = options.dependencies ?? {
1088
+ storage: new BrowserStorageProvider(),
1089
+ getWasmUtil: () => WasmUtilProvider.getInstance()
1090
+ };
1091
+ return createActor(selfieMachine, { input: {
1092
+ config: options.config,
1093
+ dependencies
1094
+ } }).start();
1095
+ }
1096
+
1097
+ //#endregion
1098
+ //#region src/modules/selfie/selfieManager.ts
1099
+ function getPermissionStatus(snapshot) {
1100
+ if (!snapshot.matches("permissions")) return;
1101
+ if (snapshot.matches({ permissions: "idle" })) return "idle";
1102
+ if (snapshot.matches({ permissions: "learnMore" })) return "learnMore";
1103
+ if (snapshot.matches({ permissions: "requesting" })) return "requesting";
1104
+ if (snapshot.matches({ permissions: "denied" })) return "denied";
1105
+ }
1106
+ function getCaptureStatus(snapshot) {
1107
+ if (snapshot.matches({ capture: "initializing" })) return "initializing";
1108
+ if (snapshot.matches({ capture: "detecting" })) return "detecting";
1109
+ if (snapshot.matches({ capture: "capturing" })) return "capturing";
1110
+ if (snapshot.matches({ capture: "capturingManual" })) return "capturing";
1111
+ if (snapshot.matches({ capture: "uploading" })) return "uploading";
1112
+ if (snapshot.matches({ capture: "uploadError" })) return "uploadError";
1113
+ if (snapshot.matches({ capture: "success" })) return "success";
1114
+ }
1115
+ function mapState(snapshot) {
1116
+ const { context } = snapshot;
1117
+ if (snapshot.matches("idle")) return { status: "idle" };
1118
+ if (snapshot.matches("loading")) return { status: "loading" };
1119
+ if (snapshot.matches("tutorial")) return { status: "tutorial" };
1120
+ if (snapshot.matches("closed")) return { status: "closed" };
1121
+ if (snapshot.matches("permissions")) {
1122
+ const permissionStatus = getPermissionStatus(snapshot);
1123
+ if (permissionStatus === void 0) return {
1124
+ status: "permissions",
1125
+ permissionStatus: "idle"
1126
+ };
1127
+ return {
1128
+ status: "permissions",
1129
+ permissionStatus
1130
+ };
1131
+ }
1132
+ if (snapshot.matches("capture")) return {
1133
+ status: "capture",
1134
+ captureStatus: getCaptureStatus(snapshot) ?? "initializing",
1135
+ stream: context.stream,
1136
+ detectionStatus: context.detectionStatus,
1137
+ debugFrame: context.debugFrame,
1138
+ attemptsRemaining: context.attemptsRemaining,
1139
+ uploadError: context.uploadError
1140
+ };
1141
+ if (snapshot.matches("processing")) return { status: "processing" };
1142
+ if (snapshot.matches("finished")) return {
1143
+ status: "finished",
1144
+ processResponse: context.processResponse
1145
+ };
1146
+ if (snapshot.matches("error")) return {
1147
+ status: "error",
1148
+ error: context.error ?? "Unknown error"
1149
+ };
1150
+ return { status: "idle" };
1151
+ }
1152
+ function createApi({ actor }) {
1153
+ return {
1154
+ load() {
1155
+ actor.send({ type: "LOAD" });
1156
+ },
1157
+ nextStep() {
1158
+ actor.send({ type: "NEXT_STEP" });
1159
+ },
1160
+ requestPermission() {
1161
+ actor.send({ type: "REQUEST_PERMISSION" });
1162
+ },
1163
+ goToLearnMore() {
1164
+ actor.send({ type: "GO_TO_LEARN_MORE" });
1165
+ },
1166
+ back() {
1167
+ actor.send({ type: "BACK" });
1168
+ },
1169
+ close() {
1170
+ actor.send({ type: "QUIT" });
1171
+ },
1172
+ reset() {
1173
+ actor.send({ type: "RESET" });
1174
+ },
1175
+ retryCapture() {
1176
+ actor.send({ type: "RETRY_CAPTURE" });
1177
+ },
1178
+ capture() {
1179
+ actor.send({ type: "MANUAL_CAPTURE" });
1180
+ }
1181
+ };
1182
+ }
1183
+ /**
1184
+ * Creates a selfie manager instance for handling selfie capture flow.
1185
+ *
1186
+ * The selfie manager provides:
1187
+ * - State management with statuses: `idle`, `loading`, `tutorial`, `permissions`, `capture`, `finished`, `closed`, `error`
1188
+ * - Permission handling with nested states: `idle`, `requesting`, `denied`, `learnMore`
1189
+ * - Capture handling with nested states: `initializing`, `startingRecorder`, `recordingActive`, `detecting`, `capturing`, `uploading`, `uploadError`, `success`
1190
+ * - Camera stream access when in `capture` state
1191
+ * - Detection status feedback during face detection
1192
+ * - Attempt tracking with `attemptsRemaining`
1193
+ *
1194
+ * @param options - Configuration for the selfie actor
1195
+ * @param options.config - The selfie module configuration from the flow
1196
+ * @returns A manager instance with state subscription, API methods, and lifecycle controls
1197
+ *
1198
+ * @example
1199
+ * ```ts
1200
+ * const selfieManager = createSelfieManager({ config: selfieConfig });
1201
+ *
1202
+ * selfieManager.subscribe((state) => {
1203
+ * if (state.status === 'capture') {
1204
+ * console.log('Camera ready:', state.stream);
1205
+ * console.log('Detection status:', state.detectionStatus);
1206
+ * }
1207
+ * });
1208
+ *
1209
+ * selfieManager.load();
1210
+ * ```
1211
+ */
1212
+ function createSelfieManager(options) {
1213
+ return createManager({
1214
+ actor: createSelfieActor(options),
1215
+ mapState,
1216
+ createApi
1217
+ });
1218
+ }
1219
+
1220
+ //#endregion
1221
+ export { createSelfieManager, processFace, selfieMachine };