@neofaceid/web-sdk 2.1.2 → 2.1.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.
package/README.md CHANGED
@@ -63,7 +63,19 @@ vai consumi-lo:
63
63
 
64
64
  Sem esses arquivos em `/models`, qualquer fluxo que dependa de detecção
65
65
  facial local (blink detection do liveness, `biometricDetection`) falha ao
66
- carregar o modelo.
66
+ carregar o modelo. Se o manifest não for encontrado (404), o console
67
+ mostra um `console.error` acionável, com os 4 arquivos e o destino:
68
+
69
+ ```
70
+ [NeoFace ID SDK] Modelos de detecção facial não encontrados em /models/.
71
+ Copie os 4 arquivos abaixo para public/models/ (ou equivalente do seu bundler):
72
+ - tiny_face_detector_model-weights_manifest.json
73
+ - tiny_face_detector_model-shard1
74
+ - face_landmark_68_model-weights_manifest.json
75
+ - face_landmark_68_model-shard1
76
+ Fonte: https://github.com/vladmandic/face-api/tree/master/model
77
+ Sem esses arquivos em /models, o fluxo de detecção facial falhará silenciosamente.
78
+ ```
67
79
  3. **Ícone/logo do SDK não precisa de asset externo.** Desde a 1.29.0 a marca
68
80
  é renderizada via SVG inline (`src/assets/brand.ts`) — não há mais PNG
69
81
  para hospedar.
