@aws-amplify/ui-react-liveness 3.6.6 → 3.6.7

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.
@@ -1,13 +1,13 @@
1
1
  import * as React from 'react';
2
2
  import { useInterpret } from '@xstate/react';
3
3
  import { livenessMachine } from './service/machine/machine.mjs';
4
+ import './service/utils/constants.mjs';
4
5
  import './service/types/liveness.mjs';
5
6
  import '@tensorflow/tfjs-core';
6
7
  import '../../packages/react-liveness/node_modules/@tensorflow-models/face-detection/dist/face-detection.esm.mjs';
7
8
  import '@tensorflow/tfjs-backend-wasm';
8
9
  import '@tensorflow/tfjs-backend-cpu';
9
10
  import '@aws-amplify/core/internals/utils';
10
- import './service/utils/constants.mjs';
11
11
  import './service/utils/ColorSequenceDisplay/ColorSequenceDisplay.mjs';
12
12
  import '@aws-amplify/ui';
13
13
  import './service/utils/createStreamingClient/createStreamingClient.mjs';
@@ -3,6 +3,7 @@ import { classNames } from '@aws-amplify/ui';
3
3
  import { View, Flex, Loader, Text, Button } from '@aws-amplify/ui-react';
4
4
  import { useColorMode } from '@aws-amplify/ui-react/internal';
5
5
  import '../service/machine/machine.mjs';
6
+ import { FACE_MOVEMENT_CHALLENGE } from '../service/utils/constants.mjs';
6
7
  import { FaceMatchState } from '../service/types/liveness.mjs';
7
8
  import '@tensorflow/tfjs-core';
8
9
  import '../../../packages/react-liveness/node_modules/@tensorflow-models/face-detection/dist/face-detection.esm.mjs';
@@ -10,7 +11,6 @@ import '@tensorflow/tfjs-backend-wasm';
10
11
  import '@tensorflow/tfjs-backend-cpu';
11
12
  import '@aws-amplify/core/internals/utils';
12
13
  import { drawStaticOval, clearOvalCanvas } from '../service/utils/liveness.mjs';
13
- import { FACE_MOVEMENT_CHALLENGE } from '../service/utils/constants.mjs';
14
14
  import '../service/utils/ColorSequenceDisplay/ColorSequenceDisplay.mjs';
15
15
  import '../service/utils/createStreamingClient/createStreamingClient.mjs';
16
16
  import '../service/utils/StreamRecorder/StreamRecorder.mjs';
@@ -1,6 +1,7 @@
1
1
  import * as React from 'react';
2
2
  import { Flex, Text, Button, View } from '@aws-amplify/ui-react';
3
3
  import '../service/machine/machine.mjs';
4
+ import '../service/utils/constants.mjs';
4
5
  import '../service/types/liveness.mjs';
5
6
  import { LivenessErrorState } from '../service/types/error.mjs';
6
7
  import '@tensorflow/tfjs-core';
@@ -8,7 +9,6 @@ import '../../../packages/react-liveness/node_modules/@tensorflow-models/face-de
8
9
  import '@tensorflow/tfjs-backend-wasm';
9
10
  import '@tensorflow/tfjs-backend-cpu';
10
11
  import '@aws-amplify/core/internals/utils';
11
- import '../service/utils/constants.mjs';
12
12
  import '../service/utils/ColorSequenceDisplay/ColorSequenceDisplay.mjs';
13
13
  import '@aws-amplify/ui';
14
14
  import '../service/utils/createStreamingClient/createStreamingClient.mjs';
@@ -1,12 +1,12 @@
1
1
  import { v4 } from 'uuid';
2
2
  import { createMachine, assign, actions, spawn } from 'xstate';
3
3
  import { fillOverlayCanvasFractional, generateBboxFromLandmarks, getOvalBoundingBox, getIntersectionOverUnion, estimateIllumination, getOvalDetailsFromSessionInformation, drawLivenessOvalInCanvas, getStaticLivenessOvalDetails, isFaceDistanceBelowThreshold, getFaceMatchState, isCameraDeviceVirtual, getColorsSequencesFromSessionInformation } from '../utils/liveness.mjs';
4
+ import { FACE_MOVEMENT_AND_LIGHT_CHALLENGE, FACE_MOVEMENT_CHALLENGE, WS_CLOSURE_CODE } from '../utils/constants.mjs';
4
5
  import { FaceMatchState } from '../types/liveness.mjs';
5
6
  import { LivenessErrorState } from '../types/error.mjs';
6
7
  import { BlazeFaceFaceDetection } from '../utils/blazefaceFaceDetection.mjs';
7
8
  import { getFaceMatchStateInLivenessOval } from '../utils/getFaceMatchStateInLivenessOval.mjs';
