@neofaceid/web-sdk 1.13.2 → 1.15.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.
@@ -1652,6 +1652,110 @@ const loginWithBiometric = async (image, applicationToken) => {
1652
1652
  throw new NeoFaceError("Unknown error occurred", ErrorType.NETWORK_ERROR);
1653
1653
  }
1654
1654
  };
1655
+ const loginWithBiometricSSE = async (image, applicationToken) => {
1656
+ var _a;
1657
+ ensureSecureContext();
1658
+ const compressedImage = await compressImage(image);
1659
+ const arrayBuffer = await compressedImage.arrayBuffer();
1660
+ const base64Image = btoa(
1661
+ new Uint8Array(arrayBuffer).reduce((data, byte) => data + String.fromCharCode(byte), "")
1662
+ );
1663
+ const controller = createTimeoutController();
1664
+ let response;
1665
+ try {
1666
+ response = await fetch(`${getApiBaseUrl()}/api/v1/auth/recognition/face/external/`, {
1667
+ method: "POST",
1668
+ headers: {
1669
+ "Content-Type": "application/json",
1670
+ "X-App-Token": applicationToken
1671
+ },
1672
+ body: JSON.stringify({ image: base64Image, purpose: "LOGIN" }),
1673
+ signal: controller.signal
1674
+ });
1675
+ } catch (error) {
1676
+ if (error instanceof Error) {
1677
+ if (error.name === "AbortError") {
1678
+ throw new NeoFaceError("Request timed out", ErrorType.NETWORK);
1679
+ }
1680
+ if (error instanceof NeoFaceError) throw error;
1681
+ throw new NeoFaceError(error.message, ErrorType.NETWORK);
1682
+ }
1683
+ throw new NeoFaceError("Unknown error occurred", ErrorType.NETWORK_ERROR);
1684
+ }
1685
+ if (!response.ok) {
1686
+ if (response.status === 401 || response.status === 403) {
1687
+ throw new NeoFaceError("Invalid application token", ErrorType.INVALID_TOKEN);
1688
+ }
1689
+ throw new NeoFaceError(`API Error ${response.status}`, ErrorType.API_ERROR);
1690
+ }
1691
+ const initData = await response.json();
1692
+ if (!initData.success || !((_a = initData.data) == null ? void 0 : _a.task_id)) {
1693
+ throw new NeoFaceError(initData.message || "Failed to start login", ErrorType.LOGIN_FAILED);
1694
+ }
1695
+ const taskId = initData.data.task_id;
1696
+ return new Promise((resolve, reject) => {
1697
+ const sseUrl = `${getApiBaseUrl()}/api/v1/auth/recognition/face/external/login/events/${taskId}/`;
1698
+ const eventSource = new EventSource(sseUrl);
1699
+ const timeoutId = setTimeout(() => {
1700
+ eventSource.close();
1701
+ reject(new NeoFaceError("Login timeout", ErrorType.NETWORK));
1702
+ }, 31e4);
1703
+ const cleanup = () => {
1704
+ clearTimeout(timeoutId);
1705
+ eventSource.close();
1706
+ };
1707
+ eventSource.addEventListener("success", (event) => {
1708
+ var _a2, _b, _c, _d, _e, _f;
1709
+ cleanup();
1710
+ try {
1711
+ const data = JSON.parse(event.data);
1712
+ resolve({
1713
+ success: true,
1714
+ accessToken: data.token || data.access_token || "",
1715
+ alias: ((_a2 = data.user) == null ? void 0 : _a2.alias) || data.alias || "",
1716
+ user: {
1717
+ id: ((_b = data.user) == null ? void 0 : _b.id) || "",
1718
+ email: ((_c = data.user) == null ? void 0 : _c.email) || "",
1719
+ role: ((_d = data.user) == null ? void 0 : _d.role) || "",
1720
+ validated: ((_e = data.user) == null ? void 0 : _e.validated) ?? false,
1721
+ active: ((_f = data.user) == null ? void 0 : _f.active) ?? false
1722
+ },
1723
+ person: data.person ? { name: data.person.name || "", birth_date: data.person.birth_date || "" } : void 0,
1724
+ confidence_score: data.confidence_score,
1725
+ recognition_id: data.recognition_id ?? data.history_id
1726
+ });
1727
+ } catch {
1728
+ reject(new NeoFaceError("Erro ao processar resposta de login", ErrorType.API_ERROR));
1729
+ }
1730
+ });
1731
+ eventSource.addEventListener("failed", (event) => {
1732
+ cleanup();
1733
+ try {
1734
+ const data = JSON.parse(event.data);
1735
+ reject(new NeoFaceError(data.message || "Login failed", ErrorType.LOGIN_FAILED));
1736
+ } catch {
1737
+ reject(new NeoFaceError("Login failed", ErrorType.LOGIN_FAILED));
1738
+ }
1739
+ });
1740
+ eventSource.addEventListener("error", (event) => {
1741
+ cleanup();
1742
+ try {
1743
+ const data = JSON.parse(event.data || "{}");
1744
+ reject(new NeoFaceError(data.message || "Processing error", ErrorType.API_ERROR));
1745
+ } catch {
1746
+ reject(new NeoFaceError("Processing error", ErrorType.API_ERROR));
1747
+ }
1748
+ });
1749
+ eventSource.addEventListener("timeout", () => {
1750
+ cleanup();
1751
+ reject(new NeoFaceError("Login timeout", ErrorType.NETWORK));
1752
+ });
1753
+ eventSource.onerror = () => {
1754
+ cleanup();
1755
+ reject(new NeoFaceError("SSE connection error", ErrorType.NETWORK));
1756
+ };
1757
+ });
1758
+ };
1655
1759
  const recognizeBiometric = async (image, applicationToken, livenessCheck = true, confidenceThreshold = 0.8) => {
1656
1760
  ensureSecureContext();
1657
1761
  const controller = createTimeoutController();
@@ -1841,11 +1945,13 @@ const registerPersonWithoutFace = async (personData, applicationToken, options)
1841
1945
  password_confirm: personData.password
1842
1946
  };