@@ -158,6 +170,7 @@ await startFaceLogin({
158
170
  appName: 'Minha Aplicação',
159
171
  onSuccess: result => console.log('Login ok:', result),
160
172
  onError: err => console.error(err.type, err.getFriendlyMessage()),
173
+ onAttemptError: (err, attempt) => analytics.track('biometric_attempt_failed', { attempt, type: err.type }),
161
174
  onCancel: () => console.log('Usuário cancelou'),
162
175
  onFallbackRequest: () => {
163
176
  /* opcional — default já abre EmailPasswordModal */
@@ -165,6 +178,15 @@ await startFaceLogin({
165
178
  });
166
179
  ```
167
180
 
181
+ > `onError` × `onAttemptError` (`BiometricLoginOptions`, NEO-563): `onError` é
182
+ > chamado quando o fluxo guiado **termina** — usuário cancelou, escolheu
183
+ > email/senha, ou erro fora do loop de tentativas (ex.: `CONSENT_DENIED`
184
+ > antes da câmera abrir). Já `onAttemptError` (opcional) é chamado a cada
185
+ > tentativa individual que falha **dentro** do loop (401, 429, rosto não
186
+ > detectado, etc.), antes de o `FallbackPrompt` interno aparecer — útil para
187
+ > registrar cada falha no seu próprio sistema de monitoramento sem esperar o
188
+ > fluxo encerrar.
189
+
168
190
  ### Login por mão / automático (`startHandLogin`, `startAutoLogin`)
169
191
 
170
192
  Mesma assinatura de `startFaceLogin` (`BiometricLoginOptions`); `startAutoLogin`
package/dist/index.d.ts CHANGED
@@ -63,7 +63,7 @@ export declare interface AuthorizationResult {
63
63
  }
64
64
 
65
65
  /**
66
- * NeoFaceSDK.authorize — autoriza uma operação por biometria, orquestrando
66
+ * authorize — autoriza uma operação por biometria, orquestrando
67
67
  * consent + captura + desafio de vivacidade (via runLivenessChallenge, NEO-408).
68
68
  *
69
69
  * O servidor sorteia a sequência de gestos, valida cada frame enviado e decide
@@ -199,7 +199,37 @@ export declare interface BiometricLoginOptions {
199
199
  */
200
200
  appName?: string;
201
201
  onSuccess: (result: BiometricLoginResult) => void;
202
+ /**
203
+ * Chamado quando o fluxo guiado **termina** — ou seja, quando o usuário
204
+ * cancela, escolhe entrar por email/senha, ou ocorre um erro fora do loop
205
+ * de tentativas (ex.: `CONSENT_DENIED` antes da câmera abrir).
206
+ *
207
+ * Nas falhas de reconhecimento que acontecem **dentro** do loop de
208
+ * tentativas (401, 429, rosto não detectado, etc.), o SDK exibe o
209
+ * `FallbackPrompt` internamente antes de propagar ao host. Use
210
+ * `onAttemptError` para ser notificado dessas falhas individuais.
211
+ */
202
212
  onError: (error: NeoFaceError) => void;
213
+ /**
214
+ * NEO-563 — Chamado a cada tentativa de login que falha, **antes** de o
215
+ * `FallbackPrompt` aparecer. Permite ao integrador registrar falhas
216
+ * individuais no próprio sistema de monitoramento.
217
+ *
218
+ * @param error O erro da tentativa que falhou.
219
+ * @param attempt Número da tentativa (começa em 1).
220
+ *
221
+ * @example
222
+ * ```ts
223
+ * startFaceLogin({
224
+ * applicationToken,
225
+ * onSuccess: r => console.log('ok', r),
226
+ * onError: err => console.error('fluxo encerrado', err),
227
+ * onAttemptError: (err, attempt) =>
228
+ * analytics.track('biometric_attempt_failed', { attempt, type: err.type }),
229
+ * });
230
+ * ```
231
+ */
232
+ onAttemptError?: (error: NeoFaceError, attempt: number) => void;
203
233
  onCancel?: () => void;
204
234
  onFallbackRequest?: () => void;
205
235
  countdown?: number;
@@ -981,6 +1011,11 @@ export declare const loginWithEmail: (email: string, password: string, applicati
981
1011
  */
982
1012
  export declare class NeoFaceError extends Error {
983
1013
  type: ErrorType;
1014
+ /**
1015
+ * NEO-562 · Presente quando o erro veio de um HTTP 429 (rate limit) — tempo
1016
+ * em ms sugerido pelo `Retry-After` da API antes de uma nova tentativa.
1017
+ */
1018
+ retryAfterMs?: number;
984
1019
  /**
985
1020
  * Constrói um erro do SDK com mensagem e tipo categórico.
986
1021
  * @param message Mensagem descritiva do erro (pode ser técnica)
@@ -1058,7 +1093,13 @@ export declare interface PersonalDataItem {
1058
1093
  }
1059
1094
 
1060
1095
  /**
1061
- * Pré-carrega modelos do face-api.js (chame no início da aplicação)
1096
+ * Pré-carrega modelos de detecção facial (chame no início da aplicação para
1097
+ * evitar o delay do primeiro `loadFromUri` durante a interação do usuário).
1098
+ *
1099
+ * Os modelos não são publicados no bundle do SDK — precisam ser auto-hospedados
1100
+ * pelo consumer em `/models/` (relativo à origem do app). Se os arquivos não
1101
+ * forem encontrados, um `console.error` acionável é emitido indicando o que
1102
+ * copiar e para onde.
1062
1103
  */
1063
1104
  export declare function preloadFaceDetectionModels(): Promise<void>;
1064
1105
 
@@ -1866,7 +1907,7 @@ export declare const validateToken: (applicationToken: string) => Promise<boolea
1866
1907
  * MINOR: Incrementado quando adicionamos funcionalidades mantendo compatibilidade
1867
1908
  * PATCH: Incrementado quando corrigimos bugs mantendo compatibilidade
1868
1909
  */
1869
- export declare const VERSION = "2.1.2";
1910
+ export declare const VERSION = "2.1.3";
1870
1911
 
1871
1912
  /**
1872
1913
  * Executa `fn(sessionId)`. Se o servidor devolver 410 (sessão consumida/expirada),
@@ -2614,6 +2614,11 @@ class NeoFaceError extends Error {
2614
2614
  constructor(message, type) {
2615
2615
  super(message);
2616
2616
  __publicField(this, "type");
2617
+ /**
2618
+ * NEO-562 · Presente quando o erro veio de um HTTP 429 (rate limit) — tempo
2619
+ * em ms sugerido pelo `Retry-After` da API antes de uma nova tentativa.
2620
+ */
2621
+ __publicField(this, "retryAfterMs");
2617
2622
  this.name = type;
2618
2623
  this.type = type;
2619
2624
  }
@@ -2645,6 +2650,7 @@ class NeoFaceError extends Error {
2645
2650
  case "GestureNotDetectedError":
2646
2651
  return "Não detectamos o polegar para cima. Mostre o gesto claramente para a câmera e tente novamente.";
2647
2652
  case "ApiError":
2653
+ if (this.retryAfterMs !== void 0) return this.message;
2648
2654
  if (this.message.includes("400")) return "Requisição inválida. Tente novamente.";
2649
2655
  if (this.message.includes("500"))
2650
2656
  return "Ocorreu um erro interno no sistema. Tente mais tarde.";
@@ -7595,6 +7601,28 @@ const loginWithBiometric = async (image, applicationToken) => {
7595
7601
  if (response.status === 401 || response.status === 403) {
7596
7602
  throw new NeoFaceError("Invalid application token", ErrorType.INVALID_TOKEN);
7597
7603
  }
7604
+ if (response.status === 429) {
7605
+ const raw = response.headers.get("Retry-After");
7606
+ let retryAfterMs = 6e4;
7607
+ if (raw) {
7608
+ const seconds = Number(raw);
7609
+ if (Number.isFinite(seconds) && seconds > 0) {
7610
+ retryAfterMs = seconds * 1e3;
7611
+ } else {
7612
+ const date2 = Date.parse(raw);
7613
+ if (Number.isFinite(date2)) {
7614
+ const delta = date2 - Date.now();
7615
+ if (delta > 0) retryAfterMs = delta;
7616
+ }
7617
+ }
7618
+ }
7619
+ const err = new NeoFaceError(
7620
+ `Muitas tentativas. Aguarde ${Math.ceil(retryAfterMs / 1e3)} segundo(s) antes de tentar novamente.`,
7621
+ ErrorType.API_ERROR
7622
+ );
7623
+ err.retryAfterMs = retryAfterMs;
7624
+ throw err;
7625
+ }
7598
7626
  if (response.status >= 500) {
7599
7627
  throw new NeoFaceError(`Server Error ${response.status}`, ErrorType.API_ERROR);
7600
7628
  }
@@ -11483,7 +11511,7 @@ function BiometricRegistrationModal({
11483
11511
  /* @__PURE__ */ jsx("canvas", { ref: canvasRef })
11484
11512
  ] });
11485
11513
  }
11486
- const VERSION = "2.1.2";
11514
+ const VERSION = "2.1.3";
11487
11515
  const RELEASE_DATE = "2026-09-23";
11488
11516
  const CONSENT_TRAIL_STORAGE_KEY = "neoface_consent_trail_v1";
11489
11517
  const CONSENT_TRAIL_MAX_LIMIT = 100;
@@ -11919,7 +11947,7 @@ function requestConsent(info = {}) {
11919
11947
  const appName = info.appName ?? globalAppName ?? DEFAULT_APP_NAME;
11920
11948
  if (!info.appName && !globalAppName) {
11921
11949
  console.warn(
11922
- '[NeoFaceID SDK] requestConsent sem appName — usando fallback "sua aplicação". Passe via NeoFaceSDK.init({ appName }) ou opts do caller.'
11950
+ '[NeoFaceID SDK] requestConsent sem appName — usando fallback "sua aplicação". Passe via init({ appName }) ou opts do caller.'
11923
11951
  );
11924
11952
  }
11925
11953
  const flow = info.flow ?? "verification";
@@ -13544,6 +13572,7 @@ class BiometricStatusOverlay {
13544
13572
  }
13545
13573
  }
13546
13574
  }
13575
+ const MIN_RETRY_COOLDOWN_MS = 3e3;
13547
13576
  function FallbackPromptComponent({
13548
13577
  error,
13549
13578
  onRetry,
@@ -13551,10 +13580,48 @@ function FallbackPromptComponent({
13551
13580
  onCancel,
13552
13581
  applicationToken
13553
13582
  }) {
13583
+ const [cooldownSec, setCooldownSec] = useState(0);
13584
+ const [retrying, setRetrying] = useState(false);
13585
+ const cooldownIntervalRef = useRef(null);
13586
+ const stopCooldown = useCallback(() => {
13587
+ if (cooldownIntervalRef.current) {
13588
+ clearInterval(cooldownIntervalRef.current);
13589
+ cooldownIntervalRef.current = null;
13590
+ }
13591
+ }, []);
13592
+ useEffect(() => stopCooldown, [stopCooldown]);
13593
+ const handleRetry = useCallback(() => {
13594
+ if (retrying) return;
13595
+ setRetrying(true);
13596
+ const waitMs = Math.max(
13597
+ (error == null ? void 0 : error.retryAfterMs) ?? 0,
13598
+ MIN_RETRY_COOLDOWN_MS
13599
+ );
13600
+ const totalSec = Math.ceil(waitMs / 1e3);
13601
+ setCooldownSec(totalSec);
13602
+ let remaining = totalSec;
13603
+ cooldownIntervalRef.current = setInterval(() => {
13604
+ remaining -= 1;
13605
+ setCooldownSec(remaining);
13606
+ if (remaining <= 0) {
13607
+ stopCooldown();
13608
+ setRetrying(false);
13609
+ onRetry();
13610
+ }
13611
+ }, 1e3);
13612
+ }, [retrying, error, onRetry, stopCooldown]);
13554
13613
  const handleForgotPassword = () => {
13555
13614
  const modal = new ForgotPasswordModal(applicationToken);
13556
13615
  modal.open();
13557
13616
  };
13617
+ const handleCredentials = useCallback(() => {
13618
+ stopCooldown();
13619
+ onCredentials();
13620
+ }, [stopCooldown, onCredentials]);
13621
+ const handleCancel = useCallback(() => {
13622
+ stopCooldown();
13623
+ onCancel == null ? void 0 : onCancel();
13624
+ }, [stopCooldown, onCancel]);
13558
13625
  return /* @__PURE__ */ jsxs(Fragment, { children: [
13559
13626
  /* @__PURE__ */ jsx("style", { children: `
13560
13627
  @keyframes slideUp {
@@ -13650,6 +13717,10 @@ function FallbackPromptComponent({
13650
13717
  outline: 2px solid var(--nfid-focus-ring);
13651
13718
  outline-offset: 2px;
13652
13719
  }
13720
+ .neofaceid-fallback-button-primary:disabled {
13721
+ opacity: 0.55;
13722
+ cursor: not-allowed;
13723
+ }
13653
13724
 
13654
13725
  .neofaceid-fallback-button-secondary {
13655
13726
  background: var(--nfid-surface-muted);
@@ -13709,8 +13780,10 @@ function FallbackPromptComponent({
13709
13780
  {
13710
13781
  type: "button",
13711
13782
  className: "neofaceid-fallback-button neofaceid-fallback-button-primary",
13712
- onClick: onRetry,
13713
- children: "Tentar Novamente"
13783
+ onClick: handleRetry,
13784
+ disabled: retrying,
13785
+ "aria-disabled": retrying,
13786
+ children: retrying ? `Aguarde ${cooldownSec}s...` : "Tentar Novamente"
13714
13787
  }
13715
13788
  ),
13716
13789
  /* @__PURE__ */ jsx(
@@ -13718,7 +13791,7 @@ function FallbackPromptComponent({
13718
13791
  {
13719
13792
  type: "button",
13720
13793
  className: "neofaceid-fallback-button neofaceid-fallback-button-secondary",
13721
- onClick: onCredentials,
13794
+ onClick: handleCredentials,
13722
13795
  children: "Entrar com Email e Senha"
13723
13796
  }
13724
13797
  ),
@@ -13736,7 +13809,7 @@ function FallbackPromptComponent({
13736
13809
  {
13737
13810
  type: "button",
13738
13811
  className: "neofaceid-fallback-button neofaceid-fallback-button-ghost",
13739
- onClick: onCancel,
13812
+ onClick: handleCancel,
13740
13813
  children: "Cancelar"
13741
13814
  }
13742
13815
  )
@@ -14120,7 +14193,10 @@ async function preloadFaceDetectionModels() {
14120
14193
  modelsLoaded = true;
14121
14194
  console.log("✅ Face detection models preloaded");
14122
14195
  } catch (error) {
14123
- console.warn("⚠️ Face-api.js models not loaded:", error);
14196
+ console.error(
14197
+ "[NeoFace ID SDK] Modelos de detecção facial não encontrados em /models/.\nCopie os 4 arquivos abaixo para public/models/ (ou equivalente do seu bundler):\n - tiny_face_detector_model-weights_manifest.json\n - tiny_face_detector_model-shard1\n - face_landmark_68_model-weights_manifest.json\n - face_landmark_68_model-shard1\nFonte: https://github.com/vladmandic/face-api/tree/master/model\nSem esses arquivos em /models, o fluxo de detecção facial falhará silenciosamente.",
14198
+ error
14199
+ );
14124
14200
  }
14125
14201
  })();
14126
14202
  return modelsLoading;
@@ -14487,6 +14563,7 @@ async function executeBiometricLoginFlow(options) {
14487
14563
  applicationToken,
14488
14564
  onSuccess,
14489
14565
  onError,
14566
+ onAttemptError,
14490
14567
  onFallbackRequest,
14491
14568
  onCancel,
14492
14569
  fastMode = false
@@ -14508,6 +14585,9 @@ async function executeBiometricLoginFlow(options) {
14508
14585
  error instanceof Error ? error.message : "Erro desconhecido",
14509
14586
  ErrorType.UNKNOWN
14510
14587
  );
14588
+ if (onAttemptError) {
14589
+ onAttemptError(lastError, attempts);
14590
+ }
14511
14591
  if (attempts < MAX_ATTEMPTS$1) {
14512
14592
  setTimeout(() => {
14513
14593
  tryLogin();
@@ -15179,6 +15259,7 @@ async function startFaceLogin(options) {
15179
15259
  applicationToken: options.applicationToken,
15180
15260
  onSuccess: options.onSuccess,
15181
15261
  onError: options.onError,
15262
+ onAttemptError: options.onAttemptError,
15182
15263
  onFallbackRequest: options.onFallbackRequest || (() => openPasswordFallback(options)),
15183
15264
  onCancel: options.onCancel
15184
15265
  });