8
9
  import { ColorSequenceDisplay } from '../utils/ColorSequenceDisplay/ColorSequenceDisplay.mjs';
9
- import { FACE_MOVEMENT_AND_LIGHT_CHALLENGE, FACE_MOVEMENT_CHALLENGE, WS_CLOSURE_CODE } from '../utils/constants.mjs';
10
10
  import { createRequestStreamGenerator } from '../utils/createRequestStreamGenerator/createRequestStreamGenerator.mjs';
11
11
  import { createSessionEndEvent, getTrackDimensions, createColorDisplayEvent, createSessionStartEvent } from '../utils/createRequestStreamGenerator/utils.mjs';
12
12
  import { createStreamingClient } from '../utils/createStreamingClient/createStreamingClient.mjs';
@@ -203,6 +203,10 @@ const livenessMachine = createMachine({
203
203
  'spawnResponseStreamActor',
204
204
  ],
205
205
  },
206
+ onError: {
207
+ target: '#livenessMachine.error',
208
+ actions: 'updateErrorStateForRuntime',
209
+ },
206
210
  },
207
211
  },
208
212
  waitForSessionInfo: {
@@ -231,6 +235,10 @@ const livenessMachine = createMachine({
231
235
  target: 'checkFaceDetectedBeforeStart',
232
236
  actions: 'updateFaceMatchBeforeStartDetails',
233
237
  },
238
+ onError: {
239
+ target: 'error',
240
+ actions: 'updateErrorStateForRuntime',
241
+ },
234
242
  },
235
243
  },
236
244
  checkFaceDetectedBeforeStart: {
@@ -249,6 +257,10 @@ const livenessMachine = createMachine({
249
257
  target: 'checkFaceDistanceBeforeRecording',
250
258
  actions: 'updateFaceDistanceBeforeRecording',
251
259
  },
260
+ onError: {
261
+ target: 'error',
262
+ actions: 'updateErrorStateForRuntime',
263
+ },
252
264
  },
253
265
  },