1843
1947
  if (documentFrontBase64) {
1844
- body.document_images = [{
1845
- front: documentFrontBase64,
1846
- back: documentBackBase64 || void 0,
1847
- type: (options == null ? void 0 : options.documentType) || "RG"
1848
- }];
1948
+ body.document_images = [
1949
+ {
1950
+ front: documentFrontBase64,
1951
+ back: documentBackBase64 || void 0,
1952
+ type: (options == null ? void 0 : options.documentType) || "RG"
1953
+ }
1954
+ ];
1849
1955
  }
1850
1956
  const response = await fetch(`${getApiBaseUrl()}/api/v1/signup/donor/`, {
1851
1957
  method: "POST",
@@ -1935,11 +2041,13 @@ const registerPersonWithBiometric = async (personData, facePhotos, applicationTo
1935
2041
  face_photos: facePhotosBase64
1936
2042
  };
1937
2043
  if (documentFrontBase64) {
1938
- body.document_images = [{
1939
- front: documentFrontBase64,
1940
- back: documentBackBase64 || void 0,
1941
- type: (options == null ? void 0 : options.documentType) || "RG"
1942
- }];
2044
+ body.document_images = [
2045
+ {
2046
+ front: documentFrontBase64,
2047
+ back: documentBackBase64 || void 0,
2048
+ type: (options == null ? void 0 : options.documentType) || "RG"
2049
+ }
2050
+ ];
1943
2051
  }
1944
2052
  const response = await fetch(`${getApiBaseUrl()}/api/v1/signup/donor/`, {
1945
2053
  method: "POST",
@@ -2322,7 +2430,10 @@ const recordFaceVideo = async (options = {}) => {
2322
2430
  const selectedMimeType = getSupportedMimeType();
2323
2431
  let recorder;
2324
2432
  try {
2325
- recorder = selectedMimeType ? new MediaRecorder(cameraStream, { mimeType: selectedMimeType, videoBitsPerSecond: 1e6 }) : new MediaRecorder(cameraStream, { videoBitsPerSecond: 1e6 });
2433
+ recorder = selectedMimeType ? new MediaRecorder(cameraStream, {
2434
+ mimeType: selectedMimeType,
2435
+ videoBitsPerSecond: 1e6
2436
+ }) : new MediaRecorder(cameraStream, { videoBitsPerSecond: 1e6 });
2326
2437
  } catch {
2327
2438
  recorder = new MediaRecorder(cameraStream);
2328
2439
  }
@@ -2495,6 +2606,68 @@ const videoToBase64 = async (videoBlob) => {
2495
2606
  reader.readAsDataURL(videoBlob);
2496
2607
  });
2497
2608
  };
2609
+ const blobToBase64 = (blob) => new Promise((resolve, reject) => {
2610
+ const reader = new FileReader();
2611
+ reader.onload = () => {
2612
+ const result = reader.result;
2613
+ resolve(result.includes(",") ? result.split(",")[1] : result);
2614
+ };
2615
+ reader.onerror = () => reject(new NeoFaceError("Failed to convert image to base64", ErrorType.CAPTURE_ERROR));
2616
+ reader.readAsDataURL(blob);
2617
+ });
2618
+ const registerDocumentByImage = async (personId, jwtToken, images) => {
2619
+ var _a, _b;
2620
+ ensureSecureContext();
2621
+ if (!personId) {
2622
+ throw new NeoFaceError("personId is required", ErrorType.VALIDATION_ERROR);
2623
+ }
2624
+ if (!jwtToken) {
2625
+ throw new NeoFaceError("jwtToken is required", ErrorType.VALIDATION_ERROR);
2626
+ }
2627
+ if (!images || images.length === 0) {
2628
+ throw new NeoFaceError("At least one document image is required", ErrorType.VALIDATION_ERROR);
2629
+ }
2630
+ const base64Images = await Promise.all(
2631
+ images.map(async (img) => {
2632
+ const compressed = await compressDocumentImage(img);
2633
+ return blobToBase64(compressed);
2634
+ })
2635
+ );
2636
+ const controller = createTimeoutController();
2637
+ try {
2638
+ const response = await fetch(`${getApiBaseUrl()}/api/v1/donors/${personId}/documents/`, {
2639
+ method: "POST",
2640
+ headers: {
2641
+ "Content-Type": "application/json",
2642
+ Authorization: `Bearer ${jwtToken}`
2643
+ },
2644
+ body: JSON.stringify({ document_images: base64Images }),
2645
+ signal: controller.signal
2646
+ });
2647
+ const data = await response.json();
2648
+ if (!response.ok) {
2649
+ throw new NeoFaceError(
2650
+ (data == null ? void 0 : data.message) || `API Error ${response.status}`,
2651
+ ErrorType.NETWORK_ERROR
2652
+ );
2653
+ }
2654
+ return {
2655
+ success: true,
2656
+ taskId: ((_a = data.data) == null ? void 0 : _a.task_id) ?? "",
2657
+ status: ((_b = data.data) == null ? void 0 : _b.status) ?? "processing",
2658
+ message: data.message ?? "Documents submitted for processing"
2659
+ };
2660
+ } catch (error) {
2661
+ if (error instanceof NeoFaceError) throw error;
2662
+ if (error instanceof Error) {
2663
+ if (error.name === "AbortError") {
2664
+ throw new NeoFaceError("Request timeout", ErrorType.NETWORK_ERROR);
2665
+ }
2666
+ throw new NeoFaceError(error.message, ErrorType.NETWORK_ERROR);
2667
+ }
2668
+ throw new NeoFaceError("Unknown error occurred", ErrorType.NETWORK_ERROR);
2669
+ }
2670
+ };
2498
2671
  const modalReducer$1 = (state, action) => {
2499
2672
  switch (action.type) {
2500
2673
  case "PERMISSION_GRANTED":
@@ -5065,6 +5238,7 @@ class EmailPasswordModal {
5065
5238
  <div class="neoface-email-modal-content">
5066
5239
  <h2>${this.options.title || "Login com Email e Senha"}</h2>
5067
5240
  <p>${this.options.subtitle || "Informe seus dados para login."}</p>
5241
+ <div id="login-error" class="neoface-email-error" style="display:none"></div>
5068
5242
  <form id="email-password-form">
5069
5243
  <input type="email" id="email" placeholder="Email" required />
5070
5244
  <input type="password" id="password" placeholder="Senha" required />
@@ -5266,6 +5440,18 @@ class EmailPasswordModal {
5266
5440
  letter-spacing: 0.3px;
5267
5441
  }
5268
5442
 
5443
+ .neoface-email-error {
5444
+ background: rgba(239, 68, 68, 0.15);
5445
+ border: 1px solid rgba(239, 68, 68, 0.4);
5446
+ border-radius: 10px;
5447
+ color: #fca5a5;
5448
+ font-size: 14px;
5449
+ padding: 12px 16px;
5450
+ margin-bottom: 16px;
5451
+ text-align: center;
5452
+ animation: fadeIn 0.2s ease-out;
5453
+ }
5454
+
5269
5455
  @media (max-width: 640px) {
5270
5456
  .neoface-email-modal-content {
5271
5457
  padding: 24px;
@@ -5283,20 +5469,40 @@ class EmailPasswordModal {
5283
5469
  event.preventDefault();
5284
5470
  const emailInput = this.modalElement.querySelector("#email");
5285
5471
  const passwordInput = this.modalElement.querySelector("#password");
5472
+ const submitBtn = this.modalElement.querySelector(
5473
+ ".neoface-email-submit-btn"
5474
+ );
5475
+ const errorDiv = this.modalElement.querySelector("#login-error");
5476
+ const showError = (msg) => {
5477
+ errorDiv.textContent = msg;
5478
+ errorDiv.style.display = "block";
5479
+ };
5480
+ errorDiv.style.display = "none";
5481
+ errorDiv.textContent = "";
5286
5482
  const email = emailInput.value.trim();
5287
5483
  const password = passwordInput.value.trim();
5288
5484
  if (!email || !password) {
5289
- this.options.onError(
5290
- new NeoFaceError("Email e senha são obrigatórios", ErrorType.VALIDATION_ERROR)
5291
- );
5485
+ showError("Preencha o email e a senha para continuar.");
5292
5486
  return;
5293
5487
  }
5488
+ submitBtn.disabled = true;
5489
+ submitBtn.textContent = "Entrando...";
5294
5490
  try {
5295
5491
  const result = await loginWithEmail(email, password, this.options.applicationToken);
5296
5492
  this.options.onSuccess(result);
5297
5493
  this.close();
5298
5494
  } catch (error) {
5299
- this.options.onError(error);
5495
+ submitBtn.disabled = false;
5496
+ submitBtn.textContent = "Entrar";
5497
+ const isCredentialError = error instanceof NeoFaceError && (error.type === ErrorType.INVALID_TOKEN || error.type === ErrorType.API_ERROR && error.message.includes("400"));
5498
+ if (isCredentialError) {
5499
+ showError("Email ou senha incorretos. Verifique seus dados e tente novamente.");
5500
+ passwordInput.value = "";
5501
+ passwordInput.focus();
5502
+ } else {
5503
+ showError("Ocorreu um erro inesperado. Tente novamente mais tarde.");
5504
+ this.options.onError(error);
5505
+ }
5300
5506
  }
5301
5507
  }
5302
5508
  }
@@ -5438,7 +5644,12 @@ async function captureFaceSilently() {
5438
5644
  }
5439
5645
  }).catch((err) => {
5440
5646
  cleanup();
5441
- reject(new NeoFaceError("Erro ao reproduzir câmera: " + err.message, ErrorType.CAMERA_ERROR));
5647
+ reject(
5648
+ new NeoFaceError(
5649
+ "Erro ao reproduzir câmera: " + err.message,
5650
+ ErrorType.CAMERA_ERROR
5651
+ )
5652
+ );
5442
5653
  });
5443
5654
  } else {
5444
5655
  video.onloadedmetadata = () => setTimeout(tryCapture, CAMERA_READY_DELAY);
@@ -5580,7 +5791,10 @@ async function attemptLogin(applicationToken, overlay, fastMode = false, isRetry
5580
5791
  if (video.parentElement) {
5581
5792
  video.parentElement.removeChild(video);
5582
5793
  }
5583
- throw new NeoFaceError("Rosto não detectado. Posicione seu rosto na frente da câmera.", ErrorType.VALIDATION_ERROR);
5794
+ throw new NeoFaceError(
5795
+ "Rosto não detectado. Posicione seu rosto na frente da câmera.",
5796
+ ErrorType.VALIDATION_ERROR
5797
+ );
5584
5798
  }
5585
5799
  overlay.updateStatus("capturing");
5586
5800
  const canvas = document.createElement("canvas");
@@ -5618,7 +5832,7 @@ async function attemptLogin(applicationToken, overlay, fastMode = false, isRetry
5618
5832
  );
5619
5833
  });
5620
5834
  overlay.updateStatus("verifying");
5621
- const result = await loginWithBiometric(imageBlob, applicationToken);
5835
+ const result = await loginWithBiometricSSE(imageBlob, applicationToken);
5622
5836
  if (!result.success) {
5623
5837
  throw new NeoFaceError("Login falhou", ErrorType.LOGIN_FAILED);
5624
5838
  }
@@ -5640,7 +5854,14 @@ async function attemptLogin(applicationToken, overlay, fastMode = false, isRetry
5640
5854
  }
5641
5855
  }
5642
5856
  async function executeBiometricLoginFlow(options) {
5643
- const { applicationToken, onSuccess, onError, onFallbackRequest, onCancel, fastMode = false } = options;
5857
+ const {
5858
+ applicationToken,
5859
+ onSuccess,
5860
+ onError,
5861
+ onFallbackRequest,
5862
+ onCancel,
5863
+ fastMode = false
5864
+ } = options;
5644
5865
  const overlay = new BiometricStatusOverlay();
5645
5866
  const fallbackPrompt = new FallbackPrompt();
5646
5867
  let attempts = 0;
@@ -6034,8 +6255,8 @@ const biometricDetection = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.
6034
6255
  initializeBiometricDetection,
6035
6256
  isAdvancedDetectionAvailable
6036
6257
  }, Symbol.toStringTag, { value: "Module" }));
