@neofaceid/web-sdk 2.0.0 → 2.1.0

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.
package/dist/index.d.ts CHANGED
@@ -729,7 +729,9 @@ export declare enum ErrorType {
729
729
  UNKNOWN = "UnknownError",
730
730
  CONSENT_DENIED = "ConsentDeniedError",
731
731
  /** NEO-224 · piscada real (EAR) não detectada dentro da janela de liveness. */
732
- LIVENESS_BLINK_MISSING = "LivenessBlinkMissingError"
732
+ LIVENESS_BLINK_MISSING = "LivenessBlinkMissingError",
733
+ /** NEO-230 · polegar para cima não detectado na janela de authorizeOperation. */
734
+ GESTURE_NOT_DETECTED = "GestureNotDetectedError"
733
735
  }
734
736
 
735
737
  export declare function FaceCaptureModal({ accessToken, onClose, onSuccess, onCannotGesture, autoLighting, }: FaceCaptureModalProps): JSX_2.Element;
@@ -1391,7 +1393,7 @@ export declare const registerPersonWithoutFace: (personData: {
1391
1393
  /**
1392
1394
  * Data de lançamento da versão atual
1393
1395
  */
1394
- export declare const RELEASE_DATE = "2026-09-19";
1396
+ export declare const RELEASE_DATE = "2026-09-23";
1395
1397
 
1396
1398
  declare interface RequestChallengeWithSessionParams {
1397
1399
  applicationToken: string;
@@ -1864,7 +1866,7 @@ export declare const validateToken: (applicationToken: string) => Promise<boolea
1864
1866
  * MINOR: Incrementado quando adicionamos funcionalidades mantendo compatibilidade
1865
1867
  * PATCH: Incrementado quando corrigimos bugs mantendo compatibilidade
1866
1868
  */
1867
- export declare const VERSION = "2.0.0";
1869
+ export declare const VERSION = "2.1.0";
1868
1870
 
1869
1871
  /**
1870
1872
  * Executa `fn(sessionId)`. Se o servidor devolver 410 (sessão consumida/expirada),
@@ -162,6 +162,7 @@ var ErrorType = /* @__PURE__ */ ((ErrorType2) => {
162
162
  ErrorType2["UNKNOWN"] = "UnknownError";
163
163
  ErrorType2["CONSENT_DENIED"] = "ConsentDeniedError";
164
164
  ErrorType2["LIVENESS_BLINK_MISSING"] = "LivenessBlinkMissingError";
165
+ ErrorType2["GESTURE_NOT_DETECTED"] = "GestureNotDetectedError";
165
166
  return ErrorType2;
166
167
  })(ErrorType || {});
167
168
  class NeoFaceError extends Error {
@@ -201,6 +202,8 @@ class NeoFaceError extends Error {
201
202
  return "É necessário aceitar o uso do seu rosto para continuar.";
202
203
  case "LivenessBlinkMissingError":
203
204
  return "Não detectamos uma piscada. Pisque os olhos naturalmente e tente novamente.";
205
+ case "GestureNotDetectedError":
206
+ return "Não detectamos o polegar para cima. Mostre o gesto claramente para a câmera e tente novamente.";
204
207
  case "ApiError":
205
208
  if (this.message.includes("400")) return "Requisição inválida. Tente novamente.";
206
209
  if (this.message.includes("500"))
@@ -6095,9 +6098,22 @@ const recordFaceVideo = async (options = {}) => {
6095
6098
  const pollTaskStatus = async (taskId, applicationToken, options = {}) => {
6096
6099
  var _a2, _b, _c, _d, _e, _f, _g, _h, _i, _j;
6097
6100
  const { maxAttempts = 60, intervalMs = 1e3, onStatusChange } = options;
6098
- const wait = () => new Promise((resolve) => {
6099
- setTimeout(resolve, intervalMs);
6100
- });
6101
+ const wait2 = (ms = intervalMs) => new Promise((resolve) => {
6102
+ setTimeout(resolve, ms);
6103
+ });
6104
+ const retryAfterMs = (response) => {
6105
+ var _a3, _b2;
6106
+ const raw = (_b2 = (_a3 = response.headers) == null ? void 0 : _a3.get) == null ? void 0 : _b2.call(_a3, "Retry-After");
6107
+ if (!raw) return intervalMs;
6108
+ const seconds = Number(raw);
6109
+ if (Number.isFinite(seconds) && seconds > 0) return seconds * 1e3;
6110
+ const date2 = Date.parse(raw);
6111
+ if (Number.isFinite(date2)) {
6112
+ const delta = date2 - Date.now();
6113
+ if (delta > 0) return delta;
6114
+ }
6115
+ return intervalMs;
6116
+ };
6101
6117
  let attempts = 0;
6102
6118
  let lastTransient = "";
6103
6119
  while (attempts < maxAttempts) {
@@ -6118,7 +6134,12 @@ const pollTaskStatus = async (taskId, applicationToken, options = {}) => {
6118
6134
  }
6119
6135
  if (response.status >= 500) {
6120
6136
  lastTransient = `HTTP ${response.status}`;
6121
- await wait();
6137
+ await wait2();
6138
+ continue;
6139
+ }
6140
+ if (response.status === 429) {
6141
+ lastTransient = "HTTP 429";
6142
+ await wait2(retryAfterMs(response));
6122
6143
  continue;
6123
6144
  }
6124
6145
  if (response.status === 401 || response.status === 403) {
@@ -6147,13 +6168,13 @@ const pollTaskStatus = async (taskId, applicationToken, options = {}) => {
6147
6168
  const reason = ((_h = (_g = data.data) == null ? void 0 : _g.result) == null ? void 0 : _h.message) || ((_i = data.data) == null ? void 0 : _i.error) || ((_j = data.data) == null ? void 0 : _j.message) || data.message || "Task failed";
6148
6169
  throw new NeoFaceError(reason, ErrorType.RECOGNITION_FAILED);
6149
6170
  }
6150
- await wait();
6171
+ await wait2();
6151
6172
  } catch (error) {
6152
6173
  if (error instanceof NeoFaceError) {
6153
6174
  throw error;
6154
6175
  }
6155
6176
  lastTransient = error instanceof Error ? error.message : "erro desconhecido";
6156
- await wait();
6177
+ await wait2();
6157
6178
  }
6158
6179
  }
6159
6180
  throw new NeoFaceError(
@@ -6615,9 +6636,9 @@ function ScreenFlashOverlay({
6615
6636
  ] })
6616
6637
  ] });
6617
6638
  }
6618
- const POLL_INTERVAL_MS = 50;
6639
+ const POLL_INTERVAL_MS$1 = 50;
6619
6640
  const MIN_HORIZONTAL_DISTANCE = 1e-6;
6620
- function distance(a, b) {
6641
+ function distance$1(a, b) {
6621
6642
  return Math.hypot(a.x - b.x, a.y - b.y);
6622
6643
  }
6623
6644
  function calculateEAR(eyePoints) {
@@ -6625,9 +6646,9 @@ function calculateEAR(eyePoints) {
6625
6646
  throw new Error(`calculateEAR espera exatamente 6 pontos do olho, recebeu ${eyePoints.length}`);
6626
6647
  }
6627
6648
  const [p1, p2, p3, p4, p5, p6] = eyePoints;
6628
- const verticalA = distance(p2, p6);
6629
- const verticalB = distance(p3, p5);
6630
- const horizontal = distance(p1, p4);
6649
+ const verticalA = distance$1(p2, p6);
6650
+ const verticalB = distance$1(p3, p5);
6651
+ const horizontal = distance$1(p1, p4);
6631
6652
  if (horizontal < MIN_HORIZONTAL_DISTANCE) return 0;
6632
6653
  return (verticalA + verticalB) / (2 * horizontal);
6633
6654
  }
@@ -6664,9 +6685,9 @@ function detectBlinkInWindow(faceapi2, videoEl, windowMs = 3e3, threshold = 0.2,
6664
6685
  return;
6665
6686
  }
6666
6687
  }
6667
- setTimeout(checkFrame, POLL_INTERVAL_MS);
6688
+ setTimeout(checkFrame, POLL_INTERVAL_MS$1);
6668
6689
  }).catch(() => {
6669
- setTimeout(checkFrame, POLL_INTERVAL_MS);
6690
+ setTimeout(checkFrame, POLL_INTERVAL_MS$1);
6670
6691
  });
6671
6692
  };