254
266
  checkFaceDistanceBeforeRecording: {
@@ -926,13 +938,7 @@ const livenessMachine = createMachine({
926
938
  const { videoEl } = context.videoAssociatedParams;
927
939
  const { faceDetector } = context.ovalAssociatedParams;
928
940
  // initialize models
929
- try {
930
- await faceDetector.modelLoadingPromise;
931
- }
932
- catch (err) {
933
- // eslint-disable-next-line no-console
934
- console.log({ err });
935
- }
941
+ await faceDetector.modelLoadingPromise;
936
942
  // detect face
937
943
  const faceMatchState = await getFaceMatchState(faceDetector, videoEl);
938
944
  return { faceMatchState };
@@ -964,13 +970,7 @@ const livenessMachine = createMachine({
964
970
  const { videoEl, canvasEl, isMobile } = context.videoAssociatedParams;
965
971
  const { faceDetector } = context.ovalAssociatedParams;
966
972
  // initialize models
967
- try {
968
- await faceDetector.modelLoadingPromise;
969
- }
970
- catch (err) {
971
- // eslint-disable-next-line no-console
972
- console.log({ err });
973
- }
973
+ await faceDetector.modelLoadingPromise;
974
974
  // detect face
975
975
  const detectedFaces = await faceDetector.detectFaces(videoEl);
976
976
  let initialFace;
@@ -1,14 +1,40 @@
1
+ import { CONNECTION_TIMEOUT } from '../utils/constants.mjs';
2
+
1
3
  /**
2
4
  * The abstract class representing FaceDetection
3
5
  * to be implemented for different libraries.
4
6
  */
5
7
  class FaceDetection {
8
+ constructor() {
9
+ this._modelLoadTriggered = false;
10
+ }
6
11
  /**
7
12
  * Triggers the `loadModels` method and stores
8
13
  * the corresponding promise to be awaited later.
14
+ * Applies a timeout to prevent indefinite hangs if the
15
+ * network request for model assets stalls.
9
16
  */
10
17
  triggerModelLoading() {
11
- this.modelLoadingPromise = this.loadModels();
18
+ if (this._modelLoadTriggered) {
19
+ return;
20
+ }
21
+ this._modelLoadTriggered = true;
22
+ let timeoutId;
23
+ this.modelLoadingPromise = Promise.race([
24
+ // `finally` ensures the timeout is cleared whether loadModels
25
+ // resolves or rejects, preventing a leaked timer on fast failures.
26
+ this.loadModels().finally(() => {
27
+ clearTimeout(timeoutId);
28
+ }),
29
+ new Promise((_, reject) => {
30
+ timeoutId = setTimeout(() => reject(new Error('Face detection model loading timed out. Check your network connection and try again.')),
31
+ // Uses the same 10s timeout as the WebSocket connection phase.
32
+ // The WASM binary (~425KB) + model (~100KB) should load well
33
+ // within 10s on any reasonable connection. If it hasn't loaded
34
+ // by then, the request is likely stalled (not just slow).
35
+ CONNECTION_TIMEOUT);
36
+ }),
37
+ ]);
12
38
  }
13
39
  }
14
40
 
@@ -142,48 +142,97 @@ class CustomWebSocketFetchHandler {
142
142
  let streamError = undefined;
143
143
  // To notify onclose event that error has occurred.
144
144
  let socketErrorOccurred = false;
145
- // initialize as no-op.
146
- let reject = () => { };
147
- let resolve = () => { };
145
+ // Buffer for messages that arrive before the consumer calls .next().
146
+ // This prevents dropped messages when the server responds faster than
147
+ // the client sets up its async iterator (race condition).
148
+ const messageQueue = [];
149
+ let waitingResolve = null;
150
+ let waitingReject = null;
151
+ let done = false;
152
+ let endError = undefined;
148
153
  socket.onmessage = (event) => {
149
- resolve({
150
- done: false,
151
- value: new Uint8Array(event.data),
152
- });
154
+ const chunk = new Uint8Array(event.data);
155
+ if (waitingResolve) {
156
+ // Consumer is waiting for data — deliver immediately
157
+ const resolve = waitingResolve;
158
+ waitingResolve = null;
159
+ waitingReject = null;
160
+ resolve({ done: false, value: chunk });
161
+ }
162
+ else {
163
+ // Consumer hasn't called .next() yet — buffer the message
164
+ messageQueue.push(chunk);
165
+ }
153
166
  };
154
167
  socket.onerror = (error) => {
155
168
  socketErrorOccurred = true;
156
169
  socket.close();
157
- reject(error);
170
+ done = true;
171
+ endError = error;
172
+ if (waitingReject) {
173
+ const reject = waitingReject;
174
+ waitingResolve = null;
175
+ waitingReject = null;
176
+ reject(error);
177
+ }
158
178
  };
159
179
  socket.onclose = (event) => {
160
180
  this.removeNotUsableSockets(socket.url);
161
181
  if (socketErrorOccurred)
162
182
  return;
183
+ done = true;
163
184
  if (streamError) {
164
- reject(streamError);
185
+ endError = streamError;
165
186
  }
166
187
  else if (event.code !== WS_CLOSURE_CODE.SUCCESS_CODE &&
167
188
  event.code !== 1001) {
168
189
  // Server closed the connection with an abnormal code (e.g. 4001
169
190
  // StreamIdleTimeout, 4003 SessionExpired, 1006 abnormal closure).
170
- reject(new Error(`Server ended the connection unexpectedly (code ${event.code}` +
191
+ endError = new Error(`Server ended the connection unexpectedly (code ${event.code}` +
171
192
  (event.reason ? `: ${event.reason}` : '') +
172
- ')'));
193
+ ')');
173
194
  }
174
- else {
175
- resolve({
176
- done: true,
177
- value: undefined,
178
- });
195
+ if (waitingResolve) {
196
+ if (endError) {
197
+ const reject = waitingReject;
198
+ waitingResolve = null;
199
+ waitingReject = null;
200
+ reject(endError);
201
+ }
202
+ else {
203
+ const resolve = waitingResolve;
204
+ waitingResolve = null;
205
+ waitingReject = null;
206
+ resolve({ done: true, value: undefined });
207
+ }
179
208
  }
180
209
  };
181
210
  const outputStream = {
182
211
  [Symbol.asyncIterator]: () => ({
183
212
  next: () => {
213
+ // If there are buffered messages, deliver the next one immediately
214
+ if (messageQueue.length > 0) {
215
+ return Promise.resolve({
216
+ done: false,
217
+ value: messageQueue.shift(),
218
+ });
219
+ }
220
+ // If the stream has ended, resolve/reject accordingly
221
+ if (done) {
222
+ if (endError) {
223
+ return Promise.reject(endError instanceof Error
224
+ ? endError
225
+ : new Error('Stream ended with an error'));
226
+ }
227
+ return Promise.resolve({
228
+ done: true,
229
+ value: undefined,
230
+ });
231
+ }
232
+ // Otherwise wait for the next message or close event
184
233
  return new Promise((_resolve, _reject) => {
185
- resolve = _resolve;
186
- reject = _reject;
234
+ waitingResolve = _resolve;
235
+ waitingReject = _reject;
187
236
  });
188
237
  },
189
238
  }),
@@ -206,8 +255,7 @@ class CustomWebSocketFetchHandler {
206
255
  catch (err) {
207
256
  // We don't throw the error here because the send()'s returned
208
257
  // would already be settled by the time sending chunk throws error.
209
- // Instead, the notify the output stream to throw if there's
210
- // exceptions
258
+ // Instead, notify the output stream to throw if there's exceptions
211
259
  if (err instanceof Error) {
212
260
  streamError = err;
213
261
  }
@@ -1,6 +1,6 @@
1
+ import { FACE_MATCH_RANGE_MAX, FACE_MATCH_WEIGHT_MAX, FACE_MATCH_WEIGHT_MIN, FACE_MATCH_RANGE_MIN } from './constants.mjs';
1
2
  import { FaceMatchState } from '../types/liveness.mjs';
2
3
  import { generateBboxFromLandmarks, getOvalBoundingBox, getIntersectionOverUnion } from './liveness.mjs';
3
- import { FACE_MATCH_RANGE_MAX, FACE_MATCH_WEIGHT_MAX, FACE_MATCH_WEIGHT_MIN, FACE_MATCH_RANGE_MIN } from './constants.mjs';
4
4
 
5
5
  /**
6
6
  * Returns the state of the provided face with respect to the provided liveness oval.
@@ -1,6 +1,6 @@
1
+ import { OVAL_HEIGHT_WIDTH_RATIO, PUPIL_DISTANCE_WEIGHT, FACE_HEIGHT_WEIGHT } from './constants.mjs';
1
2
  import { IlluminationState, FaceMatchState } from '../types/liveness.mjs';
2
3
  import { LivenessErrorState } from '../types/error.mjs';
3
- import { OVAL_HEIGHT_WIDTH_RATIO, PUPIL_DISTANCE_WEIGHT, FACE_HEIGHT_WEIGHT } from './constants.mjs';
4
4
 
5
5
  /**
6
6
  * Returns the random number between min and max
@@ -3,13 +3,13 @@ import { ComponentClassName } from '@aws-amplify/ui';
3
3
  import { Flex, View } from '@aws-amplify/ui-react';
4
4
  import { CancelButton } from './CancelButton.mjs';
5
5
  import '../service/machine/machine.mjs';
6
+ import '../service/utils/constants.mjs';
6
7
  import '../service/types/liveness.mjs';
7
8
  import '@tensorflow/tfjs-core';
8
9
  import '../../../packages/react-liveness/node_modules/@tensorflow-models/face-detection/dist/face-detection.esm.mjs';
9
10
  import '@tensorflow/tfjs-backend-wasm';
10
11
  import '@tensorflow/tfjs-backend-cpu';
11
12
  import '@aws-amplify/core/internals/utils';
12
- import '../service/utils/constants.mjs';
13
13
  import '../service/utils/ColorSequenceDisplay/ColorSequenceDisplay.mjs';
14
14
  import '../service/utils/createStreamingClient/createStreamingClient.mjs';
15
15
  import '../service/utils/StreamRecorder/StreamRecorder.mjs';
@@ -2,6 +2,7 @@ import React__default from 'react';
2
2
  import { Flex, Button, Text } from '@aws-amplify/ui-react';
3
3
  import { AlertIcon } from '@aws-amplify/ui-react/internal';
4
4
  import '../service/machine/machine.mjs';
5
+ import '../service/utils/constants.mjs';
5
6
  import '../service/types/liveness.mjs';
6
7
  import { LivenessErrorState } from '../service/types/error.mjs';
7
8
  import '@tensorflow/tfjs-core';
@@ -9,7 +10,6 @@ import '../../../packages/react-liveness/node_modules/@tensorflow-models/face-de
9
10
  import '@tensorflow/tfjs-backend-wasm';
10
11
  import '@tensorflow/tfjs-backend-cpu';
11
12
  import '@aws-amplify/core/internals/utils';
12
- import '../service/utils/constants.mjs';
13
13
  import '../service/utils/ColorSequenceDisplay/ColorSequenceDisplay.mjs';
14
14
  import '@aws-amplify/ui';
15
15
  import '../service/utils/createStreamingClient/createStreamingClient.mjs';
@@ -1,13 +1,13 @@
1
1
  import * as React from 'react';
2
2
  import { VisuallyHidden, View } from '@aws-amplify/ui-react';
3
3
  import '../service/machine/machine.mjs';
4
+ import '../service/utils/constants.mjs';
4
5
  import { FaceMatchState, IlluminationState } from '../service/types/liveness.mjs';
5
6
  import '@tensorflow/tfjs-core';
6
7
  import '../../../packages/react-liveness/node_modules/@tensorflow-models/face-detection/dist/face-detection.esm.mjs';
7
8
  import '@tensorflow/tfjs-backend-wasm';
8
9
  import '@tensorflow/tfjs-backend-cpu';
9
10
  import '@aws-amplify/core/internals/utils';
10
- import '../service/utils/constants.mjs';
11
11
  import '../service/utils/ColorSequenceDisplay/ColorSequenceDisplay.mjs';
12
12
  import '@aws-amplify/ui';
13
13
  import '../service/utils/createStreamingClient/createStreamingClient.mjs';
@@ -1,13 +1,13 @@
1
1
  export { default as FaceLivenessDetector } from './components/FaceLivenessDetector/FaceLivenessDetector.mjs';
2
2
  export { default as FaceLivenessDetectorCore } from './components/FaceLivenessDetector/FaceLivenessDetectorCore.mjs';
3
3
  import './components/FaceLivenessDetector/service/machine/machine.mjs';
4
+ import './components/FaceLivenessDetector/service/utils/constants.mjs';
4
5
  import './components/FaceLivenessDetector/service/types/liveness.mjs';
5
6
  import '@tensorflow/tfjs-core';
6
7
  import './packages/react-liveness/node_modules/@tensorflow-models/face-detection/dist/face-detection.esm.mjs';
7
8
  import '@tensorflow/tfjs-backend-wasm';
8
9
  import '@tensorflow/tfjs-backend-cpu';
9
10
  import '@aws-amplify/core/internals/utils';
10
- import './components/FaceLivenessDetector/service/utils/constants.mjs';
11
11
  import './components/FaceLivenessDetector/service/utils/ColorSequenceDisplay/ColorSequenceDisplay.mjs';
12
12
  import '@aws-amplify/ui';
13
13
  import './components/FaceLivenessDetector/service/utils/createStreamingClient/createStreamingClient.mjs';
@@ -1,3 +1,3 @@
1
- const VERSION = '3.6.6';
1
+ const VERSION = '3.6.7';
2
2
 
3
3
  export { VERSION };
package/dist/index.js CHANGED
@@ -43,17 +43,89 @@ function _interopNamespace(e) {
43
43
 
44
44
  var React__namespace = /*#__PURE__*/_interopNamespace(React);
45
45
 
46
+ // Face distance is calculated as pupilDistance / ovalWidth.
47
+ // The further away you are from the camera the distance between your pupils will decrease, thus lowering the threshold values.
48
+ // Constants from science team to determine ocular distance (space between eyes)
49
+ const PUPIL_DISTANCE_WEIGHT = 2.0;
50
+ const FACE_HEIGHT_WEIGHT = 1.8;
51
+ // Constants from science team to find face match percentage
52
+ const FACE_MATCH_RANGE_MIN = 0;
53
+ const FACE_MATCH_RANGE_MAX = 1;
54
+ const FACE_MATCH_WEIGHT_MIN = 0.25;
55
+ const FACE_MATCH_WEIGHT_MAX = 0.75;
56
+ const OVAL_HEIGHT_WIDTH_RATIO = 1.618;
57
+ const WS_CLOSURE_CODE = {
58
+ SUCCESS_CODE: 1000,
59
+ DEFAULT_ERROR_CODE: 4000,
60
+ FACE_FIT_TIMEOUT: 4001,
61
+ USER_CANCEL: 4003,
62
+ RUNTIME_ERROR: 4005,
63
+ USER_ERROR_DURING_CONNECTION: 4007,
64
+ };
65
+ // number in milliseconds to record into each video chunk.
66
+ // see https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder/start#timeslice
67
+ const TIME_SLICE = 1000;
68
+ // in MS, the rate at which colors are rendered/checked
69
+ const TICK_RATE = 10;
70
+ /**
71
+ * The number of seconds before the presigned URL expires.
72
+ * Used to override aws sdk default value of 60
73
+ */
74
+ const REQUEST_EXPIRY = 299;
75
+ /**
76
+ * The maximum time in milliseconds that the connection phase of a request
77
+ * may take before the connection attempt is abandoned.
78
+ */
79
+ const CONNECTION_TIMEOUT = 10000;
80
+ const FACE_MOVEMENT_AND_LIGHT_CHALLENGE = {
81
+ type: 'FaceMovementAndLightChallenge',
82
+ version: '2.0.0',
83
+ };
84
+ const FACE_MOVEMENT_CHALLENGE = {
85
+ type: 'FaceMovementChallenge',
86
+ version: '1.0.0',
87
+ };
88
+ const SUPPORTED_CHALLENGES = [
89
+ FACE_MOVEMENT_AND_LIGHT_CHALLENGE,
90
+ FACE_MOVEMENT_CHALLENGE,
91
+ ];
92
+ const queryParameterString = SUPPORTED_CHALLENGES.map((challenge) => `${challenge.type}_${challenge.version}`).join(',');
93
+
46
94
  /**
47
95
  * The abstract class representing FaceDetection
48
96
  * to be implemented for different libraries.
49
97
  */
50
98
  let FaceDetection$1 = class FaceDetection {
99
+ constructor() {
100
+ this._modelLoadTriggered = false;
101
+ }
51
102
  /**
52
103
  * Triggers the `loadModels` method and stores
53
104
  * the corresponding promise to be awaited later.
105
+ * Applies a timeout to prevent indefinite hangs if the
106
+ * network request for model assets stalls.
54
107
  */
55
108
  triggerModelLoading() {
56
- this.modelLoadingPromise = this.loadModels();
109
+ if (this._modelLoadTriggered) {
110
+ return;
111
+ }
112
+ this._modelLoadTriggered = true;
113
+ let timeoutId;
114
+ this.modelLoadingPromise = Promise.race([
115
+ // `finally` ensures the timeout is cleared whether loadModels
116
+ // resolves or rejects, preventing a leaked timer on fast failures.
117
+ this.loadModels().finally(() => {
118
+ clearTimeout(timeoutId);
119
+ }),
120
+ new Promise((_, reject) => {
121
+ timeoutId = setTimeout(() => reject(new Error('Face detection model loading timed out. Check your network connection and try again.')),
122
+ // Uses the same 10s timeout as the WebSocket connection phase.
123
+ // The WASM binary (~425KB) + model (~100KB) should load well
124
+ // within 10s on any reasonable connection. If it hasn't loaded
125
+ // by then, the request is likely stalled (not just slow).
126
+ CONNECTION_TIMEOUT);
127
+ }),
128
+ ]);
57
129
  }
58
130
  };
59
131
 
@@ -95,54 +167,6 @@ const LivenessErrorState = {
95
167
  DEFAULT_CAMERA_NOT_FOUND_ERROR: 'DEFAULT_CAMERA_NOT_FOUND_ERROR',
96
168
  };
97
169
 
98
- // Face distance is calculated as pupilDistance / ovalWidth.
99
- // The further away you are from the camera the distance between your pupils will decrease, thus lowering the threshold values.
100
- // Constants from science team to determine ocular distance (space between eyes)
101
- const PUPIL_DISTANCE_WEIGHT = 2.0;
102
- const FACE_HEIGHT_WEIGHT = 1.8;
103
- // Constants from science team to find face match percentage
104
- const FACE_MATCH_RANGE_MIN = 0;
105
- const FACE_MATCH_RANGE_MAX = 1;
106
- const FACE_MATCH_WEIGHT_MIN = 0.25;
107
- const FACE_MATCH_WEIGHT_MAX = 0.75;
108
- const OVAL_HEIGHT_WIDTH_RATIO = 1.618;
109
- const WS_CLOSURE_CODE = {
110
- SUCCESS_CODE: 1000,
111
- DEFAULT_ERROR_CODE: 4000,
112
- FACE_FIT_TIMEOUT: 4001,
113
- USER_CANCEL: 4003,
114
- RUNTIME_ERROR: 4005,
115
- USER_ERROR_DURING_CONNECTION: 4007,
116
- };
117
- // number in milliseconds to record into each video chunk.
118
- // see https://developer.mozilla.org/en-US/docs/Web/API/MediaRecorder/start#timeslice
119
- const TIME_SLICE = 1000;
120
- // in MS, the rate at which colors are rendered/checked
121
- const TICK_RATE = 10;
122
- /**
123
- * The number of seconds before the presigned URL expires.
124
- * Used to override aws sdk default value of 60
125
- */
126
- const REQUEST_EXPIRY = 299;
127
- /**
128
- * The maximum time in milliseconds that the connection phase of a request
129
- * may take before the connection attempt is abandoned.
130
- */
131
- const CONNECTION_TIMEOUT = 10000;
132
- const FACE_MOVEMENT_AND_LIGHT_CHALLENGE = {
133
- type: 'FaceMovementAndLightChallenge',
134
- version: '2.0.0',
135
- };
136
- const FACE_MOVEMENT_CHALLENGE = {
137
- type: 'FaceMovementChallenge',
138
- version: '1.0.0',
139
- };
140
- const SUPPORTED_CHALLENGES = [
141
- FACE_MOVEMENT_AND_LIGHT_CHALLENGE,
142
- FACE_MOVEMENT_CHALLENGE,
143
- ];
144
- const queryParameterString = SUPPORTED_CHALLENGES.map((challenge) => `${challenge.type}_${challenge.version}`).join(',');
145
-
146
170
  /**
147
171
  * Returns the random number between min and max
148
172
  * seeded with the provided random seed.
@@ -1227,7 +1251,7 @@ function createRequestStreamGenerator(stream) {
1227
1251
  };
1228
1252
  }
1229
1253
 
1230
- const VERSION = '3.6.6';
1254
+ const VERSION = '3.6.7';
1231
1255
 
1232
1256
  const BASE_USER_AGENT = `ui-react-liveness/${VERSION}`;
1233
1257
  const getLivenessUserAgent = () => {
@@ -1372,48 +1396,97 @@ class CustomWebSocketFetchHandler {
1372
1396
  let streamError = undefined;
1373
1397
  // To notify onclose event that error has occurred.
1374
1398
  let socketErrorOccurred = false;
1375
- // initialize as no-op.
1376
- let reject = () => { };
1377
- let resolve = () => { };
1399
+ // Buffer for messages that arrive before the consumer calls .next().
1400
+ // This prevents dropped messages when the server responds faster than
1401
+ // the client sets up its async iterator (race condition).
1402
+ const messageQueue = [];
1403
+ let waitingResolve = null;
1404
+ let waitingReject = null;
1405
+ let done = false;
1406
+ let endError = undefined;
1378
1407
  socket.onmessage = (event) => {
1379
- resolve({
1380
- done: false,
1381
- value: new Uint8Array(event.data),
1382
- });
1408
+ const chunk = new Uint8Array(event.data);
1409
+ if (waitingResolve) {
1410
+ // Consumer is waiting for data — deliver immediately
1411
+ const resolve = waitingResolve;
1412
+ waitingResolve = null;
1413
+ waitingReject = null;
1414
+ resolve({ done: false, value: chunk });
1415
+ }
1416
+ else {
1417
+ // Consumer hasn't called .next() yet — buffer the message
1418
+ messageQueue.push(chunk);
1419
+ }
1383
1420
  };
1384
1421
  socket.onerror = (error) => {
1385
1422
  socketErrorOccurred = true;
1386
1423
  socket.close();
1387
- reject(error);
1424
+ done = true;
1425
+ endError = error;
1426
+ if (waitingReject) {
1427
+ const reject = waitingReject;
1428
+ waitingResolve = null;
1429
+ waitingReject = null;
1430
+ reject(error);
1431
+ }
1388
1432
  };
1389
1433
  socket.onclose = (event) => {
1390
1434
  this.removeNotUsableSockets(socket.url);
1391
1435
  if (socketErrorOccurred)
1392
1436
  return;
1437
+ done = true;
1393
1438
  if (streamError) {
1394
- reject(streamError);
1439
+ endError = streamError;
1395
1440
  }
1396
1441
  else if (event.code !== WS_CLOSURE_CODE.SUCCESS_CODE &&
1397
1442
  event.code !== 1001) {
1398
1443
  // Server closed the connection with an abnormal code (e.g. 4001
1399
1444
  // StreamIdleTimeout, 4003 SessionExpired, 1006 abnormal closure).
1400
- reject(new Error(`Server ended the connection unexpectedly (code ${event.code}` +
1445
+ endError = new Error(`Server ended the connection unexpectedly (code ${event.code}` +
1401
1446
  (event.reason ? `: ${event.reason}` : '') +
1402
- ')'));
1447
+ ')');
1403
1448
  }
1404
- else {
1405
- resolve({
1406
- done: true,
1407
- value: undefined,
1408
- });
1449
+ if (waitingResolve) {
1450
+ if (endError) {
1451
+ const reject = waitingReject;
1452
+ waitingResolve = null;
1453
+ waitingReject = null;
1454
+ reject(endError);
1455
+ }
1456
+ else {
1457
+ const resolve = waitingResolve;
1458
+ waitingResolve = null;
1459
+ waitingReject = null;
1460
+ resolve({ done: true, value: undefined });
1461
+ }
1409
1462
  }
1410
1463
  };
1411
1464
  const outputStream = {
1412
1465
  [Symbol.asyncIterator]: () => ({
1413
1466
  next: () => {
1467
+ // If there are buffered messages, deliver the next one immediately
1468
+ if (messageQueue.length > 0) {
1469
+ return Promise.resolve({
1470
+ done: false,
1471
+ value: messageQueue.shift(),
1472
+ });
1473
+ }
1474
+ // If the stream has ended, resolve/reject accordingly
1475
+ if (done) {
1476
+ if (endError) {
1477
+ return Promise.reject(endError instanceof Error
1478
+ ? endError
1479
+ : new Error('Stream ended with an error'));
1480
+ }
1481
+ return Promise.resolve({
1482
+ done: true,
1483
+ value: undefined,
1484
+ });
1485
+ }
1486
+ // Otherwise wait for the next message or close event
1414
1487
  return new Promise((_resolve, _reject) => {
1415
- resolve = _resolve;
1416
- reject = _reject;
1488
+ waitingResolve = _resolve;
1489
+ waitingReject = _reject;
1417
1490
  });
1418
1491
  },
1419
1492
  }),
@@ -1436,8 +1509,7 @@ class CustomWebSocketFetchHandler {
1436
1509
  catch (err) {
1437
1510
  // We don't throw the error here because the send()'s returned
1438
1511
  // would already be settled by the time sending chunk throws error.
1439
- // Instead, the notify the output stream to throw if there's
1440
- // exceptions
1512
+ // Instead, notify the output stream to throw if there's exceptions
1441
1513
  if (err instanceof Error) {
1442
1514
  streamError = err;
1443
1515
  }
@@ -1973,6 +2045,10 @@ const livenessMachine = xstate.createMachine({
1973
2045
  'spawnResponseStreamActor',
1974
2046
  ],
1975
2047
  },
2048
+ onError: {
2049
+ target: '#livenessMachine.error',
2050
+ actions: 'updateErrorStateForRuntime',
2051
+ },
1976
2052
  },
1977
2053
  },
1978
2054
  waitForSessionInfo: {
@@ -2001,6 +2077,10 @@ const livenessMachine = xstate.createMachine({
2001
2077
  target: 'checkFaceDetectedBeforeStart',
2002
2078
  actions: 'updateFaceMatchBeforeStartDetails',
2003
2079
  },
2080
+ onError: {
2081
+ target: 'error',
2082
+ actions: 'updateErrorStateForRuntime',
2083
+ },
2004
2084
  },
2005
2085
  },
2006
2086
  checkFaceDetectedBeforeStart: {
@@ -2019,6 +2099,10 @@ const livenessMachine = xstate.createMachine({
2019
2099
  target: 'checkFaceDistanceBeforeRecording',
2020
2100
  actions: 'updateFaceDistanceBeforeRecording',
2021
2101
  },
2102
+ onError: {
2103
+ target: 'error',
2104
+ actions: 'updateErrorStateForRuntime',
2105
+ },
2022
2106
  },
2023
2107
  },
2024
2108
  checkFaceDistanceBeforeRecording: {
@@ -2696,13 +2780,7 @@ const livenessMachine = xstate.createMachine({
2696
2780
  const { videoEl } = context.videoAssociatedParams;
2697
2781
  const { faceDetector } = context.ovalAssociatedParams;
2698
2782
  // initialize models
2699
- try {
2700
- await faceDetector.modelLoadingPromise;
2701
- }
2702
- catch (err) {
2703
- // eslint-disable-next-line no-console
2704
- console.log({ err });
2705
- }
2783
+ await faceDetector.modelLoadingPromise;
2706
2784
  // detect face
2707
2785
  const faceMatchState = await getFaceMatchState(faceDetector, videoEl);
2708
2786
  return { faceMatchState };
@@ -2734,13 +2812,7 @@ const livenessMachine = xstate.createMachine({
2734
2812
  const { videoEl, canvasEl, isMobile } = context.videoAssociatedParams;
2735
2813
  const { faceDetector } = context.ovalAssociatedParams;
2736
2814
  // initialize models
2737
- try {
2738
- await faceDetector.modelLoadingPromise;
2739
- }
2740
- catch (err) {
2741
- // eslint-disable-next-line no-console
2742
- console.log({ err });
2743
- }
2815
+ await faceDetector.modelLoadingPromise;
2744
2816
  // detect face
2745
2817
  const detectedFaces = await faceDetector.detectFaces(videoEl);
2746
2818
  let initialFace;
@@ -22,9 +22,12 @@ export type Coordinate = [number, number];
22
22
  */
23
23
  export declare abstract class FaceDetection {
24
24
  modelLoadingPromise: Promise<void>;
25
+ private _modelLoadTriggered;
25
26
  /**
26
27
  * Triggers the `loadModels` method and stores
27
28
  * the corresponding promise to be awaited later.
29
+ * Applies a timeout to prevent indefinite hangs if the
30
+ * network request for model assets stalls.
28
31
  */
29
32
  triggerModelLoading(): void;
30
33
  /**
@@ -1 +1 @@
1
- export declare const VERSION = "3.6.6";
1
+ export declare const VERSION = "3.6.7";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aws-amplify/ui-react-liveness",
3
- "version": "3.6.6",
3
+ "version": "3.6.7",
4
4
  "main": "dist/index.js",
5
5
  "module": "dist/esm/index.mjs",
6
6
  "exports": {