@incodetech/core 2.0.0-alpha.1 → 2.0.0-alpha.10

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.
Files changed (106) hide show
  1. package/dist/Manager-BGfxEmyv.d.ts +19 -0
  2. package/dist/OpenViduLogger-BdPfiZO6.esm.js +3 -0
  3. package/dist/OpenViduLogger-CQyDxBvM.esm.js +803 -0
  4. package/dist/StateMachine-DRE1oH2B.d.ts +2 -0
  5. package/dist/addEvent-W0ORK0jT.esm.js +16 -0
  6. package/dist/chunk-C_Yo44FK.esm.js +49 -0
  7. package/dist/email.d.ts +264 -0
  8. package/dist/email.esm.js +479 -0
  9. package/dist/endpoints-BSTFaHYo.esm.js +1706 -0
  10. package/dist/flow.d.ts +578 -0
  11. package/dist/flow.esm.js +628 -0
  12. package/dist/index.d.ts +226 -0
  13. package/dist/index.esm.js +155 -0
  14. package/dist/lib-Bu9XGMBW.esm.js +11700 -0
  15. package/dist/permissionServices-I6vX6DBy.esm.js +72 -0
  16. package/dist/phone.d.ts +292 -0
  17. package/dist/phone.esm.js +550 -0
  18. package/dist/selfie.d.ts +759 -0
  19. package/dist/selfie.esm.js +995 -0
  20. package/dist/types-iZi2rawo.d.ts +5 -0
  21. package/dist/warmup-CEJTfxQr.d.ts +55 -0
  22. package/dist/xstate.esm-B_rda9yU.esm.js +3261 -0
  23. package/package.json +14 -11
  24. package/src/camera/cameraActor.ts +0 -21
  25. package/src/camera/cameraService.test.ts +0 -437
  26. package/src/camera/cameraService.ts +0 -165
  27. package/src/camera/cameraServices.test.ts +0 -66
  28. package/src/camera/cameraServices.ts +0 -26
  29. package/src/camera/cameraStateMachine.test.ts +0 -602
  30. package/src/camera/cameraStateMachine.ts +0 -264
  31. package/src/camera/index.ts +0 -5
  32. package/src/camera/types.ts +0 -17
  33. package/src/device/getBrowser.ts +0 -31
  34. package/src/device/getDeviceClass.ts +0 -29
  35. package/src/device/index.ts +0 -2
  36. package/src/email/__mocks__/emailMocks.ts +0 -59
  37. package/src/email/emailActor.ts +0 -15
  38. package/src/email/emailManager.test.ts +0 -573
  39. package/src/email/emailManager.ts +0 -427
  40. package/src/email/emailServices.ts +0 -66
  41. package/src/email/emailStateMachine.test.ts +0 -741
  42. package/src/email/emailStateMachine.ts +0 -367
  43. package/src/email/index.ts +0 -39
  44. package/src/email/types.ts +0 -60
  45. package/src/events/addEvent.ts +0 -20
  46. package/src/events/types.ts +0 -7
  47. package/src/flow/__mocks__/flowMocks.ts +0 -84
  48. package/src/flow/flowActor.ts +0 -13
  49. package/src/flow/flowAnalyzer.test.ts +0 -266
  50. package/src/flow/flowAnalyzer.ts +0 -37
  51. package/src/flow/flowCompletionService.ts +0 -21
  52. package/src/flow/flowManager.test.ts +0 -560
  53. package/src/flow/flowManager.ts +0 -235
  54. package/src/flow/flowServices.test.ts +0 -109
  55. package/src/flow/flowServices.ts +0 -13
  56. package/src/flow/flowStateMachine.test.ts +0 -334
  57. package/src/flow/flowStateMachine.ts +0 -182
  58. package/src/flow/index.ts +0 -21
  59. package/src/flow/moduleLoader.test.ts +0 -136
  60. package/src/flow/moduleLoader.ts +0 -73
  61. package/src/flow/orchestratedFlowManager.test.ts +0 -240
  62. package/src/flow/orchestratedFlowManager.ts +0 -231
  63. package/src/flow/orchestratedFlowStateMachine.test.ts +0 -199
  64. package/src/flow/orchestratedFlowStateMachine.ts +0 -325
  65. package/src/flow/types.ts +0 -434
  66. package/src/http/__mocks__/api.ts +0 -88
  67. package/src/http/api.test.ts +0 -231
  68. package/src/http/api.ts +0 -90
  69. package/src/http/endpoints.ts +0 -17
  70. package/src/index.ts +0 -33
  71. package/src/permissions/index.ts +0 -2
  72. package/src/permissions/permissionServices.ts +0 -31
  73. package/src/permissions/types.ts +0 -3
  74. package/src/phone/__mocks__/phoneMocks.ts +0 -71
  75. package/src/phone/index.ts +0 -39
  76. package/src/phone/phoneActor.ts +0 -15
  77. package/src/phone/phoneManager.test.ts +0 -393
  78. package/src/phone/phoneManager.ts +0 -458
  79. package/src/phone/phoneServices.ts +0 -98
  80. package/src/phone/phoneStateMachine.test.ts +0 -918
  81. package/src/phone/phoneStateMachine.ts +0 -422
  82. package/src/phone/types.ts +0 -83
  83. package/src/recordings/recordingsRepository.test.ts +0 -87
  84. package/src/recordings/recordingsRepository.ts +0 -48
  85. package/src/recordings/streamingEvents.ts +0 -10
  86. package/src/selfie/__mocks__/selfieMocks.ts +0 -26
  87. package/src/selfie/index.ts +0 -14
  88. package/src/selfie/selfieActor.ts +0 -17
  89. package/src/selfie/selfieErrorUtils.test.ts +0 -116
  90. package/src/selfie/selfieErrorUtils.ts +0 -66
  91. package/src/selfie/selfieManager.test.ts +0 -297
  92. package/src/selfie/selfieManager.ts +0 -301
  93. package/src/selfie/selfieServices.ts +0 -362
  94. package/src/selfie/selfieStateMachine.test.ts +0 -283
  95. package/src/selfie/selfieStateMachine.ts +0 -804
  96. package/src/selfie/selfieUploadService.test.ts +0 -90
  97. package/src/selfie/selfieUploadService.ts +0 -81
  98. package/src/selfie/types.ts +0 -103
  99. package/src/session/index.ts +0 -5
  100. package/src/session/sessionService.ts +0 -78
  101. package/src/setup.test.ts +0 -61
  102. package/src/setup.ts +0 -171
  103. package/tsconfig.json +0 -13
  104. package/tsdown.config.ts +0 -22
  105. package/vitest.config.ts +0 -37
  106. package/vitest.setup.ts +0 -135
