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

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