@neofaceid/web-sdk 1.39.1 → 1.40.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
@@ -483,6 +483,7 @@ export declare class DocumentCaptureModal {
483
483
  private flashController;
484
484
  private frontBlob;
485
485
  private backBlob;
486
+ private _previewObjectUrl;
486
487
  constructor(options?: DocumentCaptureOptions);
487
488
  open(): Promise<DocumentCaptureResult>;
488
489
  private render;
@@ -593,7 +594,9 @@ export declare enum ErrorType {
593
594
  CAPTURE_ERROR = "CaptureError",
594
595
  NOT_FOUND = "NotFoundError",
595
596
  UNKNOWN = "UnknownError",
596
- CONSENT_DENIED = "ConsentDeniedError"
597
+ CONSENT_DENIED = "ConsentDeniedError",
598
+ /** NEO-224 · piscada real (EAR) não detectada dentro da janela de liveness. */
599
+ LIVENESS_BLINK_MISSING = "LivenessBlinkMissingError"
597
600
  }
598
601
 
599
602
  export declare function FaceCaptureModal({ accessToken, onClose, onSuccess, onCannotGesture, autoLighting, }: FaceCaptureModalProps): JSX_2.Element;
@@ -1339,7 +1342,7 @@ export declare const registerPersonWithoutFace: (personData: {
1339
1342
  /**
1340
1343
  * Data de lançamento da versão atual
1341
1344
  */
1342
- export declare const RELEASE_DATE = "2026-09-08";
1345
+ export declare const RELEASE_DATE = "2026-09-09";
1343
1346
 
1344
1347
  declare interface RequestChallengeWithSessionParams {
1345
1348
  applicationToken: string;
@@ -1706,7 +1709,7 @@ export declare const validateToken: (applicationToken: string) => Promise<boolea
1706
1709
  * MINOR: Incrementado quando adicionamos funcionalidades mantendo compatibilidade
1707
1710
  * PATCH: Incrementado quando corrigimos bugs mantendo compatibilidade
1708
1711
  */
1709
- export declare const VERSION = "1.39.1";
1712
+ export declare const VERSION = "1.40.0";
1710
1713
 
1711
1714
  /**
1712
1715
  * Executa `fn(sessionId)`. Se o servidor devolver 410 (sessão consumida/expirada),
@@ -162,6 +162,7 @@ var ErrorType = /* @__PURE__ */ ((ErrorType2) => {
162
162
  ErrorType2["NOT_FOUND"] = "NotFoundError";
163
163
  ErrorType2["UNKNOWN"] = "UnknownError";
164
164
  ErrorType2["CONSENT_DENIED"] = "ConsentDeniedError";
165
+ ErrorType2["LIVENESS_BLINK_MISSING"] = "LivenessBlinkMissingError";
165
166
  return ErrorType2;
166
167
  })(ErrorType || {});
167
168
  class NeoFaceError extends Error {
@@ -199,6 +200,8 @@ class NeoFaceError extends Error {
199
200
  return "Dados inválidos ou rosto não detectado corretamente.";
200
201
  case "ConsentDeniedError":
201
202
  return "É necessário aceitar o uso do seu rosto para continuar.";
203
+ case "LivenessBlinkMissingError":
204
+ return "Não detectamos uma piscada. Pisque os olhos naturalmente e tente novamente.";
202
205
  case "ApiError":
203
206
  if (this.message.includes("400")) return "Requisição inválida. Tente novamente.";
204
207
  if (this.message.includes("500"))
@@ -4681,6 +4684,14 @@ function getLocale() {
4681
4684
  function getConsent() {
4682
4685
  return { ...globalConfig.consent };
4683
4686
  }
4687
+ async function withObjectURL(blob, fn) {
4688
+ const url = URL.createObjectURL(blob);
4689
+ try {
4690
+ return await fn(url);
4691
+ } finally {
4692
+ URL.revokeObjectURL(url);
4693
+ }
4694
+ }
4684
4695
  const getApiBaseUrl = () => getBaseUrl();
4685
4696
  const REQUEST_TIMEOUT = 1e4;
4686
4697
  const ensureSecureContext = () => {
@@ -4698,75 +4709,81 @@ const createTimeoutController = () => {
4698
4709
  controller.signal.addEventListener("abort", () => clearTimeout(timeoutId));
4699
4710
  return controller;
4700
4711
  };
4701
- const compressImage = async (imageBlob) => new Promise((resolve, reject) => {
4702
- const img = new Image();
4703
- img.onload = () => {
4704
- const canvas = document.createElement("canvas");
4705
- canvas.width = img.width;
4706
- canvas.height = img.height;
4707
- const ctx = canvas.getContext("2d");
4708
- if (!ctx) {
4709
- reject(new Error("Failed to get canvas context"));
4710
- return;
4711
- }
4712
- ctx.drawImage(img, 0, 0);
4713
- canvas.toBlob(
4714
- (blob) => {
4715
- if (blob) {
4716
- resolve(blob);
4717
- } else {
4718
- reject(new Error("Failed to compress image"));
4719
- }
4720
- },
4721
- "image/jpeg",
4722
- 0.7
4723
- );
4724
- };
4725
- img.onerror = () => {
4726
- reject(new Error("Failed to load image"));
4727
- };
4728
- img.src = URL.createObjectURL(imageBlob);
4729
- });
4730
- const compressDocumentImage = async (imageBlob) => new Promise((resolve, reject) => {
4731
- const img = new Image();
4732
- img.onload = () => {
4733
- const MAX_SIZE = 1280;
4734
- let { width } = img;
4735
- let { height } = img;
4736
- if (width > height && width > MAX_SIZE) {
4737
- height = height * MAX_SIZE / width;
4738
- width = MAX_SIZE;
4739
- } else if (height > MAX_SIZE) {
4740
- width = width * MAX_SIZE / height;
4741
- height = MAX_SIZE;
4742
- }
4743
- const canvas = document.createElement("canvas");
4744
- canvas.width = width;
4745
- canvas.height = height;
4746
- const ctx = canvas.getContext("2d");
4747
- if (!ctx) {
4748
- reject(new Error("Failed to get canvas context"));
4749
- return;
4750
- }
4751
- ctx.drawImage(img, 0, 0, width, height);
4752
- canvas.toBlob(
4753
- (blob) => {
4754
- if (blob) {
4755
- resolve(blob);
4756
- } else {
4757
- reject(new Error("Failed to compress document image"));
4758
- }
4759
- },
4760
- "image/jpeg",
4761
- 0.75
4762
- // 75% quality to keep file size manageable
4763
- );
4764
- };
4765
- img.onerror = () => {
4766
- reject(new Error("Failed to load document image"));
4767
- };
4768
- img.src = URL.createObjectURL(imageBlob);
4769
- });
4712
+ const compressImage = async (imageBlob) => withObjectURL(
4713
+ imageBlob,
4714
+ (url) => new Promise((resolve, reject) => {
4715
+ const img = new Image();
4716
+ img.onload = () => {
4717
+ const canvas = document.createElement("canvas");
4718
+ canvas.width = img.width;
4719
+ canvas.height = img.height;
4720
+ const ctx = canvas.getContext("2d");
4721
+ if (!ctx) {
4722
+ reject(new Error("Failed to get canvas context"));
4723
+ return;
4724
+ }
4725
+ ctx.drawImage(img, 0, 0);
4726
+ canvas.toBlob(
4727
+ (blob) => {
4728
+ if (blob) {
4729
+ resolve(blob);
4730
+ } else {
4731
+ reject(new Error("Failed to compress image"));
4732
+ }
4733
+ },
4734
+ "image/jpeg",
4735
+ 0.7
4736
+ );
4737
+ };
4738
+ img.onerror = () => {
4739
+ reject(new Error("Failed to load image"));
4740
+ };
4741
+ img.src = url;
4742
+ })
4743
+ );
4744
+ const compressDocumentImage = async (imageBlob) => withObjectURL(
4745
+ imageBlob,
4746
+ (url) => new Promise((resolve, reject) => {
4747
+ const img = new Image();
4748
+ img.onload = () => {
4749
+ const MAX_SIZE = 1280;
4750
+ let { width } = img;
4751
+ let { height } = img;
4752
+ if (width > height && width > MAX_SIZE) {
4753
+ height = height * MAX_SIZE / width;
4754
+ width = MAX_SIZE;
4755
+ } else if (height > MAX_SIZE) {
4756
+ width = width * MAX_SIZE / height;
4757
+ height = MAX_SIZE;
4758
+ }
4759
+ const canvas = document.createElement("canvas");
4760
+ canvas.width = width;
4761
+ canvas.height = height;
4762
+ const ctx = canvas.getContext("2d");
4763
+ if (!ctx) {
4764
+ reject(new Error("Failed to get canvas context"));
4765
+ return;
4766
+ }
4767
+ ctx.drawImage(img, 0, 0, width, height);
4768
+ canvas.toBlob(
4769
+ (blob) => {
4770
+ if (blob) {
4771
+ resolve(blob);
4772
+ } else {
4773
+ reject(new Error("Failed to compress document image"));
4774
+ }
4775
+ },
4776
+ "image/jpeg",
4777
+ 0.75
4778
+ // 75% quality to keep file size manageable
4779
+ );
4780
+ };
4781
+ img.onerror = () => {
4782
+ reject(new Error("Failed to load document image"));
4783
+ };
4784
+ img.src = url;
4785
+ })
4786
+ );
4770
4787
  const validateToken = async (applicationToken) => {
4771
4788
  ensureSecureContext();
4772
4789
  const controller = createTimeoutController();
@@ -6436,6 +6453,63 @@ function ScreenFlashOverlay({
6436
6453
  ] })
6437
6454
  ] });
6438
6455
  }
6456
+ const POLL_INTERVAL_MS = 50;
6457
+ const MIN_HORIZONTAL_DISTANCE = 1e-6;
6458
+ function distance(a, b) {
6459
+ return Math.hypot(a.x - b.x, a.y - b.y);
6460
+ }
6461
+ function calculateEAR(eyePoints) {
6462
+ if (eyePoints.length !== 6) {
6463
+ throw new Error(`calculateEAR espera exatamente 6 pontos do olho, recebeu ${eyePoints.length}`);
6464
+ }
6465
+ const [p1, p2, p3, p4, p5, p6] = eyePoints;
6466
+ const verticalA = distance(p2, p6);
6467
+ const verticalB = distance(p3, p5);
6468
+ const horizontal = distance(p1, p4);
6469
+ if (horizontal < MIN_HORIZONTAL_DISTANCE) return 0;
6470
+ return (verticalA + verticalB) / (2 * horizontal);
6471
+ }
6472
+ function detectBlinkInWindow(faceapi2, videoEl, windowMs = 3e3, threshold = 0.2, signal) {
6473
+ return new Promise((resolve) => {
6474
+ const deadline = Date.now() + windowMs;
6475
+ let eyesWereClosed = false;
6476
+ let isResolved = false;
6477
+ const finish = (result) => {
6478
+ if (isResolved) return;
6479
+ isResolved = true;
6480
+ resolve(result);
6481
+ };
6482
+ const checkFrame = () => {
6483
+ if (isResolved) return;
6484
+ if (signal == null ? void 0 : signal.aborted) {
6485
+ finish(false);
6486
+ return;
6487
+ }
6488
+ if (Date.now() >= deadline) {
6489
+ finish(false);
6490
+ return;
6491
+ }
6492
+ const detectionTask = faceapi2.detectSingleFace(videoEl, new faceapi2.TinyFaceDetectorOptions()).withFaceLandmarks();
6493
+ detectionTask.then((detection) => {
6494
+ if (detection) {
6495
+ const leftEAR = calculateEAR(detection.landmarks.getLeftEye());
6496
+ const rightEAR = calculateEAR(detection.landmarks.getRightEye());
6497
+ const ear = (leftEAR + rightEAR) / 2;
6498
+ if (ear < threshold) {
6499
+ eyesWereClosed = true;
6500
+ } else if (eyesWereClosed) {
6501
+ finish(true);
6502
+ return;
6503
+ }
6504
+ }
6505
+ setTimeout(checkFrame, POLL_INTERVAL_MS);
6506
+ }).catch(() => {
6507
+ setTimeout(checkFrame, POLL_INTERVAL_MS);
6508
+ });
6509
+ };
6510
+ checkFrame();
6511
+ });
6512
+ }
6439
6513
  const modalReducer$1 = (state, action) => {
6440
6514
  switch (action.type) {
6441
6515
  case "PERMISSION_GRANTED":
@@ -6582,6 +6656,8 @@ function FaceCaptureModal({
6582
6656
  const canvasRef = useRef(null);
6583
6657
  const streamRef = useRef(null);
6584
6658
  const faceDetectionTimeoutRef = useRef(null);
6659
+ const isCapturingRef = useRef(false);
6660
+ const blinkAbortControllerRef = useRef(null);
6585
6661
  const [state, dispatch] = useReducer(modalReducer$1, { status: "requestingPermission" });
6586
6662
  const [faceDetected, setFaceDetected] = useState(false);
6587
6663
  const { startCamera, stopCamera } = useCamera();
@@ -6617,6 +6693,13 @@ function FaceCaptureModal({
6617
6693
  useEffect(() => {
6618
6694
  injectThemeStyles();
6619
6695
  }, []);
6696
+ useEffect(
6697
+ () => () => {
6698
+ var _a2;
6699
+ (_a2 = blinkAbortControllerRef.current) == null ? void 0 : _a2.abort();
6700
+ },
6701
+ []
6702
+ );
6620
6703
  const flashEnabled = autoLighting && (state.status === "ready" || state.status === "capturing");
6621
6704
  const { flashOn } = useScreenFlash(videoRef, { enabled: flashEnabled });
6622
6705
  useEffect(() => {
@@ -6660,19 +6743,40 @@ function FaceCaptureModal({
6660
6743
  }, [startCamera, stopCamera]);
6661
6744
  const handleCapture = async () => {
6662
6745
  if (!canvasRef.current || !videoRef.current) return;
6746
+ if (isCapturingRef.current) return;
6747
+ isCapturingRef.current = true;
6748
+ const videoEl = videoRef.current;
6749
+ const canvasEl = canvasRef.current;
6750
+ const abortController = new AbortController();
6751
+ blinkAbortControllerRef.current = abortController;
6663
6752
  try {
6664
6753
  dispatch({ type: "START_CAPTURE" });
6665
- const context = canvasRef.current.getContext("2d");
6754
+ const blinkDetected = await detectBlinkInWindow(
6755
+ faceapi,
6756
+ videoEl,
6757
+ void 0,
6758
+ void 0,
6759
+ abortController.signal
6760
+ );
6761
+ if (abortController.signal.aborted) return;
6762
+ if (!blinkDetected) {
6763
+ const blinkError = new NeoFaceError(
6764
+ "Blink not detected within window",
6765
+ ErrorType.LIVENESS_BLINK_MISSING
6766
+ );
6767
+ dispatch({ type: "SET_ERROR", message: blinkError.getFriendlyMessage() });
6768
+ return;
6769
+ }
6770
+ const context = canvasEl.getContext("2d");
6666
6771
  if (!context) {
6667
6772
  dispatch({ type: "SET_ERROR", message: "Failed to get canvas context" });
6668
6773
  return;
6669
6774
  }
6670
- canvasRef.current.width = videoRef.current.videoWidth;
6671
- canvasRef.current.height = videoRef.current.videoHeight;
6672
- context.drawImage(videoRef.current, 0, 0, canvasRef.current.width, canvasRef.current.height);
6775
+ canvasEl.width = videoEl.videoWidth;
6776
+ canvasEl.height = videoEl.videoHeight;
6777
+ context.drawImage(videoEl, 0, 0, canvasEl.width, canvasEl.height);
6673
6778
  const blob = await new Promise((resolve, reject) => {
6674
- var _a2;
6675
- (_a2 = canvasRef.current) == null ? void 0 : _a2.toBlob(
6779
+ canvasEl.toBlob(
6676
6780
  (b) => {
6677
6781
  if (b) resolve(b);
6678
6782
  else reject(new Error("Failed to convert canvas to blob"));
@@ -6697,6 +6801,8 @@ function FaceCaptureModal({
6697
6801
  }
6698
6802
  } catch (error) {
6699
6803
  dispatch({ type: "SET_ERROR", message: "Failed to capture image" });
6804
+ } finally {
6805
+ isCapturingRef.current = false;
6700
6806
  }
6701
6807
  };
6702
6808
  const handleRetry = () => {
@@ -7735,6 +7841,7 @@ function BiometricRegistrationModal({
7735
7841
  if (onPhotosCaptured) {
7736
7842
  dispatch({ type: "SUCCESS" });
7737
7843
  onPhotosCaptured(photos);
7844
+ photos.length = 0;
7738
7845
  return;
7739
7846
  }
7740
7847
  if (!personData || !applicationToken) {
@@ -7754,6 +7861,7 @@ function BiometricRegistrationModal({
7754
7861
  dispatch({ type: "SUCCESS" });
7755
7862
  if (onSuccess) {
7756
7863
  onSuccess(adaptedResult);
7864
+ photos.length = 0;
7757
7865
  }
7758
7866
  } catch (error) {
7759
7867
  const errorMessage = error instanceof Error ? error.message : "Erro desconhecido";
@@ -11163,8 +11271,8 @@ const biometricDetection = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.
11163
11271
  initializeBiometricDetection,
11164
11272
  isAdvancedDetectionAvailable
11165
11273
  }, Symbol.toStringTag, { value: "Module" }));
11166
- const VERSION = "1.39.1";
11167
- const RELEASE_DATE = "2026-09-08";
11274
+ const VERSION = "1.40.0";
11275
+ const RELEASE_DATE = "2026-09-09";
11168
11276
  let cachedSession = null;
11169
11277
  function getCachedCaptureSession() {
11170
11278
  return cachedSession;
@@ -12130,11 +12238,13 @@ const authorizeOperation = async (applicationToken, cpf, options) => {
12130
12238
  sessionData
12131
12239
  );
12132
12240
  if (!recognitionResult.success) {
12241
+ capturedPhotos.length = 0;
12133
12242
  throw new NeoFaceError(
12134
12243
  "Authorization failed: Person not recognized",
12135
12244
  ErrorType.RECOGNITION_FAILED
12136
12245
  );
12137
12246
  }
12247
+ capturedPhotos.length = 0;
12138
12248
  return {
12139
12249
  success: true,
12140
12250
  authorizationHash: sessionData.sessionId,
@@ -13117,6 +13227,7 @@ class DocumentCaptureModal {
13117
13227
  __publicField(this, "flashController", null);
13118
13228
  __publicField(this, "frontBlob", null);
13119
13229
  __publicField(this, "backBlob", null);
13230
+ __publicField(this, "_previewObjectUrl", null);
13120
13231
  this.options = {
13121
13232
  title: (options == null ? void 0 : options.title) ?? "Validação de Identidade",
13122
13233
  subtitle: (options == null ? void 0 : options.subtitle) ?? "Selecione o tipo de documento",
@@ -13422,47 +13533,50 @@ class DocumentCaptureModal {
13422
13533
  }
13423
13534
  }
13424
13535
  compressUploadedImage(file) {
13425
- return new Promise((resolve, reject) => {
13426
- const img = new Image();
13427
- img.onload = () => {
13428
- const MAX_SIZE = 1280;
13429
- let { width } = img;
13430
- let { height } = img;
13431
- if (width > height && width > MAX_SIZE) {
13432
- height = height * MAX_SIZE / width;
13433
- width = MAX_SIZE;
13434
- } else if (height > MAX_SIZE) {
13435
- width = width * MAX_SIZE / height;
13436
- height = MAX_SIZE;
13437
- }
13438
- const canvas = document.createElement("canvas");
13439
- canvas.width = width;
13440
- canvas.height = height;
13441
- const ctx = canvas.getContext("2d");
13442
- if (!ctx) {
13443
- reject(new Error("Failed to get canvas context"));
13444
- return;
13445
- }
13446
- ctx.drawImage(img, 0, 0, width, height);
13447
- canvas.toBlob(
13448
- (blob) => {
13449
- if (blob) {
13450
- console.log(
13451
- `[DocumentCapture] Compressed image from ${(file.size / 1024).toFixed(0)}KB to ${(blob.size / 1024).toFixed(0)}KB`
13452
- );
13453
- resolve(blob);
13454
- } else {
13455
- reject(new Error("Failed to compress image"));
13456
- }
13457
- },
13458
- "image/jpeg",
13459
- 0.75
13460
- // 75% quality - keeps file size manageable for server limits
13461
- );
13462
- };
13463
- img.onerror = () => reject(new Error("Failed to load image"));
13464
- img.src = URL.createObjectURL(file);
13465
- });
13536
+ return withObjectURL(
13537
+ file,
13538
+ (url) => new Promise((resolve, reject) => {
13539
+ const img = new Image();
13540
+ img.onload = () => {
13541
+ const MAX_SIZE = 1280;
13542
+ let { width } = img;
13543
+ let { height } = img;
13544
+ if (width > height && width > MAX_SIZE) {
13545
+ height = height * MAX_SIZE / width;
13546
+ width = MAX_SIZE;
13547
+ } else if (height > MAX_SIZE) {
13548
+ width = width * MAX_SIZE / height;
13549
+ height = MAX_SIZE;
13550
+ }
13551
+ const canvas = document.createElement("canvas");
13552
+ canvas.width = width;
13553
+ canvas.height = height;
13554
+ const ctx = canvas.getContext("2d");
13555
+ if (!ctx) {
13556
+ reject(new Error("Failed to get canvas context"));
13557
+ return;
13558
+ }
13559
+ ctx.drawImage(img, 0, 0, width, height);
13560
+ canvas.toBlob(
13561
+ (blob) => {
13562
+ if (blob) {
13563
+ console.log(
13564
+ `[DocumentCapture] Compressed image from ${(file.size / 1024).toFixed(0)}KB to ${(blob.size / 1024).toFixed(0)}KB`
13565
+ );
13566
+ resolve(blob);
13567
+ } else {
13568
+ reject(new Error("Failed to compress image"));
13569
+ }
13570
+ },
13571
+ "image/jpeg",
13572
+ 0.75
13573
+ // 75% quality - keeps file size manageable for server limits
13574
+ );
13575
+ };
13576
+ img.onerror = () => reject(new Error("Failed to load image"));
13577
+ img.src = url;
13578
+ })
13579
+ );
13466
13580
  }
13467
13581
  bindPreviewEvents() {
13468
13582
  var _a2, _b, _c;
@@ -13475,7 +13589,14 @@ class DocumentCaptureModal {
13475
13589
  );
13476
13590
  if (previewImg) {
13477
13591
  const blob = this.currentStep === "preview-front" ? this.frontBlob : this.backBlob;
13478
- if (blob) previewImg.src = URL.createObjectURL(blob);
13592
+ if (blob) {
13593
+ if (this._previewObjectUrl) {
13594
+ URL.revokeObjectURL(this._previewObjectUrl);
13595
+ this._previewObjectUrl = null;
13596
+ }
13597
+ this._previewObjectUrl = URL.createObjectURL(blob);
13598
+ previewImg.src = this._previewObjectUrl;
13599
+ }
13479
13600
  }
13480
13601
  this.bindCloseEvent();
13481
13602
  }
@@ -13662,6 +13783,10 @@ class DocumentCaptureModal {
13662
13783
  }
13663
13784
  close() {
13664
13785
  this.stopCamera();
13786
+ if (this._previewObjectUrl) {
13787
+ URL.revokeObjectURL(this._previewObjectUrl);
13788
+ this._previewObjectUrl = null;
13789
+ }
13665
13790
  if (this.overlay) {
13666
13791
  this.overlay.remove();
13667
13792
  this.overlay = null;