@@ -0,0 +1,995 @@
1
+ import { n as requestPermission, r as getDeviceClass, t as checkPermission } from "./permissionServices-I6vX6DBy.esm.js";
2
+ import { c as FaceDetectionProvider, d as StreamCanvasProcessingSession, f as StreamCanvasCapture, g as createManager, h as stopCameraStream, m as requestCameraAccess, n as api, s as WasmUtilProvider, t as endpoints, u as OpenViduRecordingProvider } from "./endpoints-BSTFaHYo.esm.js";
3
+ import { a as createActor, i as fromPromise, n as assign, r as fromCallback, t as setup } from "./xstate.esm-B_rda9yU.esm.js";
4
+ import { t as addEvent } from "./addEvent-W0ORK0jT.esm.js";
5
+
6
+ //#region src/modules/selfie/types.ts
7
+ const FACE_ERROR_CODES = {
8
+ FACE_OCCLUDED: "FACE_OCCLUDED",
9
+ LIVENESS: "LIVENESS_ERROR",
10
+ BRIGHTNESS: "BRIGHTNESS_ERROR",
11
+ LENSES: "LENSES_ERROR",
12
+ MASK: "MASK_ERROR",
13
+ CLOSED_EYES: "CLOSED_EYES_ERROR",
14
+ HEAD_COVER: "HEAD_COVER_ERROR",
15
+ SERVER: "SERVER_ERROR",
16
+ FACE_NOT_FOUND: "FACE_NOT_FOUND",
17
+ MULTIPLE_FACES: "MULTIPLE_FACES",
18
+ TOO_BLURRY: "TOO_BLURRY_ERROR",
19
+ TOO_DARK: "TOO_DARK_ERROR",
20
+ USER_IS_NOT_RECOGNIZED: "USER_IS_NOT_RECOGNIZED",
21
+ SPOOF_ATTEMPT_DETECTED: "SPOOF_ATTEMPT_DETECTED",
22
+ FACE_TOO_DARK: "FACE_TOO_DARK",
23
+ LENSES_DETECTED: "LENSES_DETECTED",
24
+ FACE_MASK_DETECTED: "FACE_MASK_DETECTED",
25
+ CLOSED_EYES_DETECTED: "CLOSED_EYES_DETECTED",
26
+ HEAD_COVER_DETECTED: "HEAD_COVER_DETECTED",
27
+ FACE_CROPPING_FAILED: "FACE_CROPPING_FAILED",
28
+ FACE_TOO_SMALL: "FACE_TOO_SMALL",
29
+ FACE_TOO_BLURRY: "FACE_TOO_BLURRY",
30
+ BAD_PHOTO_QUALITY: "BAD_PHOTO_QUALITY",
31
+ PROCESSING_ERROR: "PROCESSING_ERROR",
32
+ BAD_REQUEST: "BAD_REQUEST",
33
+ NONEXISTENT_CUSTOMER: "NONEXISTENT_CUSTOMER",
34
+ HINT_NOT_PROVIDED: "HINT_NOT_PROVIDED",
35
+ SELFIE_IMAGE_LOW_QUALITY: "SELFIE_IMAGE_LOW_QUALITY"
36
+ };
37
+
38
+ //#endregion
39
+ //#region src/modules/selfie/selfieErrorUtils.ts
40
+ const FACE_ERROR_CODE_VALUES = Object.values(FACE_ERROR_CODES);
41
+ const isFaceErrorCode = (value) => {
42
+ return FACE_ERROR_CODE_VALUES.includes(value);
43
+ };
44
+ const getFaceErrorCodeFromUnknown = (value) => {
45
+ if (value instanceof Error && typeof value.message === "string") return isFaceErrorCode(value.message) ? value.message : void 0;
46
+ if (typeof value === "string") return isFaceErrorCode(value) ? value : void 0;
47
+ };
48
+ const validateUploadResponse = ({ config, response }) => {
49
+ if (!response) return FACE_ERROR_CODES.SERVER;
50
+ if (response.confidence !== 0) return FACE_ERROR_CODES.LIVENESS;
51
+ if (config.validateBrightness && !response.isBright) return FACE_ERROR_CODES.BRIGHTNESS;
52
+ if (config.validateLenses && response.hasLenses) return FACE_ERROR_CODES.LENSES;
53
+ if (config.validateFaceMask && response.hasFaceMask) return FACE_ERROR_CODES.MASK;
54
+ if (config.validateClosedEyes && response.hasClosedEyes) return FACE_ERROR_CODES.CLOSED_EYES;
55
+ if (config.validateHeadCover && response.hasHeadCover) return FACE_ERROR_CODES.HEAD_COVER;
56
+ };
57
+
58
+ //#endregion
59
+ //#region src/internal/recordings/recordingsRepository.ts
60
+ async function createRecordingSession(type) {
61
+ return (await api.post(endpoints.recordingCreateSessionV2, { type })).data;
62
+ }
63
+ async function startRecording(params) {
64
+ return (await api.post(endpoints.recordingStartV2, {
65
+ videoRecordingId: params.videoRecordingId,
66
+ frameRate: 30,
67
+ outputMode: "COMPOSED",
68
+ resolution: params.resolution,
69
+ type: params.type,
70
+ hasAudio: params.hasAudio ?? false
71
+ })).data;
72
+ }
73
+ async function stopRecording$1(videoRecordingId) {
74
+ return (await api.post(endpoints.recordingStopV2, { videoRecordingId })).data;
75
+ }
76
+
77
+ //#endregion
78
+ //#region src/internal/recordings/streamingEvents.ts
79
+ const streamingEvents = {
80
+ strSessionDidConnect: "strSessionDidConnect",
81
+ strSessionDidDisconnect: "strSessionDidDisconnect",
82
+ strSessionDidFailWithError: "strSessionDidFailWithError",
83
+ strStreamVideoCaptureStart: "strStreamVideoCaptureStart",
84
+ strStreamVideoCaptureStop: "strStreamVideoCaptureStop",
85
+ strStreamPublisherCreated: "strStreamPublisherCreated",
86
+ strStreamPublisherDestroyed: "strStreamPublisherDestroyed",
87
+ strStreamPublisherDidFailWithError: "strStreamPublisherDidFailWithError"
88
+ };
89
+
90
+ //#endregion
91
+ //#region src/modules/selfie/selfieUploadService.ts
92
+ /**
93
+ * Uploads a selfie image to the backend.
94
+ */
95
+ async function uploadSelfie({ encryptedBase64Image, faceCoordinates, signal }) {
96
+ try {
97
+ const res = await api.post(endpoints.selfie, {
98
+ base64Image: encryptedBase64Image,
99
+ faceCoordinates: faceCoordinates ?? void 0,
100
+ encrypted: true,
101
+ clientInfo: { deviceClass: getDeviceClass() }
102
+ }, {
103
+ signal,
104
+ query: { imageType: "selfie" }
105
+ });
106
+ if (!res.ok) throw new Error(`POST ${endpoints.selfie} failed: ${res.status} ${res.statusText}`);
107
+ return res.data;
108
+ } catch (error) {
109
+ const errorCode = getFaceErrorCodeFromHttpError(error);
110
+ if (errorCode) throw new Error(errorCode);
111
+ throw new Error(FACE_ERROR_CODES.SERVER);
112
+ }
113
+ }
114
+ const getFaceErrorCodeFromHttpError = (error) => {
115
+ const err = error;
116
+ if (err.ok !== false || typeof err.status !== "number") return;
117
+ if (err.status !== 400) return FACE_ERROR_CODES.SERVER;
118
+ if (typeof err.data?.status !== "number") return FACE_ERROR_CODES.BAD_REQUEST;
119
+ return {
120
+ 3004: FACE_ERROR_CODES.FACE_NOT_FOUND,
121
+ 3005: FACE_ERROR_CODES.FACE_NOT_FOUND,
122
+ 3006: FACE_ERROR_CODES.TOO_BLURRY,
123
+ 3007: FACE_ERROR_CODES.TOO_DARK,
124
+ 4010: FACE_ERROR_CODES.MULTIPLE_FACES,
125
+ 4019: FACE_ERROR_CODES.FACE_NOT_FOUND,
126
+ 4077: FACE_ERROR_CODES.BAD_PHOTO_QUALITY,
127
+ 4078: FACE_ERROR_CODES.FACE_OCCLUDED
128
+ }[err.data.status] ?? FACE_ERROR_CODES.BAD_REQUEST;
129
+ };
130
+
131
+ //#endregion
132
+ //#region src/modules/selfie/selfieServices.ts
133
+ const CAMERA_CONSTRAINTS = {
134
+ video: {
135
+ facingMode: "user",
136
+ height: { ideal: 480 },
137
+ width: { ideal: 640 }
138
+ },
139
+ audio: false
140
+ };
141
+ function stopStream(stream) {
142
+ stopCameraStream(stream);
143
+ }
144
+ async function initializeCamera(config) {
145
+ const provider = new FaceDetectionProvider();
146
+ await provider.initialize({ autocaptureInterval: config.autoCaptureTimeout * 1e3 });
147
+ provider.setChecksEnabled({
148
+ lenses: config.validateLenses,
149
+ mask: config.validateFaceMask,
150
+ closedEyes: config.validateClosedEyes,
151
+ headWear: config.validateHeadCover,
152
+ occlusion: false
153
+ });
154
+ return {
155
+ stream: await requestCameraAccess({
156
+ video: CAMERA_CONSTRAINTS.video,
157
+ audio: false
158
+ }),
159
+ provider
160
+ };
161
+ }
162
+ /**
163
+ * Encrypts the provided selfie image using WASM.
164
+ */
165
+ async function encryptSelfieImage({ canvas }) {
166
+ const base64Image = canvas.getBase64Image();
167
+ if (!base64Image) throw new Error("Canvas image is empty or null");
168
+ return (await WasmUtilProvider.getInstance()).encryptImage(base64Image);
169
+ }
170
+ function startDetection(params) {
171
+ let lastStatus;
172
+ let session;
173
+ const { provider } = params;
174
+ const setStatus = (status) => {
175
+ if (session?.isDisposed() === true) return;
176
+ if (lastStatus === status) return;
177
+ lastStatus = status;
178
+ params.onUpdate(status);
179
+ };
180
+ const stopDetectionLoop = () => {
181
+ session?.dispose();
182
+ };
183
+ const reset = () => {
184
+ provider.reset();
185
+ };
186
+ const cleanup = () => {
187
+ stopDetectionLoop();
188
+ };
189
+ (async () => {
190
+ try {
191
+ provider.setCallbacks({
192
+ onFarAway: () => setStatus("tooFar"),
193
+ onTooClose: () => setStatus("tooClose"),
194
+ onTooManyFaces: () => setStatus("tooManyFaces"),
195
+ onNoFace: () => setStatus("idle"),
196
+ onCenterFace: () => setStatus("centerFace"),
197
+ onGetReady: () => setStatus("getReady"),
198
+ onGetReadyFinished: () => setStatus("getReadyFinished"),
199
+ onDark: () => {
200
+ if (params.config.validateBrightness) setStatus("dark");
201
+ },
202
+ onBlur: () => setStatus("blur"),
203
+ onFaceAngle: () => setStatus("faceAngle"),
204
+ onLenses: () => {
205
+ if (params.config.validateLenses) setStatus("lenses");
206
+ },
207
+ onMask: () => {
208
+ if (params.config.validateFaceMask) setStatus("faceMask");
209
+ },
210
+ onEyesClosed: () => {
211
+ if (params.config.validateClosedEyes) setStatus("eyesClosed");
212
+ },
213
+ onHeadWear: () => {
214
+ if (params.config.validateHeadCover) setStatus("headWear");
215
+ },
216
+ onFaceOccluded: () => {},
217
+ onSwitchToManualCapture: () => {
218
+ setStatus("manualCapture");
219
+ stopDetectionLoop();
220
+ },
221
+ onCapture: (canvas, faceCoordinates) => {
222
+ setStatus("success");
223
+ params.onSuccess(canvas, faceCoordinates);
224
+ cleanup();
225
+ }
226
+ });
227
+ setStatus("detecting");
228
+ session = new StreamCanvasProcessingSession({
229
+ capturer: params.capturer,
230
+ provider,
231
+ onFrame: params.onFrame
232
+ });
233
+ } catch {
234
+ setStatus("error");
235
+ cleanup();
236
+ }
237
+ })();
238
+ return {
239
+ cleanup,
240
+ reset
241
+ };
242
+ }
243
+ function buildResolutionFromStream(stream) {
244
+ const track = stream.getVideoTracks()[0];
245
+ if (!track) return;
246
+ const settings = track.getSettings();
247
+ const width = settings.width;
248
+ const height = settings.height;
249
+ if (typeof width === "number" && typeof height === "number") return `${width}x${height}`;
250
+ }
251
+ async function startRecordingSession(params) {
252
+ if (params.config.enableFaceRecording !== true) return;
253
+ if (params.existing) return params.existing;
254
+ const provider = params.config.recording?.capability ?? new OpenViduRecordingProvider();
255
+ const clonedStream = params.clonedStream;
256
+ const hasAudio = clonedStream.getAudioTracks().length > 0;
257
+ const resolution = buildResolutionFromStream(clonedStream);
258
+ const session = await createRecordingSession("selfie");
259
+ const connection = await provider.connect({
260
+ sessionToken: session.token,
261
+ stream: clonedStream,
262
+ events: {
263
+ onSessionConnected: (sessionId) => {
264
+ addEvent({
265
+ code: streamingEvents.strSessionDidConnect,
266
+ payload: {
267
+ message: "Recording session connected",
268
+ sessionId
269
+ }
270
+ });
271
+ },
272
+ onSessionDisconnected: (sessionId) => {
273
+ addEvent({
274
+ code: streamingEvents.strSessionDidDisconnect,
275
+ payload: {
276
+ message: "Recording session disconnected",
277
+ sessionId
278
+ }
279
+ });
280
+ },
281
+ onSessionException: (event) => {
282
+ addEvent({
283
+ code: streamingEvents.strSessionDidFailWithError,
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
+ payload: {
297
+ message: "Recording publisher created",
298
+ sessionId: p.sessionId,
299
+ streamId: p.streamId
300
+ }
301
+ });
302
+ },
303
+ onPublisherError: (p) => {
304
+ addEvent({
305
+ code: streamingEvents.strStreamPublisherDidFailWithError,
306
+ payload: {
307
+ message: "Recording publisher failed due to an error",
308
+ sessionId: p.sessionId,
309
+ streamId: p.streamId,
310
+ error: { message: p.message ?? "Unknown error" }
311
+ }
312
+ });
313
+ }
314
+ }
315
+ });
316
+ await startRecording({
317
+ videoRecordingId: session.videoRecordingId,
318
+ type: "selfie",
319
+ resolution,
320
+ hasAudio
321
+ });
322
+ addEvent({
323
+ code: streamingEvents.strStreamVideoCaptureStart,
324
+ payload: {
325
+ message: "Recording capture started",
326
+ resolution,
327
+ videoRecordingId: session.videoRecordingId,
328
+ sessionId: session.sessionId,
329
+ streamId: connection.publisher.getStreamId()
330
+ }
331
+ });
332
+ return {
333
+ token: session.token,
334
+ sessionId: session.sessionId,
335
+ videoRecordingId: session.videoRecordingId,
336
+ connection,
337
+ resolution,
338
+ hasAudio
339
+ };
340
+ }
341
+ function stopRecording(session) {
342
+ (async () => {
343
+ try {
344
+ addEvent({
345
+ code: streamingEvents.strStreamVideoCaptureStop,
346
+ payload: {
347
+ message: "Recording capture stopped",
348
+ videoRecordingId: session.videoRecordingId,
349
+ sessionId: session.sessionId,
350
+ streamId: session.connection.publisher.getStreamId()
351
+ }
352
+ });
353
+ await stopRecording$1(session.videoRecordingId);
354
+ addEvent({
355
+ code: streamingEvents.strStreamPublisherDestroyed,
356
+ payload: {
357
+ message: "Recording publisher destroyed",
358
+ sessionId: session.sessionId,
359
+ streamId: session.connection.publisher.getStreamId()
360
+ }
361
+ });
362
+ } finally {
363
+ await session.connection.disconnect();
364
+ addEvent({
365
+ code: streamingEvents.strSessionDidDisconnect,
366
+ payload: {
367
+ message: "Recording session disconnected",
368
+ sessionId: session.sessionId
369
+ }
370
+ });
371
+ }
372
+ })();
373
+ }
374
+
375
+ //#endregion
376
+ //#region src/modules/selfie/selfieStateMachine.ts
377
+ const _selfieMachine = setup({
378
+ types: {
379
+ context: {},
380
+ events: {},
381
+ input: {}
382
+ },
383
+ actors: {
384
+ checkPermission: fromPromise(async () => {
385
+ return checkPermission();
386
+ }),
387
+ requestPermission: fromPromise(async () => {
388
+ return requestPermission();
389
+ }),
390
+ initializeCamera: fromPromise(async ({ input }) => {
391
+ return initializeCamera(input);
392
+ }),
393
+ runDetection: fromCallback(({ input, sendBack }) => {
394
+ if (!input.frameCapturer || !input.provider) {
395
+ sendBack({
396
+ type: "DETECTION_UPDATE",
397
+ status: "error"
398
+ });
399
+ return () => {};
400
+ }
401
+ const { cleanup, reset } = startDetection({
402
+ config: input.config,
403
+ capturer: input.frameCapturer,
404
+ onUpdate: (status) => sendBack({
405
+ type: "DETECTION_UPDATE",
406
+ status
407
+ }),
408
+ onFrame: (frame) => sendBack({
409
+ type: "DETECTION_FRAME",
410
+ frame
411
+ }),
412
+ onSuccess: (canvas, faceCoordinates) => sendBack({
413
+ type: "DETECTION_SUCCESS",
414
+ canvas,
415
+ faceCoordinates
416
+ }),
417
+ provider: input.provider
418
+ });
419
+ sendBack({
420
+ type: "DETECTION_RESET_READY",
421
+ reset
422
+ });
423
+ return cleanup;
424
+ }),
425
+ uploadSelfie: fromPromise(async ({ input, signal }) => {
426
+ return uploadSelfie({
427
+ encryptedBase64Image: await encryptSelfieImage({ canvas: input.canvas }),
428
+ faceCoordinates: input.faceCoordinates,
429
+ signal
430
+ });
431
+ }),
432
+ startRecording: fromPromise(async ({ input }) => {
433
+ if (!input.clonedStream) return;
434
+ return startRecordingSession({
435
+ config: input.config,
436
+ clonedStream: input.clonedStream,
437
+ existing: input.existing
438
+ });
439
+ })
440
+ },
441
+ actions: {
442
+ stopMediaStream: assign(({ context }) => {
443
+ context.frameCapturer?.dispose();
444
+ if (context.stream) stopStream(context.stream);
445
+ context.provider?.dispose();
446
+ return {
447
+ stream: void 0,
448
+ provider: void 0,
449
+ frameCapturer: void 0
450
+ };
451
+ }),
452
+ setStreamAndCapturer: assign({
453
+ stream: ({ event }) => {
454
+ if ("output" in event) return event.output.stream;
455
+ },
456
+ provider: ({ event }) => {
457
+ if ("output" in event) return event.output.provider;
458
+ },
459
+ frameCapturer: ({ event }) => {
460
+ if ("output" in event) return new StreamCanvasCapture(event.output.stream);
461
+ }
462
+ }),
463
+ trackTutorialSelfie: () => {
464
+ addEvent({
465
+ code: "tutorialSelfie",
466
+ payload: { tutorialSelfie: true }
467
+ });
468
+ },
469
+ trackContinue: () => {
470
+ addEvent({ code: "continue" });
471
+ },
472
+ resetContext: assign(({ context }) => ({
473
+ stream: void 0,
474
+ provider: void 0,
475
+ frameCapturer: void 0,
476
+ error: void 0,
477
+ detectionStatus: "idle",
478
+ debugFrame: void 0,
479
+ capturedImage: void 0,
480
+ faceCoordinates: void 0,
481
+ uploadResponse: void 0,
482
+ recordingSession: void 0,
483
+ recordingStream: void 0,
484
+ attemptsRemaining: context.config.captureAttempts,
485
+ uploadError: void 0,
486
+ permissionResult: void 0,
487
+ resetDetection: void 0
488
+ })),
489
+ resetDetection: ({ context }) => {
490
+ context.resetDetection?.();
491
+ },
492
+ captureImage: assign({ capturedImage: ({ context }) => {
493
+ if (context.capturedImage) return context.capturedImage;
494
+ return context.frameCapturer?.getLatestCanvas() ?? void 0;
495
+ } }),
496
+ captureLatestFrame: assign({ capturedImage: ({ context }) => {
497
+ return context.frameCapturer?.getLatestCanvas() ?? void 0;
498
+ } }),
499
+ clearUploadFailure: assign({
500
+ uploadError: () => void 0,
501
+ detectionStatus: () => "idle",
502
+ capturedImage: () => void 0
503
+ }),
504
+ decrementAttemptsRemaining: assign(({ context }) => ({ attemptsRemaining: context.attemptsRemaining - 1 })),
505
+ setUploadErrorFromUploadValidation: assign({ uploadError: ({ context }) => validateUploadResponse({
506
+ config: context.config,
507
+ response: context.uploadResponse
508
+ }) ?? FACE_ERROR_CODES.SERVER }),
509
+ stopMediaRecording: ({ context }) => {
510
+ if (context.recordingSession) stopRecording(context.recordingSession);
511
+ },
512
+ clearRecordingSession: assign({
513
+ recordingSession: () => void 0,
514
+ recordingStream: () => void 0
515
+ }),
516
+ createRecordingStream: assign({ recordingStream: ({ context }) => {
517
+ if (!context.config.enableFaceRecording) return;
518
+ if (context.recordingStream) return context.recordingStream;
519
+ return context.stream?.clone();
520
+ } }),
521
+ stopRecordingStream: ({ context }) => {
522
+ if (context.recordingStream) stopStream(context.recordingStream);
523
+ }
524
+ },
525
+ guards: {
526
+ hasShowTutorial: ({ context }) => context.config.showTutorial,
527
+ isPermissionGranted: ({ event }) => {
528
+ if ("output" in event) return event.output === "granted";
529
+ return false;
530
+ },
531
+ isPermissionDeniedError: ({ event }) => {
532
+ if ("error" in event) {
533
+ const error = event.error;
534
+ return error?.name === "NotAllowedError" || error?.name === "PermissionDeniedError";
535
+ }
536
+ return false;
537
+ },
538
+ hasStream: ({ context }) => context.stream !== void 0,
539
+ hasAttemptsRemaining: ({ context }) => context.attemptsRemaining > 0,
540
+ hasCapturedImage: ({ context }) => context.capturedImage !== void 0,
541
+ hasUploadValidationError: ({ context }) => validateUploadResponse({
542
+ config: context.config,
543
+ response: context.uploadResponse
544
+ }) !== void 0
545
+ }
546
+ }).createMachine({
547
+ id: "selfie",
548
+ initial: "idle",
549
+ context: ({ input }) => ({
550
+ config: input.config,
551
+ stream: void 0,
552
+ provider: void 0,
553
+ frameCapturer: void 0,
554
+ error: void 0,
555
+ detectionStatus: "idle",
556
+ debugFrame: void 0,
557
+ capturedImage: void 0,
558
+ faceCoordinates: void 0,
559
+ uploadResponse: void 0,
560
+ recordingSession: void 0,
561
+ recordingStream: void 0,
562
+ attemptsRemaining: input.config.captureAttempts,
563
+ uploadError: void 0,
564
+ permissionResult: void 0,
565
+ resetDetection: void 0
566
+ }),
567
+ on: { QUIT: { target: "#selfie.closed" } },
568
+ states: {
569
+ idle: { on: { LOAD: [{
570
+ target: "tutorial",
571
+ guard: "hasShowTutorial"
572
+ }, { target: "loading" }] } },
573
+ loading: { invoke: {
574
+ id: "checkPermissionLoading",
575
+ src: "checkPermission",
576
+ onDone: [{
577
+ target: "capture",
578
+ guard: "isPermissionGranted",
579
+ actions: assign({ permissionResult: ({ event }) => event.output })
580
+ }, {
581
+ target: "permissions",
582
+ actions: assign({ permissionResult: ({ event }) => event.output })
583
+ }]
584
+ } },
585
+ tutorial: {
586
+ initial: "checkingPermission",
587
+ entry: "trackTutorialSelfie",
588
+ states: {
589
+ checkingPermission: { invoke: {
590
+ id: "checkPermissionTutorial",
591
+ src: "checkPermission",
592
+ onDone: [{
593
+ target: "initializingCamera",
594
+ guard: "isPermissionGranted",
595
+ actions: assign({ permissionResult: ({ event }) => event.output })
596
+ }, {
597
+ target: "ready",
598
+ actions: assign({ permissionResult: ({ event }) => event.output })
599
+ }]
600
+ } },
601
+ initializingCamera: {
602
+ initial: "booting",
603
+ invoke: {
604
+ id: "tutorialInitCamera",
605
+ src: "initializeCamera",
606
+ input: ({ context }) => context.config,
607
+ onDone: { actions: "setStreamAndCapturer" },
608
+ onError: [{
609
+ target: "ready",
610
+ guard: "isPermissionDeniedError",
611
+ actions: assign({ permissionResult: () => "denied" })
612
+ }, {
613
+ target: "ready",
614
+ actions: assign({ error: ({ event }) => String(event.error) })
615
+ }]
616
+ },
617
+ states: {
618
+ booting: {
619
+ always: [{
620
+ target: "#tutorialCameraReady",
621
+ guard: "hasStream"
622
+ }],
623
+ on: { NEXT_STEP: {
624
+ target: "waitingForStream",
625
+ actions: "trackContinue"
626
+ } }
627
+ },
628
+ waitingForStream: { always: [{
629
+ target: "#selfie.capture",
630
+ guard: "hasStream"
631
+ }] }
632
+ }
633
+ },
634
+ cameraReady: {
635
+ id: "tutorialCameraReady",
636
+ on: { NEXT_STEP: {
637
+ target: "#selfie.capture",
638
+ actions: "trackContinue"
639
+ } }
640
+ },
641
+ ready: { on: { NEXT_STEP: {
642
+ target: "waitingForPermission",
643
+ actions: "trackContinue"
644
+ } } },
645
+ waitingForPermission: { invoke: {
646
+ id: "checkPermissionWaiting",
647
+ src: "checkPermission",
648
+ onDone: [{
649
+ target: "#selfie.capture",
650
+ guard: "isPermissionGranted",
651
+ actions: assign({ permissionResult: ({ event }) => event.output })
652
+ }, {
653
+ target: "#selfie.permissions",
654
+ actions: assign({ permissionResult: ({ event }) => event.output })
655
+ }]
656
+ } }
657
+ }
658
+ },
659
+ permissions: {
660
+ initial: "idle",
661
+ states: {
662
+ idle: {
663
+ invoke: {
664
+ id: "checkPermissionIdle",
665
+ src: "checkPermission",
666
+ onDone: [{
667
+ target: "#selfie.capture",
668
+ guard: "isPermissionGranted",
669
+ actions: assign({ permissionResult: ({ event }) => event.output })
670
+ }, {
671
+ target: "denied",
672
+ guard: ({ event }) => event.output === "denied",
673
+ actions: assign({ permissionResult: ({ event }) => event.output })
674
+ }]
675
+ },
676
+ on: {
677
+ REQUEST_PERMISSION: "requesting",
678
+ GO_TO_LEARN_MORE: "learnMore"
679
+ }
680
+ },
681
+ learnMore: { on: {
682
+ BACK: "idle",
683
+ REQUEST_PERMISSION: "requesting"
684
+ } },
685
+ requesting: { invoke: {
686
+ id: "requestPermission",
687
+ src: "requestPermission",
688
+ onDone: [
689
+ {
690
+ target: "#selfie.capture",
691
+ guard: "isPermissionGranted",
692
+ actions: assign({ permissionResult: ({ event }) => event.output })
693
+ },
694
+ {
695
+ target: "denied",
696
+ guard: ({ event }) => event.output === "denied",
697
+ actions: assign({ permissionResult: ({ event }) => event.output })
698
+ },
699
+ {
700
+ target: "idle",
701
+ actions: assign({ permissionResult: ({ event }) => event.output })
702
+ }
703
+ ],
704
+ onError: { target: "denied" }
705
+ } },
706
+ denied: { entry: assign({ permissionResult: () => "refresh" }) }
707
+ }
708
+ },
709
+ capture: {
710
+ initial: "checkingStream",
711
+ exit: [
712
+ "stopMediaRecording",
713
+ "stopRecordingStream",
714
+ "clearRecordingSession",
715
+ "stopMediaStream"
716
+ ],
717
+ states: {
718
+ checkingStream: { always: [{
719
+ target: "detecting",
720
+ guard: "hasStream"
721
+ }, { target: "initializing" }] },
722
+ initializing: { invoke: {
723
+ id: "initializeCamera",
724
+ src: "initializeCamera",
725
+ input: ({ context }) => context.config,
726
+ onDone: {
727
+ target: "detecting",
728
+ actions: "setStreamAndCapturer"
729
+ },
730
+ onError: [{
731
+ target: "#selfie.permissions",
732
+ guard: "isPermissionDeniedError",
733
+ actions: assign({ permissionResult: () => "denied" })
734
+ }, {
735
+ target: "#selfie.error",
736
+ actions: assign({ error: ({ event }) => String(event.error) })
737
+ }]
738
+ } },
739
+ detecting: {
740
+ entry: [assign({ detectionStatus: () => "detecting" }), "createRecordingStream"],
741
+ invoke: [{
742
+ id: "startRecording",
743
+ src: "startRecording",
744
+ input: ({ context }) => ({
745
+ config: context.config,
746
+ clonedStream: context.recordingStream,
747
+ existing: context.recordingSession
748
+ }),
749
+ onDone: { actions: assign({ recordingSession: ({ context, event }) => {
750
+ return event.output ?? context.recordingSession;
751
+ } }) },
752
+ onError: { actions: () => void 0 }
753
+ }, {
754
+ id: "runDetection",
755
+ src: "runDetection",
756
+ input: ({ context }) => ({
757
+ frameCapturer: context.frameCapturer,
758
+ provider: context.provider,
759
+ config: context.config
760
+ })
761
+ }],
762
+ on: {
763
+ DETECTION_UPDATE: { actions: assign({ detectionStatus: ({ event }) => event.status }) },
764
+ DETECTION_FRAME: { actions: assign({ debugFrame: ({ event }) => event.frame }) },
765
+ DETECTION_RESET_READY: { actions: assign({ resetDetection: ({ event }) => event.reset }) },
766
+ DETECTION_SUCCESS: {
767
+ target: "capturing",
768
+ actions: assign({
769
+ capturedImage: ({ event }) => event.canvas,
770
+ faceCoordinates: ({ event }) => event.faceCoordinates
771
+ })
772
+ },
773
+ MANUAL_CAPTURE: { target: "capturingManual" }
774
+ }
775
+ },
776
+ capturing: {
777
+ entry: ["captureImage"],
778
+ always: [{
779
+ target: "uploading",
780
+ guard: "hasCapturedImage"
781
+ }, {
782
+ target: "uploadError",
783
+ actions: assign(({ context }) => ({
784
+ uploadError: FACE_ERROR_CODES.SERVER,
785
+ attemptsRemaining: context.attemptsRemaining - 1
786
+ }))
787
+ }]
788
+ },
789
+ capturingManual: {
790
+ entry: ["captureLatestFrame"],
791
+ always: [{
792
+ target: "uploading",
793
+ guard: "hasCapturedImage"
794
+ }, {
795
+ target: "uploadError",
796
+ actions: assign(({ context }) => ({
797
+ uploadError: FACE_ERROR_CODES.SERVER,
798
+ attemptsRemaining: context.attemptsRemaining - 1
799
+ }))
800
+ }]
801
+ },
802
+ uploading: { invoke: {
803
+ id: "uploadSelfie",
804
+ src: "uploadSelfie",
805
+ input: ({ context }) => {
806
+ const canvas = context.capturedImage;
807
+ if (!canvas) throw new Error(FACE_ERROR_CODES.SERVER);
808
+ return {
809
+ canvas,
810
+ faceCoordinates: context.faceCoordinates
811
+ };
812
+ },
813
+ onDone: {
814
+ target: "validatingUpload",
815
+ actions: assign({ uploadResponse: ({ event }) => event.output })
816
+ },
817
+ onError: {
818
+ target: "uploadError",
819
+ actions: assign(({ context, event }) => ({
820
+ uploadError: getFaceErrorCodeFromUnknown(event.error) ?? FACE_ERROR_CODES.SERVER,
821
+ attemptsRemaining: context.attemptsRemaining - 1
822
+ }))
823
+ }
824
+ } },
825
+ validatingUpload: { always: [{
826
+ target: "uploadError",
827
+ guard: "hasUploadValidationError",
828
+ actions: ["setUploadErrorFromUploadValidation", "decrementAttemptsRemaining"]
829
+ }, { target: "success" }] },
830
+ uploadError: { on: { RETRY_CAPTURE: {
831
+ target: "detecting",
832
+ guard: "hasAttemptsRemaining",
833
+ actions: ["resetDetection", "clearUploadFailure"]
834
+ } } },
835
+ success: {
836
+ entry: "stopMediaRecording",
837
+ after: { 3e3: { target: "#selfie.finished" } }
838
+ }
839
+ }
840
+ },
841
+ finished: {
842
+ entry: "stopMediaStream",
843
+ on: { RESET: {
844
+ target: "idle",
845
+ actions: "resetContext"
846
+ } }
847
+ },
848
+ closed: {
849
+ entry: "stopMediaStream",
850
+ type: "final"
851
+ },
852
+ error: {
853
+ entry: "stopMediaStream",
854
+ on: { RESET: {
855
+ target: "idle",
856
+ actions: "resetContext"
857
+ } }
858
+ }
859
+ }
860
+ });
861
+ /**
862
+ * The selfie capture state machine.
863
+ *
864
+ * Note: Uses AnyStateMachine type for declaration file portability.
865
+ * Type safety is ensured via the machine configuration.
866
+ */
867
+ const selfieMachine = _selfieMachine;
868
+
869
+ //#endregion
870
+ //#region src/modules/selfie/selfieActor.ts
871
+ function createSelfieActor(options) {
872
+ return createActor(selfieMachine, { input: { config: options.config } }).start();
873
+ }
874
+
875
+ //#endregion
876
+ //#region src/modules/selfie/selfieManager.ts
877
+ function getPermissionStatus(snapshot) {
878
+ if (!snapshot.matches("permissions")) return;
879
+ if (snapshot.matches({ permissions: "idle" })) return "idle";
880
+ if (snapshot.matches({ permissions: "learnMore" })) return "learnMore";
881
+ if (snapshot.matches({ permissions: "requesting" })) return "requesting";
882
+ if (snapshot.matches({ permissions: "denied" })) return "denied";
883
+ }
884
+ function getCaptureStatus(snapshot) {
885
+ if (snapshot.matches({ capture: "initializing" })) return "initializing";
886
+ if (snapshot.matches({ capture: "detecting" })) return "detecting";
887
+ if (snapshot.matches({ capture: "capturing" })) return "capturing";
888
+ if (snapshot.matches({ capture: "capturingManual" })) return "capturing";
889
+ if (snapshot.matches({ capture: "uploading" })) return "uploading";
890
+ if (snapshot.matches({ capture: "uploadError" })) return "uploadError";
891
+ if (snapshot.matches({ capture: "success" })) return "success";
892
+ }
893
+ function mapState(snapshot) {
894
+ const { context } = snapshot;
895
+ if (snapshot.matches("idle")) return { status: "idle" };
896
+ if (snapshot.matches("loading")) return { status: "loading" };
897
+ if (snapshot.matches("tutorial")) return { status: "tutorial" };
898
+ if (snapshot.matches("closed")) return { status: "closed" };
899
+ if (snapshot.matches("permissions")) {
900
+ const permissionStatus = getPermissionStatus(snapshot);
901
+ if (permissionStatus === void 0) return {
902
+ status: "permissions",
903
+ permissionStatus: "idle"
904
+ };
905
+ return {
906
+ status: "permissions",
907
+ permissionStatus
908
+ };
909
+ }
910
+ if (snapshot.matches("capture")) return {
911
+ status: "capture",
912
+ captureStatus: getCaptureStatus(snapshot) ?? "initializing",
913
+ stream: context.stream,
914
+ detectionStatus: context.detectionStatus,
915
+ debugFrame: context.debugFrame,
916
+ attemptsRemaining: context.attemptsRemaining,
917
+ uploadError: context.uploadError
918
+ };
919
+ if (snapshot.matches("finished")) return { status: "finished" };
920
+ if (snapshot.matches("error")) return {
921
+ status: "error",
922
+ error: context.error ?? "Unknown error"
923
+ };
924
+ return { status: "idle" };
925
+ }
926
+ function createApi({ actor }) {
927
+ return {
928
+ load() {
929
+ actor.send({ type: "LOAD" });
930
+ },
931
+ nextStep() {
932
+ actor.send({ type: "NEXT_STEP" });
933
+ },
934
+ requestPermission() {
935
+ actor.send({ type: "REQUEST_PERMISSION" });
936
+ },
937
+ goToLearnMore() {
938
+ actor.send({ type: "GO_TO_LEARN_MORE" });
939
+ },
940
+ back() {
941
+ actor.send({ type: "BACK" });
942
+ },
943
+ close() {
944
+ actor.send({ type: "QUIT" });
945
+ },
946
+ reset() {
947
+ actor.send({ type: "RESET" });
948
+ },
949
+ retryCapture() {
950
+ actor.send({ type: "RETRY_CAPTURE" });
951
+ },
952
+ capture() {
953
+ actor.send({ type: "MANUAL_CAPTURE" });
954
+ }
955
+ };
956
+ }
957
+ /**
958
+ * Creates a selfie manager instance for handling selfie capture flow.
959
+ *
960
+ * The selfie manager provides:
961
+ * - State management with statuses: `idle`, `loading`, `tutorial`, `permissions`, `capture`, `finished`, `closed`, `error`
962
+ * - Permission handling with nested states: `idle`, `requesting`, `denied`, `learnMore`
963
+ * - Capture handling with nested states: `initializing`, `startingRecorder`, `recordingActive`, `detecting`, `capturing`, `uploading`, `uploadError`, `success`
964
+ * - Camera stream access when in `capture` state
965
+ * - Detection status feedback during face detection
966
+ * - Attempt tracking with `attemptsRemaining`
967
+ *
968
+ * @param options - Configuration for the selfie actor
969
+ * @param options.config - The selfie module configuration from the flow
970
+ * @returns A manager instance with state subscription, API methods, and lifecycle controls
971
+ *
972
+ * @example
973
+ * ```ts
974
+ * const selfieManager = createSelfieManager({ config: selfieConfig });
975
+ *
976
+ * selfieManager.subscribe((state) => {
977
+ * if (state.status === 'capture') {
978
+ * console.log('Camera ready:', state.stream);
979
+ * console.log('Detection status:', state.detectionStatus);
980
+ * }
981
+ * });
982
+ *
983
+ * selfieManager.load();
984
+ * ```
985
+ */
986
+ function createSelfieManager(options) {
987
+ return createManager({
988
+ actor: createSelfieActor(options),
989
+ mapState,
990
+ createApi
991
+ });
992
+ }
993
+
994
+ //#endregion
995
+ export { createSelfieManager, selfieMachine };