6672
6693
  checkFrame();
@@ -8898,8 +8919,8 @@ function BiometricRegistrationModal({
8898
8919
  /* @__PURE__ */ jsx("canvas", { ref: canvasRef })
8899
8920
  ] });
8900
8921
  }
8901
- const VERSION = "2.0.0";
8902
- const RELEASE_DATE = "2026-09-19";
8922
+ const VERSION = "2.1.0";
8923
+ const RELEASE_DATE = "2026-09-23";
8903
8924
  const CONSENT_TRAIL_STORAGE_KEY = "neoface_consent_trail_v1";
8904
8925
  const CONSENT_TRAIL_MAX_LIMIT = 100;
8905
8926
  async function hashString(inputStr) {
@@ -13307,6 +13328,70 @@ async function registerDocumentByImage(personId, jwtToken, options = {}) {
13307
13328
  if (captured.backImage) images.push(captured.backImage);
13308
13329
  return registerDocumentByImage$1(personId, jwtToken, images);
13309
13330
  }
13331
+ const WRIST = 0;
13332
+ const THUMB_MCP = 2;
13333
+ const THUMB_TIP = 4;
13334
+ const OTHER_FINGERS = [
13335
+ { tip: 8, pip: 6 },
13336
+ // indicador
13337
+ { tip: 12, pip: 10 },
13338
+ // médio
13339
+ { tip: 16, pip: 14 },
13340
+ // anelar
13341
+ { tip: 20, pip: 18 }
13342
+ // mindinho
13343
+ ];
13344
+ const distance = (a, b) => Math.hypot(a.x - b.x, a.y - b.y);
13345
+ function isThumbsUp(keypoints) {
13346
+ if (keypoints.length < 21) return false;
13347
+ const wrist = keypoints[WRIST];
13348
+ const thumbMcp = keypoints[THUMB_MCP];
13349
+ const thumbTip = keypoints[THUMB_TIP];
13350
+ const thumbExtended = distance(thumbTip, wrist) > distance(thumbMcp, wrist) * 1.3;
13351
+ if (!thumbExtended) return false;
13352
+ return OTHER_FINGERS.every(({ tip, pip }) => {
13353
+ const tipPoint = keypoints[tip];
13354
+ const pipPoint = keypoints[pip];
13355
+ return distance(tipPoint, wrist) < distance(pipPoint, wrist);
13356
+ });
13357
+ }
13358
+ const POLL_INTERVAL_MS = 150;
13359
+ let detectorPromise = null;
13360
+ async function createDetector() {
13361
+ const [tf, handPoseDetection] = await Promise.all([
13362
+ import("@tensorflow/tfjs-core"),
13363
+ import("@tensorflow-models/hand-pose-detection")
13364
+ ]);
13365
+ await import("@tensorflow/tfjs-backend-webgl");
13366
+ await tf.setBackend("webgl");
13367
+ await tf.ready();
13368
+ return handPoseDetection.createDetector(handPoseDetection.SupportedModels.MediaPipeHands, {
13369
+ runtime: "tfjs",
13370
+ modelType: "lite",
13371
+ maxHands: 1
13372
+ });
13373
+ }
13374
+ function getDetector() {
13375
+ if (!detectorPromise) {
13376
+ detectorPromise = createDetector();
13377
+ }
13378
+ return detectorPromise;
13379
+ }
13380
+ const wait = (ms) => new Promise((resolve) => {
13381
+ setTimeout(resolve, ms);
13382
+ });
13383
+ async function detectThumbsUp(videoEl, timeoutMs) {
13384
+ const detector = await getDetector();
13385
+ const deadline = Date.now() + timeoutMs;
13386
+ while (Date.now() < deadline) {
13387
+ const hands = await detector.estimateHands(videoEl, { flipHorizontal: true });
13388
+ if (hands.length > 0 && isThumbsUp(hands[0].keypoints)) {
13389
+ return true;
13390
+ }
13391
+ await wait(POLL_INTERVAL_MS);
13392
+ }
13393
+ return false;
13394
+ }
13310
13395
  const PHOTO_INSTRUCTIONS = [
13311
13396
  "Olhe diretamente para a câmera",
13312
13397
  "Vire levemente a cabeça para a esquerda",
@@ -13314,6 +13399,7 @@ const PHOTO_INSTRUCTIONS = [
13314
13399
  "Mostre o polegar para cima (👍) e olhe para a câmera"
13315
13400
  ];
13316
13401
  const TOTAL_PHOTOS = 4;
13402
+ const GESTURE_TIMEOUT_MS = 5e3;
13317
13403
  const authorizeOperation = async (applicationToken, cpf, options) => {
13318
13404
  if (!applicationToken) {
13319
13405
  throw new NeoFaceError("Application token is required", ErrorType.VALIDATION_ERROR);
@@ -13375,7 +13461,19 @@ const authorizeOperation = async (applicationToken, cpf, options) => {
13375
13461
  if (options == null ? void 0 : options.onProgress) {
13376
13462
  options.onProgress(i + 1, TOTAL_PHOTOS, instruction);
13377
13463
  }
13378
- if (i > 0) {
13464
+ const isGesturePhoto = i === TOTAL_PHOTOS - 1;
13465
+ if (isGesturePhoto) {
13466
+ const detected = await detectThumbsUp(video, GESTURE_TIMEOUT_MS);
13467
+ if (!detected) {
13468
+ guideModal.close();
13469
+ stream.getTracks().forEach((track) => track.stop());
13470
+ capturedPhotos.length = 0;
13471
+ throw new NeoFaceError(
13472
+ "Polegar para cima não detectado dentro do tempo limite.",
13473
+ ErrorType.GESTURE_NOT_DETECTED
13474
+ );
13475
+ }
13476
+ } else if (i > 0) {
13379
13477
  await new Promise((resolve) => setTimeout(resolve, captureDelay));
13380
13478
  }
13381
13479
  ctx.drawImage(video, 0, 0, canvas.width, canvas.height);