6037
- const VERSION = "1.13.2";
6038
- const RELEASE_DATE = "2026-02-21";
6258
+ const VERSION = "1.15.0";
6259
+ const RELEASE_DATE = "2026-03-04";
6039
6260
  class OnboardingCaptureModal {
6040
6261
  constructor(options) {
6041
6262
  __publicField(this, "overlay", null);
@@ -6494,6 +6715,47 @@ class NeoFaceID {
6494
6715
  * }
6495
6716
  * ```
6496
6717
  */
6718
+ /**
6719
+ * Registers document images for an already-registered donor person.
6720
+ *
6721
+ * Opens the document capture UI (front + optional back), then submits
6722
+ * the images to the backend for extraction via DocExt.
6723
+ * The backend processes this asynchronously — use the returned `taskId`
6724
+ * to poll status if needed.
6725
+ *
6726
+ * @param personId UUID of the donor's person record
6727
+ * @param jwtToken JWT Bearer token of the authenticated donor
6728
+ * @param options Optional capture configuration
6729
+ * @returns Promise with task_id and processing status
6730
+ * @throws NeoFaceError if capture is cancelled, validation fails, or API call fails
6731
+ *
6732
+ * @example
6733
+ * ```typescript
6734
+ * const sdk = new NeoFaceID({ appToken: 'your-token' });
6735
+ *
6736
+ * const result = await sdk.registerDocumentByImage(personId, userJwtToken);
6737
+ * console.log('Task ID:', result.taskId); // poll for completion
6738
+ * ```
6739
+ */
6740
+ async registerDocumentByImage(personId, jwtToken, options = {}) {
6741
+ const { DocumentCaptureModal: DocumentCaptureModal2 } = await Promise.resolve().then(() => DocumentCaptureModal$1);
6742
+ const modal = new DocumentCaptureModal2({
6743
+ useBackCamera: options.useBackCamera ?? true,
6744
+ preSelectedDocument: options.preSelectedDocument
6745
+ });
6746
+ const captureResult = await modal.open();
6747
+ if (!captureResult.success || !captureResult.frontImage) {
6748
+ throw new NeoFaceError(
6749
+ captureResult.error === "cancelled" ? "Document capture was cancelled by the user" : `Document capture failed: ${captureResult.error ?? "unknown error"}`,
6750
+ ErrorType.CAPTURE_ERROR
6751
+ );
6752
+ }
6753
+ const images = [captureResult.frontImage];
6754
+ if (captureResult.backImage) {
6755
+ images.push(captureResult.backImage);
6756
+ }
6757
+ return registerDocumentByImage(personId, jwtToken, images);
6758
+ }
6497
6759
  async proofOfLife(options = {}) {
6498
6760
  const {
6499
6761
  videoDurationMs = 3e3,
@@ -7672,6 +7934,11 @@ const startDocumentCapture = async (options) => {
7672
7934
  const modal = new DocumentCaptureModal(options);
7673
7935
  return modal.open();
7674
7936
  };
7937
+ const DocumentCaptureModal$1 = /* @__PURE__ */ Object.freeze(/* @__PURE__ */ Object.defineProperty({
7938
+ __proto__: null,
7939
+ DocumentCaptureModal,
7940
+ startDocumentCapture
7941
+ }, Symbol.toStringTag, { value: "Module" }));
7675
7942
  function start(applicationToken, callbacks) {
7676
7943
  const container = document.createElement("div");
7677
7944
  container.id = "neoface-modal-container";