@dynamicore/jumio-sdk 1.0.1 → 1.0.2

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
@@ -1,6 +1,6 @@
1
1
  # @dynamicore/jumio-sdk
2
2
 
3
- > SDK modular, tipado y agnóstico para la validación de identidad e identificación oficial (INE) mediante Jumio en el ecosistema DynamiCore.
3
+ > SDK modular, tipado y agnóstico para la validación de identidad mediante **Jumio** en el ecosistema DynamiCore. Soporta tanto el flujo de **Hosted Webflow (Redirección Web)** como el de **Carga Directa de Documentos (INE API)**.
4
4
 
5
5
  [![TypeScript](https://img.shields.io/badge/TypeScript-5.7+-blue.svg)](https://www.typescriptlang.org/)
6
6
  [![React](https://img.shields.io/badge/React-18%20%7C%2019-61dafb.svg)](https://react.dev/)
@@ -12,17 +12,15 @@
12
12
 
13
13
  ---
14
14
 
15
- ## 🌟 Características
15
+ ## 🌟 Modalidades de Verificación Soportadas
16
16
 
17
- - **Agnóstico y Universal**: Funciona en cualquier entorno JavaScript/TypeScript (Next.js App/Pages router, React, React Native, Vite, Node.js, Express, Fastify).
18
- - **Manejo Flexible de Imágenes**: Acepta `File`, `Blob`, `ArrayBuffer`, `Uint8Array`, strings en `Base64`, URLs públicas, URLs prefirmadas de S3 o rutas S3 con firmador personalizado.
19
- - **Soporte de Flujos Asíncrono y Síncrono**:
20
- - *No bloqueante* (`awaitFinalStatus: false`): Inicia la validación, responde de inmediato para no detener al usuario y ejecuta el sondeo (polling) en segundo plano notificando mediante callbacks.
21
- - *Bloqueante* (`awaitFinalStatus: true`): Aguarda activamente hasta que Jumio resuelva la validación (`APPROVED_VERIFIED`, `REJECTED`, etc.).
22
- - **Tolerancia a Fallos y Reintentos Automáticos**: Backoff exponencial configurable ante timeouts o caídas transitorias de red.
23
- - **React Hook de Primera Clase**: `useJumioVerification` provee estados reactivos (`isLoading`, `isSubmitting`, `isPolling`, `progress`, `stage`, `result`, `error`) y cancelación con `AbortController`.
24
- - **Tipado Estricto y Errores Específicos**: Jerarquía clara de errores (`JumioTimeoutError`, `JumioValidationError`, `JumioImageProcessingError`, etc.).
25
- - **Bundle Dual Optimizado**: Salida ESM (`.js`) y CommonJS (`.cjs`) con source maps y definiciones TypeScript (`.d.ts`).
17
+ | Característica | 🌐 1. Hosted Webflow (Redirección) | 📄 2. Carga Directa (INE API) |
18
+ | :--- | :--- | :--- |
19
+ | **Experiencia de Usuario** | El usuario es redirigido a la interfaz alojada oficial de Jumio para captura biométrica y de documentos. | El usuario permanece en tu app; tu interfaz captura/sube las imágenes de la INE. |
20
+ | **Casos de Uso** | Panel web, onboarding sin cámara nativa, verificación biométrica completa + Liveness. | Formularios web/móviles integrados con controles de archivo personalizados. |
21
+ | **Entrada Requerida** | `clientId`, `successUrl`, `errorUrl` | `clientId`, `frontImage`, `backImage` |
22
+ | **Hooks de React** | `useJumioWebflow` | `useJumioVerification` |
23
+ | **Métodos de Core** | `startWebflow`, `getWebflowStatus`, `pollWebflowStatus`, `parseWebflowReturnParams`, `buildRedirectUrl` | `startIneVerification`, `getIneStatus`, `verifyIne`, `pollIneStatus` |
26
24
 
27
25
  ---
28
26
 
@@ -31,110 +29,229 @@
31
29
  ### Desde npm / registro privado:
32
30
  ```bash
33
31
  npm install @dynamicore/jumio-sdk axios
34
- # o con pnpm:
32
+ # o con pnpm / yarn:
35
33
  pnpm add @dynamicore/jumio-sdk axios
36
- # o con yarn:
37
- yarn add @dynamicore/jumio-sdk axios
38
34
  ```
39
35
 
40
- ### O en proyectos locales (Monorepo o Enlace local):
36
+ ### En proyectos locales (Monorepo o enlace local):
41
37
  ```bash
42
38
  npm install file:../jumio-sdk
43
39
  ```
44
40
 
45
41
  ---
46
42
 
47
- ## 🚀 Guía de Uso
43
+ ## 🚀 Guías y Ejemplos de Uso
48
44
 
49
- ### 1. Uso con React / Next.js (`useJumioVerification`)
45
+ ---
50
46
 
51
- ```tsx
52
- import React, { useState } from "react";
53
- import { useJumioVerification } from "@dynamicore/jumio-sdk/react";
47
+ ### 🌐 Servicio 1: Hosted Webflow (Redirección Web)
48
+
49
+ En este flujo, la app solicita una sesión webflow, redirige al cliente a Jumio (`href`) y consulta el veredicto al regresar.
50
+
51
+ > [!NOTE]
52
+ > Llegar a `successUrl` solo significa que el usuario **completó los pasos en Jumio**, no que fue aprobado. Llegar a `errorUrl` significa que **abandonó o falló el flujo**. El veredicto real (`PASSED` / `WARNING` / `REJECTED` + `extraction`) solo se obtiene consultando el estado con `accountId` + `workflowId` (`getWebflowStatus` una vez, o `pollWebflowStatus` con sondeo). Por eso ambos suelen apuntar a la misma ruta `/verify/return`.
54
53
 
55
- export function IneVerificationStep({ clientId, onNext }: { clientId: string; onNext: () => void }) {
56
- const [frontFile, setFrontFile] = useState<File | null>(null);
57
- const [backFile, setBackFile] = useState<File | null>(null);
54
+ #### A. Ejemplo con React / Next.js (`useJumioWebflow`)
55
+
56
+ **Paso 1: Iniciar la verificación (Pantalla de Inicio)**
58
57
 
59
- const {
60
- verify,
61
- isSubmitting,
62
- isPolling,
63
- isLoading,
64
- stage,
65
- progress,
66
- result,
67
- error,
68
- reset,
69
- } = useJumioVerification({
58
+ ```tsx
59
+ import React from "react";
60
+ import { useJumioWebflow } from "@dynamicore/jumio-sdk/react";
61
+
62
+ export function StartIdentityVerification({ clientId }: { clientId: string }) {
63
+ const { startWebflow, isStarting, error } = useJumioWebflow({
70
64
  baseUrl: process.env.NEXT_PUBLIC_API_URL || "https://front.dynamicore.io",
71
- context: process.env.NEXT_PUBLIC_DYNAMICORE_MORAL_CONTEXT,
72
- // Callback cuando el sondeo en segundo plano resuelve el resultado final:
73
- onStatusResolved: (res) => {
74
- if (res.valid) {
75
- alert("¡Tu INE ha sido validada exitosamente!");
76
- } else {
77
- alert(`No se pudo validar el INE: ${res.errorMessage || "Documento no legible"}`);
78
- }
79
- },
80
- onStatusError: (err) => {
81
- console.error("Error en validación en segundo plano:", err);
82
- },
65
+ context: process.env.NEXT_PUBLIC_DYNAMICORE_CONTEXT,
66
+ // Opcional: URL base para proxy de redirección
67
+ redirectProxyUrl: "https://inllhuznm2.execute-api.us-west-2.amazonaws.com/prod/jumio/redirect/",
83
68
  });
84
69
 
85
- const handleSubmit = async (e: React.FormEvent) => {
86
- e.preventDefault();
87
- if (!frontFile || !backFile) return;
88
-
70
+ const handleStart = async () => {
89
71
  try {
90
- // awaitFinalStatus: false permite avanzar en el flujo sin esperar 2 minutos
91
- const res = await verify({
72
+ const baseUrl = window.location.origin;
73
+ await startWebflow({
92
74
  clientId,
93
- frontImage: frontFile,
94
- backImage: backFile,
95
- awaitFinalStatus: false,
75
+ successUrl: `${baseUrl}/verify/return`,
76
+ errorUrl: `${baseUrl}/verify/return`,
77
+ useRedirectProxy: true, // Codifica las URLs en Base64 URL-safe con proxy
78
+ openInNewTab: true, // Abre Jumio en una nueva pestaña (window.open) sin salir de la app
79
+ // autoRedirect: true, // O redirige en la misma pestaña (window.location.href)
96
80
  });
97
81
 
98
- if (res.valid) {
99
- onNext(); // Avanzar al siguiente paso del onboarding
100
- }
101
82
  } catch (err) {
102
83
  console.error("Error al iniciar verificación:", err);
103
84
  }
104
85
  };
105
86
 
106
87
  return (
107
- <form onSubmit={handleSubmit} className="space-y-4">
88
+ <div>
89
+ <h3>Verificación de Identidad</h3>
90
+ {error && <p style={{ color: "red" }}>{error.message}</p>}
91
+ <button onClick={handleStart} disabled={isStarting}>
92
+ {isStarting ? "Cargando..." : "Iniciar Verificación con Jumio"}
93
+ </button>
94
+ </div>
95
+ );
96
+ }
97
+ ```
98
+
99
+ **Paso 2: Procesar el retorno (Pantalla `/verify/return`)**
100
+
101
+ ```tsx
102
+ import React, { useEffect } from "react";
103
+ import { useJumioWebflow } from "@dynamicore/jumio-sdk/react";
104
+
105
+ export function IdentityReturnPage({ clientId }: { clientId: string }) {
106
+ const { parseReturnParams, checkResult, isChecking, result, isValid, isRejected, error } =
107
+ useJumioWebflow({
108
+ baseUrl: process.env.NEXT_PUBLIC_API_URL,
109
+ context: process.env.NEXT_PUBLIC_DYNAMICORE_CONTEXT,
110
+ onResult: (res) => {
111
+ if (res.valid) {
112
+ console.log("¡Verificación aprobada!", res.extraction);
113
+ }
114
+ },
115
+ });
116
+
117
+ useEffect(() => {
118
+ // 1. Extraer accountId y workflowId de los query params de la URL
119
+ const { accountId, workflowId } = parseReturnParams();
120
+
121
+ if (accountId && workflowId) {
122
+ // 2. Sondear el resultado final (acepta { maxAttempts, pollingIntervalMs, signal })
123
+ // Para una sola consulta sin sondeo: await client.getWebflowStatus({ accountId, workflowId, clientId })
124
+ // Retorna null si aún está pendiente.
125
+ checkResult({ accountId, workflowId, clientId });
126
+ }
127
+ return () => cancel(); // Limpia el sondeo si el componente se desmonta (abort resetea isChecking)
128
+ }, []);
129
+
130
+ if (isChecking) {
131
+ return <p>Verificando identidad, por favor espera un momento...</p>;
132
+ }
133
+
134
+ if (isValid) {
135
+ return (
108
136
  <div>
109
- <label>Frente de INE:</label>
110
- <input
111
- type="file"
112
- accept="image/*"
113
- onChange={(e) => setFrontFile(e.target.files?.[0] || null)}
114
- />
137
+ <h2>✅ Verificación Completada</h2>
138
+ <p>CURP Extraída: {String(result?.extraction?.curp || "N/A")}</p>
115
139
  </div>
140
+ );
141
+ }
116
142
 
143
+ if (isRejected || error) {
144
+ return (
117
145
  <div>
118
- <label>Reverso de INE:</label>
119
- <input
120
- type="file"
121
- accept="image/*"
122
- onChange={(e) => setBackFile(e.target.files?.[0] || null)}
123
- />
146
+ <h2>❌ Verificación No Aprobada</h2>
147
+ <p>{error?.message || "El documento fue rechazado."}</p>
124
148
  </div>
149
+ );
150
+ }
125
151
 
126
- {isLoading && (
127
- <div>
128
- <p>Estado: {stage} ({progress}%)</p>
129
- {isSubmitting && <p>Subiendo y procesando imágenes...</p>}
130
- {isPolling && <p>Verificando autenticidad en segundo plano...</p>}
131
- </div>
132
- )}
152
+ return <p>Cargando información de la sesión...</p>;
153
+ }
154
+ ```
155
+
156
+ ---
157
+
158
+ #### B. Ejemplo con TypeScript / Node.js (`JumioClient`)
159
+
160
+ ```typescript
161
+ import { JumioClient } from "@dynamicore/jumio-sdk";
162
+
163
+ const jumio = new JumioClient({
164
+ baseUrl: "https://front.dynamicore.io",
165
+ context: "MI_CONTEXTO_NEGOCIO",
166
+ authToken: () => getAuthTokenFromSession(),
167
+ redirectProxyUrl: "https://inllhuznm2.execute-api.us-west-2.amazonaws.com/prod/jumio/redirect/",
168
+ });
169
+
170
+ // 1. Solicitar la URL de verificación
171
+ async function initVerificationSession(clientId: string) {
172
+ const { href, accountId, workflowId } = await jumio.startWebflow({
173
+ clientId,
174
+ successUrl: "https://myapp.com/identity/callback",
175
+ errorUrl: "https://myapp.com/identity/callback",
176
+ useRedirectProxy: true,
177
+ });
178
+
179
+ console.log("Redirigir cliente a:", href);
180
+ return { href, accountId, workflowId };
181
+ }
182
+
183
+ // 2. Consultar o sonder el resultado al regresar
184
+ async function verifyReturnStatus(returnUrl: string, clientId: string) {
185
+ const { accountId, workflowId } = jumio.parseWebflowReturnParams(returnUrl);
186
+
187
+ if (!accountId || !workflowId) {
188
+ throw new Error("No se encontraron parámetros de seguimiento en la URL.");
189
+ }
190
+
191
+ const result = await jumio.pollWebflowStatus(
192
+ { accountId, workflowId, clientId },
193
+ { maxAttempts: 15, pollingIntervalMs: 5000 }
194
+ );
195
+
196
+ if (result.valid) {
197
+ console.log("Veredicto APROBADO:", result.extraction);
198
+ } else {
199
+ console.warn("Veredicto RECHAZADO:", result.errorMessage);
200
+ }
201
+
202
+ return result;
203
+ }
204
+ ```
205
+
206
+ ---
133
207
 
134
- {error && <p className="text-red-500">{error.message}</p>}
208
+ ### 📄 Servicio 2: Carga Directa de INE (INE API)
209
+
210
+ En este flujo, la app captura las imágenes de la INE (frente y reverso) en su propia interfaz y las envía directamente al backend.
211
+
212
+ #### A. Ejemplo con React / Next.js (`useJumioVerification`)
213
+
214
+ ```tsx
215
+ import React, { useState } from "react";
216
+ import { useJumioVerification } from "@dynamicore/jumio-sdk/react";
217
+
218
+ export function DirectIneVerificationStep({ clientId, onNext }: { clientId: string; onNext: () => void }) {
219
+ const [frontFile, setFrontFile] = useState<File | null>(null);
220
+ const [backFile, setBackFile] = useState<File | null>(null);
221
+
222
+ const { verify, isSubmitting, isPolling, isLoading, stage, progress, error } =
223
+ useJumioVerification({
224
+ baseUrl: process.env.NEXT_PUBLIC_API_URL,
225
+ context: process.env.NEXT_PUBLIC_DYNAMICORE_CONTEXT,
226
+ onStatusResolved: (res) => {
227
+ if (res.valid) {
228
+ onNext();
229
+ }
230
+ },
231
+ });
232
+
233
+ const handleSubmit = async (e: React.FormEvent) => {
234
+ e.preventDefault();
235
+ if (!frontFile || !backFile) return;
236
+
237
+ await verify({
238
+ clientId,
239
+ frontImage: frontFile,
240
+ backImage: backFile,
241
+ awaitFinalStatus: false, // Sondeo en 2do plano sin bloquear la UI
242
+ });
243
+ };
244
+
245
+ return (
246
+ <form onSubmit={handleSubmit}>
247
+ <input type="file" accept="image/*" onChange={(e) => setFrontFile(e.target.files?.[0] || null)} />
248
+ <input type="file" accept="image/*" onChange={(e) => setBackFile(e.target.files?.[0] || null)} />
249
+
250
+ {isLoading && <p>Estado: {stage} ({progress}%)</p>}
251
+ {error && <p style={{ color: "red" }}>{error.message}</p>}
135
252
 
136
253
  <button type="submit" disabled={isLoading || !frontFile || !backFile}>
137
- {isLoading ? "Validando..." : "Continuar"}
254
+ Continuar
138
255
  </button>
139
256
  </form>
140
257
  );
@@ -143,41 +260,34 @@ export function IneVerificationStep({ clientId, onNext }: { clientId: string; on
143
260
 
144
261
  ---
145
262
 
146
- ### 2. Uso con TypeScript / Node.js / Core (`JumioClient`)
263
+ #### B. Ejemplo con TypeScript / Node.js (`JumioClient.verifyIne`)
147
264
 
148
265
  ```typescript
149
266
  import { JumioClient, isJumioError } from "@dynamicore/jumio-sdk";
150
267
 
151
268
  const jumio = new JumioClient({
152
269
  baseUrl: "https://front.dynamicore.io",
153
- context: "MI_CONTEXTO_NEGOCIO", // Contexto/tenant de tu aplicación en DynamiCore
154
- authToken: () => getAuthTokenFromSession(), // Token estático o función async
155
- // Opcional: Función para firmar URLs privadas de S3
156
- s3Signer: async (path, expires) => {
157
- return await myS3SignerService(path, expires);
158
- },
270
+ context: "MI_CONTEXTO_NEGOCIO",
271
+ authToken: () => getAuthTokenFromSession(),
159
272
  });
160
273
 
161
- async function runVerification() {
274
+ async function runDirectVerification() {
162
275
  try {
163
276
  const result = await jumio.verifyIne({
164
277
  clientId: "usr_987654",
165
278
  frontImage: "https://my-bucket.s3.amazonaws.com/uploads/ine_front.jpg",
166
279
  backImage: "https://my-bucket.s3.amazonaws.com/uploads/ine_back.jpg",
167
- awaitFinalStatus: true, // Espera resolución completa
280
+ awaitFinalStatus: true,
168
281
  });
169
282
 
170
283
  if (result.valid) {
171
- console.log("Validación completada con éxito:", result.data);
284
+ console.log("INE validada con éxito:", result.data);
172
285
  } else {
173
- console.warn("Documento rechazado:", result.errorMessage);
286
+ console.warn("INE rechazada:", result.errorMessage);
174
287
  }
175
288
  } catch (error) {
176
289
  if (isJumioError(error)) {
177
290
  console.error(`Error Jumio [${error.code}]:`, error.message);
178
- console.error("Detalle crudo:", error.rawData);
179
- } else {
180
- console.error("Error inesperado:", error);
181
291
  }
182
292
  }
183
293
  }
@@ -191,85 +301,50 @@ async function runVerification() {
191
301
  | :--- | :--- | :--- | :--- |
192
302
  | `baseUrl` | `string` | `"https://front.dynamicore.io"` | URL base de la API de DynamiCore. |
193
303
  | `endpoint` | `string` | `"/marketplace/apps/jumio"` | Ruta del endpoint de verificación. |
194
- | `context` | `string` | `undefined` | Contexto moral / de aplicación en header `context`. |
195
- | `authToken` | `string \| (() => string \| Promise<string>)` | `undefined` | Token Bearer o proveedor dinámico. |
196
- | `requestTimeout` | `number` | `180000` (3 min) | Timeout para el POST inicial de imágenes. |
197
- | `statusTimeout` | `number` | `120000` (2 min) | Timeout para cada consulta de estatus. |
198
- | `maxRetries` | `number` | `3` | Número de reintentos ante caídas transitorias. |
199
- | `retryDelayMs` | `number` | `800` | Delay base de backoff para reintentos. |
200
- | `pollingIntervalMs` | `number` | `10000` (10s) | Intervalo entre consultas sucesivas de estado. |
201
- | `maxPollingAttempts` | `number` | `20` | Cantidad máxima de consultas antes de timeout. |
202
- | `s3Signer` | `S3SignerFunction` | `undefined` | Función para firmar rutas privadas de S3. |
203
- | `customHeaders` | `Record<string, string>` | `{}` | Headers HTTP personalizados. |
204
- | `axiosInstance` | `AxiosInstance` | `undefined` | Instancia personalizada de Axios. |
205
-
206
- ---
207
-
208
- ## 🖼️ Formatos de Imagen Soportados
209
-
210
- La librería detecta y convierte automáticamente cualquier formato a Base64 puro sin encabezados `data:image/...`:
211
-
212
- 1. **`File` o `Blob`**: Obtenidos desde `<input type="file" />` o drag & drop.
213
- 2. **`Base64` / `Data URL`**: Strings directos como `"data:image/jpeg;base64,..."` o Base64 crudo.
214
- 3. **`URL Remota`**: URLs `https://...` públicas o con token de firma.
215
- 4. **`Ruta S3`**: Rutas relativas como `"company/123/docs/ine.png"` (utiliza `s3Signer` para resolver la URL firmada).
216
- 5. **`ArrayBuffer` / `Uint8Array`**: Datos binarios en memoria.
217
-
218
- ---
219
-
220
- ## 🚦 Estados y Veredicto (`valid` / `decision`)
221
-
222
- > **Regla de oro:** el veredicto real de la verificación está en los campos `valid` y `decision`, **no** en `workflowStatus`. `workflowStatus: "PROCESSED"` solo significa que el workflow terminó de procesar; un `PROCESSED` + `decision: "NOT_EXECUTED"` es un **rechazo**, no una aprobación.
223
-
224
- ### Estados del workflow (`JumioWorkflowStatus`)
225
-
226
- - `PENDING` / `INITIATED`: En proceso de análisis por los motores de Jumio.
227
- - `PROCESSED`: El workflow terminó de procesar. **No implica aprobación**: el veredicto se determina con `valid` / `decision`.
228
- - `APPROVED_VERIFIED` / `DONE` / `COMPLETED`: Estados inequívocamente aprobados (fallback conservador).
229
- - `REJECTED` / `FAILED` / `DENIED` / `EXPIRED` / `ABANDONED` / `NOT_READABLE` / `FRAUD` / `UNSUPPORTED_ID_TYPE`: Estados finales no válidos.
230
-
231
- ### Precedencia del veredicto
232
-
233
- El SDK resuelve la validez del resultado priorizando los campos explícitos:
234
-
235
- 1. **`valid` booleano** (fuente de verdad de la gateway): `true` = aprobado, `false` = rechazado.
236
- 2. **`decision`** presente: `PASSED` = aprobado; cualquier otro valor (`NOT_EXECUTED`, `REJECTED`, `FAILED`, etc.) = rechazado.
237
- 3. **Fallback conservador** por `workflowStatus` (fail-closed): solo `APPROVED_VERIFIED` / `DONE` / `COMPLETED` cuentan como aprobados; si no hay `valid` ni `decision`, un `PROCESSED` se considera rechazado.
238
-
239
- > Siempre lee `result.valid` en lugar de interpretar el payload crudo de Jumio.
240
-
241
- ---
242
-
243
- ## 🔄 Flujo No Bloqueante y Ciclo de Vida del Polling
244
-
245
- Con `awaitFinalStatus: false` el SDK retorna de inmediato para no detener al usuario y ejecuta el sondeo (`polling`) en segundo plano consultando el estado del workflow hasta obtener un veredicto final:
246
-
247
- - El polling en segundo plano está **desacoplado del `signal`** del llamador: continúa aunque el componente que lo inició se desmonte o se aborte la señal original (navegación, cierre de pantalla). El `signal` / `AbortController` solo aplica a la fase inicial síncrona (upload y consulta de arranque).
248
- - El veredicto final llega a `onStatusResolved` (o a `isSuccess` / `isError` del hook); en caso de agotarse los `maxPollingAttempts` se invoca `onStatusError` con un `JumioPollingTimeoutError`.
249
- - Si la respuesta inicial ya trae un veredicto definitivo (p. ej. `valid: false` + `workflowStatus: "PROCESSED"`), el SDK lo resuelve de inmediato sin requerir los identificadores de seguimiento.
250
-
251
- ---
252
-
304
+ | `context` | `string` | `undefined` | Header de contexto enviado en las peticiones. |
305
+ | `authToken` | `string \| Provider` | `undefined` | Token Bearer o función proveedora dinámica. |
306
+ | `authTokenPrefix` | `string` | `""` | Prefijo del header `Authorization` (ej. `"Bearer"`). |
307
+ | `redirectProxyUrl` | `string` | `undefined` | URL base del proxy de redirección para Hosted Webflow. |
308
+ | `requestTimeout` | `number` | `180000` (3 min) | Timeout para peticiones POST. |
309
+ | `statusTimeout` | `number` | `120000` (2 min) | Timeout para peticiones GET de estado. |
310
+ | `maxRetries` | `number` | `3` | Número de reintentos ante caídas de red. |
311
+ | `retryDelayMs` | `number` | `800` | Delay base del backoff exponencial entre reintentos. |
312
+ | `pollingIntervalMs` | `number` | `10000` (10s) | Intervalo entre consultas de sondeo. |
313
+ | `maxPollingAttempts` | `number` | `20` | Máximo de consultas de sondeo antes de timeout. |
314
+ | `s3Signer` | `S3SignerFunction` | `undefined` | Función para firmar rutas privadas de S3. |
315
+ | `customHeaders` | `Record<string,string>` | `undefined` | Headers adicionales en cada petición. |
316
+ | `axiosInstance` | `AxiosInstance` | `undefined` | Instancia propia de Axios (opcional). |
317
+
318
+ ---
319
+
320
+ ## 🧰 Helpers y notas del flujo Webflow
321
+
322
+ - `client.buildRedirectUrl(returnUrl, useProxy)` / `buildRedirectUrl(url, proxy)` construye la URL de retorno, con Base64 URL-safe si usas `redirectProxyUrl`.
323
+ - `client.getWebflowStatus({ accountId, workflowId, clientId })` una sola consulta `GET ?type=ine&accountId&workflowId&clientId`; retorna `WebflowResult | null` (`null` = pendiente).
324
+ - `client.pollWebflowStatus(params, { maxAttempts, pollingIntervalMs, signal, onAttempt })` sondeo hasta veredicto o `JumioPollingTimeoutError`.
325
+ - Hooks exponen `cancel()` / `reset()`; un abort (desmontaje, `cancel`, StrictMode) baja `isChecking/isStarting/isLoading` sin marcar error.
326
+ - El backend puede responder con envelope `{ data }` o `{ values }` y datos anidados; el SDK los desempaqueta. `parseWebflowReturnParams` acepta `workflowExecutionId` / `workflowId` y params anidados en `?status=`.
327
+
328
+ ---
329
+
253
330
  ## 🧪 Pruebas y Construcción
254
331
 
255
- Para compilar y ejecutar la suite de pruebas del paquete:
256
-
257
332
  ```bash
258
333
  # Instalar dependencias
259
334
  npm install
260
335
 
261
- # Ejecutar pruebas unitarias
336
+ # Ejecutar suite completa de pruebas unitarias (59 tests)
262
337
  npm test
263
338
 
264
- # Compilar para producción (ESM, CJS y .d.ts)
265
- npm run build
266
-
267
339
  # Verificación de tipos TypeScript
268
340
  npm run typecheck
341
+
342
+ # Compilar para producción (ESM, CJS y .d.ts)
343
+ npm run build
269
344
  ```
270
345
 
271
346
  ---
272
347
 
273
348
  ## 📄 Licencia
274
349
 
275
- MIT © DynamiCore
350
+ MIT © DynamiCore